Document BullTron BLE commands

This commit is contained in:
2026-07-10 11:31:55 +02:00
commit 3675220179
8 changed files with 548 additions and 0 deletions
+66
View File
@@ -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()
+72
View File
@@ -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())