diff --git a/src/_P167_Vindstyrka.ino b/src/_P167_Vindstyrka.ino new file mode 100644 index 0000000000..b97fd6a47f --- /dev/null +++ b/src/_P167_Vindstyrka.ino @@ -0,0 +1,561 @@ +#include "_Plugin_Helper.h" +#ifdef USES_P167 + +// ####################################################################################################### +// ######################## Plugin 167 IKEA Vindstyrka I2C Sensor (SEN5x) ############################ +// ####################################################################################################### +// 19-06-2023 AndiBaciu creation based upon https://github.com/RobTillaart/SHT2x + +# include "./src/PluginStructs/P167_data_struct.h" + +#define PLUGIN_167 +#define PLUGIN_ID_167 167 // plugin id +#define PLUGIN_NAME_167 "Environment - Sensirion SEN5x (IKEA Vindstyrka)" // What will be dislpayed in the selection list +#define PLUGIN_VALUENAME1_167 "Temperature" // variable output of the plugin. The label is in quotation marks +#define PLUGIN_VALUENAME2_167 "Humidity" // multiple outputs are supporte +#define PLUGIN_VALUENAME3_167 "tVOC" // multiple outputs are supported +#define PLUGIN_VALUENAME4_167 "NOx" // multiple outputs are supported +#define PLUGIN_VALUENAME5_167 "PM 1.0" // multiple outputs are supported +#define PLUGIN_VALUENAME6_167 "PM 2.5" // multiple outputs are supported +#define PLUGIN_VALUENAME7_167 "PM 4.0" // multiple outputs are supported +#define PLUGIN_VALUENAME8_167 "PM 10.0" // multiple outputs are supported +#define PLUGIN_VALUENAME9_167 "DewPoint" // multiple outputs are supported +#define PLUGIN_DEFAULT_NAME_1 "IKEA_Vindstyrka" +#define PLUGIN_DEFAULT_NAME_2 "Sensirion_SEN5x" + +// PIN/port configuration is stored in the following: +// CONFIG_PIN1 - The first GPIO pin selected within the task +// CONFIG_PIN2 - The second GPIO pin selected within the task +// CONFIG_PIN3 - The third GPIO pin selected within the task +// CONFIG_PORT - The port in case the device has multiple in/out pins +// +// Custom configuration is stored in the following: +// PCONFIG(x) +// x can be between 1 - 8 and can store values between -32767 - 32768 (16 bit) +// +// N.B. these are aliases for a longer less readable amount of code. See _Plugin_Helper.h +// +// PCONFIG_LABEL(x) is a function to generate a unique label used as HTML id to be able to match +// returned values when saving a configuration. + +// Make accessing specific parameters more readable in the code +// #define Pxxx_OUTPUT_TYPE_INDEX 2 +# define P167_I2C_ADDRESS PCONFIG(0) +# define P167_I2C_ADDRESS_LABEL PCONFIG_LABEL(0) +# define P167_MODEL PCONFIG(1) +# define P167_MODEL_LABEL PCONFIG_LABEL(1) +# define P167_MON_SCL_PIN PCONFIG(2) +# define P167_MON_SCL_PIN_LABEL PCONFIG_LABEL(2) +# define P167_QUERY1 PCONFIG(3) +# define P167_QUERY2 PCONFIG(4) +# define P167_QUERY3 PCONFIG(5) +# define P167_QUERY4 PCONFIG(6) +# define P167_SEN_FIRST PCONFIG(7) +# define P167_SEN_ATTEMPT PCONFIG_LONG(1) + + +# define P167_I2C_ADDRESS_DFLT 0x69 +# define P167_MON_SCL_PIN_DFLT 13 +# define P167_MODEL_DFLT 0 // Vindstyrka or SEN54 +# define P167_QUERY1_DFLT 0 // Temperature (C) +# define P167_QUERY2_DFLT 1 // Humidity (%) +# define P167_QUERY3_DFLT 5 // PM2.5 (ug/m3) +# define P167_QUERY4_DFLT 2 // tVOC (index) + + +# define P167_NR_OUTPUT_VALUES 4 +# define P167_NR_OUTPUT_OPTIONS 10 +# define P167_QUERY1_CONFIG_POS 3 +# define P167_MAX_ATTEMPT 3 // Number of tentative before declaring NAN value + +//# define LIMIT_BUILD_SIZE 1 + +// These pointers may be used among multiple instances of the same plugin, +// as long as the same settings are used. +P167_data_struct *Plugin_167_SEN = nullptr; +boolean Plugin_167_init = false; + +void IRAM_ATTR Plugin_167_interrupt(); + +// Forward declaration helper functions +const __FlashStringHelper* p167_getQueryString(uint8_t query); +const __FlashStringHelper* p167_getQueryValueString(uint8_t query); +unsigned int p167_getRegister(uint8_t query, uint8_t model); +float p167_readVal(uint8_t query, uint8_t node, unsigned int model); + + +// A plugin has to implement the following function + +boolean Plugin_167(uint8_t function, struct EventStruct *event, String& string) +{ + // function: reason the plugin was called + // event: ??add description here?? + // string: ??add description here?? + + boolean success = false; + + switch (function) + { + + + case PLUGIN_DEVICE_ADD: + { + // This case defines the device characteristics + Device[++deviceCount].Number = PLUGIN_ID_167; + Device[deviceCount].Type = DEVICE_TYPE_I2C; + Device[deviceCount].VType = Sensor_VType::SENSOR_TYPE_QUAD; + Device[deviceCount].Ports = 0; + Device[deviceCount].PullUpOption = true; + Device[deviceCount].InverseLogicOption = false; + Device[deviceCount].FormulaOption = true; + Device[deviceCount].ValueCount = P167_NR_OUTPUT_VALUES; + Device[deviceCount].SendDataOption = true; + Device[deviceCount].TimerOption = true; + Device[deviceCount].I2CNoDeviceCheck = true; + Device[deviceCount].GlobalSyncOption = true; + Device[deviceCount].PluginStats = true; + Device[deviceCount].OutputDataType = Output_Data_type_t::Simple; + break; + } + + + case PLUGIN_GET_DEVICENAME: + { + // return the device name + string = F(PLUGIN_NAME_167); + break; + } + + + case PLUGIN_GET_DEVICEVALUENAMES: + { + // called when the user opens the module configuration page + // it allows to add a new row for each output variable of the plugin + // For plugins able to choose output types, see P026_Sysinfo.ino. + for (uint8_t i = 0; i < VARS_PER_TASK; ++i) + { + if ( i < P167_NR_OUTPUT_VALUES) + { + uint8_t choice = PCONFIG(i + P167_QUERY1_CONFIG_POS); + safe_strncpy(ExtraTaskSettings.TaskDeviceValueNames[i], p167_getQueryValueString(choice), sizeof(ExtraTaskSettings.TaskDeviceValueNames[i])); + } + else + { + ZERO_FILL(ExtraTaskSettings.TaskDeviceValueNames[i]); + } + } + break; + } + + + case PLUGIN_SET_DEFAULTS: + { + // Set a default config here, which will be called when a plugin is assigned to a task. + P167_I2C_ADDRESS = P167_I2C_ADDRESS_DFLT; + P167_MODEL = P167_MODEL_DFLT; + P167_QUERY1 = P167_QUERY1_DFLT; + P167_QUERY2 = P167_QUERY2_DFLT; + P167_QUERY3 = P167_QUERY3_DFLT; + P167_QUERY4 = P167_QUERY4_DFLT; + P167_MON_SCL_PIN = P167_MON_SCL_PIN_DFLT; + P167_SEN_FIRST = 99; + success = true; + break; + } + + + # if FEATURE_I2C_GET_ADDRESS + case PLUGIN_I2C_GET_ADDRESS: + { + event->Par1 = P167_I2C_ADDRESS_DFLT; + success = true; + break; + } + # endif // if FEATURE_I2C_GET_ADDRESS + + + case PLUGIN_I2C_HAS_ADDRESS: + case PLUGIN_WEBFORM_SHOW_I2C_PARAMS: + { + const uint8_t i2cAddressValues[] = { P167_I2C_ADDRESS_DFLT }; + if (function == PLUGIN_WEBFORM_SHOW_I2C_PARAMS) + { + if (P167_SEN_FIRST == event->TaskIndex) // If first SEN, serial config available + { + //addFormSelectorI2C(P167_I2C_ADDRESS_LABEL, 3, i2cAddressValues, P167_I2C_ADDRESS); + addFormSelectorI2C(F("i2c_addr"), 1, i2cAddressValues, P167_I2C_ADDRESS); + addFormNote(F("Vindstyrka, SEN54, SEN55 default i2c address: 0x69")); + } + } + else + { + success = intArrayContains(1, i2cAddressValues, event->Par1); + } + break; + } + + + case PLUGIN_WEBFORM_SHOW_GPIO_DESCR: + { + if (P167_SEN_FIRST == event->TaskIndex) // If first SEN, serial config available + { + if(P167_MODEL==0) + { + string = F("MonPin SCL: "); + string += formatGpioLabel(P167_MON_SCL_PIN, false); + } + } + success = true; + break; + } + + + case PLUGIN_WEBFORM_LOAD_OUTPUT_SELECTOR: + { + const __FlashStringHelper *options[P167_NR_OUTPUT_OPTIONS]; + for (int i = 0; i < P167_NR_OUTPUT_OPTIONS; ++i) + { + options[i] = p167_getQueryString(i); + } + for (uint8_t i = 0; i < P167_NR_OUTPUT_VALUES; ++i) + { + const uint8_t pconfigIndex = i + P167_QUERY1_CONFIG_POS; + sensorTypeHelper_loadOutputSelector(event, pconfigIndex, i, P167_NR_OUTPUT_OPTIONS, options); + } + addFormNote(F("NOx is available ONLY on Sensirion SEN55 model")); + break; + } + + + case PLUGIN_WEBFORM_LOAD: + { + // this case defines what should be displayed on the web form, when this plugin is selected + // The user's selection will be stored in + // PCONFIG(x) (custom configuration) + + if (Plugin_167_SEN == nullptr) + { + P167_SEN_FIRST = event->TaskIndex; // To detect if first SEN or not + } + + if (P167_SEN_FIRST == event->TaskIndex) // If first SEN, serial config available + { + addHtml(F("
This SEN5x is the first. Its configuration of Pins will affect next SEN5x.")); + addHtml(F("
If several SEN5x's foreseen, don't use other pins.
")); + + const __FlashStringHelper *options_model[3] = { F("IKEA Vindstyrka"), F("Sensirion SEN54"), F("Sensirion SEN55")}; + + addFormSelector(F("Model Type"), P167_MODEL_LABEL, 3, options_model, nullptr, P167_MODEL); + + if(P167_MODEL==0) + { + addFormPinSelect(PinSelectPurpose::Generic_input, F("MonPin SCL"), F("taskdevicepin3"), P167_MON_SCL_PIN); + addFormNote(F("Pin for monitoring i2c communication between Vindstyrka controller and SEN5x. (Only when Model - IKEA Vindstyrka is selected.)")); + } + + if (Plugin_167_SEN != nullptr) + { + addRowLabel(F("Device info")); + String prodname; + String sernum; + uint8_t firmware; + Plugin_167_SEN->getEID(prodname, sernum, firmware); + String txt = F("ProdName: "); + txt += prodname; + txt += F(" Serial Number: "); + txt += sernum; + txt += F(" Firmware: "); + txt += String (firmware); + addHtml(txt); + + addRowLabel(F("Device status")); + txt = F("Speed warning: "); + txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_speed)); + txt += F(" , Auto Cleaning: "); + txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_autoclean)); + txt += F(" , GAS Error: "); + txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_gas)); + txt += F(" , RHT Error: "); + txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_rht)); + txt += F(" , LASER Error: "); + txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_laser)); + txt += F(" , FAN Error: "); + txt += String((bool)Plugin_167_SEN->getStatusInfo(sensor_fan)); + addHtml(txt); + + addRowLabel(F("Check (pass/fail/errCode)")); + txt = Plugin_167_SEN->getSuccCount(); + txt += '/'; + txt += Plugin_167_SEN->getErrCount(); + txt += '/'; + txt += Plugin_167_SEN->getErrCode(); + addHtml(txt); + + } + } + else + { + addHtml(F("
This SEN5x is the NOT the first. Model and Pins config are DISABLED. Configuration is available in the first SEN5x plugin.")); + addHtml(F("
Only output value can be configured.
")); + + //looking for FIRST task Named "IKEA_Vindstyrka or Sensirion_SEN5x" + //Cache.taskIndexName + uint8_t allready_defined=88; + if(P167_MODEL==0) + { + allready_defined=findTaskIndexByName(PLUGIN_DEFAULT_NAME_1); + } + else + { + allready_defined=findTaskIndexByName(PLUGIN_DEFAULT_NAME_2); + } + P167_SEN_FIRST = allready_defined; + } + + success = true; + break; + } + + + case PLUGIN_WEBFORM_SAVE: + { + // this case defines the code to be executed when the form is submitted + // the plugin settings should be saved to PCONFIG(x) + // ping configuration should be read from CONFIG_PIN1 and stored + + // Save output selector parameters. + for (uint8_t i = 0; i < P167_NR_OUTPUT_VALUES; ++i) + { + const uint8_t pconfigIndex = i + P167_QUERY1_CONFIG_POS; + const uint8_t choice = PCONFIG(pconfigIndex); + sensorTypeHelper_saveOutputSelector(event, pconfigIndex, i, p167_getQueryValueString(choice)); + } + P167_MODEL = getFormItemInt(P167_MODEL_LABEL); + P167_I2C_ADDRESS = P167_I2C_ADDRESS_DFLT; + if(P167_MODEL==0) + P167_MON_SCL_PIN = getFormItemInt(F("taskdevicepin3")); + P167_SEN_FIRST = P167_SEN_FIRST; + + if (P167_SEN_FIRST == event->TaskIndex) // For first task set default name + { + if(P167_MODEL==0) + { + strcpy(ExtraTaskSettings.TaskDeviceName, PLUGIN_DEFAULT_NAME_1); // populate default name. + } + else + { + strcpy(ExtraTaskSettings.TaskDeviceName, PLUGIN_DEFAULT_NAME_2); // populate default name. + } + } + + Plugin_167_init = false; // Force device setup next time + success = true; + break; + } + + + case PLUGIN_INIT: + { + // this case defines code to be executed when the plugin is initialised + // This will fail if the set to be first taskindex is no longer enabled + + if (P167_SEN_FIRST == event->TaskIndex) // If first SEN5x, config available + { + if (Plugin_167_SEN != nullptr) + { + delete Plugin_167_SEN; + Plugin_167_SEN = nullptr; + } + + Plugin_167_SEN = new (std::nothrow) P167_data_struct(); + + if (Plugin_167_SEN != nullptr) + { + Plugin_167_SEN->setupModel(P167_MODEL); + Plugin_167_SEN->setupDevice(P167_I2C_ADDRESS); + if(P167_MODEL==0) + { + Plugin_167_SEN->setupMonPin(P167_MON_SCL_PIN); + pinMode(P167_MON_SCL_PIN, INPUT_PULLUP); + attachInterrupt(P167_MON_SCL_PIN, Plugin_167_interrupt, RISING); + } + Plugin_167_SEN->reset(); + } + } + + //UserVar[event->BaseVarIndex] = NAN; + //UserVar[event->BaseVarIndex + 1] = NAN; + //UserVar[event->BaseVarIndex + 2] = NAN; + //UserVar[event->BaseVarIndex + 3] = NAN; + UserVar.setFloat(event->BaseVarIndex, 0, NAN); + UserVar.setFloat(event->BaseVarIndex, 1, NAN); + UserVar.setFloat(event->BaseVarIndex, 2, NAN); + UserVar.setFloat(event->BaseVarIndex, 3, NAN); + + success = true; + Plugin_167_init = true; + + break; + } + + + case PLUGIN_EXIT: + { + if (P167_SEN_FIRST == event->TaskIndex) // If first SEN5x, config available + { + if (Plugin_167_SEN != nullptr) + { + if(P167_MODEL==0) + { + Plugin_167_SEN->disableInterrupt_monpin(); + } + delete Plugin_167_SEN; + Plugin_167_SEN = nullptr; + } + } + + success = true; + break; + } + + + case PLUGIN_READ: + { + // code to be executed to read data + // It is executed according to the delay configured on the device configuration page, only once + + if(event->TaskIndex!=P167_SEN_FIRST) + { + //All the DATA are in the first task of IKEA_Vindstyrka + //so all you have to do is to load this data in the current taskindex data + initPluginTaskData(event->TaskIndex, getPluginTaskData(P167_SEN_FIRST) ); + } + + if (nullptr != Plugin_167_SEN) + { + if (Plugin_167_SEN->inError()) + { + //UserVar[event->BaseVarIndex] = NAN; + //UserVar[event->BaseVarIndex + 1] = NAN; + //UserVar[event->BaseVarIndex + 2] = NAN; + //UserVar[event->BaseVarIndex + 3] = NAN; + UserVar.setFloat(event->BaseVarIndex, 0, NAN); + UserVar.setFloat(event->BaseVarIndex, 1, NAN); + UserVar.setFloat(event->BaseVarIndex, 2, NAN); + UserVar.setFloat(event->BaseVarIndex, 3, NAN); + addLog(LOG_LEVEL_ERROR, F("Vindstyrka / SEN5X: in Error!")); + } + else + { + if(event->TaskIndex==P167_SEN_FIRST) + { + Plugin_167_SEN->startMeasurements(); // getting ready for another read cycle + } + + //UserVar[event->BaseVarIndex] = Plugin_167_SEN->getRequestedValue(P167_QUERY1); + //UserVar[event->BaseVarIndex + 1] = Plugin_167_SEN->getRequestedValue(P167_QUERY2); + //UserVar[event->BaseVarIndex + 2] = Plugin_167_SEN->getRequestedValue(P167_QUERY3); + //UserVar[event->BaseVarIndex + 3] = Plugin_167_SEN->getRequestedValue(P167_QUERY4); + UserVar.setFloat(event->BaseVarIndex, 0, Plugin_167_SEN->getRequestedValue(P167_QUERY1)); + UserVar.setFloat(event->BaseVarIndex, 1, Plugin_167_SEN->getRequestedValue(P167_QUERY2)); + UserVar.setFloat(event->BaseVarIndex, 2, Plugin_167_SEN->getRequestedValue(P167_QUERY3)); + UserVar.setFloat(event->BaseVarIndex, 3, Plugin_167_SEN->getRequestedValue(P167_QUERY4)); + } + } + + success = true; + break; + } + + + case PLUGIN_ONCE_A_SECOND: + { + // code to be executed once a second. Tasks which do not require fast response can be added here + success = true; + } + + + case PLUGIN_TEN_PER_SECOND: + { + // code to be executed 10 times per second. Tasks which require fast response can be added here + // be careful on what is added here. Heavy processing will result in slowing the module down! + success = true; + } + + + case PLUGIN_FIFTY_PER_SECOND: + { + // code to be executed 10 times per second. Tasks which require fast response can be added here + // be careful on what is added here. Heavy processing will result in slowing the module down! + if(event->TaskIndex==P167_SEN_FIRST) + { + if (nullptr != Plugin_167_SEN) + { + Plugin_167_SEN->monitorSCL(); // Vind / SEN5X FSM evaluation + Plugin_167_SEN->update(); + } + } + success = true; + } + } // switch + + return success; +} // function + + + +/// @brief +/// @param query +/// @return +const __FlashStringHelper* p167_getQueryString(uint8_t query) +{ + switch(query) + { + case 0: return F("Temperature (C)"); + case 1: return F("Humidity (% RH)"); + case 2: return F("tVOC (VOC index)"); + case 3: return F("NOx (NOx index)"); + case 4: return F("PM 1.0 (ug/m3)"); + case 5: return F("PM 2.5 (ug/m3)"); + case 6: return F("PM 4.0 (ug/m3)"); + case 7: return F("PM 10.0 (ug/m3)"); + case 8: return F("DewPoint (C)"); + } + return F(""); +} + +/// @brief +/// @param query +/// @return +const __FlashStringHelper* p167_getQueryValueString(uint8_t query) +{ + switch(query) + { + case 0: return F("Temperature"); + case 1: return F("Humidity"); + case 2: return F("tVOC"); + case 3: return F("NOx"); + case 4: return F("PM1p0"); + case 5: return F("PM2p5"); + case 6: return F("PM4p0"); + case 7: return F("PM10p0"); + case 8: return F("DewPoint"); + } + return F(""); +} + + +// When using interrupts we have to call the library entry point +// whenever an interrupt is triggered +void IRAM_ATTR Plugin_167_interrupt() +{ + //addLog(LOG_LEVEL_ERROR, F("********* SEN5X: interrupt apear!")); + if (Plugin_167_SEN) + { + Plugin_167_SEN->checkPin_interrupt(); + } +} + + + +#endif //USES_P167 \ No newline at end of file diff --git a/src/src/PluginStructs/P167_data_struct.cpp b/src/src/PluginStructs/P167_data_struct.cpp new file mode 100644 index 0000000000..c9bc7d5a9e --- /dev/null +++ b/src/src/PluginStructs/P167_data_struct.cpp @@ -0,0 +1,1402 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +// P167 device class for IKEA Vindstyrka SEN54 temperature , humidity and air quality sensors +// See datasheet https://sensirion.com/media/documents/6791EFA0/62A1F68F/Sensirion_Datasheet_Environmental_Node_SEN5x.pdf +// and info about extra request https://sensirion.com/media/documents/2B6FC1F3/6409E74A/PS_AN_Read_RHT_VOC_and_NOx_RAW_signals_D1.pdf +// Based upon code from Rob Tillaart, Viktor Balint, https://github.com/RobTillaart/SHT2x +// Rewritten and adapted for ESPeasy by andibaciu +// 2023-06-20 Initial version by andibaciu +////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "../PluginStructs/P167_data_struct.h" +#include "../ESPEasyCore/ESPEasyGPIO.h" + +#include + + #ifndef CORE_POST_3_0_0 + #ifdef ESP8266 + #define IRAM_ATTR ICACHE_RAM_ATTR + #endif + #endif + +#ifdef USES_P167 + + +#define P167_START_MEAS 0x0021 // Start measurement command +#define P167_START_MEAS_RHT_GAS 0x0037 // Start measurement RHT/Gas command +#define P167_STOP_MEAS 0x0104 // Stop measurement command +#define P167_READ_DATA_RDY_FLAG 0x0202 // Read Data Ready Flag command +#define P167_READ_MEAS 0x03C4 // Read measurement command +#define P167_R_W_TEMP_COMP_PARAM 0x60B2 // Read/Write Temperature Compensation Parameters command +#define P167_R_W_TWARM_START_PARAM 0x60C6 // Read/Write Warm Start Parameters command +#define P167_R_W_VOC_ALG_PARAM 0x60D0 // Read/Write VOC Algorithm Tuning Parameters command +#define P167_R_W_NOX_ALG_PARAM 0x60E1 // Read/Write NOx Algorithm Tuning Parameters command +#define P167_R_W_RH_T_ACC_Mode 0x60F7 // Read/Write RH/T Acceleration Mode command +#define P167_R_W_VOC_ALG_STATE 0x6181 // Read/Write VOC Algorithm State command +#define P167_START_FAN_CLEAN 0x5607 // Start fan cleaning command +#define P167_R_W_AUTOCLEN_PARAM 0x8004 // Read/Write Autocleaning Interval Parameters command +#define P167_READ_PROD_NAME 0xD014 // Read Product Name command +#define P167_READ_SERIAL_NO 0xD033 // Read Serial Number command +#define P167_READ_FIRM_VER 0xD100 // Read Firmware Version command +#define P167_READ_DEVICE_STATUS 0xD206 // Read Device Status command +#define P167_CLEAR_DEVICE_STATUS 0xD210 // Clear Device Status command +#define P167_RESET_DEVICE 0xD304 // Reset Device command +#define P167_READ_RAW_MEAS 0x03D2 // Read relative humidity and temperature + // which are not compensated for temperature offset, and the + // VOC and NOx raw signals (proportional to the logarithm of the + // resistance of the MOX layer). It returns 4x2 bytes (+ 1 CRC + // byte each) command (see second datasheet fron header for more info) +#define P167_READ_RAW_MYS_MEAS 0x03F5 // Read relative humidity and temperature and MYSTERY word (probably signed offset temperature) + + +#define P167_START_MEAS_DELAY 50 // Timeout value for start measurement command [ms] +#define P167_START_MEAS_RHT_GAS_DELAY 50 // Timeout value for start measurement RHT/Gas command [ms] +#define P167_STOP_MEAS_DELAY 200 // Timeout value for start measurement command [ms] +#define P167_READ_DATA_RDY_FLAG_DELAY 20 // Timeout value for read data ready flag command [ms] +#define P167_READ_MEAS_DELAY 20 // Timeout value for read measurement command [ms] +#define P167_R_W_TEMP_COMP_PARAM_DELAY 20 // Timeout value for read/write temperature compensation parameters command [ms] +#define P167_R_W_WARM_START_PARAM_DELAY 20 // Timeout value for read/write warm start parameters command [ms] +#define P167_R_W_VOC_ALG_PARAM_DELAY 20 // Timeout value for read/write VOC algorithm tuning parameters command [ms] +#define P167_R_W_NOX_ALG_PARAM_DELAY 20 // Timeout value for read/write NOx algorithm tuning parameters command [ms] +#define P167_R_W_RH_T_ACC_MODE_DELAY 20 // Timeout value for read/write RH/T acceleration mode command [ms] +#define P167_R_W_VOC_ALG_STATE_DELAY 20 // Timeout value for read/write VOC algorithm State command [ms] +#define P167_START_FAN_CLEAN_DELAY 20 // Timeout value for start fan cleaning command [ms] +#define P167_R_W_AUTOCLEN_PARAM_DELAY 20 // Timeout value for read/write autoclean interval parameters command [ms] +#define P167_READ_PROD_NAME_DELAY 20 // Timeout value for read product name command [ms] +#define P167_READ_SERIAL_NO_DELAY 20 // Timeout value for read serial number command [ms] +#define P167_READ_FIRM_VER_DELAY 20 // Timeout value for read firmware version command [ms] +#define P167_READ_DEVICE_STATUS_DELAY 20 // Timeout value for read device status command [ms] +#define P167_CLEAR_DEVICE_STATUS_DELAY 20 // Timeout value for clear device status command [ms] +#define P167_RESET_DEVICE_DELAY 100 // Timeout value for reset device command [ms] +#define P167_READ_RAW_MEAS_DELAY 20 // Timeout value for read raw temp and humidity command [ms] + + +#define P167_MAX_RETRY 250 // Give up after amount of retries befoe going to error + + +#define SCL_MONITOR_PIN 13 //pin13 as monitor scl i2c + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// PUBLIC +// +P167_data_struct::P167_data_struct() +{ + _errCount = 0; + + _Temperature = 0.0; + _rawTemperature = 0.0; + _mysTemperature = 0.0; + _Humidity = 0.0; + _rawHumidity = 0.0; + _mysHumidity = 0.0; + _DewPoint = 0.0; + + _tVOC = 0.0; + _rawtVOC = 0.0; + _NOx = 0.0; + _rawNOx = 0.0; + _mysOffset = 0.0; + _PM1p0 = 0.0; + _PM2p5 = 0.0; + _PM4p0 = 0.0; + _PM10p0 = 0.0; + + _devicestatus.val = (uint32_t)0; + + _readingerrcode = VIND_ERR_NO_ERROR; + _readingerrcount = 0; + _readingsuccesscount = 0; + + _model = 0; + _i2caddr = 0; + _monpin = 0; + + _state = P167_state::Uninitialized; + _eid_productname = F(""); + _eid_serialnumber = F(""); + _firmware = 0; + _last_action_started = 0; + _userreg = 0; +} + + +P167_data_struct::~P167_data_struct() +{ + // +} + +// Initialize/setup device properties +// Must be called at least once before oP167::Wairperating the device +bool P167_data_struct::setupDevice(uint8_t i2caddr) +{ + _i2caddr = i2caddr; + +#ifdef PLUGIN_167_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) + { + String log = F("SEN5x: Setup with address= "); + log += formatToHex(_i2caddr); + addLog(LOG_LEVEL_INFO, log); + } +#endif + return true; +} + + +bool P167_data_struct::setupMonPin(uint8_t monpin) +{ + if (validGpio(monpin)) + { + _monpin = monpin; + pinMode(_monpin, INPUT_PULLUP); //declare monitoring pin as input with pullup's + //attachInterruptArg(digitalPinToInterrupt(_monpin), reinterpret_cast(checkPin), this, CHANGE); + //enableInterrupt_monpin(); + + #ifdef PLUGIN_167_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) + { + String log = F("SEN5x: Setup I2C SCL monpin= "); + log += _monpin; + addLog(LOG_LEVEL_INFO, log); + } + #endif + return true; + } + else + return false; +} + + +void P167_data_struct::enableInterrupt_monpin(void) +{ + //attachInterruptArg(digitalPinToInterrupt(_monpin), reinterpret_cast(checkPin), this, CHANGE); +} + + +void P167_data_struct::disableInterrupt_monpin(void) +{ + //detachInterrupt(digitalPinToInterrupt(_monpin)); +} + +// Initialize/setup device properties +// Must be called at least once before oP167::Wairperating the device +bool P167_data_struct::setupModel(uint8_t model) +{ + _model = model; + +#ifdef PLUGIN_167_DEBUG + if (loglevelActiveFor(LOG_LEVEL_INFO)) + { + String log = F("SEN5x: Setup model= "); + log += String(_model); + addLog(LOG_LEVEL_INFO, log); + } +#endif + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Evaluate FSM for data acquisition +// This is a state machine that is evaluated step by step by calling update() repetatively +// NOTE: Function is expected to run as critical section w.r.t. other provided functions +// This is typically met in ESPeasy plugin context when called from within the plugin +bool P167_data_struct::update() +{ + bool stable = false; // signals when a stable state is reached +#ifdef PLUGIN_167_DEBUG + P167_state oldState = _state; +#endif + + + if(statusMonitoring == false) + return stable; + + + switch(_state) + { + case P167_state::Uninitialized: + //we have to stop trying after a while + if (_errCount>P167_MAX_RETRY) + { + _state = P167_state::Error; + stable = true; + } + else if (I2C_wakeup(_i2caddr) != 0) // Try to access the I2C device + { + if (loglevelActiveFor(LOG_LEVEL_ERROR)) + { + String log = F("SEN5x : Not found at I2C address: "); + log += String(_i2caddr, HEX); + addLog(LOG_LEVEL_ERROR, log); + } + _errCount++; + } + else if (_model==0 ) //sensor is Vindstyrka and d'ont need to be reset + { + _errCount = 0; // Device is reachable and initialized, reset error counter + if (writeCmd(P167_READ_FIRM_VER)) // Issue a reset command + { + _state = P167_state::Read_firm_version; // Will take <20ms according to datasheet + _last_action_started = millis(); + } + } + else if (_model==1) + { + _errCount = 0; // Device is reachable and initialized, reset error counter + if (writeCmd(P167_RESET_DEVICE)) // Issue a reset command + { + _state = P167_state::Wait_for_reset; // Will take <20ms according to datasheet + _last_action_started = millis(); + } + } + break; + + case P167_state::Wait_for_reset: + if (timeOutReached(_last_action_started + P167_RESET_DEVICE_DELAY)) //we need to wait for the chip to reset + { + if (I2C_wakeup(_i2caddr) != 0) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _errCount = 0; // Device is reachable and initialized, reset error counter + if (writeCmd(P167_READ_FIRM_VER)) + { + _state = P167_state::Read_firm_version; // Will take <20ms according to datasheet + _last_action_started = millis(); + } + } + } + break; + + case P167_state::Read_firm_version: + if (timeOutReached(_last_action_started + P167_READ_FIRM_VER_DELAY)) + { + // Start read flag + if (!getFirmwareVersion()) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else if(!writeCmd(P167_READ_PROD_NAME)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Read_prod_name; + } + } + break; + + case P167_state::Read_prod_name: + if (timeOutReached(_last_action_started + P167_READ_PROD_NAME_DELAY)) + { + // Start read flag + if (!getProductName()) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else if(!writeCmd(P167_READ_SERIAL_NO)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Read_serial_no; + } + } + break; + + case P167_state::Read_serial_no: + if (timeOutReached(_last_action_started + P167_READ_SERIAL_NO_DELAY)) + { + // Start read flag + if (!getSerialNumber()) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else if(!writeCmd(P167_READ_SERIAL_NO)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Initialized; + } + } + break; + + case P167_state::Write_user_reg: + _state = P167_state::Initialized; + break; + + case P167_state::Initialized: + // For now trigger the first read cycle automatically + //_state = P167_state::Ready; + break; + + case P167_state::Ready: + // Ready to execute a measurement cycle + if( _model==0 || _model==1 || _model==2) + { + // Start measuring data + if (!writeCmd(P167_START_MEAS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_start_meas; + } + } + break; + + case P167_state::Wait_for_start_meas: + if (timeOutReached(_last_action_started + P167_START_MEAS_DELAY)) + { + // Start read flag + if (!writeCmd(P167_READ_DATA_RDY_FLAG)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_read_flag; + } + } + break; + + case P167_state::Wait_for_read_flag: + if (timeOutReached(_last_action_started + P167_READ_DATA_RDY_FLAG_DELAY)) + { + if(readDataRdyFlag()) + { + // Ready to execute a measurement cycle + if (!writeCmd(P167_READ_MEAS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_read_meas; + } + } + else //Ready Flag NOT ok, so send again Start Measurement + { + // Start measuring data + if (!writeCmd(P167_START_MEAS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_start_meas; + } + } + } + break; + + case P167_state::Wait_for_read_meas: + if (timeOutReached(_last_action_started + P167_READ_MEAS_DELAY)) + { + if (!readMeasValue()) // Read the previously measured temperature + { + _errCount++; + //_state = P167_state::Uninitialized; // Lost connection + _state = P167_state::cmdSTARTmeas; + } + else + { + if (!writeCmd(P167_READ_RAW_MEAS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_read_raw_meas; + } + } + } + break; + + case P167_state::Wait_for_read_raw_meas: + //make sure we wait for the measurement to complete + if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) + { + if (!readMeasRawValue()) + { + _errCount++; + //_state = P167_state::Uninitialized; // Lost connection + _state = P167_state::cmdSTARTmeas; + } + else + { + if (!writeCmd(P167_READ_RAW_MYS_MEAS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_read_raw_MYS_meas; + } + } + } + break; + + case P167_state::Wait_for_read_raw_MYS_meas: + //make sure we wait for the measurement to complete + if (timeOutReached(_last_action_started + P167_READ_RAW_MEAS_DELAY)) + { + if (!readMeasRawMYSValue()) + { + _errCount++; + //_state = P167_state::Uninitialized; // Lost connection + _state = P167_state::cmdSTARTmeas; + } + else + { + if (!writeCmd(P167_READ_DEVICE_STATUS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::Wait_for_read_status; + } + calculateValue(); + stable = true; + } + } + break; + + case P167_state::Wait_for_read_status: + //make sure we wait for the measurement to complete + if (timeOutReached(_last_action_started + P167_READ_DEVICE_STATUS_DELAY)) + { + if (!readDeviceStatus()) + { + _errCount++; + //_state = P167_state::Uninitialized; // Lost connection + _state = P167_state::cmdSTARTmeas; + } + else + { + _last_action_started = millis(); + _state = P167_state::cmdSTARTmeas; + stable = true; + } + } + break; + + case P167_state::cmdSTARTmeas: + // Start measuring data + if(_model==0) + { + if (!writeCmd(P167_START_MEAS)) + { + _errCount++; + _state = P167_state::Uninitialized; // Retry + } + else + { + _last_action_started = millis(); + _state = P167_state::IDLE; + } + } + else + { + _state = P167_state::IDLE; + } + break; + + case P167_state::IDLE: + stepMonitoring = 1; + startMonitoringFlag = false; + if(!_errmeas && !_errmeasraw && !_errmeasrawmys) + _state = P167_state::New_Values_Available; + stable = true; + break; + + case P167_state::Error: + case P167_state::New_Values_Available: + //this state is used outside so all we need is to stay here + stable = true; + break; + + //Missing states (enum values) to be checked by the compiler + + } // switch + +#ifdef PLUGIN_167_DEBUG + if (_state != oldState) + { + if (loglevelActiveFor(LOG_LEVEL_INFO)) + { + String log = F("SEN5x : *** state transition "); + log += String((int)oldState); + log += F("-->"); + log += String((int)_state); + addLog(LOG_LEVEL_INFO, log); + } + } +#endif + return stable; +} + + +bool P167_data_struct::monitorSCL() +{ + if(_model==0) + { + if(startMonitoringFlag) + { + + if(stepMonitoring==1) + { + lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime/1000; + if(millis() - lastSCLLowTransitionMonitoringTime < 100) + { + statusMonitoring = false; + return true; + } + else + { + lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime/1000; + statusMonitoring = true; + stepMonitoring++; + } + } + + if(stepMonitoring==2) + { + if(millis() - lastSCLLowTransitionMonitoringTime < 100) + { + lastSCLLowTransitionMonitoringTime = monpinLastTransitionTime/1000; + statusMonitoring = false; + stepMonitoring = 1; + return true; + } + else if(millis() - lastSCLLowTransitionMonitoringTime > 700) + { + statusMonitoring = false; + stepMonitoring = 1; + startMonitoringFlag = false; + + //if _state not finish reading process then start from begining + if(_state >= P167_state::Wait_for_read_meas && _state < P167_state::New_Values_Available) + { + _state = P167_state::Ready; + } + return true; + } + else + { + //processing + } + } + } + monpinValuelast = monpinValue; + } + + if(_model == 1 || _model == 2) + { + statusMonitoring = true; + startMonitoringFlag = false; + stepMonitoring = 0; + } + + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Returns the I2C connection state +// Note: based upon the FSM state without actual accessing the device +bool P167_data_struct::isConnected() const +{ + switch (_state) + { + case P167_state::Initialized: + case P167_state::Ready: + case P167_state::Wait_for_start_meas: + case P167_state::Wait_for_read_flag: + case P167_state::Wait_for_read_meas: + case P167_state::Wait_for_read_raw_meas: + case P167_state::Wait_for_read_raw_MYS_meas: + case P167_state::Wait_for_read_status: + case P167_state::cmdSTARTmeas: + case P167_state::New_Values_Available: + case P167_state::Read_firm_version: + case P167_state::Read_prod_name: + case P167_state::Read_serial_no: + case P167_state::Write_user_reg: + case P167_state::IDLE: + return true; + break; + case P167_state::Uninitialized: + case P167_state::Error: + case P167_state::Wait_for_reset: + return false; + break; + + //Missing states (enum values) to be checked by the compiler + } + return false; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Returns if the device communication is in error +// Note: based upon the FSM state without actual accessing the device +bool P167_data_struct::inError() const +{ + return _state == P167_state::Error; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Returns if new acquired values are available +bool P167_data_struct::newValues() const +{ + return _state == P167_state::New_Values_Available; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Restart the FSM used to access the device +bool P167_data_struct::reset() +{ + startMonitoringFlag = true; + stepMonitoring = 1; + _state = P167_state::Uninitialized; + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Start a new measurement cycle +bool P167_data_struct::startMeasurements() +{ + if ((_state == P167_state::New_Values_Available) || (_state == P167_state::Initialized) || (_state == P167_state::IDLE)) + { + _state = P167_state::Ready; + } + startMonitoringFlag = true; + stepMonitoring = 1; + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Get the electronic idenfification data store in the device +// Note: The data is read from the device during initialization +bool P167_data_struct::getEID(String &eid_productname, String &eid_serialnumber, uint8_t &firmware) const +{ + eid_productname = _eid_productname; + eid_serialnumber = _eid_serialnumber; + firmware = _firmware; + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Get the status informasion about different part of the sensor +// Note: The data is read from the device after every measurement read request +bool P167_data_struct::getStatusInfo(param_statusinfo param) +{ + switch(param) + { + case sensor_speed: + return (bool) _devicestatus.speed; + break; + + case sensor_autoclean: + return (bool) _devicestatus.autoclean; + break; + + case sensor_gas: + return (bool) _devicestatus.gas; + break; + + case sensor_rht: + return (bool) _devicestatus.rht; + break; + + case sensor_laser: + return (bool) _devicestatus.laser; + break; + + case sensor_fan: + return (bool) _devicestatus.fan; + break; + } + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Return the previously measured raw humidity data [bits] +float P167_data_struct::getRequestedValue(uint8_t request) const +{ + //float requested_value=0; + switch(request) + { + case 0: + { + if(_model==0) + return (float) _TemperatureX; + else + return (float) _Temperature; + } + case 1: + { + if(_model==0) + return (float) _HumidityX; + else + return (float) _Humidity; + } + case 2: return (float) _tVOC; + case 3: return (float) _NOx; + case 4: return (float) _PM1p0; + case 5: return (float) _PM2p5; + case 6: return (float) _PM4p0; + case 7: return (float) _PM10p0; + case 8: return (float) _DewPoint; + } + return -1; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// PROTECTED +// +////////////////////////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////////////////////////////// +uint8_t P167_data_struct::crc8(const uint8_t *data, uint8_t len) +{ + // CRC-8 formula from page 14 of SHT spec pdf + // Sensirion_Humidity_Sensors_SHT2x_CRC_Calculation.pdf + const uint8_t POLY = 0x31; + uint8_t crc = 0xFF; + + for (uint8_t j = 0; j> 8)); + Wire.write((uint8_t)cmd); + return Wire.endTransmission() == 0; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t value) +{ + Wire.beginTransmission(_i2caddr); + Wire.write((uint8_t)(cmd >> 8)); + Wire.write((uint8_t)cmd); + Wire.write((uint8_t)value); + return Wire.endTransmission() == 0; +} + + + +bool P167_data_struct::writeCmd(uint16_t cmd, uint8_t length, uint8_t *buffer) +{ + Wire.beginTransmission(_i2caddr); + Wire.write((uint8_t)(cmd >> 8)); + Wire.write((uint8_t)cmd); + for (int i = 0; i < length; i++) { + Wire.write(*(buffer + i)); + } + return Wire.endTransmission() == 0; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +bool P167_data_struct::readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration) +{ + // TODO check if part can be delegated to the I2C_access libraray from ESPeasy + Wire.requestFrom(_i2caddr, (uint8_t) n); + uint32_t start = millis(); + while (Wire.available() < n) + { + if (timePassedSince(start) > maxDuration) + { + return false; + } + yield(); + } + + for (uint8_t i = 0; i < n; i++) + { + val[i] = Wire.read(); + } + return true; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Read data ready flag from device +bool P167_data_struct::readDataRdyFlag() +{ + uint8_t value=0; + uint8_t buffer[3]; + + if (!readBytes(3, (uint8_t*) &buffer[0], P167_READ_DATA_RDY_FLAG_DELAY)) + { + return false; + } + if (crc8(&buffer[0], 2) == buffer[2]) + { + value += buffer[1]; + } + return value; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Read measurement values results from device +bool P167_data_struct::readMeasValue() +{ + uint16_t value=0; + int16_t valuesign=0; + uint8_t buffer[24]; + bool condition=true; + + _errmeas = false; + if (!readBytes(24, (uint8_t*) &buffer[0], P167_READ_MEAS_DELAY)) + { + _errmeas = true; + return false; + } + + String log = F("SEN5x : *** meas value "); + for(int xx=0; xx<24;xx++) + { + log += String((int)buffer[xx]); + log += F(" "); + } + + if(_model==0 || _model==1) + condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) || (buffer[12] == 0xFF && buffer[13] == 0xFF) || (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF); + if(_model==2) + condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF) || (buffer[12] == 0xFF && buffer[13] == 0xFF) || (buffer[15] == 0xFF && buffer[16] == 0xFF) || (buffer[18] == 0xFF && buffer[19] == 0xFF) || (buffer[21] == 0xFF && buffer[22] == 0xFF); + + if(condition) + { + log += F("- error"); + addLog(LOG_LEVEL_INFO, log); + _errmeas = true; + _readingerrcount++; + return false; + } + else + { + for(int xx=0; xx<8; xx++) + { + if ((crc8(&buffer[xx*3], 2) == buffer[xx*3+2]) && (buffer[xx*3] != 0xFF || buffer[xx*3+1] != 0xFF)) + { + value = buffer[xx*3] << 8; + value += buffer[xx*3+1]; + valuesign = buffer[xx*3] << 8; + valuesign += buffer[xx*3+1]; + if(xx==0) + _PM1p0 = (float)value/10; + if(xx==1) + _PM2p5 = (float)value/10; + if(xx==2) + _PM4p0 = (float)value/10; + if(xx==3) + _PM10p0 = (float)value/10; + if(xx==4) + _Humidity = (float)valuesign/100.0; + if(xx==5) + _Temperature = (float)valuesign/200.0; + if(xx==6) + _tVOC = (float)valuesign/10.0; + if(xx==7) + { + if(_model==2) + _NOx = (float)valuesign/10.0; + else + _NOx = (float)0.0; + } + } + else + { + _errmeasrawmys = true; + } + } + + if(_errmeas == true) + { + log += F("- crc error"); + _readingerrcount++; + } + else + { + log += F("- pass"); + _readingsuccesscount++; + } + addLog(LOG_LEVEL_INFO, log); + return !_errmeas; + } + + return true; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Read measurement values results from device +bool P167_data_struct::readMeasRawValue() +{ + uint16_t value=0; + int16_t valuesign=0; + uint8_t buffer[12]; + bool condition=true; + + _errmeasraw = false; + if (!readBytes(12, (uint8_t*) &buffer[0], P167_READ_RAW_MEAS_DELAY)) + { + _errmeasraw = true; + return false; + } + + String log = F("SEN5x : *** meas RAW value "); + for(int xx=0; xx<12;xx++) + { + log += String((int)buffer[xx]); + log += F(" "); + } + + if(_model==0 || _model==1) + condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF);// || (buffer[9] == 0xFF && buffer[10] == 0xFF)) + if(_model==2) + condition=(buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF) || (buffer[9] == 0xFF && buffer[10] == 0xFF); + + if(condition) + { + log += F("- error"); + addLog(LOG_LEVEL_INFO, log); + _errmeasraw = true; + _readingerrcount++; + return false; + } + else + { + for(int xx=0; xx<4; xx++) + { + if ((crc8(&buffer[xx*3], 2) == buffer[xx*3+2]) && (buffer[xx*3] != 0xFF || buffer[xx*3+1] != 0xFF)) + { + value = buffer[xx*3] << 8; + value += buffer[xx*3+1]; + valuesign = buffer[xx*3] << 8; + valuesign += buffer[xx*3+1]; + if(xx==0) + _rawHumidity = (float)valuesign/100.0; + if(xx==1) + _rawTemperature = (float)valuesign/200.0; + if(xx==2) + _rawtVOC = (float)value/10.0; + if(xx==3) + { + if(_model==2) + _rawNOx = (float)value/10.0; + else + _rawNOx = (float)0.0; + } + } + else + { + _errmeasrawmys = true; + } + } + + if(_errmeasraw == true) + { + log += F("- crc error"); + _readingerrcount++; + } + else + { + log += F("- pass"); + _readingsuccesscount++; + } + addLog(LOG_LEVEL_INFO, log); + return !_errmeasraw; + } + + + return true; +} + + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Read measurement values results from device +bool P167_data_struct::readMeasRawMYSValue() +{ + uint16_t value=0; + int16_t valuesign=0; + uint8_t buffer[9]; + + _errmeasrawmys = false; + if (!readBytes(9, (uint8_t*) &buffer[0], P167_READ_RAW_MEAS_DELAY)) + { + _errmeasrawmys = true; + return false; + } + + String log = F("SEN5x : *** meas MYS value "); + for(int xx=0; xx<9;xx++) + { + log += String((int)buffer[xx]); + log += F(" "); + } + + if((buffer[0] == 0xFF && buffer[1] == 0xFF) || (buffer[3] == 0xFF && buffer[4] == 0xFF) || (buffer[6] == 0xFF && buffer[7] == 0xFF)) + { + log += F("- error"); + addLog(LOG_LEVEL_INFO, log); + _errmeasrawmys = true; + _readingerrcount++; + return false; + } + else + { + for(int xx=0; xx<3; xx++) + { + if ((crc8(&buffer[xx*3], 2) == buffer[xx*3+2]) && (buffer[xx*3] != 0xFF || buffer[xx*3+1] != 0xFF)) + { + value = buffer[xx*3] << 8; + value += buffer[xx*3+1]; + valuesign = buffer[xx*3] << 8; + valuesign += buffer[xx*3+1]; + if(xx==0) + _mysHumidity = (float)valuesign/100.0; + if(xx==1) + _mysTemperature = (float)valuesign/200.0; + if(xx==2) + _mysOffset = (float)valuesign/200.0; + } + else + { + _errmeasrawmys = true; + } + } + + if(_errmeasrawmys == true) + { + log += F("- crc error"); + _readingerrcount++; + } + else + { + log += F("- pass"); + _readingsuccesscount++; + } + addLog(LOG_LEVEL_INFO, log); + return !_errmeasrawmys; + } + + + return true; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Calculate DewPoint, F Temp, F HUM +bool P167_data_struct::calculateValue() +{ + float lnval; + float rapval; + float aval = 17.62; + float bval = 243.12; + float Dp; + float eeval = 0.0; + + if(_model==0) + { + //_TemperatureX = _mysTemperature + _mysOffset - (_mysOffset<0.0?(_mysOffset*(-1.0)):_mysOffset)/2; + //_TemperatureX = _mysTemperature + _mysOffset*3.0/2.0; + _TemperatureX = _mysTemperature + _mysOffset - 2.4; //(2.4 - temperature offset because enclosure and esp8266 power disipation) + + //version formula with DewPoint + //lnval = logf(_mysHumidity/100.0); + //rapval = (aval * _mysTemperature)/(bval+_mysTemperature); + //Dp = (bval*(lnval+rapval))/(aval-lnval-rapval); + //eeval = expf((aval*Dp)/(bval+Dp))/expf((aval*_TemperatureX)/(bval+_TemperatureX)); + //_HumidityX = eeval*100.0; + + //version formula with interpolation + _HumidityX = _Humidity+(_TemperatureX-_Temperature)*((_rawHumidity-_Humidity)/(_rawTemperature-_Temperature)); + lnval = logf(_HumidityX/100.0); + rapval = (aval * _TemperatureX)/(bval+_TemperatureX); + Dp = (bval*(lnval+rapval))/(aval-lnval-rapval); + + if(_HumidityX < 0.0) + _HumidityX = 0.0; + if(_HumidityX > 100.0) + _HumidityX = 100.0; + } + else + { + lnval = logf(_Humidity/100.0); + rapval = (aval * _Temperature)/(bval+_Temperature); + Dp = (bval*(lnval+rapval))/(aval-lnval-rapval); + } + _DewPoint = Dp; + + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Retrieve SEN5x identification code +// Sensirion_SEN5x +bool P167_data_struct::getProductName() +{ + String prodname=F(""); + uint8_t buffer[48]; + //writeCmd(P167_READ_PROD_NAME); + if (!readBytes(48, (uint8_t *) buffer, P167_READ_PROD_NAME_DELAY)) + { + return false; + } + for (uint8_t i = 1; i <= 16; i++) + { + if(crc8(&buffer[i*3-3], 2) == buffer[i*3-1]) + { + if (buffer[i*3-3] < 32) + break; + prodname+=char(buffer[i*3-3]); + if (buffer[i*3-2] < 32) + break; + prodname+=char(buffer[i*3-2]); + } + } + _eid_productname=prodname; + + String log = F("SEN5x : *** Product name: "); + log += String(prodname); + addLog(LOG_LEVEL_INFO, log); + + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Retrieve SEN54 Serial Number +bool P167_data_struct::getSerialNumber() +{ + String serno=F(""); + uint8_t buffer[48]; + //writeCmd(P167_READ_SERIAL_NO); + if (!readBytes(48, (uint8_t *) buffer, P167_READ_SERIAL_NO_DELAY)) + { + return false; + } + for (uint8_t i = 1; i <= 16; i++) + { + if(crc8(&buffer[i*3-3], 2) == buffer[i*3-1]) + { + if (buffer[i*3-3] < 32) + break; + serno+=char(buffer[i*3-3]); + if (buffer[i*3-2] < 32) + break; + serno+=char(buffer[i*3-2]); + } + } + _eid_serialnumber=serno; + + String log = F("SEN5x : *** Serial number: "); + log += String(serno); + addLog(LOG_LEVEL_INFO, log); + + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Retrieve SEN54 Firmware version from device +bool P167_data_struct::getFirmwareVersion() +{ + uint8_t version = 0; + uint8_t read_data[3]; + //writeCmd(P167_READ_FIRM_VER); + if (!readBytes(3, (uint8_t *) &read_data, P167_READ_FIRM_VER_DELAY)) + { + return false; + } + if( read_data[2] == crc8(&read_data[0],2) ) + { + version=read_data[0]; + } + else + { + version=0; + } + _firmware=version; + + String log = F("SEN5x : *** Firmware version: "); + log += String((uint8_t)version); + addLog(LOG_LEVEL_INFO, log); + + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Retrieve SEN54 Device Status from device +bool P167_data_struct::readDeviceStatus() +{ + uint32_t value=0; + uint8_t bufferstatus[6]; + //writeCmd(P167_READ_FIRM_VER); + _errdevicestatus = false; + if (!readBytes(6, (uint8_t *) &bufferstatus, P167_READ_DEVICE_STATUS_DELAY)) + { + _errdevicestatus = true; + return false; + } + + String log = F("SEN5x : *** device status "); + for(int xx=0; xx<6;xx++) + { + log += String((int)bufferstatus[xx]); + log += F(" "); + } + + if ((crc8(&bufferstatus[0], 2) == bufferstatus[2]) && (crc8(&bufferstatus[3], 2) == bufferstatus[5])) + { + value = bufferstatus[0] << 8; + value += bufferstatus[1] << 8; + value += bufferstatus[3] << 8; + value += bufferstatus[4] << 8; + _devicestatus.val = value; + } + else + { + _errdevicestatus = true; + } + + if(_errdevicestatus == true) + { + log += F("- crc error"); + _readingerrcount++; + } + else + { + log += String((bool)_devicestatus.speed); + log += String((bool)_devicestatus.autoclean); + log += String((bool)_devicestatus.gas); + log += String((bool)_devicestatus.rht); + log += String((bool)_devicestatus.laser); + log += String((bool)_devicestatus.fan); + log += F(" - pass"); + _readingsuccesscount++; + } + addLog(LOG_LEVEL_INFO, log); + return !_errdevicestatus; +} + + +uint16_t P167_data_struct::getErrCode(bool _clear) +{ + uint16_t _tmp = _readingerrcode; + if (_clear == true) + clearErrCode(); + return (_tmp); +} + +uint16_t P167_data_struct::getErrCount(bool _clear) +{ + uint16_t _tmp = _readingerrcount; + if (_clear == true) + clearErrCount(); + return (_tmp); +} + +uint16_t P167_data_struct::getSuccCount(bool _clear) +{ + uint16_t _tmp = _readingsuccesscount; + if (_clear == true) + clearSuccCount(); + return (_tmp); +} + +void P167_data_struct::clearErrCode() +{ + _readingerrcode = VIND_ERR_NO_ERROR; +} + +void P167_data_struct::clearErrCount() +{ + _readingerrcount = 0; +} + +void P167_data_struct::clearSuccCount() +{ + _readingsuccesscount = 0; +} + + +void IRAM_ATTR P167_data_struct::checkPin_interrupt() +{ + //ISR_noInterrupts(); // s0170071: avoid nested interrups due to bouncing. + + monpinValue++; + monpinLastTransitionTime = getMicros64(); + // Mark pin value changed + monpinChanged = false; + if(monpinValue!=monpinValuelast) + monpinChanged = true; + + //ISR_interrupts(); // enable interrupts again. +} + +#endif // USES_P167 \ No newline at end of file diff --git a/src/src/PluginStructs/P167_data_struct.h b/src/src/PluginStructs/P167_data_struct.h new file mode 100644 index 0000000000..1686b351b5 --- /dev/null +++ b/src/src/PluginStructs/P167_data_struct.h @@ -0,0 +1,222 @@ +////////////////////////////////////////////////////////////////////////////////////////////////// +// P167 device class for IKEA Vindstyrka SEN54 temperature , humidity and air quality sensors +// See datasheet https://sensirion.com/media/documents/6791EFA0/62A1F68F/Sensirion_Datasheet_Environmental_Node_SEN5x.pdf +// and info about extra request https://sensirion.com/media/documents/2B6FC1F3/6409E74A/PS_AN_Read_RHT_VOC_and_NOx_RAW_signals_D1.pdf +// Based upon code from Rob Tillaart, Viktor Balint, https://github.com/RobTillaart/SHT2x +// Rewritten and adapted for ESPeasy by andibaciu +// 2023-06-20 Initial version by andibaciu +////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "../../_Plugin_Helper.h" +#include "../ESPEasyCore/ESPEasyGPIO.h" +#ifdef USES_P167 + +#ifdef LIMIT_BUILD_SIZE +#define PLUGIN_167_DEBUG false +#else +#define PLUGIN_167_DEBUG false // set to true for extra log info in the debug +#endif + +// Vindstyrka device properties +//#define P167_I2C_ADDRESS_DFLT 0x69 + + +//------------------------------------------------------------------------------ +#define VIND_ERR_NO_ERROR 0 // no error +#define VIND_ERR_CRC_ERROR 1 // crc error +#define VIND_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong +#define VIND_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm +#define VIND_ERR_TIMEOUT 4 // timeout +//------------------------------------------------------------------------------ + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Access to the Vindstyrka device is mainly by sequencing a Finate State Machine +enum class P167_state { + Uninitialized = 0, // Initial state, unknown status of sensor device + Wait_for_reset, // Reset being performed + Read_firm_version, // Reading firmware version + Read_prod_name, // Reading production + Read_serial_no, // Reading serial number + Write_user_reg, // Write the user register + Initialized, // Initialization completed + Ready, // Aquisition request is pending, ready to measure + Wait_for_start_meas, // Start measurement started + Wait_for_read_flag, // Read meas flag started + Wait_for_read_meas, // Read meas started + Wait_for_read_raw_meas, // RAW Read meas started + Wait_for_read_raw_MYS_meas, // RAW Read meas MYSTERY started + Wait_for_read_status, // Read status + cmdSTARTmeas, // send command START meas to leave SEN5x ready flag for Vindstyrka + IDLE, // Sensor device in IDLE mode + New_Values_Available, // Acqusition finished, new data available + Error // Sensor device cannot be accessed or in error +}; + + +enum param_statusinfo +{ + sensor_speed = 0, + sensor_autoclean, + sensor_gas, + sensor_rht, + sensor_laser, + sensor_fan +}; + + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// ESPeasy standard PluginTaskData structure for this plugin +//struct P167_data_struct : public PluginTaskData_base +class P167_data_struct +{ + +public: + P167_data_struct(); + //virtual ~P167_data_struct(); + ~P167_data_struct(); + + void checkPin_interrupt(void); + + ///////////////////////////////////////////////////////// + // This method runs the FSM step by step on each call + // Returns true when a stable state is reached + bool update(); + bool monitorSCL(); + + ///////////////////////////////////////////////////////// + // (re)configure the device properties + // This will result in resetting and reloading the device + bool setupDevice(uint8_t i2caddr); + bool setupModel(uint8_t model); + bool setupMonPin(uint8_t monpin); + void enableInterrupt_monpin(void); + void disableInterrupt_monpin(void); + + ///////////////////////////////////////////////////////// + // check sensor is reachable over I2C + bool isConnected() const; + + ///////////////////////////////////////////////////////// + bool newValues() const; + + ///////////////////////////////////////////////////////// + bool inError() const; + + ///////////////////////////////////////////////////////// + // Reset the FSM to initial state + bool reset(); + + ///////////////////////////////////////////////////////// + // Trigger a measurement cycle + // Only perform the measurements with big interval to prevent the sensor from warming up. + bool startMeasurements(); + + bool getStatusInfo(param_statusinfo param); + ///////////////////////////////////////////////////////// + // Electronic Identification Code + // Sensirion_Humidity_SHT2x_Electronic_Identification_Code_V1.1.pdf + // Electronic ID bytes + bool getEID(String &eid_productname, String &eid_serialnumber, uint8_t &firmware) const; + + ///////////////////////////////////////////////////////// + // Temperature, humidity, DewPoint, PMxpy retrieval + // Note: values are fetched from memory and reflect latest succesful read cycle + float getRequestedValue(uint8_t request) const; + + + uint16_t getErrCode(bool _clear = false); //return last errorcode (optional clear this value, default false) + uint16_t getErrCount(bool _clear = false); //return total errors count (optional clear this value, default false) + uint16_t getSuccCount(bool _clear = false); //return total success count (optional clear this value, default false) + void clearErrCode(); //clear last errorcode + void clearErrCount(); //clear total errors count + void clearSuccCount(); //clear total success count + +//protected: +private: + + union devicestatus + { + uint32_t val; + struct + { + uint16_t dummy1:10; + bool speed; + bool dummy2; + bool autoclean; + uint16_t dummy3:11; + bool gas; + bool rht; + bool laser; + bool fan; + uint16_t dummy4:4; + }; + }; + + devicestatus _devicestatus; + P167_state _state; + + uint8_t crc8(const uint8_t *data, uint8_t len); + bool writeCmd(uint16_t cmd); + bool writeCmd(uint16_t cmd, uint8_t value); + bool writeCmd(uint16_t cmd, uint8_t length, uint8_t *buffer); + bool readBytes(uint8_t n, uint8_t *val, uint8_t maxDuration); + + bool readMeasValue(); + bool readMeasRawValue(); + bool readMeasRawMYSValue(); + bool readDataRdyFlag(); + bool readDeviceStatus(); + bool calculateValue(); + + bool getProductName(); + bool getSerialNumber(); + bool getFirmwareVersion(); + + + float _Humidity; // Humidity as fetched from the device [bits] + float _HumidityX; // Humidity as calculated + float _Temperature; // Temperature as fetched from the device [bits] + float _TemperatureX; // Temperature as calculated + float _DewPoint; // DewPoint as calculated + float _rawHumidity; // Humidity as fetched from the device without compensation[bits] + float _rawTemperature; // Temperature as fetched from the device without compensation[bits] + float _mysHumidity; // Humidity as fetched from the device without compensation[bits] + float _mysTemperature; // Temperature as fetched from the device without compensation[bits] + float _tVOC; // tVOC as fetched from the device[bits] + float _NOx; // NOx as fetched from the device[bits] + float _rawtVOC; // tVOC as fetched from the device without compensation[bits] + float _rawNOx; // NOx as fetched from the device without compensation[bits] + float _mysOffset; // Temperature Offset fetched from the device[bits] + float _PM1p0; // PM1.0 as fetched from the device[bits] + float _PM2p5; // PM2.5 as fetched from the device[bits] + float _PM4p0; // PM4.0 as fetched from the device[bits] + float _PM10p0; // PM10.0 as fetched from the device[bits] + uint8_t _model; // Selected sensor model + uint8_t _i2caddr; // Programmed I2C address + uint8_t _monpin; // Pin to monitor I2C SCL to find when VindStyrka finish i2c communication + unsigned long _last_action_started; // Timestamp for last action that takes processing time + uint16_t _errCount; // Number of errors since last successful access + String _eid_productname; // Electronic Device ID - Product Name, read at initialization + String _eid_serialnumber; // Electronic Device ID - Serial Number, read at initialization + uint8_t _firmware; // Firmware version numer, read at initialization + uint8_t _userreg; // TODO debugging only + uint16_t _readingerrcode = VIND_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error + uint16_t _readingerrcount = 0; // total errors couter + uint32_t _readingsuccesscount = 0; // total success couter + bool _errmeas; + bool _errmeasraw; + bool _errmeasrawmys; + bool _errdevicestatus; + uint8_t stepMonitoring; // step for Monitorin SCL pin algorithm + bool startMonitoringFlag; // flag to START/STOP Monitoring algorithm + bool statusMonitoring; // flag for status return from Monitoring algorithm + unsigned long lastSCLLowTransitionMonitoringTime; // last time when SCL i2c pin rising + + volatile uint32_t monpinValue = 0; + volatile uint32_t monpinValuelast = 0; + volatile uint8_t monpinChanged = 0; + volatile uint64_t monpinLastTransitionTime = 0; + +}; +#endif // USES_P167 \ No newline at end of file