
/*
   LoRaWAN Birdhouse 2.8.2022 JS

   GrumpyOldPizza/ArduinoCore-stm32l0
   sparkfun/SparkFun_Qwiic_Scale_NAU7802_Arduino_Library
*/

#include <Arduino.h>
#include <STM32L0.h>
#include <LoRaWAN.h>
#include <TimerMillis.h>
#include <EEPROM.h>
#include "SparkFun_Qwiic_Scale_NAU7802_Arduino_Library.h"

#define VOLTAGE_DIVIDER 5
#define SCALE_POWER 12
#define LED_RED 13
#define MEASURE_INTERVAL 300000
#define TRANSMIT_INTERVAL 3600000
#define BATTERY_LOW 3.0
#define BATTERY_HIGH 4.2
#define M (LORAWAN_BATTERY_LEVEL_FULL-LORAWAN_BATTERY_LEVEL_EMPTY)/(BATTERY_HIGH-BATTERY_LOW)
#define B LORAWAN_BATTERY_LEVEL_EMPTY-(M*BATTERY_LOW)
#define RECEIVE_BUFFER_SIZE 59
#define TRANSMIT_BUFFER_SIZE 5
#define SEND_WEIGHT_FLAG 0x80
#define TEMPERATURE_CORRECTION_FLAG 0x40

enum lorawan_downlink_t {
  COMMAND_foo,
  COMMAND_flags,
  COMMAND_temperature_offset,
  COMMAND_temperature_coefficient,
  COMMAND_save_configuration,
  COMMAND_reboot,
};

enum nau7802_input_t {
  NAU7802_ch1,
  NAU7802_ch2,
  NAU7802_ts,
};

static struct {
  int32_t channel1Offset;
  float channel1Factor;
  int32_t channel2Offset;
  float channel2Factor;
  int32_t channel3Offset;
  float channel3Factor;
  float temperatureOffset;
  float temperatureCoefficient;
  boolean sendWeight;
  boolean temperatureCorrection;
} configuration = { 0, 0, 0, 0, 0, 0, 0, 0, true, true };

static float weight = 0.0;
static float temperature = 0.0;
static float voltage = 0.0;
static uint32_t measureCounter = 0;
static boolean nau7802DoInit = true;

NAU7802 Scale;
TimerMillis Timer;

uint8_t calcLevel(float voltage) {
  int level = LORAWAN_BATTERY_LEVEL_UNKNOWN;

  // external power
  if (voltage < BATTERY_LOW) {
    level = LORAWAN_BATTERY_LEVEL_EXTERNAL;
  }

  // battery usable
  if (voltage >= BATTERY_LOW && voltage <= BATTERY_HIGH) {
    level = (int)(M * voltage + B) & 0xff;
  }

  return (level);
}

void transmitData(void) {
  uint8_t buffer[TRANSMIT_BUFFER_SIZE];
  uint16_t tmp;

  if (LoRaWAN.joined() && !LoRaWAN.busy()) {
    // zero buffer
    for (tmp = 0; tmp < TRANSMIT_BUFFER_SIZE; tmp++) buffer[tmp] = 0x00;

    // weight
    tmp = configuration.sendWeight && (abs(weight) <= 800) ? (uint16_t)(weight * 10.0 + 8192) : 8192;
    buffer[0] |= tmp & 0xff;
    buffer[1] |= (tmp >> 8) & 0x3f;

    // voltage
    tmp = (voltage >= 0) && (voltage <= 10) ? (uint16_t)(voltage * 100.0) : 0;
    buffer[1] |= (tmp & 0x03) << 6;
    buffer[2] |= (tmp >> 2) & 0xff;

    // temperature
    tmp = (abs(temperature) <= 100) ? (uint16_t)(temperature * 10.0 + 1024) : 1024;
    buffer[3] |= tmp & 0xff;
    buffer[4] |= (tmp >> 8) & 0x07;

    // flags
    tmp = configuration.sendWeight ? SEND_WEIGHT_FLAG : 0x00;
    tmp |= configuration.temperatureCorrection ? TEMPERATURE_CORRECTION_FLAG : 0x00;
    buffer[4] |= tmp & 0xff;

    digitalWrite(LED_RED, HIGH);
    LoRaWAN.setBatteryLevel(calcLevel(voltage));
    LoRaWAN.sendPacket(1, buffer, TRANSMIT_BUFFER_SIZE, false);
  }
}

void dataTransmitted(void) {
  digitalWrite(LED_RED, LOW);
  measureCounter = 0;
}

void dataReceived(void) {
  uint8_t buffer[RECEIVE_BUFFER_SIZE];
  uint8_t fport = 0;
  uint32_t size = 0;
  lorawan_downlink_t cmd = COMMAND_foo;
  union {
    float f;
    uint8_t flags;
    uint8_t b[sizeof(float)];
  } data;

  // read
  if (LoRaWAN.parsePacket()) {
    fport = LoRaWAN.remotePort();
    size = LoRaWAN.read(buffer, sizeof(buffer));
  }

  if (fport == 1) {
    // command
    if (size > 0) cmd = (lorawan_downlink_t)buffer[0];

    // data
    for (uint32_t i = 0; i < size; i++) {
      if (i >= sizeof(data)) break;
      data.b[i] = buffer[1 + i];
    }

    switch (cmd) {
      case COMMAND_flags:
        if (size != 2) break;
        configuration.sendWeight = (data.flags & SEND_WEIGHT_FLAG) ? true : false;
        configuration.temperatureCorrection = (data.flags & TEMPERATURE_CORRECTION_FLAG) ? true : false;
        break;

      case COMMAND_temperature_offset:
        if (size != 5) break;
        configuration.temperatureOffset = data.f;
        break;

      case COMMAND_temperature_coefficient:
        if (size != 5) break;
        configuration.temperatureCoefficient = data.f;
        break;

      case COMMAND_save_configuration:
        EEPROM.put(0, configuration);
        break;

      case COMMAND_reboot:
        // try to prevent reboot loops with confirmed downlinks
        if (millis() < 24 * TRANSMIT_INTERVAL) break;
        STM32L0.reset();
        break;

      default:
        break;
    }
  }
}

void periodicTimer(void) {
  // start loop
  STM32L0.wakeup();
}

void checkJoin(void) {
  if (LoRaWAN.joined()) {
    digitalWrite(LED_RED, LOW);
    Timer.start(periodicTimer, MEASURE_INTERVAL, MEASURE_INTERVAL);
  } else {
    digitalWrite(LED_RED, HIGH);
    if (Timer.active()) Timer.stop();
    LoRaWAN.rejoinOTAA();
  }
}

void checkLink(void) {
  // LoRaWAN.linkMargin();
  // LoRaWAN.linkGateways();
}

bool nau7802_init(void) {
  bool ok = true;

  Scale.begin(Wire, false);
  ok &= Scale.reset();
  ok &= Scale.powerUp();
  ok &= Scale.setLDO(NAU7802_LDO_3V0);
  ok &= Scale.setGain(NAU7802_GAIN_128);
  ok &= Scale.setSampleRate(NAU7802_SPS_20);
  uint8_t adc = Scale.getRegister(NAU7802_ADC);
  ok &= Scale.setRegister(NAU7802_ADC, adc | 0x30);
  ok &= Scale.clearBit(NAU7802_PGA_LDOMODE, NAU7802_PGA);

  return (ok);
}

bool nau7802_read(nau7802_input_t input, int32_t *value) {
  bool ok = true;

  switch (input) {
    case NAU7802_ch1:
      ok &= Scale.setChannel(NAU7802_CHANNEL_1);
      ok &= Scale.setGain(NAU7802_GAIN_128);
      break;

    case NAU7802_ch2:
      ok &= Scale.setChannel(NAU7802_CHANNEL_2);
      ok &= Scale.setGain(NAU7802_GAIN_128);
      break;

    case NAU7802_ts:
      ok &= Scale.setBit(1, NAU7802_I2C_CONTROL);
      ok &= Scale.setGain(NAU7802_GAIN_2);
      break;

    default:
      break;
  }

  delay(250); // wait for LDO to stabilize
  ok &= Scale.calibrateAFE(NAU7802_CALMOD_INTERNAL);
  Scale.getAverage(3); // dummy reads
  *value = Scale.getAverage(5);

  switch (input) {
    case NAU7802_ts:
      ok &= Scale.clearBit(1, NAU7802_I2C_CONTROL);
      break;

    default:
      break;
  }

  return (ok);
}


void setup() {
  // red led
  pinMode(LED_RED, OUTPUT);
  digitalWrite(LED_RED, HIGH);

  // scale
  pinMode(SCALE_POWER, OUTPUT);
  digitalWrite(SCALE_POWER, HIGH);

  // voltage divider
  pinMode(VOLTAGE_DIVIDER, OUTPUT);
  digitalWrite(VOLTAGE_DIVIDER, LOW);

  // read eeprom
  EEPROM.get(0, configuration);

  // i2c
  Wire.begin();

  // LoRaWAN
  LoRaWAN.begin(EU868);
  LoRaWAN.addChannel(1, 868300000, 0, 6);
  LoRaWAN.setAntennaGain(2.0);
  LoRaWAN.setDutyCycle(true);
  LoRaWAN.setADR(true);
  LoRaWAN.onJoin(checkJoin);
  LoRaWAN.onLinkCheck(checkLink);
  LoRaWAN.onTransmit(dataTransmitted);
  LoRaWAN.onReceive(dataReceived);
  // devEui, appEui, appKey are stored in eeprom
  LoRaWAN.joinOTAA();

  // stop loop
  STM32L0.deepsleep();
}

void loop() {
  bool periodicTransmit, ok;
  float deltaWeight = 0.0, oldWeight = weight;
  int32_t v1, v2, t;

  // battery voltage
  digitalWrite(VOLTAGE_DIVIDER, HIGH);
  voltage = STM32L0.getVBAT();
  digitalWrite(VOLTAGE_DIVIDER, LOW);

  // init or wakeup
  if (nau7802DoInit) nau7802DoInit = !nau7802_init();
  else Scale.powerUp();

  // channel 1
  ok = nau7802_read(NAU7802_ch1, &v1);
  weight = ok ? (v1 - configuration.channel1Offset) / configuration.channel1Factor : 0.0;

  // channel 2
  ok = nau7802_read(NAU7802_ch2, &v2);
  weight +=  ok ? (v2 - configuration.channel2Offset) / configuration.channel2Factor : 0.0;

  // temperature sensor
  ok = nau7802_read(NAU7802_ts, &t);
  temperature = ok ? (t - configuration.channel3Offset) / configuration.channel3Factor : 0.0;

  // power
  Scale.powerDown();

  // temperature correction
  if (configuration.temperatureCorrection) {
    weight += configuration.temperatureCoefficient * temperature + configuration.temperatureOffset;
  }

  // transmit
  if (configuration.sendWeight) deltaWeight = abs(weight - oldWeight);
  periodicTransmit = (++measureCounter * MEASURE_INTERVAL) >= TRANSMIT_INTERVAL ? true : false;
  if (periodicTransmit || deltaWeight >= 5.0) transmitData();

  // stop loop
  STM32L0.deepsleep();
}
