# BullTron BLE Commands Reverse-engineering notes for the BullTron Android app BLE protocol. This repository documents the custom BLE GATT protocol used by the BullTron Android app to read battery telemetry such as SOC, current, voltage, remaining capacity, and estimated time-to-empty/time-to-full. ## Target App - App label: BullTron - Package: `com.inuker.bluetooth.bulltron` - Version analyzed: `1.1.33` / `1133001` - XAPK source: APKPure direct download - XAPK SHA-256: `35388e6c2b9afeeb4bafd97df09fcb1e33427802dd98f74a2a4648099dda71ff` - Base APK SHA-256: `f1d486b410c1fa2fae78837fcc27a63aa16c6cb1b45f096a17c9968908add363` - Signer CN: `smart_daly` - Signer cert SHA-256: `996caf450ecdb34d0ba45d2b2c6a2df56243338c7be3b7b497168af4e3d00ae4` The XAPK was unpacked and the base APK code was checked against a separately mirrored PGYER APK. All three DEX files matched by SHA-256. The APK was decompiled with apktool and JADX; a few mappings were cross-checked against smali where JADX reported failed methods. ## Main Finding Normal telemetry does not use the standard BLE Battery Service. The app connects as a BLE GATT client, subscribes to a custom BullTron/Daly-style service, writes Modbus-like command frames to a write characteristic, then parses notify frames from a read/notify characteristic. Android pairing/bonding does not appear to be required for normal telemetry. The core BullTron path uses connect, MTU request, service discovery, notifications, and writes. Generic library code contains bonding helpers, but the normal BMS telemetry path does not call `createBond()`. ## Quick Start Build the main live-data read frame: ```sh python3 tools/bulltron_frame.py read 0 62 ``` Expected output: ```text D2030000003ED7B9 ``` Send that frame to characteristic `0000fff2-0000-1000-8000-00805f9b34fb` after subscribing to notifications on `0000fff1-0000-1000-8000-00805f9b34fb`. ## App Workflow The app's normal telemetry flow is: 1. Scan/select a BLE device in the app UI. 2. Connect with the Inuker BLE client wrapper. 3. Request MTU `512`. 4. Discover services and find `0000fff0-0000-1000-8000-00805f9b34fb`. 5. Subscribe to notifications on `fff1`. 6. Write ASCII `HiLink` to the secret-key characteristic `02f00000-0000-0000-0000-00000000ff05`. 7. If the key readback is `0100`, query version/band information with AT-style commands. 8. Write phone time to BMS registers with a `D210` multi-register write at register `0x00D4`. 9. Periodically write `D203` read frames to `fff2`. 10. Receive notifications on `fff1`, split/buffer frames, validate CRC, parse 16-bit big-endian words, and update the UI. ## Minimal BLE Workflow For a custom client that only needs live telemetry: 1. Connect to the device over BLE GATT. 2. Request a large MTU if possible. The app requests `512`. 3. Discover services. 4. Subscribe to notifications on `fff1` under service `fff0`. 5. Write ASCII `HiLink` to `02f...ff05`. 6. Optionally query version with ASCII `AT+VER=?\r\n` on `02f...ff04`. 7. Write the main read frame to `fff2`: ```text D2030000003ED7B9 ``` 8. Parse the notify response from `fff1`. The expected live-data response has byte count `0x7C`, meaning 124 bytes / 62 16-bit words: ```text D203 7C <62 words> CRC ``` ## Live Telemetry Map The main read command reads registers `0x0000..0x003D`: ```text D203 0000 003E CRC ``` Important fields: | Register | Meaning | Formula | | ---: | --- | --- | | `0x0000`-`0x001F` | Cell voltages | `raw * 0.001 V` | | `0x0020`-`0x0027` | Battery temperatures | `raw - 40 deg C` | | `0x0028` | Pack voltage | `raw * 0.1 V` | | `0x0029` | Current | `(raw - 30000) * 0.1 A`, with app-specific correction for two voltage profiles | | `0x002A` | SOC / battery percent | `raw / 10 %` | | `0x002F` | MOS / direction state | `1 = charging`, `2 = discharging` | | `0x0030` | Remaining capacity | `raw * 0.1 Ah` | | `0x0033` | Cycle count | raw count | | `0x0035` | Charge MOS | `1 = on` | | `0x0036` | Discharge MOS | `1 = on` | | `0x003A`-`0x003D` | Alarm/status words | 16-bit bitmaps | Time-to-empty/time-to-full is computed by the app instead of read as one live BLE value: ```text if register 0x002F == 1: time-to-full = rated_capacity_Ah * (100 - SOC_percent) / abs(current_A) * 60 if register 0x002F == 2: time-to-empty = rated_capacity_Ah * SOC_percent / abs(current_A) * 60 ``` ## Repository Contents - `docs/ble-flow.md` - detailed connection/setup/query workflow, pairing answer, and value derivation. - `docs/command-catalog.md` - services, characteristics, frame formats, observed commands, and telemetry register map. - `evidence/apk-info.md` - APK/XAPK provenance, hashes, signer info, and decompilation notes. - `tools/bulltron_frame.py` - helper for building read/write frames with the same CRC format used by the app. - `tools/extract-ble-symbols.py` - helper for scanning JADX/apktool output for BLE UUIDs and GATT calls. ## Caveats These findings are derived from the decompiled Android app, not from a live BLE capture. A capture is still useful to confirm firmware-specific behavior, current sign conventions, correction edge cases, and whether a specific battery requires link-layer encryption despite the app not initiating pairing itself.