The drone sends BatteryStateChanged from two places with different values: the true figure on buffer 126, corroborated by its own log and tracking a smooth discharge, and a constant 0 on buffer 127 that appears in no log line. Keeping the newest frame meant a healthy aircraft read as flat and preflight refused. Events now carry their buffer and the store keeps the last value per source, so a disagreement is visible instead of silently resolved. get_state reports the sources whenever they differ, rather than handing over a winner. preflight breaks the tie on physical grounds, not preference: an aircraft that is powered and holding a link is not at 0%, so a zero from a live drone is not credible while a non-zero one is. It says the number was contested either way, and a genuine zero from every source still reports zero and still blocks.
61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
"""The aircraft reports its battery twice, with different values.
|
|
|
|
Observed on the live drone 2026-10-02: `BatteryStateChanged` (0-5-1) arrives as
|
|
`00 05 01 00 40` on buffer 126 and `00 05 01 00 00` on buffer 127, repeatedly.
|
|
The 64 is corroborated by the drone's own log and tracks a smooth discharge; the
|
|
zeros appear in no log line. A store that keeps the newest frame therefore
|
|
reported a healthy aircraft as flat, and preflight refused on it.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from mcbebop.arsdk.session import Reading
|
|
from mcbebop.arsdk.types import Event
|
|
from mcbebop.tools.state import _battery
|
|
|
|
KEY = "BatteryStateChanged_percent"
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self, entry):
|
|
self.entry = entry
|
|
|
|
def state(self, keys):
|
|
return {KEY: self.entry} if self.entry is not None else {}
|
|
|
|
|
|
def test_reading_detects_disagreement():
|
|
assert Reading(0, 1.0, 4, {126: 64, 127: 0}).conflicting
|
|
assert not Reading(64, 1.0, 1, {126: 64}).conflicting
|
|
assert not Reading(64, 1.0, 2, {126: 64, 127: 64}).conflicting
|
|
|
|
|
|
def test_event_carries_its_buffer():
|
|
"""Without the source, the store cannot tell two reporters apart."""
|
|
assert Event(ids=(0, 5, 1), name="x", values={}, at=0.0).buffer_id == 0
|
|
assert Event(ids=(0, 5, 1), name="x", values={}, at=0.0, buffer_id=127).buffer_id == 127
|
|
|
|
|
|
def test_the_real_conflict_resolves_to_the_credible_value():
|
|
pct, detail = _battery(FakeSession({"value": 0, "age": 0.1, "sources": {126: 64, 127: 0}}))
|
|
assert pct == 64, "the newest frame said 0; the credible one is 64"
|
|
assert "contested" in detail and "0" in detail, "the caller must learn the number was disputed"
|
|
|
|
|
|
def test_a_lone_reading_is_not_described_as_contested():
|
|
pct, detail = _battery(FakeSession({"value": 64, "age": 0.1, "sources": {126: 64}}))
|
|
assert pct == 64 and detail == "64%"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("entry", "expected"),
|
|
[
|
|
({"value": 58, "age": 0.1}, 58),
|
|
({"value": 0, "age": 0.1, "sources": {126: 0, 127: 0}}, 0),
|
|
(None, None),
|
|
],
|
|
)
|
|
def test_other_shapes(entry, expected):
|
|
"""A genuine zero from every source still reports zero, and still blocks."""
|
|
assert _battery(FakeSession(entry))[0] == expected
|