Reverse engineering of budget fitness band using quintic qn9021...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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): | |
btle.DefaultDelegate.__init__(self) | |
self.peripheral = btle.Peripheral(deviceAddress, btle.ADDR_TYPE_PUBLIC) | |
self.peripheral.setDelegate(self) | |
#self.dumpCharacteristics() | |
print 'Connected' | |
self.peripheral.writeCharacteristic(0x1d, struct.pack('<BB', 0x01, 0x00), withResponse=True) | |
print 'Notification Enabled' | |
self.set_date() | |
def cmd(self, data): | |
self.peripheral.writeCharacteristic(0x19, data, withResponse=False) | |
print '-> Command:', data.encode('hex') | |
self.waitForNotifications(5.0) | |
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 = datetime.datetime.now() | |
self.cmd(struct.pack('<BBBBBBBBBBBBBBBBBBBB', 0x5a, 0x01, 0x00, | |
dt.year - 2000, # year - 2000 | |
dt.month, # month (1 = jan) | |
dt.day, # 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, | |
0,0,0,0,0,0,0,0,0,0,0,0)) | |
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, | |
0, | |
3, # 3 for wakeup | |
0 if delete else 1, | |
hours, | |
minutes, | |
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, | |
0, | |
0, | |
0, | |
0 | |
)) | |
def handleNotification(self, cHandle, data): | |
if cHandle == 0x1c: | |
if ord(data[0]) == 0x5b: | |
if ord(data[1]) == 0x01: | |
print '<- Result:', data.encode('hex') | |
self.device_info(data) | |
elif ord(data[1]) == 0x03 or ord(data[1]) == 0x07: | |
print '<- Result:', data.encode('hex') | |
if ord(data[-1]): | |
self.waitForNotifications(15.0) | |
else: | |
print ' No Log?' | |
elif ord(data[1]) == 0x10: | |
print '<- Result:', data.encode('hex') | |
self.device_info_other(data) | |
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?' | |
else: | |
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 | |
self.waitForNotifications(5.0) | |
elif ord(data[2]) == self.logoffs + 1: | |
self.log += data[3:] | |
self.logoffs += 1 | |
self.waitForNotifications(5.0) | |
elif ord(data[2]) >= 0xfe: | |
self.log += data[3:] | |
self.logoffs = 0xff | |
self.handle_log(self.log) | |
self.test_cmd(0x5b, 0x05, 0x00) | |
if ord(data[2]) == 0xfe: | |
self.waitForNotifications(5.0) | |
else: | |
print ' Badlog!' | |
else: | |
print '<- Unexpected:', data.encode('hex') | |
else: | |
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 = datetime.date(year=year+2000, 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): | |
self.peripheral.waitForNotifications(time) | |
def disconnect(self): | |
self.peripheral.disconnect() | |
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') | |
q.query_minutes_log() | |
q.query_days_log() | |
# 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 = datetime.datetime.now() | |
# for i in range(5, 100, 5): | |
# q.set_reminder(dt.hour, dt.minute+i) | |
while True: | |
q.waitForNotifications(5.0) | |
# Must manually disconnect or you won't be able to reconnect. | |
q.disconnect() |
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.
Source: https://choosealicense.com/no-permission
It can be easily implemented by adding the SPDX identifier in the header.
#!/usr/bin/env python
+# SPDX-License-Identifier: MIT
Hello,
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
Stefano
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@ali1234 what is your firmware version? last value in days log for me is always 0. Do you know more about log format?