[bl0906] Read stale receive bytes instead of spinning on flush() (#21035)

This commit is contained in:
J. Nick Koston
2026-10-19 11:29:02 +13:00
committed by Jesse Hills
parent 26064f1415
commit f979ddcf0d
3 changed files with 123 additions and 2 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
void BL0906::loop() {
while (this->available())
this->flush();
this->read();
if (this->current_stage_ == STAGE_IDLE) {
// Woken up between cycles to drain the action queue. Go back to sleep.
@@ -92,7 +92,7 @@ void BL0906::advance_stage_() {
void BL0906::setup() {
while (this->available())
this->flush();
this->read();
this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE));
// Calibration (1: register address; 2: value before calibration; 3: value after calibration)
this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1
@@ -0,0 +1,64 @@
esphome:
name: uart-mock-bl0906-stray-rx
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: VERBOSE
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
# Dummy uart entry to satisfy DEPENDENCIES = ["uart"]
uart:
baud_rate: 9600
port: /dev/null
# Answers register reads; the first temperature answer has one stray trailing byte
uart_mock:
id: mock_uart
baud_rate: 19200
on_tx:
- then:
- lambda: |-
// Command and register address arrive as separate one-byte writes
static bool read_pending = false;
static bool stray_sent = false;
if (data.size() != 1)
return;
if (!read_pending) {
read_pending = data[0] == 0x35;
return;
}
read_pending = false;
const uint8_t address = data[0];
uint32_t raw;
switch (address) {
case 0x5E: // temperature: (300 - 64) * 12.5 / 59 - 40 = 10 C
raw = 300;
break;
case 0x16: // voltage
raw = 0x2A1B3C;
break;
default:
return;
}
const uint8_t l = raw & 0xFF, m = (raw >> 8) & 0xFF, h = (raw >> 16) & 0xFF;
std::vector<uint8_t> reply{l, m, h, static_cast<uint8_t>((address + l + m + h) ^ 0xFF)};
if (address == 0x5E && !stray_sent) {
stray_sent = true;
reply.push_back(0xAA);
}
id(mock_uart).inject_to_rx_buffer(reply);
sensor:
- platform: bl0906
uart_id: mock_uart
update_interval: 1s
temperature:
name: Temperature
voltage:
name: Voltage
@@ -0,0 +1,57 @@
"""A BL0906 that leaves a stray byte in the receive buffer keeps reading."""
from __future__ import annotations
import asyncio
from pathlib import Path
from aioesphomeapi import EntityState, SensorInfo
import pytest
from .state_utils import require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_uart_mock_bl0906_stray_rx(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A stray received byte does not stop the read cycle."""
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
lines: list[str] = []
states: dict[int, EntityState] = {}
async with (
run_compiled(yaml_config, line_callback=lines.append),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
temperature = require_entity(entities, "temperature", SensorInfo)
voltage = require_entity(entities, "voltage", SensorInfo)
client.subscribe_states(lambda state: states.__setitem__(state.key, state))
def state_of(entity: SensorInfo) -> float | None:
state = states.get(entity.key)
return None if state is None or state.missing_state else state.state
try:
async with asyncio.timeout(10.0):
while state_of(voltage) is None:
await asyncio.sleep(0.05)
except TimeoutError:
pytest.fail(f"No voltage reading; log: {lines[-20:]}")
assert any("RX inject 5 bytes" in x for x in lines), "stray byte not sent"
assert state_of(temperature) == pytest.approx(10.0, abs=0.01)
uref = 1.097 * 100000 / (13162 * 100 * 1000)
assert state_of(voltage) == pytest.approx(0x2A1B3C * uref, rel=1e-5)
assert not any("Junk on wire" in x for x in lines)