diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6733ad555..778d93f3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## [13.1.0.2]
### Added
+- Support for HDMI CEC protocol
### Breaking Changed
diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h
index 94819a7d0..25391db30 100644
--- a/tasmota/include/i18n.h
+++ b/tasmota/include/i18n.h
@@ -771,6 +771,13 @@
// Commands xdrv_60_shift595.ino - 74x595 family shift register driver
#define D_CMND_SHIFT595_DEVICE_COUNT "Shift595DeviceCount"
+// Commands xdrv_70_hdmi_cec.ino
+#define D_PRFX_HDMI "Hdmi"
+#define D_CMND_HDMI_SEND "Send"
+#define D_CMND_HDMI_SEND_RAW "SendRaw"
+#define D_CMND_HDMI_TYPE "Type"
+#define D_CMND_HDMI_ADDR "Addr"
+
// Commands xdrv_89_dali.ino
#define D_CMND_DALI_POWER "power"
#define D_CMND_DALI_DIMMER "dim"
diff --git a/tasmota/include/tasmota_template.h b/tasmota/include/tasmota_template.h
index 462b56fb4..9a689813f 100644
--- a/tasmota/include/tasmota_template.h
+++ b/tasmota/include/tasmota_template.h
@@ -211,6 +211,7 @@ enum UserSelectablePins {
GPIO_LOX_O2_RX, // LOX-O2 RX
GPIO_GM861_TX, GPIO_GM861_RX, // GM861 Serial interface
GPIO_DINGTIAN_OE, // New version of Dingtian relay board where PL is not shared with OE
+ GPIO_HDMI_CEC, // Support for HDMI CEC
GPIO_SENSOR_END };
// Error as warning to rethink GPIO usage with max 2045
@@ -469,6 +470,7 @@ const char kSensorNames[] PROGMEM =
D_SENSOR_LOX_O2_RX "|"
D_SENSOR_GM861_TX "|" D_SENSOR_GM861_RX "|"
D_GPIO_DINGTIAN_OE "|"
+ D_SENSOR_HDMI_CEC "|"
;
const char kSensorNamesFixed[] PROGMEM =
@@ -697,6 +699,10 @@ const uint16_t kGpioNiceList[] PROGMEM = {
AGPIO(GPIO_MCP23XXX_INT) + MAX_MCP23XXX,
#endif
+#ifdef USE_HDMI_CEC
+ AGPIO(GPIO_HDMI_CEC), // HDMI CEC bus
+#endif
+
AGPIO(GPIO_TXD), // Serial interface
AGPIO(GPIO_RXD), // Serial interface
diff --git a/tasmota/include/tasmota_types.h b/tasmota/include/tasmota_types.h
index bd0f5c23b..7a15e0c00 100644
--- a/tasmota/include/tasmota_types.h
+++ b/tasmota/include/tasmota_types.h
@@ -742,7 +742,7 @@ typedef struct {
uint16_t modbus_sbaudrate; // 736
uint16_t shutter_motorstop; // 738
uint8_t battery_level_percent; // 73A
- uint8_t free_73B[2]; // 73B
+ uint8_t hdmi_addr[2]; // 73B HDMI CEC physical address - warning this is a non-aligned uint16
uint8_t novasds_startingoffset; // 73D
uint8_t web_color[18][3]; // 73E
@@ -845,7 +845,7 @@ typedef struct {
uint8_t tcp_config; // F5F
uint8_t light_step_pixels; // F60
- uint8_t ex_modbus_sbaudrate; // F61 - v12.2.0.5
+ uint8_t hdmi_cec_device_type; // F61 - v13.1.0.1 (was ex_modbus_sbaudrate v12.2.0.5)
uint8_t modbus_sconfig; // F62
diff --git a/tasmota/language/af_AF.h b/tasmota/language/af_AF.h
index 5ca6ee4e9..84c94b67b 100644
--- a/tasmota/language/af_AF.h
+++ b/tasmota/language/af_AF.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Speler"
#define D_SENSOR_DFR562_BUSY "MP3 Bezet"
diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h
index d7848b3a8..d39ca9b50 100644
--- a/tasmota/language/bg_BG.h
+++ b/tasmota/language/bg_BG.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/ca_AD.h b/tasmota/language/ca_AD.h
index 7da0c2146..7c134ab31 100644
--- a/tasmota/language/ca_AD.h
+++ b/tasmota/language/ca_AD.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "Reproductor MP3"
#define D_SENSOR_DFR562_BUSY "MP3 Ocupat"
diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h
index 11ad00c90..e9115ac23 100644
--- a/tasmota/language/cs_CZ.h
+++ b/tasmota/language/cs_CZ.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h
index 46e3561bd..5a7b79ab0 100644
--- a/tasmota/language/de_DE.h
+++ b/tasmota/language/de_DE.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h
index dbff83df3..b8d96c3ba 100644
--- a/tasmota/language/el_GR.h
+++ b/tasmota/language/el_GR.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h
index 92aa33b39..46169be2f 100644
--- a/tasmota/language/en_GB.h
+++ b/tasmota/language/en_GB.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h
index 8c0d1fa3b..1d2ae8913 100644
--- a/tasmota/language/es_ES.h
+++ b/tasmota/language/es_ES.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h
index da9fc7ea0..79201509f 100644
--- a/tasmota/language/fr_FR.h
+++ b/tasmota/language/fr_FR.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS In"
#define D_SENSOR_I2S_DIN "I2S DIn"
#define D_SENSOR_I2S_DOUT "I2S DOut"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/fy_NL.h b/tasmota/language/fy_NL.h
index 19305a24c..d1cb3d46a 100644
--- a/tasmota/language/fy_NL.h
+++ b/tasmota/language/fy_NL.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Speler"
#define D_SENSOR_DFR562_BUSY "MP3 Bezet"
diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h
index 84dbb6562..6d7c67695 100644
--- a/tasmota/language/he_HE.h
+++ b/tasmota/language/he_HE.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "נגן מוזיקה"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h
index d067c5665..c99e4398e 100644
--- a/tasmota/language/hu_HU.h
+++ b/tasmota/language/hu_HU.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 lejátszó"
#define D_SENSOR_DFR562_BUSY "MP3 elfoglalt"
diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h
index c661b5552..d79c50b4e 100644
--- a/tasmota/language/it_IT.h
+++ b/tasmota/language/it_IT.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S - WS IN"
#define D_SENSOR_I2S_DIN "I2S - DIN"
#define D_SENSOR_I2S_DOUT "I2S - DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "Riproduttore MP3"
#define D_SENSOR_DFR562_BUSY "MP3 occupato"
diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h
index 6d72f296c..d27a8aad8 100644
--- a/tasmota/language/ko_KO.h
+++ b/tasmota/language/ko_KO.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h
index 5a7893293..c6325ed83 100644
--- a/tasmota/language/nl_NL.h
+++ b/tasmota/language/nl_NL.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Speler"
#define D_SENSOR_DFR562_BUSY "MP3 Bezet"
diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h
index 291fbcd46..255f75f78 100644
--- a/tasmota/language/pl_PL.h
+++ b/tasmota/language/pl_PL.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "Odtwarzacz MP3"
#define D_SENSOR_DFR562_BUSY "MP3 zajęty"
diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h
index a315ce557..d80314355 100644
--- a/tasmota/language/pt_BR.h
+++ b/tasmota/language/pt_BR.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h
index 14b1d80d6..1511eb78e 100644
--- a/tasmota/language/pt_PT.h
+++ b/tasmota/language/pt_PT.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "Leitor de MP3"
#define D_SENSOR_DFR562_BUSY "MP3 Ocupado"
diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h
index 7444d914d..a03db3241 100644
--- a/tasmota/language/ro_RO.h
+++ b/tasmota/language/ro_RO.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h
index 34cbec772..437805f0d 100644
--- a/tasmota/language/ru_RU.h
+++ b/tasmota/language/ru_RU.h
@@ -656,6 +656,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h
index 2037c0c16..2ea6ab24a 100644
--- a/tasmota/language/sk_SK.h
+++ b/tasmota/language/sk_SK.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h
index b9c856950..79f156331 100644
--- a/tasmota/language/sv_SE.h
+++ b/tasmota/language/sv_SE.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 spelare"
#define D_SENSOR_DFR562_BUSY "MP3 upptaget"
diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h
index 41ddd0f18..e03b0fa77 100644
--- a/tasmota/language/tr_TR.h
+++ b/tasmota/language/tr_TR.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h
index 7db07e448..7a4f6d05b 100644
--- a/tasmota/language/uk_UA.h
+++ b/tasmota/language/uk_UA.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/vi_VN.h b/tasmota/language/vi_VN.h
index c2a617929..f9395cf97 100644
--- a/tasmota/language/vi_VN.h
+++ b/tasmota/language/vi_VN.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h
index 84a52281f..46820d341 100644
--- a/tasmota/language/zh_CN.h
+++ b/tasmota/language/zh_CN.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h
index 2a767d3a6..d077466bc 100644
--- a/tasmota/language/zh_TW.h
+++ b/tasmota/language/zh_TW.h
@@ -655,6 +655,7 @@
#define D_SENSOR_I2S_BCLK_IN "I2S WS IN"
#define D_SENSOR_I2S_DIN "I2S DIN"
#define D_SENSOR_I2S_DOUT "I2S DOUT"
+#define D_SENSOR_HDMI_CEC "HDMI CEC"
#define D_SENSOR_WS2812 "WS2812"
#define D_SENSOR_DFR562 "MP3 Player"
#define D_SENSOR_DFR562_BUSY "MP3 Busy"
diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h
index e53b0c172..007959931 100644
--- a/tasmota/my_user_config.h
+++ b/tasmota/my_user_config.h
@@ -783,6 +783,9 @@
#endif // USE_SPI
+// -- One wire sensors ----------------------------
+// #define USE_HDMI_CEC // Add support for HDMI CEC bus (+7k code)
+
// -- Serial sensors ------------------------------
//#define USE_MHZ19 // Add support for MH-Z19 CO2 sensor (+2k code)
//#define USE_SENSEAIR // Add support for SenseAir K30, K70 and S8 CO2 sensor (+2k3 code)
diff --git a/tasmota/tasmota_support/settings.ino b/tasmota/tasmota_support/settings.ino
index dc0c31f9b..46173d79e 100644
--- a/tasmota/tasmota_support/settings.ino
+++ b/tasmota/tasmota_support/settings.ino
@@ -1731,7 +1731,7 @@ void SettingsDelta(void) {
Settings->energy_current_calibration2 = Settings->energy_current_calibration;
}
if (Settings->version < 0x0C020005) { // 12.2.0.5
- Settings->modbus_sbaudrate = Settings->ex_modbus_sbaudrate;
+ Settings->modbus_sbaudrate = Settings->hdmi_cec_device_type; // was ex_modbus_sbaudrate
Settings->param[P_SERIAL_SKIP] = 0;
}
if (Settings->version < 0x0C030102) { // 12.3.1.2
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_70_0_hdmi_cec.ino b/tasmota/tasmota_xdrv_driver/xdrv_70_0_hdmi_cec.ino
new file mode 100644
index 000000000..b2c044165
--- /dev/null
+++ b/tasmota/tasmota_xdrv_driver/xdrv_70_0_hdmi_cec.ino
@@ -0,0 +1,1189 @@
+/*
+ xdrv_70_9_hdmi_cec.ino - support for HDMI CEC bus (control TV via HDMI)
+
+ Copyright (C) 2021 Theo Arends, Stephan Hadinger
+
+ 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 .
+*/
+
+
+#ifdef USE_HDMI_CEC
+/*********************************************************************************************\
+ * Macros used for debug mode
+\*********************************************************************************************/
+
+#define HDMI_DEBUG // remove once stabilized
+
+#ifdef HDMI_DEBUG
+ #define ASSERT_LINE(x) \
+ { \
+ if (electrical_line_state != x) { \
+ _ring_buffer.log(0, 0, time_from_start_bit, 0xEE00 + x, _state, lineState()); \
+ _state = CEC_IDLE; \
+ break; \
+ } \
+ }
+#else
+ #define ASSERT_LINE(x)
+#endif
+
+volatile uint32_t cec_isr_count = 0;
+volatile uint32_t cec_isr_rcv_count = 0;
+
+/*********************************************************************************************\
+ * Ring buffer class that allows the ISR to publish logs and messages to the main event loop
+ *
+ * The ring buffer contains `OnReceiveComplete` messages for incoming messages
+ * and logs if debug mode is enabled
+\*********************************************************************************************/
+
+class CEC_RingBuffer {
+public:
+ static const uint32_t CEC_BUF_SIZE = 16; // buffer size in bytes for each message
+ // static const uint32_t CEC_RING_SIZE = 8; // size of ring buffer
+ static const uint32_t CEC_RING_SIZE = 32; // size of ring buffer TODO reduce when in production
+ typedef enum {
+ MSG_EMPTY = 0,
+ MSG_RECEIVED,
+ MSG_TRANSMITTED,
+ MSG_LOG, // debug log
+ } CEC_MSG_TYPE;
+
+ typedef struct {
+ volatile CEC_MSG_TYPE type;
+ bool ack;
+ uint8_t len;
+ uint8_t buf[CEC_BUF_SIZE];
+#ifdef HDMI_DEBUG
+ // for debugging
+ uint32_t timing_expected_low;
+ uint32_t timing_expected_high;
+ uint32_t timing;
+ uint16_t code; // hex
+ uint8_t state;
+ bool line;
+#endif
+ } CEC_msg_t;
+
+ CEC_RingBuffer() {
+ clear();
+ }
+
+ // clear all messages
+ void clear() {
+ _cur_idx = 0;
+ for (uint32_t i = 0; i < CEC_RING_SIZE; i++) {
+ _msg[i].type = MSG_EMPTY;
+ }
+ }
+
+ // returns true if ok, false if full
+ bool IRAM_ATTR push(const volatile uint8_t *buf, uint8_t len, CEC_MSG_TYPE type, bool ack) {
+ // AddLog(LOG_LEVEL_INFO, ">>>: push len=%i type=%i ack=%i", len, type, ack);
+ if (_msg[_cur_idx].type != MSG_EMPTY) {
+ return false;
+ }
+ memmove(_msg[_cur_idx].buf, (uint8_t*) buf, CEC_BUF_SIZE);
+ _msg[_cur_idx].ack = ack;
+ _msg[_cur_idx].len = len;
+ _msg[_cur_idx].type = type;
+ _cur_idx = (_cur_idx + 1) % CEC_RING_SIZE;
+ return true;
+ }
+
+ // returns true if ok, false if full
+ bool IRAM_ATTR log(uint32_t timing_expected_low, uint32_t timing_expected_high, uint32_t timing, uint16_t code, uint8_t state, bool line) {
+#ifdef HDMI_DEBUG
+ if (_msg[_cur_idx].type != MSG_EMPTY) {
+ return false;
+ }
+ _msg[_cur_idx].type = MSG_LOG;
+ _msg[_cur_idx].timing_expected_low = timing_expected_low;
+ _msg[_cur_idx].timing_expected_high = timing_expected_high;
+ _msg[_cur_idx].timing = timing;
+ _msg[_cur_idx].code = code;
+ _msg[_cur_idx].state = state;
+ _msg[_cur_idx].line = line;
+ _cur_idx = (_cur_idx + 1) % CEC_RING_SIZE;
+#endif
+ return true;
+ }
+
+ // pull the next message
+ // returns the index in the ring buffer,
+ // or -1 if no message
+ //
+ // It is the caller's responsibility to clear the message
+ int32_t pullMsgIdx() {
+ volatile uint32_t cur_idx = _cur_idx; // freeze value, since it can be changed by an interrupt
+ for (uint32_t i = 0; i < CEC_RING_SIZE; i++) {
+ uint32_t idx = (cur_idx + i) % CEC_RING_SIZE;
+ if (_msg[idx].type != MSG_EMPTY) {
+ return idx;
+ }
+ }
+ return -1;
+ }
+
+ // clear message from ring buffer
+ void ackMsg(uint32_t idx) {
+ if (idx >= 0 && idx < CEC_RING_SIZE) {
+ _msg[idx].type = MSG_EMPTY;
+ }
+ }
+
+public:
+ volatile uint32_t _cur_idx = 0; // current index in Ring Buffer
+ CEC_msg_t _msg[CEC_RING_SIZE]; // messages in ring buffer
+};
+
+/*********************************************************************************************\
+ * customized lib from https://github.com/lucadentella/ArduinoLib_CEClient
+ *
+ * The library has been refactored along the following lines:
+ * - Split the state machine between Rx and Tx
+ * - Tx is now in blocking mode to avoid any interference with other code
+ * and ensure precise timing
+ * - Rx is mostly in ISR mode which is far more robust than relying on a fast event loop.
+ * Therefore it needs to publish the incoming messages to a static ring buffer.
+ * The messages are then passed to Tasmota during the next tick (event loop).
+\*********************************************************************************************/
+
+class CEC_Device
+{
+public:
+ // device types as defined by HDMI CEC standard
+ // a logical address is negociated during the first exchange with the CEC bus
+ typedef enum {
+ CDT_TV = 0,
+ CDT_RECORDING_DEVICE, // 1
+ CDT_RESERVED, // 2
+ CDT_TUNER, // 3
+ CDT_PLAYBACK_DEVICE, // 4
+ CDT_AUDIO_SYSTEM, // 5
+ CDT_LAST
+ } CEC_DEVICE_TYPE;
+
+ typedef enum {
+ OP_ACTIVE_SOURCE = 0x82,
+ OP_IMAGE_VIEW = 0x04,
+ OP_TEXT_VIEW_ON = 0x0D,
+ OP_INACTIVE_SOURCE = 0x9D,
+ OP_REQUEST_ACITVE_SOURCE = 0x85,
+ OP_ROUTING_CHANGE = 0x80,
+ OP_ROUTING_INFORMATION = 0x81,
+ OP_SET_STREAM_PATH = 0x86,
+ OP_STANDBY = 0x36,
+ OP_RECORD_OFF = 0x0B,
+ OP_RECORD_ON = 0x09,
+ OP_RECORD_STATUS = 0x0A,
+ OP_RECORD_TV_SCREEN = 0x0F,
+ OP_CLEAR_ANALOGUE_TIMER = 0x33,
+ OP_CLEAR_DIGITAL_TIMER = 0x99,
+ OP_CLEAR_EXTERNAL_TIMER = 0xA1,
+ OP_SET_ANALOGUE_TIMER = 0x34,
+ OP_SET_DIGITAL_TIMER = 0x97,
+ OP_SET_EXTERNAL_TIMER = 0xA2,
+ OP_SET_TIMER_PROGRAM_TITLE = 0x67,
+ OP_TIMER_CLEARED_STATUS = 0x43,
+ OP_TIMER_STATUS = 0x35,
+ OP_CEC_VERSION = 0x9E,
+ OP_GET_CEC_VERSION = 0x9F,
+ OP_GIVE_PHYSICAL_ADDRESS = 0x83,
+ OP_GET_MENU_LANGUAGE = 0x91,
+ OP_REPORT_PHYSICAL_ADDRESS = 0x84,
+ OP_SET_MENU_LANGUAGE = 0x32,
+ OP_DECK_CONTROL = 0x42,
+ OP_DECK_STATUS = 0x1B,
+ OP_GIVE_DECK_STATUS = 0x1A,
+ OP_PLAY = 0x41,
+ OP_GIVE_TUNER_DEVICE_STATUS = 0x08,
+ OP_SELECT_ANALOGUE_DEVICE = 0x92,
+ OP_SELECT_DIGITAL_DEVICE = 0x93,
+ OP_TUNER_DEVICE_STATUS = 0x07,
+ OP_TUNER_STEP_DECFREMENT = 0x06,
+ OP_TUNER_STEP_INCREMENT = 0x05,
+ OP_DEVICE_VENDOR_ID = 0x87,
+ OP_GIVE_DEVICE_VENDOR_ID = 0x8C,
+ OP_VENDOR_COMMAND = 0x89,
+ OP_VENDOR_COMMAND_WITH_ID = 0xA0,
+ OP_VENDOR_REMOTE_BUTTON_DOWN = 0x8A,
+ OP_VENDOR_REMOTE_BUTTON_UP = 0x8B,
+ OP_SET_OSD_SCREEN = 0x64,
+ OP_GIVE_OSD_SCREEN = 0x46,
+ OP_SET_OSD_NAME = 0x47,
+ OP_MENU_REQUEST = 0x8D,
+ OP_MENU_STATUS = 0x8E,
+ OP_USER_CONTROL_PRESSED = 0x44,
+ OP_USER_CONTROL_RELEASED = 0x45,
+ OP_GIVE_DEVICE_POWER_STATUS = 0x8F,
+ OP_REPORT_POWER_STATUS = 0x90,
+ OP_FEATURE_ABORT = 0x00,
+ OP_ABORT = 0xFF,
+ OP_GIVE_AUDIO_STATUS = 0x71,
+ OP_GIVE_SYSTEM_AUDIO_MODE_STATUS = 0x7D,
+ OP_REPORT_AUDIO_STATUS = 0x7A,
+ OP_SET_SYSTEM_AUDIO_MODE = 0x72,
+ OP_SYSTEM_AUDIO_MODE_REQUEST = 0x70,
+ OP_SYSTEM_AUDIO_MODE_STATUS = 0x7E,
+ OP_SET_AUDIO_RATE = 0x9A,
+ } CEC_OPCODES;
+
+public:
+ // Low-level
+ CEC_Device(int gpio, CEC_DEVICE_TYPE type, bool promiscuous = false, bool monitor_mode = false);
+ void run(); // this must be called at each millisecond tick
+ bool IRAM_ATTR isTransmitting() const { return _transmit_buffer_bytes != 0; }
+ void checkMessages(); // check regularly for mailbox
+ // signal to Tasmota to exit the normal sleep and trigger a new tick event in the next millisecond
+ void enableISR(void);
+ void IRAM_ATTR serviceGpioISR(void); // handle the ISR on the CEC GPIO
+ bool transmitRaw(const unsigned char* buffer, unsigned int count);
+
+ // Getters
+ inline int32_t getPhysicalAddress() const { return _physical_address; }
+ inline int32_t getLogicalAddress() const { return _logical_address; }
+ int32_t getGPIO() const { return _gpio; }
+ CEC_DEVICE_TYPE getType() const { return _type; }
+ uint32_t getVendorId() const { return _vendor_id; }
+
+ // High-level protocol
+ typedef void (*OnReceiveCallback_t)(CEC_Device *self, int32_t from, int32_t to, uint8_t* buf, size_t len, bool ack);
+ typedef void (*OnTransmitCallback_t)(CEC_Device *self, uint8_t* buf, size_t len, bool ack);
+ typedef void (*OnReadyCallback_t)(CEC_Device *self, int logical_address);
+
+ // set callbacks
+ void setOnReceiveCB(OnReceiveCallback_t cb) { _on_rx_cb = cb; }
+ void setOnTransmitCB(OnTransmitCallback_t cb) { _on_tx_cb = cb; }
+ void setOnReadyCB(OnReadyCallback_t cb) { _on_ready_cb = cb; }
+ void setVendorID(uint32_t vendor) { _vendor_id = vendor; }
+
+ // general methods
+ void start(void);
+ uint16_t discoverPhysicalAddress(); // return 0xFFFF if not found
+ bool transmitFrame(int targetAddress, const unsigned char* buffer, int count);
+
+protected:
+ // void Initialize(int gpio, CEC_DEVICE_TYPE type, bool promiscuous = false, bool monitor_mode = false);
+ void runTransmit();
+ void IRAM_ATTR runReceiveISR();
+ void OnReceiveComplete(uint8_t* buffer, size_t count, bool ack);
+ void OnTransmitComplete(uint8_t* buffer, size_t count, bool ack);
+ void OnReady(int getLogicalAddress);
+
+private:
+ bool IRAM_ATTR lineState();
+ bool IRAM_ATTR setLineState(bool state, bool check = false);
+ bool transmit(int sourceAddress, int targetAddress, const unsigned char* buffer, unsigned int count);
+
+protected:
+ // enums
+ // CEC locical address handling
+ typedef enum {
+ CLA_TV = 0,
+ CLA_RECORDING_DEVICE_1, // 1
+ CLA_RECORDING_DEVICE_2, // 2
+ CLA_TUNER_1, // 3
+ CLA_PLAYBACK_DEVICE_1, // 4
+ CLA_AUDIO_SYSTEM, // 5
+ CLA_TUNER_2, // 6
+ CLA_TUNER_3, // 7
+ CLA_PLAYBACK_DEVICE_2, // 8
+ CLA_RECORDING_DEVICE_3, // 9
+ CLA_TUNER_4, //10
+ CLA_PLAYBACK_DEVICE_3, //11
+ CLA_RESERVED_1, //12
+ CLA_RESERVED_2, //13
+ CLA_FREE_USE, //14
+ CLA_UNREGISTERED, //15
+ } CEC_LOGICAL_ADDRESS;
+
+ // State machine
+ typedef enum {
+ CEC_IDLE, // 0
+
+ CEC_RCV_STARTBIT1, // 1
+ CEC_RCV_STARTBIT2, // 2
+ CEC_RCV_DATABIT1, // 3
+ CEC_RCV_DATABIT2, // 4
+ CEC_RCV_EOM1, // 5
+ CEC_RCV_EOM2, // 6
+ CEC_RCV_ACK_SENT, // 7
+ CEC_RCV_ACK1, // 8
+ CEC_RCV_ACK2, // 9
+ CEC_RCV_LINEERROR, //10
+
+ CEC_XMIT_WAIT, //11
+ CEC_XMIT_STARTBIT1, //12
+ CEC_XMIT_STARTBIT2, //13
+ CEC_XMIT_DATABIT1, //14
+ CEC_XMIT_DATABIT2, //15
+ CEC_XMIT_EOM1, //16
+ CEC_XMIT_EOM2, //17
+ CEC_XMIT_ACK1, //18
+ CEC_XMIT_ACK_TEST, //19
+ CEC_XMIT_ACK_WAIT, //20
+ CEC_XMIT_ACK2, //21
+ } CEC_STATE;
+
+ // timing information for HDMI CEC protocol
+ enum {
+ STARTBIT_TIME_LOW = 3700, // 3.7ms
+ STARTBIT_TIME = 4500, // 4.5ms
+ STARTBIT_TIMEOUT = 5000,
+ BIT_TIME_LOW_0 = 1500, // 1.5ms
+ BIT_TIME_LOW_1 = 600, // 0.6ms
+ BIT_TIME_SAMPLE = 1050, // 1.05ms
+ BIT_TIME = 2400, // 2.4ms
+ BIT_TIMEOUT = 2900,
+ BIT_TIME_ERR = 3600, // 3.6ms
+ BIT_TIME_LOW_MARGIN = 300, // 0.2ms plus some additional margin since we poll the bitline
+ BIT_TIME_MARGIN = 450, // 0.35ms plus some additional margin since we poll the bitline
+ };
+
+ // timing information for HIGH/LOW stabilization
+ enum {
+ CEC_MAX_RISE_TIME = 250, // 250us
+ CEC_MAX_FALL_TIME = 50, // 50us
+ };
+
+ enum {
+ CEC_MAX_RETRANSMIT = 5,
+ };
+
+ enum {
+ CEC_DEFAULT_VENDOR_ID = 0x012345
+ };
+
+ bool _promiscuous;
+ bool _monitor_mode;
+
+ int32_t _physical_address;
+ int32_t _logical_address;
+ bool _logical_address_reported; // was onReady() message sent?
+ const uint8_t *_valid_logical_addr;
+
+protected:
+ // Receive buffer
+ static const uint32_t MAILBOX_MSG_SIZE = 16;
+
+ volatile uint8_t _receive_buffer[MAILBOX_MSG_SIZE] = {0};
+ volatile uint32_t _receive_buffer_bits = 0;
+
+ // transmit buffer
+ uint8_t _transmit_buffer[MAILBOX_MSG_SIZE];
+ volatile uint32_t _transmit_buffer_bytes; // volatile because it is used in ISR to know if Transmitting is in progress
+ unsigned int _transmitBufferBitIdx;
+
+ bool _line_state_expected;
+ uint32_t _bit_start_time;
+ uint32_t _wait_after_start_bit_us;
+
+ // callbacks
+ OnReceiveCallback_t _on_rx_cb;
+ OnTransmitCallback_t _on_tx_cb;
+ OnReadyCallback_t _on_ready_cb;
+
+ uint32_t _vendor_id;
+
+ int32_t _xmitretry;
+
+ bool _eom; // end of message
+ bool _ack;
+ bool _follower;
+ bool _broadcast;
+ bool _am_last_transmittor;
+
+ volatile CEC_STATE _state;
+
+protected:
+ // Tasmota specific
+ int32_t _gpio;
+ CEC_DEVICE_TYPE _type;
+ CEC_RingBuffer _ring_buffer;
+ uint32_t _xmit_wait_ms; // number of milliseconds to wait before starting transmission
+};
+
+/*********************************************************************************************\
+ * Class implementation
+\*********************************************************************************************/
+
+CEC_Device::CEC_Device(int gpio, CEC_DEVICE_TYPE type, bool promiscuous, bool monitor_mode) :
+ _monitor_mode(true),
+ _promiscuous(false),
+ _logical_address(-1),
+ _logical_address_reported(false),
+ _state(CEC_IDLE),
+ _receive_buffer_bits(0),
+ _transmit_buffer_bytes(0),
+ _am_last_transmittor(false),
+ _bit_start_time(0),
+ _wait_after_start_bit_us(0),
+ _on_rx_cb(nullptr),
+ _on_tx_cb(nullptr),
+ _on_ready_cb(nullptr),
+ _vendor_id(CEC_DEFAULT_VENDOR_ID),
+ _xmit_wait_ms(0)
+{
+ static const uint8_t valid_LogicalAddressesTV[3] = {CLA_TV, CLA_FREE_USE, CLA_UNREGISTERED};
+ static const uint8_t valid_LogicalAddressesRec[4] = {CLA_RECORDING_DEVICE_1, CLA_RECORDING_DEVICE_2, CLA_RECORDING_DEVICE_3, CLA_UNREGISTERED};
+ static const uint8_t valid_LogicalAddressesPlay[4] = {CLA_PLAYBACK_DEVICE_1, CLA_PLAYBACK_DEVICE_2, CLA_PLAYBACK_DEVICE_3, CLA_UNREGISTERED};
+ static const uint8_t valid_LogicalAddressesTuner[5] = {CLA_TUNER_1, CLA_TUNER_2, CLA_TUNER_3, CLA_TUNER_4, CLA_UNREGISTERED};
+ static const uint8_t valid_LogicalAddressesAudio[2] = {CLA_AUDIO_SYSTEM, CLA_UNREGISTERED};
+ switch(type) {
+ case CDT_TV: _valid_logical_addr = valid_LogicalAddressesTV; break;
+ case CDT_RECORDING_DEVICE: _valid_logical_addr = valid_LogicalAddressesRec; break;
+ case CDT_PLAYBACK_DEVICE: _valid_logical_addr = valid_LogicalAddressesPlay; break;
+ case CDT_TUNER: _valid_logical_addr = valid_LogicalAddressesTuner; break;
+ case CDT_AUDIO_SYSTEM: _valid_logical_addr = valid_LogicalAddressesAudio; break;
+ default: _valid_logical_addr = NULL;
+ }
+
+ _promiscuous = promiscuous;
+ _monitor_mode = monitor_mode;
+ _physical_address = discoverPhysicalAddress();
+ _logical_address = -1;
+ _gpio = gpio;
+ _type = type;
+}
+
+void CEC_Device::start(void) {
+ setLineState(1); // default state is HIGH
+ enableISR(); // start interrupt handler
+
+ // to allocate a logical address when physical address is valid
+ if (_valid_logical_addr && _physical_address != 0xffff) {
+ transmit(*_valid_logical_addr, *_valid_logical_addr, NULL, 0);
+ }
+}
+
+///
+/// CEC_Device::runTransmit implements the state machine
+/// when transmitting data.
+///
+/// The state machine works in blocking mode
+///
+void IRAM_ATTR CEC_Device::runTransmit() {
+ if (!_xmit_wait_ms) {
+ // we haven't waited for the signal to stabilize yet
+ // compute the number of milliseconds to wait for in fast loop
+
+ // We need to wait a certain amount of time before we can transmit
+ // TODO improve waiting mechanism to avoid waiting for nothing
+ uint32_t wait_us = ((_xmitretry) ? 3 * BIT_TIME : (_am_last_transmittor) ? 7 * BIT_TIME : 5 * BIT_TIME) - BIT_TIME; // exponential backoff, we substract BIT_TIME because it will be done during CEC_XMIT_WAIT
+ uint32_t wait_ms = (wait_us / 1000) + 1;
+ // AddLog(LOG_LEVEL_INFO, PSTR("CEC: XMIT Start wait_us=%i wait_ms=%i"), wait_us, wait_ms);
+ _xmit_wait_ms = millis() + wait_ms;
+ if (!_xmit_wait_ms) { _xmit_wait_ms = 1; } // avoid accidental zero by adding one more millisecond
+ return;
+ }
+
+ if (_xmit_wait_ms && !TimeReached(_xmit_wait_ms)) {
+ return;
+ }
+
+ // timer in ms has reached
+ _xmit_wait_ms = 0; // reset timer for next transmission or next retry
+ uint32_t now = micros();
+ if (!now) { now = 1; } // avoid now == 0 which has a special meaning
+ bool electrical_line_state = lineState();
+
+ if (_xmitretry > CEC_MAX_RETRANSMIT) { // if exhausted retries, abort
+ _transmit_buffer_bytes = 0;
+ _state = CEC_IDLE;
+ }
+ _state = CEC_XMIT_WAIT; // start with XMIT_WAIT
+ _bit_start_time = now;
+
+ while (_state != CEC_IDLE) {
+ // update timing information for new iteration
+ now = micros();
+ if (!now) { now = 1; } // avoid now == 0 which has a special meaning
+ uint32_t time_from_start_bit = now - _bit_start_time;
+
+ // do we need to wait ?
+ // this would be a good place to check the line state as well TODO
+ if (_wait_after_start_bit_us > time_from_start_bit) {
+ uint32_t remaining_time_us = _wait_after_start_bit_us - time_from_start_bit;
+ if (remaining_time_us > 2500) {
+ delay(0);
+ } else if (remaining_time_us > 100) { // wait by chunks of 100us
+ delayMicroseconds(100);
+ } else {
+ delayMicroseconds(remaining_time_us);
+ }
+ continue; // loop again
+ }
+
+ // check electrical line
+ electrical_line_state = lineState();
+ if (electrical_line_state != _line_state_expected && _state > CEC_XMIT_WAIT && _state != CEC_XMIT_ACK_TEST && _state != CEC_XMIT_ACK_WAIT) {
+ // We are in a transmit state and someone else is mucking with the line
+ // Try to receive and wait for the line to clear before (re)transmit
+ // However, it is OK for a follower to ACK if we are in an ACK state
+ AddLog(LOG_LEVEL_INFO, PSTR("CEC: Error: Line transition while transmitting electrical_line_state=%i _line_state_expected=%i state=%i time_from_start_bit=%i _wait_after_start_bit_us=%i"),
+ electrical_line_state, _line_state_expected, _state, time_from_start_bit, _wait_after_start_bit_us);
+ _state = CEC_IDLE;
+ break;
+ }
+
+ _wait_after_start_bit_us = 0;
+ switch (_state) {
+ case CEC_XMIT_WAIT:
+ // We are checking during an entire BIT_TIME period
+ // and verify that the line is HIGH, i.e. no traffic is happening
+ _wait_after_start_bit_us = BIT_TIME; // add an extra BIT_TIME
+ while (1) {
+ // update timing information for new iteration
+ now = micros();
+ if (!now) { now = 1; } // avoid now == 0 which has a special meaning
+ time_from_start_bit = now - _bit_start_time;
+
+ electrical_line_state = lineState();
+ if (electrical_line_state == 0) {
+ // abort, line is busy
+ break;
+ }
+
+ if (_wait_after_start_bit_us <= time_from_start_bit) {
+ break; // timer elapsed
+ }
+ uint32_t remaining_time_us = _wait_after_start_bit_us - time_from_start_bit;
+ delayMicroseconds(remaining_time_us > 100 ? 100 : remaining_time_us);
+ // loop to next iteration
+ }
+
+ // we have finsihed waiting, of have aborted wait
+ if (electrical_line_state == 0) { // if the last measure was LOW, then the line is busy
+ // abort, line is busy
+ _state = CEC_IDLE;
+ break;
+ }
+
+ // Set line LOW for Start Bit LOW
+ if (!setLineState(0, true)) { _state = CEC_IDLE; break; } // _line_state_expected is updated in setLineState
+
+ _transmitBufferBitIdx = 0;
+ _xmitretry++;
+ _am_last_transmittor = true;
+ _broadcast = (_transmit_buffer[0] & 0x0f) == 0x0f;
+ _bit_start_time = now;
+ _wait_after_start_bit_us = STARTBIT_TIME_LOW;
+ _state = CEC_XMIT_STARTBIT1;
+ break;
+
+ case CEC_XMIT_STARTBIT1:
+ case CEC_XMIT_DATABIT1:
+ case CEC_XMIT_EOM1:
+ // We finished the first half of the bit, send the rising edge
+ if (!setLineState(1, true)) { _state = CEC_IDLE; break; } // _line_state_expected is updated in setLineState
+
+ _wait_after_start_bit_us = (_state == CEC_XMIT_STARTBIT1) ? STARTBIT_TIME : BIT_TIME;
+ _state = (CEC_STATE)(_state + 1);
+ break;
+
+ case CEC_XMIT_STARTBIT2:
+ case CEC_XMIT_DATABIT2:
+ case CEC_XMIT_EOM2:
+ case CEC_XMIT_ACK2:
+ // We finished the second half of the previous bit, send the falling edge of the new bit
+ if (!setLineState(0, true)) { _state = CEC_IDLE; break; } // _line_state_expected is updated in setLineState
+
+ _bit_start_time = now;
+ bool bit;
+ if (_state == CEC_XMIT_DATABIT2 && (_transmitBufferBitIdx & 7) == 0) {
+ _state = CEC_XMIT_EOM1;
+ // Get EOM bit: transmit buffer empty?
+ bit = _eom = (_transmit_buffer_bytes == (_transmitBufferBitIdx >> 3));
+ } else if (_state == CEC_XMIT_EOM2) {
+ _state = CEC_XMIT_ACK1;
+ bit = true; // We transmit a '1'
+ } else {
+ _state = CEC_XMIT_DATABIT1;
+ // Pull bit from transmit buffer
+ unsigned char b = _transmit_buffer[_transmitBufferBitIdx >> 3] << (_transmitBufferBitIdx++ & 7);
+ bit = b >> 7;
+ }
+ _wait_after_start_bit_us = bit ? BIT_TIME_LOW_1 : BIT_TIME_LOW_0;
+ break;
+
+ case CEC_XMIT_ACK1:
+ // We finished the first half of the ack bit, release the line
+ setLineState(1, false); // this is the only case where the line may be forced low by another party to show ACK/NAK
+
+ // _bit_start_time = time;
+ _wait_after_start_bit_us = BIT_TIME_SAMPLE; // give time for receiver to ACK
+ _state = CEC_XMIT_ACK_TEST;
+ break;
+
+ case CEC_XMIT_ACK_TEST:
+ // UNICAST first
+ if (!_broadcast) { // unicast
+
+ if (electrical_line_state == 1) {
+ // No ACK
+ _ring_buffer.push(_transmit_buffer, _transmit_buffer_bytes, CEC_RingBuffer::MSG_TRANSMITTED, false);
+ // OnTransmitComplete(_transmit_buffer, _transmitBufferBitIdx >> 3, false);
+
+ // Normally we retransmit. But this is NOT the case for as its
+ // function is basically to 'ping' a logical address in which case we just want
+ // acknowledgement that it has succeeded or failed
+ if (_transmit_buffer_bytes == 1) {
+ _transmit_buffer_bytes = 0;
+ }
+ _state = CEC_IDLE;
+ } else {
+ // Unicast was Acked
+ if (_eom) {
+ // Nothing left to transmit, go back to idle
+ _transmit_buffer_bytes = 0;
+ _state = CEC_IDLE;
+ _ring_buffer.push(_transmit_buffer, _transmitBufferBitIdx >> 3, CEC_RingBuffer::MSG_TRANSMITTED, true);
+ // OnTransmitComplete(_transmit_buffer, _transmitBufferBitIdx >> 3, true);
+ } else {
+ // Packet was Acknowledged, but there is more to transmit
+ _wait_after_start_bit_us = BIT_TIME; // wait enough time to stabilize
+ _state = CEC_XMIT_ACK_WAIT; // wait for line going high
+ }
+ }
+ } else {
+ // BROADCAST message
+ if (electrical_line_state == 0) {
+ // No ACK
+ _ring_buffer.push(_transmit_buffer, _transmit_buffer_bytes, CEC_RingBuffer::MSG_TRANSMITTED, false);
+ // OnTransmitComplete(_transmit_buffer, _transmitBufferBitIdx >> 3, false);
+ _state = CEC_IDLE;
+ } else {
+ // message was acked
+ if (_eom) {
+ // Nothing left to transmit, go back to idle
+ _transmit_buffer_bytes = 0;
+ _state = CEC_IDLE;
+ _ring_buffer.push(_transmit_buffer, _transmitBufferBitIdx >> 3, CEC_RingBuffer::MSG_TRANSMITTED, true);
+ // OnTransmitComplete(_transmit_buffer, _transmitBufferBitIdx >> 3, true);
+ } else {
+ // Packet was Acknowledged, but there is more to transmit
+ _wait_after_start_bit_us = BIT_TIME; // wait enough time to stabilize
+ _state = CEC_XMIT_ACK2; // Broadcast Ack is HIGH, no need to wait for line going high
+ }
+ }
+ }
+ break;
+
+ case CEC_XMIT_ACK_WAIT:
+ // Wait for line going high
+ if (electrical_line_state == 0) {
+ if (_wait_after_start_bit_us >= BIT_TIMEOUT) {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: Error: Waiting for line to go high"));
+ _transmit_buffer_bytes = 0;
+ _state = CEC_IDLE;
+ break;
+ }
+ _wait_after_start_bit_us = BIT_TIMEOUT; // wait a little longer
+ break;
+ }
+ _state = CEC_XMIT_ACK2;
+ break;
+
+ default:
+ AddLog(LOG_LEVEL_INFO, PSTR("CEC: Error: Unknown state %i"), _state);
+ _state = CEC_IDLE;
+ break;
+ }
+ }
+ _line_state_expected = electrical_line_state;
+ _transmit_buffer_bytes = 0; // TODO stop for now
+}
+
+///
+/// CEC_Device::runReceiveISR implements the state machine
+/// when receiving data.
+///
+/// It is mainly driven by ISR,
+/// except when sending ACK, it blocks for 2ms
+///
+void IRAM_ATTR CEC_Device::runReceiveISR() {
+ if (isTransmitting()) { return; } // ignore any interrupt caused or during active transmission
+
+ cec_isr_rcv_count += 1;
+ // update timing information for new iteration
+ uint32_t now = micros();
+ if (!now) { now = 1; } // avoid now == 0 which has a special meaning
+ uint32_t time_from_start_bit = now - _bit_start_time;
+
+ // check if we exceeded the time-out
+ if (_wait_after_start_bit_us && time_from_start_bit >= _wait_after_start_bit_us) {
+ // Timeout
+ _ring_buffer.log(_receive_buffer_bits,
+ _wait_after_start_bit_us,
+ time_from_start_bit,
+ 0x0011, _state, lineState());
+ _state = CEC_RCV_LINEERROR;
+ }
+
+ // check electrical line
+ bool electrical_line_state = lineState();
+
+ _wait_after_start_bit_us = 0;
+ bool bit;
+ switch (_state) {
+
+ case CEC_IDLE:
+ // _ring_buffer.log(0, 0, 0, 0xF000 + electrical_line_state, _state);
+ // If a high to low transition occurs, this must be the beginning of a start bit
+ if (electrical_line_state == 0) {
+ // Signal is LOW
+ // ASSERT_LINE(0); // actually not necessary since it's already tested above
+ _receive_buffer_bits = 0;
+ _bit_start_time = now;
+ _ack = true;
+ _follower = false;
+ _broadcast = false;
+ _am_last_transmittor = false;
+ _wait_after_start_bit_us = STARTBIT_TIMEOUT;
+ _state = CEC_RCV_STARTBIT1;
+ }
+ // If Low to High, ignore
+ break;
+
+ case CEC_RCV_STARTBIT1:
+ // Signal supposed to be HIGH
+ ASSERT_LINE(1);
+ // We received the rising edge of the start bit
+ if (time_from_start_bit >= (STARTBIT_TIME_LOW - BIT_TIME_LOW_MARGIN) &&
+ time_from_start_bit <= (STARTBIT_TIME_LOW + BIT_TIME_LOW_MARGIN)) {
+ // We now need to wait for the next falling edge
+ _wait_after_start_bit_us = STARTBIT_TIMEOUT;
+ _state = CEC_RCV_STARTBIT2;
+ break;
+ }
+ // Illegal state. Go back to CEC_IDLE to wait for a valid start bit or start pending transmit
+ _ring_buffer.log(STARTBIT_TIME_LOW - BIT_TIME_LOW_MARGIN,
+ STARTBIT_TIME_LOW + BIT_TIME_LOW_MARGIN,
+ time_from_start_bit,
+ 0x0001, _state, lineState());
+ _state = CEC_IDLE;
+ break;
+
+ case CEC_RCV_STARTBIT2:
+ // Signal supposed to be LOW
+ ASSERT_LINE(0);
+ // This should be the falling edge after the start bit
+ if (time_from_start_bit >= (STARTBIT_TIME - BIT_TIME_MARGIN) &&
+ time_from_start_bit <= (STARTBIT_TIME + BIT_TIME_MARGIN)) {
+ // We've fully received the start bit. Begin receiving a data bit
+ _bit_start_time = now;
+ _wait_after_start_bit_us = BIT_TIMEOUT;
+ _state = CEC_RCV_DATABIT1;
+ break;
+ }
+ // Illegal state. Go back to CEC_IDLE to wait for a valid start bit or start pending transmit
+ _ring_buffer.log(STARTBIT_TIME - BIT_TIME_MARGIN,
+ STARTBIT_TIME + BIT_TIME_MARGIN,
+ time_from_start_bit,
+ 0x0002, _state, lineState());
+ _state = CEC_IDLE;
+ break;
+
+
+ case CEC_RCV_DATABIT1:
+ case CEC_RCV_EOM1:
+ case CEC_RCV_ACK1:
+ // Signal supposed to be HIGH
+ ASSERT_LINE(1);
+ // We've received the rising edge of the data/eom/ack bit
+ if (time_from_start_bit >= (BIT_TIME_LOW_1 - BIT_TIME_LOW_MARGIN) &&
+ time_from_start_bit <= (BIT_TIME_LOW_1 + BIT_TIME_LOW_MARGIN))
+ {
+ bit = true;
+ } else if (time_from_start_bit >= (BIT_TIME_LOW_0 - BIT_TIME_LOW_MARGIN) &&
+ time_from_start_bit <= (BIT_TIME_LOW_0 + BIT_TIME_LOW_MARGIN))
+ {
+ bit = false;
+ }
+ else {
+ // Illegal state. Send NAK later.
+ _ring_buffer.log(BIT_TIME_LOW_1 - BIT_TIME_LOW_MARGIN,
+ BIT_TIME_LOW_0 + BIT_TIME_LOW_MARGIN,
+ time_from_start_bit,
+ 0x0003, _state, lineState());
+ _state = CEC_IDLE;
+ break;
+ // bit = true;
+ // _ack = false;
+ }
+ if (_state == CEC_RCV_EOM1) {
+ _eom = bit;
+ }
+ else if (_state == CEC_RCV_ACK1) {
+ _ack = (bit == _broadcast);
+ if (_eom || !_ack) {
+ // We're not going to receive anything more from the initiator.
+ // Go back to the IDLE state and wait for another start bit or start pending transmit.
+ _ring_buffer.push(_receive_buffer, _receive_buffer_bits >> 3, CEC_RingBuffer::MSG_RECEIVED, _ack);
+ // OnReceiveComplete(_receive_buffer, _receive_buffer_bits >> 3, _ack);
+ _state = CEC_IDLE;
+ break;
+ }
+ } else {
+ // Save the received bit
+ unsigned int idx = _receive_buffer_bits >> 3;
+ if (idx < sizeof(_receive_buffer)) {
+ _receive_buffer[idx] = (_receive_buffer[idx] << 1) | bit;
+ _receive_buffer_bits++;
+ }
+ }
+ _wait_after_start_bit_us = BIT_TIMEOUT;
+ _state = (CEC_STATE)(_state + 1);
+ break;
+
+ case CEC_RCV_DATABIT2:
+ case CEC_RCV_EOM2:
+ case CEC_RCV_ACK2:
+ // Signal supposed to be LOW
+ ASSERT_LINE(0);
+ // We've received the falling edge after the data/eom/ack bit
+ if (time_from_start_bit > (BIT_TIME + BIT_TIME_MARGIN)) {
+ // Illegal state. Timeout?
+ _state = CEC_IDLE;
+ _ring_buffer.log(0,
+ BIT_TIME + BIT_TIME_MARGIN,
+ time_from_start_bit,
+ 0x0004, _state, lineState());
+ break;
+ }
+ _bit_start_time = now;
+ if (time_from_start_bit >= (BIT_TIME - BIT_TIME_MARGIN)) {
+ // timing is ok
+ if (_state == CEC_RCV_EOM2) {
+ _wait_after_start_bit_us = BIT_TIMEOUT;
+ _state = CEC_RCV_ACK1;
+
+ // Check to see if the frame is addressed to us
+ // or if we are in promiscuous mode (in which case we'll receive everything)
+ int address = _receive_buffer[0] & 0x0f;
+ if (address == 0x0f)
+ _broadcast = true;
+ else if (address == _logical_address)
+ _follower = true;
+
+ // Go low for ack/nak
+ if ((_follower && _ack) || (_broadcast && !_ack)) {
+ if (!_monitor_mode) {
+ setLineState(0, false); // no need to check state
+ delayMicroseconds(BIT_TIME_LOW_0 - CEC_MAX_FALL_TIME); // keep ack low for 3700us
+ setLineState(1, true); // no need to check state
+ // now the level is supposed to be HIGH
+ }
+ _wait_after_start_bit_us = BIT_TIME;
+ _state = CEC_RCV_ACK_SENT;
+ } else if (!_ack || (!_promiscuous && !_broadcast)) {
+ // It's broken or not addressed to us.
+ // Go back to CEC_IDLE to wait for a valid start bit or start pending transmit
+ _ring_buffer.log(0,
+ 0,
+ time_from_start_bit,
+ 0x0005, _state, lineState());
+ _state = CEC_IDLE;
+ }
+ break;
+ }
+ // Receive another bit
+ _wait_after_start_bit_us = BIT_TIMEOUT;
+ _state = (_state == CEC_RCV_DATABIT2 &&
+ (_receive_buffer_bits & 7) == 0) ? CEC_RCV_EOM1 : CEC_RCV_DATABIT1;
+ break;
+ }
+ // Line error.
+ if (_monitor_mode) {
+ _state = CEC_IDLE;
+ break;
+ }
+ _wait_after_start_bit_us = BIT_TIME_ERR;
+ _state = CEC_RCV_LINEERROR;
+ break;
+
+ case CEC_RCV_ACK_SENT:
+ // Signal supposed to be HIGH
+ ASSERT_LINE(1);
+
+ if (_eom || !_ack) {
+ // We're not going to receive anything more from the initiator (EOM has been received)
+ // or we've sent the NAK for the most recent bit. Therefore this message is all done.
+ // Go back to CEC_IDLE to wait for a valid start bit or start pending transmit
+ _ring_buffer.push(_receive_buffer, _receive_buffer_bits >> 3, CEC_RingBuffer::MSG_RECEIVED, _ack);
+ // OnReceiveComplete(_receive_buffer, _receive_buffer_bits >> 3, _ack);
+ _state = CEC_IDLE;
+ break;
+ }
+ // We need to wait for the falling edge of the ACK to finish processing this ack
+ _wait_after_start_bit_us = BIT_TIMEOUT;
+ _state = CEC_RCV_ACK2;
+ break;
+
+ case CEC_RCV_LINEERROR:
+ setLineState(1, false);
+
+ // _bit_start_time = time;
+ _state = CEC_IDLE;
+ _ring_buffer.log(_receive_buffer_bits,
+ 0,
+ time_from_start_bit,
+ 0x0010, _state, lineState());
+ // AddLog(LOG_LEVEL_INFO, PSTR("CEC: Line error"));
+ break;
+
+
+ default:
+ // this should not happen
+ _ring_buffer.log(0, 0, 0, 0xFFFF, _state, lineState());
+ _state = CEC_IDLE;
+ break;
+
+ }
+}
+
+///
+/// CEC_Device::Run implements our main state machine
+/// which includes all reading and writing of state including
+/// acknowledgements and arbitration
+///
+void CEC_Device::run()
+{
+ if (isTransmitting()) {
+ runTransmit();
+ } else {
+ // safeguard to avoid pulling down by mistake
+ setLineState(1, false);
+
+ // check if we have a pending Rx that has timed-out (but didn't receive any GPIO interrupt because GPIO did not change state for a while)
+ if (_state != CEC_IDLE) {
+ // check if we didn't go into a global timeout during read
+ uint32_t now = micros();
+ if (!now) { now = 1; } // avoid now == 0 which has a special meaning
+ uint32_t time_from_start_bit = now - _bit_start_time;
+
+ // check if we exceeded the time-out
+ if (_wait_after_start_bit_us && time_from_start_bit >= _wait_after_start_bit_us) {
+ // Timeout
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: Rx timeout"));
+ // Abort current receive
+ _state = CEC_IDLE;
+ }
+ }
+ }
+ checkMessages();
+}
+
+bool CEC_Device::transmitRaw(const unsigned char* buffer, unsigned int count)
+{
+ if (_monitor_mode)
+ return false; // we must not transmit in monitor mode
+ if (_transmit_buffer_bytes != 0)
+ return false; // pending transmit packet
+ if (count > sizeof(_transmit_buffer))
+ return false; // packet too big
+
+ for (int i = 0; i < count; i++)
+ _transmit_buffer[i] = buffer[i];
+ _transmit_buffer_bytes = count;
+ _xmitretry = 0;
+ return true;
+}
+
+bool CEC_Device::transmit(int sourceAddress, int targetAddress, const unsigned char* buffer, unsigned int count)
+{
+ if (_monitor_mode)
+ return false; // we must not transmit in monitor mode
+ if (_transmit_buffer_bytes != 0)
+ return false; // pending transmit packet
+ if (count >= sizeof(_transmit_buffer))
+ return false; // packet too big
+
+ _transmit_buffer[0] = (sourceAddress << 4) | (targetAddress & 0xf);
+ for (int i = 0; i < count; i++)
+ _transmit_buffer[i+1] = buffer[i];
+ _transmit_buffer_bytes = count + 1;
+ _xmitretry = 0;
+ return true;
+}
+
+bool CEC_Device::transmitFrame(int targetAddress, const unsigned char* buffer, int count)
+{
+ if (_logical_address < 0)
+ return false;
+
+ return transmit(_logical_address, targetAddress, buffer, count);
+}
+
+void CEC_Device::checkMessages() {
+ for (int32_t idx = _ring_buffer.pullMsgIdx(); idx >= 0; idx = _ring_buffer.pullMsgIdx()) {
+ CEC_RingBuffer::CEC_msg_t *msg = &_ring_buffer._msg[idx];
+ if (msg->type == CEC_RingBuffer::MSG_RECEIVED) {
+ OnReceiveComplete(msg->buf, msg->len, msg->ack);
+ } else if (msg->type == CEC_RingBuffer::MSG_TRANSMITTED) {
+ OnTransmitComplete(msg->buf, msg->len, msg->ack);
+ }
+#ifdef HDMI_DEBUG
+ else if (msg->type == CEC_RingBuffer::MSG_LOG) {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: log (%i - %i) actual=%i code=0x%04X line=%i state=%i"),
+ msg->timing_expected_low, msg->timing_expected_high, msg->timing, msg->code, msg->line, msg->state);
+ }
+#endif
+ _ring_buffer.ackMsg(idx); // remove message from ring buffer
+ }
+}
+
+/*********************************************************************************************\
+ * Interrupt management
+\*********************************************************************************************/
+
+void CEC_Device::enableISR(void) {
+ if (_gpio >= 0) {
+ attachInterruptArg(_gpio, CEC_Run, this, CHANGE);
+ }
+}
+
+// Service gpio ISR
+void IRAM_ATTR CEC_Device::serviceGpioISR(void) {
+ cec_isr_count += 1;
+ runReceiveISR();
+}
+
+/*********************************************************************************************\
+ * General methods
+\*********************************************************************************************/
+
+uint16_t CEC_Device::discoverPhysicalAddress() {
+ uint16_t addr = HDMIGetPhysicalAddress();
+ // if not found, try the stored configuration
+ uint16_t addr_from_settings = (Settings->hdmi_addr[1] << 8) | Settings->hdmi_addr[0];
+ if (addr == 0x0000) {
+ addr = addr_from_settings;
+ }
+ // assign a default address if we can't read it
+ if (addr == 0x0000) { addr = 0x1000; } // assign a default address if we can't read it
+
+ // if addr changed, store it in Settings
+ if (addr != addr_from_settings) {
+ Settings->hdmi_addr[0] = (addr) & 0xFF;
+ Settings->hdmi_addr[1] = (addr >> 8) & 0xFF;
+ SettingsSaveAll();
+ }
+ return addr;
+}
+
+bool IRAM_ATTR CEC_Device::lineState()
+{
+ int state = digitalRead(_gpio);
+ return state != LOW;
+}
+
+bool IRAM_ATTR CEC_Device::setLineState(bool state, bool check)
+{
+ // Using the following states, we can connect directly the GPIO to CEC pin without any transistor:
+ // - for high, configure as input + pullup, there is also a pull-up in the TV
+ // - for low, configure as output and drive low
+ if (state) {
+ pinMode(_gpio, INPUT_PULLUP);
+ delayMicroseconds(CEC_MAX_RISE_TIME); // wait for the line to fall
+ } else {
+ digitalWrite(_gpio, LOW);
+ pinMode(_gpio, OUTPUT);
+ delayMicroseconds(CEC_MAX_FALL_TIME); // wait for the line to fall
+ }
+ _line_state_expected = state;
+ if (check) {
+ bool electrical_line_state = lineState();
+ if (electrical_line_state != state) {
+ // AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: ERROR: Line state invalid state=%i electrical=%i"), state, electrical_line_state);
+ return false;
+ }
+ return true;
+ } else {
+ return true; // no check, expect it was fine
+ }
+}
+
+
+// manage callbacks
+void CEC_Device::OnReady(int logical_address)
+{
+ if (_on_rx_cb) { _on_ready_cb(this, logical_address); }
+
+ // This is called after the logical address has been allocated
+ int physical_address = getPhysicalAddress();
+ uint8_t buf[4] = { OP_REPORT_PHYSICAL_ADDRESS /*0x84*/, (uint8_t)(physical_address >> 8), (uint8_t)physical_address, _type };
+ transmitFrame(0xf, buf, 4); //
+}
+
+void CEC_Device::OnReceiveComplete(uint8_t * buf, size_t len, bool ack)
+{
+ // This is called when a frame is received. To transmit
+ // a frame call transmitFrame. To receive all frames, even
+ // those not addressed to this device, set Promiscuous to true.
+ if (len == 0) { return; } // something went wrong
+
+ int32_t from = (buf[0] >> 4) & 0x0F;
+ int32_t to = buf[0] & 0x0F;
+ if (_on_rx_cb) { _on_rx_cb(this, from, to, buf+1, len-1, ack); }
+
+ // Ignore messages not sent to us
+ if (to != getLogicalAddress()) { return; }
+
+ // No command received?
+ if (len < 2) { return; }
+
+ switch (buf[1]) {
+ case OP_GIVE_PHYSICAL_ADDRESS /*0x83*/:
+ { //
+ int32_t physicalAddress = getPhysicalAddress();
+ uint8_t buf[4] = { OP_REPORT_PHYSICAL_ADDRESS /*0x84*/, (uint8_t)(physicalAddress >> 8), (uint8_t)physicalAddress, _type};
+ transmitFrame(0xf, buf, 4); //
+ break;
+ }
+ case OP_GIVE_DEVICE_VENDOR_ID /*0x8C*/: //
+ uint32_t vendor = getVendorId();
+ uint8_t buf[4] = { OP_DEVICE_VENDOR_ID /*0x87*/, (uint8_t)vendor, (uint8_t)(vendor >> 8), (uint8_t)(vendor >> 16)};
+ transmitFrame(0xf, buf, 4); //
+ break;
+ }
+}
+
+void CEC_Device::OnTransmitComplete(uint8_t* buf, size_t len, bool ack)
+{
+ // This is called after a frame is transmitted.
+ AddLog(LOG_LEVEL_DEBUG, "CEC: Packet sent: %*_H %s", len, buf, ack ? PSTR("ACK") : PSTR("NAK"));
+
+ if (len == 1 && _logical_address < 0) {
+ // we are still in the logical address discovery phase
+ if (ack) {
+ // ack received, so our logical address is already in use, try next one
+ if (*++_valid_logical_addr != CLA_UNREGISTERED) {
+ transmit(*_valid_logical_addr, *_valid_logical_addr, NULL, 0);
+ } else {
+ // No other logical address, use CLA_UNREGISTERED
+ _logical_address = CLA_UNREGISTERED;
+ OnReady(_logical_address);
+ }
+ } else {
+ // nak received, this addres is free so let's take it
+ _logical_address = *_valid_logical_addr;
+ OnReady(_logical_address);
+ }
+ return;
+ }
+}
+
+#endif // USE_HDMI_CEC
diff --git a/tasmota/tasmota_xdrv_driver/xdrv_70_1_hdmi_cec.ino b/tasmota/tasmota_xdrv_driver/xdrv_70_1_hdmi_cec.ino
new file mode 100644
index 000000000..346138199
--- /dev/null
+++ b/tasmota/tasmota_xdrv_driver/xdrv_70_1_hdmi_cec.ino
@@ -0,0 +1,324 @@
+/*
+ xdrv_70_hdmi_cec.ino - support for HDMI CEC bus (control TV via HDMI)
+
+ Copyright (C) 2021 Theo Arends, Stephan Hadinger
+
+ 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 .
+*/
+
+
+#ifdef USE_HDMI_CEC
+/*********************************************************************************************\
+ * HDMI CEC send and receive using lib https://github.com/lucadentella/ArduinoLib_CEClient
+\*********************************************************************************************/
+
+#define XDRV_70 70
+
+const char kHDMICommands[] PROGMEM = D_PRFX_HDMI "|"
+ D_CMND_HDMI_SEND_RAW "|" D_CMND_HDMI_SEND "|"
+ D_CMND_HDMI_TYPE "|" D_CMND_HDMI_ADDR;
+
+void (* const HDMICommand[])(void) PROGMEM = {
+ &CmndHDMISendRaw, CmndHDMISend,
+ &CmndHDMIType, &CmndHDMIAddr,
+ };
+
+
+// This is called after the logical address has been allocated
+void HDMI_OnReady(class CEC_Device* self, int logical_address) {
+ int physical_address = self->getPhysicalAddress();
+ AddLog(LOG_LEVEL_INFO, PSTR("CEC: HDMI CEC initialized on GPIO %i, Logical address %d, Physical address 0x%04X"), self->getGPIO(), logical_address, physical_address);
+}
+
+void HDMI_OnReceive(class CEC_Device *self, int32_t from, int32_t to, uint8_t* buf, size_t len, bool ack)
+{
+ AddLog(LOG_LEVEL_DEBUG, "CEC: Packet received: (%1X->%1X) %1X%1X%*_H %s", from, to, from, to, len, buf, ack ? PSTR("ACK") : PSTR("NAK"));
+
+ Response_P(PSTR("{\"HdmiReceived\":{\"From\":%i,\"To\":%i,\"Data\":\"%*_H\"}}"), from, to, len, buf);
+ if (to == self->getLogicalAddress() || to == 0x0F) {
+ MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings->flag.mqtt_sensor_retain);
+ }
+ XdrvRulesProcess(0); // apply rules
+}
+
+void HDMI_OnTransmit(class CEC_Device *self, uint8_t* buf, size_t len, bool ack)
+{
+ // This is called after a frame is transmitted.
+ AddLog(LOG_LEVEL_DEBUG, "CEC: Packet sent: %*_H %s", len, buf, ack ? PSTR("ACK") : PSTR("NAK"));
+}
+
+// singleton for HDMI CEC object, could be expanded if we manage multiple HDMI in parallel
+CEC_Device *HDMI_CEC_device = nullptr;
+
+void HdmiCecInit(void)
+{
+ // CEC device type
+ CEC_Device::CEC_DEVICE_TYPE device_type = (CEC_Device::CEC_DEVICE_TYPE) Settings->hdmi_cec_device_type;
+ if (device_type == CEC_Device::CDT_TV || device_type >= CEC_Device::CDT_LAST) {
+ // if type in Settings is invalid, default to PLAYBACK_DEVICE
+ device_type = CEC_Device::CDT_PLAYBACK_DEVICE;
+ Settings->hdmi_cec_device_type = (uint8_t) device_type;
+ SettingsSaveAll();
+ }
+ // GPIO configuration
+ int32_t cec_gpio = Pin(GPIO_HDMI_CEC);
+ if (cec_gpio >= 0) {
+ HDMI_CEC_device = new CEC_Device(cec_gpio, device_type, true); // Promiscuous mode
+ if (HDMI_CEC_device == nullptr) {
+ AddLog(LOG_LEVEL_ERROR, PSTR("CEC: HDMI_CEC_device init failed"));
+ return;
+ }
+ HDMI_CEC_device->setOnReceiveCB(&HDMI_OnReceive);
+ HDMI_CEC_device->setOnTransmitCB(&HDMI_OnTransmit);
+ HDMI_CEC_device->setOnReadyCB(&HDMI_OnReady);
+ HDMI_CEC_device->start(); // start the protocol
+ }
+}
+
+/*********************************************************************************************\
+ * Interrupt management
+\*********************************************************************************************/
+
+void IRAM_ATTR CEC_Run(void *self) {
+ CEC_Device *cec_device = (CEC_Device*)self;
+ cec_device->serviceGpioISR();
+}
+
+/*********************************************************************************************\
+ * Commands
+\*********************************************************************************************/
+
+//
+// Command HdmiSendRaw
+//
+// HdmiSendRaw
+// Send the HEX sequence as-is with no control
+//
+void CmndHDMISendRaw(void) {
+ if (HDMI_CEC_device) {
+ RemoveSpace(XdrvMailbox.data);
+ SBuffer buf = SBuffer::SBufferFromHex(XdrvMailbox.data, strlen(XdrvMailbox.data));
+ if (buf.len() > 0 && buf.len() < 16) {
+ HDMI_CEC_device->transmitRaw(buf.buf(), buf.len());
+ ResponseCmndDone();
+ } else {
+ ResponseCmndChar_P(PSTR("Buffer too large"));
+ }
+ } else {
+ ResponseCmndError();
+ }
+}
+
+//
+// Command HdmiSend
+//
+// HdmiSend
+// HdmiSend { ["To":,] "Data":""}
+// Send the HEX payload to the target (unicast of broadcast)
+// "To": 0-15 (optional) target logical address, defaults to 0 (TV)
+// "Hex": payload without the first byte (source/dst) which is inferred
+//
+// Examples:
+// HdmiSend 8F -- ask TV its power state
+// or HdmiSend {"Data":"8F"}
+// or HdmiSend {"To":0, "Data":"8F"}
+//
+//
+// HdmiSend 8C -- ask TV its vendor id
+// or HdmiSend {"Data":"8C"}
+// or HdmiSend {"To":0, "Data":"8C"}
+//
+void CmndHDMISend(void) {
+ if (HDMI_CEC_device) {
+ RemoveSpace(XdrvMailbox.data);
+ if (XdrvMailbox.data[0] == '{') {
+
+ // JSON
+ JsonParser parser(XdrvMailbox.data);
+ JsonParserObject root = parser.getRoot();
+
+ if (!parser || !(root.isObject())) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; }
+
+ JsonParserToken val;
+ uint32_t to = root.getUInt(PSTR("To"), 0);
+ const char * payload = root.getStr(PSTR("Data"));
+ SBuffer buf = SBuffer::SBufferFromHex(payload, strlen(payload));
+ if (buf.len() > 0 && buf.len() < 15) {
+ HDMI_CEC_device->transmitFrame(to, buf.buf(), buf.len());
+ ResponseCmndDone();
+ } else {
+ if (buf.len() == 0) {
+ ResponseCmndChar_P(PSTR("Buffer empty"));
+ } else {
+ ResponseCmndChar_P(PSTR("Buffer too large"));
+ }
+ }
+ } else {
+ // Hex
+ SBuffer buf = SBuffer::SBufferFromHex(XdrvMailbox.data, strlen(XdrvMailbox.data));
+ if (buf.len() > 0 && buf.len() < 15) {
+ HDMI_CEC_device->transmitFrame(0, buf.buf(), buf.len());
+ ResponseCmndDone();
+ } else {
+ if (buf.len() == 0) {
+ ResponseCmndChar_P(PSTR("Buffer empty"));
+ } else {
+ ResponseCmndChar_P(PSTR("Buffer too large"));
+ }
+ }
+ }
+ } else {
+ ResponseCmndError();
+ }
+}
+
+//
+// Command CmndHDMIType
+//
+//
+void CmndHDMIType(void) {
+ if (XdrvMailbox.data_len > 0) {
+ if ((XdrvMailbox.payload < 1) && (XdrvMailbox.payload >= CEC_Device::CDT_LAST)) {
+ uint8_t type = XdrvMailbox.payload;
+ if (type != Settings->hdmi_cec_device_type) {
+ Settings->hdmi_cec_device_type = XdrvMailbox.payload;
+ SettingsSaveAll();
+ }
+ }
+ }
+ ResponseCmndNumber(Settings->hdmi_cec_device_type);
+}
+
+#define HDMI_EDID_ADDRESS 0x50 // HDMI EDID address is 0x50
+
+// Read FULL EDID 256 bytes from address 0x50
+// Return true if failed
+// The buffer must be allocated to uint8_t[256] by caller
+// Only checksum is checked
+bool ReadEdid256(uint8_t *buf) {
+ if (!TasmotaGlobal.i2c_enabled) { return true; } // abort if I2C is not started
+
+ if (I2cReadBuffer(HDMI_EDID_ADDRESS, 0, buf , 128)) { return true; }
+ if (I2cReadBuffer(HDMI_EDID_ADDRESS, 128, buf + 128, 128)) { return true; }
+
+ // verify checksum for block 0
+ uint8_t chk0 = 0;
+ for (uint32_t i = 0; i < 128; i++) {
+ chk0 += buf[i];
+ }
+ if (chk0 != 0) { return true; }
+
+ // verify checksum for block 1
+ uint8_t chk1 = 0;
+ for (uint32_t i = 128; i < 256; i++) {
+ chk1 += buf[i];
+ }
+ if (chk1 != 0) { return true; }
+
+ // check prefix
+ uint32_t * buf32 = (uint32_t*) buf;
+ if (buf32[0] != 0xFFFFFF00 || buf32[1] != 0x00FFFFFF) { return true; }
+
+ return false; // OK
+}
+
+// HDMI get physical address
+// This is done by reading EDID via I2C, and looking for a vendor specific extension
+//
+// Return 0x0000 if not found
+uint16_t HDMIGetPhysicalAddress(void) {
+ uint8_t buf[256] = {0};
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: trying to read physical address"));
+ if (ReadEdid256(buf)) { return 0x0000; } // unable to get an address
+
+ uint8_t edid_extensions = buf[126];
+ if (HighestLogLevel() >= LOG_LEVEL_DEBUG) {
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: successfully read EDID 256 bytes, extensions count %i"), edid_extensions);
+ AddLog(LOG_LEVEL_DEBUG, PSTR("CEC: EDID: %*_H"), sizeof(buf),buf);
+ }
+ if (edid_extensions == 0) {
+ AddLog(LOG_LEVEL_INFO, PSTR("CEC: Error: EDID has no extension"));
+ }
+
+ // Read first extension which is mandatory for HDMI
+ if (buf[128] != 0x02 || buf[129] < 0x03) { return 0x0000; } // invalid extension
+
+ uint32_t extensions_first_byte = 128 + 4;
+ uint32_t extensions_last_byte = 128 + buf[130];
+ uint32_t idx = extensions_first_byte;
+ while (idx < extensions_last_byte) {
+ uint8_t data_block_header = buf[idx];
+ uint32_t type = (data_block_header >> 5);
+ uint32_t number_of_bytes = (data_block_header & 0x1F);
+
+ // AddLog(LOG_LEVEL_DEBUG, "CEC: idx %i extension type %i, number of bytes %i", idx, type, number_of_bytes);
+
+ if (type == 3) {
+ // Vendor specific extension
+ // 030C00 for "HDMI Licensing, LLC"
+ if (buf[idx+1] == 0x03 && buf[idx+2] == 0x0C && buf[idx+3] == 0x00) {
+ uint16_t addr = (buf[idx+4] << 8) | buf[idx+5];
+ AddLog(LOG_LEVEL_DEBUG, "CEC: physical address found: 0x%04X", addr);
+ return addr;
+ }
+ }
+
+ idx += 1 + number_of_bytes;
+ }
+
+ AddLog(LOG_LEVEL_DEBUG, "CEC: physical address not found");
+ return 0x0000; // TODO
+}
+
+
+void CmndHDMIAddr(void) {
+ if (XdrvMailbox.data_len > 0) {
+ if ((XdrvMailbox.payload < 1)) {
+ uint16_t hdmi_addr = XdrvMailbox.payload;
+ Settings->hdmi_addr[0] = (hdmi_addr) & 0xFF;
+ Settings->hdmi_addr[1] = (hdmi_addr >> 8) & 0xFF;
+ }
+ }
+ uint16_t hdmi_addr = HDMIGetPhysicalAddress();
+ Response_P(PSTR("{\"%s\":\"0x%04X\"}"), XdrvMailbox.command, hdmi_addr);
+}
+
+/*********************************************************************************************\
+ * Interface
+\*********************************************************************************************/
+
+bool Xdrv70(uint32_t function)
+{
+ bool result = false;
+
+ switch (function) {
+ case FUNC_INIT:
+ HdmiCecInit();
+ break;
+ case FUNC_LOOP:
+ case FUNC_SLEEP_LOOP:
+ if (HDMI_CEC_device) {
+ HDMI_CEC_device->run();
+ }
+ break;
+ case FUNC_COMMAND:
+ if (HDMI_CEC_device) {
+ result = DecodeCommand(kHDMICommands, HDMICommand);
+ }
+ break;
+ }
+ return result;
+}
+
+#endif // USE_HDMI_CEC