/*
  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

// Device configuration
typedef struct {
  byte powerup_state; // default power on state
  byte pwm_period;    // pwm period (seconds)
  word alarm_comm;    // alarm if no comm for n milliseconds
  byte response_mode; // response to alarm (0x00: none, 0x81: ON, 0x80: OFF)
} config_t;

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

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

// Current state
static byte state;

// PWM counter and value
static byte pwm_count = 0, pwm_value = 0;

// Device default configuration
static config_t config = {
  STATE_OFF,
  5,
  0, // No comm alarm
  0, // No alarm response
};

// Alarm
static word last_time;
static word comm_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);
}

void soft_pwm() {
  if(++pwm_count == 255) pwm_count = 0; // Skip 255 for full 0-100% range
  outputWrite(pwm_value > pwm_count);
}

// Command handler
void command(byte cmd, int params) {
  byte set_state;
  config_t set;
  boolean accept = true;
  switch(cmd) {
    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) {
        outputWrite(HIGH);
        state = STATE_ON;
      } else if(set_state == STATE_OFF) {
        outputWrite(LOW);
        state = STATE_OFF;
      } else {
        state = STATE_PWM;
        Timer1.attachInterrupt(soft_pwm);
      }
      if(state != STATE_PWM) Timer1.detachInterrupt();
      break;
    case CMD_STATE_GET:
      // Wrong parameter count? Abort
      if(params != 0) break;
      if(state == STATE_PWM) {
        // Return the state + output state
        if(digitalRead(PIN_OUTPUT)) {
          SandS.write(STATE_PWM | STATE_ON);
        } else {
          SandS.write(STATE_PWM | STATE_OFF);
        }
      } else {
        // 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();
      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 != 5) break;
      // Read configuration
      set.powerup_state  = SandS.read();
      set.pwm_period     = SandS.read();
      set.alarm_comm     = SandS.read() << 8;
      set.alarm_comm    |= 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_period != 0 && set.pwm_period <= 240) {
          if(set.response_mode == 0x00
          || set.response_mode == 0x80
          || set.response_mode == 0x81) {
            // Store configuration
            config = set;
            SandS.saveData(&config, sizeof(config));
            Timer1.setPeriod((config.pwm_period * 1000000) / 255);
          }
        }
      }
      break;
    case CMD_CONFIG_GET:
      // Wrong parameter count? Abort
      if(params != 0) break;
      // Return configuration
      SandS.write(config.powerup_state);
      SandS.write(config.pwm_period);
      SandS.write(highByte(config.alarm_comm));
      SandS.write(lowByte(config.alarm_comm));
      SandS.write(config.response_mode);
      break;
    default:
      accept = false;
  }
  if(accept) comm_timeout = 0;
}

void setup(void) {
  // Identify us as a relay-board v1.0.0
  SandS.begin(DEVTYPE_RELAY, '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 relay output pin
  pinMode(PIN_OUTPUT, OUTPUT);
  outputWrite(state == STATE_ON);
  // Initialize PWM timer
  Timer1.initialize((config.pwm_period * 1000000) / 255);
  if(state == STATE_PWM) Timer1.attachInterrupt(soft_pwm);
  last_time = millis();
}

void loop() {
//  Serial.println("Hello?");
//  delay(50);
  // Alarm timing and handling
  word time = millis();
  boolean alarm = false;
  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);
}

