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.
@@ -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)