Tasmota/lib/lib_i2c/ScioSense_ENS16x/src/ScioSense_ENS16x.cpp
Christoph Friese 5d97a73ddf
Add ENS16x (air quality) and ENS210 (temp & RH) sensors (#19479)
* Add files via upload

Added ENS16x library enabling read-out of ENS160 and ENS161 sensor component (follow-up of CCS811 and iAQcore)
Added ENS210 library to read out ENS210 temperature & humidity sensor

* Add files via upload

Add air quality sensor readout for ENS160 and ENS161 checking two possible I2C addresses (follow up sensor for CCS811 and iAQcore)
Add temperature and humidity sensor readout checking two possible I2C addresses

* Update BUILDS.md

Add USE_ENS16x and USE_ENS210

* Update decode-status.py

Add USE_ENS16x and ENS210

* Update I2CDEVICES.md

Add USE_ENS16x and USE_ENS210

* Update my_user_config.h

Add USE_ENS16x and USE_ENS210

* Update support_features.ino

Add USE_ENS16x and USE_ENS210

* Update tasmota_configurations.h

Add USE_ENS16x and ENS210

* Update tasmota_configurations_ESP32.h

Add USE_ENS16x and USE_ENS210

* Update xsns_111_ens16x.ino

Corrected I2X number

* Update xsns_112_ens210.ino

Corrected I2C number

* Disable USE_ENS16x and USE_ENS210 by default

* Added code size information

* cut down in libs

* optimize tasmota side

* fix ens16x web display

* final fix on alternate addresses

* update code & RAM usage

---------

Co-authored-by: Barbudor <barbudor@barbudor.net>
2023-09-24 18:30:15 +02:00

490 lines
14 KiB
C++

/*
ScioSense_ENS16x.h - Library for the ENS160 & ENS161 sensor with I2C interface from ScioSense
2023 Jul 03 v8 Christoph Friese Update to cover ENS160 and ENS161
2023 Mar 23 v7 Christoph Friese Bugfix measurement routine, prepare next release
2021 Nov 25 v6 Martin Herold Custom mode timing fixed
2021 July 29 v5 Christoph Friese Changed nomenclature to ScioSense as product shifted from ams
2021 Feb 04 v4 Giuseppe de Pinto Custom mode fixed
2020 Apr 06 v3 Christoph Friese Changed nomenclature to ScioSense as product shifted from ams
2020 Feb 15 v2 Giuseppe Pasetti Corrected firmware flash option
2019 May 05 v1 Christoph Friese Created
based on application note "ENS160 Software Integration.pdf" rev 0.01
*/
#include "ScioSense_ENS16x.h"
#include "math.h"
ScioSense_ENS16x::ScioSense_ENS16x(uint8_t slaveaddr) {
this->_slaveaddr = slaveaddr;
this->_ADDR = 0;
this->_nINT = 0;
this->_nCS = 0;
}
#ifndef ENS16x_DISABLE_ENHANCED_FEATURES
ScioSense_ENS16x::ScioSense_ENS16x(uint8_t ADDR, uint8_t nCS, uint8_t nINT) {
this->_slaveaddr = ENS16x_I2CADDR_0;
this->_ADDR = ADDR;
this->_nINT = nINT;
this->_nCS = nCS;
}
ScioSense_ENS16x::ScioSense_ENS16x(uint8_t slaveaddr, uint8_t ADDR, uint8_t nCS, uint8_t nINT) {
this->_slaveaddr = slaveaddr;
this->_ADDR = ADDR;
this->_nINT = nINT;
this->_nCS = nCS;
}
// Function to redefine I2C pins
void ScioSense_ENS16x::setI2C(uint8_t sda, uint8_t scl) {
this->_sdaPin = sda;
this->_sclPin = scl;
}
#endif
// Init I2C communication, resets ENS16x and checks its PART_ID. Returns false on I2C problems or wrong PART_ID.
bool ScioSense_ENS16x::begin(bool debug)
{
#ifndef ENS16x_DISABLE_DEBUG
debugENS16x = debug;
//Set pin levels
if (this->_ADDR > 0) {
pinMode(this->_ADDR, OUTPUT);
digitalWrite(this->_ADDR, LOW);
}
if (this->_nINT > 0) pinMode(this->_nINT, INPUT_PULLUP);
if (this->_nCS > 0) {
pinMode(this->_nCS, OUTPUT);
digitalWrite(this->_nCS, HIGH);
}
#endif
//init I2C
_i2c_init();
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.println("begin() - I2C init done");
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
this->_available = false;
this->_available = this->reset();
this->_available = this->checkPartID();
if (this->_available) {
this->_available = this->setMode(ENS16x_OPMODE_IDLE);
this->_available = this->clearCommand();
this->_available = this->getFirmware();
}
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.println("ENS16x in idle mode");
}
#endif
return this->_available;
}
// Sends a reset to the ENS16x. Returns false on I2C problems.
bool ScioSense_ENS16x::reset(void)
{
uint8_t result = this->write8(_slaveaddr, ENS16x_REG_OPMODE, ENS16x_OPMODE_RESET);
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("reset() result: ");
Serial.println(result == 0 ? "ok" : "nok");
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
return result == 0;
}
// Reads the part ID and confirms valid sensor
bool ScioSense_ENS16x::checkPartID(void) {
uint8_t i2cbuf[2];
uint16_t part_id;
bool result = false;
this->read(_slaveaddr, ENS16x_REG_PART_ID, i2cbuf, 2);
part_id = i2cbuf[0] | ((uint16_t)i2cbuf[1] << 8);
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("checkPartID() result: ");
if (part_id == ENS160_PARTID) Serial.println("ENS160 ok");
else if (part_id == ENS161_PARTID) Serial.println("ENS161 ok");
else Serial.println("nok");
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
if (part_id == ENS160_PARTID) { this->_revENS16x = 0; result = true; }
else if (part_id == ENS161_PARTID) { this->_revENS16x = 1; result = true; }
return result;
}
// Initialize idle mode and confirms
bool ScioSense_ENS16x::clearCommand(void) {
uint8_t status;
uint8_t result;
result = this->write8(_slaveaddr, ENS16x_REG_COMMAND, ENS16x_COMMAND_NOP);
result = this->write8(_slaveaddr, ENS16x_REG_COMMAND, ENS16x_COMMAND_CLRGPR);
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("clearCommand() result: ");
Serial.println(result == 0 ? "ok" : "nok");
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
status = this->read8(_slaveaddr, ENS16x_REG_DATA_STATUS);
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("clearCommand() status: 0x");
Serial.println(status, HEX);
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
return result == 0;
}
// Read firmware revisions
bool ScioSense_ENS16x::getFirmware() {
uint8_t i2cbuf[3];
uint8_t result;
this->clearCommand();
delay(ENS16x_BOOTING); // Wait to boot after reset
result = this->write8(_slaveaddr, ENS16x_REG_COMMAND, ENS16x_COMMAND_GET_APPVER);
result = this->read(_slaveaddr, ENS16x_REG_GPR_READ_4, i2cbuf, 3);
this->_fw_ver_major = i2cbuf[0];
this->_fw_ver_minor = i2cbuf[1];
this->_fw_ver_build = i2cbuf[2];
if (this->_fw_ver_major > 6) this->_revENS16x = 1;
else this->_revENS16x = 0;
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.println(this->_fw_ver_major);
Serial.println(this->_fw_ver_minor);
Serial.println(this->_fw_ver_build);
Serial.print("getFirmware() result: ");
Serial.println(result == 0 ? "ok" : "nok");
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
return result == 0;
}
// Set operation mode of sensor
bool ScioSense_ENS16x::setMode(uint8_t mode) {
uint8_t result;
//LP only valid for rev>0
if ((mode == ENS161_OPMODE_LP) and (_revENS16x == 0)) result = 1;
else result = this->write8(_slaveaddr, ENS16x_REG_OPMODE, mode);
#ifndef ENS16x_DISABLE_DEBUG
//if (debugENS16x) {
Serial.print("setMode() activate result: ");
Serial.println(result == 0 ? "ok" : "nok");
//}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
return result == 0;
}
#ifndef ENS16x_DISABLE_ENHANCED_FEATURES
// Initialize definition of custom mode with <n> steps
bool ScioSense_ENS16x::initCustomMode(uint16_t stepNum) {
uint8_t result;
if (stepNum > 0) {
this->_stepCount = stepNum;
result = this->setMode(ENS16x_OPMODE_IDLE);
result = this->clearCommand();
result = this->write8(_slaveaddr, ENS16x_REG_COMMAND, ENS16x_COMMAND_SETSEQ);
} else {
result = 1;
}
delay(ENS16x_BOOTING); // Wait to boot after reset
return result == 0;
}
#endif
#ifndef ENS16x_DISABLE_ENHANCED_FEATURES
// Add a step to custom measurement profile with definition of duration, enabled data acquisition and temperature for each hotplate
bool ScioSense_ENS16x::addCustomStep(uint16_t time, bool measureHP0, bool measureHP1, bool measureHP2, bool measureHP3, uint16_t tempHP0, uint16_t tempHP1, uint16_t tempHP2, uint16_t tempHP3) {
uint8_t seq_ack;
uint8_t temp;
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("setCustomMode() write step ");
Serial.println(this->_stepCount);
}
#endif
delay(ENS16x_BOOTING); // Wait to boot after reset
temp = (uint8_t)(((time / 24)-1) << 6);
if (measureHP0) temp = temp | 0x20;
if (measureHP1) temp = temp | 0x10;
if (measureHP2) temp = temp | 0x8;
if (measureHP3) temp = temp | 0x4;
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_0, temp);
temp = (uint8_t)(((time / 24)-1) >> 2);
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_1, temp);
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_2, (uint8_t)(tempHP0/2));
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_3, (uint8_t)(tempHP1/2));
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_4, (uint8_t)(tempHP2/2));
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_5, (uint8_t)(tempHP3/2));
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_6, (uint8_t)(this->_stepCount - 1));
if (this->_stepCount == 1) {
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_7, 128);
} else {
this->write8(_slaveaddr, ENS16x_REG_GPR_WRITE_7, 0);
}
delay(ENS16x_BOOTING);
seq_ack = this->read8(_slaveaddr, ENS16x_REG_GPR_READ_7);
delay(ENS16x_BOOTING); // Wait to boot after reset
if ((ENS16x_SEQ_ACK_COMPLETE | this->_stepCount) != seq_ack) {
this->_stepCount = this->_stepCount - 1;
return 0;
} else {
return 1;
}
}
#endif
// Perform prediction measurement and stores result in internal variables
bool ScioSense_ENS16x::measure(bool waitForNew) {
uint8_t i2cbuf[8];
uint8_t status;
bool newData = false;
// Set default status for early bail out
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) Serial.println("Start measurement");
#endif
if (waitForNew) {
do {
delay(10);
status = this->read8(_slaveaddr, ENS16x_REG_DATA_STATUS);
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("Status: ");
Serial.println(status);
}
#endif
} while (!IS_NEWDAT(status));
} else {
status = this->read8(_slaveaddr, ENS16x_REG_DATA_STATUS);
}
// Read predictions
if (IS_NEWDAT(status)) {
newData = true;
this->read(_slaveaddr, ENS16x_REG_DATA_AQI, i2cbuf, 7);
_data_aqi = i2cbuf[0];
_data_tvoc = i2cbuf[1] | ((uint16_t)i2cbuf[2] << 8);
_data_eco2 = i2cbuf[3] | ((uint16_t)i2cbuf[4] << 8);
if (_revENS16x > 0) _data_aqis = ((uint16_t)i2cbuf[5]) | ((uint16_t)i2cbuf[6] << 8);
else _data_aqis = 0;
}
return newData;
}
#ifndef ENS16x_DISABLE_ENHANCED_FEATURES
// Perfrom raw measurement and stores result in internal variables
bool ScioSense_ENS16x::measureRaw(bool waitForNew) {
uint8_t i2cbuf[8];
uint8_t status;
bool newData = false;
// Set default status for early bail out
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) Serial.println("Start measurement");
#endif
if (waitForNew) {
do {
delay(10);
status = this->read8(_slaveaddr, ENS16x_REG_DATA_STATUS);
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("Status: ");
Serial.println(status);
}
#endif
} while (!IS_NEWGPR(status));
} else {
status = this->read8(_slaveaddr, ENS16x_REG_DATA_STATUS);
}
if (IS_NEWGPR(status)) {
newData = true;
// Read raw resistance values
this->read(_slaveaddr, ENS16x_REG_GPR_READ_0, i2cbuf, 8);
_hp0_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[0] | ((uint16_t)i2cbuf[1] << 8)));
_hp1_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[2] | ((uint16_t)i2cbuf[3] << 8)));
_hp2_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[4] | ((uint16_t)i2cbuf[5] << 8)));
_hp3_rs = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[6] | ((uint16_t)i2cbuf[7] << 8)));
// Read baselines
this->read(_slaveaddr, ENS16x_REG_DATA_BL, i2cbuf, 8);
_hp0_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[0] | ((uint16_t)i2cbuf[1] << 8)));
_hp1_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[2] | ((uint16_t)i2cbuf[3] << 8)));
_hp2_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[4] | ((uint16_t)i2cbuf[5] << 8)));
_hp3_bl = CONVERT_RS_RAW2OHMS_F((uint32_t)(i2cbuf[6] | ((uint16_t)i2cbuf[7] << 8)));
this->read(_slaveaddr, ENS16x_REG_DATA_MISR, i2cbuf, 1);
_misr = i2cbuf[0];
}
return newData;
}
// Writes t (degC) and h (%rh) to ENV_DATA. Returns false on I2C problems.
bool ScioSense_ENS16x::set_envdata(float t, float h) {
uint16_t t_data = (uint16_t)((t + 273.15f) * 64.0f);
uint16_t rh_data = (uint16_t)(h * 512.0f);
return this->set_envdata210(t_data, rh_data);
}
// Writes t and h (in ENS210 format) to ENV_DATA. Returns false on I2C problems.
bool ScioSense_ENS16x::set_envdata210(uint16_t t, uint16_t h) {
//uint16_t temp;
uint8_t trh_in[4];
//temp = (uint16_t)((t + 273.15f) * 64.0f);
trh_in[0] = t & 0xff;
trh_in[1] = (t >> 8) & 0xff;
//temp = (uint16_t)(h * 512.0f);
trh_in[2] = h & 0xff;
trh_in[3] = (h >> 8) & 0xff;
uint8_t result = this->write(_slaveaddr, ENS16x_REG_TEMP_IN, trh_in, 4);
return result;
}
#endif
/**************************************************************************/
void ScioSense_ENS16x::_i2c_init() {
#ifndef ENS16x_DISABLE_ENHANCED_FEATURES
if (this->_sdaPin != this->_sclPin)
Wire.begin(this->_sdaPin, this->_sclPin);
else
#endif
Wire.begin();
}
/**************************************************************************/
uint8_t ScioSense_ENS16x::read8(uint8_t addr, byte reg) {
uint8_t ret;
this->read(addr, reg, &ret, 1);
return ret;
}
uint8_t ScioSense_ENS16x::read(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t num) {
uint8_t pos = 0;
uint8_t result = 0;
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("I2C read address: 0x");
Serial.print(addr, HEX);
Serial.print(" - register: 0x");
Serial.println(reg, HEX);
}
#endif
//on arduino we need to read in 32 byte chunks
while(pos < num){
uint8_t read_now = min((uint8_t)32, (uint8_t)(num - pos));
Wire.beginTransmission((uint8_t)addr);
Wire.write((uint8_t)reg + pos);
result = Wire.endTransmission();
Wire.requestFrom((uint8_t)addr, read_now);
for(int i=0; i<read_now; i++){
buf[pos] = Wire.read();
pos++;
}
}
return result;
}
/**************************************************************************/
uint8_t ScioSense_ENS16x::write8(uint8_t addr, byte reg, byte value) {
uint8_t result = this->write(addr, reg, &value, 1);
return result;
}
uint8_t ScioSense_ENS16x::write(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t num) {
#ifndef ENS16x_DISABLE_DEBUG
if (debugENS16x) {
Serial.print("I2C write address: 0x");
Serial.print(addr, HEX);
Serial.print(" - register: 0x");
Serial.print(reg, HEX);
Serial.print(" - value:");
for (int i = 0; i<num; i++) {
Serial.print(" 0x");
Serial.print(buf[i], HEX);
}
Serial.println();
}
#endif
Wire.beginTransmission((uint8_t)addr);
Wire.write((uint8_t)reg);
Wire.write((uint8_t *)buf, num);
uint8_t result = Wire.endTransmission();
return result;
}
/**************************************************************************/