"""Wire envelope — ``CommunicationObject``. JSON key order (exact, from ``toJson`` in ``core/communication_object.dart``): ``cmdType, equipNo, type, timeStamp, snno, len, crc, dataArea, data``. See ``docs/RE_DART_FINDINGS.md`` §3. Confirmed: * ``cmdType`` = :class:`CommunicationCmd` raw value * ``equipNo`` = device serial number * ``type`` = :class:`CommunicationOperate` raw value * compact JSON (no whitespace) — matches Dart ``json.encode`` Candidates (not yet confirmed byte-for-byte, see Open items): * defaults of ``crc`` / ``dataArea`` — the pool contains the string constant ``"00000000"`` used as a placeholder default; * ``len`` semantics (byte length of the payload). """ from __future__ import annotations import json import time from typing import Any, Dict, Optional from .commands import CommunicationCmd, CommunicationOperate #: Exact key order used by the app's ``toJson``. KEY_ORDER = ( "cmdType", "equipNo", "type", "timeStamp", "snno", "len", "crc", "dataArea", "data", ) #: Placeholder default observed in the object pool. PLACEHOLDER = "00000000" class CommunicationObject: """One request/response envelope travelling over any transport.""" def __init__( self, cmd_type: int, equip_no: str, op_type: int = CommunicationOperate.QUERY, snno: Optional[str] = None, time_stamp: Optional[int] = None, length: Optional[int] = None, crc: str = PLACEHOLDER, data_area: str = PLACEHOLDER, data: str = "", ) -> None: self.cmd_type = int(cmd_type) self.equip_no = str(equip_no) self.op_type = int(op_type) self.snno = snno if snno is not None else str(equip_no) self.time_stamp = int(time_stamp if time_stamp is not None else int(time.time() * 1000)) self.length = int(length) if length is not None else 0 self.crc = str(crc) self.data_area = str(data_area) self.data = str(data) # ------------------------------------------------------------------ json def to_dict(self) -> Dict[str, Any]: """Fields in the app's exact key order.""" return { "cmdType": self.cmd_type, "equipNo": self.equip_no, "type": self.op_type, "timeStamp": self.time_stamp, "snno": self.snno, "len": self.length, "crc": self.crc, "dataArea": self.data_area, "data": self.data, } def to_json_string(self) -> str: """Compact JSON string — the input of ``encrypt_object``.""" return json.dumps(self.to_dict(), separators=(",", ":")) @classmethod def from_dict(cls, raw: Dict[str, Any]) -> "CommunicationObject": """Tolerant parse (missing keys fall back to placeholders).""" return cls( cmd_type=raw.get("cmdType", 0), equip_no=raw.get("equipNo", ""), op_type=raw.get("type", 0), snno=raw.get("snno") if raw.get("snno") is not None else None, time_stamp=raw.get("timeStamp"), length=raw.get("len"), crc=raw.get("crc", PLACEHOLDER), data_area=raw.get("dataArea", PLACEHOLDER), data=raw.get("data", ""), ) @classmethod def from_json_string(cls, s: str) -> "CommunicationObject": return cls.from_dict(json.loads(s)) def __repr__(self) -> str: # pragma: no cover - debugging aid return ( f"CommunicationObject(cmdType={self.cmd_type:#04x}, " f"equipNo={self.equip_no!r}, type={self.op_type})" ) def direct_connect_request( cmd: CommunicationCmd, equip_no: str, data: str = "", ) -> CommunicationObject: """Build a *direct-connect* query envelope. Mirrors the Dart ``_query*ByDirectConnect`` family (``docs/RE_DART_FINDINGS.md`` — relevant files / RPC layer). """ return CommunicationObject( cmd_type=int(cmd), equip_no=equip_no, op_type=CommunicationOperate.QUERY, data=data, )