/*
 * Author: Jon Trulson <jtrulson@ics.com>
 * Copyright (c) 2016 Intel Corporation.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <unistd.h>
#include <iostream>
#include <stdexcept>
#include <string>
#include <sstream>

#include "t3311.hpp"

using namespace upm;
using namespace std;

// conversion from fahrenheit to celsius and back

static float f2c(float f)
{
  return ((f - 32.0) / (9.0 / 5.0));
}

static float c2f(float c)
{
  return (c * (9.0 / 5.0) + 32.0);
}

// conversion from 1-byte BCD to decimal
static uint8_t bcd2dec(uint8_t bcd)
{
  return (bcd / 16 * 10) + (bcd % 16);
}

T3311::T3311(std::string device, int address, int baud, int bits, char parity,
             int stopBits) :
  m_mbContext(0)
{
  // check some of the parameters
  if (!(bits == 7 || bits == 8))
    {
      throw std::out_of_range(std::string(__FUNCTION__) +
                              ": bits must be 7 or 8");
    }

  if (!(parity == 'N' || parity == 'E' || parity == 'O'))
    {
      throw std::out_of_range(std::string(__FUNCTION__) +
                              ": parity must be 'N', 'O', or 'E'");
    }

  if (!(stopBits == 1 || stopBits == 2))
    {
      throw std::out_of_range(std::string(__FUNCTION__) +
                              ": stopBits must be 1 or 2");
    }

  m_temperature = 0.0;
  m_humidity = 0.0;
  m_computedValue = 0.0;
  m_dewPointTemperature = 0.0;
  m_absoluteHumidity = 0.0;
  m_specificHumidity = 0.0;
  m_mixingRatio = 0.0;
  m_specificEnthalpy = 0.0;

  // addresses are only 8bits wide
  address &= 0xff;

  // now, open/init the device and modbus context

  if (!(m_mbContext = modbus_new_rtu(device.c_str(), baud, parity, bits,
                                     stopBits)))
    {
      throw std::runtime_error(std::string(__FUNCTION__) +
                               ": modbus_new_rtu() failed");
    }

  // set the slave address of the device we want to talk to
  if (modbus_set_slave(m_mbContext, address))
    {
      throw std::runtime_error(std::string(__FUNCTION__) +
                               ": modbus_set_slave() failed");
    }

  // set the serial mode
  modbus_rtu_set_serial_mode(m_mbContext, MODBUS_RTU_RS232);

  // now connect..
  if (modbus_connect(m_mbContext))
    {
      throw std::runtime_error(std::string(__FUNCTION__) +
                               ": modbus_connect() failed");
    }

  // This is a bit of a hack.  The device uses bus power, which isn't
  // provided unless the device has been opened and accessed.  As a
  // result, register reads will usually fail the first time the
  // device is accessed after a power cycle.  Here, we read the
  // temperature value (which will most likely fail), then sleep,
  // allowing the sensor to "boot".  The datasheet says it takes at
  // about 2 seconds to boot, we will wait for 5.
  uint16_t tmp;
  modbus_read_input_registers(m_mbContext, REG_TEMPERATURE, 1, &tmp);

  // sleep for 5 seconds to give time for device to powerup and boot
  sleep(5);

  // turn off debugging
  setDebug(false);

  // now read the UNIT_SETTING reg to see what units we are getting
  // our temperature data in.
  tmp = readInputReg(REG_UNIT_SETTINGS);
  if (tmp & 0x0001)
    m_isCelsius = false;
  else
    m_isCelsius = true;

  // read in the the FW_LO register (BCD encoded) and convert
  tmp = readInputReg(REG_FW_LO);

  // HI byte (major)
  m_fwRevHi = (tmp >> 8) & 0xff;
  m_fwRevHi = bcd2dec(m_fwRevHi);

  // LO byte (minor)
  m_fwRevLo = (tmp & 0xff);
  m_fwRevLo = bcd2dec(m_fwRevLo);

  if (m_fwRevHi >= 2 && m_fwRevLo >= 44)
    m_isExtendedDataAvailable = true;
  else
    m_isExtendedDataAvailable = false;

  // now get the serial number (BCD encoded 4-byte value, which we
  // will pack into a string)
  stringstream preformat;
  uint8_t b;
  // LO (but really HI)
  tmp = readInputReg(REG_SERIAL_LO);
  b = bcd2dec((tmp & 0xff00) >> 8);
  preformat << int(b);

  b = bcd2dec(tmp & 0x00ff);
  preformat << int(b);

  // HI (but really LO)
  tmp = readInputReg(REG_SERIAL_HI);

  b = bcd2dec((tmp & 0xff00) >> 8);
  preformat << int(b);
  b = bcd2dec(tmp & 0x00ff);
  preformat << int(b);

  m_serialNumber = preformat.str();
}

T3311::~T3311()
{
  if (m_mbContext)
    {
      modbus_close(m_mbContext);
      modbus_free(m_mbContext);
    }
}

uint16_t T3311::readInputReg(int reg)
{
  uint16_t val;

  if (modbus_read_input_registers(m_mbContext, reg, 1, &val) <= 0)
    {
      throw std::runtime_error(std::string(__FUNCTION__) +
                               ": modbus_read_input_registers() failed");
    }

  return val;
}

int T3311::readInputRegs(int reg, int len, uint16_t *buf)
{
  int rv;

  if ((rv = modbus_read_input_registers(m_mbContext, reg, len, buf)) < 0)
    {
      throw std::runtime_error(std::string(__FUNCTION__) +
                               ": modbus_read_input_registers() failed");
    }

  return rv;
}

void T3311::update()
{
  static const int dataLen = 9;
  uint16_t data[dataLen];

  // we read the 9 registers starting at the temperature
  if (readInputRegs(REG_TEMPERATURE, dataLen, data) != dataLen)
    {
      throw std::runtime_error(std::string(__FUNCTION__) +
                               ": read less than the expected 9 registers");
    }

  // temperature first, we always store as C
  float tmpF = float((int16_t)data[0]) / 10.0;
  if (m_isCelsius)
    m_temperature = tmpF;
  else
    m_temperature = f2c(tmpF);

  m_humidity = float((int16_t)data[1]) / 10.0;
  m_computedValue = float((int16_t)data[2]) / 10.0;

  // skip data[3], (pressure) as this device does not support it

  // if extended info is supported, grab those too
  if (extendedDataAvailable())
    {
      // we always store temps in C
      tmpF = float((int16_t)data[4]) / 10.0;
      if (m_isCelsius)
        m_dewPointTemperature = tmpF;
      else
        m_dewPointTemperature = f2c(tmpF);

      m_absoluteHumidity = float((int16_t)data[5]) / 10.0;
      m_specificHumidity = float((int16_t)data[6]) / 10.0;
      m_mixingRatio = float((int16_t)data[7]) / 10.0;
      m_specificEnthalpy = float((int16_t)data[8]) / 10.0;
    }
}

float T3311::getTemperature(bool fahrenheit)
{
  if (fahrenheit)
    return c2f(m_temperature);
  else
    return m_temperature;
}

float T3311::getTemperature()
{
  return getTemperature(false);
}

float T3311::getHumidity()
{
  return m_humidity;
}

float T3311::getComputedValue()
{
  return m_computedValue;
}

float T3311::getDewPointTemperature(bool fahrenheit)
{
  if (fahrenheit)
    return c2f(m_dewPointTemperature);
  else
    return m_dewPointTemperature;
}

float T3311::getAbsoluteHumidity()
{
  return m_absoluteHumidity;
}

float T3311::getSpecificHumidity()
{
  return m_specificHumidity;
}

float T3311::getMixingRatio()
{
  return m_mixingRatio;
}

float T3311::getSpecificEnthalpy()
{
  return m_specificEnthalpy;
}

void T3311::setDebug(bool enable)
{
  m_debugging = enable;

  if (enable)
    modbus_set_debug(m_mbContext, 1);
  else
    modbus_set_debug(m_mbContext, 0);
}
