Skip to content

[Wearable]: SY01 Ring from Aliexpress #55

Description

@Shabbypenguin

Ring / wearable model

SY01 - RWfit

Advertised BLE name

SY01

Primary BLE service UUID(s)

0000a00a-0000-1000-8000-00805f9b34fb

Characteristics (write / notify UUIDs)

Used claude + android debugging and de-compiled the official apk. had it do a whole write up of the connectivity and data. I dont have any sleep data just yet.

https://www.aliexpress.us/item/3256811727783582.html Ring in question.

SY01 Ring — Protocol Notes

How PulseLoop talks to the SY01 ring: the SDK underneath it, the connection sequence, the raw
bytes that come off the ring, how those bytes get parsed, and exactly what PulseLoop reads out of
them. Everything marked VERIFIED was checked byte-for-byte against a real reading during
reverse-engineering; everything marked UNVERIFIED is a documented guess PulseLoop does not act
on yet.

Source material: a BLE HCI snoop capture of the official RWFit app (com.rw.revivalfit) talking to
a real SY01, cross-checked against a decompile of that app's classes.dex.


1. What SDK this actually is

The ring's MCU is a Zhuhai Jieli Technology ("Jieli") chip — a very common BLE SoC used across
cheap Chinese wearables. Two pieces of evidence nail this down:

  • The APK ships native libraries libjl_ota_auth.so and libjl_bmp_convert.so (the jl_ prefix is
    Jieli's signature), plus a com.jieli.jl_bt_ota package handling firmware OTA updates.
  • The app's internal class/field naming is full of JL-prefixed identifiers for sync data —
    JLStepSyncBean, JLSleepSyncBean — matching Jieli's reference naming conventions.

Important distinction: Jieli's public SDK covers the BLE transport and OTA firmware update
mechanism — that part is genuinely "Jieli's SDK." The actual health-data protocol (the
CMD/Key/KeyFlag framing and everything below in this doc) is not part of any public Jieli SDK —
it's custom code the RWFit app itself implements on top of the Jieli transport. There was no
existing documentation or open-source project (unlike, say, the Colmi/QRing family, which
Gadgetbridge already reverse-engineered) — this was reconstructed from scratch from the capture
and decompile.


2. How PulseLoop connects to it

Standard BLE GATT, no pairing/bonding, no authentication:

  1. Scan for a peripheral advertising GATT service 0000a00a-0000-1000-8000-00805f9b34fb.
    (We don't have a captured advertised local name to match on — unlike the jring's SMART_RING or
    Colmi's R02_* patterns — so PulseLoop currently recognizes it by service UUID alone.)

  2. Connect and discover services. The ring exposes several GATT services, only one of which
    matters for health data:

    Service UUID What it is
    1800, 1801 Standard BLE generic access/attribute services — ignored
    1812 HID over GATT — the ring's tap/touch gesture interface, unrelated to health data
    ff00 Custom service, characteristics ff01/ff02 — present but never used in any capture
    a00a The real health-data service
    0bc0 Jieli OTA firmware-update service (00010203-...-1912/...-2b12) — separate from health data entirely
  3. Within service a00a: write to characteristic b002, enable notifications on
    characteristic b003 (standard CCCD write to 2902). Every command goes out on b002; every
    reply/push comes back on b003.

  4. No GATT Battery Service (180F/2a19) exists — battery comes back in-band over the same
    protocol, like everything else.

There's no encryption, no bonding, no authentication handshake beyond a capability/bind exchange
(§6). Anyone in BLE range who knows this protocol can read your ring's data — the same is true of
essentially every cheap BLE wearable in this price bracket.


3. The raw wire format

Every message in both directions is a 0xAB-prefixed frame:

byte 0:     0xAB                      magic / frame start
byte 1:     type                      0x01 on every outbound command; inbound varies 0x01/0x11
bytes 2-3:  declared length           big-endian u16 (see note below)
bytes 4-5:  CRC16                     big-endian, computed over bytes 6+ (see §4)
byte 6:     CMD                       coarse category
byte 7:     Key                       the real command selector
byte 8:     KeyFlag                   0x10 = start/query, 0x30 = stop, 0x00 = plain
byte 9+:    data                      payload, meaning depends on (CMD, Key)

Example — a real captured frame, the battery reply that decoded to 83%:

ab 11 00 06 48 bd 02 03 10 53 10 a4
│  │  │  │  │  │  │  │  │  └──────┘
│  │  │  │  │  │  │  │  │     data: [0x53, 0x10, 0xa4] → battery% = data[0] = 0x53 = 83
│  │  │  │  │  │  │  │  └─ KeyFlag = 0x10 (query)
│  │  │  │  │  │  │  └──── Key = 0x03
│  │  │  │  │  │  └─────── CMD = 0x02 ("system")
│  │  │  │  └──┴────────── CRC16 = 0x48bd, over bytes [02 03 10 53 10 a4]
│  │  └──┴───────────────── declared length = 0x0006 = 6 bytes (matches the 6 payload bytes here)
│  └────────────────────── type = 0x11 (data reply)
└───────────────────────── 0xAB magic

The "declared length" field is not fully reliable. For several commands (steps, sleep) it
declares a much larger length (e.g. 51 bytes) than what's actually delivered in the physical BLE
notification (e.g. 14 bytes) — and no continuation packet ever follows. Every capture we took
showed the app (and now PulseLoop) simply treating one physical notification as the complete
message, with the undelivered tail implicitly zero. This is different from, e.g., the Colmi ring in
the same app, which genuinely fragments large replies across multiple notifications that must be
reassembled — the SY01 doesn't appear to do that in practice.


4. "Decryption" — there isn't any, just a checksum

To be precise about terminology: nothing on this ring is encrypted. There's no cipher, no key
exchange, no signing. The only integrity mechanism is a CRC16 checksum, which exists to catch
transmission errors, not to hide or authenticate anything.

The CRC is CRC-16/ARC: polynomial 0xA001 (reflected), initial value 0x0000, no final XOR —
a standard, textbook variant. We extracted the exact 256-entry lookup table and algorithm straight
out of the app's classes.dex (Ly5/d;->a([B)Ljava/lang/String;), then independently verified it
by computing the same CRC in Python over three different real captured frames and getting an exact
match every time before shipping it. The algorithm:

crc = 0x0000
for each byte in (CMD, Key, KeyFlag, data...):
    index = (crc XOR byte) AND 0xFF
    crc = (crc >> 8) XOR table[index]
# result is stored big-endian in bytes 4-5 of the frame

So "decoding" a frame is really: read the header → verify (or in PulseLoop's case, trust and just
parse) the CRC → look at (CMD, Key) to know what kind of message this is → read the data bytes at
the offsets known for that specific command.


5. What data the ring actually sends, and what PulseLoop reads

Every row below other than Sleep has been matched against a real on-screen reading — not inferred
from the app's decompiled field names alone.

Metric CMD / Key KeyFlag Data layout Verified against
Time sync (outbound, sets the ring's clock) 02 / 01 00 [year-2000, month, day, hour, min, sec], 6 bytes Decoded value matched the capture's own connection timestamp, twice, on two different connections
Battery 02 / 03 10 data[0] = percent (0–100) 83% × 3 separate reads
Heart rate 05 / 03 (on-demand) or 02 / 24 (live push) 10 [0x31, 0xDF, counter_hi, counter_lo, VALUE, 0x00] — bpm at data[4] 93, 94, 93 bpm
SpO2 05 / 09 (on-demand) or 02 / 4e (live push) 10 Same shape as HR, value at data[4] 99%, then 98%
HRV 05 / 0a (on-demand) or 02 / 69 (live push) 10 Same shape, value at data[4], in ms 37 ms
Stress 05 / 0d 10 Same shape, value at data[4], 0–100 scale 35, then 42
Steps + calories 05 / 1a 10 [0x31,0xDF,counter,counter, steps: u32-BE @ bytes 4-7, 0x00,0x00, calories_raw @ byte 10], kcal = calories_raw / 40.0 41, 79, 95 steps; 2.75→"2.8" and 3.3 kcal exactly
Sleep 05 / 05 10 Unknown — command confirmed to exist and ack, but no real sleep session has been captured yet, so no byte offsets are claimed Not yet verified
Blood pressure, blood sugar 05 / 04, 05 / 10 Not implemented The ring likely doesn't have these sensors at all — command slots exist because they're part of the shared Jieli-adjacent app codebase used across multiple ring models, not because this hardware supports them

Distance is not transmitted by the ring at all. At 95 steps, RWFit showed 0.07 km — that implies
~0.74 m/stride, a perfectly normal stride length, and there's no unaccounted byte left in the steps
payload for a distance field. RWFit is almost certainly computing distance client-side from
steps × your stored height/stride, and PulseLoop does the same rather than reading a wire field
that doesn't exist.

Starting a live stream: heart rate, SpO2, and HRV readings can be requested two ways — a single
on-demand read (KeyFlag=0x10 then 0x30 to stop), or a continuous live stream, kicked off by a
separate trigger command: CMD=06, Key=09, data [sensorType, 0x05, enable], where sensorType is
the same Key byte as the metric's on-demand command (0x03=HR, 0x09=SpO2, 0x0a=HRV, 0x0d=stress).
This was captured immediately before/after every live reading for all four metrics.


6. The one thing PulseLoop sends that isn't a sensor read

Right after connecting, before any sensor query, the ring and app exchange a handshake:
CMD=03, Key=01 — the ring replies with what looks like a capability/feature descriptor (the
decompile calls the corresponding data class BindInfoBean, with flags like setIsHeartRate,
setIsBo (SpO2), setIsStep, setIsSleep, setIsBp, setIsHrv, setIsPressure (stress),
setIsBloodSugar). This is present and confirmed to exist on the wire, but PulseLoop doesn't parse
it yet — it's how the official app would find out which sensors a given ring model actually has,
which would be useful if this driver ever needs to support ring variants with a different sensor
set under the same protocol family.


7. Where this lives in PulseLoop

File Role
Sy01Protocol.swift UUIDs, CRC16 table, Sy01Frame (parse/wrap), the (CMD,Key) command dictionary
Sy01Encoder.swift Builds outbound logical commands (time sync, battery/HR/SpO2/steps/HRV/stress/sleep queries, realtime-stream triggers)
Sy01Decoder.swift Maps a parsed frame to the shared RingDecodedEvent — the only place byte offsets are read
Sy01Driver.swift WearableDriver conformance: BLE topology, frame wrapping, notification routing
Sy01SyncEngine.swift Connect-time handshake sequence + live-stream start/stop
Sy01Coordinator.swift Recognizes the ring by service UUID, declares which capabilities are safe to show in the UI

Sy01Coordinator.capabilities only declares heart rate, SpO2, steps, battery, HRV, and stress —
sleep and blood pressure/blood sugar are deliberately left off so the app never shows a UI card
backed by a guessed decode.

Which capabilities does this device have? (best guess is fine)

  • Heart rate (spot)
  • Heart rate (history)
  • Heart rate (live / workout)
  • SpO₂
  • Steps / distance / calories
  • Sleep stages
  • REM sleep
  • HRV
  • Stress
  • Body temperature
  • Battery level
  • Find device

Known packet formats / captures


Vendor app it ships with

No response

Do you have this device and can you test a driver?

Yes — I own it and can test builds

Would you like to write the driver yourself?

None

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions