Moved data handling outside of main script into sleepdata module

This commit is contained in:
Nate Schoolfield 2021-01-30 18:54:23 -08:00
parent 8435b2bd69
commit 7bf0e8539d
4 changed files with 306 additions and 309 deletions

View File

@ -4,12 +4,7 @@ from bluepy import btle
from bluepy.btle import BTLEDisconnectError from bluepy.btle import BTLEDisconnectError
from miband import miband from miband import miband
import sleepdata
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import csv
import random
from os import path
import threading import threading
import re import re
@ -18,69 +13,23 @@ import subprocess
import time import time
from datetime import datetime 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' auth_key_filename = 'auth_key.txt'
mac_filename = 'mac.txt' mac_filename = 'mac.txt'
csv_filename = "sleep_data.csv" csv_filename = "sleep_data.csv"
plt.style.use('dark_background') band = None
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))
#-------------------------------------------------------------------------# #-------------------------------------------------------------------------#
class regex_patterns():
def write_csv(data): mac_regex_pattern = re.compile(r'([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})')
global fieldnames authkey_regex_pattern = re.compile(r'([0-9a-fA-F]){32}')
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 get_mac_address(filename): def get_mac_address(filename):
mac_regex_pattern = re.compile(r'([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})')
try: try:
with open(filename, "r") as f: 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: if hwaddr_search:
MAC_ADDR = hwaddr_search[0] MAC_ADDR = hwaddr_search[0]
else: else:
@ -93,10 +42,9 @@ def get_mac_address(filename):
def get_auth_key(filename): def get_auth_key(filename):
authkey_regex_pattern = re.compile(r'([0-9a-fA-F]){32}')
try: try:
with open(filename, "r") as f: 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: if key_search:
AUTH_KEY = bytes.fromhex(key_search[0]) AUTH_KEY = bytes.fromhex(key_search[0])
else: else:
@ -107,210 +55,28 @@ def get_auth_key(filename):
exit(1) exit(1)
return AUTH_KEY return AUTH_KEY
def process_data(data, tick_time):
def process_heartrate_data(heartrate_data, tick_time): if data[0] == "GYRO":
print("BPM: " + str(heartrate_data)) sleepdata.process_gyro_data(data[1], tick_time)
if heartrate_data > 0: elif data[0] == "HR":
value_name = sleep_data['heartrate']['value_name'] sleepdata.process_heartrate_data(data[1], tick_time)
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 sleep_monitor_callback(data): def sleep_monitor_callback(data):
global sleep_data
global last_tick_time
tick_time = time.time() tick_time = time.time()
if not last_tick_time: if not sleepdata.last_tick_time:
last_tick_time = time.time() sleepdata.last_tick_time = time.time()
if data[0] == "GYRO": process_data(data, tick_time)
process_gyro_data(data[1], tick_time)
elif data[0] == "HR":
process_heartrate_data(data[1], tick_time)
if (tick_time - last_tick_time) >= tick_seconds: if (tick_time - sleepdata.last_tick_time) >= sleepdata.tick_seconds:
average_raw_data(tick_time) sleepdata.average_raw_data(tick_time)
last_tick_time = time.time() sleepdata.last_tick_time = time.time()
def connect(mac_filename, auth_key_filename):
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():
global band global band
global mac_filename
global auth_key_filename
success = False success = False
timeout = 3 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) MAC_ADDR = get_mac_address(mac_filename)
AUTH_KEY = get_auth_key(auth_key_filename) AUTH_KEY = get_auth_key(auth_key_filename)
@ -326,10 +92,7 @@ def connect():
print("\nExit.") print("\nExit.")
exit() exit()
def start_data_pull(): def start_data_pull():
global band
while True: while True:
try: try:
band.start_heart_and_gyro(sensitivity=1, callback=sleep_monitor_callback) band.start_heart_and_gyro(sensitivity=1, callback=sleep_monitor_callback)
@ -357,6 +120,7 @@ def vibrate_pattern(duration):
time.sleep(vibro_delay) time.sleep(vibro_delay)
def vibrate_rolling(): def vibrate_rolling():
print("Sending rolling vibration...")
for x in range(10): for x in range(10):
for x in range(20, 40, 1): for x in range(20, 40, 1):
band.vibrate(x) band.vibrate(x)
@ -364,11 +128,12 @@ def vibrate_rolling():
band.vibrate(x) band.vibrate(x)
if __name__ == "__main__": 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 = threading.Thread(target=start_data_pull)
data_gather_thread.start() data_gather_thread.start()
ani = animation.FuncAnimation(graph_figure, graph_animation, interval=1000) sleepdata.init_graph()
plt.show()
#import simpleaudio as sa #import simpleaudio as sa

45
bytepatterns.py Normal file
View File

@ -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)

View File

@ -3,6 +3,8 @@ import logging
import struct import struct
import binascii import binascii
from bytepatterns import miband4 as bytepattern
from bluepy.btle import ( from bluepy.btle import (
Peripheral, DefaultDelegate, Peripheral, DefaultDelegate,
ADDR_TYPE_RANDOM, ADDR_TYPE_PUBLIC, ADDR_TYPE_RANDOM, ADDR_TYPE_PUBLIC,
@ -55,33 +57,6 @@ class Delegate(DefaultDelegate):
print ("Unhandled handle: " + str(hnd) + " | Data: " + str(data)) 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')

View File

@ -1,6 +1,13 @@
class Sleep_Data(object): from datetime import datetime
def __init__(self): from os import path
print("init")
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 = { sleep_data = {
@ -22,3 +29,208 @@ class Sleep_Data(object):
} }
} }
} }
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 = []
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()