From 43e5f66cc15de07d23f3e1de0db08e88579569bf Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sun, 7 May 2023 18:15:31 +1000 Subject: [PATCH] Add ability to connect to hear rate sensor and post data back to server on workout complete, currently not rendering graphing data --- app.py | 25 +++++++- database/001_create_tables.sql | 8 +++ templates/new_workout.html | 111 ++++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index 4f7f965..9da9ca1 100644 --- a/app.py +++ b/app.py @@ -68,6 +68,15 @@ class CadenceReading(db.Model): power = db.Column(db.Integer, nullable=False) +class HeartRateReading(db.Model): + __tablename__ = 'heartrate_readings' + id = db.Column(db.Integer, primary_key=True) + workout_id = db.Column(db.Integer, db.ForeignKey( + 'workouts.id', ondelete='CASCADE'), nullable=False) + created_at = db.Column(db.DateTime, nullable=False) + bpm = db.Column(db.Integer, nullable=False) + + @app.route('/', methods=['GET']) def get_workouts(): return render_users_and_workouts() @@ -113,21 +122,31 @@ def workouts(user_id): elif request.method == 'POST': user = User.query.get(user_id) + app.logger.info(f'Creating workout for user {user.name} ({user.id})') data = request.json - data = data['workout'] + workout = data['workout'] or [] + heart_rate = data['heart_rate'] or [] # create a new workout workout = Workout(user_id=user_id, bike_id=user.bike_id) db.session.add(workout) db.session.commit() + app.logger.info( + f'Workout({workout.id}) created for user {user.name} ({user.id}) with {len(workout)} cadence readings and {len(heart_rate)} heart rate readings') + # add cadence readings to the workout - for d in data: + for w in workout: cadence_reading = CadenceReading( - workout_id=workout.id, created_at=d['timestamp'], rpm=d['rpm'], distance=d['distance'], speed=d['speed'], calories=d['calories'], power=d['power']) + workout_id=workout.id, created_at=w['timestamp'], rpm=w['rpm'], distance=w['distance'], speed=w['speed'], calories=w['calories'], power=w['power']) db.session.add(cadence_reading) + for h in heart_rate: + heart_rate_reading = HeartRateReading( + workout_id=workout.id, created_at=h['timestamp'], bpm=h['bpm']) + db.session.add(heart_rate_reading) + db.session.commit() return jsonify({'message': 'Workout created successfully.'}), 201 diff --git a/database/001_create_tables.sql b/database/001_create_tables.sql index 1510077..4518881 100644 --- a/database/001_create_tables.sql +++ b/database/001_create_tables.sql @@ -25,6 +25,14 @@ CREATE TABLE CONSTRAINT unique_cadence_reading_per_workout_time UNIQUE (workout_id, created_at) ); +CREATE TABLE + heartrate_readings ( + id SERIAL PRIMARY KEY, + workout_id INTEGER NOT NULL REFERENCES workouts (id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL, + bpm INTEGER NOT NULL + ); + CREATE TABLE bikes ( id SERIAL PRIMARY KEY, diff --git a/templates/new_workout.html b/templates/new_workout.html index cc1ccb1..b034e4b 100644 --- a/templates/new_workout.html +++ b/templates/new_workout.html @@ -29,7 +29,19 @@
+
@@ -115,6 +127,8 @@ } }); + + const integerNumber = (num) => parseInt(num); const decimalNumber = (num) => parseFloat(num.toFixed(1)); @@ -167,7 +181,10 @@ isRunning = false; clearInterval(intervalId); toggleButton.textContent = 'Start'; + // Disconnect from cadence sensor disconnect(); + // Disconnect from heart rate sensor + disconnectHeartRateMonitor(); fetch("{{ url_for('workouts', user_id=user.id) }}", { method: "POST", @@ -175,7 +192,7 @@ Accept: "application/json", "Content-Type": "application/json", }, - body: JSON.stringify({ workout: workout }), + body: JSON.stringify({ workout: workout, heart_rate: heartRateData }), }).then(res => res.json()) .then(res => { workout = []; @@ -283,10 +300,10 @@ if (characteristic) { try { await characteristic.stopNotifications(); - log("> Notifications stopped"); + console.log("> Notifications stopped"); characteristic.removeEventListener( "characteristicvaluechanged", - handleNotifications + parseCSC ); } catch (error) { console.log("Argh! " + error); @@ -391,6 +408,94 @@ return (energy * 0.238902957619) / 1000; } + // Heart Rate monitor code + const heartRateButton = document.getElementById('heart_rate_button'); + + let isHearRateMonitorActive = false; + let heartRateCharateristic = null; + let heartRateData = []; + + heartRateButton.addEventListener('click', async () => { + console.log('heartRateButton clicked, isHearRateMonitorActive: ', isHearRateMonitorActive); + if (isHearRateMonitorActive) { + console.log('Stopping heart rate monitor...'); + await disconnectHeartRateMonitor(); + + isHearRateMonitorActive = false; + heartRateButton.classList.remove('text-red-800'); + heartRateButton.classList.add('text-gray-700'); + } else { + console.log('Starting heart rate monitor...'); + await heartRateMonitorConnect(); + + isHearRateMonitorActive = true; + heartRateButton.classList.remove('text-gray-700'); + heartRateButton.classList.add('text-red-800'); + } + }); + + function parseHeartRate(value) { + const flags = value.getUint8(0); + const rate16Bits = flags & 0x1; + let heartRate; + + if (rate16Bits) { + heartRate = value.getUint16(1, /*littleEndian=*/ true); + } else { + heartRate = value.getUint8(1); + } + + return heartRate; + } + + + function heartRateChange(event) { + const currentHeartRate = parseHeartRate(event.target.value); + heartRateData = [...heartRateData, { timestamp: new Date(), heartRate: currentHeartRate }]; + console.log('currentHeartRate:', currentHeartRate); + } + + function heartRateMonitorConnect() { + return navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) + .then(device => { + return device.gatt.connect(); + }) + .then(server => { + return server.getPrimaryService('heart_rate') + }) + .then(service => { + return service.getCharacteristic('heart_rate_measurement') + }) + .then(character => { + heartRateCharateristic = character; + return heartRateCharateristic.startNotifications().then(_ => { + heartRateCharateristic.addEventListener('characteristicvaluechanged', heartRateChange); + }) + .catch(e => console.error(e)); + }) + }; + + async function disconnectHeartRateMonitor() { + if (heartRateCharateristic) { + try { + await heartRateCharateristic.stopNotifications(); + console.log("> Notifications stopped"); + heartRateCharateristic.removeEventListener( + "characteristicvaluechanged", heartRateChange); + + // Disconnect from GATT server + const device = heartRateCharateristic.service.device; + if (device.gatt.connected) { + device.gatt.disconnect(); + console.log('Disconnected from heart rate monitor'); + } + } catch (error) { + console.log("Argh! " + error); + swal("Oops", error, "error"); + } + } + } +