Lots of refactoring
This commit is contained in:
		
							
								
								
									
										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": "", | ||||
|   "license": "MIT", | ||||
|   "dependencies": { | ||||
|     "bme280-sensor": "^0.1.7", | ||||
|     "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; | ||||
|   | ||||
							
								
								
									
										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'; | ||||
|  | ||||
| async function bmsStatus(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), | ||||
|   ]); | ||||
|   return { | ||||
|     charging: !!((data[0] >> 7) & 1), | ||||
|     percentage: data[4] / 100, | ||||
|     temperature: data[1] - 40, | ||||
|     voltage: ((data[2] << 8) | data[3]) / 1000, | ||||
|   } | ||||
| 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), | ||||
|     ]); | ||||
|     return { | ||||
|         charging: !!((data[0] >> 7) & 1), | ||||
|         percentage: data[4] / 100, | ||||
|         temperature: data[1] - 40, | ||||
|         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 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[1]; | ||||
| let remote; | ||||
|  | ||||
| function help() { | ||||
|     return ` | ||||
| Apollo v0.0.0 | ||||
|  | ||||
| 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'; | ||||
|  | ||||
| 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 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) { | ||||
| @@ -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) { | ||||
|     return new Promise(res => setTimeout(res, ms)); | ||||
| } | ||||
|   | ||||
| @@ -1,50 +1,51 @@ | ||||
| import BME280 from 'bme280-sensor'; | ||||
| import {poll} from './misc.js'; | ||||
| import {adjustedInterval, sleep} from './misc.js'; | ||||
| import {bms} from './bms.js'; | ||||
| import {bme} from './bme280.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 = {} | ||||
|  | ||||
|     _env_sensor = false; | ||||
|     get status() { return { env_sensor: this._env_sensor ? 'ok' : 'failed' }; } | ||||
|     #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}); | ||||
|     } | ||||
|  | ||||
|     start() { | ||||
|         return this.bme280.init().then(() => { | ||||
|           // Poll environmental data | ||||
|           this._env_sensor = true; | ||||
|           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); | ||||
|     static #hPaToAltitude(hPa) { | ||||
|         return 44330 * (1 - Math.pow(hPa / 1013.25, 1 / 5.255)); | ||||
|     } | ||||
|  | ||||
|     async start() { | ||||
|         this.intervals.push(adjustedInterval(async () => { | ||||
|             this.data.environment = await this.statusWrapper(bme(), 'bme280'); | ||||
|             this.data.environment.altitude = SensorSuite.#hPaToAltitude(this.data.environment.pressure); | ||||
|         }, 1000)); | ||||
|         await sleep(500); // Offset reading sensor data | ||||
|         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 => { | ||||
|           this._env_sensor = false; | ||||
|             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); | ||||
|   } | ||||
| } | ||||
| @@ -3,9 +3,43 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <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> | ||||
|  | ||||
|   <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> | ||||
| </html> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user