/*
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>. 
*/

#include <TimerOne.h> // From Arduino playground
#include <EEPROM.h>   // Needed by SandS
#include <Wire.h>     // Needed by SandS
#include <SandS.h>

#define PIN_OUTPUT 9
#define PIN_INPUT A0

// Device configuration
typedef struct {
  byte powerup_state; // default power on state
  byte pwm_frequency; // pwm frequency (*10hz)

  word alarm_comm;  // alarm if no comm for n milliseconds

  byte alarm_mode;  // 0: no alarm, 1: alarm if level below, 2: alarm if level equal or above
  word alarm_level; // alarm level
  word alarm_delay; // alarm only if condition met for n milliseconds
  
  byte response_mode; // alarm response 0x00: none, 0x80: off, 0x81: on

} config_t;

// Valid device commands are 0x01 ... 0x7F
typedef enum {
  CMD_READ       = 0x01,
  CMD_STATE_SET  = 0x03,
  CMD_STATE_GET  = 0x04,
  CMD_PWM_SET    = 0x05,
  CMD_PWM_GET    = 0x06,
  CMD_CONFIG_SET = 0x20,
  CMD_CONFIG_GET = 0x21,
} mosfet_command_e;

// Mosfet states (for both CMD_GET and CMD_SET)
typedef enum {
  STATE_ON  = 0x01,
  STATE_OFF = 0x00,
  STATE_PWM = 0x80,
} mosfet_state_e;

// Current state
static byte state;

// PWM counter and value
static int pwm_value = 0;

// Device default configuration
static config_t config = {
  STATE_PWM, // PWM mode
  100,       // 1kHz (100*10)
};

// Current readout
word readout;

// Alarm
static word last_time;
static word comm_timeout = 0;
static word alarm_timeout = 0;
static byte forced = 0;
static boolean last_output = LOW;

void outputWrite(boolean value) {
  last_output = value;
  digitalWrite(PIN_OUTPUT, forced ? (forced == 0x81) : value);
}

// Command handler
void command(byte cmd, int params) {
  byte set_state;
  config_t set;
  boolean accept = true;
  switch(cmd) {
    case CMD_READ:
      // Return current
      SandS.write(highByte(readout));
      SandS.write(lowByte(readout));      
      break;
    case CMD_STATE_SET:
      // Read requested state
      set_state = SandS.read();
      // Change state (if new state is a valid one)
      if(set_state == STATE_ON) {
        pinMode(PIN_OUTPUT, OUTPUT);
        outputWrite(HIGH);
        state = STATE_ON;
      } else if(set_state == STATE_OFF) {
        pinMode(PIN_OUTPUT, OUTPUT);
        outputWrite(LOW);
        state = STATE_OFF;
      } else {
        state = STATE_PWM;
        Timer1.pwm(9, pwm_value, 100000 / config.pwm_frequency);
      }
      break;
    case CMD_STATE_GET:
      // Wrong parameter count? Abort
      if(params != 0) break;
      // Return the state
      SandS.write(state);
      break;
    case CMD_PWM_SET:
      // Wrong parameter count? Abort
      if(params != 1) break;
      // Read new PWM value
      pwm_value = SandS.read();
      // Scale to full range;
      pwm_value = (pwm_value << 2) | (pwm_value >> 6);
      if(state == STATE_PWM) Timer1.setPwmDuty(PIN_OUTPUT, pwm_value);
      break;
    case CMD_PWM_GET:
      // Wrong parameter count? Abort
      if(params != 0) break;
      // Return PWM value
      SandS.write(pwm_value);
      break;
    case CMD_CONFIG_SET:
      // Wrong parameter count? Abort
      if(params != 10) break;
      // Read configuration
      set.powerup_state = SandS.read();
      set.pwm_frequency = SandS.read();
      set.alarm_mode   = SandS.read();
      set.alarm_comm   = SandS.read() << 8;
      set.alarm_comm  |= SandS.read();
      set.alarm_level  = SandS.read() << 8;
      set.alarm_level |= SandS.read();
      set.alarm_delay  = SandS.read() << 8;
      set.alarm_delay |= SandS.read();
      set.response_mode = SandS.read();

      // Verify validity of configuration
      if(set.powerup_state == STATE_ON
      || set.powerup_state == STATE_OFF
      || set.powerup_state == STATE_PWM) {
        if(set.pwm_frequency != 0 && set.pwm_frequency <= 250) {
          if(set.alarm_mode < 3) {
            if(set.response_mode == 0x00
            || set.response_mode == 0x80
            || set.response_mode == 0x81) {
              // Store configuration
              config = set;
              SandS.saveData(&config, sizeof(config));
              if(state == STATE_PWM) Timer1.setPeriod(100000 / config.pwm_frequency);
            }
          }
        }
      }
      break;
    case CMD_CONFIG_GET:
      // Wrong parameter count? Abort
      if(params != 0) break;
      // Return configuration
      SandS.write(config.powerup_state);
      SandS.write(config.pwm_frequency);
      SandS.write(config.alarm_mode);
      SandS.write(highByte(config.alarm_comm));
      SandS.write(lowByte(config.alarm_comm));
      SandS.write(highByte(config.alarm_level));
      SandS.write(lowByte(config.alarm_level));
      SandS.write(highByte(config.alarm_delay));
      SandS.write(lowByte(config.alarm_delay));
      SandS.write(config.response_mode);
      break;
    default:
      accept = false;
  }
  if(accept) comm_timeout = 0;
}

void setup(void) {
  // Identify us as a mosfet-board v1.0.0
  SandS.begin(DEVTYPE_MOSFET, '1', '0', '0');
  // Set "command" as our command handler
  SandS.onCommand(command);
  // Load configuration (if it exists)
  SandS.loadData(&config, sizeof(config));
  state = config.powerup_state;
  // Configure mosfet output pin
  pinMode(PIN_OUTPUT, OUTPUT);
  outputWrite(state == STATE_ON);
  // Configure current input pin
  pinMode(PIN_INPUT, INPUT);
  // Initialize PWM output
  Timer1.initialize();
  if(state == STATE_PWM) Timer1.pwm(9, pwm_value, 100000 / config.pwm_frequency);
  last_time = millis();
}

void loop() {
  word value;
  // Read and convert the value to milliamperes
  value = analogRead(PIN_INPUT);
  value = (word)(((double)value * 49504.95) / 1023.0);
  // Copy the value
  cli();
  readout = value;
  sei();
  // Limit nof readouts per second slightly
  delay(10);

  // Alarm timing and handling
  word time = millis();
  boolean alarm = false;
  if(config.alarm_mode) {
    alarm = (readout >= config.alarm_level) != (config.alarm_mode == 1);
    if(alarm) {
      alarm_timeout += time - last_time;
      if(alarm_timeout <= config.alarm_delay) alarm = false;
    } else {
      alarm_timeout = 0;
    }
  } else {
    alarm_timeout = 0;
  }
  if(config.alarm_comm) {
    comm_timeout += time - last_time;
    if(comm_timeout > config.alarm_comm) alarm = true;
  } else {
    comm_timeout = 0;
  }
  last_time = time;
  SandS.setAlarm(alarm);
  forced = SandS.getAlarm() ? config.response_mode : 0;
  if(forced) outputWrite(last_output);
}

