arsdk: read the codec's (ids, values) decode shape
protocol/codec.py landed with decode_event returning ids and a values dict, so the session looks the command name up in the index rather than expecting it in the result. Verified end to end against that stream's codec in a stitched tree: the sim encodes events through it, the session decodes through it, and the test that skips until it exists now passes. The argument name cannot be recovered by splitting a telemetry key, because an argument can contain an underscore, so the expectation predicate matches on the suffix instead.
This commit is contained in:
@@ -24,6 +24,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from functools import cache
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from mcbebop.arsdk.connection import Connection
|
from mcbebop.arsdk.connection import Connection
|
||||||
@@ -101,14 +102,31 @@ def _default_decoder(payload: bytes) -> Any:
|
|||||||
return decode_event(payload)
|
return decode_event(payload)
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _name_for(ids: tuple[int, int, int]) -> str:
|
||||||
|
"""The command's own name, for an event that arrived without one.
|
||||||
|
|
||||||
|
`protocol.codec.decode_event` returns ids and values; the name lives in
|
||||||
|
the index. It cannot be recovered from the keys, because an argument may
|
||||||
|
itself contain an underscore (`MassStorageInfoStateListChanged_mass_storage_id`).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from mcbebop.protocol import xml_index # deferred, and optional
|
||||||
|
|
||||||
|
spec = xml_index.index().by_ids(ids)
|
||||||
|
except Exception: # pragma: no cover - only without the protocol layer
|
||||||
|
spec = None
|
||||||
|
return spec.name if spec is not None else f"cmd_{ids[0]}_{ids[1]}_{ids[2]}"
|
||||||
|
|
||||||
|
|
||||||
def _as_event(decoded: Any, payload: bytes) -> Event | None:
|
def _as_event(decoded: Any, payload: bytes) -> Event | None:
|
||||||
"""Normalise whatever the codec returned into an `Event`.
|
"""Normalise whatever the codec returned into an `Event`.
|
||||||
|
|
||||||
The codec is written by another stream against the same `Event` type, so
|
`decode_event` returns `(ids, values)`, which is the second branch. The
|
||||||
the first branch is the expected one. The rest exist because a decoder
|
others are here because an `Event`, a 3-tuple and a mapping are all
|
||||||
that returns a plain tuple or mapping is a reasonable thing to write, and
|
reasonable things for a decoder or a test fake to hand back, and a
|
||||||
a transport that crashes on the shape of a decode result would be a silly
|
transport that died on the shape of a decode result would be a silly way
|
||||||
way to lose a link.
|
to lose a link.
|
||||||
"""
|
"""
|
||||||
if decoded is None:
|
if decoded is None:
|
||||||
return None
|
return None
|
||||||
@@ -119,16 +137,18 @@ def _as_event(decoded: Any, payload: bytes) -> Event | None:
|
|||||||
values = getattr(decoded, "values", None)
|
values = getattr(decoded, "values", None)
|
||||||
if ids is None and isinstance(decoded, dict):
|
if ids is None and isinstance(decoded, dict):
|
||||||
ids, name, values = decoded.get("ids"), decoded.get("name"), decoded.get("values")
|
ids, name, values = decoded.get("ids"), decoded.get("name"), decoded.get("values")
|
||||||
if ids is None and isinstance(decoded, tuple | list) and len(decoded) == 3:
|
if ids is None and isinstance(decoded, tuple | list):
|
||||||
ids, name, values = decoded
|
if len(decoded) == 2:
|
||||||
|
ids, values = decoded
|
||||||
|
elif len(decoded) == 3:
|
||||||
|
ids, name, values = decoded
|
||||||
if ids is None:
|
if ids is None:
|
||||||
log.debug("decoder returned an unusable shape %r for %s", type(decoded), payload[:8].hex())
|
log.debug("decoder returned an unusable shape %r for %s", type(decoded), payload[:8].hex())
|
||||||
return None
|
return None
|
||||||
if name is None:
|
ids = tuple(ids)
|
||||||
name = f"cmd_{tuple(ids)}"
|
|
||||||
return Event(
|
return Event(
|
||||||
ids=tuple(ids), # type: ignore[arg-type]
|
ids=ids, # type: ignore[arg-type]
|
||||||
name=str(name),
|
name=str(name) if name else _name_for(ids), # type: ignore[arg-type]
|
||||||
values=dict(values or {}),
|
values=dict(values or {}),
|
||||||
at=getattr(decoded, "at", None) or time.monotonic(),
|
at=getattr(decoded, "at", None) or time.monotonic(),
|
||||||
)
|
)
|
||||||
@@ -409,7 +429,13 @@ class DroneSession:
|
|||||||
|
|
||||||
def predicate(event: Event) -> bool:
|
def predicate(event: Event) -> bool:
|
||||||
for name, expected in checks.get(event.ids, {}).items():
|
for name, expected in checks.get(event.ids, {}).items():
|
||||||
got = event.values.get(f"{event.name}_{name}", event.values.get(name))
|
got = event.values.get(f"{event.name}_{name}")
|
||||||
|
if got is None:
|
||||||
|
# Telemetry keys are `<Command>_<arg>`, but an argument
|
||||||
|
# can contain an underscore, so match on the suffix
|
||||||
|
# rather than trusting a split.
|
||||||
|
matches = [v for k, v in event.values.items() if k.endswith(f"_{name}") or k == name]
|
||||||
|
got = matches[0] if len(matches) == 1 else None
|
||||||
if got is not None and got != expected:
|
if got is not None and got != expected:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user