Document BullTron BLE commands

This commit is contained in:
2026-07-10 11:31:55 +02:00
commit 3675220179
8 changed files with 548 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
# BullTron BLE Flow
This is the app flow used to obtain battery/controller information such as discharge current, SOC, voltage, and time-to-empty.
## Evidence
- App package: `com.inuker.bluetooth.bulltron`
- Version: `1.1.33`
- Core classes:
- `com.inuker.bluetooth.data.BluetoothRegulate`
- `com.inuker.bluetooth.data.CreateCtrDataHelper`
- `com.inuker.bluetooth.data.DataAnalysisHelper`
- `com.inuker.bluetooth.data.DeviceWorkInfo`
- `com.inuker.bluetooth.NEWBLE.MainControlActivity`
- `com.inuker.bluetooth.NEWBLE.LiveDataActivity`
## Connection Sequence
1. Select/scan a BLE device in the app UI.
2. `BluetoothRegulate.connectDevice()` calls the Inuker BLE client connect wrapper.
3. On successful connect, the app requests MTU `512`.
4. It discovers services and looks for the custom BMS service `0000fff0-0000-1000-8000-00805f9b34fb`.
5. It subscribes to notifications on `fff1` and also registers notify callbacks on the write/auxiliary characteristics it uses.
6. When the main notification subscription is ready, it writes the ASCII secret `HiLink` to the secret-key characteristic.
7. If the key readback is `0100`, it queries version and band information with AT-style commands.
8. It writes the current phone time to BMS registers using a `D210` multi-register write at `0x00D4`.
9. Main screens periodically send `D203` read frames. Notifications are buffered, frame-split, CRC checked, parsed, then broadcast to UI screens.
## Pairing / Bonding
- Needs Android bond before normal telemetry: likely no.
- Evidence: the BullTron path uses connect, MTU request, service discovery, notifications, and writes. I did not find an explicit `createBond()` call in the core `BluetoothRegulate` BMS flow.
- Caveat: generic third-party/library code bundled in the APK contains bonding helpers. A peripheral could still enforce encryption at firmware level, but the app itself does not start pairing as a required normal step.
- App-level setup/auth: yes, but lightweight. It writes ASCII `HiLink` to the secret-key characteristic before version/band setup.
## Services And Characteristics Used
The main BMS GATT surface is:
- Service: `0000fff0-0000-1000-8000-00805f9b34fb`
- Notify/read: `0000fff1-0000-1000-8000-00805f9b34fb`
- Write: `0000fff2-0000-1000-8000-00805f9b34fb`
- Name/AT command: `0000fff3-0000-1000-8000-00805f9b34fb`
The app also uses an auxiliary/version/OTA surface:
- Service: `02f00000-0000-0000-0000-00000000fe00`
- Version read/write: `02f00000-0000-0000-0000-00000000ff04`
- Secret key: `02f00000-0000-0000-0000-00000000ff05`
- OTA notify/read: `02f00000-0000-0000-0000-00000000ff02`
- OTA write: `02f00000-0000-0000-0000-00000000ff01`
## Getting Live Values
For the main live view, `MainControlActivity` sends:
```text
read registers 0x0000..0x003D:
D203 0000 003E CRC16
```
The BMS replies on `fff1` with a `D203` frame whose byte-count is `0x7C`, meaning 62 16-bit words. `DataAnalysisHelper` dispatches that to `DeviceWorkInfo.analysisRunDataInfo()`.
Important fields in that 62-word response:
| Register | Word index | Meaning | Formula |
| ---: | ---: | --- | --- |
| `0x0000`-`0x001F` | 0-31 | cell voltages | `raw * 0.001 V` |
| `0x0020`-`0x0027` | 32-39 | battery temperatures | `raw - 40 deg C` |
| `0x0028` | 40 | pack voltage | `raw * 0.1 V` |
| `0x0029` | 41 | current | `(raw - 30000) * 0.1 A`, with app-specific correction for two voltage profiles |
| `0x002A` | 42 | SOC / battery percent | `raw / 10 %` |
| `0x002B` | 43 | highest cell voltage | `raw * 0.001 V` |
| `0x002C` | 44 | lowest cell voltage | `raw * 0.001 V` |
| `0x002F` | 47 | MOS / direction state | `1 = charging/time-to-full`, `2 = discharging/time-to-empty` |
| `0x0030` | 48 | remaining capacity | `raw * 0.1 Ah` |
| `0x0032` | 50 | temperature sensor count | used to average temp values |
| `0x0033` | 51 | cycle count | raw count |
| `0x0035` | 53 | charge MOS | `1 = on` |
| `0x0036` | 54 | discharge MOS | `1 = on` |
| `0x0038` | 56 | voltage delta | `raw * 0.001 V` |
| `0x003A`-`0x003D` | 58-61 | alarm/status words | 16-bit bitmaps |
The app computes power from voltage and current:
```text
power W = abs(pack_voltage_V) * abs(current_A)
```
Time-to-empty/full is not a separate BLE value in the live view. The app computes it:
```text
if register 0x002F == 1:
time-to-full = (rated_capacity_Ah * (100 - SOC_percent) / abs(current_A)) * 60 minutes
if register 0x002F == 2:
time-to-empty = (rated_capacity_Ah * SOC_percent / abs(current_A)) * 60 minutes
```
The `rated_capacity_Ah` input comes from the settings register set parsed into `contentByteArrayByReadSet`. The app also has a separate read of register `0x0064` length `1` that stores `residueTime`, but the visible live time calculation above is derived from SOC/current/capacity unless current is zero.
## Minimal Query Flow With A BLE Client
1. Connect to the device over BLE GATT.
2. Request a large MTU if possible. The app requests `512`; smaller MTUs may still work if frames fit or are segmented.
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 read frame for registers `0x0000` length `0x003E` to `fff2`.
8. Parse the notify response from `fff1`, validate CRC, split into 16-bit big-endian words, then apply the formulas above.
+185
View File
@@ -0,0 +1,185 @@
# BullTron BLE Command Catalog
Status: decompiled from BullTron Android app `1.1.33`. Entries are based on code paths, not live BLE captures.
## Services
| Service UUID | Purpose | Evidence |
| --- | --- | --- |
| `0000fff0-0000-1000-8000-00805f9b34fb` | Main BMS service | `BluetoothRegulate` constants and service discovery |
| `02f00000-0000-0000-0000-00000000fe00` | Version, secret key, OTA | `BluetoothRegulate` constants and setup writes |
| `0000ff00-0000-1000-8000-00805f9b34fb` | WiFi provisioning/config path | WaitConfig constants, not normal BMS telemetry |
## Characteristics
| Characteristic UUID | Direction | Purpose |
| --- | --- | --- |
| `0000fff1-0000-1000-8000-00805f9b34fb` | notify/read | Main BMS response/telemetry frames |
| `0000fff2-0000-1000-8000-00805f9b34fb` | write, notify callback registered | Main BMS command frames |
| `0000fff3-0000-1000-8000-00805f9b34fb` | write/read | AT-style BLE module/name/band commands |
| `02f00000-0000-0000-0000-00000000ff04` | write/read/notify | Version query, `AT+VER=?\r\n` |
| `02f00000-0000-0000-0000-00000000ff05` | write/read | Secret key handshake, ASCII `HiLink` |
| `02f00000-0000-0000-0000-00000000ff02` | notify/read | OTA response |
| `02f00000-0000-0000-0000-00000000ff01` | write | OTA data |
## Frame Format
The main BMS protocol is Modbus-like over BLE.
Read holding/register block:
```text
D203 <start:16-bit> <count:16-bit> <crc16>
```
Single-register write:
```text
D206 <register:16-bit> <value:16-bit> <crc16>
```
Multi-register write:
```text
D210 <start:16-bit> <count:16-bit> <payload:count*2 bytes> <crc16>
```
Password write:
```text
D210 00C9 0003 <utf8 password bytes> <crc16>
```
Time sync write:
```text
D210 00D4 0003 <YY MM DD HH MM SS> <crc16>
```
OTA chunk:
```text
8110 <location> <length> <data> <crc16>
```
The CRC is Modbus CRC16 with polynomial `0xA001`, initial value `0xFFFF`, and the app returns the final word byte-swapped as four uppercase hex digits.
## Response Format
For `D203` responses:
```text
D203 <byte_count:8-bit> <payload words> <crc16>
```
`byte_count` is the payload byte length. The live-data response uses `0x7C`, which is 124 bytes / 62 words.
For `D206` and `D210` responses, the app treats the acknowledgement as a fixed 8-byte / 16-hex-character frame.
## Commands Observed In App
| Name | Characteristic | Operation | Payload | Response/Notification | Meaning |
| --- | --- | --- | --- | --- | --- |
| Secret key | `02f...ff05` | write ASCII | `HiLink` | readback/notify `0100` means OK | Enables version/band setup path |
| Query version | `02f...ff04` | write ASCII | `AT+VER=?\r\n` | version notify/read | BLE/app/MCU version query |
| Query/change band | `fff3` | write ASCII | `AT+BAND=...` / query variant | AT response | BLE module band/region setup |
| Change BLE name | `fff3` | write ASCII | `AT+NAME=<name>` | AT response | Rename BLE module |
| Product info | `fff3` | write ASCII | `AT+PRODUCTINFO...` | AT response | Product info/config |
| Read live data | `fff2` | write hex frame | `D2030000003E<crc>` | `fff1` notify with byte count `7C` | Reads main 62-word telemetry block |
| Read last cell voltages | `fff2` | write hex frame | `D203003E0010<crc>` | `fff1` notify with byte count `20` | Reads last/extra cell voltage block |
| Read password | `fff2` | write hex frame | `D20300C90003<crc>` | `fff1` notify with byte count `06` | Reads 3-word password field |
| Read residue/calefaction flag | `fff2` | write hex frame | `D20300640001<crc>` | `fff1` notify with byte count `02` | Stores `residueTime`; `0001` also marks heating true |
| Sync time | `fff2` | write hex frame | `D21000D40003<YYMMDDHHMMSS><crc>` | `D210` ack | Writes phone time to BMS |
| Write password | `fff2` | write hex frame | `D21000C90003<password_hex><crc>` | `D210` ack | Sets password/control code |
| Single register write | `fff2` | write hex frame | `D206<register><value><crc>` | `D206` ack | Generic settings/control write |
| Multi-register write | `fff2` | write hex frame | `D210<start><count><payload><crc>` | `D210` ack | Generic settings/config write |
## Live Telemetry Register Map
Read command:
```text
D203 0000 003E CRC
```
Response dispatch:
```text
D203 7C <62 words> CRC
```
Words are parsed as unsigned 16-bit big-endian hex words before scaling.
| Register | Index | Scale / Transform | Unit | Field |
| ---: | ---: | --- | --- | --- |
| `0x0000`-`0x001F` | 0-31 | `raw * 0.001` | V | cell voltages |
| `0x0020`-`0x0027` | 32-39 | `raw - 40` | deg C | battery temperature sensors |
| `0x0028` | 40 | `raw * 0.1` | V | pack voltage |
| `0x0029` | 41 | `(raw - 30000) * 0.1` | A | current; negative/positive sign follows firmware convention |
| `0x002A` | 42 | `raw / 10` | % | battery percent / SOC |
| `0x002B` | 43 | `raw * 0.001` | V | highest cell voltage |
| `0x002C` | 44 | `raw * 0.001` | V | lowest cell voltage |
| `0x002F` | 47 | raw enum | state | `1 = charging`, `2 = discharging` in live-time UI |
| `0x0030` | 48 | `raw * 0.1` | Ah | remaining capacity |
| `0x0032` | 50 | raw count, capped at 8 | count | temperature sensor count |
| `0x0033` | 51 | raw | cycles | cycle count |
| `0x0035` | 53 | `1 = on` | boolean | charge MOS |
| `0x0036` | 54 | `1 = on` | boolean | discharge MOS |
| `0x0038` | 56 | `raw * 0.001` | V | cell voltage delta |
| `0x003A`-`0x003D` | 58-61 | 16-bit bitmaps | bits | alarm/status words |
The app applies an extra current correction for two recognized voltage profile pairs from settings registers:
| Setting values | Correction |
| --- | --- |
| settings word `0x000C` formats as `3.71` and word `0x000E` formats as `2.71` | multiply current by `3.472` |
| settings word `0x000C` formats as `3.72` and word `0x000E` formats as `2.72` | multiply current by `3.37` |
## Time-To-Empty / Time-To-Full
The UI computes time from live registers instead of just displaying a raw BLE value:
```text
current_A = abs(decoded register 0x0029)
soc_percent = register 0x002A / 10
remaining_capacity_Ah = register 0x0030 * 0.1
rated_capacity_Ah = parsed settings capacity
if register 0x002F == 1:
label = TimeToFull
minutes = rated_capacity_Ah * (100 - soc_percent) / current_A * 60
if register 0x002F == 2:
label = TimetoEmpty
minutes = rated_capacity_Ah * soc_percent / current_A * 60
```
If current is zero or the state is neither `1` nor `2`, the UI shows `-- h --m`.
## Dispatch By Read Byte Count
`DataAnalysisHelper.analysisPressValueByCan()` dispatches `D203` replies by byte count:
| Byte count | Meaning in app |
| --- | --- |
| `7C` | main run/live info |
| `20` | last battery/cell voltage block |
| `52` | settings info |
| `40` | version data |
| `06` | password data |
| `12` | balancing state |
| `0A` | new alarm info |
| `4A` | device parameter info |
| `04` | communication/protocol or production date |
| `FE` | history info |
| `18` | serial number |
| `02` | residue time / heating flag |
| `FC` | Ali/BMS data `0x00`-`0x7D` |
| `E0` | Ali/BMS data `0x80`-`0xEF` |
| `38` | BLE registers `0x63`-`0x7E` |
| `22` | BLE registers `0xDF`-`0xEF` |
## Caveats
- This catalog is code-derived. A live BLE capture is still useful to confirm sign conventions, current correction edge cases, and whether a specific BullTron battery firmware requires link-layer encryption.
- The app bundles WiFi/Ali IoT flows too; those are separate from the local BLE telemetry path documented here.