sofapython 0.0.1rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sofapython/__init__.py +136 -0
- sofapython/ack.py +79 -0
- sofapython/aio.py +552 -0
- sofapython/backup_export.py +507 -0
- sofapython/blob_decoders.py +806 -0
- sofapython/cli.py +447 -0
- sofapython/commands.py +1273 -0
- sofapython/deframer.py +73 -0
- sofapython/device_create.py +1174 -0
- sofapython/devices.py +534 -0
- sofapython/discovery.py +315 -0
- sofapython/frame_handlers.py +131 -0
- sofapython/hub_listener.py +242 -0
- sofapython/hub_logging.py +152 -0
- sofapython/hub_versions.py +112 -0
- sofapython/inputs.py +501 -0
- sofapython/macros.py +669 -0
- sofapython/notify_demuxer.py +434 -0
- sofapython/opcode_handlers.py +1655 -0
- sofapython/protocol_const.py +633 -0
- sofapython/proxy_ack_waiters.py +660 -0
- sofapython/proxy_activity_ops.py +943 -0
- sofapython/proxy_backup.py +504 -0
- sofapython/proxy_backup_export.py +486 -0
- sofapython/proxy_catalog.py +915 -0
- sofapython/proxy_frame_decode.py +227 -0
- sofapython/proxy_ir_blob.py +676 -0
- sofapython/proxy_restore.py +2004 -0
- sofapython/proxy_wifi_device.py +1101 -0
- sofapython/state_helpers.py +713 -0
- sofapython/transport_bridge.py +876 -0
- sofapython/version.py +4 -0
- sofapython/wire_schema.py +164 -0
- sofapython/x1_proxy.py +1833 -0
- sofapython-0.0.1rc1.dist-info/METADATA +162 -0
- sofapython-0.0.1rc1.dist-info/RECORD +39 -0
- sofapython-0.0.1rc1.dist-info/WHEEL +4 -0
- sofapython-0.0.1rc1.dist-info/entry_points.txt +2 -0
- sofapython-0.0.1rc1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Frame ingest and decoded logging mixin for :class:`X1Proxy`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
from .frame_handlers import FrameContext, frame_handler_registry
|
|
9
|
+
from .commands import (
|
|
10
|
+
parse_button_burst_frame,
|
|
11
|
+
parse_command_burst_frame,
|
|
12
|
+
)
|
|
13
|
+
from .macros import parse_macro_burst_frame
|
|
14
|
+
from .hub_logging import LogTag
|
|
15
|
+
from .protocol_const import (
|
|
16
|
+
FAMILY_PLAY_BLOB,
|
|
17
|
+
OP_CATALOG_ROW_DEVICE,
|
|
18
|
+
OP_REQ_DEVICES,
|
|
19
|
+
OPNAMES,
|
|
20
|
+
opcode_family,
|
|
21
|
+
opcode_family_name,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _hexdump(data: bytes) -> str:
|
|
26
|
+
return data.hex(" ")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FrameDecodeMixin:
|
|
30
|
+
"""Mixin providing deframer feed hooks and structured frame logs."""
|
|
31
|
+
|
|
32
|
+
def _handle_hub_frame(self, data: bytes, cid: int) -> None:
|
|
33
|
+
if self.diag_dump:
|
|
34
|
+
self._log.debug("%s #%d H→A %s", LogTag.WIRE, cid, _hexdump(data))
|
|
35
|
+
frames = self._df_h2a.feed(data, cid)
|
|
36
|
+
if frames:
|
|
37
|
+
self._handle_hub_frames(frames)
|
|
38
|
+
if self.diag_parse:
|
|
39
|
+
self._log_frames("H→A", frames)
|
|
40
|
+
|
|
41
|
+
def _handle_app_frame(self, data: bytes, cid: int) -> None:
|
|
42
|
+
if self.diag_dump:
|
|
43
|
+
self._log.debug("%s #%d A→H %s", LogTag.WIRE, cid, _hexdump(data))
|
|
44
|
+
frames = self._df_a2h.feed(data, cid)
|
|
45
|
+
if frames:
|
|
46
|
+
self._handle_app_frames(frames)
|
|
47
|
+
if self.diag_parse:
|
|
48
|
+
self._log_frames("A→H", frames)
|
|
49
|
+
|
|
50
|
+
def _handle_app_frames(self, frames: List[Tuple[int, bytes, bytes, int, int]]) -> None:
|
|
51
|
+
for opcode, _raw, _payload, _scid, _ecid in frames:
|
|
52
|
+
if opcode == OP_REQ_DEVICES:
|
|
53
|
+
self._begin_device_request()
|
|
54
|
+
self._app_devices_deadline = self._time_monotonic() + 1.0
|
|
55
|
+
self._app_devices_retry_sent = False
|
|
56
|
+
|
|
57
|
+
def _handle_hub_frames(self, frames: List[Tuple[int, bytes, bytes, int, int]]) -> None:
|
|
58
|
+
for opcode, _raw, _payload, _scid, _ecid in frames:
|
|
59
|
+
if opcode == OP_CATALOG_ROW_DEVICE:
|
|
60
|
+
self._clear_app_device_retry()
|
|
61
|
+
|
|
62
|
+
def _clear_app_device_retry(self) -> None:
|
|
63
|
+
self._app_devices_deadline = None
|
|
64
|
+
self._app_devices_retry_sent = False
|
|
65
|
+
|
|
66
|
+
def parse_device_commands(self, payload: bytes, dev_id: int) -> Dict[int, str]:
|
|
67
|
+
"""Parse an assembled REQ_COMMANDS body using the fixed-width schema."""
|
|
68
|
+
|
|
69
|
+
return self.state.parse_device_commands(
|
|
70
|
+
payload, dev_id, hub_version=self.hub_version
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _time_monotonic(self) -> float:
|
|
74
|
+
import time
|
|
75
|
+
|
|
76
|
+
return time.monotonic()
|
|
77
|
+
|
|
78
|
+
def _log_frames(self, direction: str, frames: List[Tuple[int, bytes, bytes, int, int]]) -> None:
|
|
79
|
+
# This method has two responsibilities, only one of which is logging:
|
|
80
|
+
# 1. dispatch each frame to its registered frame_handler_registry
|
|
81
|
+
# handler — that is the path that ingests activities/devices/
|
|
82
|
+
# buttons/commands/macros into the proxy state cache;
|
|
83
|
+
# 2. emit DEBUG-level decoded summaries for the tools-card logs tab
|
|
84
|
+
# and the diagnostics download.
|
|
85
|
+
# The DEBUG-only work is skipped when nothing is listening, but the
|
|
86
|
+
# handler dispatch must run regardless of log level — gating it would
|
|
87
|
+
# leave the catalog empty whenever hex logging is off.
|
|
88
|
+
debug_enabled = self._log.isEnabledFor(logging.DEBUG)
|
|
89
|
+
for op, raw, payload, scid, ecid in frames:
|
|
90
|
+
name: str | None = None
|
|
91
|
+
|
|
92
|
+
if debug_enabled:
|
|
93
|
+
name = OPNAMES.get(op)
|
|
94
|
+
fam_name = opcode_family_name(op)
|
|
95
|
+
fam = opcode_family(op)
|
|
96
|
+
note = f"chunk={scid}→{ecid}" if scid != ecid else f"chunk={ecid}"
|
|
97
|
+
parsed = parse_command_burst_frame(
|
|
98
|
+
op,
|
|
99
|
+
raw,
|
|
100
|
+
hub_version=self.hub_version,
|
|
101
|
+
)
|
|
102
|
+
parsed_macro = parse_macro_burst_frame(op, raw)
|
|
103
|
+
if name is None and parsed_macro is not None:
|
|
104
|
+
name = parsed_macro.display_name
|
|
105
|
+
|
|
106
|
+
if name is not None:
|
|
107
|
+
label = (
|
|
108
|
+
f"{name} (0x{op:04X})"
|
|
109
|
+
if fam_name is None
|
|
110
|
+
else f"{name} (0x{op:04X}) family={fam_name}"
|
|
111
|
+
)
|
|
112
|
+
elif fam_name is not None:
|
|
113
|
+
label = f"family={fam_name} op=0x{op:04X}"
|
|
114
|
+
else:
|
|
115
|
+
label = f"unknown op=0x{op:04X}"
|
|
116
|
+
self._log.debug(
|
|
117
|
+
"%s %s %s len=%d %s", LogTag.FRAME, direction, label, len(raw), note
|
|
118
|
+
)
|
|
119
|
+
if parsed is not None:
|
|
120
|
+
totals = (
|
|
121
|
+
f"{parsed.frame_no}/{parsed.total_frames}"
|
|
122
|
+
if parsed.total_frames is not None
|
|
123
|
+
else f"{parsed.frame_no}"
|
|
124
|
+
)
|
|
125
|
+
first_cmd = (
|
|
126
|
+
f" first_cmd=0x{parsed.first_command_id:02X}"
|
|
127
|
+
if parsed.first_command_id is not None
|
|
128
|
+
else ""
|
|
129
|
+
)
|
|
130
|
+
fmt = (
|
|
131
|
+
f" fmt=0x{parsed.format_marker:02X}"
|
|
132
|
+
if parsed.format_marker is not None
|
|
133
|
+
else ""
|
|
134
|
+
)
|
|
135
|
+
total_commands = (
|
|
136
|
+
f" total_cmds={parsed.total_commands}"
|
|
137
|
+
if parsed.total_commands is not None
|
|
138
|
+
else ""
|
|
139
|
+
)
|
|
140
|
+
self._log.debug(
|
|
141
|
+
f"{LogTag.FRAME} %s REQ_COMMANDS role=%s variant=%s page=%s dev=0x%02X%s%s%s",
|
|
142
|
+
note,
|
|
143
|
+
parsed.role,
|
|
144
|
+
parsed.layout_kind,
|
|
145
|
+
totals,
|
|
146
|
+
parsed.device_id,
|
|
147
|
+
total_commands,
|
|
148
|
+
first_cmd,
|
|
149
|
+
fmt,
|
|
150
|
+
)
|
|
151
|
+
parsed_buttons = parse_button_burst_frame(op, raw, hub_version=self.hub_version)
|
|
152
|
+
if parsed_buttons is not None:
|
|
153
|
+
totals = (
|
|
154
|
+
f"{parsed_buttons.frame_no}/{parsed_buttons.total_frames}"
|
|
155
|
+
if parsed_buttons.total_frames is not None
|
|
156
|
+
else f"{parsed_buttons.frame_no}"
|
|
157
|
+
)
|
|
158
|
+
total_rows = (
|
|
159
|
+
f" total_rows={parsed_buttons.total_rows}"
|
|
160
|
+
if parsed_buttons.total_rows is not None
|
|
161
|
+
else ""
|
|
162
|
+
)
|
|
163
|
+
activity = (
|
|
164
|
+
f" act=0x{parsed_buttons.activity_id:02X}"
|
|
165
|
+
if parsed_buttons.activity_id is not None
|
|
166
|
+
else ""
|
|
167
|
+
)
|
|
168
|
+
row_data = " row_data=yes" if parsed_buttons.has_row_data else " row_data=no"
|
|
169
|
+
self._log.debug(
|
|
170
|
+
f"{LogTag.FRAME} %s REQ_BUTTONS role=%s variant=%s page=%s%s%s%s",
|
|
171
|
+
note,
|
|
172
|
+
parsed_buttons.role,
|
|
173
|
+
parsed_buttons.layout_kind,
|
|
174
|
+
totals,
|
|
175
|
+
activity,
|
|
176
|
+
total_rows,
|
|
177
|
+
row_data,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if parsed_macro is not None:
|
|
181
|
+
frag = (
|
|
182
|
+
f"{parsed_macro.fragment_index}/{parsed_macro.total_fragments}"
|
|
183
|
+
if parsed_macro.fragment_index is not None and parsed_macro.total_fragments is not None
|
|
184
|
+
else (f"{parsed_macro.fragment_index}" if parsed_macro.fragment_index is not None else "?")
|
|
185
|
+
)
|
|
186
|
+
activity = (
|
|
187
|
+
f" act=0x{parsed_macro.activity_id:02X}"
|
|
188
|
+
if parsed_macro.activity_id is not None
|
|
189
|
+
else ""
|
|
190
|
+
)
|
|
191
|
+
start_cmd = (
|
|
192
|
+
f" start_cmd=0x{parsed_macro.start_command_id:02X}"
|
|
193
|
+
if parsed_macro.start_command_id is not None
|
|
194
|
+
else ""
|
|
195
|
+
)
|
|
196
|
+
len_ok = " len_ok=yes" if parsed_macro.payload_length_matches_hi else " len_ok=no"
|
|
197
|
+
self._log.debug(
|
|
198
|
+
f"{LogTag.FRAME} %s REQ_MACROS role=%s frag=%s%s%s%s",
|
|
199
|
+
note,
|
|
200
|
+
parsed_macro.role,
|
|
201
|
+
frag,
|
|
202
|
+
activity,
|
|
203
|
+
start_cmd,
|
|
204
|
+
len_ok,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if direction == "A→H" and fam == FAMILY_PLAY_BLOB:
|
|
208
|
+
blob = self._extract_single_frame_play_blob(payload)
|
|
209
|
+
if blob is not None:
|
|
210
|
+
descriptor_text = self._descriptive_play_blob_text(blob)
|
|
211
|
+
if descriptor_text is not None:
|
|
212
|
+
self._log.debug("%s descriptor %s", LogTag.IR, descriptor_text)
|
|
213
|
+
|
|
214
|
+
context = FrameContext(
|
|
215
|
+
proxy=self,
|
|
216
|
+
opcode=op,
|
|
217
|
+
direction=direction,
|
|
218
|
+
payload=payload,
|
|
219
|
+
raw=raw,
|
|
220
|
+
name=name,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
for handler in frame_handler_registry.iter_for(op, direction):
|
|
224
|
+
try:
|
|
225
|
+
handler.handle(context)
|
|
226
|
+
except Exception:
|
|
227
|
+
self._log.debug("%s error while decoding op 0x%04X via %s", LogTag.PARSE, op, handler.__class__.__name__, exc_info=True)
|