Files
franklinwh-agate-client/docs/RE_DART_FINDINGS.md
T
kfj001 9e69f07636 Correct wire format to disassembly: raw S1||crypt(S2), radix 32, int snno/seconds, decryptData response parser, --probe
- envelope: 7 fixed keys + conditional dataArea, no data key; int
  type/snno, seconds timestamp, data setter auto len/crc (CRC32 upper);
  SequenceNumber 1..9999 (increaseSN)
- crypto: encrypt is plaintext-header-first a||transform(b), key radix 32
  at both tryParse sites; decrypt_data ports decryptData (dual-0x58
  markers, s+'}' equipNo probe, s+','+T return); decrypt_request kept
  for the device-side known-header split
- socket: raw send (no length prefix), accumulate-and-try receive
- battery: queryAPowerData shape (data={'update':2}), radix default 32,
  --probe prints exact TX/RX bytes per variant; device_local.send_probe
- docs: RE_DART_FINDINGS ss3/5/8 + open items, BATTERY_POC host recipe
- tests: 19 new protocol tests, suite 44/44 green
2026-10-15 23:01:58 +00:00

406 lines
22 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.
# FranklinWH aGate — Dart AOT Reverse-Engineering Findings
Target: `split_config.arm64_v8a.apk` → `lib/arm64-v8a/libapp.so` (arm64 Dart AOT snapshot),
plus Android glue in `base.apk`.
## Tooling (installed & working)
- **blutter** (Dart AOT runtime reconstruction) at `/tmp/opencode/blutter`.
- Host is `aarch64` (matches the binary arch).
- Dart snapshot version detected automatically: **3.11.5**.
- Build deps installed via apt: `python3-pyelftools python3-requests cmake ninja-build
build-essential pkg-config libicu-dev libcapstone-dev`.
- Output: `/tmp/opencode/blutter_out/`
- `asm/<package>/<file>.dart` — per-file Dart "assembly" **with symbols, field offsets,
called-function names and inline string literals** (this is the readable form).
- `pp.txt` — object pool incl. **enum instances with their names + raw values**.
- `objs.txt`, `ida_script/`, `blutter_frida.js`.
- **jadx 1.5.6** (Java/Kotlin decompiler) for the `base.apk` DEX glue.
- Java 21 (`OpenJDK 21.0.12.1`, `/usr/bin/java`); CLI at `/usr/local/bin/jadx`,
full install under `/home/node/jadx-1.5.6`
(`bin/jadx`, `bin/jadx-gui`, `lib/jadx-1.5.6-all.jar`).
- Full decompile of `base.apk` at `/tmp/opencode/jadx-out/base/`
(`sources/` — **18,617 `.java` files** — and `resources/` incl. the decoded
`AndroidManifest.xml`). Reproduce:
`jadx -d /tmp/opencode/jadx-out/base /workspaces/typescript-node/base.apk`
- REA MCP's DEX tools (`search_android_classes`, `inspect_android_class`, …) need
`REA_JADX_MCP_JAR` → a **jadx-headless-mcp 0.7.1** JAR. That JAR is *not* shipped in
the jadx 1.5.6 distribution and is not configured yet — not a blocker, grepping the
decompiled `sources/` has covered everything so far.
Reproduce:
```
python3 /tmp/opencode/blutter/blutter.py /tmp/opencode/apk/lib/arm64-v8a /tmp/opencode/blutter_out
```
> Note: blutter yields *annotated disassembly + recovered objects*, not high-level source.
> String literals, class/field names, enum values, and call graphs are reliable; arithmetic
> is read from the disassembly.
---
## 1. Package layout (relevant)
```
communication_center/
core/communication_object.dart # wire envelope
core/communication_cmd.dart # command enum + paramKey
core/communication_mode.dart # transport enum
core/encryption.dart # secret key + payload transform
core/base_communication.dart
mqtt/mqtt_manager.dart # cloud (AWS IoT) transport
mqtt/mqtt_topic_type.dart # cmd -> topic mapping
socket/client_socket.dart, socket_manager.dart # local TCP transport
bluetooth/bluetooth_manager.dart, bluetooth_frame.dart, bluetooth_write_queue.dart
utils/crc16.dart, crc32.dart, utils.dart
franklinwh2/services/communication_service/communication_service.dart # RPC layer
franklinwh2/services/remote_service/remote_interface.dart # HTTP RPC layer
franklinwh2/services/eaton_service/eaton_ble_transport.dart # ESP-svc BLE transport
franklinwh2/utils/device_wifi_utils.dart # hotspot join + creds
esp_provisioning_ble/src/... # ESP protocomm
```
## 2. Transport selector — `CommunicationMode`
| name | raw |
|---|---|
| mqtt | 0 |
| socket | 1 |
| bluetooth | 2 |
| http | 3 |
| local | 4 |
`CommunicationCenter` exposes `connectOrganized`, `sendObjectOrganized`,
`completeFutureConnect/Request`, `timeoutFutureConnect/Request`, `disposeFutureConnect/Request`,
`matchedRequest`. It contains sentinels `"do not support http communication"` and
`"do not support local communication"` (those modes are handled elsewhere).
## 3. Wire envelope — `CommunicationObject`
JSON keys, in the exact insertion order used by `toJson`:
```
cmdType, equipNo, type, timeStamp, snno, len, crc[, dataArea]
```
`dataArea` is appended **only when the payload is present and non-empty**;
there is **no** `data` key on the wire. `cmdType` = `CommunicationCmd` raw,
`type` = `CommunicationOperate` raw (always 0 on the `communicate`-factory
path — the ctor zeroes it), `equipNo` = device SN (`"00000000"` fallback
when empty), `timeStamp` = whole **seconds** (`micros ~/ 1000 ~/ 1000`),
`snno` = `int` sequence (see below, **not** the serial string), `len` =
UTF-8 byte length of the compact JSON of the payload, `crc` =
`Crc32.calculate(jsonEncode(data))` as **uppercase** unpadded hex.
Null/empty payload ⇒ `len == 0`, `crc == ""`, no `dataArea` key.
- `CommunicationOperate`: `query=0, set=1, command=2, modify=5`.
- `Crc32::calculate` is used in `fromJsonString` (integrity check of the payload).
- `toJsonString()` = `jsonEncode(toJson())` — compact JSON, the input of
`encryptObject` (see §5). `toString()` likewise returns `toJsonString()`.
- `CommunicationObject(cmd, equipNo, {data})` ctor zeroes `type`/`snno`,
stamps seconds, and routes `data` through the `data=` setter (which
recomputes `len`/`crc`).
- `sendObject` stamps `snno = increaseSN()` first: a 1-based counter that
wraps to 1 past 9999 (manager field starts at 0, so the first request
carries `snno == 1`).
- The `communicate` factory (`0xc9264c`) builds the `data` map from the
caller's `arguments` (e.g. `queryAPowerData` passes `{"update": 2}` with
cmd `deviceApowerInfo`/`0x19`, operate `query`) and calls the ctor with
a named `data:` argument.
## 4. `CommunicationCmd` (raw → name)
```
0x00 queryDeviceInfo 0x13 pairPower 0x26 smartLoad2
0x01 networkInfo 0x14 processPairPower 0x27 rebootDevice
0x02 resourceAddress 0x15 checkBluetoothPassword 0x28 debugMode
0x03 sunspecModbus 0x16 goOffGridStatus 0x29 powerOn
0x04 networkSwitch 0x17 aPowerLedSwitch 0x2a chargeDevice
0x05 feedingGridPower 0x18 deviceDetailInfo 0x2b clearFault
0x06 mode 0x19 deviceApowerInfo 0x2d deviceAddressInfo
0x07 modeV2 0x1a deviceAlarms 0x2e overloadCutOff
0x08 modeSettingV2 0x1c pe 0x2f wifiFrequecy
0x0a solarInfo 0x1d bms 0x30 breakerCommissionNotify
0x0b energyManageInfo 0x1e ems 0x32 breakerComState
0x0c generatorInfo 0x1f mppt 0x33 breakerLoadStatus
0x0d bluetoothHeartbeat 0x20 aHub 0x34 breakerIdentifyMe
0x0e scanWifiList 0x21 ctTestReverse
0x0f connectWifi 0x22 deviceData
0x10 connectionStatus 0x23 loadStatus
0x11 networkChannelStatus 0x24 loadStatus2
0x12 networkChannel 0x25 smartLoad
```
(missing: 0x09, 0x1b, 0x2c, 0x31 — not surfaced as instances.)
## 5. Encryption — `communication_center/core/encryption.dart`
**`generateSecretKey(String equipNo)`** = hex of `(sum(utf8(equipNo)) & 0xFF)` (via
`reduce(sum)` → `&0xFF` → `_toPow2String(16)`), i.e. a 1-byte key rendered as hex,
unpadded. E.g. SN `A03X2516013210060005` → byte sum `1052 & 0xFF = 28` → `"1c"`.
**`encryptObject(object, key)`** (object in arg slot 0, key in slot 1):
1. `s = object.toJsonString()`; split on `","`.
2. `S1 = parts[0] + "," + parts[1] + ","` (header, *with* trailing comma);
`a = UTF-8(S1)`.
3. `S2 = join(",", parts[2..])` (the rest); `b = UTF-8(S2)`.
4. `keyInt = int.tryParse(key, radix: 32)` (literal `0x20` at the call site;
null → 0).
5. `out[i] = (b[i] + i + len(a) + keyInt) & 0xFF` for all i (so each tail
byte is shifted by its *absolute* JSON index plus the key).
6. Result = `a ++ out` — the plaintext header goes on the wire **first**,
transformed tail after (`addAll` onto the header copy).
**`decryptData(bytes)`** (single arg, the accumulated receive buffer):
1. `i = indexWhere(byte == 0x58)`, `j = indexWhere(byte == 0x58, start: i+1)`;
either missing → return `""`.
2. `s = utf8(wire[0..j])`; `probe = jsonDecode(s + "}")`, null → `""`.
3. `equipNo = probe["equipNo"]`; `key = generateSecretKey(equipNo)`;
`keyInt = int.tryParse(key, radix: 32) ?? 0`.
4. `tail = wire[j+1..]`; `out[k] = (tail[k] - k - len(utf8(s + ",")) - keyInt) & 0xFF`.
5. Return `s + "," + utf8(out)` (the `","` re-inserts the comma the `0x58`
separator replaced). The caller (`_onSocketReceiveData`) `jsonDecode`s
that, builds `CommunicationObject.fromJson`, logs
`Socket Manager receive object: …`, and clears the buffer; `""`/parse
failures keep accumulating ("receive incomplete data").
So requests are `S1 || transform(S2)` (split at the computable header
length) while responses are `s || 0x58 || transform(tail)` (split at the
second `0x58` marker). Both halves share the one additive stream.
## 6. Checksums & byte order
- **CRC16** (`utils/crc16.dart`): init `0xFFFF`, poly `0xA001` (reflected), i.e.
**CRC-16/MODBUS**; then hi/lo bytes swapped; returned as a **4-hex-digit string**
(`_toPow2String(16)`).
- **CRC32** (`utils/crc32.dart`) used for the JSON envelope integrity.
- `utils.int16ToBytes` / `bytesToInt16` = **little-endian** 16-bit.
## 7. MQTT transport (cloud) — `communication_center/mqtt` + Java bridge
Dart side: manager singleton `MqttManager._defaultManager`; endpoint constant
**`https://a6f63crce3ufk-ats.iot.us-east-2.amazonaws.com`** (AWS IoT custom domain).
Java side (`com.p000franklinwh.mqtt_plugin.MqttPlugin`, verified from the jadx decompile):
- Method channel `com.franklinwh/plugins/mqtt_plugin/method_channel`, events on
`…/event_channel` (name from `MqttStatus`).
- Method surface: **`setup / connect / disconnect / subscribe / unsubscribe / publish`**
(the earlier `init / setEndpoint / …` guess was wrong).
- `setup(regionType, identityPoolId, endPointUrl)`:
- `regionType`: **2 → `us-east-2`, 3 → `us-west-1`, 4 → `us-west-2`** (any other value
is rejected before setup).
- Client = **`AWSIotMqttManager`** (AWS IoT Android SDK, not raw Paho) with
**clientID = random `UUID` per setup**, **keepAlive = 10 s**.
- Credentials: **`CognitoCachingCredentialsProvider`** (unauthenticated Cognito
identity pool `5deecc22-4e7f-43b3-8f1d-e54137220126`) → AWS IoT credentials
provider; MQTT over TLS with temporary credentials issued by AWS IoT — no
app-side client certs.
- Last-will: topic **`my/lwt/topic`**, payload **`"Android client lost connection"`**,
QoS 0.
- `connect` takes no arguments → `mqttManager.connect(credentialsProvider, statusCallback)`.
- `subscribe(topic)` → `subscribeToTopic(topic, QoS 0)`; `publish(topic, message)` →
`publishString(topic, message, QoS 0)`. Both string payloads.
- Event-channel events:
- `subscribe {topic, message}` — `message` = received bytes decoded as UTF-8;
- `connectionStatuesChange {status: <rawValue>}` — sic, "Statues" is the literal
event name; `status` = raw value of the `MqttStatus` mapping of the SDK status.
### Topics — `MqttTopicType` (name=raw → prefix)
```
undefined=0 "" rtC2S=3 "fwh/c2s/rt/" mngS2C=6 "fwh/s2c/mng/"
authC2S=1 "fwh/c2s/auth/" rtS2C=4 "fwh/s2c/rt/" deviceC2S=… "fwh/c2s/device/"
authS2C=2 "fwh/s2c/auth/" mngC2S=5 "fwh/c2s/mng/" deviceS2C "fwh/s2c/device/"
debugC2S/S2C "fwh/…/{c2s,s2c}/debug/"
```
`MqttTopicType.fromCmd(cmd)` selects the topic family for a `CommunicationCmd`.
## 8. Socket transport (local TCP) — `communication_center/socket`
- `SocketManager._defaultManager`, `ClientSocket` (getters `connected`/`closed`), status
enum `SocketStatus`: `closed / connected / connecting`.
- `CommunicationService._connectBeforeRequest` calls
`CommunicationCenter.connectOrganized(host:, port:)` with:
- host = stored host, else **`"10.100.1.1"`** (device hotspot gateway);
- port constant **`0x4650 = 18000`** (candidate local port),
- a `Duration` and retry loop (up to ~2 retries) are involved.
- `sendObject`: stamps `snno = increaseSN()`, logs
`Socket Manager send object: <json>`, derives the key with
`generateSecretKey(equipNo)`, calls `encryptObject(object, key)`, then
`_sendData`, which checks `connected` status and calls `ClientSocket.send`
→ **`dart:io _Socket.add` with the bytes as-is: raw send, no length
prefix, no markers, no handshake** — then clears the receive buffer.
- Receive: socket `listen` chunk → `ClientSocket._onSocketReceiveData` →
`SocketManager._onSocketReceiveData` → buffer `addAll` → `decryptData`
in try/catch (`""` = keep accumulating) → `jsonDecode` →
`CommunicationObject.fromJson` → `Socket Manager receive object: …` log.
### 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).
- Earlier probes are superseded: they used an int16LE length prefix (the
socket layer sends **raw**), the old `transform(b) || a` order (correct
is `a || transform(b)` — §5), a truncated `equipNo`, and default radix
16. All are corrected in the current PoC; see `BATTERY_POC.md` for the
host probe recipe with the true SN.
## 9. Bluetooth (BLE) transport — `communication_center/bluetooth`
- `BluetoothManager` (singleton), `BluetoothStatus`: `closed / connected / connecting / interrupted`.
- **Frame magics (2 bytes)**: `0x4A 0xA4` = ACK frame; `0x5A 0xA5` = data frame (triggers `_sendAck`).
- `bluetooth_frame.dart`:
- `createBluetoothFrame(content, ?, value)`: builds `content ++ [00 00 00 00] ++ int16LE(totalLen) ++ int16LE(value) ++ sn`,
computes `Crc16::calculate` over that, hex→2 bytes, writes them into the `00 00 00 00`
placeholder (indices 2..3). `totalLen = len(content)+6+len(sn)`.
- helpers: `magicEquals`, `magicInData`, `snInData`, `contentInData`, `mtuToData` (MTU chunking).
- Exact field order still to be confirmed against a live `btsnoop` capture.
- Heartbeat = Dart timer (`startBluetoothHeartbeat`, `cancelBluetoothHeartbeat`,
`_bluetoothHeartbeat`, `[Bluetooth] Heartbeat Timeout`), command `bluetoothHeartbeat` (0x0d).
### ESP provisioning (WiFi onboarding over BLE) — `esp_provisioning_ble`
- `/terminal`-style endpoints (ESP protocomm): **`prov-session`, `prov-scan`, `prov-config`, `custom-data`**.
- `EspProv` handles `EstablishSessionStatus`; `security.dart` (sec0) & `security1.dart` (sec1):
ECDH (Curve25519) → shared key, verification, `abcd1234` present as a PoP, AES-CTR (crypt.dart: key/iv/crypt).
- Service UUID `021a9004-0382-4aea-bff4-6b3f1c5adfb4` is used by
`franklinwh2/services/eaton_service/eaton_ble_transport.dart` (Eaton/EatonBreaker path).
The provisioning GATT characteristics are referenced indirectly; confirm via a device scan.
## 10. Local hot-spot credential derivation — `franklinwh2/utils/device_wifi_utils.dart`
`DeviceWifiUtils` (static):
- `getDeviceApSsid(sn)`:
- if `sn.length == 20` → `"AP_" + sn.substring(11)`; else log `"deviceSn is invalid"` →
`"AP_" + sn`.
- `getDeviceApDefaultPwd(sn)`:
- if `sn.length == 20` → `sn.substring(8)`; else → `"12345678"`.
- `connectDeviceWifiWithSSID(ssid, pwd)` → `wifi_iot` `connect`; `disconnectDeviceWifi` → `disconnect`;
`forceWifiUsage` toggles the process binding; caches last ssid/pwd in statics.
> The length check is `field_7 == 0x28` on a Smi length ⇒ `length == 20`.
> Derived values are the **offline fallback**; the authoritative values come from
> `/terminal/getHotSpotInfo` and `/terminal/getHosSpotPassword`.
## 11. HTTP layer — local vs cloud
Every operation has a **local** (`/terminal/…`) and **cloud** (`/manage/…`) route, selected by
mode. Local base is `https://IP:PORT` (string literal), device IP in `deviceIp`, plus four
`X-Device-*` headers (`-IP`, `-Referer`, `-Requested-With`, `-Accept-Language`) located in
the `base.apk` DEX string pool — class binding in progress, see §13.
**Local device endpoints (subset, `/terminal/`)**:
```
/terminal/getHotSpotInfo /terminal/getHotSpotInfo/v2 /terminal/getHosSpotPassword
/terminal/obtainAgateInfo /terminal/updateHotSpotInfo
/terminal/sendMqtt /terminal/getDeviceInfoV2 /terminal/getDeviceCompositeInfo
/terminal/queryGeneratorData /terminal/selectDeviceOverallInfo
/terminal/getGatewaySystemSetting /terminal/system/getSystemSetting
/terminal/initialize/installerOktaLogin /terminal/initialize/mfa/*
/terminal/mfa/* /terminal/getPersonalInfo /terminal/updatePersonalInfo
```
**Cloud**: `https://e-api.franklinwh.com/api/appapi`; `https://energy.franklinwh.com/{hes-gateway,
hes-tariff-collection, api-energy/*, api-smart/*, api-user/mfa/*, api-vpp-portal}`;
`https://api.franklin-wh.com:10443/api/appapi`; `https://sunrun.okta.com/oauth2/default`.
`RemoteInterface` returns `Stream<HttpResultEntity<…>>` per endpoint. `CommunicationService`
exposes typed RPCs: `queryDeviceInfo`, `queryAPowerData`, `queryCircuitsInfo`,
`queryModes1/2`, `querySmartLoadInfo`, `queryNewSmartLoadInfo`, `queryCTTestReverse`,
`queryAHubDetailInfo`, `queryEmsInfoBySocket`, `queryLoadStatusInfo`, `queryNewLoadStatusInfo`,
`queryOverloadCutOff`, `queryDebugModeInfo`, `queryChargePower`, `queryGoOffGridStatus`, plus
`_request(cmd, sn, operate, {arguments, mode})` as the core dispatch.
## 12. Eaton breaker / UDP keys
`eaton_service/eaton_udp_keys.dart` + `eaton_breaker_service.dart` deal with
`primary/secondary Unicast/Multicast UDP keys` (`etn_udp_keys_…`), versioned (`VER1`), keyed
storage, status strings (`Success`, `Failure to store UDP Keys`, `UDP Key Format error`).
## 13. Java glue (`base.apk`) — JADX decompile
`/tmp/opencode/jadx-out/base/sources` (18,617 files; see Tooling). The Java layer is
**platform plumbing only** — no device-protocol logic; the protocol lives entirely in
the Dart AOT layer. (The MQTT bridge is covered in §7.)
### `com.franklinwh.franklinwh2.method_channel.MethodHandlerManager`
Singleton holding a `LinkedHashMap<String, BaseMethodHandler>`; the constructor calls
`registerHandlers()`, which registers exactly five handlers on the
`com.FranklinWH.FamilyEnergy/method_channel` channel: `OpenSystemWifiSetting`,
`NetworkInfoCompat`, `BuildConfig`, `MigrateData`, `ShareInfoToOtherApp`
(all under `method_channel/method_handlers/`). All UI/system helpers — opening the
system Wi-Fi settings, network state, build config, DB migration, Android share sheet.
No device-communication surface.
### `com.alternadom.wifiiot.WifiIotPlugin` (the `wifi_iot` plugin)
Thin wrapper over Android `WifiManager` — this is how the app joins the device SoftAP.
Full method surface (26, from the Kotlin string dispatch):
```
connect, findAndConnect, disconnect, isConnected, setEnabled,
getIP, getSSID, getBSSID, getFrequency, getCurrentSignalStrength,
loadWifiList, registerWifiNetwork, removeWifiNetwork, isRegisteredWifiNetwork,
setSSIDHidden, isSSIDHidden, getClientList,
SoftAP: setWiFiAPEnabled, getWiFiAPState, isWiFiAPEnabled, getWiFiAPSSID,
setWiFiAPSSID, getWiFiAPPreSharedKey, setWiFiAPPreSharedKey,
forceWifiUsage, showWritePermissionSettings
```
Location-permission request codes: `65655435` (load list), `65655436` (listen),
`65655437` (find & connect), `65655438` (isConnected). Nothing protocol-specific.
### `X-Device-*` headers — located in DEX, class binding in progress
`X-Device-IP`, `X-Device-Referer`, `X-Device-Requested-With`, `X-Device-Accept-Language`
exist as **adjacent entries in the `classes5.dex` string pool** (i.e. one constant
map of 4 headers) but do **not** appear as literals in any decompiled class (the only
jadx decompile errors were in unrelated `com.google.android.material.shadow.*`). They
are absent from the Dart `libapp.so` string pool too. So a Kotlin class in
`classes5.dex` sets them at runtime — consistent with cloud HTTP requests carrying a
device-IP header (cloud → device proxying). Identifying the referencing class via a
direct DEX string-reference scan is in progress (workaround while REA's DEX tools
stay unavailable without `REA_JADX_MCP_JAR`).
---
## Open items
**Resolved by the JADX decompile of `base.apk`** (see §7, §13): `MqttPlugin` internals
(actual method names `setup/connect/…`, `AWSIotMqttManager` + Cognito credentials,
keepAlive 10 s, LWT, QoS 0, event names), `MethodHandlerManager` handler list,
`WifiIotPlugin` method surface.
Still open:
1. Exact BLE frame field order & the 2-byte magic semantics (confirm with `btsnoop`).
2. ~~Exact `encryptObject` radix + trailing-header handling~~ — **resolved
from disassembly (see §5): radix 32, plaintext header first
(`S1 || transform(S2)`), `decryptData` marker model. Remaining: first
confirmed live round-trip.**
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~~ — **partially resolved: `9000` open on
one unit** (§8 empirical); the `0x4650` constant is not listening there,
so the port may be firmware-/device-dependent. Remaining: first confirmed
RPC round-trip — the corrected request bytes (raw `S1 || transform(S2)`,
radix 32, int `snno`, seconds `timeStamp`, true SN
`A03X2516013210060005`) are ready via
`battery --probe`; needs a host on `10.100.1.0/24`.
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
and set `REA_JADX_MCP_JAR`.