diff --git a/bluesleep.py b/bluesleep.py index 020e296..495af01 100755 --- a/bluesleep.py +++ b/bluesleep.py @@ -4,12 +4,7 @@ from bluepy import btle from bluepy.btle import BTLEDisconnectError from miband import miband - -import matplotlib.pyplot as plt -import matplotlib.animation as animation -import csv -import random -from os import path +import sleepdata import threading import re @@ -18,69 +13,23 @@ import subprocess import time from datetime import datetime -sleep_data = { - 'heartrate': { - 'value_name': 'bpm', - 'periods': [2, 5, 10, 15], - 'raw_data': [], - 'averaged_data': [], - }, - 'movement':{ - 'value_name': 'movement', - 'periods': [10, 30, 60], - 'raw_data': [], - 'averaged_data': [], - 'workspace': { - 'gyro_last_x' : 0, - 'gyro_last_y' : 0, - 'gyro_last_z' : 0 - } - } - } - auth_key_filename = 'auth_key.txt' mac_filename = 'mac.txt' csv_filename = "sleep_data.csv" -plt.style.use('dark_background') -graph_figure = plt.figure() -graph_axes = graph_figure.add_subplot(1, 1, 1) -graph_data = {} - -last_heartrate = 0 -last_tick_time = None -tick_seconds = 0.5 - -fieldnames = ['time'] -for data_type in sleep_data: - periods = sleep_data[data_type]['periods'] - for period in periods: - fieldnames.append(data_type + str(period)) - +band = None #-------------------------------------------------------------------------# - -def write_csv(data): - global fieldnames - global csv_filename - if not path.exists(csv_filename): - with open(csv_filename, 'w', newline='') as csvfile: - csv_writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - csv_writer.writeheader() - csv_writer.writerow(data) - else: - with open(csv_filename, 'a', newline='') as csvfile: - csv_writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - csv_writer.writerow(data) +class regex_patterns(): + mac_regex_pattern = re.compile(r'([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})') + authkey_regex_pattern = re.compile(r'([0-9a-fA-F]){32}') def get_mac_address(filename): - mac_regex_pattern = re.compile(r'([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})') try: with open(filename, "r") as f: - hwaddr_search = re.search(mac_regex_pattern, f.read().strip()) - + hwaddr_search = re.search(regex_patterns.mac_regex_pattern, f.read().strip()) if hwaddr_search: MAC_ADDR = hwaddr_search[0] else: @@ -93,10 +42,9 @@ def get_mac_address(filename): def get_auth_key(filename): - authkey_regex_pattern = re.compile(r'([0-9a-fA-F]){32}') try: with open(filename, "r") as f: - key_search = re.search(authkey_regex_pattern, f.read().strip()) + key_search = re.search(regex_patterns.authkey_regex_pattern, f.read().strip()) if key_search: AUTH_KEY = bytes.fromhex(key_search[0]) else: @@ -107,210 +55,28 @@ def get_auth_key(filename): exit(1) return AUTH_KEY - -def process_heartrate_data(heartrate_data, tick_time): - print("BPM: " + str(heartrate_data)) - if heartrate_data > 0: - value_name = sleep_data['heartrate']['value_name'] - sleep_data['heartrate']['raw_data'].append({ - 'time': tick_time, - value_name: heartrate_data - } ) - - -def process_gyro_data(gyro_data, tick_time): - # Each gyro reading from miband4 comes over as a group of three, - # each containing x,y,z values. This function summarizes the - # values into a single consolidated movement value. - - global sleep_data - - sleep_move = sleep_data['movement'] - sleep_workspace = sleep_move['workspace'] - - gyro_last_x = sleep_workspace['gyro_last_x'] - gyro_last_y = sleep_workspace['gyro_last_y'] - gyro_last_z = sleep_workspace['gyro_last_z'] - value_name = sleep_move['value_name'] - gyro_movement = 0 - for gyro_datum in gyro_data: - gyro_delta_x = abs(gyro_datum['x'] - gyro_last_x) - gyro_last_x = gyro_datum['x'] - gyro_delta_y = abs(gyro_datum['y'] - gyro_last_y) - gyro_last_y = gyro_datum['y'] - gyro_delta_z = abs(gyro_datum['z'] - gyro_last_z) - gyro_last_z = gyro_datum['z'] - gyro_delta_sum = gyro_delta_x + gyro_delta_y + gyro_delta_z - gyro_movement += gyro_delta_sum - - sleep_workspace['gyro_last_x'] = gyro_last_x - sleep_workspace['gyro_last_y'] = gyro_last_y - sleep_workspace['gyro_last_z'] = gyro_last_z - - sleep_move['raw_data'].append({ - 'time': tick_time, - value_name: gyro_movement - }) - - -def flush_old_raw_data(tick_time): - global sleep_data - - for data_type in sleep_data: - s_data = sleep_data[data_type] - periods = s_data['periods'] - - cleaned_raw_data = [] - - for raw_datum in s_data['raw_data']: - datum_age = tick_time - raw_datum['time'] - if datum_age < max(periods): - cleaned_raw_data.append(raw_datum) - - s_data['raw_data'] = cleaned_raw_data - - -def average_raw_data(tick_time): - global sleep_data - global last_heartrate - timestamp = datetime.fromtimestamp(tick_time) - csv_out = {'time': timestamp } - - for data_type in sleep_data: - s_data = sleep_data[data_type] - period_averages_dict = {'time': timestamp} - periods = s_data['periods'] - value_name = s_data['value_name'] - - flush_old_raw_data(tick_time) - - for period_seconds in periods: - period_data = [] - period_averages_dict[period_seconds] = 0 - for raw_datum in s_data['raw_data']: - datum_age_seconds = tick_time - raw_datum['time'] - if datum_age_seconds < period_seconds: - period_data.append(raw_datum[value_name]) - - if len(period_data) > 0: - period_data_average = sum(period_data) / len(period_data) - else: - print("({}) Period data empty: {}".format(data_type, - period_seconds)) - if data_type == "heartrate" and period_seconds == min(periods): - period_data_average = last_heartrate - else: - period_data_average = 0 - - period_averages_dict[period_seconds] = zero_to_nan(period_data_average) - - csv_out[data_type + str(period_seconds)] = zero_to_nan(period_data_average) - - s_data['averaged_data'].append(period_averages_dict) - write_csv(csv_out) - - -def zero_to_nan(value): - if value == 0: - return (float('nan')) - return int(value) - +def process_data(data, tick_time): + if data[0] == "GYRO": + sleepdata.process_gyro_data(data[1], tick_time) + elif data[0] == "HR": + sleepdata.process_heartrate_data(data[1], tick_time) def sleep_monitor_callback(data): - global sleep_data - global last_tick_time - tick_time = time.time() - if not last_tick_time: - last_tick_time = time.time() + if not sleepdata.last_tick_time: + sleepdata.last_tick_time = time.time() - if data[0] == "GYRO": - process_gyro_data(data[1], tick_time) - elif data[0] == "HR": - process_heartrate_data(data[1], tick_time) + process_data(data, tick_time) - if (tick_time - last_tick_time) >= tick_seconds: - average_raw_data(tick_time) - last_tick_time = time.time() + if (tick_time - sleepdata.last_tick_time) >= sleepdata.tick_seconds: + sleepdata.average_raw_data(tick_time) + sleepdata.last_tick_time = time.time() - -def init_graph_data(): - for data_type in sleep_data: - data_periods = sleep_data[data_type]['periods'] - graph_data[data_type] = { - 'time': [], - 'data': {} - } - for period in data_periods: - graph_data[data_type]['data'][period] = [] - - -def update_graph_data(): - global sleep_data - global graph_data - - for data_type in sleep_data: - s_data = sleep_data[data_type] # Re-referenced to shorten name - avg_data = s_data['averaged_data'] - - if len(avg_data) > 1: - - g_data = graph_data[data_type] # Re-referenced to short name - data_periods = s_data['periods'] - - starting_index = max([(len(g_data['time']) - 1), 0]) - ending_index = len(avg_data) - 1 - - # Re-referenced to shorten name - sleep_data_range = avg_data[starting_index:ending_index] - - for sleep_datum in sleep_data_range: - g_data['time'].append(sleep_datum['time']) - for period in data_periods: - if g_data['data'][period] != 'nan': - g_data['data'][period].append(sleep_datum[period]) - - -def graph_animation(i): - global sleep_data - global graph_axes - global graph_data - plotflag = False - - if len(graph_data) == 0: - init_graph_data() - - update_graph_data() - - for data_type in graph_data: - if len(graph_data[data_type]['time']) > 0: - graph_axes.clear() - break - - for data_type in sleep_data: - s_data = sleep_data[data_type] - g_data = graph_data[data_type] - if len(g_data['time']) > 0: - plotflag = True - data_periods = sleep_data[data_type]['periods'] - for period in data_periods: - axis_label = "{} {} sec".format(s_data['value_name'], period) - graph_axes.plot(g_data['time'], - g_data['data'][period], - label=axis_label) - - if plotflag: - plt.legend() - - -def connect(): +def connect(mac_filename, auth_key_filename): global band - global mac_filename - global auth_key_filename - success = False timeout = 3 - msg = 'Connection to the MIBand failed. Trying again in {} seconds' + msg = 'Connection to the band failed. Trying again in {} seconds' MAC_ADDR = get_mac_address(mac_filename) AUTH_KEY = get_auth_key(auth_key_filename) @@ -326,10 +92,7 @@ def connect(): print("\nExit.") exit() - def start_data_pull(): - global band - while True: try: band.start_heart_and_gyro(sensitivity=1, callback=sleep_monitor_callback) @@ -357,6 +120,7 @@ def vibrate_pattern(duration): time.sleep(vibro_delay) def vibrate_rolling(): + print("Sending rolling vibration...") for x in range(10): for x in range(20, 40, 1): band.vibrate(x) @@ -364,11 +128,12 @@ def vibrate_rolling(): band.vibrate(x) if __name__ == "__main__": - connect() + connect(mac_filename, auth_key_filename) + #vibrate_pattern(10) data_gather_thread = threading.Thread(target=start_data_pull) data_gather_thread.start() - ani = animation.FuncAnimation(graph_figure, graph_animation, interval=1000) - plt.show() + sleepdata.init_graph() + #import simpleaudio as sa diff --git a/bytepatterns.py b/bytepatterns.py new file mode 100644 index 0000000..3d34cec --- /dev/null +++ b/bytepatterns.py @@ -0,0 +1,45 @@ + +class miband4(): + + class bytepatterns(): + vibration = 'ff{:02x}00000001' + gyro_start = '01{:02x}19' + start = '0100' + stop = '0000' + heart_measure_keepalive = '16' + stop_heart_measure_continues = '150100' + start_heart_measure_continues = '150101' + stop_heart_measure_manual = '150200' + fetch_begin = '100101' + fetch_error = '100104' + fetch_continue = '100201' + fetch_complete = '100204' + auth_ok = '100301' + request_random_number = '0200' + auth_key_prefix = '0300' + + def vibration(duration): + byte_pattern = miband4.bytepatterns.vibration + return bytes.fromhex(byte_pattern.format(duration)) + + def gyro_start(sensitivity): + byte_pattern = miband4.bytepatterns.gyro_start + return bytes.fromhex(byte_pattern.format(sensitivity)) + + start = bytes.fromhex(bytepatterns.start) + stop = bytes.fromhex(bytepatterns.stop) + + heart_measure_keepalive = bytes.fromhex(bytepatterns.heart_measure_keepalive) + stop_heart_measure_continues = bytes.fromhex(bytepatterns.stop_heart_measure_continues) + start_heart_measure_continues = bytes.fromhex(bytepatterns.start_heart_measure_continues) + stop_heart_measure_manual = bytes.fromhex(bytepatterns.stop_heart_measure_manual) + + fetch_begin = bytes.fromhex(bytepatterns.fetch_begin) + fetch_error = bytes.fromhex(bytepatterns.fetch_error) + fetch_continue = bytes.fromhex(bytepatterns.fetch_continue) + fetch_complete = bytes.fromhex(bytepatterns.fetch_complete) + + auth_ok = bytes.fromhex(bytepatterns.auth_ok) + request_random_number = bytes.fromhex(bytepatterns.request_random_number) + auth_key_prefix = bytes.fromhex(bytepatterns.auth_key_prefix) + \ No newline at end of file diff --git a/miband.py b/miband.py index 581d72f..dc69c97 100644 --- a/miband.py +++ b/miband.py @@ -3,6 +3,8 @@ import logging import struct import binascii +from bytepatterns import miband4 as bytepattern + from bluepy.btle import ( Peripheral, DefaultDelegate, ADDR_TYPE_RANDOM, ADDR_TYPE_PUBLIC, @@ -55,34 +57,7 @@ class Delegate(DefaultDelegate): print ("Unhandled handle: " + str(hnd) + " | Data: " + str(data)) -class bytepattern(): - def vibration(duration): - byte_pattern = 'ff{:02x}00000001' - return bytes.fromhex(byte_pattern.format(duration)) - def gyro_start(sensitivity): - byte_pattern = '01{:02x}19' - return bytes.fromhex(byte_pattern.format(sensitivity)) - - start = bytes.fromhex('0100') - stop = bytes.fromhex('0000') - - heart_measure_keepalive = bytes.fromhex('16') - stop_heart_measure_continues = bytes.fromhex('150100') - start_heart_measure_continues = bytes.fromhex('150101') - stop_heart_measure_manual = bytes.fromhex('150200') - - fetch_begin = bytes.fromhex('100101') - fetch_error = bytes.fromhex('100104') - - fetch_continue = bytes.fromhex('100201') - fetch_complete = bytes.fromhex('100204') - - auth_ok = bytes.fromhex('100301') - - request_random_number = bytes.fromhex('0200') - auth_key_prefix = bytes.fromhex('0300') - class miband(Peripheral): diff --git a/sleepdata.py b/sleepdata.py index 7a42b7a..58f3058 100644 --- a/sleepdata.py +++ b/sleepdata.py @@ -1,24 +1,236 @@ -class Sleep_Data(object): - def __init__(self): - print("init") +from datetime import datetime +from os import path +import csv + +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +#Todo: separate graph animation from data averaging +#Todo: log raw data separately from average data + + +sleep_data = { + 'heartrate': { + 'value_name': 'bpm', + 'periods': [2, 5, 10, 15], + 'raw_data': [], + 'averaged_data': [], + }, + 'movement':{ + 'value_name': 'movement', + 'periods': [10, 30, 60], + 'raw_data': [], + 'averaged_data': [], + 'workspace': { + 'gyro_last_x' : 0, + 'gyro_last_y' : 0, + 'gyro_last_z' : 0 + } + } + } + +last_heartrate = 0 +last_tick_time = None +tick_seconds = 0.5 + +csv_filename = "sleep_data.csv" + +fieldnames = ['time'] +for data_type in sleep_data: + periods = sleep_data[data_type]['periods'] + for period in periods: + fieldnames.append(data_type + str(period)) + + +plt.style.use('dark_background') +graph_figure = plt.figure() +graph_axes = graph_figure.add_subplot(1, 1, 1) +graph_data = {} + + +def write_csv(data): + global fieldnames + global csv_filename + if not path.exists(csv_filename): + with open(csv_filename, 'w', newline='') as csvfile: + csv_writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + csv_writer.writeheader() + csv_writer.writerow(data) + else: + with open(csv_filename, 'a', newline='') as csvfile: + csv_writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + csv_writer.writerow(data) + + +def flush_old_raw_data(tick_time): + for data_type in sleep_data: + s_data = sleep_data[data_type] + periods = s_data['periods'] + + cleaned_raw_data = [] - sleep_data = { - 'heartrate': { - 'value_name': 'bpm', - 'periods': [2, 5, 10, 15], - 'raw_data': [], - 'averaged_data': [], - }, - 'movement':{ - 'value_name': 'movement', - 'periods': [10, 30, 60], - 'raw_data': [], - 'averaged_data': [], - 'workspace': { - 'gyro_last_x' : 0, - 'gyro_last_y' : 0, - 'gyro_last_z' : 0 - } - } - } \ No newline at end of file + for raw_datum in s_data['raw_data']: + datum_age = tick_time - raw_datum['time'] + if datum_age < max(periods): + cleaned_raw_data.append(raw_datum) + + s_data['raw_data'] = cleaned_raw_data + +def write_csv(data): + a = '' + + +def average_raw_data(tick_time): + global last_heartrate + timestamp = datetime.fromtimestamp(tick_time) + csv_out = {'time': timestamp } + + for data_type in sleep_data: + s_data = sleep_data[data_type] + period_averages_dict = {'time': timestamp} + periods = s_data['periods'] + value_name = s_data['value_name'] + + flush_old_raw_data(tick_time) + + for period_seconds in periods: + period_data = [] + period_averages_dict[period_seconds] = 0 + for raw_datum in s_data['raw_data']: + datum_age_seconds = tick_time - raw_datum['time'] + if datum_age_seconds < period_seconds: + period_data.append(raw_datum[value_name]) + + if len(period_data) > 0: + period_data_average = sum(period_data) / len(period_data) + else: + print("({}) Period data empty: {}".format(data_type, + period_seconds)) + if data_type == "heartrate" and period_seconds == min(periods): + period_data_average = last_heartrate + else: + period_data_average = 0 + + period_averages_dict[period_seconds] = zero_to_nan(period_data_average) + + csv_out[data_type + str(period_seconds)] = zero_to_nan(period_data_average) + + s_data['averaged_data'].append(period_averages_dict) + write_csv(csv_out) + + + +def process_gyro_data(gyro_data, tick_time): + # Each gyro reading from miband4 comes over as a group of three, + # each containing x,y,z values. This function summarizes the + # values into a single consolidated movement value. + + sleep_move = sleep_data['movement'] + sleep_workspace = sleep_move['workspace'] + + gyro_last_x = sleep_workspace['gyro_last_x'] + gyro_last_y = sleep_workspace['gyro_last_y'] + gyro_last_z = sleep_workspace['gyro_last_z'] + value_name = sleep_move['value_name'] + gyro_movement = 0 + for gyro_datum in gyro_data: + gyro_delta_x = abs(gyro_datum['x'] - gyro_last_x) + gyro_last_x = gyro_datum['x'] + gyro_delta_y = abs(gyro_datum['y'] - gyro_last_y) + gyro_last_y = gyro_datum['y'] + gyro_delta_z = abs(gyro_datum['z'] - gyro_last_z) + gyro_last_z = gyro_datum['z'] + gyro_delta_sum = gyro_delta_x + gyro_delta_y + gyro_delta_z + gyro_movement += gyro_delta_sum + + sleep_workspace['gyro_last_x'] = gyro_last_x + sleep_workspace['gyro_last_y'] = gyro_last_y + sleep_workspace['gyro_last_z'] = gyro_last_z + + sleep_move['raw_data'].append({ + 'time': tick_time, + value_name: gyro_movement + }) + + +def process_heartrate_data(heartrate_data, tick_time): + print("BPM: " + str(heartrate_data)) + if heartrate_data > 0: + value_name = sleep_data['heartrate']['value_name'] + sleep_data['heartrate']['raw_data'].append({ + 'time': tick_time, + value_name: heartrate_data + } ) + +def zero_to_nan(value): + if value == 0: + return (float('nan')) + return int(value) + +def update_graph_data(): + for data_type in sleep_data: + s_data = sleep_data[data_type] # Re-referenced to shorten name + avg_data = s_data['averaged_data'] + + if len(avg_data) > 1: + + g_data = graph_data[data_type] # Re-referenced to short name + data_periods = s_data['periods'] + + starting_index = max([(len(g_data['time']) - 1), 0]) + ending_index = len(avg_data) - 1 + + # Re-referenced to shorten name + sleep_data_range = avg_data[starting_index:ending_index] + + for sleep_datum in sleep_data_range: + g_data['time'].append(sleep_datum['time']) + for period in data_periods: + if g_data['data'][period] != 'nan': + g_data['data'][period].append(sleep_datum[period]) + +def init_graph_data(): + for data_type in sleep_data: + data_periods = sleep_data[data_type]['periods'] + graph_data[data_type] = { + 'time': [], + 'data': {} + } + for period in data_periods: + graph_data[data_type]['data'][period] = [] + + +def graph_animation(i): + global graph_axes + global graph_data + plotflag = False + + if len(graph_data) == 0: + init_graph_data() + + update_graph_data() + + for data_type in graph_data: + if len(graph_data[data_type]['time']) > 0: + graph_axes.clear() + break + + for data_type in sleep_data: + s_data = sleep_data[data_type] + g_data = graph_data[data_type] + if len(g_data['time']) > 0: + plotflag = True + data_periods = sleep_data[data_type]['periods'] + for period in data_periods: + axis_label = "{} {} sec".format(s_data['value_name'], period) + graph_axes.plot(g_data['time'], + g_data['data'][period], + label=axis_label) + + if plotflag: + plt.legend() + +def init_graph(): + ani = animation.FuncAnimation(graph_figure, graph_animation, interval=1000) + plt.show()