From f7c4c16a9c5973746021a024298534b7d1794b59 Mon Sep 17 00:00:00 2001 From: s-hadinger <49731213+s-hadinger@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:46:18 +0200 Subject: [PATCH] Support for HDMI CEC protocol (#19434) --- CHANGELOG.md | 1 + tasmota/include/i18n.h | 7 + tasmota/include/tasmota_template.h | 6 + tasmota/include/tasmota_types.h | 4 +- tasmota/language/af_AF.h | 1 + tasmota/language/bg_BG.h | 1 + tasmota/language/ca_AD.h | 1 + tasmota/language/cs_CZ.h | 1 + tasmota/language/de_DE.h | 1 + tasmota/language/el_GR.h | 1 + tasmota/language/en_GB.h | 1 + tasmota/language/es_ES.h | 1 + tasmota/language/fr_FR.h | 1 + tasmota/language/fy_NL.h | 1 + tasmota/language/he_HE.h | 1 + tasmota/language/hu_HU.h | 1 + tasmota/language/it_IT.h | 1 + tasmota/language/ko_KO.h | 1 + tasmota/language/nl_NL.h | 1 + tasmota/language/pl_PL.h | 1 + tasmota/language/pt_BR.h | 1 + tasmota/language/pt_PT.h | 1 + tasmota/language/ro_RO.h | 1 + tasmota/language/ru_RU.h | 1 + tasmota/language/sk_SK.h | 1 + tasmota/language/sv_SE.h | 1 + tasmota/language/tr_TR.h | 1 + tasmota/language/uk_UA.h | 1 + tasmota/language/vi_VN.h | 1 + tasmota/language/zh_CN.h | 1 + tasmota/language/zh_TW.h | 1 + tasmota/my_user_config.h | 3 + tasmota/tasmota_support/settings.ino | 2 +- .../xdrv_70_0_hdmi_cec.ino | 1189 +++++++++++++++++ .../xdrv_70_1_hdmi_cec.ino | 324 +++++ 35 files changed, 1560 insertions(+), 3 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_70_0_hdmi_cec.ino create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_70_1_hdmi_cec.ino 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