Beta: Switch to using static site instead(doesnt yet post data back to server)
This commit is contained in:
10
app.py
10
app.py
@@ -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
302
templates/attemptv2.html
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user