diff --git a/.gitignore b/.gitignore
index 83658ec..0caef0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
*.pyc
*.log
+heartrate_*.*
+.env
+env
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.
diff --git a/README.md b/README.md
index b34b615..a104806 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,14 @@
# 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
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)
+
+## Online Course "Object Detection with PyTorch"
+Subscribe to my new online course: [LearnML.Today](http://learnml.today/)
# Run
@@ -18,11 +22,56 @@ pip install -r requirements.txt
```sh
sudo hcitool lescan
```
-5) Run example.py
+5) Run this to auth device
+```sh
+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
```
-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
```
+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
+```
+
+
+
+
+
+Or you can also view raw the PPG data in realtime:
+
+```sh
+python3 plot_ppg.py -m MAC_address
+```
+
+# Donate
+If you like what I'm doing, you can send me some money for pepsi(I dont drink alcohol). https://patreon.com/mlworld
+
+
+
+
+
+
+ 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 .
+
diff --git a/base.py b/base.py
index 28c32d7..6868f5c 100644
--- a/base.py
+++ b/base.py
@@ -1,12 +1,16 @@
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
+try:
+ from Queue import Queue, Empty
+except ImportError:
+ from queue import Queue, Empty
+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):
@@ -19,14 +23,13 @@ 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()
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,10 +42,79 @@ def handleNotification(self, hnd, data):
else:
self.device.state = AUTH_STATES.AUTH_FAILED
elif hnd == self.device._char_heart_measure.getHandle():
- 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 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():
+ 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
+ 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
+ # 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
+ 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}
- return datetime(*(year, month, day, hours, minutes, seconds))
+ @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])[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])
@@ -149,19 +273,38 @@ 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
}
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))
@@ -171,7 +314,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
@@ -184,8 +330,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
@@ -196,21 +343,75 @@ 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 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]
+ 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:
+ 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' + 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]
+ 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]
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])[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
}
@@ -228,32 +429,182 @@ 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
+ 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 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.NOTIFICATION_DESCRIPTOR)[0]
+ char_ctrl = self.svc_heart.getCharacteristics(UUIDS.CHARACTERISTIC_HEART_RATE_CONTROL)[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]
+
+ 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_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]
+ # char_sens_d2 = char_sensor2.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0]
+
+ # char_sensor3 = self.svc_1.getCharacteristics('000000070000351221180009af100700')[0]
+ # char_sens_d3 = char_sensor3.getDescriptors(forUUID=UUIDS.NOTIFICATION_DESCRIPTOR)[0]
+
+ # 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_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 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")
+ 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.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
+
+ 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()
+ 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")
+
+ 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')
+
+ 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]
+ 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_SENSOR)[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
+
+ 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("= 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):
+ 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.disconnect()
+ writedata()
+except BTLEException:
+ pass
diff --git a/example.py b/example.py
new file mode 100644
index 0000000..612100c
--- /dev/null
+++ b/example.py
@@ -0,0 +1,81 @@
+import sys
+import time
+import argparse
+from datetime import datetime
+from base import MiBand2
+from constants import ALERT_TYPES
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-s', '--standard', action='store_true',help='Shows device information')
+parser.add_argument('-r', '--recorded', action='store_true',help='Shows previews recorded data')
+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]
+
+band = MiBand2(MAC, debug=True)
+band.setSecurityLevel(level="medium")
+
+if args.init:
+ if band.initialize():
+ print("Init OK")
+ band.set_heart_monitor_sleep_support(enabled=False)
+ band.disconnect()
+ sys.exit(0)
+else:
+ band.authenticate()
+
+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())
+
+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)
+
+
+def b(x):
+ print ('Raw heart:', x)
+
+
+def f(x):
+ print ('Raw accel heart:', x)
+
+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()
diff --git a/exmaple.py b/exmaple.py
deleted file mode 100644
index 7680218..0000000
--- a/exmaple.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import sys
-import time
-from base import MiBand2
-from constants import ALERT_TYPES
-
-MAC = sys.argv[1]
-
-band = MiBand2(MAC, debug=True)
-band.setSecurityLevel(level="medium")
-
-if len(sys.argv) > 1:
- band.initialize()
- 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.disconnect()
diff --git a/plot.py b/plot.py
new file mode 100644
index 0000000..d29941d
--- /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('120s').mean().plot()
+plt.show()
diff --git a/plot_ppg.py b/plot_ppg.py
new file mode 100644
index 0000000..0e0bfc2
--- /dev/null
+++ b/plot_ppg.py
@@ -0,0 +1,157 @@
+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
+
+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_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(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(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.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()
+ # 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)
+ old_data = None
+
+ 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.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)
diff --git a/raw_ppg.png b/raw_ppg.png
new file mode 100644
index 0000000..0cb8d0a
Binary files /dev/null and b/raw_ppg.png differ
diff --git a/raw_ppg_zoom.png b/raw_ppg_zoom.png
new file mode 100644
index 0000000..0a7a4af
Binary files /dev/null and b/raw_ppg_zoom.png differ
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