Document BullTron BLE commands
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
decompiled/
|
||||
apk/
|
||||
*.apk
|
||||
*.apks
|
||||
*.xapk
|
||||
*.dex
|
||||
*.jar
|
||||
*.class
|
||||
@@ -0,0 +1,31 @@
|
||||
# BullTron BLE Commands
|
||||
|
||||
Reverse-engineering notes for the BullTron Android app BLE protocol.
|
||||
|
||||
## Target App
|
||||
|
||||
- App label: BullTron
|
||||
- Package: `com.inuker.bluetooth.bulltron`
|
||||
- Version analyzed: `1.1.33` / `1133001`
|
||||
- APK source: PGYER old-version download
|
||||
- APK SHA-256: `f11c49499cf226ccd6dc45e8f1780c11c4d84d785846182781886ab135b9a476`
|
||||
- Signer CN: `smart_daly`
|
||||
- Signer cert SHA-256: `996caf450ecdb34d0ba45d2b2c6a2df56243338c7be3b7b497168af4e3d00ae4`
|
||||
|
||||
The APK was decompiled with apktool and JADX. JADX reported some failed methods, so a few UI mappings were cross-checked against smali.
|
||||
|
||||
## Main Finding
|
||||
|
||||
Normal telemetry does not use the standard BLE Battery Service. The app connects as a BLE GATT client, subscribes to a custom BullTron/Daly-style service, writes Modbus-like frames to a write characteristic, then parses notify frames from a read/notify characteristic.
|
||||
|
||||
Android pairing/bonding is not explicitly required in the app's normal BMS flow. The core path uses `connect`, MTU request, service discovery, notifications, and writes. Generic library code contains bonding helpers, but the BullTron telemetry path does not call `createBond()`.
|
||||
|
||||
## Documents
|
||||
|
||||
- `docs/ble-flow.md` - connection/setup/query flow, pairing answer, and value derivation
|
||||
- `docs/command-catalog.md` - services, characteristics, command frames, and telemetry register map
|
||||
- `tools/extract-ble-symbols.py` - helper for scanning JADX/apktool output for BLE UUIDs and GATT calls
|
||||
|
||||
## Status
|
||||
|
||||
The local repo contains the reverse-engineering results. Pushing to `git@git.mannheim.ccc.de:C3MA/bulltron-ble-commands.git` is currently blocked because C3MA Gitea does not allow organization push-to-create for this repo. Create an empty `C3MA/bulltron-ble-commands` repository or provide a token with repo creation rights, then push `main`.
|
||||
@@ -0,0 +1,111 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,185 @@
|
||||
# BullTron BLE Command Catalog
|
||||
|
||||
Status: decompiled from BullTron Android app `1.1.33`. Entries are based on code paths, not live BLE captures.
|
||||
|
||||
## 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 <start:16-bit> <count:16-bit> <crc16>
|
||||
```
|
||||
|
||||
Single-register write:
|
||||
|
||||
```text
|
||||
D206 <register:16-bit> <value:16-bit> <crc16>
|
||||
```
|
||||
|
||||
Multi-register write:
|
||||
|
||||
```text
|
||||
D210 <start:16-bit> <count:16-bit> <payload:count*2 bytes> <crc16>
|
||||
```
|
||||
|
||||
Password write:
|
||||
|
||||
```text
|
||||
D210 00C9 0003 <utf8 password bytes> <crc16>
|
||||
```
|
||||
|
||||
Time sync write:
|
||||
|
||||
```text
|
||||
D210 00D4 0003 <YY MM DD HH MM SS> <crc16>
|
||||
```
|
||||
|
||||
OTA chunk:
|
||||
|
||||
```text
|
||||
8110 <location> <length> <data> <crc16>
|
||||
```
|
||||
|
||||
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:8-bit> <payload words> <crc16>
|
||||
```
|
||||
|
||||
`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=<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<crc>` | `fff1` notify with byte count `7C` | Reads main 62-word telemetry block |
|
||||
| Read last cell voltages | `fff2` | write hex frame | `D203003E0010<crc>` | `fff1` notify with byte count `20` | Reads last/extra cell voltage block |
|
||||
| Read password | `fff2` | write hex frame | `D20300C90003<crc>` | `fff1` notify with byte count `06` | Reads 3-word password field |
|
||||
| Read residue/calefaction flag | `fff2` | write hex frame | `D20300640001<crc>` | `fff1` notify with byte count `02` | Stores `residueTime`; `0001` also marks heating true |
|
||||
| Sync time | `fff2` | write hex frame | `D21000D40003<YYMMDDHHMMSS><crc>` | `D210` ack | Writes phone time to BMS |
|
||||
| Write password | `fff2` | write hex frame | `D21000C90003<password_hex><crc>` | `D210` ack | Sets password/control code |
|
||||
| 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 |
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,19 @@
|
||||
# APK Evidence
|
||||
|
||||
- File analyzed locally: `bulltron-1.1.33-pgyer.apk`
|
||||
- Source: PGYER old-version download page
|
||||
- App package: `com.inuker.bluetooth.bulltron`
|
||||
- App label: `BullTron`
|
||||
- Version: `1.1.33` / `1133001`
|
||||
- SHA-256: `f11c49499cf226ccd6dc45e8f1780c11c4d84d785846182781886ab135b9a476`
|
||||
- Signer DN: `CN=smart_daly, OU=smart_daly, O=smart_daly, L=smart_daly, ST=smart_daly`
|
||||
- Signer cert SHA-256: `996caf450ecdb34d0ba45d2b2c6a2df56243338c7be3b7b497168af4e3d00ae4`
|
||||
|
||||
Local decompile outputs used during analysis:
|
||||
|
||||
- apktool output: `bulltron-ble-work/apktool-out`
|
||||
- JADX output: `bulltron-ble-work/jadx-out`
|
||||
- BLE symbol scan: `bulltron-ble-work/ble-symbols-jadx.md`
|
||||
- smali BLE symbol scan: `bulltron-ble-work/ble-symbols-smali.md`
|
||||
|
||||
The APK itself is intentionally not committed into this repository.
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"backupVersionCode": 8003,
|
||||
"packageName": "com.inuker.bluetooth.bulltron",
|
||||
"packageLabel": "BullTron",
|
||||
"versionName": "1.1.33",
|
||||
"versionCode": 1133001,
|
||||
"sourceDir": "/data/app/~~PtflDjIaun5IdJ3Zt9haCA==/com.inuker.bluetooth.bulltron-YRoA8T7fLzV9a4JZCdXfYQ==/base.apk",
|
||||
"splitSourceDirs": [
|
||||
"/data/app/~~PtflDjIaun5IdJ3Zt9haCA==/com.inuker.bluetooth.bulltron-YRoA8T7fLzV9a4JZCdXfYQ==/split_config.arm64_v8a.apk",
|
||||
"/data/app/~~PtflDjIaun5IdJ3Zt9haCA==/com.inuker.bluetooth.bulltron-YRoA8T7fLzV9a4JZCdXfYQ==/split_config.de.apk",
|
||||
"/data/app/~~PtflDjIaun5IdJ3Zt9haCA==/com.inuker.bluetooth.bulltron-YRoA8T7fLzV9a4JZCdXfYQ==/split_config.xxhdpi.apk"
|
||||
],
|
||||
"backupDate": "2026-07-10T11:03:53.576193",
|
||||
"hasApk": true,
|
||||
"hasAppData": true,
|
||||
"hasDevicesProtectedData": true,
|
||||
"iv": [
|
||||
-11,
|
||||
-9,
|
||||
-95,
|
||||
-112,
|
||||
9,
|
||||
89,
|
||||
77,
|
||||
119,
|
||||
-123,
|
||||
-93,
|
||||
-40,
|
||||
-121,
|
||||
44,
|
||||
-31,
|
||||
-46,
|
||||
-31
|
||||
],
|
||||
"cpuArch": "arm64-v8a",
|
||||
"permissions": [
|
||||
" android.permission.ACCESS_COARSE_LOCATION",
|
||||
" android.permission.ACCESS_FINE_LOCATION",
|
||||
" android.permission.BLUETOOTH_ADMIN",
|
||||
" android.permission.BLUETOOTH_ADVERTISE",
|
||||
" android.permission.BLUETOOTH_CONNECT",
|
||||
" android.permission.BLUETOOTH_SCAN",
|
||||
" android.permission.CAMERA",
|
||||
" android.permission.CHANGE_NETWORK_STATE",
|
||||
" android.permission.CHANGE_WIFI_STATE",
|
||||
" android.permission.FLASHLIGHT",
|
||||
" android.permission.GET_TASKS",
|
||||
" android.permission.USE_EXACT_ALARM",
|
||||
" android.permission.VIBRATE",
|
||||
" com.google.android.c2dm.permission.RECEIVE",
|
||||
" com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE",
|
||||
" com.google.android.gms.permission.AD_ID",
|
||||
"android.permission.BLUETOOTH"
|
||||
],
|
||||
"size": 34260163
|
||||
}
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build BullTron/Daly-style BLE frames observed in the Android app."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
def crc16_modbus_swapped(hex_data: str) -> str:
|
||||
data = bytes.fromhex(hex_data)
|
||||
crc = 0xFFFF
|
||||
for byte in data:
|
||||
crc ^= byte
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
swapped = ((crc & 0xFF00) >> 8) | ((crc & 0x00FF) << 8)
|
||||
return f"{swapped:04X}"
|
||||
|
||||
|
||||
def read_frame(start: int, count: int) -> str:
|
||||
body = f"D203{start:04X}{count:04X}"
|
||||
return body + crc16_modbus_swapped(body)
|
||||
|
||||
|
||||
def write_single_frame(register: int, value: int) -> str:
|
||||
body = f"D206{register:04X}{value:04X}"
|
||||
return body + crc16_modbus_swapped(body)
|
||||
|
||||
|
||||
def write_multi_frame(start: int, count: int, payload_hex: str) -> str:
|
||||
payload = payload_hex.replace(" ", "").upper()
|
||||
body = f"D210{start:04X}{count:04X}{payload}"
|
||||
return body + crc16_modbus_swapped(body)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_read = sub.add_parser("read")
|
||||
p_read.add_argument("start", type=lambda x: int(x, 0))
|
||||
p_read.add_argument("count", type=lambda x: int(x, 0))
|
||||
|
||||
p_write = sub.add_parser("write")
|
||||
p_write.add_argument("register", type=lambda x: int(x, 0))
|
||||
p_write.add_argument("value", type=lambda x: int(x, 0))
|
||||
|
||||
p_multi = sub.add_parser("write-multi")
|
||||
p_multi.add_argument("start", type=lambda x: int(x, 0))
|
||||
p_multi.add_argument("count", type=lambda x: int(x, 0))
|
||||
p_multi.add_argument("payload_hex")
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.cmd == "read":
|
||||
print(read_frame(args.start, args.count))
|
||||
elif args.cmd == "write":
|
||||
print(write_single_frame(args.register, args.value))
|
||||
elif args.cmd == "write-multi":
|
||||
print(write_multi_frame(args.start, args.count, args.payload_hex))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan decompiled Android sources for BLE UUIDs and GATT-related calls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
UUID_RE = re.compile(
|
||||
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
||||
)
|
||||
|
||||
GATT_TERMS = [
|
||||
"BluetoothGatt",
|
||||
"BluetoothGattCallback",
|
||||
"BluetoothGattCharacteristic",
|
||||
"BluetoothGattDescriptor",
|
||||
"BluetoothLeScanner",
|
||||
"startScan",
|
||||
"startLeScan",
|
||||
"connectGatt",
|
||||
"createBond",
|
||||
"discoverServices",
|
||||
"getService",
|
||||
"getCharacteristic",
|
||||
"setCharacteristicNotification",
|
||||
"writeCharacteristic",
|
||||
"readCharacteristic",
|
||||
"writeDescriptor",
|
||||
"onCharacteristicChanged",
|
||||
"onCharacteristicRead",
|
||||
"onCharacteristicWrite",
|
||||
]
|
||||
|
||||
|
||||
def interesting(path: Path) -> bool:
|
||||
return path.suffix.lower() in {".java", ".kt", ".xml", ".smali"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source", type=Path, help="JADX/apktool output directory")
|
||||
args = parser.parse_args()
|
||||
|
||||
for path in sorted(p for p in args.source.rglob("*") if p.is_file() and interesting(p)):
|
||||
try:
|
||||
text = path.read_text(errors="ignore")
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
matches = sorted(set(UUID_RE.findall(text)))
|
||||
terms = [term for term in GATT_TERMS if term in text]
|
||||
if not matches and not terms:
|
||||
continue
|
||||
|
||||
print(f"## {path}")
|
||||
if matches:
|
||||
print("UUIDs:")
|
||||
for value in matches:
|
||||
print(f"- {value.lower()}")
|
||||
if terms:
|
||||
print("GATT terms:")
|
||||
for value in terms:
|
||||
print(f"- {value}")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user