Lots of refactoring
This commit is contained in:
parent
19f76c3f10
commit
a28c18a1d6
1132
package-lock.json
generated
1132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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
20
src/bme280.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
31
src/bms.js
31
src/bms.js
@ -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());
|
|
||||||
|
17
src/cli.js
17
src/cli.js
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
45
src/daemon.js
Normal 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}`;
|
||||||
|
}
|
||||||
|
}
|
28
src/http.js
28
src/http.js
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
53
src/main.js
53
src/main.js
@ -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');
|
||||||
|
}
|
||||||
|
42
src/misc.js
42
src/misc.js
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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 = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user