Add recovered protocol stack, Wi-Fi hotspot join, and RE findings
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.
This commit is contained in:
1 parent
c2d261fb34
commit
a87dbe200e
10 files changed
+1436
-19
No files matched your search
@@ -0,0 +1,131 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in new issue
Block a user