# BullTron BLE Command Catalog 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 | | --- | --- | --- | | `0000fff0-0000-1000-8000-00805f9b34fb` | Main BMS service | `BluetoothRegulate` constants and service discovery | | `02f00000-0000-0000-0000-00000000fe00` | Version, secret key, OTA | `BluetoothRegulate` constants and setup writes | | `0000ff00-0000-1000-8000-00805f9b34fb` | WiFi provisioning/config path | WaitConfig constants, not normal BMS telemetry | ## Characteristics | Characteristic UUID | Direction | Purpose | | --- | --- | --- | | `0000fff1-0000-1000-8000-00805f9b34fb` | notify/read | Main BMS response/telemetry frames | | `0000fff2-0000-1000-8000-00805f9b34fb` | write, notify callback registered | Main BMS command frames | | `0000fff3-0000-1000-8000-00805f9b34fb` | write/read | AT-style BLE module/name/band commands | | `02f00000-0000-0000-0000-00000000ff04` | write/read/notify | Version query, `AT+VER=?\r\n` | | `02f00000-0000-0000-0000-00000000ff05` | write/read | Secret key handshake, ASCII `HiLink` | | `02f00000-0000-0000-0000-00000000ff02` | notify/read | OTA response | | `02f00000-0000-0000-0000-00000000ff01` | write | OTA data | ## Frame Format The main BMS protocol is Modbus-like over BLE. Read holding/register block: ```text D203 ``` Single-register write: ```text D206 ``` Multi-register write: ```text D210 ``` Password write: ```text D210 00C9 0003 ``` Time sync write: ```text D210 00D4 0003 ``` OTA chunk: ```text 8110 ``` The CRC is Modbus CRC16 with polynomial `0xA001`, initial value `0xFFFF`, and the app returns the final word byte-swapped as four uppercase hex digits. ## Response Format For `D203` responses: ```text D203 ``` `byte_count` is the payload byte length. The live-data response uses `0x7C`, which is 124 bytes / 62 words. For `D206` and `D210` responses, the app treats the acknowledgement as a fixed 8-byte / 16-hex-character frame. ## Commands Observed In App | Name | Characteristic | Operation | Payload | Response/Notification | Meaning | | --- | --- | --- | --- | --- | --- | | Secret key | `02f...ff05` | write ASCII | `HiLink` | readback/notify `0100` means OK | Enables version/band setup path | | Query version | `02f...ff04` | write ASCII | `AT+VER=?\r\n` | version notify/read | BLE/app/MCU version query | | Query/change band | `fff3` | write ASCII | `AT+BAND=...` / query variant | AT response | BLE module band/region setup | | Change BLE name | `fff3` | write ASCII | `AT+NAME=` | AT response | Rename BLE module | | Product info | `fff3` | write ASCII | `AT+PRODUCTINFO...` | AT response | Product info/config | | Read live data | `fff2` | write hex frame | `D2030000003E` | `fff1` notify with byte count `7C` | Reads main 62-word telemetry block | | Read last cell voltages | `fff2` | write hex frame | `D203003E0010` | `fff1` notify with byte count `20` | Reads last/extra cell voltage block | | Read password | `fff2` | write hex frame | `D20300C90003` | `fff1` notify with byte count `06` | Reads 3-word password field | | Read residue/calefaction flag | `fff2` | write hex frame | `D20300640001` | `fff1` notify with byte count `02` | Stores `residueTime`; `0001` also marks heating true | | Sync time | `fff2` | write hex frame | `D21000D40003` | `D210` ack | Writes phone time to BMS | | Write password | `fff2` | write hex frame | `D21000C90003` | `D210` ack | Sets password/control code | | 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: ```text D203 0000 003E CRC ``` Response dispatch: ```text D203 7C <62 words> CRC ``` Words are parsed as unsigned 16-bit big-endian hex words before scaling. | Register | Index | Scale / Transform | Unit | Field | | ---: | ---: | --- | --- | --- | | `0x0000`-`0x001F` | 0-31 | `raw * 0.001` | V | cell voltages | | `0x0020`-`0x0027` | 32-39 | `raw - 40` | deg C | battery temperature sensors | | `0x0028` | 40 | `raw * 0.1` | V | pack voltage | | `0x0029` | 41 | `(raw - 30000) * 0.1` | A | current; negative/positive sign follows firmware convention | | `0x002A` | 42 | `raw / 10` | % | battery percent / SOC | | `0x002B` | 43 | `raw * 0.001` | V | highest cell voltage | | `0x002C` | 44 | `raw * 0.001` | V | lowest cell voltage | | `0x002F` | 47 | raw enum | state | `1 = charging`, `2 = discharging` in live-time UI | | `0x0030` | 48 | `raw * 0.1` | Ah | remaining capacity | | `0x0032` | 50 | raw count, capped at 8 | count | temperature sensor count | | `0x0033` | 51 | raw | cycles | cycle count | | `0x0035` | 53 | `1 = on` | boolean | charge MOS | | `0x0036` | 54 | `1 = on` | boolean | discharge MOS | | `0x0038` | 56 | `raw * 0.001` | V | cell voltage delta | | `0x003A`-`0x003D` | 58-61 | 16-bit bitmaps | bits | alarm/status words | The app applies an extra current correction for two recognized voltage profile pairs from settings registers: | Setting values | Correction | | --- | --- | | settings word `0x000C` formats as `3.71` and word `0x000E` formats as `2.71` | multiply current by `3.472` | | settings word `0x000C` formats as `3.72` and word `0x000E` formats as `2.72` | multiply current by `3.37` | ## Time-To-Empty / Time-To-Full The UI computes time from live registers instead of just displaying a raw BLE value: ```text current_A = abs(decoded register 0x0029) soc_percent = register 0x002A / 10 remaining_capacity_Ah = register 0x0030 * 0.1 rated_capacity_Ah = parsed settings capacity if register 0x002F == 1: label = TimeToFull minutes = rated_capacity_Ah * (100 - soc_percent) / current_A * 60 if register 0x002F == 2: label = TimetoEmpty minutes = rated_capacity_Ah * soc_percent / current_A * 60 ``` If current is zero or the state is neither `1` nor `2`, the UI shows `-- h --m`. ## Dispatch By Read Byte Count `DataAnalysisHelper.analysisPressValueByCan()` dispatches `D203` replies by byte count: | Byte count | Meaning in app | | --- | --- | | `7C` | main run/live info | | `20` | last battery/cell voltage block | | `52` | settings info | | `40` | version data | | `06` | password data | | `12` | balancing state | | `0A` | new alarm info | | `4A` | device parameter info | | `04` | communication/protocol or production date | | `FE` | history info | | `18` | serial number | | `02` | residue time / heating flag | | `FC` | Ali/BMS data `0x00`-`0x7D` | | `E0` | Ali/BMS data `0x80`-`0xEF` | | `38` | BLE registers `0x63`-`0x7E` | | `22` | BLE registers `0xDF`-`0xEF` | ## Caveats - This catalog is code-derived. A live BLE capture is still useful to confirm sign conventions, current correction edge cases, and whether a specific BullTron battery firmware requires link-layer encryption. - The app bundles WiFi/Ali IoT flows too; those are separate from the local BLE telemetry path documented here.