From 069a86c8dde489cbc7cae5bf5b1aad8cd39f82a3 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Sun, 18 Mar 2018 17:48:01 +0200 Subject: [PATCH 01/48] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b34b615..23de583 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Library to work with Xiaomi MiBand 2 # Contributors & Info Sources 1) Base lib provided by [Leo Soares](https://github.com/leojrfs/miband2) 2) Additional debug & fixes was made by my friend [Volodymyr Shymanskyy](https://github.com/vshymanskyy/miband2-python-test) -3) Some info that really helped i got from (Freeyourgadget team)[https://github.com/Freeyourgadget/Gadgetbridge/tree/master/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2] +3) Some info that really helped i got from [Freeyourgadget team](https://github.com/Freeyourgadget/Gadgetbridge/tree/master/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2) # Run From ca92d2903747f3205947b5bfb1b3a44442fa0716 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Sun, 18 Mar 2018 18:05:33 +0200 Subject: [PATCH 02/48] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 23de583..512a997 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,6 @@ python example.py MAC_ADDRESS ```sh sudo hciconfig hci0 reset ``` + +# Donation +If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://www.paypal.me/creotiv From 2e8972b01c413975283a960ebe1899688473689b Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Sun, 18 Mar 2018 20:31:18 +0200 Subject: [PATCH 03/48] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 512a997..186c88a 100644 --- a/README.md +++ b/README.md @@ -27,5 +27,5 @@ python example.py MAC_ADDRESS sudo hciconfig hci0 reset ``` -# Donation +# Donate If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://www.paypal.me/creotiv From 63117cb6fe3b399744cf03e268623d724bf33eae Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Sun, 18 Mar 2018 23:09:18 +0200 Subject: [PATCH 04/48] fix --- dump.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dump.py b/dump.py index f393e81..6f3723d 100644 --- a/dump.py +++ b/dump.py @@ -1,14 +1,18 @@ import sys +import os import time from base import MiBand2 MAC = sys.argv[1] -fp = open(sys.argv[2], 'a') +filepath = sys.argv[2] +if os.path.exists(sys.argv[2]): + os.remove(sys.argv[2]) +fp = open(filepath, 'a') fp.write('time, heartrate\n') def log(rate): - data = "%s, %s" % (int(time.time()), rate) + data = "%s, %s\n" % (int(time.time()), rate) fp.write(data) print data From e1db9dbfdae645cff8a7ad76fcf49e5fc6d5c097 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Mon, 19 Mar 2018 10:09:40 +0200 Subject: [PATCH 05/48] dump and plot scripts --- dump.py | 15 ++++++++++----- plot.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 plot.py diff --git a/dump.py b/dump.py index 6f3723d..9e0321c 100644 --- a/dump.py +++ b/dump.py @@ -2,6 +2,7 @@ import os import time from base import MiBand2 +from bluepy.btle import BTLEException MAC = sys.argv[1] filepath = sys.argv[2] @@ -16,9 +17,13 @@ def log(rate): fp.write(data) print data -band = MiBand2(MAC, debug=True) -band.setSecurityLevel(level="medium") -band.authenticate() -band.get_heart_rate_realtime(log, 60 * 60 * 12) +while True: + try: + band = MiBand2(MAC, debug=True) + band.setSecurityLevel(level="medium") + band.authenticate() + band.get_heart_rate_realtime(log, 60 * 60 * 12) -band.disconnect() + band.disconnect() + except BTLEException: + pass diff --git a/plot.py b/plot.py new file mode 100644 index 0000000..bd88af9 --- /dev/null +++ b/plot.py @@ -0,0 +1,17 @@ +import numpy as np +import pandas as pd +import sys +from stockstats import StockDataFrame +import matplotlib.pyplot as plt + +df = pd.DataFrame.from_csv(sys.argv[1], index_col=None) +print df.head +df['time'] = pd.to_datetime(df['time'], unit='s') +df = df.set_index('time') +print df.describe() +# plt.subplot('111') +# df.plot(kind='line') +# plt.subplot('122') +# df.plot(kind='histogram') +df.rolling(60).mean().plot() +plt.show() \ No newline at end of file From d070148920127b086c68f38f8fa95df7c600f63e Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Mon, 19 Mar 2018 14:57:53 +0200 Subject: [PATCH 06/48] Rename exmaple.py to example.py --- exmaple.py => example.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename exmaple.py => example.py (100%) diff --git a/exmaple.py b/example.py similarity index 100% rename from exmaple.py rename to example.py From 63412f7f632caf92fcf9b98b1dd0f3a47215cbe7 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Mon, 19 Mar 2018 22:48:39 +0200 Subject: [PATCH 07/48] Update example.py --- example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.py b/example.py index 7680218..f7386df 100644 --- a/example.py +++ b/example.py @@ -8,7 +8,7 @@ band = MiBand2(MAC, debug=True) band.setSecurityLevel(level="medium") -if len(sys.argv) > 1: +if len(sys.argv) > 2: band.initialize() band.disconnect() sys.exit(0) From cf99d2f98d88c2f435fb9ffe2f6e439620c1f915 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Mon, 19 Mar 2018 22:49:51 +0200 Subject: [PATCH 08/48] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 186c88a..7bdc3e3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ pip install -r requirements.txt sudo hcitool lescan ``` 5) Run example.py +This will auth device +```sh +python example.py MAC_ADDRESS init +``` +This will run demo ```sh python example.py MAC_ADDRESS ``` From 3f25ffa5b90960be5bf30ab3fbd1722b1e73ad73 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Mon, 19 Mar 2018 22:53:09 +0200 Subject: [PATCH 09/48] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7bdc3e3..6a1a751 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,15 @@ pip install -r requirements.txt ```sh sudo hcitool lescan ``` -5) Run example.py -This will auth device +5) Run this to auth device ```sh python example.py MAC_ADDRESS init ``` -This will run demo +6) Run this to call demo functions ```sh python example.py MAC_ADDRESS ``` -6) If you having problems(BLE can glitch sometimes) try this and repeat from 4) +7) If you having problems(BLE can glitch sometimes) try this and repeat from 4) ```sh sudo hciconfig hci0 reset ``` From fb07564cf8f2d859e217b4275939019b7d476c47 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Mon, 19 Mar 2018 23:11:27 +0200 Subject: [PATCH 10/48] fix --- base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base.py b/base.py index 28c32d7..201d9fd 100644 --- a/base.py +++ b/base.py @@ -228,7 +228,7 @@ def get_heart_rate_one_time(self): self._char_heart_ctrl.write(b'\x15\x02\x01', True) res = None while not res: - band.waitForNotifications(self.timeout) + self.waitForNotifications(self.timeout) res = self._get_from_queue(QUEUE_TYPES.HEART) rate = res From 1872ea69afa1e39747b5378f702c16cb444475e7 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Tue, 20 Mar 2018 12:41:25 +0200 Subject: [PATCH 11/48] Added raw sensor data grabbing for testing --- base.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++- constants.py | 2 +- example.py | 33 +++++++++++++++-------------- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/base.py b/base.py index 201d9fd..adea3bf 100644 --- a/base.py +++ b/base.py @@ -39,10 +39,12 @@ def handleNotification(self, hnd, data): else: self.device.state = AUTH_STATES.AUTH_FAILED elif hnd == self.device._char_heart_measure.getHandle(): + print 'LEN:', len(data) rate = struct.unpack('bb', data)[1] self.device.queue.put((QUEUE_TYPES.HEART, rate)) else: - self.device._log.error("Unhandled Response " + hex(hnd) + ": " + str(data.encode("hex"))) + self.device._log.error("Unhandled Response " + hex(hnd) + ": " + + str(data.encode("hex")) + " len:" + str(len(data))) class MiBand2(Peripheral): @@ -257,3 +259,58 @@ def get_heart_rate_realtime(self, callback, timeout=10): if res: callback(res) + def debug(self): + char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] + char_d = char_m.getDescriptors(forUUID=UUIDS.CHARACTERISTIC_HEART_RATE_CONFIG)[0] + char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] + + char_sensor1 = self.svc_1.getCharacteristics('000000020000351221180009af100700')[0] + char_sens_d1 = char_sensor1.getDescriptors(forUUID=0x2902)[0] + + char_sensor2 = self.svc_1.getCharacteristics('000000010000351221180009af100700')[0] + char_sens_d2 = char_sensor2.getDescriptors(forUUID=0x2902)[0] + + char_sensor3 = self.svc_1.getCharacteristics('000000070000351221180009af100700')[0] + char_sens_d3 = char_sensor3.getDescriptors(forUUID=0x2902)[0] + + # stop heart monitor continues + char_ctrl.write(b'\x15\x01\x00', True) + # stop heart monitor notifications + char_d.write(b'\x00\x00', True) + # IMO: enable notifications from Sensor 1 + char_sens_d1.write(b'\x01\x00', True) + + # IMO: This set band to get raw measures and processed heart measures in parallel + char_sens_d2.write(b'\x01\x00', True) + char_sensor2.write(b'\x01\x03\x19') + char_sens_d2.write(b'\x00\x00', True) + + # char_sens_d3.write(b'\x01\x00', True) + + # IMO: enablee heart monitor notifications + char_d.write(b'\x01\x00', True) + + # start hear monitor continues + char_ctrl.write(b'\x15\x01\x01', True) + # WTF + char_sensor2.write(b'\x02') + try: + while True: + self.waitForNotifications(0.5) + char_ctrl.write(b'\x16') + char_sensor2.write(b'\x00\x00') + except Exception as e: + print e + pass + finally: + # stop heart monitor continues + char_ctrl.write(b'\x15\x01\x00', True) + char_ctrl.write(b'\x15\x01\x00', True) + # IMO: stop heart monitor notifications + char_d.write(b'\x00\x00', True) + # WTF + char_sensor2.write(b'\x03') + # IMO: stop notifications from sensors + char_sens_d1.write(b'\x00\x00', True) + char_sens_d2.write(b'\x00\x00', True) + char_sens_d3.write(b'\x00\x00', True) diff --git a/constants.py b/constants.py index 79f0f3f..b9fe21f 100644 --- a/constants.py +++ b/constants.py @@ -25,7 +25,7 @@ class UUIDS(object): CHARACTERISTIC_AUTH = "00000009-0000-3512-2118-0009af100700" CHARACTERISTIC_HEART_RATE_MEASURE = "00002a37-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_HEART_RATE_CONTROL = "00002a39-0000-1000-8000-00805f9b34fb" - CHARACTERISTIC_HEART_RATE_DESCRIPTOR = "00002902-0000-1000-8000-00805f9b34fb" + CHARACTERISTIC_HEART_RATE_CONFIG = "00002902-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_ALERT = "00002a06-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_BATTERY = "00000006-0000-3512-2118-0009af100700" CHARACTERISTIC_STEPS = "00000007-0000-3512-2118-0009af100700" diff --git a/example.py b/example.py index f7386df..16ff343 100644 --- a/example.py +++ b/example.py @@ -15,22 +15,23 @@ else: band.authenticate() -print 'Message notif' -band.send_alert(ALERT_TYPES.MESSAGE) -time.sleep(3) -# this will vibrate till not off -print 'Phone notif' -band.send_alert(ALERT_TYPES.PHONE) -time.sleep(8) -print 'OFF' -band.send_alert(ALERT_TYPES.NONE) -print 'Battery:', band.get_battery_info() -print 'Time:', band.get_current_time() -print 'Steps:', band.get_steps() -print 'Heart rate oneshot:', band.get_heart_rate_one_time() +# print 'Message notif' +# band.send_alert(ALERT_TYPES.MESSAGE) +# time.sleep(3) +# # this will vibrate till not off +# print 'Phone notif' +# band.send_alert(ALERT_TYPES.PHONE) +# time.sleep(8) +# print 'OFF' +# band.send_alert(ALERT_TYPES.NONE) +# print 'Battery:', band.get_battery_info() +# print 'Time:', band.get_current_time() +# print 'Steps:', band.get_steps() +# print 'Heart rate oneshot:', band.get_heart_rate_one_time() -def l(x): - print 'Realtime heart:', x -band.get_heart_rate_realtime(l, 60) +# def l(x): +# print 'Realtime heart:', x +# band.get_heart_rate_realtime(l, 60) +band.debug() band.disconnect() From 791cd6e612dee3f0cd306186a31763c997d30480 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Tue, 20 Mar 2018 12:41:42 +0200 Subject: [PATCH 12/48] Added debug data from phone --- btsnoop_hci.dump | Bin 0 -> 90712 bytes uuids_list.txt | 179 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 btsnoop_hci.dump create mode 100644 uuids_list.txt diff --git a/btsnoop_hci.dump b/btsnoop_hci.dump new file mode 100644 index 0000000000000000000000000000000000000000..a7f06e55eb60c5a3e343dd3c6201eb905eda3402 GIT binary patch literal 90712 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 literal 0 HcmV?d00001 diff --git a/uuids_list.txt b/uuids_list.txt new file mode 100644 index 0000000..9213b08 --- /dev/null +++ b/uuids_list.txt @@ -0,0 +1,179 @@ +Services: +attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb +['0x3', '0x5', '0x7'] + +attr handle: 0x0008, end grp handle: 0x000b uuid: 00001801-0000-1000-8000-00805f9b34fb +['0xa'] + +attr handle: 0x000c, end grp handle: 0x0016 uuid: 0000180a-0000-1000-8000-00805f9b34fb +['0xe', '0x10', '0x12', '0x14', '0x16'] + +attr handle: 0x0017, end grp handle: 0x001c uuid: 00001530-0000-3512-2118-0009af100700 +['0x19', '0x1c'] + +attr handle: 0x001d, end grp handle: 0x0023 uuid: 00001811-0000-1000-8000-00805f9b34fb +['0x1f', '0x22'] + +Alert +attr handle: 0x0024, end grp handle: 0x0026 uuid: 00001802-0000-1000-8000-00805f9b34fb +['0x26'] + +Heart +attr handle: 0x0027, end grp handle: 0x002c uuid: 0000180d-0000-1000-8000-00805f9b34fb +['0x29', '0x2c'] + +Miband1 +attr handle: 0x002d, end grp handle: 0x0051 uuid: 0000fee0-0000-1000-8000-00805f9b34fb +['0x2f', '0x32', '0x35', '0x38', '0x3b', '0x3e', '0x41', '0x44', '0x47', '0x4a', '0x4d', '0x50'] + +Miband2 +attr handle: 0x0052, end grp handle: 0x0066 uuid: 0000fee1-0000-1000-8000-00805f9b34fb +['0x54', '0x57', '0x59', '0x5b', '0x5d', '0x5f', '0x61', '0x63', '0x65'] + + +Characteristics + +handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb +handle: 0x0004, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb +handle: 0x0006, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0007, uuid: 00002a04-0000-1000-8000-00805f9b34fb +handle: 0x0008, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x0009, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x000a, uuid: 00002a05-0000-1000-8000-00805f9b34fb +handle: 0x000b, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x000c, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x000d, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x000e, uuid: 00002a25-0000-1000-8000-00805f9b34fb +handle: 0x000f, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0010, uuid: 00002a27-0000-1000-8000-00805f9b34fb +handle: 0x0011, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0012, uuid: 00002a28-0000-1000-8000-00805f9b34fb +handle: 0x0013, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0014, uuid: 00002a23-0000-1000-8000-00805f9b34fb +handle: 0x0015, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0016, uuid: 00002a50-0000-1000-8000-00805f9b34fb +handle: 0x0017, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x0018, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0019, uuid: 00001531-0000-3512-2118-0009af100700 +handle: 0x001a, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x001b, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x001c, uuid: 00001532-0000-3512-2118-0009af100700 +handle: 0x001d, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x001e, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x001f, uuid: 00002a46-0000-1000-8000-00805f9b34fb +handle: 0x0020, uuid: 00002901-0000-1000-8000-00805f9b34fb +handle: 0x0021, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0022, uuid: 00002a44-0000-1000-8000-00805f9b34fb +handle: 0x0023, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0024, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x0025, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0026, uuid: 00002a06-0000-1000-8000-00805f9b34fb +handle: 0x0027, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x0028, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0029, uuid: 00002a37-0000-1000-8000-00805f9b34fb +handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x002b, uuid: 00002803-0000-1000-8000-00805f9b34fb +# controll start of getting data from sensors +# set \x15\x01\x01 to start +handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb + +handle: 0x002d, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x002e, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x002f, uuid: 00002a2b-0000-1000-8000-00805f9b34fb +handle: 0x0030, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0031, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0032, uuid: 00000020-0000-3512-2118-0009af100700 +handle: 0x0033, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0034, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 +handle: 0x0036, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0037, uuid: 00002803-0000-1000-8000-00805f9b34fb + +# getting data from sensors +handle: 0x0038, uuid: 00000002-0000-3512-2118-0009af100700 + + +handle: 0x0039, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x003a, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x003b, uuid: 00000003-0000-3512-2118-0009af100700 +handle: 0x003c, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x003d, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x003e, uuid: 00002a04-0000-1000-8000-00805f9b34fb +handle: 0x003f, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0040, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0041, uuid: 00000004-0000-3512-2118-0009af100700 +handle: 0x0042, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0043, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0044, uuid: 00000005-0000-3512-2118-0009af100700 +handle: 0x0045, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0046, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0047, uuid: 00000006-0000-3512-2118-0009af100700 +handle: 0x0048, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0049, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x004a, uuid: 00000007-0000-3512-2118-0009af100700 +handle: 0x004b, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x004c, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x004d, uuid: 00000008-0000-3512-2118-0009af100700 +handle: 0x004e, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x004f, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0050, uuid: 00000010-0000-3512-2118-0009af100700 +handle: 0x0051, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0052, uuid: 00002800-0000-1000-8000-00805f9b34fb +handle: 0x0053, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0054, uuid: 00000009-0000-3512-2118-0009af100700 +handle: 0x0055, uuid: 00002902-0000-1000-8000-00805f9b34fb +handle: 0x0056, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0057, uuid: 0000fedd-0000-1000-8000-00805f9b34fb +handle: 0x0058, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0059, uuid: 0000fede-0000-1000-8000-00805f9b34fb +handle: 0x005a, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x005b, uuid: 0000fedf-0000-1000-8000-00805f9b34fb +handle: 0x005c, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x005d, uuid: 0000fed0-0000-1000-8000-00805f9b34fb +handle: 0x005e, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x005f, uuid: 0000fed1-0000-1000-8000-00805f9b34fb +handle: 0x0060, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0061, uuid: 0000fed2-0000-1000-8000-00805f9b34fb +handle: 0x0062, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0063, uuid: 0000fed3-0000-1000-8000-00805f9b34fb +handle: 0x0064, uuid: 00002803-0000-1000-8000-00805f9b34fb +handle: 0x0065, uuid: 0000fec1-0000-3512-2118-0009af100700 +handle: 0x0066, uuid: 00002902-0000-1000-8000-00805f9b34fb + + +''' Sensors +# sent \x01\x00 +handle: 0x0039, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x01\x00 +handle: 0x0036, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x01\x03\x19 +handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 +# sent \x00\x00 +handle: 0x0036, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x01\x00 +handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x15\x01\x01 start +handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb +# sent \x02 +handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 + +# after some time +# sent \x16 +handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb + +# sent \x15\x01\x00 stop +handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb +# sent \x00\x00 +handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x00\x00 +handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x03 +handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 +# sent \x00\x00 +handle: 0x0039, uuid: 00002902-0000-1000-8000-00805f9b34fb +# sent \x00\x00 +handle: 0x004b, uuid: 00002902-0000-1000-8000-00805f9b34fb +''' \ No newline at end of file From 554dbe244901c18c36a9f1d23c57d580d96d31e0 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Fri, 23 Mar 2018 20:42:39 +0200 Subject: [PATCH 13/48] Fixed realtime heart measuring. Added realtime raw data from sensors. --- base.py | 286 ++++++++++++++++++++++++++++++++++----------------- constants.py | 26 +++-- example.py | 52 ++++++---- 3 files changed, 243 insertions(+), 121 deletions(-) diff --git a/base.py b/base.py index adea3bf..732a63c 100644 --- a/base.py +++ b/base.py @@ -4,7 +4,8 @@ from datetime import datetime from Crypto.Cipher import AES from Queue import Queue, Empty -from bluepy.btle import Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM +from bluepy.btle import Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM, BTLEException + from constants import UUIDS, AUTH_STATES, ALERT_TYPES, QUEUE_TYPES @@ -27,6 +28,7 @@ def handleNotification(self, hnd, data): elif data[:3] == b'\x10\x01\x04': self.device.state = AUTH_STATES.KEY_SENDING_FAILED elif data[:3] == b'\x10\x02\x01': + # 16 bytes random_nr = data[3:] self.device._send_enc_rdn(random_nr) elif data[:3] == b'\x10\x02\x04': @@ -39,19 +41,27 @@ def handleNotification(self, hnd, data): else: self.device.state = AUTH_STATES.AUTH_FAILED elif hnd == self.device._char_heart_measure.getHandle(): - print 'LEN:', len(data) - rate = struct.unpack('bb', data)[1] - self.device.queue.put((QUEUE_TYPES.HEART, rate)) + self.device.queue.put((QUEUE_TYPES.HEART, data)) + elif hnd == 0x38: + # Not sure about this, need test + if len(data) == 20: + self.device.queue.put((QUEUE_TYPES.RAW_ACCEL, data)) + elif len(data) < 20: + self.device.queue.put((QUEUE_TYPES.RAW_HEART, data)) else: self.device._log.error("Unhandled Response " + hex(hnd) + ": " + str(data.encode("hex")) + " len:" + str(len(data))) class MiBand2(Peripheral): - _KEY = b'\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x43\x44\x45' - _send_key_cmd = struct.pack('<18s', b'\x01\x08' + _KEY) - _send_rnd_cmd = struct.pack('<2s', b'\x02\x08') - _send_enc_key = struct.pack('<2s', b'\x03\x08') + # _KEY = b'\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x43\x44\x45' + # _send_key_cmd = struct.pack('<18s', b'\x01\x08' + _KEY) + # _send_rnd_cmd = struct.pack('<2s', b'\x02\x08') + # _send_enc_key = struct.pack('<2s', b'\x03\x08') + _KEY = b'\xf5\xd2\x29\x87\x65\x0a\x1d\x82\x05\xab\x82\xbe\xb9\x38\x59\xcf' + _send_key_cmd = struct.pack('<18s', b'\x01\x00' + _KEY) + _send_rnd_cmd = struct.pack('<2s', b'\x02\x00') + _send_enc_key = struct.pack('<2s', b'\x03\x00') def __init__(self, mac_address, timeout=0.5, debug=False): FORMAT = '%(asctime)-15s %(name)s (%(levelname)s) > %(message)s' @@ -68,13 +78,16 @@ def __init__(self, mac_address, timeout=0.5, debug=False): self.mac_address = mac_address self.state = None self.queue = Queue() + self.heart_measure_callback = None + self.heart_raw_callback = None + self.accel_raw_callback = None self.svc_1 = self.getServiceByUUID(UUIDS.SERVICE_MIBAND1) self.svc_2 = self.getServiceByUUID(UUIDS.SERVICE_MIBAND2) self.svc_heart = self.getServiceByUUID(UUIDS.SERVICE_HEART_RATE) self._char_auth = self.svc_2.getCharacteristics(UUIDS.CHARACTERISTIC_AUTH)[0] - self._desc_auth = self._char_auth.getDescriptors(forUUID=UUIDS.DESCRIPTOR_AUTH)[0] + self._desc_auth = self._char_auth.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] self._char_heart_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] self._char_heart_measure = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] @@ -84,6 +97,8 @@ def __init__(self, mac_address, timeout=0.5, debug=False): # Let MiBand2 to settle self.waitForNotifications(0.1) + # Auth helpers ###################################################################### + def _auth_notif(self, enabled): if enabled: self._log.info("Enabling Auth Service notifications status...") @@ -94,16 +109,6 @@ def _auth_notif(self, enabled): else: self._log.error("Something went wrong while changing the Auth Service notifications status...") - def _get_from_queue(self, _type): - try: - res = self.queue.get(False) - except Empty: - return None - if res[0] != _type: - self.queue.put(res) - return None - return res[1] - def _encrypt(self, message): aes = AES.new(self._KEY, AES.MODE_ECB) return aes.encrypt(message) @@ -125,6 +130,14 @@ def _send_enc_rdn(self, data): self._char_auth.write(send_cmd) self.waitForNotifications(self.timeout) + # Parse helpers ################################################################### + + def _parse_raw_accel(self, bytes): + return bytes + + def _parse_raw_heart(self, bytes): + return bytes + def _parse_date(self, bytes): year = struct.unpack('h', bytes[0:2])[0] if len(bytes) >= 2 else None month = struct.unpack('b', bytes[2])[0] if len(bytes) >= 3 else None @@ -132,8 +145,10 @@ def _parse_date(self, bytes): hours = struct.unpack('b', bytes[4])[0] if len(bytes) >= 5 else None minutes = struct.unpack('b', bytes[5])[0] if len(bytes) >= 6 else None seconds = struct.unpack('b', bytes[6])[0] if len(bytes) >= 7 else None + day_of_week = struct.unpack('b', bytes[7])[0] if len(bytes) >= 8 else None + fractions256 = struct.unpack('b', bytes[8])[0] if len(bytes) >= 9 else None - return datetime(*(year, month, day, hours, minutes, seconds)) + return {"date": datetime(*(year, month, day, hours, minutes, seconds)), "day_of_week": day_of_week, "fractions256": fractions256} def _parse_battery_response(self, bytes): level = struct.unpack('b', bytes[1])[0] if len(bytes) >= 2 else None @@ -157,13 +172,33 @@ def _parse_battery_response(self, bytes): } return res - def _init_after_auth(self): - # char = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_LE_PARAMS)[0] - # print char.read() - # char.write(b"\x03\x01", True) - return - # desc = self._char_heart_measure.getDescriptors(forUUID=UUIDS.DESCRIPTOR_AUTH)[0] - # desc.write(b"\x01\x01", True) + # Queue ################################################################### + + def _get_from_queue(self, _type): + try: + res = self.queue.get(False) + except Empty: + return None + if res[0] != _type: + self.queue.put(res) + return None + return res[1] + + def _parse_queue(self): + while True: + try: + res = self.queue.get(False) + _type = res[0] + if self.heart_measure_callback and _type == QUEUE_TYPES.HEART: + self.heart_measure_callback(struct.unpack('bb', res[1])[1]) + elif self.heart_raw_callback and _type == QUEUE_TYPES.RAW_HEART: + self.heart_raw_callback(self._parse_raw_heart(res[1])) + elif self.accel_raw_callback and _type == QUEUE_TYPES.RAW_ACCEL: + self.accel_raw_callback(self._parse_raw_accel(res[1])) + except Empty: + break + + # API #################################################################### def initialize(self): self.setDelegate(AuthenticationDelegate(self)) @@ -173,7 +208,10 @@ def initialize(self): self.waitForNotifications(0.1) if self.state == AUTH_STATES.AUTH_OK: self._log.info('Initialized') + self._auth_notif(False) return True + elif self.state is None: + continue self._log.error(self.state) return False @@ -186,8 +224,9 @@ def authenticate(self): self.waitForNotifications(0.1) if self.state == AUTH_STATES.AUTH_OK: self._log.info('Authenticated') - self._init_after_auth() return True + elif self.state is None: + continue self._log.error(self.state) return False @@ -198,7 +237,47 @@ def get_battery_info(self): def get_current_time(self): char = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_CURRENT_TIME)[0] - return self._parse_date(char.read()[0:7]) + return self._parse_date(char.read()[0:9]) + + def get_revision(self): + svc = self.getServiceByUUID(UUIDS.SERVICE_DEVICE_INFO) + char = svc.getCharacteristics(UUIDS.CHARACTERISTIC_REVISION)[0] + data = char.read() + revision = struct.unpack('9s', data[-9:])[0] if len(data) == 9 else None + return revision + + def get_hrdw_revision(self): + svc = self.getServiceByUUID(UUIDS.SERVICE_DEVICE_INFO) + char = svc.getCharacteristics(UUIDS.CHARACTERISTIC_HRDW_REVISION)[0] + data = char.read() + revision = struct.unpack('8s', data[-8:])[0] if len(data) == 8 else None + return revision + + def set_encoding(self, encoding="en_US"): + char = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_CONFIGURATION)[0] + packet = struct.pack('5s', encoding) + packet = b'\x06\x17\x00' + packet + return char.write(packet) + + def set_heart_monitor_sleep_support(self, enabled=True, measure_minute_interval=1): + char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] + char_d = char_m.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] + char_d.write(b'\x01\x00', True) + self._char_heart_ctrl.write(b'\x15\x00\x00', True) + # measure interval set to off + self._char_heart_ctrl.write(b'\x14\x00', True) + if enabled: + self._char_heart_ctrl.write(b'\x15\x00\x01', True) + # measure interval set + self._char_heart_ctrl.write(b'\x14' + str(measure_minute_interval).encode(), True) + char_d.write(b'\x00\x00', True) + + def get_serial(self): + svc = self.getServiceByUUID(UUIDS.SERVICE_DEVICE_INFO) + char = svc.getCharacteristics(UUIDS.CHARACTERISTIC_SERIAL)[0] + data = char.read() + serial = struct.unpack('12s', data[-12:])[0] if len(data) == 12 else None + return serial def get_steps(self): char = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_STEPS)[0] @@ -233,84 +312,103 @@ def get_heart_rate_one_time(self): self.waitForNotifications(self.timeout) res = self._get_from_queue(QUEUE_TYPES.HEART) - rate = res + rate = struct.unpack('bb', res)[1] return rate - def get_heart_rate_realtime(self, callback, timeout=10): - """ - I though that mechanics of this request is not a realtime heart rate measure, - but instead iterative measure of N times with sending reasults one by one. - Thus we just make loop in which we sending iterative heart measure request after each 15sec, and that - seems to work. - """ - # stop continous - self._char_heart_ctrl.write(b'\x15\x01\x00', True) - # stop manual - self._char_heart_ctrl.write(b'\x15\x02\x00', True) - - timeout = time.time() + timeout - while timeout > time.time(): - timeout_base = time.time() + 15 - # start continous - self._char_heart_ctrl.write(b'\x15\x01\x01', True) - while timeout_base > time.time(): - self.waitForNotifications(self.timeout) - res = self._get_from_queue(QUEUE_TYPES.HEART) - if res: - callback(res) - - def debug(self): + def start_heart_rate_realtime(self, heart_measure_callback): char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] - char_d = char_m.getDescriptors(forUUID=UUIDS.CHARACTERISTIC_HEART_RATE_CONFIG)[0] + char_d = char_m.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] - char_sensor1 = self.svc_1.getCharacteristics('000000020000351221180009af100700')[0] - char_sens_d1 = char_sensor1.getDescriptors(forUUID=0x2902)[0] + self.heart_measure_callback = heart_measure_callback + + # stop heart monitor continues & manual + char_ctrl.write(b'\x15\x02\x00', True) + char_ctrl.write(b'\x15\x01\x00', True) + # enable heart monitor notifications + char_d.write(b'\x01\x00', True) + # start hear monitor continues + char_ctrl.write(b'\x15\x01\x01', True) + t = time.time() + while True: + self.waitForNotifications(0.5) + self._parse_queue() + # send ping request every 12 sec + if (time.time() - t) >= 12: + char_ctrl.write(b'\x16', True) + t = time.time() + + def start_raw_data_realtime(self, heart_measure_callback=None, heart_raw_callback=None, accel_raw_callback=None): + char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] + char_d = char_m.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] + char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] - char_sensor2 = self.svc_1.getCharacteristics('000000010000351221180009af100700')[0] - char_sens_d2 = char_sensor2.getDescriptors(forUUID=0x2902)[0] + if heart_measure_callback: + self.heart_measure_callback = heart_measure_callback + if heart_raw_callback: + self.heart_raw_callback = heart_raw_callback + if accel_raw_callback: + self.accel_raw_callback = accel_raw_callback - char_sensor3 = self.svc_1.getCharacteristics('000000070000351221180009af100700')[0] - char_sens_d3 = char_sensor3.getDescriptors(forUUID=0x2902)[0] + char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_ACCELEROMETER)[0] + # char_sens_d = char_sensor1.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] - # stop heart monitor continues - char_ctrl.write(b'\x15\x01\x00', True) - # stop heart monitor notifications - char_d.write(b'\x00\x00', True) - # IMO: enable notifications from Sensor 1 - char_sens_d1.write(b'\x01\x00', True) + # char_sensor2 = self.svc_1.getCharacteristics('000000010000351221180009af100700')[0] + # char_sens_d2 = char_sensor2.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] - # IMO: This set band to get raw measures and processed heart measures in parallel - char_sens_d2.write(b'\x01\x00', True) - char_sensor2.write(b'\x01\x03\x19') - char_sens_d2.write(b'\x00\x00', True) + # char_sensor3 = self.svc_1.getCharacteristics('000000070000351221180009af100700')[0] + # char_sens_d3 = char_sensor3.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] - # char_sens_d3.write(b'\x01\x00', True) + # char_sens_d1.write(b'\x01\x00', True) + # char_sens_d2.write(b'\x01\x00', True) + # char_sensor2.write(b'\x01\x03\x19') + # char_sens_d2.write(b'\x00\x00', True) + # char_d.write(b'\x01\x00', True) + # char_ctrl.write(b'\x15\x01\x01', True) + # char_sensor2.write(b'\x02') + # stop heart monitor continues & manual + char_ctrl.write(b'\x15\x02\x00', True) + char_ctrl.write(b'\x15\x01\x00', True) + # WTF + # char_sens_d1.write(b'\x01\x00', True) + # enabling accelerometer & heart monitor raw data notifications + char_sensor.write(b'\x01\x03\x19') # IMO: enablee heart monitor notifications char_d.write(b'\x01\x00', True) - # start hear monitor continues char_ctrl.write(b'\x15\x01\x01', True) # WTF - char_sensor2.write(b'\x02') - try: - while True: - self.waitForNotifications(0.5) - char_ctrl.write(b'\x16') - char_sensor2.write(b'\x00\x00') - except Exception as e: - print e - pass - finally: - # stop heart monitor continues - char_ctrl.write(b'\x15\x01\x00', True) - char_ctrl.write(b'\x15\x01\x00', True) - # IMO: stop heart monitor notifications - char_d.write(b'\x00\x00', True) - # WTF - char_sensor2.write(b'\x03') - # IMO: stop notifications from sensors - char_sens_d1.write(b'\x00\x00', True) - char_sens_d2.write(b'\x00\x00', True) - char_sens_d3.write(b'\x00\x00', True) + char_sensor.write(b'\x02') + t = time.time() + while True: + self.waitForNotifications(0.5) + self._parse_queue() + # send ping request every 12 sec + if (time.time() - t) >= 12: + char_ctrl.write(b'\x16', True) + t = time.time() + + def stop_realtime(self): + char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] + char_d = char_m.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] + char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] + + char_sensor1 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_HZ)[0] + char_sens_d1 = char_sensor1.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] + + char_sensor2 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_ACCELEROMETER)[0] + + # stop heart monitor continues + char_ctrl.write(b'\x15\x01\x00', True) + char_ctrl.write(b'\x15\x01\x00', True) + # IMO: stop heart monitor notifications + char_d.write(b'\x00\x00', True) + # WTF + char_sensor2.write(b'\x03') + # IMO: stop notifications from sensors + char_sens_d1.write(b'\x00\x00', True) + + self.heart_measure_callback = None + self.heart_raw_callback = None + self.accel_raw_callback = None diff --git a/constants.py b/constants.py index b9fe21f..e8825f6 100644 --- a/constants.py +++ b/constants.py @@ -16,26 +16,34 @@ class UUIDS(object): BASE = "0000%s-0000-1000-8000-00805f9b34fb" - SERVICE_MIBAND1 = "0000fee0-0000-1000-8000-00805f9b34fb" - SERVICE_MIBAND2 = "0000fee1-0000-1000-8000-00805f9b34fb" - - SERVICE_ALERT = "00001802-0000-1000-8000-00805f9b34fb" - SERVICE_HEART_RATE = "0000180d-0000-1000-8000-00805f9b34fb" - + SERVICE_MIBAND1 = BASE % 'fee0' + SERVICE_MIBAND2 = BASE % 'fee1' + + SERVICE_ALERT = BASE % '1802' + SERVICE_ALERT_NOTIFICATION = BASE % '1811' + SERVICE_HEART_RATE = BASE % '180d' + SERVICE_DEVICE_INFO = BASE % '180a' + + CHARACTERISTIC_HZ = "00000002-0000-3512-2118-0009af100700" + CHARACTERISTIC_ACCELEROMETER = "00000001-0000-3512-2118-0009af100700" CHARACTERISTIC_AUTH = "00000009-0000-3512-2118-0009af100700" CHARACTERISTIC_HEART_RATE_MEASURE = "00002a37-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_HEART_RATE_CONTROL = "00002a39-0000-1000-8000-00805f9b34fb" - CHARACTERISTIC_HEART_RATE_CONFIG = "00002902-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_ALERT = "00002a06-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_BATTERY = "00000006-0000-3512-2118-0009af100700" CHARACTERISTIC_STEPS = "00000007-0000-3512-2118-0009af100700" CHARACTERISTIC_LE_PARAMS = BASE % "FF09" + CHARACTERISTIC_REVISION = 0x2a28 + CHARACTERISTIC_SERIAL = 0x2a25 + CHARACTERISTIC_HRDW_REVISION = 0x2a27 + CHARACTERISTIC_CONFIGURATION = "00000003-0000-3512-2118-0009af100700" + CHARACTERISTIC_DEVICEEVENT = "00000010-0000-3512-2118-0009af100700" CHARACTERISTIC_CURRENT_TIME = BASE % '2A2B' CHARACTERISTIC_AGE = BASE % '2A80' CHARACTERISTIC_USER_SETTINGS = "00000008-0000-3512-2118-0009af100700" - DESCRIPTOR_AUTH = BASE % '2902' + NOTIFICATION_DESCRIPTOR = 0x2902 class AUTH_STATES(object): @@ -63,3 +71,5 @@ class QUEUE_TYPES(object): __metaclass__ = Immutable HEART = 'heart' + RAW_ACCEL = 'raw_accel' + RAW_HEART = 'raw_heart' diff --git a/example.py b/example.py index 16ff343..1341603 100644 --- a/example.py +++ b/example.py @@ -9,29 +9,43 @@ band.setSecurityLevel(level="medium") if len(sys.argv) > 2: - band.initialize() + if band.initialize(): + print("Init OK") + band.set_heart_monitor_sleep_support(enabled=False) band.disconnect() sys.exit(0) else: band.authenticate() -# print 'Message notif' -# band.send_alert(ALERT_TYPES.MESSAGE) -# time.sleep(3) -# # this will vibrate till not off -# print 'Phone notif' -# band.send_alert(ALERT_TYPES.PHONE) -# time.sleep(8) -# print 'OFF' -# band.send_alert(ALERT_TYPES.NONE) -# print 'Battery:', band.get_battery_info() -# print 'Time:', band.get_current_time() -# print 'Steps:', band.get_steps() -# print 'Heart rate oneshot:', band.get_heart_rate_one_time() - -# def l(x): -# print 'Realtime heart:', x -# band.get_heart_rate_realtime(l, 60) -band.debug() +print 'Message notif' +band.send_alert(ALERT_TYPES.MESSAGE) +time.sleep(3) +# this will vibrate till not off +print 'Phone notif' +band.send_alert(ALERT_TYPES.PHONE) +time.sleep(8) +print 'OFF' +band.send_alert(ALERT_TYPES.NONE) +print 'Soft revision:',band.get_revision() +print 'Hardware revision:',band.get_hrdw_revision() +print 'Serial:',band.get_serial() +print 'Battery:', band.get_battery_info() +print 'Time:', band.get_current_time() +print 'Steps:', band.get_steps() +print 'Heart rate oneshot:', band.get_heart_rate_one_time() + +def l(x): + print 'Realtime heart:', x + + +def b(x): + print 'Raw heart:', x.encode('hex') + + +def f(x): + print 'Raw accel heart:', x.encode('hex') + +# band.start_heart_rate_realtime(heart_measure_callback=l) +band.start_raw_data_realtime(heart_measure_callback=l, heart_raw_callback=b, accel_raw_callback=f) band.disconnect() From 56a9103dba4d7ebbb739e62aac2cfb69fe724d90 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Sat, 24 Mar 2018 12:57:59 +0200 Subject: [PATCH 14/48] added parsing of gyro and raw heart data --- base.py | 23 +++++++++++++++++------ constants.py | 4 ++-- example.py | 4 ++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/base.py b/base.py index 732a63c..f5aa2de 100644 --- a/base.py +++ b/base.py @@ -44,9 +44,9 @@ def handleNotification(self, hnd, data): self.device.queue.put((QUEUE_TYPES.HEART, data)) elif hnd == 0x38: # Not sure about this, need test - if len(data) == 20: + if len(data) == 20 and struct.unpack('b', data[0])[0] == 1: self.device.queue.put((QUEUE_TYPES.RAW_ACCEL, data)) - elif len(data) < 20: + elif len(data) == 16: self.device.queue.put((QUEUE_TYPES.RAW_HEART, data)) else: self.device._log.error("Unhandled Response " + hex(hnd) + ": " + @@ -133,10 +133,21 @@ def _send_enc_rdn(self, data): # Parse helpers ################################################################### def _parse_raw_accel(self, bytes): - return bytes + res = [] + for i in xrange(3): + g = struct.unpack('hhh', bytes[2 + i * 6:8 + i * 6]) + res.append({'x': g[0], 'y': g[1], 'wtf': g[2]}) + # WTF + # if len(bytes) == 20 and struct.unpack('b', bytes[0])[0] == 2: + # print struct.unpack('B', bytes[1]) + # print "Accel x: %s y: %s z: %s" % struct.unpack('hhh', bytes[2:8]) + # print "Accel x: %s y: %s z: %s" % struct.unpack('hhh', bytes[8:14]) + # print "Accel x: %s y: %s z: %s" % struct.unpack('hhh', bytes[14:]) + return res def _parse_raw_heart(self, bytes): - return bytes + res = struct.unpack('HHHHHHH', bytes[2:]) + return res def _parse_date(self, bytes): year = struct.unpack('h', bytes[0:2])[0] if len(bytes) >= 2 else None @@ -350,7 +361,7 @@ def start_raw_data_realtime(self, heart_measure_callback=None, heart_raw_callbac if accel_raw_callback: self.accel_raw_callback = accel_raw_callback - char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_ACCELEROMETER)[0] + char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_GYROSCOPE)[0] # char_sens_d = char_sensor1.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] # char_sensor2 = self.svc_1.getCharacteristics('000000010000351221180009af100700')[0] @@ -397,7 +408,7 @@ def stop_realtime(self): char_sensor1 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_HZ)[0] char_sens_d1 = char_sensor1.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] - char_sensor2 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_ACCELEROMETER)[0] + char_sensor2 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_GYROSCOPE)[0] # stop heart monitor continues char_ctrl.write(b'\x15\x01\x00', True) diff --git a/constants.py b/constants.py index e8825f6..9bf1109 100644 --- a/constants.py +++ b/constants.py @@ -23,9 +23,9 @@ class UUIDS(object): SERVICE_ALERT_NOTIFICATION = BASE % '1811' SERVICE_HEART_RATE = BASE % '180d' SERVICE_DEVICE_INFO = BASE % '180a' - + CHARACTERISTIC_HZ = "00000002-0000-3512-2118-0009af100700" - CHARACTERISTIC_ACCELEROMETER = "00000001-0000-3512-2118-0009af100700" + CHARACTERISTIC_GYROSCOPE = "00000001-0000-3512-2118-0009af100700" CHARACTERISTIC_AUTH = "00000009-0000-3512-2118-0009af100700" CHARACTERISTIC_HEART_RATE_MEASURE = "00002a37-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_HEART_RATE_CONTROL = "00002a39-0000-1000-8000-00805f9b34fb" diff --git a/example.py b/example.py index 1341603..ca56589 100644 --- a/example.py +++ b/example.py @@ -40,11 +40,11 @@ def l(x): def b(x): - print 'Raw heart:', x.encode('hex') + print 'Raw heart:', x def f(x): - print 'Raw accel heart:', x.encode('hex') + print 'Raw accel heart:', x # band.start_heart_rate_realtime(heart_measure_callback=l) band.start_raw_data_realtime(heart_measure_callback=l, heart_raw_callback=b, accel_raw_callback=f) From 34bc29d9fb96f3984984c1498684f07e4556ea69 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Mon, 26 Mar 2018 12:37:32 +0300 Subject: [PATCH 15/48] fixes --- .gitignore | 1 + base.py | 4 +- btsnoop_hci.dump | Bin 90712 -> 0 bytes constants.py | 2 +- dump.py | 3 +- plot.py | 4 +- uuids_list.txt | 179 ----------------------------------------------- 7 files changed, 7 insertions(+), 186 deletions(-) delete mode 100644 btsnoop_hci.dump delete mode 100644 uuids_list.txt diff --git a/.gitignore b/.gitignore index 83658ec..429a590 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc *.log +heartrate_*.* diff --git a/base.py b/base.py index f5aa2de..099b553 100644 --- a/base.py +++ b/base.py @@ -361,7 +361,7 @@ def start_raw_data_realtime(self, heart_measure_callback=None, heart_raw_callbac if accel_raw_callback: self.accel_raw_callback = accel_raw_callback - char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_GYROSCOPE)[0] + char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_SENSOR)[0] # char_sens_d = char_sensor1.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] # char_sensor2 = self.svc_1.getCharacteristics('000000010000351221180009af100700')[0] @@ -408,7 +408,7 @@ def stop_realtime(self): char_sensor1 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_HZ)[0] char_sens_d1 = char_sensor1.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] - char_sensor2 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_GYROSCOPE)[0] + char_sensor2 = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_SENSOR)[0] # stop heart monitor continues char_ctrl.write(b'\x15\x01\x00', True) diff --git a/btsnoop_hci.dump b/btsnoop_hci.dump deleted file mode 100644 index a7f06e55eb60c5a3e343dd3c6201eb905eda3402..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90712 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 diff --git a/constants.py b/constants.py index 9bf1109..76266a0 100644 --- a/constants.py +++ b/constants.py @@ -25,7 +25,7 @@ class UUIDS(object): SERVICE_DEVICE_INFO = BASE % '180a' CHARACTERISTIC_HZ = "00000002-0000-3512-2118-0009af100700" - CHARACTERISTIC_GYROSCOPE = "00000001-0000-3512-2118-0009af100700" + CHARACTERISTIC_SENSOR = "00000001-0000-3512-2118-0009af100700" CHARACTERISTIC_AUTH = "00000009-0000-3512-2118-0009af100700" CHARACTERISTIC_HEART_RATE_MEASURE = "00002a37-0000-1000-8000-00805f9b34fb" CHARACTERISTIC_HEART_RATE_CONTROL = "00002a39-0000-1000-8000-00805f9b34fb" diff --git a/dump.py b/dump.py index 9e0321c..6a8dea8 100644 --- a/dump.py +++ b/dump.py @@ -22,8 +22,7 @@ def log(rate): band = MiBand2(MAC, debug=True) band.setSecurityLevel(level="medium") band.authenticate() - band.get_heart_rate_realtime(log, 60 * 60 * 12) - + band.start_heart_rate_realtime(heart_measure_callback=log) band.disconnect() except BTLEException: pass diff --git a/plot.py b/plot.py index bd88af9..94ba994 100644 --- a/plot.py +++ b/plot.py @@ -13,5 +13,5 @@ # df.plot(kind='line') # plt.subplot('122') # df.plot(kind='histogram') -df.rolling(60).mean().plot() -plt.show() \ No newline at end of file +df.rolling(120).mean().plot() +plt.show() diff --git a/uuids_list.txt b/uuids_list.txt deleted file mode 100644 index 9213b08..0000000 --- a/uuids_list.txt +++ /dev/null @@ -1,179 +0,0 @@ -Services: -attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb -['0x3', '0x5', '0x7'] - -attr handle: 0x0008, end grp handle: 0x000b uuid: 00001801-0000-1000-8000-00805f9b34fb -['0xa'] - -attr handle: 0x000c, end grp handle: 0x0016 uuid: 0000180a-0000-1000-8000-00805f9b34fb -['0xe', '0x10', '0x12', '0x14', '0x16'] - -attr handle: 0x0017, end grp handle: 0x001c uuid: 00001530-0000-3512-2118-0009af100700 -['0x19', '0x1c'] - -attr handle: 0x001d, end grp handle: 0x0023 uuid: 00001811-0000-1000-8000-00805f9b34fb -['0x1f', '0x22'] - -Alert -attr handle: 0x0024, end grp handle: 0x0026 uuid: 00001802-0000-1000-8000-00805f9b34fb -['0x26'] - -Heart -attr handle: 0x0027, end grp handle: 0x002c uuid: 0000180d-0000-1000-8000-00805f9b34fb -['0x29', '0x2c'] - -Miband1 -attr handle: 0x002d, end grp handle: 0x0051 uuid: 0000fee0-0000-1000-8000-00805f9b34fb -['0x2f', '0x32', '0x35', '0x38', '0x3b', '0x3e', '0x41', '0x44', '0x47', '0x4a', '0x4d', '0x50'] - -Miband2 -attr handle: 0x0052, end grp handle: 0x0066 uuid: 0000fee1-0000-1000-8000-00805f9b34fb -['0x54', '0x57', '0x59', '0x5b', '0x5d', '0x5f', '0x61', '0x63', '0x65'] - - -Characteristics - -handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb -handle: 0x0004, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb -handle: 0x0006, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0007, uuid: 00002a04-0000-1000-8000-00805f9b34fb -handle: 0x0008, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x0009, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x000a, uuid: 00002a05-0000-1000-8000-00805f9b34fb -handle: 0x000b, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x000c, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x000d, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x000e, uuid: 00002a25-0000-1000-8000-00805f9b34fb -handle: 0x000f, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0010, uuid: 00002a27-0000-1000-8000-00805f9b34fb -handle: 0x0011, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0012, uuid: 00002a28-0000-1000-8000-00805f9b34fb -handle: 0x0013, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0014, uuid: 00002a23-0000-1000-8000-00805f9b34fb -handle: 0x0015, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0016, uuid: 00002a50-0000-1000-8000-00805f9b34fb -handle: 0x0017, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x0018, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0019, uuid: 00001531-0000-3512-2118-0009af100700 -handle: 0x001a, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x001b, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x001c, uuid: 00001532-0000-3512-2118-0009af100700 -handle: 0x001d, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x001e, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x001f, uuid: 00002a46-0000-1000-8000-00805f9b34fb -handle: 0x0020, uuid: 00002901-0000-1000-8000-00805f9b34fb -handle: 0x0021, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0022, uuid: 00002a44-0000-1000-8000-00805f9b34fb -handle: 0x0023, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0024, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x0025, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0026, uuid: 00002a06-0000-1000-8000-00805f9b34fb -handle: 0x0027, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x0028, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0029, uuid: 00002a37-0000-1000-8000-00805f9b34fb -handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x002b, uuid: 00002803-0000-1000-8000-00805f9b34fb -# controll start of getting data from sensors -# set \x15\x01\x01 to start -handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb - -handle: 0x002d, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x002e, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x002f, uuid: 00002a2b-0000-1000-8000-00805f9b34fb -handle: 0x0030, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0031, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0032, uuid: 00000020-0000-3512-2118-0009af100700 -handle: 0x0033, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0034, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 -handle: 0x0036, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0037, uuid: 00002803-0000-1000-8000-00805f9b34fb - -# getting data from sensors -handle: 0x0038, uuid: 00000002-0000-3512-2118-0009af100700 - - -handle: 0x0039, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x003a, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x003b, uuid: 00000003-0000-3512-2118-0009af100700 -handle: 0x003c, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x003d, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x003e, uuid: 00002a04-0000-1000-8000-00805f9b34fb -handle: 0x003f, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0040, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0041, uuid: 00000004-0000-3512-2118-0009af100700 -handle: 0x0042, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0043, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0044, uuid: 00000005-0000-3512-2118-0009af100700 -handle: 0x0045, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0046, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0047, uuid: 00000006-0000-3512-2118-0009af100700 -handle: 0x0048, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0049, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x004a, uuid: 00000007-0000-3512-2118-0009af100700 -handle: 0x004b, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x004c, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x004d, uuid: 00000008-0000-3512-2118-0009af100700 -handle: 0x004e, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x004f, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0050, uuid: 00000010-0000-3512-2118-0009af100700 -handle: 0x0051, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0052, uuid: 00002800-0000-1000-8000-00805f9b34fb -handle: 0x0053, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0054, uuid: 00000009-0000-3512-2118-0009af100700 -handle: 0x0055, uuid: 00002902-0000-1000-8000-00805f9b34fb -handle: 0x0056, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0057, uuid: 0000fedd-0000-1000-8000-00805f9b34fb -handle: 0x0058, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0059, uuid: 0000fede-0000-1000-8000-00805f9b34fb -handle: 0x005a, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x005b, uuid: 0000fedf-0000-1000-8000-00805f9b34fb -handle: 0x005c, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x005d, uuid: 0000fed0-0000-1000-8000-00805f9b34fb -handle: 0x005e, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x005f, uuid: 0000fed1-0000-1000-8000-00805f9b34fb -handle: 0x0060, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0061, uuid: 0000fed2-0000-1000-8000-00805f9b34fb -handle: 0x0062, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0063, uuid: 0000fed3-0000-1000-8000-00805f9b34fb -handle: 0x0064, uuid: 00002803-0000-1000-8000-00805f9b34fb -handle: 0x0065, uuid: 0000fec1-0000-3512-2118-0009af100700 -handle: 0x0066, uuid: 00002902-0000-1000-8000-00805f9b34fb - - -''' Sensors -# sent \x01\x00 -handle: 0x0039, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x01\x00 -handle: 0x0036, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x01\x03\x19 -handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 -# sent \x00\x00 -handle: 0x0036, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x01\x00 -handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x15\x01\x01 start -handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb -# sent \x02 -handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 - -# after some time -# sent \x16 -handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb - -# sent \x15\x01\x00 stop -handle: 0x002c, uuid: 00002a39-0000-1000-8000-00805f9b34fb -# sent \x00\x00 -handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x00\x00 -handle: 0x002a, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x03 -handle: 0x0035, uuid: 00000001-0000-3512-2118-0009af100700 -# sent \x00\x00 -handle: 0x0039, uuid: 00002902-0000-1000-8000-00805f9b34fb -# sent \x00\x00 -handle: 0x004b, uuid: 00002902-0000-1000-8000-00805f9b34fb -''' \ No newline at end of file From 6bca1e2120729d26151c453ddc4784ad5949f64b Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Mon, 26 Mar 2018 12:43:48 +0300 Subject: [PATCH 16/48] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6a1a751..38430ea 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # MiBand2 Library to work with Xiaomi MiBand 2 +[Read the Article here](https://medium.com/@a.nikishaev/how-i-hacked-xiaomi-miband-2-to-control-it-from-linux-a5bd2f36d3ad) # Contributors & Info Sources 1) Base lib provided by [Leo Soares](https://github.com/leojrfs/miband2) From 1c22670e99d89c043163f8d5710b8b0b4fdffcdb Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Thu, 29 Mar 2018 10:11:31 +0300 Subject: [PATCH 17/48] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 38430ea..d6701f2 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ python example.py MAC_ADDRESS ```sh sudo hciconfig hci0 reset ``` +Also there is cool JS library that made Volodymyr Shymansky https://github.com/vshymanskyy/miband-js partly based on this work. # Donate If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://www.paypal.me/creotiv From 089ad2bb91863f655316900f36c61d08a547c4e3 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Thu, 29 Mar 2018 10:12:04 +0300 Subject: [PATCH 18/48] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6701f2..8fa03c6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ python example.py MAC_ADDRESS ```sh sudo hciconfig hci0 reset ``` -Also there is cool JS library that made Volodymyr Shymansky https://github.com/vshymanskyy/miband-js partly based on this work. +Also there is cool JS library that made Volodymyr Shymansky https://github.com/vshymanskyy/miband-js # Donate If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://www.paypal.me/creotiv From 9929aa6881bdf76ad5f01ae4262412cd9f726cc3 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Sun, 19 Aug 2018 18:56:34 +0300 Subject: [PATCH 19/48] Update plot.py --- plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plot.py b/plot.py index 94ba994..bd67014 100644 --- a/plot.py +++ b/plot.py @@ -13,5 +13,5 @@ # df.plot(kind='line') # plt.subplot('122') # df.plot(kind='histogram') -df.rolling(120).mean().plot() +df.rolling('120s').mean().plot() plt.show() From d6603ff59da1930137b6656b4d7bca259f17a051 Mon Sep 17 00:00:00 2001 From: florian hantke Date: Mon, 5 Nov 2018 22:51:39 +0100 Subject: [PATCH 20/48] added previews data feature --- README.md | 5 +-- base.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++-- constants.py | 3 ++ example.py | 64 ++++++++++++++++++++++----------- 4 files changed, 147 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 8fa03c6..b34bb90 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,12 @@ sudo hcitool lescan ``` 5) Run this to auth device ```sh -python example.py MAC_ADDRESS init +python example.py --mac MAC_ADDRESS init ``` 6) Run this to call demo functions ```sh -python example.py MAC_ADDRESS +python example.py --standard --mac MAC_ADDRESS +python example.py --help ``` 7) If you having problems(BLE can glitch sometimes) try this and repeat from 4) ```sh diff --git a/base.py b/base.py index 099b553..d440121 100644 --- a/base.py +++ b/base.py @@ -1,7 +1,7 @@ import struct import time import logging -from datetime import datetime +from datetime import datetime, timedelta from Crypto.Cipher import AES from Queue import Queue, Empty from bluepy.btle import Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM, BTLEException @@ -20,8 +20,6 @@ def __init__(self, device): def handleNotification(self, hnd, data): # Debug purposes - # self.device._log.debug("DATA: " + str(data.encode("hex"))) - # self.device._log.debug("HNd" + str(hnd)) if hnd == self.device._char_auth.getHandle(): if data[:3] == b'\x10\x01\x01': self.device._req_rdn() @@ -48,6 +46,66 @@ def handleNotification(self, hnd, data): self.device.queue.put((QUEUE_TYPES.RAW_ACCEL, data)) elif len(data) == 16: self.device.queue.put((QUEUE_TYPES.RAW_HEART, data)) + # The fetch characteristic controls the communication with the activity characteristic. + # It can trigger the communication. + elif hnd == self.device._char_fetch.getHandle(): + if data[:3] == b'\x10\x01\x01': + # get timestamp from what date the data actually is received + year = struct.unpack(" datetime.now() - timedelta(minutes=1): + self.device.active = False + return + print("Trigger more communication") + time.sleep(1) + t = self.device.last_timestamp + timedelta(minutes=1) + self.device.start_get_previews_data(t) + else: + pkg = self.device.pkg + # pkg_head = int.from_bytes(data[0:1], byteorder='little') + # print("Packet: {} (real) - {} (head)".format(pkg, pkg_head)) + self.device.pkg += 1 + i = 1 + while i < len(data): + index = int(pkg) * 4 + (i - 1) / 4 + timestamp = self.device.first_timestamp + timedelta(minutes=index) + self.device.last_timestamp = timestamp + # timestamp = timestamp + timedelta(hours=1) + # category = int.from_bytes(data[i:i + 1], byteorder='little') + intensity = struct.unpack("B", data[i + 1:i + 2])[0] + steps = struct.unpack("B", data[i + 2:i + 3])[0] + heart_rate = struct.unpack("B", data[i + 3:i + 4])[0] + + print("{}: acceleration {}; steps {}; heart rate {};".format( + timestamp.strftime('%d.%m - %H:%M'), + intensity, + steps, + heart_rate) + ) + + i += 4 + + d = datetime.now().replace(second=0, microsecond=0) - timedelta(minutes=1) + if timestamp == d: + self.device.active = False + return else: self.device._log.error("Unhandled Response " + hex(hnd) + ": " + str(data.encode("hex")) + " len:" + str(len(data))) @@ -62,6 +120,7 @@ class MiBand2(Peripheral): _send_key_cmd = struct.pack('<18s', b'\x01\x00' + _KEY) _send_rnd_cmd = struct.pack('<2s', b'\x02\x00') _send_enc_key = struct.pack('<2s', b'\x03\x00') + pkg = 0 def __init__(self, mac_address, timeout=0.5, debug=False): FORMAT = '%(asctime)-15s %(name)s (%(levelname)s) > %(message)s' @@ -92,6 +151,12 @@ def __init__(self, mac_address, timeout=0.5, debug=False): self._char_heart_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] self._char_heart_measure = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] + # Recorded information + self._char_fetch = self.getCharacteristics(uuid=UUIDS.CHARACTERISTIC_FETCH)[0] + self._desc_fetch = self._char_fetch.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] + self._char_activity = self.getCharacteristics(uuid=UUIDS.CHARACTERISTIC_ACTIVITY_DATA)[0] + self._desc_activity = self._char_activity.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] + # Enable auth service notifications on startup self._auth_notif(True) # Let MiBand2 to settle @@ -109,6 +174,20 @@ def _auth_notif(self, enabled): else: self._log.error("Something went wrong while changing the Auth Service notifications status...") + def _auth_previews_data_notif(self, enabled): + if enabled: + self._log.info("Enabling Fetch Char notifications status...") + self._desc_fetch.write(b"\x01\x00", True) + self._log.info("Enabling Activity Char notifications status...") + self._desc_activity.write(b"\x01\x00", True) + elif not enabled: + self._log.info("Disabling Fetch Char notifications status...") + self._desc_fetch.write(b"\x00\x00", True) + self._log.info("Disabling Activity Char notifications status...") + self._desc_activity.write(b"\x00\x00", True) + else: + self._log.error("Something went wrong while changing the Fetch and Activity notifications status...") + def _encrypt(self, message): aes = AES.new(self._KEY, AES.MODE_ECB) return aes.encrypt(message) @@ -423,3 +502,18 @@ def stop_realtime(self): self.heart_measure_callback = None self.heart_raw_callback = None self.accel_raw_callback = None + + def start_get_previews_data(self, start_timestamp): + self._auth_previews_data_notif(True) + self.waitForNotifications(0.1) + print("Trigger activity communication") + year = struct.pack(" 2: +if args.init: if band.initialize(): print("Init OK") band.set_heart_monitor_sleep_support(enabled=False) @@ -17,22 +27,31 @@ else: band.authenticate() -print 'Message notif' -band.send_alert(ALERT_TYPES.MESSAGE) -time.sleep(3) -# this will vibrate till not off -print 'Phone notif' -band.send_alert(ALERT_TYPES.PHONE) -time.sleep(8) -print 'OFF' -band.send_alert(ALERT_TYPES.NONE) -print 'Soft revision:',band.get_revision() -print 'Hardware revision:',band.get_hrdw_revision() -print 'Serial:',band.get_serial() -print 'Battery:', band.get_battery_info() -print 'Time:', band.get_current_time() -print 'Steps:', band.get_steps() -print 'Heart rate oneshot:', band.get_heart_rate_one_time() +if args.recorded: + print 'Print previews recorded data' + band._auth_previews_data_notif(True) + start_time = datetime.strptime("12.03.2018 01:01", "%d.%m.%Y %H:%M") + band.start_get_previews_data(start_time) + while band.active: + band.waitForNotifications(0.1) + +if args.standard: + print 'Message notif' + band.send_alert(ALERT_TYPES.MESSAGE) + time.sleep(3) + # this will vibrate till not off + print 'Phone notif' + band.send_alert(ALERT_TYPES.PHONE) + time.sleep(8) + print 'OFF' + band.send_alert(ALERT_TYPES.NONE) + print 'Soft revision:',band.get_revision() + print 'Hardware revision:',band.get_hrdw_revision() + print 'Serial:',band.get_serial() + print 'Battery:', band.get_battery_info() + print 'Time:', band.get_current_time() + print 'Steps:', band.get_steps() + print 'Heart rate oneshot:', band.get_heart_rate_one_time() def l(x): @@ -46,6 +65,11 @@ def b(x): def f(x): print 'Raw accel heart:', x -# band.start_heart_rate_realtime(heart_measure_callback=l) -band.start_raw_data_realtime(heart_measure_callback=l, heart_raw_callback=b, accel_raw_callback=f) +if args.live: + # band.start_heart_rate_realtime(heart_measure_callback=l) + band.start_raw_data_realtime( + heart_measure_callback=l, + heart_raw_callback=b, + accel_raw_callback=f) + band.disconnect() From 5b89be566870db406cf0d5c8ff3d86d5d58b1c99 Mon Sep 17 00:00:00 2001 From: florian hantke Date: Mon, 5 Nov 2018 22:53:31 +0100 Subject: [PATCH 21/48] fixed readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b34bb90..e4f11fb 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ sudo hcitool lescan ``` 5) Run this to auth device ```sh -python example.py --mac MAC_ADDRESS init +python example.py --mac MAC_ADDRESS --init ``` 6) Run this to call demo functions ```sh From 4874776e378ad0ba49d02ae1ae658036b1a5e605 Mon Sep 17 00:00:00 2001 From: florian hantke Date: Mon, 5 Nov 2018 23:08:26 +0100 Subject: [PATCH 22/48] clean --- base.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/base.py b/base.py index d440121..40d4a58 100644 --- a/base.py +++ b/base.py @@ -56,7 +56,6 @@ def handleNotification(self, hnd, data): day = struct.unpack("b", data[10:11])[0] hour = struct.unpack("b", data[11:12])[0] minute = struct.unpack("b", data[12:13])[0] - self.device.timestamp = datetime(year, month, day, hour, minute) self.device.first_timestamp = datetime(year, month, day, hour, minute) print("Fetch data from {}-{}-{} {}:{}".format(year, month, day, hour, minute)) self.device._char_fetch.write(b'\x02', False) @@ -64,7 +63,7 @@ def handleNotification(self, hnd, data): self.device.active = False return else: - print("Unexpected data on handle " + hex(hnd) + ": " + str(data) + " " + data) + print("Unexpected data on handle " + str(hnd) + ": " + str(data.encode("hex"))) return # The activity characteristic sends the previews recorded information # from one given timestamp until now. @@ -79,22 +78,20 @@ def handleNotification(self, hnd, data): self.device.start_get_previews_data(t) else: pkg = self.device.pkg - # pkg_head = int.from_bytes(data[0:1], byteorder='little') - # print("Packet: {} (real) - {} (head)".format(pkg, pkg_head)) self.device.pkg += 1 i = 1 while i < len(data): index = int(pkg) * 4 + (i - 1) / 4 timestamp = self.device.first_timestamp + timedelta(minutes=index) self.device.last_timestamp = timestamp - # timestamp = timestamp + timedelta(hours=1) - # category = int.from_bytes(data[i:i + 1], byteorder='little') + category = int.from_bytes(data[i:i + 1], byteorder='little') intensity = struct.unpack("B", data[i + 1:i + 2])[0] steps = struct.unpack("B", data[i + 2:i + 3])[0] heart_rate = struct.unpack("B", data[i + 3:i + 4])[0] - print("{}: acceleration {}; steps {}; heart rate {};".format( + print("{}: category: {}; acceleration {}; steps {}; heart rate {};".format( timestamp.strftime('%d.%m - %H:%M'), + category, intensity, steps, heart_rate) From 626cfb7780014a990659f9eb76b5d9c2f8df3556 Mon Sep 17 00:00:00 2001 From: Pablo Hinojosa Date: Thu, 20 Dec 2018 02:14:35 +0100 Subject: [PATCH 23/48] Fix broken link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4f11fb..a7af6a6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Library to work with Xiaomi MiBand 2 # Contributors & Info Sources 1) Base lib provided by [Leo Soares](https://github.com/leojrfs/miband2) 2) Additional debug & fixes was made by my friend [Volodymyr Shymanskyy](https://github.com/vshymanskyy/miband2-python-test) -3) Some info that really helped i got from [Freeyourgadget team](https://github.com/Freeyourgadget/Gadgetbridge/tree/master/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2) +3) Some info that really helped i got from [Freeyourgadget team](https://github.com/Freeyourgadget/Gadgetbridge/tree/master/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2) # Run From a15aa5fb83244d79c8b963f31051da1e93f62d4b Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Tue, 21 May 2019 12:59:43 +0300 Subject: [PATCH 24/48] Update dump.py --- dump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dump.py b/dump.py index 6a8dea8..583c537 100644 --- a/dump.py +++ b/dump.py @@ -15,7 +15,7 @@ def log(rate): data = "%s, %s\n" % (int(time.time()), rate) fp.write(data) - print data + print(data) while True: try: From ae967525b2b29f597b7aeb287d732fb31e39b21c Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Tue, 21 May 2019 13:00:44 +0300 Subject: [PATCH 25/48] Update example.py --- example.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/example.py b/example.py index 930de49..a9114cc 100644 --- a/example.py +++ b/example.py @@ -28,7 +28,7 @@ band.authenticate() if args.recorded: - print 'Print previews recorded data' + print('Print previews recorded data') band._auth_previews_data_notif(True) start_time = datetime.strptime("12.03.2018 01:01", "%d.%m.%Y %H:%M") band.start_get_previews_data(start_time) @@ -36,34 +36,34 @@ band.waitForNotifications(0.1) if args.standard: - print 'Message notif' + print ('Message notif') band.send_alert(ALERT_TYPES.MESSAGE) time.sleep(3) # this will vibrate till not off - print 'Phone notif' + print ('Phone notif') band.send_alert(ALERT_TYPES.PHONE) time.sleep(8) - print 'OFF' + print ('OFF') band.send_alert(ALERT_TYPES.NONE) - print 'Soft revision:',band.get_revision() - print 'Hardware revision:',band.get_hrdw_revision() - print 'Serial:',band.get_serial() - print 'Battery:', band.get_battery_info() - print 'Time:', band.get_current_time() - print 'Steps:', band.get_steps() - print 'Heart rate oneshot:', band.get_heart_rate_one_time() + print ('Soft revision:',band.get_revision()) + print ('Hardware revision:',band.get_hrdw_revision()) + print ('Serial:',band.get_serial()) + print ('Battery:', band.get_battery_info()) + print ('Time:', band.get_current_time()) + print ('Steps:', band.get_steps()) + print ('Heart rate oneshot:', band.get_heart_rate_one_time()) def l(x): - print 'Realtime heart:', x + print ('Realtime heart:', x) def b(x): - print 'Raw heart:', x + print ('Raw heart:', x) def f(x): - print 'Raw accel heart:', x + print ('Raw accel heart:', x) if args.live: # band.start_heart_rate_realtime(heart_measure_callback=l) From c8ac6345716137acddc0fa9f2410e8881b33b610 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Tue, 21 May 2019 13:01:09 +0300 Subject: [PATCH 26/48] Update plot.py --- plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plot.py b/plot.py index bd67014..d29941d 100644 --- a/plot.py +++ b/plot.py @@ -5,10 +5,10 @@ import matplotlib.pyplot as plt df = pd.DataFrame.from_csv(sys.argv[1], index_col=None) -print df.head +print (df.head()) df['time'] = pd.to_datetime(df['time'], unit='s') df = df.set_index('time') -print df.describe() +print (df.describe()) # plt.subplot('111') # df.plot(kind='line') # plt.subplot('122') From 401a058b430fec87d76c1a65b6fc1124ab39b890 Mon Sep 17 00:00:00 2001 From: ANdrey Nikishaev Date: Tue, 21 May 2019 13:59:38 +0300 Subject: [PATCH 27/48] fixes for python2/3 --- .gitignore | 2 ++ README.md | 2 +- base.py | 32 ++++++++++++++++++-------------- example.py | 1 + requirements.txt | 4 ++-- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 429a590..0caef0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.pyc *.log heartrate_*.* +.env +env diff --git a/README.md b/README.md index e4f11fb..500732e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # MiBand2 -Library to work with Xiaomi MiBand 2 +Library to work with Xiaomi MiBand 2 (Support python2/python3) [Read the Article here](https://medium.com/@a.nikishaev/how-i-hacked-xiaomi-miband-2-to-control-it-from-linux-a5bd2f36d3ad) # Contributors & Info Sources diff --git a/base.py b/base.py index 40d4a58..9e14d9b 100644 --- a/base.py +++ b/base.py @@ -3,7 +3,10 @@ import logging from datetime import datetime, timedelta from Crypto.Cipher import AES -from Queue import Queue, Empty +try: + from Queue import Queue, Empty +except ImportError: + from queue import Queue, Empty from bluepy.btle import Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM, BTLEException @@ -42,7 +45,7 @@ def handleNotification(self, hnd, data): self.device.queue.put((QUEUE_TYPES.HEART, data)) elif hnd == 0x38: # Not sure about this, need test - if len(data) == 20 and struct.unpack('b', data[0])[0] == 1: + if len(data) == 20 and struct.unpack('b', data[0:1])[0] == 1: self.device.queue.put((QUEUE_TYPES.RAW_ACCEL, data)) elif len(data) == 16: self.device.queue.put((QUEUE_TYPES.RAW_HEART, data)) @@ -84,7 +87,8 @@ def handleNotification(self, hnd, data): index = int(pkg) * 4 + (i - 1) / 4 timestamp = self.device.first_timestamp + timedelta(minutes=index) self.device.last_timestamp = timestamp - category = int.from_bytes(data[i:i + 1], byteorder='little') + # category = int.from_bytes(data[i:i + 1], byteorder='little') + category = struct.unpack("= 2 else None - month = struct.unpack('b', bytes[2])[0] if len(bytes) >= 3 else None - day = struct.unpack('b', bytes[3])[0] if len(bytes) >= 4 else None - hours = struct.unpack('b', bytes[4])[0] if len(bytes) >= 5 else None - minutes = struct.unpack('b', bytes[5])[0] if len(bytes) >= 6 else None - seconds = struct.unpack('b', bytes[6])[0] if len(bytes) >= 7 else None - day_of_week = struct.unpack('b', bytes[7])[0] if len(bytes) >= 8 else None - fractions256 = struct.unpack('b', bytes[8])[0] if len(bytes) >= 9 else None + month = struct.unpack('b', bytes[2:3])[0] if len(bytes) >= 3 else None + day = struct.unpack('b', bytes[3:4])[0] if len(bytes) >= 4 else None + hours = struct.unpack('b', bytes[4:5])[0] if len(bytes) >= 5 else None + minutes = struct.unpack('b', bytes[5:6])[0] if len(bytes) >= 6 else None + seconds = struct.unpack('b', bytes[6:7])[0] if len(bytes) >= 7 else None + day_of_week = struct.unpack('b', bytes[7:8])[0] if len(bytes) >= 8 else None + fractions256 = struct.unpack('b', bytes[8:9])[0] if len(bytes) >= 9 else None return {"date": datetime(*(year, month, day, hours, minutes, seconds)), "day_of_week": day_of_week, "fractions256": fractions256} def _parse_battery_response(self, bytes): - level = struct.unpack('b', bytes[1])[0] if len(bytes) >= 2 else None - last_level = struct.unpack('b', bytes[19])[0] if len(bytes) >= 20 else None - status = 'normal' if struct.unpack('b', bytes[2])[0] == 0 else "charging" + level = struct.unpack('b', bytes[1:2])[0] if len(bytes) >= 2 else None + last_level = struct.unpack('b', bytes[19:20])[0] if len(bytes) >= 20 else None + status = 'normal' if struct.unpack('b', bytes[2:3])[0] == 0 else "charging" datetime_last_charge = self._parse_date(bytes[11:18]) datetime_last_off = self._parse_date(bytes[3:10]) @@ -373,7 +377,7 @@ def get_steps(self): meters = struct.unpack('h', a[5:7])[0] if len(a) >= 7 else None fat_gramms = struct.unpack('h', a[2:4])[0] if len(a) >= 4 else None # why only 1 byte?? - callories = struct.unpack('b', a[9])[0] if len(a) >= 10 else None + callories = struct.unpack('b', a[9:10])[0] if len(a) >= 10 else None return { "steps": steps, "meters": meters, diff --git a/example.py b/example.py index a9114cc..bfc72d1 100644 --- a/example.py +++ b/example.py @@ -52,6 +52,7 @@ print ('Time:', band.get_current_time()) print ('Steps:', band.get_steps()) print ('Heart rate oneshot:', band.get_heart_rate_one_time()) + def l(x): diff --git a/requirements.txt b/requirements.txt index 1bc9be0..0fd7f22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -bluepy -pycrypto \ No newline at end of file +bluepy==1.3.0 +pycrypto==2.6.1 From 22b8eb576a8459a8eda268091a9cf21900abfe54 Mon Sep 17 00:00:00 2001 From: Typere Date: Sat, 22 Jun 2019 20:34:40 +0200 Subject: [PATCH 28/48] Add setter for current time --- base.py | 12 +++++++++++- example.py | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/base.py b/base.py index 9e14d9b..d06837e 100644 --- a/base.py +++ b/base.py @@ -229,7 +229,8 @@ def _parse_raw_heart(self, bytes): res = struct.unpack('HHHHHHH', bytes[2:]) return res - def _parse_date(self, bytes): + @staticmethod + def _parse_date(bytes): year = struct.unpack('h', bytes[0:2])[0] if len(bytes) >= 2 else None month = struct.unpack('b', bytes[2:3])[0] if len(bytes) >= 3 else None day = struct.unpack('b', bytes[3:4])[0] if len(bytes) >= 4 else None @@ -241,6 +242,11 @@ def _parse_date(self, bytes): return {"date": datetime(*(year, month, day, hours, minutes, seconds)), "day_of_week": day_of_week, "fractions256": fractions256} + @staticmethod + def create_date_data(date): + data = struct.pack( 'hbbbbbbbxx', date.year, date.month, date.day, date.hour, date.minute, date.second, date.weekday(), 0 ) + return data + def _parse_battery_response(self, bytes): level = struct.unpack('b', bytes[1:2])[0] if len(bytes) >= 2 else None last_level = struct.unpack('b', bytes[19:20])[0] if len(bytes) >= 20 else None @@ -330,6 +336,10 @@ def get_current_time(self): char = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_CURRENT_TIME)[0] return self._parse_date(char.read()[0:9]) + def set_current_time(self, date): + char = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_CURRENT_TIME)[0] + return char.write(self.create_date_data(date), True) + def get_revision(self): svc = self.getServiceByUUID(UUIDS.SERVICE_DEVICE_INFO) char = svc.getCharacteristics(UUIDS.CHARACTERISTIC_REVISION)[0] diff --git a/example.py b/example.py index bfc72d1..612100c 100644 --- a/example.py +++ b/example.py @@ -11,6 +11,7 @@ parser.add_argument('-l', '--live', action='store_true',help='Measures live heart rate') parser.add_argument('-i', '--init', action='store_true',help='Initializes the device') parser.add_argument('-m', '--mac', required=True, help='Mac address of the device') +parser.add_argument('-t', '--set_current_time', action='store_true',help='Set time') args = parser.parse_args() MAC = args.mac # sys.argv[1] @@ -53,7 +54,11 @@ print ('Steps:', band.get_steps()) print ('Heart rate oneshot:', band.get_heart_rate_one_time()) - +if args.set_current_time: + now = datetime.now() + print ('Set time to:', now) + print ('Returned: ', band.set_current_time(now)) + print ('Time:', band.get_current_time()) def l(x): print ('Realtime heart:', x) From 4da74a48643acb8550434a8f04794a6ae2a9c44b Mon Sep 17 00:00:00 2001 From: zhm Date: Mon, 2 Sep 2019 00:25:40 +0800 Subject: [PATCH 29/48] new alert_type --- constants.py | 2 ++ 1 file changed, 2 insertions(+) mode change 100644 => 100755 constants.py diff --git a/constants.py b/constants.py old mode 100644 new mode 100755 index c2e1b07..8852775 --- a/constants.py +++ b/constants.py @@ -67,6 +67,8 @@ class ALERT_TYPES(object): NONE = b'\x00' MESSAGE = b'\x01' PHONE = b'\x02' + FIND_DEVICE = b'\x03' + FOCUS_MESSAGE = b'\xfe' class QUEUE_TYPES(object): From 61428673d5862e2519f9cd5b865ed51ad3b0698b Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Tue, 12 Nov 2019 23:22:20 +0200 Subject: [PATCH 30/48] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 47008f6..39be39f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Library to work with Xiaomi MiBand 2 (Support python2/python3) 2) Additional debug & fixes was made by my friend [Volodymyr Shymanskyy](https://github.com/vshymanskyy/miband2-python-test) 3) Some info that really helped i got from [Freeyourgadget team](https://github.com/Freeyourgadget/Gadgetbridge/tree/master/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2) +## Online Course "Object Detection with PyTorch" +Subscribe to my new online course: [LearnML.Today](http://learnml.today/) + # Run 1) Install dependencies From 29cb938e1433a21db2f44600f5cbd177a14c1ff2 Mon Sep 17 00:00:00 2001 From: Andrey Nikishaev Date: Tue, 3 Dec 2019 15:13:48 +0200 Subject: [PATCH 31/48] Update README.md --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 39be39f..5554924 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,18 @@ sudo hciconfig hci0 reset Also there is cool JS library that made Volodymyr Shymansky https://github.com/vshymanskyy/miband-js # Donate -If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://www.paypal.me/creotiv +If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://patreon.com/mlworld + +

+ + CC0 + +
+ To the extent possible under law, + + Andrey Nikishaev + has waived all copyright and related or neighboring rights to + Library to work with Xiaomi MiBand 2 . +

From 6ada906daca7beb469a3784aad3342491d178cb2 Mon Sep 17 00:00:00 2001 From: Tomasz Wieczorek Date: Sun, 19 Jan 2020 02:09:41 +0100 Subject: [PATCH 32/48] base.py duplicate key removal Removed duplicated line --- base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/base.py b/base.py index d06837e..308d290 100644 --- a/base.py +++ b/base.py @@ -263,7 +263,6 @@ def _parse_battery_response(self, bytes): "status": status, "level": level, "last_level": last_level, - "last_level": last_level, "last_charge": datetime_last_charge, "last_off": datetime_last_off } From d40f6796c3435ef34bc6a72216f2efe580f0059c Mon Sep 17 00:00:00 2001 From: Victor Rajewski Date: Thu, 23 Jan 2020 12:05:10 +1100 Subject: [PATCH 33/48] * Fixes binary payload for setting heart rate measurement interval in sleep support * Creates a new method for setting the measurement interval without enabling sleep support * Fixes some spelling errors * Fixes a few bytes problems * removes duplicate keys in a dict * replaces xrange with range (xrange not needed in python3) * creates a property 'outfile' of the Authentication Delegate to allow writing of activity to a file --- base.py | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/base.py b/base.py index d06837e..0b4b3cf 100644 --- a/base.py +++ b/base.py @@ -66,7 +66,7 @@ def handleNotification(self, hnd, data): self.device.active = False return else: - print("Unexpected data on handle " + str(hnd) + ": " + str(data.encode("hex"))) + print("Unexpected data on handle " + str(hnd) + ": " + data.hex()) return # The activity characteristic sends the previews recorded information # from one given timestamp until now. @@ -100,6 +100,8 @@ def handleNotification(self, hnd, data): steps, heart_rate) ) + if self.device.outfile: + self.device.outfile.write(f"{timestamp.strftime('%d.%m.%Y - %H:%M')},{category},{intensity},{steps},{heart_rate}\n") i += 4 @@ -134,6 +136,8 @@ def __init__(self, mac_address, timeout=0.5, debug=False): Peripheral.__init__(self, mac_address, addrType=ADDR_TYPE_RANDOM) self._log.info('Connected') + self.outfile = None + self.timeout = timeout self.mac_address = mac_address self.state = None @@ -214,7 +218,7 @@ def _send_enc_rdn(self, data): def _parse_raw_accel(self, bytes): res = [] - for i in xrange(3): + for i in range(3): g = struct.unpack('hhh', bytes[2 + i * 6:8 + i * 6]) res.append({'x': g[0], 'y': g[1], 'wtf': g[2]}) # WTF @@ -263,7 +267,6 @@ def _parse_battery_response(self, bytes): "status": status, "level": level, "last_level": last_level, - "last_level": last_level, "last_charge": datetime_last_charge, "last_off": datetime_last_off } @@ -368,11 +371,21 @@ def set_heart_monitor_sleep_support(self, enabled=True, measure_minute_interval= # measure interval set to off self._char_heart_ctrl.write(b'\x14\x00', True) if enabled: + if measure_minute_interval > 120: + measure_minute_interval = 120 self._char_heart_ctrl.write(b'\x15\x00\x01', True) # measure interval set - self._char_heart_ctrl.write(b'\x14' + str(measure_minute_interval).encode(), True) + self._char_heart_ctrl.write(b'\x14' + bytes([measure_minute_interval]), True) char_d.write(b'\x00\x00', True) + def set_heart_monitor_measurement_interval(self, enabled=True, measure_minute_interval=1): + if enabled: + if measure_minute_interval > 120: + measure_minute_interval = 120 + self._char_heart_ctrl.write(b'\x14' + bytes([measure_minute_interval]), True) + else: + self._char_heart_ctrl.write(b'\x14\x00', True) + def get_serial(self): svc = self.getServiceByUUID(UUIDS.SERVICE_DEVICE_INFO) char = svc.getCharacteristics(UUIDS.CHARACTERISTIC_SERIAL)[0] @@ -385,14 +398,14 @@ def get_steps(self): a = char.read() steps = struct.unpack('h', a[1:3])[0] if len(a) >= 3 else None meters = struct.unpack('h', a[5:7])[0] if len(a) >= 7 else None - fat_gramms = struct.unpack('h', a[2:4])[0] if len(a) >= 4 else None + fat_grams = struct.unpack('h', a[2:4])[0] if len(a) >= 4 else None # why only 1 byte?? - callories = struct.unpack('b', a[9:10])[0] if len(a) >= 10 else None + calories = struct.unpack('b', a[9:10])[0] if len(a) >= 10 else None return { "steps": steps, "meters": meters, - "fat_gramms": fat_gramms, - "callories": callories + "fat_grams": fat_grams, + "calories": calories } @@ -519,10 +532,10 @@ def start_get_previews_data(self, start_timestamp): self.waitForNotifications(0.1) print("Trigger activity communication") year = struct.pack(" Date: Mon, 6 Apr 2020 23:07:15 +0300 Subject: [PATCH 34/48] Create LICENSE --- LICENSE | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. From 80df560ae22dbab30d6521e93fbe6d3612aa5435 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 28 Jun 2020 10:39:20 +0200 Subject: [PATCH 35/48] Using default authentication. Adding default BLE interface --- base.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/base.py b/base.py index 0b4b3cf..a48aa1f 100644 --- a/base.py +++ b/base.py @@ -71,7 +71,7 @@ def handleNotification(self, hnd, data): # The activity characteristic sends the previews recorded information # from one given timestamp until now. elif hnd == self.device._char_activity.getHandle(): - if len(data) % 4 is not 1: + if len(data) % 4 != 1: if self.device.last_timestamp > datetime.now() - timedelta(minutes=1): self.device.active = False return @@ -115,17 +115,13 @@ def handleNotification(self, hnd, data): class MiBand2(Peripheral): - # _KEY = b'\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x43\x44\x45' - # _send_key_cmd = struct.pack('<18s', b'\x01\x08' + _KEY) - # _send_rnd_cmd = struct.pack('<2s', b'\x02\x08') - # _send_enc_key = struct.pack('<2s', b'\x03\x08') - _KEY = b'\xf5\xd2\x29\x87\x65\x0a\x1d\x82\x05\xab\x82\xbe\xb9\x38\x59\xcf' - _send_key_cmd = struct.pack('<18s', b'\x01\x00' + _KEY) - _send_rnd_cmd = struct.pack('<2s', b'\x02\x00') - _send_enc_key = struct.pack('<2s', b'\x03\x00') + _KEY = b'\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x43\x44\x45' + _send_key_cmd = struct.pack('<18s', b'\x01\x08' + _KEY) + _send_rnd_cmd = struct.pack('<2s', b'\x02\x08') + _send_enc_key = struct.pack('<2s', b'\x03\x08') pkg = 0 - def __init__(self, mac_address, timeout=0.5, debug=False): + def __init__(self, mac_address, timeout=0.5, debug=False, iface=0): FORMAT = '%(asctime)-15s %(name)s (%(levelname)s) > %(message)s' logging.basicConfig(format=FORMAT) log_level = logging.WARNING if not debug else logging.DEBUG @@ -133,7 +129,7 @@ def __init__(self, mac_address, timeout=0.5, debug=False): self._log.setLevel(log_level) self._log.info('Connecting to ' + mac_address) - Peripheral.__init__(self, mac_address, addrType=ADDR_TYPE_RANDOM) + Peripheral.__init__(self, mac_address, addrType=ADDR_TYPE_RANDOM, iface=iface) self._log.info('Connected') self.outfile = None From 42d8b1644e90191c023b7eac2fb23e9daba3b0f5 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 28 Jun 2020 11:04:11 +0200 Subject: [PATCH 36/48] Adding raw PPG support --- base.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++- dump_ppg.py | 28 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 dump_ppg.py diff --git a/base.py b/base.py index a48aa1f..8909d0e 100644 --- a/base.py +++ b/base.py @@ -111,7 +111,7 @@ def handleNotification(self, hnd, data): return else: self.device._log.error("Unhandled Response " + hex(hnd) + ": " + - str(data.encode("hex")) + " len:" + str(len(data))) + str(data) + " len:" + str(len(data))) class MiBand2(Peripheral): @@ -499,6 +499,62 @@ def start_raw_data_realtime(self, heart_measure_callback=None, heart_raw_callbac char_ctrl.write(b'\x16', True) t = time.time() + def writeHandle(self, handle, data, reply=False): + logging.debug("writeHandle %x %s", handle, data) + self.writeCharacteristic(handle, data, reply) + + def start_ppg_data_realtime(self, sample_duration_seconds=30, heart_measure_callback=None, heart_raw_callback=None, accel_raw_callback=None): + if heart_measure_callback: + self.heart_measure_callback = heart_measure_callback + if heart_raw_callback: + self.heart_raw_callback = heart_raw_callback + if accel_raw_callback: + self.accel_raw_callback = accel_raw_callback + + logging.debug("start_ppg_data_realtime") + + char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_SENSOR)[0] + char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] + + self.writeHandle(0x36, b'\x01\x00', True) # 36 01 00 + + self._log.debug("Enable PPG raw data") + char_sensor.write(b'\x01\x02\x19') # 35 01 02 19 + + self._log.debug("Stop heart continuous") + char_ctrl.write(b'\x15\x01\x00', True) # 2C 15 01 00 + + self._log.debug("Start heart continuous") + char_ctrl.write(b'\x15\x01\x01', True) # 2C 15 01 01 + + self._log.debug("Start sensor data") + char_sensor.write(b'\x02') # 35 02 + + ping_time = datetime.now() + timedelta(seconds=10) + stop_time = datetime.now() + timedelta(seconds=sample_duration_seconds) + now_time = datetime.now() + while now_time < stop_time: + self.waitForNotifications(0.5) + self._parse_queue() + if now_time > ping_time: + logging.debug("Writing ping") + char_ctrl.write(b'\x16', True) + ping_time += timedelta(seconds=10) + now_time = datetime.now() + + self.stop_ppg_data_realtime() + + + def stop_ppg_data_realtime(self): + self._log.debug("stop_ppg_data_realtime") + + self._log.debug("Stop sensor data") + char_sensor.write(b'\x03') + + self._log.debug("Stop heart continuous") + char_ctrl.write(b'\x15\x01\x00', True) # 2C 15 01 00 + + def stop_realtime(self): char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] char_d = char_m.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0] diff --git a/dump_ppg.py b/dump_ppg.py new file mode 100644 index 0000000..a85f3c0 --- /dev/null +++ b/dump_ppg.py @@ -0,0 +1,28 @@ +import sys +import os +import time +from base import MiBand2 +from bluepy.btle import BTLEException + +MAC = sys.argv[1] +filepath = sys.argv[2] +if os.path.exists(sys.argv[2]): + os.remove(sys.argv[2]) +fp = open(filepath, 'a') +fp.write('time, PPG_data\n') + + +def log(raw_ppg_array): + data = "%s, %s\n" % (int(time.time()), raw_ppg_array) + fp.write(data) + print(data) + +while True: + try: + band = MiBand2(MAC, debug=True) + band.setSecurityLevel(level="medium") + band.authenticate() + band.start_ppg_data_realtime(heart_raw_callback=log) + band.disconnect() + except BTLEException: + pass From 185a0a6f83f6f995b1c8748b587128e8840b7035 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 28 Jun 2020 12:06:19 +0200 Subject: [PATCH 37/48] Starts to acquire some data --- base.py | 10 +++++++++- constants.py | 8 ++++++++ dump_ppg.py | 17 ++++++++--------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/base.py b/base.py index 8909d0e..a7ac543 100644 --- a/base.py +++ b/base.py @@ -10,7 +10,7 @@ from bluepy.btle import Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM, BTLEException -from constants import UUIDS, AUTH_STATES, ALERT_TYPES, QUEUE_TYPES +from constants import UUIDS, AUTH_STATES, ALERT_TYPES, QUEUE_TYPES, DATA_TYPES class AuthenticationDelegate(DefaultDelegate): @@ -48,7 +48,10 @@ def handleNotification(self, hnd, data): if len(data) == 20 and struct.unpack('b', data[0:1])[0] == 1: self.device.queue.put((QUEUE_TYPES.RAW_ACCEL, data)) elif len(data) == 16: + self.device.queue.put((QUEUE_TYPES.RAW_HEART, data)) + elif self.device.dataType == DATA_TYPES.PPG: self.device.queue.put((QUEUE_TYPES.RAW_HEART, data)) + # The fetch characteristic controls the communication with the activity characteristic. # It can trigger the communication. elif hnd == self.device._char_fetch.getHandle(): @@ -141,6 +144,7 @@ def __init__(self, mac_address, timeout=0.5, debug=False, iface=0): self.heart_measure_callback = None self.heart_raw_callback = None self.accel_raw_callback = None + self.dataType = DATA_TYPES.NONE self.svc_1 = self.getServiceByUUID(UUIDS.SERVICE_MIBAND1) self.svc_2 = self.getServiceByUUID(UUIDS.SERVICE_MIBAND2) @@ -547,6 +551,10 @@ def start_ppg_data_realtime(self, sample_duration_seconds=30, heart_measure_call def stop_ppg_data_realtime(self): self._log.debug("stop_ppg_data_realtime") + self.dataType = DATA_TYPES.PPG + + char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_SENSOR)[0] + char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] self._log.debug("Stop sensor data") char_sensor.write(b'\x03') diff --git a/constants.py b/constants.py index 8852775..e45fd84 100755 --- a/constants.py +++ b/constants.py @@ -78,3 +78,11 @@ class QUEUE_TYPES(object): HEART = 'heart' RAW_ACCEL = 'raw_accel' RAW_HEART = 'raw_heart' + +class DATA_TYPES(object): + + __metaclass__ = Immutable + + NONE = 0x00 + PPG = 0x01 + ACC = 0x02 diff --git a/dump_ppg.py b/dump_ppg.py index a85f3c0..e21a40b 100644 --- a/dump_ppg.py +++ b/dump_ppg.py @@ -17,12 +17,11 @@ def log(raw_ppg_array): fp.write(data) print(data) -while True: - try: - band = MiBand2(MAC, debug=True) - band.setSecurityLevel(level="medium") - band.authenticate() - band.start_ppg_data_realtime(heart_raw_callback=log) - band.disconnect() - except BTLEException: - pass +try: + band = MiBand2(MAC, debug=True) + band.setSecurityLevel(level="medium") + band.authenticate() + band.start_ppg_data_realtime(heart_raw_callback=log) + band.disconnect() +except BTLEException: + pass From 247a14b8793d03d9fc5ed8499b145c56a4ba5f71 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 28 Jun 2020 16:04:21 +0200 Subject: [PATCH 38/48] No use printing integer time to the human --- dump_ppg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dump_ppg.py b/dump_ppg.py index e21a40b..1305841 100644 --- a/dump_ppg.py +++ b/dump_ppg.py @@ -15,7 +15,7 @@ def log(raw_ppg_array): data = "%s, %s\n" % (int(time.time()), raw_ppg_array) fp.write(data) - print(data) + print(raw_ppg_array) try: band = MiBand2(MAC, debug=True) From 318ccdec744665b834806764b186ef6bc9265ece Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 28 Jun 2020 16:09:45 +0200 Subject: [PATCH 39/48] Extracting and parsing PPG data --- base.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/base.py b/base.py index a7ac543..d2f1a1a 100644 --- a/base.py +++ b/base.py @@ -230,7 +230,13 @@ def _parse_raw_accel(self, bytes): return res def _parse_raw_heart(self, bytes): - res = struct.unpack('HHHHHHH', bytes[2:]) + #logging.debug("_parse_raw_heart: %s", bytes) + data_len = len(bytes) + data_points = (data_len-2)//2 + res = [] + for i in range(data_points): + offset = 2*(i+1) + res += struct.unpack('H', bytes[offset:offset+2]) return res @staticmethod @@ -516,6 +522,7 @@ def start_ppg_data_realtime(self, sample_duration_seconds=30, heart_measure_call self.accel_raw_callback = accel_raw_callback logging.debug("start_ppg_data_realtime") + self.dataType = DATA_TYPES.PPG char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_SENSOR)[0] char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] @@ -551,7 +558,6 @@ def start_ppg_data_realtime(self, sample_duration_seconds=30, heart_measure_call def stop_ppg_data_realtime(self): self._log.debug("stop_ppg_data_realtime") - self.dataType = DATA_TYPES.PPG char_sensor = self.svc_1.getCharacteristics(UUIDS.CHARACTERISTIC_SENSOR)[0] char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[0] @@ -562,6 +568,7 @@ def stop_ppg_data_realtime(self): self._log.debug("Stop heart continuous") char_ctrl.write(b'\x15\x01\x00', True) # 2C 15 01 00 + self.dataType = DATA_TYPES.NONE def stop_realtime(self): char_m = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_MEASURE)[0] From 7c81032c2efd2dd886e7fd2b82a4ff07277bb042 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 28 Jun 2020 16:43:56 +0200 Subject: [PATCH 40/48] Writing out 10 elements on a line --- dump_ppg.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/dump_ppg.py b/dump_ppg.py index 1305841..2e15ffa 100644 --- a/dump_ppg.py +++ b/dump_ppg.py @@ -11,17 +11,46 @@ fp = open(filepath, 'a') fp.write('time, PPG_data\n') +def heart(x): + print ('Realtime heart:', x) + + +# Data arrives in arrays of either 3, 4 or 9 shorts: +#Realtime heart: 62 +#[20073, 20084, 20044, 19940, 19825, 19737, 19660, 19624, 19605] +#[19581, 19570, 19556, 19553, 19553, 19582, 19595, 19612, 19652] +#[19682, 19701, 19754, 19784, 19830, 19873, 19900, 19945, 20044] +#[20202, 20399, 20611, 20756] +#Realtime heart: 63 +# +# Print only lines of 10. + +buffer = [] +buffer_len = 10 + +def writedata(data=None): + global buffer, buffer_len + if data is not None: + buffer += data + + if len(buffer) >= buffer_len: + data = "%s, %s\n" % (int(time.time()), buffer[:buffer_len]) + fp.write(data) + buffer = buffer[buffer_len:] + else: + data = "%s, %s\n" % (int(time.time()), buffer) + fp.write(data) def log(raw_ppg_array): - data = "%s, %s\n" % (int(time.time()), raw_ppg_array) - fp.write(data) print(raw_ppg_array) + writedata(raw_ppg_array) try: band = MiBand2(MAC, debug=True) band.setSecurityLevel(level="medium") band.authenticate() - band.start_ppg_data_realtime(heart_raw_callback=log) + band.start_ppg_data_realtime(sample_duration_seconds=30, heart_raw_callback=log, heart_measure_callback=heart) band.disconnect() + writedata() except BTLEException: pass From ca5d9f3493c2a3581d628164f1a40b318627ba9d Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 14:55:26 +0200 Subject: [PATCH 41/48] Adding utility to plot recorded PPG data TODO: Plot live data --- plot_ppg.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 plot_ppg.py diff --git a/plot_ppg.py b/plot_ppg.py new file mode 100644 index 0000000..721f203 --- /dev/null +++ b/plot_ppg.py @@ -0,0 +1,82 @@ +import argparse +import logging +import matplotlib.pyplot as plt +import numpy as np + +import csv + +parser = argparse.ArgumentParser() +parser.add_argument('-m', '--mac', help='MAC address of the device', default="None") +parser.add_argument('-f', '--file', help='CSV data file containing data') +parser.add_argument('-d', '--debug', help='MAC address of the device', default=0) +args = parser.parse_args() + +if args.file and args.mac != "None": + print("Only one of -f/--file or -m/--mac can be specified!") + exit(1) + +if not args.file and args.mac == "None": + print("One of -f/--file or -m/--mac must be specified!") + exit(3) + +FORMAT = '%(asctime)s.%(msecs)03d-%(levelname)s: %(message)s' +if args.debug == 0: + logging.basicConfig( + format=FORMAT, datefmt='%H:%M:%S', level=logging.INFO) +else: + logging.basicConfig( + format=FORMAT, datefmt='%H:%M:%S', level=logging.DEBUG) + +def plot_file_type1(file_name): + all_data = [] + time_start = 0 + time_end = time_start + with open(file_name, 'r') as data_file: + header = data_file.readline() + data = data_file.readlines() + for raw_row in data: + # Input data is of the form: time, [csv array] + # Extract the time + first_comma = raw_row.index(',') + time = raw_row[:first_comma] + if time_start == 0: + time_start = int(time) + else: + time_end = int(time) + # Extract the csv_array + # Strip spaces, and replace the + row_with_brackets = raw_row[first_comma+1:].strip() + array_string = row_with_brackets.replace('[', '').replace(']','').replace(' ', '') + logging.debug("%s | %s", time, array_string) + array_int = [int(i) for i in array_string.split(',')] + all_data += array_int + + times = np.array(range(len(all_data))) + duration = time_end - time_start + logging.debug("Data spans %d seconds", duration) + timesd = times/(len(all_data)*1.0) + timesd *= duration + + plot_data(timesd, all_data) + +def start_plot(): + hl, = plt.plot([], []) + return hl + +def update_line(hl, new_data): + #hl.set_xdata(np.append(hl.get_xdata(), new_data)) + hl.set_ydata(np.append(hl.get_ydata(), new_data)) + plt.draw() + plt.pause(0.0001) + +def plot_data(xdata, ydata): + plt.plot(xdata, ydata) + plt.title('Raw PPG data') + plt.xlabel('Time (s)') + plt.ylabel('Intensity (arb.)') + plt.show() + +if args.file: + print("Plotting data from file: %s " % args.file) + plot_file_type1(args.file) + From 3204816a53ee1f27d734e6dc9afb88cb9c984b2b Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 19:32:36 +0200 Subject: [PATCH 42/48] Cleaning up code --- base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base.py b/base.py index d2f1a1a..6868f5c 100644 --- a/base.py +++ b/base.py @@ -541,9 +541,9 @@ def start_ppg_data_realtime(self, sample_duration_seconds=30, heart_measure_call self._log.debug("Start sensor data") char_sensor.write(b'\x02') # 35 02 - ping_time = datetime.now() + timedelta(seconds=10) - stop_time = datetime.now() + timedelta(seconds=sample_duration_seconds) now_time = datetime.now() + ping_time = now_time + timedelta(seconds=10) + stop_time = now_time + timedelta(seconds=sample_duration_seconds) while now_time < stop_time: self.waitForNotifications(0.5) self._parse_queue() From dee16fd0f8f939fe226ba8c39a82c62fd9c59c78 Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 19:33:29 +0200 Subject: [PATCH 43/48] Live plotting works, but tweaking delta_t calculation --- plot_ppg.py | 96 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/plot_ppg.py b/plot_ppg.py index 721f203..c73a11f 100644 --- a/plot_ppg.py +++ b/plot_ppg.py @@ -1,9 +1,13 @@ import argparse import logging import matplotlib.pyplot as plt +import matplotlib.axes as plt_axes import numpy as np +import time + +from base import MiBand2 +from bluepy.btle import BTLEException -import csv parser = argparse.ArgumentParser() parser.add_argument('-m', '--mac', help='MAC address of the device', default="None") @@ -57,26 +61,98 @@ def plot_file_type1(file_name): timesd = times/(len(all_data)*1.0) timesd *= duration - plot_data(timesd, all_data) + plot_all_data(timesd, all_data) + +def plot_all_data(xdata, ydata): + plt.plot(xdata, ydata) + plt.title('Raw PPG data') + plt.xlabel('Time (s)') + plt.ylabel('Intensity (arb.)') + plt.show() def start_plot(): + global ax + fig = plt.figure() + ax = fig.add_subplot(111) + hl, = plt.plot([], []) + plt.title('Raw PPG data') + plt.xlabel('Time (s)') + plt.ylabel('Intensity (arb.)') return hl -def update_line(hl, new_data): - #hl.set_xdata(np.append(hl.get_xdata(), new_data)) - hl.set_ydata(np.append(hl.get_ydata(), new_data)) +def update_line(ax, hl, new_xdata, new_ydata): + hl.set_xdata(np.append(hl.get_xdata(), new_xdata)) + hl.set_ydata(np.append(hl.get_ydata(), new_ydata)) + + ax.relim() + ax.autoscale_view() + plt.draw() plt.pause(0.0001) -def plot_data(xdata, ydata): - plt.plot(xdata, ydata) - plt.title('Raw PPG data') - plt.xlabel('Time (s)') - plt.ylabel('Intensity (arb.)') + +def plot_data(time_offset, delta_t, data): + global ax, plot + new_times = np.array(range(len(data))) + new_times = new_times/(len(data)*1.0) + new_times *= delta_t*len(data) + new_times += time_offset + + logging.info("dt: %s, Offset: %s", delta_t, time_offset) + update_line(ax, plot, new_times, data) + +def raw_data(data): + global num_data_points, start_time, last_time, dt, old_data + + now = time.time() + + if num_data_points == 0: + num_data_points = len(data) + old_data = data + last_time = now + return + + + if old_data is not None: + dt = 0.04# (now - last_time)/len(data) + start_time = now - dt*num_data_points + logging.info("Initial dt: %s", dt) + plot_data(0, dt, old_data) + old_data = None + + dt = 0.04# (now - start_time)/(len(data)+num_data_points) + logging.debug("Updated dt: %s", dt) + + time_offset = num_data_points * dt + plot_data(time_offset, dt, data) + + num_data_points += len(data) + last_time = now + +def plot_live_test(MAC): + raw_data([1,3,2]) + time.sleep(1) + raw_data([4,3,5]) plt.show() +def plot_live(MAC): + try: + band = MiBand2(MAC, debug=True) + band.setSecurityLevel(level="medium") + band.authenticate() + band.start_ppg_data_realtime(sample_duration_seconds=60, heart_raw_callback=raw_data) + band.disconnect() + plt.show() + except BTLEException: + pass + if args.file: print("Plotting data from file: %s " % args.file) plot_file_type1(args.file) +elif args.mac != "None": + print("Plotting live data from device: %s " % args.mac) + num_data_points = 0 + plot = start_plot() + plot_live(args.mac) From 356234a3e6b11f33a0c86faba49aed379a996d4c Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 19:43:57 +0200 Subject: [PATCH 44/48] With hardcoded dt = 41, commented out calculation of dt using realtime data --- plot_ppg.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/plot_ppg.py b/plot_ppg.py index c73a11f..a1a8794 100644 --- a/plot_ppg.py +++ b/plot_ppg.py @@ -115,13 +115,13 @@ def raw_data(data): if old_data is not None: - dt = 0.04# (now - last_time)/len(data) + dt = 0.041# (now - last_time)/len(data) start_time = now - dt*num_data_points logging.info("Initial dt: %s", dt) plot_data(0, dt, old_data) old_data = None - dt = 0.04# (now - start_time)/(len(data)+num_data_points) + dt = 0.041# (now - start_time)/(len(data)+num_data_points) logging.debug("Updated dt: %s", dt) time_offset = num_data_points * dt @@ -130,12 +130,6 @@ def raw_data(data): num_data_points += len(data) last_time = now -def plot_live_test(MAC): - raw_data([1,3,2]) - time.sleep(1) - raw_data([4,3,5]) - plt.show() - def plot_live(MAC): try: band = MiBand2(MAC, debug=True) From 5151ab0f9febabaf314181c8502689858629a871 Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 19:46:20 +0200 Subject: [PATCH 45/48] Hard coded dt = 40, removed runtime calculation of dt using realtime data --- plot_ppg.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/plot_ppg.py b/plot_ppg.py index a1a8794..c675198 100644 --- a/plot_ppg.py +++ b/plot_ppg.py @@ -99,13 +99,14 @@ def plot_data(time_offset, delta_t, data): new_times *= delta_t*len(data) new_times += time_offset - logging.info("dt: %s, Offset: %s", delta_t, time_offset) + logging.debug("dt: %s, Offset: %s", delta_t, time_offset) update_line(ax, plot, new_times, data) def raw_data(data): - global num_data_points, start_time, last_time, dt, old_data + global num_data_points, start_time, last_time, old_data now = time.time() + dt = 0.04 # Manually calculated via timed calibration run (~1225 data points over ~50 seconds) if num_data_points == 0: num_data_points = len(data) @@ -115,15 +116,10 @@ def raw_data(data): if old_data is not None: - dt = 0.041# (now - last_time)/len(data) start_time = now - dt*num_data_points - logging.info("Initial dt: %s", dt) plot_data(0, dt, old_data) old_data = None - dt = 0.041# (now - start_time)/(len(data)+num_data_points) - logging.debug("Updated dt: %s", dt) - time_offset = num_data_points * dt plot_data(time_offset, dt, data) From ae9f71e3dff1a7bdfc592bd1f7eeb209b3a69e28 Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 19:54:44 +0200 Subject: [PATCH 46/48] Adding details about raw PPG support --- README.md | 24 +++++++++++++++++++++++- dump_ppg.py | 2 +- raw_ppg.png | Bin 0 -> 56219 bytes raw_ppg_zoom.png | Bin 0 -> 51157 bytes 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 raw_ppg.png create mode 100644 raw_ppg_zoom.png diff --git a/README.md b/README.md index 5554924..a104806 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,30 @@ sudo hciconfig hci0 reset ``` Also there is cool JS library that made Volodymyr Shymansky https://github.com/vshymanskyy/miband-js +# Raw PPG data + +You can record raw [PPG](https://en.wikipedia.org/wiki/Photoplethysmogram) data to a file: +```sh +python3 dump_ppg.py MAC_address ppg_data.csv +``` + +The resulting data can be plotted: +```sh +python3 plot_ppg.py -f ppg_data.csv +``` + +![Raw Data](/raw_ppg.png) + +![Raw Data Zoomed in](/raw_ppg_zoom.png) + +Or you can also view raw the PPG data in realtime: + +```sh +python3 plot_ppg.py -m MAC_address +``` + # Donate -If you like what im doing, you can send me some money for pepsi(i dont drink alcohol). https://patreon.com/mlworld +If you like what I'm doing, you can send me some money for pepsi(I dont drink alcohol). https://patreon.com/mlworld

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 literal 0 HcmV?d00001 diff --git a/raw_ppg_zoom.png b/raw_ppg_zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..0a7a4afa5cea9037b050c77aae3879c82d6aa1f3 GIT binary patch literal 51157 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 Date: Tue, 30 Jun 2020 20:03:19 +0200 Subject: [PATCH 47/48] Reformatting with autopep8 --- dump_ppg.py | 19 ++++++++++++------- plot_ppg.py | 50 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/dump_ppg.py b/dump_ppg.py index e685bb9..b3615e7 100644 --- a/dump_ppg.py +++ b/dump_ppg.py @@ -11,23 +11,25 @@ fp = open(filepath, 'a') fp.write('time, PPG_data\n') + def heart(x): - print ('Realtime heart:', x) + print('Realtime heart:', x) # Data arrives in arrays of either 3, 4 or 9 shorts: -#Realtime heart: 62 +# Realtime heart: 62 #[20073, 20084, 20044, 19940, 19825, 19737, 19660, 19624, 19605] #[19581, 19570, 19556, 19553, 19553, 19582, 19595, 19612, 19652] #[19682, 19701, 19754, 19784, 19830, 19873, 19900, 19945, 20044] #[20202, 20399, 20611, 20756] -#Realtime heart: 63 +# Realtime heart: 63 # # Print only lines of 10. -buffer = [] +buffer = [] buffer_len = 10 + def writedata(data=None): global buffer, buffer_len if data is not None: @@ -38,18 +40,21 @@ def writedata(data=None): fp.write(data) buffer = buffer[buffer_len:] else: - data = "%s, %s\n" % (int(time.time()), buffer) - fp.write(data) + data = "%s, %s\n" % (int(time.time()), buffer) + fp.write(data) + def log(raw_ppg_array): print(raw_ppg_array) writedata(raw_ppg_array) + try: band = MiBand2(MAC, debug=True) band.setSecurityLevel(level="medium") band.authenticate() - band.start_ppg_data_realtime(sample_duration_seconds=60, heart_raw_callback=log, heart_measure_callback=heart) + band.start_ppg_data_realtime( + sample_duration_seconds=60, heart_raw_callback=log, heart_measure_callback=heart) band.disconnect() writedata() except BTLEException: diff --git a/plot_ppg.py b/plot_ppg.py index c675198..202bccb 100644 --- a/plot_ppg.py +++ b/plot_ppg.py @@ -10,9 +10,11 @@ parser = argparse.ArgumentParser() -parser.add_argument('-m', '--mac', help='MAC address of the device', default="None") +parser.add_argument( + '-m', '--mac', help='MAC address of the device', default="None") parser.add_argument('-f', '--file', help='CSV data file containing data') -parser.add_argument('-d', '--debug', help='MAC address of the device', default=0) +parser.add_argument( + '-d', '--debug', help='MAC address of the device', default=0) args = parser.parse_args() if args.file and args.mac != "None": @@ -31,6 +33,7 @@ logging.basicConfig( format=FORMAT, datefmt='%H:%M:%S', level=logging.DEBUG) + def plot_file_type1(file_name): all_data = [] time_start = 0 @@ -48,9 +51,10 @@ def plot_file_type1(file_name): else: time_end = int(time) # Extract the csv_array - # Strip spaces, and replace the + # Strip spaces, and replace the row_with_brackets = raw_row[first_comma+1:].strip() - array_string = row_with_brackets.replace('[', '').replace(']','').replace(' ', '') + array_string = row_with_brackets.replace( + '[', '').replace(']', '').replace(' ', '') logging.debug("%s | %s", time, array_string) array_int = [int(i) for i in array_string.split(',')] all_data += array_int @@ -60,34 +64,37 @@ def plot_file_type1(file_name): logging.debug("Data spans %d seconds", duration) timesd = times/(len(all_data)*1.0) timesd *= duration - + plot_all_data(timesd, all_data) + def plot_all_data(xdata, ydata): plt.plot(xdata, ydata) plt.title('Raw PPG data') plt.xlabel('Time (s)') - plt.ylabel('Intensity (arb.)') + plt.ylabel('Intensity (arb.)') plt.show() - + + def start_plot(): global ax fig = plt.figure() ax = fig.add_subplot(111) - + hl, = plt.plot([], []) plt.title('Raw PPG data') plt.xlabel('Time (s)') - plt.ylabel('Intensity (arb.)') + plt.ylabel('Intensity (arb.)') return hl + def update_line(ax, hl, new_xdata, new_ydata): hl.set_xdata(np.append(hl.get_xdata(), new_xdata)) hl.set_ydata(np.append(hl.get_ydata(), new_ydata)) - + ax.relim() - ax.autoscale_view() - + ax.autoscale_view() + plt.draw() plt.pause(0.0001) @@ -98,23 +105,24 @@ def plot_data(time_offset, delta_t, data): new_times = new_times/(len(data)*1.0) new_times *= delta_t*len(data) new_times += time_offset - + logging.debug("dt: %s, Offset: %s", delta_t, time_offset) update_line(ax, plot, new_times, data) + def raw_data(data): global num_data_points, start_time, last_time, old_data - + now = time.time() - dt = 0.04 # Manually calculated via timed calibration run (~1225 data points over ~50 seconds) - + # Manually calculated via timed calibration run (~1225 data points over ~50 seconds) + dt = 0.04 + if num_data_points == 0: num_data_points = len(data) old_data = data last_time = now return - if old_data is not None: start_time = now - dt*num_data_points plot_data(0, dt, old_data) @@ -122,21 +130,24 @@ def raw_data(data): time_offset = num_data_points * dt plot_data(time_offset, dt, data) - + num_data_points += len(data) last_time = now + def plot_live(MAC): try: band = MiBand2(MAC, debug=True) band.setSecurityLevel(level="medium") band.authenticate() - band.start_ppg_data_realtime(sample_duration_seconds=60, heart_raw_callback=raw_data) + band.start_ppg_data_realtime( + sample_duration_seconds=60, heart_raw_callback=raw_data) band.disconnect() plt.show() except BTLEException: pass + if args.file: print("Plotting data from file: %s " % args.file) plot_file_type1(args.file) @@ -145,4 +156,3 @@ def plot_live(MAC): num_data_points = 0 plot = start_plot() plot_live(args.mac) - From 1ade97d0e968ef8f0a33b3111f6aea98f4d4a2f9 Mon Sep 17 00:00:00 2001 From: kev-m Date: Tue, 30 Jun 2020 20:29:44 +0200 Subject: [PATCH 48/48] Username check --- plot_ppg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plot_ppg.py b/plot_ppg.py index 202bccb..0e0bfc2 100644 --- a/plot_ppg.py +++ b/plot_ppg.py @@ -8,7 +8,6 @@ from base import MiBand2 from bluepy.btle import BTLEException - parser = argparse.ArgumentParser() parser.add_argument( '-m', '--mac', help='MAC address of the device', default="None")