16#if defined(USE_ESP8266) || defined(USE_RP2040)
18static HDMICEC *ack_timer_owner =
nullptr;
22static constexpr uint32_t START_BIT_MIN_US = 3500;
23static constexpr uint32_t START_BIT_NOM_US = 3700;
24static constexpr uint32_t START_BIT_HIGH_US = 800;
25static constexpr uint32_t HIGH_BIT_MIN_US = 400;
26static constexpr uint32_t HIGH_BIT_US = 600;
27static constexpr uint32_t LOW_BIT_US = 1500;
28static constexpr uint32_t SAFE_SAMPLE_US = 1050;
29static constexpr uint32_t TOTAL_BIT_US = 2400;
30static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_RECEIVE = TOTAL_BIT_US * 5;
31static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_FAIL = TOTAL_BIT_US * 3;
32static constexpr uint32_t SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS = TOTAL_BIT_US * 7;
33static constexpr uint32_t ABORTED_FRAME_US = TOTAL_BIT_US * 15;
34static constexpr uint8_t MAX_ATTEMPTS = 8;
36Frame::Frame(uint8_t initiator_addr, uint8_t destination_addr, std::span<const uint8_t> payload) {
38 this->
size_ = 1 + payload.size();
39 std::copy(
payload.begin(),
payload.end(), this->data_.begin() + 1);
49 ESP_LOGE(TAG,
"Failed to set up the acknowledge timer");
63 " Promiscuous mode: %s",
65 LOG_PIN(
" Pin: ", this->
pin_);
72 ESP_LOGW(TAG,
"%u received frame%s dropped (queue full or oversized)",
count,
73 count == 1 ? LOG_STR_LITERAL(
"") : LOG_STR_LITERAL(
"s"));
74 this->reported_dropped_frames_ = dropped;
78#ifdef ESPHOME_LOG_HAS_DEBUG
82#ifdef HDMI_CEC_MESSAGE_CALLBACK_COUNT
83 this->
message_callbacks_.call(frame->initiator_addr(), frame->destination_addr(), frame->payload());
96bool HDMICEC::send(uint8_t source, uint8_t destination, std::span<const uint8_t> payload) {
98 ESP_LOGW(TAG,
"Monitor mode, not sending 0x%X->0x%X", source, destination);
101 if (source > 0xf || destination > 0xf) {
102 ESP_LOGE(TAG,
"Send abort: logical address 0x%X->0x%X out of range", source, destination);
106 ESP_LOGE(TAG,
"Send abort 0x%X->0x%X: payload of %zu bytes exceeds %zu", source, destination, payload.size(),
111 if (frame ==
nullptr) {
112 ESP_LOGW(TAG,
"Send queue full, frame 0x%X->0x%X dropped", source, destination);
115 *frame =
Frame(source, destination, payload);
136 if (bytes_ok && acks_ok) {
139 }
else if (!bytes_ok) {
140 ESP_LOGD(TAG,
"Send frame incorrect on attempt %u, saw %u bytes but expected %zu", this->
transmit_attempts_,
162#ifdef ESPHOME_LOG_HAS_DEBUG
182 this->
required_idle_period_ = sent_ok ? SIGNAL_FREE_TIME_AFTER_XMIT_SUCCESS : SIGNAL_FREE_TIME_AFTER_XMIT_FAIL;
197 const auto bytes = frame->
bytes();
198 for (
size_t byte_inx = 0; byte_inx < bytes.size(); byte_inx++) {
199 const uint8_t current_byte = bytes[byte_inx];
202 for (int8_t i = 7; i >= 0; i--) {
203 const bool bit_value = (current_byte >> i) & 0x1;
204 if (!this->
send_bit_(bit_value) && byte_inx == 0 && i >= 4) {
205 ESP_LOGD(TAG,
"Bus collision on sending initiator address 0x%X on attempt %u", frame->
initiator_addr(),
206 this->transmit_attempts_);
211 this->
send_bit_(byte_inx == bytes.size() - 1);
219 ESP_LOGD(TAG,
"Bus occupied before start bit on attempt %u", this->
transmit_attempts_);
231 ESP_LOGD(TAG,
"Bus collision on sending start bit on attempt %u", this->
transmit_attempts_);
246 if (elapsed < SAFE_SAMPLE_US) {
252 if (elapsed < TOTAL_BIT_US) {
264#if defined(USE_ESP32)
266 gptimer_config_t config = {
267 .clk_src = GPTIMER_CLK_SRC_DEFAULT,
268 .direction = GPTIMER_COUNT_UP,
269 .resolution_hz = 1000000,
271 if (gptimer_new_timer(&config, &this->
ack_timer_) != ESP_OK) {
276 gptimer_alarm_config_t alarm = {
277 .alarm_count = LOW_BIT_US, .reload_count = 0, .flags = {.auto_reload_on_alarm =
true}};
278 return gptimer_register_event_callbacks(this->
ack_timer_, &callbacks,
this) == ESP_OK &&
279 gptimer_set_alarm_action(this->
ack_timer_, &alarm) == ESP_OK && gptimer_enable(this->
ack_timer_) == ESP_OK;
285 auto *self =
static_cast<HDMICEC *
>(arg);
291#elif defined(USE_ESP8266)
292static constexpr uint32_t TIMER1_TICKS_PER_US = 5;
295 ack_timer_owner =
this;
298 timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
306#elif defined(USE_RP2040)
308 this->
ack_alarm_ = hardware_alarm_claim_unused(
false);
312 ack_timer_owner =
this;
318 hardware_alarm_set_target(this->
ack_alarm_, make_timeout_time_us(LOW_BIT_US));
361 if (pulse_duration > START_BIT_MIN_US) {
371 if (pulse_duration < HIGH_BIT_MIN_US / 4) {
374 const bool value = pulse_duration <= SAFE_SAMPLE_US;
void mark_failed()
Mark this component as failed.
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void enable_loop()
Enable this component's loop.
void disable_loop()
Disable this component's loop.
void attach_interrupt(void(*func)(T *), T *arg, gpio::InterruptType type) const
ISRInternalGPIOPin to_isr() const
One CEC bus message: a header byte with the initiator and destination addresses, then up to 15 payloa...
ESPHOME_ALWAYS_INLINE bool push_back(uint8_t data)
False when the frame is full already; the bus allows no more.
std::span< const uint8_t > bytes() const
static constexpr size_t MAX_LENGTH
static constexpr size_t MAX_PAYLOAD_LENGTH
std::span< const uint8_t > payload() const
The bytes after the header, starting with the opcode.
ESPHOME_ALWAYS_INLINE uint8_t initiator_addr() const
ESPHOME_ALWAYS_INLINE size_t size() const
ESPHOME_ALWAYS_INLINE bool is_broadcast() const
ESPHOME_ALWAYS_INLINE uint8_t destination_addr() const
std::array< uint8_t, MAX_LENGTH > data_
ESPHOME_ALWAYS_INLINE void clear()
ESPHOME_ALWAYS_INLINE void pop_front()
ESPHOME_ALWAYS_INLINE Frame * back()
ESPHOME_ALWAYS_INLINE void push_back()
ESPHOME_ALWAYS_INLINE Frame * front()
ESPHOME_ALWAYS_INLINE bool is_empty() const
The HDMI-CEC bus on one GPIO pin.
std::atomic< uint32_t > last_edge_us_
void dump_config() override
FrameRingBuffer< QUEUE_SIZE > send_queue_
uint8_t reported_dropped_frames_
static void gpio_isr_s(HDMICEC *self)
std::atomic< uint8_t > dropped_frames_
uint32_t last_falling_edge_us_
bool send(uint8_t source, uint8_t destination, std::span< const uint8_t > payload)
Queue a frame for transmission; false when the queue is full or in monitor mode.
gptimer_handle_t ack_timer_
static void ack_timer_isr_s()
StaticCallbackManager< HDMI_CEC_MESSAGE_CALLBACK_COUNT, void(uint8_t, uint8_t, std::span< const uint8_t >)> message_callbacks_
uint8_t transmit_attempts_
uint8_t recv_bit_counter_
ISRInternalGPIOPin isr_pin_
void send_ack_()
Pull the line low for the acknowledge bit of a frame addressed to us.
ESPHOME_ALWAYS_INLINE void set_pin_output_low_()
void finish_attempt_(bool sent_ok)
Keep the frame for another attempt, or drop it once the attempts are used up.
ReceiverState receiver_state_
uint16_t required_idle_period_
uint32_t last_attempt_us_
std::atomic< bool > eom_received_
uint8_t n_bytes_received_
bool send_bit_(bool bit_value)
Send one bit; false when a logical 1 found the line held low by another initiator.
@ RECEIVER_STATE_WAITING_FOR_ACK
@ RECEIVER_STATE_RECEIVING_BYTE
@ RECEIVER_STATE_WAITING_FOR_EOM
ESPHOME_ALWAYS_INLINE void set_pin_input_high_()
FrameRingBuffer< QUEUE_SIZE > frames_queue_
bool transmit_frame_(const Frame *frame)
Drive the frame onto the line; false when another initiator sent at the same time.
ESPHOME_ALWAYS_INLINE void atomic_set(std::atomic< T > &atom, T value)
ESPHOME_LOG_TAG(TAG, "hdmi_cec")
ESPHOME_ALWAYS_INLINE T atomic_get(const std::atomic< T > &atom)
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
uint32_t IRAM_ATTR HOT micros()
void delay_microseconds_safe(uint32_t us)
Delay for the given amount of microseconds, possibly yielding to other processes during the wait.
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".