Tested on the aircraft. A second ARSDK handshake is accepted and telemetry is redirected to it; the first session's frames stop while it still reports connected = True. So the claim inherited from pyparrot's error text, which had reached our error messages, tool descriptions, simulator behaviour and a test name, was wrong in the most misleading direction: a refusal would be loud, and this is silent. The simulator now models the takeover by default; refusal stays available because a client must handle a non-zero status anyway.
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
"""Regressions for three bugs that only a real aircraft exposed.
|
|
|
|
Each of these passed every simulator test while being wrong against the drone,
|
|
which is why they have their own file: the simulator was written from the same
|
|
assumptions as the code, so it agreed with it.
|
|
"""
|
|
|
|
from mcbebop.arsdk.types import BufferId, DataType, Frame
|
|
from mcbebop.protocol import codec, xml_index
|
|
from mcbebop.protocol.types import Direction
|
|
|
|
|
|
def test_argument_less_events_are_recorded():
|
|
"""Eight events carry no arguments; their arrival IS the message.
|
|
|
|
AllStatesChanged and AllSettingsChanged are how the drone says a state dump
|
|
is finished. Keying only by argument meant they decoded to an empty dict and
|
|
disappeared, so nothing could wait for the end of a burst.
|
|
"""
|
|
for name in ("common.CommonState.AllStatesChanged", "common.SettingsState.AllSettingsChanged"):
|
|
spec = xml_index.get(name)
|
|
assert spec.args == (), "this test is about events that have no arguments"
|
|
_, values = codec.decode_event(codec.encode_command(spec, {}))
|
|
assert values == {spec.name: True}
|
|
|
|
bare = [c for c in xml_index.all_commands() if c.direction is Direction.FROM_DRONE and not c.args]
|
|
assert len(bare) == 8
|
|
|
|
|
|
def test_every_bare_event_decodes_to_something():
|
|
for spec in xml_index.all_commands():
|
|
if spec.direction is Direction.FROM_DRONE and not spec.args:
|
|
_, values = codec.decode_event(codec.encode_command(spec, {}))
|
|
assert values, f"{spec.full_name} decoded to nothing"
|
|
|
|
|
|
def test_acknowledgement_is_decided_by_data_type_not_buffer():
|
|
"""A live Bebop 2 sends DATA_WITH_ACK on buffer 126, not 127.
|
|
|
|
The naming says 127 is the drone's acknowledged buffer, and requiring both
|
|
the type and the buffer to agree meant no frame was ever acknowledged. The
|
|
drone then resent its state rather than continuing, and most of it never
|
|
arrived: 35 keys instead of 192.
|
|
"""
|
|
import inspect
|
|
|
|
from mcbebop.arsdk import connection as conn_mod
|
|
|
|
source = inspect.getsource(conn_mod.Connection._handle)
|
|
ack_line = next(line for line in source.splitlines() if "DataType.DATA_WITH_ACK" in line)
|
|
assert "buffer_id" not in ack_line, "the ack decision must not depend on which buffer it arrived on"
|
|
|
|
# Both buffers must round-trip to a valid ack target.
|
|
for buffer_id in (BufferId.D2C_NON_ACK, BufferId.D2C_ACK):
|
|
frame = Frame(DataType.DATA_WITH_ACK, buffer_id, 9, b"\x00\x05\x00\x00")
|
|
assert Frame.decode_all(frame.encode())[0].buffer_id == buffer_id
|
|
assert 0 <= BufferId.ack_for(buffer_id) <= 255
|
|
|
|
|
|
def test_simulator_models_takeover_not_refusal_by_default():
|
|
"""The aircraft accepts a second controller; it does not refuse one.
|
|
|
|
Tested on the real drone 2026-10-02: session A's frames froze at 264 while
|
|
session B took over, and A went on reporting connected = True. The
|
|
simulator defaulted to refusing, which is the same unverified assumption
|
|
the client carried, so no test could catch the difference.
|
|
"""
|
|
import dataclasses
|
|
|
|
from mcbebop.sim import FakeBebop
|
|
|
|
field = next(f for f in dataclasses.fields(FakeBebop) if f.name == "single_controller")
|
|
assert field.default is False, "the default must model the aircraft, not the folklore"
|