Files
franklinwh-agate-client/docs/BATTERY_POC.md
T

107 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)` with `data={"update": 2}` (exactly what
the app's `queryAPowerData` passes as `arguments`), then `BMS (0x1D)`,
`EMS (0x1E)`: compact-JSON `CommunicationObject`
(`cmdType, equipNo, type, timeStamp, snno, len, crc[, dataArea]`, no
`data` key) → `encrypt_object` (`S1` plaintext header first, tail
`(byte + abs_index + n) & 0xFF` with `n = int(key, 32)`) → sent **raw**
over the socket (no length prefix — `ClientSocket.send` forwards to
`_Socket.add` directly).
3. Accumulates reply chunks and runs the app's `decryptData` logic (second
`0x58` marker → `s + "}"` equipNo probe → tail decode → `s + "," + T`),
then 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
# low-level probe: exact TX bytes + exact RX bytes per request variant
python -m franklinwh_agate_client.battery --sn <20-char-SN> --probe
```
Options: `--host` (default `10.100.1.1`), `--ports 9000,18000`,
`--key-radix {16,32}` (default 32, per the `radix: 32` literal at both
`tryParse` sites), `--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
```
## Host probe recipe (run on the machine joined to the device AP)
The agent container cannot reach `10.100.1.x`; run this on the host:
```bash
cd franklinwh-agate-client
python3 -m franklinwh_agate_client.battery \
--sn A03X2516013210060005 --host 10.100.1.1 --ports 9000,18000 --probe
```
> Prefer the standalone scripts in `scripts/` — they need no install and
> track the latest corrections: `host_probe4.py` is current (socket-space
> ids + `opt`-seeded payloads). Copy the file content and run
> `python3 host_probe4.py --sn A03X2516013210060005 --ports 9000`.
Expected: `secret key for ...: 1c (radix 32)`, then per-variant `tx`
(encrypted envelope, starts with the ASCII header
`{"cmdType":25,"equipNo":"A03X2516013210060005",`) and `rx` bytes.
A rejected frame is met with zero bytes + close; any non-empty `rx` is
the device answering — paste the `rx` hex back and the reply parses via
`decrypt_data`.
If every variant still returns zero bytes, capture one app-driven session
instead: install the Frida hook (`blutter_out/blutter_frida.js` pattern —
log `Socket Manager send object: ` / `receive object: `), tap *Refresh* in
the app while joined to the AP, and compare its TX bytes against ours.
## Status (2026-10-16) — live round-trip works
Two-way comms verified against `10.100.1.1:9000` from a host on
`10.100.1.0/24`: discovery → `1102` device-info reply (versions, `FHP_SN`,
`BMS_SN`), per-pack BMS reads (`{"id": 1}` / `{"id": 2}` → live `batSoc`),
EMS telemetry. `get_battery()` runs that exact sequence (discovery,
apower, BMS packs 1–2, EMS) and returns the first SoC found; an all-zero
BMS slot is skipped, not reported as 0 %.
`DEVICE_APOWER_INFO` answers `{"result": 1, "reason": -1}` on the tested
unit — SoC comes from the BMS packs instead.
Remaining limits:
- Needs L2 reachability to `10.100.1.1` (host on the device AP or bridged
container); the app's own socket port `18000` is closed on this unit.
- BLE field order and `X-Device-*` binding remain open
(`RE_DART_FINDINGS.md` Open items).