python-mobius 0.1.4__tar.gz → 0.2.1__tar.gz
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.
- {python_mobius-0.1.4 → python_mobius-0.2.1}/CHANGELOG.md +227 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/PKG-INFO +1 -1
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/00-overview.md +1 -1
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/06-light-schedule.md +58 -55
- python_mobius-0.2.1/documentation/09-thread-coap-relay.md +214 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/10-known-gaps-and-open-questions.md +0 -11
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/11-device-status-attributes.md +57 -16
- python_mobius-0.2.1/documentation/12-device-identity-and-address-stability.md +105 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/pyproject.toml +1 -1
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/__init__.py +21 -5
- python_mobius-0.2.1/src/mobius/cli.py +393 -0
- python_mobius-0.2.1/src/mobius/coap.py +253 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/constants.py +17 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/device.py +377 -66
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/device_status.py +45 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/discovery.py +68 -0
- python_mobius-0.2.1/src/mobius/mesh_address.py +116 -0
- python_mobius-0.2.1/src/mobius/relay.py +319 -0
- python_mobius-0.2.1/tests/test_coap.py +191 -0
- python_mobius-0.2.1/tests/test_coap_indication_handling.py +160 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_device_summary.py +21 -7
- python_mobius-0.2.1/tests/test_device_time.py +125 -0
- python_mobius-0.2.1/tests/test_firmware_versions.py +228 -0
- python_mobius-0.2.1/tests/test_frame_sniffer.py +87 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_hyperdrive_device.py +20 -20
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_light_intensity_branching.py +16 -0
- python_mobius-0.2.1/tests/test_mesh_address.py +126 -0
- python_mobius-0.2.1/tests/test_mesh_discovery.py +235 -0
- python_mobius-0.2.1/tests/test_mesh_discovery_direct_connect.py +180 -0
- python_mobius-0.2.1/tests/test_relay.py +388 -0
- python_mobius-0.1.4/documentation/09-thread-coap-relay.md +0 -78
- python_mobius-0.1.4/documentation/12-device-identity-and-address-stability.md +0 -114
- python_mobius-0.1.4/src/mobius/cli.py +0 -125
- python_mobius-0.1.4/tests/test_firmware_versions.py +0 -141
- {python_mobius-0.1.4 → python_mobius-0.2.1}/.gitignore +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/LICENSE +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/README.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/03-attributes-and-opcodes.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/05-scenes.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/crc.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/frame.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/modifiers.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/power.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/src/mobius/schedule.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_device_status.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_frame.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_modifiers.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_power.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_pump_status.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.1}/tests/test_serial_identity.py +0 -0
|
@@ -1,5 +1,232 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- **Fixed a real bug found via real hardware testing: some "get all
|
|
6
|
+
elements" responses were silently truncated to their first block.**
|
|
7
|
+
`get_attribute_raw()` returned only the first matching
|
|
8
|
+
`(attrId, index, count, values)` block in a response and discarded any
|
|
9
|
+
others -- a Radion light showing 8 firmware components in the official
|
|
10
|
+
app came back as only 5 via `get_firmware_versions()`, because that
|
|
11
|
+
light's `FirmwareVersion` response genuinely splits across two
|
|
12
|
+
separate blocks (one covering `MainMicroOS`/`MainMicroBootloader`,
|
|
13
|
+
another covering the `QCA4020`-prefixed types), not one block covering
|
|
14
|
+
every element. Added `get_attribute_raw_all()`/`get_attribute_all()`
|
|
15
|
+
(`MobiusDevice`, with a matching `RelayedMobiusDevice` override --
|
|
16
|
+
without one, callers built on these would have silently fallen through
|
|
17
|
+
to `MobiusDevice`'s own non-relay implementation when called on a
|
|
18
|
+
relayed device) -- return every matching block instead of just the
|
|
19
|
+
first. `get_firmware_versions()`, `get_hardware_info()`,
|
|
20
|
+
`get_channel_power_info()`, and `get_supported_channels()` are now
|
|
21
|
+
built on these rather than the single-block versions, merging every
|
|
22
|
+
block's values, each interpreted using its own block's starting index.
|
|
23
|
+
5 new tests, including ones that specifically reproduce the two-block
|
|
24
|
+
scenario found on real hardware, both directly and through relay.
|
|
25
|
+
|
|
26
|
+
## 0.2.0
|
|
27
|
+
|
|
28
|
+
Thread/CoAP device relay -- see
|
|
29
|
+
[documentation/09-thread-coap-relay.md](documentation/09-thread-coap-relay.md)
|
|
30
|
+
for the full protocol trace and implementation history. The `coap-relay`
|
|
31
|
+
branch has been merged into main as part of this release.
|
|
32
|
+
|
|
33
|
+
- **Root cause of relayed reads returning empty data found and fixed,**
|
|
34
|
+
after the user provided the raw smali disassembly for
|
|
35
|
+
`BaseConnection.java` -- JADX's decompiler had given up specifically on
|
|
36
|
+
`handleIndication()`, the one method that held the answer, but the
|
|
37
|
+
underlying bytecode was still traceable. Confirmed precisely: the real
|
|
38
|
+
CoAP relay response arrives on a completely separate opcode (`0xDF`/
|
|
39
|
+
`26`, `COAP_INDICATION_OPCODE` -- not the request's `25`, whose confirm
|
|
40
|
+
is a bare, meaningless ack the app itself discards, confirmed
|
|
41
|
+
byte-for-byte via real hardware testing showing exactly 1 byte in every
|
|
42
|
+
relayed read's outer response), matched purely by CoAP token
|
|
43
|
+
(`lambda$getSentRequest$6`: `coapRequest.getToken() ==
|
|
44
|
+
coapResponse.getToken()`), never by the outer FSCI message ID. Added
|
|
45
|
+
`MobiusDevice._register_coap_token()`/`wait_for_coap_response()` -- a
|
|
46
|
+
token-keyed pending-response registry, entirely separate from the
|
|
47
|
+
existing message-ID-based `self._pending` mechanism -- wired into
|
|
48
|
+
`_on_rx_final()`. `RelayedMobiusDevice._relay()` was rewritten around
|
|
49
|
+
this: registers the token before sending (eliminating any race against
|
|
50
|
+
the response arriving first), sends without waiting for the
|
|
51
|
+
meaningless confirm, and awaits the token-matched indication instead.
|
|
52
|
+
`test_relay.py` rewritten entirely for the new architecture (14 tests);
|
|
53
|
+
new dedicated `test_coap_indication_handling.py` for the token-registry
|
|
54
|
+
mechanism itself (9 tests). Verified end-to-end with a full mocked
|
|
55
|
+
round-trip (both the discarded ack and the real indication arriving,
|
|
56
|
+
in that order) before asking for another real hardware run -- **this is
|
|
57
|
+
the next real test.**
|
|
58
|
+
|
|
59
|
+
- **Real hardware testing confirmed Thread IS active on regular
|
|
60
|
+
VorTech/Radion devices** (a genuine, positive result -- `MeshLocalAddresses`
|
|
61
|
+
reads back a valid, non-zero address), **but the three-array peer
|
|
62
|
+
discovery mechanism (`discover_mesh_peers()`) is Cowboy-hub-specific**
|
|
63
|
+
(`FsciStatus.UnsupportedAttribute` on hardware with no Cowboy hub,
|
|
64
|
+
even though that same device's own address reads back fine). Added
|
|
65
|
+
`MobiusDevice.get_own_mesh_address()` and `mobius.discovery.
|
|
66
|
+
discover_mesh_peers_via_direct_connect()` as the alternative --
|
|
67
|
+
confirmed via `PeripheralConnection.java` to be exactly how the app
|
|
68
|
+
itself always learns a device's address (automatic on every
|
|
69
|
+
connection, not Cowboy-specific). Added `mobius-scan --build-peer-map`
|
|
70
|
+
to exercise this from the CLI. `discover_mesh_peers()` remains in
|
|
71
|
+
place as a cheap, fail-soft first attempt in case a Cowboy hub IS
|
|
72
|
+
present, but is no longer the primary/recommended discovery path. 10
|
|
73
|
+
new tests.
|
|
74
|
+
|
|
75
|
+
- **Fixed a real bug found via real hardware testing: `--relay-target`
|
|
76
|
+
was never actually wired to the new Cowboy-hub-free discovery
|
|
77
|
+
mechanism.** After adding `discover_mesh_peers_via_direct_connect()`,
|
|
78
|
+
a real `--relay-target` attempt still failed with "not found among
|
|
79
|
+
discovered peers" -- not because relay itself failed, but because the
|
|
80
|
+
CLI's target lookup only ever checked the Cowboy-hub-specific peer
|
|
81
|
+
list. `--relay-target` now falls back to a brief direct connection to
|
|
82
|
+
the target itself (`find_device_by_serial()` +
|
|
83
|
+
`get_own_mesh_address()`) if the Cowboy-hub lookup doesn't find it.
|
|
84
|
+
|
|
85
|
+
- **Added `--debug-relay`** after the fixed `--relay-target` reached the
|
|
86
|
+
target but `get_device_summary()` came back entirely empty with no
|
|
87
|
+
visible error. Turned out every attribute fetch WAS raising an
|
|
88
|
+
exception internally (a malformed CoAP response) -- silently, since
|
|
89
|
+
`get_device_summary()`'s own per-field fail-soft handling was catching
|
|
90
|
+
and discarding them without trace. Prints raw bytes at every layer of
|
|
91
|
+
a relayed request/response so this stops being invisible. Verified
|
|
92
|
+
with a mocked round-trip that debug output itself is correct before
|
|
93
|
+
asking for another real hardware run.
|
|
94
|
+
|
|
95
|
+
- **Confirmed directly from source why the response comes back
|
|
96
|
+
malformed: the immediate confirm to a relay request is explicitly
|
|
97
|
+
discarded by the app itself, on purpose.** `BaseConnection.java`'s
|
|
98
|
+
`handleBytes()` matches `opGroup==0xDF && opCode==25` unconditionally
|
|
99
|
+
and logs it only -- before the dispatcher ever checks whether it
|
|
100
|
+
resolves a pending request. Real hardware testing independently
|
|
101
|
+
confirmed this byte-for-byte: every relayed read's outer response is
|
|
102
|
+
exactly 1 byte (`0x00`), not a truncated response but the genuine
|
|
103
|
+
complete confirm, thrown away by the app too. The real response, if it
|
|
104
|
+
arrives at all, comes through a separate "indication" handled by
|
|
105
|
+
`handleIndication()` -- a method JADX couldn't decompile, a genuine
|
|
106
|
+
limit of what source tracing alone can determine here. Added
|
|
107
|
+
`MobiusDevice.on_any_frame` (fires for every parsed incoming frame,
|
|
108
|
+
not just ones resolving a pending request) and extended
|
|
109
|
+
`--debug-relay` to watch for 2 seconds after the initial confirm, to
|
|
110
|
+
find out empirically whether a second frame actually arrives. 4 new
|
|
111
|
+
tests.
|
|
112
|
+
|
|
113
|
+
- **Added `mobius.coap`**: CoAP (RFC 7252) protocol primitives --
|
|
114
|
+
`CoapRequestType`/`CoapMethod`/`CoapResponseCode` enums,
|
|
115
|
+
`encode_coap_request()`/`decode_coap_response()`. Pure functions, no
|
|
116
|
+
hardware dependency. Confirmed wire format traced directly from the
|
|
117
|
+
decompiled app (`Coap.java`/`CoapRequest.java`/`CoapResponse.java`) --
|
|
118
|
+
see the module's docstring for exactly which enum values are
|
|
119
|
+
confirmed-literal, cross-referenced from elsewhere in this project, or
|
|
120
|
+
inferred-only (deliberately not guessing at unconfirmed ones -- an
|
|
121
|
+
unrecognized response code decodes to a plain int, matching the real
|
|
122
|
+
app's own graceful fallback behavior). 15 new tests, including
|
|
123
|
+
specific coverage of the asymmetric IPv6 byte-reversal between
|
|
124
|
+
requests and responses, which is easy to get backwards.
|
|
125
|
+
|
|
126
|
+
- **Added `mobius.mesh_address`**: Thread mesh-local RLOC IPv6 address
|
|
127
|
+
construction (`build_rloc_address()`/`extract_short_address()`/
|
|
128
|
+
`mesh_local_prefix_from_own_address()`/`is_valid_mesh_address()`/
|
|
129
|
+
`is_short_address_derived()`). Pure functions, no hardware dependency.
|
|
130
|
+
Includes a genuinely surprising, verified-not-assumed finding: the
|
|
131
|
+
short-address portion is little-endian (confirmed via
|
|
132
|
+
`ByteUtilities.getShort()`'s explicit `ByteOrder.LITTLE_ENDIAN`), the
|
|
133
|
+
opposite of IPv6's own conventional big-endian byte order -- easy to
|
|
134
|
+
get backwards by assuming the IPv6 standard's own convention applies
|
|
135
|
+
here too. 15 new tests.
|
|
136
|
+
|
|
137
|
+
- **Added `MobiusDevice.discover_mesh_peers()`**: fetches
|
|
138
|
+
`MeshLocalAddresses` (1005) + the three parallel `ShortAddressArray`/
|
|
139
|
+
`SerialNumberArray`/`DeviceModelArray` attributes (3700/3701/3702)
|
|
140
|
+
from a connected device, returning every other device it knows about
|
|
141
|
+
over the Thread mesh as a list of new `MeshPeer` dataclasses (serial,
|
|
142
|
+
model, short address, ready-to-use IPv6). Confirms `SerialNumberArray`'s
|
|
143
|
+
per-element format matches the existing 14-byte ASCII serial format
|
|
144
|
+
already used elsewhere in this library -- no new parsing logic needed.
|
|
145
|
+
Filters out cleared/empty slots (the literal sentinel string
|
|
146
|
+
`"00000000000000"`, confirmed via `CowboyNetworkProcess.java`'s own
|
|
147
|
+
cleanup logic). Fails soft to an empty list for unsupported devices or
|
|
148
|
+
ones not currently part of an active Thread network, matching this
|
|
149
|
+
library's usual convention. Also recorded (but not yet used) a fourth
|
|
150
|
+
parallel array, `BladeBleAddressArray` (3714, each peer's real BLE MAC
|
|
151
|
+
address) -- found alongside the other three but not needed for CoAP
|
|
152
|
+
relay itself. 7 new tests.
|
|
153
|
+
|
|
154
|
+
- **Added `RelayedMobiusDevice`** (`mobius.relay`): a `MobiusDevice`
|
|
155
|
+
subclass that overrides `get_attribute()`/`get_attribute_raw()`/
|
|
156
|
+
`set_attribute()` to route through a gateway device via CoAP instead of
|
|
157
|
+
owning its own BLE connection. Every other inherited method
|
|
158
|
+
(`get_device_summary()`, `get_current_light_intensities()`, etc.) works
|
|
159
|
+
transparently through relay, confirmed with a test that specifically
|
|
160
|
+
exercises an inherited high-level method rather than just the
|
|
161
|
+
overridden primitives. Confirmed `request.getData(true)` builds a
|
|
162
|
+
complete self-contained FSCI frame (byte-for-byte identical in
|
|
163
|
+
structure to this library's own `build_frame()`), resolving an
|
|
164
|
+
ambiguity flagged in an earlier commit -- relaying a request needed no
|
|
165
|
+
new encoding path, just wrapping the same frame a direct request would
|
|
166
|
+
use. Also confirmed, directly from the one real call site in the
|
|
167
|
+
decompile, a subtle and easy-to-get-wrong detail: the outer CoAP method
|
|
168
|
+
is always `GET`, even for a relayed Set -- the actual operation is
|
|
169
|
+
determined by the inner FSCI opcode, not the CoAP transport layer. 13
|
|
170
|
+
new tests.
|
|
171
|
+
|
|
172
|
+
- **Added `mobius-scan --dump-mesh-peers` / `--relay-target SERIAL`**
|
|
173
|
+
(both usable alongside `--by-serial`): prints every peer a connected
|
|
174
|
+
device knows about over the Thread mesh, and/or relays a full
|
|
175
|
+
`get_device_summary()` read to a specified target through it. This is
|
|
176
|
+
the last of the 5 planned pieces for this feature -- **implementation
|
|
177
|
+
is now complete, but not yet validated against real hardware.** Run a
|
|
178
|
+
separate direct `--by-serial <target>` command to confirm a relayed
|
|
179
|
+
read actually matches a direct one; this is also how the biggest open
|
|
180
|
+
question (whether these devices run Thread at all) finally gets
|
|
181
|
+
answered.
|
|
182
|
+
|
|
183
|
+
- **Added a verbose, step-by-step mesh discovery diagnostic
|
|
184
|
+
(`_debug_mesh_discovery()`)** after real hardware testing showed every
|
|
185
|
+
device returning "No peers found" with no further detail.
|
|
186
|
+
`discover_mesh_peers()`'s fail-soft-to-empty-list behavior is correct
|
|
187
|
+
for production/library use, but collapses several genuinely different
|
|
188
|
+
failure modes into the same result -- device doesn't support Thread at
|
|
189
|
+
all, vs. supports it but isn't on an active network right now, vs.
|
|
190
|
+
the peer arrays came back empty for some other reason. `--dump-mesh-peers`
|
|
191
|
+
now surfaces each step's raw result or exception individually instead
|
|
192
|
+
of just the final empty-or-not outcome.
|
|
193
|
+
|
|
194
|
+
- **Added `get_device_time_info()`** (`Epoch`/`RTCTime`, attributes
|
|
195
|
+
201/219) -- device clock drift vs. local system time. Not speculative:
|
|
196
|
+
confirmed via the app's own "Time Difference" troubleshooting feature
|
|
197
|
+
(`TimeDifferenceFragment.java`), which checks these exact two attributes
|
|
198
|
+
the same way. Included automatically in `get_device_summary()`'s
|
|
199
|
+
`"device_time"` field. Important caveat documented: the app's own
|
|
200
|
+
schedule interpolation uses the phone/tank clock, not either of these,
|
|
201
|
+
so this doesn't directly explain a schedule-interpolation mismatch by
|
|
202
|
+
itself -- still worth checking as a general health signal. 5 new tests.
|
|
203
|
+
|
|
204
|
+
- **Added `mobius-scan --dump-schedule`**: prints each connected device's
|
|
205
|
+
full raw programmed schedule (every point's time, flags -- ACTIVE/NIGHT/
|
|
206
|
+
SUNRISE/SUNSET -- and per-channel/mode values), not just the
|
|
207
|
+
interpolated "current" value `get_device_summary()` already shows. For
|
|
208
|
+
lights, also prints `lunar_enabled`/`insolation_active`/
|
|
209
|
+
`schedule_intensity`/`acclimation` state. Combines with either the
|
|
210
|
+
normal scan or `--by-serial`. Added so a schedule's actual point
|
|
211
|
+
structure can be inspected directly -- needed to verify
|
|
212
|
+
`is_night_segment()`'s bracket-finding logic against real data, rather
|
|
213
|
+
than guessed at or transcribed by hand.
|
|
214
|
+
|
|
215
|
+
- **Added debugging diagnostics to `get_current_light_intensities()`**:
|
|
216
|
+
`.diagnostics` now also includes `is_night_segment`, `lunar_enabled`,
|
|
217
|
+
`scalar_source` (`"lunar"`/`"night_no_lunar"`/`"schedule_intensity"`),
|
|
218
|
+
and `scalar` (the actual final multiplier applied). Added after a real
|
|
219
|
+
discrepancy was reported between this library's computed value and the
|
|
220
|
+
app's own displayed value for a light at night (app showing lunar-
|
|
221
|
+
reduced moonlight intensities, this library showing the unreduced
|
|
222
|
+
schedule-intensity value instead) -- these fields exist specifically so
|
|
223
|
+
that class of mismatch can be diagnosed with real data (which branch was
|
|
224
|
+
taken, and why) rather than guessed at blindly. Also threaded into
|
|
225
|
+
`get_device_summary()` as `"light_diagnostics"`, so `mobius-scan`
|
|
226
|
+
surfaces this directly -- debugging this kind of mismatch belongs in the
|
|
227
|
+
library/CLI, not in a specific downstream integration. Existing tests
|
|
228
|
+
updated to assert on these fields directly.
|
|
229
|
+
|
|
3
230
|
## 0.1.4
|
|
4
231
|
|
|
5
232
|
- Added `MobiusDevice.is_connected` property -- lets callers check
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)
|
|
5
5
|
Project-URL: Homepage, https://code.r3pek.org/r3pek/python-mobius
|
|
6
6
|
Project-URL: Documentation, https://code.r3pek.org/r3pek/python-mobius/src/branch/main/documentation
|
|
@@ -49,7 +49,7 @@ Interface conventions, extended by EcoTech).
|
|
|
49
49
|
| [06-light-schedule.md](./06-light-schedule.md) | Light point-schedule format + client-side interpolation |
|
|
50
50
|
| [07-pump-schedule.md](./07-pump-schedule.md) | Pump primitive format, mode/parameter tables, block lookup |
|
|
51
51
|
| [08-manufacturer-data.md](./08-manufacturer-data.md) | BLE advertisement format (no-connection discovery) |
|
|
52
|
-
| [09-thread-coap-relay.md](./09-thread-coap-relay.md) |
|
|
52
|
+
| [09-thread-coap-relay.md](./09-thread-coap-relay.md) | Relaying requests to devices without a direct BLE connection, through another connected device |
|
|
53
53
|
| [10-known-gaps-and-open-questions.md](./10-known-gaps-and-open-questions.md) | Everything not yet confirmed or implemented |
|
|
54
54
|
| [11-device-status-attributes.md](./11-device-status-attributes.md) | Flow range, override mode, battery backup, group/sync, calibration (light-only), maintenance |
|
|
55
55
|
| [12-device-identity-and-address-stability.md](./12-device-identity-and-address-stability.md) | Why device identity is serial-number-based, not address-based |
|
|
@@ -8,18 +8,12 @@ There's no attribute you can `Get` that tells you what a light is currently
|
|
|
8
8
|
displaying. Instead, the app fetches the entire programmed point-schedule
|
|
9
9
|
and computes the current intensity **client-side**, by linearly
|
|
10
10
|
interpolating between the two points that bracket the current time, then
|
|
11
|
-
applying a schedule-level master intensity scalar
|
|
12
|
-
|
|
13
|
-
implement.
|
|
11
|
+
applying a schedule-level master intensity scalar (see below), plus
|
|
12
|
+
lunar-cycle and acclimation-ramp modifiers under specific conditions.
|
|
14
13
|
|
|
15
14
|
This library's `get_current_light_intensities()` replicates that
|
|
16
|
-
client-side computation
|
|
17
|
-
|
|
18
|
-
real fetched schedules" -- that validation only checked the per-channel
|
|
19
|
-
interpolation curve, not the schedule-level scalar below, which was
|
|
20
|
-
missing entirely at the time and silently returned values that didn't
|
|
21
|
-
match the app's real displayed numbers. Caught by direct comparison
|
|
22
|
-
against real app screenshots. Fixed; see below.
|
|
15
|
+
client-side computation, including the master intensity scalar and the
|
|
16
|
+
lunar/acclimation modifiers.
|
|
23
17
|
|
|
24
18
|
## Schedule-level master intensity (`Schedule1Intensity`, attribute 511)
|
|
25
19
|
|
|
@@ -34,13 +28,13 @@ regular per-channel value some models expose, not this).
|
|
|
34
28
|
`get_schedule_intensity()` fetches this (a plain `Get` on
|
|
35
29
|
`Schedule1Intensity`/`Schedule2Intensity`, no special indexing -- a short,
|
|
36
30
|
0-1000, default 500/50% per the app's own initialization code).
|
|
37
|
-
`get_current_light_intensities()`
|
|
31
|
+
`get_current_light_intensities()` applies it automatically.
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
Lunar-phase and acclimation-ramp reductions layer on top of this same
|
|
34
|
+
scalar under specific conditions (night + lunar phases enabled; an
|
|
35
|
+
active, incomplete acclimation period) -- see "Lunar phase and
|
|
36
|
+
acclimation ramp" below. "Insolation" (a separate, rarer modifier) is
|
|
37
|
+
not implemented.
|
|
44
38
|
|
|
45
39
|
## Wire format
|
|
46
40
|
|
|
@@ -76,10 +70,8 @@ Repeating triplets, one per channel the point defines:
|
|
|
76
70
|
VisualID (u8) + intensity (u16 LE, 0-1000 = 0.0-100.0%)
|
|
77
71
|
```
|
|
78
72
|
|
|
79
|
-
Confirmed
|
|
80
|
-
lights
|
|
81
|
-
parsing artifact, confirmed by the two lights having different serial
|
|
82
|
-
numbers).
|
|
73
|
+
Confirmed against a full 9-point schedule fetched live from real XR15
|
|
74
|
+
lights.
|
|
83
75
|
|
|
84
76
|
## Interpolation algorithm
|
|
85
77
|
|
|
@@ -94,32 +86,29 @@ Ported from `PointSchedule.getIntensitiesAtTime()`:
|
|
|
94
86
|
4. Channels absent from a point default to `0` (off).
|
|
95
87
|
|
|
96
88
|
Verified against a synthetic two-point schedule (exact endpoint values,
|
|
97
|
-
midpoint blending, and midnight-wraparound blending
|
|
98
|
-
|
|
99
|
-
fetched live, which
|
|
100
|
-
|
|
89
|
+
midpoint blending, and midnight-wraparound blending -- see
|
|
90
|
+
`tests/test_light_schedule.py`) and against a real 9-point schedule
|
|
91
|
+
fetched live, which produces a coherent daily light-intensity curve (dim
|
|
92
|
+
moonlight overnight → ramp to full blues/UV midday → wind back down).
|
|
101
93
|
|
|
102
94
|
## `SupportedColorChannels` (attribute 901)
|
|
103
95
|
|
|
104
|
-
A static list of `VisualID` values (channel identities) a given light has
|
|
105
|
-
this is capability metadata, not live state. Confirmed via `Get` with
|
|
106
|
-
`index=0, count=0xFFFF` against real hardware
|
|
107
|
-
the same 13-channel set (`Violet, CoolWhite, WarmWhite, Blue, UV, RoyalBlue,
|
|
108
|
-
Green, Red, MoonlightBlue, MoonlightWhite, Brightness, StormProbability,
|
|
109
|
-
CloudProbability`).
|
|
96
|
+
A static list of `VisualID` values (channel identities) a given light has
|
|
97
|
+
-- this is capability metadata, not live state. Confirmed via `Get` with
|
|
98
|
+
`index=0, count=0xFFFF` against real hardware.
|
|
110
99
|
|
|
111
100
|
## Lunar phase and acclimation ramp
|
|
112
101
|
|
|
113
|
-
Both
|
|
102
|
+
Both implemented, on top of the schedule-level master intensity above.
|
|
114
103
|
Ported from `LunarInfo.java`/`AcclimationInfo.java` in the decompiled app.
|
|
115
104
|
|
|
116
105
|
**Lunar phase** (`lunar_percent_reduction(date)`): a simplified moon-phase
|
|
117
106
|
model -- a Julian-Day-based calculation (`julian_day_from_date()`, the
|
|
118
107
|
standard Fliegel-Van Flandern conversion) feeding a 16-entry lookup table
|
|
119
108
|
(`LUNAR_CYCLE_SCALERS`), returning `0.0` at new moon ramping to `1.0` at
|
|
120
|
-
full moon. Verified against
|
|
121
|
-
dates (full moon Jan 3 2026, new moon Jan 18 2026) -- the ported
|
|
122
|
-
matches exactly.
|
|
109
|
+
full moon. Verified against independently-confirmed real 2026 moon phase
|
|
110
|
+
dates (full moon Jan 3 2026, new moon Jan 18 2026) -- the ported
|
|
111
|
+
algorithm matches exactly.
|
|
123
112
|
|
|
124
113
|
This replaces the normal schedule-intensity scalar specifically during the
|
|
125
114
|
**dusk-to-night segment** of the schedule (see `is_night_segment()` below),
|
|
@@ -129,10 +118,10 @@ matching the app's own fallback.
|
|
|
129
118
|
|
|
130
119
|
**`is_night_segment()`**: ported from the boolean half of
|
|
131
120
|
`PointSchedule.getIntensitiesAtTime()`'s return value. This is *not*
|
|
132
|
-
simply "is it night" --
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
121
|
+
simply "is it night" -- it's specifically true for the segment whose
|
|
122
|
+
start point is NOT flagged `NIGHT` or `SUNRISE` and whose end point IS
|
|
123
|
+
flagged `NIGHT` (the dusk-transition segment), or when the query time
|
|
124
|
+
exactly matches a `NIGHT`-flagged point.
|
|
136
125
|
|
|
137
126
|
**Acclimation ramp** (`AcclimationInfo`): a linear intensity ramp from
|
|
138
127
|
`start_intensity` (0-1000 permille) up to full (1000) over `period_days`,
|
|
@@ -147,8 +136,8 @@ automatically -- no separate calls needed for the common case.
|
|
|
147
136
|
## Insolation detection (flagged, not implemented)
|
|
148
137
|
|
|
149
138
|
Insolation itself is not implemented (see below), but
|
|
150
|
-
`get_current_light_intensities()`
|
|
151
|
-
and
|
|
139
|
+
`get_current_light_intensities()` cheaply detects whether it's active
|
|
140
|
+
and flags it, via `LightIntensityResult` -- the return value is a real
|
|
152
141
|
dict (fully backward compatible with code that indexes/iterates it like a
|
|
153
142
|
plain dict), with an added `.diagnostics` attribute:
|
|
154
143
|
|
|
@@ -165,6 +154,27 @@ wrong-but-present value is often more useful than nothing, and the
|
|
|
165
154
|
severity depends on how different the real Insolation-derived schedule is
|
|
166
155
|
from the manually-programmed one on a given day.
|
|
167
156
|
|
|
157
|
+
## Diagnostics: which branch/scalar was actually used
|
|
158
|
+
|
|
159
|
+
`.diagnostics` also includes `"is_night_segment"`, `"lunar_enabled"`,
|
|
160
|
+
`"scalar_source"` (one of `"lunar"`/`"night_no_lunar"`/
|
|
161
|
+
`"schedule_intensity"`), and `"scalar"` (the actual final multiplier
|
|
162
|
+
applied) -- useful for telling apart which branch/scalar produced a given
|
|
163
|
+
value, e.g. when a computed value doesn't match the app's own displayed
|
|
164
|
+
one and it isn't obvious whether `is_night_segment()` misjudged a
|
|
165
|
+
schedule's point structure or `get_lunar_enabled()` read incorrectly:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
result = await device.get_current_light_intensities()
|
|
169
|
+
print(result.diagnostics)
|
|
170
|
+
# {'insolation_active': False, 'is_night_segment': True,
|
|
171
|
+
# 'lunar_enabled': True, 'scalar_source': 'lunar', 'scalar': 0.35}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`lunar_enabled` is `None` (not `False`) when `is_night_segment` is
|
|
175
|
+
`False`, since it isn't checked/relevant outside the night segment --
|
|
176
|
+
don't read a `None` here as "lunar phases are off."
|
|
177
|
+
|
|
168
178
|
## Hyperdrive (boosted channels above 100%)
|
|
169
179
|
|
|
170
180
|
Some light channels can be driven above their normally-rated power
|
|
@@ -174,26 +184,19 @@ intensity values above `1000` permille (100%) on the wire, up to `2000`
|
|
|
174
184
|
percentage is wrong once it's above 1000 -- the true percentage depends on
|
|
175
185
|
how much headroom that specific channel actually has.
|
|
176
186
|
|
|
177
|
-
`
|
|
178
|
-
underneath it) already handled boosted raw values correctly all along --
|
|
179
|
-
nothing was capped or clamped, so the interpolation itself was never
|
|
180
|
-
wrong. What was missing was converting a boosted raw value to an accurate
|
|
181
|
-
*percentage* for display.
|
|
182
|
-
|
|
183
|
-
`get_current_light_percentages()` is the corrected alternative: it fetches
|
|
187
|
+
`get_current_light_percentages()` accounts for this: it fetches
|
|
184
188
|
`MaxPower`/`NormalPower` (`get_channel_power_info()`, attributes `1504`/
|
|
185
189
|
`1513`) and applies `channel_percent_value()` (`mobius.power`, ported from
|
|
186
|
-
`LightPowerInfo.getPercentValue()`) per channel. For the
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
channels
|
|
191
|
-
`.diagnostics["hyperdrive_channels"]`.
|
|
190
|
+
`LightPowerInfo.getPercentValue()`) per channel. For the common case --
|
|
191
|
+
non-hyperdrive channels/devices, i.e. any raw value at or below 1000 --
|
|
192
|
+
this gives the same results as a simple `raw/10`; the extra machinery
|
|
193
|
+
only changes anything for genuinely boosted channels. It also flags which
|
|
194
|
+
channels are currently boosted via `.diagnostics["hyperdrive_channels"]`.
|
|
192
195
|
|
|
193
196
|
This costs extra BLE round trips beyond `get_current_light_intensities()`
|
|
194
197
|
alone (two more `Get`s for `MaxPower`/`NormalPower`). Use
|
|
195
|
-
`get_current_light_intensities()` directly if
|
|
196
|
-
|
|
198
|
+
`get_current_light_intensities()` directly if hyperdrive accuracy isn't
|
|
199
|
+
needed, or to minimize round trips.
|
|
197
200
|
|
|
198
201
|
**Not replicated**: `LightPowerInfo`'s `gm8b4()` lookup table -- a
|
|
199
202
|
hardcoded wattage override for a handful of specific AquaIllumination
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Thread/CoAP device relay
|
|
2
|
+
|
|
3
|
+
Every device this library talks to normally needs its own direct,
|
|
4
|
+
persistent BLE connection. The official app doesn't work this way -- it
|
|
5
|
+
connects to just one or a few devices and reaches the rest through them.
|
|
6
|
+
If something else (this library, a downstream integration, or another
|
|
7
|
+
instance of the app) is already holding direct BLE connections to every
|
|
8
|
+
device, the app can end up with nothing to connect to at all. Relay lets
|
|
9
|
+
this library behave the same way the app does: connect to one device,
|
|
10
|
+
reach the others through it.
|
|
11
|
+
|
|
12
|
+
`mobius.relay.RelayedMobiusDevice` implements this. Every method on
|
|
13
|
+
`MobiusDevice` (`get_device_summary()`, `get_current_light_intensities()`,
|
|
14
|
+
everything) works transparently through it, since all of them are built
|
|
15
|
+
in terms of `get_attribute()`/`get_attribute_raw()`/`set_attribute()` --
|
|
16
|
+
the three methods `RelayedMobiusDevice` overrides.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
Devices form a Thread mesh network (802.15.4 radio, separate hardware
|
|
21
|
+
from BLE) identified by a `pan_id` (see
|
|
22
|
+
[08-manufacturer-data.md](./08-manufacturer-data.md)). The Thread
|
|
23
|
+
border-router function lives on the connected device itself, not on the
|
|
24
|
+
phone -- a phone (or a laptop) never needs its own 802.15.4 radio, only
|
|
25
|
+
BLE to reach one mesh member, which does the Thread-side forwarding
|
|
26
|
+
using its own onboard hardware.
|
|
27
|
+
|
|
28
|
+
Routing decision, from `Comm.java`'s `sendRequest()`:
|
|
29
|
+
|
|
30
|
+
```java
|
|
31
|
+
if (commDevice.equals(request.getTarget())) {
|
|
32
|
+
// direct: send over the existing BLE connection as-is
|
|
33
|
+
} else if (iPv6 != null) {
|
|
34
|
+
// relay: wrap in CoAP, address it to the target's mesh IPv6,
|
|
35
|
+
// send through commDevice (the connected gateway)
|
|
36
|
+
} else {
|
|
37
|
+
// no known address for the target -- fail
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CoAP wire format
|
|
42
|
+
|
|
43
|
+
Reuses the same C2CI envelope described in
|
|
44
|
+
[02-framing-and-crc.md](./02-framing-and-crc.md), with two new opcodes
|
|
45
|
+
alongside the existing Get(`0x17`)/Set(`0x18`) attribute opcodes:
|
|
46
|
+
|
|
47
|
+
- **`25`** (`mobius.coap.COAP_OPCODE`) -- the request opcode. Its
|
|
48
|
+
confirm is a bare, meaningless 1-byte ack (`Coap.RequestType`/etc. are
|
|
49
|
+
set, but the payload carries no real data) -- the app itself
|
|
50
|
+
unconditionally discards it in `BaseConnection.java`'s `handleBytes()`
|
|
51
|
+
before ever checking whether it resolves a pending request. This
|
|
52
|
+
library sends on this opcode without waiting for its confirm
|
|
53
|
+
(`wait_response=False`).
|
|
54
|
+
- **`26`** (`mobius.coap.COAP_INDICATION_OPCODE`) -- where the real
|
|
55
|
+
response actually arrives, as a separate, asynchronous message
|
|
56
|
+
("indication"). Matched to its originating request purely by CoAP
|
|
57
|
+
token, from `BaseConnection.java`'s `lambda$getSentRequest$6`:
|
|
58
|
+
`coapRequest.getToken() == coapResponse.getToken()`. The outer FSCI
|
|
59
|
+
message ID is never used for this correlation -- it only has meaning
|
|
60
|
+
for the single BLE hop to the gateway, not across a full Thread mesh
|
|
61
|
+
round-trip.
|
|
62
|
+
|
|
63
|
+
**Request** (`encode_coap_request()`) -- `CoapRequest`'s constructor:
|
|
64
|
+
```
|
|
65
|
+
IPv6(16 bytes, REVERSED) + token(4, LE) + method(2, LE)
|
|
66
|
+
+ payload_length(2, LE) + request_type(1) + payload
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Response** (`decode_coap_response()`) -- `CoapResponse`'s constructor:
|
|
70
|
+
```
|
|
71
|
+
IPv6(16 bytes, NOT reversed) + token(4, LE) + response_code(2, LE)
|
|
72
|
+
+ payload_length(2, LE) + request_type(1) + payload
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The IPv6 reversal is asymmetric: requests reverse the address, responses
|
|
76
|
+
don't. Easy to get backwards -- `mobius.coap`'s test suite specifically
|
|
77
|
+
checks this with a deliberately non-palindromic test address, since a
|
|
78
|
+
palindromic one would still pass even with the reversal backwards or
|
|
79
|
+
missing.
|
|
80
|
+
|
|
81
|
+
**The payload is a complete, self-contained FSCI frame** -- byte-for-byte
|
|
82
|
+
identical in structure to what this library's own `build_frame()`
|
|
83
|
+
produces for a direct request (`opGroup + opCode + messageId(2) +
|
|
84
|
+
reserved(1) + dataLength(2) + data + CRC16(2)`, prefixed with the magic
|
|
85
|
+
byte `0x02`). Relaying a request is exactly as simple as that sounds:
|
|
86
|
+
build the request the normal way, wrap that complete frame wholesale as
|
|
87
|
+
the CoAP payload, and send it to the gateway on the CoAP opcode instead
|
|
88
|
+
of sending it directly. No separate encoding path for relayed vs. direct
|
|
89
|
+
requests -- only the outer envelope differs.
|
|
90
|
+
|
|
91
|
+
The CoAP method field is always `GET`, regardless of whether the
|
|
92
|
+
relayed operation is itself a Get or Set attribute request -- the CoAP
|
|
93
|
+
method is a generic transport-layer wrapper; the actual semantic
|
|
94
|
+
operation is determined by the *inner* FSCI opcode.
|
|
95
|
+
|
|
96
|
+
`Coap.ResponseCode` values are standard RFC 7252 codes (`2.01 Created`,
|
|
97
|
+
`2.05 Content`, `4.00 Bad Request`, `5.00 Internal Server Error`, etc.,
|
|
98
|
+
encoded as `class*100 + detail`) -- but `mobius.coap.CoapResponseCode`
|
|
99
|
+
only defines the subset that could be independently confirmed as
|
|
100
|
+
literal or reliably cross-referenced values (see that module's
|
|
101
|
+
docstring for exactly which). An unrecognized response code decodes to
|
|
102
|
+
a plain `int` rather than a guessed enum member, matching the real app's
|
|
103
|
+
own graceful fallback behavior. In practice, the response code isn't
|
|
104
|
+
load-bearing for this library -- the payload's own embedded FSCI frame
|
|
105
|
+
carries its own CRC, which is what's actually checked.
|
|
106
|
+
|
|
107
|
+
## Response correlation implementation
|
|
108
|
+
|
|
109
|
+
`MobiusDevice` maintains a token-keyed pending-response registry
|
|
110
|
+
(`_pending_coap_tokens`), separate from the existing message-ID-based
|
|
111
|
+
`_pending` used for direct requests:
|
|
112
|
+
|
|
113
|
+
- `_register_coap_token(token)` -- synchronous, registers and returns a
|
|
114
|
+
future immediately. Call this *before* sending the triggering request,
|
|
115
|
+
eliminating any race against the response arriving first.
|
|
116
|
+
- `wait_for_coap_response(token, timeout)` -- convenience wrapper that
|
|
117
|
+
registers and awaits in one call; only race-free if nothing could
|
|
118
|
+
plausibly respond before it starts running.
|
|
119
|
+
- `_on_rx_final()` resolves a pending token's future when a frame
|
|
120
|
+
matching `opGroup==0xDF && opCode==26` arrives and decodes to a
|
|
121
|
+
`CoapResponse` with a matching token -- entirely independent of the
|
|
122
|
+
normal message-ID-based resolution used for direct requests.
|
|
123
|
+
|
|
124
|
+
`RelayedMobiusDevice._relay()` registers the token, sends the outer
|
|
125
|
+
frame without waiting for its confirm, and awaits the token-matched
|
|
126
|
+
indication with a timeout.
|
|
127
|
+
|
|
128
|
+
## Address discovery
|
|
129
|
+
|
|
130
|
+
A device's Thread mesh-local RLOC (Routing Locator) IPv6 address:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
mesh_local_prefix(8 bytes) + 00:00(2 bytes) + 00:FF:FE:00(4 bytes)
|
|
134
|
+
+ short_address(2 bytes, LITTLE-ENDIAN)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The `00:FF:FE:00` marker is the standard Thread/6LoWPAN convention for a
|
|
138
|
+
short-address-derived interface identifier (confirmed via
|
|
139
|
+
`IPv6.isEui64Address()`'s exact byte check) -- a real, publicly
|
|
140
|
+
documented Thread networking convention, not proprietary to EcoTech. The
|
|
141
|
+
little-endian byte order for the short-address portion is the opposite
|
|
142
|
+
of IPv6's own conventional big-endian (network byte order) -- confirmed
|
|
143
|
+
via `ByteUtilities.getShort()`'s explicit `ByteOrder.LITTLE_ENDIAN`, not
|
|
144
|
+
assumed.
|
|
145
|
+
|
|
146
|
+
`mobius.mesh_address` implements construction (`build_rloc_address()`)
|
|
147
|
+
and the reverse (`extract_short_address()`), plus
|
|
148
|
+
`mesh_local_prefix_from_own_address()`/`is_valid_mesh_address()` for
|
|
149
|
+
deriving a network's shared prefix from any one device's own address.
|
|
150
|
+
|
|
151
|
+
### Two discovery mechanisms
|
|
152
|
+
|
|
153
|
+
**`MobiusDevice.get_own_mesh_address()`** -- fetches `MeshLocalAddresses`
|
|
154
|
+
(attribute `1005`) from the connected device itself. Returns that
|
|
155
|
+
device's own complete 16-byte mesh-local address (not "the prefix" on
|
|
156
|
+
its own -- extract the first 8 bytes yourself for the shared network
|
|
157
|
+
prefix), sanity-checked as non-all-zero. Confirmed via
|
|
158
|
+
`PeripheralConnection.java`'s `updateConnectionState()` ->
|
|
159
|
+
`getDeviceInfo()` to be exactly what the official app does automatically
|
|
160
|
+
on every connection to any non-legacy device -- universal, standard
|
|
161
|
+
behavior, not tied to any specific hardware.
|
|
162
|
+
|
|
163
|
+
**`mobius.discovery.discover_mesh_peers_via_direct_connect()`** -- the
|
|
164
|
+
practical way to build a serial→IPv6 map across multiple devices:
|
|
165
|
+
connects directly and briefly to each one in turn (sequentially, not
|
|
166
|
+
concurrently), calling `get_own_mesh_address()` on each. Meant to be run
|
|
167
|
+
once (or occasionally) to build the map, not on every poll cycle -- keep
|
|
168
|
+
only one connection open afterward (the "gateway") and reach the rest via
|
|
169
|
+
`RelayedMobiusDevice`.
|
|
170
|
+
|
|
171
|
+
**`MobiusDevice.discover_mesh_peers()`** -- an alternative that asks a
|
|
172
|
+
*connected* device to report on other devices it already knows about,
|
|
173
|
+
via three parallel attributes:
|
|
174
|
+
`ShortAddressArray`/`SerialNumberArray`/`DeviceModelArray` (`3700`/
|
|
175
|
+
`3701`/`3702`, fetched via the same "get all elements" pattern used
|
|
176
|
+
elsewhere in this library, index-matched). `SerialNumberArray`'s
|
|
177
|
+
per-element format is the same 14-byte ASCII serial format used
|
|
178
|
+
elsewhere in this library; a cleared/empty slot is the literal sentinel
|
|
179
|
+
string `"00000000000000"` (fourteen ASCII zero characters, not null
|
|
180
|
+
bytes), filtered out automatically. A fourth parallel array,
|
|
181
|
+
`BladeBleAddressArray` (`3714`, each peer's real BLE MAC address), is
|
|
182
|
+
recorded in `constants.py` but not used by this method.
|
|
183
|
+
|
|
184
|
+
**This mechanism requires a dedicated Cowboy hub device.** These three
|
|
185
|
+
arrays were originally found in `CowboyNetworkProcess.java` -- the class
|
|
186
|
+
specifically for setting up a network via a Cowboy hub -- and on
|
|
187
|
+
hardware without one, all three return `FsciStatus.UnsupportedAttribute`
|
|
188
|
+
even though `MeshLocalAddresses` on that same device succeeds. Cheap and
|
|
189
|
+
fails soft, so it's a reasonable first attempt, but
|
|
190
|
+
`discover_mesh_peers_via_direct_connect()` is the mechanism to actually
|
|
191
|
+
rely on unless a Cowboy hub is confirmed present.
|
|
192
|
+
|
|
193
|
+
## CLI support
|
|
194
|
+
|
|
195
|
+
`mobius-scan` flags for working with relay against real hardware:
|
|
196
|
+
|
|
197
|
+
- `--dump-mesh-peers` -- runs `discover_mesh_peers()` (the Cowboy-hub
|
|
198
|
+
path) with verbose, step-by-step output at each stage (own address,
|
|
199
|
+
each of the three arrays), rather than collapsing every possible
|
|
200
|
+
failure into a single empty result.
|
|
201
|
+
- `--build-peer-map` -- runs `discover_mesh_peers_via_direct_connect()`
|
|
202
|
+
across every device found in a scan.
|
|
203
|
+
- `--relay-target SERIAL` (used with `--by-serial <gateway>`) -- looks
|
|
204
|
+
for the target among the gateway's Cowboy-tracked peers first; if not
|
|
205
|
+
found there, falls back to a brief direct connection to the target
|
|
206
|
+
itself to learn its address. Either way, relays a full
|
|
207
|
+
`get_device_summary()` read to it and prints the result.
|
|
208
|
+
- `--debug-relay` (with `--relay-target`) -- prints raw bytes at every
|
|
209
|
+
layer of a relayed request/response: the inner request frame, the
|
|
210
|
+
outer request frame, every frame the gateway receives while waiting
|
|
211
|
+
(via `MobiusDevice.on_any_frame`, a hook called for every parsed
|
|
212
|
+
incoming frame regardless of whether it resolves anything), the
|
|
213
|
+
decoded CoAP envelope, the inner response frame, and the decoded
|
|
214
|
+
attribute status/values.
|