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,1174 @@
|
|
|
1
|
+
"""Device-create / device-restore step machinery.
|
|
2
|
+
|
|
3
|
+
The hub's device-create flow is a strictly serialised sequence of
|
|
4
|
+
``(opcode, payload, ack)`` exchanges. Each phase -- create-header,
|
|
5
|
+
per-command record pages, button bindings, idle behaviour, macros,
|
|
6
|
+
inputs slot, device-update commit, remote sync -- follows the same
|
|
7
|
+
transport pattern, differing only in opcode and ack-correlation.
|
|
8
|
+
|
|
9
|
+
This module factors that pattern into two layers so the device-restore
|
|
10
|
+
flow and the existing WiFi-create flow can share code:
|
|
11
|
+
|
|
12
|
+
- :class:`CreateStep` (Layer 2): one frame to send and one ack to
|
|
13
|
+
wait for. Carries the family byte, the payload bytes, the expected
|
|
14
|
+
ack opcode, and an optional ack-correlation byte (set when the ack
|
|
15
|
+
echoes a request-side byte such as a button id).
|
|
16
|
+
- :func:`run_create_sequence` (Layer 2): drives a list of steps in
|
|
17
|
+
order against a duck-typed proxy. Captures the assigned device id
|
|
18
|
+
from a flagged step's ack payload so subsequent steps can target
|
|
19
|
+
the new device. Aborts on first failure.
|
|
20
|
+
- Step builders (Layer 3): pure functions that produce
|
|
21
|
+
:class:`CreateStep` (or lists of them) for each phase. Frame layout
|
|
22
|
+
is locked in here, isolated from transport concerns.
|
|
23
|
+
|
|
24
|
+
The proxy is duck-typed: callers pass anything that exposes
|
|
25
|
+
``_send_family_frame(family, payload)`` and
|
|
26
|
+
``wait_for_ack_any(candidates, *, timeout, not_before)``. That
|
|
27
|
+
lets tests substitute a lightweight fake without spinning up the full
|
|
28
|
+
:class:`X1Proxy`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any, Iterable, Literal, Protocol
|
|
36
|
+
|
|
37
|
+
from .devices import DeviceConfig, build_device_create_payload
|
|
38
|
+
from .wire_schema import schema_for
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: Discriminant for :class:`DeviceCreateRequest.transport`. ``"ir"`` covers
|
|
42
|
+
#: every record-shaped device class -- IR, Bluetooth, RF -- that uses the
|
|
43
|
+
#: canonical ``build_device_create_step`` / ``build_command_write_steps``
|
|
44
|
+
#: pipeline. ``"network_callback"`` covers WiFi-commands devices whose
|
|
45
|
+
#: writes are wifi-create-shape payloads bound to a callback service on
|
|
46
|
+
#: the integration; the Roku-on-X1 vs IP-generic-on-X1S/X2 split is an
|
|
47
|
+
#: internal variant of this transport, resolved from ``hub_version`` /
|
|
48
|
+
#: ``device_class_code`` inside the orchestrator rather than from the
|
|
49
|
+
#: request.
|
|
50
|
+
TransportKind = Literal["ir", "network_callback"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Wire-format constants
|
|
55
|
+
#
|
|
56
|
+
# Each constant carries a short note pointing to where it was observed on
|
|
57
|
+
# the wire (frame numbers refer to the user's full IR-create capture).
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#: Family byte for the device-create header (A->H). Wire example:
|
|
62
|
+
#: ``OP_7B07`` on frame #191. Hub assigns ``device_id`` and replies on
|
|
63
|
+
#: :data:`ACK_OPCODE_DEVICE_CREATE`.
|
|
64
|
+
FAMILY_DEVICE_CREATE = 0x07
|
|
65
|
+
|
|
66
|
+
#: Family byte for the device-update commit (A->H, sent at the end of
|
|
67
|
+
#: the create flow with the real ``device_id`` and ``tail_marker=1``).
|
|
68
|
+
#: Wire example: ``OP_7B08`` on frame #485. Acked via the generic
|
|
69
|
+
#: STATUS_ACK family.
|
|
70
|
+
FAMILY_DEVICE_UPDATE = 0x08
|
|
71
|
+
|
|
72
|
+
#: Family byte for the activity-create header (A->H). Mirrors the
|
|
73
|
+
#: device-create flow but writes the same 120/210-byte body to a
|
|
74
|
+
#: different opcode family. Acked via :data:`ACK_OPCODE_DEVICE_CREATE`
|
|
75
|
+
#: with the assigned activity id in ``payload[0]`` (the hub uses the
|
|
76
|
+
#: same id space for devices and activities).
|
|
77
|
+
FAMILY_ACTIVITY_CREATE = 0x37
|
|
78
|
+
|
|
79
|
+
#: Family byte for per-command record writes (A->H, paged). Used for
|
|
80
|
+
#: all device classes -- IR-DB, BT, RF, learned codes -- with the codec
|
|
81
|
+
#: selected by the ``library_type`` field inside the body, not by
|
|
82
|
+
#: opcode. The opcode-hi byte equals the payload length so a full page
|
|
83
|
+
#: is ``OP_FA0E`` (250-byte payload) and a final short page is e.g.
|
|
84
|
+
#: ``OP_6C0E`` (108-byte payload).
|
|
85
|
+
FAMILY_COMMAND_WRITE = 0x0E
|
|
86
|
+
|
|
87
|
+
#: Family byte for button bindings (A->H). Wire example: ``OP_193E``
|
|
88
|
+
#: on frame #445. Acked via :data:`ACK_OPCODE_BUTTON_BINDING` with the
|
|
89
|
+
#: request's button id echoed at ``payload[0]``.
|
|
90
|
+
FAMILY_BUTTON_BINDING = 0x3E
|
|
91
|
+
|
|
92
|
+
#: Family byte for ``SET_IDLE_BEHAVIOR`` (A->H). Wire example:
|
|
93
|
+
#: ``OP_0241`` on frame #477. Acked via the generic STATUS_ACK family.
|
|
94
|
+
FAMILY_SET_IDLE_BEHAVIOR = 0x41
|
|
95
|
+
|
|
96
|
+
#: Family byte for macro placeholder writes (A->H). Wire example:
|
|
97
|
+
#: ``OP_3212`` on frames #479, #481 (``POWER_ON``, ``POWER_OFF``).
|
|
98
|
+
#: Acked via :data:`ACK_OPCODE_MACRO` with the macro's key id echoed
|
|
99
|
+
#: at ``payload[0]``.
|
|
100
|
+
FAMILY_MACRO = 0x12
|
|
101
|
+
|
|
102
|
+
#: Family byte for the inputs slot write (A->H). Wire example:
|
|
103
|
+
#: ``OP_7746`` on frame #483 (all-zeros body == "no input switching").
|
|
104
|
+
#: Acked via the generic STATUS_ACK family.
|
|
105
|
+
FAMILY_INPUTS = 0x46
|
|
106
|
+
|
|
107
|
+
#: Family byte for device key-sort writes.
|
|
108
|
+
FAMILY_KEY_SORT = 0x61
|
|
109
|
+
|
|
110
|
+
#: Family byte for ``REMOTE_SYNC`` (A->H). Wire example: ``OP_0064``
|
|
111
|
+
#: on frame #487. Acked via the generic STATUS_ACK family.
|
|
112
|
+
FAMILY_REMOTE_SYNC = 0x64
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#: Full ack opcode the hub returns for a device-create. ``payload[0]``
|
|
116
|
+
#: carries the freshly assigned ``device_id``.
|
|
117
|
+
ACK_OPCODE_DEVICE_CREATE = 0x0107
|
|
118
|
+
|
|
119
|
+
#: Full ack opcode the X1 returns for an activity-create. Observed as an
|
|
120
|
+
#: immediate reply to family-0x37 writes, with ``payload[0]`` carrying the
|
|
121
|
+
#: freshly assigned activity id.
|
|
122
|
+
ACK_OPCODE_ACTIVITY_CREATE = 0x0137
|
|
123
|
+
|
|
124
|
+
#: Full ack opcode for a button-binding write. ``payload[0]`` echoes
|
|
125
|
+
#: the request's button id.
|
|
126
|
+
ACK_OPCODE_BUTTON_BINDING = 0x013E
|
|
127
|
+
|
|
128
|
+
#: Full ack opcode for a macro write. ``payload[0]`` echoes the
|
|
129
|
+
#: request's macro key id.
|
|
130
|
+
ACK_OPCODE_MACRO = 0x0112
|
|
131
|
+
|
|
132
|
+
#: Full ack opcode for a generic STATUS_ACK (used by most other write
|
|
133
|
+
#: steps). The success rule is ``payload[0] == 0x00``.
|
|
134
|
+
ACK_OPCODE_STATUS = 0x0103
|
|
135
|
+
|
|
136
|
+
#: Status byte value that indicates "accepted" on the generic ack.
|
|
137
|
+
ACK_STATUS_BYTE_OK = 0x00
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Layer 2 -- step sequencer
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_PAGED_WRITE_OUTER_WRAPPER_LEN = 3
|
|
146
|
+
_PAGED_WRITE_BODY_CHUNK = 247
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _ProxyLike(Protocol):
|
|
150
|
+
"""Subset of :class:`X1Proxy` the sequencer needs."""
|
|
151
|
+
|
|
152
|
+
def _send_family_frame(self, family: int, payload: bytes) -> None: ...
|
|
153
|
+
|
|
154
|
+
def wait_for_ack_any(
|
|
155
|
+
self,
|
|
156
|
+
candidates: list[tuple[int, int | None]],
|
|
157
|
+
*,
|
|
158
|
+
timeout: float = 5.0,
|
|
159
|
+
not_before: float | None = None,
|
|
160
|
+
) -> tuple[int, bytes] | None: ...
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True, slots=True)
|
|
164
|
+
class CreateStep:
|
|
165
|
+
"""One create-flow exchange: send a frame, wait for a specific ack.
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
label: Human-readable identifier shown in logs ("device-create",
|
|
169
|
+
"button-binding btn=0xBD", etc.). Has no semantic meaning.
|
|
170
|
+
family: Opcode low byte. The high byte is derived from the
|
|
171
|
+
payload length by :meth:`_send_family_frame`, so we never
|
|
172
|
+
store an explicit opcode-hi.
|
|
173
|
+
payload: Bytes to send as the frame body (no checksum, no magic).
|
|
174
|
+
ack_opcode: Full opcode of the ack frame we wait for. ``0x0103``
|
|
175
|
+
for STATUS_ACK, ``0x0107`` for device-create ack, etc.
|
|
176
|
+
ack_first_byte: Optional ``payload[0]`` correlation. ``None``
|
|
177
|
+
means "any byte"; an integer matches only when the ack's
|
|
178
|
+
first payload byte equals it. Used for STATUS_ACK success
|
|
179
|
+
checks (``ack_first_byte=0``) and for correlation acks that
|
|
180
|
+
echo a request id (button id, macro key id).
|
|
181
|
+
ack_reject_first_bytes: ``payload[0]`` values that indicate the
|
|
182
|
+
hub *explicitly rejected* this write. The sequencer accepts
|
|
183
|
+
these as ack-arrivals (so it does not time out waiting) but
|
|
184
|
+
reports them as a rejection rather than success. The
|
|
185
|
+
classic example is STATUS_ACK with ``payload[0] == 0x0C``
|
|
186
|
+
for a malformed save page.
|
|
187
|
+
capture_device_id: When ``True``, the sequencer reads the ack
|
|
188
|
+
``payload[0]`` as the freshly-assigned device id. Set on the
|
|
189
|
+
device-create step.
|
|
190
|
+
timeout: Per-attempt ack-wait timeout in seconds.
|
|
191
|
+
retries: Number of additional resend attempts on ack timeout
|
|
192
|
+
(default ``0`` = send once).
|
|
193
|
+
retry_delay: Seconds to sleep between retries.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
label: str
|
|
197
|
+
family: int
|
|
198
|
+
payload: bytes
|
|
199
|
+
ack_opcode: int
|
|
200
|
+
ack_first_byte: int | None = None
|
|
201
|
+
ack_reject_first_bytes: tuple[int, ...] = ()
|
|
202
|
+
capture_device_id: bool = False
|
|
203
|
+
timeout: float = 5.0
|
|
204
|
+
retries: int = 0
|
|
205
|
+
retry_delay: float = 0.0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True, slots=True)
|
|
209
|
+
class CreateSequenceResult:
|
|
210
|
+
"""Outcome of :func:`run_create_sequence`.
|
|
211
|
+
|
|
212
|
+
On success, ``failed_step`` and ``failed_index`` are ``None`` and
|
|
213
|
+
``assigned_device_id`` is set iff one of the steps had
|
|
214
|
+
``capture_device_id=True``.
|
|
215
|
+
|
|
216
|
+
On failure, ``failed_step`` and ``failed_index`` point at the first
|
|
217
|
+
step that did not get a successful ack. ``rejected`` distinguishes
|
|
218
|
+
"hub said no" (an ack arrived with a byte listed in
|
|
219
|
+
``ack_reject_first_bytes``) from "no ack at all within the retry
|
|
220
|
+
budget" (a timeout). ``reject_payload`` carries the rejection ack's
|
|
221
|
+
full payload when ``rejected`` is True, for diagnostics.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
success: bool
|
|
225
|
+
assigned_device_id: int | None
|
|
226
|
+
failed_step: CreateStep | None
|
|
227
|
+
failed_index: int | None
|
|
228
|
+
rejected: bool = False
|
|
229
|
+
reject_payload: bytes | None = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _page_create_step_payloads(step: CreateStep) -> list[bytes]:
|
|
233
|
+
"""Return one or more wire payloads for a create step.
|
|
234
|
+
|
|
235
|
+
Most create-step payloads already fit in one family frame. The
|
|
236
|
+
family-0x46 inputs writer is different: ``build_inputs_write``
|
|
237
|
+
returns the full canonical payload with ``total_pages`` already
|
|
238
|
+
populated in the body header, and callers rely on the transport
|
|
239
|
+
layer to split oversized bodies into 247-byte chunks with a fresh
|
|
240
|
+
3-byte ``[0x01][page_no_be]`` wrapper on each page.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
step.family != FAMILY_INPUTS
|
|
245
|
+
or len(step.payload) <= 250
|
|
246
|
+
or len(step.payload) <= _PAGED_WRITE_OUTER_WRAPPER_LEN
|
|
247
|
+
):
|
|
248
|
+
return [step.payload]
|
|
249
|
+
|
|
250
|
+
body = step.payload[_PAGED_WRITE_OUTER_WRAPPER_LEN:]
|
|
251
|
+
payloads: list[bytes] = []
|
|
252
|
+
total_pages = max(1, (len(body) + _PAGED_WRITE_BODY_CHUNK - 1) // _PAGED_WRITE_BODY_CHUNK)
|
|
253
|
+
for seq in range(1, total_pages + 1):
|
|
254
|
+
chunk = body[(seq - 1) * _PAGED_WRITE_BODY_CHUNK : seq * _PAGED_WRITE_BODY_CHUNK]
|
|
255
|
+
payloads.append(bytes([0x01]) + seq.to_bytes(2, "big") + bytes(chunk))
|
|
256
|
+
return payloads
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def run_create_sequence(
|
|
260
|
+
proxy: _ProxyLike,
|
|
261
|
+
steps: Iterable[CreateStep],
|
|
262
|
+
) -> CreateSequenceResult:
|
|
263
|
+
"""Drive a serial sequence of :class:`CreateStep` exchanges.
|
|
264
|
+
|
|
265
|
+
For each step:
|
|
266
|
+
|
|
267
|
+
1. Send the frame via ``proxy._send_family_frame(family, payload)``.
|
|
268
|
+
2. Wait for an ack matching ``(ack_opcode, ack_first_byte)``.
|
|
269
|
+
3. On match, advance. If ``capture_device_id`` is set, record the
|
|
270
|
+
ack's ``payload[0]`` as :attr:`CreateSequenceResult.assigned_device_id`.
|
|
271
|
+
4. On timeout, retry up to ``retries`` times. If still no ack, abort
|
|
272
|
+
the sequence and return a failure result pointing at this step.
|
|
273
|
+
|
|
274
|
+
The caller is responsible for clearing any leftover ack state before
|
|
275
|
+
calling (typically via ``proxy.reset_ack_queues()``). This function
|
|
276
|
+
deliberately does not touch proxy lifecycle so the same sequencer
|
|
277
|
+
can be reused mid-session if needed.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
assigned_device_id: int | None = None
|
|
281
|
+
steps_list = list(steps)
|
|
282
|
+
|
|
283
|
+
for index, step in enumerate(steps_list):
|
|
284
|
+
# Build the list of (opcode, first_byte) candidates the wait
|
|
285
|
+
# call should accept. We include both the success-first-byte
|
|
286
|
+
# and any rejection-first-bytes so the wait returns as soon as
|
|
287
|
+
# the hub answers either way; the sequencer then classifies.
|
|
288
|
+
#
|
|
289
|
+
# Special case: a STATUS_ACK step that expects success on
|
|
290
|
+
# ``payload[0]==0x00`` should also wake up on any non-zero
|
|
291
|
+
# first byte. STATUS_ACK frames carry a binary verdict in
|
|
292
|
+
# ``payload[0]`` (``0x00`` = accepted, anything else = rejected
|
|
293
|
+
# with a status code), so a non-zero byte is always a
|
|
294
|
+
# rejection of the in-flight write. Without this, a rejection
|
|
295
|
+
# silently waits out the per-step timeout (5 s by default)
|
|
296
|
+
# instead of failing fast with the status code in the log.
|
|
297
|
+
candidates: list[tuple[int, int | None]] = [
|
|
298
|
+
(step.ack_opcode, step.ack_first_byte)
|
|
299
|
+
]
|
|
300
|
+
wildcard_status_reject = (
|
|
301
|
+
step.ack_opcode == ACK_OPCODE_STATUS
|
|
302
|
+
and step.ack_first_byte == ACK_STATUS_BYTE_OK
|
|
303
|
+
)
|
|
304
|
+
if wildcard_status_reject:
|
|
305
|
+
candidates.append((step.ack_opcode, None))
|
|
306
|
+
for reject_byte in step.ack_reject_first_bytes:
|
|
307
|
+
candidates.append((step.ack_opcode, reject_byte & 0xFF))
|
|
308
|
+
|
|
309
|
+
matched: tuple[int, bytes] | None = None
|
|
310
|
+
page_payloads = _page_create_step_payloads(step)
|
|
311
|
+
|
|
312
|
+
for page_payload in page_payloads:
|
|
313
|
+
attempts_left = max(1, int(step.retries) + 1)
|
|
314
|
+
matched = None
|
|
315
|
+
|
|
316
|
+
while attempts_left > 0:
|
|
317
|
+
attempts_left -= 1
|
|
318
|
+
send_ts = time.monotonic()
|
|
319
|
+
proxy._send_family_frame(step.family, page_payload)
|
|
320
|
+
matched = proxy.wait_for_ack_any(
|
|
321
|
+
candidates,
|
|
322
|
+
timeout=step.timeout,
|
|
323
|
+
not_before=send_ts,
|
|
324
|
+
)
|
|
325
|
+
if matched is not None:
|
|
326
|
+
break
|
|
327
|
+
if attempts_left > 0 and step.retry_delay > 0:
|
|
328
|
+
time.sleep(step.retry_delay)
|
|
329
|
+
|
|
330
|
+
if matched is None:
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
if matched is None:
|
|
334
|
+
return CreateSequenceResult(
|
|
335
|
+
success=False,
|
|
336
|
+
assigned_device_id=assigned_device_id,
|
|
337
|
+
failed_step=step,
|
|
338
|
+
failed_index=index,
|
|
339
|
+
rejected=False,
|
|
340
|
+
reject_payload=None,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
_ack_opcode, ack_payload = matched
|
|
344
|
+
first_byte = ack_payload[0] if ack_payload else None
|
|
345
|
+
is_explicit_reject = (
|
|
346
|
+
step.ack_reject_first_bytes
|
|
347
|
+
and first_byte is not None
|
|
348
|
+
and (first_byte & 0xFF) in step.ack_reject_first_bytes
|
|
349
|
+
)
|
|
350
|
+
is_status_wildcard_reject = (
|
|
351
|
+
wildcard_status_reject
|
|
352
|
+
and first_byte is not None
|
|
353
|
+
and (first_byte & 0xFF) != ACK_STATUS_BYTE_OK
|
|
354
|
+
)
|
|
355
|
+
if is_explicit_reject or is_status_wildcard_reject:
|
|
356
|
+
return CreateSequenceResult(
|
|
357
|
+
success=False,
|
|
358
|
+
assigned_device_id=assigned_device_id,
|
|
359
|
+
failed_step=step,
|
|
360
|
+
failed_index=index,
|
|
361
|
+
rejected=True,
|
|
362
|
+
reject_payload=bytes(ack_payload),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
if step.capture_device_id and ack_payload:
|
|
366
|
+
assigned_device_id = ack_payload[0] & 0xFF
|
|
367
|
+
|
|
368
|
+
return CreateSequenceResult(
|
|
369
|
+
success=True,
|
|
370
|
+
assigned_device_id=assigned_device_id,
|
|
371
|
+
failed_step=None,
|
|
372
|
+
failed_index=None,
|
|
373
|
+
rejected=False,
|
|
374
|
+
reject_payload=None,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Layer 1 -- unified device-create request / orchestrator
|
|
380
|
+
#
|
|
381
|
+
# Phase 7 of the protocol refactor folds the four previously-parallel
|
|
382
|
+
# entry points -- ``create_wifi_device`` (X1 Roku), ``_create_virtual_ip_wifi_device``
|
|
383
|
+
# (X1S/X2 IP), ``_restore_network_callback_device`` and the IR/BT/RF
|
|
384
|
+
# branch of ``restore_device`` -- into a single :func:`run_device_create`
|
|
385
|
+
# orchestrator that takes a typed :class:`DeviceCreateRequest`. The
|
|
386
|
+
# orchestrator dispatches on :attr:`DeviceCreateRequest.transport`;
|
|
387
|
+
# per-transport details (variant selection inside ``network_callback``,
|
|
388
|
+
# command-class selection inside ``ir``) live on the proxy mixins so
|
|
389
|
+
# this module stays free of wire layout for the wifi family-0x07 body.
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@dataclass(frozen=True, slots=True)
|
|
394
|
+
class DeviceCreateRequest:
|
|
395
|
+
"""Unified input for :func:`run_device_create`.
|
|
396
|
+
|
|
397
|
+
Both the user-driven "create wifi device" entry point and the
|
|
398
|
+
backup-driven "restore device" entry point construct one of these
|
|
399
|
+
and hand it to the orchestrator. The orchestrator decides which
|
|
400
|
+
on-the-wire sequence to issue based on :attr:`transport`; everything
|
|
401
|
+
else in this dataclass is the data the chosen pipeline needs.
|
|
402
|
+
|
|
403
|
+
Field groupings:
|
|
404
|
+
|
|
405
|
+
- **Always set:** :attr:`transport`.
|
|
406
|
+
- **IR transport:** :attr:`device_block` (full backup device block
|
|
407
|
+
including ``device_class``, ``device_class_code``, body fields),
|
|
408
|
+
:attr:`commands` (rows with ``restore_data`` carrying
|
|
409
|
+
``library_type`` / ``button_code`` / ``data_hex``),
|
|
410
|
+
:attr:`button_bindings`, :attr:`macros`, :attr:`inputs`,
|
|
411
|
+
:attr:`favorites`.
|
|
412
|
+
- **Network-callback transport:** :attr:`network_callback_profile`
|
|
413
|
+
(carries ``device_name``, ``brand_name``, ``ip_address``,
|
|
414
|
+
``request_port``, ``slots``, ``input_command_ids``,
|
|
415
|
+
``power_on_command_id``, ``power_off_command_id``,
|
|
416
|
+
``device_class``, ``device_class_code``). The orchestrator
|
|
417
|
+
reads everything wifi-specific from this dict so the top-level
|
|
418
|
+
dataclass shape stays the same across transports.
|
|
419
|
+
|
|
420
|
+
:attr:`entity_kind` reserves a slot for Phase 8's activity-restore
|
|
421
|
+
unification: ``"device"`` writes the family-0x07 create body,
|
|
422
|
+
``"activity"`` writes family-0x37. Phase 7 ships only the
|
|
423
|
+
``"device"`` path; passing ``"activity"`` raises until Phase 8.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
transport: TransportKind
|
|
427
|
+
device_block: dict[str, Any] = field(default_factory=dict)
|
|
428
|
+
commands: list[dict[str, Any]] = field(default_factory=list)
|
|
429
|
+
button_bindings: list[dict[str, Any]] = field(default_factory=list)
|
|
430
|
+
macros: list[dict[str, Any]] = field(default_factory=list)
|
|
431
|
+
inputs: list[dict[str, Any]] = field(default_factory=list)
|
|
432
|
+
input_record: dict[str, Any] | None = None
|
|
433
|
+
favorites: list[dict[str, Any]] = field(default_factory=list)
|
|
434
|
+
key_sort: dict[str, Any] | None = None
|
|
435
|
+
network_callback_profile: dict[str, Any] | None = None
|
|
436
|
+
entity_kind: Literal["device", "activity"] = "device"
|
|
437
|
+
#: Source-device-id -> destination-device-id translation used when
|
|
438
|
+
#: ``entity_kind="activity"``. Activity content references commands
|
|
439
|
+
#: on *other* devices, and the destination hub will have assigned
|
|
440
|
+
#: those devices new ids at restore time. Empty for device-create
|
|
441
|
+
#: requests (which have no cross-device references).
|
|
442
|
+
device_id_map: dict[int, int] = field(default_factory=dict)
|
|
443
|
+
#: Source-device-id -> {source_command_id: new_command_id} mapping
|
|
444
|
+
#: captured during a bundle restore's devices phase. Used by the
|
|
445
|
+
#: activity-create path to resolve macro steps whose ``key_id`` is
|
|
446
|
+
#: ``0xC5`` (the "switch input on device" marker): the step's
|
|
447
|
+
#: ``duration`` byte is a 1-based ordinal into the *source* device's
|
|
448
|
+
#: input list, which has to be re-resolved against the *destination*
|
|
449
|
+
#: device's freshly-assigned command ids. Empty outside the
|
|
450
|
+
#: bundle-restore code path.
|
|
451
|
+
command_id_maps_by_source_device_id: dict[int, dict[int, int]] = field(
|
|
452
|
+
default_factory=dict
|
|
453
|
+
)
|
|
454
|
+
#: Per-device backup payloads from the bundle currently being
|
|
455
|
+
#: restored, keyed by source device_id. The activity-create path
|
|
456
|
+
#: uses this to look up each step's source-device input table when
|
|
457
|
+
#: resolving ``0xC5`` ordinals. Empty outside the bundle-restore
|
|
458
|
+
#: code path; the per-entity restore_activity entry point never
|
|
459
|
+
#: populates this.
|
|
460
|
+
bundle_devices_by_source_id: dict[int, dict[str, Any]] = field(
|
|
461
|
+
default_factory=dict
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@dataclass(frozen=True, slots=True)
|
|
466
|
+
class DeviceCreateResult:
|
|
467
|
+
"""Outcome of :func:`run_device_create`.
|
|
468
|
+
|
|
469
|
+
On success, ``device_id`` carries the freshly assigned id and the
|
|
470
|
+
``restored_*`` / ``skipped_*`` counters describe what the
|
|
471
|
+
orchestrator wrote (or chose to skip with a log line). The
|
|
472
|
+
``command_id_map`` translates source-side command ids (from a
|
|
473
|
+
backup payload) to the ids the destination hub assigned at write
|
|
474
|
+
time -- empty for fresh-create calls that didn't reference any
|
|
475
|
+
backup ids.
|
|
476
|
+
|
|
477
|
+
On failure, ``success`` is ``False`` and ``failed_step_label``
|
|
478
|
+
points at the step that didn't get a successful ack.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
success: bool
|
|
482
|
+
device_id: int | None = None
|
|
483
|
+
restored_commands: int = 0
|
|
484
|
+
restored_button_bindings: int = 0
|
|
485
|
+
restored_macros: int = 0
|
|
486
|
+
restored_inputs: int = 0
|
|
487
|
+
skipped_favorites: int = 0
|
|
488
|
+
skipped_macro_steps: int = 0
|
|
489
|
+
#: Bundle-restore specific: macro 0xC5 ("set input on device") rows
|
|
490
|
+
#: whose source-ordinal could not be re-resolved against the
|
|
491
|
+
#: destination device's freshly-assigned command ids. Always 0
|
|
492
|
+
#: outside the bundle-restore code path.
|
|
493
|
+
skipped_input_ordinals: int = 0
|
|
494
|
+
command_id_map: dict[int, int] = field(default_factory=dict)
|
|
495
|
+
failed_step_label: str | None = None
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class _DeviceCreateProxyLike(Protocol):
|
|
499
|
+
"""Subset of :class:`X1Proxy` the unified orchestrator needs.
|
|
500
|
+
|
|
501
|
+
Each pipeline lives on a proxy mixin so the wire-layout code stays
|
|
502
|
+
close to the rest of the mixin family (wifi-create stays with the
|
|
503
|
+
wifi flow, IR restore stays with the rest of the restore
|
|
504
|
+
plumbing). The orchestrator only dispatches.
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
def _run_network_callback_create(
|
|
508
|
+
self, request: "DeviceCreateRequest"
|
|
509
|
+
) -> "DeviceCreateResult": ...
|
|
510
|
+
|
|
511
|
+
def _run_ir_device_create(
|
|
512
|
+
self, request: "DeviceCreateRequest"
|
|
513
|
+
) -> "DeviceCreateResult": ...
|
|
514
|
+
|
|
515
|
+
def _run_activity_create(
|
|
516
|
+
self, request: "DeviceCreateRequest"
|
|
517
|
+
) -> "DeviceCreateResult": ...
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def run_device_create(
|
|
521
|
+
proxy: _DeviceCreateProxyLike,
|
|
522
|
+
request: DeviceCreateRequest,
|
|
523
|
+
) -> DeviceCreateResult:
|
|
524
|
+
"""Drive the unified device-create pipeline.
|
|
525
|
+
|
|
526
|
+
Both ``create_wifi_device`` and ``restore_device`` route through
|
|
527
|
+
this entry point; the public methods are thin adapters that build
|
|
528
|
+
a :class:`DeviceCreateRequest` from their callers' inputs (live
|
|
529
|
+
user kwargs vs a backup payload) and hand it off here.
|
|
530
|
+
|
|
531
|
+
The orchestrator dispatches in two layers:
|
|
532
|
+
|
|
533
|
+
- ``entity_kind="activity"`` -- the activity-create pipeline on
|
|
534
|
+
the restore mixin. Activities always use the canonical
|
|
535
|
+
schema-driven record path (family ``0x37``) and reference
|
|
536
|
+
commands on other devices via :attr:`DeviceCreateRequest.device_id_map`;
|
|
537
|
+
transport is therefore implicit.
|
|
538
|
+
- ``entity_kind="device"`` -- dispatch by transport:
|
|
539
|
+
|
|
540
|
+
- ``"network_callback"`` -- the wifi-create flow on the wifi
|
|
541
|
+
mixin. Variant resolution between Roku-on-X1 and
|
|
542
|
+
IP-generic-on-X1S/X2 is internal to that pipeline.
|
|
543
|
+
- ``"ir"`` -- the IR / Bluetooth / RF restore flow on the
|
|
544
|
+
restore mixin. Command persistence goes through the
|
|
545
|
+
codec-aware :meth:`persist_command_record` /
|
|
546
|
+
:meth:`persist_ir_blob` writers; the post-create step
|
|
547
|
+
sequence is the canonical bindings/macros/inputs/update/sync
|
|
548
|
+
sequence built from schema-driven step builders.
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
if request.entity_kind == "activity":
|
|
552
|
+
return proxy._run_activity_create(request)
|
|
553
|
+
if request.entity_kind != "device":
|
|
554
|
+
raise ValueError(
|
|
555
|
+
f"run_device_create received unsupported entity_kind={request.entity_kind!r}; "
|
|
556
|
+
"expected 'device' or 'activity'"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
if request.transport == "network_callback":
|
|
560
|
+
return proxy._run_network_callback_create(request)
|
|
561
|
+
if request.transport == "ir":
|
|
562
|
+
return proxy._run_ir_device_create(request)
|
|
563
|
+
raise ValueError(
|
|
564
|
+
f"run_device_create received unsupported transport={request.transport!r}; "
|
|
565
|
+
"expected one of 'ir', 'network_callback'"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
# Layer 3 -- step builders for the IR-create flow
|
|
571
|
+
#
|
|
572
|
+
# Each builder is a pure function: same inputs -> same bytes -> testable
|
|
573
|
+
# against captured wire frames without any proxy involvement.
|
|
574
|
+
# ---------------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# Common 6-byte header observed at payload[0..5] of nearly every A->H
|
|
578
|
+
# write in the create flow: button bindings, macros, inputs, etc. The
|
|
579
|
+
# meaning of the trailing ``01 00 01`` isn't fully decoded; treating it
|
|
580
|
+
# as a fixed constant matches every captured frame.
|
|
581
|
+
_COMMON_WRITE_PREAMBLE = bytes([0x01, 0x00, 0x01, 0x01, 0x00, 0x01])
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def build_device_create_step(
|
|
585
|
+
config: DeviceConfig,
|
|
586
|
+
*,
|
|
587
|
+
hub_version: str,
|
|
588
|
+
family: int = FAMILY_DEVICE_CREATE,
|
|
589
|
+
) -> CreateStep:
|
|
590
|
+
"""Build the device-create step (``OP_7B07`` on X1).
|
|
591
|
+
|
|
592
|
+
``config`` should carry ``device_id=0xFF`` (the create sentinel) and
|
|
593
|
+
``tail_marker=0`` (the pre-commit value -- the device-update step
|
|
594
|
+
later in the flow sets it to ``1``). The hub assigns the real
|
|
595
|
+
``device_id`` and returns it via the :data:`ACK_OPCODE_DEVICE_CREATE`
|
|
596
|
+
ack.
|
|
597
|
+
|
|
598
|
+
Pass ``family=FAMILY_ACTIVITY_CREATE`` (``0x37``) to write an
|
|
599
|
+
activity record instead of a device record. The body layout is the
|
|
600
|
+
same 120/210-byte schema; only the opcode family changes. The hub
|
|
601
|
+
assigns activity ids out of the same id space and returns them on
|
|
602
|
+
the same ack opcode.
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
payload = build_device_create_payload(config, hub_version=hub_version)
|
|
606
|
+
label = (
|
|
607
|
+
"activity-create" if family == FAMILY_ACTIVITY_CREATE else "device-create"
|
|
608
|
+
)
|
|
609
|
+
ack_opcode = (
|
|
610
|
+
ACK_OPCODE_ACTIVITY_CREATE
|
|
611
|
+
if family == FAMILY_ACTIVITY_CREATE
|
|
612
|
+
else ACK_OPCODE_DEVICE_CREATE
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
return CreateStep(
|
|
616
|
+
label=label,
|
|
617
|
+
family=family,
|
|
618
|
+
payload=payload,
|
|
619
|
+
ack_opcode=ack_opcode,
|
|
620
|
+
ack_first_byte=None, # any byte -- the assigned id is captured, not matched
|
|
621
|
+
capture_device_id=True,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def build_device_update_step(
|
|
626
|
+
config: DeviceConfig,
|
|
627
|
+
*,
|
|
628
|
+
hub_version: str,
|
|
629
|
+
) -> CreateStep:
|
|
630
|
+
"""Build the device-update / commit step (``OP_7B08`` on X1).
|
|
631
|
+
|
|
632
|
+
Sent at the end of the create flow with the real ``device_id``
|
|
633
|
+
assigned by the hub and ``tail_marker=1`` (the "device is committed"
|
|
634
|
+
value). The body shape is identical to the create body.
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
payload = build_device_create_payload(config, hub_version=hub_version)
|
|
638
|
+
return CreateStep(
|
|
639
|
+
label="device-update",
|
|
640
|
+
family=FAMILY_DEVICE_UPDATE,
|
|
641
|
+
payload=payload,
|
|
642
|
+
ack_opcode=ACK_OPCODE_STATUS,
|
|
643
|
+
ack_first_byte=ACK_STATUS_BYTE_OK,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _body_checksum(body: bytes) -> int:
|
|
648
|
+
"""Compute the internal body-checksum byte.
|
|
649
|
+
|
|
650
|
+
Every multi-byte write body in this family ends with a 1-byte
|
|
651
|
+
checksum at the last position; the hub validates it before
|
|
652
|
+
accepting the write. The reduction is ``sum(body[:-1]) & 0xFF``
|
|
653
|
+
(the same algorithm the transport uses for the outer frame
|
|
654
|
+
checksum). Pass the body **without** the trailing checksum byte
|
|
655
|
+
or with that byte zeroed; both produce the same result.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
return sum(body[:-1] if body else b"") & 0xFF
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _seal_body(body: bytearray) -> bytes:
|
|
662
|
+
"""Write the body checksum into ``body[-1]`` and return as bytes."""
|
|
663
|
+
|
|
664
|
+
body[-1] = sum(body[:-1]) & 0xFF
|
|
665
|
+
return bytes(body)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def synthesize_command_code(command_id: int) -> int:
|
|
669
|
+
"""Return the X1 synthetic command-code used by keymap/input writes."""
|
|
670
|
+
|
|
671
|
+
if command_id < 0 or command_id > 0xFF:
|
|
672
|
+
raise ValueError(f"command_id {command_id} out of byte range")
|
|
673
|
+
return 0x4E20 + (command_id & 0xFF)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def build_macro_step_record(
|
|
677
|
+
*,
|
|
678
|
+
device_id: int,
|
|
679
|
+
command_id: int,
|
|
680
|
+
fid: int = 0,
|
|
681
|
+
duration: int = 0,
|
|
682
|
+
delay: int = 0xFF,
|
|
683
|
+
) -> bytes:
|
|
684
|
+
"""Build one 10-byte macro-step record for :func:`build_macro_step`."""
|
|
685
|
+
|
|
686
|
+
if device_id < 0 or device_id > 0xFF:
|
|
687
|
+
raise ValueError(f"device_id {device_id} out of byte range")
|
|
688
|
+
if command_id < 0 or command_id > 0xFF:
|
|
689
|
+
raise ValueError(f"command_id {command_id} out of byte range")
|
|
690
|
+
if fid < 0 or fid > 0xFFFFFFFFFFFF:
|
|
691
|
+
raise ValueError(f"fid {fid} out of 48-bit range")
|
|
692
|
+
if duration < 0 or duration > 0xFF:
|
|
693
|
+
raise ValueError(f"duration {duration} out of byte range")
|
|
694
|
+
if delay < 0 or delay > 0xFF:
|
|
695
|
+
raise ValueError(f"delay {delay} out of byte range")
|
|
696
|
+
|
|
697
|
+
return (
|
|
698
|
+
bytes([device_id & 0xFF, command_id & 0xFF])
|
|
699
|
+
+ fid.to_bytes(6, "big")
|
|
700
|
+
+ bytes([duration & 0xFF, delay & 0xFF])
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# NOTE: ``build_x1_input_entry`` and ``build_inputs_step`` were removed
|
|
705
|
+
# in Phase 3 of the protocol refactor. The unified family-0x46 builder
|
|
706
|
+
# lives in :mod:`custom_components.sofabaton_x1s.lib.inputs`
|
|
707
|
+
# (:func:`~custom_components.sofabaton_x1s.lib.inputs.build_inputs_write`)
|
|
708
|
+
# and replaces the X1-only entry/page builders that used to live here.
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
#: Per-page payload capacity for command-record pages. 250 bytes maxes
|
|
712
|
+
#: out the opcode-hi byte (``0xFA``) which equals payload length on the
|
|
713
|
+
#: wire.
|
|
714
|
+
_COMMAND_PAGE_FULL_PAYLOAD = 250
|
|
715
|
+
|
|
716
|
+
#: Per-page header size: ``[command_seq, page_num_be]``. 3 bytes,
|
|
717
|
+
#: present on every page of every command.
|
|
718
|
+
_COMMAND_PAGE_HEADER_LEN = 3
|
|
719
|
+
|
|
720
|
+
#: Pre-label body-header size (size + total_pages + dev + btn + lib +
|
|
721
|
+
#: button_code). 12 bytes.
|
|
722
|
+
_COMMAND_BODY_PRELABEL_LEN = 12
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
#: Default rejection byte the hub sends on STATUS_ACK to indicate a
|
|
726
|
+
#: malformed or otherwise unacceptable save page. Used by command
|
|
727
|
+
#: writes so the sequencer can surface "hub said no" distinctly from
|
|
728
|
+
#: "no ack arrived".
|
|
729
|
+
_REJECT_BYTE_BAD_SAVE = 0x0C
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def build_command_write_steps(
|
|
733
|
+
*,
|
|
734
|
+
hub_version: str,
|
|
735
|
+
command_seq: int,
|
|
736
|
+
command_burst_size: int,
|
|
737
|
+
device_id: int,
|
|
738
|
+
button_id: int,
|
|
739
|
+
library_type: int,
|
|
740
|
+
button_code: int,
|
|
741
|
+
label: str,
|
|
742
|
+
library_data: bytes,
|
|
743
|
+
ack_timeout: float = 5.0,
|
|
744
|
+
inter_page_retry_delay: float = 0.0,
|
|
745
|
+
) -> list[CreateStep]:
|
|
746
|
+
"""Build the paged command-record write for one command (family ``0x0E``).
|
|
747
|
+
|
|
748
|
+
The same wire shape carries IR-DB blobs, RF blobs, BT blobs, and
|
|
749
|
+
learned codes. The codec is selected by ``library_type``; the
|
|
750
|
+
payload bytes are opaque ``library_data``.
|
|
751
|
+
|
|
752
|
+
Wire layout. The "body" is a single logical record chunked across
|
|
753
|
+
one or more pages, each page prefixed with ``[command_seq,
|
|
754
|
+
page_num_be]``. The label slot width and encoding follow the hub
|
|
755
|
+
variant: 30-byte ASCII on X1, 60-byte UTF-16BE on X1S/X2 (read
|
|
756
|
+
from :func:`schema_for`)::
|
|
757
|
+
|
|
758
|
+
body[0] size (== command_burst_size)
|
|
759
|
+
body[1..2] total_pages_be (for *this* command)
|
|
760
|
+
body[3] device_id
|
|
761
|
+
body[4] button_id (default key-slot, 0 = unbound)
|
|
762
|
+
body[5] library_type (codec selector)
|
|
763
|
+
body[6..11] button_code (BE) (6-byte / 48-bit identifier)
|
|
764
|
+
body[12..label_end]
|
|
765
|
+
label slot (30 ASCII / 60 UTF-16BE,
|
|
766
|
+
null-padded)
|
|
767
|
+
body[label_end..-2]
|
|
768
|
+
library_data (opaque codec bytes)
|
|
769
|
+
body[-1] body checksum
|
|
770
|
+
|
|
771
|
+
Arguments:
|
|
772
|
+
command_seq: 1-based index of this command within the enclosing
|
|
773
|
+
burst. Appears in ``payload[0]`` of every page of this
|
|
774
|
+
command.
|
|
775
|
+
command_burst_size: Total number of commands in the burst the
|
|
776
|
+
caller intends to write. Written verbatim into ``body[0]``
|
|
777
|
+
of every page of every command in the burst.
|
|
778
|
+
device_id: 1-byte hub-assigned device id (returned by the
|
|
779
|
+
preceding device-create ack).
|
|
780
|
+
button_id: Default remote key slot the command should land on.
|
|
781
|
+
Often ``0`` (unbound at write time; bound later via the
|
|
782
|
+
button-binding step).
|
|
783
|
+
library_type: Codec selector. Observed values: ``0x0D`` on
|
|
784
|
+
IR-DB sourced devices, others for BT / RF / learned codes.
|
|
785
|
+
button_code: 48-bit canonical command identifier used by
|
|
786
|
+
binding and macro flows to reference this command.
|
|
787
|
+
label: User-visible command label, ASCII, truncated to 30
|
|
788
|
+
bytes.
|
|
789
|
+
library_data: Opaque codec bytes appended after the label
|
|
790
|
+
slot. Format depends on ``library_type``.
|
|
791
|
+
ack_timeout: Per-page ack timeout. Defaults to 5 s, matching
|
|
792
|
+
the hub's typical command-write turnaround.
|
|
793
|
+
inter_page_retry_delay: Sleep between page retries on the
|
|
794
|
+
sequencer. Defaults to ``0`` -- pages do not retry by
|
|
795
|
+
default; the caller can pass a positive value to enable a
|
|
796
|
+
single-retry pattern.
|
|
797
|
+
"""
|
|
798
|
+
|
|
799
|
+
if command_seq < 1 or command_seq > 0xFF:
|
|
800
|
+
raise ValueError(f"command_seq {command_seq} out of byte range")
|
|
801
|
+
if command_burst_size < 1 or command_burst_size > 0xFF:
|
|
802
|
+
raise ValueError(f"command_burst_size {command_burst_size} out of byte range")
|
|
803
|
+
if device_id < 0 or device_id > 0xFF:
|
|
804
|
+
raise ValueError(f"device_id {device_id} out of byte range")
|
|
805
|
+
if button_id < 0 or button_id > 0xFF:
|
|
806
|
+
raise ValueError(f"button_id {button_id} out of byte range")
|
|
807
|
+
if library_type < 0 or library_type > 0xFF:
|
|
808
|
+
raise ValueError(f"library_type {library_type} out of byte range")
|
|
809
|
+
if button_code < 0 or button_code > 0xFFFFFFFFFFFF:
|
|
810
|
+
raise ValueError(f"button_code {button_code} out of 48-bit range")
|
|
811
|
+
if not library_data:
|
|
812
|
+
raise ValueError("library_data must not be empty")
|
|
813
|
+
|
|
814
|
+
schema = schema_for(hub_version)
|
|
815
|
+
label_slot_len = schema.command_label_slot_len
|
|
816
|
+
label_encoding = schema.command_label_encoding
|
|
817
|
+
label_bytes = label.encode(label_encoding, errors="replace")[:label_slot_len]
|
|
818
|
+
label_slot_bytes = label_bytes + b"\x00" * (label_slot_len - len(label_bytes))
|
|
819
|
+
|
|
820
|
+
# Body content excluding final checksum byte. Pages chunk this
|
|
821
|
+
# buffer; the checksum is computed over the whole sealed body and
|
|
822
|
+
# written at the last position before paging.
|
|
823
|
+
body_content_len = (
|
|
824
|
+
_COMMAND_BODY_PRELABEL_LEN
|
|
825
|
+
+ len(label_slot_bytes)
|
|
826
|
+
+ len(library_data)
|
|
827
|
+
)
|
|
828
|
+
body = bytearray(body_content_len + 1) # +1 for trailing body checksum
|
|
829
|
+
|
|
830
|
+
# Total pages is computed from final body length divided by the
|
|
831
|
+
# page-chunk capacity. The page chunk capacity is full-payload
|
|
832
|
+
# minus the 3-byte page header.
|
|
833
|
+
page_chunk_capacity = _COMMAND_PAGE_FULL_PAYLOAD - _COMMAND_PAGE_HEADER_LEN
|
|
834
|
+
total_pages = (len(body) + page_chunk_capacity - 1) // page_chunk_capacity
|
|
835
|
+
|
|
836
|
+
body[0] = command_burst_size & 0xFF
|
|
837
|
+
body[1:3] = total_pages.to_bytes(2, "big")
|
|
838
|
+
body[3] = device_id & 0xFF
|
|
839
|
+
body[4] = button_id & 0xFF
|
|
840
|
+
body[5] = library_type & 0xFF
|
|
841
|
+
body[6:12] = button_code.to_bytes(6, "big")
|
|
842
|
+
label_end = 12 + len(label_slot_bytes)
|
|
843
|
+
body[12:label_end] = label_slot_bytes
|
|
844
|
+
body[label_end : label_end + len(library_data)] = library_data
|
|
845
|
+
body_bytes = _seal_body(body)
|
|
846
|
+
|
|
847
|
+
steps: list[CreateStep] = []
|
|
848
|
+
for page_index in range(total_pages):
|
|
849
|
+
page_no = page_index + 1
|
|
850
|
+
start = page_index * page_chunk_capacity
|
|
851
|
+
chunk = body_bytes[start : start + page_chunk_capacity]
|
|
852
|
+
header = bytes([command_seq & 0xFF]) + page_no.to_bytes(2, "big")
|
|
853
|
+
steps.append(
|
|
854
|
+
CreateStep(
|
|
855
|
+
label=(
|
|
856
|
+
f"command-write code=0x{button_code:012X} "
|
|
857
|
+
f"page={page_no}/{total_pages}"
|
|
858
|
+
),
|
|
859
|
+
family=FAMILY_COMMAND_WRITE,
|
|
860
|
+
payload=header + chunk,
|
|
861
|
+
ack_opcode=ACK_OPCODE_STATUS,
|
|
862
|
+
ack_first_byte=ACK_STATUS_BYTE_OK,
|
|
863
|
+
ack_reject_first_bytes=(_REJECT_BYTE_BAD_SAVE,),
|
|
864
|
+
timeout=ack_timeout,
|
|
865
|
+
retry_delay=inter_page_retry_delay,
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
return steps
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def encode_command_sort_body(
|
|
872
|
+
ordered_pairs: list[tuple[int, int]],
|
|
873
|
+
) -> bytes:
|
|
874
|
+
"""Encode the inner key-sort body as a flat stream of ``(command_id,
|
|
875
|
+
sort_id)`` pairs.
|
|
876
|
+
|
|
877
|
+
The hub stores a per-device display ordering for the physical
|
|
878
|
+
remote's device-browse screen as a sequence of 2-byte pairs --
|
|
879
|
+
``(command_id, sort_position)`` -- carried inside the family-0x61
|
|
880
|
+
paged write body (the portion between the device id and the
|
|
881
|
+
trailing checksum byte). Each entry is byte-clamped to fit the
|
|
882
|
+
wire field width.
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
out = bytearray(len(ordered_pairs) * 2)
|
|
886
|
+
for index, (command_id, sort_id) in enumerate(ordered_pairs):
|
|
887
|
+
out[index * 2] = command_id & 0xFF
|
|
888
|
+
out[index * 2 + 1] = sort_id & 0xFF
|
|
889
|
+
return bytes(out)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def build_key_sort_steps(
|
|
893
|
+
*,
|
|
894
|
+
device_id: int,
|
|
895
|
+
msg_hex: str,
|
|
896
|
+
ack_timeout: float = 5.0,
|
|
897
|
+
) -> list[CreateStep]:
|
|
898
|
+
"""Build the app-style family-0x61 device key-sort write."""
|
|
899
|
+
|
|
900
|
+
if device_id < 0 or device_id > 0xFF:
|
|
901
|
+
raise ValueError(f"device_id {device_id} out of byte range")
|
|
902
|
+
raw_hex = str(msg_hex or "").strip()
|
|
903
|
+
try:
|
|
904
|
+
msg_bytes = bytes.fromhex(raw_hex) if raw_hex else b""
|
|
905
|
+
except ValueError as exc:
|
|
906
|
+
raise ValueError(f"invalid key-sort msg_hex: {msg_hex!r}") from exc
|
|
907
|
+
|
|
908
|
+
page_chunk_capacity = _COMMAND_PAGE_FULL_PAYLOAD - _COMMAND_PAGE_HEADER_LEN
|
|
909
|
+
body = bytearray(len(msg_bytes) + 5)
|
|
910
|
+
total_pages = max(1, (len(body) + page_chunk_capacity - 1) // page_chunk_capacity)
|
|
911
|
+
body[0] = 0x01
|
|
912
|
+
body[1:3] = total_pages.to_bytes(2, "big")
|
|
913
|
+
body[3] = device_id & 0xFF
|
|
914
|
+
if msg_bytes:
|
|
915
|
+
body[4 : 4 + len(msg_bytes)] = msg_bytes
|
|
916
|
+
body_bytes = _seal_body(body)
|
|
917
|
+
|
|
918
|
+
steps: list[CreateStep] = []
|
|
919
|
+
for page_index in range(total_pages):
|
|
920
|
+
page_no = page_index + 1
|
|
921
|
+
start = page_index * page_chunk_capacity
|
|
922
|
+
chunk = body_bytes[start : start + page_chunk_capacity]
|
|
923
|
+
payload = bytes([0x01]) + page_no.to_bytes(2, "big") + chunk
|
|
924
|
+
steps.append(
|
|
925
|
+
CreateStep(
|
|
926
|
+
label=f"key-sort page={page_no}/{total_pages}",
|
|
927
|
+
family=FAMILY_KEY_SORT,
|
|
928
|
+
payload=payload,
|
|
929
|
+
ack_opcode=ACK_OPCODE_STATUS,
|
|
930
|
+
ack_first_byte=ACK_STATUS_BYTE_OK,
|
|
931
|
+
timeout=ack_timeout,
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
return steps
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def build_button_binding_step(
|
|
938
|
+
*,
|
|
939
|
+
device_id: int,
|
|
940
|
+
button_id: int,
|
|
941
|
+
short_press_device_id: int,
|
|
942
|
+
short_press_button_code: int,
|
|
943
|
+
short_press_button_id: int = 0,
|
|
944
|
+
long_press_device_id: int = 0,
|
|
945
|
+
long_press_button_code: int = 0,
|
|
946
|
+
long_press_button_id: int = 0,
|
|
947
|
+
) -> CreateStep:
|
|
948
|
+
"""Build a button-binding write (``OP_193E``).
|
|
949
|
+
|
|
950
|
+
Writes one ``button_id`` slot on ``device_id`` with full short-
|
|
951
|
+
and long-press triples. Each triple is
|
|
952
|
+
``(target_device_id, button_code, button_id)`` where
|
|
953
|
+
``button_code`` is the 6-byte / 48-bit canonical command
|
|
954
|
+
identifier and ``button_id`` is an optional remote-key slot
|
|
955
|
+
reference (often ``0`` when the bound command is not also assigned
|
|
956
|
+
a default key slot).
|
|
957
|
+
|
|
958
|
+
Wire layout (3-byte outer wrapper + 22-byte body)::
|
|
959
|
+
|
|
960
|
+
payload[0..2] [01, 0, 1] outer wrapper
|
|
961
|
+
body[0..2] [01, 0, 1] page constants
|
|
962
|
+
body[3] device_id keymap entity
|
|
963
|
+
body[4] button_id slot being written
|
|
964
|
+
body[5] short_press_device_id
|
|
965
|
+
body[6..11] short_press_button_code (BE)
|
|
966
|
+
body[12] short_press_button_id
|
|
967
|
+
body[13] long_press_device_id
|
|
968
|
+
body[14..19] long_press_button_code (BE)
|
|
969
|
+
body[20] long_press_button_id
|
|
970
|
+
body[21] body checksum
|
|
971
|
+
|
|
972
|
+
Total payload = 25 bytes. The ack on :data:`ACK_OPCODE_BUTTON_BINDING`
|
|
973
|
+
carries ``button_id`` echoed at ``payload[0]`` for correlation.
|
|
974
|
+
"""
|
|
975
|
+
|
|
976
|
+
for name, value in (
|
|
977
|
+
("button_id", button_id),
|
|
978
|
+
("device_id", device_id),
|
|
979
|
+
("short_press_device_id", short_press_device_id),
|
|
980
|
+
("short_press_button_id", short_press_button_id),
|
|
981
|
+
("long_press_device_id", long_press_device_id),
|
|
982
|
+
("long_press_button_id", long_press_button_id),
|
|
983
|
+
):
|
|
984
|
+
if value < 0 or value > 0xFF:
|
|
985
|
+
raise ValueError(f"{name}={value} out of byte range")
|
|
986
|
+
for name, value in (
|
|
987
|
+
("short_press_button_code", short_press_button_code),
|
|
988
|
+
("long_press_button_code", long_press_button_code),
|
|
989
|
+
):
|
|
990
|
+
if value < 0 or value > 0xFFFFFFFFFFFF:
|
|
991
|
+
raise ValueError(f"{name}={value} out of 48-bit range")
|
|
992
|
+
|
|
993
|
+
body = bytearray(22)
|
|
994
|
+
# body[0..2] is the inner-page header: [marker, total_pages_be=1].
|
|
995
|
+
# Button-binding is always a single-page write across X1 / X1S /
|
|
996
|
+
# X2 (the body never exceeds the per-page chunk), so [0x01, 0x00,
|
|
997
|
+
# 0x01] is correct on every variant -- not just an X1 coincidence.
|
|
998
|
+
body[0:3] = bytes([0x01, 0x00, 0x01])
|
|
999
|
+
body[3] = device_id & 0xFF
|
|
1000
|
+
body[4] = button_id & 0xFF
|
|
1001
|
+
body[5] = short_press_device_id & 0xFF
|
|
1002
|
+
body[6:12] = short_press_button_code.to_bytes(6, "big")
|
|
1003
|
+
body[12] = short_press_button_id & 0xFF
|
|
1004
|
+
body[13] = long_press_device_id & 0xFF
|
|
1005
|
+
body[14:20] = long_press_button_code.to_bytes(6, "big")
|
|
1006
|
+
body[20] = long_press_button_id & 0xFF
|
|
1007
|
+
body_bytes = _seal_body(body)
|
|
1008
|
+
|
|
1009
|
+
payload = bytes([0x01, 0x00, 0x01]) + body_bytes
|
|
1010
|
+
assert len(payload) == 25, (
|
|
1011
|
+
f"button-binding payload is {len(payload)} bytes, expected 25"
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
return CreateStep(
|
|
1015
|
+
label=(
|
|
1016
|
+
f"button-binding btn=0x{button_id:02X} "
|
|
1017
|
+
f"-> dev=0x{short_press_device_id:02X} "
|
|
1018
|
+
f"code=0x{short_press_button_code:012X}"
|
|
1019
|
+
),
|
|
1020
|
+
family=FAMILY_BUTTON_BINDING,
|
|
1021
|
+
payload=payload,
|
|
1022
|
+
ack_opcode=ACK_OPCODE_BUTTON_BINDING,
|
|
1023
|
+
ack_first_byte=button_id & 0xFF,
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def build_set_idle_behavior_step(*, device_id: int, mode: int) -> CreateStep:
|
|
1028
|
+
"""Build a ``SET_IDLE_BEHAVIOR`` write (``OP_0241``).
|
|
1029
|
+
|
|
1030
|
+
Two-byte payload: ``[device_id, mode]``. Acked via STATUS_ACK with
|
|
1031
|
+
``payload[0] == 0x00``.
|
|
1032
|
+
"""
|
|
1033
|
+
|
|
1034
|
+
return CreateStep(
|
|
1035
|
+
label=f"set-idle-behavior dev=0x{device_id:02X} mode={mode}",
|
|
1036
|
+
family=FAMILY_SET_IDLE_BEHAVIOR,
|
|
1037
|
+
payload=bytes([device_id & 0xFF, mode & 0xFF]),
|
|
1038
|
+
ack_opcode=ACK_OPCODE_STATUS,
|
|
1039
|
+
ack_first_byte=ACK_STATUS_BYTE_OK,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
#: Width of one macro-step record. Observed at 10 bytes.
|
|
1044
|
+
MACRO_STEP_RECORD_SIZE = 10
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def build_macro_step(
|
|
1048
|
+
*,
|
|
1049
|
+
hub_version: str,
|
|
1050
|
+
device_id: int,
|
|
1051
|
+
key_id: int,
|
|
1052
|
+
label: str,
|
|
1053
|
+
step_records: bytes = b"",
|
|
1054
|
+
) -> CreateStep:
|
|
1055
|
+
"""Build a macro write (``OP_3212``).
|
|
1056
|
+
|
|
1057
|
+
Single-page write. The label slot width and encoding follow the
|
|
1058
|
+
hub variant (30-byte ASCII on X1, 60-byte UTF-16BE on X1S/X2,
|
|
1059
|
+
read from :func:`schema_for`)::
|
|
1060
|
+
|
|
1061
|
+
body[0..2] [01, 00, 01] page header (page 1 of 1)
|
|
1062
|
+
body[3] device_id
|
|
1063
|
+
body[4] key_id (macro key id, e.g. 0xC6 POWER_ON)
|
|
1064
|
+
body[5] step_count
|
|
1065
|
+
body[6 .. 6 + 10*step_count - 1] step records (10 bytes each)
|
|
1066
|
+
body[next .. next + L - 1] label slot (L=30/60)
|
|
1067
|
+
body[next + L] body checksum
|
|
1068
|
+
|
|
1069
|
+
``step_records`` is the concatenated step-bytes blob (size must be
|
|
1070
|
+
a multiple of :data:`MACRO_STEP_RECORD_SIZE`). Pass ``b""`` to
|
|
1071
|
+
write a zero-step record (the hub then stores the macro as label
|
|
1072
|
+
only, no playback).
|
|
1073
|
+
|
|
1074
|
+
The hub's auto-written ``POWER_ON`` / ``POWER_OFF`` macros also
|
|
1075
|
+
use this builder; they always carry one step. The backup flow
|
|
1076
|
+
skips ``REQ_MACROS`` for unconfigured-power devices because those
|
|
1077
|
+
auto-written macros are not user content, but the wire write does
|
|
1078
|
+
include a real step.
|
|
1079
|
+
"""
|
|
1080
|
+
|
|
1081
|
+
if key_id < 0 or key_id > 0xFF:
|
|
1082
|
+
raise ValueError(f"key_id {key_id} out of byte range")
|
|
1083
|
+
if device_id < 0 or device_id > 0xFF:
|
|
1084
|
+
raise ValueError(f"device_id {device_id} out of byte range")
|
|
1085
|
+
if len(step_records) % MACRO_STEP_RECORD_SIZE != 0:
|
|
1086
|
+
raise ValueError(
|
|
1087
|
+
f"step_records length {len(step_records)} is not a multiple of "
|
|
1088
|
+
f"MACRO_STEP_RECORD_SIZE ({MACRO_STEP_RECORD_SIZE})"
|
|
1089
|
+
)
|
|
1090
|
+
step_count = len(step_records) // MACRO_STEP_RECORD_SIZE
|
|
1091
|
+
if step_count > 0xFF:
|
|
1092
|
+
raise ValueError(f"too many steps ({step_count}); max 255")
|
|
1093
|
+
|
|
1094
|
+
schema = schema_for(hub_version)
|
|
1095
|
+
label_slot_len = schema.macro_label_slot_len
|
|
1096
|
+
label_encoding = schema.macro_label_encoding
|
|
1097
|
+
label_bytes = label.encode(label_encoding, errors="replace")[:label_slot_len]
|
|
1098
|
+
label_slot = label_bytes + b"\x00" * (label_slot_len - len(label_bytes))
|
|
1099
|
+
|
|
1100
|
+
# body layout: 6-byte preamble + steps + L-byte label slot + 1-byte checksum
|
|
1101
|
+
body_len = 6 + len(step_records) + label_slot_len + 1
|
|
1102
|
+
body = bytearray(body_len)
|
|
1103
|
+
body[0:3] = bytes([0x01, 0x00, 0x01])
|
|
1104
|
+
body[3] = device_id & 0xFF
|
|
1105
|
+
body[4] = key_id & 0xFF
|
|
1106
|
+
body[5] = step_count & 0xFF
|
|
1107
|
+
body[6 : 6 + len(step_records)] = step_records
|
|
1108
|
+
label_start = 6 + len(step_records)
|
|
1109
|
+
body[label_start : label_start + label_slot_len] = label_slot
|
|
1110
|
+
body_bytes = _seal_body(body)
|
|
1111
|
+
|
|
1112
|
+
# Outer 3-byte page wrapper: [page_const=1, page_num_be=1].
|
|
1113
|
+
payload = bytes([0x01, 0x00, 0x01]) + body_bytes
|
|
1114
|
+
|
|
1115
|
+
return CreateStep(
|
|
1116
|
+
label=f"macro key=0x{key_id:02X} label={label!r} steps={step_count}",
|
|
1117
|
+
family=FAMILY_MACRO,
|
|
1118
|
+
payload=payload,
|
|
1119
|
+
ack_opcode=ACK_OPCODE_MACRO,
|
|
1120
|
+
ack_first_byte=key_id & 0xFF,
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def build_remote_sync_step() -> CreateStep:
|
|
1125
|
+
"""Build the terminal ``REMOTE_SYNC`` step (``OP_0064``).
|
|
1126
|
+
|
|
1127
|
+
Sent at the very end of the create flow with an empty payload.
|
|
1128
|
+
Tells the remote to refresh its local view of the hub state.
|
|
1129
|
+
"""
|
|
1130
|
+
|
|
1131
|
+
return CreateStep(
|
|
1132
|
+
label="remote-sync",
|
|
1133
|
+
family=FAMILY_REMOTE_SYNC,
|
|
1134
|
+
payload=b"",
|
|
1135
|
+
ack_opcode=ACK_OPCODE_STATUS,
|
|
1136
|
+
ack_first_byte=ACK_STATUS_BYTE_OK,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
__all__ = [
|
|
1141
|
+
"ACK_OPCODE_BUTTON_BINDING",
|
|
1142
|
+
"ACK_OPCODE_DEVICE_CREATE",
|
|
1143
|
+
"ACK_OPCODE_MACRO",
|
|
1144
|
+
"ACK_OPCODE_STATUS",
|
|
1145
|
+
"ACK_STATUS_BYTE_OK",
|
|
1146
|
+
"CreateSequenceResult",
|
|
1147
|
+
"CreateStep",
|
|
1148
|
+
"DeviceCreateRequest",
|
|
1149
|
+
"DeviceCreateResult",
|
|
1150
|
+
"FAMILY_ACTIVITY_CREATE",
|
|
1151
|
+
"FAMILY_BUTTON_BINDING",
|
|
1152
|
+
"FAMILY_DEVICE_CREATE",
|
|
1153
|
+
"FAMILY_DEVICE_UPDATE",
|
|
1154
|
+
"FAMILY_INPUTS",
|
|
1155
|
+
"FAMILY_KEY_SORT",
|
|
1156
|
+
"FAMILY_COMMAND_WRITE",
|
|
1157
|
+
"FAMILY_MACRO",
|
|
1158
|
+
"FAMILY_REMOTE_SYNC",
|
|
1159
|
+
"FAMILY_SET_IDLE_BEHAVIOR",
|
|
1160
|
+
"MACRO_STEP_RECORD_SIZE",
|
|
1161
|
+
"build_button_binding_step",
|
|
1162
|
+
"build_device_create_step",
|
|
1163
|
+
"build_device_update_step",
|
|
1164
|
+
"build_command_write_steps",
|
|
1165
|
+
"build_key_sort_steps",
|
|
1166
|
+
"encode_command_sort_body",
|
|
1167
|
+
"build_macro_step_record",
|
|
1168
|
+
"build_macro_step",
|
|
1169
|
+
"build_remote_sync_step",
|
|
1170
|
+
"build_set_idle_behavior_step",
|
|
1171
|
+
"run_create_sequence",
|
|
1172
|
+
"run_device_create",
|
|
1173
|
+
"synthesize_command_code",
|
|
1174
|
+
]
|