diff --git a/tasmota/include/tasmota_template.h b/tasmota/include/tasmota_template.h index 6fa170292991..6ed62e4fd729 100644 --- a/tasmota/include/tasmota_template.h +++ b/tasmota/include/tasmota_template.h @@ -122,6 +122,7 @@ enum UserSelectablePins { GPIO_TELEINFO_ENABLE, // Teleinfo Enable Receive Pin GPIO_LMT01, // LMT01 input counting pin GPIO_IEM3000_TX, GPIO_IEM3000_RX, // IEM3000 Serial interface + GPIO_AOX_RX, GPIO_AOX_TX, // AOX3000Z01 UART interface GPIO_ZIGBEE_RST, // Zigbee reset GPIO_DYP_RX, GPIO_MIEL_HVAC_TX, GPIO_MIEL_HVAC_RX, // Mitsubishi Electric HVAC @@ -401,6 +402,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_TELEINFO_RX "|" D_SENSOR_TELEINFO_ENABLE "|" D_SENSOR_LMT01_PULSE "|" D_SENSOR_IEM3000_TX "|" D_SENSOR_IEM3000_RX "|" + D_SENSOR_AOX_RX "|" D_SENSOR_AOX_TX "|" D_SENSOR_ZIGBEE_RST "|" D_SENSOR_DYP_RX "|" D_SENSOR_MIEL_HVAC_TX "|" D_SENSOR_MIEL_HVAC_RX "|" @@ -1173,6 +1175,8 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_C8_CO2_5K_TX), // SC8-CO2-5K Serial interface AGPIO(GPIO_C8_CO2_5K_RX), // SC8-CO2-5K Serial interface #endif + AGPIO(GPIO_AOX_RX), // AOX3000Z01 Serial interface + AGPIO(GPIO_AOX_TX), // AOX3000Z01 Serial interface #ifdef ESP32 #ifdef USE_ESP32_TWAI diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h index fc0a1e1d45d3..50f3d2108357 100644 --- a/tasmota/language/af_AF.h +++ b/tasmota/language/af_AF.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h index 92b1ef06542b..d84663ad2494 100644 --- a/tasmota/language/bg_BG.h +++ b/tasmota/language/bg_BG.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/ca_AD.h b/tasmota/language/ca_AD.h index 0595f28544fe..8ab5de7abfff 100644 --- a/tasmota/language/ca_AD.h +++ b/tasmota/language/ca_AD.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h index 8141b889d755..1290c91500ce 100644 --- a/tasmota/language/cs_CZ.h +++ b/tasmota/language/cs_CZ.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index 760090988eec..17aa2ed4f255 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h index 2d5357fe1398..a447febf9eca 100644 --- a/tasmota/language/el_GR.h +++ b/tasmota/language/el_GR.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index 2f30a6a3d4aa..56bbde40d363 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h index bffc6ecd077f..1895d4cdb390 100644 --- a/tasmota/language/es_ES.h +++ b/tasmota/language/es_ES.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h index bd14341bf38f..7f57285bb0f1 100644 --- a/tasmota/language/fr_FR.h +++ b/tasmota/language/fr_FR.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units diff --git a/tasmota/language/fy_NL.h b/tasmota/language/fy_NL.h index d856bbe32799..26efa3461144 100644 --- a/tasmota/language/fy_NL.h +++ b/tasmota/language/fy_NL.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h index 9a7389071c6e..cb1bbc94dfd6 100644 --- a/tasmota/language/he_HE.h +++ b/tasmota/language/he_HE.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h index f92d994ff522..b142fed65ddb 100644 --- a/tasmota/language/hu_HU.h +++ b/tasmota/language/hu_HU.h @@ -1052,6 +1052,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h index f1b134112f23..823015937239 100644 --- a/tasmota/language/it_IT.h +++ b/tasmota/language/it_IT.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K - TX" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu - TX" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu - RX" +#define D_SENSOR_AOX_RX "AOX - RX" +#define D_SENSOR_AOX_TX "AOX - TX" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h index c751921ccbe1..a75e219eb942 100644 --- a/tasmota/language/ko_KO.h +++ b/tasmota/language/ko_KO.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/lt_LT.h b/tasmota/language/lt_LT.h index d67afe90edc2..e840d10d9c36 100644 --- a/tasmota/language/lt_LT.h +++ b/tasmota/language/lt_LT.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h index abbe8bb203db..47749127a59f 100644 --- a/tasmota/language/nl_NL.h +++ b/tasmota/language/nl_NL.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h index 6554b45105d6..1bedef7cab87 100644 --- a/tasmota/language/pl_PL.h +++ b/tasmota/language/pl_PL.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h index 913622bc25bf..884d2ded965c 100644 --- a/tasmota/language/pt_BR.h +++ b/tasmota/language/pt_BR.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h index 7d2eed1d59d7..1907fa5efac2 100644 --- a/tasmota/language/pt_PT.h +++ b/tasmota/language/pt_PT.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h index d70ddbf1914d..69bee8f36e58 100644 --- a/tasmota/language/ro_RO.h +++ b/tasmota/language/ro_RO.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h index 5d02c386f4e1..b706688d5335 100644 --- a/tasmota/language/ru_RU.h +++ b/tasmota/language/ru_RU.h @@ -1046,6 +1046,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h index e4d967586b1e..8673734fe3f4 100644 --- a/tasmota/language/sk_SK.h +++ b/tasmota/language/sk_SK.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h index 34252434ba09..db40412bb120 100644 --- a/tasmota/language/sv_SE.h +++ b/tasmota/language/sv_SE.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h index e2f79b415e44..0aa53ad48ac3 100644 --- a/tasmota/language/tr_TR.h +++ b/tasmota/language/tr_TR.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h index fb7c9dc18ca4..722c0f74c17b 100644 --- a/tasmota/language/uk_UA.h +++ b/tasmota/language/uk_UA.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "А" diff --git a/tasmota/language/vi_VN.h b/tasmota/language/vi_VN.h index c5d2c3a0002a..c4cecb1efe6d 100644 --- a/tasmota/language/vi_VN.h +++ b/tasmota/language/vi_VN.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h index bca1d949d7a2..0ed92233d9b6 100644 --- a/tasmota/language/zh_CN.h +++ b/tasmota/language/zh_CN.h @@ -1045,6 +1045,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units #define D_UNIT_AMPERE "A" diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h index 83fff1dc4761..1d09104da593 100644 --- a/tasmota/language/zh_TW.h +++ b/tasmota/language/zh_TW.h @@ -1046,6 +1046,8 @@ #define D_SENSOR_C8_CO2_5K_TX "C8-CO2-5K Tx" #define D_SENSOR_MKSKYBLU_TX "MkSkyBlu Tx" #define D_SENSOR_MKSKYBLU_RX "MkSkyBlu Rx" +#define D_SENSOR_AOX_RX "AOX Rx" +#define D_SENSOR_AOX_TX "AOX Tx" // Units // 原則上不翻譯 diff --git a/tasmota/tasmota_xsns_sensor/xsns_126_aox3000z01.ino b/tasmota/tasmota_xsns_sensor/xsns_126_aox3000z01.ino new file mode 100644 index 000000000000..ac20534dc462 --- /dev/null +++ b/tasmota/tasmota_xsns_sensor/xsns_126_aox3000z01.ino @@ -0,0 +1,240 @@ +/* + xsns_126_aox3000z01.ino - AOX3000Z01 oxygen sensor support for Tasmota + + Copyright (C) 2026 by Sven Arke + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Based in part on xsns_102_ld2410s.ino by: + SPDX-FileCopyrightText: 2022 Theo Arends, 2024 md5sum-as (https://github.com/md5sum-as) + + SPDX-License-Identifier: GPL-3.0-only +*/ + +#ifdef USE_AOX3000Z01 +/*********************************************************************************************\ + * AOX3000Z01 - Oxygen sensor (UART streaming) + * + * Important: + * Uses HardwareSerial (Serial2) on ESP32 for stability + * GPIO4 = [AOX Rx] + * + * JSON output: "AOX3000Z01":{"Oxygen":20.9,"Status":"Normal"} + * + * Protocol: 2400 baud, Frame: 0x78 + BC(len) + DATA + FCC(checksum) + * Oxygen: DATA0..DATA1 as uint16 BE, unit = 0.1 %vol O2 + * Status: 0x01=Error, 0x02=Warmup, 0x03=Normal +\*********************************************************************************************/ + +#define XSNS_126 126 + +#ifdef ESP32 + #include + HardwareSerial *AoxSerial = nullptr; +#else + #include + TasmotaSerial *AoxSerial = nullptr; +#endif + +float Aox_oxygen = NAN; +uint8_t Aox_status = 0; +uint32_t Aox_last_valid = 0; +bool Aox_ready = false; +int8_t Aox_rx_pin = -1; +uint32_t Aox_last_data = 0; +int Aox_reinit_count = 0; + +// Frame parser state +uint8_t Aox_buf[32]; +uint8_t Aox_buf_idx = 0; +uint8_t Aox_expected_len = 0; + +void AoxLoop(void) { + if (!Aox_ready || !AoxSerial) return; + + while (AoxSerial->available()) { + uint8_t b = AoxSerial->read(); + Aox_last_data = millis(); + + if (Aox_buf_idx == 0) { + // Wait for header 0x78 + if (b == 0x78) { + Aox_buf[Aox_buf_idx++] = b; + } + continue; + } + + if (Aox_buf_idx == 1) { + // BC (data length) + if (b == 0 || b > 20) { + Aox_buf_idx = 0; // Invalid, reset + continue; + } + Aox_buf[Aox_buf_idx++] = b; + Aox_expected_len = 2 + b + 1; // CC + BC + DATA + FCC + continue; + } + + // Collect remaining bytes + Aox_buf[Aox_buf_idx++] = b; + + if (Aox_buf_idx >= Aox_expected_len) { + // Frame complete - verify checksum + uint16_t sum = 0; + for (int i = 0; i < Aox_expected_len - 1; i++) sum += Aox_buf[i]; + uint8_t fcc_calc = (~sum + 1) & 0xFF; + uint8_t fcc_got = Aox_buf[Aox_expected_len - 1]; + + if (fcc_calc == fcc_got) { + // Valid frame - parse + uint8_t bc = Aox_buf[1]; + if (bc >= 2) { + uint16_t raw_o2 = (Aox_buf[2] << 8) | Aox_buf[3]; + Aox_oxygen = raw_o2 / 10.0f; + } + if (bc >= 7) { + Aox_status = Aox_buf[8]; + } + Aox_last_valid = millis(); + } + Aox_buf_idx = 0; // Reset for next frame + } + } + // Auto-reinit if 5s no data (AOX3000 serial hangs) + if ((millis() - Aox_last_data > 5000) && (Aox_last_valid == 0)) { + Aox_reinit_count++; + AddLog(LOG_LEVEL_INFO, PSTR("AOX: No data - reinit #%d..."), Aox_reinit_count); + Aox_ready = false; + if (AoxSerial) { + AoxSerial->end(); + delay(50); + delete AoxSerial; + AoxSerial = nullptr; + } + AoxInit(); + Aox_last_data = millis(); + } +} + +void AoxEverySecond(void) { + if (!Aox_ready) return; + + // Debug: show bytes available + int avail = AoxSerial ? AoxSerial->available() : 0; + AddLog(LOG_LEVEL_DEBUG, PSTR("AOX: avail=%d last=%dms"), avail, Aox_last_valid ? (millis() - Aox_last_valid) : -1); + + // Log current value once per second + if (Aox_last_valid && (millis() - Aox_last_valid < 5000)) { + AddLog(LOG_LEVEL_DEBUG, PSTR("AOX: O2=%.1f%% Status=%d"), Aox_oxygen, Aox_status); + } + + // Invalidate if no valid data for 15 seconds + if (Aox_last_valid && (millis() - Aox_last_valid > 15000)) { + Aox_oxygen = NAN; + Aox_status = 0; + Aox_last_valid = 0; + AddLog(LOG_LEVEL_DEBUG, PSTR("AOX: No data - invalidated")); + } +} + +void AoxInit(void) { + if (!PinUsed(GPIO_AOX_RX)) return; + + Aox_rx_pin = Pin(GPIO_AOX_RX); + + #ifdef ESP32 + // Set pin HIGH immediately (UART idle state) + pinMode(Aox_rx_pin, INPUT); + if (AoxSerial) { + AoxSerial->end(); + delay(50); + delete AoxSerial; + AoxSerial = nullptr; + } + // Use HardwareSerial (Serial2) on ESP32 + AoxSerial = new HardwareSerial(2); + + // Force GPIO mux to a known state (helps on ESP32 in some environments) + pinMode(Aox_rx_pin, INPUT); + delay(50); + + AoxSerial->setRxBufferSize(512); // Set buffer BEFORE begin() + AoxSerial->begin(2400, SERIAL_8N1, Aox_rx_pin, -1); // RX only, no TX + // Flush any garbage + while(AoxSerial->available()) AoxSerial->read(); + Aox_ready = true; + AddLog(LOG_LEVEL_INFO, PSTR("AOX: HardwareSerial2 on GPIO%d @ 2400 baud"), Aox_rx_pin); + #else + // ESP8266: use TasmotaSerial + AoxSerial = new TasmotaSerial(Aox_rx_pin, -1, 1, 0, 256); + if (AoxSerial && AoxSerial->begin(2400)) { + if (AoxSerial->hardwareSerial()) { ClaimSerial(); } + Aox_ready = true; + AddLog(LOG_LEVEL_INFO, PSTR("AOX: TasmotaSerial on GPIO%d @ 2400 baud"), Aox_rx_pin); + } +#endif +} + +void AoxShow(bool json) { + if (!Aox_ready) return; + + const char *status_str = (Aox_status == 0x01) ? "Error" : + (Aox_status == 0x02) ? "Warmup" : + (Aox_status == 0x03) ? "Normal" : "Unknown"; + + if (json) { + if (!isnan(Aox_oxygen)) { + ResponseAppend_P(PSTR(",\"AOX3000Z01\":{\"Oxygen\":%1_f,\"Status\":\"%s\"}"), &Aox_oxygen, status_str); + } else { + ResponseAppend_P(PSTR(",\"AOX3000Z01\":{\"Oxygen\":null,\"Status\":\"%s\"}"), status_str); + } +#ifdef USE_WEBSERVER + } else { + if (!isnan(Aox_oxygen)) { + WSContentSend_PD(PSTR("{s}AOX3000Z01 O2{m}%1_f " D_UNIT_PERCENT "{e}"), &Aox_oxygen); + WSContentSend_PD(PSTR("{s}AOX3000Z01 Status{m}%s{e}"), status_str); + } +#endif + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns126(uint32_t function) { + bool result = false; + switch (function) { + case FUNC_INIT: + AoxInit(); + break; + case FUNC_LOOP: + AoxLoop(); + break; + case FUNC_EVERY_SECOND: + AoxEverySecond(); + break; + case FUNC_JSON_APPEND: + AoxShow(true); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + AoxShow(false); + break; +#endif + } + return result; +} + +#endif // USE_AOX3000Z01