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