Polish README workflow overview
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Reverse-engineering notes for the BullTron Android app BLE protocol.
|
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
|
## Target App
|
||||||
|
|
||||||
- App label: BullTron
|
- App label: BullTron
|
||||||
@@ -13,20 +17,136 @@ Reverse-engineering notes for the BullTron Android app BLE protocol.
|
|||||||
- Signer CN: `smart_daly`
|
- Signer CN: `smart_daly`
|
||||||
- Signer cert SHA-256: `996caf450ecdb34d0ba45d2b2c6a2df56243338c7be3b7b497168af4e3d00ae4`
|
- Signer cert SHA-256: `996caf450ecdb34d0ba45d2b2c6a2df56243338c7be3b7b497168af4e3d00ae4`
|
||||||
|
|
||||||
The XAPK was unpacked and the base APK code was verified against the separately mirrored PGYER APK: all three DEX files have identical SHA-256 hashes. The APK was decompiled with apktool and JADX. JADX reported some failed methods, so a few UI mappings were cross-checked against smali.
|
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
|
## 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 frames to a write characteristic, then parses notify frames from a read/notify characteristic.
|
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 is not explicitly required in the app's normal BMS flow. The core path uses `connect`, MTU request, service discovery, notifications, and writes. Generic library code contains bonding helpers, but the BullTron telemetry path does not call `createBond()`.
|
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()`.
|
||||||
|
|
||||||
## Documents
|
## Quick Start
|
||||||
|
|
||||||
- `docs/ble-flow.md` - connection/setup/query flow, pairing answer, and value derivation
|
Build the main live-data read frame:
|
||||||
- `docs/command-catalog.md` - services, characteristics, command frames, and telemetry register map
|
|
||||||
- `tools/extract-ble-symbols.py` - helper for scanning JADX/apktool output for BLE UUIDs and GATT calls
|
|
||||||
|
|
||||||
## Status
|
```sh
|
||||||
|
python3 tools/bulltron_frame.py read 0 62
|
||||||
|
```
|
||||||
|
|
||||||
The local repo contains the reverse-engineering results. Pushing to `git@git.mannheim.ccc.de:C3MA/bulltron-ble-commands.git` is currently blocked because C3MA Gitea does not allow organization push-to-create for this repo. Create an empty `C3MA/bulltron-ble-commands` repository or provide a token with repo creation rights, then push `main`.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user