From 5319b8593b352f314b1baa8d9cc32017e17e1105 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 18:07:18 -1000 Subject: [PATCH] Add latency simulation to uart_mock for USB UART test Add inject_to_rx_buffer_delayed() to uart_mock which stages bytes that aren't visible to available() until the delay elapses. This simulates USB packet delivery latency. The test uses a 40ms delay which is: - Greater than the old ~2ms timeout (fails without fix) - Less than the new 50ms fallback timeout (passes with fix) --- .../external_components/uart_mock/__init__.py | 3 +++ .../uart_mock/automation.h | 16 ++++++++++++++-- .../uart_mock/uart_mock.cpp | 19 +++++++++++++++++++ .../external_components/uart_mock/uart_mock.h | 10 ++++++++++ .../uart_mock_modbus_no_threshold.yaml | 12 ++++++------ 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/tests/integration/fixtures/external_components/uart_mock/__init__.py b/tests/integration/fixtures/external_components/uart_mock/__init__.py index 6b9d0e2497..fdd481b397 100644 --- a/tests/integration/fixtures/external_components/uart_mock/__init__.py +++ b/tests/integration/fixtures/external_components/uart_mock/__init__.py @@ -62,6 +62,7 @@ CONFIG_INJECT_RX_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MockUartComponent), cv.Required("data"): cv.templatable(validate_raw_data), + cv.Optional(CONF_DELAY): cv.positive_time_period_milliseconds, }, key=CONF_DATA, ) @@ -126,6 +127,8 @@ async def inject_rx_to_code(config, action_id, template_arg, args): arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) + if CONF_DELAY in config: + cg.add(var.set_delay(config[CONF_DELAY])) return var diff --git a/tests/integration/fixtures/external_components/uart_mock/automation.h b/tests/integration/fixtures/external_components/uart_mock/automation.h index 83a057d3a0..58a8e858de 100644 --- a/tests/integration/fixtures/external_components/uart_mock/automation.h +++ b/tests/integration/fixtures/external_components/uart_mock/automation.h @@ -22,18 +22,30 @@ template class MockUartInjectRXAction : public Action, pu this->len_ = len; // Length >= 0 indicates static mode } + void set_delay(uint32_t delay_ms) { this->delay_ms_ = delay_ms; } + void play(const Ts &...x) override { if (this->len_ >= 0) { // Static mode: use pointer and length - this->parent_->inject_to_rx_buffer(this->code_.data, static_cast(this->len_)); + std::vector data(this->code_.data, this->code_.data + this->len_); + if (this->delay_ms_ > 0) { + this->parent_->inject_to_rx_buffer_delayed(data, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(this->code_.data, static_cast(this->len_)); + } } else { // Template mode: call function auto val = this->code_.func(x...); - this->parent_->inject_to_rx_buffer(val); + if (this->delay_ms_ > 0) { + this->parent_->inject_to_rx_buffer_delayed(val, this->delay_ms_); + } else { + this->parent_->inject_to_rx_buffer(val); + } } } protected: + uint32_t delay_ms_{0}; ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Code { std::vector (*func)(Ts...); // Function pointer (stateless lambdas) diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp index e8c07d632d..395fe99b59 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.cpp @@ -53,6 +53,14 @@ void MockUartComponent::loop() { } } + // Process staged RX - deliver bytes whose delay has elapsed + while (!this->staged_rx_.empty() && millis() >= this->staged_rx_.front().available_at_ms) { + auto &staged = this->staged_rx_.front(); + ESP_LOGD(TAG, "Delivering %zu staged RX bytes", staged.data.size()); + this->inject_to_rx_buffer(staged.data); + this->staged_rx_.pop_front(); + } + // Process delayed responses for (auto &response : this->responses_) { if (response.delay_ms > 0 && response.last_match_ms > 0 && now - response.last_match_ms >= response.delay_ms) { @@ -208,4 +216,15 @@ void MockUartComponent::inject_to_rx_buffer(const std::vector &data) { } } +void MockUartComponent::inject_to_rx_buffer_delayed(const std::vector &data, uint32_t delay_ms) { + if (!data.empty() && data.size() <= 64) { + char hex_buf[format_hex_pretty_size(64)]; + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay: %s", data.size(), delay_ms, + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); + } else if (data.size() > 64) { + ESP_LOGD(TAG, "Staging %zu RX bytes with %ums delay (too large to log inline)", data.size(), delay_ms); + } + this->staged_rx_.push_back({data, millis() + delay_ms}); +} + } // namespace esphome::uart_mock diff --git a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h index 901e371dec..32517bffba 100644 --- a/tests/integration/fixtures/external_components/uart_mock/uart_mock.h +++ b/tests/integration/fixtures/external_components/uart_mock/uart_mock.h @@ -43,6 +43,8 @@ class MockUartComponent : public uart::UARTComponent, public Component { void set_tx_hook(std::function &)> &&cb) { this->tx_hook_ = std::move(cb); } void inject_to_rx_buffer(const std::vector &data); void inject_to_rx_buffer(const uint8_t *data, size_t len); + // Stage bytes for delayed delivery - simulates transport-level latency (e.g., USB packets) + void inject_to_rx_buffer_delayed(const std::vector &data, uint32_t delay_ms); protected: void check_logger_conflict() override {} @@ -82,6 +84,14 @@ class MockUartComponent : public uart::UARTComponent, public Component { }; std::vector periodic_rx_; + // Staged RX - bytes that are pending delivery after a delay + // Simulates transport-level latency (e.g., USB packet delivery) + struct StagedRx { + std::vector data; + uint32_t available_at_ms; // millis() time when bytes become available + }; + std::deque staged_rx_; + // Observability uint32_t tx_count_{0}; uint32_t rx_count_{0}; diff --git a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml index 1beb01e9fa..f4cfd613ea 100644 --- a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml @@ -30,7 +30,7 @@ uart_mock: condition: #Read 80 input registers on device 2, starting at address 0 (SDM meter request) lambda: "return data == std::vector({0x02,0x04,0x00,0x00,0x00,0x50,0xF0,0x05});" then: - - uart_mock.inject_rx: # Good response from SDM meter with CRC. Voltage is 243V + - uart_mock.inject_rx: # First USB packet: SDM meter response part 1 !lambda return {0x02,0x04,0xA0,0x43,0x73,0x19,0x9A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, @@ -38,11 +38,11 @@ uart_mock: 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; - - delay: 20ms # Simulate USB packet gap - would fail with old ~2ms timeout - - uart_mock.inject_rx: # Rest of response (including CRC) - !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, - 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; + - uart_mock.inject_rx: # Second USB packet: rest of response (staged with 20ms latency) + delay: 40ms + data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, + 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; modbus: uart_id: virtual_uart_dev