Compare commits

..

22 Commits

Author SHA1 Message Date
16ea6dafe3 Fixed IMU address 2024-10-30 11:29:35 -04:00
2e580b44d2 Added IMU status light 2024-10-30 11:23:54 -04:00
4068b8bb01 Added IMU 2024-10-30 11:22:19 -04:00
a9a8cf2745 Calibrate the temp sensor 2024-10-30 11:16:24 -04:00
85e6ab01ae Fixed startup command 2024-10-30 11:06:19 -04:00
31585c4b6b Log server start 2024-10-30 11:05:21 -04:00
1de19b208a Temp fix 2024-10-30 11:03:30 -04:00
72800546a8 Added environment data to dashboard 2024-10-30 10:41:03 -04:00
287a28d065 Fixed ENV status light 2024-10-30 10:37:08 -04:00
efc56aae40 Fix API URL 2024-10-30 10:35:09 -04:00
a127bcfbbd Merge branch 'master' of https://git.zakscode.com/ztimson/Apollo 2024-10-30 10:28:21 -04:00
f5e1d73988 Fixed moved utilies 2024-10-30 14:27:55 +00:00
90456f5800 Added missing ui 2024-10-30 10:27:33 -04:00
00f11f8c08 Merge 2024-10-30 14:22:07 +00:00
3e06856c70 Added cors 2024-10-30 14:20:55 +00:00
4b70f8d67b UI updates 2024-10-30 10:19:47 -04:00
2144d7ef69 Fixes 2024-10-29 00:39:52 +00:00
02a533358e fixed bugs 2024-10-29 00:23:56 +00:00
a28c18a1d6 Lots of refactoring 2024-10-28 20:17:38 -04:00
19f76c3f10 Update README.md 2024-10-28 16:31:38 -04:00
8d30c10f18 Merge branch 'master' of https://git.zakscode.com/ztimson/Apollo 2024-10-28 20:29:54 +00:00
9178b0a83f Added bms monitoring script 2024-10-28 20:28:50 +00:00
28 changed files with 583 additions and 232 deletions

View File

@ -3,25 +3,7 @@
## Setup ## Setup
```sh ```sh
sudo apt update && sudo apt upgrade -y sudo apt update && sudo apt upgrade -y
sudo apt install htop i2c-tools sudo apt install htop i2c-tools hwclock
raspi-config # enable i2c raspi-config # enable i2c & UART
``` echo "@reboot root hwclock -s" >> /etc/crontab
### 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
``` ```

40
package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bme280-sensor": "^0.1.7", "@ztimson/utils": "^0.21.6",
"cors": "^2.8.5",
"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"
@ -220,6 +221,12 @@
"url": "https://opencollective.com/serialport/donate" "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -246,15 +253,6 @@
"file-uri-to-path": "1.0.0" "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": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -358,6 +356,19 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT" "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": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -815,6 +826,15 @@
"node-gyp-build-test": "build-test.js" "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": { "node_modules/object-inspect": {
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",

View File

@ -5,12 +5,13 @@
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node src/main.js" "apollo": "node src/main.js"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bme280-sensor": "^0.1.7", "cors": "^2.8.5",
"@ztimson/utils": "^0.21.6",
"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;

35
src/bme280.js Normal file
View 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
View 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
View 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 },
};
}

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}`;
}
}

53
src/daemon.js Normal file
View 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}`;
}
}

View File

@ -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);
}
}

View File

@ -1,14 +1,54 @@
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.slice(2).join(' ');
const apollo = new Apollo(); let remote = 'localhost:1969';
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(); Remote: ${remote}
// serial.start();
})(); 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');
}
}

View File

@ -2,11 +2,11 @@ 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 ask(prompt, hide = false) { 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));
}

View File

@ -1,46 +1,57 @@
import BME280 from 'bme280-sensor'; import {adjustedInterval, sleep} from '@ztimson/utils';
import {poll} from './misc.js'; import {bms} from './bms.js';
import {bme} from './bme280.js';
import {imu} from './bno080.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 = {}
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() { 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() { 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 statusWrapper(promise, key) {
this.stopBme280 = poll(async () => { return promise.then(resp => {
const d = await this.bme280.readSensorData(); this.status[key] = 'ok'
this._data.humidity = d.humidity / 100; return resp;
this._data.temperature = d.temperature_C; }).catch(err => {
this._data.pressure = d.pressure_hPa; this.status[key] = err.message;
this._data.altitude = BME280.calculateAltitudeMeters(this.data.pressure); });
}, 1000);
} }
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);
}
}

6
ui/assets/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

BIN
ui/assets/navball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
ui/assets/navball2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
ui/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -1,11 +1,240 @@
<!Doctype html> <!Doctype html>
<html> <html>
<head> <head>
<title>Apollo v0.0.0</title> <title>Apollo v0.0.0</title>
</head> <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{
<h1>Apollo v0.0.0</h1> color: white;
</body> }
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>&deg;</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>&deg;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>&deg;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> </html>