# 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//.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/inverse order used by `toJson`: ``` cmdType, equipNo, type, timeStamp, snno, len, crc, dataArea, data ``` Fields present: `_Mint field_8`, `_OneByteString field_10`, `_Mint field_14` (+ `_Mint`/`_OneByteString` further). `data` is a setter; string const `"00000000"` is a default (likely `crc`/`dataArea` placeholder). `cmdType` = `CommunicationCmd` raw, `type` = `CommunicationOperate` raw, `equipNo` = device SN. - `CommunicationOperate`: `query=0, set=1, command=2, modify=5`. - `Crc32::calculate` is used in `fromJsonString` (integrity check of the payload). - `toJsonString()` produces the string that is then transformed (see §5). ## 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. **`encryptObject(obj, key)`**: 1. `s = obj.toJsonString()`; split on `","`. 2. `a` = UTF-8 of `part[0] + "," + part[1]` (i.e. `{"cmdType":..` and `"equipNo":..`). 3. `b` = UTF-8 of `join(",", parts[2..])` (the rest). 4. `keyInt = int.tryParse(key, radix: …)`. 5. `out[i] = (b[i] + i + len(a) + keyInt) & 0xFF` for all i, then append `a` **unchanged**. So on the wire the encrypted body is `transform(b) || a` (header/keys appended at the end). `decryptData` is the inverse. > This is a bespoke additive/KSA stream; the exact radix of the key parse and the trailing > `a` handling should be confirmed against a captured frame before trusting it byte-for-byte. ## 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: }` — 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. - `BytesToInt16/Int16ToBytes` LE framing; `Socket Manager send object` / `receive object`. ## 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>` 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`; 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. 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. 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`.