Add ability to connect to hear rate sensor and post data back to server on workout complete, currently not rendering graphing data
This commit is contained in:
25
app.py
25
app.py
@@ -68,6 +68,15 @@ class CadenceReading(db.Model):
|
|||||||
power = db.Column(db.Integer, nullable=False)
|
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'])
|
@app.route('/', methods=['GET'])
|
||||||
def get_workouts():
|
def get_workouts():
|
||||||
return render_users_and_workouts()
|
return render_users_and_workouts()
|
||||||
@@ -113,21 +122,31 @@ def workouts(user_id):
|
|||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
user = User.query.get(user_id)
|
user = User.query.get(user_id)
|
||||||
|
app.logger.info(f'Creating workout for user {user.name} ({user.id})')
|
||||||
|
|
||||||
data = request.json
|
data = request.json
|
||||||
data = data['workout']
|
workout = data['workout'] or []
|
||||||
|
heart_rate = data['heart_rate'] or []
|
||||||
|
|
||||||
# create a new workout
|
# create a new workout
|
||||||
workout = Workout(user_id=user_id, bike_id=user.bike_id)
|
workout = Workout(user_id=user_id, bike_id=user.bike_id)
|
||||||
db.session.add(workout)
|
db.session.add(workout)
|
||||||
db.session.commit()
|
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
|
# add cadence readings to the workout
|
||||||
for d in data:
|
for w in workout:
|
||||||
cadence_reading = CadenceReading(
|
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)
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({'message': 'Workout created successfully.'}), 201
|
return jsonify({'message': 'Workout created successfully.'}), 201
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ CREATE TABLE
|
|||||||
CONSTRAINT unique_cadence_reading_per_workout_time UNIQUE (workout_id, created_at)
|
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
|
CREATE TABLE
|
||||||
bikes (
|
bikes (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|||||||
@@ -29,7 +29,19 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
<button>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve"
|
||||||
|
class="inline w-12 h-12 text-red-800 fill-current" id="heart_rate_button"
|
||||||
|
viewBox="0 0 33.087 33.087">
|
||||||
|
<path
|
||||||
|
d="M6.802 13.924h.007c.25.003.477.15.58.379l1.482 3.288 1.142-1.517a.617.617 0 0 1 .559-.256c.217.015.412.14.518.33l1.939 3.497 1.606-5.947a.643.643 0 0 1 1.192-.13l1.244 2.377 2.316-4.499a.658.658 0 0 1 .666-.344.647.647 0 0 1 .54.521l1.254 6.762 2.903-2.826a.628.628 0 0 1 .574-.17c.206.04.381.182.466.376l.859 1.988 1.407-4.622c.06-.193.215-.314.396-.383.013-.384.029-.766-.012-1.157-.698-6.77-8.258-6.665-11.898-3.399-3.64-3.266-11.195-3.37-11.897 3.399-.164 1.58.133 3.094.711 4.517l.865-1.817a.64.64 0 0 1 .581-.367z">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
d="M21.883 20.146a.643.643 0 0 1-1.083-.345l-1.13-6.092-2.023 3.927a.642.642 0 0 1-.568.349h-.001a.647.647 0 0 1-.571-.345l-1.058-2.018-1.591 5.893a.647.647 0 0 1-.621.476.64.64 0 0 1-.562-.332l-2.229-4.019-1.2 1.593a.642.642 0 0 1-1.101-.124L6.787 16.1l-.711 1.494c3.192 5.625 10.468 9.468 10.468 9.468s5.849-3.095 9.359-7.791l-.925-2.138-3.095 3.013zM4.637 17.621l-1.6-1.185a.641.641 0 0 0-.621-.083l-2.009.797a.643.643 0 1 0 .473 1.196l1.678-.664 1.94 1.436a.642.642 0 0 0 .964-.24l.612-1.286a12.902 12.902 0 0 1-.719-1.484l-.718 1.513zM32.441 17.105h-1.912l-1.242-3.977a.64.64 0 0 0-.614-.451h-.003c-.081 0-.146.044-.217.072-.076 2.377-1.104 4.584-2.55 6.521l.262.604c.106.248.353.419.627.387a.647.647 0 0 0 .58-.455l1.309-4.303.763 2.438a.64.64 0 0 0 .614.451h2.385a.645.645 0 0 0 .645-.645.646.646 0 0 0-.647-.642z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,6 +127,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const integerNumber = (num) => parseInt(num);
|
const integerNumber = (num) => parseInt(num);
|
||||||
const decimalNumber = (num) => parseFloat(num.toFixed(1));
|
const decimalNumber = (num) => parseFloat(num.toFixed(1));
|
||||||
|
|
||||||
@@ -167,7 +181,10 @@
|
|||||||
isRunning = false;
|
isRunning = false;
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
toggleButton.textContent = 'Start';
|
toggleButton.textContent = 'Start';
|
||||||
|
// Disconnect from cadence sensor
|
||||||
disconnect();
|
disconnect();
|
||||||
|
// Disconnect from heart rate sensor
|
||||||
|
disconnectHeartRateMonitor();
|
||||||
|
|
||||||
fetch("{{ url_for('workouts', user_id=user.id) }}", {
|
fetch("{{ url_for('workouts', user_id=user.id) }}", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -175,7 +192,7 @@
|
|||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "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 => res.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
workout = [];
|
workout = [];
|
||||||
@@ -283,10 +300,10 @@
|
|||||||
if (characteristic) {
|
if (characteristic) {
|
||||||
try {
|
try {
|
||||||
await characteristic.stopNotifications();
|
await characteristic.stopNotifications();
|
||||||
log("> Notifications stopped");
|
console.log("> Notifications stopped");
|
||||||
characteristic.removeEventListener(
|
characteristic.removeEventListener(
|
||||||
"characteristicvaluechanged",
|
"characteristicvaluechanged",
|
||||||
handleNotifications
|
parseCSC
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Argh! " + error);
|
console.log("Argh! " + error);
|
||||||
@@ -391,6 +408,94 @@
|
|||||||
return (energy * 0.238902957619) / 1000;
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user