mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66bf06b5d3 |
No files matched your search
@@ -1,22 +1,14 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
from esphome.components.logger import UART1
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_DATA,
|
||||
CONF_HARDWARE_UART,
|
||||
CONF_ID,
|
||||
CONF_LOGGER,
|
||||
CONF_NUMBER,
|
||||
CONF_ON_MESSAGE,
|
||||
CONF_PIN,
|
||||
CONF_RX_PIN,
|
||||
CONF_SOURCE,
|
||||
CONF_TX_PIN,
|
||||
CONF_UART_ID,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
@@ -25,7 +17,6 @@ from esphome.const import (
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@JosVanEijndhoven"]
|
||||
@@ -33,14 +24,10 @@ DOMAIN = "hdmi_cec"
|
||||
|
||||
MAX_LOGICAL_ADDRESS = 15
|
||||
MAX_PAYLOAD_LENGTH = 15
|
||||
MAX_OSD_NAME_LENGTH = MAX_PAYLOAD_LENGTH - 1 # the opcode takes one byte
|
||||
UART_BAUD_RATE = 2083 # five UART bits per 2.4 ms CEC bit
|
||||
CONF_PROMISCUOUS_MODE = "promiscuous_mode"
|
||||
CONF_MONITOR_MODE = "monitor_mode"
|
||||
CONF_DESTINATION = "destination"
|
||||
CONF_OPCODE = "opcode"
|
||||
CONF_PHYSICAL_ADDRESS = "physical_address"
|
||||
CONF_OSD_NAME = "osd_name"
|
||||
|
||||
hdmi_cec_ns = cg.esphome_ns.namespace("hdmi_cec")
|
||||
HDMICEC = hdmi_cec_ns.class_("HDMICEC", cg.Component)
|
||||
@@ -59,14 +46,6 @@ payload = cv.All(cv.ensure_list(cv.hex_uint8_t), cv.Length(max=MAX_PAYLOAD_LENGT
|
||||
payload_prefix = cv.All(payload, cv.Length(min=1))
|
||||
|
||||
|
||||
def _osd_name(value: object) -> str:
|
||||
# The name goes on the bus as plain ASCII, one byte per character
|
||||
value = cv.All(cv.string_strict, cv.Length(min=1, max=MAX_OSD_NAME_LENGTH))(value)
|
||||
if not all(0x20 <= ord(char) <= 0x7E for char in value):
|
||||
raise cv.Invalid("osd_name may only contain printable ASCII characters")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_message_filter(config: ConfigType) -> ConfigType:
|
||||
# The opcode is the first payload byte: fold it into the data prefix
|
||||
if (opcode := config.pop(CONF_OPCODE, None)) is not None:
|
||||
@@ -97,9 +76,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(): cv.declare_id(HDMICEC),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_input_pullup_pin_schema,
|
||||
cv.Required(CONF_ADDRESS): logical_address,
|
||||
cv.Optional(CONF_PHYSICAL_ADDRESS): cv.hex_uint16_t,
|
||||
cv.Optional(CONF_OSD_NAME): _osd_name,
|
||||
cv.Optional(CONF_UART_ID): cv.use_id(uart.UARTComponent),
|
||||
cv.Optional(CONF_PROMISCUOUS_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MONITOR_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
|
||||
@@ -126,52 +102,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
# On the ESP8266 only UART1, transmit only on GPIO2, is free of the logger; UART0 carries the log
|
||||
# and any other pin gets a software UART that sends with interrupts off, which holds the main loop
|
||||
# and hides our own frame from the receive interrupt
|
||||
ESP8266_UART1_TX_PIN = 2
|
||||
|
||||
|
||||
def _esp8266_uart1(hub_config: ConfigType) -> ConfigType:
|
||||
if (
|
||||
hub_config[CONF_TX_PIN][CONF_NUMBER] != ESP8266_UART1_TX_PIN
|
||||
or CONF_RX_PIN in hub_config
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{DOMAIN} needs the hardware UART1 on the ESP8266: tx_pin GPIO2 and no rx_pin"
|
||||
)
|
||||
logger = fv.full_config.get().get(CONF_LOGGER, {})
|
||||
if logger.get(CONF_HARDWARE_UART) == UART1:
|
||||
raise cv.Invalid(f"{DOMAIN} needs UART1 on the ESP8266, which the logger uses")
|
||||
return hub_config
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
if CONF_UART_ID in config:
|
||||
# The UART only drives the line, through a diode; its settings make one CEC bit of five UART bits
|
||||
uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=UART_BAUD_RATE,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)(config)
|
||||
if CORE.is_esp8266:
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_UART_ID): fv.id_declaration_match_schema(
|
||||
_esp8266_uart1
|
||||
)
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)(config)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.is_esp32:
|
||||
# The acknowledge bit is released from a general purpose timer, left out of the build by default
|
||||
@@ -183,36 +113,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_promiscuous_mode(True))
|
||||
if config[CONF_MONITOR_MODE]:
|
||||
cg.add(var.set_monitor_mode(True))
|
||||
if (physical_address := config.get(CONF_PHYSICAL_ADDRESS)) is not None:
|
||||
cg.add(var.set_physical_address(physical_address))
|
||||
if (osd_name := config.get(CONF_OSD_NAME)) is not None:
|
||||
cg.extern_progmem_array("esphome::hdmi_cec::OSD_NAME", cg.char, osd_name)
|
||||
cg.add_define("HDMI_CEC_OSD_NAME_LENGTH", len(osd_name))
|
||||
if (uart_id := config.get(CONF_UART_ID)) is not None:
|
||||
cg.add_define("USE_HDMI_CEC_UART")
|
||||
cg.add(var.set_uart(await cg.get_variable(uart_id)))
|
||||
|
||||
on_message = config.get(CONF_ON_MESSAGE, [])
|
||||
# An automation for frames addressed to us with a data prefix claims its opcode from every
|
||||
# initiator, whatever its source filter or the rest of its prefix, so the built-in replies never
|
||||
# contradict an answer it sends. One without a prefix sees every such frame, so nothing is
|
||||
# unhandled and no Feature Abort is sent
|
||||
address = config[CONF_ADDRESS]
|
||||
for_us = [
|
||||
conf for conf in on_message if conf.get(CONF_DESTINATION, address) == address
|
||||
]
|
||||
claimed_opcodes = sorted(
|
||||
{conf[CONF_DATA][0] for conf in for_us if CONF_DATA in conf}
|
||||
)
|
||||
if claimed_opcodes:
|
||||
cg.extern_progmem_array(
|
||||
"esphome::hdmi_cec::CLAIMED_OPCODES", cg.uint8, claimed_opcodes
|
||||
)
|
||||
cg.add_define("HDMI_CEC_CLAIMED_OPCODE_COUNT", len(claimed_opcodes))
|
||||
if all(CONF_DATA in conf for conf in for_us):
|
||||
cg.add_define("USE_HDMI_CEC_FEATURE_ABORT")
|
||||
|
||||
for conf in on_message:
|
||||
for conf in config.get(CONF_ON_MESSAGE, []):
|
||||
_request_message_callback_slot()
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
@@ -237,24 +139,3 @@ automation.register_apply_action(
|
||||
((CONF_DESTINATION, cg.uint8), (CONF_DATA, _PAYLOAD_VECTOR)),
|
||||
),
|
||||
)
|
||||
|
||||
# A frame under another logical address, for a device that stands in for several
|
||||
automation.register_apply_action(
|
||||
"hdmi_cec.send_as",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HDMICEC),
|
||||
cv.Required(CONF_SOURCE): cv.templatable(logical_address),
|
||||
cv.Required(CONF_DESTINATION): cv.templatable(logical_address),
|
||||
cv.Required(CONF_DATA): cv.templatable(payload),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"send({}, {}, {})",
|
||||
(
|
||||
(CONF_SOURCE, cg.uint8),
|
||||
(CONF_DESTINATION, cg.uint8),
|
||||
(CONF_DATA, _PAYLOAD_VECTOR),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,484 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <array>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef HDMI_CEC_USE_DECODER
|
||||
#include "hdmi_cec.h"
|
||||
#include "cec_decoder.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hdmi_cec {
|
||||
|
||||
const std::array<const char *, 0x77> Decoder::UI_COMMANDS = {
|
||||
/* 0x00 = */ "Select",
|
||||
"Up",
|
||||
"Down",
|
||||
"Left",
|
||||
"Right",
|
||||
"Right-Up",
|
||||
"Right-Down",
|
||||
"Left-Up",
|
||||
/* 0x08 = */ "Left-Down",
|
||||
"Root Menu",
|
||||
"Setup Menu",
|
||||
"Contents Menu",
|
||||
"Favorite Menu",
|
||||
"Exit",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
/* 0x10 = */ "Media Top Menu",
|
||||
"Media Context-sensitive Menu",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
/* 0x18 = */ "Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Number Entry Mode",
|
||||
"11",
|
||||
"12",
|
||||
/* 0x20 = */ "0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
/* 0x28 = */ "8",
|
||||
"9",
|
||||
"Dot",
|
||||
"Enter",
|
||||
"Clear",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Next Favorite",
|
||||
/* 0x30 = */ "Channel Up",
|
||||
"Channel Down",
|
||||
"Previous Channel",
|
||||
"Sound Select",
|
||||
"Input Select",
|
||||
"Display Information",
|
||||
"Help",
|
||||
"Page Up",
|
||||
/* 0x38 = */ "Page Down",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
/* 0x40 = */ "Power",
|
||||
"Volume Up",
|
||||
"Volume Down",
|
||||
"Mute",
|
||||
"Play",
|
||||
"Stop",
|
||||
"Pause",
|
||||
"Record",
|
||||
/* 0x48 = */ "Rewind",
|
||||
"Fast forward",
|
||||
"Eject",
|
||||
"Forward",
|
||||
"Backward",
|
||||
"Stop-Record",
|
||||
"Pause-Record",
|
||||
"Reserved",
|
||||
/* 0x50 = */ "Angle",
|
||||
"Sub picture",
|
||||
"Video on Demand",
|
||||
"Electronic Program Guide",
|
||||
"Timer Programming",
|
||||
"Initial Configuration",
|
||||
"Select Broadcast Type",
|
||||
"Select Sound Presentation",
|
||||
/* 0x58 = */ "Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
/* 0x60 = */ "Play Function",
|
||||
"Pause-Play Function",
|
||||
"Record Function",
|
||||
"Pause-Record Function",
|
||||
"Stop Function",
|
||||
"Mute Function",
|
||||
"Restore Volume Function",
|
||||
"Tune Function",
|
||||
/* 0x68 = */ "Select Media Function",
|
||||
"Select A/V Input Function",
|
||||
"Select Audio Input Function",
|
||||
"Power Toggle Function",
|
||||
"Power Off Function",
|
||||
"Power On Function",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
/* 0x70 = */ "Reserved",
|
||||
"F1 (Blue)",
|
||||
"F2 (Red)",
|
||||
"F3 (Green)",
|
||||
"F4 (Yellow)",
|
||||
"F5",
|
||||
"Data"};
|
||||
|
||||
// See 'short audio descriptor' in https://en.wikipedia.org/wiki/Extended_Display_Identification_Data
|
||||
const std::array<const char *, 0x11> Decoder::AUDIO_FORMATS = {
|
||||
"reserved", "LPCM", "AC3", "MPEG-1", "MP3", "MPEG-2", "AAC", "DTS", "ATRAC",
|
||||
"DSD", "DD+", "DTS-HD", "MAT/Dolby TrueHD", "DST Audio", "WMA Pro", "Extension?"};
|
||||
|
||||
const std::array<const char *, 8> Decoder::AUDIO_SAMPLERATES = {"32", "44.1", "48", "88",
|
||||
"96", "176", "192", "Reserved"};
|
||||
|
||||
template<uint32_t OPERANDS> bool Decoder::do_operand_() {
|
||||
if (OPERANDS <= 0xFF) {
|
||||
// generic function called for single operand of unkown type and length
|
||||
return append_operand_(".");
|
||||
} else {
|
||||
// generic function to handle two or more operands
|
||||
return do_operand_<OPERANDS & 0xFF>() && do_operand_<(OPERANDS >> 8u)>();
|
||||
}
|
||||
}
|
||||
|
||||
#define APPEND_LINE(...) (length_ = buf_append_printf(line_, size_, length_, __VA_ARGS__))
|
||||
|
||||
/**
|
||||
* List of specialised operand decode functions, one function per operand type.
|
||||
* (Not fully complete, might be extended later)
|
||||
*/
|
||||
template<> bool Decoder::do_operand_<Decoder::NONE>() { return false; }
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::ABORT_REASON>() {
|
||||
const static std::array<const char *, 6> NAMES = {
|
||||
"Unrecognized opcode", "Not in correct mode to respond", "Cannot provide source", "Invalid operand", "Refused",
|
||||
"Unable to determine"};
|
||||
return append_operand_<NAMES.size()>(NAMES);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::AUDIO_FORMAT>() {
|
||||
// this type of operand comes in a sequence, until exhausted
|
||||
bool ok = true;
|
||||
while (ok && (offset_ < frame_.size())) {
|
||||
ok = append_operand_<AUDIO_FORMATS.size()>(AUDIO_FORMATS);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::AUDIO_STATUS>() {
|
||||
char line[20];
|
||||
if (offset_ < frame_.size()) {
|
||||
uint8_t field = frame_[offset_];
|
||||
std::snprintf(line, sizeof(line), "Mute=%d,Vol=%02X", (field >> 7), (field & 0x7f));
|
||||
return append_operand_(line);
|
||||
} else {
|
||||
return append_operand_("?");
|
||||
}
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::DEVICE_TYPE>() {
|
||||
const static std::array<const char *, 8> NAMES = {"TV", "Recording Device", "Reserved",
|
||||
"Tuner", "Playback Device", "Audio System",
|
||||
"CEC Switch", "Video Processor"};
|
||||
return append_operand_<NAMES.size()>(NAMES);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::DISPLAY_CONTROL>() {
|
||||
const static std::array<const char *, 4> NAMES = {"Default time", "Until cleared", "Clear previous", "Reserved"};
|
||||
return append_operand_<NAMES.size()>(NAMES);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::FEATURE_OPCODE>() {
|
||||
if (offset_ >= frame_.size()) {
|
||||
return false;
|
||||
}
|
||||
uint8_t opcode = frame_[offset_];
|
||||
return append_operand_(find_opcode_name_(opcode));
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::OSD_STRING>() {
|
||||
char line[20]; // frame size() is at most 16
|
||||
unsigned int i;
|
||||
for (i = 0; i < frame_.size() - offset_; i++) {
|
||||
line[i] = (char) (frame_[offset_ + i]);
|
||||
}
|
||||
line[i] = '\0'; // append '\0' termination: that is likely NOT part of the frame
|
||||
offset_ = frame_.size(); // no useful data further in this frame
|
||||
return append_operand_(line);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::PHYSICAL_ADDRESS>() {
|
||||
// Exception: if this is an operand of <System Audio Mode Request> 0x70, then this operand is
|
||||
// merely optional, and its absence means 'Off'
|
||||
if (frame_.at(1) == 0x70 && offset_ >= frame_.size()) {
|
||||
return append_operand_("Off");
|
||||
}
|
||||
if (offset_ + 1 >= frame_.size()) {
|
||||
return append_operand_("?", 2);
|
||||
}
|
||||
char line[12];
|
||||
std::snprintf(line, sizeof(line), "%1x.%1x.%1x.%1x", (frame_[offset_] >> 4) & 0xF, frame_[offset_] & 0xF,
|
||||
(frame_[offset_ + 1] >> 4) & 0xF, frame_[offset_ + 1] & 0xF);
|
||||
return append_operand_(line, 2);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::POWER_STATUS>() {
|
||||
const static std::array<const char *, 4> NAMES = {"On", "Standby", "Standby->On", "On->Standby"};
|
||||
return append_operand_<NAMES.size()>(NAMES);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::SHORT_AUDIO_DESCRIPTOR>() {
|
||||
// The frame can have a sequence of these operands, count is not fixed;
|
||||
// each such operand takes 3 bytes in the frame.
|
||||
// For a specification of this "Short Audio descriptor" format, see the Wikipedia page on
|
||||
// "Extended Display Identification Data (EDID)", section "Audio Data Blocks".
|
||||
std::array<char, 100> line;
|
||||
uint32_t pos = 0;
|
||||
bool ok = true;
|
||||
while (ok && (offset_ + 2 < frame_.size())) {
|
||||
const uint8_t descriptor = frame_[offset_];
|
||||
uint8_t format = (descriptor >> 3) & 0x0F;
|
||||
pos = buf_append_printf(&line[0], line.size(), 0, "%s", AUDIO_FORMATS[format]);
|
||||
pos = buf_append_printf(&line[0], line.size(), pos, ",num_channels=%d", (descriptor & 0x07));
|
||||
uint8_t rates = frame_[offset_ + 1];
|
||||
for (int bit = 0; rates; bit++, rates >>= 1) {
|
||||
if (rates & 0x1) {
|
||||
// show support of various audio sample rates
|
||||
pos = buf_append_printf(&line[0], line.size(), pos, ",%skHz", AUDIO_SAMPLERATES[bit]);
|
||||
}
|
||||
}
|
||||
if (format == 1) {
|
||||
// for LPCM format
|
||||
uint8_t widths = frame_[offset_ + 2] & 0x7;
|
||||
for (int i = 0; widths; i++, widths >>= 1) {
|
||||
if (widths & 0x1) {
|
||||
// show support of audio samble bit widths of 16, 20, and/or 24
|
||||
pos = buf_append_printf(&line[0], line.size(), pos, ",%dbits", (16 + 4 * i));
|
||||
}
|
||||
}
|
||||
}
|
||||
ok = append_operand_(line.data(), 3);
|
||||
}
|
||||
// Note: Further descriptor 'extensions' not yet decoded
|
||||
return ok;
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::SYSTEM_AUDIO_STATUS>() {
|
||||
const static std::array<const char *, 2> NAMES = {"Off", "On"};
|
||||
return append_operand_<NAMES.size()>(NAMES);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::UI_COMMAND>() {
|
||||
uint8_t command = frame_[offset_];
|
||||
bool ok = append_operand_<UI_COMMANDS.size()>(UI_COMMANDS);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
// out of the 100+ UI commands, a few exceptional UI commands have appended an extra parameter:
|
||||
switch (command) {
|
||||
case 0x56:
|
||||
return do_operand_<UI_BROADCAST_TYPE>();
|
||||
case 0x57:
|
||||
return do_operand_<UI_SOUND_PRESENTATION_CONTROL>();
|
||||
case 0x60:
|
||||
return do_operand_<PLAY_MODE>();
|
||||
case 0x67:
|
||||
return do_operand_<CHANNEL_IDENTIFIER>();
|
||||
case 0x68:
|
||||
return do_operand_<UI_FUNCTION_MEDIA>();
|
||||
case 0x69:
|
||||
return do_operand_<UI_FUNCTION_SELECT_AV_INPUT>();
|
||||
case 0x6A:
|
||||
return do_operand_<UI_FUNCTION_SELECT_AUDIO_INPUT>();
|
||||
default:
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::VENDOR_ID>() {
|
||||
if (offset_ + 2 >= frame_.size()) {
|
||||
return append_operand_("?", 3);
|
||||
}
|
||||
uint32_t id = (uint32_t) (frame_[offset_]) << 16 | (uint32_t) (frame_[offset_ + 1]) << 8 | frame_[offset_ + 2];
|
||||
const char *const *name_lookup = VENDOR_IDS.find(id);
|
||||
const char *vendor_name = name_lookup ? (*name_lookup) : nullptr;
|
||||
if (!vendor_name) {
|
||||
// if the hdmi-cec vendor id is not in our list, the id value itself is printed.
|
||||
char line[12];
|
||||
std::snprintf(line, sizeof(line), "ID=%06x", id);
|
||||
return append_operand_(line, 3);
|
||||
}
|
||||
return append_operand_(vendor_name, 3);
|
||||
}
|
||||
|
||||
template<> bool Decoder::do_operand_<Decoder::CEC_VERSION>() {
|
||||
const static std::array<const char *, 9> NAMES = {"?", "1.2", "1.2a", "1.3", "1.3a", "1.4", "2.0", "2.x", "2.x"};
|
||||
return append_operand_<NAMES.size()>(NAMES);
|
||||
}
|
||||
|
||||
const Decoder::CecOpcodeTable Decoder::CEC_OPCODE_TABLE = {
|
||||
{// Must be given sorted on increasing opcode:
|
||||
// opcode, name, function pointer with specification of its operands
|
||||
{0x00, {"Feature Abort", &Decoder::do_operand_<two(FEATURE_OPCODE, ABORT_REASON)>}},
|
||||
{0x04, {"Image View On", &Decoder::do_operand_<NONE>}},
|
||||
{0x05, {"Tuner Step Increment", &Decoder::do_operand_<NONE>}},
|
||||
{0x06, {"Tuner Step Decrement", &Decoder::do_operand_<NONE>}},
|
||||
{0x07, {"Tuner Device Status", &Decoder::do_operand_<TUNER_DEVICE_INFO>}},
|
||||
{0x08, {"Give Tuner Device Status", &Decoder::do_operand_<STATUS_REQUEST>}},
|
||||
{0x09, {"Record On", &Decoder::do_operand_<RECORD_SOURCE>}},
|
||||
{0x0A, {"Record Status", &Decoder::do_operand_<RECORD_STATUS_INFO>}},
|
||||
{0x0B, {"Record Off", &Decoder::do_operand_<NONE>}},
|
||||
{0x0D, {"Text View On", &Decoder::do_operand_<NONE>}},
|
||||
{0x0F, {"Record TV Screen", &Decoder::do_operand_<NONE>}},
|
||||
{0x1A, {"Give Deck Status", &Decoder::do_operand_<STATUS_REQUEST>}},
|
||||
{0x1B, {"Deck Status", &Decoder::do_operand_<DECK_INFO>}},
|
||||
{0x32, {"Set Menu Language", &Decoder::do_operand_<LANGUAGE>}},
|
||||
{0x33, {"Clear Analogue Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
|
||||
{0x34, {"Set Analogue Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
|
||||
{0x35, {"Timer Status", &Decoder::do_operand_<TIMER_STATUS_DATA>}},
|
||||
{0x36, {"Standby", &Decoder::do_operand_<NONE>}},
|
||||
{0x41, {"Play", &Decoder::do_operand_<PLAY_MODE>}},
|
||||
{0x42, {"Deck Control", &Decoder::do_operand_<DECK_CONTROL_MODE>}},
|
||||
{0x43, {"Timer Cleared Status", &Decoder::do_operand_<TIMER_CLEARED_STATUS_DATA>}},
|
||||
{0x44, {"User Control Pressed", &Decoder::do_operand_<UI_COMMAND>}},
|
||||
{0x45, {"User Control Released", &Decoder::do_operand_<NONE>}},
|
||||
{0x46, {"Give OSD Name", &Decoder::do_operand_<NONE>}},
|
||||
{0x47, {"Set OSD Name", &Decoder::do_operand_<OSD_NAME>}},
|
||||
{0x64, {"Set OSD String", &Decoder::do_operand_<two(DISPLAY_CONTROL, OSD_STRING)>}},
|
||||
{0x67, {"Set Timer Program Title", &Decoder::do_operand_<PROGRAM_TITLE_STRING>}},
|
||||
{0x70, {"System Audio Mode Request", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
|
||||
{0x71, {"Give Audio Status", &Decoder::do_operand_<NONE>}},
|
||||
{0x72, {"Set System Audio Mode", &Decoder::do_operand_<SYSTEM_AUDIO_STATUS>}},
|
||||
{0x7A, {"Report Audio Status", &Decoder::do_operand_<AUDIO_STATUS>}},
|
||||
{0x7D, {"Give System Audio Mode Status", &Decoder::do_operand_<NONE>}},
|
||||
{0x7E, {"System Audio Mode Status", &Decoder::do_operand_<SYSTEM_AUDIO_STATUS>}},
|
||||
{0x80, {"Routing Change", &Decoder::do_operand_<two(PHYSICAL_ADDRESS, PHYSICAL_ADDRESS)>}},
|
||||
{0x81, {"Routing Information", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
|
||||
{0x82, {"Active Source", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
|
||||
{0x83, {"Give Physical Address", &Decoder::do_operand_<NONE>}},
|
||||
{0x84, {"Report Physical Address", &Decoder::do_operand_<two(PHYSICAL_ADDRESS, DEVICE_TYPE)>}},
|
||||
{0x85, {"Request Active Source", &Decoder::do_operand_<NONE>}},
|
||||
{0x86, {"Set Stream Path", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
|
||||
{0x87, {"Device Vendor ID", &Decoder::do_operand_<VENDOR_ID>}},
|
||||
{0x89, {"Vendor Command", &Decoder::do_operand_<VENDOR_SPECIFIC_DATA>}},
|
||||
{0x8A, {"Vendor Remote Button Down", &Decoder::do_operand_<VENDOR_SPECIFIC_RC_CODE>}},
|
||||
{0x8B, {"Vendor Remote Button Up", &Decoder::do_operand_<NONE>}},
|
||||
{0x8C, {"Give Device Vendor ID", &Decoder::do_operand_<NONE>}},
|
||||
{0x8D, {"Menu Request", &Decoder::do_operand_<MENU_REQUEST_TYPE>}},
|
||||
{0x8E, {"Menu Status", &Decoder::do_operand_<MENU_STATE>}},
|
||||
{0x8F, {"Give Device Power Status", &Decoder::do_operand_<NONE>}},
|
||||
{0x90, {"Report Power Status", &Decoder::do_operand_<POWER_STATUS>}},
|
||||
{0x91, {"Get Menu Language", &Decoder::do_operand_<NONE>}},
|
||||
{0x92,
|
||||
{"Select Analogue Service",
|
||||
&Decoder::do_operand_<three(ANALOG_BROADCAST_TYPE, ANALOG_FREQUENCY, BROADCAST_SYSTEM)>}},
|
||||
{0x93, {"Select Digital Service", &Decoder::do_operand_<DIGITAL_SERVICE_IDENTIFICATION>}},
|
||||
{0x9A, {"Set Audio Rate", &Decoder::do_operand_<AUDIO_RATE>}},
|
||||
{0x9D, {"Inactive Source", &Decoder::do_operand_<PHYSICAL_ADDRESS>}},
|
||||
{0x97, {"Set Digital Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
|
||||
{0x99, {"Clear Digital Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
|
||||
{0x9E, {"CEC Version", &Decoder::do_operand_<CEC_VERSION>}},
|
||||
{0x9F, {"Get CEC Version", &Decoder::do_operand_<NONE>}},
|
||||
{0xA0, {"Vendor Command With ID", &Decoder::do_operand_<two(VENDOR_ID, VENDOR_SPECIFIC_DATA)>}},
|
||||
{0xA1, {"Clear External Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
|
||||
{0xA2, {"Set External Timer", &Decoder::do_operand_<two(START_DATE_TIME, DURATION)>}},
|
||||
{0xA3, {"Report Short Audio Descriptor", &Decoder::do_operand_<SHORT_AUDIO_DESCRIPTOR>}},
|
||||
{0xA4, {"Request Short Audio Descriptor", &Decoder::do_operand_<AUDIO_FORMAT>}},
|
||||
{0xC0, {"Initiate ARC", &Decoder::do_operand_<NONE>}},
|
||||
{0xC1, {"Report ARC Initiated", &Decoder::do_operand_<NONE>}},
|
||||
{0xC2, {"Report ARC Terminated", &Decoder::do_operand_<NONE>}},
|
||||
{0xC3, {"Request ARC Initiation", &Decoder::do_operand_<NONE>}},
|
||||
{0xC4, {"Request ARC Termination", &Decoder::do_operand_<NONE>}},
|
||||
{0xC5, {"Terminate ARC", &Decoder::do_operand_<NONE>}},
|
||||
{0xF8, {"CDC Message", &Decoder::do_operand_<NONE>}},
|
||||
{0xFF, {"Abort", &Decoder::do_operand_<NONE>}}}};
|
||||
|
||||
const Decoder::VendorIdTable Decoder::VENDOR_IDS = {
|
||||
{// Must be given sorted on increasing id:
|
||||
{0x000039, "Toshiba"}, {0x0000F0, "Samsung"}, {0x0005CD, "Denon"}, {0x000678, "Maranz"},
|
||||
{0x000982, "Loewe"}, {0x0009B0, "Onkyo"}, {0x000CB8, "Medion"}, {0x000CE7, "Toshiba"},
|
||||
{0x0010FA, "Apple"}, {0x001582, "Pulse Eight"}, {0x001950, "Harman Kardon"}, {0x001A11, "Google"},
|
||||
{0x0020C7, "Akai"}, {0x002467, "AOC"}, {0x008045, "Panasonic"}, {0x00903E, "Philips"},
|
||||
{0x009053, "Daewoo"}, {0x00A0DE, "Yamaha"}, {0x00D0D5, "Grundig"}, {0x00E036, "Pioneer"},
|
||||
{0x00E091, "LG"}, {0x08001F, "Sharp"}, {0x080046, "Sony"}, {0x18C086, "Broadcom"},
|
||||
{0x534850, "Sharp"}, {0x6B746D, "Vizio"}, {0x8065E9, "Benq"}, {0x9C645E, "Harman Kardon"}}};
|
||||
|
||||
void Decoder::address_decode_() {
|
||||
const static std::array<const char *, 16> NAMES = {"TV", "RecordingDev1", "RecordingDev2", "Tuner1",
|
||||
"PlaybackDev1", "AudioSystem", "Tuner2", "Tuner3",
|
||||
"PlaybackDev2", "RecordingDev3", "Tuner4", "PlaybackDev3",
|
||||
"Reserved", "Reserved", "SpecificUse", "Unregistered"};
|
||||
const char *dst = (frame_.is_broadcast()) ? "All" : NAMES[frame_.destination_addr()];
|
||||
const char *src = NAMES[frame_.initiator_addr()];
|
||||
APPEND_LINE("%s to %s: ", src, dst);
|
||||
}
|
||||
|
||||
const char *Decoder::find_opcode_name_(uint32_t opcode) const {
|
||||
const FrameType *frametype = CEC_OPCODE_TABLE.find(opcode);
|
||||
|
||||
if (!frametype)
|
||||
return "?";
|
||||
|
||||
return frametype->name_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to implement the 'do_operand' methods, to gather a textual representation.
|
||||
* @return true if a further operand can be decoded, false otherwise
|
||||
*/
|
||||
bool Decoder::append_operand_(const char *word, uint8_t offset_incr /* default 1 */) {
|
||||
APPEND_LINE("[%s]", word);
|
||||
offset_ += offset_incr;
|
||||
return (length_ < size_) && (offset_ < frame_.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry function 'decode' to call for full decode of a CEC frame
|
||||
*/
|
||||
int Decoder::decode() {
|
||||
// prepare text buffer state variables for an empty line
|
||||
length_ = 0; // currently accumulated length of text of operands
|
||||
line_[length_] = '\0'; // initialise output text to empty string
|
||||
|
||||
if (!line_ || !size_)
|
||||
return 0;
|
||||
|
||||
// print src and dest fields into 'line_'
|
||||
address_decode_();
|
||||
|
||||
// print opcode field
|
||||
const FrameType *frametype = nullptr;
|
||||
if (frame_.size() <= 1) {
|
||||
// Missing frame operation field?
|
||||
APPEND_LINE("Ping");
|
||||
} else {
|
||||
// find operation name
|
||||
frametype = CEC_OPCODE_TABLE.find(frame_.opcode());
|
||||
const char *opname = frametype ? frametype->name_ : "?";
|
||||
APPEND_LINE("<%s>", opname);
|
||||
}
|
||||
|
||||
// convert operand fields to text:
|
||||
if (frametype) {
|
||||
offset_ = 2; // location in frame of first operand, after address-byte and opcode-byte
|
||||
OperandDecode_f f = frametype->decode_f_;
|
||||
(this->*f)(); // call one of the above 'do_operand_<xx>' methods
|
||||
}
|
||||
return length_;
|
||||
}
|
||||
|
||||
} // namespace hdmi_cec
|
||||
} // namespace esphome
|
||||
#endif // HDMI_CEC_USE_DECODER
|
||||
@@ -0,0 +1,196 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <array>
|
||||
|
||||
#include "hdmi_cec.h"
|
||||
|
||||
namespace esphome::hdmi_cec {
|
||||
|
||||
/**
|
||||
* This LookupTable is const-initialised and only provides a simple 'find()'.
|
||||
* This creates a compact (smaller code) alternative to an std::map;
|
||||
*/
|
||||
template<typename KeyT, typename ValueT, unsigned int SIZE> class LookupTable {
|
||||
public:
|
||||
struct KeyValue {
|
||||
KeyT key_;
|
||||
ValueT value_;
|
||||
};
|
||||
|
||||
const ValueT *find(KeyT key) const {
|
||||
int lo_inx = 0;
|
||||
int hi_inx = SIZE - 1;
|
||||
while (lo_inx <= hi_inx) {
|
||||
// Search for 'key' in the array range lo_inx..hi_inx, bounds included.
|
||||
// Rely on the array being sorted on increasing opcode!
|
||||
int mid = (lo_inx + hi_inx) / 2;
|
||||
if (key == table_[mid].key_) {
|
||||
return &table_[mid].value_;
|
||||
}
|
||||
if (key < table_[mid].key_) {
|
||||
hi_inx = mid - 1;
|
||||
} else {
|
||||
lo_inx = mid + 1;
|
||||
}
|
||||
}
|
||||
return nullptr; // requested 'key' not found in table
|
||||
}
|
||||
|
||||
const KeyValue table_[SIZE];
|
||||
};
|
||||
|
||||
/**
|
||||
* This Decoder class interprets a binary CEC Frame to create a textual representation.
|
||||
* The information to create this decoder is mostly extracted from the HDMI 1.3a standard document,
|
||||
* from its section "Supplement 1 Consumer Electronics Control (CEC)".
|
||||
* Some further details were found in the Linux kernel source code of the "v4l-utils" repository,
|
||||
* such as the "ARC" related functionality of HDMI-CEC 1.4, and the HDMI vendor ID names.
|
||||
* Details on the digital audio format decoding came from the wikipedia page on
|
||||
* "Extended Display Identification Data (EDID)"
|
||||
*/
|
||||
class Decoder {
|
||||
public:
|
||||
Decoder(const Frame &frame, char *line, unsigned int size)
|
||||
: frame_(frame), line_(line), size_(size), length_(0), offset_(2) {}
|
||||
|
||||
/**
|
||||
* Create a one-line textual representation of the Frame content for logging and debugging
|
||||
* Store the text in the earlier provided 'line' buffer
|
||||
* @return the length of the written text, excluding the terminating null character
|
||||
*/
|
||||
int decode();
|
||||
|
||||
protected:
|
||||
const char *find_opcode_name_(uint32_t opcode) const;
|
||||
void address_decode_();
|
||||
|
||||
/**
|
||||
* Generic operand decode method, later specialised with operand-type-specific methods
|
||||
* @return true if further conversions can continue, false when to stop.
|
||||
*/
|
||||
template<uint32_t OPERANDS> bool do_operand_();
|
||||
|
||||
/**
|
||||
* The CecOpcodeTable is extracted from the HDMI CEC standard (1.4):
|
||||
* It lists all Frame opcodes with their <name> and their expected [operand argument type(s)]
|
||||
*/
|
||||
using OperandDecode_f = bool (Decoder::*)();
|
||||
struct FrameType {
|
||||
const char *name_; // name of the operation (of the op_code)
|
||||
const OperandDecode_f decode_f_; // a pointer to the corresponding 'do_operand_()' method
|
||||
};
|
||||
|
||||
using CecOpcodeTable = LookupTable<uint8_t, FrameType, 71>;
|
||||
using VendorIdTable = LookupTable<uint32_t, const char *, 28>;
|
||||
|
||||
const Frame &frame_;
|
||||
char *const line_; // buffer to hold the text of the decoded frame
|
||||
const unsigned int size_; // size of the 'line_' buffer
|
||||
unsigned int length_; // currently accumulated length of output text in 'line'
|
||||
unsigned int offset_; // current offset in frame to process next operand byte(s) (frame[0] and [1] are skipped)
|
||||
|
||||
/**
|
||||
* The HDMI CEC standard specifies a set of distinct operand (parameter) types,
|
||||
* used across the frame opcodes, denoted with "[operand type name]".
|
||||
* These specified operand types are enumerated here for later type-specific decoding to text
|
||||
*/
|
||||
enum Operand : uint8_t {
|
||||
NONE,
|
||||
ABORT_REASON,
|
||||
ANALOG_BROADCAST_TYPE,
|
||||
ANALOG_FREQUENCY,
|
||||
ASCII_DIGIT,
|
||||
ASCII,
|
||||
AUDIO_FORMAT,
|
||||
AUDIO_RATE,
|
||||
AUDIO_STATUS,
|
||||
BOOLEAN,
|
||||
BROADCAST_SYSTEM,
|
||||
CEC_VERSION,
|
||||
CHANNEL_IDENTIFIER,
|
||||
// DayOfMonth, integrated in StartDateTime
|
||||
DECK_CONTROL_MODE,
|
||||
DECK_INFO,
|
||||
DEVICE_TYPE,
|
||||
DIGITAL_SERVICE_IDENTIFICATION,
|
||||
DISPLAY_CONTROL,
|
||||
DURATION,
|
||||
EXTERNAL_PHYSICAL_ADDRESS,
|
||||
EXTERNAL_PLUG,
|
||||
EXTERNAL_SOURCE_SPECIFIER,
|
||||
HOUR,
|
||||
FEATURE_OPCODE,
|
||||
LANGUAGE,
|
||||
MENU_REQUEST_TYPE,
|
||||
MENU_STATE,
|
||||
MINUTE,
|
||||
// MonthOfYear, integrated in StartDateTime
|
||||
NEW_ADDRESS,
|
||||
ORIGINAL_ADDRESS,
|
||||
OSD_NAME,
|
||||
OSD_STRING = OSD_NAME,
|
||||
PHYSICAL_ADDRESS,
|
||||
PLAY_MODE,
|
||||
POWER_STATUS,
|
||||
PROGRAM_TITLE_STRING,
|
||||
RECORD_SOURCE,
|
||||
RECORD_STATUS_INFO,
|
||||
RECORDING_SEQUENCE,
|
||||
SHORT_AUDIO_DESCRIPTOR,
|
||||
STATUS_REQUEST,
|
||||
START_DATE_TIME,
|
||||
SYSTEM_AUDIO_STATUS,
|
||||
TIME,
|
||||
TIMER_CLEARED_STATUS_DATA,
|
||||
TIMER_STATUS_DATA,
|
||||
TUNER_DEVICE_INFO,
|
||||
UI_BROADCAST_TYPE,
|
||||
UI_COMMAND,
|
||||
UI_FUNCTION_MEDIA,
|
||||
UI_FUNCTION_SELECT_AV_INPUT,
|
||||
UI_FUNCTION_SELECT_AUDIO_INPUT,
|
||||
UI_SOUND_PRESENTATION_CONTROL,
|
||||
VENDOR_ID,
|
||||
VENDOR_SPECIFIC_DATA,
|
||||
VENDOR_SPECIFIC_RC_CODE,
|
||||
};
|
||||
/**
|
||||
* The plain 'operand types' are uint8.
|
||||
* Further uint32 'operand type' values are used to encode a sequence of upto 4 (potentially different) operands
|
||||
* in the MSB bytes of an uint32 value.
|
||||
*/
|
||||
constexpr static uint32_t two(uint32_t first, uint32_t secnd) { return first | (secnd << 8); }
|
||||
constexpr static uint32_t three(uint32_t first, uint32_t secnd, uint32_t third) {
|
||||
return first | (secnd << 8) | (third << 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to implement the 'do_operand' methods:
|
||||
* Append one word
|
||||
* @return true if a further operand shall be decoded, false to terminate operand decoding
|
||||
*/
|
||||
bool append_operand_(const char *word, uint8_t offset_incr = 1);
|
||||
|
||||
/**
|
||||
* Append one word, determined by the operand value as index to an array of names
|
||||
* @return true if a further operand shall be decoded, false to terminate operand decoding
|
||||
*/
|
||||
template<uint32_t N_STRINGS> bool append_operand_(const std::array<const char *, N_STRINGS> &strings) {
|
||||
uint32_t operand_value = frame_[offset_];
|
||||
const char *s = (operand_value < N_STRINGS) ? strings[operand_value] : "?";
|
||||
return append_operand_(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* String tables used in the subsequent 'do_operand' decode functions
|
||||
*/
|
||||
const static std::array<const char *, 0x77> UI_COMMANDS;
|
||||
const static std::array<const char *, 0x11> AUDIO_FORMATS;
|
||||
const static std::array<const char *, 8> AUDIO_SAMPLERATES;
|
||||
const static CecOpcodeTable CEC_OPCODE_TABLE;
|
||||
const static VendorIdTable VENDOR_IDS;
|
||||
}; // class Decoder
|
||||
} // namespace esphome::hdmi_cec
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
@@ -36,24 +33,6 @@ static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS = TOTAL_BIT_US * 7
|
||||
static constexpr uint32_t ABORTED_FRAME_US = TOTAL_BIT_US * 15; // no edge for this long: our frame was cut short
|
||||
static constexpr uint8_t MAX_ATTEMPTS = 8; // CEC allows 5, but a busy bus counts as an attempt here too
|
||||
|
||||
// Opcodes of the built-in replies
|
||||
static constexpr uint8_t OPCODE_FEATURE_ABORT = 0x00;
|
||||
static constexpr uint8_t OPCODE_GIVE_OSD_NAME = 0x46;
|
||||
static constexpr uint8_t OPCODE_SET_OSD_NAME = 0x47;
|
||||
static constexpr uint8_t OPCODE_GIVE_PHYSICAL_ADDRESS = 0x83;
|
||||
static constexpr uint8_t OPCODE_REPORT_PHYSICAL_ADDRESS = 0x84;
|
||||
static constexpr uint8_t OPCODE_GET_CEC_VERSION = 0x9F;
|
||||
static constexpr uint8_t OPCODE_CEC_VERSION = 0x9E;
|
||||
static constexpr uint8_t CEC_VERSION_1_4 = 0x05;
|
||||
|
||||
/// The CEC device type a logical address stands for, for "Report Physical Address": one nibble per
|
||||
/// address, from 0 up. 0 TV, 1 Recording Device, 3 Tuner, 4 Playback Device, 5 Audio System; the
|
||||
/// reserved, specific use and unregistered addresses count as a playback device
|
||||
static uint8_t device_type_of(uint8_t logical_address) {
|
||||
constexpr uint64_t device_types = 0x4444431433543110ULL;
|
||||
return (device_types >> (4 * logical_address)) & 0xF;
|
||||
}
|
||||
|
||||
Frame::Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload) {
|
||||
this->data_[0] = ((initiator_addr & 0xf) << 4) | (destination_addr & 0xf);
|
||||
this->size_ = 1 + payload.size();
|
||||
@@ -84,20 +63,6 @@ void HDMICEC::dump_config() {
|
||||
" Promiscuous mode: %s",
|
||||
this->address_, YESNO(this->monitor_mode_), YESNO(this->promiscuous_mode_));
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
if (this->physical_address_ != UNSET_PHYSICAL_ADDRESS) {
|
||||
ESP_LOGCONFIG(TAG, " Physical address: %X.%X.%X.%X", this->physical_address_ >> 12,
|
||||
(this->physical_address_ >> 8) & 0xF, (this->physical_address_ >> 4) & 0xF,
|
||||
this->physical_address_ & 0xF);
|
||||
}
|
||||
#ifdef HDMI_CEC_OSD_NAME_LENGTH
|
||||
char name[HDMI_CEC_OSD_NAME_LENGTH + 1];
|
||||
progmem_memcpy(name, OSD_NAME, HDMI_CEC_OSD_NAME_LENGTH);
|
||||
name[HDMI_CEC_OSD_NAME_LENGTH] = '\0';
|
||||
ESP_LOGCONFIG(TAG, " OSD name: %s", name);
|
||||
#endif
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
ESP_LOGCONFIG(TAG, " Transmit: UART");
|
||||
#endif
|
||||
}
|
||||
|
||||
void HDMICEC::loop() {
|
||||
@@ -117,13 +82,6 @@ void HDMICEC::loop() {
|
||||
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
|
||||
this->message_callbacks_.call(frame->initiator_addr(), frame->destination_addr(), frame->payload());
|
||||
#endif
|
||||
// Only a frame directed at us from a registered device gets a built-in reply: a broadcast is
|
||||
// never answered (so an instance on the unregistered address 0xF answers nothing, though it may
|
||||
// still send), and an unregistered initiator cannot be answered directly
|
||||
if (!this->monitor_mode_ && !frame->is_broadcast() && frame->destination_addr() == this->address_ &&
|
||||
frame->initiator_addr() != Frame::BROADCAST_ADDRESS && !is_claimed(frame->payload()[0])) {
|
||||
this->reply_builtin_(frame);
|
||||
}
|
||||
this->frames_queue_.pop_front();
|
||||
}
|
||||
// A frame being sent stays at the front of the queue until its attempts are over
|
||||
@@ -135,50 +93,6 @@ void HDMICEC::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool HDMICEC::is_claimed(uint8_t opcode) {
|
||||
#ifdef HDMI_CEC_CLAIMED_OPCODE_COUNT
|
||||
for (uint8_t i = 0; i < HDMI_CEC_CLAIMED_OPCODE_COUNT; i++) {
|
||||
if (progmem_read_byte(&CLAIMED_OPCODES[i]) == opcode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
void HDMICEC::reply_builtin_(const Frame *frame) {
|
||||
const uint8_t source = frame->initiator_addr();
|
||||
const uint8_t opcode = frame->payload()[0];
|
||||
switch (opcode) {
|
||||
case OPCODE_FEATURE_ABORT:
|
||||
return; // never answered
|
||||
case OPCODE_GET_CEC_VERSION:
|
||||
this->send(source, {OPCODE_CEC_VERSION, CEC_VERSION_1_4});
|
||||
return;
|
||||
case OPCODE_GIVE_PHYSICAL_ADDRESS:
|
||||
if (this->physical_address_ != UNSET_PHYSICAL_ADDRESS) {
|
||||
// Reported to everybody, with the device type the logical address stands for
|
||||
this->send(Frame::BROADCAST_ADDRESS,
|
||||
{OPCODE_REPORT_PHYSICAL_ADDRESS, static_cast<uint8_t>(this->physical_address_ >> 8),
|
||||
static_cast<uint8_t>(this->physical_address_), device_type_of(this->address_)});
|
||||
return;
|
||||
}
|
||||
break;
|
||||
#ifdef HDMI_CEC_OSD_NAME_LENGTH
|
||||
case OPCODE_GIVE_OSD_NAME: {
|
||||
uint8_t reply[1 + HDMI_CEC_OSD_NAME_LENGTH] = {OPCODE_SET_OSD_NAME};
|
||||
progmem_memcpy(reply + 1, OSD_NAME, HDMI_CEC_OSD_NAME_LENGTH);
|
||||
this->send(source, std::span<const uint8_t>(reply, sizeof(reply)));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_HDMI_CEC_FEATURE_ABORT
|
||||
// Nothing here or in an automation answers it: tell the initiator, as the bus expects
|
||||
this->send(source, {OPCODE_FEATURE_ABORT, opcode, 0x00}); // reason: unrecognized opcode
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HDMICEC::send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload) {
|
||||
if (this->monitor_mode_) {
|
||||
ESP_LOGW(TAG, "Monitor mode, not sending 0x%X->0x%X", source, destination);
|
||||
@@ -280,55 +194,25 @@ bool HDMICEC::transmit_frame_(const Frame *frame) {
|
||||
if (!this->send_start_bit_()) {
|
||||
return false;
|
||||
}
|
||||
// The four initiator address bits, most significant first, watch for a collision with another
|
||||
// initiator that started at the same moment; a lower address wins by holding the line
|
||||
const uint8_t header = frame->bytes()[0];
|
||||
for (int8_t i = 7; i >= 4; i--) {
|
||||
if (!this->send_bit_((header >> i) & 0x1)) {
|
||||
ESP_LOGD(TAG, "Bus collision on sending initiator address 0x%X on attempt %u", frame->initiator_addr(),
|
||||
this->transmit_attempts_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->transmit_body_(frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
void HDMICEC::transmit_body_(const Frame *frame) {
|
||||
// At 2083 baud one UART byte (start bit, 8 data bits, stop bit) spans two CEC bits of five UART
|
||||
// bits each. The UART start bit is low and its stop bit high, so a byte always begins the first
|
||||
// CEC bit low and ends the second one high; the data bits, sent least significant first, fill in
|
||||
// the rest: a logical 0 is low for three UART bits, a logical 1 for one.
|
||||
static const uint8_t TWO_BITS_TO_UART_BYTE[4] PROGMEM = {0x8C, 0xEC, 0x8F, 0xEF}; // 00, 01, 10, 11
|
||||
uint8_t data[Frame::MAX_LENGTH * 5 - 2]; // ten CEC bits per byte, two per UART byte, minus the address
|
||||
size_t length = 0;
|
||||
const auto bytes = frame->bytes();
|
||||
for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) {
|
||||
// The byte, its end of message bit and the acknowledge bit (sent high, the receiver pulls it low)
|
||||
const uint16_t block =
|
||||
(static_cast<uint16_t>(bytes[byte_inx]) << 2) | (byte_inx == bytes.size() - 1 ? 0x2 : 0) | 0x1;
|
||||
// The header starts after its initiator address bits
|
||||
for (int8_t pair = (byte_inx == 0 ? 2 : 4); pair >= 0; pair--) {
|
||||
data[length++] = progmem_read_byte(&TWO_BITS_TO_UART_BYTE[(block >> (2 * pair)) & 0x3]);
|
||||
}
|
||||
}
|
||||
this->uart_->write_array(data, length);
|
||||
}
|
||||
#else
|
||||
void HDMICEC::transmit_body_(const Frame *frame) {
|
||||
const auto bytes = frame->bytes();
|
||||
for (size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) {
|
||||
// The header starts after its initiator address bits
|
||||
for (int8_t i = (byte_inx == 0 ? 3 : 7); i >= 0; i--) {
|
||||
this->send_bit_((bytes[byte_inx] >> i) & 0x1);
|
||||
const uint8_t current_byte = bytes[byte_inx];
|
||||
// Most significant bit first. The four initiator address bits watch for a collision with
|
||||
// another initiator that started at the same moment; a lower address wins by holding the line
|
||||
for (int8_t i = 7; i >= 0; i--) {
|
||||
const bool bit_value = (current_byte >> i) & 0x1;
|
||||
if (!this->send_bit_(bit_value) && byte_inx == 0 && i >= 4) {
|
||||
ESP_LOGD(TAG, "Bus collision on sending initiator address 0x%X on attempt %u", frame->initiator_addr(),
|
||||
this->transmit_attempts_);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// End of message bit, then the acknowledge bit which the receiver may pull low
|
||||
this->send_bit_(byte_inx == bytes.size() - 1);
|
||||
this->send_bit_(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool HDMICEC::send_start_bit_() {
|
||||
if (!this->isr_pin_.digital_read()) {
|
||||
|
||||
@@ -17,22 +17,8 @@
|
||||
#include <hardware/timer.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
namespace esphome::uart {
|
||||
class UARTComponent;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace esphome::hdmi_cec {
|
||||
|
||||
// Generated from the configuration, in flash
|
||||
#ifdef HDMI_CEC_OSD_NAME_LENGTH
|
||||
extern const char OSD_NAME[] PROGMEM;
|
||||
#endif
|
||||
#ifdef HDMI_CEC_CLAIMED_OPCODE_COUNT
|
||||
extern const uint8_t CLAIMED_OPCODES[] PROGMEM;
|
||||
#endif
|
||||
|
||||
// Release/acquire store and load. The read-modify-write forms of std::atomic are not available on
|
||||
// every target (the ESP8266 has none), so a writer computes the new value itself; that is safe here
|
||||
// because every atomic in this component has exactly one writer.
|
||||
@@ -50,7 +36,6 @@ class Frame {
|
||||
public:
|
||||
static constexpr size_t MAX_LENGTH = 16; // HDMI-CEC 1.4
|
||||
static constexpr size_t MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1;
|
||||
static constexpr uint8_t BROADCAST_ADDRESS = 0xF; // also the address of an unregistered device
|
||||
|
||||
Frame() = default;
|
||||
/// The payload is at most MAX_PAYLOAD_LENGTH bytes; send() checks that
|
||||
@@ -58,7 +43,7 @@ class Frame {
|
||||
|
||||
ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const { return this->data_[0] >> 4; }
|
||||
ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const { return this->data_[0] & 0xf; }
|
||||
ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == BROADCAST_ADDRESS; }
|
||||
ESPHOME_ALWAYS_INLINE bool is_broadcast() const { return this->destination_addr() == 0xf; }
|
||||
ESPHOME_ALWAYS_INLINE size_t size() const { return this->size_; }
|
||||
std::span<const uint8_t> bytes() const { return {this->data_.data(), this->size_}; }
|
||||
/// The bytes after the header, starting with the opcode
|
||||
@@ -118,14 +103,6 @@ class HDMICEC : public Component {
|
||||
|
||||
void set_promiscuous_mode(bool promiscuous) { this->promiscuous_mode_ = promiscuous; }
|
||||
void set_monitor_mode(bool monitor_mode) { this->monitor_mode_ = monitor_mode; }
|
||||
/// Answer "Give Physical Address" with this address and the device type of the logical address.
|
||||
/// A lambda may change it at run time, for instance after reading it from the HDMI sink's EDID
|
||||
void set_physical_address(uint16_t physical_address) { this->physical_address_ = physical_address; }
|
||||
uint16_t get_physical_address() const { return this->physical_address_; }
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
/// Send the frame body from this UART (its TX pin on the line through a diode) instead of bit-banging it
|
||||
void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; }
|
||||
#endif
|
||||
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
|
||||
/// Called with the initiator, the destination and the payload (opcode first) of every received
|
||||
/// frame; the payload is only valid during the call
|
||||
@@ -143,9 +120,6 @@ class HDMICEC : public Component {
|
||||
bool send(uint8_t destination, std::initializer_list<uint8_t> payload) {
|
||||
return this->send(destination, std::span<const uint8_t>(payload.begin(), payload.size()));
|
||||
}
|
||||
bool send(uint8_t source, uint8_t destination, std::initializer_list<uint8_t> payload) {
|
||||
return this->send(source, destination, std::span<const uint8_t>(payload.begin(), payload.size()));
|
||||
}
|
||||
|
||||
protected:
|
||||
enum class ReceiverState : uint8_t {
|
||||
@@ -176,10 +150,6 @@ class HDMICEC : public Component {
|
||||
static void ack_timer_isr_s(uint alarm_num);
|
||||
#endif
|
||||
|
||||
/// The built-in replies to a frame directed at us that no automation claims
|
||||
void reply_builtin_(const Frame *frame);
|
||||
static bool is_claimed(uint8_t opcode);
|
||||
|
||||
// Transmit side, in loop()
|
||||
void transmit_message_();
|
||||
/// Keep the frame for another attempt, or drop it once the attempts are used up
|
||||
@@ -189,9 +159,6 @@ class HDMICEC : public Component {
|
||||
bool send_start_bit_();
|
||||
/// Send one bit; false when a logical 1 found the line held low by another initiator
|
||||
bool send_bit_(bool bit_value);
|
||||
/// Drive the frame after the initiator address: bit-banged, or from the UART, five UART bits
|
||||
/// per CEC bit, while loop() goes on
|
||||
void transmit_body_(const Frame *frame);
|
||||
ESPHOME_ALWAYS_INLINE void set_pin_input_high_() { this->isr_pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
|
||||
ESPHOME_ALWAYS_INLINE void set_pin_output_low_() {
|
||||
this->isr_pin_.pin_mode(gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
|
||||
@@ -212,9 +179,6 @@ class HDMICEC : public Component {
|
||||
#endif
|
||||
InternalGPIOPin *pin_;
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
uart::UARTComponent *uart_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
gptimer_handle_t ack_timer_{nullptr};
|
||||
#endif
|
||||
@@ -228,7 +192,6 @@ class HDMICEC : public Component {
|
||||
uint32_t last_falling_edge_us_{0}; // interrupt only
|
||||
uint32_t last_attempt_us_{0}; // loop() only; a refused start bit drives no edge, so it is timed here
|
||||
uint16_t required_idle_period_{0}; // loop() only
|
||||
uint16_t physical_address_{UNSET_PHYSICAL_ADDRESS};
|
||||
|
||||
// Shared with the interrupt. The tally of our own frame is written by the interrupt while
|
||||
// transmitting_ is set and no end of message has passed yet; loop() reads it once eom_received_
|
||||
@@ -247,11 +210,9 @@ class HDMICEC : public Component {
|
||||
bool recv_ack_queued_{false};
|
||||
bool prev_pin_level_{true};
|
||||
|
||||
uint8_t address_; // logical address, Frame::BROADCAST_ADDRESS when only listening
|
||||
uint8_t address_; // logical address, 0xf when only listening
|
||||
bool promiscuous_mode_{false};
|
||||
bool monitor_mode_{false};
|
||||
|
||||
static constexpr uint16_t UNSET_PHYSICAL_ADDRESS = 0xFFFF; // the bus reserves this value
|
||||
};
|
||||
|
||||
} // namespace esphome::hdmi_cec
|
||||
@@ -154,11 +154,7 @@
|
||||
#define USE_LVGL_TOUCHSCREEN
|
||||
#define USE_MDNS
|
||||
#define USE_MDNS_STORE_SERVICES
|
||||
#define HDMI_CEC_CLAIMED_OPCODE_COUNT 1
|
||||
#define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2
|
||||
#define HDMI_CEC_OSD_NAME_LENGTH 7
|
||||
#define USE_HDMI_CEC_FEATURE_ABORT
|
||||
#define USE_HDMI_CEC_UART
|
||||
#define I2C_PORT_FREQUENCY_COUNT 1
|
||||
#define MDNS_SERVICE_COUNT 3
|
||||
#define USE_MDNS_DYNAMIC_TXT
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
hdmi_cec:
|
||||
id: cec
|
||||
pin: ${cec_pin} # HDMI pin 13, with HDMI pin 17 as ground
|
||||
address: 0x5
|
||||
uart_id: uart_bus # its tx_pin drives the same line through a diode
|
||||
@@ -2,8 +2,6 @@ hdmi_cec:
|
||||
id: cec
|
||||
pin: ${cec_pin} # HDMI pin 13, with HDMI pin 17 as ground
|
||||
address: 0x5
|
||||
physical_address: 0x4000
|
||||
osd_name: ESPHome
|
||||
promiscuous_mode: true
|
||||
on_message:
|
||||
- opcode: 0x8F # Give Device Power Status
|
||||
@@ -15,13 +13,6 @@ hdmi_cec:
|
||||
then:
|
||||
- lambda: |-
|
||||
id(cec).send(0xf, {0x87, 0x00, 0x09, 0xb0});
|
||||
- opcode: 0x9F # Get CEC Version, asked of a playback device we send for (frames to it are not acknowledged)
|
||||
destination: 0x4
|
||||
then:
|
||||
- hdmi_cec.send_as:
|
||||
source: 0x4
|
||||
destination: !lambda return source;
|
||||
data: [0x9E, 0x05] # CEC Version 1.4
|
||||
- data: [0x44, 0x41] # User Control Pressed: Volume Up
|
||||
then:
|
||||
- logger.log: "HDMI-CEC Volume Up"
|
||||
@@ -1,6 +0,0 @@
|
||||
substitutions:
|
||||
cec_pin: GPIO14
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_2083/esp32-idf.yaml
|
||||
hdmi_cec: !include common-uart.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
substitutions:
|
||||
cec_pin: GPIO14
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_2083/esp8266-ard.yaml
|
||||
hdmi_cec: !include common-uart.yaml
|
||||
@@ -1,11 +0,0 @@
|
||||
# Common UART configuration for ESP32 IDF tests - 2083 baud
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO17
|
||||
rx_pin: GPIO16
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 2083
|
||||
@@ -1,9 +0,0 @@
|
||||
# Common UART configuration for ESP8266 Arduino tests - 2083 baud on the hardware UART1 (transmit only)
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO2
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
baud_rate: 2083
|
||||
Reference in new issue
Block a user