python-mobius 0.1.4__tar.gz → 0.2.0__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.0}/CHANGELOG.md +204 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/PKG-INFO +1 -1
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/06-light-schedule.md +24 -0
- python_mobius-0.2.0/documentation/09-thread-coap-relay.md +337 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/11-device-status-attributes.md +24 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/pyproject.toml +1 -1
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/__init__.py +21 -5
- python_mobius-0.2.0/src/mobius/cli.py +393 -0
- python_mobius-0.2.0/src/mobius/coap.py +253 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/constants.py +17 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/device.py +265 -7
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/device_status.py +45 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/discovery.py +68 -0
- python_mobius-0.2.0/src/mobius/mesh_address.py +116 -0
- python_mobius-0.2.0/src/mobius/relay.py +289 -0
- python_mobius-0.2.0/tests/test_coap.py +191 -0
- python_mobius-0.2.0/tests/test_coap_indication_handling.py +160 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_device_summary.py +12 -0
- python_mobius-0.2.0/tests/test_device_time.py +125 -0
- python_mobius-0.2.0/tests/test_frame_sniffer.py +87 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_light_intensity_branching.py +16 -0
- python_mobius-0.2.0/tests/test_mesh_address.py +126 -0
- python_mobius-0.2.0/tests/test_mesh_discovery.py +235 -0
- python_mobius-0.2.0/tests/test_mesh_discovery_direct_connect.py +180 -0
- python_mobius-0.2.0/tests/test_relay.py +313 -0
- python_mobius-0.1.4/documentation/09-thread-coap-relay.md +0 -78
- python_mobius-0.1.4/src/mobius/cli.py +0 -125
- {python_mobius-0.1.4 → python_mobius-0.2.0}/.gitignore +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/LICENSE +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/README.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/00-overview.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/03-attributes-and-opcodes.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/05-scenes.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/crc.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/frame.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/modifiers.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/power.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/src/mobius/schedule.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_device_status.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_frame.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_modifiers.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_power.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_pump_status.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.1.4 → python_mobius-0.2.0}/tests/test_serial_identity.py +0 -0
|
@@ -1,5 +1,209 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Thread/CoAP device relay -- see
|
|
6
|
+
[documentation/09-thread-coap-relay.md](documentation/09-thread-coap-relay.md)
|
|
7
|
+
for the full protocol trace and implementation history. The `coap-relay`
|
|
8
|
+
branch has been merged into main as part of this release.
|
|
9
|
+
|
|
10
|
+
- **Root cause of relayed reads returning empty data found and fixed,**
|
|
11
|
+
after the user provided the raw smali disassembly for
|
|
12
|
+
`BaseConnection.java` -- JADX's decompiler had given up specifically on
|
|
13
|
+
`handleIndication()`, the one method that held the answer, but the
|
|
14
|
+
underlying bytecode was still traceable. Confirmed precisely: the real
|
|
15
|
+
CoAP relay response arrives on a completely separate opcode (`0xDF`/
|
|
16
|
+
`26`, `COAP_INDICATION_OPCODE` -- not the request's `25`, whose confirm
|
|
17
|
+
is a bare, meaningless ack the app itself discards, confirmed
|
|
18
|
+
byte-for-byte via real hardware testing showing exactly 1 byte in every
|
|
19
|
+
relayed read's outer response), matched purely by CoAP token
|
|
20
|
+
(`lambda$getSentRequest$6`: `coapRequest.getToken() ==
|
|
21
|
+
coapResponse.getToken()`), never by the outer FSCI message ID. Added
|
|
22
|
+
`MobiusDevice._register_coap_token()`/`wait_for_coap_response()` -- a
|
|
23
|
+
token-keyed pending-response registry, entirely separate from the
|
|
24
|
+
existing message-ID-based `self._pending` mechanism -- wired into
|
|
25
|
+
`_on_rx_final()`. `RelayedMobiusDevice._relay()` was rewritten around
|
|
26
|
+
this: registers the token before sending (eliminating any race against
|
|
27
|
+
the response arriving first), sends without waiting for the
|
|
28
|
+
meaningless confirm, and awaits the token-matched indication instead.
|
|
29
|
+
`test_relay.py` rewritten entirely for the new architecture (14 tests);
|
|
30
|
+
new dedicated `test_coap_indication_handling.py` for the token-registry
|
|
31
|
+
mechanism itself (9 tests). Verified end-to-end with a full mocked
|
|
32
|
+
round-trip (both the discarded ack and the real indication arriving,
|
|
33
|
+
in that order) before asking for another real hardware run -- **this is
|
|
34
|
+
the next real test.**
|
|
35
|
+
|
|
36
|
+
- **Real hardware testing confirmed Thread IS active on regular
|
|
37
|
+
VorTech/Radion devices** (a genuine, positive result -- `MeshLocalAddresses`
|
|
38
|
+
reads back a valid, non-zero address), **but the three-array peer
|
|
39
|
+
discovery mechanism (`discover_mesh_peers()`) is Cowboy-hub-specific**
|
|
40
|
+
(`FsciStatus.UnsupportedAttribute` on hardware with no Cowboy hub,
|
|
41
|
+
even though that same device's own address reads back fine). Added
|
|
42
|
+
`MobiusDevice.get_own_mesh_address()` and `mobius.discovery.
|
|
43
|
+
discover_mesh_peers_via_direct_connect()` as the alternative --
|
|
44
|
+
confirmed via `PeripheralConnection.java` to be exactly how the app
|
|
45
|
+
itself always learns a device's address (automatic on every
|
|
46
|
+
connection, not Cowboy-specific). Added `mobius-scan --build-peer-map`
|
|
47
|
+
to exercise this from the CLI. `discover_mesh_peers()` remains in
|
|
48
|
+
place as a cheap, fail-soft first attempt in case a Cowboy hub IS
|
|
49
|
+
present, but is no longer the primary/recommended discovery path. 10
|
|
50
|
+
new tests.
|
|
51
|
+
|
|
52
|
+
- **Fixed a real bug found via real hardware testing: `--relay-target`
|
|
53
|
+
was never actually wired to the new Cowboy-hub-free discovery
|
|
54
|
+
mechanism.** After adding `discover_mesh_peers_via_direct_connect()`,
|
|
55
|
+
a real `--relay-target` attempt still failed with "not found among
|
|
56
|
+
discovered peers" -- not because relay itself failed, but because the
|
|
57
|
+
CLI's target lookup only ever checked the Cowboy-hub-specific peer
|
|
58
|
+
list. `--relay-target` now falls back to a brief direct connection to
|
|
59
|
+
the target itself (`find_device_by_serial()` +
|
|
60
|
+
`get_own_mesh_address()`) if the Cowboy-hub lookup doesn't find it.
|
|
61
|
+
|
|
62
|
+
- **Added `--debug-relay`** after the fixed `--relay-target` reached the
|
|
63
|
+
target but `get_device_summary()` came back entirely empty with no
|
|
64
|
+
visible error. Turned out every attribute fetch WAS raising an
|
|
65
|
+
exception internally (a malformed CoAP response) -- silently, since
|
|
66
|
+
`get_device_summary()`'s own per-field fail-soft handling was catching
|
|
67
|
+
and discarding them without trace. Prints raw bytes at every layer of
|
|
68
|
+
a relayed request/response so this stops being invisible. Verified
|
|
69
|
+
with a mocked round-trip that debug output itself is correct before
|
|
70
|
+
asking for another real hardware run.
|
|
71
|
+
|
|
72
|
+
- **Confirmed directly from source why the response comes back
|
|
73
|
+
malformed: the immediate confirm to a relay request is explicitly
|
|
74
|
+
discarded by the app itself, on purpose.** `BaseConnection.java`'s
|
|
75
|
+
`handleBytes()` matches `opGroup==0xDF && opCode==25` unconditionally
|
|
76
|
+
and logs it only -- before the dispatcher ever checks whether it
|
|
77
|
+
resolves a pending request. Real hardware testing independently
|
|
78
|
+
confirmed this byte-for-byte: every relayed read's outer response is
|
|
79
|
+
exactly 1 byte (`0x00`), not a truncated response but the genuine
|
|
80
|
+
complete confirm, thrown away by the app too. The real response, if it
|
|
81
|
+
arrives at all, comes through a separate "indication" handled by
|
|
82
|
+
`handleIndication()` -- a method JADX couldn't decompile, a genuine
|
|
83
|
+
limit of what source tracing alone can determine here. Added
|
|
84
|
+
`MobiusDevice.on_any_frame` (fires for every parsed incoming frame,
|
|
85
|
+
not just ones resolving a pending request) and extended
|
|
86
|
+
`--debug-relay` to watch for 2 seconds after the initial confirm, to
|
|
87
|
+
find out empirically whether a second frame actually arrives. 4 new
|
|
88
|
+
tests.
|
|
89
|
+
|
|
90
|
+
- **Added `mobius.coap`**: CoAP (RFC 7252) protocol primitives --
|
|
91
|
+
`CoapRequestType`/`CoapMethod`/`CoapResponseCode` enums,
|
|
92
|
+
`encode_coap_request()`/`decode_coap_response()`. Pure functions, no
|
|
93
|
+
hardware dependency. Confirmed wire format traced directly from the
|
|
94
|
+
decompiled app (`Coap.java`/`CoapRequest.java`/`CoapResponse.java`) --
|
|
95
|
+
see the module's docstring for exactly which enum values are
|
|
96
|
+
confirmed-literal, cross-referenced from elsewhere in this project, or
|
|
97
|
+
inferred-only (deliberately not guessing at unconfirmed ones -- an
|
|
98
|
+
unrecognized response code decodes to a plain int, matching the real
|
|
99
|
+
app's own graceful fallback behavior). 15 new tests, including
|
|
100
|
+
specific coverage of the asymmetric IPv6 byte-reversal between
|
|
101
|
+
requests and responses, which is easy to get backwards.
|
|
102
|
+
|
|
103
|
+
- **Added `mobius.mesh_address`**: Thread mesh-local RLOC IPv6 address
|
|
104
|
+
construction (`build_rloc_address()`/`extract_short_address()`/
|
|
105
|
+
`mesh_local_prefix_from_own_address()`/`is_valid_mesh_address()`/
|
|
106
|
+
`is_short_address_derived()`). Pure functions, no hardware dependency.
|
|
107
|
+
Includes a genuinely surprising, verified-not-assumed finding: the
|
|
108
|
+
short-address portion is little-endian (confirmed via
|
|
109
|
+
`ByteUtilities.getShort()`'s explicit `ByteOrder.LITTLE_ENDIAN`), the
|
|
110
|
+
opposite of IPv6's own conventional big-endian byte order -- easy to
|
|
111
|
+
get backwards by assuming the IPv6 standard's own convention applies
|
|
112
|
+
here too. 15 new tests.
|
|
113
|
+
|
|
114
|
+
- **Added `MobiusDevice.discover_mesh_peers()`**: fetches
|
|
115
|
+
`MeshLocalAddresses` (1005) + the three parallel `ShortAddressArray`/
|
|
116
|
+
`SerialNumberArray`/`DeviceModelArray` attributes (3700/3701/3702)
|
|
117
|
+
from a connected device, returning every other device it knows about
|
|
118
|
+
over the Thread mesh as a list of new `MeshPeer` dataclasses (serial,
|
|
119
|
+
model, short address, ready-to-use IPv6). Confirms `SerialNumberArray`'s
|
|
120
|
+
per-element format matches the existing 14-byte ASCII serial format
|
|
121
|
+
already used elsewhere in this library -- no new parsing logic needed.
|
|
122
|
+
Filters out cleared/empty slots (the literal sentinel string
|
|
123
|
+
`"00000000000000"`, confirmed via `CowboyNetworkProcess.java`'s own
|
|
124
|
+
cleanup logic). Fails soft to an empty list for unsupported devices or
|
|
125
|
+
ones not currently part of an active Thread network, matching this
|
|
126
|
+
library's usual convention. Also recorded (but not yet used) a fourth
|
|
127
|
+
parallel array, `BladeBleAddressArray` (3714, each peer's real BLE MAC
|
|
128
|
+
address) -- found alongside the other three but not needed for CoAP
|
|
129
|
+
relay itself. 7 new tests.
|
|
130
|
+
|
|
131
|
+
- **Added `RelayedMobiusDevice`** (`mobius.relay`): a `MobiusDevice`
|
|
132
|
+
subclass that overrides `get_attribute()`/`get_attribute_raw()`/
|
|
133
|
+
`set_attribute()` to route through a gateway device via CoAP instead of
|
|
134
|
+
owning its own BLE connection. Every other inherited method
|
|
135
|
+
(`get_device_summary()`, `get_current_light_intensities()`, etc.) works
|
|
136
|
+
transparently through relay, confirmed with a test that specifically
|
|
137
|
+
exercises an inherited high-level method rather than just the
|
|
138
|
+
overridden primitives. Confirmed `request.getData(true)` builds a
|
|
139
|
+
complete self-contained FSCI frame (byte-for-byte identical in
|
|
140
|
+
structure to this library's own `build_frame()`), resolving an
|
|
141
|
+
ambiguity flagged in an earlier commit -- relaying a request needed no
|
|
142
|
+
new encoding path, just wrapping the same frame a direct request would
|
|
143
|
+
use. Also confirmed, directly from the one real call site in the
|
|
144
|
+
decompile, a subtle and easy-to-get-wrong detail: the outer CoAP method
|
|
145
|
+
is always `GET`, even for a relayed Set -- the actual operation is
|
|
146
|
+
determined by the inner FSCI opcode, not the CoAP transport layer. 13
|
|
147
|
+
new tests.
|
|
148
|
+
|
|
149
|
+
- **Added `mobius-scan --dump-mesh-peers` / `--relay-target SERIAL`**
|
|
150
|
+
(both usable alongside `--by-serial`): prints every peer a connected
|
|
151
|
+
device knows about over the Thread mesh, and/or relays a full
|
|
152
|
+
`get_device_summary()` read to a specified target through it. This is
|
|
153
|
+
the last of the 5 planned pieces for this feature -- **implementation
|
|
154
|
+
is now complete, but not yet validated against real hardware.** Run a
|
|
155
|
+
separate direct `--by-serial <target>` command to confirm a relayed
|
|
156
|
+
read actually matches a direct one; this is also how the biggest open
|
|
157
|
+
question (whether these devices run Thread at all) finally gets
|
|
158
|
+
answered.
|
|
159
|
+
|
|
160
|
+
- **Added a verbose, step-by-step mesh discovery diagnostic
|
|
161
|
+
(`_debug_mesh_discovery()`)** after real hardware testing showed every
|
|
162
|
+
device returning "No peers found" with no further detail.
|
|
163
|
+
`discover_mesh_peers()`'s fail-soft-to-empty-list behavior is correct
|
|
164
|
+
for production/library use, but collapses several genuinely different
|
|
165
|
+
failure modes into the same result -- device doesn't support Thread at
|
|
166
|
+
all, vs. supports it but isn't on an active network right now, vs.
|
|
167
|
+
the peer arrays came back empty for some other reason. `--dump-mesh-peers`
|
|
168
|
+
now surfaces each step's raw result or exception individually instead
|
|
169
|
+
of just the final empty-or-not outcome.
|
|
170
|
+
|
|
171
|
+
- **Added `get_device_time_info()`** (`Epoch`/`RTCTime`, attributes
|
|
172
|
+
201/219) -- device clock drift vs. local system time. Not speculative:
|
|
173
|
+
confirmed via the app's own "Time Difference" troubleshooting feature
|
|
174
|
+
(`TimeDifferenceFragment.java`), which checks these exact two attributes
|
|
175
|
+
the same way. Included automatically in `get_device_summary()`'s
|
|
176
|
+
`"device_time"` field. Important caveat documented: the app's own
|
|
177
|
+
schedule interpolation uses the phone/tank clock, not either of these,
|
|
178
|
+
so this doesn't directly explain a schedule-interpolation mismatch by
|
|
179
|
+
itself -- still worth checking as a general health signal. 5 new tests.
|
|
180
|
+
|
|
181
|
+
- **Added `mobius-scan --dump-schedule`**: prints each connected device's
|
|
182
|
+
full raw programmed schedule (every point's time, flags -- ACTIVE/NIGHT/
|
|
183
|
+
SUNRISE/SUNSET -- and per-channel/mode values), not just the
|
|
184
|
+
interpolated "current" value `get_device_summary()` already shows. For
|
|
185
|
+
lights, also prints `lunar_enabled`/`insolation_active`/
|
|
186
|
+
`schedule_intensity`/`acclimation` state. Combines with either the
|
|
187
|
+
normal scan or `--by-serial`. Added so a schedule's actual point
|
|
188
|
+
structure can be inspected directly -- needed to verify
|
|
189
|
+
`is_night_segment()`'s bracket-finding logic against real data, rather
|
|
190
|
+
than guessed at or transcribed by hand.
|
|
191
|
+
|
|
192
|
+
- **Added debugging diagnostics to `get_current_light_intensities()`**:
|
|
193
|
+
`.diagnostics` now also includes `is_night_segment`, `lunar_enabled`,
|
|
194
|
+
`scalar_source` (`"lunar"`/`"night_no_lunar"`/`"schedule_intensity"`),
|
|
195
|
+
and `scalar` (the actual final multiplier applied). Added after a real
|
|
196
|
+
discrepancy was reported between this library's computed value and the
|
|
197
|
+
app's own displayed value for a light at night (app showing lunar-
|
|
198
|
+
reduced moonlight intensities, this library showing the unreduced
|
|
199
|
+
schedule-intensity value instead) -- these fields exist specifically so
|
|
200
|
+
that class of mismatch can be diagnosed with real data (which branch was
|
|
201
|
+
taken, and why) rather than guessed at blindly. Also threaded into
|
|
202
|
+
`get_device_summary()` as `"light_diagnostics"`, so `mobius-scan`
|
|
203
|
+
surfaces this directly -- debugging this kind of mismatch belongs in the
|
|
204
|
+
library/CLI, not in a specific downstream integration. Existing tests
|
|
205
|
+
updated to assert on these fields directly.
|
|
206
|
+
|
|
3
207
|
## 0.1.4
|
|
4
208
|
|
|
5
209
|
- 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.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -165,6 +165,30 @@ wrong-but-present value is often more useful than nothing, and the
|
|
|
165
165
|
severity depends on how different the real Insolation-derived schedule is
|
|
166
166
|
from the manually-programmed one on a given day.
|
|
167
167
|
|
|
168
|
+
## Debugging diagnostics: which branch/scalar was actually used
|
|
169
|
+
|
|
170
|
+
`.diagnostics` also includes `"is_night_segment"`, `"lunar_enabled"`,
|
|
171
|
+
`"scalar_source"` (one of `"lunar"`/`"night_no_lunar"`/
|
|
172
|
+
`"schedule_intensity"`), and `"scalar"` (the actual final multiplier
|
|
173
|
+
applied). Added specifically because a mismatch between this library's
|
|
174
|
+
computed value and the app's own displayed value could stem from either
|
|
175
|
+
`is_night_segment()` misjudging a specific schedule's point structure, or
|
|
176
|
+
`get_lunar_enabled()` reading incorrectly -- and those need to be told
|
|
177
|
+
apart with real data rather than guessed at. If you ever see a value that
|
|
178
|
+
doesn't match the app, check these fields first before assuming which
|
|
179
|
+
piece is wrong:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
result = await device.get_current_light_intensities()
|
|
183
|
+
print(result.diagnostics)
|
|
184
|
+
# {'insolation_active': False, 'is_night_segment': True,
|
|
185
|
+
# 'lunar_enabled': True, 'scalar_source': 'lunar', 'scalar': 0.35}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`lunar_enabled` is `None` (not `False`) when `is_night_segment` is
|
|
189
|
+
`False`, since it isn't checked/relevant outside the night segment --
|
|
190
|
+
don't read a `None` here as "lunar phases are off."
|
|
191
|
+
|
|
168
192
|
## Hyperdrive (boosted channels above 100%)
|
|
169
193
|
|
|
170
194
|
Some light channels can be driven above their normally-rated power
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Thread/CoAP device relay
|
|
2
|
+
|
|
3
|
+
**Status: root cause found and fixed, confirmed directly from smali
|
|
4
|
+
bytecode (not just source) -- ready for a real hardware retest.** The
|
|
5
|
+
user provided the raw smali disassembly for `BaseConnection.java`
|
|
6
|
+
(specifically `handleIndication()`, the method JADX couldn't decompile).
|
|
7
|
+
This resolved the mystery completely: the real CoAP relay response
|
|
8
|
+
arrives as a genuinely separate message, on a different opcode (`0xDF`/
|
|
9
|
+
`26`, not the request's `25`), matched purely by CoAP token -- never by
|
|
10
|
+
the outer FSCI message ID, and never via the (confirmed meaningless,
|
|
11
|
+
app-discarded) confirm to opcode 25. Implemented: `MobiusDevice` now
|
|
12
|
+
tracks pending CoAP responses by token (`_register_coap_token()`/
|
|
13
|
+
`wait_for_coap_response()`), and `_on_rx_final()` resolves them when a
|
|
14
|
+
matching opcode-26 indication arrives, entirely independent of the
|
|
15
|
+
existing message-ID-based mechanism used for direct requests.
|
|
16
|
+
`RelayedMobiusDevice._relay()` was rewritten around this. See
|
|
17
|
+
"What's confirmed vs. not yet" below for the full, current picture.
|
|
18
|
+
|
|
19
|
+
## Why this matters in practice
|
|
20
|
+
|
|
21
|
+
Every device this library talks to gets its own persistent, direct BLE
|
|
22
|
+
connection. The official app doesn't -- it connects to just **one or a
|
|
23
|
+
few** devices and reaches the rest through them. Concretely, this means:
|
|
24
|
+
if something else (this library, `ha-mobius`, or another instance of the
|
|
25
|
+
app) is already holding direct BLE connections to every device in a tank,
|
|
26
|
+
the official app can end up with nothing to connect to at all, and stops
|
|
27
|
+
working. Implementing relay properly is the fix -- it lets this library
|
|
28
|
+
behave the way the app does, freeing up connections rather than holding
|
|
29
|
+
one per device indefinitely.
|
|
30
|
+
|
|
31
|
+
## How the app's relay actually works
|
|
32
|
+
|
|
33
|
+
Devices form a **Thread mesh network** (802.15.4 radio -- separate
|
|
34
|
+
hardware from BLE) identified by a `pan_id` (see
|
|
35
|
+
[08-manufacturer-data.md](./08-manufacturer-data.md)). `Comm.java`'s
|
|
36
|
+
`sendRequest()` routing logic (confirmed directly from source, not
|
|
37
|
+
inferred):
|
|
38
|
+
|
|
39
|
+
```java
|
|
40
|
+
if (commDevice.equals(request.getTarget())) {
|
|
41
|
+
// direct: send over the existing BLE connection as-is
|
|
42
|
+
} else if (iPv6 != null) {
|
|
43
|
+
// relay: wrap in CoAP, address it to the target's mesh IPv6,
|
|
44
|
+
// send through commDevice (the connected gateway)
|
|
45
|
+
} else {
|
|
46
|
+
// no known address for the target -- fail
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The key insight: **the Thread border-router function lives on the
|
|
51
|
+
connected device itself**, not on the phone. A phone (or a laptop) never
|
|
52
|
+
needs its own 802.15.4 radio -- it only needs BLE to reach *one* mesh
|
|
53
|
+
member, and that device does the Thread-side forwarding using its own
|
|
54
|
+
onboard hardware.
|
|
55
|
+
|
|
56
|
+
## CoAP wire format (confirmed)
|
|
57
|
+
|
|
58
|
+
Traced directly from `Coap.java` / `CoapRequest.java` / `CoapResponse.java`
|
|
59
|
+
/ `CoapFsciRequest.java`. Reuses the same C2CI envelope described in
|
|
60
|
+
[02-framing-and-crc.md](./02-framing-and-crc.md), just a new opcode
|
|
61
|
+
(confirmed literal: **25**) alongside the existing Get(`0x17`)/Set(`0x18`)
|
|
62
|
+
attribute opcodes.
|
|
63
|
+
|
|
64
|
+
**Request** -- `CoapRequest`'s first constructor:
|
|
65
|
+
```
|
|
66
|
+
IPv6(16 bytes, REVERSED) + token(4, LE) + method(2, LE)
|
|
67
|
+
+ payload_length(2, LE) + request_type(1) + payload
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Response** -- `CoapResponse`'s constructor:
|
|
71
|
+
```
|
|
72
|
+
IPv6(16 bytes, NOT reversed) + token(4, LE) + response_code(2, LE)
|
|
73
|
+
+ payload_length(2, LE) + request_type(1) + payload
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The asymmetric reversal (request reverses the address, response doesn't)
|
|
77
|
+
is confirmed directly from source and easy to get backwards -- see
|
|
78
|
+
`mobius.coap`'s test suite, which specifically checks this doesn't happen
|
|
79
|
+
by accident (a palindromic test address would pass even with the reversal
|
|
80
|
+
backwards or missing; the tests deliberately avoid that).
|
|
81
|
+
|
|
82
|
+
**The payload is a complete, self-contained FSCI frame.** `CoapFsciRequest`
|
|
83
|
+
wraps `request.getData(true)` -- and `Request.getData(true)`'s exact
|
|
84
|
+
implementation is now confirmed directly from source: it builds
|
|
85
|
+
`opGroup + opCode + messageId(2) + reserved(1) + dataLength(2) + data +
|
|
86
|
+
CRC16(2)`, then prepends the magic byte (`0x02`) to that -- i.e. the
|
|
87
|
+
**complete outer FSCI frame**, byte-for-byte identical in structure to
|
|
88
|
+
what this library's own `build_frame()` produces for a direct (non-
|
|
89
|
+
relayed) request. This means relaying a request is exactly as simple as
|
|
90
|
+
it sounds: build the request the normal way, wrap that complete frame
|
|
91
|
+
wholesale as the CoAP payload, and send it to the gateway on the CoAP
|
|
92
|
+
opcode instead of sending it directly. No separate encoding path needed
|
|
93
|
+
for relayed vs. direct requests -- only the outer envelope differs.
|
|
94
|
+
|
|
95
|
+
`Coap.ResponseCode` values are standard RFC 7252 codes (`2.01 Created`,
|
|
96
|
+
`2.05 Content`, `4.00 Bad Request`, `5.00 Internal Server Error`, etc.,
|
|
97
|
+
encoded as `class*100 + detail`). Several are JADX symbolic substitutions
|
|
98
|
+
rather than plain literals in the decompile; not all could be
|
|
99
|
+
independently confirmed (see `mobius.coap`'s module docstring for exactly
|
|
100
|
+
which values are literal, which are cross-referenced from elsewhere in
|
|
101
|
+
this project, and which are inferred-only). One confirmed value
|
|
102
|
+
(`ProxyingNotSupported = 512`) doesn't fit the otherwise-clean pattern --
|
|
103
|
+
a reminder that guessing at the unconfirmed ones isn't safe.
|
|
104
|
+
|
|
105
|
+
## Address discovery (confirmed -- this is what the earlier version of
|
|
106
|
+
## this document got wrong)
|
|
107
|
+
|
|
108
|
+
The earlier version of this document treated the target's Thread IPv6
|
|
109
|
+
address as unverified/only-readable-from-the-target-itself. A more
|
|
110
|
+
thorough trace found the actual mechanism, confirmed directly in
|
|
111
|
+
`CowboyNetworkProcess.java` and `Comm.java`:
|
|
112
|
+
|
|
113
|
+
1. **`MeshLocalAddresses`** (attribute `1005`) -- read from the connected
|
|
114
|
+
gateway device. Confirmed via `PeripheralConnection.java`: returns the
|
|
115
|
+
gateway's OWN complete 16-byte mesh-local address (not "the prefix" as
|
|
116
|
+
its own standalone value), sanity-checked there as non-all-zero before
|
|
117
|
+
use. Since every device on the same Thread network shares the same
|
|
118
|
+
8-byte prefix, extracting the first 8 bytes of the gateway's own
|
|
119
|
+
address gives the network's shared prefix.
|
|
120
|
+
2. **`ShortAddressArray`/`SerialNumberArray`/`DeviceModelArray`**
|
|
121
|
+
(attributes `3700`/`3701`/`3702`) -- three parallel arrays, fetched via
|
|
122
|
+
the same "get all elements" pattern (`index=0, count=0xFFFF`) already
|
|
123
|
+
used elsewhere in this library, index-matched to give every mesh peer's
|
|
124
|
+
serial, model, and Thread short address (RLOC16) the gateway knows
|
|
125
|
+
about. **All readable from a single connected gateway device** --
|
|
126
|
+
nothing from the target itself is required. `SerialNumberArray`'s
|
|
127
|
+
per-element format is confirmed identical to the existing 14-byte
|
|
128
|
+
ASCII serial format already used elsewhere in this library (confirmed
|
|
129
|
+
via `CowboyNetworkProcess.java`'s own cleanup logic, which sets a
|
|
130
|
+
cleared slot to the literal string `"00000000000000"` -- fourteen
|
|
131
|
+
ASCII zero characters, the same length/format as a real serial, not
|
|
132
|
+
null bytes) -- no new serial-parsing logic needed.
|
|
133
|
+
3. **Target IPv6 = `prefix(8) + 00:00 + 00:FF:FE:00 + short_address(2,
|
|
134
|
+
little-endian)`** -- the standard Thread RLOC IID format. Confirmed
|
|
135
|
+
via `IPv6.isEui64Address()`'s exact byte check (`bytes[10:14] ==
|
|
136
|
+
00:FF:FE:00`), which matches the real Thread specification's own
|
|
137
|
+
convention for short-address-derived interface identifiers -- not
|
|
138
|
+
something EcoTech invented, a well-known, publicly documented mesh
|
|
139
|
+
networking format. The little-endian byte order for the short-address
|
|
140
|
+
portion is a genuinely surprising, verified-not-assumed detail --
|
|
141
|
+
confirmed via `ByteUtilities.getShort()`'s explicit
|
|
142
|
+
`ByteOrder.LITTLE_ENDIAN`, which is the opposite of IPv6's own
|
|
143
|
+
conventional big-endian (network byte order); easy to get backwards by
|
|
144
|
+
just assuming the IPv6 standard's convention applies here too.
|
|
145
|
+
|
|
146
|
+
## What's confirmed vs. not yet
|
|
147
|
+
|
|
148
|
+
**Confirmed from source** (traced directly, not guessed): the CoAP
|
|
149
|
+
envelope wire format (both directions), the routing decision logic, the
|
|
150
|
+
RLOC-based address construction formula, that `request.getData(true)`
|
|
151
|
+
produces a complete self-contained FSCI frame (byte-for-byte identical in
|
|
152
|
+
structure to this library's own `build_frame()` output), and that the
|
|
153
|
+
outer CoAP method is always `GET` regardless of whether the relayed
|
|
154
|
+
operation is itself a Get or Set -- confirmed via the one real call site
|
|
155
|
+
(`Comm.java`), which hardcodes `Request.Method.GET` unconditionally.
|
|
156
|
+
|
|
157
|
+
**Confirmed from real hardware testing**:
|
|
158
|
+
- **Thread IS active on regular VorTech/Radion devices** -- a real,
|
|
159
|
+
positive result. `MeshLocalAddresses` reads back a genuine, valid,
|
|
160
|
+
non-zero mesh-local address on hardware with no Cowboy hub present.
|
|
161
|
+
This resolves what had been the single biggest open question.
|
|
162
|
+
- **The three-array peer discovery mechanism
|
|
163
|
+
(`ShortAddressArray`/`SerialNumberArray`/`DeviceModelArray`,
|
|
164
|
+
`MobiusDevice.discover_mesh_peers()`) is Cowboy-hub-specific.** On real
|
|
165
|
+
VorTech/Radion hardware with no Cowboy hub, all three return
|
|
166
|
+
`FsciStatus.UnsupportedAttribute` (an explicit "I don't support this,"
|
|
167
|
+
not a timeout or generic failure) -- even though `MeshLocalAddresses`
|
|
168
|
+
on that same device succeeds. This lines up with where these arrays
|
|
169
|
+
were originally found in the decompile: inside
|
|
170
|
+
`CowboyNetworkProcess.java`, the class specifically for setting up a
|
|
171
|
+
network via a dedicated Cowboy hub.
|
|
172
|
+
- **The alternative -- connecting directly to each device to read its
|
|
173
|
+
own `MeshLocalAddresses` -- doesn't depend on a Cowboy hub, and is
|
|
174
|
+
confirmed to be exactly how the app itself always learns a device's
|
|
175
|
+
address.** `PeripheralConnection.java`'s `updateConnectionState()` (the
|
|
176
|
+
standard GATT-setup-completion handler, checking the same
|
|
177
|
+
RxData/RxFinal/TxData/TxFinal characteristics this library's own
|
|
178
|
+
transport already uses) calls `getDeviceInfo()` -- which reads
|
|
179
|
+
`MeshLocalAddresses` -- automatically on every connection to any
|
|
180
|
+
non-legacy device. Not Cowboy-specific, not conditional: universal,
|
|
181
|
+
standard behavior. `MobiusDevice.get_own_mesh_address()` and
|
|
182
|
+
`mobius.discovery.discover_mesh_peers_via_direct_connect()` implement
|
|
183
|
+
this directly.
|
|
184
|
+
- **The real CoAP relay response arrives on a completely different
|
|
185
|
+
opcode than the request, matched purely by CoAP token.** Confirmed
|
|
186
|
+
directly from `BaseConnection.smali` (the user provided the raw
|
|
187
|
+
disassembly for the one method JADX couldn't decompile,
|
|
188
|
+
`handleIndication()`) -- traced from actual bytecode, not source-level
|
|
189
|
+
guessing:
|
|
190
|
+
- The confirm to a CoAP request (opGroup `0xDF`/opCode `25`,
|
|
191
|
+
`COAP_OPCODE`) is a **bare, meaningless ack**. `BaseConnection.java`'s
|
|
192
|
+
`handleBytes()` matches `opGroup==0xDF && opCode==25` unconditionally
|
|
193
|
+
and does nothing but log it, before the dispatcher ever checks
|
|
194
|
+
whether it resolves a pending request. Real hardware testing
|
|
195
|
+
independently confirmed this byte-for-byte: every relayed read's
|
|
196
|
+
outer response is exactly 1 byte (`0x00`) -- not a truncated
|
|
197
|
+
response, the genuine, complete confirm, thrown away by the app too.
|
|
198
|
+
- **The real response arrives as a separate message on opGroup
|
|
199
|
+
`0xDF`/opCode `26`** (`COAP_INDICATION_OPCODE`) -- confirmed via the
|
|
200
|
+
packed-switch inside `handleIndication()`.
|
|
201
|
+
- **Matched purely by CoAP token, never by the outer FSCI message ID.**
|
|
202
|
+
Confirmed via `lambda$getSentRequest$6`'s exact comparison:
|
|
203
|
+
`coapRequest.getToken() == coapResponse.getToken()`.
|
|
204
|
+
- Implemented in `MobiusDevice._register_coap_token()`/
|
|
205
|
+
`wait_for_coap_response()` and wired into `_on_rx_final()`, entirely
|
|
206
|
+
independent of the existing message-ID-based `self._pending`
|
|
207
|
+
mechanism used for direct (non-relayed) requests.
|
|
208
|
+
`RelayedMobiusDevice._relay()` was rewritten around this: registers
|
|
209
|
+
the token before sending (eliminating any race), sends without
|
|
210
|
+
waiting for the meaningless confirm, and awaits the token-matched
|
|
211
|
+
indication instead.
|
|
212
|
+
|
|
213
|
+
**Not yet confirmed**: whether this actually works end-to-end against
|
|
214
|
+
real hardware. Everything above is confirmed from source/bytecode and
|
|
215
|
+
covered by unit tests with mocked notifications, but hasn't been
|
|
216
|
+
exercised against a real device yet -- that's the next test.
|
|
217
|
+
hardware instead, which `--debug-relay`'s frame-sniffing window is for.
|
|
218
|
+
|
|
219
|
+
## Implementation plan (this branch)
|
|
220
|
+
|
|
221
|
+
Split by functionality, each independently testable before the next
|
|
222
|
+
depends on it:
|
|
223
|
+
|
|
224
|
+
1. **CoAP protocol primitives** (`mobius.coap`) -- envelope encode/decode,
|
|
225
|
+
pure functions, no hardware needed. *(done)*
|
|
226
|
+
2. **Thread mesh IPv6 addressing** (`mobius.mesh_address`) -- RLOC
|
|
227
|
+
address construction from a mesh-local prefix + short address.
|
|
228
|
+
*(done -- includes a verified, non-obvious finding: the short-address
|
|
229
|
+
portion is little-endian, confirmed via `ByteUtilities.getShort()`'s
|
|
230
|
+
explicit `ByteOrder.LITTLE_ENDIAN`, not the IPv6 standard's own
|
|
231
|
+
big-endian convention one might otherwise assume)*
|
|
232
|
+
3. **Peer discovery** (`MobiusDevice.discover_mesh_peers()`) -- fetching
|
|
233
|
+
and parsing the three parallel arrays into a serial→IPv6 map. *(done
|
|
234
|
+
-- also found and recorded a fourth parallel array,
|
|
235
|
+
`BladeBleAddressArray` (3714, each peer's real BLE MAC address), not
|
|
236
|
+
used by relay itself but included in constants.py for completeness.
|
|
237
|
+
**Confirmed via real hardware testing to be Cowboy-hub-specific** --
|
|
238
|
+
see item 6 below for the alternative this made necessary.)*
|
|
239
|
+
4. **`RelayedMobiusDevice`** (`mobius.relay`) -- a `MobiusDevice` subclass
|
|
240
|
+
that overrides `get_attribute()`/`get_attribute_raw()`/`set_attribute()`
|
|
241
|
+
to route via CoAP through a gateway device instead of owning its own
|
|
242
|
+
connection. Every other inherited method (`get_device_summary()`,
|
|
243
|
+
`get_current_light_intensities()`, everything) works transparently
|
|
244
|
+
through relay, since they're all built in terms of those three
|
|
245
|
+
methods. *(done -- confirmed via a test that exercises an inherited
|
|
246
|
+
high-level method, not just the overridden primitives directly)*.
|
|
247
|
+
Also confirmed a subtle, easy-to-get-wrong detail directly from the
|
|
248
|
+
one real call site in the decompile: the outer CoAP method is always
|
|
249
|
+
`GET`, even for a relayed Set -- the actual operation is determined by
|
|
250
|
+
the *inner* FSCI opcode, not the CoAP transport layer.
|
|
251
|
+
5. **CLI support** (`mobius-scan --dump-mesh-peers` /
|
|
252
|
+
`--relay-target SERIAL`) -- a way to actually validate this against
|
|
253
|
+
real hardware: connect to one device (the gateway), discover its mesh
|
|
254
|
+
peers, and relay a full `get_device_summary()` read to a specified
|
|
255
|
+
target through it. Run a separate direct `--by-serial <target>`
|
|
256
|
+
command to confirm the two match. *(done -- this is the step that
|
|
257
|
+
actually answers the biggest open question: whether these devices run
|
|
258
|
+
Thread at all. Everything above this point is implemented and unit-
|
|
259
|
+
tested, but genuinely unverified against real hardware until this is
|
|
260
|
+
run.)* Added a verbose, step-by-step diagnostic path
|
|
261
|
+
(`_debug_mesh_discovery()`) after real hardware testing showed every
|
|
262
|
+
device returning "No peers found" with no further detail --
|
|
263
|
+
`discover_mesh_peers()`'s production fail-soft behavior (the right
|
|
264
|
+
choice for library use) collapses several genuinely different failure
|
|
265
|
+
modes into the same empty result, which made the CLI output useless
|
|
266
|
+
for telling apart "this device doesn't support Thread at all" from
|
|
267
|
+
"it supports Thread but isn't on an active network right now" from
|
|
268
|
+
"the arrays came back empty." The CLI now surfaces each step's raw
|
|
269
|
+
result or exception individually instead.
|
|
270
|
+
6. **Cowboy-hub-free peer discovery** (`MobiusDevice.
|
|
271
|
+
get_own_mesh_address()` / `mobius.discovery.
|
|
272
|
+
discover_mesh_peers_via_direct_connect()` / `mobius-scan
|
|
273
|
+
--build-peer-map`) -- added after real hardware testing confirmed
|
|
274
|
+
item 3's three-array approach requires a Cowboy hub, which this
|
|
275
|
+
project's test hardware doesn't have. Connects directly and briefly to
|
|
276
|
+
each device in turn to read its own `MeshLocalAddresses`, confirmed
|
|
277
|
+
via `PeripheralConnection.java` to be exactly how the app itself
|
|
278
|
+
always learns a device's mesh address (`getDeviceInfo()`, called
|
|
279
|
+
automatically on every connection as standard setup, not something
|
|
280
|
+
Cowboy-specific). *(done -- 10 new tests. This is the discovery
|
|
281
|
+
mechanism to actually use going forward; `discover_mesh_peers()`
|
|
282
|
+
remains in place as a cheap, fail-soft first attempt in case a Cowboy
|
|
283
|
+
hub IS present, but isn't the primary path anymore.)*
|
|
284
|
+
|
|
285
|
+
**Follow-up fix**: `--relay-target` was still only looking through the
|
|
286
|
+
Cowboy-specific peer list after this item landed -- it was never wired
|
|
287
|
+
to fall back to a direct connection, so real hardware testing hit
|
|
288
|
+
"not found among discovered peers" even with this new discovery
|
|
289
|
+
mechanism in place. Fixed: `--relay-target` now falls back to briefly
|
|
290
|
+
connecting directly to the target itself (via `find_device_by_serial()`
|
|
291
|
+
+ `get_own_mesh_address()`) if the Cowboy-hub lookup doesn't find it.
|
|
292
|
+
7. **`--debug-relay`** -- added after `--relay-target` reached the
|
|
293
|
+
target but `get_device_summary()` came back entirely empty
|
|
294
|
+
(model/serial/everything `None`), with no visible error. Turned out
|
|
295
|
+
every individual attribute fetch WAS raising an exception internally
|
|
296
|
+
(a too-short, malformed CoAP response) -- just silently, since
|
|
297
|
+
`get_device_summary()`'s own per-field fail-soft handling (correct
|
|
298
|
+
behavior for genuinely optional attributes) was catching and
|
|
299
|
+
discarding them without any visible trace. Prints raw bytes at every
|
|
300
|
+
layer of a relayed request/response (outer frame, CoAP envelope
|
|
301
|
+
fields, inner frame, decoded status/values) so this stops being
|
|
302
|
+
invisible. Verified with a mocked round-trip that the debug output
|
|
303
|
+
itself is correct and doesn't interfere with normal operation before
|
|
304
|
+
asking for another real hardware run.
|
|
305
|
+
8. **Discovered the immediate confirm to a relay request is explicitly
|
|
306
|
+
discarded by the app itself** -- confirmed directly from
|
|
307
|
+
`BaseConnection.java`'s `handleBytes()` (see "What's confirmed" above)
|
|
308
|
+
after `--debug-relay`'s raw bytes showed every relayed read's outer
|
|
309
|
+
response was exactly 1 byte, too short to be a valid CoAP envelope at
|
|
310
|
+
all. The real response-correlation mechanism lives in
|
|
311
|
+
`handleIndication()`, a method JADX couldn't decompile -- a genuine
|
|
312
|
+
limit of what source tracing alone could determine at this point.
|
|
313
|
+
Added `MobiusDevice.on_any_frame` (a hook called for every parsed
|
|
314
|
+
incoming frame, not just ones resolving a pending request) and
|
|
315
|
+
extended `--debug-relay` to watch for 2 seconds after the initial
|
|
316
|
+
confirm. 4 new tests for the hook itself.
|
|
317
|
+
9. **Root cause found and fixed**, after the user provided the raw smali
|
|
318
|
+
disassembly for `BaseConnection.java` -- JADX's decompiler had given
|
|
319
|
+
up on `handleIndication()` specifically, but the underlying bytecode
|
|
320
|
+
was still traceable. This resolved everything precisely: the real
|
|
321
|
+
response arrives on a completely separate opcode (`0xDF`/`26`,
|
|
322
|
+
`COAP_INDICATION_OPCODE` -- not the request's `25`), matched purely by
|
|
323
|
+
CoAP token (`lambda$getSentRequest$6`:
|
|
324
|
+
`coapRequest.getToken() == coapResponse.getToken()`), never by the
|
|
325
|
+
outer FSCI message ID. Implemented:
|
|
326
|
+
`MobiusDevice._register_coap_token()`/`wait_for_coap_response()` (a
|
|
327
|
+
token-keyed pending-response registry, separate from the existing
|
|
328
|
+
message-ID-based `self._pending`), wired into `_on_rx_final()`.
|
|
329
|
+
`RelayedMobiusDevice._relay()` was rewritten around this: registers
|
|
330
|
+
the token before sending (eliminating any race against the response
|
|
331
|
+
arriving first), sends without waiting for the meaningless confirm,
|
|
332
|
+
and awaits the token-matched indication instead. `test_relay.py`
|
|
333
|
+
rewritten entirely for the new architecture; new dedicated
|
|
334
|
+
`test_coap_indication_handling.py` for the token-registry mechanism
|
|
335
|
+
itself. Verified end-to-end with a full mocked round-trip (both the
|
|
336
|
+
discarded ack and the real indication arriving, in that order) before
|
|
337
|
+
asking for another real hardware run. **This is the next real test.**
|
|
@@ -164,3 +164,27 @@ Both are included automatically in `get_device_summary()`'s
|
|
|
164
164
|
`get_device_summary()` already has the device's `Model` on hand).
|
|
165
165
|
`get_hardware_info()` is not currently wired into the summary -- call it
|
|
166
166
|
directly if you want it.
|
|
167
|
+
|
|
168
|
+
## Device clock drift (`get_device_time_info()`)
|
|
169
|
+
|
|
170
|
+
`Epoch` (201) / `RTCTime` (219) -- two separate device-reported UNIX
|
|
171
|
+
timestamps, with drift computed against local system time. **This is not
|
|
172
|
+
a speculative diagnostic** -- it's confirmed via the app's own
|
|
173
|
+
`TimeDifferenceFragment.java` ("Time Difference" under Troubleshoot in
|
|
174
|
+
the app): it checks both of these attributes for drift against the
|
|
175
|
+
phone's clock, using this exact same computation
|
|
176
|
+
(`local_time_seconds - device_reported_seconds`), treating a large
|
|
177
|
+
discrepancy as a real, known failure mode.
|
|
178
|
+
|
|
179
|
+
**Important caveat**: the app's own schedule interpolation
|
|
180
|
+
(`WidgetViewCell.java`) uses the phone/tank's own clock
|
|
181
|
+
(`selectedTank.getDateTime()`), NOT either of these device-reported
|
|
182
|
+
values -- so this does not directly explain a mismatch between this
|
|
183
|
+
library's computed schedule value and the app's displayed one. It's
|
|
184
|
+
still worth checking as a general device-health signal, exactly as the
|
|
185
|
+
app's own troubleshooting tool treats it, but a clean result here doesn't
|
|
186
|
+
rule out a schedule-interpolation bug, and a drifted result here doesn't
|
|
187
|
+
necessarily explain one either.
|
|
188
|
+
|
|
189
|
+
Included automatically in `get_device_summary()`'s `"device_time"` field.
|
|
190
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-mobius"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "GPL-2.0-only" }
|