Skip to content

Instantly share code, notes, and snippets.

Created June 3, 2016 01:42
Show Gist options
  • Save ali1234/ac9170132e5abfc5381353428e70f1d9 to your computer and use it in GitHub Desktop.
Save ali1234/ac9170132e5abfc5381353428e70f1d9 to your computer and use it in GitHub Desktop.
Reverse engineering of budget fitness band using quintic qn9021...
#!/usr/bin/env python
#Connecting to: 08:7C:BE:8F:3C:FB, address type: public
#Service <uuid=Generic Attribute handleStart=12 handleEnd=15> :
# Characteristic <Service Changed>, hnd=0xd, supports READ INDICATE
# -> '\x01\x00\xff\xff'
#Service <uuid=Generic Access handleStart=1 handleEnd=11> :
# Characteristic <Device Name>, hnd=0x2, supports READ WRITE
# -> 'Quintic BLE'
# Characteristic <Appearance>, hnd=0x4, supports READ
# -> '\x00\x00'
# Characteristic <Peripheral Privacy Flag>, hnd=0x6, supports READ WRITE
# -> '\x00'
# Characteristic <Peripheral Preferred Connection Parameters>, hnd=0x8, supports READ
# -> 'd\x00\xc8\x00\x00\x00\xd0\x07'
# Characteristic <Reconnection Address>, hnd=0xa, supports READ WRITE NO RESPONSE WRITE
# -> ''
#Service <uuid=fee9 handleStart=23 handleEnd=29> :
# Characteristic <d44bc439-abfd-45a2-b575-925416129600>, hnd=0x18, supports WRITE NO RESPONSE WRITE
# Characteristic <d44bc439-abfd-45a2-b575-925416129601>, hnd=0x1b, supports NOTIFY
#Service <uuid=fee8 handleStart=16 handleEnd=22> :
# Characteristic <003784cf-f7e3-55b4-6c4c-9fd140100a16>, hnd=0x11, supports NOTIFY
# Characteristic <013784cf-f7e3-55b4-6c4c-9fd140100a16>, hnd=0x15, supports WRITE NO RESPONSE
from bluepy.bluepy import btle
import time,struct,datetime
class Quintic(btle.DefaultDelegate):
def __init__(self, deviceAddress):
self.peripheral = btle.Peripheral(deviceAddress, btle.ADDR_TYPE_PUBLIC)
print 'Connected'
self.peripheral.writeCharacteristic(0x1d, struct.pack('<BB', 0x01, 0x00), withResponse=True)
print 'Notification Enabled'
def cmd(self, data):
self.peripheral.writeCharacteristic(0x19, data, withResponse=False)
print '-> Command:', data.encode('hex')
def test_cmd(self, a, b, c):
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', a, b, c, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0))
def set_date(self, antilost=False, metric=True):
dt =
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x01, 0x00,
dt.year - 2000, # year - 2000
dt.month, # month (1 = jan), # day of month
dt.hour, # hours
dt.minute, # minutes
dt.second, # seconds
0x00, 0x64, # target steps / 100, hi then lo
0x00, # wear position
0x01, # motion mode
0x00, # sex
0x00, 0x02, # fixed
0xa4, # "i"
0x46, # weight
0xb7, # height
0x80 | (0x40 if metric else 0x00) | (0x20 if antilost else 0x00)
# options:
# 0x01 - always off?
# 0x02 - always off?
# 0x04 - always off?
# 0x08 - always off?
# 0x10 - always off?
# 0x20 - anti-lost
# 0x40 - 1: metric, 0: imperial
# 0x80 - always on?
def vibrate(self, icon=0, msg=''):
# icon: 0 = none, 1 = ringing phone, 2 = email, 3 = penguin, 4 = phone, 5 = phone
self.cmd(struct.pack('<BBBBB15s', 0x5a, 0x15, 0x00, icon, min(len(msg),15), msg))
def name_alert(self, msg=''):
# not supported on W007C
self.cmd(struct.pack('<BBBB16s', 0x5a, 0x15, 0x00, 0x83, msg))
def button_mode(self, on=True):
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x0c, 0x00, 0x04, 0x01 if on else 0x00, 0x00, 0x1e, 0x00,
def query_firmware(self):
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x10, 0x00, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0))
def query_minutes_log(self):
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x03, 0x00, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0))
def query_days_log(self):
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x07, 0x00, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0))
def set_reminder(self, hours, minutes, delete=False):
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x14, 0x00,
3, # 3 for wakeup
0 if delete else 1,
0 if delete else 1,
0 if delete else 1,
0 if delete else 1,
0 if delete else 1,
0 if delete else 1,
0 if delete else 1,
0 if delete else 1,
def handleNotification(self, cHandle, data):
if cHandle == 0x1c:
if ord(data[0]) == 0x5b:
if ord(data[1]) == 0x01:
print '<- Result:', data.encode('hex')
elif ord(data[1]) == 0x03 or ord(data[1]) == 0x07:
print '<- Result:', data.encode('hex')
if ord(data[-1]):
print ' No Log?'
elif ord(data[1]) == 0x10:
print '<- Result:', data.encode('hex')
elif data[6:17] == 'not support':
print '<- Command not supported:', hex(cHandle), data.encode('hex')
elif ord(data[1]) == 0x0c:
print '<- Result:', data.encode('hex')
print ' Button Mode!'
elif ord(data[1]) == 0x14:
print '<- Result:', data.encode('hex')
print ' Reminder Set?'
print '<- Unknown:', data.encode('hex'), data
elif ord(data[0]) == 0x5a:
print '<- Data:', data.encode('hex')
if ord(data[1]) == 0x05:
if len(data) == 5:
print ' Log ACK?'
elif ord(data[2]) == 0x01:
self.log = data[3:]
self.logoffs = 0x01
elif ord(data[2]) == self.logoffs + 1:
self.log += data[3:]
self.logoffs += 1
elif ord(data[2]) >= 0xfe:
self.log += data[3:]
self.logoffs = 0xff
self.test_cmd(0x5b, 0x05, 0x00)
if ord(data[2]) == 0xfe:
print ' Badlog!'
print '<- Unexpected:', data.encode('hex')
print '<- Message from other handle', hex(cHandle)
def device_info(self, data):
print ' Model:', data[-5:], 'MAC:', data[5:11].encode('hex'), 'Firmware:', ord(data[-6]) | (ord(data[-7])<<8), 'Protocol:', ord(data[12]) | (ord(data[11])<<8)
def device_info_other(self, data):
print ' Model:', data[-5:], 'MAC:', data[7:13].encode('hex'), 'Firmware:', ord(data[4]) | (ord(data[3])<<8), 'Protocol:', ord(data[14]) | (ord(data[13])<<8)
def handle_log(self, log):
(length, b, c, d, e, logtype, year, month, day, first, last, v, w, x, y, z) = struct.unpack('>HBBBBBBBBBBBBBBB', log[:17])
date =, month=month, day=day)
print ' Log Type:', logtype
print ' Date:', date
print ' First?:', first, 'Last?:', last
data = struct.unpack('>'+('H'*(length/2)), log[17:])
print ' Count:', sum(data[first:last]), data[first:last]
print b, c, d, e
print v, w, x, y, z
print sum(data), data
def waitForNotifications(self, time):
def disconnect(self):
if __name__ == '__main__':
from itertools import product
# Connect by address. Use "sudo hcitool lescan" to find address.
q = Quintic('08:7C:BE:8F:3C:FB')
# for b in [0x02, 0x05, 0x06, 0x0b, 0x0e, 0x0f]:
# for a in [0x5a, 0x5b]:
# q.test_cmd(a, b, 0)
# q.waitForNotifications(5.0)
# dt =
# for i in range(5, 100, 5):
# q.set_reminder(dt.hour, dt.minute+i)
while True:
# Must manually disconnect or you won't be able to reconnect.
Copy link

dpavlin commented Jul 24, 2016

@ali1234 what is your firmware version? last value in days log for me is always 0. Do you know more about log format?

Copy link

oxr463 commented Jul 24, 2019

Could you provide a license for this gist?

When you make a creative work (which includes code), the work is under exclusive copyright by default. Unless you include a license that specifies otherwise, nobody else can copy, distribute, or modify your work without being at risk of take-downs, shake-downs, or litigation. Once the work has other contributors (each a copyright holder), “nobody” starts including you.


It can be easily implemented by adding the SPDX identifier in the header.

#!/usr/bin/env python
+# SPDX-License-Identifier: MIT

Copy link

wes1993 commented Sep 11, 2022

Did someone could help me to decode the Sherlock Smart Lock that use the Quintic Chip with this same UUID?

I just need to understand the notification and the correct command for open/close

best regards

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment