From bf4396d360dfae9a4dad975e6431d159ae64a3e5 Mon Sep 17 00:00:00 2001 From: maxinemuster Date: Mon, 9 Mar 2026 20:21:23 +0100 Subject: [PATCH 1/5] Windows: Add I2C simulator for BMP280/BME28, AHT2X, SHT3x/SHT4x, CHT83xx Add SHTxx driver to server (multiple) STH3x / SHT4x sensors Add extension to tokenizer to allow "named" arguments like "SDA=" Add command to "register" a pin as used by a driver w/o an IOrole set in webif --- openBeken_win32_mvsc2017.vcxproj | 7 + openBeken_win32_mvsc2017.vcxproj.filters | 5 + platforms/obk_main.cmake | 1 + platforms/obk_main.mk | 1 + src/cmnds/cmd_public.h | 3 + src/cmnds/cmd_tokenizer.c | 51 ++ src/driver/BMP280.h | 37 +- src/driver/drv_aht2x.c | 3 + src/driver/drv_bmp280.c | 6 + src/driver/drv_bmpi2c.h | 65 +- src/driver/drv_cht8305.c | 15 +- src/driver/drv_local.h | 5 + src/driver/drv_main.c | 26 + src/driver/drv_public.h | 1 + src/driver/drv_shtxx.c | 774 +++++++++++++++++++++++ src/driver/drv_shtxx.h | 85 +++ src/driver/drv_soft_i2c.c | 4 +- src/driver/drv_soft_i2c_sim.c | 315 +++++++++ src/driver/drv_soft_i2c_sim.h | 190 ++++++ src/driver/drv_soft_i2c_sim_sensors.c | 681 ++++++++++++++++++++ src/httpserver/http_fns.c | 54 +- src/obk_config.h | 7 + 22 files changed, 2312 insertions(+), 24 deletions(-) create mode 100644 src/driver/drv_shtxx.c create mode 100644 src/driver/drv_shtxx.h create mode 100644 src/driver/drv_soft_i2c_sim.c create mode 100644 src/driver/drv_soft_i2c_sim.h create mode 100644 src/driver/drv_soft_i2c_sim_sensors.c diff --git a/openBeken_win32_mvsc2017.vcxproj b/openBeken_win32_mvsc2017.vcxproj index 32dbfab7b..821d62eb7 100644 --- a/openBeken_win32_mvsc2017.vcxproj +++ b/openBeken_win32_mvsc2017.vcxproj @@ -203,6 +203,7 @@ + @@ -266,6 +267,9 @@ + + + @@ -969,6 +973,9 @@ + + + diff --git a/openBeken_win32_mvsc2017.vcxproj.filters b/openBeken_win32_mvsc2017.vcxproj.filters index 6039a7bbf..1239eecec 100644 --- a/openBeken_win32_mvsc2017.vcxproj.filters +++ b/openBeken_win32_mvsc2017.vcxproj.filters @@ -70,6 +70,9 @@ + + + @@ -442,6 +445,8 @@ + + diff --git a/platforms/obk_main.cmake b/platforms/obk_main.cmake index dabbf935c..0ce589187 100644 --- a/platforms/obk_main.cmake +++ b/platforms/obk_main.cmake @@ -119,6 +119,7 @@ set(OBKM_SRC ${OBK_SRCS}driver/drv_sgp.c ${OBK_SRCS}driver/drv_shiftRegister.c ${OBK_SRCS}driver/drv_sht3x.c + ${OBK_SRCS}driver/drv_shtxx.c ${OBK_SRCS}driver/drv_sm2135.c ${OBK_SRCS}driver/drv_sm2235.c ${OBK_SRCS}driver/drv_soft_i2c.c diff --git a/platforms/obk_main.mk b/platforms/obk_main.mk index c1edc31d8..18cf75a47 100644 --- a/platforms/obk_main.mk +++ b/platforms/obk_main.mk @@ -139,6 +139,7 @@ OBKM_SRC += $(OBK_SRCS)driver/drv_rn8209.c OBKM_SRC += $(OBK_SRCS)driver/drv_sgp.c OBKM_SRC += $(OBK_SRCS)driver/drv_shiftRegister.c OBKM_SRC += $(OBK_SRCS)driver/drv_sht3x.c +OBKM_SRC += $(OBK_SRCS)driver/drv_shtxx.c OBKM_SRC += $(OBK_SRCS)driver/drv_sm15155e.c OBKM_SRC += $(OBK_SRCS)driver/drv_sm16703P.c OBKM_SRC += $(OBK_SRCS)driver/drv_simpleEEPROM.c diff --git a/src/cmnds/cmd_public.h b/src/cmnds/cmd_public.h index b08935a62..da677af4b 100644 --- a/src/cmnds/cmd_public.h +++ b/src/cmnds/cmd_public.h @@ -226,6 +226,9 @@ bool Tokenizer_IsArgInteger(int i); float Tokenizer_GetArgFloat(int i); int Tokenizer_GetArgIntegerRange(int i, int rangeMax, int rangeMin); void Tokenizer_TokenizeString(const char* s, int flags); +const char *Tokenizer_GetArgEqualDefault(const char *search, const char *def); +int Tokenizer_GetArgEqualInteger(const char *search, const int def); +int Tokenizer_GetPinEqual(const char *search, const int def); // cmd_repeatingEvents.c void RepeatingEvents_Init(); void RepeatingEvents_RunUpdate(float deltaTimeSeconds); diff --git a/src/cmnds/cmd_tokenizer.c b/src/cmnds/cmd_tokenizer.c index 4233ea807..bc890477c 100644 --- a/src/cmnds/cmd_tokenizer.c +++ b/src/cmnds/cmd_tokenizer.c @@ -323,6 +323,57 @@ void expandQuotes(char* str) { str[writeIndex] = 0; } + +// search for a "named" argument like channel=5 +// with Tokenizer_GetArgEqual("channel=") +// will always return char pointer, so you'll might need to convert in later +// since we can't be sure, the argument is present, we need a mandatory "default" for this function +const char *Tokenizer_GetArgEqualDefault(const char *search, const char *def) { + const char* s=NULL; + const char* arg; + const char* found=NULL; + + for (int i=0; i < g_numArgs; i++){ + arg = Tokenizer_GetArg(i); + found=NULL; +// ADDLOG_INFO(LOG_FEATURE_CMD,"Tokenizer_GetArgEqual: argument %i/%i is %s",i,argnum,arg); + + found=strstr(arg,search); + if ( arg && found) { + s=(found + strlen(search)); +// ADDLOG_INFO(LOG_FEATURE_DRV,"Tokenizer_GetArgEqual: Found %s . Value is %s",search,s); + break; + } + } + if (!s) s = def; + return s; +} + + +// search for a "named" integer argument like channel=5 +// with Tokenizer_GetGetArgEqualInteger("SDA=") +int Tokenizer_GetArgEqualInteger(const char *search, const int def) { + int ret=def; + const char* found=Tokenizer_GetArgEqualDefault(search, "##X##"); // search for argument, default must be no number + if(strlen(found) > 2 && found[0] == '0' && (found[1] == 'x' || found[1] == 'X') ) { // also handle hex numbers (e.g. i2c addreses), at least 0x[one digit] --> strlen(found) > 2 + sscanf(found, "%x", &ret); + return ret; + } + if (strIsInteger(found)) ret = atoi(found); // will check for number - use default else + return ret; +} + +// search for a "named" argument like channel=5 +// with Tokenizer_GetPinEqual("SDA=") +// will return pin or default pin +int Tokenizer_GetPinEqual(const char *search, const int def) { + int ret=def, temp; + const char* found=Tokenizer_GetArgEqualDefault(search, "#x#X"); // search for argument, default must neither be a number nor a valid pin name + temp=PIN_FindIndexFromString(found); // will check for number and pin names + if (temp != -1) ret=temp; + return ret; +} + void Tokenizer_TokenizeString(const char *s, int flags) { char *p; diff --git a/src/driver/BMP280.h b/src/driver/BMP280.h index a9ae46b20..ec3eb6e9c 100644 --- a/src/driver/BMP280.h +++ b/src/driver/BMP280.h @@ -52,7 +52,7 @@ #define BMP280_SOFT_RESET 0xB6 -int32_t adc_T, adc_P, adc_H, t_fine; +static int32_t adc_T, adc_P, adc_H, t_fine; // BMP280 sensor modes, register ctrl_meas mode[1:0] typedef enum @@ -126,8 +126,9 @@ struct // writes 1 byte '_data' to register 'reg_addr' void BMP280_Write8(uint8_t reg_addr, uint8_t _data) { - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit); + BMP280_Start_new(g_softI2C.address8bit); BMP280_Write(reg_addr); BMP280_Write(_data); BMP280_Stop(); @@ -138,12 +139,14 @@ uint8_t BMP280_Read8(uint8_t reg_addr) { uint8_t ret; - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit); + BMP280_Start_new(g_softI2C.address8bit); BMP280_Write(reg_addr); Soft_I2C_Stop(&g_softI2C); - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit | 1); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit | 1); + BMP280_Start_new(g_softI2C.address8bit | 1); ret = BMP280_Read(0); BMP280_Stop(); @@ -159,12 +162,14 @@ uint16_t BMP280_Read16(uint8_t reg_addr) uint16_t w; } ret; - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit); + BMP280_Start_new(g_softI2C.address8bit); BMP280_Write(reg_addr); Soft_I2C_Stop(&g_softI2C); - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit | 1); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit | 1); + BMP280_Start_new(g_softI2C.address8bit | 1); ret.b[0] = BMP280_Read(1); ret.b[1] = BMP280_Read(0); BMP280_Stop(); @@ -286,12 +291,14 @@ void BMP280_Update() } ret; ret.b[3] = 0x00; - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit); + BMP280_Start_new(g_softI2C.address8bit); BMP280_Write(BMP280_REG_PRESS_MSB); Soft_I2C_Stop(&g_softI2C); - BMP280_Start(); - BMP280_Write(g_softI2C.address8bit | 1); +// BMP280_Start(); +// BMP280_Write(g_softI2C.address8bit | 1); + BMP280_Start_new(g_softI2C.address8bit | 1); ret.b[2] = BMP280_Read(1); ret.b[1] = BMP280_Read(1); ret.b[0] = BMP280_Read(1); diff --git a/src/driver/drv_aht2x.c b/src/driver/drv_aht2x.c index e2cfef56c..dcae532be 100644 --- a/src/driver/drv_aht2x.c +++ b/src/driver/drv_aht2x.c @@ -190,6 +190,9 @@ void AHT2X_Init() Soft_I2C_PreInit(&g_softI2C); rtos_delay_milliseconds(100); + setPinUsedString(g_softI2C.pin_clk, "AHT2X SCL"); + setPinUsedString(g_softI2C.pin_data, "AHT2X SDA"); + AHT2X_Initialization(); diff --git a/src/driver/drv_bmp280.c b/src/driver/drv_bmp280.c index 4ee41dd4b..dc32a2fe2 100644 --- a/src/driver/drv_bmp280.c +++ b/src/driver/drv_bmp280.c @@ -24,6 +24,10 @@ unsigned short BMP280_Start(void) { Soft_I2C_Start_Internal(&g_softI2C); return 0; } +unsigned short BMP280_Start_new(uint8_t addr) { + Soft_I2C_Start(&g_softI2C,addr); + return 0; +} unsigned short BMP280_Write(byte data_) { Soft_I2C_WriteByte(&g_softI2C, data_); return 0; @@ -53,6 +57,8 @@ void BMP280_Init() { g_softI2C.address8bit = Tokenizer_GetArgIntegerDefault(6, 236); Soft_I2C_PreInit(&g_softI2C); + setPinUsedString(g_softI2C.pin_clk, "BMP280 SCL"); + setPinUsedString(g_softI2C.pin_data, "BMP280 SDA"); usleep(100); if (BMP280_begin(MODE_NORMAL, SAMPLING_X1, SAMPLING_X1, SAMPLING_X1, FILTER_OFF, STANDBY_0_5) == 0) { diff --git a/src/driver/drv_bmpi2c.h b/src/driver/drv_bmpi2c.h index 375f0579b..32d3b6e20 100644 --- a/src/driver/drv_bmpi2c.h +++ b/src/driver/drv_bmpi2c.h @@ -271,6 +271,7 @@ BMP_mode GetMode(int val) } } +#ifndef WINDOWS BMP_sampling GetSampling(int val) { switch(val) @@ -284,7 +285,6 @@ BMP_sampling GetSampling(int val) case 12 ... INT_MAX: return SAMPLING_X16; } } - BMP_filter GetFilter(int val) { switch(val) @@ -300,7 +300,6 @@ BMP_filter GetFilter(int val) case 96 ... INT_MAX: return FILTER_128X; } } - standby_time GetStandbyTime(int val) { switch(val) @@ -316,7 +315,67 @@ standby_time GetStandbyTime(int val) case 3001 ... INT_MAX: return STANDBY_4000; } } - +#else +BMP_sampling GetSampling(int val) +{ + if (val == -1) + return SAMPLING_SKIPPED; + else if (val == 1) + return SAMPLING_X1; + else if (val == 2) + return SAMPLING_X2; + else if (val >= 3 && val <= 5) + return SAMPLING_X4; + else if (val >= 6 && val <= 11) + return SAMPLING_X8; + else if (val >= 12 && val <= INT_MAX) + return SAMPLING_X16; + else + return SAMPLING_X1; +} +BMP_filter GetFilter(int val) +{ + if (val == 0) + return FILTER_OFF; + else if (val >= 1 && val <= 2) + return FILTER_2X; + else if (val >= 3 && val <= 5) + return FILTER_4X; + else if (val >= 6 && val <= 11) + return FILTER_8X; + else if (val >= 12 && val <= 23) + return FILTER_16X; + else if (val >= 24 && val <= 47) + return FILTER_32X; + else if (val >= 48 && val <= 95) + return FILTER_64X; + else if (val >= 96 && val <= INT_MAX) // INT_MAX not needed, any large value fits this branch + return FILTER_128X; + else + return FILTER_OFF; +} +standby_time GetStandbyTime(int val) +{ + if (val >= 0 && val <= 31) + return STANDBY_0_5; + else if (val >= 32 && val <= 94) + return STANDBY_62_5; + else if (val >= 95 && val <= 174) + return STANDBY_125; + else if (val >= 175 && val <= 374) + return STANDBY_250; + else if (val >= 375 && val <= 750) + return STANDBY_500; + else if (val >= 751 && val <= 1500) + return STANDBY_1000; + else if (val >= 1501 && val <= 3000) + return STANDBY_2000; + else if (val >= 3001 && val <= INT_MAX) + return STANDBY_4000; + else + return STANDBY_0_5; +} +#endif void ReadCalibData_BMX280() { BMX280_calib.T1 = BMP_Read16(BMX280_REG_T1); diff --git a/src/driver/drv_cht8305.c b/src/driver/drv_cht8305.c index b0b8c59dd..d1c674405 100644 --- a/src/driver/drv_cht8305.c +++ b/src/driver/drv_cht8305.c @@ -208,6 +208,18 @@ void CHT83XX_Init() g_softI2C.pin_data = 14; g_softI2C.pin_clk = PIN_FindPinIndexForRole(IOR_CHT83XX_CLK, g_softI2C.pin_clk); g_softI2C.pin_data = PIN_FindPinIndexForRole(IOR_CHT83XX_DAT, g_softI2C.pin_data); + channel_temp = g_cfg.pins.channels[g_softI2C.pin_data]; + channel_humid = g_cfg.pins.channels2[g_softI2C.pin_data]; + + // if cli data is given, use instead of pin cfg page + g_softI2C.pin_clk = Tokenizer_GetPinEqual("SCL=", g_softI2C.pin_clk); + g_softI2C.pin_data = Tokenizer_GetPinEqual("SDA=", g_softI2C.pin_data); + channel_temp = Tokenizer_GetArgEqualInteger("chan_t=", channel_temp); + channel_humid = Tokenizer_GetArgEqualInteger("chan_h=", channel_humid); + + + setPinUsedString(g_softI2C.pin_clk, "CHT83XX SCL"); + setPinUsedString(g_softI2C.pin_data, "CHT83XX SDA"); Soft_I2C_PreInit(&g_softI2C); @@ -291,9 +303,6 @@ void CHT83XX_Init() void CHT83XX_Measure() { CHT83XX_ReadEnv(&g_temp, &g_humid); - - channel_temp = g_cfg.pins.channels[g_softI2C.pin_data]; - channel_humid = g_cfg.pins.channels2[g_softI2C.pin_data]; // don't want to loose accuracy, so multiply by 10 // We have a channel types to handle that CHANNEL_Set(channel_temp, (int)(g_temp * 10), 0); diff --git a/src/driver/drv_local.h b/src/driver/drv_local.h index 45b53cf04..fae93c41b 100644 --- a/src/driver/drv_local.h +++ b/src/driver/drv_local.h @@ -313,3 +313,8 @@ void LTR_AppendInformationToHTTPIndexPage(http_request_t* request, int bPreState void TinyIR_NEC_Init(); void TinyIR_NEC_Deinit(); void TinyIR_NEC_RunFrame(); + +// show pins w/o own IORole as "used" in config page +// implemented in http_fns.c +int setPinUsedString(int index, const char *str); + diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c index 8998d105e..2855a990a 100644 --- a/src/driver/drv_main.c +++ b/src/driver/drv_main.c @@ -22,6 +22,7 @@ #include "drv_ds1820_common.h" #include "drv_ds3231.h" #include "drv_hlw8112.h" +#include "drv_shtxx.h" void DRV_MQTTServer_Init(); void DRV_MQTTServer_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState); @@ -1190,6 +1191,22 @@ static driver_t g_drivers[] = { false, // loaded }, #endif +#if ENABLE_DRIVER_SHTXX + //drvdetail:{"name":"SHTXX", + //drvdetail:"title":"TODO", + //drvdetail:"descr":"Humidity/temperature sensor. Testing for unknown sensor on 0x44", + //drvdetail:"requires":""} + { "SHTXX", // Driver Name + SHTXX_Init, // Init + SHTXX_OnEverySecond, // onEverySecond + SHTXX_AppendInformationToHTTPIndexPage, // appendInformationToHTTPIndexPage + NULL, // runQuickTick + SHTXX_StopDriver, // stopFunction + NULL, // onChannelChanged + NULL, // onHassDiscovery + false, // loaded + }, +#endif #if ENABLE_DRIVER_SGP //drvdetail:{"name":"SGP", //drvdetail:"title":"TODO", @@ -1711,6 +1728,15 @@ void DRV_Generic_Init() { #ifndef OBK_DISABLE_ALL_DRIVERS // init TIME unconditionally on start TIME_Init(); +#if WIN32 +#include "drv_soft_i2c_sim.h" + //cmddetail:{"name":"Sim_AddI2Csensor","args":"[type=SHT3x/SHT4x/AHT2x/CHT83xx/BMP280] [SCL=] [SDA=] [adress= optional, try default if ommited]", + //cmddetail:"descr":"Ads a pseudo sensor to the given pins", + //cmddetail:"fn":"CMD_SoftI2C_simAddSensor","file":"driver/drv_soft_i2c_sim.c","requires":"", + //cmddetail:"examples":"Sim_AddI2Csensor SHT3x SCL=24 SDA=17 adress=0x45"} + CMD_RegisterCommand("Sim_AddI2Csensor", CMD_SoftI2C_simAddSensor, NULL); + +#endif #endif } diff --git a/src/driver/drv_public.h b/src/driver/drv_public.h index 72a8bc4ee..0036250bb 100644 --- a/src/driver/drv_public.h +++ b/src/driver/drv_public.h @@ -89,5 +89,6 @@ bool TuyaMCU_IsLEDRunning(); void Shutter_MoveByIndex(int index, float frac, bool bStopOnDuplicate); + #endif /* __DRV_PUBLIC_H__ */ diff --git a/src/driver/drv_shtxx.c b/src/driver/drv_shtxx.c new file mode 100644 index 000000000..76c9184d8 --- /dev/null +++ b/src/driver/drv_shtxx.c @@ -0,0 +1,774 @@ +// Do NOT add new_common.h – it pulls in stdio/stdlib transitively +// and costs 1-3KB on newlib-based toolchains. +#include "../new_pins.h" +#include "../new_cfg.h" // needed for g_cfg.pins – included explicitly + // rather than relying on transitive includes +#include "../cmnds/cmd_public.h" +#include "../mqtt/new_mqtt.h" +#include "../logging/logging.h" +#include "drv_local.h" +#include "drv_uart.h" +#include "../httpserver/new_http.h" +#include "../hal/hal_pins.h" +#include "drv_shtxx.h" + +// ------------------------------------------------------- +// Config table in flash – zero RAM cost. +// hum_scale stays uint8_t (100/125) matching the original; +// the ×10 factor is applied at the single call site in +// SHTXX_Measure / SHTXX_ConvertAndStore, so no struct change. +// ------------------------------------------------------- +static const shtxx_cfg_t SHT_g_cfg[2] = +{ + [SHTXX_TYPE_SHT3X] = { + .rst_cmd = { 0x30, 0xA2 }, + .rst_len = 2, + .rst_delay_ms = 2, + .meas_cmd = { 0x24, 0x00 }, // single-shot, high rep., no clock stretch + .meas_len = 2, + .meas_delay_ms= 15, + .hum_scale = 100, + .hum_offset = 0, + }, + [SHTXX_TYPE_SHT4X] = { + .rst_cmd = { 0x94, 0x00 }, // 2nd byte unused, len=1 + .rst_len = 1, + .rst_delay_ms = 1, + .meas_cmd = { 0xFD, 0x00 }, // high repeatability, 2nd byte unused + .meas_len = 1, + .meas_delay_ms= 10, + .hum_scale = 125, + .hum_offset = -6, + }, +}; + +static shtxx_dev_t g_sensors[SHTXX_MAX_SENSORS]; +static uint8_t g_numSensors = 0; + +#ifdef ENABLE_SHT3_EXTENDED_FEATURES +// Single shared string for SHT3x-only guard; avoids one rodata +// entry per call site. +static const char g_onlySht3[] = "SHTXX: SHT3x only."; +#endif + +// ------------------------------------------------------- +// CRC-8 poly=0x31, init=0xFF, outer loop unrolled. +// ------------------------------------------------------- +static bool SHTXX_CRC8(const uint8_t *data, uint8_t expected) +{ + uint8_t crc = 0xFF ^ data[0]; + for(uint8_t bit = 0; bit < 8; bit++) + crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1); + crc ^= data[1]; + for(uint8_t bit = 0; bit < 8; bit++) + crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1); + return crc == expected; +} + +// ------------------------------------------------------- +// Send a byte sequence. Inlined at -Os (small, few sites). +// ------------------------------------------------------- +static inline void SHTXX_SendCmd(shtxx_dev_t *dev, const uint8_t *cmd, uint8_t len) +{ +ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX_SendCmd: Calling Soft_I2C_Start() for addr=0x%02X",dev->i2cAddr); + Soft_I2C_Start(&dev->i2c, dev->i2cAddr); + for(uint8_t i = 0; i < len; i++) + Soft_I2C_WriteByte(&dev->i2c, cmd[i]); + Soft_I2C_Stop(&dev->i2c); +} + +// ------------------------------------------------------- +// Send command → wait → read 6 bytes → verify both CRCs. +// ------------------------------------------------------- +static bool SHTXX_SendCmdReadData(shtxx_dev_t *dev, + const uint8_t *cmd, uint8_t cmd_len, + uint8_t delay, uint8_t *out) +{ +ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX_SendCmdReadData: Calling Soft_I2C_Start() for addr=0x%02X",dev->i2cAddr | 1); + SHTXX_SendCmd(dev, cmd, cmd_len); + if(delay) rtos_delay_milliseconds(delay); + Soft_I2C_Start(&dev->i2c, dev->i2cAddr | 1); + Soft_I2C_ReadBytes(&dev->i2c, out, 6); + Soft_I2C_Stop(&dev->i2c); + if(!SHTXX_CRC8(&out[0], out[2]) || !SHTXX_CRC8(&out[3], out[5])) + { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: CRC mismatch."); + return false; + } + return true; +} + +// ------------------------------------------------------- +// Convert raw words → 0.1-unit integers, store, set channels. +// +// WHEN BOTH FEATURE FLAGS ARE OFF this function has exactly +// one caller (SHTXX_Measure). It is marked static inline so +// the compiler folds it back in at -Os, producing zero +// function-call overhead – identical to the original layout. +// +// WHEN ENABLE_SHT3_EXTENDED_FEATURES IS ON, SHTXX_FetchPeriodic +// also calls it, so the compiler will emit it as a real +// function automatically (the inline is advisory, not forced). +// +// Integer math only – no float. +// Error vs float: <0.02 °C, <0.05 %RH (within sensor spec). +// ------------------------------------------------------- +static inline void SHTXX_ConvertAndStore(shtxx_dev_t *dev, const uint8_t *data) +{ + const shtxx_cfg_t *cfg = &SHT_g_cfg[dev->typeIdx]; + + uint16_t raw_t = ((uint16_t)data[0] << 8) | data[1]; + uint16_t raw_h = ((uint16_t)data[3] << 8) | data[4]; + + int16_t t10 = (int16_t)((1750u * raw_t + 32767u) / 65535u) - 450 + dev->calTemp; + + // hum_scale stays uint8_t (100/125); ×10 applied here for 0.1 %RH resolution. + // 10 * 125 * 65535 = 81,918,750 – fits uint32_t without overflow. + int16_t h10 = (int16_t)((10u * cfg->hum_scale * (uint32_t)raw_h + 32767u) / 65535u) + + 10 * (cfg->hum_offset + dev->calHum); + if(h10 < 0) h10 = 0; + if(h10 > 1000) h10 = 1000; + + dev->lastTemp = t10; + dev->lastHumid = h10; + + if(dev->channel_temp >= 0) CHANNEL_Set(dev->channel_temp, t10, 0); + if(dev->channel_humid >= 0) CHANNEL_Set(dev->channel_humid, h10, 0); + + // Avoid stdlib abs() – single ternary, one NEG on Thumb + int16_t t_frac = t10 % 10; + if(t_frac < 0) t_frac = -t_frac; + ADDLOG_INFO(LOG_FEATURE_SENSOR, + "SHTXX (SDA=%i SCL=%i): T:%d.%dC H:%d.%d%%", + dev->i2c.pin_data, dev->i2c.pin_clk, + t10 / 10, t_frac, h10 / 10, h10 % 10); +} + +// ------------------------------------------------------- +// One-shot measurement (both types). +// ------------------------------------------------------- +static void SHTXX_Measure(shtxx_dev_t *dev) +{ + const shtxx_cfg_t *cfg = &SHT_g_cfg[dev->typeIdx]; + uint8_t data[6]; + if(!SHTXX_SendCmdReadData(dev, cfg->meas_cmd, cfg->meas_len, + cfg->meas_delay_ms, data)) + { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: Measurement failed."); + return; + } + SHTXX_ConvertAndStore(dev, data); +} + +// ------------------------------------------------------- +// Soft-reset, then probe with a real measurement. +// ------------------------------------------------------- +static void SHTXX_Initialization(shtxx_dev_t *dev) +{ + const shtxx_cfg_t *cfg = &SHT_g_cfg[dev->typeIdx]; + uint8_t probe[6]; + SHTXX_SendCmd(dev, cfg->rst_cmd, cfg->rst_len); + rtos_delay_milliseconds(cfg->rst_delay_ms); + dev->isWorking = SHTXX_SendCmdReadData(dev, cfg->meas_cmd, cfg->meas_len, + cfg->meas_delay_ms, probe); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: SHT%cx init %s.", + (dev->typeIdx == SHTXX_TYPE_SHT3X) ? '3' : '4', + dev->isWorking ? "ok" : "failed"); +} + +// ======================================================= +// Serial number + auto-detect (ENABLE_SERIAL_READ) +// ======================================================= +#ifdef ENABLE_SERIAL_READ + +static bool SHTXX_ReadSerial(shtxx_dev_t *dev) +{ + static const uint8_t sn_cmd[2][2] = { { 0x36, 0x82 }, { 0x89, 0x00 } }; + static const uint8_t sn_len[2] = { 2, 1 }; + uint8_t data[6]; + SHTXX_SendCmd(dev, sn_cmd[dev->typeIdx], sn_len[dev->typeIdx]); + rtos_delay_milliseconds(2); +ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX_ReadSerial: Calling Soft_I2C_Start() for addr=0x%02X",dev->i2cAddr | 1); + Soft_I2C_Start(&dev->i2c, dev->i2cAddr | 1); + Soft_I2C_ReadBytes(&dev->i2c, data, 6); + Soft_I2C_Stop(&dev->i2c); + if(!SHTXX_CRC8(&data[0], data[2]) || !SHTXX_CRC8(&data[3], data[5])) + return false; + dev->serial = ((uint32_t)data[0] << 24) | ((uint32_t)data[1] << 16) + | ((uint32_t)data[3] << 8) | (uint32_t)data[4]; + return true; +} + +static uint8_t SHTXX_DetectType(shtxx_dev_t *dev) +{ + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: Detecting (SDA=%i)...", dev->i2c.pin_data); + dev->typeIdx = SHTXX_TYPE_SHT4X; + if(SHTXX_ReadSerial(dev)) return SHTXX_TYPE_SHT4X; + rtos_delay_milliseconds(10); + dev->typeIdx = SHTXX_TYPE_SHT3X; + if(SHTXX_ReadSerial(dev)) return SHTXX_TYPE_SHT3X; + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: Detection failed, defaulting SHT3x."); + return SHTXX_TYPE_SHT3X; +} + +// Sensor index resolver – only compiled when ENABLE_SERIAL_READ is on, +// because multi-sensor selection only makes sense when sensors can be +// identified (and when you have multiple sensors you need serial anyway). +// When ENABLE_SERIAL_READ is OFF all commands get context-sensor directly. +static shtxx_dev_t *SHTXX_GetSensorByArg(const char *cmd, int arg_index, bool arg_present) +{ + if(!arg_present) return &g_sensors[0]; + int n = Tokenizer_GetArgInteger(arg_index); + if(n < 1 || n > (int)g_numSensors) + { + ADDLOG_ERROR(LOG_FEATURE_SENSOR, "%s: sensor %i out of range (1..%i)", + cmd, n, g_numSensors); + return NULL; + } + return &g_sensors[n - 1]; +} + +#endif // ENABLE_SERIAL_READ + +// ======================================================= +// SHT3x-only extended features (ENABLE_SHT3_EXTENDED_FEATURES) +// ======================================================= +#ifdef ENABLE_SHT3_EXTENDED_FEATURES + +#define SHTXX_REQUIRE_SHT3X(dev, code) do { \ + if((dev)->typeIdx != SHTXX_TYPE_SHT3X) { \ + ADDLOG_ERROR(LOG_FEATURE_SENSOR, g_onlySht3); \ + return (code); \ + } \ +} while(0) + +static void SHTXX_StartPeriodic(shtxx_dev_t *dev, uint8_t msb, uint8_t lsb) +{ + const uint8_t cmd[2] = { msb, lsb }; + SHTXX_SendCmd(dev, cmd, 2); + dev->periodicActive = true; +} + +static void SHTXX_StopPeriodic(shtxx_dev_t *dev) +{ + if(!dev->periodicActive) return; + static const uint8_t cmd[2] = { 0x30, 0x93 }; + SHTXX_SendCmd(dev, cmd, 2); + rtos_delay_milliseconds(1); + dev->periodicActive = false; +} + +static void SHTXX_FetchPeriodic(shtxx_dev_t *dev) +{ + static const uint8_t cmd[2] = { 0xE0, 0x00 }; + uint8_t data[6]; + if(!SHTXX_SendCmdReadData(dev, cmd, 2, 0, data)) + { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: Periodic fetch failed."); + return; + } + SHTXX_ConvertAndStore(dev, data); // second caller – compiler emits real fn +} + +static void SHTXX_SetHeater(shtxx_dev_t *dev, bool on) +{ + const uint8_t cmd[2] = { 0x30, on ? 0x6D : 0x66 }; + SHTXX_SendCmd(dev, cmd, 2); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX (SDA=%i): Heater %s", + dev->i2c.pin_data, on ? "on" : "off"); +} + +static void SHTXX_ReadStatus(shtxx_dev_t *dev) +{ + static const uint8_t cmd[2] = { 0xF3, 0x2D }; + uint8_t buf[3]; + SHTXX_SendCmd(dev, cmd, 2); + Soft_I2C_Start(&dev->i2c, dev->i2cAddr | 1); + Soft_I2C_ReadBytes(&dev->i2c, buf, 3); + Soft_I2C_Stop(&dev->i2c); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX (SDA=%i): Status %02X%02X", + dev->i2c.pin_data, buf[0], buf[1]); +} + +static void SHTXX_ClearStatus(shtxx_dev_t *dev) +{ + static const uint8_t cmd[2] = { 0x30, 0x41 }; + SHTXX_SendCmd(dev, cmd, 2); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX (SDA=%i): Status cleared", dev->i2c.pin_data); +} + +// Alert limits – float unavoidable for the register bit-packing. +// These are user-command paths only, never the hot measurement path. +static void SHTXX_ReadAlertReg(shtxx_dev_t *dev, uint8_t reg, + float *out_hum, float *out_temp) +{ + const uint8_t cmd[2] = { 0xE1, reg }; + uint8_t data[2]; + SHTXX_SendCmd(dev, cmd, 2); + Soft_I2C_Start(&dev->i2c, dev->i2cAddr | 1); + Soft_I2C_ReadBytes(&dev->i2c, data, 2); + Soft_I2C_Stop(&dev->i2c); + uint16_t w = ((uint16_t)data[0] << 8) | data[1]; + *out_hum = 100.0f * (w & 0xFE00) / 65535.0f; + *out_temp = 175.0f * ((uint16_t)(w << 7) / 65535.0f) - 45.0f; +} + +static void SHTXX_WriteAlertReg(shtxx_dev_t *dev, uint8_t reg, + float hum, float temp) +{ + if(hum < 0.0f || hum > 100.0f || temp < -45.0f || temp > 130.0f) + { + ADDLOG_INFO(LOG_FEATURE_CMD, "SHTXX: Alert value out of range."); + return; + } + uint16_t rawH = (uint16_t)(hum / 100.0f * 65535.0f); + uint16_t rawT = (uint16_t)((temp + 45.0f) / 175.0f * 65535.0f); + uint16_t w = (rawH & 0xFE00) | ((rawT >> 7) & 0x01FF); + uint8_t d[2] = { (uint8_t)(w >> 8), (uint8_t)(w & 0xFF) }; + uint8_t crc = 0xFF ^ d[0]; + for(uint8_t b = 0; b < 8; b++) crc = (crc & 0x80) ? ((crc<<1)^0x31) : (crc<<1); + crc ^= d[1]; + for(uint8_t b = 0; b < 8; b++) crc = (crc & 0x80) ? ((crc<<1)^0x31) : (crc<<1); + const uint8_t cmd[2] = { 0x61, reg }; + Soft_I2C_Start(&dev->i2c, dev->i2cAddr); + Soft_I2C_WriteByte(&dev->i2c, cmd[0]); + Soft_I2C_WriteByte(&dev->i2c, cmd[1]); + Soft_I2C_WriteByte(&dev->i2c, d[0]); + Soft_I2C_WriteByte(&dev->i2c, d[1]); + Soft_I2C_WriteByte(&dev->i2c, crc); + Soft_I2C_Stop(&dev->i2c); +} + +static void SHTXX_GetAllAlerts(shtxx_dev_t *dev) +{ + float tLS, tLC, tHC, tHS, hLS, hLC, hHC, hHS; + SHTXX_ReadAlertReg(dev, 0x1F, &hHS, &tHS); + SHTXX_ReadAlertReg(dev, 0x14, &hHC, &tHC); + SHTXX_ReadAlertReg(dev, 0x09, &hLC, &tLC); + SHTXX_ReadAlertReg(dev, 0x02, &hLS, &tLS); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX Alert T (LS/LC/HC/HS): %.1f/%.1f/%.1f/%.1f", + tLS, tLC, tHC, tHS); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX Alert H (LS/LC/HC/HS): %.1f/%.1f/%.1f/%.1f", + hLS, hLC, hHC, hHS); +} + +// --- Extended command handlers --- + +commandResult_t SHTXX_CMD_LaunchPer(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + int argc = Tokenizer_GetArgsCount(); + uint8_t msb = 0x23, lsb = 0x22; // 4 mps, high rep (good default) + shtxx_dev_t *dev; + if(argc >= 2) { + msb = (uint8_t)Tokenizer_GetArgInteger(0); + lsb = (uint8_t)Tokenizer_GetArgInteger(1); + dev = SHTXX_GetSensorByArg(cmd, 2, argc >= 3); + } else { + dev = SHTXX_GetSensorByArg(cmd, 0, argc >= 1); + } + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + SHTXX_StopPeriodic(dev); + rtos_delay_milliseconds(25); + SHTXX_StartPeriodic(dev, msb, lsb); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX (SDA=%i): Periodic 0x%02X/0x%02X", + dev->i2c.pin_data, msb, lsb); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_FetchPer(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 0, Tokenizer_GetArgsCount() >= 1); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + if(!dev->periodicActive) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: Start periodic first."); + return CMD_RES_ERROR; + } + SHTXX_FetchPeriodic(dev); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_StopPer(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 0, Tokenizer_GetArgsCount() >= 1); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + SHTXX_StopPeriodic(dev); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_Heater(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + int state = Tokenizer_GetArgInteger(0); + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 1, Tokenizer_GetArgsCount() >= 2); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + SHTXX_SetHeater(dev, state != 0); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_GetStatus(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 0, Tokenizer_GetArgsCount() >= 1); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + SHTXX_ReadStatus(dev); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_ClearStatus(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 0, Tokenizer_GetArgsCount() >= 1); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + SHTXX_ClearStatus(dev); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_ReadAlert(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 0, Tokenizer_GetArgsCount() >= 1); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + SHTXX_GetAllAlerts(dev); + return CMD_RES_OK; +} + +// SHTXX_SetAlert [sensor] +commandResult_t SHTXX_CMD_SetAlert(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 4)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 4, Tokenizer_GetArgsCount() >= 5); + if(!dev) return CMD_RES_BAD_ARGUMENT; + SHTXX_REQUIRE_SHT3X(dev, CMD_RES_ERROR); + float tHS = Tokenizer_GetArgFloat(0), tLS = Tokenizer_GetArgFloat(1); + float hHS = Tokenizer_GetArgFloat(2), hLS = Tokenizer_GetArgFloat(3); + SHTXX_WriteAlertReg(dev, 0x1D, hHS, tHS); + SHTXX_WriteAlertReg(dev, 0x16, hHS - 0.5f, tHS - 0.5f); + SHTXX_WriteAlertReg(dev, 0x0B, hLS + 0.5f, tLS + 0.5f); + SHTXX_WriteAlertReg(dev, 0x00, hLS, tLS); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX (SDA=%i): Alert T=%.1f/%.1f H=%.1f/%.1f", + dev->i2c.pin_data, tLS, tHS, hLS, hHS); + return CMD_RES_OK; +} + +#endif // ENABLE_SHT3_EXTENDED_FEATURES + +// ======================================================= +// Core command handlers – always compiled. +// When ENABLE_SERIAL_READ is OFF these use context directly +// (same pattern as the original) to avoid linking +// SHTXX_GetSensorByArg and Tokenizer_GetArgInteger for the +// multi-sensor path that is never needed in minimal builds. +// ======================================================= + +commandResult_t SHTXX_CMD_Calibrate(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + +#ifdef ENABLE_SERIAL_READ + // Multi-sensor: optional 3rd arg selects target sensor + shtxx_dev_t *dev = SHTXX_GetSensorByArg(cmd, 2, Tokenizer_GetArgsCount() >= 3); + if(!dev) return CMD_RES_BAD_ARGUMENT; +#else + // Single-sensor fast path: context IS the device pointer + shtxx_dev_t *dev = (shtxx_dev_t *)context; +#endif + + dev->calTemp = (int16_t)(Tokenizer_GetArgFloat(0) * 10.0f); + dev->calHum = (int8_t) (Tokenizer_GetArgFloat(1) * 10.0f); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_Cycle(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + shtxx_dev_t *dev = (shtxx_dev_t *)context; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + int s = Tokenizer_GetArgInteger(0); + if(s < 1) { ADDLOG_INFO(LOG_FEATURE_CMD, "SHTXX: Min 1s."); return CMD_RES_BAD_ARGUMENT; } + dev->secondsBetween = (uint8_t)s; + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_Force(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + shtxx_dev_t *dev = (shtxx_dev_t *)context; + dev->secondsUntilNext = dev->secondsBetween; + SHTXX_Measure(dev); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_Reinit(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + shtxx_dev_t *dev = (shtxx_dev_t *)context; + dev->secondsUntilNext = dev->secondsBetween; + SHTXX_Initialization(dev); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_AddSensor(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + SHTXX_Init(); + return CMD_RES_OK; +} + +commandResult_t SHTXX_CMD_ListSensors(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + if(!g_numSensors) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: No sensors."); + return CMD_RES_OK; + } + for(uint8_t i = 0; i < g_numSensors; i++) + { + shtxx_dev_t *s = &g_sensors[i]; + int16_t tf = s->lastTemp % 10; + if(tf < 0) tf = -tf; +#ifdef ENABLE_SERIAL_READ + ADDLOG_INFO(LOG_FEATURE_SENSOR, + " [%i] SHT%cx SDA=%i SCL=%i sn=%08X T=%d.%dC H=%d.%d%% ch=%i/%i", + i, (s->typeIdx==SHTXX_TYPE_SHT3X)?'3':'4', + s->i2c.pin_data, s->i2c.pin_clk, s->serial, + s->lastTemp/10, tf, s->lastHumid/10, s->lastHumid%10, + s->channel_temp, s->channel_humid); +#else + ADDLOG_INFO(LOG_FEATURE_SENSOR, + " [%i] SHT%cx SDA=%i SCL=%i T=%d.%dC H=%d.%d%% ch=%i/%i", + i, (s->typeIdx==SHTXX_TYPE_SHT3X)?'3':'4', + s->i2c.pin_data, s->i2c.pin_clk, + s->lastTemp/10, tf, s->lastHumid/10, s->lastHumid%10, + s->channel_temp, s->channel_humid); +#endif + } + return CMD_RES_OK; +} + +// ------------------------------------------------------- +// startDriver SHTXX [SCL=] [SDA=] [chan_t=] +// [chan_h=] [type=3|4] [address=] +// +// type=0 or omitted → auto-detect (needs ENABLE_SERIAL_READ). +// address= accepts decimal (68) or "0x45" notation. +// ------------------------------------------------------- +void SHTXX_Init() +{ + if(g_numSensors >= SHTXX_MAX_SENSORS) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "SHTXX: Max sensors reached."); + return; + } + shtxx_dev_t *dev = &g_sensors[g_numSensors]; + + dev->i2c.pin_clk = 9; + dev->i2c.pin_data = 17; + dev->channel_temp = -1; + dev->channel_humid = -1; + if(g_numSensors == 0) { + dev->i2c.pin_clk = PIN_FindPinIndexForRole(IOR_SHT3X_CLK, dev->i2c.pin_clk); + dev->i2c.pin_data = PIN_FindPinIndexForRole(IOR_SHT3X_DAT, dev->i2c.pin_data); + dev->channel_temp = g_cfg.pins.channels [dev->i2c.pin_data]; + dev->channel_humid = g_cfg.pins.channels2[dev->i2c.pin_data]; + } + + dev->i2c.pin_clk = Tokenizer_GetPinEqual("SCL=", dev->i2c.pin_clk); + dev->i2c.pin_data = Tokenizer_GetPinEqual("SDA=", dev->i2c.pin_data); + dev->channel_temp = Tokenizer_GetArgEqualInteger("chan_t=", dev->channel_temp); + dev->channel_humid = Tokenizer_GetArgEqualInteger("chan_h=", dev->channel_humid); + + dev->secondsBetween = 10; + dev->secondsUntilNext = 1; + dev->calTemp = 0; + dev->calHum = 0; + + int typeArg = Tokenizer_GetArgEqualInteger("type=", 3); + dev->typeIdx = (typeArg == 4) ? SHTXX_TYPE_SHT4X : SHTXX_TYPE_SHT3X; + + // Address: parse "0x45" without strtol (saves ~400B if not already in fw). + // Use A/B suffix for backward compat: address=A → 0x44, address=B → 0x45. + dev->i2cAddr = SHTXX_I2C_ADDR; + if(dev->typeIdx == SHTXX_TYPE_SHT3X) { + uint8_t A = (int8_t)(Tokenizer_GetArgEqualInteger("address=", 0x44)); + if (A != SHTXX_I2C_ADDR) dev->i2cAddr = A << 1; + } + + Soft_I2C_PreInit(&dev->i2c); + rtos_delay_milliseconds(50); + + setPinUsedString(dev->i2c.pin_clk, "SHTXX SCL"); + setPinUsedString(dev->i2c.pin_data, "SHTXX SDA"); + +#ifdef ENABLE_SERIAL_READ + dev->serial = 0; + if(typeArg == 0 || typeArg == 3) { // explicit type=0 or default=3 → try detect + dev->typeIdx = SHTXX_DetectType(dev); + } else { + SHTXX_ReadSerial(dev); // type known, just log the serial + } +#endif + + SHTXX_Initialization(dev); + + if(g_numSensors == 0) + { + //cmddetail:{"name":"SHTXX_Calibrate","args":"[DeltaTemp] [DeltaHum] [sensor]", + //cmddetail:"descr":"Offset calibration. Floats in °C and %RH. Sensor index optional (1-based).", + //cmddetail:"fn":"SHTXX_CMD_Calibrate","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_Calibrate -1.5 3"} + CMD_RegisterCommand("SHTXX_Calibrate", SHTXX_CMD_Calibrate, dev); + + //cmddetail:{"name":"SHTXX_Cycle","args":"[seconds]", + //cmddetail:"descr":"Measurement interval in seconds (min 1).", + //cmddetail:"fn":"SHTXX_CMD_Cycle","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_Cycle 30"} + CMD_RegisterCommand("SHTXX_Cycle", SHTXX_CMD_Cycle, dev); + + //cmddetail:{"name":"SHTXX_Measure","args":"", + //cmddetail:"descr":"Trigger immediate one-shot measurement.", + //cmddetail:"fn":"SHTXX_CMD_Force","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_Measure"} + CMD_RegisterCommand("SHTXX_Measure", SHTXX_CMD_Force, dev); + + //cmddetail:{"name":"SHTXX_Reinit","args":"", + //cmddetail:"descr":"Soft-reset and re-probe.", + //cmddetail:"fn":"SHTXX_CMD_Reinit","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_Reinit"} + CMD_RegisterCommand("SHTXX_Reinit", SHTXX_CMD_Reinit, dev); + + //cmddetail:{"name":"SHTXX_AddSensor","args":"[SCL=pin] [SDA=pin] [type=3|4] [chan_t=ch] [chan_h=ch]", + //cmddetail:"descr":"Add an additional SHT sensor on different pins.", + //cmddetail:"fn":"SHTXX_CMD_AddSensor","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_AddSensor SDA=4 SCL=5 type=3"} + CMD_RegisterCommand("SHTXX_AddSensor", SHTXX_CMD_AddSensor, dev); + + //cmddetail:{"name":"SHTXX_ListSensors","args":"", + //cmddetail:"descr":"List all registered sensors.", + //cmddetail:"fn":"SHTXX_CMD_ListSensors","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_ListSensors"} + CMD_RegisterCommand("SHTXX_ListSensors", SHTXX_CMD_ListSensors, dev); + +#ifdef ENABLE_SHT3_EXTENDED_FEATURES + //cmddetail:{"name":"SHTXX_LaunchPer","args":"[msb] [lsb] [sensor]", + //cmddetail:"descr":"Start SHT3x periodic capture. Default 0x23/0x22 = 4mps high rep.", + //cmddetail:"fn":"SHTXX_CMD_LaunchPer","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_LaunchPer\nSHTXX_LaunchPer 0x23 0x22 2"} + CMD_RegisterCommand("SHTXX_LaunchPer", SHTXX_CMD_LaunchPer, dev); + + //cmddetail:{"name":"SHTXX_FetchPer","args":"[sensor]", + //cmddetail:"descr":"Fetch latest periodic result. SHT3x only.", + //cmddetail:"fn":"SHTXX_CMD_FetchPer","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_FetchPer"} + CMD_RegisterCommand("SHTXX_FetchPer", SHTXX_CMD_FetchPer, dev); + + //cmddetail:{"name":"SHTXX_StopPer","args":"[sensor]", + //cmddetail:"descr":"Stop SHT3x periodic capture.", + //cmddetail:"fn":"SHTXX_CMD_StopPer","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_StopPer"} + CMD_RegisterCommand("SHTXX_StopPer", SHTXX_CMD_StopPer, dev); + + //cmddetail:{"name":"SHTXX_Heater","args":"[0|1] [sensor]", + //cmddetail:"descr":"Enable/disable heater. SHT3x only.", + //cmddetail:"fn":"SHTXX_CMD_Heater","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_Heater 1"} + CMD_RegisterCommand("SHTXX_Heater", SHTXX_CMD_Heater, dev); + + //cmddetail:{"name":"SHTXX_GetStatus","args":"[sensor]", + //cmddetail:"descr":"Read status register. SHT3x only.", + //cmddetail:"fn":"SHTXX_CMD_GetStatus","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_GetStatus"} + CMD_RegisterCommand("SHTXX_GetStatus", SHTXX_CMD_GetStatus, dev); + + //cmddetail:{"name":"SHTXX_ClearStatus","args":"[sensor]", + //cmddetail:"descr":"Clear status register. SHT3x only.", + //cmddetail:"fn":"SHTXX_CMD_ClearStatus","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_ClearStatus"} + CMD_RegisterCommand("SHTXX_ClearStatus", SHTXX_CMD_ClearStatus, dev); + + //cmddetail:{"name":"SHTXX_ReadAlert","args":"[sensor]", + //cmddetail:"descr":"Read alert thresholds. SHT3x only.", + //cmddetail:"fn":"SHTXX_CMD_ReadAlert","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_ReadAlert"} + CMD_RegisterCommand("SHTXX_ReadAlert", SHTXX_CMD_ReadAlert, dev); + + //cmddetail:{"name":"SHTXX_SetAlert","args":"[tHS] [tLS] [hHS] [hLS] [sensor]", + //cmddetail:"descr":"Set alert thresholds. SHT3x only.", + //cmddetail:"fn":"SHTXX_CMD_SetAlert","file":"driver/drv_shtxx.c","requires":"", + //cmddetail:"examples":"SHTXX_SetAlert 40 10 80 20"} + CMD_RegisterCommand("SHTXX_SetAlert", SHTXX_CMD_SetAlert, dev); +#endif + } + + g_numSensors++; +} + +void SHTXX_StopDriver() +{ + for(uint8_t i = 0; i < g_numSensors; i++) { + shtxx_dev_t *dev = &g_sensors[i]; +#ifdef ENABLE_SHT3_EXTENDED_FEATURES + if(dev->typeIdx == SHTXX_TYPE_SHT3X) SHTXX_StopPeriodic(dev); +#endif + const shtxx_cfg_t *cfg = &SHT_g_cfg[dev->typeIdx]; + SHTXX_SendCmd(dev, cfg->rst_cmd, cfg->rst_len); + } +} + +void SHTXX_OnEverySecond() +{ + for(uint8_t i = 0; i < g_numSensors; i++) { + shtxx_dev_t *dev = &g_sensors[i]; + if(dev->secondsUntilNext == 0) { + if(dev->isWorking) { +#ifdef ENABLE_SHT3_EXTENDED_FEATURES + if(dev->periodicActive) SHTXX_FetchPeriodic(dev); + else +#endif + SHTXX_Measure(dev); + } + dev->secondsUntilNext = dev->secondsBetween; + } else { + dev->secondsUntilNext--; + } + } +} + +void SHTXX_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState) +{ + if(bPreState) return; + for(uint8_t i = 0; i < g_numSensors; i++) { + shtxx_dev_t *dev = &g_sensors[i]; + int16_t tf = dev->lastTemp % 10; if(tf < 0) tf = -tf; + hprintf255(request, + "

SHT%cx[%u] Temp=%d.%dC Humid=%d.%d%%

", + (dev->typeIdx==SHTXX_TYPE_SHT3X)?'3':'4', i, + dev->lastTemp/10, tf, dev->lastHumid/10, dev->lastHumid%10); + if(!dev->isWorking) + hprintf255(request, "WARNING: SHT[%u] init failed – check pins!", i); + if(dev->channel_humid >= 0 && dev->channel_humid == dev->channel_temp) + hprintf255(request, "WARNING: SHT[%u] temp/humid share a channel!", i); + } +} diff --git a/src/driver/drv_shtxx.h b/src/driver/drv_shtxx.h new file mode 100644 index 000000000..6e3f9b790 --- /dev/null +++ b/src/driver/drv_shtxx.h @@ -0,0 +1,85 @@ +#pragma once +#include +#include + +// ------------------------------------------------------- +// Feature gates. +// +// ENABLE_SERIAL_READ +// Reads serial number at init; shows it in ListSensors; +// enables type=0 auto-detect. ~150B extra flash, 4B RAM +// per sensor. Safe to disable when type= is always +// specified and serial identification is not needed. +// +// ENABLE_SHT3_EXTENDED_FEATURES +// SHT3x-only commands: heater, status, alert limits, +// periodic capture. Requires float for alert bit-math. +// ~1.5KB code + float softlib (~2KB) if not already in fw. +// ENABLE_SERIAL_READ must also be defined when this is on. +// ------------------------------------------------------- +#define ENABLE_SERIAL_READ +//#define ENABLE_SHT3_EXTENDED_FEATURES + +#define SHTXX_I2C_ADDR (0x44 << 1) +#define SHTXX_TYPE_SHT3X 0 +#define SHTXX_TYPE_SHT4X 1 +#define SHTXX_MAX_SENSORS 4 + +// ------------------------------------------------------- +// Config table – one entry per sensor family, in flash. +// +// hum_scale / hum_offset kept as uint8_t/int8_t (same as +// original) so the struct stays compact and the multiply +// stays cheap. The ×10 scaling is applied inline: +// RH10 = (10 * hum_scale * raw + 32767) / 65535 + 10 * hum_offset +// SHT3x: scale=100, offset=0 → 0..1000 +// SHT4x: scale=125, offset=-6 → -60..1190 → clamped to 0..1000 +// max intermediate: 10*125*65535 = 81,918,750 → fits uint32_t ✓ +// ------------------------------------------------------- +typedef struct +{ + uint8_t rst_cmd[2]; + uint8_t rst_len; + uint8_t rst_delay_ms; + uint8_t meas_cmd[2]; + uint8_t meas_len; + uint8_t meas_delay_ms; + uint8_t hum_scale; // 100 or 125 + int8_t hum_offset; // 0 or -6 +} shtxx_cfg_t; + +// ------------------------------------------------------- +// Per-instance state. +// +// lastTemp: 0.1 °C (e.g. 225 = 22.5 °C) +// lastHumid: 0.1 %RH (e.g. 456 = 45.6 %RH) +// calTemp: 0.1 °C offset +// calHum: 0.1 %RH offset (int8_t: range ±12.7 %RH – +// sufficient for any realistic humidity offset) +// ------------------------------------------------------- +typedef struct +{ + softI2C_t i2c; + int16_t calTemp; // 0.1 °C + int8_t calHum; // 0.1 %RH (±12.7 %RH range) + int8_t channel_temp; // -1 = not used + int8_t channel_humid; // -1 = not used + uint8_t i2cAddr; + uint8_t typeIdx; + uint8_t secondsBetween; + uint8_t secondsUntilNext; + bool isWorking; + int16_t lastTemp; // 0.1 °C + int16_t lastHumid; // 0.1 %RH +#ifdef ENABLE_SERIAL_READ + uint32_t serial; +#endif +#ifdef ENABLE_SHT3_EXTENDED_FEATURES + bool periodicActive; +#endif +} shtxx_dev_t; + +void SHTXX_Init(); +void SHTXX_StopDriver(); +void SHTXX_OnEverySecond(); +void SHTXX_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState); diff --git a/src/driver/drv_soft_i2c.c b/src/driver/drv_soft_i2c.c index a7d2d651e..a42da01da 100644 --- a/src/driver/drv_soft_i2c.c +++ b/src/driver/drv_soft_i2c.c @@ -11,6 +11,7 @@ #include "../hal/hal_pins.h" static int g_clk_period = SM2135_DELAY; +#ifndef WIN32 #if !PLATFORM_ESPIDF && !PLATFORM_XR806 && !PLATFORM_XR872 && !PLATFORM_ESP8266 && !PLATFORM_REALTEK_NEW && !PLATFORM_TXW81X void usleep(int r) //delay function do 10*r nops, because rtos_delay_milliseconds is too much @@ -24,6 +25,7 @@ void usleep(int r) //delay function do 10*r nops, because rtos_delay_millisecond } #endif + void Soft_I2C_SetLow(uint8_t pin) { HAL_PIN_Setup_Output(pin); HAL_PIN_SetOutputValue(pin, 0); @@ -153,4 +155,4 @@ uint8_t Soft_I2C_ReadByte(softI2C_t *i2c, bool nack) return val; } - +#endif // to #ifndef WIN32 diff --git a/src/driver/drv_soft_i2c_sim.c b/src/driver/drv_soft_i2c_sim.c new file mode 100644 index 000000000..e097d9f44 --- /dev/null +++ b/src/driver/drv_soft_i2c_sim.c @@ -0,0 +1,315 @@ +// drv_soft_i2c_sim.c +// ------------------------------------------------------- +// Generic soft-I2C bus simulator – core layer. +// +// This file is entirely wrapped in #ifdef WIN32. +// It replaces the real hardware Soft_I2C_* functions on +// Windows and routes every transaction to a matching +// virtual device whose protocol is handled by plugin +// callbacks (sim_sensor_ops_t). +// +// This file contains NO sensor-specific code. +// See drv_soft_i2c_sim_sensors.c for the sensor plugins. +// ------------------------------------------------------- +#ifdef WIN32 + +#include +#include +#include +#include +#include +#include + +#include "drv_soft_i2c_sim.h" + +// Include the real softI2C_t definition so the stub +// signatures match what the sensor drivers expect. +#include "drv_local.h" + +// ------------------------------------------------------- +// Compile-time limits +// ------------------------------------------------------- +#define SIM_MAX_DEVICES 16 +#define SIM_DEFAULT_STEP 10 // default drift per read in x10 units + + + + +// Internally we use the shifted address in g_devices. +// So e.g. for SHT3x/SHT4x with address 0x44 we store 0x44 << 1 ( = 0x88) +// but in displaying / logging we should use "known" 7 bit address (like 0x44 for SHT3x) +#define dispI2Cadress(S) S >> 1 + + + + +// ------------------------------------------------------- +// Internal device record +// ------------------------------------------------------- +typedef struct { + bool active; + const sim_sensor_ops_t *ops; + sim_ctx_t ctx; +} sim_device_t; + +static sim_device_t g_devices[SIM_MAX_DEVICES]; +static bool g_inited = false; + +// Currently active device for the ongoing I2C transaction +static sim_device_t *g_cur = NULL; +static bool g_cur_is_read = false; // current Start was a read + +// ------------------------------------------------------- +// Minimal LCG – no dependency on rand() quality +// ------------------------------------------------------- +static uint32_t g_rng; + +static int32_t sim_rand(int32_t lo, int32_t hi) { + g_rng = g_rng * 1664525u + 1013904223u; + int32_t range = hi - lo + 1; + if (range <= 0) return lo; + return lo + (int32_t)(g_rng % (uint32_t)range); +} + +// ------------------------------------------------------- +// Device lookup by (pin_data, pin_clk, shifted 7-bit addr) +// ------------------------------------------------------- +static sim_device_t *sim_find(uint8_t pin_data, uint8_t pin_clk, uint8_t addr) { + for (int i = 0; i < SIM_MAX_DEVICES; i++) { + if (!g_devices[i].active) continue; + sim_ctx_t *c = &g_devices[i].ctx; + if (c->pin_data == pin_data && + c->pin_clk == pin_clk && + c->i2c_addr == addr) + return &g_devices[i]; + } + return NULL; +} + +// ------------------------------------------------------- +// Value helpers (public – used by plugins) +// ------------------------------------------------------- + +void SoftI2C_Sim_SetValue(sim_ctx_t *ctx, sim_quantity_t q, + int32_t initial, int32_t min, int32_t max, + int32_t step) { + if (!ctx || q >= SIM_Q_COUNT) return; + sim_value_t *v = &ctx->values[q]; + v->active = true; + v->current = initial; + v->min = min; + v->max = max; + v->step = step > 0 ? step : SIM_DEFAULT_STEP; +} + +int32_t SoftI2C_Sim_NextValue(sim_ctx_t *ctx, sim_quantity_t q) { + if (!ctx || q >= SIM_Q_COUNT) return 0; + sim_value_t *v = &ctx->values[q]; + if (!v->active) return 0; + v->current += sim_rand(-v->step, v->step); + if (v->current < v->min) v->current = v->min; + if (v->current > v->max) v->current = v->max; + return v->current; +} + +int32_t SoftI2C_Sim_PeekValue(sim_ctx_t *ctx, sim_quantity_t q) { + if (!ctx || q >= SIM_Q_COUNT) return 0; + return ctx->values[q].current; +} + +void SoftI2C_Sim_ForceValue(sim_ctx_t *ctx, sim_quantity_t q, int32_t value) { + if (!ctx || q >= SIM_Q_COUNT) return; + ctx->values[q].current = value; +} + +// ------------------------------------------------------- +// Core API +// ------------------------------------------------------- + +void SoftI2C_Sim_Init(void) { + if (g_inited) return; + memset(g_devices, 0, sizeof(g_devices)); + g_rng = (uint32_t)time(NULL); + g_inited = true; +} + +int SoftI2C_Sim_Register(uint8_t pin_data, uint8_t pin_clk, + uint8_t i2c_addr, + const sim_sensor_ops_t *ops) { + SoftI2C_Sim_Init(); + if (!ops || !ops->encode_response) { + printf("[SIM] Register: ops->encode_response is required\n"); + return -1; + } + for (int i = 0; i < SIM_MAX_DEVICES; i++) { + if (!g_devices[i].active) { + sim_device_t *d = &g_devices[i]; + memset(d, 0, sizeof(sim_device_t)); + d->active = true; + d->ops = ops; + d->ctx.pin_data = pin_data; + d->ctx.pin_clk = pin_clk; + d->ctx.i2c_addr = i2c_addr & 0xFE; // strip R/W bit just in case + if (ops->init) ops->init(&d->ctx); + printf("[SIM] Registered '%s' addr=0x%02X pins(dat=%u,clk=%u) slot=%d\n", + ops->name ? ops->name : "?", dispI2Cadress(i2c_addr), pin_data, pin_clk, i); // dispI2Cadress() display 7 bit adress --> >> 1 + return i; + } + } + printf("[SIM] Register: table full (max %d devices)\n", SIM_MAX_DEVICES); + return -1; +} + +sim_ctx_t *SoftI2C_Sim_GetCtx(int slot) { + if (slot < 0 || slot >= SIM_MAX_DEVICES) return NULL; + if (!g_devices[slot].active) return NULL; + return &g_devices[slot].ctx; +} + + +// ------------------------------------------------------- +// Soft_I2C_* stubs – these replace the hardware versions +// on Windows. Sensor drivers call these unchanged. +// ------------------------------------------------------- + +bool Soft_I2C_PreInit(softI2C_t *i2c) { + SoftI2C_Sim_Init(); + + printf("[SIM] PreInit pins dat=%u clk=%u\n", i2c->pin_data, i2c->pin_clk); + return true; // always report bus healthy +} + +// Start a transaction. +// addr is the 8-bit wire address (7-bit device addr | R/W bit). +// Returns true (ACK) if a virtual device is found, false (NACK) otherwise. +// Internally we use the shifted address, e.g. 0x44 << 1 ( =0x88) +// but in displaying / logging use "known" 7 bit address (like 0x44 for SHT3x) +bool Soft_I2C_Start(softI2C_t *i2c, uint8_t addr) { + SoftI2C_Sim_Init(); + bool is_read = (addr & 0x01) != 0; + // Normalise to 8-bit wire address with R/W bit cleared, matching + // how SoftI2C_Sim_Register stores it. + uint8_t wire_addr = addr & 0xFE; + + g_cur = sim_find(i2c->pin_data, i2c->pin_clk, wire_addr); + if (!g_cur) { + printf("[SIM] no device: address=0x%02X pins(dat=%u,clk=%u)\n", + dispI2Cadress(wire_addr), i2c->pin_data, i2c->pin_clk); + return false; + } + // Found – only log on write-start (R/W=0) to avoid flooding on every read + g_cur_is_read = is_read; + if (!is_read) { + printf("[SIM] found: address=0x%02X (%s) pins(dat=%u,clk=%u)\n", + dispI2Cadress(wire_addr), g_cur->ops->name ? g_cur->ops->name : "?", + i2c->pin_data, i2c->pin_clk); + } + + if (is_read) { + // Reading – response was prepared during the preceding write Stop. + // Just reset the read cursor; do NOT clear resp_buf. + g_cur->ctx.resp_pos = 0; + } else { + // Writing a new command – clear command + response buffers. + g_cur->ctx.cmd_len = 0; + g_cur->ctx.resp_len = 0; + g_cur->ctx.resp_pos = 0; + } + return true; // ACK +} + +/* +// Identical to Soft_I2C_Start but without writing an address byte. +// Used by BMP280 which calls Soft_I2C_Start_Internal() directly. +void Soft_I2C_Start_Internal(softI2C_t *i2c) { + // For BMP280 the address is written as the first data byte, + // so we can't resolve the device here. We simply leave g_cur + // as-is (the BMP280 shim handles it at the BMP280.h level). + (void)i2c; +} +*/ +// Used by drv_bmp280.c / BMP280.h instead of Soft_I2C_Start. +// On real hardware the caller clocks the address byte manually. +// In simulation the address is already in i2c->address8bit, so +// we resolve the device here exactly like a normal write Start. +void Soft_I2C_Start_Internal(softI2C_t *i2c) { + SoftI2C_Sim_Init(); + uint8_t wire_addr = i2c->address8bit & 0xFE; + g_cur_is_read = false; + g_cur = sim_find(i2c->pin_data, i2c->pin_clk, wire_addr); + if (!g_cur) { + printf("[SIM] no device (Internal): wire=0x%02X (address=0x%02X) pins(dat=%u,clk=%u)\n", + wire_addr, dispI2Cadress(wire_addr), i2c->pin_data, i2c->pin_clk); + return; + } + printf("[SIM] found (Internal): wire=0x%02X (address=0x%02X) (%s) pins(dat=%u,clk=%u)\n", + wire_addr, dispI2Cadress(wire_addr), g_cur->ops->name ? g_cur->ops->name : "?", + i2c->pin_data, i2c->pin_clk); + g_cur->ctx.cmd_len = 0; + g_cur->ctx.resp_len = 0; + g_cur->ctx.resp_pos = 0; +} + +bool Soft_I2C_WriteByte(softI2C_t *i2c, uint8_t value) { + (void)i2c; + if (!g_cur || g_cur_is_read) return false; + sim_ctx_t *ctx = &g_cur->ctx; + if (ctx->cmd_len < (uint8_t)sizeof(ctx->cmd)) { + ctx->cmd[ctx->cmd_len++] = value; + } + return true; // ACK +} + +void Soft_I2C_Stop(softI2C_t *i2c) { + (void)i2c; + if (!g_cur) return; + + if (!g_cur_is_read) { + // Write phase ended – ask the plugin to prepare a response. + sim_ctx_t *ctx = &g_cur->ctx; + const sim_sensor_ops_t *ops = g_cur->ops; + ctx->resp_len = 0; + ctx->resp_pos = 0; + bool ack = ops->encode_response(ctx); + if (ack) { + printf("[SIM] '%s' addr=0x%02X cmd=0x%02X (len=%u) -> %u byte response\n", + ops->name ? ops->name : "?", + dispI2Cadress(ctx->i2c_addr), ctx->cmd_len ? ctx->cmd[0] : 0xFF, + ctx->cmd_len, ctx->resp_len); + } else { + printf("[SIM] '%s' addr=0x%02X cmd=0x%02X NACK\n", + ops->name ? ops->name : "?", + dispI2Cadress(ctx->i2c_addr), ctx->cmd_len ? ctx->cmd[0] : 0xFF); + } + } else { + // Read phase ended. + if (g_cur->ops->on_read_complete) + g_cur->ops->on_read_complete(&g_cur->ctx); + } + + g_cur = NULL; +} + +uint8_t Soft_I2C_ReadByte(softI2C_t *i2c, bool nack) { + (void)i2c; + (void)nack; + if (!g_cur) return 0xFF; + sim_ctx_t *ctx = &g_cur->ctx; + if (ctx->resp_pos < ctx->resp_len) + return ctx->resp[ctx->resp_pos++]; + return 0xFF; // bus float – return high +} + +void Soft_I2C_ReadBytes(softI2C_t *i2c, uint8_t *buf, int n) { + for (int i = 0; i < n; i++) { + bool nack = (i == n - 1); + buf[i] = Soft_I2C_ReadByte(i2c, nack); + } +} + +// GPIO and timing stubs – not needed on Windows +void Soft_I2C_SetLow (uint8_t pin) { (void)pin; } +void Soft_I2C_SetHigh(uint8_t pin) { (void)pin; } +void usleep(int r) { (void)r; } + +#endif // WIN32 diff --git a/src/driver/drv_soft_i2c_sim.h b/src/driver/drv_soft_i2c_sim.h new file mode 100644 index 000000000..ede80a9ea --- /dev/null +++ b/src/driver/drv_soft_i2c_sim.h @@ -0,0 +1,190 @@ +// drv_soft_i2c_sim.h +// ------------------------------------------------------- +// Windows-only generic soft-I2C bus simulator. +// +// Architecture +// ------------ +// Core: drv_soft_i2c_sim.c +// Intercepts all Soft_I2C_* calls and routes them +// to a matching virtual device. The core has NO +// knowledge of any sensor protocol. +// +// Plugins: drv_soft_i2c_sim_sensors.c (and any future file) +// Each sensor family registers itself by filling a +// sim_sensor_ops_t and calling SoftI2C_Sim_Register(). +// The only required callback is encode_response(). +// +// Dynamic values +// -------------- +// All physical quantities are stored as int32_t in x10 units +// (e.g. 224 = 22.4 C, 553 = 55.3 %RH, 10132 = 1013.2 hPa). +// After every read the value drifts by a random +/-step clamped +// to [min, max], producing natural-looking sensor data. +// ------------------------------------------------------- +#pragma once +#ifdef WIN32 + +#include +#include +#include "../new_common.h" +#include "../new_pins.h" +#include "../new_cfg.h" +// Commands register, execution API and cmd tokenizer +#include "../cmnds/cmd_public.h" + + + +// ------------------------------------------------------- +// Physical quantity IDs (extend freely) +// ------------------------------------------------------- +typedef enum { + SIM_Q_TEMPERATURE = 0, // x10 -> 224 = 22.4 C + SIM_Q_HUMIDITY = 1, // x10 -> 553 = 55.3 %RH + SIM_Q_PRESSURE = 2, // x10 -> 10132 = 1013.2 hPa + SIM_Q_CO2 = 3, // x1 -> 412 ppm + SIM_Q_ALTITUDE = 4, // x10 -> 1234 = 123.4 m + SIM_Q_COUNT // sentinel – keep last +} sim_quantity_t; + +// ------------------------------------------------------- +// One dynamic physical quantity +// ------------------------------------------------------- +typedef struct { + bool active; + int32_t current; // current value (x10 units) + int32_t min; // lower bound + int32_t max; // upper bound + int32_t step; // max random drift per read (0 = default 10) +} sim_value_t; + +// ------------------------------------------------------- +// Transaction context – one per registered virtual device. +// Passed to every callback so the plugin is self-contained. +// ------------------------------------------------------- +typedef struct { + uint8_t pin_data; + uint8_t pin_clk; + uint8_t i2c_addr; // 7-bit (R/W bit already stripped) + + // Command bytes collected during the current write phase + uint8_t cmd[8]; + uint8_t cmd_len; + + // Response buffer – plugin fills this in encode_response() + uint8_t resp[32]; + uint8_t resp_len; + uint8_t resp_pos; // read cursor (managed by core) + + // Dynamic physical quantities + sim_value_t values[SIM_Q_COUNT]; + + // Arbitrary per-sensor state (plugin allocates and owns this) + void *user; +} sim_ctx_t; + +// ------------------------------------------------------- +// Sensor plugin interface +// The only mandatory callback is encode_response(). +// ------------------------------------------------------- +typedef struct { + // Human-readable name printed in debug output, e.g. "AHT2x" + const char *name; + + // Called once after SoftI2C_Sim_Register() so the plugin can + // seed ctx->values[] with default ranges and allocate ctx->user. + // May be NULL. + void (*init)(sim_ctx_t *ctx); + + // Called every time the I2C master completes a write phase + // (i.e. after the Stop that follows a write-addressed Start). + // ctx->cmd[0..cmd_len-1] holds the bytes the master sent. + // The plugin must fill ctx->resp[] and set ctx->resp_len. + // Return false to signal NACK (no response). + bool (*encode_response)(sim_ctx_t *ctx); + + // Called after a read cycle completes (optional). + // Useful when a sensor needs a side-effect after being polled + // (e.g. clearing a "data ready" flag). + // May be NULL. + void (*on_read_complete)(sim_ctx_t *ctx); +} sim_sensor_ops_t; + +// ------------------------------------------------------- +// Core API +// ------------------------------------------------------- + +// Initialise the simulator. Idempotent – safe to call multiple times. +void SoftI2C_Sim_Init(void); + +// Register a virtual device on a specific (pin_data, pin_clk, addr) tuple. +// ops must remain valid for the lifetime of the simulation. +// Returns a slot handle >= 0, or -1 on failure (table full). +int SoftI2C_Sim_Register(uint8_t pin_data, uint8_t pin_clk, + uint8_t i2c_addr, + const sim_sensor_ops_t *ops); + +// Get a pointer to a registered context (for test-script tweaks). +// Returns NULL if the slot is invalid. +sim_ctx_t *SoftI2C_Sim_GetCtx(int slot); + +// ------------------------------------------------------- +// Value helpers – call from init() or encode_response() +// ------------------------------------------------------- + +// Define (or redefine) a dynamic quantity. All values in x10 units. +// step = 0 uses the compiled-in default of 10 (= 1.0 in the physical unit). +void SoftI2C_Sim_SetValue(sim_ctx_t *ctx, sim_quantity_t q, + int32_t initial, int32_t min, int32_t max, + int32_t step); + +// Advance the value by one random drift step and return the new value. +int32_t SoftI2C_Sim_NextValue(sim_ctx_t *ctx, sim_quantity_t q); + +// Return the current value without advancing it. +int32_t SoftI2C_Sim_PeekValue(sim_ctx_t *ctx, sim_quantity_t q); + +// Force-set the current value without changing the range (test injection). +void SoftI2C_Sim_ForceValue(sim_ctx_t *ctx, sim_quantity_t q, int32_t value); + +// ------------------------------------------------------- +// Pre-built sensor plugins (implemented in drv_soft_i2c_sim_sensors.c) +// +// All "addr" parameters are 7-bit addresses (without the R/W bit). +// The drivers use 8-bit addresses (addr << 1), so for example: +// SHT3x default 0x44 (8-bit: 0x88) -> pass 0x44 here +// AHT2x fixed 0x38 (8-bit: 0x70) -> pass 0x38 here +// BMP280 0x76 (8-bit: 0xEC) -> pass 0x76 here +// CHT83xx 0x40 (8-bit: 0x80) -> pass 0x40 here +// ------------------------------------------------------- +int SoftI2C_Sim_AddSHT3x (uint8_t pin_data, uint8_t pin_clk, uint8_t addr); +int SoftI2C_Sim_AddSHT4x (uint8_t pin_data, uint8_t pin_clk, uint8_t addr); +int SoftI2C_Sim_AddAHT2x (uint8_t pin_data, uint8_t pin_clk); +int SoftI2C_Sim_AddBMP280(uint8_t pin_data, uint8_t pin_clk, uint8_t addr); +int SoftI2C_Sim_AddCHT83xx(uint8_t pin_data, uint8_t pin_clk, uint8_t addr); + +// defined in drv_soft_i2c_sim_sensors.c, initialized in drv_main.c +commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, const char* args, int cmdFlags); +// ------------------------------------------------------- +// Minimal usage example (WIN32 startup code): +// +// SoftI2C_Sim_Init(); +// +// // SHT3x on SDA=17, SCL=9, address 0x44 +// int slot = SoftI2C_Sim_AddSHT3x(17, 9, 0x44); +// // Narrow temperature range to a warm summer room: +// SoftI2C_Sim_SetValue(SoftI2C_Sim_GetCtx(slot), +// SIM_Q_TEMPERATURE, 280, 250, 320, 3); +// +// // AHT2x on same bus (different pin pair) +// SoftI2C_Sim_AddAHT2x(4, 5); +// +// // BMP280 with pressure simulation +// SoftI2C_Sim_AddBMP280(17, 9, 0x76); +// +// // Now just run the real driver init – no changes there: +// SHTXX_StartDriver(...); +// AHT2X_Init(); +// BMP280_Init(); +// ------------------------------------------------------- + +#endif // WIN32 diff --git a/src/driver/drv_soft_i2c_sim_sensors.c b/src/driver/drv_soft_i2c_sim_sensors.c new file mode 100644 index 000000000..96c57804b --- /dev/null +++ b/src/driver/drv_soft_i2c_sim_sensors.c @@ -0,0 +1,681 @@ +// drv_soft_i2c_sim_sensors.c +// ------------------------------------------------------- +// Sensor plugins the soft-I2C "simulator". +// +// All protocol and encoding details are derived from the +// official manufacturer datasheets: +// +// SHT3x – Sensirion Datasheet SHT3x-DIS v7 (Dec 2022) +// SHT4x – Sensirion Datasheet SHT4x v6.5 (Apr 2024) +// AHT2x – Aosong AHT20/AHT21 Datasheet A0/A1 (2020) +// BMP280 – Bosch BST-BMP280-DS001 v1.14 (2015) +// BME280 – Bosch BST-BME280-DS002 (2018) +// CHT8305/8310/8315 – Sensylink CHT83xx Datasheet +// +// Each plugin: +// init() – seed default value ranges +// encode_response() – produce the exact byte sequence +// the real chip would return +// +// ------------------------------------------------------- +#ifdef WIN32 + +#include +#include +#include +#include +#include +#include "../logging/logging.h" + +// e.g. for wal_stricmp (instead of missing strcasecmp) +#include "../new_common.h" + +// Commands register, execution API and cmd tokenizer +#include "../cmnds/cmd_public.h" + + +#include "drv_soft_i2c_sim.h" + +// =================================================================== +// Shared helpers +// =================================================================== + +// CRC-8/NRSC-5 poly=0x31 init=0xFF (Sensirion convention, all sensors) +static uint8_t crc8_sensirion(uint8_t a, uint8_t b) { + uint8_t crc = 0xFF ^ a; + for (int i = 0; i < 8; i++) crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); + crc ^= b; + for (int i = 0; i < 8; i++) crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); + return crc; +} + +// Pack a 3-byte Sensirion word [MSB, LSB, CRC] into buf +static void pack_word(uint8_t *buf, uint16_t raw) { + buf[0] = (uint8_t)(raw >> 8); + buf[1] = (uint8_t)(raw & 0xFF); + buf[2] = crc8_sensirion(buf[0], buf[1]); +} + +// Pack two Sensirion words (T then H) = 6 bytes +static void pack_th(uint8_t *resp, uint16_t raw_t, uint16_t raw_h) { + pack_word(resp, raw_t); + pack_word(resp + 3, raw_h); +} + +// =================================================================== +// SHT3x plugin +// =================================================================== +// Reference: Sensirion Datasheet SHT3x-DIS v7 (Dec 2022) +// +// I2C address: 0x44 (ADDR=GND) or 0x45 (ADDR=VDD) +// +// Commands (16-bit, MSB first): +// 0x2400 single-shot high-rep, no clock stretch +// 0x240B single-shot medium-rep, no clock stretch +// 0x2416 single-shot low-rep, no clock stretch +// 0x2C06 single-shot high-rep, clock stretch +// 0x2C0D single-shot medium-rep, clock stretch +// 0x2C10 single-shot low-rep, clock stretch +// 0x20xx..0x27xx periodic measurement start +// 0xE000 fetch data (periodic mode) +// 0x30A2 soft reset +// 0x3066 heater on +// 0x3098 heater off +// 0xF32D read status register +// 0x3780 read serial number +// +// Response (6 bytes): [T_MSB T_LSB T_CRC H_MSB H_LSB H_CRC] +// +// Conversion (datasheet eq. 1 & 2): +// T(°C) = -45 + 175 * raw_T / 65535 +// RH(%) = 100 * raw_H / 65535 +// Inversion: +// raw_T = (T + 45) * 65535 / 175 +// raw_H = RH * 65535 / 100 +// =================================================================== + +static void sht3x_init(sim_ctx_t *ctx) { + SoftI2C_Sim_SetValue(ctx, SIM_Q_TEMPERATURE, 220, 200, 250, 3); + SoftI2C_Sim_SetValue(ctx, SIM_Q_HUMIDITY, 500, 400, 600, 5); +} + +static void sht3x_build_meas(sim_ctx_t *ctx) { + int32_t t10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_TEMPERATURE); + int32_t h10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_HUMIDITY); + uint16_t raw_t = (uint16_t)(((int32_t)(t10 + 450) * 65535) / 1750); + uint16_t raw_h = (uint16_t)(((int32_t) h10 * 65535) / 1000); + pack_th(ctx->resp, raw_t, raw_h); + ctx->resp_len = 6; + printf("[SIM][SHT3x] T=%d.%d C H=%d.%d%% raw_T=0x%04X raw_H=0x%04X\n", + t10/10, abs(t10%10), h10/10, h10%10, raw_t, raw_h); +} + +static bool sht3x_encode(sim_ctx_t *ctx) { + if (ctx->cmd_len < 1) return false; + uint8_t c0 = ctx->cmd[0], c1 = (ctx->cmd_len >= 2) ? ctx->cmd[1] : 0; + + // Single-shot (all repeatability / clock-stretch variants) + if (c0 == 0x24 || c0 == 0x2C) { sht3x_build_meas(ctx); return true; } + // Periodic fetch + if (c0 == 0xE0 && c1 == 0x00) { sht3x_build_meas(ctx); return true; } + // Serial number → 6 bytes (two CRC-valid fake words) + if (c0 == 0x37) { + pack_word(ctx->resp, 0xDEAD); pack_word(ctx->resp+3, 0xBEEF); + ctx->resp_len = 6; return true; + } + // Status register → 2 bytes + CRC (0x0000 = no alerts) + if (c0 == 0xF3) { + pack_word(ctx->resp, 0x0000); ctx->resp_len = 3; return true; + } + // All other commands (reset, heater, break, periodic start) → ACK only + ctx->resp_len = 0; + return true; +} + +static const sim_sensor_ops_t g_sht3x_ops = { + .name = "SHT3x", .init = sht3x_init, .encode_response = sht3x_encode, +}; + +// =================================================================== +// SHT4x plugin +// =================================================================== +// Reference: Sensirion Datasheet SHT4x v6.5 (Apr 2024) +// +// I2C address: 0x44 (SHT40) or 0x45 (SHT41/42) +// +// Commands (8-bit, unlike SHT3x's 16-bit): +// 0xFD measure T+RH, high precision +// 0xF6 measure T+RH, medium precision +// 0xE0 measure T+RH, low precision +// 0x39/0x32/0x2F/0x24/0x1E/0x15 heater variants + measure +// 0x89 read serial number +// 0x94 soft reset +// +// Response (6 bytes): [T_MSB T_LSB T_CRC H_MSB H_LSB H_CRC] +// +// Conversion (datasheet eq. 1 & 2): +// T(°C) = -45 + 175 * raw_T / 65535 ← identical to SHT3x +// RH(%) = -6 + 125 * raw_H / 65535 ← different offset/scale from SHT3x +// Inversion: +// raw_T = (T + 45) * 65535 / 175 +// raw_H = (RH + 6) * 65535 / 125 +// =================================================================== + +static void sht4x_init(sim_ctx_t *ctx) { + SoftI2C_Sim_SetValue(ctx, SIM_Q_TEMPERATURE, 220, 200, 250, 3); + SoftI2C_Sim_SetValue(ctx, SIM_Q_HUMIDITY, 500, 400, 600, 5); +} + +static bool sht4x_encode(sim_ctx_t *ctx) { + if (ctx->cmd_len < 1) return false; + uint8_t c0 = ctx->cmd[0]; + + // All measurement commands (all precision levels + heater variants) + if (c0==0xFD||c0==0xF6||c0==0xE0|| + c0==0x39||c0==0x32||c0==0x2F||c0==0x24||c0==0x1E||c0==0x15) { + int32_t t10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_TEMPERATURE); + int32_t h10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_HUMIDITY); + uint16_t raw_t = (uint16_t)(((int32_t)(t10 + 450) * 65535) / 1750); + uint16_t raw_h = (uint16_t)(((int32_t)(h10 + 60) * 65535) / 1250); + pack_th(ctx->resp, raw_t, raw_h); + ctx->resp_len = 6; + printf("[SIM][SHT4x] T=%d.%d C H=%d.%d%% raw_T=0x%04X raw_H=0x%04X\n", + t10/10, abs(t10%10), h10/10, h10%10, raw_t, raw_h); + return true; + } + // Serial number → 6 bytes + if (c0 == 0x89) { + pack_word(ctx->resp, 0xDEAD); pack_word(ctx->resp+3, 0xBEEF); + ctx->resp_len = 6; return true; + } + // Soft reset and any unknown → ACK only + ctx->resp_len = 0; + return true; +} + +static const sim_sensor_ops_t g_sht4x_ops = { + .name = "SHT4x", .init = sht4x_init, .encode_response = sht4x_encode, +}; + +// =================================================================== +// AHT2x plugin (AHT20 / AHT21) +// =================================================================== +// Reference: Aosong AHT20 Datasheet A0 (2020), AHT21 Datasheet A1 (2020) +// +// I2C address: 0x38 (fixed) +// +// Commands: +// 0x71 status poll (standalone read, no preceding write needed) +// 0xBE 0x08 0x00 initialise / calibrate +// 0xAC 0x33 0x00 trigger measurement +// 0xBA soft reset +// +// Status byte: +// bit7 = busy (0=ready, 1=converting) +// bit3 = calibrated (1=ok) +// +// Measurement response (6 bytes, no CRC in standard mode): +// [0] = status byte +// [1] = H[19:12] +// [2] = H[11:4] +// [3] = H[3:0] | T[19:16] +// [4] = T[15:8] +// [5] = T[7:0] +// +// Conversion (datasheet section 5.4): +// RH(%) = raw_H / 2^20 * 100 raw_H = RH / 100 * 2^20 +// T(°C) = raw_T / 2^20 * 200 - 50 raw_T = (T+50) / 200 * 2^20 +// =================================================================== + +typedef struct { bool calibrated; } aht2x_state_t; + +static void aht2x_init(sim_ctx_t *ctx) { + SoftI2C_Sim_SetValue(ctx, SIM_Q_TEMPERATURE, 220, 150, 350, 3); + SoftI2C_Sim_SetValue(ctx, SIM_Q_HUMIDITY, 500, 200, 900, 5); + aht2x_state_t *s = (aht2x_state_t *)malloc(sizeof(aht2x_state_t)); + s->calibrated = false; + ctx->user = s; +} + +static bool aht2x_encode(sim_ctx_t *ctx) { + aht2x_state_t *s = (aht2x_state_t *)ctx->user; + if (!s) return false; + + // Bare read (cmd_len==0): status poll via 0x71 or post-init poll + if (ctx->cmd_len == 0) { + ctx->resp[0] = s->calibrated ? 0x08 : 0x00; + ctx->resp_len = 1; + return true; + } + + switch (ctx->cmd[0]) { + case 0xBA: // soft reset → uncalibrated + s->calibrated = false; + ctx->resp[0] = 0x00; ctx->resp_len = 1; + return true; + + case 0xBE: // initialise → calibrated; pre-load status for immediate poll + s->calibrated = true; + ctx->resp[0] = 0x08; ctx->resp_len = 1; + return true; + + case 0xAC: { // trigger measurement → 6-byte result + int32_t t10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_TEMPERATURE); + int32_t h10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_HUMIDITY); + uint32_t raw_h = (uint32_t)((int64_t) h10 * (1<<20) / 1000); + uint32_t raw_t = (uint32_t)((int64_t)(t10 + 500) * (1<<20) / 2000); + if (raw_h > 0xFFFFF) raw_h = 0xFFFFF; + if (raw_t > 0xFFFFF) raw_t = 0xFFFFF; + ctx->resp[0] = s->calibrated ? 0x08 : 0x00; + ctx->resp[1] = (uint8_t)((raw_h >> 12) & 0xFF); + ctx->resp[2] = (uint8_t)((raw_h >> 4) & 0xFF); + ctx->resp[3] = (uint8_t)(((raw_h & 0x0F) << 4) | ((raw_t >> 16) & 0x0F)); + ctx->resp[4] = (uint8_t)((raw_t >> 8) & 0xFF); + ctx->resp[5] = (uint8_t)( raw_t & 0xFF); + ctx->resp_len = 6; + printf("[SIM][AHT2x] T=%d.%d C H=%d.%d%% raw_T=0x%05X raw_H=0x%05X\n", + t10/10, abs(t10%10), h10/10, h10%10, raw_t, raw_h); + return true; + } + default: + ctx->resp_len = 0; + return true; + } +} + +static const sim_sensor_ops_t g_aht2x_ops = { + .name = "AHT2x", .init = aht2x_init, .encode_response = aht2x_encode, +}; + +// =================================================================== +// BMP280 / BME280 plugin +// =================================================================== +// Reference: Bosch BST-BMP280-DS001 v1.14 (2015) +// Bosch BST-BME280-DS002 (2018) +// +// I2C address: 0x76 (SDO=GND) or 0x77 (SDO=VDD) +// +// Key registers: +// 0xD0 chip_id (BMP280=0x58, BME280=0x60) +// 0xE0 reset (write 0xB6) +// 0xF2 ctrl_hum (BME280 only, must be written before ctrl_meas) +// 0xF3 status (bit3=measuring, bit0=im_update) +// 0xF4 ctrl_meas (osrs_t[7:5], osrs_p[4:2], mode[1:0]) +// 0xF5 config +// 0x88..0x9F calibration T1-T3, P1-P9 (24 bytes LE, burst-readable) +// 0xA1 dig_H1 (BME280, 1 byte) +// 0xE1..0xE7 dig_H2..H6 (BME280, 7 bytes LE, burst-readable) +// +// Data registers (0xF7 auto-increments): +// 0xF7 0xF8 0xF9 press_msb/lsb/xlsb → adc_P = [0]<<12|[1]<<4|[2]>>4 +// 0xFA 0xFB 0xFC temp_msb/lsb/xlsb → adc_T = [3]<<12|[4]<<4|[5]>>4 +// 0xFD 0xFE hum_msb/lsb → adc_H = [6]<<8|[7] (BME280 only) +// +// We use fixed calibration constants from a real BMP280 eval board. +// =================================================================== + +#define BMP_DIG_T1 27504u +#define BMP_DIG_T2 26435 +#define BMP_DIG_T3 -1000 +#define BMP_DIG_P1 36477u +#define BMP_DIG_P2 -10685 +#define BMP_DIG_P3 3024 +#define BMP_DIG_P4 2855 +#define BMP_DIG_P5 140 +#define BMP_DIG_P6 -7 +#define BMP_DIG_P7 15500 +#define BMP_DIG_P8 -14600 +#define BMP_DIG_P9 6000 + +typedef struct { uint8_t reg; bool is_bme280; } bmp280_state_t; + +// Invert BMP280 temperature compensation (linear approx. of the Bosch formula) +static uint32_t bmp280_temp_to_adc(int32_t t10) { + int32_t t_fine = (t10 * 10 * 320) / 5; + int32_t adc_T = ((t_fine * 2048 / BMP_DIG_T2) + (int32_t)BMP_DIG_T1 * 2) * 8; + if (adc_T < 0) adc_T = 0; + if (adc_T > 0xFFFFF) adc_T = 0xFFFFF; + return (uint32_t)adc_T; +} + +// Empirical linear pressure inversion (calibrated at 1013 hPa / 25°C) +static uint32_t bmp280_press_to_adc(int32_t p10) { + int32_t adc_P = 415000 + (p10 - 10132) * 38; + if (adc_P < 0) adc_P = 0; + if (adc_P > 0xFFFFF) adc_P = 0xFFFFF; + return (uint32_t)adc_P; +} + +// Produce 24-byte calibration block (0x88..0x9F), little-endian +static void bmp280_pack_calib(uint8_t *buf) { + static const int16_t c[] = { + (int16_t)BMP_DIG_T1, BMP_DIG_T2, BMP_DIG_T3, + (int16_t)BMP_DIG_P1, BMP_DIG_P2, BMP_DIG_P3, BMP_DIG_P4, + BMP_DIG_P5, BMP_DIG_P6, BMP_DIG_P7, BMP_DIG_P8, BMP_DIG_P9, + }; + for (int i = 0; i < 12; i++) { + buf[i*2] = (uint8_t)((uint16_t)c[i] & 0xFF); + buf[i*2+1] = (uint8_t)((uint16_t)c[i] >> 8); + } +} + +static void bmp280_init(sim_ctx_t *ctx) { + SoftI2C_Sim_SetValue(ctx, SIM_Q_TEMPERATURE, 250, 180, 450, 3); + SoftI2C_Sim_SetValue(ctx, SIM_Q_PRESSURE, 10132, 9800, 10400, 5); + SoftI2C_Sim_SetValue(ctx, SIM_Q_HUMIDITY, 500, 200, 900, 4); + bmp280_state_t *s = (bmp280_state_t *)malloc(sizeof(bmp280_state_t)); + s->reg = 0; s->is_bme280 = false; + ctx->user = s; +} + +static bool bmp280_encode(sim_ctx_t *ctx) { + bmp280_state_t *s = (bmp280_state_t *)ctx->user; + if (!s || ctx->cmd_len < 1) return false; + uint8_t reg = ctx->cmd[0]; + + // Register write (reg + data bytes) → ACK only + if (ctx->cmd_len >= 2) { s->reg = reg; ctx->resp_len = 0; return true; } + s->reg = reg; + + // 0xD0 – chip_id + if (reg == 0xD0) { + ctx->resp[0] = s->is_bme280 ? 0x60 : 0x58; + ctx->resp_len = 1; return true; + } + // 0x88..0x9F – calibration (burst or per-register; return all remaining bytes) + if (reg >= 0x88 && reg <= 0x9F) { + uint8_t calib[24]; bmp280_pack_calib(calib); + uint8_t off = reg - 0x88; + memcpy(ctx->resp, calib + off, 24 - off); + ctx->resp_len = (uint8_t)(24 - off); + return true; + } + // BME280 humidity calibration + if (s->is_bme280) { + // Humidity calibration chosen so the Bosch formula decodes trivially: + // adc_H = RH * 65536 / 100 (our encoding, see 0xF7/0xFD handlers) + // With H1=0, H2=100, H3=H4=H5=H6=0 the compensation reduces to: + // RH% = adc_H * 100 / 65536 + if (reg == 0xA1) { ctx->resp[0] = 0x00; ctx->resp_len = 1; return true; } // dig_H1=0 + if (reg >= 0xE1 && reg <= 0xE7) { + // E1,E2=dig_H2=100(0x0064 LE), E3=H3=0, E4=H4=0, E5=H5=0, E6=H5=0, E7=H6=0 + static const uint8_t hc[] = {0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t off = reg - 0xE1; + memcpy(ctx->resp, hc + off, 7 - off); + ctx->resp_len = (uint8_t)(7 - off); return true; + } + if (reg == 0xF2) { ctx->resp[0] = 0x01; ctx->resp_len = 1; return true; } + } + if (reg == 0xF3) { ctx->resp[0] = 0x00; ctx->resp_len = 1; return true; } // status: ready + if (reg == 0xF4) { ctx->resp[0] = 0x27; ctx->resp_len = 1; return true; } // ctrl_meas + if (reg == 0xF5) { ctx->resp[0] = 0x00; ctx->resp_len = 1; return true; } // config + + // 0xF7 – data burst (press + temp [+ hum for BME280]) + // Datasheet: 0xF7 auto-increments through 0xFC (0xFE for BME280). + // Return all bytes so both burst-read and split-read drivers work. + if (reg == 0xF7) { + int32_t t10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_TEMPERATURE); + int32_t p10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_PRESSURE); + uint32_t adc_P = bmp280_press_to_adc(p10); + uint32_t adc_T = bmp280_temp_to_adc(t10); + ctx->resp[0] = (uint8_t)((adc_P>>12)&0xFF); + ctx->resp[1] = (uint8_t)((adc_P>> 4)&0xFF); + ctx->resp[2] = (uint8_t)((adc_P<< 4)&0xF0); + ctx->resp[3] = (uint8_t)((adc_T>>12)&0xFF); + ctx->resp[4] = (uint8_t)((adc_T>> 4)&0xFF); + ctx->resp[5] = (uint8_t)((adc_T<< 4)&0xF0); + ctx->resp_len = 6; + if (s->is_bme280) { + int32_t h10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_HUMIDITY); + uint32_t adc_H = (uint32_t)((int64_t)h10 * 65536 / 1000); + if (adc_H > 0xFFFF) adc_H = 0xFFFF; + ctx->resp[6] = (uint8_t)(adc_H>>8); + ctx->resp[7] = (uint8_t)(adc_H&0xFF); + ctx->resp_len = 8; + printf("[SIM][BME280] T=%d.%d C P=%d.%d hPa H=%d.%d%% adc_T=0x%05X adc_P=0x%05X adc_H=0x%04X\n", + t10/10, abs(t10%10), p10/10, abs(p10%10), + h10/10, abs(h10%10), adc_T, adc_P, adc_H); + } else { + printf("[SIM][BMP280] T=%d.%d C P=%d.%d hPa adc_T=0x%05X adc_P=0x%05X\n", + t10/10, abs(t10%10), p10/10, abs(p10%10), adc_T, adc_P); + } + return true; + } + // 0xFD – humidity registers (BME280 only, 2 bytes) + // Some drivers issue a separate Write(0xFD)+Read(2) transaction for humidity + // instead of relying on the 0xF7 burst. Peek: 0xF7 already advanced the cycle. + if (reg == 0xFD && s->is_bme280) { + int32_t h10 = SoftI2C_Sim_PeekValue(ctx, SIM_Q_HUMIDITY); + uint32_t adc_H = (uint32_t)((int64_t)h10 * 65536 / 1000); + if (adc_H > 0xFFFF) adc_H = 0xFFFF; + ctx->resp[0] = (uint8_t)(adc_H >> 8); + ctx->resp[1] = (uint8_t)(adc_H & 0xFF); + ctx->resp_len = 2; + return true; + } + // 0xFA – temperature sub-address (drivers that split the burst at 0xFA) + // Peek: 0xF7 already advanced the measurement cycle. + if (reg == 0xFA) { + int32_t t10 = SoftI2C_Sim_PeekValue(ctx, SIM_Q_TEMPERATURE); + uint32_t adc_T = bmp280_temp_to_adc(t10); + ctx->resp[0] = (uint8_t)((adc_T>>12)&0xFF); + ctx->resp[1] = (uint8_t)((adc_T>> 4)&0xFF); + ctx->resp[2] = (uint8_t)((adc_T<< 4)&0xF0); + ctx->resp_len = 3; + if (s->is_bme280) { + int32_t h10 = SoftI2C_Sim_PeekValue(ctx, SIM_Q_HUMIDITY); + uint32_t adc_H = (uint32_t)((int64_t)h10 * 65536 / 1000); + if (adc_H > 0xFFFF) adc_H = 0xFFFF; + ctx->resp[3] = (uint8_t)(adc_H>>8); + ctx->resp[4] = (uint8_t)(adc_H&0xFF); + ctx->resp_len = 5; + } + return true; + } + + // Unknown register → 0x00 + ctx->resp[0] = 0x00; ctx->resp_len = 1; + return true; +} + +static const sim_sensor_ops_t g_bmp280_ops = { + .name = "BMP280", .init = bmp280_init, .encode_response = bmp280_encode, +}; + +// =================================================================== +// CHT83xx plugin (CHT8305, CHT8310, CHT8315) +// =================================================================== +// Reference: Sensylink CHT8305 / CHT8310 / CHT8315 Datasheet +// +// I2C address: 0x40 (default, ADDR pin selectable) +// +// Register map (all 2 bytes wide): +// 0x00 T[1:0] + H[1:0] read-only, 4 bytes [T_MSB T_LSB H_MSB H_LSB] +// 0x01 H[1:0] only read-only, 2 bytes (CHT831X) +// 0x02 Status register +// 0x03 Configuration +// 0x04 Conversion rate +// 0x05 Temperature high alert +// 0x06 Temperature low alert +// 0x07 Humidity high alert +// 0x08 Humidity low alert +// 0x0F One-shot trigger (CHT831X, write-only) +// 0xFE Manufacturer ID (2 bytes) +// 0xFF Sensor/chip ID (0x0000=CHT8305, 0x8215=CHT8310, 0x8315=CHT8315) +// +// CHT8305 encoding: +// T(°C) = raw_T * 165 / 65535 - 40 → raw_T = (T+40) * 65535 / 165 +// RH(%) = raw_H * 100 / 65535 → raw_H = RH * 65535 / 100 +// +// CHT831X (8310/8315) encoding: +// T(°C) = int16(raw_T) / 32 * 0.03125 → raw_T = int16(T/0.03125) << 3 +// RH(%) = (raw_H & 0x7FFF)/32768*100 → raw_H = RH*32768/100, parity=bit15 +// =================================================================== + +typedef struct { + uint8_t reg; + uint16_t sensor_id; // 0x0000=CHT8305, 0x8215=CHT8310, 0x8315=CHT8315 +} cht83xx_state_t; + +static void cht83xx_init(sim_ctx_t *ctx) { + SoftI2C_Sim_SetValue(ctx, SIM_Q_TEMPERATURE, 220, 150, 400, 3); + SoftI2C_Sim_SetValue(ctx, SIM_Q_HUMIDITY, 500, 200, 900, 5); + cht83xx_state_t *s = (cht83xx_state_t *)malloc(sizeof(cht83xx_state_t)); + s->reg = 0x00; + s->sensor_id = 0x0000; // CHT8305 default; set 0x8215/0x8315 for CHT831X + ctx->user = s; +} + +static uint16_t cht831x_hum_raw(int32_t h10) { + uint16_t h15 = (uint16_t)((int64_t)h10 * 32768 / 1000); + uint8_t par = 0; + for (int b = 0; b < 15; b++) par ^= (h15 >> b) & 1; + return (uint16_t)((h15 & 0x7FFF) | (par ? 0x8000u : 0)); +} + +static bool cht83xx_encode(sim_ctx_t *ctx) { + cht83xx_state_t *s = (cht83xx_state_t *)ctx->user; + if (!s) return false; + + // Update register pointer if a write was received + if (ctx->cmd_len > 0) { + s->reg = ctx->cmd[0]; + // Multi-byte write (config, alert limit, etc.) → ACK only + if (ctx->cmd_len > 1) { ctx->resp_len = 0; return true; } + } + + bool is_831x = (s->sensor_id == 0x8215 || s->sensor_id == 0x8315); + int32_t t10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_TEMPERATURE); + int32_t h10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_HUMIDITY); + + switch (s->reg) { + case 0x00: { // T + H (4 bytes) + uint16_t raw_t = is_831x + ? (uint16_t)((int16_t)(t10 * 10000 / 3125) << 3) + : (uint16_t)(((int32_t)(t10 + 400) * 65535) / 1650); + uint16_t raw_h = is_831x + ? cht831x_hum_raw(h10) + : (uint16_t)(((int32_t)h10 * 65535) / 1000); + ctx->resp[0]=(uint8_t)(raw_t>>8); ctx->resp[1]=(uint8_t)(raw_t&0xFF); + ctx->resp[2]=(uint8_t)(raw_h>>8); ctx->resp[3]=(uint8_t)(raw_h&0xFF); + ctx->resp_len = 4; + printf("[SIM][CHT83xx] T=%d.%d C H=%d.%d%% raw_T=0x%04X raw_H=0x%04X\n", + t10/10, abs(t10%10), h10/10, h10%10, raw_t, raw_h); + return true; + } + case 0x01: { // H only (CHT831X; peek so 0x00+0x01 agree in same cycle) + int32_t hp = SoftI2C_Sim_PeekValue(ctx, SIM_Q_HUMIDITY); + uint16_t raw_h = is_831x ? cht831x_hum_raw(hp) + : (uint16_t)(((int32_t)hp * 65535) / 1000); + ctx->resp[0]=(uint8_t)(raw_h>>8); ctx->resp[1]=(uint8_t)(raw_h&0xFF); + ctx->resp_len = 2; return true; + } + case 0x0F: ctx->resp_len = 0; return true; // one-shot trigger (write-only) + case 0xFE: ctx->resp[0]=0x54; ctx->resp[1]=0x53; ctx->resp_len=2; return true; // mfr ID "TS" + case 0xFF: ctx->resp[0]=(uint8_t)(s->sensor_id>>8); // chip/sensor ID + ctx->resp[1]=(uint8_t)(s->sensor_id&0xFF); + ctx->resp_len=2; return true; + default: ctx->resp[0]=0x00; ctx->resp[1]=0x00; // status, config, limits → 0 + ctx->resp_len=2; return true; + } +} + +static const sim_sensor_ops_t g_cht83xx_ops = { + .name = "CHT83xx", .init = cht83xx_init, .encode_response = cht83xx_encode, +}; +commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, const char* args, int cmdFlags) { + Tokenizer_TokenizeString(args, 0); + + + uint8_t pin_data=9, pin_clk=17; + pin_clk = (uint8_t)Tokenizer_GetPinEqual("SCL=", pin_clk); + pin_data = (uint8_t)Tokenizer_GetPinEqual("SDA=", pin_data); + const char *type = Tokenizer_GetArgEqualDefault("type=","NO"); + uint8_t def_addr,addr; + sim_sensor_ops_t *sens_ops; + if (!strcmp(type,"NO")){ + ADDLOG_ERROR(LOG_FEATURE_SENSOR, "No sensor type given!"); + return CMD_RES_BAD_ARGUMENT; + }else { + if (wal_stricmp(type, "SHT3x") == 0) { +// printf("Detected: SHT3x\n"); + sens_ops = &g_sht3x_ops; + def_addr = 0x44 << 1; + } else if (wal_stricmp(type, "SHT4x") == 0) { +// printf("Detected: SHT4x\n"); + sens_ops = &g_sht4x_ops; + def_addr = 0x44 << 1; + } else if (wal_stricmp(type, "AHT2x") == 0) { +// printf("Detected: AHT2x\n"); + sens_ops = &g_aht2x_ops; + def_addr = 0x38 << 1; + } else if (wal_stricmp(type, "CHT83xx") == 0 || wal_stricmp(type, "CHT8305") == 0 || wal_stricmp(type, "CHT8310") == 0 || wal_stricmp(type, "CHT8315") == 0) { +// printf("Detected: CHT83xx\n"); + sens_ops = &g_cht83xx_ops; + def_addr = 0x40 << 1; + } else if (wal_stricmp(type, "BMP280") == 0 || wal_stricmp(type, "BME280") == 0) { +// printf("Detected: BMP280\n"); + sens_ops = &g_bmp280_ops; + def_addr = 0x76 << 1; // to be dicussed, what is "default" + } else { + ADDLOG_ERROR(LOG_FEATURE_SENSOR, "Unknown sensor type %s.",type); + return CMD_RES_BAD_ARGUMENT; + } + } + uint8_t A = (int8_t)(Tokenizer_GetArgEqualInteger("address=", 0)); + if (A != 0){ + addr = A << 1; + } else { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "No device address given, using default 0x%02X!",def_addr >> 1 ); + addr = def_addr; + } + ADDLOG_INFO(LOG_FEATURE_SENSOR, "Adding %s sensor at address 0x%02X SDA=%i SCL=%i",type, addr >> 1, pin_data, pin_clk ); + int slot = SoftI2C_Sim_Register(pin_data, pin_clk, addr, sens_ops); + if (slot >= 0){ + if (wal_stricmp(type, "BME280") == 0) { + ((bmp280_state_t *)SoftI2C_Sim_GetCtx(slot)->user)->is_bme280 = true; + } else if (wal_stricmp(type, "CHT8305") == 0) { + //Sensor/chip ID CHT83xx: 0x0000=CHT8305, 0x8215=CHT8310, 0x8315=CHT8315 + ((cht83xx_state_t *)SoftI2C_Sim_GetCtx(slot)->user)->sensor_id = 0x0000; + } else if (wal_stricmp(type, "CHT8310") == 0) { + //Sensor/chip ID CHT83xx: 0x0000=CHT8305, 0x8215=CHT8310, 0x8315=CHT8315 + ((cht83xx_state_t *)SoftI2C_Sim_GetCtx(slot)->user)->sensor_id = 0x8215; + } else if (wal_stricmp(type, "CHT8315") == 0) { + //Sensor/chip ID CHT83xx: 0x0000=CHT8305, 0x8215=CHT8310, 0x8315=CHT8315 + ((cht83xx_state_t *)SoftI2C_Sim_GetCtx(slot)->user)->sensor_id = 0x8315; + } + } + + return CMD_RES_OK; +} + + +// =================================================================== +// Convenience registration functions +// =================================================================== + +int SoftI2C_Sim_AddSHT3x(uint8_t pin_data, uint8_t pin_clk, uint8_t addr) { + return SoftI2C_Sim_Register(pin_data, pin_clk, addr, &g_sht3x_ops); +} +int SoftI2C_Sim_AddSHT4x(uint8_t pin_data, uint8_t pin_clk, uint8_t addr) { + return SoftI2C_Sim_Register(pin_data, pin_clk, addr, &g_sht4x_ops); +} +int SoftI2C_Sim_AddAHT2x(uint8_t pin_data, uint8_t pin_clk) { + return SoftI2C_Sim_Register(pin_data, pin_clk, 0x70, &g_aht2x_ops); // 0x38<<1 +} +int SoftI2C_Sim_AddBMP280(uint8_t pin_data, uint8_t pin_clk, uint8_t addr) { + return SoftI2C_Sim_Register(pin_data, pin_clk, addr, &g_bmp280_ops); +} +int SoftI2C_Sim_AddBME280(uint8_t pin_data, uint8_t pin_clk, uint8_t addr) { + int slot = SoftI2C_Sim_Register(pin_data, pin_clk, addr, &g_bmp280_ops); + if (slot >= 0) { + bmp280_state_t *s = (bmp280_state_t *)SoftI2C_Sim_GetCtx(slot)->user; + s->is_bme280 = true; + } + return slot; +} +int SoftI2C_Sim_AddCHT83xx(uint8_t pin_data, uint8_t pin_clk, uint8_t addr) { + return SoftI2C_Sim_Register(pin_data, pin_clk, addr, &g_cht83xx_ops); +} + +#endif // WIN32 diff --git a/src/httpserver/http_fns.c b/src/httpserver/http_fns.c index 9920e0f8b..4c0d3b33d 100644 --- a/src/httpserver/http_fns.c +++ b/src/httpserver/http_fns.c @@ -208,6 +208,39 @@ int http_fn_testmsg(http_request_t* request) { } +// START add code to show pins in use py driver w/o IORole +// array to hold possible used function names +char *pinUsedName[PLATFORM_GPIO_MAX]; + +// Function to set the string in the pinUsedName array +int setPinUsedString(int index, const char *str) { + if (index < 0 || index >= IOR_Total_Options) { + return -1; // Return an error if index is out of bounds + } + + // If setting to NULL, free existing memory + if (str == NULL) { + if (pinUsedName[index] != NULL) { + free(pinUsedName[index]); // Free the existing string + pinUsedName[index] = NULL; // Reset to NULL + } + } else { + // Allocate memory for the string and copy it into the array + // We could use MAX_STRING_LENGTH to limit the string length if needed + pinUsedName[index] = malloc((strlen(str) + 1) * sizeof(char)); // +1 for the null terminator + if (pinUsedName[index]) { + strcpy(pinUsedName[index], str); + } else { + return -1; // Allocation error + } + } + + return 0; // Success +} + + +// END add code to show pins in use py driver w/o IORole + #if ENABLE_TIME_PMNTP // poor mans NTP int http_fn_pmntp(http_request_t* request) { @@ -2977,6 +3010,17 @@ int http_fn_cfg_pins(http_request_t* request) { "d.className = \"hdiv\";" "d.innerHTML = \"\"+alias+\"\";" "f.appendChild(d);" + "var y = document.createElement(\"input\");" +// START add code to show pins in use py driver w/o IORole + "if (typeof c =='string'){" + "y.disabled = true;" + "y.value='Pin '+id+' used by '+c;" + "y.style.color='dimgray';" + "y.style.width='56%';" + "d.appendChild(y);" + "return;" + "}" +// END add code to show pins in use py driver w/o IORole "let s = document.createElement(\"select\");" "s.className = \"hele\";" "s.name = id;" @@ -2989,7 +3033,6 @@ int http_fn_cfg_pins(http_request_t* request) { " o.selected = (sr[i][1] == c);" "s.add(o);s.onchange = hide_show;" "}" - "var y = document.createElement(\"input\");" "y.className = \"hele\";" "y.name = \"r\"+id;" "y.id = \"r\"+id;" @@ -3035,7 +3078,14 @@ int http_fn_cfg_pins(http_request_t* request) { else { hprintf255(request, "P%i ", i); } - hprintf255(request, "\",%i,%i, %i,", i, si, !bCanThisPINbePWM); +// hprintf255(request, "\",%i,%i, %i,", i, si, !bCanThisPINbePWM); +// changed code to show pins in use py driver w/o IORole + hprintf255(request, "\",%i,", i); + if (pinUsedName[i]){ + hprintf255(request, "'%s',", pinUsedName[i]); + } else { + hprintf255(request, "%i, %i,", si, !bCanThisPINbePWM); + } // Primary linked channel int NofC = PIN_IOR_NofChan(si); if (NofC >= 1) diff --git a/src/obk_config.h b/src/obk_config.h index 3abb3f6e4..05531549f 100644 --- a/src/obk_config.h +++ b/src/obk_config.h @@ -200,6 +200,10 @@ #define ENABLE_DRIVER_DMX 1 #define ENABLE_DRIVER_MQTTSERVER 1 //#define ENABLE_DRIVER_ARISTON 1 +#define ENABLE_DRIVER_AHT2X 1 +#define ENABLE_DRIVER_CHT83XX 1 +#define ENABLE_DRIVER_BMP280 1 + #elif PLATFORM_BL602 @@ -672,5 +676,8 @@ #undef ENABLE_DRIVER_IR #endif +#define ENABLE_DRIVER_SHTXX 1 + + // closing OBK_CONFIG_H #endif From b12bc72450f653b571bb9b4c362ed883aef5561c Mon Sep 17 00:00:00 2001 From: maxinemuster Date: Thu, 12 Mar 2026 18:52:27 +0100 Subject: [PATCH 2/5] Fix Linux simulator to run HTTP-Server, too Mainly a wrong replacement for Sleep (sleeping seconds(!) instead of ms) Main_Init() and HTTPServer_Start() was never called (missing OBK_Start) onLinux --- src/driver/drv_main.c | 2 +- src/driver/drv_soft_i2c.c | 4 +++- src/driver/drv_soft_i2c_sim.c | 2 +- src/driver/drv_soft_i2c_sim.h | 2 +- src/driver/drv_soft_i2c_sim_sensors.c | 2 +- src/hal/win32/hal_wifi_win32.c | 9 +++++++++ src/new_common.h | 11 +++++++++++ src/win32/stubs/win_rtos_stub.c | 4 +++- src/win_main.c | 8 +++++++- 9 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c index 2855a990a..62de9829f 100644 --- a/src/driver/drv_main.c +++ b/src/driver/drv_main.c @@ -1728,7 +1728,7 @@ void DRV_Generic_Init() { #ifndef OBK_DISABLE_ALL_DRIVERS // init TIME unconditionally on start TIME_Init(); -#if WIN32 +#if WINDOWS #include "drv_soft_i2c_sim.h" //cmddetail:{"name":"Sim_AddI2Csensor","args":"[type=SHT3x/SHT4x/AHT2x/CHT83xx/BMP280] [SCL=] [SDA=] [adress= optional, try default if ommited]", //cmddetail:"descr":"Ads a pseudo sensor to the given pins", diff --git a/src/driver/drv_soft_i2c.c b/src/driver/drv_soft_i2c.c index a42da01da..bf8159193 100644 --- a/src/driver/drv_soft_i2c.c +++ b/src/driver/drv_soft_i2c.c @@ -11,7 +11,9 @@ #include "../hal/hal_pins.h" static int g_clk_period = SM2135_DELAY; -#ifndef WIN32 + +// we will define our own simulated I2C code for Windows simulator) +#ifndef WINDOWS #if !PLATFORM_ESPIDF && !PLATFORM_XR806 && !PLATFORM_XR872 && !PLATFORM_ESP8266 && !PLATFORM_REALTEK_NEW && !PLATFORM_TXW81X void usleep(int r) //delay function do 10*r nops, because rtos_delay_milliseconds is too much diff --git a/src/driver/drv_soft_i2c_sim.c b/src/driver/drv_soft_i2c_sim.c index e097d9f44..242fc90f2 100644 --- a/src/driver/drv_soft_i2c_sim.c +++ b/src/driver/drv_soft_i2c_sim.c @@ -11,7 +11,7 @@ // This file contains NO sensor-specific code. // See drv_soft_i2c_sim_sensors.c for the sensor plugins. // ------------------------------------------------------- -#ifdef WIN32 +#ifdef WINDOWS #include #include diff --git a/src/driver/drv_soft_i2c_sim.h b/src/driver/drv_soft_i2c_sim.h index ede80a9ea..5c3b414ce 100644 --- a/src/driver/drv_soft_i2c_sim.h +++ b/src/driver/drv_soft_i2c_sim.h @@ -22,7 +22,7 @@ // to [min, max], producing natural-looking sensor data. // ------------------------------------------------------- #pragma once -#ifdef WIN32 +#ifdef WINDOWS #include #include diff --git a/src/driver/drv_soft_i2c_sim_sensors.c b/src/driver/drv_soft_i2c_sim_sensors.c index 96c57804b..a5907c21f 100644 --- a/src/driver/drv_soft_i2c_sim_sensors.c +++ b/src/driver/drv_soft_i2c_sim_sensors.c @@ -18,7 +18,7 @@ // the real chip would return // // ------------------------------------------------------- -#ifdef WIN32 +#ifdef WINDOWS #include #include diff --git a/src/hal/win32/hal_wifi_win32.c b/src/hal/win32/hal_wifi_win32.c index 61ffdcb4c..97548e107 100644 --- a/src/hal/win32/hal_wifi_win32.c +++ b/src/hal/win32/hal_wifi_win32.c @@ -61,12 +61,21 @@ int WiFI_SetMacAddress(char *mac) { } void WiFI_GetMacAddress(char *mac) { +#ifndef LINUX mac[0] = 0xBA; mac[1] = 0xDA; mac[2] = 0x31; mac[3] = 0x45; mac[4] = 0xCA; mac[5] = 0xFF; +#else + mac[0] = 0xCA; + mac[1] = 0xFF; + mac[2] = 0xEE; + mac[3] = 0xCA; + mac[4] = 0xFF; + mac[5] = 0xEE; +#endif } void HAL_PrintNetworkInfo() { diff --git a/src/new_common.h b/src/new_common.h index 49c850064..5ec3d47ce 100644 --- a/src/new_common.h +++ b/src/new_common.h @@ -42,10 +42,17 @@ extern unsigned char hexbyte(const char* hex); void OTA_RequestDownloadFromHTTP(const char *s); #if WINDOWS +#ifndef LINUX #define DEVICENAME_PREFIX_FULL "WinTest" #define DEVICENAME_PREFIX_SHORT "WT" #define PLATFORM_MCU_NAME "WIN32" #define MANUFACTURER "Microsoft" +#else +#define DEVICENAME_PREFIX_FULL "LinuxSim" +#define DEVICENAME_PREFIX_SHORT "LS" +#define PLATFORM_MCU_NAME "LIN" +#define MANUFACTURER "Linux" +#endif #elif PLATFORM_XR806 #define DEVICENAME_PREFIX_FULL "OpenXR806" #define DEVICENAME_PREFIX_SHORT "oxr" @@ -207,7 +214,11 @@ This platform is not supported, error! // but it may not be set while doing a test build on developer PC #ifndef USER_SW_VER #ifdef WINDOWS +#ifndef LINUX #define USER_SW_VER "Win_Test" +#else +#define USER_SW_VER "Lin_Test" +#endif #elif PLATFORM_XR809 #define USER_SW_VER "XR809_Test" #elif PLATFORM_XR872 diff --git a/src/win32/stubs/win_rtos_stub.c b/src/win32/stubs/win_rtos_stub.c index 0118fee34..1cb9ccb32 100644 --- a/src/win32/stubs/win_rtos_stub.c +++ b/src/win32/stubs/win_rtos_stub.c @@ -16,7 +16,9 @@ #define DWORD uint #define SOCKET_ERROR SO_ERROR -#define Sleep sleep +// sleep ms, not seconds! +//#define Sleep sleep +#define Sleep(x) usleep((x*1000)) #define ioctlsocket ioctl #define closesocket close #define GETSOCKETERRNO() (errno) diff --git a/src/win_main.c b/src/win_main.c index e561eff9b..5d699c5ec 100644 --- a/src/win_main.c +++ b/src/win_main.c @@ -24,7 +24,9 @@ #include #include -#define Sleep sleep +// sleep ms, not seconds! +//#define Sleep sleep +#define Sleep(x) usleep((x*1000)) #endif @@ -620,6 +622,10 @@ int __cdecl main(int argc, char **argv) #ifndef LINUX _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF); +#else + // I still didn't figure out, where Init_Main is called in Windows binyry if g_selfTestsMode is 0 + // but we need it, so commands are registered and HTTPServer_Start() is called ... + SIM_StartOBK(0); #endif if (g_selfTestsMode) From a433734b20e3ff18c99ea70955eaf4dbccde8b87 Mon Sep 17 00:00:00 2001 From: maxinemuster Date: Thu, 12 Mar 2026 19:48:44 +0100 Subject: [PATCH 3/5] Change/Extend tokenizer names: now supports -XY 23 and XY=23 --- src/cmnds/cmd_public.h | 10 +++ src/cmnds/cmd_tokenizer.c | 116 ++++++++++++++++++-------- src/driver/drv_aht2x.c | 6 ++ src/driver/drv_cht8305.c | 8 +- src/driver/drv_shtxx.c | 12 +-- src/driver/drv_soft_i2c_sim_sensors.c | 8 +- 6 files changed, 111 insertions(+), 49 deletions(-) diff --git a/src/cmnds/cmd_public.h b/src/cmnds/cmd_public.h index da677af4b..d5e0946f0 100644 --- a/src/cmnds/cmd_public.h +++ b/src/cmnds/cmd_public.h @@ -212,6 +212,16 @@ enum LightMode { #define TOKENIZER_FORCE_SINGLE_ARGUMENT_MODE 8 #define TOKENIZER_ALLOW_ESCAPING_QUOTATIONS 16 #define TOKENIZER_EXPAND_EARLY 32 +// allow quotes in a named argument - so '"' is not at beginning or after a whitespace +// but e.g. in a string like +// mystr="that's my string" +// w/o this, it will be tokenized as +// mystr="that's +// my +// string" +// but we want the string to be one token +// we need this together with TOKENIZER_ALLOW_QUOTES ! +#define TOKENIZER_ALLOW_QUOTES_IN_NAMEDARG_VALUE (64 | TOKENIZER_ALLOW_QUOTES) // cmd_tokenizer.c int Tokenizer_GetArgsCount(); diff --git a/src/cmnds/cmd_tokenizer.c b/src/cmnds/cmd_tokenizer.c index bc890477c..2e44232cc 100644 --- a/src/cmnds/cmd_tokenizer.c +++ b/src/cmnds/cmd_tokenizer.c @@ -324,38 +324,57 @@ void expandQuotes(char* str) { } -// search for a "named" argument like channel=5 -// with Tokenizer_GetArgEqual("channel=") -// will always return char pointer, so you'll might need to convert in later -// since we can't be sure, the argument is present, we need a mandatory "default" for this function -const char *Tokenizer_GetArgEqualDefault(const char *search, const char *def) { - const char* s=NULL; - const char* arg; - const char* found=NULL; - - for (int i=0; i < g_numArgs; i++){ - arg = Tokenizer_GetArg(i); - found=NULL; -// ADDLOG_INFO(LOG_FEATURE_CMD,"Tokenizer_GetArgEqual: argument %i/%i is %s",i,argnum,arg); - - found=strstr(arg,search); - if ( arg && found) { - s=(found + strlen(search)); -// ADDLOG_INFO(LOG_FEATURE_DRV,"Tokenizer_GetArgEqual: Found %s . Value is %s",search,s); - break; - } - } - if (!s) s = def; - return s; +// "parse" arguments to search for a "named" argument: +// will accept +// - like "-port 80" +// = like "port=80" +// we can never know, if the "name" is present at all, so a default is needed to return in case its not found +const char *Tokenizer_GetArgEqualDefault(const char *name, const char *def) { + size_t name_len = strlen(name); + + for (int i = 0; i < g_numArgs; i++) { + char* p = strstr(g_args[i], name); + if (p != NULL) { + size_t arg_len = strlen(g_args[i]); + + // Pattern 1: = + // --> g_arg[i] must be "=" so + // p must point to the beginning of g_arg[i], so it p == g_args[i] + // g_args[i] must be at least two chars longer than (one is '=' + at least one for the value) + // following the there must be the char '=' --> g_args[i][name_len] == '=' + // g_args[i] must start with '-' + if (p == g_args[i] && + arg_len >= name_len + 2 && + g_args[i][name_len] == '=' + ) { + return g_args[i] + name_len + 1; + } + + // Pattern 2: - + // --> g_arg[i] must be "-" so + // g_args[i] must be exactly one char longer than + // p must point to second char, so it p > g_args[i] + // p - 1 == g_args[i] no need to test, must be the case if the first two are true + // g_args[i] must start with '-' + if (arg_len - 1 == name_len && + p > g_args[i] && +// p - 1 == g_args[i] && + g_args[i][0] == '-' + ) { + if (i + 1 < g_numArgs) { + return g_args[i + 1]; + } + } + } + } + return def; } - - // search for a "named" integer argument like channel=5 -// with Tokenizer_GetGetArgEqualInteger("SDA=") +// with Tokenizer_GetGetArgEqualInteger("channel") int Tokenizer_GetArgEqualInteger(const char *search, const int def) { int ret=def; - const char* found=Tokenizer_GetArgEqualDefault(search, "##X##"); // search for argument, default must be no number - if(strlen(found) > 2 && found[0] == '0' && (found[1] == 'x' || found[1] == 'X') ) { // also handle hex numbers (e.g. i2c addreses), at least 0x[one digit] --> strlen(found) > 2 + const char* found=Tokenizer_GetArgEqualDefault(search, NULL); // search for argument, default must be no number + if(found && found[0] == '0' && (found[1] == 'x' || found[1] == 'X') ) { // also handle hex numbers (e.g. i2c addreses) sscanf(found, "%x", &ret); return ret; } @@ -363,15 +382,14 @@ int Tokenizer_GetArgEqualInteger(const char *search, const int def) { return ret; } -// search for a "named" argument like channel=5 -// with Tokenizer_GetPinEqual("SDA=") +// search for a "named" pin argument like // will return pin or default pin int Tokenizer_GetPinEqual(const char *search, const int def) { - int ret=def, temp; - const char* found=Tokenizer_GetArgEqualDefault(search, "#x#X"); // search for argument, default must neither be a number nor a valid pin name - temp=PIN_FindIndexFromString(found); // will check for number and pin names - if (temp != -1) ret=temp; - return ret; + const char* found=Tokenizer_GetArgEqualDefault(search, NULL); // search for argument, default must neither be a number nor a valid pin name + if (found == NULL) return def; + int temp = PIN_FindIndexFromString(found); // will check for number and pin names + if (temp != -1) return temp; + return def; } void Tokenizer_TokenizeString(const char *s, int flags) { @@ -435,6 +453,34 @@ void Tokenizer_TokenizeString(const char *s, int flags) { g_numArgs++; } } + // special handling to allow quotes with named arguments like + // myarg="that's my arg" + // in this case, the quote is "inside" the arg (no whitespace before the '"' - but a '=') + // + // We will pretend it's -myarg "that's my arg" + // by shifting the argument one char to the right and prepend '-' + // --> so if we had + // myarg="that's my arg" + // we now have + // -myarg"thats my arg" + // + // and immediately jump to "quote:" + // This works, because in "quote" first code will change the '"' to '\0' so we have the "name" string terminated by \0 + // Then the next arg is started after the " (turned to \0 before) and continued until the closing '"' + // + // we know p[1] must be present, even if string is ending, it will be there as '\0', so it's safe to compare p[1] to '"' + if ((flags & TOKENIZER_ALLOW_QUOTES_IN_NAMEDARG_VALUE) == TOKENIZER_ALLOW_QUOTES_IN_NAMEDARG_VALUE && *p == '=' && p[1] == '"'){ + + // shift the nem one to the left ... + for (int j = p - g_args[g_numArgs-1]; j > 0; j--){ + g_args[g_numArgs-1][j]=g_args[g_numArgs-1][j-1]; + } + // ... and make first char a '-' + g_args[g_numArgs-1][0]='-'; + // forward to '"' (*p was '=') + p=p+1; + goto quote; + } //if(*p == ',') { // *p = 0; // g_args[g_numArgs] = p+1; diff --git a/src/driver/drv_aht2x.c b/src/driver/drv_aht2x.c index dcae532be..3e1f07c9a 100644 --- a/src/driver/drv_aht2x.c +++ b/src/driver/drv_aht2x.c @@ -188,6 +188,12 @@ void AHT2X_Init() channel_temp = Tokenizer_GetArgIntegerDefault(3, -1); channel_humid = Tokenizer_GetArgIntegerDefault(4, -1); + // test, if extended arguments are present + g_softI2C.pin_clk = Tokenizer_GetPinEqual("SCL", g_softI2C.pin_clk); + g_softI2C.pin_data = Tokenizer_GetPinEqual("SDA", g_softI2C.pin_data); + channel_temp = Tokenizer_GetArgEqualInteger("chan_t", channel_temp); + channel_humid = Tokenizer_GetArgEqualInteger("chan_h", channel_humid); + Soft_I2C_PreInit(&g_softI2C); rtos_delay_milliseconds(100); setPinUsedString(g_softI2C.pin_clk, "AHT2X SCL"); diff --git a/src/driver/drv_cht8305.c b/src/driver/drv_cht8305.c index d1c674405..531a244a6 100644 --- a/src/driver/drv_cht8305.c +++ b/src/driver/drv_cht8305.c @@ -212,10 +212,10 @@ void CHT83XX_Init() channel_humid = g_cfg.pins.channels2[g_softI2C.pin_data]; // if cli data is given, use instead of pin cfg page - g_softI2C.pin_clk = Tokenizer_GetPinEqual("SCL=", g_softI2C.pin_clk); - g_softI2C.pin_data = Tokenizer_GetPinEqual("SDA=", g_softI2C.pin_data); - channel_temp = Tokenizer_GetArgEqualInteger("chan_t=", channel_temp); - channel_humid = Tokenizer_GetArgEqualInteger("chan_h=", channel_humid); + g_softI2C.pin_clk = Tokenizer_GetPinEqual("SCL", g_softI2C.pin_clk); + g_softI2C.pin_data = Tokenizer_GetPinEqual("SDA", g_softI2C.pin_data); + channel_temp = Tokenizer_GetArgEqualInteger("chan_t", channel_temp); + channel_humid = Tokenizer_GetArgEqualInteger("chan_h", channel_humid); setPinUsedString(g_softI2C.pin_clk, "CHT83XX SCL"); diff --git a/src/driver/drv_shtxx.c b/src/driver/drv_shtxx.c index 76c9184d8..79cf11d91 100644 --- a/src/driver/drv_shtxx.c +++ b/src/driver/drv_shtxx.c @@ -595,24 +595,24 @@ void SHTXX_Init() dev->channel_humid = g_cfg.pins.channels2[dev->i2c.pin_data]; } - dev->i2c.pin_clk = Tokenizer_GetPinEqual("SCL=", dev->i2c.pin_clk); - dev->i2c.pin_data = Tokenizer_GetPinEqual("SDA=", dev->i2c.pin_data); - dev->channel_temp = Tokenizer_GetArgEqualInteger("chan_t=", dev->channel_temp); - dev->channel_humid = Tokenizer_GetArgEqualInteger("chan_h=", dev->channel_humid); + dev->i2c.pin_clk = Tokenizer_GetPinEqual("SCL", dev->i2c.pin_clk); + dev->i2c.pin_data = Tokenizer_GetPinEqual("SDA", dev->i2c.pin_data); + dev->channel_temp = Tokenizer_GetArgEqualInteger("chan_t", dev->channel_temp); + dev->channel_humid = Tokenizer_GetArgEqualInteger("chan_h", dev->channel_humid); dev->secondsBetween = 10; dev->secondsUntilNext = 1; dev->calTemp = 0; dev->calHum = 0; - int typeArg = Tokenizer_GetArgEqualInteger("type=", 3); + int typeArg = Tokenizer_GetArgEqualInteger("type", 3); dev->typeIdx = (typeArg == 4) ? SHTXX_TYPE_SHT4X : SHTXX_TYPE_SHT3X; // Address: parse "0x45" without strtol (saves ~400B if not already in fw). // Use A/B suffix for backward compat: address=A → 0x44, address=B → 0x45. dev->i2cAddr = SHTXX_I2C_ADDR; if(dev->typeIdx == SHTXX_TYPE_SHT3X) { - uint8_t A = (int8_t)(Tokenizer_GetArgEqualInteger("address=", 0x44)); + uint8_t A = (int8_t)(Tokenizer_GetArgEqualInteger("address", 0x44)); if (A != SHTXX_I2C_ADDR) dev->i2cAddr = A << 1; } diff --git a/src/driver/drv_soft_i2c_sim_sensors.c b/src/driver/drv_soft_i2c_sim_sensors.c index a5907c21f..71015c936 100644 --- a/src/driver/drv_soft_i2c_sim_sensors.c +++ b/src/driver/drv_soft_i2c_sim_sensors.c @@ -588,9 +588,9 @@ commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, c uint8_t pin_data=9, pin_clk=17; - pin_clk = (uint8_t)Tokenizer_GetPinEqual("SCL=", pin_clk); - pin_data = (uint8_t)Tokenizer_GetPinEqual("SDA=", pin_data); - const char *type = Tokenizer_GetArgEqualDefault("type=","NO"); + pin_clk = (uint8_t)Tokenizer_GetPinEqual("SCL", pin_clk); + pin_data = (uint8_t)Tokenizer_GetPinEqual("SDA", pin_data); + const char *type = Tokenizer_GetArgEqualDefault("type","NO"); uint8_t def_addr,addr; sim_sensor_ops_t *sens_ops; if (!strcmp(type,"NO")){ @@ -622,7 +622,7 @@ commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, c return CMD_RES_BAD_ARGUMENT; } } - uint8_t A = (int8_t)(Tokenizer_GetArgEqualInteger("address=", 0)); + uint8_t A = (int8_t)(Tokenizer_GetArgEqualInteger("address", 0)); if (A != 0){ addr = A << 1; } else { From 5a0ceb3041426c85e3388e0c312af4b826d2853d Mon Sep 17 00:00:00 2001 From: maxinemuster Date: Mon, 30 Mar 2026 18:31:00 +0200 Subject: [PATCH 4/5] add VEML7700 --- src/driver/drv_soft_i2c_sim.h | 3 +- src/driver/drv_soft_i2c_sim_sensors.c | 176 +++++++++++++++++++++++++- 2 files changed, 174 insertions(+), 5 deletions(-) diff --git a/src/driver/drv_soft_i2c_sim.h b/src/driver/drv_soft_i2c_sim.h index 5c3b414ce..fe1dca266 100644 --- a/src/driver/drv_soft_i2c_sim.h +++ b/src/driver/drv_soft_i2c_sim.h @@ -43,7 +43,8 @@ typedef enum { SIM_Q_PRESSURE = 2, // x10 -> 10132 = 1013.2 hPa SIM_Q_CO2 = 3, // x1 -> 412 ppm SIM_Q_ALTITUDE = 4, // x10 -> 1234 = 123.4 m - SIM_Q_COUNT // sentinel – keep last + SIM_Q_LIGHT = 5, // x10 -> 3000 = 300.0 lux + SIM_Q_COUNT // sentinel – keep last } sim_quantity_t; // ------------------------------------------------------- diff --git a/src/driver/drv_soft_i2c_sim_sensors.c b/src/driver/drv_soft_i2c_sim_sensors.c index 71015c936..10a4776aa 100644 --- a/src/driver/drv_soft_i2c_sim_sensors.c +++ b/src/driver/drv_soft_i2c_sim_sensors.c @@ -12,6 +12,8 @@ // BME280 – Bosch BST-BME280-DS002 (2018) // CHT8305/8310/8315 – Sensylink CHT83xx Datasheet // +// VEML7700 - Vishay VEML7700 Datasheet Rev. 1.8, 28-Nov-2024 (Doc 84286) +// // Each plugin: // init() – seed default value ranges // encode_response() – produce the exact byte sequence @@ -41,6 +43,7 @@ // =================================================================== // CRC-8/NRSC-5 poly=0x31 init=0xFF (Sensirion convention, all sensors) +/* static uint8_t crc8_sensirion(uint8_t a, uint8_t b) { uint8_t crc = 0xFF ^ a; for (int i = 0; i < 8; i++) crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); @@ -48,12 +51,25 @@ static uint8_t crc8_sensirion(uint8_t a, uint8_t b) { for (int i = 0; i < 8; i++) crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); return crc; } - +*/ +static uint8_t crc8(const uint8_t *data, size_t len) { + uint8_t crc = 0xFF; + + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); + } + } + + return crc; +} // Pack a 3-byte Sensirion word [MSB, LSB, CRC] into buf static void pack_word(uint8_t *buf, uint16_t raw) { buf[0] = (uint8_t)(raw >> 8); buf[1] = (uint8_t)(raw & 0xFF); - buf[2] = crc8_sensirion(buf[0], buf[1]); +// buf[2] = crc8_sensirion(buf[0], buf[1]); + buf[2] = crc8(&buf[0],2); } // Pack two Sensirion words (T then H) = 6 bytes @@ -82,7 +98,8 @@ static void pack_th(uint8_t *resp, uint16_t raw_t, uint16_t raw_h) { // 0x3066 heater on // 0x3098 heater off // 0xF32D read status register -// 0x3780 read serial number +// 0x3780 read serial number - clock stretch +// 0x3682 read serial number - no clock stretch // // Response (6 bytes): [T_MSB T_LSB T_CRC H_MSB H_LSB H_CRC] // @@ -119,7 +136,7 @@ static bool sht3x_encode(sim_ctx_t *ctx) { // Periodic fetch if (c0 == 0xE0 && c1 == 0x00) { sht3x_build_meas(ctx); return true; } // Serial number → 6 bytes (two CRC-valid fake words) - if (c0 == 0x37) { + if ((c0 == 0x36 && c1 == 0x82) || c0 == 0x37 && c1 == 0x80) { pack_word(ctx->resp, 0xDEAD); pack_word(ctx->resp+3, 0xBEEF); ctx->resp_len = 6; return true; } @@ -249,6 +266,11 @@ static bool aht2x_encode(sim_ctx_t *ctx) { } switch (ctx->cmd[0]) { + case 0x71: // Status + if (s->calibrated) ctx->resp[0] = 0x18; + else ctx->resp[0] = 0x00; ctx->resp_len = 1; + return true; + case 0xBA: // soft reset → uncalibrated s->calibrated = false; ctx->resp[0] = 0x00; ctx->resp_len = 1; @@ -273,8 +295,18 @@ static bool aht2x_encode(sim_ctx_t *ctx) { ctx->resp[4] = (uint8_t)((raw_t >> 8) & 0xFF); ctx->resp[5] = (uint8_t)( raw_t & 0xFF); ctx->resp_len = 6; + + // Calculate CRC over the 6 measurement bytes + uint8_t crc = crc8(&ctx->resp[0], 6); + ctx->resp[6] = crc; + ctx->resp_len = 7; // 6 data bytes + 1 CRC byte + + printf("[SIM][AHT2x] T=%d.%d C H=%d.%d%% raw_T=0x%05X raw_H=0x%05X CRC=0x%02X\n", + t10/10, abs(t10%10), h10/10, h10%10, raw_t, raw_h, crc); +/* printf("[SIM][AHT2x] T=%d.%d C H=%d.%d%% raw_T=0x%05X raw_H=0x%05X\n", t10/10, abs(t10%10), h10/10, h10%10, raw_t, raw_h); +*/ return true; } default: @@ -583,6 +615,136 @@ static bool cht83xx_encode(sim_ctx_t *ctx) { static const sim_sensor_ops_t g_cht83xx_ops = { .name = "CHT83xx", .init = cht83xx_init, .encode_response = cht83xx_encode, }; + +// =================================================================== +// VEML7700 plugin +// =================================================================== +// Reference: Vishay VEML7700 Datasheet Rev. 1.8, 28-Nov-2024 (Doc 84286) +// +// I2C address: 7-bit = 0x10, 8-bit wire = 0x20 (write) / 0x21 (read) +// +// Protocol (datasheet Fig. 3 / Fig. 4): +// Write: S | 0x20 | A | cmd | A | LSB | A | MSB | A | P +// Read: S | 0x20 | A | cmd | A | S | 0x21 | A | LSB | A | MSB | N | P +// All 16-bit registers are transmitted LSB first. +// +// Register map (datasheet table p.7): +// 0x00 ALS_CONF R/W gain[12:11], IT[9:6], INT_EN[1], SD[0] +// 0x01 ALS_WH R/W high threshold +// 0x02 ALS_WL R/W low threshold +// 0x03 PSM R/W power saving mode +// 0x04 ALS R 16-bit ALS output +// 0x05 WHITE R 16-bit WHITE output +// 0x06 ALS_INT R interrupt status +// 0x07 ID R [15:8]=0xC4 (addr 0x20), [7:0]=0x81 +// +// ALS_CONF default (POR) = 0x0001 → ALS_SD=1 (shutdown) +// Driver writes 0x0000 to power on with gain x1, IT 100ms. +// +// Lux conversion at default settings (gain x1, IT 100ms): +// lux = raw_ALS * 0.0042 * 2 * 8 = raw_ALS * 0.0672 +// Inverse: raw_ALS = lux / 0.0672 = lux * 10000 / 672 = lux * 125 / 84 +// +// SIM_Q_LIGHT is in x10 units: 3000 = 300.0 lux +// raw_ALS = lux10 * 125 / 84 (integer arithmetic, < 0.1% error) +// raw_WHITE ≈ raw_ALS * 11 / 10 (WHITE is ~10% higher than ALS) +// =================================================================== + +typedef struct { + uint8_t reg; // last written command/register address + uint16_t conf; // shadow of ALS_CONF (to echo back writes) + uint16_t wh; // high threshold shadow + uint16_t wl; // low threshold shadow +} veml7700_state_t; + +static void veml7700_init(sim_ctx_t *ctx) { + // Indoor office default: 300 lux, drifts between 50 and 5000 lux + SoftI2C_Sim_SetValue(ctx, SIM_Q_LIGHT, 3000, 500, 50000, 200); + veml7700_state_t *s = (veml7700_state_t *)malloc(sizeof(veml7700_state_t)); + s->reg = 0x04; // after power-on read, most drivers go straight for ALS + s->conf = 0x0001; // POR default: shutdown + s->wh = 0xFFFF; + s->wl = 0x0000; + ctx->user = s; +} + +static bool veml7700_encode(sim_ctx_t *ctx) { + veml7700_state_t *s = (veml7700_state_t *)ctx->user; + if (!s) return false; + + // A write transaction sets the register pointer and optionally + // writes data bytes (16-bit, LSB first). + // cmd[0] = command code, cmd[1]=LSB, cmd[2]=MSB (if present) + if (ctx->cmd_len >= 1) { + s->reg = ctx->cmd[0]; + if (ctx->cmd_len >= 3) { + // Data write: shadow the register + uint16_t val = (uint16_t)((ctx->cmd[2] << 8) | ctx->cmd[1]); + switch (s->reg) { + case 0x00: s->conf = val; break; + case 0x01: s->wh = val; break; + case 0x02: s->wl = val; break; + default: break; + } + ctx->resp_len = 0; // write-only transaction, no read data queued + return true; + } + } + + // Now serve the read (the master will issue a repeated-start after setting reg) + uint16_t val = 0; + switch (s->reg) { + case 0x00: // ALS_CONF – echo back the shadow + val = s->conf; + break; + case 0x01: // ALS_WH + val = s->wh; + break; + case 0x02: // ALS_WL + val = s->wl; + break; + case 0x04: { // ALS – advance light value, convert to raw counts + int32_t lux10 = SoftI2C_Sim_NextValue(ctx, SIM_Q_LIGHT); + uint32_t raw_als = (uint32_t)((int64_t)lux10 * 125 / 84); + if (raw_als > 0xFFFF) raw_als = 0xFFFF; + val = (uint16_t)raw_als; + printf("[SIM][VEML7700] Lux=%d.%d raw_ALS=0x%04X", + (int)(lux10/10), (int)abs(lux10%10), val); + break; + } + case 0x05: { // WHITE – peek same cycle, add ~10% + int32_t lux10 = SoftI2C_Sim_PeekValue(ctx, SIM_Q_LIGHT); + uint32_t raw_als = (uint32_t)((int64_t)lux10 * 125 / 84); + uint32_t raw_w = raw_als * 11 / 10; // WHITE ~10% above ALS + if (raw_w > 0xFFFF) raw_w = 0xFFFF; + val = (uint16_t)raw_w; + break; + } + case 0x06: // ALS_INT – no interrupt pending + val = 0x0000; + break; + case 0x07: // ID – low byte 0x81 (fixed), high byte 0xC4 (addr 0x20) + val = (uint16_t)((0xC4u << 8) | 0x81u); + break; + default: + val = 0x0000; + break; + } + + // Return LSB first (datasheet Fig. 4) + ctx->resp[0] = (uint8_t)(val & 0xFF); + ctx->resp[1] = (uint8_t)(val >> 8); + ctx->resp_len = 2; + return true; +} + +static const sim_sensor_ops_t g_veml7700_ops = { + .name = "VEML7700", .init = veml7700_init, .encode_response = veml7700_encode, +}; + + + + commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, const char* args, int cmdFlags) { Tokenizer_TokenizeString(args, 0); @@ -617,6 +779,10 @@ commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, c // printf("Detected: BMP280\n"); sens_ops = &g_bmp280_ops; def_addr = 0x76 << 1; // to be dicussed, what is "default" + } else if (wal_stricmp(type, "VEML7700") == 0 ) { +// printf("Detected: VEML7700\n"); + sens_ops = &g_veml7700_ops; + def_addr = 0x10 << 1; } else { ADDLOG_ERROR(LOG_FEATURE_SENSOR, "Unknown sensor type %s.",type); return CMD_RES_BAD_ARGUMENT; @@ -650,6 +816,8 @@ commandResult_t CMD_SoftI2C_simAddSensor(const void* context, const char* cmd, c } + + // =================================================================== // Convenience registration functions // =================================================================== From 938a50676a3cd8b5ecc68efc9f5a12105db6d58e Mon Sep 17 00:00:00 2001 From: maxinemuster Date: Mon, 30 Mar 2026 19:01:54 +0200 Subject: [PATCH 5/5] Added veml7700 driver and xhtxx driver (both selected for simulator) --- openBeken_win32_mvsc2017.vcxproj | 8 +- platforms/obk_main.cmake | 2 + platforms/obk_main.mk | 2 + src/driver/drv_main.c | 35 + src/driver/drv_veml7700.c | 356 ++++++++++ src/driver/drv_veml7700.h | 60 ++ src/driver/drv_xhtxx.c | 1100 ++++++++++++++++++++++++++++++ src/driver/drv_xhtxx.h | 113 +++ src/obk_config.h | 5 +- 9 files changed, 1677 insertions(+), 4 deletions(-) create mode 100644 src/driver/drv_veml7700.c create mode 100644 src/driver/drv_veml7700.h create mode 100644 src/driver/drv_xhtxx.c create mode 100644 src/driver/drv_xhtxx.h diff --git a/openBeken_win32_mvsc2017.vcxproj b/openBeken_win32_mvsc2017.vcxproj index 821d62eb7..21d1d0cc6 100644 --- a/openBeken_win32_mvsc2017.vcxproj +++ b/openBeken_win32_mvsc2017.vcxproj @@ -268,6 +268,8 @@ + + @@ -973,9 +975,9 @@ - - - + + + diff --git a/platforms/obk_main.cmake b/platforms/obk_main.cmake index 65b607dae..b41d20af1 100644 --- a/platforms/obk_main.cmake +++ b/platforms/obk_main.cmake @@ -124,6 +124,8 @@ set(OBKM_SRC ${OBK_SRCS}driver/drv_shiftRegister.c ${OBK_SRCS}driver/drv_sht3x.c ${OBK_SRCS}driver/drv_shtxx.c + ${OBK_SRCS}driver/drv_xhtxx.c + ${OBK_SRCS}driver/drv_veml7700.c ${OBK_SRCS}driver/drv_sm2135.c ${OBK_SRCS}driver/drv_sm2235.c ${OBK_SRCS}driver/drv_soft_i2c.c diff --git a/platforms/obk_main.mk b/platforms/obk_main.mk index cc15642f6..b7881c87b 100644 --- a/platforms/obk_main.mk +++ b/platforms/obk_main.mk @@ -144,6 +144,8 @@ OBKM_SRC += $(OBK_SRCS)driver/drv_sgp.c OBKM_SRC += $(OBK_SRCS)driver/drv_shiftRegister.c OBKM_SRC += $(OBK_SRCS)driver/drv_sht3x.c OBKM_SRC += $(OBK_SRCS)driver/drv_shtxx.c +OBKM_SRC += $(OBK_SRCS)driver/drv_xhtxx.c +OBKM_SRC += $(OBK_SRCS)driver/drv_veml7700.c OBKM_SRC += $(OBK_SRCS)driver/drv_sm15155e.c OBKM_SRC += $(OBK_SRCS)driver/drv_sm16703P.c OBKM_SRC += $(OBK_SRCS)driver/drv_simpleEEPROM.c diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c index 75039c6dd..6a494208e 100644 --- a/src/driver/drv_main.c +++ b/src/driver/drv_main.c @@ -24,6 +24,9 @@ #include "drv_hlw8112.h" #include "drv_DCF77.h" #include "drv_shtxx.h" +#include "drv_xhtxx.h" +#include "drv_veml7700.h" + void DRV_MQTTServer_Init(); void DRV_MQTTServer_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState); @@ -1240,6 +1243,38 @@ static driver_t g_drivers[] = { false, // loaded }, #endif +#if ENABLE_DRIVER_XHTXX + //drvdetail:{"name":"XHTXX", + //drvdetail:"title":"TODO", + //drvdetail:"descr":"Humidity/temperature sensor. For SHT3x/SHT4x/GXV4 ...", + //drvdetail:"requires":""} + { "XHTXX", // Driver Name + XHTXX_Init, // Init + XHTXX_OnEverySecond, // onEverySecond + XHTXX_AppendInformationToHTTPIndexPage, // appendInformationToHTTPIndexPage + NULL, // runQuickTick + XHTXX_StopDriver, // stopFunction + NULL, // onChannelChanged + NULL, // onHassDiscovery + false, // loaded + }, +#endif +#if ENABLE_DRIVER_VEML7700 + //drvdetail:{"name":"VEML7700", + //drvdetail:"title":"TODO", + //drvdetail:"descr":"Ambient Light Sensor. Testing for unknown sensor on 0x20", + //drvdetail:"requires":""} + { "VEML7700", // Driver Name + VEML7700_Init, // Init + VEML7700_OnEverySecond, // onEverySecond + VEML7700_AppendInformationToHTTPIndexPage, // appendInformationToHTTPIndexPage + NULL, // runQuickTick + NULL, // stopFunction + NULL, // onChannelChanged + NULL, // onHassDiscovery + false, // loaded + }, +#endif #if ENABLE_DRIVER_SGP //drvdetail:{"name":"SGP", //drvdetail:"title":"TODO", diff --git a/src/driver/drv_veml7700.c b/src/driver/drv_veml7700.c new file mode 100644 index 000000000..8e1a044c1 --- /dev/null +++ b/src/driver/drv_veml7700.c @@ -0,0 +1,356 @@ +// drv_veml7700.c +// VEML7700 High Accuracy Ambient Light Sensor driver +// Reference: Vishay VEML7700 Datasheet Rev. 1.8, 28-Nov-2024 (Doc 84286) +// +// Protocol (datasheet Fig. 9): +// All registers are 16-bit, addressed by a 1-byte command code (0x00-0x07). +// Write: S | addr_W | A | cmd | A | LSB | A | MSB | A | P +// Read: S | addr_W | A | cmd | A | S | addr_R | A | LSB | A | MSB | N | P +// All 16-bit values are transmitted LSB first on the wire. +// +// Lux formula (datasheet base resolution 0.0042 lx/ct at gain x2, IT 800ms): +// lux = raw_ALS * 0.0042 * gain_factor * it_factor +// Gain factors: x1→×2, x2→×1, x1/8→×16, x1/4→×8 +// IT factors: 800ms→×1, 400ms→×2, 200ms→×4, 100ms→×8, 50ms→×16, 25ms→×32 +// Default (gain x1, IT 100ms): lux = raw_ALS * 0.0672 +// +// startDriver VEML7700 [SCL=pin] [SDA=pin] [chan_lux=ch] [chan_white=ch] +// chan_lux stores lux*10 (0.1 lux resolution) +// chan_white stores raw WHITE count + + +// My personal extensions, to tokenizer and PIN cfg gui not (yet?) merged +// can be enabled here + + +#define TOKENIZER_EXT 0 +#define PINUSED_EXT 0 + + +#include "../obk_config.h" + +#if ENABLE_DRIVER_VEML7700 + +#include "../new_pins.h" +#include "../new_cfg.h" +#include "../cmnds/cmd_public.h" +#include "../mqtt/new_mqtt.h" +#include "../logging/logging.h" +#include "drv_local.h" +#include "drv_uart.h" +#include "../httpserver/new_http.h" +#include "../hal/hal_pins.h" +#include "drv_veml7700.h" + +// Single sensor instance (VEML7700 has a fixed I2C address, so one per bus) +static veml7700_dev_t g_dev; + +// ------------------------------------------------------- +// Low-level I2C helpers +// ------------------------------------------------------- + +// Write a 16-bit register (LSB first on wire, datasheet Fig. 3) +static void VEML7700_WriteReg(veml7700_dev_t *dev, uint8_t cmd, uint16_t val) +{ + Soft_I2C_Start(&dev->i2c, VEML7700_I2C_ADDR); + Soft_I2C_WriteByte(&dev->i2c, cmd); + Soft_I2C_WriteByte(&dev->i2c, (uint8_t)(val & 0xFF)); // LSB first + Soft_I2C_WriteByte(&dev->i2c, (uint8_t)((val >> 8) & 0xFF)); // then MSB + Soft_I2C_Stop(&dev->i2c); +} + +// Read a 16-bit register (LSB first on wire, datasheet Fig. 4) +static uint16_t VEML7700_ReadReg(veml7700_dev_t *dev, uint8_t cmd) +{ + uint8_t lo, hi; + Soft_I2C_Start(&dev->i2c, VEML7700_I2C_ADDR); + Soft_I2C_WriteByte(&dev->i2c, cmd); + Soft_I2C_Stop(&dev->i2c); + + Soft_I2C_Start(&dev->i2c, VEML7700_I2C_ADDR | 1); + lo = Soft_I2C_ReadByte(&dev->i2c, true); // ACK – more bytes follow + hi = Soft_I2C_ReadByte(&dev->i2c, false); // NACK – last byte + Soft_I2C_Stop(&dev->i2c); + + return ((uint16_t)hi << 8) | lo; +} + +// ------------------------------------------------------- +// Lux calculation +// Base resolution 0.0042 lx/ct at gain x2, IT 800ms. +// Other settings: multiply by gain_factor * it_factor. +// gain_mul10000[]: (0.0042 * gain_factor) * 10000, indexed by gain_sel [12:11] +// gain_sel 00 (x1) → gain_factor 2 → 0.0042*2 = 0.0084 → *10000 = 84 +// gain_sel 01 (x2) → gain_factor 1 → 0.0042*1 = 0.0042 → *10000 = 42 +// gain_sel 10 (x1/8) → gain_factor 16 → 0.0042*16 = 0.0672 → *10000 = 672 +// gain_sel 11 (x1/4) → gain_factor 8 → 0.0042*8 = 0.0336 → *10000 = 336 +// it_fac: relative to 800ms=×1; each halving of IT doubles the factor. +// ------------------------------------------------------- +static float VEML7700_CalcLux(veml7700_dev_t *dev, uint16_t raw) +{ + static const uint16_t gain_mul10000[] = { 84, 42, 672, 336 }; + uint8_t gain_sel = (uint8_t)((dev->conf >> 11) & 0x03u); + uint8_t it_sel = (uint8_t)((dev->conf >> 6) & 0x0Fu); + + uint8_t it_fac; + switch (it_sel) { + case 0x00: it_fac = 8; break; // 100 ms + case 0x01: it_fac = 4; break; // 200 ms + case 0x02: it_fac = 2; break; // 400 ms + case 0x03: it_fac = 1; break; // 800 ms + case 0x08: it_fac = 16; break; // 50 ms + case 0x0C: it_fac = 32; break; // 25 ms + default: it_fac = 8; break; // unknown → treat as 100 ms + } + + return (float)raw * (float)((uint32_t)gain_mul10000[gain_sel] * it_fac) / 10000.0f; +} + +// ------------------------------------------------------- +// Initialization – sets dev->isWorking (mirrors SHTXX_Initialization) +// ------------------------------------------------------- +static void VEML7700_Initialization(veml7700_dev_t *dev) +{ + // Power on with gain x1, IT 100ms. + // POR value is 0x0001 (ALS_SD=1, shutdown), so we must write 0x0000. + dev->conf = VEML7700_GAIN_x1 | VEML7700_IT_100MS; // = 0x0000 + VEML7700_WriteReg(dev, VEML7700_REG_ALS_CONF, dev->conf); + rtos_delay_milliseconds(5); + + // Verify device ID – low byte must be 0x81 (datasheet Table 8) + uint16_t id = VEML7700_ReadReg(dev, VEML7700_REG_ID); + dev->isWorking = ((id & 0xFF) == VEML7700_DEVICE_ID_LO); + + ADDLOG_INFO(LOG_FEATURE_SENSOR, + "VEML7700 (SDA=%i SCL=%i): Init %s (ID=0x%04X).", + dev->i2c.pin_data, dev->i2c.pin_clk, + dev->isWorking ? "ok" : "failed", id); +} + +// ------------------------------------------------------- +// Measure + store – mirrors SHTXX_ConvertAndStore +// ------------------------------------------------------- +static void VEML7700_Measure(veml7700_dev_t *dev) +{ + dev->rawALS = VEML7700_ReadReg(dev, VEML7700_REG_ALS); + dev->rawWhite = VEML7700_ReadReg(dev, VEML7700_REG_WHITE); + dev->lux = VEML7700_CalcLux(dev, dev->rawALS); + + // Store lux*10 so 0.1 lux resolution is preserved in the channel integer + if(dev->channel_lux >= 0) CHANNEL_Set(dev->channel_lux, (int)(dev->lux * 10.0f), 0); + if(dev->channel_white >= 0) CHANNEL_Set(dev->channel_white, dev->rawWhite, 0); + + ADDLOG_INFO(LOG_FEATURE_SENSOR, + "VEML7700 (SDA=%i SCL=%i): Lux=%.2f ALS=%u WHITE=%u", + dev->i2c.pin_data, dev->i2c.pin_clk, + dev->lux, dev->rawALS, dev->rawWhite); +} + +// ------------------------------------------------------- +// Command handlers – all take (context, cmd, args, flags) +// context is always the veml7700_dev_t pointer (same as SHT pattern) +// ------------------------------------------------------- + +// VEML7700_ALS [gain] [it] +// Gain: 0=x1 1=x2 2=x1/8 3=x1/4 +// IT: 0=100ms 1=200ms 2=400ms 3=800ms 8=50ms 12=25ms +// VEML7700: ALS gain and integration time. Gain: 0=x1 1=x2 2=x1/8 3=x1/4. IT: 0=100ms 1=200ms 2=400ms 3=800ms 8=50ms 12=25ms +commandResult_t VEML7700_CMD_ALS(const void *context, const char *cmd, + const char *args, int flags) +{ + veml7700_dev_t *dev = (veml7700_dev_t *)context; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 2)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + + uint8_t gain = (uint8_t)(Tokenizer_GetArgInteger(0) & 0x03); + uint8_t it = (uint8_t)(Tokenizer_GetArgInteger(1) & 0x0F); + + // Preserve INT_EN[1], SD[0] and ALS_PERS[5:4]; replace gain[12:11] and IT[9:6] + dev->conf = (uint16_t)((dev->conf & 0xE83Fu) | ((uint16_t)gain << 11) | ((uint16_t)it << 6)); + VEML7700_WriteReg(dev, VEML7700_REG_ALS_CONF, dev->conf); + return CMD_RES_OK; +} + +// VEML7700_INT [enable] [als_low] [als_high] [persist] +// enable=0/1, als_low/als_high=raw thresholds (0-65535), persist=0..3 (1/2/4/8 samples) +// VEML7700 Interrupt config. enable=0/1. als_low/als_high raw thresholds (0-65535). persist=0..3 (1/2/4/8 samples) +commandResult_t VEML7700_CMD_INT(const void *context, const char *cmd, + const char *args, int flags) +{ + veml7700_dev_t *dev = (veml7700_dev_t *)context; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 4)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + + int en = Tokenizer_GetArgInteger(0); + int lo = Tokenizer_GetArgInteger(1); + int hi = Tokenizer_GetArgInteger(2); + int persist = Tokenizer_GetArgInteger(3) & 0x03; + + VEML7700_WriteReg(dev, VEML7700_REG_ALS_WL, (uint16_t)lo); + VEML7700_WriteReg(dev, VEML7700_REG_ALS_WH, (uint16_t)hi); + + // ALS_PERS lives in ALS_CONF[5:4]; INT_EN in ALS_CONF[1] + dev->conf &= ~(VEML7700_CONF_INT_EN | (uint16_t)(0x03u << 4)); + dev->conf |= (uint16_t)((uint16_t)(persist & 0x03) << 4); + if(en) dev->conf |= VEML7700_CONF_INT_EN; + VEML7700_WriteReg(dev, VEML7700_REG_ALS_CONF, dev->conf); + return CMD_RES_OK; +} + +// VEML7700_Cycle [seconds] +// VEML7700 Measurement interval in seconds (min 1, max 255) +commandResult_t VEML7700_CMD_Cycle(const void *context, const char *cmd, + const char *args, int flags) +{ + veml7700_dev_t *dev = (veml7700_dev_t *)context; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + int s = Tokenizer_GetArgInteger(0); + if(s < 1) { ADDLOG_INFO(LOG_FEATURE_CMD, "VEML7700: Min 1s."); return CMD_RES_BAD_ARGUMENT; } + dev->secondsBetween = (uint8_t)s; + return CMD_RES_OK; +} + +// VEML7700_Measure – immediate on-demand read +// VEML7700: Trigger an immediate ALS + WHITE measurement +commandResult_t VEML7700_CMD_Measure(const void *context, const char *cmd, + const char *args, int flags) +{ + veml7700_dev_t *dev = (veml7700_dev_t *)context; + dev->secondsUntilNext = dev->secondsBetween; + VEML7700_Measure(dev); + return CMD_RES_OK; +} + +// VEML7700_Reinit – soft reinitialise +// VEML7700: Re-run sensor initialisation (power-on and ID check) +commandResult_t VEML7700_CMD_Reinit(const void *context, const char *cmd, + const char *args, int flags) +{ + veml7700_dev_t *dev = (veml7700_dev_t *)context; + dev->secondsUntilNext = dev->secondsBetween; + VEML7700_Initialization(dev); + return CMD_RES_OK; +} + +// ------------------------------------------------------- +// Public entry points +// ------------------------------------------------------- + +// startDriver VEML7700 [SCL=pin] [SDA=pin] [chan_lux=ch] [chan_white=ch] +// Named-arg style is identical to SHTXX_Init(). +void VEML7700_Init(void) +{ + veml7700_dev_t *dev = &g_dev; + + // Default pins + dev->i2c.pin_clk = 9; + dev->i2c.pin_data = 14; + dev->channel_lux = -1; + dev->channel_white = -1; + +/* + // Role-based pin lookup (first sensor only, same guard as SHT) + // - not implemented - no IORoles (yet) + dev->i2c.pin_clk = PIN_FindPinIndexForRole(IOR_VEML7700_CLK, dev->i2c.pin_clk); + dev->i2c.pin_data = PIN_FindPinIndexForRole(IOR_VEML7700_DAT, dev->i2c.pin_data); + dev->channel_lux = g_cfg.pins.channels [dev->i2c.pin_data]; + dev->channel_white = g_cfg.pins.channels2[dev->i2c.pin_data]; +*/ + + dev->i2c.pin_clk = Tokenizer_GetPin(1, dev->i2c.pin_clk); + dev->i2c.pin_data = Tokenizer_GetPin(2, dev->i2c.pin_data); + + dev->channel_lux = Tokenizer_GetArgIntegerDefault(3, dev->channel_lux); + dev->channel_white = Tokenizer_GetArgIntegerDefault(4, dev->channel_white); + +#if TOKENIZER_EXT + // My personal extensions, not (yet?) merged + + // Named-arg overrides (startDriver line arguments) + // test, if extended arguments are present + dev->i2c.pin_clk = Tokenizer_GetPinEqual("SCL", dev->i2c.pin_clk); + dev->i2c.pin_data = Tokenizer_GetPinEqual("SDA", dev->i2c.pin_data); + dev->channel_lux = Tokenizer_GetArgEqualInteger("chan_lux", dev->channel_lux); + dev->channel_white = Tokenizer_GetArgEqualInteger("chan_white", dev->channel_white); +#endif + +#if PINUSED_EXT + // My personal extensions, not (yet?) merged + setPinUsedString(dev->i2c.pin_clk, "VEML7700 SCL"); + setPinUsedString(dev->i2c.pin_data, "VEML7700 SDA"); +#endif + + + dev->secondsBetween = 10; + dev->secondsUntilNext = 1; + + Soft_I2C_PreInit(&dev->i2c); + rtos_delay_milliseconds(5); // allow supply to settle after bus init + + VEML7700_Initialization(dev); + + //cmddetail:{"name":"VEML7700_ALS","args":"[gain] [it]", + //cmddetail:"descr":"ALS gain and integration time. Gain: 0=x1 1=x2 2=x1/8 3=x1/4. IT: 0=100ms 1=200ms 2=400ms 3=800ms 8=50ms 12=25ms.", + //cmddetail:"fn":"VEML7700_CMD_ALS","file":"driver/drv_veml7700.c","requires":"", + //cmddetail:"examples":"VEML7700_ALS 1 3"} + CMD_RegisterCommand("VEML7700_ALS", VEML7700_CMD_ALS, dev); + + //cmddetail:{"name":"VEML7700_INT","args":"[enable] [als_low] [als_high] [persist]", + //cmddetail:"descr":"Interrupt config. enable=0/1. als_low/als_high raw thresholds. persist=0..3.", + //cmddetail:"fn":"VEML7700_CMD_INT","file":"driver/drv_veml7700.c","requires":"", + //cmddetail:"examples":"VEML7700_INT 1 500 8000 1"} + CMD_RegisterCommand("VEML7700_INT", VEML7700_CMD_INT, dev); + + //cmddetail:{"name":"VEML7700_Cycle","args":"[IntervalSeconds]", + //cmddetail:"descr":"Measurement interval in seconds (min 1).", + //cmddetail:"fn":"VEML7700_CMD_Cycle","file":"driver/drv_veml7700.c","requires":"", + //cmddetail:"examples":"VEML7700_Cycle 10"} + CMD_RegisterCommand("VEML7700_Cycle", VEML7700_CMD_Cycle, dev); + + //cmddetail:{"name":"VEML7700_Measure","args":"", + //cmddetail:"descr":"Trigger an immediate ALS + WHITE measurement.", + //cmddetail:"fn":"VEML7700_CMD_Measure","file":"driver/drv_veml7700.c","requires":"", + //cmddetail:"examples":"VEML7700_Measure"} + CMD_RegisterCommand("VEML7700_Measure", VEML7700_CMD_Measure, dev); + + //cmddetail:{"name":"VEML7700_Reinit","args":"", + //cmddetail:"descr":"Re-run sensor initialisation (power-on and ID check).", + //cmddetail:"fn":"VEML7700_CMD_Reinit","file":"driver/drv_veml7700.c","requires":"", + //cmddetail:"examples":"VEML7700_Reinit"} + CMD_RegisterCommand("VEML7700_Reinit", VEML7700_CMD_Reinit, dev); +} + +// Put sensor into shutdown on driver stop (mirrors SHTXX_StopDriver reset) +void VEML7700_StopDriver(void) +{ + veml7700_dev_t *dev = &g_dev; + VEML7700_WriteReg(dev, VEML7700_REG_ALS_CONF, VEML7700_CONF_SD); +} + +void VEML7700_OnEverySecond(void) +{ + veml7700_dev_t *dev = &g_dev; + if(dev->secondsUntilNext == 0) + { + if(dev->isWorking) + VEML7700_Measure(dev); + dev->secondsUntilNext = dev->secondsBetween; + } + else + { + dev->secondsUntilNext--; + } +} + +void VEML7700_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState) +{ + if(bPreState) return; + veml7700_dev_t *dev = &g_dev; + if(!dev->isWorking) + hprintf255(request, "VEML7700 not detected"); + else + hprintf255(request, "

VEML7700 Lux=%.2f WHITE=%u

", dev->lux, dev->rawWhite); +} + +#endif // ENABLE_DRIVER_VEML7700 diff --git a/src/driver/drv_veml7700.h b/src/driver/drv_veml7700.h new file mode 100644 index 000000000..6ff4b30a4 --- /dev/null +++ b/src/driver/drv_veml7700.h @@ -0,0 +1,60 @@ +// drv_veml7700.h +// VEML7700 High Accuracy Ambient Light Sensor driver +// Reference: Vishay VEML7700 Datasheet Rev. 1.8, 28-Nov-2024 (Doc 84286) +#ifndef DRV_VEML7700_H +#define DRV_VEML7700_H + +// I2C address: 7-bit = 0x10, 8-bit wire = 0x20 write / 0x21 read +#define VEML7700_I2C_ADDR (0x10 << 1) // = 0x20 + +// Command codes (register addresses, datasheet p.7) +#define VEML7700_REG_ALS_CONF 0x00 // Configuration R/W +#define VEML7700_REG_ALS_WH 0x01 // High threshold window R/W +#define VEML7700_REG_ALS_WL 0x02 // Low threshold window R/W +#define VEML7700_REG_PSM 0x03 // Power saving mode R/W +#define VEML7700_REG_ALS 0x04 // ALS output data R +#define VEML7700_REG_WHITE 0x05 // WHITE channel output R +#define VEML7700_REG_ALS_INT 0x06 // Interrupt status R +#define VEML7700_REG_ID 0x07 // Device ID R + +// ALS_CONF bit fields (register 0x00, datasheet Table 1) +#define VEML7700_CONF_SD (1u << 0) // Shutdown: 0=power on, 1=shutdown (POR default) +#define VEML7700_CONF_INT_EN (1u << 1) // Interrupt enable +// ALS_GAIN [12:11] +#define VEML7700_GAIN_x1 (0u << 11) +#define VEML7700_GAIN_x2 (1u << 11) +#define VEML7700_GAIN_x1_8 (2u << 11) +#define VEML7700_GAIN_x1_4 (3u << 11) +// ALS_IT [9:6]: integration time +#define VEML7700_IT_25MS (0x0Cu << 6) +#define VEML7700_IT_50MS (0x08u << 6) +#define VEML7700_IT_100MS (0x00u << 6) // default +#define VEML7700_IT_200MS (0x01u << 6) +#define VEML7700_IT_400MS (0x02u << 6) +#define VEML7700_IT_800MS (0x03u << 6) + +// Device ID (register 0x07, datasheet Table 8) +// Low byte = 0x81 (fixed device ID code) +// High byte = 0xC4 for slave address 0x20 +#define VEML7700_DEVICE_ID_LO 0x81 + +// Device state – one instance per sensor (matches shtxx_dev_t pattern) +typedef struct { + softI2C_t i2c; + int channel_lux; // output channel for lux*10 (-1 = unused) + int channel_white; // output channel for raw WHITE count (-1 = unused) + uint16_t conf; // shadow of ALS_CONF register + uint16_t rawALS; // last raw ALS reading + uint16_t rawWhite; // last raw WHITE reading + float lux; // last calculated lux value + bool isWorking; + uint8_t secondsBetween; + uint8_t secondsUntilNext; +} veml7700_dev_t; + +void VEML7700_Init(void); +void VEML7700_StopDriver(void); +void VEML7700_OnEverySecond(void); +void VEML7700_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState); + +#endif // DRV_VEML7700_H diff --git a/src/driver/drv_xhtxx.c b/src/driver/drv_xhtxx.c new file mode 100644 index 000000000..69173283e --- /dev/null +++ b/src/driver/drv_xhtxx.c @@ -0,0 +1,1100 @@ +// drv_xhtxx.c – SHT3x/SHT4x (Sensirion), AHT2x (Aosong), CHT83xx (Sensylink) +// +// Bugs fixed vs original driver: +// [1] AHT2x probe: status mask 0x68→0x08 (bit 3 only, per AHT20 datasheet §5.4) +// [2] AHT2x probe: read status first; only send 0xBE if calibration bit clear +// [3] AHT2x measure: read 7 bytes, verify CRC (was 6 bytes, no CRC) +// [4] AHT2x zero guard: raw_h=0 is valid 0%RH; warn-only, don't discard +// [5] CHT831x temperature UB: clean int16_t>>3 sign extension +// [6] CHT831x one-shot: 2-byte write, not 3 +// [7] SHT3x periodic fetch: missing func-name arg (compile error) +// [8] Cycle/Force/Reinit: accept optional [sensorN] like all other commands +// [9] SHT4x humidity: clamp before int16_t cast to prevent underflow +// [10] CHT83xx probe: reject 0xFFFF (floating bus) as well as 0x0000 +// [11] CHT831x temperature formula: factor-10 error; (s13*50+8)/16 → (s13*5+8)/16 + +#include "../new_pins.h" +#include "../new_cfg.h" +#include "../cmnds/cmd_public.h" +#include "../mqtt/new_mqtt.h" +#include "../logging/logging.h" +#include "drv_local.h" +#include "drv_uart.h" +#include "../httpserver/new_http.h" +#include "../hal/hal_pins.h" +#include "drv_xhtxx.h" +#include "../obk_config.h" + +#if ENABLE_DRIVER_XHTXX + +#define CMD_UNUSED (void)context; (void)cmdFlags + +static xhtxx_dev_t g_sensors[XHTXX_MAX_SENSORS]; +static uint8_t g_numSensors = 0; + +// ----------------------------------------------------------------------- +// Forward declarations +// ----------------------------------------------------------------------- +/* +static bool XHTXX_SHT3x_Probe (xhtxx_dev_t *dev); +static void XHTXX_SHT3x_Init (xhtxx_dev_t *dev); +static void XHTXX_SHT3x_Measure(xhtxx_dev_t *dev); +static void XHTXX_SHT3x_Reset (xhtxx_dev_t *dev); +static bool XHTXX_SHT4x_Probe (xhtxx_dev_t *dev); +static void XHTXX_SHT4x_Init (xhtxx_dev_t *dev); +static void XHTXX_SHT4x_Measure(xhtxx_dev_t *dev); +static void XHTXX_SHT4x_Reset (xhtxx_dev_t *dev); +*/ + +static bool XHTXX_SHT_UnifiedProbe (xhtxx_dev_t *dev); +static void XHTXX_SHT_UnifiedInit (xhtxx_dev_t *dev); +static void XHTXX_SHT_UnifiedMeasure(xhtxx_dev_t *dev); +static void XHTXX_SHT_UnifiedReset (xhtxx_dev_t *dev); +static bool XHTXX_AHT2x_Probe (xhtxx_dev_t *dev); +static void XHTXX_AHT2x_Init (xhtxx_dev_t *dev); +static void XHTXX_AHT2x_Measure(xhtxx_dev_t *dev); +static void XHTXX_AHT2x_Reset (xhtxx_dev_t *dev); +static bool XHTXX_CHT83xx_Probe (xhtxx_dev_t *dev); +static void XHTXX_CHT83xx_Init (xhtxx_dev_t *dev); +static void XHTXX_CHT83xx_Measure(xhtxx_dev_t *dev); +static void XHTXX_CHT83xx_Reset (xhtxx_dev_t *dev); + +/* +// old version, below with "unified" code for SHT3x/SHT4x +static const xhtxx_family_t g_families[XHTXX_FAMILY_COUNT] = { + [XHTXX_FAMILY_AUTO] = { NULL, NULL, NULL, NULL, "auto", 0 }, + [XHTXX_FAMILY_SHT3X] = { XHTXX_SHT3x_Probe, XHTXX_SHT3x_Init, XHTXX_SHT3x_Measure, XHTXX_SHT3x_Reset, "SHT3x", XHTXX_ADDR_SHT }, + [XHTXX_FAMILY_SHT4X] = { XHTXX_SHT4x_Probe, XHTXX_SHT4x_Init, XHTXX_SHT4x_Measure, XHTXX_SHT4x_Reset, "SHT4x", XHTXX_ADDR_SHT }, + [XHTXX_FAMILY_AHT2X] = { XHTXX_AHT2x_Probe, XHTXX_AHT2x_Init, XHTXX_AHT2x_Measure, XHTXX_AHT2x_Reset, "AHT2x", XHTXX_ADDR_AHT2X }, + [XHTXX_FAMILY_CHT83XX] = { XHTXX_CHT83xx_Probe, XHTXX_CHT83xx_Init, XHTXX_CHT83xx_Measure, XHTXX_CHT83xx_Reset, "CHT83xx", XHTXX_ADDR_CHT83XX }, +}; +*/ +static const xhtxx_family_t g_families[XHTXX_FAMILY_COUNT] = { + [XHTXX_FAMILY_AUTO] = { NULL, NULL, NULL, NULL, "auto", 0 }, + [XHTXX_FAMILY_SHT3X] = { XHTXX_SHT_UnifiedProbe, XHTXX_SHT_UnifiedInit, XHTXX_SHT_UnifiedMeasure, XHTXX_SHT_UnifiedReset, "SHT3x", XHTXX_ADDR_SHT }, + [XHTXX_FAMILY_SHT4X] = { XHTXX_SHT_UnifiedProbe, XHTXX_SHT_UnifiedInit, XHTXX_SHT_UnifiedMeasure, XHTXX_SHT_UnifiedReset, "SHT4x", XHTXX_ADDR_SHT }, + [XHTXX_FAMILY_AHT2X] = { XHTXX_AHT2x_Probe, XHTXX_AHT2x_Init, XHTXX_AHT2x_Measure, XHTXX_AHT2x_Reset, "AHT2x", XHTXX_ADDR_AHT2X }, + [XHTXX_FAMILY_CHT83XX] = { XHTXX_CHT83xx_Probe, XHTXX_CHT83xx_Init, XHTXX_CHT83xx_Measure, XHTXX_CHT83xx_Reset, "CHT83xx", XHTXX_ADDR_CHT83XX }, +}; + +static const struct { uint8_t family; uint8_t addr; } g_probeOrder[] = { + { XHTXX_FAMILY_SHT4X, XHTXX_ADDR_SHT }, + { XHTXX_FAMILY_SHT3X, XHTXX_ADDR_SHT }, + { XHTXX_FAMILY_SHT3X, XHTXX_ADDR_SHT_ALT }, + { XHTXX_FAMILY_AHT2X, XHTXX_ADDR_AHT2X }, + { XHTXX_FAMILY_CHT83XX, XHTXX_ADDR_CHT83XX }, +}; +#define XHTXX_PROBE_STEPS (sizeof(g_probeOrder)/sizeof(g_probeOrder[0])) + +// ----------------------------------------------------------------------- +// I²C primitives — same structure as original, no extra call layers +// ----------------------------------------------------------------------- +static void I2C_Write(xhtxx_dev_t *dev, uint8_t b0, uint8_t b1, uint8_t b2, uint8_t n) +{ + Soft_I2C_Start(&dev->i2c, dev->i2cAddr); + Soft_I2C_WriteByte(&dev->i2c, b0); + if(n >= 2) Soft_I2C_WriteByte(&dev->i2c, b1); + if(n >= 3) Soft_I2C_WriteByte(&dev->i2c, b2); + Soft_I2C_Stop(&dev->i2c); +} +static void I2C_Read(xhtxx_dev_t *dev, uint8_t *buf, uint8_t n) +{ + Soft_I2C_Start(&dev->i2c, dev->i2cAddr | 1); + Soft_I2C_ReadBytes(&dev->i2c, buf, n); + Soft_I2C_Stop(&dev->i2c); +} +static void I2C_ReadReg(xhtxx_dev_t *dev, uint8_t reg, uint8_t *buf, uint8_t n, uint8_t del) +{ + I2C_Write(dev, reg, 0, 0, 1); + rtos_delay_milliseconds(del); + I2C_Read(dev, buf, n); +} + +// ----------------------------------------------------------------------- +// CRC-8 (poly=0x31, init=0xFF) — returns computed value, works for any n. +// Verify: XHTXX_CRC8(data, n) == expected +// Compute: uint8_t crc = XHTXX_CRC8(data, n) +// ----------------------------------------------------------------------- +static uint8_t XHTXX_CRC8(const uint8_t *data, uint8_t n) +{ + uint8_t crc = 0xFF; + while(n--) { + crc ^= *data++; + for(uint8_t b = 0; b < 8; b++) + crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1); + } + return crc; +} + +// ----------------------------------------------------------------------- +// Shared helpers +// ----------------------------------------------------------------------- +static void XHTXX_StoreAndLog(xhtxx_dev_t *dev, int16_t t10, int16_t h10) +{ + if(h10 < 0) h10 = 0; + if(h10 > 1000) h10 = 1000; + dev->lastTemp = t10; + dev->lastHumid = h10; + if(dev->channel_temp >= 0) CHANNEL_Set(dev->channel_temp, t10, 0); + if(dev->channel_humid >= 0) CHANNEL_Set(dev->channel_humid, h10, 0); + int16_t tf = t10 % 10; if(tf < 0) tf = -tf; + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX %s (SDA=%i): T=%d.%d°C H=%d.%d%%", + g_families[dev->familyIdx].name, dev->i2c.pin_data, + t10/10, tf, h10/10, h10%10); +} + +static xhtxx_dev_t *XHTXX_GetSensor(const char *cmd, int argIdx, bool present, uint8_t def_family) +{ + int usesensor=-1; + if(!present){ + if (def_family == 0) { + usesensor = 0; + } + else for (int i=0; i< (int)g_numSensors; i++) { // return first sensor of this family + if (g_sensors[i].familyIdx == def_family) { + usesensor = i; + break; + } + } + if (usesensor>-1){ + ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: No sensor provided. Using %s[%i]", cmd, g_families[g_sensors[usesensor].familyIdx].name, usesensor+1); + return &g_sensors[usesensor]; + } + } + int n = Tokenizer_GetArgInteger(argIdx); + if(n < 1 || n > (int)g_numSensors) { + ADDLOG_ERROR(LOG_FEATURE_SENSOR, "%s: sensor %i out of range (1..%i)", cmd, n, g_numSensors); + return NULL; + } + return &g_sensors[n - 1]; +} + +// ----------------------------------------------------------------------- +// SHT shared: send command, wait, read 6 bytes, verify both CRCs. +// ----------------------------------------------------------------------- +static bool XHTXX_SHT_CmdRead6(xhtxx_dev_t *dev, uint8_t b0, uint8_t b1, + uint8_t cmdlen, uint8_t dly_ms, uint8_t *out, + const char *func) +{ + I2C_Write(dev, b0, b1, 0, cmdlen); + if(dly_ms) rtos_delay_milliseconds(dly_ms); + I2C_Read(dev, out, 6); + if(XHTXX_CRC8(&out[0], 2) != out[2] || XHTXX_CRC8(&out[3], 2) != out[5]) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: CRC (SDA=%i)", func, dev->i2c.pin_data); + return false; + } + return true; +} + + +// Replacement for XHTXX_SHT3x_ConvertStore and XHTXX_SHT4x_ConvertStore +static void XHTXX_SHT_UnifiedConvertStore(xhtxx_dev_t *dev, const uint8_t *d) +{ + uint16_t raw_t = ((uint16_t)d[0] << 8) | d[1]; + uint16_t raw_h = ((uint16_t)d[3] << 8) | d[4]; + + // Common Temperature: T = -45 + 175 * raw/65535 + // Optimized: (1750 * raw + 32768) >> 16 + int16_t t10 = (int16_t)((1750u * (uint32_t)raw_t + 32768u) >> 16) - 450 + dev->calTemp; + + int16_t h10; + if (dev->familyIdx == XHTXX_FAMILY_SHT4X) { + // SHT4x Humidity: H = -6 + 125 * raw/65535 + // Optimized: (1250 * raw + 32768) >> 16 + int32_t h_raw = (int32_t)((1250u * (uint32_t)raw_h + 32768u) >> 16) - 60; + h10 = (int16_t)h_raw; // StoreAndLog handles the 0-100% clamping + } else { + // SHT3x Humidity: H = 100 * raw/65535 + // Optimized: (1000 * raw + 32768) >> 16 + h10 = (int16_t)((1000u * (uint32_t)raw_h + 32768u) >> 16); + } + + XHTXX_StoreAndLog(dev, t10, h10 + dev->calHum); +} + +// Consolidated Measure for both SHT3x and SHT4x +static void XHTXX_SHT_UnifiedMeasure(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + // SHT3x: 0x2400 (2 bytes), 16ms | SHT4x: 0xFD (1 byte), 10ms + uint8_t cmd = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 0x24 : 0xFD; + uint8_t len = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 2 : 1; + uint8_t dly = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 16 : 10; + + if(!XHTXX_SHT_CmdRead6(dev, cmd, 0x00, len, dly, d, "SHT_Meas")) return; + XHTXX_SHT_UnifiedConvertStore(dev, d); +} + +// Unified Reset handler +static void XHTXX_SHT_UnifiedReset(xhtxx_dev_t *dev) +{ + if (dev->familyIdx == XHTXX_FAMILY_SHT3X) { + I2C_Write(dev, 0x30, 0xA2, 0, 2); + } else { + I2C_Write(dev, 0x94, 0, 0, 1); + } + rtos_delay_milliseconds(2); +} + +// Unified Serial/Probe for SHT3x/SHT4x +static bool XHTXX_SHT_UnifiedProbe(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + // SHT3x: 0x3682 (2 bytes) | SHT4x: 0x89 (1 byte) + uint8_t cmd = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 0x36 : 0x89; + uint8_t sub = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 0x82 : 0x00; + uint8_t len = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 2 : 1; + + if(!XHTXX_SHT_CmdRead6(dev, cmd, sub, len, 2, d, "SHT_Probe")) return false; +#ifdef XHTXX_ENABLE_SERIAL_LOG + dev->serial = ((uint32_t)d[0]<<24)|((uint32_t)d[1]<<16)|((uint32_t)d[3]<<8)|d[4]; +#endif + return true; +} + +// Unified Init for SHT3x/SHT4x +static void XHTXX_SHT_UnifiedInit(xhtxx_dev_t *dev) +{ +#ifdef XHTXX_ENABLE_SERIAL_LOG + if(!dev->serial) XHTXX_SHT_UnifiedProbe(dev); +#endif + // Perform the specific reset for this family + XHTXX_SHT_UnifiedReset(dev); + + // Perform first measurement to confirm "isWorking" + uint8_t d[6]; + uint8_t cmd = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 0x24 : 0xFD; + uint8_t len = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 2 : 1; + uint8_t dly = (dev->familyIdx == XHTXX_FAMILY_SHT3X) ? 16 : 10; + + dev->isWorking = XHTXX_SHT_CmdRead6(dev, cmd, 0x00, len, dly, d, "SHT_Init"); + if(dev->isWorking) XHTXX_SHT_UnifiedConvertStore(dev, d); + + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX %s %s (SDA=%i)", + g_families[dev->familyIdx].name, + dev->isWorking ? "ok" : "FAILED", dev->i2c.pin_data); +} +// ----------------------------------------------------------------------- +// SHT3x — T = -45+175×raw/65535, H = 100×raw/65535 +// ----------------------------------------------------------------------- +/* +static void XHTXX_SHT3x_ConvertStore(xhtxx_dev_t *dev, const uint8_t *d) +{ + uint16_t raw_t = ((uint16_t)d[0] << 8) | d[1]; + uint16_t raw_h = ((uint16_t)d[3] << 8) | d[4]; + // Optimized: Use >> 16 instead of / 65535 for smaller code size + // Dividing by 65536 (216) instead of 65535 introduces an error of approximately 0.0015%. + // For a temperature reading of 100∘C, the error is 0.0015°C, + // (far below the sensor's physical tolerance of approx ±0.2∘C.) + //int16_t t10 = (int16_t)((1750u * raw_t + 32767u) / 65535u) - 450 + dev->calTemp; + //int16_t h10 = (int16_t)((1000u * (uint32_t)raw_h + 32767u) / 65535u) + dev->calHum; + int16_t t10 = (int16_t)((1750u * (uint32_t)raw_t + 32768u) >> 16) - 450 + dev->calTemp; + int16_t h10 = (int16_t)((1000u * (uint32_t)raw_h + 32768u) >> 16) + (int16_t)dev->calHum; + + XHTXX_StoreAndLog(dev, t10, h10); +} +*/ +/* +static bool XHTXX_SHT3x_Probe(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + if(!XHTXX_SHT_CmdRead6(dev, 0x36, 0x82, 2, 2, d, "SHT3x_Probe")) return false; +#ifdef XHTXX_ENABLE_SERIAL_LOG + dev->serial = ((uint32_t)d[0]<<24)|((uint32_t)d[1]<<16)|((uint32_t)d[3]<<8)|d[4]; +#endif + return true; +} +static void XHTXX_SHT3x_Init(xhtxx_dev_t *dev) +{ +#ifdef XHTXX_ENABLE_SERIAL_LOG + if(!dev->serial) XHTXX_SHT3x_Probe(dev); +#endif + I2C_Write(dev, 0x30, 0xA2, 0, 2); + rtos_delay_milliseconds(2); + uint8_t d[6]; + dev->isWorking = XHTXX_SHT_CmdRead6(dev, 0x24, 0x00, 2, 16, d, "SHT3x_Init"); + //if(dev->isWorking) XHTXX_SHT3x_ConvertStore(dev, d); + if(dev->isWorking) XHTXX_SHT_UnifiedConvertStore(dev, d); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX SHT3x %s (SDA=%i addr=0x%02X)", + dev->isWorking ? "ok" : "FAILED", dev->i2c.pin_data, dev->i2cAddr>>1); +} +*/ +/* +// now consolidated wit SHT4x +static void XHTXX_SHT3x_Measure(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + if(!XHTXX_SHT_CmdRead6(dev, 0x24, 0x00, 2, 16, d, "SHT3x")) return; + //XHTXX_SHT3x_ConvertStore(dev, d); + XHTXX_SHT_UnifiedConvertStore(dev, d); +} +static void XHTXX_SHT3x_Reset(xhtxx_dev_t *dev) +{ + I2C_Write(dev, 0x30, 0xA2, 0, 2); + rtos_delay_milliseconds(2); +} +*/ + +/* +// ----------------------------------------------------------------------- +// SHT4x — T = -45+175×raw/65535, H = -6+125×raw/65535 +// ----------------------------------------------------------------------- +static void XHTXX_SHT4x_ConvertStore(xhtxx_dev_t *dev, const uint8_t *d) +{ + uint16_t raw_t = ((uint16_t)d[0] << 8) | d[1]; + uint16_t raw_h = ((uint16_t)d[3] << 8) | d[4]; + // Optimized: Use >> 16 instead of / 65535 for smaller code size + // Dividing by 65536 (216) instead of 65535 introduces an error of approximately 0.0015%. + // For a temperature reading of 100∘C, the error is 0.0015°C, + // (far below the sensor's physical tolerance of approx ±0.2∘C.) + //int16_t t10 = (int16_t)((1750u * raw_t + 32767u) / 65535u) - 450 + dev->calTemp; + // int32_t h_raw = (int32_t)((1250u * (uint32_t)raw_h + 32767u) / 65535u) - 60 + dev->calHum; + int16_t t10 = (int16_t)((1750u * (uint32_t)raw_t + 32768u) >> 16) - 450 + dev->calTemp; + int16_t h10 = (int16_t)((1250u * (uint32_t)raw_h + 32768u) >> 16) - 60 + (int16_t)dev->calHum; + int16_t h10 = (h_raw < 0) ? 0 : (h_raw > 1000) ? 1000 : (int16_t)h_raw; + XHTXX_StoreAndLog(dev, t10, h10); +} +*/ +/* +static bool XHTXX_SHT4x_Probe(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + if(!XHTXX_SHT_CmdRead6(dev, 0x89, 0x00, 1, 2, d, "SHT4x_Probe")) return false; +#ifdef XHTXX_ENABLE_SERIAL_LOG + dev->serial = ((uint32_t)d[0]<<24)|((uint32_t)d[1]<<16)|((uint32_t)d[3]<<8)|d[4]; +#endif + return true; +} +static void XHTXX_SHT4x_Init(xhtxx_dev_t *dev) +{ +#ifdef XHTXX_ENABLE_SERIAL_LOG + if(!dev->serial) XHTXX_SHT4x_Probe(dev); +#endif + I2C_Write(dev, 0x94, 0, 0, 1); + rtos_delay_milliseconds(1); + uint8_t d[6]; + dev->isWorking = XHTXX_SHT_CmdRead6(dev, 0xFD, 0x00, 1, 10, d, "SHT4x_Init"); + //if(dev->isWorking) XHTXX_SHT4x_ConvertStore(dev, d); + if(dev->isWorking) XHTXX_SHT_UnifiedConvertStore(dev, d); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX SHT4x %s (SDA=%i)", + dev->isWorking ? "ok" : "FAILED", dev->i2c.pin_data); +} +*/ +/* +// now consolidated wit SHT3x +static void XHTXX_SHT4x_Measure(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + if(!XHTXX_SHT_CmdRead6(dev, 0xFD, 0x00, 1, 10, d, "SHT4x")) return; + //XHTXX_SHT4x_ConvertStore(dev, d); + XHTXX_SHT_UnifiedConvertStore(dev, d); +} +static void XHTXX_SHT4x_Reset(xhtxx_dev_t *dev) +{ + I2C_Write(dev, 0x94, 0, 0, 1); + rtos_delay_milliseconds(1); +} +*/ +// ----------------------------------------------------------------------- +// AHT2x (AHT20/21/25) +// Datasheet startup: wait 40ms, read status 0x71, init 0xBE only if cal=0. +// Measurement: 0xAC 0x33 0x00 → poll busy → read 7 bytes → check CRC. +// ----------------------------------------------------------------------- +static bool XHTXX_AHT2x_Probe(xhtxx_dev_t *dev) +{ + rtos_delay_milliseconds(40); + uint8_t s; + I2C_ReadReg(dev, 0x71, &s, 1, 1); + if(s == 0xFF) return false; // bus floating — no device + if(!(s & 0x08)) { // cal bit clear → send init + I2C_Write(dev, 0xBE, 0x08, 0x00, 3); + rtos_delay_milliseconds(10); + I2C_ReadReg(dev, 0x71, &s, 1, 1); + } + return (s != 0xFF) && (s & 0x08); // [1] bit 3 only, not 0x68 +} +static void XHTXX_AHT2x_Init(xhtxx_dev_t *dev) +{ + dev->isWorking = XHTXX_AHT2x_Probe(dev); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX AHT2x %s (SDA=%i)", + dev->isWorking ? "ok" : "FAILED", dev->i2c.pin_data); +} +static void XHTXX_AHT2x_Measure(xhtxx_dev_t *dev) +{ + I2C_Write(dev, 0xAC, 0x33, 0x00, 3); + rtos_delay_milliseconds(80); + + uint8_t data[7] = { 0 }; + for(uint8_t i = 0; i < 10; i++) { + I2C_Read(dev, data, 7); + if(!(data[0] & 0x80)) break; + ADDLOG_DEBUG(LOG_FEATURE_SENSOR, "XHTXX AHT2x busy (%ims)", (i+1)*20); + rtos_delay_milliseconds(20); + } + if(data[0] & 0x80) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX AHT2x timed out (SDA=%i)", dev->i2c.pin_data); + return; + } + if(XHTXX_CRC8(data, 6) != data[6]) { // [3] verify 7th-byte CRC + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX AHT2x CRC fail (SDA=%i)", dev->i2c.pin_data); + return; + } + uint32_t raw_h = ((uint32_t)data[1]<<12) | ((uint32_t)data[2]<<4) | (data[3]>>4); + uint32_t raw_t = ((uint32_t)(data[3]&0x0F)<<16) | ((uint32_t)data[4]<<8) | data[5]; + // Optimized: AHT2x uses 2^20 scale. Rounding constant is 2^19 (524288) + + //int16_t h10 = (int16_t)((raw_h * 1000u + 524288u) / 1048576u) + dev->calHum; + //int16_t t10 = (int16_t)((raw_t * 2000u + 524288u) / 1048576u) - 500 + dev->calTemp; + int16_t t10 = (int16_t)((2000u * (raw_t & 0xFFFFF) + 524288u) >> 20) - 500 + dev->calTemp; + int16_t h10 = (int16_t)((raw_h * 1000u + 524288u) >> 20) + (int16_t)dev->calHum; + XHTXX_StoreAndLog(dev, t10, h10); +} +static void XHTXX_AHT2x_Reset(xhtxx_dev_t *dev) +{ + I2C_Write(dev, 0xBA, 0, 0, 1); + rtos_delay_milliseconds(20); +} + +// ----------------------------------------------------------------------- +// CHT83xx (CHT8305 / CHT8310 / CHT8315) +// ----------------------------------------------------------------------- +#define CHT_REG_TEMP 0x00 +#define CHT_REG_HUM 0x01 +#define CHT_REG_STATUS 0x02 +#define CHT_REG_CFG 0x03 +#define CHT_REG_ONESHOT 0x0F +#define IS_CHT831X(dev) ((dev)->subtype == 0x8215u || (dev)->subtype == 0x8315u) + +static bool XHTXX_CHT83xx_Probe(xhtxx_dev_t *dev) +{ + uint8_t buf[2]; + I2C_ReadReg(dev, 0xFE, buf, 2, 10); + uint16_t mfr = ((uint16_t)buf[0] << 8) | buf[1]; + if(mfr == 0x0000u || mfr == 0xFFFFu) return false; // [10] also reject float-high + I2C_ReadReg(dev, 0xFF, buf, 2, 10); + uint16_t dev_id = ((uint16_t)buf[0] << 8) | buf[1]; + if(dev_id == 0xFFFFu) return false; + dev->subtype = dev_id; + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX CHT83xx mfr=%04X dev=%04X", mfr, dev_id); + return true; +} +static void XHTXX_CHT83xx_Init(xhtxx_dev_t *dev) +{ + if(IS_CHT831X(dev)) { + uint8_t status; + I2C_ReadReg(dev, CHT_REG_STATUS, &status, 1, 10); + if(status) + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX CHT831x wake status: 0x%02X", status); + I2C_Write(dev, CHT_REG_CFG, 0x48, 0x80, 3); + } + const char *v = IS_CHT831X(dev) ? (dev->subtype==0x8215u ? "CHT8310":"CHT8315") : "CHT8305"; + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX %s init (SDA=%i)", v, dev->i2c.pin_data); + dev->isWorking = true; +} +static void XHTXX_CHT83xx_Measure(xhtxx_dev_t *dev) +{ + if(IS_CHT831X(dev)) + I2C_Write(dev, CHT_REG_ONESHOT, 0x00, 0, 2); // [6] 2-byte write + rtos_delay_milliseconds(20); + + uint8_t buf[4]; + I2C_ReadReg(dev, CHT_REG_TEMP, buf, 4, 10); + + int16_t t10, h10; + if(IS_CHT831X(dev)) { + // Re-read humidity separately to avoid parity issues in burst read + I2C_ReadReg(dev, CHT_REG_HUM, buf+2, 2, 10); + // [5] clean int16_t sign extension; [11] correct formula: ×5/16 not ×50/16 + int16_t s13 = (int16_t)(((uint16_t)buf[0]<<8)|buf[1]) >> 3; + t10 = (int16_t)((s13 * 5 + 8) / 16) + dev->calTemp; + uint16_t rh = ((uint16_t)buf[2]<<8) | buf[3]; + h10 = (int16_t)(((rh & 0x7FFFu) * 1000u + 16384u) / 32768u) + dev->calHum; + } else { + uint16_t raw_t = ((uint16_t)buf[0]<<8) | buf[1]; + uint16_t raw_h = ((uint16_t)buf[2]<<8) | buf[3]; + // [10] guard against bus-idle reads + if(raw_t == 0xFFFFu && raw_h == 0xFFFFu) return; + t10 = (int16_t)((raw_t * 1650u + 32767u) / 65535u) - 400 + dev->calTemp; + h10 = (int16_t)((raw_h * 1000u + 32767u) / 65535u) + dev->calHum; + } + XHTXX_StoreAndLog(dev, t10, h10); +} +static void XHTXX_CHT83xx_Reset(xhtxx_dev_t *dev) +{ + if(IS_CHT831X(dev)) + I2C_Write(dev, CHT_REG_CFG, 0x08, 0x80, 3); +} + +// ----------------------------------------------------------------------- +// SHT3x extended features +// ----------------------------------------------------------------------- +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + +static const char g_onlySht3[] = "XHTXX: SHT3x only."; +#define REQUIRE_SHT3X(dev, code) do { \ + if((dev)->familyIdx != XHTXX_FAMILY_SHT3X) { \ + ADDLOG_ERROR(LOG_FEATURE_SENSOR, g_onlySht3); return (code); } \ +} while(0) + +static void XHTXX_SHT3x_StartPeriodic(xhtxx_dev_t *dev, uint8_t msb, uint8_t lsb) +{ + I2C_Write(dev, msb, lsb, 0, 2); + dev->periodicActive = true; +} +static void XHTXX_SHT3x_StopPeriodic(xhtxx_dev_t *dev) +{ + if(!dev->periodicActive) return; + I2C_Write(dev, 0x30, 0x93, 0, 2); + rtos_delay_milliseconds(1); + dev->periodicActive = false; +} +static void XHTXX_SHT3x_FetchPeriodic(xhtxx_dev_t *dev) +{ + uint8_t d[6]; + if(!XHTXX_SHT_CmdRead6(dev, 0xE0, 0x00, 2, 0, d, "SHT3x_Per")) return; // [7] + //XHTXX_SHT3x_ConvertStore(dev, d); + XHTXX_SHT_UnifiedConvertStore(dev, d); +} +/* +// float version - switched to integer math below +static void XHTXX_SHT3x_ReadAlertReg(xhtxx_dev_t *dev, uint8_t sub, + float *out_hum, float *out_temp) +{ + uint8_t d[2]; + I2C_Write(dev, 0xE1, sub, 0, 2); + I2C_Read(dev, d, 2); + uint16_t w = ((uint16_t)d[0] << 8) | d[1]; + *out_hum = 100.0f * (w & 0xFE00u) / 65535.0f; + *out_temp = 175.0f * ((uint16_t)(w << 7) / 65535.0f) - 45.0f; +} +static void XHTXX_SHT3x_WriteAlertReg(xhtxx_dev_t *dev, uint8_t sub, + float hum, float temp) +{ + if(hum < 0.0f || hum > 100.0f || temp < -45.0f || temp > 130.0f) + { ADDLOG_INFO(LOG_FEATURE_CMD, "XHTXX: Alert value out of range."); return; } + uint16_t rawH = (uint16_t)(hum / 100.0f * 65535.0f); + uint16_t rawT = (uint16_t)((temp + 45.0f) / 175.0f * 65535.0f); + uint16_t w = (rawH & 0xFE00u) | ((rawT >> 7) & 0x01FFu); + uint8_t d[2] = { (uint8_t)(w >> 8), (uint8_t)(w & 0xFF) }; + uint8_t crc = XHTXX_CRC8(d, 2); + Soft_I2C_Start(&dev->i2c, dev->i2cAddr); + Soft_I2C_WriteByte(&dev->i2c, 0x61); + Soft_I2C_WriteByte(&dev->i2c, sub); + Soft_I2C_WriteByte(&dev->i2c, d[0]); + Soft_I2C_WriteByte(&dev->i2c, d[1]); + Soft_I2C_WriteByte(&dev->i2c, crc); + Soft_I2C_Stop(&dev->i2c); +} +*/ +// Change from float to int16_t (representing value * 10) +static void XHTXX_SHT3x_ReadAlertReg(xhtxx_dev_t *dev, uint8_t sub, + int16_t *out_h10, int16_t *out_t10) +{ + uint8_t d[3]; // two bytes + CRC (ignored for now) + I2C_Write(dev, 0xE1, sub, 0, 2); + I2C_Read(dev, d, 3); + uint16_t w = ((uint16_t)d[0] << 8) | d[1]; + + // Fixed-point conversion: + // Hum: 100 * (raw / 65535) * 10 -> (1000 * raw) / 65535 + *out_h10 = (int16_t)((1000u * (uint32_t)(w & 0xFE00u) + 32767u) / 65535u); + // Temp: (175 * (raw / 65535) - 45) * 10 -> (1750 * raw) / 65535 - 450 + *out_t10 = (int16_t)((1750u * (uint32_t)((uint16_t)(w << 7)) + 32767u) / 65535u) - 450; +// ADDLOG_INFO(LOG_FEATURE_CMD, "ReadAlertReg read 0x%02x 0x%02x 0x%02x - w=%i out_h10=%i out_t10=%i",d[0], d[1], d[2],w,*out_h10,*out_t10); + +} + +static void XHTXX_SHT3x_WriteAlertReg(xhtxx_dev_t *dev, uint8_t sub, + int16_t h10, int16_t t10) +{ + // Range check using fixed point (-450 to 1300 instead of -45.0 to 130.0) + if(h10 < 0 || h10 > 1000 || t10 < -450 || t10 > 1300) + { ADDLOG_INFO(LOG_FEATURE_CMD, "SHT3x: Alert out of range."); return; } + + // Reverse conversion: + // rawH = (h / 100) * 65535 -> (h10 * 65535) / 1000 + uint16_t rawH = (uint16_t)(((uint32_t)h10 * 65535u + 500u) / 1000u); + // rawT = ((t + 45) / 175) * 65535 -> ((t10 + 450) * 65535) / 1750 + uint16_t rawT = (uint16_t)(((uint32_t)(t10 + 450) * 65535u + 875u) / 1750u); +// ADDLOG_INFO(LOG_FEATURE_CMD, "WriteAlertReg rawH=%i rawT=%i",rawH, rawT); + uint16_t w = (rawH & 0xFE00u) | ((rawT >> 7) & 0x01FFu); + uint8_t d[2] = { (uint8_t)(w >> 8), (uint8_t)(w & 0xFF) }; + uint8_t crc = XHTXX_CRC8(d, 2); + + bool ACK=false; + int rep=1; + do { + Soft_I2C_Start(&dev->i2c, dev->i2cAddr); + Soft_I2C_WriteByte(&dev->i2c, 0x61); + Soft_I2C_WriteByte(&dev->i2c, sub); + Soft_I2C_WriteByte(&dev->i2c, d[0]); + Soft_I2C_WriteByte(&dev->i2c, d[1]); + ACK=Soft_I2C_WriteByte(&dev->i2c, crc); +// ADDLOG_INFO(LOG_FEATURE_CMD, "WriteAlertReg try %i/3 writing 0x%02x 0x%02x 0x%02x returned %s",rep, d[0], d[1], crc, ACK ? "ACK":"NACK"); + Soft_I2C_Stop(&dev->i2c); + rep++; + } while ( !ACK && rep <= 3); +} +#endif // XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + +// ----------------------------------------------------------------------- +// Auto-detect + init +// ----------------------------------------------------------------------- +static bool XHTXX_AutoDetect(xhtxx_dev_t *dev) +{ + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: auto-detect SDA=%i SCL=%i...", + dev->i2c.pin_data, dev->i2c.pin_clk); + for(uint8_t i = 0; i < XHTXX_PROBE_STEPS; i++) { + dev->familyIdx = g_probeOrder[i].family; + dev->i2cAddr = g_probeOrder[i].addr; + if(g_families[dev->familyIdx].probe_fn(dev)) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: found %s at 0x%02X", + g_families[dev->familyIdx].name, dev->i2cAddr >> 1); + return true; + } + rtos_delay_milliseconds(10); + } + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: nothing found, defaulting SHT3x @ 0x44"); + dev->familyIdx = XHTXX_FAMILY_SHT3X; + dev->i2cAddr = XHTXX_ADDR_SHT; + return false; +} + +// ----------------------------------------------------------------------- +// Commands — all use XHTXX_GetSensor with optional [sensorN] [8] +// ----------------------------------------------------------------------- +//cmddetail:{"name":"XHTXX_Calibrate","args":"[DeltaTemp] [DeltaHum] [sensorN]", +//cmddetail:"descr":"Offset calibration in °C and %%RH (0.1 resolution). sensorN is 1-based.", +//cmddetail:"fn":"XHTXX_CMD_Calibrate","file":"driver/drv_xhtxx.c","requires":"", +//cmddetail:"examples":"XHTXX_Calibrate -1.5 3
XHTXX_Calibrate -1.5 3 2"} +commandResult_t XHTXX_CMD_Calibrate(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + int argc = Tokenizer_GetArgsCount(); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 2, argc >= 3, 0); + if(!dev) return CMD_RES_BAD_ARGUMENT; + dev->calTemp = (int16_t)(Tokenizer_GetArgFloat(0) * 10.0f); + dev->calHum = (int8_t) (Tokenizer_GetArgFloat(1) * 10.0f); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX calibrate: calT=%d calH=%d (x0.1)", + dev->calTemp, dev->calHum); + return CMD_RES_OK; +} + +//cmddetail:{"name":"XHTXX_Cycle","args":"[Seconds] [sensorN]", +//cmddetail:"descr":"Measurement interval in seconds (min 1). sensorN is 1-based.", +//cmddetail:"fn":"XHTXX_CMD_Cycle","file":"driver/drv_xhtxx.c","requires":"", +//cmddetail:"examples":"XHTXX_Cycle 30
XHTXX_Cycle 30 2"} +commandResult_t XHTXX_CMD_Cycle(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 1, Tokenizer_GetArgsCount() >= 2, 0); + if(!dev) return CMD_RES_BAD_ARGUMENT; + int s = Tokenizer_GetArgInteger(0); + if(s < 1) { ADDLOG_INFO(LOG_FEATURE_CMD, "XHTXX: min 1s."); return CMD_RES_BAD_ARGUMENT; } + dev->secondsBetween = (uint8_t)s; + ADDLOG_INFO(LOG_FEATURE_CMD, "XHTXX: measure every %i s", s); + return CMD_RES_OK; +} + +//cmddetail:{"name":"XHTXX_Measure","args":"[sensorN]", +//cmddetail:"descr":"Immediate one-shot measurement. sensorN is 1-based.", +//cmddetail:"fn":"XHTXX_CMD_Force","file":"driver/drv_xhtxx.c","requires":"", +//cmddetail:"examples":"XHTXX_Measure
XHTXX_Measure 2"} +commandResult_t XHTXX_CMD_Force(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, 0); + if(!dev || !dev->isWorking) return CMD_RES_BAD_ARGUMENT; + dev->secondsUntilNext = dev->secondsBetween; + g_families[dev->familyIdx].measure_fn(dev); + return CMD_RES_OK; +} + +//cmddetail:{"name":"XHTXX_Reinit","args":"[sensorN]", +//cmddetail:"descr":"Soft-reset and re-initialise sensor. sensorN is 1-based.", +//cmddetail:"fn":"XHTXX_CMD_Reinit","file":"driver/drv_xhtxx.c","requires":"", +//cmddetail:"examples":"XHTXX_Reinit
XHTXX_Reinit 2"} +commandResult_t XHTXX_CMD_Reinit(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, 0); + if(!dev) return CMD_RES_BAD_ARGUMENT; +#ifdef XHTXX_ENABLE_SERIAL_LOG + dev->serial = 0; +#endif + g_families[dev->familyIdx].reset_fn(dev); + g_families[dev->familyIdx].init_fn(dev); + return CMD_RES_OK; +} + +//cmddetail:{"name":"XHTXX_AddSensor","args":"[SDA=pin] [SCL=pin] [family=…] [chan_t=ch] [chan_h=ch]", +//cmddetail:"descr":"Register an additional sensor on different pins or family.", +//cmddetail:"fn":"XHTXX_CMD_AddSensor","file":"driver/drv_xhtxx.c","requires":"", +//cmddetail:"examples":"XHTXX_AddSensor SDA=4 SCL=5 family=aht2"} +commandResult_t XHTXX_CMD_AddSensor(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; (void)cmd; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + XHTXX_Init(); + return CMD_RES_OK; +} + +//cmddetail:{"name":"XHTXX_ListSensors","args":"", +//cmddetail:"descr":"List all registered sensors and their last readings.", +//cmddetail:"fn":"XHTXX_CMD_ListSensors","file":"driver/drv_xhtxx.c","requires":"", +//cmddetail:"examples":"XHTXX_ListSensors"} +commandResult_t XHTXX_CMD_ListSensors(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; (void)cmd; (void)args; + if(!g_numSensors) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: no sensors registered."); + return CMD_RES_OK; + } + for(uint8_t i = 0; i < g_numSensors; i++) { + xhtxx_dev_t *s = &g_sensors[i]; + int16_t tf = s->lastTemp % 10; if(tf < 0) tf = -tf; +#ifdef XHTXX_ENABLE_SERIAL_LOG + ADDLOG_INFO(LOG_FEATURE_SENSOR, + " [%u] %s sn=%08X SDA=%i SCL=%i addr=0x%02X T=%d.%d°C H=%d.%d%% ch=%i/%i", + i+1, g_families[s->familyIdx].name, s->serial, + s->i2c.pin_data, s->i2c.pin_clk, s->i2cAddr>>1, + s->lastTemp/10, tf, s->lastHumid/10, s->lastHumid%10, + s->channel_temp, s->channel_humid); +#else + ADDLOG_INFO(LOG_FEATURE_SENSOR, + " [%u] %s SDA=%i SCL=%i addr=0x%02X T=%d.%d°C H=%d.%d%% ch=%i/%i", + i+1, g_families[s->familyIdx].name, + s->i2c.pin_data, s->i2c.pin_clk, s->i2cAddr>>1, + s->lastTemp/10, tf, s->lastHumid/10, s->lastHumid%10, + s->channel_temp, s->channel_humid); +#endif + } + return CMD_RES_OK; +} + +// ----------------------------------------------------------------------- +// SHT3x extended commands +// ----------------------------------------------------------------------- +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES +commandResult_t XHTXX_CMD_LaunchPer(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + int argc = Tokenizer_GetArgsCount(); + uint8_t msb = 0x23, lsb = 0x22; + xhtxx_dev_t *dev; + if(argc >= 2) { msb=(uint8_t)Tokenizer_GetArgInteger(0); lsb=(uint8_t)Tokenizer_GetArgInteger(1); dev=XHTXX_GetSensor(cmd,2,argc>=3, XHTXX_FAMILY_SHT3X); } + else { dev = XHTXX_GetSensor(cmd, 0, 0, XHTXX_FAMILY_SHT3X); } + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + XHTXX_SHT3x_StopPeriodic(dev); rtos_delay_milliseconds(25); + XHTXX_SHT3x_StartPeriodic(dev, msb, lsb); + return CMD_RES_OK; +} +commandResult_t XHTXX_CMD_FetchPer(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + if(!dev->periodicActive) { ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: periodic not running."); return CMD_RES_ERROR; } + XHTXX_SHT3x_FetchPeriodic(dev); + return CMD_RES_OK; +} +commandResult_t XHTXX_CMD_StopPer(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + XHTXX_SHT3x_StopPeriodic(dev); + return CMD_RES_OK; +} +commandResult_t XHTXX_CMD_Heater(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + int on = Tokenizer_GetArgInteger(0); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 1, Tokenizer_GetArgsCount() >= 2, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + I2C_Write(dev, 0x30, on ? 0x6D : 0x66, 0, 2); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX SHT3x heater %s (SDA=%i)", on?"on":"off", dev->i2c.pin_data); + return CMD_RES_OK; +} +commandResult_t XHTXX_CMD_GetStatus(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + uint8_t buf[3]; + I2C_Write(dev, 0xF3, 0x2D, 0, 2); + I2C_Read(dev, buf, 3); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX SHT3x status: %02X%02X (SDA=%i)", buf[0], buf[1], dev->i2c.pin_data); + return CMD_RES_OK; +} +commandResult_t XHTXX_CMD_ClearStatus(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + I2C_Write(dev, 0x30, 0x41, 0, 2); + return CMD_RES_OK; +} +// try avoiding "abs" +static inline int16_t abs16(int16_t x) { return x < 0 ? -x : x; } +commandResult_t XHTXX_CMD_ReadAlert(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 0, Tokenizer_GetArgsCount() >= 1, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + + int16_t t[4], h[4]; // LS, LC, HC, HS + XHTXX_SHT3x_ReadAlertReg(dev, 0x1F, &h[3], &t[3]); + XHTXX_SHT3x_ReadAlertReg(dev, 0x14, &h[2], &t[2]); + XHTXX_SHT3x_ReadAlertReg(dev, 0x09, &h[1], &t[1]); + XHTXX_SHT3x_ReadAlertReg(dev, 0x02, &h[0], &t[0]); + + // Use integer formatting to save space + ADDLOG_INFO(LOG_FEATURE_SENSOR, "Alert T: %d.%d/%d.%d/%d.%d/%d.%d", + t[0]/10, abs16(t[0]%10), t[1]/10, abs16(t[1]%10), + t[2]/10, abs16(t[2]%10), t[3]/10, abs16(t[3]%10)); + ADDLOG_INFO(LOG_FEATURE_SENSOR, "Alert H: %d.%d/%d.%d/%d.%d/%d.%d", + h[0]/10, abs16(h[0]%10), h[1]/10, abs16(h[1]%10), + h[2]/10, abs16(h[2]%10), h[3]/10, abs16(h[3]%10)); + return CMD_RES_OK; +} + +commandResult_t XHTXX_CMD_SetAlert(const void *context, const char *cmd, + const char *args, int cmdFlags) +{ + CMD_UNUSED; + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 4)) return CMD_RES_NOT_ENOUGH_ARGUMENTS; + xhtxx_dev_t *dev = XHTXX_GetSensor(cmd, 4, Tokenizer_GetArgsCount() >= 5, XHTXX_FAMILY_SHT3X); + if(!dev) return CMD_RES_BAD_ARGUMENT; + REQUIRE_SHT3X(dev, CMD_RES_ERROR); + + int16_t tHS = (int16_t)(Tokenizer_GetArgInteger(0)*10); + int16_t tLS = (int16_t)(Tokenizer_GetArgInteger(1)*10); + int16_t hHS = (int16_t)(Tokenizer_GetArgInteger(2)*10); + int16_t hLS = (int16_t)(Tokenizer_GetArgInteger(3)*10); +// ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: Writing alerts: tHS=%i tLS=%i / hHS=%i hLS=%i.", tHS, tLS, hHS, hLS); + + // Using 5 (0.5 * 10) for hysteresis offset + XHTXX_SHT3x_WriteAlertReg(dev, 0x1D, hHS, tHS); + usleep(1); + XHTXX_SHT3x_WriteAlertReg(dev, 0x16, hHS - 5, tHS - 5); + usleep(1); + XHTXX_SHT3x_WriteAlertReg(dev, 0x0B, hLS + 5, tLS + 5); + usleep(1); + XHTXX_SHT3x_WriteAlertReg(dev, 0x00, hLS, tLS); + return CMD_RES_OK; +} + +#endif // XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + +// ----------------------------------------------------------------------- +// Driver entry points +// ----------------------------------------------------------------------- + + +void XHTXX_Init(void) +{ + if(g_numSensors >= XHTXX_MAX_SENSORS) { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "XHTXX: sensor array full (%i).", XHTXX_MAX_SENSORS); + return; + } + xhtxx_dev_t *dev = &g_sensors[g_numSensors]; + + dev->i2c.pin_clk = 9; + dev->i2c.pin_data = 17; + dev->channel_temp = -1; + dev->channel_humid = -1; + if(g_numSensors == 0) { + dev->i2c.pin_clk = PIN_FindPinIndexForRole(IOR_SHT3X_CLK, dev->i2c.pin_clk); + dev->i2c.pin_data = PIN_FindPinIndexForRole(IOR_SHT3X_DAT, dev->i2c.pin_data); + dev->channel_temp = g_cfg.pins.channels [dev->i2c.pin_data]; + dev->channel_humid = g_cfg.pins.channels2[dev->i2c.pin_data]; + } + dev->i2c.pin_clk = Tokenizer_GetPinEqual("-SCL", dev->i2c.pin_clk); + dev->i2c.pin_data = Tokenizer_GetPinEqual("-SDA", dev->i2c.pin_data); + dev->channel_temp = Tokenizer_GetArgEqualInteger("-chan_t", dev->channel_temp); + dev->channel_humid = Tokenizer_GetArgEqualInteger("-chan_h", dev->channel_humid); + dev->secondsBetween = 10; + dev->secondsUntilNext = 1; + dev->calTemp = 0; + dev->calHum = 0; +#ifdef XHTXX_ENABLE_SERIAL_LOG + dev->serial = 0; +#endif +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + dev->periodicActive = false; +#endif + + const char *fam = Tokenizer_GetArgEqualDefault("-family", "default"); + uint8_t reqFam = XHTXX_FAMILY_AUTO; + // keep it simple, just compare first (or in case of SHT first and 4st) char of given type + switch(fam[0] | 0x20) { // "fold" char to lowercase in one op + case 's': reqFam = (fam[3] == '4') ? XHTXX_FAMILY_SHT4X : XHTXX_FAMILY_SHT3X; break; + case 'a': reqFam = XHTXX_FAMILY_AHT2X; break; + case 'c': reqFam = XHTXX_FAMILY_CHT83XX; break; + } + + uint8_t addrArg = (uint8_t)Tokenizer_GetArgEqualInteger("-address", 0); + if(reqFam == XHTXX_FAMILY_AUTO) { + if(addrArg) dev->i2cAddr = addrArg << 1; + XHTXX_AutoDetect(dev); + } else { + dev->familyIdx = reqFam; + dev->i2cAddr = addrArg ? (addrArg << 1) : g_families[reqFam].defaultAddr; + } + + Soft_I2C_PreInit(&dev->i2c); + rtos_delay_milliseconds(50); +#if ENABLE_USED_PIN + setPinUsedString(dev->i2c.pin_clk, "XHTXX SCL"); + setPinUsedString(dev->i2c.pin_data, "XHTXX SDA"); +#endif + g_families[dev->familyIdx].init_fn(dev); + + if(g_numSensors == 0) { + CMD_RegisterCommand("XHTXX_Calibrate", XHTXX_CMD_Calibrate, NULL); + CMD_RegisterCommand("XHTXX_Cycle", XHTXX_CMD_Cycle, NULL); + CMD_RegisterCommand("XHTXX_Measure", XHTXX_CMD_Force, NULL); + CMD_RegisterCommand("XHTXX_Reinit", XHTXX_CMD_Reinit, NULL); + CMD_RegisterCommand("XHTXX_AddSensor", XHTXX_CMD_AddSensor, NULL); + CMD_RegisterCommand("XHTXX_ListSensors", XHTXX_CMD_ListSensors, NULL); +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES +// use command names equal to old SHT3x driver + //cmddetail:{"name":"SHT_LaunchPer","args":"[msb][lsb] [sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Launch/Change periodical capture for SHT3x sensor", + //cmddetail:"fn":"XHTXX_CMD_LaunchPer","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_LaunchPer 0x23 0x22"} + CMD_RegisterCommand("SHT_LaunchPer", XHTXX_CMD_LaunchPer, NULL); + //cmddetail:{"name":"SHT_MeasurePer","args":"[sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Retrieve Periodical measurement for SHT3x sensor", + //cmddetail:"fn":"XHTXX_CMD_FetchPer","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_Measure"} + CMD_RegisterCommand("SHT_MeasurePer", XHTXX_CMD_FetchPer, NULL); + //cmddetail:{"name":"SHT_StopPer","args":"[sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Stop periodical capture for SHT3x sensor", + //cmddetail:"fn":"XHTXX_CMD_StopPer","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":""} + CMD_RegisterCommand("SHT_StopPer", XHTXX_CMD_StopPer, NULL); + //cmddetail:{"name":"SHT_Heater","args":"[1or0] [sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Activate or Deactivate Heater (0 / 1) for SHT3x sensor", + //cmddetail:"fn":"XHTXX_CMD_Heater","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_Heater 1"} + CMD_RegisterCommand("SHT_Heater", XHTXX_CMD_Heater, NULL); + //cmddetail:{"name":"SHT_GetStatus","args":"[sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Get SHT3x sensor status", + //cmddetail:"fn":"XHTXX_CMD_GetStatus","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_GetStatusCmd"} + CMD_RegisterCommand("SHT_GetStatus", XHTXX_CMD_GetStatus, NULL); + //cmddetail:{"name":"SHT_ClearStatus","args":"[sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Clear SHT3x sensor status", + //cmddetail:"fn":"XHTXX_CMD_ClearStatus","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_ClearStatusCmd"} + CMD_RegisterCommand("SHT_ClearStatus", XHTXX_CMD_ClearStatus, NULL); + //cmddetail:{"name":"SHT_ReadAlert","args":"[sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Get SHT3x sensors alert configuration", + //cmddetail:"fn":"XHTXX_CMD_ReadAlert","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_ReadAlertCmd"} + CMD_RegisterCommand("SHT_ReadAlert", XHTXX_CMD_ReadAlert, NULL); + //cmddetail:{"name":"SHT_SetAlert","args":"[temp_high, temp_low, hum_high, hum_low] [sensor-index - first SHT3x if ommitted]", + //cmddetail:"descr":"Set SHT3x sensors alert configuration", + //cmddetail:"fn":"XHTXX_CMD_SetAlert","file":"driver/drv_xhtxx.c","requires":"ENABLE_DRIVER_XHTXX && XHTXX_ENABLE_SHT3_EXTENDED_FEATURES", + //cmddetail:"examples":"SHT_SetAlertCmd"} + CMD_RegisterCommand("SHT_SetAlert", XHTXX_CMD_SetAlert, NULL); +#endif + } + g_numSensors++; +} + +void XHTXX_StopDriver(void) +{ + // Stop periodic tasks if SHT3x is used to prevent + // the sensor from flooding the I2C bus after the driver is "stopped" +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + for(uint8_t i = 0; i < g_numSensors; i++) { + if(g_sensors[i].familyIdx == XHTXX_FAMILY_SHT3X && g_sensors[i].periodicActive) { + XHTXX_SHT3x_StopPeriodic(&g_sensors[i]); + } + } +#endif + + // 2. Clear all sensor structures in memory + memset(g_sensors, 0, sizeof(g_sensors)); + + // 3. Reset the global sensor counter + g_numSensors = 0; + +} + + +void XHTXX_OnEverySecond(void) +{ + for(uint8_t i = 0; i < g_numSensors; i++) { + xhtxx_dev_t *dev = &g_sensors[i]; + if(dev->secondsUntilNext == 0) { + if(dev->isWorking) { +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + if(dev->periodicActive) XHTXX_SHT3x_FetchPeriodic(dev); else +#endif + g_families[dev->familyIdx].measure_fn(dev); + } else { + if (XHTXX_AutoDetect(dev)) + g_families[dev->familyIdx].init_fn(dev); + } + dev->secondsUntilNext = dev->secondsBetween; + } else { + dev->secondsUntilNext--; + } + } +} + +void XHTXX_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState) +{ + if(bPreState) return; + for(uint8_t i = 0; i < g_numSensors; i++) { + xhtxx_dev_t *dev = &g_sensors[i]; + int16_t tf = dev->lastTemp % 10; if(tf < 0) tf = -tf; + hprintf255(request, "

%s[%u] T=%d.%d°C H=%d.%d%%

", + g_families[dev->familyIdx].name, i+1, + dev->lastTemp/10, tf, dev->lastHumid/10, dev->lastHumid%10); + if(!dev->isWorking) + hprintf255(request, "

WARNING: %s[%u] init failed

", + g_families[dev->familyIdx].name, i+1); + } +} + +#endif // ENABLE_DRIVER_XHTXX diff --git a/src/driver/drv_xhtxx.h b/src/driver/drv_xhtxx.h new file mode 100644 index 000000000..6fd0726ea --- /dev/null +++ b/src/driver/drv_xhtxx.h @@ -0,0 +1,113 @@ +// drv_xhtxx.h – SHT3x/SHT4x (Sensirion), AHT2x (Aosong), CHT83xx (Sensylink) +// +// Public types and API only. All implementation lives in drv_xhtxx.c. +// +// Supported families +// SHT3x – SHT30 / SHT31 / SHT35 (addr 0x44 or 0x45) +// SHT4x – SHT40 / SHT41 / SHT43 / SHT45 (addr 0x44) +// AHT2x – AHT20 / AHT21 / AHT25 (addr 0x38) +// CHT83xx– CHT8305 / CHT8310 / CHT8315 (addr 0x40) +// +// startDriver syntax: +// startDriver XHTXX [-SDA ] [-SCL ] +// [-family sht3|sht4|aht2|cht] omit → auto-detect +// [-address <7-bit hex>] override I²C addr +// [-chan_t ] [chan_h ] +// +// Additional sensors: +// XHTXX_AddSensor -SDA -SCL [-family …] [-address …] … +// +// Feature gates (define before including or in build flags): +// XHTXX_ENABLE_SERIAL_LOG – store + print SHT serial numbers +// XHTXX_ENABLE_SHT3_EXTENDED_FEATURES – SHT3x heater/alerts/periodic mode + +#ifndef DRV_XHTXX_H +#define DRV_XHTXX_H + +#define XHTXX_ENABLE_SERIAL_LOG 1 +#define XHTXX_ENABLE_SHT3_EXTENDED_FEATURES 1 + +#include +#include + +// ----------------------------------------------------------------------- +// Limits +// ----------------------------------------------------------------------- +#ifndef XHTXX_MAX_SENSORS +# define XHTXX_MAX_SENSORS 4 +#endif + +// ----------------------------------------------------------------------- +// Family indices +// ----------------------------------------------------------------------- +#define XHTXX_FAMILY_AUTO 0 // auto-detect (sentinel, never stored) +#define XHTXX_FAMILY_SHT3X 1 +#define XHTXX_FAMILY_SHT4X 2 +#define XHTXX_FAMILY_AHT2X 3 +#define XHTXX_FAMILY_CHT83XX 4 +#define XHTXX_FAMILY_COUNT 5 + +// ----------------------------------------------------------------------- +// I²C addresses (pre-shifted to 8-bit form, LSB = R/W) +// ----------------------------------------------------------------------- +#define XHTXX_ADDR_SHT (0x44 << 1) +#define XHTXX_ADDR_SHT_ALT (0x45 << 1) +#define XHTXX_ADDR_AHT2X (0x38 << 1) +#define XHTXX_ADDR_CHT83XX (0x40 << 1) + +// ----------------------------------------------------------------------- +// Per-sensor state (forward-declared for use in family dispatch table) +// ----------------------------------------------------------------------- +typedef struct xhtxx_dev_s xhtxx_dev_t; + +// ----------------------------------------------------------------------- +// Family dispatch table entry (stored in flash) +// ----------------------------------------------------------------------- +typedef struct { + bool (*probe_fn) (xhtxx_dev_t *dev); // non-destructive probe + void (*init_fn) (xhtxx_dev_t *dev); // full init, sets isWorking + void (*measure_fn)(xhtxx_dev_t *dev); // one-shot measurement + void (*reset_fn) (xhtxx_dev_t *dev); // soft reset + const char *name; // "SHT3x", "AHT2x", … + uint8_t defaultAddr; // pre-shifted +} xhtxx_family_t; + +// ----------------------------------------------------------------------- +// Per-sensor state +// +// lastTemp : °C × 10 (e.g. 225 = 22.5 °C) +// lastHumid : %RH × 10 (e.g. 456 = 45.6 %RH) +// calTemp : °C × 10 calibration offset +// calHum : %RH × 10 calibration offset +// ----------------------------------------------------------------------- +struct xhtxx_dev_s { + softI2C_t i2c; + uint8_t i2cAddr; // pre-shifted 8-bit address + uint8_t familyIdx; // XHTXX_FAMILY_* (never AUTO after init) + uint16_t subtype; // CHT variant id; 0 for others + int16_t calTemp; // °C × 10 + int8_t calHum; // %RH × 10 + int8_t channel_temp; // -1 = unused + int8_t channel_humid; // -1 = unused + uint8_t secondsBetween; + uint8_t secondsUntilNext; + bool isWorking; + int16_t lastTemp; // °C × 10 + int16_t lastHumid; // %RH × 10 +#ifdef XHTXX_ENABLE_SERIAL_LOG + uint32_t serial; +#endif +#ifdef XHTXX_ENABLE_SHT3_EXTENDED_FEATURES + bool periodicActive; +#endif +}; + +// ----------------------------------------------------------------------- +// Public API +// ----------------------------------------------------------------------- +void XHTXX_Init(void); +void XHTXX_StopDriver(void); +void XHTXX_OnEverySecond(void); +void XHTXX_AppendInformationToHTTPIndexPage(http_request_t *request, int bPreState); + +#endif // DRV_XHTXX_H diff --git a/src/obk_config.h b/src/obk_config.h index 1c6973f4c..994938c06 100644 --- a/src/obk_config.h +++ b/src/obk_config.h @@ -723,7 +723,10 @@ #undef ENABLE_DRIVER_IR #endif -#define ENABLE_DRIVER_SHTXX 1 + +// for testing: enable new driver by default +#define ENABLE_DRIVER_VEML7700 1 +#define ENABLE_DRIVER_XHTXX 1 // closing OBK_CONFIG_H