diff --git a/README.md b/README.md index 2db6e75..29238bf 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,11 @@ The expected live-data response has byte count `0x7C`, meaning 124 bytes / 62 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 The main read command reads registers `0x0000..0x003D`: @@ -137,6 +142,8 @@ if register 0x002F == 2: and value derivation. - `docs/command-catalog.md` - services, characteristics, frame formats, observed 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 decompilation notes. - `tools/bulltron_frame.py` - helper for building read/write frames with the diff --git a/docs/ble-flow.md b/docs/ble-flow.md index 376a8c7..ad0ce81 100644 --- a/docs/ble-flow.md +++ b/docs/ble-flow.md @@ -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`. 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. @@ -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`. 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. diff --git a/docs/command-catalog.md b/docs/command-catalog.md index 0d1e169..3850129 100644 --- a/docs/command-catalog.md +++ b/docs/command-catalog.md @@ -2,6 +2,9 @@ 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 | 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` | `D206` ack | Generic settings/control write | | Multi-register write | `fff2` | write hex frame | `D210` | `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 Read command: diff --git a/docs/live-capture.md b/docs/live-capture.md new file mode 100644 index 0000000..d0857b7 --- /dev/null +++ b/docs/live-capture.md @@ -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> +``` + +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.