120 lines
5.7 KiB
Markdown
120 lines
5.7 KiB
Markdown
# 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.
|
|
|
|
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 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.
|
|
|
|
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.
|