Files
bulltron-ble-commands/docs/ble-flow.md
T

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

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

  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.