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

437 lines
24 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. On socket mode it **always seeds the map with
`"opt"`** (the operation type — `0` for query) before merging arguments,
so the app's apower query sends
`dataArea = {"opt": 0, "update": 2}` and its device-info query sends
`{"opt": 0, "minProtocolVer": "V1.00.00"}` (empty/non-query args still
yield `{"opt": 0}`). This matches the live capture sizes (§8).
## 4. `CommunicationCmd` (raw → name)
`off_8` values (the enum's first id field):
```
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.)
**Two id spaces.** `CommunicationCmd::cmd(cmd, mode)` (0xc92894) returns the
*second* id field (`off_14` in the pool) for `socket`/`bluetooth`/`local`
modes and throws `"do not support http communication"` for `http`. The
socket-transport ids live in `SOCKET_CMD_TYPE`
(`protocol/commands.py`) — e.g. `queryDeviceInfo → 1101 (0x44D)`,
`deviceApowerInfo → 1307 (0x51B)`, `bms → 1705`, `ems → 1707`. The `off_8`
values above are **not** used on the socket/BLE wire. Verified live: a
PCAPdroid capture of the working app shows
`{"cmdType":1101,"equipNo":"00000000",…}` with the S1-plaintext-first
framing (§5).
## 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.
Live corroboration (2026-10-15, PCAPdroid capture of the working app): a
request frame opens with the ASCII header
`{"cmdType":1101,"equipNo":"00000000",` — plaintext `S1` first, exactly as
derived, with the socket-space id (§4). A second frame shows
`{"cmdType":1102,"equipNo":"1006005A03X25160132",` (larger payload,
direction under analysis — see §8). Note the captures were exported as
text, so non-UTF-8 bytes were replaced with U+FFFD and only the ASCII
prefixes survive; a binary `.pcap` export is still wanted.
## 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. No `9000` reference exists anywhere in the app (Dart or Java) — but see the 2026-10-15 update below.
- **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** — accepts TCP, reads one frame, answers zero bytes, closes —
for *every* probe (TLS ClientHello, HTTP GET, MQTT CONNECT, exact-spec
envelopes raw/split/length-prefixed, greeting-first reads).
### Update (2026-10-15) — socket cmd space + live capture
All pre-2026-10-15 probes used `off_8` ids (`25/0/29/30`) and were dropped
silently — the socket transport speaks the `off_14` id space (§4), so the
device saw unknown commands. A PCAPdroid capture of the working app shows
real socket frames: `{"cmdType":1101,…}` (queryDeviceInfo, empty SN) and
`{"cmdType":1102,"equipNo":"1006005A03X25160132",…}` (922 B). Open:
dst IP:port + direction per frame (need capture metadata / full `.pcap`),
the meaning of `1102` (no pool cmd maps to `0x44E`), and the 17-char
`equipNo` (sub-device? battery unit? reply convention?). Current probes
use socket-space ids (`1101/1307/1705/1707`) via `scripts/host_probe3.py`.
## 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`.