aboutsummaryrefslogtreecommitdiff

zwzn-freefit-re

Reverse-engineering a sketchy Chinese watch app

Documentation/protocols derived from decompiled FereFit Android app (jadx)
FereFit APK obtained from APKPure
(com.czw.freefit, SHENZHEN ZHONGWEI INTELLIGENT TECHNOLOGY Co.,Ltd)

Disclaimer

I request that none of this code and/or documentation, in part or in full, be hosted on GitHub, SourceForge, or any other proprietary platform. This request is made out of respect for both me, the developer, and you, the user.

What this is and why it exists

This is a git repo containing protocol documentation of the freefit watch app, commonly known as "FereFit", "Lyne_Wearables", "HomieFit", "zwsvibe", and "WIRCASS". It allows us to make our own app implementations for some watches that use these apps. In particular, my testing occurs with the "Watch ULTRA", which I cannot find any more brand information on, though it's a sort of no-name Apple Watch ULTRA knockoff. Although, this protocol should work fine for most other watches that use these apps.

This exists for many reasons, though here are the top few:

  • I'm 13 and have too much free time
  • I wanted to get into reverse-engineering
  • I had an old knockoff "Watch ULTRA" from the arcade, and the original app was spyware
  • The RTC on this watch drifts badly, so without an app to periodically synchronize it, it becomes almost unusable

Anyway, talk is cheap, let's get into the documentation!

BLE characteristics

Main/"Zk" characteristics (presumably Nordic UART/Nordic semiconductor chips)

  • Service UUID: 6E40FC00-B5A3-F393-E0A9-E50E24DCCA9E
  • Write characteristic UUID: 6E40FC20-B5A3-F393-E0A9-E50E24DCCA9E
  • Notify characteristic UUID: 6E40FC21-B5A3-F393-E0A9-E50E24DCCA9E

Jieli/JL chip characteristics (untested)

  • Service UUID: 0000ae00-0000-1000-8000-00805f9b34fb
  • Write characteristic UUID: 0000ae01-0000-1000-8000-00805f9b34fb
  • Notify characteristic UUID: 0000ae02-0000-1000-8000-00805f9b34fb

Protocols/functions

Generic measurement response

Sent by watch depending on context. For some reason it sends this for a battery percentage if you send an invalid command. TODO find proper trigger for this

byte[0] = 0x94 (response code)
byte[1] = measurement result (0-100)

syncTime

Time sync packet structure (write/6E40FC20)

byte[0]   = 0x01 (command)  
byte[1-4] = Unix timestamp (big endian, seconds)  
byte[5-8] = timezone offset (big endian, seconds)  
byte[9]   = i (unknown param, use 0x00)  
byte[10]  = language code  
byte[11]  = 0x01 if traditional Chinese, else 0x00  

Response: 0x81 0x00 (success, notify/6E40FC21)

switchFindBand (vibrate/find watch)

Find band packet structure (write/6E40FC20)

byte[0] = 0x51 (command)
byte[1] = 0x01 to start vibrating, else 0x00

Response: 0xD1 0x01 for vibrating, else 0xD1 0x00 (6E40FC21)

configRealTimeMeasure (heart rate/blood oxygen/blood pressure/blood sugar)

Real-time measurements packet structure (write/6E40FC20)

byte[0] = 0x60 (command)
byte[1] = 0x00 if heart rate, 0x02 if blood oxygen, 0x03 if blood sugar, 0x01 if blood pressure
byte[2] = 0x01 to start measuring, else 0x00

Heart rate possible responses:

  • Generic measurements: 0x94 <measurement>, e.g. 0x94 0x55
  • Aggregate measurements: 0xE1 <measurement 1> 0x00 0x00 0x00 <measurement 2> <measurement 3> <measurement 4> 0x00, e.g. 0xE1 0x58 0x00 0x00 0x00 0x57 0x58 0x55 0x00

Other responses (blood oxygen, blood pressure, blood sugar) have not been tested yet.

sendMessageByZk (push notification to watch)

Must send configDeviceSettings first to enable notifications on the watch, otherwise messages will not display.

Notification enable packet (write/6E40FC20)

byte[0]  = 0x02 (command)
byte[1]  = 0x02 (subcommand)
byte[2]  = skype enabled (0/1)
byte[3]  = line enabled (0/1)
byte[4]  = long time sit reminder interval
byte[5]  = long time sit enabled (0/1)
byte[6]  = call notice enabled (0/1)
byte[7]  = SMS notice enabled (0/1)
byte[8]  = WeChat enabled (0/1)
byte[9]  = QQ enabled (0/1)
byte[10] = KakaoTalk enabled (0/1)
byte[11] = Facebook enabled (0/1)
byte[12] = Twitter enabled (0/1)
byte[13] = WhatsApp enabled (0/1)
byte[14] = LinkedIn enabled (0/1)
byte[15] = heart rate monitor (0/1)
byte[16] = hands up screen on (0/1)
byte[17] = heart rate loop monitor (0/1)
byte[18] = heart rate monitor interval time
byte[19] = Instagram enabled (0/1)
byte[20] = other push enabled (0/1)
byte[21] = Zalo (bit 0) + Messenger (bit 1) flags

Message packet structure (write/6E40FC20), one packet per 17 byte chunk:

byte[0]    = 0x23 (command)
byte[1]    = chunk index (0, 1, 2...)
byte[2]    = notification type
             0x01 = SMS
             0x02 = WeChat
             0x03 = QQ
             0x04 = DingTalk
             0x05 = WhatsApp
             0x06 = Facebook
             0x07 = Twitter
             0x08 = LinkedIn
byte[3-19] = up to 17 bytes of UTF-8 message text
byte[20]   = 0xFF end marker (last chunk only, appended after text)

Message format from FereFit app: "Title: Content", title truncated to 23 bytes, content to 240 bytes.
In practice, you can just send random arbitrary text. The watch doesn't really care.

Send chunks sequentially with ~100ms delay between each.

enterMakeDial (watch face)

THIS IS NOT GUARANTEED TO BE STABLE!!
ENTERMAKEDIAL CAN VARY HEAVILY DEPENDING ON THE WATCH VARIANT
SOME WATCHES EXPECT RAW RGB565, SOME PRESUMABLY EXPECT BMP-WRAPPED RGB565(?)
THE FOLLOWING DOCUMENTATION WORKS FOR MY WATCH, IT MAY BE DIFFERENT FOR OTHER VARIANTS
I CANNOT TEST MORE THAN THE WATCH I CURRENTLY HAVE

This protocol sends a custom watchface to the watch. The watch expects raw RGB565 pixel data, sent in chunks.

Protocol flow:

  1. Send header packet (command 0x51)
  2. Wait for ACK from watch
  3. Send data chunks (command 0x52)
  4. Wait for ACK after each chunk

Watchface header packet (write/6E40FC20):

byte[0] = 0xE4 (ZK_DIAL command)
byte[1] = 0x51 (mode flag)
byte[2] = 0x01
byte[3] = 0x00
byte[4-5] = total packet count (big endian)
byte[6-9] = total image bytes (big endian) = width * height * 2
byte[10] = 0x00 (unknown, padding)
byte[11-12] = chunk size (big endian, usually 140)
byte[13] = dial type (0x01 = background, 0x02 = full watchface)
byte[14] = 0x01 (always)
byte[15] = UI overlay enable (0x00 = no UI, 0x01 = show time/date)
byte[16] = 0x00 (padding)
byte[17-18] = text color (RGB565, big endian)
byte[19-20] = checksum (sum of all pixel bytes, big endian)
byte[21] = hide date flag (0x00 = show date, 0x01 = hide date)

Data chunk packet (write/6E40FC20)

byte[0-3] = 0xE4, 0x52, 0x01, 0x02 (command)
byte[4-5] = chunk number (1-indexed, big endian)
byte[6-9] = byte offset (big endian)
byte[10] = progress percentage (0-100)
byte[11] = last chunk flag (0x01 if last, else 0x00)
byte[12-13] = checksum (sum of header[0:14] + data bytes, big endian)
byte[14+] = raw RGB565 pixel data (up to chunk size bytes)

Image format:

  • Resolution: 240x296 (actual display size, not 240x280 as advertised)
  • Format: Raw RGB565, no headers
  • Byte order: High byte first, low byte second (big endian)
  • Pixel layout: Top to bottom, left to right
  • Color conversion: RGB888 -> RGB565 = ((R>>3)<<11) | ((G>>2)<<5) | (B>>3)

MTU/chunk settings:

  • Fixed packet size: 148 bytes
  • Data payload per chunk: 140 bytes
  • Header size: 14 bytes

Chunked data example:

  • 240x296 = 142,080 bytes total
  • 142,080 / 140 = 1,015 chunks

Response (notify/6E40FC21):

  • Header ACK: 0xE4 0x51 0x02 ... 0x00 (byte[9] = 0x00 for success)
  • Chunk ACK: 0xE4 0x52 0x02 ... 0x00 (byte[9] = 0x00 for success)

Notes: - UI overlay (time/date) only appears if byte[15] = 0x01

  • Text color is presumably set via bytes[17-18] (RGB565)
  • The watch does NOT automatically overlay UI - you must enable it
  • Actual display height is 296, not 280 (white bar appears at bottom with 280)
  • Resolution and variables can vary quite a lot depending on watch variant

Scripts and tests

Located in the root of this git repo are some test scripts and files to get you going with your own implementations. BLE implementations are written in Python + the bleak library from PyPI. enterMakeDial requires Pillow.

The following scripts are available:

  • FereFit_syncTime_BLE.py --- synchronizes the time on the watch
  • FereFit_switchFindBand_BLE.py --- vibrates the watch for 5 seconds
  • FereFit_configRealTimeMeasure_BLE.py --- measures heart rate, TODO add more measurements later
  • FereFit_sendMessage_BLE.py --- sends a test message/notification of your choosing to the watch
  • FereFit_enterMakeDial_BLE.py --- uploads an image of your choosing to the watch face

They are named after FereFit, one of the names of the app, because I am decompiling the FereFit APK (even though it's all one codebase under different names, the app detects which one it's meant to be using functions such as isFereFit() and isHomieFit(), etc and just sets the UI and API to that).
They will probably stay that way because I can't be bothered to rename them to freefit, and therefore future ones will also be named after FereFit for consistency or something like that.

Status

As of 2026-03-29 (NZDT), I am actively reverse engineering this device. Hopefully, as I continue to take apart the freefit app, I can add more test scripts and documentation to this repo.