/*
  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 <SoftI2CMaster.h> // From https://github.com/todbot/SoftI2CMaster
#include <EEPROM.h>        // Needed by SandS
#include <Wire.h>          // Needed by SandS
#include <SandS.h>

// Inputs
#define PIN_DIGITAL_INPUT  9
#define PIN_ANALOG_INPUT  A3

// Configuration
#define PIN_CFG_SENSOR    A2
#define PIN_CFG_12V       A1
#define PIN_CFG_24V       A0

// Bias (MCP4716) I²C pins
#define PIN_BIAS_SCL       7
#define PIN_BIAS_SDA       3

// Gain (LTC6915) SPI pins
#define PIN_GAIN_SEL       6
#define PIN_GAIN_SCL       4
#define PIN_GAIN_SDA       5

// Address (0-7) of MCP4716
#define DAC_ADDRESS       0

// Commands for MCP4716
#define DAC_CMD_WDR    0x00 // Write DAC register
#define DAC_CMD_WVM    0x40 // Write volatile memory

// Register values for MCP4716
#define DAC_VREF_UNBUFFERED 0x2 // Use Vref without buffering

SoftI2CMaster Bias = SoftI2CMaster(PIN_BIAS_SCL, PIN_BIAS_SDA, true);

// Digital AC filter configuration
// Only half wave is measured, so set up filter to read 40 samples every second
// and consider HIGH if 50% of the halfwave is above half the voltage range.
// Host needs to set BIAS to a threshold voltage that ensures this
// condition to be true for the measured AC signal.
#define FILTER_SAMPLES   40 // Samples per 20mS
#define FILTER_THRESHOLD 10 // Threshold

// Gain:  0x1  ...  0xD  RealGain=2^(Gain-1))
//        0x0            Disabled
// Bias: 0x000 ... 0xFFF RealBias=3.3(Bias/1023.0)
// Mode:  0x00           Analog input 0 ... 3.3V
//        0x01           Analog input 0 ... 12 V
//        0x02           Analog input 0 ... 24 V
//        0x03           Analog resistance
//        0x10           Digital DC 0 ... 3.3V
//        0x11           Digital DC 0 ... 12 V
//        0x12           Digital DC 0 ... 24 V
//        0x20           Digital AC 0 ... 3.3V
//        0x21           Digital AC 0 ... 12 V
//        0x22           Digital AC 0 ... 24 V

// Device configuration
typedef struct {
  word bias; // bias
  byte gain; // gain
  byte mode; // mode
  
  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
  
} config_t;

// Valid device commands are 0x01 ... 0x7F
typedef enum {
  CMD_READ_ANALOG  = 0x01,
  CMD_READ_DIGITAL = 0x02,
  CMD_SET_BIAS     = 0x03,
  CMD_GET_BIAS     = 0x04,
  CMD_SET_GAIN     = 0x05,
  CMD_GET_GAIN     = 0x06,
  CMD_CONFIG_SET   = 0x20,
  CMD_CONFIG_GET   = 0x21,
} input_command_e;

// Device default configuration
static config_t config = {
  0, // Bias is 0.0V
  1, // Gain is x1
  2, // Mod e is analog 24V
  0, // No comm alarm
  0, // Alarm level
  0, // No level alarm
  0, // Immediate alarm
};

// Current readouts
static word analog;
static boolean digital;

// Current bias & gain
static word set_bias;
static byte set_gain;

// AC filtering
static boolean filter[FILTER_SAMPLES];
static byte filter_n;

// Alarm
static word last_time;
static word comm_timeout = 0;
static word alarm_timeout = 0;

// Initialize SPI communication against LTC6915
void Gain_begin() {
  digitalWrite(PIN_GAIN_SCL, LOW);
  digitalWrite(PIN_GAIN_SEL, HIGH);
  pinMode(PIN_GAIN_SEL,  OUTPUT);
  pinMode(PIN_GAIN_SCL, OUTPUT);
  pinMode(PIN_GAIN_SDA, OUTPUT);
  Gain_write(0);
}

// Configure LTC6915 with new gain using SPI
void Gain_write(byte reg) {
  // Enable transfer
  digitalWrite(PIN_GAIN_SEL, LOW);
  delay(5);
  for(byte n = 0; n < 2; n++) {
    // Write bit 3
    digitalWrite(PIN_GAIN_SDA, reg & 8 ? HIGH : LOW);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, HIGH);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, LOW);
    delay(5);
    // Write bit 2
    digitalWrite(PIN_GAIN_SDA, reg & 4 ? HIGH : LOW);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, HIGH);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, LOW);
    delay(5);
    // Write bit 1
    digitalWrite(PIN_GAIN_SDA, reg & 2 ? HIGH : LOW);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, HIGH);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, LOW);
    delay(5);
    // Write bit 0
    digitalWrite(PIN_GAIN_SDA, reg & 1 ? HIGH : LOW);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, HIGH);
    delay(5);
    digitalWrite(PIN_GAIN_SCL, LOW);
    delay(5);
  }
  // Latch (activate) new gain
  digitalWrite(PIN_GAIN_SEL, HIGH);
  delay(5);
  // Remember
  set_gain = reg;
}

// Initialize MCP4716
void Bias_begin() {
  Bias.beginTransmission(0x60 | DAC_ADDRESS);
  Bias.send(DAC_CMD_WVM | (DAC_VREF_UNBUFFERED << 3) ); // Configuration
  Bias.send(0x00); // MSB of DAC register
  Bias.send(0x00); // LSB of DAC register
  Bias.endTransmission();
}

// Write new DAC value to MCP4716
boolean Bias_write(word reg) {
  if(Bias.beginTransmission(0x60 | DAC_ADDRESS)) return false;
  Bias.send(DAC_CMD_WDR | (highByte(reg) & 0xF)); // Command + MSB of DAC register
  Bias.send(lowByte(reg)); // LSB of DAC register
  // Remember
  set_bias = reg;
  return true;
}

// Configure mode
void Mode_write(byte mode) {
  if(mode == 0x03) {
    digitalWrite(PIN_CFG_12V, LOW);
    digitalWrite(PIN_CFG_24V, LOW);
    digitalWrite(PIN_CFG_SENSOR, HIGH);
  } else {
    digitalWrite(PIN_CFG_SENSOR, LOW);
    switch(mode & 0x0F) {
      case 0: // 0 ... 3.3V
        digitalWrite(PIN_CFG_12V, LOW);
        digitalWrite(PIN_CFG_24V, LOW);
        break;
      case 1: // 0 ... 12V
        digitalWrite(PIN_CFG_24V, LOW);
        digitalWrite(PIN_CFG_12V, HIGH);
        break;
      case 2: // 0 ... 24V
        digitalWrite(PIN_CFG_12V, LOW);
        digitalWrite(PIN_CFG_24V, HIGH);
        break;
    }
  }
}

// Command handler
void command(byte cmd, int params) {
  config_t set;
  boolean accept = true;
  
  switch(cmd) {
    case CMD_READ_ANALOG:
      // Return current analog
      SandS.write(highByte(analog));
      SandS.write(lowByte(analog));      
      break;
    case CMD_READ_DIGITAL:
      // Return current analog
      SandS.write(digital ? 0x8F : 0x80);
      break;
    case CMD_SET_BIAS:
      set.bias = SandS.read() << 8;
      set.bias |= SandS.read();
      if(set.bias <= 4095) Bias_write(set.bias);
      break;
    case CMD_GET_BIAS:
      SandS.write(highByte(set_bias));
      SandS.write(lowByte(set_bias));      
      break;
    case CMD_SET_GAIN:
      set.gain = SandS.read();
      if(set.gain <= 0x0D) Gain_write(set.gain);
      break;
    case CMD_GET_GAIN:
      SandS.write(set_gain);
      break;
    case CMD_CONFIG_SET:
      // Wrong parameter count? Abort
      if(params != 11) break;
      // Read configuration
      set.mode         = SandS.read();
      set.bias         = SandS.read() << 8;
      set.bias        |= SandS.read();
      set.gain         = SandS.read();
      set.alarm_comm   = SandS.read() << 8;
      set.alarm_comm  |= SandS.read();
      set.alarm_mode   = SandS.read();
      set.alarm_level  = SandS.read() << 8;
      set.alarm_level |= SandS.read();
      set.alarm_delay  = SandS.read() << 8;
      set.alarm_delay |= SandS.read();
      // Verify validity of configuration
      if(set.mode == 0x03 || ((set.mode >> 4) <=2 && (set.mode & 0xF) <= 2)) {
        if(set.bias < 4096) {
          if(set.gain != 0 && set.gain <= 0xD) {
            if(set.alarm_mode < 3) {
              // Store configuration
              config = set;
              SandS.saveData(&config, sizeof(config));
              // Activate configuration
              Bias_write(config.bias);
              Gain_write(config.gain);
              Mode_write(config.mode);
            }
          }
        }
      } 
      break;
    case CMD_CONFIG_GET:
      // Wrong parameter count? Abort
      if(params != 0) break;
      // Return configuration
      SandS.write(config.mode);
      SandS.write(highByte(config.bias));
      SandS.write(lowByte(config.bias));
      SandS.write(config.gain);
      SandS.write(highByte(config.alarm_comm));
      SandS.write(lowByte(config.alarm_comm));
      SandS.write(config.alarm_mode);
      SandS.write(highByte(config.alarm_level));
      SandS.write(lowByte(config.alarm_level));
      SandS.write(highByte(config.alarm_delay));
      SandS.write(lowByte(config.alarm_delay));
      break;
    default:
      accept = false;
  }
  if(accept) comm_timeout = 0;
}

void setup(void) {
  // Identify us as a consumption-board v1.0.0
  SandS.begin(DEVTYPE_INPUT, '1', '0', '0');
  // Set "command" as our command handler
  SandS.onCommand(command);
  // Load configuration (if it exists)
  SandS.loadData(&config, sizeof(config));
  // Set up input configuration
  pinMode(PIN_CFG_SENSOR, OUTPUT);
  pinMode(PIN_CFG_12V, OUTPUT);
  pinMode(PIN_CFG_24V, OUTPUT);
  digitalWrite(PIN_CFG_SENSOR, LOW);
  digitalWrite(PIN_CFG_12V, LOW);
  digitalWrite(PIN_CFG_24V, LOW);
  // Set up the gain
  Gain_begin();
  // Set up the bias
  Bias_begin();
  // Set up the ADC
  pinMode(PIN_ANALOG_INPUT, INPUT);
  analogReference(EXTERNAL);
  // Activate configuration
  Bias_write(config.bias);
  Gain_write(config.gain);
  Mode_write(config.mode);
  last_time = millis();
}

void loop() {
  word value;
  byte n, count = 0;
  if((config.mode >> 4) == 0x0) {
    // MODE: ANALOG
    // Read and convert the value to milliamperes
    value = analogRead(PIN_ANALOG_INPUT);
    // Copy the value
    cli();
    analog = value;
    sei();
  } else {
    // MODE: DIGITAL
    if((config.mode >> 4) == 2) {
      // DIGITAL: AC
      filter[filter_n] = digitalRead(PIN_DIGITAL_INPUT);
      if(++filter_n >= FILTER_SAMPLES) filter_n = 0;
      for(n = 0; n < FILTER_SAMPLES; n++) {
        if(filter[filter_n]) count++;
      }
      digital = count > FILTER_THRESHOLD;
      // TODO: is this delay really good enough?
      delay(20 / FILTER_SAMPLES);
    } else {
      // DIGITAL: DC
      digital = digitalRead(PIN_DIGITAL_INPUT);
    }
  }

  // Alarm timing and handling
  word time = millis();
  boolean alarm = false;
  if(config.alarm_mode) {
    if((config.mode >> 4) == 0x0) {
      alarm = (digital) != (config.alarm_mode == 1);
    } else {
      alarm = (analog >= 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);
}

