mirror of
https://github.com/esphome/esphome.git
synced 2026-10-19 20:49:35 +00:00
Rebuild on the merged bus core: flash tables, claimed opcodes, the UART after the address bits
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
@@ -9,177 +10,187 @@ from esphome.const import (
|
||||
CONF_ON_MESSAGE,
|
||||
CONF_PIN,
|
||||
CONF_SOURCE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_UART_ID,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@JosVanEijndhoven"]
|
||||
DOMAIN = "hdmi_cec"
|
||||
|
||||
MAX_LOGICAL_ADDRESS = 15
|
||||
MAX_PAYLOAD_LENGTH = 15
|
||||
MAX_OSD_NAME_LENGTH = MAX_PAYLOAD_LENGTH - 1 # 1 byte reserved for opcode
|
||||
CONF_PHYSICAL_ADDRESS = "physical_address"
|
||||
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_DECODE_MESSAGES = "decode_messages"
|
||||
CONF_OSD_NAME = "osd_name"
|
||||
CONF_DESTINATION = "destination"
|
||||
CONF_OPCODE = "opcode"
|
||||
CONF_PARENT = "parent"
|
||||
|
||||
|
||||
def validate_data_array(value):
|
||||
if isinstance(value, list):
|
||||
if len(value) > MAX_PAYLOAD_LENGTH:
|
||||
raise cv.Invalid(
|
||||
f"HDMI-CEC data payload cannot exceed {MAX_PAYLOAD_LENGTH} bytes."
|
||||
)
|
||||
return cv.Schema([cv.hex_uint8_t])(value)
|
||||
# If it's a lambda string, we can't inspect length in Python, let cv handle it
|
||||
return cv.lambda_(value)
|
||||
|
||||
|
||||
def validate_osd_name(value):
|
||||
if not isinstance(value, str):
|
||||
raise cv.Invalid("Must be a string")
|
||||
if len(value) < 1:
|
||||
raise cv.Invalid("Must be a non-empty string")
|
||||
if len(value) > MAX_OSD_NAME_LENGTH:
|
||||
raise cv.Invalid(f"Must not be more than {MAX_OSD_NAME_LENGTH}-characters long")
|
||||
|
||||
for char in value:
|
||||
if not 0x20 <= ord(char) < 0x7E:
|
||||
raise cv.Invalid(
|
||||
f"character '{char}' ({ord(char)}) is outside of the supported character range (0x20..0x7e)"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
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)
|
||||
MessageTrigger = hdmi_cec_ns.class_(
|
||||
"MessageTrigger",
|
||||
automation.Trigger.template(cg.uint8, cg.uint8, cg.std_vector.template(cg.uint8)),
|
||||
)
|
||||
SendAction = hdmi_cec_ns.class_("SendAction", automation.Action)
|
||||
_PAYLOAD_SPAN = cg.std_span.template(cg.uint8.operator("const"))
|
||||
_PAYLOAD_VECTOR = cg.std_vector.template(cg.uint8)
|
||||
_MESSAGE_ARGS = [
|
||||
(cg.uint8, "source"),
|
||||
(cg.uint8, "destination"),
|
||||
(_PAYLOAD_SPAN, "data"),
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HDMICEC),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Required(CONF_ADDRESS): cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS),
|
||||
cv.Required(CONF_PHYSICAL_ADDRESS): cv.uint16_t,
|
||||
cv.Optional(CONF_PROMISCUOUS_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MONITOR_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DECODE_MESSAGES, default=True): cv.boolean,
|
||||
cv.Optional(CONF_OSD_NAME, default="esphome"): validate_osd_name,
|
||||
cv.Optional(CONF_UART_ID): cv.use_id(uart.UARTComponent),
|
||||
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MessageTrigger),
|
||||
cv.Optional(CONF_SOURCE): cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS),
|
||||
cv.Optional(CONF_DESTINATION): cv.int_range(
|
||||
min=0, max=MAX_LOGICAL_ADDRESS
|
||||
),
|
||||
cv.Optional(CONF_OPCODE): cv.uint8_t,
|
||||
cv.Optional(CONF_DATA): validate_data_array,
|
||||
}
|
||||
),
|
||||
}
|
||||
_request_message_callback_slot = cg.slot_counter("HDMI_CEC_MESSAGE_CALLBACK_COUNT")
|
||||
|
||||
logical_address = cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS)
|
||||
payload = cv.All(cv.ensure_list(cv.hex_uint8_t), cv.Length(max=MAX_PAYLOAD_LENGTH))
|
||||
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:
|
||||
data = config.setdefault(CONF_DATA, [opcode])
|
||||
if data[0] != opcode:
|
||||
raise cv.Invalid(
|
||||
f"opcode 0x{opcode:02X} differs from the first data byte 0x{data[0]:02X}"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _message_filter(conf: ConfigType) -> str | None:
|
||||
"""The C++ condition an on_message automation runs under, or None for every frame."""
|
||||
checks = []
|
||||
if (source := conf.get(CONF_SOURCE)) is not None:
|
||||
checks.append(f"source == {source}")
|
||||
if (destination := conf.get(CONF_DESTINATION)) is not None:
|
||||
checks.append(f"destination == {destination}")
|
||||
if data := conf.get(CONF_DATA):
|
||||
checks.append(f"data.size() >= {len(data)}")
|
||||
checks.extend(f"data[{i}] == 0x{byte:02X}" for i, byte in enumerate(data))
|
||||
return " && ".join(checks) or None
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
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(
|
||||
{
|
||||
cv.Optional(CONF_SOURCE): logical_address,
|
||||
cv.Optional(CONF_DESTINATION): logical_address,
|
||||
cv.Optional(CONF_OPCODE): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_DATA): payload_prefix,
|
||||
},
|
||||
_validate_message_filter,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_RTL87XX,
|
||||
PLATFORM_LN882X,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if config[CONF_DECODE_MESSAGES]:
|
||||
cg.add_define("HDMI_CEC_USE_DECODER")
|
||||
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)
|
||||
return config
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
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
|
||||
include_builtin_idf_component("esp_driver_gptimer")
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_ADDRESS])
|
||||
await cg.register_component(var, config)
|
||||
if config[CONF_PROMISCUOUS_MODE]:
|
||||
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.add(var.set_osd_name(cg.shared_progmem_string(osd_name), 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)))
|
||||
|
||||
cec_pin_ = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(cec_pin_))
|
||||
on_message = config.get(CONF_ON_MESSAGE, [])
|
||||
# An automation with a data prefix claims its opcode: the built-in replies leave it alone. One
|
||||
# without a prefix sees every frame, so nothing is unhandled and no Feature Abort is sent
|
||||
claimed_opcodes = sorted(
|
||||
{conf[CONF_DATA][0] for conf in on_message if CONF_DATA in conf}
|
||||
)
|
||||
if claimed_opcodes:
|
||||
table = cg.shared_progmem_array(
|
||||
"hdmi_cec_claimed_opcodes", cg.uint8, claimed_opcodes
|
||||
)
|
||||
cg.add(var.set_claimed_opcodes(table, len(claimed_opcodes)))
|
||||
if all(CONF_DATA in conf for conf in on_message):
|
||||
cg.add_define("USE_HDMI_CEC_FEATURE_ABORT")
|
||||
|
||||
if config.get(CONF_UART_ID) is not None:
|
||||
uart_component = await cg.get_variable(config[CONF_UART_ID])
|
||||
cg.add_define("HDMI_CEC_USE_UART")
|
||||
cg.add(var.set_uart(uart_component))
|
||||
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
cg.add(var.set_physical_address(config[CONF_PHYSICAL_ADDRESS]))
|
||||
cg.add(var.set_promiscuous_mode(config[CONF_PROMISCUOUS_MODE]))
|
||||
cg.add(var.set_monitor_mode(config[CONF_MONITOR_MODE]))
|
||||
|
||||
osd_name_bytes = bytes(
|
||||
config[CONF_OSD_NAME], "ascii", "ignore"
|
||||
) # convert string to ascii bytes
|
||||
osd_name_bytes = list(osd_name_bytes) # convert bytes to ints
|
||||
osd_name_bytes = cg.std_vector.template(cg.uint8)(osd_name_bytes)
|
||||
cg.add(var.set_osd_name_bytes(osd_name_bytes))
|
||||
|
||||
for conf in config.get(CONF_ON_MESSAGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
||||
source = conf.get(CONF_SOURCE)
|
||||
if source is not None:
|
||||
cg.add(trigger.set_source(source))
|
||||
|
||||
destination = conf.get(CONF_DESTINATION)
|
||||
if destination is not None:
|
||||
cg.add(trigger.set_destination(destination))
|
||||
|
||||
opcode = conf.get(CONF_OPCODE)
|
||||
if opcode is not None:
|
||||
cg.add(trigger.set_opcode(opcode))
|
||||
|
||||
data = conf.get(CONF_DATA)
|
||||
if data is not None:
|
||||
cg.add(trigger.set_data(data))
|
||||
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.uint8, "source"),
|
||||
(cg.uint8, "destination"),
|
||||
(cg.std_vector.template(cg.uint8), "data"),
|
||||
],
|
||||
for conf in on_message:
|
||||
_request_message_callback_slot()
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_message_callback",
|
||||
_MESSAGE_ARGS,
|
||||
conf,
|
||||
when=_message_filter(conf),
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"hdmi_cec.send",
|
||||
SendAction,
|
||||
{
|
||||
cv.GenerateID(CONF_PARENT): cv.use_id(HDMICEC),
|
||||
cv.Optional(CONF_SOURCE): cv.templatable(
|
||||
cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS)
|
||||
),
|
||||
cv.Required(CONF_DESTINATION): cv.templatable(
|
||||
cv.int_range(min=0, max=MAX_LOGICAL_ADDRESS)
|
||||
),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_data_array),
|
||||
},
|
||||
synchronous=True,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HDMICEC),
|
||||
cv.Required(CONF_DESTINATION): cv.templatable(logical_address),
|
||||
cv.Required(CONF_DATA): cv.templatable(payload),
|
||||
}
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"send({}, {})",
|
||||
((CONF_DESTINATION, cg.uint8), (CONF_DATA, _PAYLOAD_VECTOR)),
|
||||
),
|
||||
)
|
||||
async def send_action_to_code(config, action_id, template_args, args):
|
||||
parent = await cg.get_variable(config[CONF_PARENT])
|
||||
var = cg.new_Pvariable(action_id, template_args, parent)
|
||||
|
||||
source = config.get(CONF_SOURCE)
|
||||
if source is not None:
|
||||
source_template_ = await cg.templatable(source, args, cg.uint8)
|
||||
cg.add(var.set_source(source_template_))
|
||||
|
||||
destination_template_ = await cg.templatable(
|
||||
config.get(CONF_DESTINATION), args, cg.uint8
|
||||
)
|
||||
cg.add(var.set_destination(destination_template_))
|
||||
|
||||
data_vec_ = cg.std_vector.template(cg.uint8)
|
||||
data_template_ = await cg.templatable(
|
||||
config.get(CONF_DATA), args, data_vec_, data_vec_
|
||||
)
|
||||
cg.add(var.set_data(data_template_))
|
||||
|
||||
return var
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,335 +1,258 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <initializer_list>
|
||||
#include <span>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace uart {
|
||||
#ifdef USE_ESP32
|
||||
#include "driver/gptimer.h"
|
||||
#endif
|
||||
#ifdef USE_RP2040
|
||||
#include <hardware/timer.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
namespace esphome::uart {
|
||||
class UARTComponent;
|
||||
}
|
||||
namespace hdmi_cec {
|
||||
#endif
|
||||
|
||||
class MessageTrigger;
|
||||
namespace esphome::hdmi_cec {
|
||||
|
||||
// For atomics, the '++' operator is explicitly avoided here, because that requires an atomic 'read-modify-write'
|
||||
// which is not supported by all targets. (In particular not by the esp8266)
|
||||
// The fall-back here to a non-atomic read-modify-write is OK for this application, as no other thread (or isr)
|
||||
// will modify the same variable.
|
||||
// The 'atomic' semantics is required here for its memory-barrier.
|
||||
#define ATOMIC_SET(atom, value) (atom).store((value), std::memory_order_release)
|
||||
#define ATOMIC_INCR(atom) ATOMIC_SET(atom, (atom) + 1)
|
||||
#define ATOMIC_GET(atom) (atom).load(std::memory_order_acquire)
|
||||
// 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.
|
||||
template<typename T> ESPHOME_ALWAYS_INLINE void atomic_set(std::atomic<T> &atom, T value) {
|
||||
atom.store(value, std::memory_order_release);
|
||||
}
|
||||
template<typename T> ESPHOME_ALWAYS_INLINE T atomic_get(const std::atomic<T> &atom) {
|
||||
return atom.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Frame stores an HDMI-CEC bus message as sequence of bytes with variable length.
|
||||
* Such bus message is either received or to be transmitted.
|
||||
* The naming 'frame' corresponds to the nomenclature in the HDMI-CEC standard.
|
||||
* A frame contains one 'header' byte and a variable number of 'payload' bytes.
|
||||
* The HDMI CEC standard 1.4 specifies a maximum length of 16 for any frame.
|
||||
* Filling a frame with 'push_back' in an ISR shall not cause dynamic memory allocation.
|
||||
*/
|
||||
/// One CEC bus message: a header byte with the initiator and destination addresses, then up to
|
||||
/// 15 payload bytes of which the first is the opcode. Fixed storage, so the receive interrupt
|
||||
/// never allocates.
|
||||
class Frame {
|
||||
public:
|
||||
Frame() = default;
|
||||
Frame(uint8_t initiator_addr, uint8_t target_addr, const uint8_t *payload, unsigned int payload_size);
|
||||
Frame(uint8_t initiator_addr, uint8_t target_addr, const std::initializer_list<uint8_t> &payload)
|
||||
: Frame(initiator_addr, target_addr, payload.begin(), payload.size()) {}
|
||||
static constexpr size_t MAX_LENGTH = 16; // HDMI-CEC 1.4
|
||||
static constexpr size_t MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1;
|
||||
|
||||
void set_header(uint8_t initiator_addr, uint8_t target_addr);
|
||||
uint8_t initiator_addr() const { return (data_[0] >> 4) & 0xf; }
|
||||
uint8_t destination_addr() const { return data_[0] & 0xf; }
|
||||
uint8_t opcode() const { return (this->size() >= 2) ? data_[1] : 0; }
|
||||
bool is_broadcast() const { return this->destination_addr() == 0xf; }
|
||||
uint8_t size() const { return size_; }
|
||||
uint8_t at(unsigned int i) const { return (i < size_) ? data_[i] : 0; }
|
||||
uint8_t operator[](unsigned int i) const { return at(i); }
|
||||
void clear() { size_ = 0; }
|
||||
void push_back(uint8_t data) {
|
||||
if (size_ < data_.size()) {
|
||||
data_[size_++] = data;
|
||||
Frame() = default;
|
||||
/// The payload is at most MAX_PAYLOAD_LENGTH bytes; send() checks that
|
||||
Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload);
|
||||
|
||||
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() == 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
|
||||
std::span<const uint8_t> payload() const { return this->bytes().subspan(1); }
|
||||
ESPHOME_ALWAYS_INLINE void clear() { this->size_ = 0; }
|
||||
/// False when the frame is full already; the bus allows no more
|
||||
ESPHOME_ALWAYS_INLINE bool push_back(uint8_t data) {
|
||||
if (this->size_ == MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
this->data_[this->size_++] = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a one-line textual representation of the Frame content for logging and debugging
|
||||
* @param line: a buffer in which the text is to be written
|
||||
* @param size: The size of this buffer not to exceed with text
|
||||
* @return the length of the written text, excluding the terminating null character
|
||||
*/
|
||||
int to_text(char *line, unsigned int size) const;
|
||||
|
||||
constexpr static unsigned int MAX_LENGTH = 16;
|
||||
constexpr static unsigned int MAX_PAYLOAD_LENGTH = MAX_LENGTH - 1; // subtract 1 byte for the src&dst field
|
||||
|
||||
protected:
|
||||
std::array<uint8_t, MAX_LENGTH> data_;
|
||||
std::array<uint8_t, MAX_LENGTH> data_{};
|
||||
uint8_t size_{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* The FrameRingBuffer is a container for Frames to queue data in a consumer-producer
|
||||
* application. The use of std::Atomics allows safe multi-thread operation when used with
|
||||
* a single producer and single consumer thread, where each Atomic index is updated
|
||||
* by one thread only.
|
||||
* After initialization, it operates without dynamic memory allocation.
|
||||
* This allows the gpio isr to safely and efficiently pick-up and pass Frames.
|
||||
* Due to its fixed memory size, it might return NULL pointers in case the buffer is full or empty.
|
||||
*/
|
||||
template<unsigned int SIZE> class FrameRingBuffer {
|
||||
public:
|
||||
FrameRingBuffer() : front_inx_{0}, back_inx_{0}, store_{} {
|
||||
for (auto &t : store_) {
|
||||
t = new Frame;
|
||||
}
|
||||
}
|
||||
~FrameRingBuffer() {
|
||||
for (auto &t : store_) {
|
||||
delete t;
|
||||
}
|
||||
}
|
||||
// 'front' is used to access a Frame, and use its content until it is no longer needed
|
||||
Frame *front() const { return is_empty() ? nullptr : store_[front_inx_]; }
|
||||
// 'pop_front' recycles the storage area of the last 'front()' call.
|
||||
// This invalidates further use of that ptr by the caller
|
||||
void pop_front() { cyclic_incr_(front_inx_); }
|
||||
// 'back' is used to fetch a free Frame, fill with data, and queue for later pick-up
|
||||
// Note that its returned Frame likely contains 'dirty' data.
|
||||
Frame *back() const { return is_full() ? nullptr : store_[back_inx_]; }
|
||||
// 'push_back' commits the frame that was earlier obtained by 'back()', presumably it got filled with content
|
||||
// This invalidates further use of this frame by the caller.
|
||||
void push_back() { cyclic_incr_(back_inx_); }
|
||||
bool is_empty() const { return count_() == 0; }
|
||||
bool is_full() const { return count_() == SIZE; }
|
||||
void reset() {
|
||||
front_inx_ = 0;
|
||||
back_inx_ = 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
using Index = std::atomic<unsigned int>;
|
||||
// this simple increment scheme is sufficiently 'atomic' if the front and back are each used by
|
||||
// one thread only. (So, at most one reader thread and one writer thread in the application.)
|
||||
int count_() const {
|
||||
int n = (int) (ATOMIC_GET(back_inx_) - ATOMIC_GET(front_inx_));
|
||||
if (n < 0)
|
||||
n += SIZE + 1;
|
||||
return n;
|
||||
}
|
||||
void cyclic_incr_(Index &inx) { ATOMIC_SET(inx, (inx == SIZE) ? 0 : (inx + 1)); }
|
||||
Index front_inx_; // ranging 0 .. SIZE
|
||||
Index back_inx_; // ranging 0 .. SIZE
|
||||
// if front_inx_ == back_inx_ the store is considered empty, so it can hold at most SIZE elements
|
||||
std::array<Frame *, SIZE + 1> store_;
|
||||
};
|
||||
|
||||
/**
|
||||
* class CECTransmit accepts bus messages ("frames") for transmission.
|
||||
* These messages, taken from "queue_for_send" are buffered, because a 'send' on the bus
|
||||
* might take a really long time, which should not be directly consumed in its calling thread:
|
||||
* With the local buffering, 'send' retries are now handled in subsequent time slices.
|
||||
* The 'send' queue has a limited depth, fairly shallow, because in practice an application
|
||||
* is expected not to queue so many 'send's before requesting (waiting for) an
|
||||
* inbound reply message.
|
||||
*/
|
||||
class CECTransmit {
|
||||
enum class TransmitState : uint8_t {
|
||||
IDLE,
|
||||
BUSY,
|
||||
EOM_CONFIRMED,
|
||||
};
|
||||
/// A queue of frames between one producer and one consumer, for instance the receive interrupt and
|
||||
/// loop(). The indices run freely and each is written by one side only, so no lock is needed.
|
||||
/// back() hands out a slot to fill and push_back() commits it; front() and pop_front() do the same
|
||||
/// on the reading side.
|
||||
template<size_t SIZE> class FrameRingBuffer {
|
||||
static_assert((SIZE & (SIZE - 1)) == 0, "SIZE must be a power of two");
|
||||
|
||||
public:
|
||||
void setup(InternalGPIOPin *pin);
|
||||
void dump_config();
|
||||
bool queue_for_send(uint8_t source, uint8_t destination, const uint8_t *payload_bytes, unsigned int payload_size);
|
||||
bool is_idle() const { return send_queue_.is_empty() && (transmit_state_ == TransmitState::IDLE); }
|
||||
void set_uart(uart::UARTComponent *uart) { uart_ = uart; }
|
||||
bool has_uart() const { return uart_ != nullptr; }
|
||||
// Note: 'ESPHOME_ALWAYS_INLINE' allows the following methods to be also used in 'IRAM_ATTR' methods.
|
||||
ESPHOME_ALWAYS_INLINE void set_pin_input_high() { pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
|
||||
ESPHOME_ALWAYS_INLINE void set_pin_output_low() {
|
||||
pin_->pin_mode(gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
|
||||
pin_->digital_write(false);
|
||||
ESPHOME_ALWAYS_INLINE Frame *front() { return this->is_empty() ? nullptr : this->slot_(this->front_inx_); }
|
||||
ESPHOME_ALWAYS_INLINE void pop_front() { this->advance_(this->front_inx_); }
|
||||
ESPHOME_ALWAYS_INLINE Frame *back() { return this->count_() == SIZE ? nullptr : this->slot_(this->back_inx_); }
|
||||
ESPHOME_ALWAYS_INLINE void push_back() { this->advance_(this->back_inx_); }
|
||||
ESPHOME_ALWAYS_INLINE bool is_empty() const { return this->count_() == 0; }
|
||||
|
||||
protected:
|
||||
ESPHOME_ALWAYS_INLINE uint8_t count_() const {
|
||||
return static_cast<uint8_t>(atomic_get(this->back_inx_) - atomic_get(this->front_inx_));
|
||||
}
|
||||
ESPHOME_ALWAYS_INLINE Frame *slot_(const std::atomic<uint8_t> &inx) { return &this->store_[atomic_get(inx) % SIZE]; }
|
||||
ESPHOME_ALWAYS_INLINE void advance_(std::atomic<uint8_t> &inx) {
|
||||
atomic_set<uint8_t>(inx, static_cast<uint8_t>(atomic_get(inx) + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit the message on the front of the send_queue out on the CEC line
|
||||
*/
|
||||
void transmit_message();
|
||||
|
||||
/**
|
||||
* This method is called from within the receiver isr method after receiving each byte.
|
||||
* So, the fields that it updates need to be 'atomic'.
|
||||
* @param eom the value of the received 'eom' bit
|
||||
* @param ack the value of the received 'ack' bit
|
||||
*/
|
||||
void got_byte_eom_ack(bool eom, bool ack);
|
||||
|
||||
/**
|
||||
* This method is called from within the receiver isr method,
|
||||
* when receiving a start-bit, to notify that the CEC bus line is busy
|
||||
*/
|
||||
void got_start_of_activity();
|
||||
|
||||
/**
|
||||
* The transmitter immediatly sends an acknowledge bit on behalf of
|
||||
* the receiver, during the receipt of a message.
|
||||
* This is either done by activating the UART, if configured and available,
|
||||
* otherwise by direct GPIO pin manipulation.
|
||||
*/
|
||||
void send_ack();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Send the CEC protocol frame start bit.
|
||||
* While doing so, check if another initiator tries to do the same, and if so, abort.
|
||||
* @return true: start bit was successful, false: bus collision is detected and abort
|
||||
*/
|
||||
bool send_start_bit_();
|
||||
void send_bit_(bool bit_value);
|
||||
bool send_high_and_test_();
|
||||
void transmit_message_on_gpio_(const Frame *frame);
|
||||
void transmit_message_on_uart_(const Frame *frame);
|
||||
void convert_byte_to_uart_(std::vector<uint8_t> &uart_data, uint8_t byte, bool is_header, bool is_eom);
|
||||
bool transmit_my_address_(uint8_t initiator_addr); // send 4 bits with my address and check for bus collision
|
||||
|
||||
FrameRingBuffer<5> send_queue_; // queue at most a handful of messages to send
|
||||
uint8_t transmit_attempts_{0};
|
||||
uint8_t n_bytes_received_{0};
|
||||
uint8_t n_acks_received_{0};
|
||||
TransmitState transmit_state_{TransmitState::IDLE};
|
||||
std::atomic<bool> eom_received_{false};
|
||||
std::atomic<bool> receiver_is_busy_{false};
|
||||
uint32_t confirm_received_us_{0};
|
||||
uint32_t required_idle_period_{0};
|
||||
uart::UARTComponent *uart_{nullptr};
|
||||
InternalGPIOPin *pin_{nullptr};
|
||||
};
|
||||
|
||||
class CECReceive {
|
||||
enum class ReceiverState : uint8_t { IDLE, RECEIVING_BYTE, WAITING_FOR_EOM, WAITING_FOR_ACK, WAITING_FOR_EOM_ACK };
|
||||
|
||||
public:
|
||||
constexpr static int MAX_FRAMES_QUEUED = 4;
|
||||
CECReceive(CECTransmit &xmit) : xmit_(xmit) {}
|
||||
void setup(InternalGPIOPin *pin, uint8_t address);
|
||||
void dump_config();
|
||||
void set_promiscuous_mode(bool promiscuous) { promiscuous_mode_ = promiscuous; }
|
||||
void set_monitor_mode(bool monitor_mode) { monitor_mode_ = monitor_mode; }
|
||||
bool get_monitor_mode() const { return monitor_mode_; }
|
||||
FrameRingBuffer<MAX_FRAMES_QUEUED> frames_queue_;
|
||||
|
||||
protected:
|
||||
static void gpio_isr_s(CECReceive *self);
|
||||
void gpio_isr_();
|
||||
void reset_state_variables_();
|
||||
|
||||
CECTransmit &xmit_; // the bus 'Receiver' needs to sync with 'sent' messages
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
bool promiscuous_mode_{false};
|
||||
bool monitor_mode_{false};
|
||||
bool recv_ack_queued_{false};
|
||||
bool prev_pin_level_{true};
|
||||
uint8_t recv_bit_counter_{0};
|
||||
uint8_t recv_byte_buffer_{0};
|
||||
uint8_t num_acks_{0}; // numer of 'low' acknowledge bits received in the current message
|
||||
uint8_t address_{0};
|
||||
uint32_t last_falling_edge_us_{0};
|
||||
ReceiverState receiver_state_{ReceiverState::IDLE};
|
||||
Frame *recv_frame_buffer_{nullptr};
|
||||
std::array<Frame, SIZE> store_{};
|
||||
std::atomic<uint8_t> front_inx_{0};
|
||||
std::atomic<uint8_t> back_inx_{0};
|
||||
};
|
||||
|
||||
/// The HDMI-CEC bus on one GPIO pin. The receive interrupt decodes the edges of every frame,
|
||||
/// including those this device sends, and queues complete frames for loop(); loop() runs the
|
||||
/// on_message automations and transmits queued frames, one blocking bit-banged attempt per pass.
|
||||
class HDMICEC : public Component {
|
||||
public:
|
||||
void set_pin(InternalGPIOPin *pin);
|
||||
void set_address(uint8_t address) { address_ = address & 0xf; }
|
||||
uint8_t address() const { return address_; }
|
||||
void set_physical_address(uint16_t physical_address) { physical_address_ = physical_address; }
|
||||
uint16_t get_physical_address() const { return physical_address_; }
|
||||
void set_promiscuous_mode(bool promiscuous) { recv_.set_promiscuous_mode(promiscuous); }
|
||||
void set_monitor_mode(bool monitor_mode) { recv_.set_monitor_mode(monitor_mode); }
|
||||
void set_osd_name_bytes(const std::vector<uint8_t> &osd_name_bytes) { osd_name_bytes_ = osd_name_bytes; }
|
||||
void set_uart(uart::UARTComponent *uart) { xmit_.set_uart(uart); }
|
||||
void add_message_trigger(MessageTrigger *trigger) { message_triggers_.push_back(trigger); }
|
||||
uint8_t get_device_type() const;
|
||||
|
||||
// Generic 'send' of a bus message, it pushes its message into a send buffer for later transmit.
|
||||
bool send(uint8_t source, uint8_t destination, const uint8_t *payload_bytes, unsigned int payload_size);
|
||||
|
||||
// More compact 'send' wrappers for easier use:
|
||||
// Preferred for most use cases, with a compile-time-fixed argument list:
|
||||
bool send(uint8_t destination, const std::initializer_list<uint8_t> &payload) {
|
||||
return send(address_, destination, payload.begin(), payload.size());
|
||||
}
|
||||
// For use when the payload is stored by the caller in an std::vector or std::array:
|
||||
bool send(uint8_t destination, const uint8_t *payload_bytes, unsigned int payload_size) {
|
||||
return send(address_, destination, payload_bytes, payload_size);
|
||||
}
|
||||
|
||||
// Component overrides
|
||||
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
|
||||
HDMICEC(InternalGPIOPin *pin, uint8_t address) : pin_(pin), address_(address) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
void try_builtin_handler_(uint8_t source, uint8_t destination, const std::vector<uint8_t> &data);
|
||||
void handle_received_message_(const Frame *frame);
|
||||
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
|
||||
void set_physical_address(uint16_t physical_address) { this->physical_address_ = physical_address; }
|
||||
/// Answer "Give OSD Name" with this name; it lives in flash
|
||||
void set_osd_name(const char *name, uint8_t length) {
|
||||
this->osd_name_ = name;
|
||||
this->osd_name_length_ = length;
|
||||
}
|
||||
/// Opcodes an on_message automation answers itself, sorted, in flash; no built-in reply for them
|
||||
void set_claimed_opcodes(const uint8_t *opcodes, uint8_t count) {
|
||||
this->claimed_opcodes_ = opcodes;
|
||||
this->claimed_opcode_count_ = count;
|
||||
}
|
||||
#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
|
||||
template<typename F> void add_on_message_callback(F &&callback) {
|
||||
this->message_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif
|
||||
|
||||
HighFrequencyLoopRequester fast_loop_;
|
||||
InternalGPIOPin *pin_{nullptr};
|
||||
uint8_t address_; // logical address, relates to device type
|
||||
uint16_t physical_address_;
|
||||
std::vector<uint8_t> osd_name_bytes_;
|
||||
std::vector<MessageTrigger *> message_triggers_;
|
||||
|
||||
CECTransmit xmit_;
|
||||
CECReceive recv_{xmit_};
|
||||
};
|
||||
|
||||
class MessageTrigger : public Trigger<uint8_t, uint8_t, std::vector<uint8_t>> {
|
||||
friend class HDMICEC;
|
||||
|
||||
public:
|
||||
explicit MessageTrigger(HDMICEC *parent) { parent->add_message_trigger(this); };
|
||||
void set_source(uint8_t source) { source_ = source; };
|
||||
void set_destination(uint8_t destination) { destination_ = destination; };
|
||||
void set_opcode(uint8_t opcode) { opcode_ = opcode; };
|
||||
void set_data(const std::vector<uint8_t> &data) { data_ = data; };
|
||||
|
||||
protected:
|
||||
optional<uint8_t> source_;
|
||||
optional<uint8_t> destination_;
|
||||
optional<uint8_t> opcode_;
|
||||
optional<std::vector<uint8_t>> data_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SendAction : public Action<Ts...> {
|
||||
public:
|
||||
SendAction(HDMICEC *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(uint8_t, source)
|
||||
TEMPLATABLE_VALUE(uint8_t, destination)
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto source_address = this->source_.has_value() ? this->source_.value(x...) : this->parent_->address();
|
||||
auto destination_address = this->destination_.value(x...);
|
||||
const std::vector<uint8_t> &payload = this->data_.value(x...);
|
||||
|
||||
this->parent_->send(source_address, destination_address, payload.data(), payload.size());
|
||||
/// Queue a frame for transmission; false when the queue is full or in monitor mode
|
||||
bool send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload);
|
||||
bool send(uint8_t destination, std::span<const uint8_t> payload) {
|
||||
return this->send(this->address_, destination, payload);
|
||||
}
|
||||
/// For lambdas: id(cec).send(0x0, {0x44, 0x41});
|
||||
bool send(uint8_t destination, std::initializer_list<uint8_t> payload) {
|
||||
return this->send(destination, std::span<const uint8_t>(payload.begin(), payload.size()));
|
||||
}
|
||||
|
||||
protected:
|
||||
HDMICEC *parent_;
|
||||
enum class ReceiverState : uint8_t {
|
||||
RECEIVER_STATE_IDLE,
|
||||
RECEIVER_STATE_RECEIVING_BYTE,
|
||||
RECEIVER_STATE_WAITING_FOR_EOM,
|
||||
RECEIVER_STATE_WAITING_FOR_ACK,
|
||||
};
|
||||
|
||||
// Receive side, in the interrupt
|
||||
static void gpio_isr_s(HDMICEC *self);
|
||||
void gpio_isr_();
|
||||
void reset_receiver_();
|
||||
/// Pull the line low for the acknowledge bit of a frame addressed to us
|
||||
void send_ack_();
|
||||
// A hardware one-shot timer releases the line after the acknowledge bit (gptimer on the ESP32,
|
||||
// timer 1 on the ESP8266, a hardware alarm on the RP2040), so the interrupt returns at once;
|
||||
// without one the interrupt holds the line for the 1.5 ms itself
|
||||
bool setup_ack_timer_();
|
||||
void start_ack_timer_();
|
||||
#ifdef USE_ESP32
|
||||
static bool ack_timer_isr_s(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg);
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
static void ack_timer_isr_s();
|
||||
#endif
|
||||
#ifdef USE_RP2040
|
||||
static void ack_timer_isr_s(uint alarm_num);
|
||||
#endif
|
||||
|
||||
/// The built-in replies to a frame addressed to us that no automation claims
|
||||
void reply_builtin_(const Frame *frame);
|
||||
bool is_claimed_(uint8_t opcode) const;
|
||||
|
||||
// Transmit side, in loop()
|
||||
void transmit_message_();
|
||||
/// Keep the frame for another attempt, or drop it once the attempts are used up
|
||||
void finish_attempt_(bool sent_ok);
|
||||
/// Drive the frame onto the line; false when another initiator sent at the same time
|
||||
bool transmit_frame_(const Frame *frame);
|
||||
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);
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
/// Hand the frame after the initiator address to the UART, five UART bits per CEC bit
|
||||
void transmit_on_uart_(const Frame *frame);
|
||||
#endif
|
||||
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);
|
||||
this->isr_pin_.digital_write(false);
|
||||
}
|
||||
|
||||
static constexpr size_t QUEUE_SIZE = 4;
|
||||
// Each queue is 70 bytes, so two single-byte fields follow each to fill the alignment gap
|
||||
FrameRingBuffer<QUEUE_SIZE> frames_queue_; // received, interrupt to loop()
|
||||
uint8_t recv_bit_counter_{0};
|
||||
uint8_t recv_byte_{0};
|
||||
FrameRingBuffer<QUEUE_SIZE> send_queue_; // to send, both ends on loop()
|
||||
uint8_t transmit_attempts_{0};
|
||||
uint8_t reported_dropped_frames_{0};
|
||||
#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
|
||||
StaticCallbackManager<HDMI_CEC_MESSAGE_CALLBACK_COUNT, void(uint8_t, uint8_t, std::span<const uint8_t>)>
|
||||
message_callbacks_;
|
||||
#endif
|
||||
InternalGPIOPin *pin_;
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
const char *osd_name_{nullptr}; // in flash
|
||||
const uint8_t *claimed_opcodes_{nullptr}; // in flash, sorted
|
||||
#ifdef USE_HDMI_CEC_UART
|
||||
uart::UARTComponent *uart_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
gptimer_handle_t ack_timer_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_RP2040
|
||||
int ack_alarm_{-1};
|
||||
#endif
|
||||
|
||||
// The word-sized fields come first and the single-byte ones form one run, so nothing is padded
|
||||
std::atomic<uint32_t> last_edge_us_{0}; // the bus is busy until this is some time ago; interrupt writes
|
||||
Frame *recv_frame_{nullptr}; // interrupt only; null when the queue was full or the frame too long
|
||||
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
|
||||
|
||||
// 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_
|
||||
// says the frame is over
|
||||
std::atomic<bool> eom_received_{false}; // set for every frame, ours or not
|
||||
std::atomic<uint8_t> dropped_frames_{0};
|
||||
uint8_t n_bytes_received_{0};
|
||||
uint8_t n_acks_received_{0};
|
||||
bool transmitting_{false};
|
||||
|
||||
// Receive state, interrupt only
|
||||
uint8_t recv_header_{0}; // kept outside the frame so a dropped frame can still be judged
|
||||
uint8_t recv_length_{0};
|
||||
ReceiverState receiver_state_{ReceiverState::RECEIVER_STATE_IDLE};
|
||||
bool recv_eom_{false};
|
||||
bool recv_ack_queued_{false};
|
||||
bool prev_pin_level_{true};
|
||||
|
||||
uint16_t physical_address_{UNSET_PHYSICAL_ADDRESS};
|
||||
uint8_t osd_name_length_{0};
|
||||
uint8_t claimed_opcode_count_{0};
|
||||
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 hdmi_cec
|
||||
} // namespace esphome
|
||||
} // namespace esphome::hdmi_cec
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
#define USE_MDNS
|
||||
#define USE_MDNS_STORE_SERVICES
|
||||
#define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2
|
||||
#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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
hdmi_cec:
|
||||
id: cec
|
||||
pin: ${cec_pin} # HDMI pin 13, with HDMI pin 17 as ground
|
||||
address: 0x5
|
||||
physical_address: 0x4000
|
||||
osd_name: ESPHome
|
||||
uart_id: uart_bus # its tx_pin drives the same line through a diode
|
||||
on_message:
|
||||
- opcode: 0x8F # Give Device Power Status
|
||||
then:
|
||||
- hdmi_cec.send:
|
||||
destination: !lambda return source;
|
||||
data: [0x90, 0x01] # Report Power Status: standby
|
||||
@@ -2,6 +2,8 @@ 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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
hdmi_cec:
|
||||
id: cec
|
||||
pin: # to HDMI pin 13 (CEC), with HDMI pin 17 (DDC/CEC ground)
|
||||
number: ${cec_pin}
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
|
||||
address: 0x5
|
||||
physical_address: 0x4000
|
||||
promiscuous_mode: true
|
||||
on_message:
|
||||
# Give Device Power Status
|
||||
- opcode: 0x8F
|
||||
then:
|
||||
- lambda: !lambda |-
|
||||
id(cec).send(source, {0x90, 0x01});
|
||||
# Volume Up from TV remote
|
||||
- data: [0x44, 0x41]
|
||||
then:
|
||||
- logger.log: "HDMI-CEC Volume Up"
|
||||
@@ -1,31 +0,0 @@
|
||||
uart:
|
||||
id: cec_uart
|
||||
tx_pin: ${uart_pin} # has diode to cec pin, with kathode here on uart
|
||||
baud_rate: 38400
|
||||
|
||||
hdmi_cec:
|
||||
id: cec
|
||||
pin: # to HDMI pin 13 (CEC), with HDMI pin 17 (DDC/CEC ground)
|
||||
number: ${cec_pin}
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
|
||||
address: 0x5
|
||||
physical_address: 0x4000
|
||||
uart_id: cec_uart
|
||||
promiscuous_mode: true
|
||||
monitor_mode: false
|
||||
on_message:
|
||||
- opcode: 0x8F # Give Device Power Status
|
||||
then:
|
||||
hdmi_cec.send:
|
||||
destination: 0 # answer to TV
|
||||
data: [0x90, 0x01] # power is 'standby'
|
||||
- opcode: 0x8C # Give Device VendorID
|
||||
then:
|
||||
- lambda: |-
|
||||
id(cec).send(0xf, {0x87, 0x00, 0x09, 0xb0}); // broadcast myself as Onkyo
|
||||
- data: [0x44, 0x41] # Volume Up from TV remote
|
||||
then:
|
||||
- logger.log: "HDMI-CEC Volume Up"
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
cec_pin: GPIO14
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_2083/esp32-idf.yaml
|
||||
hdmi_cec: !include common-uart.yaml
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
cec_pin: GPIO14
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_2083/esp8266-ard.yaml
|
||||
hdmi_cec: !include common-uart.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
# tested with Seeed Studio XIAO ESP32C3
|
||||
substitutions:
|
||||
uart_pin: GPIO21
|
||||
cec_pin: GPIO10
|
||||
|
||||
<<: !include common_with_uart.yaml
|
||||
@@ -1,9 +0,0 @@
|
||||
# tested with LOLIN D1 mini which has an esp8266ex
|
||||
substitutions:
|
||||
uart_pin: GPIO2
|
||||
cec_pin: GPIO14
|
||||
|
||||
# UART0 has Tx on GPIO1, and creates a print-message during boot
|
||||
# UART1 has Tx on GPIO2, is a transmit-only uart, often used for logging
|
||||
|
||||
<<: !include common_with_uart.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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
|
||||
@@ -0,0 +1,11 @@
|
||||
# Common UART configuration for ESP8266 Arduino tests - 2083 baud
|
||||
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
uart:
|
||||
- id: uart_bus
|
||||
tx_pin: ${tx_pin}
|
||||
rx_pin: ${rx_pin}
|
||||
baud_rate: 2083
|
||||
Reference in New Issue
Block a user