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
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cache
|
||||
from typing import Any
|
||||
|
||||
from mcbebop.arsdk.connection import Connection
|
||||
@@ -101,14 +102,31 @@ def _default_decoder(payload: bytes) -> Any:
|
||||
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:
|
||||
"""Normalise whatever the codec returned into an `Event`.
|
||||
|
||||
The codec is written by another stream against the same `Event` type, so
|
||||
the first branch is the expected one. The rest exist because a decoder
|
||||
that returns a plain tuple or mapping is a reasonable thing to write, and
|
||||
a transport that crashes on the shape of a decode result would be a silly
|
||||
way to lose a link.
|
||||
`decode_event` returns `(ids, values)`, which is the second branch. The
|
||||
others are here because an `Event`, a 3-tuple and a mapping are all
|
||||
reasonable things for a decoder or a test fake to hand back, and a
|
||||
transport that died on the shape of a decode result would be a silly way
|
||||
to lose a link.
|
||||
"""
|
||||
if decoded is None:
|
||||
return None
|
||||
@@ -119,16 +137,18 @@ def _as_event(decoded: Any, payload: bytes) -> Event | None:
|
||||
values = getattr(decoded, "values", None)
|
||||
if ids is None and isinstance(decoded, dict):
|
||||
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):
|
||||
if len(decoded) == 2:
|
||||
ids, values = decoded
|
||||
elif len(decoded) == 3:
|
||||
ids, name, values = decoded
|
||||
if ids is None:
|
||||
log.debug("decoder returned an unusable shape %r for %s", type(decoded), payload[:8].hex())
|
||||
return None
|
||||
if name is None:
|
||||
name = f"cmd_{tuple(ids)}"
|
||||
ids = tuple(ids)
|
||||
return Event(
|
||||
ids=tuple(ids), # type: ignore[arg-type]
|
||||
name=str(name),
|
||||
ids=ids, # type: ignore[arg-type]
|
||||
name=str(name) if name else _name_for(ids), # type: ignore[arg-type]
|
||||
values=dict(values or {}),
|
||||
at=getattr(decoded, "at", None) or time.monotonic(),
|
||||
)
|
||||
@@ -409,7 +429,13 @@ class DroneSession:
|
||||
|
||||
def predicate(event: Event) -> bool:
|
||||
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:
|
||||
return False
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user