Compare commits
22 Commits
a242439c47
...
master
Author | SHA1 | Date | |
---|---|---|---|
16ea6dafe3 | |||
2e580b44d2 | |||
4068b8bb01 | |||
a9a8cf2745 | |||
85e6ab01ae | |||
31585c4b6b | |||
1de19b208a | |||
72800546a8 | |||
287a28d065 | |||
efc56aae40 | |||
a127bcfbbd | |||
f5e1d73988 | |||
90456f5800 | |||
00f11f8c08 | |||
3e06856c70 | |||
4b70f8d67b | |||
2144d7ef69 | |||
02a533358e | |||
a28c18a1d6 | |||
19f76c3f10 | |||
8d30c10f18 | |||
9178b0a83f |
24
README.md
24
README.md
@ -3,25 +3,7 @@
|
||||
## Setup
|
||||
```sh
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install htop i2c-tools
|
||||
raspi-config # enable i2c
|
||||
```
|
||||
|
||||
### MAC fix
|
||||
```sh
|
||||
MAC_PREFIX="80:86:00"
|
||||
|
||||
files=(
|
||||
"/etc/systemd/network/eth0.network"
|
||||
"/etc/systemd/network/wlan0.network"
|
||||
)
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ -n "$(cat $file | grep MACAddress )" ]; then continue; fi
|
||||
mac_address=$(printf '%02X:%02X:%02X' $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)))
|
||||
cat <<EOF >> "$file"
|
||||
[Link]
|
||||
MACAddress=$MAC_PREFIX:$mac_address
|
||||
EOF
|
||||
done
|
||||
sudo apt install htop i2c-tools hwclock
|
||||
raspi-config # enable i2c & UART
|
||||
echo "@reboot root hwclock -s" >> /etc/crontab
|
||||
```
|
||||
|
40
package-lock.json
generated
40
package-lock.json
generated
@ -9,7 +9,8 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bme280-sensor": "^0.1.7",
|
||||
"@ztimson/utils": "^0.21.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.1",
|
||||
"i2c-bus": "^5.2.3",
|
||||
"serialport": "^12.0.0"
|
||||
@ -220,6 +221,12 @@
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@ztimson/utils": {
|
||||
"version": "0.21.6",
|
||||
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.21.6.tgz",
|
||||
"integrity": "sha512-wOjwd9N6m73NgsuCIWr1yrmWwp7CLoDu6vdlhE0CiXhEqzC9qRhkOcP8Sgn7n0nK3RANlwDtYKJl02iuRAcoJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@ -246,15 +253,6 @@
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bme280-sensor": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/bme280-sensor/-/bme280-sensor-0.1.7.tgz",
|
||||
"integrity": "sha512-8+ss7jXMpRUUaR6bJWcvCt1bwdNqcusGjQtsuSqh5xTEZhJocDX21eo/j1g0zVZW9WEn4JAGNXyGwKTx4/Dj+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"i2c-bus": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@ -358,6 +356,19 @@
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@ -815,6 +826,15 @@
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
|
@ -5,12 +5,13 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/main.js"
|
||||
"apollo": "node src/main.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bme280-sensor": "^0.1.7",
|
||||
"cors": "^2.8.5",
|
||||
"@ztimson/utils": "^0.21.6",
|
||||
"express": "^4.21.1",
|
||||
"i2c-bus": "^5.2.3",
|
||||
"serialport": "^12.0.0"
|
||||
|
@ -1,23 +1,26 @@
|
||||
import SensorSuite from './sensor-suite.js'
|
||||
|
||||
export default class Apollo {
|
||||
sensor;
|
||||
onStop;
|
||||
class Apollo {
|
||||
sensor;
|
||||
onStop;
|
||||
|
||||
get status() {
|
||||
return {...this.sensor.status};
|
||||
}
|
||||
get status() {
|
||||
return {...this.sensor.status};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.sensor = new SensorSuite();
|
||||
}
|
||||
constructor() {
|
||||
this.sensor = new SensorSuite();
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.sensor.start();
|
||||
}
|
||||
async start() {
|
||||
await this.sensor.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.sensor.stop();
|
||||
if(this.onStop) this.onStop();
|
||||
}
|
||||
stop() {
|
||||
this.sensor.stop();
|
||||
if (this.onStop) this.onStop();
|
||||
}
|
||||
}
|
||||
|
||||
const APOLLO = new Apollo();
|
||||
export default APOLLO;
|
||||
|
35
src/bme280.js
Normal file
35
src/bme280.js
Normal file
@ -0,0 +1,35 @@
|
||||
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), // Temp MSB
|
||||
i2cBus.readByte(address, 0xFB), // Temp LSB
|
||||
i2cBus.readByte(address, 0xFC), // Temp XLSB
|
||||
i2cBus.readByte(address, 0xF7), // Pressure MSB
|
||||
i2cBus.readByte(address, 0xF8), // Pressure LSB
|
||||
i2cBus.readByte(address, 0xF9), // Pressure XLSB
|
||||
i2cBus.readByte(address, 0xFD), // Humidity MSB
|
||||
i2cBus.readByte(address, 0xFE), // Humidity LSB
|
||||
]);
|
||||
|
||||
// Calibration registers
|
||||
const CALIBRATION_LENGTH = 24;
|
||||
const calibBuffer = Buffer.alloc(CALIBRATION_LENGTH);
|
||||
await i2cBus.readI2cBlock(address, 0x88, CALIBRATION_LENGTH, calibBuffer);
|
||||
const T1 = calibBuffer.readUInt16LE(0);
|
||||
const T2 = calibBuffer.readInt16LE(2);
|
||||
const T3 = calibBuffer.readInt16LE(4);
|
||||
|
||||
const rawTemp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
|
||||
const var1 = (((rawTemp >> 3) - (T1 << 1)) * T2) / 2048;
|
||||
const var2 = (((((rawTemp >> 4) - T1) * ((rawTemp >> 4) - T1)) >> 12) * T3) / 16384;
|
||||
const temp = (var1 + var2) / 5120.0;
|
||||
|
||||
await i2cBus.close();
|
||||
return {
|
||||
temperature: temp,
|
||||
pressure: ((data[3] << 12) | (data[4] << 4) | (data[5] >> 4)) / 25600,
|
||||
humidity: ((data[6] << 8) | data[7]) / 1024.0,
|
||||
};
|
||||
}
|
19
src/bms.js
Normal file
19
src/bms.js
Normal file
@ -0,0 +1,19 @@
|
||||
import i2c from 'i2c-bus';
|
||||
|
||||
export async function bms(address = 0x57) {
|
||||
const i2cBus = await i2c.openPromisified(1);
|
||||
const data = await Promise.all([
|
||||
i2cBus.readByte(address, 0x02),
|
||||
i2cBus.readByte(address, 0x04),
|
||||
i2cBus.readByte(address, 0x22),
|
||||
i2cBus.readByte(address, 0x23),
|
||||
i2cBus.readByte(address, 0x2a),
|
||||
]);
|
||||
await i2cBus.close();
|
||||
return {
|
||||
charging: !!((data[0] >> 7) & 1),
|
||||
percentage: data[4] / 100,
|
||||
temperature: data[1] - 40,
|
||||
voltage: ((data[2] << 8) | data[3]) / 1000,
|
||||
}
|
||||
}
|
71
src/bno080.js
Normal file
71
src/bno080.js
Normal file
@ -0,0 +1,71 @@
|
||||
import i2c from 'i2c-bus';
|
||||
|
||||
export async function imu(address = 0x4B) { // BNO080 default I2C address
|
||||
const i2cBus = await i2c.openPromisified(1);
|
||||
|
||||
// Register addresses for accelerometer, gyroscope, and magnetometer data (assumes configuration is done)
|
||||
const ACCEL_X_LSB = 0x08;
|
||||
const ACCEL_X_MSB = 0x09;
|
||||
const ACCEL_Y_LSB = 0x0A;
|
||||
const ACCEL_Y_MSB = 0x0B;
|
||||
const ACCEL_Z_LSB = 0x0C;
|
||||
const ACCEL_Z_MSB = 0x0D;
|
||||
|
||||
const GYRO_X_LSB = 0x10;
|
||||
const GYRO_X_MSB = 0x11;
|
||||
const GYRO_Y_LSB = 0x12;
|
||||
const GYRO_Y_MSB = 0x13;
|
||||
const GYRO_Z_LSB = 0x14;
|
||||
const GYRO_Z_MSB = 0x15;
|
||||
|
||||
const MAG_X_LSB = 0x16; // Hypothetical register for magnetometer X LSB
|
||||
const MAG_X_MSB = 0x17; // Hypothetical register for magnetometer X MSB
|
||||
const MAG_Y_LSB = 0x18; // Hypothetical register for magnetometer Y LSB
|
||||
const MAG_Y_MSB = 0x19; // Hypothetical register for magnetometer Y MSB
|
||||
const MAG_Z_LSB = 0x1A; // Hypothetical register for magnetometer Z LSB
|
||||
const MAG_Z_MSB = 0x1B; // Hypothetical register for magnetometer Z MSB
|
||||
|
||||
// Read data from the sensor registers
|
||||
const data = await Promise.all([
|
||||
i2cBus.readByte(address, ACCEL_X_LSB),
|
||||
i2cBus.readByte(address, ACCEL_X_MSB),
|
||||
i2cBus.readByte(address, ACCEL_Y_LSB),
|
||||
i2cBus.readByte(address, ACCEL_Y_MSB),
|
||||
i2cBus.readByte(address, ACCEL_Z_LSB),
|
||||
i2cBus.readByte(address, ACCEL_Z_MSB),
|
||||
i2cBus.readByte(address, GYRO_X_LSB),
|
||||
i2cBus.readByte(address, GYRO_X_MSB),
|
||||
i2cBus.readByte(address, GYRO_Y_LSB),
|
||||
i2cBus.readByte(address, GYRO_Y_MSB),
|
||||
i2cBus.readByte(address, GYRO_Z_LSB),
|
||||
i2cBus.readByte(address, GYRO_Z_MSB),
|
||||
i2cBus.readByte(address, MAG_X_LSB),
|
||||
i2cBus.readByte(address, MAG_X_MSB),
|
||||
i2cBus.readByte(address, MAG_Y_LSB),
|
||||
i2cBus.readByte(address, MAG_Y_MSB),
|
||||
i2cBus.readByte(address, MAG_Z_LSB),
|
||||
i2cBus.readByte(address, MAG_Z_MSB),
|
||||
]);
|
||||
|
||||
// Convert accelerometer data
|
||||
const accelX = ((data[1] << 8) | data[0]) / 1000;
|
||||
const accelY = ((data[3] << 8) | data[2]) / 1000;
|
||||
const accelZ = ((data[5] << 8) | data[4]) / 1000;
|
||||
|
||||
// Convert gyroscope data
|
||||
const gyroX = ((data[7] << 8) | data[6]) / 1000;
|
||||
const gyroY = ((data[9] << 8) | data[8]) / 1000;
|
||||
const gyroZ = ((data[11] << 8) | data[10]) / 1000;
|
||||
|
||||
// Convert magnetometer data
|
||||
const magX = ((data[13] << 8) | data[12]) / 1000; // Hypothetical conversion factor
|
||||
const magY = ((data[15] << 8) | data[14]) / 1000;
|
||||
const magZ = ((data[17] << 8) | data[16]) / 1000;
|
||||
|
||||
await i2cBus.close();
|
||||
return {
|
||||
acceleration: { x: accelX, y: accelY, z: accelZ },
|
||||
gyroscope: { x: gyroX, y: gyroY, z: gyroZ },
|
||||
magnetometer: { x: magX, y: magY, z: magZ },
|
||||
};
|
||||
}
|
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}`;
|
||||
}
|
||||
}
|
53
src/daemon.js
Normal file
53
src/daemon.js
Normal file
@ -0,0 +1,53 @@
|
||||
import cors from 'cors';
|
||||
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 = 1969) {
|
||||
this.apollo = APOLLO;
|
||||
this.apollo.start();
|
||||
|
||||
this.express = express();
|
||||
|
||||
this.express.use(cors('*'));
|
||||
|
||||
this.express.get('/api/*', async (req, res) => {
|
||||
const cmd = req.params['0'];
|
||||
res.json(await this.run(cmd));
|
||||
});
|
||||
|
||||
this.express.get('/favicon.*', (req, res) => {
|
||||
const absolute = path.join(import.meta.url, '/../../ui/favicon.png').replace('file:', '');
|
||||
res.sendFile(absolute);
|
||||
});
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
25
src/http.js
25
src/http.js
@ -1,25 +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('*', (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.get('/api/*', async (req, res) => {
|
||||
const cmd = req.params['0'];
|
||||
console.log(cmd);
|
||||
res.json(await this.run(cmd));
|
||||
});
|
||||
this.express.listen(port);
|
||||
}
|
||||
}
|
66
src/main.js
66
src/main.js
@ -1,14 +1,54 @@
|
||||
import Apollo from './apollo.js';
|
||||
import Cli from './cli.js';
|
||||
import Http from './http.js';
|
||||
import Serial from './serial.js';
|
||||
import {ask} from './misc.js';
|
||||
import Daemon from './daemon.js';
|
||||
|
||||
(async () => {
|
||||
const apollo = new Apollo();
|
||||
const cli = new Cli(apollo);
|
||||
const serial = new Serial(apollo);
|
||||
const http = new Http(apollo);
|
||||
await apollo.start();
|
||||
cli.start();
|
||||
// serial.start();
|
||||
})();
|
||||
const command = process.argv.slice(2).join(' ');
|
||||
let remote = 'localhost:1969';
|
||||
|
||||
function help() {
|
||||
return `
|
||||
Apollo v0.0.0
|
||||
Remote: ${remote}
|
||||
|
||||
Commands:
|
||||
exit - Exit CLI
|
||||
help - Display this manual
|
||||
reboot - Reboot System
|
||||
remote <addr> - Get/Set Apollo address
|
||||
sensors - Display sensor data
|
||||
shutdown - Shutdown System
|
||||
start <port> - Start Apollo server
|
||||
stop - Stop Apollo server
|
||||
status - Apollo Subsystems status
|
||||
`;
|
||||
}
|
||||
|
||||
function run(cmd) {
|
||||
try {
|
||||
if(cmd.toLowerCase() === 'exit') process.exit();
|
||||
else if(cmd === 'help') return help();
|
||||
else if(cmd === 'remote') return remote;
|
||||
else if(cmd.startsWith('remote')) {
|
||||
remote = cmd.split(' ').pop();
|
||||
return `Remote Set: ${remote}`;
|
||||
} else if(cmd.startsWith('start')) {
|
||||
const port = +cmd.split(' ').pop() || 1969;
|
||||
new Daemon(port);
|
||||
return `Listening on: http://localhost:${port}`;
|
||||
} else return fetch(`${remote.startsWith('http') ? '' : 'http://'}${remote}/api/${cmd}`).then(resp => {
|
||||
if(resp.ok && resp.headers['Content-Type']?.includes('json'))
|
||||
return resp.json();
|
||||
else return resp.text();
|
||||
}).catch(err => err.message);
|
||||
} catch(err) {
|
||||
return err.message || err;
|
||||
}
|
||||
}
|
||||
|
||||
if(command) console.log(run(command));
|
||||
else {
|
||||
console.log(help());
|
||||
while(true) {
|
||||
const cmd = await ask('> ');
|
||||
console.log(await run(cmd), '\n');
|
||||
}
|
||||
}
|
||||
|
30
src/misc.js
30
src/misc.js
@ -2,11 +2,11 @@ import * as readline from 'node:readline';
|
||||
import {exec} from 'child_process';
|
||||
|
||||
export function $(str, ...args) {
|
||||
let cmd = str.reduce((acc, part, i) => acc + part + (args[i] || ''), '');
|
||||
return new Promise((res, rej) => exec(cmd, (err, stdout, stderr) => {
|
||||
if(err || stderr) return rej(err || stderr);
|
||||
return res(stdout);
|
||||
}))
|
||||
let cmd = str.reduce((acc, part, i) => acc + part + (args[i] || ''), '');
|
||||
return new Promise((res, rej) => exec(cmd, (err, stdout, stderr) => {
|
||||
if (err || stderr) return rej(err || stderr);
|
||||
return res(stdout);
|
||||
}))
|
||||
}
|
||||
|
||||
export function ask(prompt, hide = false) {
|
||||
@ -49,23 +49,3 @@ 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) {
|
||||
return new Promise(res => setTimeout(res, ms));
|
||||
}
|
||||
|
@ -1,46 +1,57 @@
|
||||
import BME280 from 'bme280-sensor';
|
||||
import {poll} from './misc.js';
|
||||
import {adjustedInterval, sleep} from '@ztimson/utils';
|
||||
import {bms} from './bms.js';
|
||||
import {bme} from './bme280.js';
|
||||
import {imu} from './bno080.js';
|
||||
|
||||
export default class SensorSuite {
|
||||
bme280;
|
||||
stopBme280;
|
||||
|
||||
_data = {
|
||||
acceleration: [null, null, null],
|
||||
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,
|
||||
data = {
|
||||
battery: {charging: false, temperature: 0, percentage: 0, voltage: 0},
|
||||
environment: {humidity: 0, temperature: 0, pressure: 0, altitude: 0},
|
||||
movement: {accelerometer: null, gyro: null, magnetometer: null},
|
||||
gps: {accuracy: null, lat: null, long: null, altitude: 0, ground: 0, time: 0}
|
||||
}
|
||||
set data(d) { this._data = d; }
|
||||
get data() { return {timestamp: new Date().getTime(), ...this._data}; }
|
||||
intervals = [];
|
||||
status = {}
|
||||
|
||||
get status() { return { sensors: 'ok' }; }
|
||||
#altitude; // Pressure <-> GPS correction for future calculations TODO: Update on GPS events
|
||||
get altitude() {
|
||||
return data.environment.altitude + (this.#altitude ?? 0);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.bme280 = new BME280({i2cBusNo: 1, i2cAddress: 0x76});
|
||||
}
|
||||
|
||||
static #hPaToAltitude(hPa) {
|
||||
return 44330 * (1 - Math.pow(hPa / 1013.25, 1 / 5.255));
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.bme280.init();
|
||||
// Movement
|
||||
this.intervals.push(adjustedInterval(async () => {
|
||||
this.data.movement = await this.statusWrapper(imu(), 'imu');
|
||||
}, 1000));
|
||||
// Environment
|
||||
this.intervals.push(adjustedInterval(async () => {
|
||||
this.data.environment = await this.statusWrapper(bme(), 'bme280');
|
||||
}, 1000));
|
||||
// Battery
|
||||
await sleep(500); // Offset reading sensor data
|
||||
this.intervals.push(adjustedInterval(async () => {
|
||||
this.data.battery = await this.statusWrapper(bms(), 'bms');
|
||||
}, 1000));
|
||||
}
|
||||
|
||||
// Poll environmental data
|
||||
this.stopBme280 = poll(async () => {
|
||||
const d = await this.bme280.readSensorData();
|
||||
this._data.humidity = d.humidity / 100;
|
||||
this._data.temperature = d.temperature_C;
|
||||
this._data.pressure = d.pressure_hPa;
|
||||
this._data.altitude = BME280.calculateAltitudeMeters(this.data.pressure);
|
||||
}, 1000);
|
||||
statusWrapper(promise, key) {
|
||||
return promise.then(resp => {
|
||||
this.status[key] = 'ok'
|
||||
return resp;
|
||||
}).catch(err => {
|
||||
this.status[key] = err.message;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
6
ui/assets/bootstrap.min.css
vendored
Normal file
6
ui/assets/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
ui/assets/font-awesome/css/font-awesome.css
vendored
Normal file
9
ui/assets/font-awesome/css/font-awesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
ui/assets/font-awesome/webfonts/fa-brands-400.ttf
Normal file
BIN
ui/assets/font-awesome/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
ui/assets/font-awesome/webfonts/fa-brands-400.woff2
Normal file
BIN
ui/assets/font-awesome/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
ui/assets/font-awesome/webfonts/fa-regular-400.ttf
Normal file
BIN
ui/assets/font-awesome/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
ui/assets/font-awesome/webfonts/fa-regular-400.woff2
Normal file
BIN
ui/assets/font-awesome/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
ui/assets/font-awesome/webfonts/fa-solid-900.ttf
Normal file
BIN
ui/assets/font-awesome/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
ui/assets/font-awesome/webfonts/fa-solid-900.woff2
Normal file
BIN
ui/assets/font-awesome/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
ui/assets/navball-cursor.png
Normal file
BIN
ui/assets/navball-cursor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 665 B |
BIN
ui/assets/navball.png
Normal file
BIN
ui/assets/navball.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
BIN
ui/assets/navball2.png
Normal file
BIN
ui/assets/navball2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
BIN
ui/favicon.png
Normal file
BIN
ui/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
241
ui/index.html
241
ui/index.html
@ -1,11 +1,240 @@
|
||||
<!Doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Apollo v0.0.0</title>
|
||||
</head>
|
||||
<head>
|
||||
<title>Apollo v0.0.0</title>
|
||||
<link href="./assets/bootstrap.min.css" rel="stylesheet"/>
|
||||
<link href="./assets/font-awesome/css/font-awesome.css" rel="stylesheet"/>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1a1a1a;
|
||||
color: #aaa;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
<body>
|
||||
<h1>Apollo v0.0.0</h1>
|
||||
</body>
|
||||
h1{
|
||||
color: white;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6, p {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
|
||||
.navball {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: url('./assets/navball2.png') repeat;
|
||||
background-size: 360px;
|
||||
transition: all 1s linear;
|
||||
}
|
||||
|
||||
.navball-container {
|
||||
position: relative;
|
||||
height: 180px;
|
||||
width: 180px;
|
||||
border: #aaa 2px solid;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navball-cursor {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.no-signal {
|
||||
top: 50%;
|
||||
background: red;
|
||||
border: darkred;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.no-signal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
|
||||
backdrop-filter: blur(5px); /* Optional blur effect */
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline;
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
width: 62px;
|
||||
text-align: center;
|
||||
color: white !important;
|
||||
background: grey;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="px-3 pt-3">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<h1 style="font-size: 1rem;">Apollo Control</h1>
|
||||
</div>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div class="text-muted" style="font-size: 1rem">
|
||||
<i class="fa fa-signal mx-2"></i>
|
||||
<i class="fa fa-location-crosshairs mx-2"></i>
|
||||
<span class="battery mx-2">0</span>% <i class="battery-icon fa fa-battery"></i>
|
||||
<span class="mx-2"><span class="time">00:00:00</span> UTC</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex">
|
||||
<div style="width: 250px">
|
||||
|
||||
<div class="card m-3 p-3">
|
||||
<h2 class="mb-1" style="font-size: 1rem">Altitude</h2>
|
||||
<hr class="my-1" style="border-color: #aaa">
|
||||
</div>
|
||||
|
||||
<div class="card m-3 p-3">
|
||||
<h2 class="mb-1" style="font-size: 1rem">Model</h2>
|
||||
<hr class="my-1" style="border-color: #aaa">
|
||||
</div>
|
||||
|
||||
<div class="card m-3 p-3">
|
||||
<h2 class="mb-1" style="font-size: 1rem">Nav Ball</h2>
|
||||
<hr class="mt-1 mb-2" style="border-color: #aaa">
|
||||
<div class="navball-container">
|
||||
<div class="navball"></div>
|
||||
<img class="navball-cursor" src="./assets/navball-cursor.png" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1 my-3">
|
||||
|
||||
<div class="card flex-grow-1 p-3 mb-3"></div>
|
||||
|
||||
<div class="card p-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<p class="mb-0">T-0</p>
|
||||
<p class="mb-0">T+<span class="t-plus">0</span></p>
|
||||
</div>
|
||||
<input type="range" min="1" max="100" value="50" class="slider" id="myRange">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="width: 250px">
|
||||
|
||||
<div class="d-flex flex-wrap m-3">
|
||||
<div class="status status-bat">BAT</div>
|
||||
<div class="status status-env">ENV</div>
|
||||
<div class="status status-gps">GPS</div>
|
||||
<div class="status status-imu">IMU</div>
|
||||
<div class="status status-tel">TEL</div>
|
||||
</div>
|
||||
|
||||
<div class="card m-3 p-3">
|
||||
<h2 class="mb-1" style="font-size: 1rem">Position</h2>
|
||||
<hr class="my-1" style="border-color: #aaa">
|
||||
<p class="mb-1">Altitude: <span class="altitude">0</span> m</p>
|
||||
<p class="mb-1">Azimuth: <span class="azimuth">0</span>°</p>
|
||||
<p class="mb-1">Coordinates: <span class="latlng">0, 0</span></p>
|
||||
</div>
|
||||
|
||||
<div class="card m-3 p-3">
|
||||
<h2 class="mb-1" style="font-size: 1rem">Sensors</h2>
|
||||
<hr class="my-1" style="border-color: #aaa">
|
||||
<p class="mb-1">Battery: <span class="battery">0</span>%</p>
|
||||
<p class="mb-1">Bat. Temp: <span class="battery-temperature">0</span>°C</p>
|
||||
<p class="mb-1">Humidity: <span class="humidity">0</span>%</p>
|
||||
<p class="mb-1">Pressure: <span class="pressure">0</span> hPa</p>
|
||||
<p class="mb-1">Temperature: <span class="temperature">0</span>°C</p>
|
||||
<p class="mb-1">Voltage: <span class="voltage">0</span> v</p>
|
||||
</div>
|
||||
|
||||
<div class="card m-3 p-3">
|
||||
<h2 class="mb-1" style="font-size: 1rem">Telemetry</h2>
|
||||
<hr class="my-1" style="border-color: #aaa">
|
||||
<p class="mb-1">Strength: <span class="azimuth">0</span> db</p>
|
||||
<p class="mb-1">TX/RX: <span class="azimuth">0</span>/0</p>
|
||||
<p class="mb-1">WiFi: AP Mode</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog class="no-signal">
|
||||
<i class="fa fa-tower-broadcast pe-2"></i><strong>NO SIGNAL</strong>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
const remote = localStorage.getItem('remote') || '';
|
||||
|
||||
function run(cmd) {
|
||||
return fetch(`${remote}/api/${cmd}`).then(async resp => {
|
||||
const value = await resp.text();
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}).catch(err => err.message);
|
||||
}
|
||||
|
||||
const battery = document.querySelectorAll('.battery');
|
||||
const batteryTemp = document.querySelectorAll('.battery-temperature');
|
||||
const batteryIcon = document.querySelectorAll('.battery-icon');
|
||||
const humidity = document.querySelectorAll('.humidity');
|
||||
const navball = document.querySelectorAll('.navball');
|
||||
const pressure = document.querySelectorAll('.pressure');
|
||||
const statusBat = document.querySelectorAll('.status-bat');
|
||||
const statusEnv = document.querySelectorAll('.status-env');
|
||||
const statusImu = document.querySelectorAll('.status-imu');
|
||||
const statusGps = document.querySelectorAll('.status-gps');
|
||||
const statusTel = document.querySelectorAll('.status-tel');
|
||||
const temperature = document.querySelectorAll('.temperature');
|
||||
const time = document.querySelectorAll('.time');
|
||||
const tPlus = document.querySelectorAll('.t-plus');
|
||||
const voltage = document.querySelectorAll('.voltage');
|
||||
|
||||
let count = 0;
|
||||
setInterval(async () => {
|
||||
count++;
|
||||
const now = new Date();
|
||||
time.forEach(t => t.innerHTML = `${now.getUTCHours().toString().padStart(2, 0)}:${now.getMinutes().toString().padStart(2, 0)}:${now.getSeconds().toString().padStart(2, 0)}`);
|
||||
tPlus.forEach(t => t.innerHTML = count.toString());
|
||||
|
||||
run('sensors').then(sensors => {
|
||||
battery.forEach(b => b.innerHTML = (sensors.battery?.percentage || 0) * 100);
|
||||
batteryTemp.forEach(bt => bt.innerHTML = sensors.battery?.temperature || 0);
|
||||
batteryIcon.forEach(bi => bi.className = sensors.battery?.charging ? 'fa fa-bolt' : 'fa fa-battery');
|
||||
humidity.forEach(h => h.innerHTML = sensors.environment?.humidity ?? 0);
|
||||
pressure.forEach(p => p.innerHTML = sensors.environment?.pressure ?? 0);
|
||||
temperature.forEach(t => t.innerHTML = sensors.environment?.temperature ?? 0);
|
||||
voltage.forEach(v => v.innerHTML = sensors.battery?.voltage || 0);
|
||||
});
|
||||
|
||||
run('status').then(status => {
|
||||
statusBat.forEach(sb => sb.style.background = status['bms'] === 'ok' ? 'green' : 'red');
|
||||
statusEnv.forEach(se => se.style.background = status['bme280'] === 'ok' ? 'green' : 'red');
|
||||
statusImu.forEach(sb => sb.style.background = status['imu'] === 'ok' ? 'green' : 'red');
|
||||
});
|
||||
|
||||
navball.forEach(n => n.style.transform = `rotate(${count * 5}deg)`);
|
||||
navball.forEach(n => n.style.backgroundPositionY = `${count}px`);
|
||||
}, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Reference in New Issue
Block a user