python-mobius 0.5.0__py3-none-any.whl → 0.7.0__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.
- mobius/__init__.py +16 -7
- mobius/cli.py +164 -8
- mobius/coap.py +18 -14
- mobius/constants.py +186 -65
- mobius/device.py +1954 -180
- mobius/device_status.py +428 -0
- mobius/discovery.py +5 -5
- mobius/dump.py +378 -0
- mobius/frame.py +98 -2
- mobius/manufacturer.py +6 -5
- mobius/mesh_address.py +2 -2
- mobius/modifiers.py +7 -4
- mobius/power.py +9 -8
- mobius/relay.py +61 -4
- {python_mobius-0.5.0.dist-info → python_mobius-0.7.0.dist-info}/METADATA +7 -1
- python_mobius-0.7.0.dist-info/RECORD +22 -0
- python_mobius-0.5.0.dist-info/RECORD +0 -21
- {python_mobius-0.5.0.dist-info → python_mobius-0.7.0.dist-info}/WHEEL +0 -0
- {python_mobius-0.5.0.dist-info → python_mobius-0.7.0.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.5.0.dist-info → python_mobius-0.7.0.dist-info}/licenses/LICENSE +0 -0
mobius/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ protocol writeup and confirmation evidence for every field.
|
|
|
13
13
|
from .crc import crc16, CRC16_TABLE
|
|
14
14
|
from .frame import (
|
|
15
15
|
build_frame, parse_frame, ParsedFrame,
|
|
16
|
-
encode_get_attribute, encode_set_attribute, decode_attribute_response, AttributeValue,
|
|
16
|
+
encode_get_attribute, encode_get_attributes_batch, encode_set_attribute, decode_attribute_response, AttributeValue,
|
|
17
17
|
OPGROUP_C2CI_REQUEST, OPGROUP_C2CI_CONFIRM,
|
|
18
18
|
OPGROUP_FSCI_REQUEST, OPGROUP_FSCI_CONFIRM,
|
|
19
19
|
OPGROUP_QOTAP_REQUEST, OPGROUP_QOTAP_CONFIRM,
|
|
@@ -43,7 +43,7 @@ from .modifiers import (
|
|
|
43
43
|
)
|
|
44
44
|
from .power import ChannelPowerInfo, channel_percent_value
|
|
45
45
|
from .pump_status import PumpFlowRange, BatteryBackupInfo, BoostedBatteryInfo
|
|
46
|
-
from .device_status import GroupInfo, CalibrationInfo, AdvancedFeatures, VectraInfo, CoffeeInfo, MaintenanceInfo, DeviceTimeInfo, MeshPeer, Tank, LunarPhaseInfo
|
|
46
|
+
from .device_status import GroupInfo, CalibrationInfo, AdvancedFeatures, VectraInfo, CoffeeInfo, MaintenanceInfo, DeviceTimeInfo, MeshPeer, Tank, LunarPhaseInfo, MetadataSnapshot
|
|
47
47
|
from .coap import (
|
|
48
48
|
CoapRequestType, CoapMethod, CoapResponseCode, CoapResponse,
|
|
49
49
|
encode_coap_request, decode_coap_response, COAP_OPCODE, COAP_INDICATION_OPCODE,
|
|
@@ -56,7 +56,7 @@ from .mesh_address import (
|
|
|
56
56
|
)
|
|
57
57
|
from .relay import RelayedMobiusDevice
|
|
58
58
|
from .device import (
|
|
59
|
-
MobiusDevice, MobiusPump, LightIntensityResult,
|
|
59
|
+
MobiusDevice, MobiusPump, LightIntensityResult, LightPollResult, FullPollResult,
|
|
60
60
|
SERVICE_GENERAL, CHAR_RX_DATA, CHAR_RX_FINAL, CHAR_TX_DATA, CHAR_TX_FINAL,
|
|
61
61
|
)
|
|
62
62
|
from .discovery import (
|
|
@@ -64,8 +64,14 @@ from .discovery import (
|
|
|
64
64
|
dedupe_by_serial, find_device_by_serial, discover_mesh_peers_via_direct_connect,
|
|
65
65
|
discover_tank,
|
|
66
66
|
)
|
|
67
|
+
from .frame import SupportedAttribute
|
|
68
|
+
from .dump import (
|
|
69
|
+
dump_attributes, format_attribute_dump_json, parse_attribute_dump_json,
|
|
70
|
+
format_attribute_dump_text, parse_attribute_dump_text, parse_attribute_dump,
|
|
71
|
+
enrich_attribute_dump,
|
|
72
|
+
)
|
|
67
73
|
|
|
68
|
-
__version__ = "0.
|
|
74
|
+
__version__ = "0.7.0"
|
|
69
75
|
|
|
70
76
|
__all__ = [
|
|
71
77
|
"__version__",
|
|
@@ -73,7 +79,7 @@ __all__ = [
|
|
|
73
79
|
"crc16", "CRC16_TABLE",
|
|
74
80
|
# frame
|
|
75
81
|
"build_frame", "parse_frame", "ParsedFrame",
|
|
76
|
-
"encode_get_attribute", "encode_set_attribute", "decode_attribute_response", "AttributeValue",
|
|
82
|
+
"encode_get_attribute", "encode_get_attributes_batch", "encode_set_attribute", "decode_attribute_response", "AttributeValue",
|
|
77
83
|
"OPGROUP_C2CI_REQUEST", "OPGROUP_C2CI_CONFIRM",
|
|
78
84
|
"OPGROUP_FSCI_REQUEST", "OPGROUP_FSCI_CONFIRM",
|
|
79
85
|
"OPGROUP_QOTAP_REQUEST", "OPGROUP_QOTAP_CONFIRM",
|
|
@@ -103,7 +109,7 @@ __all__ = [
|
|
|
103
109
|
# pump_status
|
|
104
110
|
"PumpFlowRange", "BatteryBackupInfo", "BoostedBatteryInfo",
|
|
105
111
|
# device_status
|
|
106
|
-
"GroupInfo", "CalibrationInfo", "AdvancedFeatures", "VectraInfo", "CoffeeInfo", "MaintenanceInfo", "DeviceTimeInfo", "MeshPeer", "Tank", "LunarPhaseInfo",
|
|
112
|
+
"GroupInfo", "CalibrationInfo", "AdvancedFeatures", "VectraInfo", "CoffeeInfo", "MaintenanceInfo", "DeviceTimeInfo", "MeshPeer", "Tank", "LunarPhaseInfo", "MetadataSnapshot",
|
|
107
113
|
"CoapRequestType", "CoapMethod", "CoapResponseCode", "CoapResponse",
|
|
108
114
|
"encode_coap_request", "decode_coap_response", "COAP_OPCODE", "COAP_INDICATION_OPCODE",
|
|
109
115
|
"next_coap_token",
|
|
@@ -111,10 +117,13 @@ __all__ = [
|
|
|
111
117
|
"build_rloc_address", "extract_short_address", "is_short_address_derived",
|
|
112
118
|
"RelayedMobiusDevice",
|
|
113
119
|
# device
|
|
114
|
-
"MobiusDevice", "MobiusPump", "LightIntensityResult",
|
|
120
|
+
"MobiusDevice", "MobiusPump", "LightIntensityResult", "LightPollResult", "FullPollResult",
|
|
115
121
|
"SERVICE_GENERAL", "CHAR_RX_DATA", "CHAR_RX_FINAL", "CHAR_TX_DATA", "CHAR_TX_FINAL",
|
|
116
122
|
# discovery
|
|
117
123
|
"scan_for_mobius_devices", "scan_for_mobius_devices_with_info", "group_by_pan_id",
|
|
118
124
|
"dedupe_by_serial", "find_device_by_serial", "discover_mesh_peers_via_direct_connect",
|
|
119
125
|
"discover_tank",
|
|
126
|
+
"SupportedAttribute", "dump_attributes", "enrich_attribute_dump",
|
|
127
|
+
"format_attribute_dump_json", "parse_attribute_dump_json",
|
|
128
|
+
"format_attribute_dump_text", "parse_attribute_dump_text", "parse_attribute_dump",
|
|
120
129
|
]
|
mobius/cli.py
CHANGED
|
@@ -23,6 +23,7 @@ from .discovery import (
|
|
|
23
23
|
find_device_by_serial, discover_mesh_peers_via_direct_connect, discover_tank,
|
|
24
24
|
)
|
|
25
25
|
from .schedule import SchedulePoint
|
|
26
|
+
from .dump import dump_attributes, format_attribute_dump_json, parse_attribute_dump, enrich_attribute_dump
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def _format_flags(point) -> str:
|
|
@@ -373,12 +374,93 @@ async def _run(args) -> None:
|
|
|
373
374
|
print(f" reboot() FAILED: {e}")
|
|
374
375
|
|
|
375
376
|
if args.set_time_to_now:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
377
|
+
# Same relay-aware targeting as --reboot above -- see
|
|
378
|
+
# that block's own comment for why this deliberately
|
|
379
|
+
# doesn't fall back to the directly-connected device
|
|
380
|
+
# if relay resolution failed.
|
|
381
|
+
if args.relay_target:
|
|
382
|
+
if relayed is None:
|
|
383
|
+
print(f"\n=== set_time_to_now() on {args.relay_target!r} (via relay) -- "
|
|
384
|
+
f"SKIPPED: relay target could not be resolved above ===")
|
|
385
|
+
time_target = None
|
|
386
|
+
else:
|
|
387
|
+
time_target_label = f"{args.relay_target!r} (via relay through {args.by_serial!r})"
|
|
388
|
+
time_target = relayed
|
|
389
|
+
else:
|
|
390
|
+
time_target_label = f"{args.by_serial!r} (direct)"
|
|
391
|
+
time_target = mdevice
|
|
392
|
+
|
|
393
|
+
if time_target is not None:
|
|
394
|
+
print(f"\n=== set_time_to_now() on {time_target_label} (WRITE -- Epoch) ===")
|
|
395
|
+
try:
|
|
396
|
+
await time_target.set_time_to_now()
|
|
397
|
+
print(f" set_time_to_now() on {time_target_label} succeeded.")
|
|
398
|
+
except Exception as e:
|
|
399
|
+
print(f" set_time_to_now() FAILED: {e}")
|
|
400
|
+
|
|
401
|
+
if args.dump_attributes:
|
|
402
|
+
# Same relay-aware targeting as --reboot above.
|
|
403
|
+
if args.relay_target:
|
|
404
|
+
if relayed is None:
|
|
405
|
+
print(f"\n=== dump_attributes() on {args.relay_target!r} (via relay) -- "
|
|
406
|
+
f"SKIPPED: relay target could not be resolved above ===")
|
|
407
|
+
dump_target = None
|
|
408
|
+
else:
|
|
409
|
+
dump_target_label = f"{args.relay_target!r} (via relay through {args.by_serial!r})"
|
|
410
|
+
dump_target = relayed
|
|
411
|
+
else:
|
|
412
|
+
dump_target_label = f"{args.by_serial!r} (direct)"
|
|
413
|
+
dump_target = mdevice
|
|
414
|
+
|
|
415
|
+
if dump_target is not None:
|
|
416
|
+
print(f"\n=== dump_attributes() on {dump_target_label} -- "
|
|
417
|
+
f"writing to {args.dump_attributes!r} ===")
|
|
418
|
+
print(" Discovering supported attributes...")
|
|
419
|
+
try:
|
|
420
|
+
values = await dump_attributes(dump_target)
|
|
421
|
+
text = format_attribute_dump_json(values)
|
|
422
|
+
with open(args.dump_attributes, "w") as f:
|
|
423
|
+
f.write(text)
|
|
424
|
+
print(f" Wrote {len(values)} attribute(s) to {args.dump_attributes!r}.")
|
|
425
|
+
except Exception as e:
|
|
426
|
+
print(f" dump_attributes() FAILED: {e}")
|
|
427
|
+
|
|
428
|
+
requested_features = {}
|
|
429
|
+
if args.set_local_control_enabled is not None:
|
|
430
|
+
requested_features["local_control_enabled"] = args.set_local_control_enabled == "true"
|
|
431
|
+
if args.set_auto_dim_timeout is not None:
|
|
432
|
+
requested_features["auto_dim_timeout"] = args.set_auto_dim_timeout
|
|
433
|
+
if args.set_max_fan_speed is not None:
|
|
434
|
+
requested_features["max_fan_speed"] = args.set_max_fan_speed
|
|
435
|
+
if args.set_fan_shutdown_enabled is not None:
|
|
436
|
+
requested_features["fan_shutdown_enabled"] = args.set_fan_shutdown_enabled == "true"
|
|
437
|
+
|
|
438
|
+
if requested_features:
|
|
439
|
+
# Same relay-aware targeting as --reboot above.
|
|
440
|
+
if args.relay_target:
|
|
441
|
+
if relayed is None:
|
|
442
|
+
print(f"\n=== set_advanced_features() on {args.relay_target!r} (via relay) -- "
|
|
443
|
+
f"SKIPPED: relay target could not be resolved above ===")
|
|
444
|
+
features_target = None
|
|
445
|
+
else:
|
|
446
|
+
features_target_label = f"{args.relay_target!r} (via relay through {args.by_serial!r})"
|
|
447
|
+
features_target = relayed
|
|
448
|
+
else:
|
|
449
|
+
features_target_label = f"{args.by_serial!r} (direct)"
|
|
450
|
+
features_target = mdevice
|
|
451
|
+
|
|
452
|
+
if features_target is not None:
|
|
453
|
+
print(f"\n=== set_advanced_features({', '.join(f'{k}={v!r}' for k, v in requested_features.items())}) "
|
|
454
|
+
f"on {features_target_label} (WRITE) ===")
|
|
455
|
+
try:
|
|
456
|
+
result = await features_target.set_advanced_features(**requested_features)
|
|
457
|
+
for field, error in result.items():
|
|
458
|
+
if error is None:
|
|
459
|
+
print(f" {field}: succeeded.")
|
|
460
|
+
else:
|
|
461
|
+
print(f" {field}: FAILED: {error}")
|
|
462
|
+
except Exception as e:
|
|
463
|
+
print(f" set_advanced_features() FAILED entirely: {e}")
|
|
382
464
|
|
|
383
465
|
except Exception as e:
|
|
384
466
|
print(" failed to connect/read:", e)
|
|
@@ -620,9 +702,12 @@ def main() -> None:
|
|
|
620
702
|
parser.add_argument("--set-time-to-now", action="store_true",
|
|
621
703
|
help="Only usable with --by-serial. A WRITE -- use on hardware you're "
|
|
622
704
|
"prepared to have its clock changed on. Calls set_time_to_now() "
|
|
623
|
-
"on the
|
|
705
|
+
"on the target device (Epoch, reserved-byte group=1 "
|
|
624
706
|
"-- required for the write to be accepted at all, confirmed "
|
|
625
|
-
"against real hardware).
|
|
707
|
+
"against real hardware). If --relay-target is also given, sets "
|
|
708
|
+
"the time on THAT device via relay through the connected one "
|
|
709
|
+
"instead, same targeting as --reboot below. See that method's own "
|
|
710
|
+
"docstring, and "
|
|
626
711
|
"documentation/09-thread-coap-relay.md, for what's confirmed "
|
|
627
712
|
"about this vs the rest of the mesh.")
|
|
628
713
|
parser.add_argument("--reboot", action="store_true",
|
|
@@ -645,7 +730,78 @@ def main() -> None:
|
|
|
645
730
|
"once in that case, so you can later keep only one connection "
|
|
646
731
|
"open and relay to the rest. See "
|
|
647
732
|
"documentation/09-thread-coap-relay.md.")
|
|
733
|
+
parser.add_argument("--dump-attributes", default=None, metavar="FILE",
|
|
734
|
+
help="Only usable with --by-serial. Discovers every attribute the "
|
|
735
|
+
"target device reports supporting -- the relay target if "
|
|
736
|
+
"--relay-target is also given, otherwise the directly-connected "
|
|
737
|
+
"device itself (via "
|
|
738
|
+
"get_supported_attributes(), confirmed against real hardware), "
|
|
739
|
+
"reads every one, and writes the result to FILE as enriched JSON "
|
|
740
|
+
"-- resolved attribute names and decoded values where this "
|
|
741
|
+
"library has a confirmed decoder, generic best-effort numeric "
|
|
742
|
+
"interpretations otherwise. NOT the app's own plain-text format "
|
|
743
|
+
"(see format_attribute_dump_text() if you specifically need "
|
|
744
|
+
"that, e.g. to compare against a dump shared directly from the "
|
|
745
|
+
"app). Read-only -- doesn't write anything to the device itself.")
|
|
746
|
+
parser.add_argument("--set-local-control-enabled", default=None, choices=["true", "false"],
|
|
747
|
+
help="Only usable with --by-serial. A WRITE -- use on hardware you're "
|
|
748
|
+
"prepared to have this setting changed on. VorTech-relevant (the "
|
|
749
|
+
"app's own \"Local Control\" setting) -- see set_advanced_features() "
|
|
750
|
+
"for the confirmed wire format. Combinable with the other "
|
|
751
|
+
"--set-*-enabled/--set-auto-dim-timeout/--set-max-fan-speed flags "
|
|
752
|
+
"below -- all requested fields are sent in one call, whichever of "
|
|
753
|
+
"them the target device actually supports.")
|
|
754
|
+
parser.add_argument("--set-auto-dim-timeout", default=None, type=int, metavar="SECONDS",
|
|
755
|
+
help="Only usable with --by-serial. A WRITE. VorTech-relevant (the "
|
|
756
|
+
"app's own \"Led Auto Dim\" setting). The app's own UI only ever "
|
|
757
|
+
"offers 0/30/60/300/600/1800/3600 (0 = \"Always On\") as a fixed "
|
|
758
|
+
"chooser -- this library does NOT restrict to those, since that's "
|
|
759
|
+
"a UI-level choice, not a confirmed protocol-level one; validate "
|
|
760
|
+
"against that list yourself first if you want to match the app's "
|
|
761
|
+
"own restricted choices exactly.")
|
|
762
|
+
parser.add_argument("--set-max-fan-speed", default=None, type=float, metavar="PERCENT",
|
|
763
|
+
help="Only usable with --by-serial. A WRITE. Radion-relevant (the "
|
|
764
|
+
"app's own \"Max Fan Speed\" setting), 0-100 percent -- 100 "
|
|
765
|
+
"encodes as the raw attribute's own -1 (0xFFFF) sentinel, "
|
|
766
|
+
"confirmed matching the app's own \"100%%\" preset exactly (see "
|
|
767
|
+
"set_advanced_features()'s own docstring). Same non-restriction "
|
|
768
|
+
"note as --set-auto-dim-timeout above -- the app's own presets "
|
|
769
|
+
"are 10/20/40/60/80/100, not enforced here.")
|
|
770
|
+
parser.add_argument("--set-fan-shutdown-enabled", default=None, choices=["true", "false"],
|
|
771
|
+
help="Only usable with --by-serial. A WRITE. Radion-relevant (the "
|
|
772
|
+
"app's own \"Fan Shutdown\" setting).")
|
|
773
|
+
parser.add_argument("--parse-dump", default=None, metavar="FILE",
|
|
774
|
+
help="Standalone -- no device connection, no scanning, ignores every "
|
|
775
|
+
"other flag. Parses an existing attribute-dump file (this "
|
|
776
|
+
"library's own JSON from --dump-attributes above, or the app's "
|
|
777
|
+
"own plain-text format -- auto-detected either way) and prints "
|
|
778
|
+
"its contents enriched: resolved attribute names and decoded "
|
|
779
|
+
"values where known, for offline inspection/debugging without "
|
|
780
|
+
"the device present.")
|
|
648
781
|
args = parser.parse_args()
|
|
782
|
+
|
|
783
|
+
if args.parse_dump:
|
|
784
|
+
# Standalone: no device, no event loop needed at all -- deliberately
|
|
785
|
+
# handled here, before asyncio.run(_run(args)), rather than as
|
|
786
|
+
# another branch inside _run(), since every other flag assumes a
|
|
787
|
+
# real connection is at least attempted.
|
|
788
|
+
with open(args.parse_dump) as f:
|
|
789
|
+
text = f.read()
|
|
790
|
+
values = parse_attribute_dump(text)
|
|
791
|
+
enriched = enrich_attribute_dump(values)
|
|
792
|
+
print(f"=== Parsed {len(enriched)} attribute(s) from {args.parse_dump!r} ===")
|
|
793
|
+
for entry in enriched:
|
|
794
|
+
label = f"{entry['attr_id']} ({entry['name']})" if entry["name"] else str(entry["attr_id"])
|
|
795
|
+
print(f"{label}: {entry['index']}")
|
|
796
|
+
for element in entry["elements"]:
|
|
797
|
+
if element["decoded"] is not None:
|
|
798
|
+
print(f"\t{element['decoded']} [raw: {element['raw_hex']}]")
|
|
799
|
+
else:
|
|
800
|
+
interp = ", ".join(f"{k}={v}" for k, v in element["interpretations"].items())
|
|
801
|
+
suffix = f" ({interp})" if interp else ""
|
|
802
|
+
print(f"\t{element['raw_hex']}{suffix}")
|
|
803
|
+
return
|
|
804
|
+
|
|
649
805
|
asyncio.run(_run(args))
|
|
650
806
|
|
|
651
807
|
|
mobius/coap.py
CHANGED
|
@@ -24,14 +24,14 @@ Get/Set-attribute opcodes this library already uses. Carried inside the
|
|
|
24
24
|
same outer FSCI frame (see mobius.frame) as everything else -- this is
|
|
25
25
|
not a separate transport, just a different C2CI opcode.
|
|
26
26
|
|
|
27
|
-
**Request** (`encode_coap_request()`): confirmed via
|
|
28
|
-
|
|
27
|
+
**Request** (`encode_coap_request()`): confirmed via reverse engineering
|
|
28
|
+
the app's own equivalent request-construction logic --
|
|
29
29
|
|
|
30
30
|
IPv6(16 bytes, REVERSED) + token(4) + method(2) + payload_length(2)
|
|
31
31
|
+ request_type(1) + payload
|
|
32
32
|
|
|
33
|
-
**Response** (`decode_coap_response()`): confirmed via
|
|
34
|
-
|
|
33
|
+
**Response** (`decode_coap_response()`): confirmed via reverse
|
|
34
|
+
engineering the app's own equivalent response-construction logic --
|
|
35
35
|
|
|
36
36
|
IPv6(16 bytes, NOT reversed) + token(4) + response_code(2)
|
|
37
37
|
+ payload_length(2) + request_type(1) + payload
|
|
@@ -41,7 +41,8 @@ doesn't) is easy to get backwards -- confirmed directly from the
|
|
|
41
41
|
decompiled source, not a guess, so implemented exactly as traced.
|
|
42
42
|
|
|
43
43
|
**The payload is just an existing FSCI request/response's raw bytes.**
|
|
44
|
-
|
|
44
|
+
Confirmed via reverse engineering: the app's own CoAP request wraps an
|
|
45
|
+
existing FSCI request's own raw data directly
|
|
45
46
|
-- meaning none of this library's existing Get/Set attribute encoding
|
|
46
47
|
needs reimplementing. This module only handles the CoAP envelope; the
|
|
47
48
|
payload is built with mobius.frame's existing encode_get_attribute() /
|
|
@@ -70,8 +71,8 @@ lookups for these specific obscure constants). Rather than guess, this
|
|
|
70
71
|
module only defines the CONFIRMED response codes (all literal, or
|
|
71
72
|
independently cross-referenced) in CoapResponseCode -- an unrecognized
|
|
72
73
|
code decodes to a plain int, not a guessed enum member, matching the real
|
|
73
|
-
app's own graceful-fallback behavior
|
|
74
|
-
`coap_Empty`
|
|
74
|
+
app's own graceful-fallback behavior for an unrecognized response code
|
|
75
|
+
(falling back to `coap_Empty` rather than crashing).
|
|
75
76
|
`Content` (matching the real "2.05 Content" success response to a GET,
|
|
76
77
|
which we'll see constantly in practice) is the one value here confirmed
|
|
77
78
|
only by strong pattern inference, not direct source confirmation -- flagged
|
|
@@ -97,9 +98,9 @@ COAP_OPCODE = 25
|
|
|
97
98
|
# ack the app itself explicitly discards (see mobius.relay's module
|
|
98
99
|
# docstring for the full trace) -- opcode 26 is a genuinely distinct
|
|
99
100
|
# "indication," and matching it back to the request that triggered it is
|
|
100
|
-
# done purely by CoAP token (confirmed via reverse engineering:
|
|
101
|
-
#
|
|
102
|
-
# FSCI message ID.
|
|
101
|
+
# done purely by CoAP token (confirmed via reverse engineering: the
|
|
102
|
+
# request's own token must match the response's own token), never by
|
|
103
|
+
# the outer FSCI message ID.
|
|
103
104
|
COAP_INDICATION_OPCODE = 26
|
|
104
105
|
|
|
105
106
|
|
|
@@ -192,7 +193,8 @@ def encode_coap_request(
|
|
|
192
193
|
`target_address` must be exactly 16 bytes (a full IPv6 address -- see
|
|
193
194
|
mesh_local_address() for how to construct one for a specific device).
|
|
194
195
|
|
|
195
|
-
Confirmed wire format (
|
|
196
|
+
Confirmed wire format (via reverse engineering the app's own
|
|
197
|
+
equivalent request-construction logic):
|
|
196
198
|
IPv6(16, REVERSED) + token(4) + method(2) + payload_length(2)
|
|
197
199
|
+ request_type(1) + payload.
|
|
198
200
|
"""
|
|
@@ -213,9 +215,11 @@ def decode_coap_response(data: bytes) -> Optional[CoapResponse]:
|
|
|
213
215
|
"""
|
|
214
216
|
Parses a C2CI-opcode-25 response payload. Returns None if the data is
|
|
215
217
|
too short to be a valid CoAP response (confirmed minimum: 25 bytes
|
|
216
|
-
before any payload, matching
|
|
218
|
+
before any payload, matching the app's own equivalent validation
|
|
219
|
+
check).
|
|
217
220
|
|
|
218
|
-
Confirmed wire format (
|
|
221
|
+
Confirmed wire format (via reverse engineering the app's own
|
|
222
|
+
response-parsing constructor):
|
|
219
223
|
IPv6(16, NOT reversed) + token(4) + response_code(2) + payload_length(2)
|
|
220
224
|
+ request_type(1) + payload.
|
|
221
225
|
"""
|
|
@@ -234,7 +238,7 @@ def decode_coap_response(data: bytes) -> Optional[CoapResponse]:
|
|
|
234
238
|
try:
|
|
235
239
|
request_type = CoapRequestType(data[24])
|
|
236
240
|
except ValueError:
|
|
237
|
-
request_type = CoapRequestType.Con # matches
|
|
241
|
+
request_type = CoapRequestType.Con # matches the app's own confirmed fallback behavior
|
|
238
242
|
|
|
239
243
|
payload = data[25:25 + payload_length]
|
|
240
244
|
|