// Piscivore Camera Controller
// P. McGill, MBARI, 2023
//
// v1.0.0  Initial release
// v1.1.0  Added camera power on and off, camera wifi button, and delay after power off to charge Li cell
// v1.2.0  Fixed bug where an empty command was considered an invalid command,
//          added ability to correct mistyped command with backspace,
//          removed delay during power off if camera isn't deployed,
//          added warning message if deployed in TIMEWARP mode,
//          and added filtering of line feeds so they're ignored
// v1.3.0  Added ability to skip number of first start times to make deployment easier   

#include <RTCZero.h>

#define LINE_BUF_SIZE   32     // Maximum input string length
#define ARG_BUF_SIZE    10     // Maximum argument string length
#define MAX_NUM_ARGS    3      // Maximum number of arguments
#define BUTTON_MS       500    // Number of milliseconds to press button
#define REC_DELAY_S     30     // Number of seconds delay between power and button
#define SER_TIMEOUT_S   30     // Number of seconds before serial input is automatically accepted
#define CAM_OFF_DELAY_S 600    // Number of seconds to leave DC-DC power on after turning camera off
#define LED_PIN         13     // Set hi to turn on green LED
#define DCDC_PWR_PIN    A1     // Set as analog input to turn on DC-to-DC power, set as low output to turn off power
#define CAM_PWR_ON_PIN  1      // Set high to turn camera on
#define CAM_PWR_OFF_PIN 3      // Set high to turn camera off
#define WIFI_BTN_PIN    2      // Set high to push camera wifi button   
#define REC_BTN_PIN     4      // Set high to push camera record button
#define BATV_PIN        A4     // Connected to controller battery through 1/2 voltage divider
#define BS              0x08   // Backspace character
#define CR              0x0D   // Carriage return character
#define LF              0x0A   // Line feed character
#define TIMEWARP        0      // Set to 1 to make camera record once per hour instead of once per day
                               //  In this mode, only the minutes of the event times matter and hours are ignored

// Create an RTC object
RTCZero rtc;
 
char line[LINE_BUF_SIZE];
char args[MAX_NUM_ARGS][ARG_BUF_SIZE];

bool  gErrorFlag = false;
bool  gPowerIsOn = false;
bool  gDeployed = false;
int   gFirst = 0;     // time to start camera on first day of deployment
int   gStart = 0;     // time to start camera every day after
int   gStop = 0;      // time to stop camera every day
int   gSkip = 0;      // number of first-start times to skip
 
// Function declarations
int cmd_help();
int cmd_event();
int cmd_power();
int cmd_button();
int cmd_wifi();
int cmd_record();
int cmd_clock();
int cmd_battery();
int cmd_deploy();
int getTime();
void printTime(int);
void powerOn();
void powerOff();
void pressRecButton();
void alarmMatch();
 
// List of functions pointers corresponding to each command
int (*commands_func[])() {
  &cmd_help,
  &cmd_event,
  &cmd_power,
  &cmd_button,
  &cmd_wifi,
  &cmd_record,
  &cmd_clock,
  &cmd_battery,
  &cmd_deploy
};
 
// List of command names
const char *commands_str[] = {
  "help",
  "event",
  "power",
  "button",
  "wifi",
  "record",
  "clock",
  "battery",
  "deploy"
};
 
// List of event subcommand names
const char *event_args[] = {
  "first",
  "start",
  "stop",
  "skip",
  "list"
};

// List of power subcommand names
const char *power_args[] = {
  "on",
  "off"
};

// List of record subcommand names
const char *record_args[] = {
  "start",
  "stop"
};

// List of clock subcommand names
const char *clock_args[] = {
  "set",
  "read"
};

// List of battery subcommand names
const char *battery_args[] = {
  "main",
  "cont"
};

int num_commands = sizeof(commands_str) / sizeof(char *);


void setup() {
  SerialUSB.begin(9600);  // set up serial port
  SerialUSB.setTimeout(SER_TIMEOUT_S * 1000);
  pinMode(DCDC_PWR_PIN, OUTPUT);    // set up output pins
  pinMode(REC_BTN_PIN, OUTPUT);
  pinMode(CAM_PWR_ON_PIN, OUTPUT);
  pinMode(CAM_PWR_OFF_PIN, OUTPUT);
  pinMode(WIFI_BTN_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BATV_PIN,INPUT);          // set up aux batt voltage read pin
  analogReference(AR_INTERNAL2V23); // set analog ref to 2.23 V

  digitalWrite(DCDC_PWR_PIN, LOW);  // make sure DC-to-DC power is off
  digitalWrite(REC_BTN_PIN, LOW);   // make sure record button isn't pressed
  digitalWrite(CAM_PWR_ON_PIN, LOW);  // make sure camera power is off
  digitalWrite(CAM_PWR_OFF_PIN, LOW); // make sure camera power is off
  digitalWrite(REC_BTN_PIN, LOW);   // make sure record button isn't pressed
  digitalWrite(WIFI_BTN_PIN, LOW);  // make sure wifi button isn't pressed

  cli_init();  // print welcome message
  rtc.begin(); // initialize RTC
} // end setup()
 

void loop() {
  my_cli();
} // end loop()


void cli_init() {
  delay(3000);  // wait for USB to initialize
  SerialUSB.println("Piscivore Camera v1.3.0");
  SerialUSB.println("Type \"help\" to see a list of commands.");
} // end cli_init()


// Get a command line from serial USB, parse it, and then execute the command.
void my_cli() {
  SerialUSB.print("> ");

  read_line();
  if(!gErrorFlag) {
    parse_line();
  } // end if
  if(!gErrorFlag) {
    execute();
  } // end if

  memset(line, 0, LINE_BUF_SIZE);
  memset(args, 0, sizeof(args[0][0]) * MAX_NUM_ARGS * ARG_BUF_SIZE);

  gErrorFlag = false;
} // end my_cli()


// Read a line of input. The characters entered over serial USB are returned in the global var "line".
void read_line() {
  char    input_char;
  int     i = 0;

  do {                        // read chars until carriage return
    if(SerialUSB.available()) {
      input_char = (char) SerialUSB.read();       // get the char
      if(input_char == BS) {  // if char is a backspace
        if(i > 0) {           //  and not at beginning of line
          i--;
          SerialUSB.write(input_char);  // send backspace
          SerialUSB.write(' ');         // erase previous char
          SerialUSB.write(input_char);  // send backspace
        } // end if
      } else if(input_char == LF) {
        // ignore any line feeds
      } else {                // otherwise put in buffer
          line[i++] = input_char;
          SerialUSB.write(input_char);  // and echo char
      } // end if
    } // end if
  } while((input_char != CR) && (i < LINE_BUF_SIZE - 1));
  line[i-1] = NULL;           // replace CR with NULL
} // end read_line()


// Parse a line of input. The input line is in the global var "line", and the parsed args are returned in the
//  global var "args".
void parse_line() {
  char *argument;
  int counter = 0;

  argument = strtok(line, " ");

  while((argument != NULL)) {
    if(counter < MAX_NUM_ARGS){
      if(strlen(argument) < ARG_BUF_SIZE) {
        strcpy(args[counter], argument);
        argument = strtok(NULL, " ");
        counter++;
      } else {
        SerialUSB.println("Input string too long.");
        gErrorFlag = true;
        break;
      } // end if
    } else {
      break;
    } // end if
  } // end while
} // end parse_line()


// Compare the entered command to the list of known commands and if a match if found, return a pointer to
//  the command function to execute it.
int execute() {  
  for(int i = 0; i < num_commands; i++) {
    if(strcmp(args[0], commands_str[i]) == 0) {
      return(*commands_func[i])();
    } // end if
  } // end for
 
  if(args[0][0] != NULL) {
    SerialUSB.println("Invalid command. Type \"help\" for more.");
  } // end if  
  return 0;
} // end execute()


int cmd_help() {
  if(args[1] == NULL) {
    help_help();
  } else if(strcmp(args[1], commands_str[0]) == 0){
    help_help();
  } else if(strcmp(args[1], commands_str[1]) == 0){
    help_event();
  } else if(strcmp(args[1], commands_str[2]) == 0){
    help_power();
  } else if(strcmp(args[1], commands_str[3]) == 0){
    help_button();
  } else if(strcmp(args[1], commands_str[4]) == 0){
    help_wifi();
  } else if(strcmp(args[1], commands_str[5]) == 0){
    help_record();
  } else if(strcmp(args[1], commands_str[6]) == 0){
    help_clock();
  } else if(strcmp(args[1], commands_str[7]) == 0){
    help_battery();
  } else if(strcmp(args[1], commands_str[8]) == 0){
    help_deploy();
  } else {
    help_help();
  } // end if
} // end cmd_help()


void help_help() {
  SerialUSB.println("The following commands are available:");

  for(int i = 0; i < num_commands; i++) {
    SerialUSB.print("  ");
    SerialUSB.println(commands_str[i]);
  } // end for
  SerialUSB.println("");
  SerialUSB.println("You can for instance type \"help clock\" for more info on the clock command.");
} // end help_help()


void help_event() {
  SerialUSB.println("Set or list the camera event times: ");
  SerialUSB.println("  event first");
  SerialUSB.println("  event start");
  SerialUSB.println("  event stop");
  SerialUSB.println("  event skip");
  SerialUSB.println("  event list");
} // end help_event()


void help_power() {
  SerialUSB.println("Control the camera power, either on or off:");
  SerialUSB.println("  power on");
  SerialUSB.println("  power off");
} // end help_power()


void help_button() {
  SerialUSB.println("Press the record button.");
} // end help_button()

void help_wifi() {
  SerialUSB.println("Press the wifi button.");
} // end help_button()

void help_record() {
  SerialUSB.println("Control the camera recording, either start or stop:");
  SerialUSB.println("  record start");
  SerialUSB.println("  record stop");
} // end help_power()


void help_clock() {
  SerialUSB.println("Set or read the real-time clock:");
  SerialUSB.println("  clock set");
  SerialUSB.println("  clock read");
} // end help_clock()


void help_battery() {
  SerialUSB.println("Read the main or controller battery voltage:");
  SerialUSB.println("  battery main");
  SerialUSB.println("  battery cont");
} // end help_battery()


void help_deploy() {
  SerialUSB.println("Start the program to deploy the camera.");
} // end help_deploy()


int cmd_event() {
  int skip;
  char* end;

  if(strcmp(args[1], event_args[0]) == 0) { // if arg is "first"
    SerialUSB.print("Enter camera start time for first day (HHMM, 24-hour format): ");
    gFirst = getTime();
    if(gFirst >= 0) {
      SerialUSB.print("First time set to ");
      printTime(gFirst);
    } else {
      SerialUSB.println("Invalid time, first time not set.");
    } // end if
  } else if(strcmp(args[1], event_args[1]) == 0) { // if arg is "start"
    SerialUSB.print("Enter camera start time (HHMM, 24-hour format): ");
    gStart = getTime();
    if(gStart >= 0) {
      SerialUSB.print("Start time set to ");
      printTime(gStart);
    } else {
      SerialUSB.println("Invalid time, start time not set.");
    } // end if
  } else if(strcmp(args[1], event_args[2]) == 0) { // if arg is "stop"
    SerialUSB.print("Enter camera stop time (HHMM, 24-hour format): ");
    gStop = getTime();
    if(gStop >= 0) {
      SerialUSB.print("Stop time set to ");
      printTime(gStop);
    } else {
      SerialUSB.println("Invalid time, stop time not set.");
    } // end if
  } else if(strcmp(args[1], event_args[3]) == 0) { // if arg is "skip"
    SerialUSB.print("Enter number of first-start times to skip (0-7): ");
    read_line(); // get number of skips from user
    skip = (int) strtoul(line, &end, 10); // convert string to integer
    if(skip >= 0 && skip <= 7) {
      gSkip = skip;
      SerialUSB.print("Skip set to ");
      SerialUSB.print(gSkip);
      if(gSkip == 1) {
        SerialUSB.println(" time");
      } else {
        SerialUSB.println(" times");
      } // end if
    } else {
      SerialUSB.println("Invalid number, skip first times not set.");
    } // end if
  } else if(strcmp(args[1], event_args[4]) == 0) { // if arg is "list"
    SerialUSB.print("First time is ");
    printTime(gFirst);
    SerialUSB.print("Start time is ");
    printTime(gStart);
    SerialUSB.print("Stop time is  ");
    printTime(gStop);
    SerialUSB.print("Skip first start ");
    SerialUSB.print(gSkip);
    if(gSkip == 1) {
      SerialUSB.println(" time");
    } else {
      SerialUSB.println(" times");
    } // end if
  } else {
    SerialUSB.println("Invalid command. Type \"help event\" to see how to use the event command.");
  } // end if
} // end cmd_event()


int cmd_power() {
  if(strcmp(args[1], power_args[0]) == 0) { // if arg is "on"
    powerOn();    // turn on camera power
  } else 
  if(strcmp(args[1], power_args[1]) == 0) { // if arg is "off"
    powerOff();   // turn off camera power
  } else {
    SerialUSB.println("Invalid command. Type \"help power\" to see how to use the power command.");
  } // end if
} // end cmd_power()


int cmd_button() {
  pressRecButton();
} // end cmd_button()


int cmd_wifi() {
  SerialUSB.println("Pressing the wifi button.");
  digitalWrite(WIFI_BTN_PIN, HIGH);
  delay(BUTTON_MS);
  digitalWrite(WIFI_BTN_PIN, LOW);
} // end cmd_wifi()


int cmd_record() {
  if(strcmp(args[1], record_args[0]) == 0) { // if arg is "start"
    powerOn();    // turn on camera power

    SerialUSB.print("Waiting ");
    SerialUSB.print(REC_DELAY_S);
    SerialUSB.println(" secs...");
    delay(REC_DELAY_S * 1000);

    pressRecButton(); // start recording
  } else if(strcmp(args[1], record_args[1]) == 0) { // if arg is "stop"
    pressRecButton(); // stop recording

    SerialUSB.print("Waiting ");
    SerialUSB.print(REC_DELAY_S);
    SerialUSB.println(" secs...");
    delay(REC_DELAY_S * 1000);

    powerOff();     // turn off camera power
  } else {
    SerialUSB.println("Invalid command. Type \"help record\" to see how to use the record command.");
  } // end if
} // end cmd_record()


int cmd_clock() {
  int time;
  int hours;
  int minutes;
  int seconds;

  if(strcmp(args[1], clock_args[0]) == 0) { // if arg is "set"
    SerialUSB.print("Enter current time (HHMM, 24-hour format): ");
    time = getTime();       // get time
    if(time >= 0) {         // if time is valid
      hours = time / 100;
      minutes = time % 100;
      rtc.setTime(hours, minutes, 0);  // set the RTC to nearest minute
      SerialUSB.print("Clock set to ");
      printTime(time);
    } else {
      SerialUSB.println("Invalid time, clock not set.");
    } // end if
  } else if(strcmp(args[1], clock_args[1]) == 0) { // if arg is "read"
    hours = rtc.getHours();
    minutes = rtc.getMinutes();
    seconds = rtc.getSeconds();
    SerialUSB.print("Current time is ");
    printTime(hours * 100 + minutes);
  } else {
    SerialUSB.println("Invalid command. Type \"help clock\" to see how to use the clock command.");
  } // end if
} // end cmd_clock()


int cmd_battery() {
  float battVolt;

  if(strcmp(args[1], battery_args[0]) == 0) { // if arg is "main"
    if(gPowerIsOn) {
      battVolt = analogRead(DCDC_PWR_PIN) * 4 * 2.23 / 1023;  // read thru 4:1 voltage divider
      SerialUSB.print("Main battery is at ");
      SerialUSB.print(battVolt);
      SerialUSB.println(" V");
    } else {
      SerialUSB.println("Camera power must be on to measure main battery voltage.");
    } // end if
  } else if(strcmp(args[1], battery_args[1]) == 0) { // if arg is "cont"
    battVolt = analogRead(BATV_PIN) * 2 * 2.23 / 1023;  // read thru 2:1 voltage divider
    SerialUSB.print("Controller battery is at ");
    SerialUSB.print(battVolt);
    SerialUSB.println(" V");
  } else {
    SerialUSB.println("Invalid command. Type \"help battery\" to see how to use the battery command.");
  } // end if
} // end cmd_battery()


int cmd_deploy() {
  char rcvdChar;
  byte  secs;

  if(TIMEWARP) SerialUSB.println("This code is in TIMEWARP mode!");
  SerialUSB.print("Are you sure you want to deploy the camera? [y/n] ");
  while(SerialUSB.available() == 0); // wait here until serial input
  rcvdChar = SerialUSB.read();       // get the char
  if(rcvdChar == 'y' || rcvdChar == 'Y') {  // if user answers "yes"
    SerialUSB.println("\nDeployment started.\nDisconnect USB cable now.\nCamera will record for 1 minute before sleep.");

    // record for 1 minute before sleeping
    powerOn();      // turn on camera power
    delay(REC_DELAY_S * 1000); // wait for camera to initialize
    pressRecButton();  // press button to start recording
    for(secs = 0; secs < 60; secs++) {  // record for 1 minute
      digitalWrite(LED_BUILTIN, HIGH);  // turn LED on
      delay(500);
      digitalWrite(LED_BUILTIN, LOW);   // turn LED off
      delay(500);
    } // end for
    pressRecButton();  // press button to stop recording
    delay(REC_DELAY_S * 1000); // wait for camera to finish writing files
    #if !TIMEWARP // if not in test mode, set gDeploy so camera battery can charge after first power off
    gDeployed = true;
    #endif
    powerOff();      // turn off camera power
    gDeployed = true; // set deployed flag so camera battery will always charge after power off

    // set up the first camera start time and go to sleep
    rtc.setDay(1); // set day to first of month
    rtc.setAlarmDay(1 + gSkip); // set up to skip a number of first-start times
    rtc.setAlarmTime(gFirst/100, gFirst%100, 0); // set the first-start time
    #if !TIMEWARP // if not in test mode, cycle once per day after skipping set number of first-start times
    rtc.enableAlarm(rtc.MATCH_DHHMMSS);
    #else         // otherwise cycle once per hour after skipping set number of hours
    rtc.setAlarmHours((rtc.getHours() + gSkip) % 24); // set up to skip a number of first-start times
    rtc.enableAlarm(rtc.MATCH_HHMMSS);
    #endif
    SerialUSB.println("Sleeping...");
    delay(100); // wait for print to finish
    rtc.attachInterrupt(alarmMatch);
    rtc.standbyMode();  // sleep here until time for first camera on

    // now awake after sleeping until first camera start time
    rtc.disableAlarm();
    powerOn();      // turn on camera power
    delay(REC_DELAY_S * 1000); // wait for camera to initialize
    pressRecButton();  // press button to start recording

    while(1) {  // loop here forever until main battery dies
      // set up the camera stop time and go to sleep
      rtc.setAlarmTime(gStop/100, gStop%100, 0);
    #if !TIMEWARP // if not in test mode, cycle once per day
      rtc.enableAlarm(rtc.MATCH_HHMMSS);
    #else         // otherwise cycle once per hour
      rtc.enableAlarm(rtc.MATCH_MMSS);
    #endif
      SerialUSB.println("Sleeping...");
      delay(100); // wait for print to finish
      rtc.standbyMode();  // sleep here until time for camera off

      // now awake after sleeping until camera stop time
      digitalWrite(LED_BUILTIN, LOW); // turn off LED, which got turned on by RTC interrupt
      rtc.disableAlarm();
      pressRecButton();  // press button to stop recording
      delay(REC_DELAY_S * 1000); // wait for camera to finish writing files
      powerOff();     // turn off camera power

      // set up the camera start time and go to sleep
      rtc.setAlarmTime(gStart/100, gStart%100, 0);
    #if !TIMEWARP // if not in test mode, cycle once per day
      rtc.enableAlarm(rtc.MATCH_HHMMSS);
    #else         // otherwise cycle once per hour
      rtc.enableAlarm(rtc.MATCH_MMSS);
    #endif
      SerialUSB.println("Sleeping...");
      delay(100); // wait for print to finish
      rtc.standbyMode();  // sleep here until time for camera off

      // now awake after sleeping until camera start time
      rtc.disableAlarm();
      powerOn();      // turn on camera power
      delay(REC_DELAY_S * 1000); // wait for camera to initialize
      pressRecButton();  // press button to start recording

    } // end while
  } else {
    SerialUSB.println("\nDeployment aborted.");
  } // end if
} // end cmd_deploy()


int getTime() {
  int time;
  int hours;
  int minutes;
  int seconds = 0;
  int dummy;
  char* end;

  read_line(); // get time from user
  time = (int) strtoul(line, &end, 10); // convert string to integer
  hours = time / 100;   // extract hours
  minutes = time % 100; // extract minutes
  if(hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { // check for valid time
    return(-1);   // time is bad, return error
  } else {
    return(time); // time is good, return it
  } // end if
} // end getTime()


void printTime(int time) {
  int hours;
  int minutes;

  hours = time / 100;
  minutes = time % 100;

  if(hours < 10) SerialUSB.print("0"); // print a 0 before if the number is less than 10
  SerialUSB.print(hours);
  if(minutes < 10) SerialUSB.print("0"); // print a 0 before if the number is less than 10
  SerialUSB.println(minutes);
} // end printTime()


void powerOn() {
    SerialUSB.println("Turning on the camera power.");
    pinMode(DCDC_PWR_PIN, INPUT);     // set pin to high Z to turn on DC-to-DC converter
    delay(100);                       // wait for power to stabilize
    digitalWrite(CAM_PWR_OFF_PIN, HIGH);  // turn camera power off
    delay(1000);
    digitalWrite(CAM_PWR_OFF_PIN, LOW);  // turn camera power on
    delay(10);
    digitalWrite(CAM_PWR_ON_PIN, HIGH);
    gPowerIsOn = true;                // set power on flag
} // end powerOn()


void powerOff() {
    int secs;

    SerialUSB.println("Turning off the camera power.");
    digitalWrite(CAM_PWR_ON_PIN, LOW);    // turn camera power off
    digitalWrite(CAM_PWR_OFF_PIN, HIGH);
    if(gDeployed) { // only leave power on for a while if camera is in deploy mode
      for(secs = 0; secs < CAM_OFF_DELAY_S; secs++) {  // leave DC-DC on for a bit to charge cam battery
        delay(1000);
      } // end for
    } // end if
    pinMode(DCDC_PWR_PIN, OUTPUT);    // set pin to output
    digitalWrite(DCDC_PWR_PIN, LOW);  //  and drive pin low to turn off DC-to-DC converter
    gPowerIsOn = false;               // clear power on flag
} // end powerOff()


void pressRecButton() {
  SerialUSB.println("Pressing the record button.");
  digitalWrite(REC_BTN_PIN, HIGH);
  delay(BUTTON_MS);
  digitalWrite(REC_BTN_PIN, LOW);
} // end pressRecButton()


void alarmMatch()
{
  digitalWrite(LED_BUILTIN, HIGH);  // turn on LED
} // end alarmMatch()