Reconstruct the app's Dart-layer behaviour (blutter over libapp.so, jadx over base.apk — see docs/RE_DART_FINDINGS.md) as working Python, replacing the library stubs with a real local-connect stack. protocol/ (new package, hardware-independent): - commands: CommunicationMode / CommunicationOperate / CommunicationCmd enums with the raw values recovered from the Dart object pool - envelope: CommunicationObject with the app's exact JSON key order - crypto: SN-derived secret key and the position-dependent additive cipher (encrypt/decrypt; radix 16 default, 32 documented alternate) - crc: CRC-16/MODBUS with the app's hi/lo swap + 4-hex string form, CRC32 envelope check, little-endian int16 helpers - framing: BLE frame build/parse (0x5AA5 data / 0x4AA4 ACK magics, CRC slots, MTU chunking) and local-socket int16LE length framing plus a streaming SocketFrameDecoder wifi.py (rewritten): - derive_hotspot_credentials(): the AP_<sn> SSID/password rules (20-char SN branch, full-SN fallback with the app's warning log) - WifiBackend abstraction: NmcliWifiBackend for Linux hosts via nmcli, NoOpWifiBackend for radio-less containers - WifiManager keeps the original stub method names for API compat and adds join_device_hotspot(sn) / leave_device_hotspot() / local_ip() device_local.py (new): LocalDeviceClient for the mode==local path — the /terminal/* HTTP endpoint family (hotspot info, aGate info, overall info, MQTT relay, ...) and direct-connect socket RPC (envelope -> cipher -> length frame -> decrypt -> JSON) with convenience query_* RPCs. docs/RE_DART_FINDINGS.md (new): the full findings — transport selector, wire envelope, crypto, CRCs, MQTT (AWS IoT + Cognito), socket, BLE + ESP provisioning, hotspot credential derivation, local/cloud HTTP layers, and the Java glue — plus the open items; README now links to it. Unconfirmed candidates (local port 18000, cipher key radix, BLE field order, X-Device-* header values) are flagged in the docs and kept configurable in code.
132 lines
4.1 KiB
Python
132 lines
4.1 KiB
Python
"""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,
|
|
)
|