+#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;
+}
+*/
+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(&buf[0],2);
+}
+
+// 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 - clock stretch
+// 0x3682 read serial number - no clock stretch
+//
+// 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 == 0x36 && c1 == 0x82) || c0 == 0x37 && c1 == 0x80) {
+ 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 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;
+ 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;
+
+ // 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:
+ 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,
+};
+
+// ===================================================================
+// 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);
+
+
+ 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 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;
+ }
+ }
+ 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/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/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 58ac1825e..994938c06 100644
--- a/src/obk_config.h
+++ b/src/obk_config.h
@@ -206,6 +206,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
@@ -719,5 +723,11 @@
#undef ENABLE_DRIVER_IR
#endif
+
+// for testing: enable new driver by default
+#define ENABLE_DRIVER_VEML7700 1
+#define ENABLE_DRIVER_XHTXX 1
+
+
// closing OBK_CONFIG_H
#endif