8.5 KiB
BullTron BLE Command Catalog
Status: decompiled from BullTron Android app 1.1.33. Entries are based on code paths, not live BLE captures.
Update: the main BLE flow and several command frames are now confirmed by an
Android HCI snoop capture. See docs/live-capture.md.
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:
D203 <start:16-bit> <count:16-bit> <crc16>
Single-register write:
D206 <register:16-bit> <value:16-bit> <crc16>
Multi-register write:
D210 <start:16-bit> <count:16-bit> <payload:count*2 bytes> <crc16>
Password write:
D210 00C9 0003 <utf8 password bytes> <crc16>
Time sync write:
D210 00D4 0003 <YY MM DD HH MM SS> <crc16>
OTA chunk:
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:
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 Capture Confirmed Reads
The following read frames were observed in the HCI snoop capture. All listed frames have valid Modbus CRC16 using the app's swapped CRC representation.
| Payload | Start | Count | Notes |
|---|---|---|---|
D2030000003ED7B9 |
0x0000 |
0x003E |
Main live telemetry block |
D203003E0009F7A3 |
0x003E |
0x0009 |
Extra cell/pack data block |
D2030057000CE7BC |
0x0057 |
0x000C |
Additional info/status block |
D20300640001D676 |
0x0064 |
0x0001 |
Residue time / heating flag |
D20300800029965F |
0x0080 |
0x0029 |
Settings/system block |
D20300A900208791 |
0x00A9 |
0x0020 |
System/settings block |
D20300A900254792 |
0x00A9 |
0x0025 |
Extended system/settings block |
D20300C90003C656 |
0x00C9 |
0x0003 |
Password/control-code field |
D20300CC00021797 |
0x00CC |
0x0002 |
Small settings/status field |
D20300D900054791 |
0x00D9 |
0x0005 |
Small settings/status field |
The capture also confirms time sync:
D21000D400031A070A122E2C9FE4
with acknowledgement:
D21000D40003D393
The system/settings screen writes AT+PRODUCTINFO=? to fff3.
Live Telemetry Register Map
Read command:
D203 0000 003E CRC
Response dispatch:
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:
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.