Document BullTron live BLE capture
This commit is contained in:
@@ -96,6 +96,11 @@ The expected live-data response has byte count `0x7C`, meaning 124 bytes / 62
|
|||||||
D203 7C <62 words> CRC
|
D203 7C <62 words> CRC
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This workflow has now been confirmed in a real Android HCI snoop capture. The
|
||||||
|
capture shows the app using handle `0x0015` (`fff2`) for `D203` command writes
|
||||||
|
and handle `0x0012` (`fff1`) for `D203` notifications. The observed server MTU
|
||||||
|
is `247` after the app requests `517`.
|
||||||
|
|
||||||
## Live Telemetry Map
|
## Live Telemetry Map
|
||||||
|
|
||||||
The main read command reads registers `0x0000..0x003D`:
|
The main read command reads registers `0x0000..0x003D`:
|
||||||
@@ -137,6 +142,8 @@ if register 0x002F == 2:
|
|||||||
and value derivation.
|
and value derivation.
|
||||||
- `docs/command-catalog.md` - services, characteristics, frame formats, observed
|
- `docs/command-catalog.md` - services, characteristics, frame formats, observed
|
||||||
commands, and telemetry register map.
|
commands, and telemetry register map.
|
||||||
|
- `docs/live-capture.md` - Android HCI snoop confirmation of handles, commands,
|
||||||
|
response byte counts, and decoded live telemetry.
|
||||||
- `evidence/apk-info.md` - APK/XAPK provenance, hashes, signer info, and
|
- `evidence/apk-info.md` - APK/XAPK provenance, hashes, signer info, and
|
||||||
decompilation notes.
|
decompilation notes.
|
||||||
- `tools/bulltron_frame.py` - helper for building read/write frames with the
|
- `tools/bulltron_frame.py` - helper for building read/write frames with the
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ This is the app flow used to obtain battery/controller information such as disch
|
|||||||
8. It writes the current phone time to BMS registers using a `D210` multi-register write at `0x00D4`.
|
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.
|
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
|
## Pairing / Bonding
|
||||||
|
|
||||||
- Needs Android bond before normal telemetry: likely no.
|
- Needs Android bond before normal telemetry: likely no.
|
||||||
@@ -109,3 +113,7 @@ The `rated_capacity_Ah` input comes from the settings register set parsed into `
|
|||||||
6. Optionally query version with ASCII `AT+VER=?\r\n` on `02f...ff04`.
|
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`.
|
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.
|
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.
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
Status: decompiled from BullTron Android app `1.1.33`. Entries are based on code paths, not live BLE captures.
|
Status: decompiled from BullTron Android app `1.1.33`. Entries are based on code paths, not live BLE captures.
|
||||||
|
|
||||||
|
Update: the main BLE flow and several command frames are now confirmed by an
|
||||||
|
Android HCI snoop capture. See `docs/live-capture.md`.
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
| Service UUID | Purpose | Evidence |
|
| Service UUID | Purpose | Evidence |
|
||||||
@@ -94,6 +97,38 @@ For `D206` and `D210` responses, the app treats the acknowledgement as a fixed 8
|
|||||||
| Single register write | `fff2` | write hex frame | `D206<register><value><crc>` | `D206` ack | Generic settings/control write |
|
| Single register write | `fff2` | write hex frame | `D206<register><value><crc>` | `D206` ack | Generic settings/control write |
|
||||||
| Multi-register write | `fff2` | write hex frame | `D210<start><count><payload><crc>` | `D210` ack | Generic settings/config write |
|
| Multi-register write | `fff2` | write hex frame | `D210<start><count><payload><crc>` | `D210` ack | Generic settings/config write |
|
||||||
|
|
||||||
|
## Live Capture Confirmed Reads
|
||||||
|
|
||||||
|
The following read frames were observed in the HCI snoop capture. All listed
|
||||||
|
frames have valid Modbus CRC16 using the app's swapped CRC representation.
|
||||||
|
|
||||||
|
| Payload | Start | Count | Notes |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| `D2030000003ED7B9` | `0x0000` | `0x003E` | Main live telemetry block |
|
||||||
|
| `D203003E0009F7A3` | `0x003E` | `0x0009` | Extra cell/pack data block |
|
||||||
|
| `D2030057000CE7BC` | `0x0057` | `0x000C` | Additional info/status block |
|
||||||
|
| `D20300640001D676` | `0x0064` | `0x0001` | Residue time / heating flag |
|
||||||
|
| `D20300800029965F` | `0x0080` | `0x0029` | Settings/system block |
|
||||||
|
| `D20300A900208791` | `0x00A9` | `0x0020` | System/settings block |
|
||||||
|
| `D20300A900254792` | `0x00A9` | `0x0025` | Extended system/settings block |
|
||||||
|
| `D20300C90003C656` | `0x00C9` | `0x0003` | Password/control-code field |
|
||||||
|
| `D20300CC00021797` | `0x00CC` | `0x0002` | Small settings/status field |
|
||||||
|
| `D20300D900054791` | `0x00D9` | `0x0005` | Small settings/status field |
|
||||||
|
|
||||||
|
The capture also confirms time sync:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D21000D400031A070A122E2C9FE4
|
||||||
|
```
|
||||||
|
|
||||||
|
with acknowledgement:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D21000D40003D393
|
||||||
|
```
|
||||||
|
|
||||||
|
The system/settings screen writes `AT+PRODUCTINFO=?` to `fff3`.
|
||||||
|
|
||||||
## Live Telemetry Register Map
|
## Live Telemetry Register Map
|
||||||
|
|
||||||
Read command:
|
Read command:
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# BullTron BLE Live Capture
|
||||||
|
|
||||||
|
Status: confirmed from Android Bluetooth HCI snoop log captured with the
|
||||||
|
BullTron app connected to a BullTron battery.
|
||||||
|
|
||||||
|
The raw `btsnoop_hci.log` is not committed because it contains unrelated nearby
|
||||||
|
Bluetooth advertisements and device addresses. This file contains the protocol
|
||||||
|
facts extracted from the capture.
|
||||||
|
|
||||||
|
## Capture Summary
|
||||||
|
|
||||||
|
- Format: BTSnoop version 1, HCI UART (H4)
|
||||||
|
- Tool used for inspection: `btmon`
|
||||||
|
- Connected LE device: BullTron/Daly-style BLE peripheral
|
||||||
|
- Connection handle in capture: `65`
|
||||||
|
- Remote controller manufacturer: Hong Kong Bouffalo Lab Limited
|
||||||
|
- Remote controller Bluetooth version: Bluetooth 5.0
|
||||||
|
- App behavior: reconnects automatically, discovers services, negotiates MTU,
|
||||||
|
subscribes to notifications, then polls BMS registers.
|
||||||
|
|
||||||
|
## Discovery And Setup
|
||||||
|
|
||||||
|
The capture confirms the app-discovered main BMS service:
|
||||||
|
|
||||||
|
| Handle range | UUID | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `0x0010`-`0x001c` | `0000fff0-0000-1000-8000-00805f9b34fb` | Main BMS service |
|
||||||
|
|
||||||
|
Characteristics under that service:
|
||||||
|
|
||||||
|
| Declaration handle | Value handle | UUID | Properties |
|
||||||
|
| ---: | ---: | --- | --- |
|
||||||
|
| `0x0011` | `0x0012` | `0000fff1-0000-1000-8000-00805f9b34fb` | read, notify |
|
||||||
|
| `0x0014` | `0x0015` | `0000fff2-0000-1000-8000-00805f9b34fb` | read, write, write without response |
|
||||||
|
| `0x001a` | `0x001b` | `0000fff3-0000-1000-8000-00805f9b34fb` | read, write, notify |
|
||||||
|
|
||||||
|
Setup observed:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ATT Exchange MTU Request: client RX MTU 517
|
||||||
|
ATT Exchange MTU Response: server RX MTU 247
|
||||||
|
LE Set Data Length: TX octets 251
|
||||||
|
```
|
||||||
|
|
||||||
|
The app enables notifications by writing `0100` to a Client Characteristic
|
||||||
|
Configuration descriptor. The capture view labels that descriptor as handle
|
||||||
|
`0x0013`.
|
||||||
|
|
||||||
|
## Confirmed Main Data Flow
|
||||||
|
|
||||||
|
Normal BMS command frames are written with ATT Write Command to handle `0x0015`
|
||||||
|
(`fff2`). BMS responses arrive as Handle Value Notifications on handle `0x0012`
|
||||||
|
(`fff1`).
|
||||||
|
|
||||||
|
The live telemetry command is repeatedly sent:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TX handle 0x0015 / fff2:
|
||||||
|
D2030000003ED7B9
|
||||||
|
```
|
||||||
|
|
||||||
|
The battery replies with a `D203` frame with byte count `0x7C`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RX handle 0x0012 / fff1:
|
||||||
|
D2037C <124-byte payload> <crc16>
|
||||||
|
```
|
||||||
|
|
||||||
|
In this capture, 28 live-data `D2037C` frames were observed and all had valid
|
||||||
|
Modbus CRC16 using the same swapped CRC format implemented in
|
||||||
|
`tools/bulltron_frame.py`.
|
||||||
|
|
||||||
|
## Confirmed Read Commands
|
||||||
|
|
||||||
|
These `D203` read commands were observed on `fff2` with valid CRCs:
|
||||||
|
|
||||||
|
| Payload | Start register | Count | Observed use |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| `D2030000003ED7B9` | `0x0000` | `0x003E` | Main live telemetry block |
|
||||||
|
| `D203003E0009F7A3` | `0x003E` | `0x0009` | Extra cell/pack data block |
|
||||||
|
| `D2030057000CE7BC` | `0x0057` | `0x000C` | Additional info/status block |
|
||||||
|
| `D20300640001D676` | `0x0064` | `0x0001` | Residue time / heating flag |
|
||||||
|
| `D20300800029965F` | `0x0080` | `0x0029` | Settings/system block |
|
||||||
|
| `D20300A900208791` | `0x00A9` | `0x0020` | System/settings block |
|
||||||
|
| `D20300A900254792` | `0x00A9` | `0x0025` | Extended system/settings block |
|
||||||
|
| `D20300C90003C656` | `0x00C9` | `0x0003` | Password/control-code field |
|
||||||
|
| `D20300CC00021797` | `0x00CC` | `0x0002` | Small settings/status field |
|
||||||
|
| `D20300D900054791` | `0x00D9` | `0x0005` | Small settings/status field |
|
||||||
|
|
||||||
|
The app also performs a phone-time sync immediately after setup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TX handle 0x0015 / fff2:
|
||||||
|
D21000D400031A070A122E2C9FE4
|
||||||
|
|
||||||
|
RX handle 0x0012 / fff1:
|
||||||
|
D21000D40003D393
|
||||||
|
```
|
||||||
|
|
||||||
|
The timestamp payload decodes as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1A 07 0A 12 2E 2C
|
||||||
|
YY MM DD HH MM SS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Confirmed Response Byte Counts
|
||||||
|
|
||||||
|
Observed `D203` response byte-count dispatch values:
|
||||||
|
|
||||||
|
| Byte count | Count in capture | Meaning |
|
||||||
|
| ---: | ---: | --- |
|
||||||
|
| `0x7C` | 28 | Main 62-word live telemetry block |
|
||||||
|
| `0x52` | 28 | Settings info block |
|
||||||
|
| `0x40` | 18 | Version/device info block |
|
||||||
|
| `0x18` | 38 | Serial number / identifier block |
|
||||||
|
| `0x12` | 42 | Small status block |
|
||||||
|
| `0x06` | 18 | Password/control-code field |
|
||||||
|
| `0x04` | 34 | Small status/ack-style block |
|
||||||
|
| `0x02` | 17 | Residue time / heating flag |
|
||||||
|
| `0x0A` | 1 | Small status block |
|
||||||
|
|
||||||
|
Some notifications contain more than one `D203` frame concatenated. A parser
|
||||||
|
must split by `D203`, use the response byte count to determine frame length, and
|
||||||
|
then validate CRC per frame.
|
||||||
|
|
||||||
|
## Decoded Live Telemetry Example
|
||||||
|
|
||||||
|
One live `D2037C` frame from the capture decodes to:
|
||||||
|
|
||||||
|
| Field | Register | Decoded value |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| Cell 1 | `0x0000` | `3.306 V` |
|
||||||
|
| Cell 2 | `0x0001` | `3.312 V` |
|
||||||
|
| Cell 3 | `0x0002` | `3.308 V` |
|
||||||
|
| Cell 4 | `0x0003` | `3.311 V` |
|
||||||
|
| Pack voltage | `0x0028` | `13.2 V` |
|
||||||
|
| Current raw | `0x0029` | `29925` |
|
||||||
|
| Current | `0x0029` | `-7.5 A` using `(raw - 30000) * 0.1` |
|
||||||
|
| SOC | `0x002A` | `65.2 %` |
|
||||||
|
| State | `0x002F` | `2`, discharging |
|
||||||
|
| Remaining capacity | `0x0030` | `107.5 Ah` |
|
||||||
|
| Temperature sensors | `0x0032` | `2` |
|
||||||
|
| Cycle count | `0x0033` | `3` |
|
||||||
|
| Charge MOS | `0x0035` | `1`, on |
|
||||||
|
| Discharge MOS | `0x0036` | `1`, on |
|
||||||
|
| Cell voltage delta | `0x0038` | `0.006 V` |
|
||||||
|
| Alarm/status words | `0x003A`-`0x003D` | `0000 0000 0000 0010` |
|
||||||
|
|
||||||
|
Later frames show the same battery around:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pack voltage: 13.2 V
|
||||||
|
current: -7.4 A
|
||||||
|
SOC: 65.0-65.2 %
|
||||||
|
remaining capacity: 107.2-107.5 Ah
|
||||||
|
state: 2 / discharging
|
||||||
|
```
|
||||||
|
|
||||||
|
This confirms the decompiled formulas for voltage, current, SOC, remaining
|
||||||
|
capacity, MOS state, cell delta, and alarm bitmaps.
|
||||||
|
|
||||||
|
## System Screen Observations
|
||||||
|
|
||||||
|
After entering the system code `000000`, the capture shows reads of:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D20300C90003C656
|
||||||
|
```
|
||||||
|
|
||||||
|
with a corresponding response:
|
||||||
|
|
||||||
|
```text
|
||||||
|
D20306303030303030F6AF
|
||||||
|
```
|
||||||
|
|
||||||
|
The payload is ASCII `000000`, confirming register `0x00C9` length `3` as the
|
||||||
|
password/control-code field.
|
||||||
|
|
||||||
|
The app also writes `AT+PRODUCTINFO=?` to handle `0x001B` (`fff3`) during the
|
||||||
|
system/settings path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
41542B50524F44554354494E464F3D3F
|
||||||
|
```
|
||||||
|
|
||||||
|
ASCII:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AT+PRODUCTINFO=?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pairing / Bonding Result
|
||||||
|
|
||||||
|
This capture supports the earlier code-derived conclusion: normal telemetry did
|
||||||
|
not require an Android pairing dialog or explicit bond setup. The app connected,
|
||||||
|
discovered services, negotiated MTU, enabled notifications, and exchanged BMS
|
||||||
|
frames directly.
|
||||||
Reference in New Issue
Block a user