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