Hacking a $7 AliExpress Smart Ring

Reverse-engineering the BLE protocol of a chinese smart ring

I bought a smart ring for $7 from AliExpress. You can easily find similar rings for under $20 on AliExpress or Temu. I found it really cool that they were able to compress sensors, battery, microcontroller, and other electronics into a ring for this price. I mostly wanted to see how the quality compared to other health wearables I use. I did not expect much from the ring, mostly PPG heart rate measurements and a bad app with poor English translations. But to my surprise the ring packed a decent amount of features and the app was actually usable. The ring can estimate heart rate, SpO2, count steps, calories, distance, and sleep. The app is not great but it is usable. I still haven’t gotten around to comparing the actual numbers, but the ring seems to be close in heart rate and SpO2, but off in steps, calories, and distance. For sleep I see a variance of around 15 minutes. The battery life is not great and lasts about 5 days on a single charge.

Seller-provided specs:

Material: 304 stainless steel, glue pouring process
Main chip: Coolchip AB2026
Memory: 64KB + 8K cache + 8Mbit flash
Bluetooth: 5.4
Charging time: 2 hours
Runtime: 3-5 days normal use
Charging: magnetic charging
Compatibility: Android 9.0+, iOS 10.0+
Sizes: 7-13
Thickness: 2.5 mm

Next I wanted to see if I could reverse engineer the BLE data from this ring and use this data to build my own apps around it. The AliExpress listing [1] I bought the ring from is very generic and does not provide any useful information like manufacturer, model, chipset, etc. It mentions some generic information like the chip being a Coolchip AB2026, which I am pretty sure is not a thing. I found some other listings for slightly more expensive rings mentioning the manufacturer as Colmi and the chip as Realtek RTL8762. There are some other open-source projects that have successfully reverse engineered the Colmi ring [2, 3]. I started out by trying these but I was not able to get them to work with my ring. The rings do look very similar; my guess would be that they are using the same hardware, just different firmware that implements the protocol in different ways.

The ring is pretty neat: it has anodized metal outside with electronics neatly packed inside. It appears to have a PPG sensor, IMU, BLE-enabled microcontroller, battery, and a magnetic charging contact. My ring does not appear to have a display, but there are some other Chinese rings that do have a display to show time [4]. Over BLE it advertised itself as SMART_RING, and the app showed SpO2, heart rate, steps, calories, sleep, and a funny selfie mode where clenching the ring triggers the phone camera. It works with an app called JRing on iOS and Android (see screenshot below).

The official JRing app — home screen with steps, sleep, and heart rate (left), and a detailed sleep breakdown (right). This is the ground truth I kept comparing my sniffed packets against.

I was curious how much of the app protocol I could figure out. So I started out by using an nRF52840 BLE dongle with Wireshark and the nRF Sniffer plugin to capture the interaction between the ring and the app. I tried to perform different actions in the app like syncing the ring, triggering instantaneous measurements for heart rate and SpO2, changing settings for the ring, etc., and then manually analyzed the packets to see if I could figure out the protocol. This was my first real attempt at BLE packet sniffing, so the beginning was mostly failure. My first captures only had advertising packets. I could see the ring, but I was not catching the actual connection. The useful breakthrough was learning to capture the CONNECT_IND, follow the connection, and filter down to ATT/GATT traffic. Once I had a good capture, the protocol started to show itself. To my surprise there was no signing or encryption whatsoever. The packets were all cleartext and I was able to see the raw data from the ring. I did this over 10 times trying to capture all variations of the different commands and data packets (see my detailed notes that I built while trying to break down the protocol [5]).

Next, I used Codex to help go through the captures, compare packets, and build a Python CLI with Bleak. That CLI became the fastest way to test guesses. Instead of opening the app every time, I could connect to the ring and send commands directly and see if I was getting the correct responses. Then, I would look at the notifications and compare them with what the official app showed.

The protocol

Here is a high-level view of what I pieced together. The full implementer reference, with confidence markers and open questions, lives in my protocol notes [6].

Transport

The whole app protocol runs over a single custom GATT service, 0x56ff. There is no encryption or signing anywhere — every command and response is cleartext, which is what made this tractable in the first place.

Advertised name:  SMART_RING
Service:          0x56ff
Write char:       0x33f3   (central -> ring)
Notify char:      0x33f4   (ring -> central)

The ring advertises as SMART_RING, and the first six bytes of its manufacturer data are its own BLE address. To talk to it you connect, enable notifications on 0x33f4, write 20-byte commands to 0x33f3, and read 20-byte notifications back. That is the entire transport.

Packet format

Every packet is a fixed 20 bytes. There is no length field or framing header. Byte 0 is the command/response ID, and the rest is payload zero-padded out to 20 bytes. Multi-byte integers (timestamps, steps, distance, calories) are little-endian.

byte 0       command / response id
bytes 1-19   payload fields and zero padding

Annotated example

The current-activity response (0x03) is a good example of how the fields pack in. The app showed 328 steps, 0.28 km, and 18 calories, and the matching packet decodes cleanly:

03 fd7c156a 48010000 1f010000 12000000 504600
│  │        │        │        │        └ unknown / padding
│  │        │        │        └ calories = 18
│  │        │        └ distance units = 287  (~0.28 km)
│  │        └ steps = 328
└ command 0x03 = current activity

I repeated this loop several times: trigger an action, read what the app displayed, then go find the packet whose bytes matched.

Commands

These are the commands I confirmed, either by sending them from the CLI or by matching responses against app-visible data. Multi-packet flows (activity, sleep, HR, SpO2) send a small acknowledgement first and then stream data packets back.

Command Byte 0 Direction What it does
Time sync 0x01 write Set clock (unix time) + timezone offset
Current activity query 0x02 write Request current activity; triggers 0x03/0x13
Current activity 0x03 notify Steps, distance, calories, timestamp
Find ring 0x04 write Buzz the ring to locate it (byte 1 = parameter)
Selfie shutter event 0x06 notify Fired when you clench the ring
Selfie mode 0x07 write Enable/disable clench-to-trigger camera
Status 0x0c write Device status; response embeds ring MAC
Factory reset 0x0e write Reset device; magic payload fedcba9876543210
Sleep / history query 0x10 write Request sleep + history; triggers 0x11
Sleep timeline 0x11 notify 15 one-minute sleep-stage samples per packet
Activity summary 0x13 notify Companion summary to 0x03 (partly decoded)
Live heart rate 0x14 write/notify Start HR; BPM streams back at byte 5
HR stop 0x15 write Stop/cleanup HR measurement
Stored measurement query 0x16 write Pull stored HR/measurement history
Automatic HR schedule 0x19 write Configure auto-HR window, cadence, on/off
Step goal 0x1a write Set daily step goal (e.g. 10000)
Locale 0x21 write Set locale string, e.g. en-US
SpO2 0x23 write/notify Start/stop SpO2; progress packets 0x24
SpO2 result 0x24 notify SpO2 percentage at byte 4
HR complete 0x27 notify Heart-rate measurement finished
Air control / HID mode 0x52 write Likely toggles BLE HID air-control (unconfirmed)

Battery is the one thing that does not go through this protocol — the ring exposes the standard BLE Battery Service (0x180f / 0x2a19), so you just read the percentage directly.

This is still work in progress and a few commands are still murky. The 0x52 air-control writes were accepted but never visibly did anything from my CLI, because the air-control feature rides on a BLE HID service that macOS/CoreBluetooth hides from Bleak entirely. And several notification IDs (0x0b, 0x20, 0x44, 0x48, 0xf6) show up during sync but I never fully decoded them.

How to use the CLI

The CLI [7] is a Python script that uses the Bleak library to connect to the ring and send commands.

The CLI provides the following commands:

Commands:
  help                         Show this help.
  scan [seconds]               Scan for BLE devices and auto-select SMART_RING.
  connect [address]            Connect to selected ring, or specific address/UUID.
  disconnect                   Disconnect.
  services                     Print discovered services/chars.
  notify on                    Enable notifications on 0x33f4.
  notify off                   Disable notifications.
  hid scan                     Print HID service/report characteristics.
  hid on                       Enable HID report notifications.
  hid off                      Disable HID report notifications.
  hid map                      Read raw HID report map.

  battery                      Read standard BLE battery percentage.
  status                       Send device/status query.
  time sync                    Send app-style current time/timezone command.
  locale                       Send en-US locale command.
  activity                     Request current steps/activity packet.
  sync baseline                Send status, time sync, locale, activity query.
  history                      Request history summary and measurement stream.
  sleep                        Request sleep/history timeline packets.
  spo2 start                   Start SpO2 measurement.
  spo2 stop                    Stop SpO2 measurement.
  spo2 run [seconds]           Start SpO2, wait, stop. Default 25s.
  hr start                     Start HR measurement.
  hr stop                      Stop/cleanup HR measurement.
  hr run [seconds]             Start HR, wait, stop. Default 30s.

  selfie on                    Enable selfie/clench mode.
  selfie off                   Disable selfie/clench mode.
  find                         Send possible find-ring 04 0a command.
  mode 1                       Send 52...01 candidate.
  mode 2                       Send 52...02 candidate.
  mode reset                   Send 52...ffffffff reset candidate.
  phase2                       Run known-variant test sequence automatically.

  raw <hex>                    Send any hex payload to write char.
  sleep <seconds>              Wait while notifications keep logging.
  quit                         Disconnect and exit.

One macOS quirk worth noting: CoreBluetooth hides the real BLE MAC and hands you a per-machine CoreBluetooth UUID instead, so you cannot hard-code the address across machines. So you need to forget the ring everytime you rerun the CLI.

References

  • [1] AliExpress listing for the ring: https://www.aliexpress.us/item/3256810466598469.html
  • [2] Colmi R02 ring reverse engineering by tahnok: https://tahnok.github.io/colmi_r02_client/colmi_r02_client.html
  • [3] Hackaday post about revererse engineering a smart ring: https://hackaday.com/2024/06/16/new-part-day-a-hackable-smart-ring/
  • [4] Reverse engineering the SR08 smart ring with display: https://github.com/atc1441/ATC_SR08_Ring
  • [5] My detailed notes that I made while reverse engineering the protocol: https://github.com/saksham2001/Smart-Ring-Protocol/blob/main/lab-notes.md
  • [6] The full protocol reference: https://github.com/saksham2001/Smart-Ring-Protocol/blob/main/Protocol.md
  • [7] The CLI code: https://github.com/saksham2001/Smart-Ring-Protocol/blob/main/smart_ring_cli.py