EnvelopeCodec message layer; scripts are thin business-API wrappers
This commit is contained in:
1 parent
f15ae42db7
commit
363c7cc6a2
14 files changed
+551
-900
No files matched your search
@@ -19,7 +19,6 @@ the direct-connect path of the app (see ``docs/RE_DART_FINDINGS.md`` §8,
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any, Dict, Optional
|
||||
@@ -32,12 +31,11 @@ from .protocol import (
|
||||
CommunicationOperate,
|
||||
SequenceNumber,
|
||||
SocketFrameDecoder,
|
||||
decrypt_wire,
|
||||
direct_connect_request,
|
||||
encrypt_object,
|
||||
frame_socket_payload,
|
||||
generate_secret_key,
|
||||
)
|
||||
from .protocol.messages import EnvelopeCodec
|
||||
from .wifi import DEVICE_GATEWAY_IP
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -96,6 +94,7 @@ class LocalDeviceClient:
|
||||
self.key_radix = int(key_radix)
|
||||
self.timeout = timeout
|
||||
self.key = generate_secret_key(sn)
|
||||
self.codec = EnvelopeCodec(self.key, radix=self.key_radix)
|
||||
self._sn_seq = SequenceNumber()
|
||||
self._session = session or requests.Session()
|
||||
self._session.headers.update(
|
||||
@@ -196,57 +195,60 @@ class LocalDeviceClient:
|
||||
return self.send_envelope(envelope)
|
||||
|
||||
def send_envelope(self, envelope: CommunicationObject) -> Dict[str, Any]:
|
||||
wire = encrypt_object(envelope.to_json_string(), self.key,
|
||||
radix=self.key_radix)
|
||||
raw = self._socket_roundtrip(frame_socket_payload(wire))
|
||||
raw = self._socket_roundtrip(
|
||||
frame_socket_payload(self.codec.encode(envelope)))
|
||||
try:
|
||||
reply = decrypt_wire(raw, radix=self.key_radix)
|
||||
reply = self.codec.decode(raw).to_dict()
|
||||
except ValueError:
|
||||
raise ConnectionError(
|
||||
f"undecodable reply ({len(raw)} bytes) from "
|
||||
f"{self.socket_host}:{self.socket_port}"
|
||||
)
|
||||
return json.loads(reply)
|
||||
return reply
|
||||
|
||||
def send_probe(
|
||||
self,
|
||||
cmd: CommunicationCmd,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
timeout: Optional[float] = None,
|
||||
equip_no: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Send one envelope, return exact TX/RX bytes plus any parsed reply.
|
||||
|
||||
Never raises on a device-level failure: ``reply`` is the parsed JSON
|
||||
(or ``None``), ``reply_raw`` the exact bytes received (``b""`` when
|
||||
the device accepted the frame but answered with zero bytes).
|
||||
Never raises on a device-level failure: ``reply`` is the parsed
|
||||
envelope dict (or ``None``), ``reply_raw`` the exact bytes received
|
||||
(``b""`` when the device accepted the frame but answered with zero
|
||||
bytes). ``equip_no`` overrides the serial (``""`` reproduces the
|
||||
app's empty-SN discovery frame).
|
||||
"""
|
||||
envelope = direct_connect_request(
|
||||
cmd, self.sn, data=data, snno=self._sn_seq.next(),
|
||||
cmd, self.sn if equip_no is None else equip_no, data=data,
|
||||
snno=self._sn_seq.next(),
|
||||
)
|
||||
wire = encrypt_object(envelope.to_json_string(), self.key,
|
||||
radix=self.key_radix)
|
||||
wire = self.codec.encode(envelope)
|
||||
reply_raw = self._socket_roundtrip(
|
||||
frame_socket_payload(wire), timeout=timeout, collect_only=True,
|
||||
)
|
||||
reply: Optional[Dict[str, Any]] = None
|
||||
if reply_raw:
|
||||
try:
|
||||
decoded = decrypt_wire(reply_raw, radix=self.key_radix)
|
||||
except ValueError:
|
||||
decoded = ""
|
||||
if decoded:
|
||||
try:
|
||||
reply = json.loads(decoded)
|
||||
except ValueError:
|
||||
reply = None
|
||||
return {
|
||||
"cmd": str(cmd),
|
||||
"envelope": envelope.to_dict(),
|
||||
"tx": wire,
|
||||
"reply_raw": reply_raw,
|
||||
"reply": reply,
|
||||
"reply": self.codec.try_decode(reply_raw),
|
||||
}
|
||||
|
||||
def send_raw(self, payload: bytes,
|
||||
timeout: Optional[float] = None) -> bytes:
|
||||
"""Low-level escape hatch: one connection, send *payload*, drain.
|
||||
|
||||
Used by transport-level probes (framing/handshake variants) that
|
||||
are not valid envelopes. Returns the exact bytes received (``b""``
|
||||
when the peer answers with nothing). Envelope traffic must use
|
||||
:meth:`send_envelope` / :meth:`send_probe` instead.
|
||||
"""
|
||||
return self._socket_roundtrip(bytes(payload), timeout=timeout,
|
||||
collect_only=True)
|
||||
|
||||
def _socket_roundtrip(self, framed: bytes,
|
||||
timeout: Optional[float] = None,
|
||||
collect_only: bool = False) -> bytes:
|
||||
@@ -278,7 +280,7 @@ class LocalDeviceClient:
|
||||
if collect_only:
|
||||
return bytes(received)
|
||||
try:
|
||||
decrypt_wire(bytes(received), radix=self.key_radix)
|
||||
self.codec.decode(bytes(received))
|
||||
except ValueError:
|
||||
raise ConnectionError(
|
||||
f"no reply frame from {self.socket_host}:{self.socket_port}"
|
||||
|
||||
Reference in new issue
Block a user