mirror of
https://github.com/esphome/esphome.git
synced 2026-09-01 02:26:01 +00:00
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)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -22,18 +22,30 @@ template<typename... Ts> class MockUartInjectRXAction : public Action<Ts...>, 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<size_t>(this->len_));
|
||||
std::vector<uint8_t> 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<size_t>(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<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
|
||||
@@ -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<uint8_t> &data) {
|
||||
}
|
||||
}
|
||||
|
||||
void MockUartComponent::inject_to_rx_buffer_delayed(const std::vector<uint8_t> &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
|
||||
|
||||
@@ -43,6 +43,8 @@ class MockUartComponent : public uart::UARTComponent, public Component {
|
||||
void set_tx_hook(std::function<void(const std::vector<uint8_t> &)> &&cb) { this->tx_hook_ = std::move(cb); }
|
||||
void inject_to_rx_buffer(const std::vector<uint8_t> &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<uint8_t> &data, uint32_t delay_ms);
|
||||
|
||||
protected:
|
||||
void check_logger_conflict() override {}
|
||||
@@ -82,6 +84,14 @@ class MockUartComponent : public uart::UARTComponent, public Component {
|
||||
};
|
||||
std::vector<PeriodicRx> 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<uint8_t> data;
|
||||
uint32_t available_at_ms; // millis() time when bytes become available
|
||||
};
|
||||
std::deque<StagedRx> staged_rx_;
|
||||
|
||||
// Observability
|
||||
uint32_t tx_count_{0};
|
||||
uint32_t rx_count_{0};
|
||||
|
||||
@@ -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<uint8_t>({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
|
||||
|
||||
Reference in New Issue
Block a user