Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
171e560
Enhance KissModemWrapper command handling and response management
agessaman Apr 26, 2026
4b5981f
Implement UART write serialization in KissModemWrapper
agessaman May 10, 2026
81800cd
Update versioning to indicate development stage
agessaman May 10, 2026
1e1ba9b
fix(kiss modem): improved connection handling and recovery
agessaman May 16, 2026
f270faf
fix(kiss modem): refine connection state management and improve threa…
agessaman May 16, 2026
95f85d8
Update versioning to reflect development stage
agessaman May 16, 2026
a2d70d4
feat(kiss modem): enhance post-connect configuration and retry logic
agessaman May 16, 2026
c7fc741
refactor: improve error handling in CompanionFrameServer and KissMode…
agessaman May 17, 2026
f5fc607
feat: enhance KissModemWrapper with improved connection resilience an…
agessaman May 30, 2026
cab743e
Merge branch 'fix/kisswrapper-singleflight-sethardware' into fix/esp3…
agessaman May 30, 2026
7c6a564
companion: fix region discovery and sync protocol to firmware v12
agessaman May 30, 2026
5a9e475
companion: implement send_raw_packet method for direct packet transmi…
agessaman May 30, 2026
327dc5e
feat: add AnonRequestHandler and AnonRateLimiter to message handlers
agessaman May 31, 2026
47bc974
feat: add TX_BUSY error handling and single-flight transmit logic to …
agessaman May 31, 2026
b2bd4d5
Merge branch 'fix/esp32-kiss-fixes' into int/espfix-1160
agessaman May 31, 2026
9a30884
fix(companion): slow and unreliable companion login/stats to firmware…
agessaman Jun 1, 2026
480da5f
feat(companion): add _fmt_path function for improved path logging
agessaman Jun 1, 2026
cbe968d
fix(companion): Retry requests on an adaptive, firmware-matched timeout
agessaman Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion src/pymc_core/companion/binary_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import logging
from typing import Optional

from .constants import BinaryReqType
from .constants import (
ANON_REQ_TYPE_BASIC,
ANON_REQ_TYPE_OWNER,
ANON_REQ_TYPE_REGIONS,
BinaryReqType,
)

logger = logging.getLogger(__name__)

Expand All @@ -17,6 +22,19 @@ def parse_binary_response(
context: Optional[dict] = None,
) -> Optional[dict]:
"""Parse response_data by request_type. Returns dict or None."""
context = context or {}
# Anonymous requests (CMD_SEND_ANON_REQ) all carry request_type 0x07, which
# collides with BinaryReqType.OWNER_INFO. Disambiguate by the recorded
# ANON_REQ_TYPE_* sub-type so a regions reply is not parsed as owner info.
anon_sub_type = context.get("anon_sub_type")
if anon_sub_type is not None:
if anon_sub_type == ANON_REQ_TYPE_REGIONS:
return _parse_regions(data)
if anon_sub_type == ANON_REQ_TYPE_OWNER:
return _parse_anon_owner(data)
if anon_sub_type == ANON_REQ_TYPE_BASIC:
return _parse_anon_basic(data)
return {"raw_hex": data.hex(), "anon_sub_type": anon_sub_type}
if request_type == BinaryReqType.STATUS and len(data) >= 52:
return _parse_status(data, pubkey_prefix=pubkey_prefix or None)
if request_type == BinaryReqType.TELEMETRY and len(data) >= 0:
Expand Down Expand Up @@ -111,6 +129,55 @@ def _parse_owner_info(data: bytes) -> dict:
return {"raw_hex": data.hex(), "request_type": BinaryReqType.OWNER_INFO}


def _parse_regions(data: bytes) -> dict:
"""Parse ANON_REQ_TYPE_REGIONS response: clock(4) + region-name list.
The responder replies with tag(4) + clock(4) + names; the tag is stripped by
the caller, so ``data`` is clock(4) + names. Names are a null-terminated,
comma-separated string ('*' denotes the wildcard region; '#' prefixes are
already stripped by the firmware's exportNamesTo).
"""
try:
clock = int.from_bytes(data[:4], "little") if len(data) >= 4 else 0
raw = data[4:].split(b"\x00", 1)[0]
text = raw.decode("utf-8", errors="replace")
regions = [r for r in text.split(",") if r != ""]
return {"type": "regions", "clock": clock, "regions": regions}
except Exception:
logger.debug("Regions parse failed, returning fallback", exc_info=True)
return {"raw_hex": data.hex(), "anon_sub_type": ANON_REQ_TYPE_REGIONS}


def _parse_anon_owner(data: bytes) -> dict:
"""Parse ANON_REQ_TYPE_OWNER response: clock(4) + 'name\\nowner'."""
try:
clock = int.from_bytes(data[:4], "little") if len(data) >= 4 else 0
text = data[4:].split(b"\x00", 1)[0].decode("utf-8", errors="replace")
parts = text.split("\n", 1)
return {
"type": "owner",
"clock": clock,
"node_name": parts[0] if len(parts) > 0 else "",
"owner_info": parts[1] if len(parts) > 1 else "",
}
except Exception:
logger.debug("Anon owner parse failed, returning fallback", exc_info=True)
return {"raw_hex": data.hex(), "anon_sub_type": ANON_REQ_TYPE_OWNER}


def _parse_anon_basic(data: bytes) -> dict:
"""Parse ANON_REQ_TYPE_BASIC response: clock(4) + feature flags(1)."""
clock = int.from_bytes(data[:4], "little") if len(data) >= 4 else 0
features = data[4] if len(data) >= 5 else 0
return {
"type": "basic",
"clock": clock,
"features": features,
"is_bridge": bool(features & 0x01),
"is_disabled": bool(features & 0x80),
}


def _parse_acl(buf: bytes) -> dict:
"""ACL: 7-byte entries (key 6 + perm 1)."""
res = []
Expand Down
Loading