Merge remote-tracking branch 'upstream/devirtualize-decode' into integration

This commit is contained in:
J. Nick Koston
2026-03-22 21:39:51 -10:00
31 changed files with 112 additions and 116 deletions
+4 -4
View File
@@ -1519,16 +1519,16 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
resp.instance = msg.instance;
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH;
switch (proxies[msg.instance]->flush_port()) {
case uart::FlushResult::SUCCESS:
case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS:
resp.status = enums::SERIAL_PROXY_STATUS_OK;
break;
case uart::FlushResult::ASSUMED_SUCCESS:
case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS:
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
break;
case uart::FlushResult::TIMEOUT:
case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT:
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
break;
case uart::FlushResult::FAILED:
case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED:
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
break;
}
@@ -1,9 +1,7 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#include "api_pb2_service.h"
#ifdef USE_API
#include "api_connection.h"
#endif
#include "esphome/core/log.h"
namespace esphome::api {
@@ -23,7 +21,6 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
}
#endif
#ifdef USE_API
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
@@ -709,6 +706,5 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
}
#endif // USE_API
} // namespace esphome::api
+3 -3
View File
@@ -103,17 +103,17 @@ size_t BLENUS::available() {
#endif
}
uart::FlushResult BLENUS::flush() {
uart::UARTFlushResult BLENUS::flush() {
constexpr uint32_t timeout_500ms = 500;
uint32_t start = millis();
while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) {
if (millis() - start > timeout_500ms) {
ESP_LOGW(TAG, "Flush timeout");
return uart::FlushResult::TIMEOUT;
return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
}
delay(1);
}
return uart::FlushResult::SUCCESS;
return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
}
void BLENUS::connected(bt_conn *conn, uint8_t err) {
+1 -1
View File
@@ -26,7 +26,7 @@ class BLENUS : public uart::UARTComponent, public Component {
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
uart::FlushResult flush() override;
uart::UARTFlushResult flush() override;
void check_logger_conflict() override {}
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
#ifdef USE_LOGGER
+42 -28
View File
@@ -4,12 +4,40 @@ import re
# pylint: disable=E0602
Import("env") # noqa
# IRAM size for testing mode (2MB - large enough to accommodate grouped tests)
TESTING_IRAM_SIZE = 0x200000
# Memory sizes for testing mode (large enough to accommodate grouped tests)
TESTING_IRAM_SIZE = 0x200000 # 2MB
TESTING_DRAM_SIZE = 0x200000 # 2MB
def patch_segment(content, segment_name, new_size):
"""Patch a memory segment's length in linker script content.
Handles both single-line and multi-line segment definitions, e.g.:
iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
or split across lines:
dram0_0_seg (RW) : org = 0x3FFB0000 + 0xdb5c,
len = 0x2c200 - 0xdb5c
Args:
content: Full linker script content as string
segment_name: Name of the segment (e.g., 'iram0_0_seg')
new_size: New size as integer
Returns:
Tuple of (new_content, was_patched)
"""
# Match segment name through to "len = <value>" allowing newlines between org and len
pattern = rf'({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)'
if match := re.search(pattern, content, re.DOTALL):
replacement = f"{match.group(1)}{new_size:#x}"
new_content = content[:match.start()] + replacement + content[match.end():]
if new_content != content:
return new_content, True
return content, False
def patch_idf_linker_script(source, target, env):
"""Patch ESP-IDF linker script to increase IRAM size for testing mode."""
"""Patch ESP-IDF linker script to increase IRAM and DRAM size for testing mode."""
# Check if we're in testing mode by looking for the define
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
@@ -34,36 +62,22 @@ def patch_idf_linker_script(source, target, env):
print(f"ESPHome: Error reading linker script: {e}")
return
# Check if this file contains iram0_0_seg
if 'iram0_0_seg' not in content:
print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}")
return
patches = []
# Look for iram0_0_seg definition and increase its length
# ESP-IDF format can be:
# iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
# or more complex with nested parentheses:
# iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000)
# We want to change len to TESTING_IRAM_SIZE for testing
content, patched = patch_segment(content, 'iram0_0_seg', TESTING_IRAM_SIZE)
if patched:
patches.append(f"IRAM={TESTING_IRAM_SIZE:#x}")
# Use a more robust approach: find the line and manually parse it
lines = content.split('\n')
for i, line in enumerate(lines):
if 'iram0_0_seg' in line and 'len' in line:
# Find the position of "len = " and replace everything after it until the end of the statement
match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line)
if match:
lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}"
break
content, patched = patch_segment(content, 'dram0_0_seg', TESTING_DRAM_SIZE)
if patched:
patches.append(f"DRAM={TESTING_DRAM_SIZE:#x}")
updated = '\n'.join(lines)
if updated != content:
if patches:
with open(memory_ld, "w") as f:
f.write(updated)
print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode")
f.write(content)
print(f"ESPHome: Patched {', '.join(patches)} in {memory_ld} for testing mode")
else:
print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}")
print(f"ESPHome: Warning - could not patch memory segments in {memory_ld}")
# Hook into the build process before linking
@@ -165,7 +165,7 @@ uint32_t SerialProxy::get_modem_pins() const {
(this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u);
}
uart::FlushResult SerialProxy::flush_port() {
uart::UARTFlushResult SerialProxy::flush_port() {
ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_);
return this->flush();
}
@@ -92,7 +92,7 @@ class SerialProxy : public uart::UARTDevice, public Component {
uint32_t get_modem_pins() const;
/// Flush the serial port (block until all TX data is sent)
uart::FlushResult flush_port();
uart::UARTFlushResult flush_port();
/// Set the RTS GPIO pin (from YAML configuration)
void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; }
+1 -1
View File
@@ -45,7 +45,7 @@ class UARTDevice {
size_t available() { return this->parent_->available(); }
FlushResult flush() { return this->parent_->flush(); }
UARTFlushResult flush() { return this->parent_->flush(); }
// Compat APIs
int read() {
+7 -12
View File
@@ -30,17 +30,12 @@ enum UARTDirection {
const LogString *parity_to_str(UARTParityOptions parity);
/// Result of a flush() call.
// Some vendor SDKs (e.g., Realtek) define SUCCESS as a macro.
// Save and restore around the enum to avoid collisions with our scoped enum value.
#pragma push_macro("SUCCESS")
#undef SUCCESS
enum class FlushResult {
SUCCESS, ///< Confirmed: all bytes left the TX FIFO.
TIMEOUT, ///< Confirmed: timed out before TX completed.
FAILED, ///< Confirmed: driver or hardware error.
ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed.
enum class UARTFlushResult {
UART_FLUSH_RESULT_SUCCESS, ///< Confirmed: all bytes left the TX FIFO.
UART_FLUSH_RESULT_TIMEOUT, ///< Confirmed: timed out before TX completed.
UART_FLUSH_RESULT_FAILED, ///< Confirmed: driver or hardware error.
UART_FLUSH_RESULT_ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed.
};
#pragma pop_macro("SUCCESS")
class UARTComponent {
public:
@@ -87,8 +82,8 @@ class UARTComponent {
virtual size_t available() = 0;
// Pure virtual method to block until all bytes have been written to the UART bus.
// @return FlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful.
virtual FlushResult flush() = 0;
// @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful.
virtual UARTFlushResult flush() = 0;
// Sets the maximum time to wait for TX to drain during flush().
// Only meaningful on ESP32 (IDF). Other platforms ignore this value.
@@ -213,14 +213,14 @@ size_t ESP8266UartComponent::available() {
return this->sw_serial_->available();
}
}
FlushResult ESP8266UartComponent::flush() {
UARTFlushResult ESP8266UartComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
if (this->hw_serial_ != nullptr) {
this->hw_serial_->flush();
} else {
this->sw_serial_->flush();
}
return FlushResult::ASSUMED_SUCCESS;
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate,
uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity,
@@ -58,7 +58,7 @@ class ESP8266UartComponent : public UARTComponent, public Component {
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
FlushResult flush() override;
UARTFlushResult flush() override;
uint32_t get_config();
@@ -360,15 +360,15 @@ size_t IDFUARTComponent::available() {
return available;
}
FlushResult IDFUARTComponent::flush() {
UARTFlushResult IDFUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_);
esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks);
if (err == ESP_OK)
return FlushResult::SUCCESS;
return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
if (err == ESP_ERR_TIMEOUT)
return FlushResult::TIMEOUT;
return FlushResult::FAILED;
return UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
return UARTFlushResult::UART_FLUSH_RESULT_FAILED;
}
void IDFUARTComponent::check_logger_conflict() {}
@@ -31,7 +31,7 @@ class IDFUARTComponent : public UARTComponent, public Component {
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
FlushResult flush() override;
UARTFlushResult flush() override;
void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; }
@@ -274,13 +274,13 @@ size_t HostUartComponent::available() {
return result;
};
FlushResult HostUartComponent::flush() {
UARTFlushResult HostUartComponent::flush() {
if (this->file_descriptor_ == -1) {
return FlushResult::ASSUMED_SUCCESS;
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
tcflush(this->file_descriptor_, TCIOFLUSH);
ESP_LOGV(TAG, " Flushing");
return FlushResult::ASSUMED_SUCCESS;
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
void HostUartComponent::update_error_(const std::string &error) {
@@ -18,7 +18,7 @@ class HostUartComponent : public UARTComponent, public Component {
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
FlushResult flush() override;
UARTFlushResult flush() override;
void set_name(std::string port_name) { port_name_ = port_name; };
protected:
@@ -170,10 +170,10 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) {
}
size_t LibreTinyUARTComponent::available() { return this->serial_->available(); }
FlushResult LibreTinyUARTComponent::flush() {
UARTFlushResult LibreTinyUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
this->serial_->flush();
return FlushResult::ASSUMED_SUCCESS;
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
void LibreTinyUARTComponent::check_logger_conflict() {
@@ -22,7 +22,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component {
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
FlushResult flush() override;
UARTFlushResult flush() override;
uint16_t get_config();
@@ -208,10 +208,10 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) {
return true;
}
size_t RP2040UartComponent::available() { return this->serial_->available(); }
FlushResult RP2040UartComponent::flush() {
UARTFlushResult RP2040UartComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
this->serial_->flush();
return FlushResult::ASSUMED_SUCCESS;
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
} // namespace esphome::uart
@@ -25,7 +25,7 @@ class RP2040UartComponent : public UARTComponent, public Component {
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
FlushResult flush() override;
UARTFlushResult flush() override;
uint16_t get_config();
+1 -1
View File
@@ -82,7 +82,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
uart::FlushResult flush() override;
uart::UARTFlushResult flush() override;
protected:
void check_logger_conflict() override;
@@ -325,10 +325,10 @@ size_t USBCDCACMInstance::available() {
return waiting + (this->has_peek_ ? 1 : 0);
}
uart::FlushResult USBCDCACMInstance::flush() {
uart::UARTFlushResult USBCDCACMInstance::flush() {
// Wait for TX ring buffer to be empty
if (this->usb_tx_ringbuf_ == nullptr) {
return uart::FlushResult::ASSUMED_SUCCESS;
return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
UBaseType_t waiting = 1;
@@ -342,10 +342,10 @@ uart::FlushResult USBCDCACMInstance::flush() {
// Also wait for USB to finish transmitting
esp_err_t err = tinyusb_cdcacm_write_flush(static_cast<tinyusb_cdcacm_itf_t>(this->itf_), pdMS_TO_TICKS(100));
if (err == ESP_OK)
return uart::FlushResult::SUCCESS;
return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
if (err == ESP_ERR_TIMEOUT)
return uart::FlushResult::TIMEOUT;
return uart::FlushResult::FAILED;
return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
return uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED;
}
void USBCDCACMInstance::check_logger_conflict() {}
+3 -3
View File
@@ -169,7 +169,7 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) {
this->parent_->start_output(this);
}
uart::FlushResult USBUartChannel::flush() {
uart::UARTFlushResult USBUartChannel::flush() {
// Spin until the output queue is drained and the last USB transfer completes.
// Safe to call from the main loop only.
// The flush_timeout_ms_ timeout guards against a device that stops responding mid-flush;
@@ -181,8 +181,8 @@ uart::FlushResult USBUartChannel::flush() {
yield();
}
if (!this->output_queue_.empty() || this->output_started_.load())
return uart::FlushResult::TIMEOUT;
return uart::FlushResult::SUCCESS;
return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
}
bool USBUartChannel::peek_byte(uint8_t *data) {
+1 -1
View File
@@ -140,7 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override { return this->input_buffer_.get_available(); }
uart::FlushResult flush() override;
uart::UARTFlushResult flush() override;
void check_logger_conflict() override {}
void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
void set_debug(bool debug) { this->debug_ = debug; }
+3 -3
View File
@@ -433,16 +433,16 @@ void WeikaiChannel::write_array(const uint8_t *buffer, size_t length) {
this->reg(0).write_fifo(const_cast<uint8_t *>(buffer), length);
}
uart::FlushResult WeikaiChannel::flush() {
uart::UARTFlushResult WeikaiChannel::flush() {
uint32_t const start_time = millis();
while (this->tx_fifo_is_not_empty_()) { // wait until buffer empty
if (millis() - start_time > 200) {
ESP_LOGW(TAG, "WARNING flush timeout - still %d bytes not sent after 200 ms", this->tx_in_fifo_());
return uart::FlushResult::TIMEOUT;
return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
}
yield(); // reschedule our thread to avoid blocking
}
return uart::FlushResult::SUCCESS;
return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
}
size_t WeikaiChannel::xfer_fifo_to_buffer_() {
+1 -1
View File
@@ -380,7 +380,7 @@ class WeikaiChannel : public uart::UARTComponent {
/// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data
/// to complete. (Prior to Arduino 1.0, this the method was removing any buffered incoming serial data.). ** Therefore
/// we wait until all bytes are gone with a timeout of 100 ms
uart::FlushResult flush() override;
uart::UARTFlushResult flush() override;
protected:
friend class WeikaiComponent;
+1 -5
View File
@@ -3059,9 +3059,7 @@ namespace esphome::api {
cpp = FILE_HEADER
cpp += """\
#include "api_pb2_service.h"
#ifdef USE_API
#include "api_connection.h"
#endif
#include "esphome/core/log.h"
namespace esphome::api {
@@ -3167,8 +3165,7 @@ static const char *const TAG = "api.service";
# can devirtualize and inline the on_* handler calls within the same class.
# APIConnection declares this method in api_connection.h.
out = "#ifdef USE_API\n"
out += "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n"
out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n"
# Auth check block before dispatch switch
out += " // Check authentication/connection requirements\n"
@@ -3213,7 +3210,6 @@ static const char *const TAG = "api.service";
out += " break;\n"
out += " }\n"
out += "}\n"
out += "#endif // USE_API\n"
cpp += out
hpp += "};\n"
+1 -1
View File
@@ -16,7 +16,7 @@ class MockUARTComponent : public uart::UARTComponent {
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
MOCK_METHOD(size_t, available, (), (override));
MOCK_METHOD(uart::FlushResult, flush, (), (override));
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override));
};
+1 -1
View File
@@ -30,7 +30,7 @@ class MockUARTComponent : public UARTComponent {
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
MOCK_METHOD(size_t, available, (), (override));
MOCK_METHOD(FlushResult, flush, (), (override));
MOCK_METHOD(UARTFlushResult, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override));
};
@@ -153,9 +153,9 @@ bool MockUartComponent::read_array(uint8_t *data, size_t len) {
size_t MockUartComponent::available() { return this->rx_buffer_.size(); }
uart::FlushResult MockUartComponent::flush() {
uart::UARTFlushResult MockUartComponent::flush() {
// Nothing to flush in mock
return uart::FlushResult::ASSUMED_SUCCESS;
return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
void MockUartComponent::set_rx_full_threshold(size_t rx_full_threshold) {
@@ -28,7 +28,7 @@ class MockUartComponent : public uart::UARTComponent, public Component {
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
uart::FlushResult flush() override;
uart::UARTFlushResult flush() override;
void set_rx_full_threshold(size_t rx_full_threshold) override;
void set_rx_timeout(size_t rx_timeout) override;
+14 -19
View File
@@ -79,9 +79,15 @@ async def test_uart_mock_ld2412(
],
)
# Signal when we see recovery frame values
# Signal when we see all recovery frame values
recovery_received = collector.add_waiter(
lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"]
lambda: (
pytest.approx(50.0) in collector.sensor_states["moving_distance"]
and pytest.approx(75.0) in collector.sensor_states["still_distance"]
and pytest.approx(100.0) in collector.sensor_states["moving_energy"]
and pytest.approx(80.0) in collector.sensor_states["still_energy"]
and pytest.approx(50.0) in collector.sensor_states["detection_distance"]
)
)
async with (
@@ -150,23 +156,12 @@ async def test_uart_mock_ld2412(
)
# Recovery frame: moving=50, still=75, energy=100/80, detect=50
recovery_idx = next(
i
for i, v in enumerate(collector.sensor_states["moving_distance"])
if v == pytest.approx(50.0)
)
assert collector.sensor_states["still_distance"][recovery_idx] == pytest.approx(
75.0
)
assert collector.sensor_states["moving_energy"][recovery_idx] == pytest.approx(
100.0
)
assert collector.sensor_states["still_energy"][recovery_idx] == pytest.approx(
80.0
)
assert collector.sensor_states["detection_distance"][
recovery_idx
] == pytest.approx(50.0)
# Check values exist (waiter already ensured all are present)
assert pytest.approx(50.0) in collector.sensor_states["moving_distance"]
assert pytest.approx(75.0) in collector.sensor_states["still_distance"]
assert pytest.approx(100.0) in collector.sensor_states["moving_energy"]
assert pytest.approx(80.0) in collector.sensor_states["still_energy"]
assert pytest.approx(50.0) in collector.sensor_states["detection_distance"]
# Verify binary sensors detected targets (from Phase 1 frame)
assert collector.binary_states["has_target"][0] is True