5.7 KiB
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.BluetoothRegulatecom.inuker.bluetooth.data.CreateCtrDataHelpercom.inuker.bluetooth.data.DataAnalysisHelpercom.inuker.bluetooth.data.DeviceWorkInfocom.inuker.bluetooth.NEWBLE.MainControlActivitycom.inuker.bluetooth.NEWBLE.LiveDataActivity
Connection Sequence
- Select/scan a BLE device in the app UI.
BluetoothRegulate.connectDevice()calls the Inuker BLE client connect wrapper.- On successful connect, the app requests MTU
512. - It discovers services and looks for the custom BMS service
0000fff0-0000-1000-8000-00805f9b34fb. - It subscribes to notifications on
fff1and also registers notify callbacks on the write/auxiliary characteristics it uses. - When the main notification subscription is ready, it writes the ASCII secret
HiLinkto the secret-key characteristic. - If the key readback is
0100, it queries version and band information with AT-style commands. - It writes the current phone time to BMS registers using a
D210multi-register write at0x00D4. - Main screens periodically send
D203read frames. Notifications are buffered, frame-split, CRC checked, parsed, then broadcast to UI screens.
Live capture note: Android requested ATT MTU 517; the battery responded with
server MTU 247. The app then used handle 0x0015 (fff2) for command writes
and handle 0x0012 (fff1) for notifications.
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 coreBluetoothRegulateBMS 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
HiLinkto 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:
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:
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:
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
- Connect to the device over BLE GATT.
- Request a large MTU if possible. The app requests
512; smaller MTUs may still work if frames fit or are segmented. - Discover services.
- Subscribe to notifications on
fff1under servicefff0. - Write ASCII
HiLinkto02f...ff05. - Optionally query version with ASCII
AT+VER=?\r\non02f...ff04. - Write the read frame for registers
0x0000length0x003Etofff2. - Parse the notify response from
fff1, validate CRC, split into 16-bit big-endian words, then apply the formulas above.
The live capture confirms that notifications may contain more than one D203
frame concatenated. Split by frame header and byte count before applying CRC and
register decoding.