/*
  This is a library written for the SCD30
  SparkFun sells these at its website: www.sparkfun.com
  Do you like this library? Help support SparkFun. Buy a board!
  https://www.sparkfun.com/products/14751

  Written by Nathan Seidle @ SparkFun Electronics, May 22nd, 2018

  Updated February 1st 2021 to include some of the features of paulvha's version of the library
  (while maintaining backward-compatibility):
  https://github.com/paulvha/scd30
  Thank you Paul!

  The SCD30 measures CO2 with accuracy of +/- 30ppm.

  This library handles the initialization of the SCD30 and outputs
  CO2 levels, relative humidty, and temperature.

  https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library

  Development environment specifics:
  Arduino IDE 1.8.13

  SparkFun code, firmware, and software is released under the MIT License.
  Please see LICENSE.md for more details.
*/

#include "SparkFun_SCD30_Arduino_Library.h"

SCD30::SCD30(void)
{
  // Constructor
}

// Initialize the Serial port
#ifdef USE_TEENSY3_I2C_LIB
bool SCD30::begin(i2c_t3 &wirePort, bool autoCalibrate, bool measBegin)
#else
bool SCD30::begin(TwoWire &wirePort, bool autoCalibrate, bool measBegin)
#endif
{
  _i2cPort = &wirePort; // Grab which port the user wants us to use

  /* Especially during obtaining the ACK BIT after a byte sent the SCD30 is using clock stretching  (but NOT only there)!
   * The need for clock stretching is described in the Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
   *
   * The default clock stretch (maximum wait time) on the ESP8266-library (2.4.2) is 230us which is set during _i2cPort->begin();
   * In the current implementation of the ESP8266 I2C driver there is NO error message when this time expired, while
   * the clock stretch is still happening, causing uncontrolled behaviour of the hardware combination.
   *
   * To set ClockStretchlimit() a check for ESP8266 boards has been added in the driver.
   *
   * With setting to 20000, we set a max timeout of 20mS (> 20x the maximum measured) basically disabling the time-out
   * and now wait for clock stretch to be controlled by the client.
   */

#if defined(ARDUINO_ARCH_ESP8266)
  _i2cPort->setClockStretchLimit(200000);
#endif

  if (isConnected() == false)
    return (false);

  if (measBegin == false) // Exit now if measBegin is false
    return (true);

  // Check for device to respond correctly
  if (beginMeasuring() == true) // Start continuous measurements
  {
    setMeasurementInterval(2);             // 2 seconds between measurements
    setAutoSelfCalibration(autoCalibrate); // Enable auto-self-calibration

    return (true);
  }

  return (false); // Something went wrong
}

// Returns true if device responds to a firmware request
bool SCD30::isConnected()
{
  uint16_t fwVer;
  if (getFirmwareVersion(&fwVer) == false) // Read the firmware version. Return false if the CRC check fails.
    return (false);

  if (_printDebug == true)
  {
    _debugPort->print(F("Firmware version 0x"));
    _debugPort->println(fwVer, HEX);
  }

  return (true);
}

// Calling this function with nothing sets the debug port to Serial
// You can also call it with other streams like Serial1, SerialUSB, etc.
void SCD30::enableDebugging(Stream &debugPort)
{
  _debugPort = &debugPort;
  _printDebug = true;
}

// Returns the latest available CO2 level
// If the current level has already been reported, trigger a new read
uint16_t SCD30::getCO2(void)
{
  if (co2HasBeenReported == true) // Trigger a new read
  {
    if (readMeasurement() == false) // Pull in new co2, humidity, and temp into global vars
      if (!_useStaleData)
        co2 = 0; // Failed to read sensor
  }

  co2HasBeenReported = true;

  return (uint16_t)co2; // Cut off decimal as co2 is 0 to 10,000
}

// Returns the latest available humidity
// If the current level has already been reported, trigger a new read
float SCD30::getHumidity(void)
{
  if (humidityHasBeenReported == true) // Trigger a new read
    if (readMeasurement() == false)    // Pull in new co2, humidity, and temp into global vars
      if (!_useStaleData)
        humidity = 0; // Failed to read sensor

  humidityHasBeenReported = true;

  return humidity;
}

// Returns the latest available temperature
// If the current level has already been reported, trigger a new read
float SCD30::getTemperature(void)
{
  if (temperatureHasBeenReported == true) // Trigger a new read
    if (readMeasurement() == false)       // Pull in new co2, humidity, and temp into global vars
      if (!_useStaleData)
        temperature = 0; // Failed to read sensor

  temperatureHasBeenReported = true;

  return temperature;
}

// Enables or disables the ASC
bool SCD30::setAutoSelfCalibration(bool enable)
{
  if (enable)
    return sendCommand(COMMAND_AUTOMATIC_SELF_CALIBRATION, 1); // Activate continuous ASC
  else
    return sendCommand(COMMAND_AUTOMATIC_SELF_CALIBRATION, 0); // Deactivate continuous ASC
}

// Set the forced recalibration factor. See 1.3.7.
// The reference CO2 concentration has to be within the range 400 ppm ≤ cref(CO2) ≤ 2000 ppm.
bool SCD30::setForcedRecalibrationFactor(uint16_t concentration)
{
  if (concentration < 400 || concentration > 2000)
  {
    return false; // Error check.
  }
  return sendCommand(COMMAND_SET_FORCED_RECALIBRATION_FACTOR, concentration);
}

// Get the temperature offset. See 1.3.8.
float SCD30::getTemperatureOffset(void)
{
  uint16_t response = readRegister(COMMAND_SET_TEMPERATURE_OFFSET);

  union
  {
    int16_t signed16;
    uint16_t unsigned16;
  } signedUnsigned; // Avoid any ambiguity casting int16_t to uint16_t
  signedUnsigned.unsigned16 = response;

  return (((float)signedUnsigned.signed16) / 100.0);
}

// Set the temperature offset to remove module heating from temp reading
bool SCD30::setTemperatureOffset(float tempOffset)
{
  // Temp offset is only positive. See: https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/issues/27#issuecomment-971986826
  //"The SCD30 offset temperature is obtained by subtracting the reference temperature from the SCD30 output temperature"
  // https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/9.5_CO2/Sensirion_CO2_Sensors_SCD30_Low_Power_Mode.pdf

  if (tempOffset < 0.0)
    return (false);

  uint16_t value = tempOffset * 100;

  return sendCommand(COMMAND_SET_TEMPERATURE_OFFSET, value);
}

// Get the altitude compenstation. See 1.3.9.
uint16_t SCD30::getAltitudeCompensation(void)
{
  return readRegister(COMMAND_SET_ALTITUDE_COMPENSATION);
}

// Set the altitude compenstation. See 1.3.9.
bool SCD30::setAltitudeCompensation(uint16_t altitude)
{
  return sendCommand(COMMAND_SET_ALTITUDE_COMPENSATION, altitude);
}

// Set the pressure compenstation. This is passed during measurement startup.
// mbar can be 700 to 1200
bool SCD30::setAmbientPressure(uint16_t pressure_mbar)
{
  if (pressure_mbar < 700 || pressure_mbar > 1200)
  {
    return false;
  }
  return sendCommand(COMMAND_CONTINUOUS_MEASUREMENT, pressure_mbar);
}

// SCD30 soft reset
void SCD30::reset()
{
  sendCommand(COMMAND_RESET);
}

// Get the current ASC setting
bool SCD30::getAutoSelfCalibration()
{
  uint16_t response = readRegister(COMMAND_AUTOMATIC_SELF_CALIBRATION);
  if (response == 1)
  {
    return true;
  }
  else
  {
    return false;
  }
}

// Begins continuous measurements
// Continuous measurement status is saved in non-volatile memory. When the sensor
// is powered down while continuous measurement mode is active SCD30 will measure
// continuously after repowering without sending the measurement command.
// Returns true if successful
bool SCD30::beginMeasuring(uint16_t pressureOffset)
{
  return (sendCommand(COMMAND_CONTINUOUS_MEASUREMENT, pressureOffset));
}

// Overload - no pressureOffset
bool SCD30::beginMeasuring(void)
{
  return (beginMeasuring(0));
}

// Stop continuous measurement
bool SCD30::StopMeasurement(void)
{
  return (sendCommand(COMMAND_STOP_MEAS));
}

// Sets interval between measurements
// 2 seconds to 1800 seconds (30 minutes)
bool SCD30::setMeasurementInterval(uint16_t interval)
{
  return sendCommand(COMMAND_SET_MEASUREMENT_INTERVAL, interval);
}

// Gets interval between measurements
// 2 seconds to 1800 seconds (30 minutes)
uint16_t SCD30::getMeasurementInterval(void)
{
  uint16_t interval = 0;
  getSettingValue(COMMAND_SET_MEASUREMENT_INTERVAL, &interval);
  return (interval);
}

// Returns true when data is available
bool SCD30::dataAvailable()
{
  uint16_t response = readRegister(COMMAND_GET_DATA_READY);

  if (response == 1)
    return (true);
  return (false);
}

// Get 18 bytes from SCD30
// Updates global variables with floats
// Returns true if success
bool SCD30::readMeasurement()
{
  // Verify we have data from the sensor
  if (dataAvailable() == false)
    return (false);

  ByteToFl tempCO2;
  tempCO2.value = 0;
  ByteToFl tempHumidity;
  tempHumidity.value = 0;
  ByteToFl tempTemperature;
  tempTemperature.value = 0;

  _i2cPort->beginTransmission(SCD30_ADDRESS);
  _i2cPort->write(COMMAND_READ_MEASUREMENT >> 8);   // MSB
  _i2cPort->write(COMMAND_READ_MEASUREMENT & 0xFF); // LSB
  if (_i2cPort->endTransmission() != 0)
    return (0); // Sensor did not ACK

  delay(3);

  const uint8_t receivedBytes = _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)18);
  bool error = false;
  if (_i2cPort->available())
  {
    uint8_t bytesToCrc[2];
    for (uint8_t x = 0; x < 18; x++)
    {
      uint8_t incoming = _i2cPort->read();

      switch (x)
      {
      case 0:
      case 1:
      case 3:
      case 4:
        tempCO2.array[x < 3 ? 3 - x : 4 - x] = incoming;
        bytesToCrc[x % 3] = incoming;
        break;
      case 6:
      case 7:
      case 9:
      case 10:
        tempTemperature.array[x < 9 ? 9 - x : 10 - x] = incoming;
        bytesToCrc[x % 3] = incoming;
        break;
      case 12:
      case 13:
      case 15:
      case 16:
        tempHumidity.array[x < 15 ? 15 - x : 16 - x] = incoming;
        bytesToCrc[x % 3] = incoming;
        break;
      default:
        // Validate CRC
        uint8_t foundCrc = computeCRC8(bytesToCrc, 2);
        if (foundCrc != incoming)
        {
          if (_printDebug == true)
          {
            _debugPort->print(F("readMeasurement: found CRC in byte "));
            _debugPort->print(x);
            _debugPort->print(F(", expected 0x"));
            _debugPort->print(foundCrc, HEX);
            _debugPort->print(F(", got 0x"));
            _debugPort->println(incoming, HEX);
          }
          error = true;
        }
        break;
      }
    }
  }
  else
  {
    if (_printDebug == true)
    {
      _debugPort->print(F("readMeasurement: no SCD30 data found from I2C, i2c claims we should receive "));
      _debugPort->print(receivedBytes);
      _debugPort->println(F(" bytes"));
    }
    return false;
  }

  if (error)
  {
    if (_printDebug == true)
      _debugPort->println(F("readMeasurement: encountered error reading SCD30 data."));
    return false;
  }
  // Now copy the uint32s into their associated floats
  co2 = tempCO2.value;
  temperature = tempTemperature.value;
  humidity = tempHumidity.value;

  // Mark our global variables as fresh
  co2HasBeenReported = false;
  humidityHasBeenReported = false;
  temperatureHasBeenReported = false;

  return (true); // Success! New data available in globals.
}

// Gets a setting by reading the appropriate register.
// Returns true if the CRC is valid.
bool SCD30::getSettingValue(uint16_t registerAddress, uint16_t *val)
{
  _i2cPort->beginTransmission(SCD30_ADDRESS);
  _i2cPort->write(registerAddress >> 8);   // MSB
  _i2cPort->write(registerAddress & 0xFF); // LSB
  if (_i2cPort->endTransmission() != 0)
    return (false); // Sensor did not ACK

  delay(3);

  _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)3); // Request data and CRC
  if (_i2cPort->available())
  {
    uint8_t data[2];
    data[0] = _i2cPort->read();
    data[1] = _i2cPort->read();
    uint8_t crc = _i2cPort->read();
    *val = (uint16_t)data[0] << 8 | data[1];
    uint8_t expectedCRC = computeCRC8(data, 2);
    if (crc == expectedCRC) // Return true if CRC check is OK
      return (true);
    if (_printDebug == true)
    {
      _debugPort->print(F("getSettingValue: CRC fail: expected 0x"));
      _debugPort->print(expectedCRC, HEX);
      _debugPort->print(F(", got 0x"));
      _debugPort->println(crc, HEX);
    }
  }
  return (false);
}

// Gets two bytes from SCD30
uint16_t SCD30::readRegister(uint16_t registerAddress)
{
  _i2cPort->beginTransmission(SCD30_ADDRESS);
  _i2cPort->write(registerAddress >> 8);   // MSB
  _i2cPort->write(registerAddress & 0xFF); // LSB
  if (_i2cPort->endTransmission() != 0)
    return (0); // Sensor did not ACK

  delay(3);

  _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)2);
  if (_i2cPort->available())
  {
    uint8_t msb = _i2cPort->read();
    uint8_t lsb = _i2cPort->read();
    return ((uint16_t)msb << 8 | lsb);
  }
  return (0); // Sensor did not respond
}

// Sends a command along with arguments and CRC
bool SCD30::sendCommand(uint16_t command, uint16_t arguments)
{
  uint8_t data[2];
  data[0] = arguments >> 8;
  data[1] = arguments & 0xFF;
  uint8_t crc = computeCRC8(data, 2); // Calc CRC on the arguments only, not the command

  _i2cPort->beginTransmission(SCD30_ADDRESS);
  _i2cPort->write(command >> 8);     // MSB
  _i2cPort->write(command & 0xFF);   // LSB
  _i2cPort->write(arguments >> 8);   // MSB
  _i2cPort->write(arguments & 0xFF); // LSB
  _i2cPort->write(crc);
  if (_i2cPort->endTransmission() != 0)
    return (false); // Sensor did not ACK

  return (true);
}

// Sends just a command, no arguments, no CRC
bool SCD30::sendCommand(uint16_t command)
{
  _i2cPort->beginTransmission(SCD30_ADDRESS);
  _i2cPort->write(command >> 8);   // MSB
  _i2cPort->write(command & 0xFF); // LSB
  if (_i2cPort->endTransmission() != 0)
    return (false); // Sensor did not ACK

  return (true);
}

// Given an array and a number of bytes, this calculate CRC8 for those bytes
// CRC is only calc'd on the data portion (two bytes) of the four bytes being sent
// From: http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html
// Tested with: http://www.sunshine2k.de/coding/javascript/crc/crc_js.html
// x^8+x^5+x^4+1 = 0x31
uint8_t SCD30::computeCRC8(uint8_t data[], uint8_t len)
{
  uint8_t crc = 0xFF; // Init with 0xFF

  for (uint8_t x = 0; x < len; x++)
  {
    crc ^= data[x]; // XOR-in the next input byte

    for (uint8_t i = 0; i < 8; i++)
    {
      if ((crc & 0x80) != 0)
        crc = (uint8_t)((crc << 1) ^ 0x31);
      else
        crc <<= 1;
    }
  }

  return crc; // No output reflection
}
