Android app for the Qingping Cleargrass CGD1 (cgllc.clock.dove) - Bluetooth LE alarm clock with sensors.
Table of Contents
- Warning
- Features
- Screenshots
- Technical Details
- Protocol Specification
- 1. Service & Characteristics Profile
- 2. Protocol Structure
- 3. Managing Alarms
- 4. Device Settings
- 5. Real-Time Sensor Stream (Connected)
- 6. Passive Sensor Stream (Advertising)
- 7. Battery Level (Connected)
- 8. Firmware Version
- 9. Audio Transfer Protocol (Ringtone Upload)
- 10. Known Command IDs Summary
- 11. GATT Disconnection Status Codes
App was largely created using LLMs. I still have reviewed the code, so it's only semi-slop, but you have been warned, etc., etc.
- Initial setup screen to guide the user through finding and selecting their device
- Scans for a specific Bluetooth LE device by its MAC address
- Share a saved device with others using QR code
- Parses and displays sensor data
- Management of up to 16 device alarms
- Custom ringtones support
- Global alarm switch to enable or disable all device alarms at once
- Bluetooth state monitoring with automatic prompts to enable it
- Interactive real-time previews for brightness and volume settings
- Widget for displaying sensor data on the home screen
- Configurable background updates to fetch data periodically
- Settings to customize the device's MAC address, theme (light/dark/system), and language
The application is built with modern Android development technologies and targets recent Android versions.
- Target API: The application targets Android 16 (API level 36) and has a minimum requirement of Android 14 (API level 34)
- UI: Jetpack Compose for a declarative and modern UI
- Background Processing:
WorkManagerandAlarmManagerfor scheduling periodic data fetches, ensuring the widget is always up to date
| Version | Status | Notes |
|---|---|---|
1.0.1_0046 |
Unknown | |
1.0.1_0063 |
Unknown | |
1.0.1_0067 |
Unknown | |
1.0.1_0126 |
Unknown | |
1.0.1_0130 |
Working | Marked as the latest for some devices, perhaps different HW? |
1.0.1_0132 |
Working | Marked as the latest for some devices, perhaps different HW? |
This section describes the reverse-engineered Bluetooth Low Energy (BLE) protocol for the Qingping CGD1 Alarm Clock.
The device uses a custom service structure but relies on standard 128-bit base UUIDs for characteristics in this specific firmware version.
Target Service UUID: 22210000-554a-4546-5542-46534450464d (Advertised)
| Function | Characteristic UUID | Properties |
|---|---|---|
| Auth Write | 00000001-0000-1000-8000-00805f9b34fb |
Write |
| Auth Notify | 00000002-0000-1000-8000-00805f9b34fb |
Notify |
| Data Write | 0000000b-0000-1000-8000-00805f9b34fb |
Write |
| Data Notify | 0000000c-0000-1000-8000-00805f9b34fb |
Notify |
| Sensor Notify | 00000100-0000-1000-8000-00805f9b34fb |
Notify |
Most commands follow a simple Header + Command + Payload structure.
Request Format: [Header] [Command] [Payload...]
ACK Format (Notify): 04 ff [Command] [Len] [Status]
| Value | Constant | Used for |
|---|---|---|
0x11 |
Header.AUTH |
Authentication steps |
0x05 |
Header.TIME |
Time synchronization |
0x01 |
Header.GET_DATA |
Read-only requests (Settings, Alarms, Firmware) |
0x07 |
Header.SET_ALARM |
Writing alarm data |
0x02 |
Header.BRIGHTNESS |
Immediate brightness preview |
0x01/0x02 |
Header.RINGTONE_V1/V2 |
Ringtone preview |
0x13 |
Header.SET_SETTINGS |
Writing device settings |
0x08 |
Header.AUDIO_INIT |
Audio upload initialization |
0x81 |
Header.AUDIO_PACKET |
Audio data stream packets |
The device uses a two-step authentication protocol with a 16-byte random token. Once paired, the same token must be used for all future connections.
Flow:
- Connect to the device and discover services
- Enable Notifications on Auth Notify (
...0002) - Send Auth Init to Auth Write (
...0001):11 01 [Token 16B] - Wait for ACK on Auth Notify:
04 ff 01 00 02(success, proceed to step 5) - Send Auth Confirm to Auth Write:
11 02 [Token 16B] - Wait for final ACK:
04 ff 02 00 00
Device will send you an ACK even when the token is bad. Try to sync time or do other "privileged" action and check if the device will close connection with you.
Token Management:
- For new devices: Generate a random 16-byte token
- For paired devices: Use the stored token from the previous pairing
- Token must match what the device expects (first successful pairing establishes the token)
ACK Response Format: 04 ff [CmdID] [Len] [Status]
- Status
00= Success - Status
01= Failure - Status
02= Continue (for Auth Init, proceed to step 5)
After authentication, it is recommended to synchronize the time.
- Command (Auth Write):
05 09 [Timestamp 4B LE] - Response (Auth Notify):
04 ff 09 00 00(Success)
The device supports a fixed capacity of 16 alarm slots (indexed 0-15). All alarm/settings operations happen on the Data characteristics.
To create or modify an alarm:
- Command:
07 05 [ID] [Enabled] [HH] [MM] [Days] [Snooze] - ID: The alarm index (0-15)
- Enabled:
0x01= On,0x00= Off - HH, MM: Hour (0-23) and Minute (0-59)
- Days (Bitmask):
0x01= Monday0x02= Tuesday0x04= Wednesday0x08= Thursday0x10= Friday0x20= Saturday0x40= Sunday0x00= Once
- Snooze:
0x01= On,0x00= Off
The 5-byte alarm entry structure used in both Set Alarm and Read Alarms:
| Byte | Description | Range / Values |
|---|---|---|
| 0 | Enabled State | 0x01 (On), 0x00 (Off), 0xFF (Empty) |
| 1 | Hour | 0-23, 0xFF (Empty) |
| 2 | Minute | 0-59, 0xFF (Empty) |
| 3 | Repeat Days | Bitmask (see above), 0xFF (Empty) |
| 4 | Snooze | 0x01 (On), 0x00 (Off), 0xFF (Empty) |
To delete an alarm, overwrite it with FF values (marking it as empty/unused).
- Command:
07 05 [ID] FF FF FF FF FF
- Command:
01 06 - Response:
11 06 [Base Index] [Alarm Entry 1 (5B)] ... - Alarm Entry:
[Enabled] [HH] [MM] [Days] [Snooze]
Note: Device sends multiple packets if needed (up to 4 alarms per packet). All 16 slots are
returned, empty slots have FF FF FF FF FF values.
- ACK (after Set/Delete):
04 ff 05 00 00(Success)
Managed via a single comprehensive payload on Data Write.
-
Command: Start with
13(Set Settings) or01 02(Read Settings) -
Set Settings Payload (20 bytes):
13 01 [Vol] [Hdr1] [Hdr2] [Flags] [Timezone] [Duration] [Brightness] [NightStartH] [NightStartM] [NightEndH] [NightEndM] [TzSign] [NightEn] [Sig 4B]
| Byte | Value | Description |
|---|---|---|
| 0 | 0x13 |
Command ID |
| 1 | 0x01 / 0x02 |
Set / Read Response |
| 2 | 1-5 |
Sound Volume |
| 3-4 | 58 02 |
Fixed Header / Version (???) |
| 5 | Bitmask | Mode Flags: See the Mode Flags Breakdown table below. |
| 6 | Integer | Timezone Offset (Units of 6 minutes). Device does not handle DST automatically. |
| 7 | Seconds | Backlight Duration (0=Off) |
| 8 | Packed | Brightness (High nibble: Day/10, Low nibble: Night/10) |
| 9-10 | HH:MM | Night Start Time |
| 11-12 | HH:MM | Night End Time |
| 13 | 0/1 |
Timezone Sign (1=Positive, 0=Negative) |
| 14 | 0/1 |
Night Mode Enabled |
| 15 | - | Reserved (preserved from device response) |
| 16-19 | Sig 4B |
Ringtone signature (4 bytes). Identifies the device ringtone — see the "Known Ringtone Signatures" list below. |
This byte acts as a bitfield where individual bits control specific boolean settings.
| Bit | Value (Hex) | Description | 0 (Off/Default) | 1 (On/Active) |
|---|---|---|---|---|
| 0 | 0x01 |
Language | Chinese | English |
| 1 | 0x02 |
Time Format | 24-hour | 12-hour |
| 2 | 0x04 |
Temp Unit | Celsius | Fahrenheit |
| 3 | 0x08 |
(Reserved ?) | - | - |
| 4 | 0x10 |
Master Alarm Disable (!) | Enabled | Disabled |
| 5-7 | - | (Unused ?) | - | - |
Workaround: Disabling night mode is being done via setting 1-minute night mode (i.e.
00:00 - 00:01). Yup, it's that stupid; even official app does this.
- Command (Data Write):
02 03 [Value] - Value: Brightness level / 10 (
0-10). - Response (Data Notify):
04 ff 03 00 00(Success).
Plays a generic "beep" sound for testing volume level (not the user's selected ringtone).
- Command (Data Write):
01 04(Play at current volume) or02 04 [Vol](Play at volume1-5) - Response (Data Notify):
04 ff 04 00 00(Success)
- Target:
00000100-...(Notify) - Format:
[00] [Temp L] [Temp H] [Hum L] [Hum H] - Values: Little Endian Int16 / 100.0
The device also broadcasts sensor data in its BLE advertisement packets via Service Data.
- Service UUID:
0000fdcd-0000-1000-8000-00805f9b34fb(ClearGrass/Qingping Service) - Format (Service Data):
| Byte | Value | Description |
|---|---|---|
| 0 | 0x08 |
Packet Type (???) |
| 1 | 0x0c |
Model ID (0x0C = CGD1) |
| 2-7 | MAC | Device MAC Address (6 bytes) |
| 8-9 | ??? | Unknown |
| 10-11 | Int16 LE |
Temperature (Value / 10.0) |
| 12-13 | UInt16 LE |
Humidity (Value / 10.0) |
| 14-15 | ??? | Unknown |
| 16 | UInt8 |
Battery Percentage (0-100) |
- Service UUID:
0x180f, Char UUID:0x2a19 - Format: 1 byte (percentage)
- Command (Auth Write):
01 0d - Response (Auth Notify):
0b [Length] [ASCII String]
Official apps are using these 4-byte signatures to identify ringtones:
| Ringtone | Signature (Hex) |
|---|---|
| Beep | fd c3 66 a5 |
| Digital Ringtone | 09 61 bb 77 |
| Digital Ringtone 2 | ba 2c 2c 8c |
| Cuckoo | ea 2d 4c 02 |
| Telephone | 79 1b ac b3 |
| Exotic Guitar | 1d 01 9f d6 |
| Lively Piano | 6e 70 b6 59 |
| Story Piano | 8f 00 48 86 |
| Forest Piano | 26 52 25 19 |
For uploading custom ringtones, this app is using these alternating slot signatures:
| Slot | Signature (Hex) |
|---|---|
| Custom 1 | de ad de ad |
| Custom 2 | be ef be ef |
Important: Always alternate between slots when uploading new custom audio. Doesn't matter how you name it. The device may reject uploads if the target signature matches the currently active ringtone, but audio itself is different from the one you are uploading.
If you want to host your own ringtone repository, you can create a JSON manifest file. Just point to
a JSON file (e.g., https://example.com/rings/index.json), and the app will parse the manifest for
ringtone URLs
Manifest Format:
The JSON file should map hex signatures to objects containing at least a "wav" URL. The official
app uses an additional "pcm" field, but this app takes the Wave and converts it on its own.
{
"1d019fd6": {
"name": "Exotic Guitar",
"wav": "https://example.com/rings/1d019fd6.wav"
},
"6e70b659": {
"name": "Lively Piano",
"wav": "https://example.com/rings/6e70b659.wav"
},
"8f004886": {
"name": "Story Piano",
"wav": "https://example.com/rings/8f004886.wav"
},
"26522519": {
"name": "Forest Piano",
"wav": "https://example.com/rings/26522519.wav"
},
"fdc366a5": {
"name": "Beep",
"wav": "https://example.com/rings/fdc366a5.wav"
},
"ea2d4c02": {
"name": "Cuckoo",
"wav": "https://example.com/rings/ea2d4c02.wav"
},
"0961bb77": {
"name": "Digital Ringtone",
"wav": "https://example.com/rings/0961bb77.wav"
},
"ba2c2c8c": {
"name": "Digital Ringtone 2",
"wav": "https://example.com/rings/ba2c2c8c.wav"
},
"791bacb3": {
"name": "Telephone Ringtone",
"wav": "https://example.com/rings/791bacb3.wav"
}
}Key: Hex signature (without 0x prefix).
Value: Object with name (display name) and wav (full URL to WAV file).
Audio Format: 8-bit Unsigned PCM, 8000 Hz, Mono
Step 1 - Init Command (Data Write):
08 10 [Size 3B LE] [Signature 4B]
- Size: Audio length in bytes (Little Endian, 3 bytes)
- Signature: Target ringtone slot signature
Step 2 - Wait for Init ACK (Data Notify):
04 ff 10 00 [Status]
- Status
00or09= Success, proceed with upload
Step 3 - Send Audio Data:
- Packet size: 128 bytes
- Packets per block: 4 (512 bytes per block)
- First packet header: Prepend
81 08to the audio data - Wait for block ACK (
04 ff 08 ...) after every 4 packets
Step 4 - Completion:
After sending all audio data, the device will apply the new ringtone.
| Cmd | Sub | Characteristic | Description |
|---|---|---|---|
| 11 | 01 | Auth Write | Auth Init (+ 16B token) |
| 11 | 02 | Auth Write | Auth Confirm (+ 16B token) |
| 05 | 09 | Auth Write | Time Sync (+ 4B timestamp LE) |
| 01 | 0D | Auth Write | Read Firmware Version |
| 13 | 01 | Data Write | Set Settings (Volume, Brightness, etc.) |
| 01 | 02 | Data Write | Read Settings |
| 02 | 03 | Data Write | Set Immediate Brightness |
| 01 | 04 | Data Write | Preview Ringtone (current volume) |
| 02 | 04 | Data Write | Preview Ringtone (+ 1B volume) |
| 07 | 05 | Data Write | Set Alarm |
| 01 | 06 | Data Write | Read Alarms |
| 08 | 10 | Data Write | Audio Upload Init |
ACK Format (Notify characteristics): 04 ff [CmdSub] [Len] [Status]
When the device disconnects, the GATT status indicates the reason:
| Status | Meaning | Description |
|---|---|---|
| 0 | GATT_SUCCESS |
Normal disconnect (user requested) |
| 8 | GATT_CONN_TIMEOUT |
Connection timeout |
| 19 | GATT_CONN_TERMINATE_PEER |
Device terminated connection |
| 22 | GATT_CONN_TERMINATE_LOCAL |
Link lost / local host terminated |











