// VLCB_4in4out

/*
  Copyright (C) 2023 Martin Da Costa
  This file is part of VLCB-Arduino project on https://github.com/SvenRosvall/VLCB-Arduino
  Licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
  The full licence can be found at: http://creativecommons.org/licenses/by-nc-sa/4.0

  3rd party libraries needed for compilation: (not for binary-only distributions)

  Streaming   -- C++ stream style output, v5, (http://arduiniana.org/libraries/streaming/)
*/

//
// This is a modified version of VLCB_4in4out
// It uses the ModifiedGridConnect protocol over serial instead of a CAN interface
// this allows a single module to connect via a USB cable to a management tool like FCU
// without a CAN network
//
///////////////////////////////////////////////////////////////////////////////////
// Pin Use map UNO:
// Digital pin 2
// Digital pin 3 (PWM)    Module LED 1
// Digital pin 4          VLCB Green LED
// Digital pin 5 (PWM)    Module LED 2
// Digital pin 6 (PWM)    Module LED 3
// Digital pin 7          VLCB Yellow LED
// Digital pin 8          VLCB Switch
// Digital pin 9 (PWM)    Module LED 4
// Digital pin 10
// Digital pin 11
// Digital pin 12
// Digital pin 13

// Digital / Analog pin 0     Module Switch 1
// Digital / Analog pin 1     Module Switch 2
// Digital / Analog pin 2     Module Switch 3
// Digital / Analog pin 3     Module Switch 4
// Digital / Analog pin 4     Not Used
// Digital / Analog pin 5     Not Used
//////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////
// 
// There are 4 node variables, one for each switch.
//  NV1-4 - Behaviour of switch 1-4: 0) None 1) On/Off 2) On only 3) Off only 4) Toggle
//
// Event variables:
//  EV1 - Produce event for switch N where N is EV1 value in the range 1-4. Must be unique.
//        A value of 255 means a Start-of-Day event.
//  EV2-5 - Change LED 1-4: 0) No change 1) Normal 2) Slow blink 3) Fast blink

#define DEBUG 1  // set to 0 for no serial debug

#if DEBUG
#define DEBUG_PRINT(S) Serial << S << endl
#else
#define DEBUG_PRINT(S)
#endif

// 3rd party libraries
#include <Streaming.h>

// VLCB library header files
#include <VLCB.h>
#include <SerialGC.h>               // replaces CAN controller

// forward function declarations
void eventhandler(byte, const VLCB::VlcbMessage *);
byte eventValidator(int nn, int en, byte evNum, byte evValue);
void printConfig();
void processSwitches();

// constants
const byte VER_MAJ = 1;             // code major version
const char VER_MIN = 'b';           // code minor version
const byte VER_BETA = 0;            // code beta sub-version
const byte MANUFACTURER = MANU_DEV; // for boards in development.
const byte MODULE_ID = 82;          // VLCB module type

// module name, must be at most 7 characters
char mname[] = "4IN4OUT";

const byte LED_GRN = 4;             // VLCB green Unitialised LED pin
const byte LED_YLW = 7;             // VLCB yellow Normal LED pin
const byte SWITCH0 = 8;             // VLCB push button switch pin

// Module objects
const byte LED[] = {3, 5, 6, 9};     // LED pin connections through typ. 1K8 resistor
const byte SWITCH[] = {A0, A1, A2, A3}; // Module Switch takes input to 0V.

const byte NUM_LEDS = sizeof(LED) / sizeof(LED[0]);
const byte NUM_SWITCHES = sizeof(SWITCH) / sizeof(SWITCH[0]);


// instantiate module objects
VLCB::LED moduleLED[NUM_LEDS];     //  LED as output
VLCB::Switch moduleSwitch[NUM_SWITCHES];  //  switch as input
bool state[NUM_SWITCHES];

VLCB::SerialGC serialGC;                  // CAN transport object using serial

// Service objects
VLCB::LEDUserInterface ledUserInterface(LED_GRN, LED_YLW, SWITCH0);
VLCB::MinimumNodeService mnService;
VLCB::CanService canService(&serialGC);
VLCB::NodeVariableService nvService;
VLCB::ConsumeOwnEventsService coeService;
VLCB::EventConsumerService ecService;
VLCB::EventTeachingService etService;
VLCB::EventProducerService epService;

//
/// setup VLCB - runs once at power on from setup()
//
void setupVLCB()
{
  VLCB::checkStartupAction(LED_GRN, LED_YLW, SWITCH0);

  VLCB::setServices({
    &mnService, &ledUserInterface, &canService, &nvService,
    &ecService, &epService, &etService, &coeService});

  // set config layout parameters
  VLCB::setNumNodeVariables(NUM_SWITCHES);
  VLCB::setMaxEvents(64);
  VLCB::setNumEventVariables(1 + NUM_LEDS);

  // set module parameters
  VLCB::setVersion(VER_MAJ, VER_MIN, VER_BETA);
  VLCB::setModuleId(MANUFACTURER, MODULE_ID);

  // set module name
  VLCB::setName(mname);

  // register our VLCB event handler, to receive event messages of learned events
  ecService.setEventHandler(eventhandler);
  // register the VLCB request event handler to receive event status requests.
  epService.setRequestEventHandler(eventhandler);
  
  // register a validator for taught VLCB events.
  etService.setEventValidator(eventValidator);

  // configure and start Serial GridConnect transport
  if (!serialGC.begin())
  {
    Serial << F("> error starting VLCB") << endl;
  }

  // initialise and load configuration
  VLCB::begin();

  Serial << F("> mode = (") << _HEX(VLCB::getCurrentMode()) << ") " << VLCB::Configuration::modeString(VLCB::getCurrentMode());
  Serial << F(", CANID = ") << VLCB::getCANID();
  Serial << F(", NN = ") << VLCB::getNodeNum() << endl;

  // show code version and copyright notice
  printConfig();
}

void setupModule()
{
  // configure the module switches, active low
  for (byte i = 0; i < NUM_SWITCHES; i++)
  {
    moduleSwitch[i].setPin(SWITCH[i], INPUT_PULLUP);
    state[i] = false;
  }

  // configure the module LEDs
  for (byte i = 0; i < NUM_LEDS; i++)
  {
    moduleLED[i].setPin(LED[i], LOW);  //Second argument active low or active high. Default if no second argument is active high.
  }

  Serial << "> Module has " << NUM_LEDS << " LEDs and " << NUM_SWITCHES << " switches." << endl;
}

//
/// setup - runs once at power on
//
void setup()
{
  Serial.begin (115200);
  Serial << endl << endl << F("> ** VLCB Arduino basic example module ** ") << __FILE__ << endl;

  setupVLCB();
  setupModule();

  // end of setup
  Serial << F("> ready") << endl << endl;
}

//
/// loop - runs forever
//
void loop()
{
  //
  /// do VLCB message, switch and LED processing
  //
  VLCB::process();

  // Run the LED code
  for (int i = 0; i < NUM_LEDS; i++)
  {
    moduleLED[i].run();
  }

  // test for switch input
  processSwitches();

  // bottom of loop()
}

byte eventValidator(int nn, int en, byte evNum, byte evValue)
{
  // EV#1 is for produced events. It specifies which switch triggers this event.
  // There can only be one event with EV#1 set to a specific switch.
  if (evNum == 1)
  {
    // Search for an event where EV#1 has the same value.
    byte index = VLCB::findExistingEventByEv(evNum, evValue);
    if (VLCB::isEventIndexValid(index))
    {
      // Yes, one such event does exist.
      return CMDERR_INV_EV_VALUE;
    }
  }
  return GRSP_OK;
}

byte createEvent(unsigned int nn, byte preferredEN)
{
  //DEBUG_PRINT(F("sk> Will create event nn=") << nn << " en=" << preferredEN);
  byte eventIndex = VLCB::findExistingEvent(nn, preferredEN);
  if (VLCB::isEventIndexValid(eventIndex))
  {
    //DEBUG_PRINT(F("sk> Preferred event already exists. Try to find a free event number"));
    // Find an unused EN
    for (unsigned int en = 1 ; en < 65535 ; ++en)
    {
      //DEBUG_PRINT(F("sk> Trying en=") << en);
      eventIndex = VLCB::findExistingEvent(nn, en);
      if (!VLCB::isEventIndexValid(eventIndex))
      {
        //DEBUG_PRINT(F("sk> is free, create it."));
        eventIndex = VLCB::findEmptyEventSpace();
        if (VLCB::isEventIndexValid(eventIndex))
        {
          VLCB::createEventAtIndex(eventIndex, nn, en);
          //DEBUG_PRINT(F("sk> Created event at index=") << eventIndex);
          return eventIndex;
        }
        else
        {
          //DEBUG_PRINT(F("sk> No empty space for event. index=") << eventIndex);
          return 0xFF;
        }
      }
    }
    //DEBUG_PRINT(F("sk> No free event number"));
    return 0xFF;
  }
  else
  {
    //DEBUG_PRINT(F("sk> Preferred event does not exist."));
    // Find an empty slot to create an event.
    eventIndex = VLCB::findEmptyEventSpace();
    if (VLCB::isEventIndexValid(eventIndex))
    {
      //DEBUG_PRINT(F("sk> Creating preferred event"));
      VLCB::createEventAtIndex(eventIndex, nn, preferredEN);
      return eventIndex;
    }
    else
    {
      //DEBUG_PRINT(F("sk> No empty space for event. index=") << eventIndex);
      return 0xFF;
    }
  }
}

void processSwitches(void) 
{
  for (byte i = 0; i < NUM_SWITCHES; i++)
  {
    moduleSwitch[i].run();
    if (moduleSwitch[i].stateChanged())
    {
      byte nv = i + 1;
      byte nvval = VLCB::readNV(nv);
      byte swNum = i + 1;
      DEBUG_PRINT(F("sk> Button ") << swNum << F(" state change detected. NV Value = ") << nvval);

      byte eventIndex = VLCB::findExistingEventByEv(1, swNum);
      if (!VLCB::isEventIndexValid(eventIndex))
      {
        //DEBUG_PRINT(F("sk> No event for this button."));
        eventIndex = createEvent(VLCB::getNodeNum(), swNum);
        if (!VLCB::isEventIndexValid(eventIndex))
        {
          //DEBUG_PRINT(F("sk> Could not create default event"));
          // Could not create default event. Ignore it and don't send an event.
          continue;
        }
        // Created a valid event, now set EV1 to the switch number.
        VLCB::writeEventVariable(eventIndex, 1, swNum);
        //DEBUG_PRINT(F("sk> Wrote event variable 1 value=") << swNum);
      }
      
      DEBUG_PRINT(F("sk> Using event at index=") << eventIndex);

      switch (nvval)
      {
        case 1:
          // ON and OFF
          state[i] = (moduleSwitch[i].isPressed());
          DEBUG_PRINT(F("sk> Button calling case 1: ") << swNum << (state[i] ? F(" pressed, send state: ") : F(" released, send state: ")) << state[i]);
          epService.sendEventAtIndex(state[i], eventIndex);
          break;

        case 2:
          // Only ON
          if (moduleSwitch[i].isPressed()) 
          {
            state[i] = true;
            DEBUG_PRINT(F("sk> Button calling case 2: ") << swNum << F(" pressed, send state: ") << state[i]);
            epService.sendEventAtIndex(state[i], eventIndex);
          }
          break;

        case 3:
          // Only OFF
          if (moduleSwitch[i].isPressed())
          {
            state[i] = false;
            DEBUG_PRINT(F("sk> Button calling case 3: ") << swNum << F(" pressed, send state: ") << state[i]);
            epService.sendEventAtIndex(state[i], eventIndex);
          }
          break;

        case 4:
          // Toggle button
          if (moduleSwitch[i].isPressed())
          {
            state[i] = !state[i];
            DEBUG_PRINT(F("sk> Button calling case 4: ") << swNum << (state[i] ? F(" pressed, send state: ") : F(" released, send state: ")) << state[i]);
            epService.sendEventAtIndex(state[i], eventIndex);
          }
          break;

        default:
          DEBUG_PRINT(F("sk> Button ") << swNum << F(" do nothing."));
          break;
      }
    }
  }
}

//
/// Do Start-of-Day reporting. I.e. send events for each input with their current state.
//
void doStartOfDay()
{
  DEBUG_PRINT(F("sk> Starting of Day Start"));
  for (byte i = 0; i < NUM_LEDS; i++)
  {
    byte swNum = i + 1;
    DEBUG_PRINT(F("sk> Button ") << swNum);

    byte eventIndex = VLCB::findExistingEventByEv(1, swNum);
    if (!VLCB::isEventIndexValid(eventIndex))
    {
      // No event for this switch.
      continue;
    }

    // TODO: Should we send events for on/off only switches?
    epService.sendEventAtIndex(state[i], eventIndex);
  }
  DEBUG_PRINT(F("sk> Stopping of Day Start"));
}

//
/// called from the VLCB library when a learned event is received
//
void eventhandler(byte index, const VLCB::VlcbMessage *msg)
{
  byte opc = msg->data[0];

  DEBUG_PRINT(F("sk> event handler: index = ") << index << F(", opcode = 0x") << _HEX(msg->data[0]));

  unsigned int node_number = (msg->data[1] << 8) + msg->data[2];
  unsigned int event_number = (msg->data[3] << 8) + msg->data[4];
  DEBUG_PRINT(F("sk> NN = ") << node_number << F(", EN = ") << event_number);
  DEBUG_PRINT(F("sk> op_code = ") << opc);

  switch (opc)
  {
    case OPC_ACON:
    case OPC_ASON:
      DEBUG_PRINT(F("sk> case is opCode ON"));
      if (VLCB::getEventEVval(index, 1) == 0xFF)
      {
        doStartOfDay();
        return;
      }

      for (byte i = 0; i < NUM_LEDS; i++)
      {
        byte ev = i + 2;
        byte evval = VLCB::getEventEVval(index, ev);
        DEBUG_PRINT(F("sk> EV = ") << ev << (" Value = ") << evval);

        switch (evval)
        {
          case 1:
            moduleLED[i].on();
            break;

          case 2:
            moduleLED[i].blink(500);
            break;

          case 3:
            moduleLED[i].blink(250);
            break;

          default:
            break;
        }
      }
      break;

    case OPC_ACOF:
    case OPC_ASOF:
      DEBUG_PRINT(F("sk> case is opCode OFF"));
      for (byte i = 0; i < NUM_LEDS; i++)
      {
        byte ev = i + 2;
        byte evval = VLCB::getEventEVval(index, ev);

        if (evval > 0)
        {
          moduleLED[i].off();
        }
      }
      break;
      
    case OPC_AREQ:
    case OPC_ASRQ:
      byte evval = VLCB::getEventEVval(index, 1) - 1;
      DEBUG_PRINT(F("> Handling request op =  ") << _HEX(opc) << F(", request input = ") << evval << F(", state = ") << state[evval]);
      epService.sendEventResponse(state[evval], index);
  }
}

//
/// print code version config details and copyright notice
//
void printConfig()
{
  // code version
  Serial << F("> code version = ") << VER_MAJ << VER_MIN << F(" beta ") << VER_BETA << endl;
  Serial << F("> compiled on ") << __DATE__ << F(" at ") << __TIME__ << F(", compiler ver = ") << __cplusplus << endl;

  // copyright
  Serial << F("> © Martin Da Costa (MERG M6237) 2023") << endl;
}
