Compare commits

..

1 Commits

Author SHA1 Message Date
NateSchoolfield a94b699bbd First commit 2021-02-10 14:58:37 -08:00
11 changed files with 177 additions and 82 deletions

8
.gitignore vendored
View File

@ -1,6 +1,5 @@
mac*.txt
auth_key*.txt
*.sh
mac.txt
auth_key.txt
*.pyc
/__pycache__
*.swp
@ -9,6 +8,3 @@ auth_key*.txt
*.code-workspace
/.vscode
/data
datafilter.py
crymonitor.py

11
README
View File

@ -10,8 +10,10 @@ You will need to create two .txt files in the base directory:
CURRENT STATUS:
The project now supports basic "heartrate alarming". Currently it's configured to send a 10-second random vibration pattern if the heartrate increase percentage goes above 17% of the lowest HR value for the last 10 readings, with a 20-minute delay between vibrations. This was tested on the Miband5, which has a better HR sensor, though I've not yet fully figured out the gyroscope data. Right now only HR and vibration are supported on the Miband5.
Right now the project does not yet fulfill its primary purpose (responding to biometrics).
If executed now, it will:
* Log raw and averaged biometric data to disk
* Vibrate in a random pattern every 30 minutes (adjustable)
GOALS:
* Publish statistics to Google Sheets or other easy-to-use target. I'd like to make this usable by non-technical folks, so S3 buckets and CloudWatch are out of scope.
@ -22,4 +24,7 @@ GOALS:
DISCLAIMER:
None of the statements on this web site have been evaluated by the FDA. Furthermore, none of the statements herein should be construed as dispensing medical advice, or making claims regarding the cure or treatment of diseases. These statements have not been evaluated by the Food and Drug Administration. This project is not intended to diagnose, treat, cure, or prevent any diseases.
None of the statements on this web site have been evaluated by the FDA.
Furthermore, none of the statements herein should be construed as dispensing medical advice, or making claims regarding the cure or treatment of diseases.
These statements have not been evaluated by the Food and Drug Administration.
These project is not intended to diagnose, treat, cure, or prevent any diseases.

1
auth_key_miband4.txt Normal file
View File

@ -0,0 +1 @@
c04bc1eb2efaea8efc20882624b9a0f6

View File

@ -15,10 +15,9 @@ mac_filename = 'mac.txt'
maximize_graph = False
vibration_settings = {
'interval_minutes': 20,
'duration_seconds': 10,
'type': 'random',
'heartrate_alarm_pct': 17
'interval_minutes': 45,
'duration_seconds': 5,
'type': 'random'
}
band = None
@ -66,7 +65,7 @@ def average_data(tick_time):
sleepdata.average_raw_data(tick_time)
sleepdata.last_tick_time = time.time()
def sleep_monitor_callback(data):
tick_time = time.time()
@ -80,9 +79,6 @@ def sleep_monitor_callback(data):
average_data(tick_time)
vibration.heartrate_increase_pct = sleepdata.analyze_heartrate(10)
print("HR increase percent: {}".format(vibration.heartrate_increase_pct))
def connect():
global band
@ -117,9 +113,7 @@ def start_data_pull():
def start_vibration():
while True:
try:
#vibration.timed_vibration(vibration_settings)
vibration.heartrate_alarm(vibration_settings)
vibration.timed_vibration(vibration_settings)
except BTLEDisconnectError:
print("Vibration thread waiting for band reconnect...")
time.sleep(1)

63
crymonitor.py Normal file
View File

@ -0,0 +1,63 @@
import pyaudio, time, threading
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.animation as animation
CHUNKSIZE = 1024 # fixed chunk size
# initialize portaudio
p = pyaudio.PyAudio()
info = p.get_host_api_info_by_index(0)
numdevices = info.get('deviceCount')
for i in range(0, numdevices):
if (p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0:
print("Input Device id ", i, " - ", p.get_device_info_by_host_api_device_index(0, i).get('name'))
stream = p.open(
format=pyaudio.paInt16,
channels=1,
rate=44100,
input=True,
frames_per_buffer=CHUNKSIZE,
input_device_index=18
)
plt.style.use('dark_background')
graph_figure = plt.figure()
graph_figure.canvas.set_window_title('blesleep')
graph_axes = graph_figure.add_subplot(1, 1, 1)
graph_data = {}
def graph_animation(i):
graph_axes.clear()
graph_axes.plot(numpydata)
ani = animation.FuncAnimation(graph_figure, graph_animation, interval=1000)
def get_audio_data():
global numpydata
while True:
data = stream.read(CHUNKSIZE)
numpydata = np.frombuffer(data, dtype=np.int16)
time.sleep(1)
threading.Thread(target=get_audio_data).start()
plt.show()
# close stream
stream.stop_stream()
stream.close()
p.terminate()

76
datafilter.py Normal file
View File

@ -0,0 +1,76 @@
import time, os, csv
from datetime import datetime
from matplotlib import pyplot as plt
datapath = '/home/daddy/Projects/miband/data/2021_02_07/'
window_min = 30
figure_height = 9
figure_width = 18
fullscreen = False
files = os.listdir(datapath)
wavs = []
csvs = []
for file in files:
if 'wav' in file:
wavs.append(file)
elif 'csv' in file and 'raw' in file:
csvs.append(file)
event_data_list = []
for wav in wavs:
event_dict = {
'name': wav,
'data': {
'mov_x': [],
'mov_y': [],
'bpm_x': [],
'bpm_y': []
}
}
wavtime = datetime.strptime(wav, '%Y_%m_%d__%H_%M_%S.wav')
wavestamp = wavtime.timestamp()
plotdata_mov_x = []
plotdata_mov_y = []
plotdata_bpm_x = []
plotdata_bpm_y = []
for mycsv in csvs:
with open((datapath + mycsv), newline='') as csvfile:
csvreader = csv.DictReader(csvfile, delimiter=',')
for row in csvreader:
if abs((float(wavestamp) - float(row['time']))) <= (window_min * 60):
if 'bpm' in row:
event_dict['data']['bpm_x'].append( int(row['bpm']) )
event_dict['data']['bpm_y'].append( datetime.fromtimestamp(float(row['time'])) )
elif 'movement' in row:
event_dict['data']['mov_x'].append( int(row['movement']) )
event_dict['data']['mov_y'].append( datetime.fromtimestamp(float(row['time'])) )
event_data_list.append(event_dict)
for event_data in event_data_list:
event_name = event_data['name'].rsplit('.', 1)[0]
output_png_filename = '{}{}'.format(event_name, ".png")
data = event_data['data']
plt.close("all")
fig, ax = plt.subplots()
ax2 = ax.twinx()
ax.plot(data['bpm_y'], data['bpm_x'], color="red")
ax2.plot(data['mov_y'], data['mov_x'], color="blue")
fig.set_figheight(figure_height)
fig.set_figwidth(figure_width)
fig.autofmt_xdate()
if fullscreen:
plt.get_current_fig_manager().full_screen_toggle()
plt.title(event_name)
if data['bpm_y']:
plt.savefig(datapath + output_png_filename)
plt.show()

1
mac_miband4.txt Normal file
View File

@ -0,0 +1 @@
EB:DD:13:97:9C:0D

View File

@ -231,7 +231,7 @@ class miband(Peripheral):
def _parse_heart_measure(self, bytes):
res = struct.unpack('bb', bytes)[1]
return_tuple = ["HR", res]
#print("BPM: {}".format(res))
print("BPM: {}".format(res))
return return_tuple

View File

@ -12,7 +12,6 @@ sleep_data = {
'periods': [2, 5, 10, 15],
'raw_data': [],
'averaged_data': [],
'last_hr': []
},
'movement':{
'value_name': 'movement',
@ -174,7 +173,6 @@ def process_gyro_data(gyro_data, tick_time):
def process_heartrate_data(heartrate_data, tick_time):
last_heartrate_count = 20
print("BPM: " + str(heartrate_data))
if heartrate_data > 0:
value_name = sleep_data['heartrate']['value_name']
@ -183,21 +181,6 @@ def process_heartrate_data(heartrate_data, tick_time):
value_name: heartrate_data
} )
if len(sleep_data['heartrate']['last_hr']) > last_heartrate_count:
sleep_data['heartrate']['last_hr'].pop(0)
sleep_data['heartrate']['last_hr'].append(heartrate_data)
def analyze_heartrate(hr_count):
# Finds the pct change between the lowest HR in the last $hr_count samples and the current HR
pct_heartrate_increase = 0
if len(sleep_data['heartrate']['last_hr']) >= hr_count:
last_heartrate_list = sleep_data['heartrate']['last_hr'][-hr_count:]
last_heartrate_min = min(last_heartrate_list)
current_heartrate = last_heartrate_list[-1]
pct_heartrate_increase = int((current_heartrate - last_heartrate_min)/last_heartrate_min*100)
return pct_heartrate_increase
def zero_to_nan(value):
if value == 0:

19
test.py Normal file
View File

@ -0,0 +1,19 @@
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0, 10, 0.1)
y1 = 0.05 * x**2
y2 = -1 *y1
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
figures=[manager.canvas.figure
for manager in plt._pylab_helpers.Gcf.get_all_fig_managers()]
print(figures)
ax1.plot(x, y1, 'g-')
ax2.plot(x, y2, 'b-')
plt.show()

View File

@ -12,8 +12,6 @@ import logging
class Vibrate():
vibrate_band = None
vibration_log = None
heartrate_increase_pct = 0
def __init__(self, band):
self.vibrate_band = band
@ -24,53 +22,12 @@ class Vibrate():
self.vibration_log = logging.getLogger(__name__)
self.vibration_log.setLevel(vibration_log_level)
def heartrate_alarm(self, settings):
interval_minutes = settings['interval_minutes']
duration_seconds = settings['duration_seconds']
vibration_type = settings['type']
heartrate_alarm_pct = settings['heartrate_alarm_pct']
tick_time = time.time()
buzz_delay = interval_minutes * 60
buzz_timer = tick_time - buzz_delay
self.vibration_log.info("Starting heartrate alarm timer, alarming at {} percent for {} seconds with a {} minute interval".format(
heartrate_alarm_pct,
duration_seconds,
interval_minutes))
if vibration_type not in ['random', 'pattern', 'rolling', 'continuous']:
self.vibration_log.warn("Invalid or no vibration type specified: {}".format(type))
self.vibration_log.warn("Must be one of these: random, pattern, rolling, continuous")
return
while True:
elapsed_time = tick_time - buzz_timer
if elapsed_time >= buzz_delay and self.heartrate_increase_pct >= heartrate_alarm_pct:
self.vibration_log.info("Heartrate alarm triggered at {} percent, buzzing".format(self.heartrate_increase_pct))
if vibration_type == 'random':
self.vibrate_random(duration_seconds)
elif vibration_type == 'pattern':
self.vibrate_pattern(duration_seconds)
elif vibration_type == 'rolling':
self.vibrate_rolling(duration_seconds)
elif vibration_type == 'continuous':
self.vibrate_continuous(duration_seconds)
buzz_timer = tick_time
elif not elapsed_time >= buzz_delay and self.heartrate_increase_pct >= heartrate_alarm_pct:
self.vibration_log.info("Heartrate alarm threshold reached ({} percent) but timout not expired".format(self.heartrate_increase_pct))
else:
tick_time = time.time()
time.sleep(0.5)
def timed_vibration(self, settings):
interval_minutes = settings['interval_minutes']
duration_seconds = settings['duration_seconds']
type = settings['type']
buzz_timer = time.time()
buzz_timer = time.time()
tick_time = time.time()
buzz_delay = interval_minutes * 60