Files
mcbebop/tests/test_battery_conflict.py
Ryan Malloy 907f876773 Track which part of the aircraft reported a value, and resolve the battery
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.
2026-10-02 07:00:10 -06:00

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