Skip to content

Protocol

Full ledger·

The single-file protocol ledger (formerly PROTOCOL.md), in the order the findings were made — every section verbatim, collapsed. The organised reference lives under Protocol.

  • hackers who want every row with its evidence
  • 2 min read
  • 388 words

In 10 seconds

The lab notebook: every finding in the order it was made on 2026-09-19, verbatim, as the audit trail — early notes that were later corrected are flagged in the table. The organised reference is Protocol.

One screen of the ledger·

§ what it established tag superseded / current home
0 LoraFit = four radio stacks; the ring path is "JiangYun ring" (hr7/hy7), selected by 56FF APK chip "unknown" → Dialog DA14xxx (§7b)
1 advertising: name <model>-<mac4>, silent while held APK XOR-0xAD layout does not apply (§7b)
2 GATT 56FF · write 33F3 · notify 33F4 · BAS 2A19 APK BAS lies, use cmd 6 (§7b)
3 frame FE FC · cmd · total · index · len, no checksum; ts6 local seconds APK field order corrected the same day
4 · 4b 40+ command ids with payloads; connect sequence APK Commands — 28 now ✅
5 audio path: 0x2F/0x30/0x3D/0x40/0x34/0x36, Opus 16 kHz 40 B frames APK proven §7c · Audio
6 sample contract proposal shipped as tiny → Health platform
7 vendor-app HCI capture matched the APK read byte for byte CAPTURE Lab → Live capture
7b first live session from the Mac: info, name, features, battery, HR, terminators, alarms empty LIVE evidence/*.jsonl
7c a 6-minute memo pulled: 1522 packets, counter semantics, Ogg muxing LIVE Audio
7d macOS bonds the ring as a Mouse — and that is the fastest transport LIVE macOS transport
8–9 open items of the day current list: 3 minutes → still open

The ledger, verbatim·

Status legend: [APK] = derived from decompiled LoraFit com.wq.lorafit (base.apk pulled from Pixel via adb, jadx 1.5.x, see docs/raw/); [LIVE] = confirmed against a real ring (evidence/*.jsonl); [FCC] = FCC filing. Anything marked only [APK] is not yet proven on the metal.

§0. Identity / family
  • App code path: Kotlin source com/watchfun/bleManager/device/JiangYunRingDevice.kt (obfuscated defpackage/hr7), protocol builder/parser BLEProtocolManager (defpackage/hy7, log tag JY RING). Vendor SDK namespace watchfun, family name JiangYun (JY) ring. [APK: docs/raw/hy7_BLEProtocolManager.java]
  • LoraFit is a multi-product app: the same binary also carries Jieli RCSP (earbuds/glasses, svc AE00/AE01/AE02, libjl_*.so), a "JieTuo" ring protocol (svc 6800/6801/6802, defpackage/vr7) and a com.huahen.btutil earbud layer (svc DF97/DF98/DF99, adv filter FDB3/DF87). None of those is the SR116 — the SR116 path is selected when the GATT table contains a service whose UUID string contains 56FF (defpackage/t01.checkService).
  • Chipset: unknown. The brief's FCC ID "2BSR6-SR116" is NOT printed on the packaging (only an FC logo — see ~/.tiny/ring-20260919/photos) and the FCC OET database returns "no applications on file" for grantee 2BSR6 / product SR116 (tools/fcc.mjs, docs/fcc/apps.fcc.gov-search.html, 2026-09-19). fccid.io / fcc.report are behind Cloudflare Turnstile even for headed Chrome. → chipset will come from the live GATT (DIS 0x180A strings, vendor services present, OTA service UUID) once ring 2 is reachable. The JY protocol itself is chip-agnostic (plain GATT write/notify, no RCSP framing).
§1. Advertising [APK]
  • Name SR116-088D (last 4 hex of MAC). Owner's ring 1 MAC 73:01:06:00:08:8D (from LoraFit iOS).
  • Manufacturer-specific AD (0xFF) as parsed by BleVendorDataParser (huahen layer — may or may not apply to the ring; verify LIVE): cid u16le must be 0x22EB, bid u16le, pid u16le, state u8 (0 = free, 1 = connected), mac[6] XOR 0xAD, reserve[16]; min 29 bytes. [APK: docs/raw/BleVendorDataParser.java]
  • The ring does not advertise while connected to a phone (observed: 12 s scan, LoraFit holding ring 1).
§2. GATT [APK]
Role UUID (16-bit, Bluetooth base) Notes
Service 0x56FF000056ff-0000-1000-8000-00805f9b34fb selected by substring match "56FF"
Write (app → ring) 0x33F3 writeChara in mz0.delegateConnect
Notify (ring → app) 0x33F4 notifyChara; CCCD 0x2902 enable
Battery 0x180F / 0x2A19 standard BAS, read (hr7.getBattery), also JY cmd 6
Device Info 0x180A standard DIS strings present in APK string table (2A24..2A29) — verify

MTU: app requests MTU (getMTU cmd 3); packets are chunked to the negotiated MTU-3.

§3. Frame format [APK: hy7.buildPacket / parseReceivedData]

All little-endian, no checksum, no CRC:

offset  size  field
0       2     magic   0xFCFE  → bytes FE FC on the wire (short -770)
2       2     cmd     command id (see §4)
4       2     total   total packets in this logical message; requests always 1
6       2     idx     packet index (1-based); requests always 1   ← order CORRECTED 2026-09-19 (was swapped in v1;
                      hy7 logs "BLE包进度 %d/%d" with (field6, field4) = index/total; credit lane S)
8       2     len     payload length N
10      N     payload

Receiver rejects frames where len(frame) != 10 + len. Multi-packet replies (cmd 33/34/36/43/45/52) carry idx/total and are reassembled by the app; idx == total = last chunk. Timestamps in payloads are 6-byte LE "device time" = local wall-clock seconds (UTC + tz offset); convert with utc = dev - tz_offset_seconds (BleCommonUtils.deviceTimestampToUtcTimestamp).

§4. Command matrix (request payloads) [APK: hy7]
cmd name (APK) payload reply
1 setTime ts6 (device time) + 0x00 1: year u16, month, day, hour, min, sec
2 setUserInfo u8 ×5 (sex, age, height, weight, ?) + u64 ack
3 getMTU 00 00 ?
4 vibrateLED(mode) u8 ack — find-ring candidate
6 getBattery 00 00 6: battery_percent u8, charging u8
7 openHeartRate 7 ack; live HR then streams as cmd 11 notifications
8 closeHeartRate ack
9 getSportStatus 00 00 ?
10 pauseOrResumeSport(u8) u8, 0 10: current_sport_id u8, pause_status u8
11 (stream) live heart rate ts6, heart_rate u8 — APK self-test vector FEFC 0B00 0100 0100 0700 29FA656900 00 5F = 95 bpm
12 setSportTarget u32 steps ack
13 setAuxFunction 13 bytes of flags ack
14 setTimeFormat u8 (0 = 24h) ack
15 openCloseBpBsHrv(a,b) u8 a, u8 b ? (no parser branch → BP/HRV likely unsupported on SR116)
19 setBpCalibration u16 sys, u16 dia ack
20 openCloseTemperature u8 21: ts6, temperature_raw i16 (/100 °C)
23 openCloseSpo2 u8 24: ts6, spo2 u8
26 setAppInfo 0x00 + 12-byte app id (SimpleDeviceId from app UUID) ack
27 (stream) multi-sport realtime ts6 start, sport_type u8, ts6 now, duration u16, steps u32, calories u16, distance u16, heart_rate u8, temp i16 (/10), spo2 u8 (26 B)
28 setAppRunningInfo u32 ack
29 getStepInfo 29: ts6, steps u32, calories u16, distance u16
30 setAutoHeartRateSetting start_h, start_m, end_h, end_m, enable u8, interval_min u8 (app: 0,0,23,59,on,5) ack
33 getStepCountInfo(day) u8 day-offset items[16 B]: ts6, duration u16, steps u32, calories u16, distance u16 (0xFF-filled = end)
34 getSleepData(day) u8 day-offset items[8 B]: ts6, sleepQuality u8, moveIntensity u8
36 getHealthRecord(day) u8 day-offset items[14 B]: ts6, heart_rate u8, spo2 u8, temperature_tenths i16, u32 (unused)
37 getDeviceInfo firmware_version u16, mac[6], manufacturer_code u16, product_model u16
38 controlMultiSport(u8) u8 38
39 deviceOperation(u8) u8
40 (event) button button_event u8
41 sendPaste 01 – (HID paste trigger for translation flow)
42/43 (event) sleep / step-history sync finished ts6
45 (event) health record sync finished ts6
47 queryAudioState 00 00 audio_state, ext_value
48 controlAudioMode(u8) u8, 0 48/49: action, final_state
49 pauseOrResumeAudio(u8) u8, 0
50/51/52 offline audio stream 52 = getOfflineAudioData → Opus packets (40 B frames) via parseAudioPacketData
53 offline audio clear reply
54 ? streaming, audio_source
55 queryDeviceFeature 00 00 remote_recording_enabled, recording_storage_enabled, classic_bt_supported, alarm_supported (u8 each)
61 queryOfflineAudioFileCount offline_file_count u16
62 sendAppHeartbeat – (sent every 2 s by the app once features are known)
63 stopOfflineRecord
64 stopOfflineRecordTransfer
65 setAlarmList 5 × (enabled u8, hour, minute, repeatMask&0x7F) = 20 B alarm_set_result
66 ? android_connect_request / ios_connect_request / classic_bt_address
67 requestDeviceUnbind unbind_result
69 queryAlarmList alarms[] (4 B each)
68 (event) device name len u8 + ASCII BLE name (LIVE; not in APK parser)
0x36 (54) clearOfflineAudioData(u16) u16 remaining (echo what the ring pushed) LIVE: ring pushes 0x36 <remaining> after the last 0x34 packet; app echoes it → file deleted
§4b. Connection sequence [APK: CardManager2.onConnected / hr7.enableNotifications]
  1. GATT connect, requestConnectionPriority(HIGH), read BAS 0x2A19, enable CCCD on 0x33F4 (≈100 ms delays between).
  2. +1000 ms: setTime (1) → setAppInfo (26: 00 + 12-byte app id derived from the app's install UUID — identity, not a secret) → openHeartRate(7) if the user enabled live HR, else closeHeartRate(8) → setAutoHeartRateSetting(30) = 00 00 17 3B <enabled> <interval_min> (window 00:00–23:59, default 5 min).
  3. queryDeviceFeature (55); when recording is supported the app starts sendAppHeartbeat (62) every 2000 ms.
  4. Writes are queued and paced ≥300 ms apart (150 ms during audio transfer); a write that fails 3× is dropped. No pairing/bonding, no challenge–response was found anywhere in the JY path.
§5. Audio ("AI recording") path [APK]

The ring records to on-board flash (recording_storage_enabled) as Opus, 40-byte frames, and streams them over the same 33F4 notify char in cmd-52 multi-packet messages (parseAudioPacketData: totalPackets / currentPacket, rate log "离线音频同步速率"). Live "remote recording" (remote_recording_enabled) uses cmd 48/49. Translation itself happens in the phone (Zego / AIKIT / ChatGPT libs) — the ring is only a mic + storage.

§6. Normalized sample contract (proposal to lane H)

{ts, device_id, metric, value, unit, source:"sr116", raw} with metrics: heart_rate bpm, spo2 %, temperature °C, steps count, calories kcal, distance m, sleep_stage (sleepQuality enum), battery %.

§7. Live capture corroboration (lane DYN, Pixel HCI snoop of LoraFit ↔ ring 2, 2026-09-19)

Independent of the APK read, the on-air bytes confirm: write handle 0x0017 / notify 0x0019, FE FC op 00 01 00 01 00 len16 payload, ops 0x25 devinfo (MAC echoed), 0x06 battery (63 01 = 99 %, charging), 0x01 set-time → ring echoes Y/M/D/h/m/s, 0x3E heartbeat every ~2 s, 0x21→0x2A history, 0x07 open HR, 0x1E auto-HR, 0x08, 0x1A app-info (13 B), 0x2F, 0x3D. One op 0x44 (68) ("name") is not in the APK's BLEProtocolManager — bytes requested from DYN. Full dumps: captures/ + docs/LIVE-CAPTURE.md (DYN).

§7b. LIVE from the Mac mini (bleak, 2026-09-19 15:2x–15:4xZ, evidence/{gatt,handshake,hr,sync,alarms,find}.jsonl)

Connected to ring 1 SR116-088D (73:01:06:00:08:8D, on charger, fw 0x0098=152, LoraFit had released it) and observed ring 2 advertising as JY-Y3-011A (73:01:06:00:01:1A). Facts now [LIVE]:

  • Advertising: name SR116-088D / JY-Y3-011A (per-unit label, NOT a fixed prefix — filter on 0x594A mfr id or FEF5 svc); service UUIDs 0xFEF5 + 0x1812 (HID); manufacturer data company id 0x594A ("JY") = mac[6] | fw u16 | 3a00 | 4800 | 01 01 | 00×4 (MAC in clear, no XOR — the huahen 0x22EB layout does NOT apply).
  • Chipset: Dialog/Renesas DA145xx — service 0xFEF5 with the standard SUOTA characteristic set (5f78df94-… status notify, 8082caa8 mem-dev, 724249f0 gpio-map, 6c53db25 mem-info, 9d84b9a3 patch-len, 457871e8 patch-data, 64b4e8b5/42c3dfdd/b7de1eea version/mtu/pd). Never write these (OTA). No DIS strings; 0x2A50 PnP ID = 01 ac05 2002 1001 (Bluetooth SIG vendor 0x05AC, product 0x0220, version 0x0110). Extra vendor svc 0xFF12 (FF15 read/write-wo-resp, FF14 notify) — unknown, untouched.
  • GATT handles: 33F3 = h22, 33F4 = h24 (matches DYN's 0x0017/0x0019 att handles incl. CCCD), BAS 2A19 = h43 (read 64 = 100 % while the JY cmd 6 said 77 % — BAS is not reliable; use cmd 6). MTU negotiated 512 (CoreBluetooth).
  • On connect (unsolicited, in this order): cmd 37 devinfo 9800 73010600088d daa5 01a0 (fw 152, mfr 0xA5DA, model 0xA001), a headerless 13-B frame 0c 9800 73010600088d 3a00 4800, cmd 68 name 0a "SR116-088D", cmd 55 features 00 01 (remote-recording off, storage on), cmd 29 step snapshot, cmd 6 battery 4d 01 (77 %, charging).
  • Echo semantics: settings commands (1, 8, 26, 30) are acknowledged by the ring echoing the SAME frame back (set-time echoes decoded Y/M/D/h/m/s ea07 09 13 0b 1b 33 = 2026-09-19 11:27:51 local).
  • History syncs: 33/34/36 + day → item packets (each idx=1,total=1, several items per packet) → terminator 42/43/45 carrying the day's 00:00 local timestamp (80d0ad6a0000 = 2026-09-19 00:00 EDT). For the in-progress day the sleep sync returned items but no 43 → driver idle-closes after 3 s. Sleep bucket = 300 s (036aae6a…2f6bae6a…), quality 0 + motion 2 while on the charger.
  • Proven acks: cmd 69 alarms → empty list; cmd 4 vibrate mode 1 → echo 01 (physical vibration not yet confirmed by a human); cmd 7 open HR → ack, no cmd-11 samples while on charger / off finger (expected).
  • Not yet proven (need the ring on a finger): live HR values, SpO2 (23→24), temperature (20→21), health records (36), step buckets (33) with non-zero data, button events (40), audio (52).
§8–9. Open items
  • LIVE proof of the on-finger rows (§7b last bullet) — needs a ring worn for ~10 min near the Mac mini.
  • cmd 9 payload; exact vibrateLED modes (owner to confirm buzz); which byte in cmd 2 is which; svc 0xFF12 purpose.
  • Chipset detail (DA14531 vs DA14585) — SUOTA version char read returned empty; ask the ring for 0x2A50 only.
§7c. Audio path — LIVE PROVEN 2026-09-19 (ring 2 JY-Y3-011A, on charger)
step wire result
2F 00 00 queryAudioState 2F 00 00 idle; 3D00 00 files
30 01 controlAudioMode(1 = online) 30 01 00 ack, then 2F40 00 for the whole window no 0x32/0x33 frames on the charger — but the ring started an offline recording to flash at that instant (state 0x40 = hr7 Z; sa0 maps 64 → onOfflineRecordingCompleted)
30 00 stop 30 00 00 recording continued on-ring (see below), file count → 1
40 stopOfflineRecordTransfer · 1 s · 34 getOfflineAudioData 1522 × 34 packets, header total=1522 index=1..1522, 486-B payload each 18 264 Opus frames = 6 min 05 s; then ring pushes 36 01 00 (upload completed, remaining=1)
36 01 00 (echo the ring's number, like LoraFit clearOfflineAudioData(remaining)) 3D00 00 file deleted

Audio packet payload (all 3 audio cmds): u32 LE seconds | u16 0000 | 12 × 40 B Opus. The u32 is the ring clock (local-epoch, same base as cmd 1) at recording start, then +1 per packet — a sequence counter seeded with the start time, not a per-packet timestamp (1522 packets = 240 ms of audio each, counter advanced 1522). First packet of the capture read 1789818700 = 11:51:40 local = the second my 30 01 was written. Opus TOC byte is 0xB8 on every frame (CELT fullband, 20 ms, mono) — libopus decodes it; ffmpeg accepts our Ogg wrap (ring.audio.write_ogg_opus). Content check: -31.6 dBFS mean, clicks + a harmonic burst around 100 s → the mic IS live in the charger cradle. So: "mic gated by charging" is wrong; what is gated is streaming — on the charger mode 1 falls back to on-ring recording. Open: how long the on-ring recording ran (6 min for an 8-s request → probably until the 0x2F poll/idle-close or a fixed cap; retest worn).

Driver: ring-cli audio-state | audio-record --seconds N | audio-pull --out memo.opus (+ .wav via ffmpeg). Audio cmds 0x32/0x33/0x34 bypass the transport's multi-packet reassembly (each packet has its own header; PASSTHROUGH_CMDS).

§7d. macOS bonds the ring as a "Mouse" — and that is the BEST transport on a Mac

The ring advertises HID 0x1812 and reports PnP ID VID 0x05AC / PID 0x0220 (Apple's IDs). During any CoreBluetooth session the RING sends an SMP Security Request (HID habit); bluetoothd pairs Just-Works silently, files the ring as a Mouse and from then on auto-connects it whenever it advertises (system_profiler SPBluetoothDataType → Connected, Minor Type Mouse; blueutil --paired | grep -iE 'JY|SR116'). Re-bonding happened within 3 min of "Forget" with no BAS read, so it is not something the driver can avoid.

Consequences: - Phones/LoraFit/other centrals cannot see the ring while the Mac holds it. To hand it over: System Settings → Bluetooth → ⓘ → Forget This Device (blueutil --unpair/--disconnect silently no-op on LE bonds), then keep the Mac's CoreBluetooth apps off it, or turn the Mac's Bluetooth off. - On the Mac it is a gift: CBCentralManager.retrieveConnectedPeripheralsWithServices([56FF]) returns the held ring and BleakClient(device).connect() attaches in ~10 ms — no scan, no advertising needed, MTU 512, ring answers immediately (ring-cli battery end-to-end 0.28 s vs ~10 s via scan). Several processes can attach at once; detaching (disconnect()) does not drop the system link. ring.transport.system_connected_rings() implements it and Ring.__aenter__ tries it first (evidence via: system_link), falling back to a 45-s scan. - The bonded ring stays connected on the charger and off it, as long as it is in range → the bridge never needs to reconnect; battery cost on the ring side is the HID connection interval macOS negotiates (unmeasured yet).

Decision for the owner: the Mac mini becomes the ring's hub (tiny is the app) — or LoraFit keeps the ring and the Mac only borrows it while the phone is out of range. Both can't hold it at once.