diff --git a/src/driver/drv_aht2x.c b/src/driver/drv_aht2x.c index e2cfef56c4..820f922a5b 100644 --- a/src/driver/drv_aht2x.c +++ b/src/driver/drv_aht2x.c @@ -4,58 +4,218 @@ #include "drv_aht2x.h" #define AHT2X_I2C_ADDR (0x38 << 1) +#define AHT2X_CRC_MODE_OFF 0 +#define AHT2X_CRC_MODE_AUTO 1 +#define AHT2X_CRC_MODE_REQUIRED 2 +#define AHT2X_MAX_MEASURE_FAILURES 3 -static byte g_aht_secondsUntilNextMeasurement = 1, g_aht_secondsBetweenMeasurements = 10, - max_retries = 20, channel_temp = 0, channel_humid = 0; +static byte max_retries = 20; +static int g_aht_secondsUntilNextMeasurement = 1, g_aht_secondsBetweenMeasurements = 10, channel_temp = 0, channel_humid = 0; static float g_temp = 0.0, g_humid = 0.0, g_calTemp = 0.0, g_calHum = 0.0; static softI2C_t g_softI2C; static bool isWorking = false; +static bool g_crcSupported = false; +static byte g_crcMode = AHT2X_CRC_MODE_OFF; +static uint8_t g_lastStatus = 0; +static byte g_measureFailures = 0; -void AHT2X_SoftReset() +static bool AHT2X_IsReadyAndCalibrated(uint8_t status) +{ + return ((status & AHT2X_DAT_BUSY) == 0) && ((status & AHT2X_DAT_CALIBRATED) == AHT2X_DAT_CALIBRATED); +} + +static bool AHT2X_WriteCommand(uint8_t cmd) { - Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_CMD_RST); + bool ok = Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR); + if(ok) + { + ok = Soft_I2C_WriteByte(&g_softI2C, cmd); + } Soft_I2C_Stop(&g_softI2C); - rtos_delay_milliseconds(20); + return ok; } -void AHT2X_Initialization() +static bool AHT2X_WriteCommand3(uint8_t cmd, uint8_t data1, uint8_t data2) { - AHT2X_SoftReset(); + bool ok = Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR); + if(ok) + { + ok = Soft_I2C_WriteByte(&g_softI2C, cmd); + } + if(ok) + { + ok = Soft_I2C_WriteByte(&g_softI2C, data1); + } + if(ok) + { + ok = Soft_I2C_WriteByte(&g_softI2C, data2); + } + Soft_I2C_Stop(&g_softI2C); + return ok; +} - Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_CMD_INI); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_DAT_INI1); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_DAT_INI2); +static bool AHT2X_ReadBytes(uint8_t *data, int len) +{ + bool ok = Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR | 1); + if(ok) + { + Soft_I2C_ReadBytes(&g_softI2C, data, len); + } Soft_I2C_Stop(&g_softI2C); + return ok; +} - uint8_t data = AHT2X_DAT_BUSY; +static bool AHT2X_ReadStatus(uint8_t *status) +{ + bool ok = Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR | 1); + if(ok) + { + *status = Soft_I2C_ReadByte(&g_softI2C, true); + g_lastStatus = *status; + } + Soft_I2C_Stop(&g_softI2C); + return ok; +} + +static bool AHT2X_WaitReady(uint8_t *status) +{ uint8_t attempts = 0; + *status = AHT2X_DAT_BUSY; - while(data & AHT2X_DAT_BUSY) + while(*status & AHT2X_DAT_BUSY) { rtos_delay_milliseconds(20); - Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR | 1); - data = Soft_I2C_ReadByte(&g_softI2C, true); - Soft_I2C_Stop(&g_softI2C); + if(!AHT2X_ReadStatus(status)) + { + return false; + } attempts++; if(attempts > max_retries) { - ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Sensor timed out.", __func__); - isWorking = false; - break; + return false; } } - if((data & 0x68) != 0x08) + return true; +} + +static uint8_t AHT2X_CalcCrc8(const uint8_t *data, int len) +{ + uint8_t crc = AHT2X_CRC8_INIT; + for(int idx = 0; idx < len; idx++) { - ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Initialization failed.", __func__); - isWorking = false; + crc ^= data[idx]; + for(uint8_t bit = 8; bit > 0; bit--) + { + if(crc & 0x80) + { + crc = (crc << 1) ^ AHT2X_CRC8_POLY; + } + else + { + crc <<= 1; + } + } } - else + return crc; +} + +static bool AHT2X_ValidateCrcIfPresent(const uint8_t *data) +{ + if(g_crcMode == AHT2X_CRC_MODE_OFF) + { + return true; + } + + uint8_t crc = AHT2X_CalcCrc8(data, AHT2X_READ_LEN); + if(crc == data[AHT2X_READ_LEN]) + { + if(!g_crcSupported) + { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "AHT2X: CRC byte detected and will be validated."); + } + g_crcSupported = true; + return true; + } + + if(g_crcMode == AHT2X_CRC_MODE_REQUIRED || g_crcSupported) + { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "AHT2X: CRC mismatch, expected 0x%02X got 0x%02X; measurement ignored.", crc, data[AHT2X_READ_LEN]); + return false; + } + + /* + * Some AHT1x/AHT2x modules expose only the six measurement bytes described + * by older datasheets/libraries. When CRC auto mode is explicitly enabled, + * a non-matching seventh byte is treated as "CRC not present" rather than as + * a sensor fault. + */ + return true; +} + +void AHT2X_SoftReset() +{ + if(!AHT2X_WriteCommand(AHT2X_CMD_RST)) + { + ADDLOG_DEBUG(LOG_FEATURE_SENSOR, "%s: Sensor did not ACK soft reset.", __func__); + } + rtos_delay_milliseconds(20); +} + +static bool AHT2X_TryInitializationCommand(uint8_t initCmd, const char *name) +{ + uint8_t status = AHT2X_DAT_BUSY; + + if(!AHT2X_WriteCommand3(initCmd, AHT2X_DAT_INI1, AHT2X_DAT_INI2)) + { + ADDLOG_DEBUG(LOG_FEATURE_SENSOR, "%s: %s init command was not ACKed.", __func__, name); + return false; + } + + if(!AHT2X_WaitReady(&status)) + { + ADDLOG_DEBUG(LOG_FEATURE_SENSOR, "%s: %s init timed out or sensor did not ACK status read.", __func__, name); + return false; + } + + if(!AHT2X_IsReadyAndCalibrated(status)) + { + ADDLOG_DEBUG(LOG_FEATURE_SENSOR, "%s: %s init returned unexpected status 0x%02X.", __func__, name, status); + return false; + } + + ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Initialization successful using %s command set, status 0x%02X.", __func__, name, status); + return true; +} + +void AHT2X_Initialization() +{ + isWorking = false; + g_crcSupported = false; + g_lastStatus = 0; + g_measureFailures = 0; + + AHT2X_SoftReset(); + + /* + * Keep the original AHT2X/AHT20-style init first for OTA compatibility. + * AHT20/AHT21-style datasheets document 0xBE 0x08 0x00; AHT10/AHT15 + * datasheets document 0xE1 0x08 0x00. Falling back to 0xE1 fixes + * stricter AHT1x parts without changing existing AHT2X start commands. + */ + if(AHT2X_TryInitializationCommand(AHT2X_CMD_INI, "AHT2X-compatible")) { - ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Initialization successful.", __func__); isWorking = true; + return; } + + AHT2X_SoftReset(); + if(AHT2X_TryInitializationCommand(AHT2X_CMD_INI_AHT1X, "AHT1X-compatible")) + { + isWorking = true; + return; + } + + ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Initialization failed. Last status 0x%02X.", __func__, g_lastStatus); } void AHT2X_StopDriver() @@ -63,25 +223,43 @@ void AHT2X_StopDriver() AHT2X_SoftReset(); } -void AHT2X_Measure() +static void AHT2X_RecordMeasurementFailure(const char *caller, const char *reason) { - uint8_t data[6] = { 0, }; + g_measureFailures++; + ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: %s Consecutive failures %u/%u.", caller, reason, (unsigned)g_measureFailures, AHT2X_MAX_MEASURE_FAILURES); - Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_CMD_TMS); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_DAT_TMS1); - Soft_I2C_WriteByte(&g_softI2C, AHT2X_DAT_TMS2); - Soft_I2C_Stop(&g_softI2C); + if(g_measureFailures >= AHT2X_MAX_MEASURE_FAILURES) + { + ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Reinitializing sensor after %u consecutive communication failures.", caller, (unsigned)g_measureFailures); + AHT2X_Initialization(); + } +} +void AHT2X_Measure() +{ + uint8_t data[AHT2X_READ_LEN_CRC] = { 0, }; + int readLen = (g_crcMode == AHT2X_CRC_MODE_OFF) ? AHT2X_READ_LEN : AHT2X_READ_LEN_CRC; bool ready = false; + /* AHT-family measurement trigger documented as 0xAC 0x33 0x00. */ + if(!AHT2X_WriteCommand3(AHT2X_CMD_TMS, AHT2X_DAT_TMS1, AHT2X_DAT_TMS2)) + { + AHT2X_RecordMeasurementFailure(__func__, "Sensor did not ACK measurement command."); + return; + } + + /* AHT20/AHT30 datasheets specify an 80 ms conversion wait after trigger. */ rtos_delay_milliseconds(80); for(uint8_t i = 0; i < 10; i++) { - Soft_I2C_Start(&g_softI2C, AHT2X_I2C_ADDR | 1); - Soft_I2C_ReadBytes(&g_softI2C, data, 6); - Soft_I2C_Stop(&g_softI2C); + if(!AHT2X_ReadBytes(data, readLen)) + { + AHT2X_RecordMeasurementFailure(__func__, "Sensor did not ACK measurement read."); + return; + } + + g_lastStatus = data[0]; if((data[0] & AHT2X_DAT_BUSY) != AHT2X_DAT_BUSY) { ready = true; @@ -96,7 +274,12 @@ void AHT2X_Measure() if(!ready) { - ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Measurements reading timed out.", __func__); + AHT2X_RecordMeasurementFailure(__func__, "Measurement read timed out."); + return; + } + + if(!AHT2X_ValidateCrcIfPresent(data)) + { return; } @@ -106,21 +289,24 @@ void AHT2X_Measure() return; } - uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4; + uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; - g_humid = ((float)raw_humidity * 100.0f / 1048576.0f) + g_calHum; - g_temp = (((200.0f * (float)raw_temperature) / 1048576.0f) - 50.0f) + g_calTemp; + g_humid = ((float)raw_humidity * 100.0f / AHT2X_RAW_DIVISOR) + g_calHum; + g_temp = (((200.0f * (float)raw_temperature) / AHT2X_RAW_DIVISOR) - 50.0f) + g_calTemp; - if(channel_temp > -1) + /* Preserve the existing MQTT/HA path: same channel indexes and same scaling. */ + if(channel_temp >= 0) { CHANNEL_Set(channel_temp, (int)(g_temp * 10), 0); } - if(channel_humid > -1) + if(channel_humid >= 0) { CHANNEL_Set(channel_humid, (int)(g_humid), 0); } + g_measureFailures = 0; + isWorking = true; ADDLOG_INFO(LOG_FEATURE_SENSOR, "%s: Temperature:%fC Humidity:%f%%", __func__, g_temp, g_humid); } @@ -162,6 +348,28 @@ commandResult_t AHT2X_Cycle(const void* context, const char* cmd, const char* ar return CMD_RES_OK; } +commandResult_t AHT2X_CRC(const void* context, const char* cmd, const char* args, int cmdFlags) +{ + Tokenizer_TokenizeString(args, TOKENIZER_ALLOW_QUOTES | TOKENIZER_DONT_EXPAND); + if(Tokenizer_CheckArgsCountAndPrintWarning(cmd, 1)) + { + return CMD_RES_NOT_ENOUGH_ARGUMENTS; + } + + int mode = Tokenizer_GetArgInteger(0); + if(mode < AHT2X_CRC_MODE_OFF || mode > AHT2X_CRC_MODE_REQUIRED) + { + ADDLOG_INFO(LOG_FEATURE_CMD, "%s: Use 0=off, 1=auto, 2=require CRC.", __func__); + return CMD_RES_BAD_ARGUMENT; + } + + g_crcMode = (byte)mode; + g_crcSupported = false; + ADDLOG_INFO(LOG_FEATURE_CMD, "%s: CRC mode is %i (0=off, 1=auto, 2=require).", __func__, g_crcMode); + + return CMD_RES_OK; +} + commandResult_t AHT2X_Force(const void* context, const char* cmd, const char* args, int cmdFlags) { g_aht_secondsUntilNextMeasurement = g_aht_secondsBetweenMeasurements; @@ -194,15 +402,20 @@ void AHT2X_Init() AHT2X_Initialization(); //cmddetail:{"name":"AHT2X_Calibrate","args":"[DeltaTemp][DeltaHumidity]", - //cmddetail:"descr":"Calibrate the AHT2X Sensor as Tolerance is +/-2 degrees C.", + //cmddetail:"descr":"Calibrate the AHT-family sensor reading without changing channel scaling.", //cmddetail:"fn":"AHT2X_Calibrate","file":"driver/drv_aht2x.c","requires":"", //cmddetail:"examples":"AHT2X_Calibrate -4 10
meaning -4 on current temp reading and +10 on current humidity reading"} CMD_RegisterCommand("AHT2X_Calibrate", AHT2X_Calibrate, NULL); //cmddetail:{"name":"AHT2X_Cycle","args":"[IntervalSeconds]", - //cmddetail:"descr":"This is the interval between measurements in seconds, by default 1. Max is 255.", + //cmddetail:"descr":"This is the interval between measurements in seconds, by default 10.", //cmddetail:"fn":"AHT2X_Cycle","file":"driver/drv_aht2x.c","requires":"", //cmddetail:"examples":"AHT2X_Cycle 60
measurement is taken every 60 seconds"} CMD_RegisterCommand("AHT2X_Cycle", AHT2X_Cycle, NULL); + //cmddetail:{"name":"AHT2X_CRC","args":"[0/1/2]", + //cmddetail:"descr":"Set AHT CRC handling. 0 = off/legacy six-byte read (default), 1 = automatic when a valid CRC byte is detected, 2 = require CRC validation.", + //cmddetail:"fn":"AHT2X_CRC","file":"driver/drv_aht2x.c","requires":"", + //cmddetail:"examples":"AHT2X_CRC 1
enable automatic CRC validation where the sensor provides a valid CRC byte"} + CMD_RegisterCommand("AHT2X_CRC", AHT2X_CRC, NULL); //cmddetail:{"name":"AHT2X_Measure","args":"", //cmddetail:"descr":"Retrieve OneShot measurement.", //cmddetail:"fn":"AHT2X_Force","file":"driver/drv_aht2x.c","requires":"", @@ -235,7 +448,7 @@ void AHT2X_AppendInformationToHTTPIndexPage(http_request_t* request, int bPreSta hprintf255(request, "

AHT2X Temperature=%.1fC, Humidity=%.0f%%

", g_temp, g_humid); if(!isWorking) { - hprintf255(request, "WARNING: AHT sensor appears to have failed initialization, check if configured pins are correct!"); + hprintf255(request, "WARNING: AHT sensor appears to have failed initialization or communication, check if configured pins are correct!"); } if(channel_humid == channel_temp) { diff --git a/src/driver/drv_aht2x.h b/src/driver/drv_aht2x.h index a417218ae4..80567505ef 100644 --- a/src/driver/drv_aht2x.h +++ b/src/driver/drv_aht2x.h @@ -1,8 +1,29 @@ -#define AHT2X_CMD_INI 0xBE -#define AHT2X_DAT_INI1 0x08 -#define AHT2X_DAT_INI2 0x00 -#define AHT2X_CMD_TMS 0xAC -#define AHT2X_DAT_TMS1 0x33 -#define AHT2X_DAT_TMS2 0x00 -#define AHT2X_CMD_RST 0xBA -#define AHT2X_DAT_BUSY 0x80 +/* + * AHT family command constants. + * + * Public OpenBeken compatibility note: + * this driver is still registered as AHT2X and keeps the existing AHT2X_* + * commands. AHT10/AHT15 use the documented 0xE1 init command. AHT20/AHT21- + * style devices use the documented 0xBE 0x08 0x00 init sequence. The family + * shares the 0xAC 0x33 0x00 measurement command and 20-bit humidity/temperature + * conversion format, while exact model auto-identification is not available. + */ +#define AHT2X_CMD_INI 0xBE +#define AHT2X_CMD_INI_AHT1X 0xE1 +#define AHT2X_DAT_INI1 0x08 +#define AHT2X_DAT_INI2 0x00 +#define AHT2X_CMD_TMS 0xAC +#define AHT2X_DAT_TMS1 0x33 +#define AHT2X_DAT_TMS2 0x00 +#define AHT2X_CMD_RST 0xBA + +#define AHT2X_DAT_BUSY 0x80 +#define AHT2X_DAT_CALIBRATED 0x08 + +#define AHT2X_READ_LEN 6 +#define AHT2X_READ_LEN_CRC 7 +#define AHT2X_RAW_DIVISOR 1048576.0f + +/* AHT-family CRC8 used by CRC-capable parts: polynomial 0x31, initial value 0xFF. */ +#define AHT2X_CRC8_POLY 0x31 +#define AHT2X_CRC8_INIT 0xFF diff --git a/src/driver/drv_main.c b/src/driver/drv_main.c index 215a13dbf4..7190413c6b 100644 --- a/src/driver/drv_main.c +++ b/src/driver/drv_main.c @@ -1258,7 +1258,7 @@ static driver_t g_drivers[] = { #if ENABLE_DRIVER_AHT2X //drvdetail:{"name":"AHT2X", //drvdetail:"title":"TODO", - //drvdetail:"descr":"AHT Humidity/temperature sensor. Supported sensors are: AHT10, AHT2X, AHT30. See [presentation guide](https://www.elektroda.com/rtvforum/topic4052685.html)", + //drvdetail:"descr":"AHT-family humidity/temperature sensor. Driver name and commands remain AHT2X; supports AHT1x init fallback and the AHT2x-compatible measurement path used by AHT2x/AHT3x-class modules. Exact model auto-identification is not available. See [presentation guide](https://www.elektroda.com/rtvforum/topic4052685.html)", //drvdetail:"requires":""} { "AHT2X", // Driver Name AHT2X_Init, // Init