/******************************************************************************
SparkFunCCS811.cpp
CCS811 Arduino library

Marshall Taylor @ SparkFun Electronics
Nathan Seidle @ SparkFun Electronics

April 4, 2017

https://github.com/sparkfun/CCS811_Air_Quality_Breakout
https://github.com/sparkfun/SparkFun_CCS811_Arduino_Library

Resources:
Uses Wire.h for i2c operation

Development environment specifics:
Arduino IDE 1.8.1

This code is released under the [MIT License](http://opensource.org/licenses/MIT).

Please review the LICENSE.md file included with this example. If you have any questions 
or concerns with licensing, please contact techsupport@sparkfun.com.

Distributed as-is; no warranty is given.
******************************************************************************/

//See SparkFunCCS811.h for additional topology notes.

#include "SparkFunCCS811.h"
#include "stdint.h"

#include <Arduino.h>
#include "Wire.h"
#include <math.h>

//****************************************************************************//
//
//  CCS811Core functions
//
//  Default <address> is 0x5B.
//
//****************************************************************************//
CCS811Core::CCS811Core(uint8_t inputArg) : I2CAddress(inputArg)
{
}

CCS811Core::CCS811_Status_e CCS811Core::beginCore(TwoWire &wirePort)
{
	CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;

	_i2cPort = &wirePort; //Pull in user's choice of I2C hardware

	//Wire.begin(); //See issue 13 https://github.com/sparkfun/SparkFun_CCS811_Arduino_Library/issues/13

#ifdef __AVR__
#endif

#ifdef __MK20DX256__
#endif

#if defined(ARDUINO_ARCH_ESP8266)
	_i2cPort->setClockStretchLimit(200000); // was default 230 uS, now 200ms
#endif

	//Spin for a few ms
	volatile uint8_t temp = 0;
	for (uint16_t i = 0; i < 10000; i++)
	{
		temp++;
	}

	//Check the ID register to determine if the operation was a success.
	uint8_t readCheck;
	readCheck = 0;
	returnError = readRegister(CSS811_HW_ID, &readCheck);

	if (returnError != CCS811_Stat_SUCCESS)
		return returnError;

	if (readCheck != 0x81)
	{
		returnError = CCS811_Stat_ID_ERROR;
	}

	return returnError;
}

//****************************************************************************//
//
//  ReadRegister
//
//  Parameters:
//    offset -- register to read
//    *outputPointer -- Pass &variable (address of) to save read data to
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::readRegister(uint8_t offset, uint8_t *outputPointer)
{
	//Return value
	uint8_t result = 1;
	uint8_t numBytes = 1;
	CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;

	_i2cPort->beginTransmission(I2CAddress);
	_i2cPort->write(offset);
	if (_i2cPort->endTransmission() != 0)
	{
		returnError = CCS811_Stat_I2C_ERROR;
	}

	_i2cPort->requestFrom(I2CAddress, numBytes);
	*outputPointer = _i2cPort->read(); // receive a byte as a proper uint8_t

	return returnError;
}

//****************************************************************************//
//
//  multiReadRegister
//
//  Parameters:
//    offset -- register to read
//    *outputPointer -- Pass &variable (base address of) to save read data to
//    length -- number of bytes to read
//
//  Note:  Does not know if the target memory space is an array or not, or
//    if there is the array is big enough.  if the variable passed is only
//    two bytes long and 3 bytes are requested, this will over-write some
//    other memory!
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::multiReadRegister(uint8_t offset, uint8_t *outputPointer, uint8_t length)
{
	CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;

	//define pointer that will point to the external space
	uint8_t i = 0;
	uint8_t c = 0;
	//Set the address
	_i2cPort->beginTransmission(I2CAddress);
	_i2cPort->write(offset);
	if (_i2cPort->endTransmission() != 0)
	{
		returnError = CCS811_Stat_I2C_ERROR;
	}
	else //OK, all worked, keep going
	{
		// request 6 bytes from slave device
		_i2cPort->requestFrom(I2CAddress, length);
		while ((_i2cPort->available()) && (i < length)) // slave may send less than requested
		{
			c = _i2cPort->read(); // receive a byte as character
			*outputPointer = c;
			outputPointer++;
			i++;
		}
	}

	return returnError;
}

//****************************************************************************//
//
//  writeRegister
//
//  Parameters:
//    offset -- register to write
//    dataToWrite -- 8 bit data to write to register
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::writeRegister(uint8_t offset, uint8_t dataToWrite)
{
	CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;

	_i2cPort->beginTransmission(I2CAddress);
	_i2cPort->write(offset);
	_i2cPort->write(dataToWrite);
	if (_i2cPort->endTransmission() != 0)
	{
		returnError = CCS811_Stat_I2C_ERROR;
	}
	return returnError;
}

//****************************************************************************//
//
//  multiReadRegister
//
//  Parameters:
//    offset -- register to read
//    *inputPointer -- Pass &variable (base address of) to save read data to
//    length -- number of bytes to read
//
//  Note:  Does not know if the target memory space is an array or not, or
//    if there is the array is big enough.  if the variable passed is only
//    two bytes long and 3 bytes are requested, this will over-write some
//    other memory!
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::multiWriteRegister(uint8_t offset, uint8_t *inputPointer, uint8_t length)
{
	CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;
	//define pointer that will point to the external space
	uint8_t i = 0;
	//Set the address
	_i2cPort->beginTransmission(I2CAddress);
	_i2cPort->write(offset);
	while (i < length) // send data bytes
	{
		_i2cPort->write(*inputPointer); // receive a byte as character
		inputPointer++;
		i++;
	}
	if (_i2cPort->endTransmission() != 0)
	{
		returnError = CCS811_Stat_I2C_ERROR;
	}
	return returnError;
}

//****************************************************************************//
//
//  Main user class -- wrapper for the core class + maths
//
//  Construct with same rules as the core ( uint8_t busType, uint8_t inputArg )
//
//****************************************************************************//
CCS811::CCS811(uint8_t inputArg) : CCS811Core(inputArg)
{
	refResistance = 10000; //Unsupported feature. 
	resistance = 0; //Unsupported feature. 
	temperature = 0;
	tVOC = 0;
	CO2 = 0;
}

//****************************************************************************//
//
//  Begin
//
//  This starts the lower level begin, then applies settings
//
//****************************************************************************//
bool CCS811::begin(TwoWire &wirePort)
{
	if (beginWithStatus(wirePort) == CCS811_Stat_SUCCESS)
		return true;
	return false;
}

//****************************************************************************//
//
//  Begin
//
//  This starts the lower level begin, then applies settings
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811::beginWithStatus(TwoWire &wirePort)
{
	uint8_t data[4] = {0x11, 0xE5, 0x72, 0x8A};					   //Reset key
	CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS; //Default error state

	//restart the core
	returnError = beginCore(wirePort);

	if (returnError != CCS811_Stat_SUCCESS)
		return returnError;

	//Reset the device
	multiWriteRegister(CSS811_SW_RESET, data, 4);

	//Tclk = 1/16MHz = 0x0000000625
	//0.001 s / tclk = 16000 counts
	volatile uint8_t temp = 0;

#ifdef ARDUINO_ARCH_ESP32
	for (uint32_t i = 0; i < 80000; i++) //This waits > 1ms @ 80MHz clock
	{
		temp++;
	}
#elif __AVR__
	for (uint16_t i = 0; i < 16000; i++) //This waits > 1ms @ 16MHz clock
	{
		temp++;
	}
#else
	for (uint32_t i = 0; i < 200000; i++) //Spin for a good while
	{
		temp++;
	}
#endif

	if (checkForStatusError() == true)
		return CCS811_Stat_INTERNAL_ERROR;

	if (appValid() == false)
		return CCS811_Stat_INTERNAL_ERROR;

	//Write 0 bytes to this register to start app
	_i2cPort->beginTransmission(I2CAddress);
	_i2cPort->write(CSS811_APP_START);
	if (_i2cPort->endTransmission() != 0)
	{
		return CCS811_Stat_I2C_ERROR;
	}

	//Added from issue 6
	// Without a delay here, the CCS811 and I2C can be put in a bad state.
	// Seems to work with 50us delay, but make a bit longer to be sure.
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266)
	delayMicroseconds(100);
#endif

	returnError = setDriveMode(1); //Read every second

	return returnError;
}

//****************************************************************************//
//
//  Sensor functions
//
//****************************************************************************//
//Updates the total voltatile organic compounds (TVOC) in parts per billion (PPB)
//and the CO2 value
//Returns nothing
CCS811Core::CCS811_Status_e CCS811::readAlgorithmResults(void)
{
	uint8_t data[4];
	CCS811Core::CCS811_Status_e returnError = multiReadRegister(CSS811_ALG_RESULT_DATA, data, 4);
	if (returnError != CCS811_Stat_SUCCESS)
		return returnError;
	// Data ordered:
	// co2MSB, co2LSB, tvocMSB, tvocLSB

	CO2 = ((uint16_t)data[0] << 8) | data[1];
	tVOC = ((uint16_t)data[2] << 8) | data[3];
	return CCS811_Stat_SUCCESS;
}

//Checks to see if error bit is set
bool CCS811::checkForStatusError(void)
{
	uint8_t value;
	//return the status bit
	readRegister(CSS811_STATUS, &value);
	return (value & 1 << 0);
}

//Checks to see if DATA_READ flag is set in the status register
bool CCS811::dataAvailable(void)
{
	uint8_t value;
	CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_STATUS, &value);
	if (returnError != CCS811_Stat_SUCCESS)
	{
		return 0;
	}
	else
	{
		return (value & 1 << 3);
	}
}

//Checks to see if APP_VALID flag is set in the status register
bool CCS811::appValid(void)
{
	uint8_t value;
	CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_STATUS, &value);
	if (returnError != CCS811_Stat_SUCCESS)
	{
		return 0;
	}
	else
	{
		return (value & 1 << 4);
	}
}

uint8_t CCS811::getErrorRegister(void)
{
	uint8_t value;

	CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_ERROR_ID, &value);
	if (returnError != CCS811_Stat_SUCCESS)
	{
		return 0xFF;
	}
	else
	{
		return value; //Send all errors in the event of communication error
	}
}

//Returns the baseline value
//Used for telling sensor what 'clean' air is
//You must put the sensor in clean air and record this value
uint16_t CCS811::getBaseline(void)
{
	uint8_t data[2];
	CCS811Core::CCS811_Status_e returnError = multiReadRegister(CSS811_BASELINE, data, 2);

	unsigned int baseline = ((uint16_t)data[0] << 8) | data[1];
	if (returnError != CCS811_Stat_SUCCESS)
	{
		return 0;
	}
	else
	{
		return (baseline);
	}
}

CCS811Core::CCS811_Status_e CCS811::setBaseline(uint16_t input)
{
	uint8_t data[2];
	data[0] = (input >> 8) & 0x00FF;
	data[1] = input & 0x00FF;

	CCS811Core::CCS811_Status_e returnError = multiWriteRegister(CSS811_BASELINE, data, 2);

	return returnError;
}

//Enable the nINT signal
CCS811Core::CCS811_Status_e CCS811::enableInterrupts(void)
{
	uint8_t value;
	CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_MEAS_MODE, &value); //Read what's currently there
	if (returnError != CCS811_Stat_SUCCESS)
		return returnError;
	value |= (1 << 3); //Set INTERRUPT bit
	writeRegister(CSS811_MEAS_MODE, value);
	return returnError;
}

//Disable the nINT signal
CCS811Core::CCS811_Status_e CCS811::disableInterrupts(void)
{
	uint8_t value;
	CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_MEAS_MODE, &value); //Read what's currently there
	if (returnError != CCS811_Stat_SUCCESS)
		return returnError;
	value &= ~(1 << 3); //Clear INTERRUPT bit
	returnError = writeRegister(CSS811_MEAS_MODE, value);
	return returnError;
}

//Mode 0 = Idle
//Mode 1 = read every 1s
//Mode 2 = every 10s
//Mode 3 = every 60s
//Mode 4 = RAW mode
CCS811Core::CCS811_Status_e CCS811::setDriveMode(uint8_t mode)
{
	if (mode > 4)
		mode = 4; //sanitize input

	uint8_t value;
	CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_MEAS_MODE, &value); //Read what's currently there
	if (returnError != CCS811_Stat_SUCCESS)
		return returnError;
	value &= ~(0b00000111 << 4); //Clear DRIVE_MODE bits
	value |= (mode << 4);		 //Mask in mode
	returnError = writeRegister(CSS811_MEAS_MODE, value);
	return returnError;
}

//Given a temp and humidity, write this data to the CSS811 for better compensation
//This function expects the humidity and temp to come in as floats
CCS811Core::CCS811_Status_e CCS811::setEnvironmentalData(float relativeHumidity, float temperature)
{
	//Check for invalid temperatures
	if ((temperature < -25) || (temperature > 50))
		return CCS811_Stat_GENERIC_ERROR;

	//Check for invalid humidity
	if ((relativeHumidity < 0) || (relativeHumidity > 100))
		return CCS811_Stat_GENERIC_ERROR;

	uint32_t rH = relativeHumidity * 1000; //42.348 becomes 42348
	uint32_t temp = temperature * 1000;	//23.2 becomes 23200

	byte envData[4];

	//Split value into 7-bit integer and 9-bit fractional

	//Incorrect way from datasheet.
	//envData[0] = ((rH % 1000) / 100) > 7 ? (rH / 1000 + 1) << 1 : (rH / 1000) << 1;
	//envData[1] = 0; //CCS811 only supports increments of 0.5 so bits 7-0 will always be zero
	//if (((rH % 1000) / 100) > 2 && (((rH % 1000) / 100) < 8))
	//{
	//	envData[0] |= 1; //Set 9th bit of fractional to indicate 0.5%
	//}

	//Correct rounding. See issue 8: https://github.com/sparkfun/Qwiic_BME280_CCS811_Combo/issues/8
	envData[0] = (rH + 250) / 500;
	envData[1] = 0; //CCS811 only supports increments of 0.5 so bits 7-0 will always be zero

	temp += 25000; //Add the 25C offset
	//Split value into 7-bit integer and 9-bit fractional
	//envData[2] = ((temp % 1000) / 100) > 7 ? (temp / 1000 + 1) << 1 : (temp / 1000) << 1;
	//envData[3] = 0;
	//if (((temp % 1000) / 100) > 2 && (((temp % 1000) / 100) < 8))
	//{
	//	envData[2] |= 1;  //Set 9th bit of fractional to indicate 0.5C
	//}

	//Correct rounding
	envData[2] = (temp + 250) / 500;
	envData[3] = 0;

	CCS811Core::CCS811_Status_e returnError = multiWriteRegister(CSS811_ENV_DATA, envData, 4);
	return returnError;
}

uint16_t CCS811::getTVOC(void)
{
	return tVOC;
}

uint16_t CCS811::getCO2(void)
{
	return CO2;
}

//****************************************************************************//
//
//	The CCS811 no longer supports temperature compensation from an NTC thermistor.
//	NTC thermistor compensation will only work on boards purchased in 2017.
//	List of unsupported functions:
//		setRefResistance();
//		readNTC();
//		getResistance();
//		getTemperature();	
//
//****************************************************************************//

void CCS811::setRefResistance(float input)
{
	refResistance = input;
}

CCS811Core::CCS811_Status_e CCS811::readNTC(void)
{
	uint8_t data[4];
	CCS811Core::CCS811_Status_e returnError = multiReadRegister(CSS811_NTC, data, 4);

	vrefCounts = ((uint16_t)data[0] << 8) | data[1];
	//Serial.print("vrefCounts: ");
	//Serial.println(vrefCounts);
	ntcCounts = ((uint16_t)data[2] << 8) | data[3];
	//Serial.print("ntcCounts: ");
	//Serial.println(ntcCounts);
	//Serial.print("sum: ");
	//Serial.println(ntcCounts + vrefCounts);
	resistance = ((float)ntcCounts * refResistance / (float)vrefCounts);

	//Code from Milan Malesevic and Zoran Stupic, 2011,
	//Modified by Max Mayfield,
	temperature = log((long)resistance);
	temperature = 1 / (0.001129148 + (0.000234125 * temperature) + (0.0000000876741 * temperature * temperature * temperature));
	temperature = temperature - 273.15; // Convert Kelvin to Celsius

	return returnError;
}

float CCS811::getResistance(void)
{
	return resistance;
}

float CCS811::getTemperature(void)
{
	return temperature;
}

const char *CCS811::statusString(CCS811_Status_e stat)
{
	CCS811_Status_e val;
	if (stat == CCS811_Stat_NUM)
	{
		val = stat;
	}
	else
	{
		val = stat;
	}

	switch (val)
	{
	case CCS811_Stat_SUCCESS:
		return "All is well.";
		break;
	case CCS811_Stat_ID_ERROR:
		return "ID Error";
		break;
	case CCS811_Stat_I2C_ERROR:
		return "I2C Error";
		break;
	case CCS811_Stat_INTERNAL_ERROR:
		return "Internal Error";
		break;
	case CCS811_Stat_GENERIC_ERROR:
		return "Generic Error";
		break;
	default:
		return "Unknown Status";
		break;
	}
	return "None";
}
