Beta: Switch to using static site instead(doesnt yet post data back to server)

This commit is contained in:
Peter Stockings
2023-03-07 23:45:03 +11:00
parent 5c1a3acec3
commit a1a14b5aaf
3 changed files with 312 additions and 3 deletions

10
app.py
View File

@@ -1,4 +1,5 @@
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
import decimal
import json import json
from urllib import response from urllib import response
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
@@ -37,7 +38,7 @@ def response_minify(response):
@ app.route("/") @ app.route("/")
def home(): def home():
return render_template('base.html') return render_template('attemptv2.html')
@ app.route("/devices") @ app.route("/devices")
@@ -63,11 +64,14 @@ def overview(device_id):
last_cadence = cadences[-1]['rpm'] last_cadence = cadences[-1]['rpm']
power = round(decimal.Decimal(0.0011)*last_cadence ** 3 + decimal.Decimal(
0.0026) * last_cadence ** 2 + decimal.Decimal(0.5642)*last_cadence)
graph_data = generate_sparkline_graph( graph_data = generate_sparkline_graph(
[c['rpm'] for c in cadences[-100:]]) [c['rpm'] for c in cadences[-100:]])
return render_template('overview.html', last_cadence=last_cadence, duration=duration, cadences=cadences[-15:], graph_data=graph_data) return render_template('overview.html', last_cadence=last_cadence, power=power, duration=duration, cadences=cadences[-15:], graph_data=graph_data)
return render_template('overview.html', last_cadence=0, duration=duration, cadences=[], graph_data='') return render_template('overview.html', last_cadence=0, power=0, duration=duration, cadences=[], graph_data='')
@ app.route("/cadence", methods=['POST']) @ app.route("/cadence", methods=['POST'])

302
templates/attemptv2.html Normal file
View File

@@ -0,0 +1,302 @@
<!DOCTYPE html>
<html>
<head>
<title>Exercise Bike Display</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
<style>
.graph-container {
width: 100%;
height: 200px;
}
#graph {
background-color: #ffffff;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body class="bg-gray-200">
<div class="mx-auto max-w-md p-6 bg-white rounded-md shadow-md md:mt-5">
<div class="mt-6">
<div class="flex items-center justify-between">
<div>
<p class="text-lg font-medium">RPM</p>
<p id="rpm-display" class="text-3xl font-bold">0</p>
</div>
<div>
<p class="text-lg font-medium">Power</p>
<p id="power-display" class="text-3xl font-bold">0</p>
</div>
<div>
<p class="text-lg font-medium">Duration</p>
<p id="duration-display" class="text-3xl font-bold">00:00</p>
</div>
</div>
<div class="mt-8">
<button id="toggle-button"
class="px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600">Start</button>
</div>
<div class="mt-8">
<div class="flex justify-center items-center bg-gray-200">
<svg id="graph" class="bg-white shadow-md"></svg>
</div>
</div>
</div>
</div>
<script>
// Get DOM elements
const rpmDisplay = document.getElementById('rpm-display');
const powerDisplay = document.getElementById('power-display');
const durationDisplay = document.getElementById('duration-display');
const toggleButton = document.getElementById('toggle-button');
// Initialize variables
let rpm = 0;
let power = 0;
let duration = 0;
let isRunning = false;
let startTime = 0;
let intervalId = null;
// Update RPM and Power
function updateRpmPower(rpm, power) {
// Update displays
rpmDisplay.textContent = rpm;
powerDisplay.textContent = power;
const newData = {
x: data.length,
y: rpm,
};
// Add the new data point to the data array
data.push(newData);
}
// Update Duration
function updateDuration() {
duration++;
// Format duration as mm:ss
const minutes = Math.floor(duration / 60).toString().padStart(2, '0');
const seconds = (duration % 60).toString().padStart(2, '0');
durationDisplay.textContent = `${minutes}:${seconds}`;
}
// Start workout
function startWorkout() {
isRunning = true;
startTime = Date.now();
intervalId = setInterval(() => {
updateDuration();
}, 1000);
// Connect to cadence sensor
connect({
onChange: parseCSC,
}).catch((err) => {
swal("Oops", err.message, "error");
});
toggleButton.textContent = 'Stop';
}
// Stop workout
function stopWorkout() {
isRunning = false;
clearInterval(intervalId);
toggleButton.textContent = 'Start';
disconnect();
}
// Toggle workout
function toggleWorkout() {
if (isRunning) {
stopWorkout();
} else {
startWorkout();
}
}
// Attach event listener to toggle button
toggleButton.addEventListener('click', toggleWorkout);
// Set up the live plot
const svg = d3.select('#graph');
// Set the SVG viewBox and dimensions
svg.attr('viewBox', '0 0 600 400');
svg.attr('width', '100%');
svg.attr('height', '100%');
// Set up the x-axis and y-axis scales
const xScale = d3.scaleLinear().range([50, 550]);
const yScale = d3.scaleLinear().range([350, 50]);
// Set up the x-axis and y-axis
const xAxis = d3.axisBottom(xScale).tickSize(0).tickFormat('');
const yAxis = d3.axisLeft(yScale).tickSize(0).tickFormat('');
// Add the x-axis and y-axis to the SVG
svg.append('g')
.attr('transform', 'translate(0, 350)')
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(50, 0)')
.call(yAxis);
// Set up the line function
const line = d3.line()
.x((d) => xScale(d.x))
.y((d) => yScale(d.y))
.curve(d3.curveCardinal);
// Create an empty data array
let data = [];
// Set up the graph update function
const updateGraph = () => {
// Update the x-axis and y-axis scales
xScale.domain([0, data.length]);
yScale.domain([0, d3.max(data, d => d.y)]);
// Remove the old line path from the SVG
svg.selectAll('path').remove();
// Add a new line path to the SVG with the updated data
svg.append('path')
.datum(data)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('stroke-width', '2');
};
// Call the updateGraph function to start the live updates
updateGraph();
</script>
<script>
let characteristic = null;
let prevRes = null;
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function connect(props) {
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ["cycling_speed_and_cadence"] }],
acceptAllDevices: false,
});
console.log(`%c\n👩🏼‍⚕️`, "font-size: 82px;", "Starting CSC...\n\n");
const server = await device.gatt.connect();
await delay(500);
console.log("Getting Service...");
const service = await server.getPrimaryService("cycling_speed_and_cadence");
console.log("Getting Characteristic...");
characteristic = await service.getCharacteristic("csc_measurement");
await characteristic.startNotifications();
console.log("> Notifications started");
characteristic.addEventListener("characteristicvaluechanged", props.onChange);
console.log("> Characteristic value changed event listener added");
btn.classList.remove("bg-blue-600");
btn.classList.add("bg-green-600");
}
async function disconnect() {
if (characteristic) {
try {
await characteristic.stopNotifications();
log("> Notifications stopped");
characteristic.removeEventListener(
"characteristicvaluechanged",
handleNotifications
);
btn.classList.remove("bg-green-600");
btn.classList.add("bg-blue-600");
} catch (error) {
console.log("Argh! " + error);
swal("Oops", error, "error");
}
}
}
function parseCSC(e) {
const value = e.target.value;
const flags = value.getUint8(0, true);
const hasWheel = !!(flags & 0x01);
const hasCrank = !!(flags & 0x02);
let index = 1;
const res = {
wheelRevs: null,
wheelTime: null,
crankRevs: null,
crankTime: null,
};
if (hasWheel) {
res.wheelRevs = value.getUint32(index, true);
index += 4;
res.wheelTime = value.getUint16(index, true);
index += 2;
}
if (hasCrank) {
res.crankRevs = value.getUint16(index, true);
index += 2;
res.crankTime = value.getUint16(index, true);
index += 2;
}
console.log("CSC", res);
if (prevRes) {
let rpm = revsToRPM(prevRes, res);
if (rpm > 0) {
updateRpmPower(rpm, 0);
updateGraph();
/*
fetch("/cadence", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ rpm: parseFloat(rpm.toFixed(2)), id: 1 }),
});
*/
}
}
prevRes = res;
return res;
}
function revsToRPM(prevRes, res) {
const deltaRevs = res.crankRevs - prevRes.crankRevs;
if (deltaRevs === 0) {
// no rotation
return 0;
}
let deltaTime = (res.crankTime - prevRes.crankTime) / 1024;
if (deltaTime < 0) {
// time counter wraparound
deltaTime += Math.pow(2, 16) / 1024;
}
deltaTime /= 60; // seconds to minutes
const rpm = deltaRevs / deltaTime;
return rpm;
}
</script>
</body>
</html>

View File

@@ -1,6 +1,9 @@
<h1 <h1
class="mb-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white text-center"> class="mb-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white text-center">
{{last_cadence}} rpm</h1> {{last_cadence}} rpm</h1>
<h1
class="mb-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white text-center">
{{power}} Watts</h1>
<h3 class="mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-700 md:text-3xl lg:text-3xl dark:text-white text-center"> <h3 class="mb-4 text-3xl font-extrabold leading-none tracking-tight text-gray-700 md:text-3xl lg:text-3xl dark:text-white text-center">
{{duration}}</h3> {{duration}}</h3>