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,434 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import ipaddress
|
|
5
|
+
import socket
|
|
6
|
+
import struct
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Callable, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from .hub_versions import HUB_VERSION_X1, HUB_VERSION_X1S, HUB_VERSION_X2, classify_hub_version
|
|
13
|
+
from .hub_logging import get_hub_logger
|
|
14
|
+
from .protocol_const import OP_CALL_ME, SYNC0, SYNC1
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger("x1proxy.notify")
|
|
17
|
+
|
|
18
|
+
NOTIFY_ME_PAYLOAD = bytes.fromhex("a55a00c1c0")
|
|
19
|
+
BROADCAST_LISTEN_PORT = 8100
|
|
20
|
+
_NOTIFY_BATCH_BYTES: dict[str, bytes] = {
|
|
21
|
+
HUB_VERSION_X1: bytes.fromhex("20210609"),
|
|
22
|
+
HUB_VERSION_X1S: bytes.fromhex("20221120"),
|
|
23
|
+
HUB_VERSION_X2: bytes.fromhex("20221120"),
|
|
24
|
+
}
|
|
25
|
+
_NOTIFY_FW_DEFAULTS: dict[str, int] = {
|
|
26
|
+
HUB_VERSION_X1: 17,
|
|
27
|
+
HUB_VERSION_X1S: 5,
|
|
28
|
+
HUB_VERSION_X2: 8,
|
|
29
|
+
}
|
|
30
|
+
_NOTIFY_MODEL_BYTES: dict[str, int] = {
|
|
31
|
+
HUB_VERSION_X1: 0x01,
|
|
32
|
+
HUB_VERSION_X1S: 0x02,
|
|
33
|
+
HUB_VERSION_X2: 0x03,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _sum8(payload: bytes) -> int:
|
|
38
|
+
return sum(payload) & 0xFF
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _route_local_ip(peer_ip: str) -> str:
|
|
42
|
+
try:
|
|
43
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
44
|
+
s.connect((peer_ip, 80))
|
|
45
|
+
return s.getsockname()[0]
|
|
46
|
+
except Exception:
|
|
47
|
+
return "127.0.0.1"
|
|
48
|
+
finally:
|
|
49
|
+
try:
|
|
50
|
+
s.close()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _broadcast_ip(peer_ip: str) -> str:
|
|
56
|
+
try:
|
|
57
|
+
addr = ipaddress.ip_address(peer_ip)
|
|
58
|
+
network = ipaddress.ip_network(f"{addr}/24", strict=False)
|
|
59
|
+
return str(network.broadcast_address)
|
|
60
|
+
except ValueError:
|
|
61
|
+
return "255.255.255.255"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class NotifyRegistration:
|
|
66
|
+
proxy_id: str
|
|
67
|
+
real_hub_ip: str
|
|
68
|
+
mdns_txt: Dict[str, str]
|
|
69
|
+
hub_version: str
|
|
70
|
+
call_me_port: int
|
|
71
|
+
call_me_cb: Callable[[str, int, str, int], None]
|
|
72
|
+
mac_bytes: bytes
|
|
73
|
+
device_id: bytes
|
|
74
|
+
call_me_hint: bytes
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _classify_or_x1(mdns_txt: Dict[str, str]) -> str:
|
|
78
|
+
"""Classify ``mdns_txt`` for transport-layer NOTIFY routing.
|
|
79
|
+
|
|
80
|
+
NOTIFY framing only branches on the X1 vs X1S/X2 vs X2 envelope
|
|
81
|
+
shape; an unrecognised advertisement does not justify dropping the
|
|
82
|
+
listener registration, so this helper falls back to the X1 line
|
|
83
|
+
when classification fails. The shape is byte-compatible across the
|
|
84
|
+
rest of the family and the subsequent connect banner re-classifies
|
|
85
|
+
authoritatively.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
return classify_hub_version(mdns_txt)
|
|
90
|
+
except ValueError:
|
|
91
|
+
return HUB_VERSION_X1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_connect_ready_beacon(mdns_txt: Dict[str, str]) -> bytes:
|
|
95
|
+
"""Build the UDP post-connect readiness beacon emitted by physical hubs."""
|
|
96
|
+
|
|
97
|
+
mac_bytes = NotifyDemuxer._extract_mac_bytes(mdns_txt)
|
|
98
|
+
hub_version = _classify_or_x1(mdns_txt)
|
|
99
|
+
_device_id, call_me_hint = NotifyDemuxer._build_device_identifiers(mac_bytes, hub_version)
|
|
100
|
+
frame = bytes([SYNC0, SYNC1, 0x07, 0xC4]) + call_me_hint + b"\x00"
|
|
101
|
+
return frame + bytes([_sum8(frame)])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class NotifyDemuxer:
|
|
105
|
+
"""Listen for NOTIFY_ME broadcasts and respond for registered proxies."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, listen_port: int = 8102) -> None:
|
|
108
|
+
self.listen_port = listen_port
|
|
109
|
+
self._sock: Optional[socket.socket] = None
|
|
110
|
+
self._thr: Optional[threading.Thread] = None
|
|
111
|
+
self._stop_event = threading.Event()
|
|
112
|
+
self._lock = threading.Lock()
|
|
113
|
+
self._registrations: Dict[str, NotifyRegistration] = {}
|
|
114
|
+
self._last_reply: Dict[tuple[str, int, str], float] = {}
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
# Public API
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
def register_proxy(
|
|
120
|
+
self,
|
|
121
|
+
proxy_id: str,
|
|
122
|
+
real_hub_ip: str,
|
|
123
|
+
mdns_txt: Dict[str, str],
|
|
124
|
+
call_me_port: int,
|
|
125
|
+
call_me_cb: Callable[[str, int, str, int], None],
|
|
126
|
+
) -> None:
|
|
127
|
+
mac_bytes = self._extract_mac_bytes(mdns_txt)
|
|
128
|
+
hub_version = _classify_or_x1(mdns_txt)
|
|
129
|
+
device_id, call_me_hint = self._build_device_identifiers(
|
|
130
|
+
mac_bytes, hub_version
|
|
131
|
+
)
|
|
132
|
+
reg = NotifyRegistration(
|
|
133
|
+
proxy_id,
|
|
134
|
+
real_hub_ip,
|
|
135
|
+
dict(mdns_txt),
|
|
136
|
+
hub_version,
|
|
137
|
+
int(call_me_port),
|
|
138
|
+
call_me_cb,
|
|
139
|
+
mac_bytes,
|
|
140
|
+
device_id,
|
|
141
|
+
call_me_hint,
|
|
142
|
+
)
|
|
143
|
+
with self._lock:
|
|
144
|
+
self._registrations[proxy_id] = reg
|
|
145
|
+
get_hub_logger(log, proxy_id).info(
|
|
146
|
+
"[DEMUX] registered proxy for hub %s (CALL_ME -> %s:%d)",
|
|
147
|
+
real_hub_ip,
|
|
148
|
+
_route_local_ip(real_hub_ip),
|
|
149
|
+
reg.call_me_port,
|
|
150
|
+
)
|
|
151
|
+
self._ensure_running_locked()
|
|
152
|
+
|
|
153
|
+
def unregister_proxy(self, proxy_id: str) -> None:
|
|
154
|
+
with self._lock:
|
|
155
|
+
if proxy_id in self._registrations:
|
|
156
|
+
self._registrations.pop(proxy_id, None)
|
|
157
|
+
get_hub_logger(log, proxy_id).info("[DEMUX] unregistered proxy")
|
|
158
|
+
self._stop_if_idle_locked()
|
|
159
|
+
|
|
160
|
+
def shutdown(self) -> None:
|
|
161
|
+
with self._lock:
|
|
162
|
+
self._registrations.clear()
|
|
163
|
+
self._stop_thread_locked()
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------
|
|
166
|
+
# Internals
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
def _ensure_running_locked(self) -> None:
|
|
169
|
+
if self._thr and self._thr.is_alive():
|
|
170
|
+
return
|
|
171
|
+
self._stop_event = threading.Event()
|
|
172
|
+
self._sock = self._open_socket()
|
|
173
|
+
self._thr = threading.Thread(
|
|
174
|
+
target=self._notify_loop, name="x1proxy-notify-demux", daemon=True
|
|
175
|
+
)
|
|
176
|
+
self._thr.start()
|
|
177
|
+
|
|
178
|
+
def _stop_if_idle_locked(self) -> None:
|
|
179
|
+
if self._registrations:
|
|
180
|
+
return
|
|
181
|
+
self._stop_thread_locked()
|
|
182
|
+
|
|
183
|
+
def _stop_thread_locked(self) -> None:
|
|
184
|
+
self._stop_event.set()
|
|
185
|
+
if self._sock is not None:
|
|
186
|
+
try:
|
|
187
|
+
self._sock.close()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
self._sock = None
|
|
191
|
+
if self._thr is not None:
|
|
192
|
+
self._thr.join(timeout=1.0)
|
|
193
|
+
self._thr = None
|
|
194
|
+
self._last_reply.clear()
|
|
195
|
+
|
|
196
|
+
def _open_socket(self) -> socket.socket:
|
|
197
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
198
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
199
|
+
reuseport_enabled = False
|
|
200
|
+
reuseport_opt = getattr(socket, "SO_REUSEPORT", None)
|
|
201
|
+
if reuseport_opt is not None:
|
|
202
|
+
try:
|
|
203
|
+
s.setsockopt(socket.SOL_SOCKET, reuseport_opt, 1)
|
|
204
|
+
reuseport_enabled = True
|
|
205
|
+
except OSError:
|
|
206
|
+
log.warning("[DEMUX] SO_REUSEPORT not available")
|
|
207
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
208
|
+
s.bind(("0.0.0.0", self.listen_port))
|
|
209
|
+
s.settimeout(1.0)
|
|
210
|
+
log.info(
|
|
211
|
+
"[DEMUX] listening for NOTIFY_ME/CALL_ME on *:%d (SO_REUSEPORT=%s)",
|
|
212
|
+
self.listen_port,
|
|
213
|
+
reuseport_enabled,
|
|
214
|
+
)
|
|
215
|
+
return s
|
|
216
|
+
|
|
217
|
+
def _notify_loop(self) -> None:
|
|
218
|
+
sock = self._sock
|
|
219
|
+
if sock is None:
|
|
220
|
+
return
|
|
221
|
+
while not self._stop_event.is_set():
|
|
222
|
+
try:
|
|
223
|
+
pkt, (src_ip, src_port) = sock.recvfrom(2048)
|
|
224
|
+
except socket.timeout:
|
|
225
|
+
continue
|
|
226
|
+
except OSError:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if pkt == NOTIFY_ME_PAYLOAD:
|
|
230
|
+
self._handle_notify_me(sock, pkt, src_ip, src_port)
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if len(pkt) >= 16 and pkt[0] == SYNC0 and pkt[1] == SYNC1:
|
|
234
|
+
op = (pkt[2] << 8) | pkt[3]
|
|
235
|
+
if op == OP_CALL_ME:
|
|
236
|
+
self._handle_call_me(pkt, src_ip, src_port)
|
|
237
|
+
|
|
238
|
+
def _build_notify_reply(self, reg: NotifyRegistration) -> Optional[bytes]:
|
|
239
|
+
name = (
|
|
240
|
+
reg.mdns_txt.get("NAME")
|
|
241
|
+
or reg.mdns_txt.get("name")
|
|
242
|
+
or reg.mdns_txt.get("Name")
|
|
243
|
+
or "X1 Hub"
|
|
244
|
+
).encode("utf-8")
|
|
245
|
+
# Physical hubs use a variable-length UTF-8 name field. The byte after the
|
|
246
|
+
# sync header is a length covering the 6-byte device-id tail, 9-byte version
|
|
247
|
+
# block, and UTF-8 name bytes; it does not count the fixed leading 0xC2.
|
|
248
|
+
name_bytes = name[:30]
|
|
249
|
+
version_block = self._build_notify_version_block(reg)
|
|
250
|
+
payload_len = (len(reg.device_id) - 1) + len(version_block) + len(name_bytes)
|
|
251
|
+
frame = (
|
|
252
|
+
bytes([SYNC0, SYNC1, payload_len & 0xFF])
|
|
253
|
+
+ reg.device_id
|
|
254
|
+
+ version_block
|
|
255
|
+
+ name_bytes
|
|
256
|
+
)
|
|
257
|
+
frame += bytes([_sum8(frame)])
|
|
258
|
+
|
|
259
|
+
get_hub_logger(log, reg.proxy_id).info(
|
|
260
|
+
"[DEMUX][REPLY] mac=%s name=%s",
|
|
261
|
+
reg.mac_bytes.hex(":"),
|
|
262
|
+
name.decode("utf-8", "ignore"),
|
|
263
|
+
)
|
|
264
|
+
return frame
|
|
265
|
+
|
|
266
|
+
def _build_notify_version_block(self, reg: NotifyRegistration) -> bytes:
|
|
267
|
+
hub_version = reg.hub_version
|
|
268
|
+
model_byte = _NOTIFY_MODEL_BYTES.get(
|
|
269
|
+
hub_version, _NOTIFY_MODEL_BYTES[HUB_VERSION_X1]
|
|
270
|
+
)
|
|
271
|
+
batch_bytes = _NOTIFY_BATCH_BYTES.get(
|
|
272
|
+
hub_version, _NOTIFY_BATCH_BYTES[HUB_VERSION_X1]
|
|
273
|
+
)
|
|
274
|
+
firmware_version = self._extract_firmware_version(reg.mdns_txt, hub_version)
|
|
275
|
+
tail_flags = (
|
|
276
|
+
b"\x00\x00" if hub_version == HUB_VERSION_X1 else b"\x01\x00"
|
|
277
|
+
)
|
|
278
|
+
return (
|
|
279
|
+
bytes([0x64, model_byte])
|
|
280
|
+
+ batch_bytes
|
|
281
|
+
+ bytes([firmware_version])
|
|
282
|
+
+ tail_flags
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _extract_firmware_version(self, mdns_txt: Dict[str, str], hub_version: str) -> int:
|
|
286
|
+
raw = mdns_txt.get("AVER")
|
|
287
|
+
try:
|
|
288
|
+
if raw is not None:
|
|
289
|
+
value = int(str(raw).strip(), 10)
|
|
290
|
+
if 0 <= value <= 0xFF:
|
|
291
|
+
return value
|
|
292
|
+
except (TypeError, ValueError):
|
|
293
|
+
pass
|
|
294
|
+
return _NOTIFY_FW_DEFAULTS.get(hub_version, _NOTIFY_FW_DEFAULTS[HUB_VERSION_X1])
|
|
295
|
+
|
|
296
|
+
def _handle_notify_me(
|
|
297
|
+
self, sock: socket.socket, pkt: bytes, src_ip: str, src_port: int
|
|
298
|
+
) -> None:
|
|
299
|
+
with self._lock:
|
|
300
|
+
registrations = list(self._registrations.values())
|
|
301
|
+
|
|
302
|
+
for reg in registrations:
|
|
303
|
+
key = (src_ip, src_port, reg.proxy_id)
|
|
304
|
+
now = time.monotonic()
|
|
305
|
+
last = self._last_reply.get(key, 0.0)
|
|
306
|
+
if now - last < 2.0:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
reply = self._build_notify_reply(reg)
|
|
310
|
+
if reply is None:
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
self._last_reply[key] = now
|
|
314
|
+
dest_ip = _broadcast_ip(src_ip)
|
|
315
|
+
get_hub_logger(log, reg.proxy_id).info(
|
|
316
|
+
"[DEMUX] NOTIFY_ME from %s:%d -> CALL_ME=%d broadcast=%s",
|
|
317
|
+
src_ip,
|
|
318
|
+
src_port,
|
|
319
|
+
reg.call_me_port,
|
|
320
|
+
dest_ip,
|
|
321
|
+
)
|
|
322
|
+
try:
|
|
323
|
+
sock.sendto(reply, (dest_ip, BROADCAST_LISTEN_PORT))
|
|
324
|
+
except OSError:
|
|
325
|
+
get_hub_logger(log, reg.proxy_id).exception("[DEMUX] failed to send NOTIFY_ME reply")
|
|
326
|
+
|
|
327
|
+
def _handle_call_me(self, pkt: bytes, src_ip: str, src_port: int) -> None:
|
|
328
|
+
try:
|
|
329
|
+
app_ip = socket.inet_ntoa(pkt[10:14])
|
|
330
|
+
app_port = struct.unpack(">H", pkt[14:16])[0]
|
|
331
|
+
except Exception:
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
mac_hint = pkt[4:10]
|
|
335
|
+
with self._lock:
|
|
336
|
+
registrations = list(self._registrations.values())
|
|
337
|
+
|
|
338
|
+
reg = self._select_registration(mac_hint, registrations)
|
|
339
|
+
if reg is None:
|
|
340
|
+
log.warning(
|
|
341
|
+
"[DEMUX] CALL_ME from %s:%d ignored (no proxy match, mac=%s)",
|
|
342
|
+
src_ip,
|
|
343
|
+
src_port,
|
|
344
|
+
mac_hint.hex(":"),
|
|
345
|
+
)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
get_hub_logger(log, reg.proxy_id).info(
|
|
349
|
+
"[DEMUX] CALL_ME from %s:%d -> app tcp %s:%d",
|
|
350
|
+
src_ip,
|
|
351
|
+
src_port,
|
|
352
|
+
app_ip,
|
|
353
|
+
app_port,
|
|
354
|
+
)
|
|
355
|
+
try:
|
|
356
|
+
reg.call_me_cb(src_ip, src_port, app_ip, app_port)
|
|
357
|
+
except Exception:
|
|
358
|
+
get_hub_logger(log, reg.proxy_id).exception("[DEMUX] proxy callback failed")
|
|
359
|
+
|
|
360
|
+
def _select_registration(
|
|
361
|
+
self, mac_hint: bytes, registrations: list[NotifyRegistration]
|
|
362
|
+
) -> Optional[NotifyRegistration]:
|
|
363
|
+
if not registrations:
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
has_hint = mac_hint and any(mac_hint)
|
|
367
|
+
if has_hint:
|
|
368
|
+
for reg in registrations:
|
|
369
|
+
if reg.call_me_hint == mac_hint:
|
|
370
|
+
return reg
|
|
371
|
+
for reg in registrations:
|
|
372
|
+
if reg.mac_bytes == mac_hint:
|
|
373
|
+
return reg
|
|
374
|
+
|
|
375
|
+
if len(registrations) == 1:
|
|
376
|
+
return registrations[0]
|
|
377
|
+
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def _extract_mac_bytes(mdns_txt: Dict[str, str]) -> bytes:
|
|
382
|
+
try:
|
|
383
|
+
mac_raw = (
|
|
384
|
+
mdns_txt.get("MAC")
|
|
385
|
+
or mdns_txt.get("mac")
|
|
386
|
+
or mdns_txt.get("macaddress")
|
|
387
|
+
)
|
|
388
|
+
mac_bytes = (
|
|
389
|
+
bytes.fromhex(str(mac_raw).replace(":", "").replace("-", ""))
|
|
390
|
+
if mac_raw
|
|
391
|
+
else b""
|
|
392
|
+
)
|
|
393
|
+
except ValueError:
|
|
394
|
+
mac_bytes = b""
|
|
395
|
+
|
|
396
|
+
if len(mac_bytes) < 6:
|
|
397
|
+
mac_bytes = mac_bytes.ljust(6, b"\x00")
|
|
398
|
+
else:
|
|
399
|
+
mac_bytes = mac_bytes[:6]
|
|
400
|
+
|
|
401
|
+
return mac_bytes
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def _build_device_identifiers(mac_bytes: bytes, hub_version: str) -> tuple[bytes, bytes]:
|
|
405
|
+
"""Return the device id used in NOTIFY_ME replies and the CALL_ME hint."""
|
|
406
|
+
|
|
407
|
+
required_prefix_byte = b"\xc2"
|
|
408
|
+
if hub_version == HUB_VERSION_X2:
|
|
409
|
+
device_id = required_prefix_byte + mac_bytes
|
|
410
|
+
call_me_hint = mac_bytes
|
|
411
|
+
return device_id, call_me_hint
|
|
412
|
+
|
|
413
|
+
static_id_suffix_byte = b"\x4b" if hub_version == HUB_VERSION_X1 else b"\x45"
|
|
414
|
+
unique_tail_mac = mac_bytes[0:5]
|
|
415
|
+
device_id = required_prefix_byte + unique_tail_mac + static_id_suffix_byte
|
|
416
|
+
call_me_hint = unique_tail_mac + static_id_suffix_byte
|
|
417
|
+
|
|
418
|
+
return device_id, call_me_hint
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
_GLOBAL_DEMUXER: Optional[NotifyDemuxer] = None
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_notify_demuxer(listen_port: Optional[int] = None) -> NotifyDemuxer:
|
|
425
|
+
global _GLOBAL_DEMUXER
|
|
426
|
+
if _GLOBAL_DEMUXER is None:
|
|
427
|
+
_GLOBAL_DEMUXER = NotifyDemuxer(listen_port or 8102)
|
|
428
|
+
elif listen_port is not None and listen_port != _GLOBAL_DEMUXER.listen_port:
|
|
429
|
+
log.warning(
|
|
430
|
+
"[DEMUX] existing listener on %d (ignoring requested %d)",
|
|
431
|
+
_GLOBAL_DEMUXER.listen_port,
|
|
432
|
+
listen_port,
|
|
433
|
+
)
|
|
434
|
+
return _GLOBAL_DEMUXER
|