Lots of refactoring

This commit is contained in:
Zakary Timson 2024-10-28 20:17:38 -04:00
parent 19f76c3f10
commit a28c18a1d6
14 changed files with 231 additions and 1346 deletions

1132
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,6 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bme280-sensor": "^0.1.7",
"express": "^4.21.1", "express": "^4.21.1",
"i2c-bus": "^5.2.3", "i2c-bus": "^5.2.3",
"serialport": "^12.0.0" "serialport": "^12.0.0"

View File

@ -1,23 +1,26 @@
import SensorSuite from './sensor-suite.js' import SensorSuite from './sensor-suite.js'
export default class Apollo { class Apollo {
sensor; sensor;
onStop; onStop;
get status() { get status() {
return {...this.sensor.status}; return {...this.sensor.status};
} }
constructor() { constructor() {
this.sensor = new SensorSuite(); this.sensor = new SensorSuite();
} }
async start() { async start() {
await this.sensor.start(); await this.sensor.start();
} }
stop() { stop() {
this.sensor.stop(); this.sensor.stop();
if(this.onStop) this.onStop(); if (this.onStop) this.onStop();
} }
} }
const APOLLO = new Apollo();
export default APOLLO;

20
src/bme280.js Normal file
View File

@ -0,0 +1,20 @@
import i2c from 'i2c-bus';
export async function bme(address = 0x76) {
const i2cBus = await i2c.openPromisified(1);
const data = await Promise.all([
i2cBus.readByte(address, 0xFA),
i2cBus.readByte(address, 0xFB),
i2cBus.readByte(address, 0xFC),
i2cBus.readByte(address, 0xF7),
i2cBus.readByte(address, 0xF8),
i2cBus.readByte(address, 0xF9),
i2cBus.readByte(address, 0xFD),
i2cBus.readByte(address, 0xFE),
]);
return {
temperature: (((data[0] << 12) | (data[1] << 4) | (data[2] >> 4)) / 16384.0 - 5120.0) / 100,
pressure: ((data[3] << 12) | (data[4] << 4) | (data[5] >> 4)) / 25600,
humidity: ((data[6] << 8) | data[7]) / 1024.0,
};
}

View File

@ -1,19 +1,18 @@
import i2c from 'i2c-bus'; import i2c from 'i2c-bus';
async function bmsStatus(address = 0x57) { export async function bms(address = 0x57) {
const i2cBus = await i2c.openPromisified(1); const i2cBus = await i2c.openPromisified(1);
const data = await Promise.all([ const data = await Promise.all([
i2cBus.readByte(address, 0x02), i2cBus.readByte(address, 0x02),
i2cBus.readByte(address, 0x04), i2cBus.readByte(address, 0x04),
i2cBus.readByte(address, 0x22), i2cBus.readByte(address, 0x22),
i2cBus.readByte(address, 0x23), i2cBus.readByte(address, 0x23),
i2cBus.readByte(address, 0x2a), i2cBus.readByte(address, 0x2a),
]); ]);
return { return {
charging: !!((data[0] >> 7) & 1), charging: !!((data[0] >> 7) & 1),
percentage: data[4] / 100, percentage: data[4] / 100,
temperature: data[1] - 40, temperature: data[1] - 40,
voltage: ((data[2] << 8) | data[3]) / 1000, voltage: ((data[2] << 8) | data[3]) / 1000,
} }
} }
console.log(await bmsStatus());

View File

@ -1,17 +0,0 @@
import Controller from './controller.js';
import {ask} from './misc.js';
export default class Cli extends Controller {
constructor(apollo) {
super(apollo);
console.log(this.help());
}
async start() {
while(true) {
const cmd = await ask('> ');
console.log(this.run(cmd));
console.log();
}
}
}

View File

@ -1,37 +0,0 @@
import {$} from './misc.js';
export default class Controller {
apollo;
constructor(apollo) {
this.apollo = apollo;
}
help() {
return `
Apollo v0.0.0
Commands:
exit - Stop Apollo
help - Display manual
reboot - Reboot System
sensors - All sensor data
shutdown - Shutdown System
stop - Stop Apollo
status - Subsystem status
`;
}
run(cmd) {
cmd = cmd.toLowerCase();
if(cmd == 'help') return this.help();
else if(cmd == 'reboot') $`reboot now`;
else if(cmd == 'sensors') return this.apollo.sensor.data;
else if(cmd == 'shutdown') {
$`shutdown now`;
this.apollo.stop();
} else if(cmd == 'status') return this.apollo.status;
else if(cmd == 'stop' || cmd == 'exit') process.exit();
else return `Unknown Command: ${cmd}`;
}
}

45
src/daemon.js Normal file
View File

@ -0,0 +1,45 @@
import express from 'express';
import path from 'path';
import APOLLO from './apollo.js';
import {$} from './misc.js';
export default class Daemon {
apollo;
express;
constructor(port = 80) {
this.apollo = APOLLO;
this.apollo.start();
this.express = express();
this.express.get('/api/*', async (req, res) => {
const cmd = req.params['0'];
res.json(await this.run(cmd));
});
this.express.get('*', (req, res) => {
let p = req.params['0'];
if (!p || p == '/') p = 'index.html';
const absolute = path.join(import.meta.url, '/../../ui/', p).replace('file:', '');
res.sendFile(absolute);
});
this.express.listen(port, () => 'Started Apollo');
}
run(cmd) {
cmd = cmd.toLowerCase();
if (cmd === 'reboot') $`reboot now`;
else if (cmd === 'sensors') return this.apollo.sensor.data;
else if (cmd === 'shutdown') {
this.run('stop');
$`shutdown now`;
} else if (cmd === 'status') return this.apollo.status;
else if (cmd === 'stop') {
this.express.stop();
this.apollo.stop();
}
else return `Unknown Command: ${cmd}`;
}
}

View File

@ -1,28 +0,0 @@
import express from 'express';
import path from 'path';
import Controller from './controller.js';
export default class Http extends Controller {
express;
constructor(apollo, port = 8000) {
super(apollo);
this.express = express();
this.express.get('/api/*', async (req, res) => {
const cmd = req.params['0'];
console.log(cmd);
res.json(await this.run(cmd));
});
this.express.get('*', (req, res) => {
let p = req.params['0'];
if(!p || p == '/') p = 'index.html';
const absolute = path.join(import.meta.url, '/../../ui/', p).replace('file:', '');
res.sendFile(absolute);
});
this.express.listen(port);
}
}

View File

@ -1,14 +1,41 @@
import Apollo from './apollo.js'; import {ask} from './misc.js';
import Cli from './cli.js'; import Daemon from './daemon.js';
import Http from './http.js';
import Serial from './serial.js';
(async () => { const command = process.argv[1];
const apollo = new Apollo(); let remote;
const cli = new Cli(apollo);
const serial = new Serial(apollo); function help() {
const http = new Http(apollo); return `
await apollo.start(); Apollo v0.0.0
cli.start();
// serial.start(); Commands:
})(); exit - Exit CLI
help - Display this manual
reboot - Reboot System
remote <addr> - Connect to remote Apollo
sensors - Display sensor data
shutdown - Shutdown System
start <port> - Start Apollo server
stop - Stop Apollo server
status - Apollo Subsystems status
`;
}
function run(cmd) {
if(cmd.toLowerCase() === 'exit') process.exit();
else if(cmd === 'help') return this.help();
else if(cmd.startsWith('remote')) remote = cmd.split(' ').pop();
else if(cmd.startsWith('start')) new Daemon(+cmd.split(' ').pop() || 80);
else return fetch(`${remote}/api/${cmd}`).then(resp => {
if(resp.ok && resp.headers['Content-Type'].includes('json'))
return resp.json();
else return resp.text();
});
}
if(command) console.log(run(command));
else console.log(help());
while(true) {
const cmd = await ask('> ');
console.log(run(cmd), '\n');
}

View File

@ -2,11 +2,27 @@ import * as readline from 'node:readline';
import {exec} from 'child_process'; import {exec} from 'child_process';
export function $(str, ...args) { export function $(str, ...args) {
let cmd = str.reduce((acc, part, i) => acc + part + (args[i] || ''), ''); let cmd = str.reduce((acc, part, i) => acc + part + (args[i] || ''), '');
return new Promise((res, rej) => exec(cmd, (err, stdout, stderr) => { return new Promise((res, rej) => exec(cmd, (err, stdout, stderr) => {
if(err || stderr) return rej(err || stderr); if (err || stderr) return rej(err || stderr);
return res(stdout); return res(stdout);
})) }))
}
export function adjustedInterval(cb, ms) {
let cancel = false, timeout = null;
const p = async () => {
if (cancel) return;
const start = new Date().getTime();
await cb();
const end = new Date().getTime();
timeout = setTimeout(() => p(), ms - (end - start) || 1);
};
p();
return () => {
cancel = true;
if (timeout) clearTimeout(timeout);
}
} }
export function ask(prompt, hide = false) { export function ask(prompt, hide = false) {
@ -50,22 +66,6 @@ export function ask(prompt, hide = false) {
}); });
} }
export function poll(cb, ms) {
let cancel = false, timeout = null;
const p = async () => {
if(cancel) return;
const start = new Date().getTime();
await cb();
const end = new Date().getTime();
timeout = setTimeout(() => p(), ms - (end - start));
};
p();
return () => {
cancel = true;
if(timeout) clearTimeout(timeout);
}
}
export function sleep(ms) { export function sleep(ms) {
return new Promise(res => setTimeout(res, ms)); return new Promise(res => setTimeout(res, ms));
} }

View File

@ -1,50 +1,51 @@
import BME280 from 'bme280-sensor'; import {adjustedInterval, sleep} from './misc.js';
import {poll} from './misc.js'; import {bms} from './bms.js';
import {bme} from './bme280.js';
export default class SensorSuite { export default class SensorSuite {
bme280; data = {
stopBme280; battery: {charging: false, temperature: 0, percentage: 0, voltage: 0},
environment: {humidity: 0, temperature: 0, pressure: 0, altitude: 0},
_data = { movement: {accelerometer: null, gyro: null, magnetometer: null},
acceleration: [null, null, null], gps: {accuracy: null, lat: null, long: null, altitude: 0, ground: 0, time: 0}
altitude: null,
battery: null,
gpsStrength: null,
gyro: [null, null, null],
humidity: null,
magnetometer: [null, null, null],
position: [null, null],
pressure: null,
temperature: null,
voltage: null,
} }
set data(d) { this._data = d; } intervals = [];
get data() { return {timestamp: new Date().getTime(), ...this._data}; } status = {}
_env_sensor = false; #altitude; // Pressure <-> GPS correction for future calculations TODO: Update on GPS events
get status() { return { env_sensor: this._env_sensor ? 'ok' : 'failed' }; } get altitude() {
return data.environment.altitude + (this.#altitude ?? 0);
}
constructor() { constructor() {
this.bme280 = new BME280({i2cBusNo: 1, i2cAddress: 0x76});
} }
start() { static #hPaToAltitude(hPa) {
return this.bme280.init().then(() => { return 44330 * (1 - Math.pow(hPa / 1013.25, 1 / 5.255));
// Poll environmental data }
this._env_sensor = true;
this.stopBme280 = poll(async () => { async start() {
const d = await this.bme280.readSensorData(); this.intervals.push(adjustedInterval(async () => {
this._data.humidity = d.humidity / 100; this.data.environment = await this.statusWrapper(bme(), 'bme280');
this._data.temperature = d.temperature_C; this.data.environment.altitude = SensorSuite.#hPaToAltitude(this.data.environment.pressure);
this._data.pressure = d.pressure_hPa; }, 1000));
this._data.altitude = BME280.calculateAltitudeMeters(this.data.pressure); await sleep(500); // Offset reading sensor data
}, 1000); this.intervals.push(adjustedInterval(async () => {
this.data.battery = await this.statusWrapper(bms(), 'bms');
}, 1000));
}
statusWrapper(promise, key) {
return promise.then(resp => {
this.status[key] = 'ok'
return resp;
}).catch(err => { }).catch(err => {
this._env_sensor = false; this.status[key] = err.message;
}); });
} }
stop() { stop() {
if(this.stopBme280) this.stopBme280(); this.intervals.forEach(unsubscribe => unsubscribe());
this.intervals = [];
} }
} }

View File

@ -1,29 +0,0 @@
import {SerialPort} from 'serialport';
import Controller from './controller.js';
export default class Serial extends Controller {
baud;
interval;
port;
constructor(apollo, options) {
super(apollo);
this.options = {
baud: 9600,
...options
};
}
async start() {
this.interval = setInterval(async () => {
const cons = (await SerialPort.list()).map(p => p.path);
if(this.ports.length > 0) {
this.port = new SerialPort({path: cons[0], baudRate: this.options.baud});
this.port.on('open', () => this.help());
this.port.on('data', cmd => port.write(JSON.stringify(this.run(cmd))));
this.port.on('close', () => this.start());
clearInterval(this.interval);
}
}, 1000);
}
}

View File

@ -3,9 +3,43 @@
<html> <html>
<head> <head>
<title>Apollo v0.0.0</title> <title>Apollo v0.0.0</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #1a1a1a;
color: white;
font-family: sans-serif;
}
.text-muted {
color: #aaa;
}
</style>
</head> </head>
<body> <body>
<h1>Apollo v0.0.0</h1> <nav style="padding: 0.5rem;">
<div style="display: flex; flex-direction: row">
<div>
<h1 style="display: inline; font-size: 1rem;">Apollo Control</h1>
</div>
<div style="flex-grow: 1"></div>
<div class="text-muted" style="font-size: 1rem">
<span id="time">00:00:00</span> UTC
</div>
</div>
</nav>
<script>
const time = document.querySelector('#time');
setInterval(() => {
const now = new Date();
time.innerHTML = `${now.getHours().toString().padStart(2, 0)}:${now.getMinutes().toString().padStart(2, 0)}:${now.getSeconds().toString().padStart(2, 0)}`;
}, 1000);
</script>
</body> </body>
</html> </html>