5.3 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.
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.