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.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user