Add SoftAP battery PoC (apower/BMS/EMS socket read) with docs and tests
Probes 10.100.1.1:9000,18000, sends 0x19/0x1D/0x1E envelopes, extracts currentBatterySoc/soc/avg(socArray). Includes offline extract_soc tests, BATTERY_POC docs, README usage, and empirical port-9000 RE notes.
This commit is contained in:
1 parent
4c0b9de3df
commit
2438a04ed5
6 files changed
+359
-3
No files matched your search
@@ -0,0 +1,58 @@
|
||||
# Battery PoC — direct-connect charge state
|
||||
|
||||
Reads the aggregate battery percentage straight from the aGate over its own
|
||||
SoftAP. No cloud, no MQTT, no Bluetooth. Recovered from the Dart AOT layer;
|
||||
see `RE_DART_FINDINGS.md` §§3–5, 8, 10.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Host whose Wi-Fi adapter is associated to the device AP
|
||||
(`AP_<sn[11:]>` / `<sn[8:]>`, `wifi.derive_hotspot_credentials`).
|
||||
- L2 reachability to `10.100.1.1`. In a container this means a bridged
|
||||
`macvlan`/`ipvlan` address on `10.100.1.0/24` or `network_mode: host`
|
||||
(see repo-root `.devcontainer/docker-compose.yml`). A default Docker
|
||||
bridge (`172.17.0.0/16`) is NATed to a non-`10.100.1.x` source and gets
|
||||
`ECONNREFUSED`.
|
||||
- Python `requests` for the HTTP helpers in `device_local.py`.
|
||||
|
||||
## What it does
|
||||
|
||||
1. Probes TCP ports `9000, 18000` in order (`9000` was open on the tested
|
||||
unit; `0x4650 = 18000` is the app constant but was closed there).
|
||||
2. Sends `DEVICE_APOWER_INFO (0x19)`, then `BMS (0x1D)`, `EMS (0x1E)`:
|
||||
compact-JSON `CommunicationObject` → `encrypt_object` (key
|
||||
`hex(sum(sn) & 0xFF)`) → int16LE length framing.
|
||||
3. Decrypts the reply and extracts `currentBatterySoc` → `soc` →
|
||||
`avg(socArray)`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# show derived hotspot creds only
|
||||
python -m franklinwh_agate_client.battery --sn <20-char-SN> --show-hotspot
|
||||
|
||||
# print just the percentage
|
||||
python -m franklinwh_agate_client.battery --sn <20-char-SN>
|
||||
|
||||
# full reply envelope
|
||||
python -m franklinwh_agate_client.battery --sn <20-char-SN> --raw
|
||||
```
|
||||
|
||||
Options: `--host` (default `10.100.1.1`), `--ports 9000,18000`,
|
||||
`--key-radix {16,32}` (default 16; 32 is the alternate candidate in the
|
||||
disassembly), `--timeout`.
|
||||
|
||||
Programmatic:
|
||||
|
||||
```python
|
||||
from franklinwh_agate_client import get_battery
|
||||
out = get_battery("...", host="10.100.1.1")
|
||||
print(out["value"], out["source"]) # e.g. 78.0 currentBatterySoc
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
- No live round-trip has succeeded yet from an isolated container netns
|
||||
(all probes `ECONNREFUSED`); first success needs a `10.100.1.x` source.
|
||||
- `key_radix`, BLE field order, and `X-Device-*` binding remain open
|
||||
(`RE_DART_FINDINGS.md` Open items).
|
||||
@@ -193,6 +193,25 @@ authS2C=2 "fwh/s2c/auth/" mngC2S=5 "fwh/c2s/mng/" deviceS2C "fwh/s2c/
|
||||
- a `Duration` and retry loop (up to ~2 retries) are involved.
|
||||
- `BytesToInt16/Int16ToBytes` LE framing; `Socket Manager send object` / `receive object`.
|
||||
|
||||
### Empirical — live aGate on its SoftAP (2026-10-13, from a client on `10.100.1.0/24`)
|
||||
|
||||
Full TCP sweep of `10.100.1.1` (ports 1–65535): exactly **two** ports open — **53** and **9000**. No 80/443, no web UI, and **18000 (`0x4650`) is closed** on this unit.
|
||||
|
||||
- **Port 53** — authoritative, *closed* DNS: well-formed `NXDOMAIN` for everything
|
||||
(even `example.com`), no PTR for `1.1.0.100.in-addr.arpa`, no forwarding (the SoftAP
|
||||
is an island). Serves some local name pattern we haven't identified yet.
|
||||
- **Port 9000** — a **length-prefixed framing service** (int16LE length), consistent
|
||||
with the app's socket layer:
|
||||
- every TLS `ClientHello` gets `UNEXPECTED_EOF` mid-handshake — first two bytes
|
||||
`16 03` = int16LE `790`, so the decoder waits for a 790-byte frame that never
|
||||
completes, then closes on idle;
|
||||
- cleartext HTTP `GET` (`47 45` = `17735`) hangs forever (read timeout).
|
||||
- A *complete* 147-byte frame (int16LE length + encrypted `QUERY_DEVICE_INFO`
|
||||
envelope, `equipNo = X25160132`, key from `sum(utf8(sn))&0xFF`) is accepted
|
||||
but met with **zero bytes** — every command tried (`0x00`/`0x01`/`0x02`/`0x06`)
|
||||
and both key radices (16/32). Content-level rejection; the leading hypothesis is
|
||||
`equipNo` validation against the real serial. **Pending: retry with the true SN.**
|
||||
|
||||
## 9. Bluetooth (BLE) transport — `communication_center/bluetooth`
|
||||
|
||||
- `BluetoothManager` (singleton), `BluetoothStatus`: `closed / connected / connecting / interrupted`.
|
||||
@@ -323,9 +342,15 @@ Still open:
|
||||
|
||||
1. Exact BLE frame field order & the 2-byte magic semantics (confirm with `btsnoop`).
|
||||
2. Exact `encryptObject` radix + trailing-header handling.
|
||||
3. Local HTTP port + TLS behavior for `https://IP:PORT` (device self-signed cert?).
|
||||
4. Local socket port confirmation (`18000` candidate) and whether local RPC uses
|
||||
socket vs `/terminal` HTTP vs local MQTT.
|
||||
3. ~~Local HTTP port + TLS behavior for `https://IP:PORT`~~ — **revised (2026-10-13,
|
||||
§8 empirical):** no HTTP/TLS port exists on the device at all (only 53 + 9000).
|
||||
Either local mode is socket-only on this firmware, or the HTTP service is
|
||||
state-dependent and wasn't running. Revisit if a `/terminal` response is ever
|
||||
captured from the SoftAP.
|
||||
4. ~~Local socket port confirmation~~ — **resolved on one unit (2026-10-13): `9000`**
|
||||
(§8 empirical). The `0x4650` constant is not listening; the port may be
|
||||
firmware-/device-dependent. Remaining: first confirmed RPC round-trip
|
||||
(blocked on the real SN — see §8 empirical).
|
||||
5. Which class in `classes5.dex` sets the `X-Device-*` headers and their exact values
|
||||
(string-pool location confirmed; class binding in progress — §13).
|
||||
6. (Tooling, optional) Enable REA's DEX tools: obtain a **jadx-headless-mcp 0.7.1** JAR
|
||||
|
||||
Reference in new issue
Block a user