// Piscavore Camera Controller
// P. McGill, MBARI, 2023
//
// v1.0.0  Initial release

#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 LED_PIN 13          // Set hi to turn on green LED
#define PWR_PIN A1          // Set as analog input to turn on camera power, set as low output to turn off power
#define BTN_PIN 4           // Set high to push camera button
#define BATV_PIN A4         // Connected to aux battery through 1/2 voltage divider
#define TIMEWARP 0          // Set to 1 to make camera record once per hour instead of once per day

// 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;
int   gFirst = 0;     // time to start camera on day of deployment
int   gStart = 0;     // time to start camera every day after
int   gStop = 0;      // time to stop camera every day
 
// Function declarations
int cmd_help();
int cmd_event();
int cmd_power();
int cmd_button();
int cmd_record();
int cmd_clock();
int cmd_battery();
int cmd_deploy();
int getTime();
void printTime(int);
void powerOn();
void powerOff();
void pressButton();
void alarmMatch();
 
// List of functions pointers corresponding to each command
int (*commands_func[])() {
  &cmd_help,
  &cmd_event,
  &cmd_power,
  &cmd_button,
  &cmd_record,
  &cmd_clock,
  &cmd_battery,
  &cmd_deploy
};
 
// List of command names
const char *commands_str[] = {
  "help",
  "event",
  "power",
  "button",
  "record",
  "clock",
  "battery",
  "deploy"
};
 
// List of event subcommand names
const char *event_args[] = {
    "first",
    "start",
    "stop",
    "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",
    "aux"
};

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


void setup() {
  SerialUSB.begin(9600);  // set up serial port
  SerialUSB.setTimeout(SER_TIMEOUT_S * 1000);
  pinMode(PWR_PIN, OUTPUT);    // set up output pins
  pinMode(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(PWR_PIN, LOW);  // make sure camera power is off
  digitalWrite(BTN_PIN, LOW);  // make sure button isn't pressed

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

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


void cli_init() {
  SerialUSB.println("Welcome to this simple Arduino command line interface (CLI).");
  SerialUSB.println("Type \"help\" to see a list of commands.");
} // end cli_init()


void my_cli() {
  SerialUSB.print("> ");

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

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

  gErrorFlag = false;
} // end my_cli()


void read_line() {
  String line_string;

  while(!SerialUSB.available()); // wait here until a string is entered

  if(SerialUSB.available()) {
    line_string = SerialUSB.readStringUntil('\n');
    if(line_string.length() < LINE_BUF_SIZE) {
      line_string.toCharArray(line, LINE_BUF_SIZE);
      // SerialUSB.println(line_string);
    } else {
      SerialUSB.println("Input string too long.");
      gErrorFlag = true;
    } // end if
  } // end if
} // end read_line()


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()


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] != 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_record();
  } else if(strcmp(args[1], commands_str[5]) == 0){
    help_clock();
  } else if(strcmp(args[1], commands_str[6]) == 0){
    help_battery();
  } else if(strcmp(args[1], commands_str[7]) == 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 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 camera 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 auxiliary battery voltage:");
  SerialUSB.println("  battery main");
  SerialUSB.println("  battery aux");
} // end help_battery()


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


int cmd_event() {
  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 "list"
    SerialUSB.print("First time is ");
    printTime(gFirst);
    SerialUSB.print("Start time is ");
    printTime(gStart);
    SerialUSB.print("Stop time is  ");
    printTime(gStop);
  } 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() {
  pressButton();
} // end cmd_button()


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);

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

    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 ");
      SerialUSB.println(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);
    //if(seconds < 10) SerialUSB.print("0"); // print a 0 before if the number is less than 10
    //SerialUSB.println(seconds);

  } 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(PWR_PIN) * 4 * 2.23 / 1023;
      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 "aux"
    battVolt = analogRead(BATV_PIN) * 2 * 2.23 / 1023;
    SerialUSB.print("Aux 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;

  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
    pressButton();  // 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
    pressButton();  // 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 first camera start time and go to sleep
    rtc.setAlarmTime(gFirst/100, gFirst%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
    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
    pressButton();  // 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
      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();
      pressButton();  // 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...");
      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
      pressButton();  // 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;

    time = SerialUSB.parseInt(SKIP_ALL,':');      // get time, ignore colon if present
    while(SerialUSB.available() > 0) dummy = SerialUSB.read(); // flush serial buffer
    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(PWR_PIN, INPUT);      // set pin to high Z
    gPowerIsOn = true;            // set power on flag
} // end powerOn()


void powerOff() {
    SerialUSB.println("Turning off the camera power.");
    pinMode(PWR_PIN, OUTPUT);     // set pin to output
    digitalWrite(PWR_PIN, LOW);   //  and drive pin low
    gPowerIsOn = false;           // clear power on flag
} // end powerOff()


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


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