Browse Source

first commit

master
Gisle Aune 5 years ago
commit
f47d8af9d3
  1. 4
      .gitignore
  2. 1573
      package-lock.json
  3. 21
      package.json
  4. 74
      src/api/bike.js
  5. 74
      src/api/program.js
  6. 178
      src/api/workout.js
  7. 10
      src/drivers/index.js
  8. 103
      src/drivers/mock.js
  9. 227
      src/repositories/sqlite3.js
  10. 20
      src/server.js
  11. 135
      src/systems/workout.js
  12. BIN
      stuff.db

4
.gitignore

@ -0,0 +1,4 @@
/node_modules
/.vscode
/.idea

1573
package-lock.json
File diff suppressed because it is too large
View File

21
package.json

@ -0,0 +1,21 @@
{
"name": "ykonsole-server",
"private": true,
"version": "0.1.0",
"description": "Monitoring tool using the bluetooth client.",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Gisle",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"iconsole-bike-client": "^0.1.3",
"sqlite3": "^4.1.0",
"underscore": "^1.9.1"
},
"devDependencies": {
"@types/express": "^4.17.1"
}
}

74
src/api/bike.js

@ -0,0 +1,74 @@
const express = require("express");
/**
* @param {import("../repositories/sqlite3")} repo
*/
module.exports = function bikeRouter(repo) {
const router = express.Router({caseSensitive: false});
router.get("/", async(_, res) => {
try {
const bikes = await repo.listBikes();
return res.status(200).json(bikes);
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.get("/:id", async(req, res) => {
try {
const bike = await repo.findBike(req.params.id);
if (bike == null) {
return res.status(404).json({code: 404, message: "bike not found"})
}
return res.status(200).json(bike);
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
router.post("/", async(req, res) => {
try {
const {name, driver, maxLevel, connect} = req.body;
const id = await repo.insertBike({name, driver, maxLevel, connect})
return res.status(200).json({id, name, driver, maxLevel, connect});
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.put("/:id", async(req, res) => {
try {
const bike = await repo.findBike(req.params.id);
if (bike == null) {
return res.status(404).json({code: 404, message: "bike not found"})
}
bike.name = req.body.name || "";
await repo.updateBike(bike);
return res.status(200).json(bike);
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.delete("/:id", async(req, res) => {
try {
const bike = await repo.findBike(req.params.id);
if (bike == null) {
return res.status(404).json({code: 404, message: "bike not found"})
}
await repo.deleteBike(bike);
return res.status(200).json(bike);
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
return router;
}

74
src/api/program.js

@ -0,0 +1,74 @@
const express = require("express");
/**
* @param {import("../repositories/sqlite3")} repo
*/
module.exports = function programRouter(repo) {
const router = express.Router({caseSensitive: false});
router.get("/", async(_, res) => {
try {
const programs = await repo.listPrograms();
return res.status(200).json(programs);
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.get("/:id", async(req, res) => {
try {
const program = await repo.findProgram(req.params.id);
if (program == null) {
return res.status(404).json({code: 404, message: "program not found"})
}
return res.status(200).json(program);
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
router.post("/", async(req, res) => {
try {
const {name, cpm, warmupMin, warmupCpm} = req.body;
const id = await repo.insertProgram({name, cpm, warmupMin, warmupCpm})
return res.status(200).json({id, name, cpm, warmupMin, warmupCpm});
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.put("/:id", async(req, res) => {
try {
const program = await repo.findProgram(req.params.id);
if (program == null) {
return res.status(404).json({code: 404, message: "program not found"})
}
program.name = req.body.name || "";
await repo.updateProgram(program);
return res.status(200).json(program);
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.delete("/:id", async(req, res) => {
try {
const program = await repo.findProgram(req.params.id);
if (program == null) {
return res.status(404).json({code: 404, message: "program not found"})
}
await repo.deleteProgram(program);
return res.status(200).json(program);
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
return router;
}

178
src/api/workout.js

@ -0,0 +1,178 @@
const express = require("express");
const Workout = require("../systems/workout");
/**
* @param {import("../repositories/sqlite3")} repo
*/
module.exports = function workoutRouter(repo) {
const router = express.Router({caseSensitive: false});
/** @type {Workout[]} */
let workouts = [];
router.get("/", async(req, res) => {
if (req.query.active === "true") {
return res.status(200).json(workouts.map(w => ({
id: w.id,
bike: w.bike,
program: w.program,
date: w.date,
active: true,
})));
}
try {
const dbWorkouts = await repo.listWorkouts();
return res.status(200).json(dbWorkouts.map(w => ({
...w,
active: workouts.find(w2 => w2.id === w.id) != null,
})));
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.get("/:id", async(req, res) => {
let workout = workouts.find(w => w.id == req.params.id);
if (workout != null) {
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date, active: true})
}
try {
workout = await repo.findWorkout(req.params.id);
if (workout == null) {
return res.status(404).json({code: 404, message: "workout not found"})
}
return res.status(200).json({active: false, ...workout});
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
router.get("/:id/measurements/", async(req, res) => {
try {
const measurements = await repo.listMeasurements(req.params.id);
if (measurements == null) {
return res.status(404).json({code: 404, message: "measurements not found"})
}
return res.status(200).json(measurements);
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
router.post("/", async(req, res) => {
try {
const {bikeId, programId} = req.body;
const workout = await Workout.create(repo, bikeId, programId);
workouts.push(workout);
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date});
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err});
}
});
router.post("/:id/continue", async(req, res) => {
let workout = workouts.find(w => w.id == req.params.id);
if (workout != null) {
return res.status(400).json({code: 400, message: "Workout already active."})
}
try {
workout = await Workout.continue(repo, req.params.id);
workouts.push(workout);
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date});
} catch (err) {
return res.status(400).json({code: 400, message: err.message || err})
}
});
router.post("/:id/connect", async(req, res) => {
const workout = workouts.find(w => w.id == req.params.id);
if (workout == null) {
return res.status(404).json({code: 404, message: "Workout not found or inactive."})
}
try {
await workout.connect();
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date});
} catch(err) {
return res.status(500).json({code: 500, message: err.message || err});
}
});
router.post("/:id/start", async(req, res) => {
const workout = workouts.find(w => w.id == req.params.id);
if (workout == null) {
return res.status(404).json({code: 404, message: "Workout not found or inactive."})
}
try {
await workout.start();
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date});
} catch(err) {
return res.status(500).json({code: 500, message: err.message || err});
}
});
router.post("/:id/pause", async(req, res) => {
const workout = workouts.find(w => w.id == req.params.id);
if (workout == null) {
return res.status(404).json({code: 404, message: "Workout not found or inactive."})
}
try {
await workout.pause();
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date});
} catch(err) {
return res.status(500).json({code: 500, message: err.message || err});
}
});
router.post("/:id/stop", async(req, res) => {
const workout = workouts.find(w => w.id == req.params.id);
if (workout == null) {
return res.status(404).json({code: 404, message: "Workout not found or inactive."})
}
try {
if (workout.driver != null) {
await workout.stop();
}
workouts = workouts.filter(w => w !== workout);
await workout.destroy();
return res.status(200).json({id: workout.id, bike: workout.bike, program: workout.program, date: workout.date});
} catch(err) {
return res.status(500).json({code: 500, message: err.message || err});
}
});
router.delete("/:id", async(req, res) => {
let workout = workouts.find(w => w.id == req.params.id);
if (workout != null) {
return res.status(400).json({code: 400, message: "Workout already active."})
}
try {
workout = await repo.findWorkout(req.params.id);
if (workout == null) {
return res.status(404).json({code: 404, message: "workout not found"})
}
await repo.deleteWorkout(workout);
return res.status(200).json(workout);
} catch (err) {
return res.status(500).json({code: 500, message: err.message || err})
}
});
return router;
}

10
src/drivers/index.js

@ -0,0 +1,10 @@
const MockDriver = require("./mock");
module.exports = function createDriver(name) {
switch (name) {
case "mock":
return new MockDriver();
default:
throw new Error(`Driver ${name} doesn't exist!`)
}
}

103
src/drivers/mock.js

@ -0,0 +1,103 @@
const EventEmitter = require("events");
const _ = require("underscore");
class MockDriver {
constructor() {
this.started = false;
this.events = new EventEmitter();
this.seconds = 0;
this.level = 1;
this.workoutState = {
minutes: 0,
seconds: 0,
speed: 0,
rpm: 0,
distance: 0,
calories: 0,
pulse: 0,
watt: 0,
level: 1,
};
this.interval = null;
}
connect() {
return Promise.resolve();
}
start() {
if (this.started) {
return;
}
this.started = true;
this.interval = setInterval(() => {
this.seconds++;
this.workoutState.minutes = Math.floor(this.seconds / 60);
this.workoutState.seconds = this.seconds % 60;
this.workoutState.speed = Math.floor(300 + Math.random() * 200) / 10;
this.workoutState.rpm = Math.floor(57 + Math.random() * 5);
this.workoutState.distance += (this.seconds % 5 == 0) ? 0.1 : 0;
this.workoutState.calories += (this.seconds % 2 == 0) ? 1 : 0;
this.workoutState.pulse = null;
this.workoutState.watt = Math.floor(100 + Math.random() * 20);
this.workoutState.level = this.level;
this.events.emit("workoutState", {...this.workoutState});
}, 1000);
this.workoutState.level = 1;
this.workoutState.speed = 0;
this.workoutState.rpm = 0;
this.workoutState.watt = 0;
if (this.seconds === 0) {
this.events.emit("workoutState", {...this.workoutState});
}
return Promise.resolve();
}
setLevel(n) {
if (n < 0 || n > 32) {
return Promise.reject(new Error("Level is out of range (0..=32)"))
}
this.level = n;
}
pause() {
clearInterval(this.interval);
this.started = false;
return Promise.resolve();
}
stop() {
this.pause();
this.workoutState = {
minutes: 0,
seconds: 0,
speed: 0,
rpm: 0,
distance: 0,
calories: 0,
pulse: 0,
watt: 0,
level: 1,
};
this.seconds = 0;
return Promise.resolve();
}
destroy() {
this.stop();
this.events.emit("destroy");
this.events.removeAllListeners();
}
}
module.exports = MockDriver;

227
src/repositories/sqlite3.js

@ -0,0 +1,227 @@
const sqlite3 = require("sqlite3").verbose();
class SQLite3Repository {
constructor(path) {
this.db = new sqlite3.Database(path);
}
/**
* @returns {Promise<void>}
*/
setup() {
return new Promise((resolve, reject) => {
this.db.run(SQL_TABLE_BIKE, (err) => {
if (err != null) {
reject(err);
}
this.db.run(SQL_TABLE_PROGRAM, (err) => {
if (err != null) {
reject(err);
}
this.db.run(SQL_TABLE_WORKOUT, (err) => {
if (err != null) {
reject(err);
}
this.db.run(SQL_TABLE_MEASUREMENT, (err) => {
if (err != null) {
reject(err);
}
resolve();
});
});
});
});
});
}
insert(obj, table, params) {
return new Promise((resolve, reject) => {
const query = `INSERT INTO ${table} (${params.join(", ")}) VALUES (${params.map(p => ':'+p).join(", ")});`;
const values = params.map(p => obj[p]);
this.db.run(query, values, function(err) {
if (err != null) {
return reject(err);
}
resolve(this.lastID);
});
});
}
update(obj, table, key, keys) {
return new Promise((resolve, reject) => {
const query = `UPDATE ${table} SET ${keys.map(k => `${k}=:${k}`).join(" AND ")} WHERE ${key}=:${key}`;
const values = [
...keys.map(k => obj[k]),
obj[key],
];
this.db.run(query, values, function(err) {
if (err != null) {
return reject(err);
}
resolve();
});
});
}
delete(obj, table, key) {
return new Promise((resolve, reject) => {
const query = `DELETE FROM ${table} WHERE ${key}=:${key};`
this.db.run(query, [obj[key]], function(err) {
if (err != null) {
return reject(err);
}
resolve(this.changes);
});
});
}
findOne(table, match) {
return this.find(table, match).then(res => res[0] || null)
}
find(table, match) {
return new Promise((resolve, reject) => {
const keys = Object.keys(match).filter(k => match[k] != null);
const params = keys.map(k => match[k]);
const query = `SELECT * FROM ${table}${keys.length > 0 ? ` WHERE ${keys.map(k => `${k}=:${k}`).join(" AND ")}` : ""};`;
this.db.all(query, params, function(err, rows) {
if (err != null) {
return reject(err);
}
resolve(rows);
});
});
}
findBike(id) {
return this.findOne("bike", {id});
}
listBikes() {
return this.find("bike", {});
}
insertBike(bike) {
return this.insert(bike, "bike", ["name", "driver", "connect", "maxLevel"]);
}
updateBike(bike) {
return this.update(bike, "bike", "id", ["name"]);
}
deleteBike(bike) {
return this.delete(bike, "bike", "id");
}
findProgram(id) {
return this.findOne("program", {id});
}
listPrograms() {
return this.find("program", {});
}
insertProgram(program) {
return this.insert(program, "program", ["name", "cpm", "warmupMin", "warmupCpm"]);
}
updateProgram(program) {
return this.update(program, "program", "id", ["name"]);
}
deleteProgram(program) {
return this.delete(program, "program", "id");
}
findWorkout(id) {
return this.findOne("workout", {id}).then(d => ({...d, date: new Date(d.date)}));
}
listWorkouts({programId, bikeId} = {}) {
return this.find("workout", {programId, bikeId}).then(l => l.map(d => ({...d, date: new Date(d.date)})));
}
insertWorkout(workout) {
return this.insert(workout, "workout", ["bikeId","programId","date"]);
}
deleteWorkout(workout) {
return this.delete(workout, "workout", "id");
}
findMeasurement(id) {
return this.findOne("measurement", {id});
}
listMeasurements(workoutId) {
return this.find("measurement", {workoutId});
}
insertMeasurement(measurement) {
return this.insert(measurement, "measurement", ["workoutId","minutes","seconds","speed","rpm","distance","calories","pulse","watt","level"]);
}
deleteMeasurement(measurement) {
return this.delete(measurement, "measurement", "id");
}
deleteMeasurements({workoutId}) {
return this.delete({workoutId}, "measurement", "workoutId");
}
}
const SQL_TABLE_BIKE = `
CREATE TABLE IF NOT EXISTS bike (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
driver VARCHAR(255) NOT NULL,
connect VARCHAR(255) NOT NULL,
maxLevel INT
);
`;
const SQL_TABLE_PROGRAM = `
CREATE TABLE IF NOT EXISTS program (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
cpm INT NOT NULL,
warmupMin INT NOT NULL,
warmupCpm INT NOT NULL
);
`;
const SQL_TABLE_WORKOUT = `
CREATE TABLE IF NOT EXISTS workout (
id INTEGER PRIMARY KEY,
bikeId INT NOT NULL,
programId INT NOT NULL,
date DATETIME NOT NULL
);
`;
const SQL_TABLE_MEASUREMENT = `
CREATE TABLE IF NOT EXISTS measurement (
id INTEGER PRIMARY KEY,
workoutId INT NOT NULL,
minutes INT,
seconds INT,
speed REAL,
rpm REAL,
distance REAL,
calories REAL,
pulse INT,
watt REAL,
level INT
);
`;
module.exports = SQLite3Repository;

20
src/server.js

@ -0,0 +1,20 @@
const express = require('express');
const SQLite3Repository = require("./repositories/sqlite3");
const repo = new SQLite3Repository("stuff.db");
repo.setup().catch(err => {
console.error("Failed to setup db:", err);
process.exit(1);
});
const app = express();
app.use(express.json({strict: false}));
app.use("/api/bike/", require("./api/bike")(repo));
app.use("/api/program/", require("./api/program")(repo));
app.use("/api/workout/", require("./api/workout")(repo));
app.listen(8780);

135
src/systems/workout.js

@ -0,0 +1,135 @@
const EventEmitter = require("events");
const createDriver = require("../drivers");
class Workout {
/**
* @param {import("../repositories/sqlite3")} repo
* @param {number} id
* @param {{
* id: string,
* name: string,
* driver: string,
* connect: string,
* maxLevel: number,
* }} bike
* @param {{
* id: number,
* name: string,
* cpm: number,
* warmupMin: number,
* warmupCpm: number,
* }} program
* @param {Date} date
*/
constructor(repo, id, bike, program, date) {
this.repo = repo;
this.id = id;
this.bike = bike;
this.program = program;
this.date = date;
this.driver = null;
this.offsetSeconds = 0;
this.offsets = {};
this.events = new EventEmitter();
}
async connect() {
this.driver = createDriver(this.bike.driver);
this.driver.events.on("workoutState", ws => this.handleWorkoutState(ws));
return this.driver.connect();
}
start() {
return this.driver.start();
}
pause() {
return this.driver.pause();
}
stop() {
return this.driver.stop();
}
destroy() {
this.events.emit("destroy");
this.events.removeAllListeners();
if (this.driver != null) {
this.driver.destroy();
this.driver = null;
}
}
listMeasurements() {
return this.repo.listMeasurements(this.id);
}
handleWorkoutState(ws) {
if (this.offsetSeconds > 0) {
const seconds = (ws.minutes * 60) + ws.seconds + this.offsetSeconds;
ws.minutes = Math.floor(seconds / 60);
ws.seconds = seconds % 60;
for (const key in this.offsets) {
if (!this.offsets.hasOwnProperty(key)) {
continue;
}
ws[key] += this.offsets[key];
}
}
this.repo.insertMeasurement({...ws, workoutId: this.id});
this.events.emit("workoutState", ws);
}
/**
* @param {import("../repositories/sqlite3")} repo
* @param {number} bikeId
* @param {number} programId
*/
static async create(repo, bikeId, programId) {
const bike = await repo.findBike(bikeId);
const program = await repo.findProgram(programId);
const date = new Date();
const id = await repo.insertWorkout({
bikeId: bike.id,
programId: program.id,
date: date,
});
return new Workout(repo, id, bike, program, date);
}
/**
* @param {import("../repositories/sqlite3")} repo
* @param {number} id
*/
static async continue(repo, id) {
const data = await repo.findWorkout(id);
const bike = await repo.findBike(data.bikeId);
const program = await repo.findWorkout(data.workoutId);
const workout = new Workout(repo, parseInt(id), bike, program, new Date(data.date));
const list = await repo.listMeasurements(id);
if (list.length > 0) {
const last = list[list.length - 1];
workout.offsetSeconds = (last.minutes * 60 + last.seconds);
workout.offsets = {
distance: last.distance,
calories: last.calories,
}
}
return workout;
}
}
module.exports = Workout;

BIN
stuff.db

Loading…
Cancel
Save