Protocol
Live capture·
How the vendor app's own traffic was captured on a Pixel and matched byte-for-byte to the APK read and the driver.
In 10 seconds
Turn on the Pixel's HCI snoop socket, let LoraFit talk to a ring, mark every tap, decode the ATT layer with
tools/btsnoop_att.py — that is how the APK read was confirmed byte for byte (66 frames in the first capture)
and how 0x19 and 0x44 were found. tools/capture.sh start lorafit · mark "tap Start HR" · stop
Recipe (Pixel, Android 14+)·
| step | do |
|---|---|
| 1 | Developer options → Enable Bluetooth HCI snoop log → Enabled and the snoop log socket toggle on (settings put secure bluetooth_hci_log 1 alone is not enough) |
| 2 | adb forward tcp:8872 tcp:8872 → tools/capture.sh start lorafit (background reader → captures/btsnoop_lorafit_<ts>.log) |
| 3 | drive the app: connect, every screen, start/stop every measurement — tools/capture.sh mark "tap Start HR" per action → captures/markers.jsonl |
| 4 | tools/capture.sh stop → decodes to .att.jsonl + .txt via tools/btsnoop_att.py (below) |
The stream is standard btsnoop v1 — the same framing as btsnoop_hci.log inside a bugreport, which works.
99 % of the bytes are advertising reports; the ATT traffic is tiny.
The socket dies with the stack
Any Bluetooth restart on the phone (svc bluetooth disable, airplane mode) ends the stream — start a new file.
Two lanes sharing one phone radio killed each other's captures twice.
What the captures proved — link, connect burst, HR, SpO₂ 0x19, what is not on the wire
| link | handles 0x0017 write / 0x0019 notify, CCCD 0x001a ← 01 00; MTU 512 (ring) / 517 (phone); LE-encrypted by the phone's Just-Works pairing — the protocol has no auth |
| connect burst | 37 → 13-byte headerless → 68 → 55 → 29 → 6, then the app's handshake (0x2F, 0x3E every 2 s, 0x3D, 0x21 × 5 days, 0x01, 0x1A, 0x08, 0x22, 0x24) — identical across two sessions |
| HR | 0x07 then 0x1E 0000 173b 01 05; off-finger → no 0x0B frames, no error, the UI just says Measuring… |
| SpO₂ | 0x17 01 → echo → 0x19 empty = ended without value (APK case 25 → setBloodStatus(false)) |
| not on the wire | the interpreter screen uses the phone mic (only 0x2F/0x3D queries); LoraFit v89 has no find-ring, alarm or temperature UI for this ring |
| never captured | the 4-tap memo transfer (0x3D → 0x40 → 0x34 → 0x36) — needs a human tapping during a window; proven [LIVE] from the driver instead (Audio) |
The full ledger in the order things were found: Capture ledger.
tools/btsnoop_att.py — the dependency-free btsnoop → ATT decoder
tools/btsnoop_att.py turns a btsnoop (HCI H4) log — from the Android snoop
socket or a bugreport — into the ATT conversation: every write, notification
and read with direction, handle and payload hex. No dependencies beyond
Python 3.
python3 tools/btsnoop_att.py capture.log # writes + notifies, human-readable
python3 tools/btsnoop_att.py capture.log --all # every ATT PDU (MTU, discovery, reads)
python3 tools/btsnoop_att.py capture.log --handles # the recovered handle → UUID map
python3 tools/btsnoop_att.py capture.log --json out.att.jsonl
Output line:
15:05:23.412 TX WRITE_REQ h=0017 fe fc 01 00 01 00 01 00 07 00 b7 7e ae 6a 00 00 00
15:05:23.501 RX NOTIFY h=0019 fe fc 01 00 01 00 01 00 07 00 ea 07 09 13 0b 05 19
What it does
- Walks btsnoop records (timestamps are µs since year 0; the epoch constant
is
0x00E03AB44A676000), keeps HCI ACL packets, reassembles L2CAP fragments, and decodes CID 4 (ATT). - Recovers the handle → UUID map from Find-Information / Read-By-Type / Read-By-Group responses when the phone discovered services in-band. Android caches GATT for bonded peers, so a capture may have no discovery at all — then handles stay numeric (the JY handles are constant anyway).
--jsonemits one object per PDU:{ts, dir, op, handle, uuid?, hex}— thecaptures/*.att.jsonlfiles in the repo.
Reading JY frames in the output
Every JY frame starts fe fc; the cmd is the next byte (LE u16, high byte 0).
Payload begins at offset 10. The 13-byte headerless block after device info
(0c 98 00 <mac> 3a 00 48 00) is not a JY frame — it is the advertising
manufacturer data echoed on the link.
Pair the decoded lines with captures/markers.jsonl (tools/capture.sh mark)
to attribute a frame to a UI action.