wifi.py presented itself as a join/leave abstraction but provided no real
behavior: the default backend was a no-op, the facade never verified the
association, and the nmcli queries were wrong. This makes the module do what
it claims, verified end-to-end against the recovered protocol and nmcli 1.52.
wifi.py
- WifiManager: join now verifies the association by polling
backend.is_connected against a settle_timeout (fail-closed). An
unverified join raises HotspotJoinError and leaves connected=False;
verify=False restores fire-and-forget. Tracks last_profile and adds
gateway_ip().
- NmcliWifiBackend: the association check now uses the ACTIVE column
(`device wifi list -f SSID,ACTIVE`). The old `device wifi show` was a
prefix-match accident onto `show-password` and reported in-range APs, not
the associated one. get_ip() queries per-Wi-Fi-interface IP4.ADDRESS
(preferring the 10.100 aGate subnet) instead of the unfiltered all-device
dump that would return a wired/VPN address. Signal is read from the active
AP; the dead ipcmd param is removed; leave() is best-effort.
- WifiBackend: documented the fail-closed contract and added get_signal()
so the manager no longer reaches into a private _run via isinstance.
- NoOpWifiBackend: added assume_connected() for manual/external joins.
client.py
- AgateClient now owns a WifiManager and exposes join_hotspot(),
leave_hotspot(), and attach_local() (join + verify + LocalDeviceClient
aimed at the device gateway). LocalDeviceClient is imported lazily so the
package still works without `requests`.
__init__.py
- Export the backends, HotspotProfile, HotspotJoinError, the SN-derivation
helper, and the module constants.
tests/test_wifi.py (new)
- 20 stdlib-unittest cases: SN derivation, verify pass/fail, fail-closed
behavior, the in-range-but-not-associated regression, IP preference,
tolerant leave, and the client wiring. No radio or pytest required.
Also:
- README: documented the local-connect (device hotspot) flow.
- .gitignore: ignore Python build/cache artifacts.
Reconstruct the app's Dart-layer behaviour (blutter over libapp.so,
jadx over base.apk — see docs/RE_DART_FINDINGS.md) as working Python,
replacing the library stubs with a real local-connect stack.
protocol/ (new package, hardware-independent):
- commands: CommunicationMode / CommunicationOperate / CommunicationCmd
enums with the raw values recovered from the Dart object pool
- envelope: CommunicationObject with the app's exact JSON key order
- crypto: SN-derived secret key and the position-dependent additive
cipher (encrypt/decrypt; radix 16 default, 32 documented alternate)
- crc: CRC-16/MODBUS with the app's hi/lo swap + 4-hex string form,
CRC32 envelope check, little-endian int16 helpers
- framing: BLE frame build/parse (0x5AA5 data / 0x4AA4 ACK magics,
CRC slots, MTU chunking) and local-socket int16LE length framing
plus a streaming SocketFrameDecoder
wifi.py (rewritten):
- derive_hotspot_credentials(): the AP_<sn> SSID/password rules
(20-char SN branch, full-SN fallback with the app's warning log)
- WifiBackend abstraction: NmcliWifiBackend for Linux hosts via
nmcli, NoOpWifiBackend for radio-less containers
- WifiManager keeps the original stub method names for API compat
and adds join_device_hotspot(sn) / leave_device_hotspot() / local_ip()
device_local.py (new): LocalDeviceClient for the mode==local path —
the /terminal/* HTTP endpoint family (hotspot info, aGate info,
overall info, MQTT relay, ...) and direct-connect socket RPC
(envelope -> cipher -> length frame -> decrypt -> JSON) with
convenience query_* RPCs.
docs/RE_DART_FINDINGS.md (new): the full findings — transport
selector, wire envelope, crypto, CRCs, MQTT (AWS IoT + Cognito),
socket, BLE + ESP provisioning, hotspot credential derivation,
local/cloud HTTP layers, and the Java glue — plus the open items;
README now links to it.
Unconfirmed candidates (local port 18000, cipher key radix, BLE
field order, X-Device-* header values) are flagged in the docs and
kept configurable in code.