Compare commits
59 Commits
Author | SHA1 | Date |
---|---|---|
Skylar Grant | 0b0a09942d | |
Skylar Grant | 67769232cb | |
Skylar Grant | 38c4a6a644 | |
Skylar Grant | e6b64b340e | |
Skylar Grant | 754150a91b | |
Skylar Grant | 1e1d3488c2 | |
Skylar Grant | 2bb376868d | |
Skylar Grant | 057b2a6b3a | |
Skylar Grant | 87631c8aa7 | |
Skylar Grant | 7dfafc95d9 | |
Skylar Grant | 3ea70b0898 | |
Skylar Grant | 8f4bf90334 | |
Skylar Grant | 5c852f2c97 | |
Skylar Grant | f4cfbb71cf | |
Skylar Grant | 38b767034d | |
Skylar Grant | a7f9a7b6fb | |
Skylar Grant | a673b15ab9 | |
Skylar Grant | 9f5811d90f | |
Skylar Grant | 299a8b2efa | |
Skylar Grant | 6ee01cc214 | |
Skylar Grant | 53e3042940 | |
Skylar Grant | afce8a68a7 | |
Skylar Grant | da745e212f | |
Skylar Grant | 44104f11c1 | |
Skylar Grant | 5393f93a97 | |
Skylar Grant | f26090e79a | |
Skylar Grant | df229db09e | |
Skylar Grant | 6c0d5b5b04 | |
Skylar Grant | 72923d94ca | |
Skylar Grant | 7ff3ceab96 | |
Skylar Grant | eee6d27375 | |
Skylar Grant | d8a6ea9577 | |
Skylar Grant | 5b59d6f38f | |
Skylar Grant | cbcc182e9c | |
Skylar Grant | 21f076f403 | |
Skylar Grant | df349afede | |
Skylar Grant | 69e41e6f0d | |
Skylar Grant | f37ff53bb6 | |
Skylar Grant | d7a32e18ce | |
Skylar Grant | dd2dc0ac65 | |
Skylar Grant | 7cf4677791 | |
Skylar Grant | 81d34723f1 | |
Skylar Grant | d2769fad5e | |
Skylar Grant | 1532d6e3d3 | |
Skylar Grant | 4daec6513d | |
Skylar Grant | e6bcf90741 | |
Skylar Grant | 7f58dc87ac | |
Skylar Grant | c505c858dc | |
Skylar Grant | 88206c8208 | |
Skylar Grant | 8a4ab13b0c | |
Skylar Grant | 7c19ee0bdc | |
Skylar Grant | c982cd535a | |
Skylar Grant | d177706bb1 | |
Skylar Grant | 72f814b016 | |
Skylar Grant | 85bf00a35a | |
Skylar Grant | 5da303a24a | |
Skylar Grant | dfb56ab68c | |
Skylar Grant | 283f963e38 | |
Skylar Grant | b7f9bacfa1 |
|
@ -8,6 +8,12 @@ lerna-debug.log*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
.vscode
|
.vscode
|
||||||
|
.vscode/*
|
||||||
|
config.json
|
||||||
|
log.txt
|
||||||
|
nohup.out
|
||||||
|
data/config.db
|
||||||
|
config.db
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
"skipFiles": [
|
"skipFiles": [
|
||||||
"<node_internals>/**"
|
"<node_internals>/**"
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/websvr.js"
|
"program": "${workspaceFolder}/main.js"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -16,10 +16,10 @@
|
||||||
// "description": "Log output to console"
|
// "description": "Log output to console"
|
||||||
// }
|
// }
|
||||||
|
|
||||||
"Log if in Debug mode": {
|
"Hestia Debug Log": {
|
||||||
"scope": "javascript",
|
"scope": "javascript",
|
||||||
"prefix": "log",
|
"prefix": "log",
|
||||||
"body": "if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: $1`);\n$0",
|
"body": "if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: $1`);$0",
|
||||||
"description": "Log output to console if in debug mode"
|
"description": "Log output to console if in debug mode"
|
||||||
},
|
},
|
||||||
"Run only on Pi": {
|
"Run only on Pi": {
|
||||||
|
@ -27,5 +27,17 @@
|
||||||
"prefix": "onpi",
|
"prefix": "onpi",
|
||||||
"body": "if (process.env.ONPI == 'true') {\n\t$1\n} else {\n\t$2\n}$0",
|
"body": "if (process.env.ONPI == 'true') {\n\t$1\n} else {\n\t$2\n}$0",
|
||||||
"description": "Run something only if the ONPI env var is set"
|
"description": "Run something only if the ONPI env var is set"
|
||||||
}
|
},
|
||||||
|
"Hestia Error": {
|
||||||
|
"scope": "javascript",
|
||||||
|
"prefix": "err",
|
||||||
|
"body": "console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: $1`)$0",
|
||||||
|
"description": "Log an error to the console with timestamp"
|
||||||
|
},
|
||||||
|
"Hestia Log": {
|
||||||
|
"scope": "javascript",
|
||||||
|
"prefix": "log",
|
||||||
|
"body": "console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: $1`);$0",
|
||||||
|
"description": "Log output to console always"
|
||||||
|
},
|
||||||
}
|
}
|
50
README.md
50
README.md
|
@ -54,3 +54,53 @@ For ease of adaption, connection, and prototyping I've decided to use Cat 5 ethe
|
||||||
* Vacuum Switch OPEN after igniter start.
|
* Vacuum Switch OPEN after igniter start.
|
||||||
4. Test manipulation of feed rates.
|
4. Test manipulation of feed rates.
|
||||||
5. Test shutdown sequence.
|
5. Test shutdown sequence.
|
||||||
|
|
||||||
|
# SQLite Database Tables
|
||||||
|
|
||||||
|
## status
|
||||||
|
| Field | Type | Null | Key | Default | Extra |
|
||||||
|
| ----- | ---- | ---- | --- | ------- | ----- |
|
||||||
|
| id | int(10) | No | PRI | NULL | auto_increment |
|
||||||
|
| key | varchar(100) | No | | |
|
||||||
|
| value | varchar(1000) | No | | |
|
||||||
|
|
||||||
|
| id | key | value |
|
||||||
|
| -- | --- | ----- |
|
||||||
|
0 | igniter | 0
|
||||||
|
1 | blower | 0
|
||||||
|
2 | auger | 0
|
||||||
|
3 | igniter_finished | false
|
||||||
|
4 | shutdown_initiated | 0
|
||||||
|
5 | vacuum | 0
|
||||||
|
6 | proof_of_fire | 0
|
||||||
|
7 | shutdown_next_cycle | 0
|
||||||
|
|
||||||
|
## timestamps
|
||||||
|
| Field | Type | Null | Key | Default | Extra |
|
||||||
|
| ----- | ---- | ---- | --- | ------- | ----- |
|
||||||
|
| id | int(10) | No | PRI | NULL | auto_increment |
|
||||||
|
| key | varchar(100) | No | | |
|
||||||
|
| value | varchar(1000) | No | | |
|
||||||
|
|
||||||
|
| id | key | value |
|
||||||
|
| -- | --- | ----- |
|
||||||
|
0 | process_start | 0
|
||||||
|
1 | blower_on | 0
|
||||||
|
2 | blower_off | 0
|
||||||
|
3 | igniter_on | 0
|
||||||
|
4 | igniter_off | 0
|
||||||
|
|
||||||
|
## intervals
|
||||||
|
| Field | Type | Null | Key | Default | Extra |
|
||||||
|
| ----- | ---- | ---- | --- | ------- | ----- |
|
||||||
|
| id | int(10) | No | PRI | NULL | auto_increment |
|
||||||
|
| key | varchar(100) | No | | |
|
||||||
|
| value | varchar(1000) | No | | |
|
||||||
|
|
||||||
|
| id | key | value |
|
||||||
|
| -- | --- | ----- |
|
||||||
|
0 | auger_on | 600
|
||||||
|
1 | auger_off | 1400
|
||||||
|
2 | pause | 5000
|
||||||
|
3 | igniter_start | 420000
|
||||||
|
4 | blower_stop | 600000
|
|
@ -0,0 +1,8 @@
|
||||||
|
Logic Goal:
|
||||||
|
|
||||||
|
1. Start Script & Web Server
|
||||||
|
2. Await a command from the user.
|
||||||
|
3. Startup enables auger
|
||||||
|
4. If auger is enabled, run the cycle command.
|
||||||
|
5. Once per cycle, read then rewrite the config file for interoperability
|
||||||
|
6. Set feed rate based on config.
|
|
@ -1 +0,0 @@
|
||||||
{"debugMode":true,"status":{"igniter":0,"blower":0,"auger":0,"igniterFinished":true,"shutdown":1,"vacuum":0,"pof":0},"timestamps":{"procStart":1672761393394,"blowerOn":1672761445945,"blowerOff":1672761459451,"igniterOn":1672761445946,"igniterOff":1672761447442},"intervals":{"augerOn":500,"augerOff":1500,"pause":3000,"igniterStart":5000,"blowerStop":5000},"web":{"port":8080,"ip":"0.0.0.0"}}
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"database": {
|
||||||
|
"createConfigTable": ""
|
||||||
|
}
|
||||||
|
}
|
526
functions.js
526
functions.js
|
@ -1,526 +0,0 @@
|
||||||
// TODOs: Add tests for PoF and Vacuum switches, add delays for shutting down blower, test logic for igniter
|
|
||||||
|
|
||||||
// TODO: Move these to config
|
|
||||||
// Physical Pin numbers for GPIO
|
|
||||||
const augerPin = 26; // Pin for controlling the relay for the pellet auger motor.
|
|
||||||
const igniterPin = 13; // Pin for controlling the relay for the igniter.
|
|
||||||
const blowerPin = 15; // Pin for controlling the relay for the combustion blower/exhaust.
|
|
||||||
const pofPin = 16; // Pin for sensing the status (open/closed) of the Proof of Fire switch.
|
|
||||||
// const tempPin = 18; // Pin for receiving data from a DS18B20 OneWire temperature sensor.
|
|
||||||
const vacuumPin = 22; // Pin for sensing the status (open/closed) of the vacuum switch.
|
|
||||||
|
|
||||||
// Require the package for pulling version numbers
|
|
||||||
const package = require('./package.json');
|
|
||||||
// Import the config file
|
|
||||||
var config = require('./config.json');
|
|
||||||
|
|
||||||
// Get environment variables
|
|
||||||
const dotenv = require('dotenv').config();
|
|
||||||
// Module for working with files
|
|
||||||
const fs = require('fs');
|
|
||||||
const { time } = require('console');
|
|
||||||
|
|
||||||
|
|
||||||
// The functions we'll export to be used in other files
|
|
||||||
const functions = {
|
|
||||||
auger: {
|
|
||||||
// Gets called once the Auger Pin has been setup by rpi-gpio
|
|
||||||
ready(err) {
|
|
||||||
if (err) throw err;
|
|
||||||
console.log('Auger GPIO Ready');
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
// Turns the auger on (Pin 7 high)
|
|
||||||
on(gpio) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.write(augerPin, true, function(err) {
|
|
||||||
if (err) throw err;
|
|
||||||
resolve('Auger turned on.');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve('Simulated auger turned on.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
// Turns the auger off (pin 7 low)
|
|
||||||
off(gpio) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.write(augerPin, false, function(err) {
|
|
||||||
if (err) throw err;
|
|
||||||
resolve('Auger turned off.');
|
|
||||||
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve('Simulated auger turned off.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
// Cycles the auger using the two functions above this one (functions.auger.on() and functions.auger.off())
|
|
||||||
// Sleeps in between cycles using functions.sleep()
|
|
||||||
cycle(gpio) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Turn the auger on
|
|
||||||
this.on(gpio).then((res) => {
|
|
||||||
// Log action if in debug mode
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Sleep for the time set in env variables
|
|
||||||
functions.sleep(config.intervals.augerOn).then((res) => {
|
|
||||||
// Log action if in debug mode
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Turn the auger off
|
|
||||||
this.off(gpio).then((res) => {
|
|
||||||
// Log action if in debug mode
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Sleep for the time set in env variables
|
|
||||||
functions.sleep(config.intervals.augerOff).then((res) => {
|
|
||||||
// Log action if in debug mode
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Resolve the promise, letting the main script know the cycle is complete
|
|
||||||
resolve("Auger cycled.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
blower: {
|
|
||||||
blocksShutdown() {
|
|
||||||
// If the current time is past the blowerOff timestamp, we can turn finish shutting down the stove
|
|
||||||
if ((config.timestamps.blowerOff > 0) && (Date.now() > config.timestamps.blowerOff)) {
|
|
||||||
return false;
|
|
||||||
// Otherwise, return true because we're not ready to shutdown yet
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
// Check for a preset-list of files in the root directory of the app
|
|
||||||
check() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Check for pause file existing
|
|
||||||
if (fs.existsSync('./pause')) {
|
|
||||||
// Resolve the promise, letting the main script know what we found
|
|
||||||
resolve("pause");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for reload file existing
|
|
||||||
if (fs.existsSync('./reload')) {
|
|
||||||
// Resolve the promise, letting the main script know what we found
|
|
||||||
resolve("reload");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for quit file existing
|
|
||||||
if (fs.existsSync('./quit')) {
|
|
||||||
// Resolve the promise, letting the main script know what we found
|
|
||||||
resolve("quit");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for ignite file existing
|
|
||||||
if (fs.existsSync('./ignite')) {
|
|
||||||
resolve('ignite');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for start file existing
|
|
||||||
if (fs.existsSync('./start')) {
|
|
||||||
resolve('start');
|
|
||||||
}
|
|
||||||
// Resolve the promise, letting the main script know what we found (nothing)
|
|
||||||
resolve("none");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
commands: {
|
|
||||||
// Prepare the stove for starting
|
|
||||||
startup (gpio) {
|
|
||||||
fs.unlink('./start', (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
// Turn the combustion blower on
|
|
||||||
functions.power.blower.on(gpio).then(res => {
|
|
||||||
resolve(`I: Combustion blower has been enabled.`);
|
|
||||||
}).catch(rej => {
|
|
||||||
reject(`E: There was a problem starting the combustion blower: ${rej}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(`I: Simulated combustion blower turned on.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// Pauses the script for the time defined in env variables
|
|
||||||
pause() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Pausing for ${config.intervals.pause}ms`);
|
|
||||||
|
|
||||||
functions.sleep(config.intervals.pause).then(() => { resolve(); });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// Reload the environment variables on the fly
|
|
||||||
reload(envs) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Re-require dotenv because inheritance in js sucks
|
|
||||||
const dotenv = require('dotenv').config({ override: true });
|
|
||||||
// Delete the reload file
|
|
||||||
fs.unlink('./reload', (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
if (config.debugMode) console.log('Deleted reload file.');
|
|
||||||
});
|
|
||||||
// Print out the new environment variables
|
|
||||||
// This should be printed regardless of debug status, maybe prettied up TODO?
|
|
||||||
console.log('Reloaded environment variables.');
|
|
||||||
console.log(`ONTIME=${config.intervals.augerOn}\nOFFTIME=${config.intervals.augerOff}\nPAUSETIME=${config.intervals.pause}\nDEBUG=${config.debugMode}\nONPI=${process.env.ONPI}`);
|
|
||||||
// Resolve the promise, letting the main script know we're done reloading the variables and the cycle can continue
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
// Shutdown the script gracefully
|
|
||||||
quit() {
|
|
||||||
// TODO add quit file detection, not always going to be quitting from files
|
|
||||||
// Delete the quit file
|
|
||||||
fs.unlink('./quit', (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
if (config.debugMode) console.log('Removed quit file.');
|
|
||||||
});
|
|
||||||
// Print out that the script is quitting
|
|
||||||
console.log('Quitting...');
|
|
||||||
// Quit the script
|
|
||||||
process.exit();
|
|
||||||
},
|
|
||||||
ignite(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Check if we got here from a file, then delete it.
|
|
||||||
if (fs.existsSync('./ignite')) fs.unlink('./ignite', (err) => { if (err) throw err; });
|
|
||||||
functions.power.blower.on(gpio).then(res => {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Turn on the igniter
|
|
||||||
functions.power.igniter.on(gpio).then(res => {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Enable the auger
|
|
||||||
config.status.auger = 1;
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger enabled.`);
|
|
||||||
|
|
||||||
resolve('Ignition sequence started successfully.');
|
|
||||||
}).catch(err => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
shutdown(gpio) {
|
|
||||||
// Only run if a shutdown isn't already started
|
|
||||||
if (config.status.shutdown == 0) {
|
|
||||||
// set shutdown flag to 1
|
|
||||||
config.status.shutdown = 1;
|
|
||||||
// Check if this was invoked from a 'quit' file, if so, delete the file
|
|
||||||
if (fs.existsSync('./quit')) fs.unlink('./quit', (err) => { if (err) throw err; });
|
|
||||||
// If the auger is enabled, disable it
|
|
||||||
if (config.status.auger == 1) {
|
|
||||||
config.status.auger = 0;
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger disabled.`);
|
|
||||||
}
|
|
||||||
// If the igniter is on, shut it off.
|
|
||||||
if (config.status.igniter == 1) {
|
|
||||||
functions.power.igniter.off(gpio).then(res => {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
}); // TODO catch an error here
|
|
||||||
}
|
|
||||||
// TODO Change this so it gives a delay after shutting down so smoke doesn't enter the house
|
|
||||||
if (config.status.blower == 1) {
|
|
||||||
// Set the timestamp to turn the blower off at
|
|
||||||
config.timestamps.blowerOff = Date.now() + config.intervals.blowerStop;
|
|
||||||
}
|
|
||||||
return "Shutdown has been initiated.";
|
|
||||||
} else {
|
|
||||||
// blower.blocksShutdown() returns false only if the blower shutdown has
|
|
||||||
// been initiated AND the specified cooldown time has passed
|
|
||||||
if(!(functions.blower.blocksShutdown())) {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Blower can be turned off.`);
|
|
||||||
functions.power.blower.off(gpio).then(res => {
|
|
||||||
// Since the blower shutting off is the last step in the shutdown, we can quit.
|
|
||||||
// TODO eventually we don't want to ever quit the program, so it can be restarted remotely
|
|
||||||
// functions.commands.quit();
|
|
||||||
config.status.shutdown = 0;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return "A shutdown has already been initiated and the blower is preventing shutdown.";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
writeConfig() {
|
|
||||||
fs.writeFile('./config.json', JSON.stringify(config), (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tests: {
|
|
||||||
vacuum(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (config.status.blower == 1) {
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.read(vacuumPin, (err, status) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
config.status.vacuum = status;
|
|
||||||
resolve(status);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
switch (config.status.vacuum) {
|
|
||||||
case 0:
|
|
||||||
resolve(false);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
resolve(true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reject('Unable to determine vacuum status.');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the blower isn't on, the vacuum doesn't matter so always return true
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
pof(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.read(pofPin, (err, status) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
config.status.pof = status;
|
|
||||||
resolve(status);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
switch (config.status.pof) {
|
|
||||||
case 0:
|
|
||||||
resolve(false);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
resolve(true);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reject('Unable to determine proof of fire status.');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
igniter(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Create a blank string to store the status message in as we build it
|
|
||||||
var statusMsg = "";
|
|
||||||
// Determine if the igniter is on
|
|
||||||
if (config.status.igniter == 1) {
|
|
||||||
statusMsg += "The igniter is on. ";
|
|
||||||
} else if (config.status.igniter == 0) {
|
|
||||||
statusMsg += "The igniter is off. ";
|
|
||||||
} else {
|
|
||||||
reject("Unable to determine igniter status.");
|
|
||||||
}
|
|
||||||
// Run this if the igniter has been turned on
|
|
||||||
if (config.timestamps.igniterOn > 0) {
|
|
||||||
if (Date.now() < config.timestamps.igniterOff && config.status.igniter == 1) {
|
|
||||||
statusMsg += `Started: ${functions.time(config.timestamps.igniterOn)}. `;
|
|
||||||
statusMsg += `Stopping: ${functions.time(config.timestamps.igniterOff)}. `;
|
|
||||||
}
|
|
||||||
// Shut the igniter off if it's past the waiting period
|
|
||||||
if ((Date.now() > config.timestamps.igniterOff) && (config.status.igniter == 1)) {
|
|
||||||
// if (process.env.ONPI == 'true') {
|
|
||||||
// gpio.write(igniterPin, false, (err) => {
|
|
||||||
// if (err) throw(err);
|
|
||||||
// config.status.igniter = 0;
|
|
||||||
// statusMsg += `${new Date().toISOString()} I: Turned off igniter.`;
|
|
||||||
// functions.tests.pof(gpio).then(res => {
|
|
||||||
// if (res) {
|
|
||||||
// config.status.seenFire = true;
|
|
||||||
// } else {
|
|
||||||
// reject(`E: No Proof of Fire after igniter shut off.`);
|
|
||||||
// }
|
|
||||||
// }).catch(rej => {
|
|
||||||
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// config.status.igniter = 0;
|
|
||||||
// statusMsg += `${new Date().toISOString()} I: Simulated igniter turned off.`;
|
|
||||||
// }
|
|
||||||
// TODO I think this needs to be moved elsewhere, it doesn't finish resolving before the resolve call on line 354 is called (344+10=354)
|
|
||||||
functions.power.igniter.off(gpio).then(res => {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
});
|
|
||||||
} else if ((Date.now() > config.timestamps.igniterOff) && (config.status.igniter == 0)) {
|
|
||||||
statusMsg += `The igniter was turned off at ${new Date(config.timestamps.igniterOff).toISOString()}.`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusMsg += 'The igniter hasn\'t been started yet.';
|
|
||||||
}
|
|
||||||
resolve(statusMsg);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
blowerOffDelay() {
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
power: {
|
|
||||||
igniter: {
|
|
||||||
on(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
config.timestamps.igniterOn = Date.now();
|
|
||||||
config.timestamps.igniterOff = Date.now() + config.intervals.igniterStart;
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.write(igniterPin, true, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
config.status.igniter = 1;
|
|
||||||
resolve('Igniter turned on.');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
config.status.igniter = 1;
|
|
||||||
resolve('Igniter turned on.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
off(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
config.timestamps.igniterOff = Date.now();
|
|
||||||
config.status.igniterFinished = true;
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.write(igniterPin, false, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
config.status.igniter = 0;
|
|
||||||
resolve('Igniter turned off.');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
config.status.igniter = 0;
|
|
||||||
resolve('Igniter turned off.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
blower: {
|
|
||||||
on(gpio) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
config.timestamps.blowerOn = Date.now();
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.write(blowerPin, true, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
config.status.blower = 1;
|
|
||||||
resolve('Blower turned on.');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
config.status.blower = 1;
|
|
||||||
resolve('Blower turned on.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
off(gpio) {
|
|
||||||
config.timestamps.blowerOff = Date.now();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
gpio.write(blowerPin, false, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
config.status.blower = 0;
|
|
||||||
resolve('Blower turned off.');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
config.status.blower = 0;
|
|
||||||
resolve('Blower turned off.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Sleeps for any given milliseconds
|
|
||||||
sleep(ms) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// if (config.debugMode) console.log(`Sleeping for ${ms}ms`);
|
|
||||||
// Function to be called when setTimeout finishes
|
|
||||||
const finish = () => {
|
|
||||||
// Resolve the promise
|
|
||||||
resolve(`Slept for ${ms}ms`);
|
|
||||||
};
|
|
||||||
// The actual sleep function, sleeps for ms then calls finish()
|
|
||||||
setTimeout(finish, ms);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// Initializes rpi-gpio, or resolves if not on a raspberry pi
|
|
||||||
init(gpio) {
|
|
||||||
fs.readFile('./templates/config.json', (err, data) => {
|
|
||||||
fs.writeFile('./config.json', data, (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
config = require('./config.json');
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// TODO this boot splash needs updating
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Boot/About/Info
|
|
||||||
console.log(`== Lennox Winslow PS40
|
|
||||||
== Pellet Stove Control Panel
|
|
||||||
== Author: Skylar Grant
|
|
||||||
== Version: v${package.version}
|
|
||||||
==
|
|
||||||
== Startup Time: ${new Date().toISOString()}
|
|
||||||
==
|
|
||||||
== Environment variables:
|
|
||||||
== == ONTIME=${config.intervals.augerOn}
|
|
||||||
== == OFFTIME=${config.intervals.augerOff}
|
|
||||||
== == PAUSETIME=${config.intervals.pause}
|
|
||||||
== == DEBUG=${config.debugMode}
|
|
||||||
== == ONPI=${process.env.ONPI}`);
|
|
||||||
// Set up GPIO 4 (pysical pin 7) as output, then call functions.auger.ready()
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
// Init the Auger pin
|
|
||||||
gpio.setup(augerPin, gpio.DIR_OUT, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
if (config.debugMode) console.log('== Auger pin initialized.');
|
|
||||||
// Init the igniter pin
|
|
||||||
gpio.setup(igniterPin, gpio.DIR_OUT, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
if (config.debugMode) console.log('== Igniter pin initialized.');
|
|
||||||
// Init the blower pin
|
|
||||||
gpio.setup(blowerPin, gpio.DIR_OUT, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
if (config.debugMode) console.log('== Combustion blower pin initialized.');
|
|
||||||
// Init the Proof of Fire pin
|
|
||||||
gpio.setup(pofPin, gpio.DIR_IN, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
if (config.debugMode) console.log('== Proof of Fire pin initialized.');
|
|
||||||
// Init the Vacuum Switch pin
|
|
||||||
gpio.setup(vacuumPin, gpio.DIR_IN, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
if (config.debugMode) console.log('== Vacuum Switch pin initialized.');
|
|
||||||
// Resolve the promise now that all pins have been initialized
|
|
||||||
resolve('== GPIO Initialized.');
|
|
||||||
});
|
|
||||||
// Init the Temp Sensor pin
|
|
||||||
// gpio.setup(tempPin, gpio.DIR_IN, (err) => {
|
|
||||||
// if (err) reject(err);
|
|
||||||
// if (config.debugMode) console.log('== Temperature pin initialized.');
|
|
||||||
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Resolve the promise
|
|
||||||
resolve('== GPIO Not Available');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
time(stamp) {
|
|
||||||
const time = new Date(stamp);
|
|
||||||
return `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export the above object, functions, as a module
|
|
||||||
module.exports = { functions };
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#####################################################
|
||||||
|
# Interactive script for managing Hestia Web Portal #
|
||||||
|
#####################################################
|
||||||
|
# Formatting Tips:
|
||||||
|
# https://misc.flogisoft.com/bash/tip_colors_and_formatting
|
||||||
|
#
|
||||||
|
# Formatting:
|
||||||
|
# \e[1m - Bold
|
||||||
|
# \e[2m - Dim
|
||||||
|
# \e[8m - Hidden (passwords)
|
||||||
|
#
|
||||||
|
# Reset:
|
||||||
|
# \e[0m - Reset All Attributes
|
||||||
|
# \e[21m - Reset Bold/Bright
|
||||||
|
# \e[22m - Reset Dim
|
||||||
|
# \e[28m - Reset Hidden
|
||||||
|
#
|
||||||
|
# Colors:
|
||||||
|
# \e[39m - Default Foreground Color
|
||||||
|
# \e[30m - Black
|
||||||
|
# \e[31m - Red
|
||||||
|
# \e[32m - Green
|
||||||
|
# \e[34m - Blue
|
||||||
|
#####################################################
|
||||||
|
|
||||||
|
# Some initial variables to work with
|
||||||
|
timestamp=$(date "+%Y%m%d_%H%M")
|
||||||
|
filename="backup_$timestamp.tar.gz"
|
||||||
|
|
||||||
|
# Initial Prompt
|
||||||
|
# Bash allows for linebreaks in string literals and will
|
||||||
|
# break lines accordingly in the shell
|
||||||
|
echo -e "
|
||||||
|
[ Hestia Control Panel ]
|
||||||
|
|
||||||
|
This script is being run from: '$(pwd)'
|
||||||
|
Active Nodes: $(ps ax -o pid,user,command | grep 'node main.js' | grep -v grep)
|
||||||
|
|
||||||
|
Please enter an option from below:
|
||||||
|
|
||||||
|
[1] Launch Hestia Web Portal
|
||||||
|
[2] Quit Hestia Web Portal
|
||||||
|
[3] View the logs
|
||||||
|
[4] Update Hestia
|
||||||
|
[5] Set up database
|
||||||
|
|
||||||
|
[0] Quit Control Panel"
|
||||||
|
|
||||||
|
# Wait for input
|
||||||
|
read -p "Option: " opt
|
||||||
|
|
||||||
|
# Execute the correct commands based on input.
|
||||||
|
case "$opt" in
|
||||||
|
1)
|
||||||
|
# Launch Hestia Web Portal
|
||||||
|
clear
|
||||||
|
echo "Launching Hestia Web Portal"
|
||||||
|
nohup node main.js > log.txt &
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
# Quit Hestia Web Portal
|
||||||
|
clear
|
||||||
|
echo "Quitting Hestia Web Portal Gracefully"
|
||||||
|
touch quit
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
# View logs
|
||||||
|
clear
|
||||||
|
less +F log.txt
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
# Update Hestia
|
||||||
|
rm data/config.db
|
||||||
|
git pull
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
# Set up database
|
||||||
|
node modules/_setupdb.js
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
# Exit the script
|
||||||
|
clear
|
||||||
|
echo "Quitting..."
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
clear
|
||||||
|
echo "Invalid Option!"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exec ./hestia.sh
|
56
log.txt
56
log.txt
|
@ -1,56 +0,0 @@
|
||||||
[0] I: Not running on a Raspberry Pi.
|
|
||||||
== Lennox Winslow PS40
|
|
||||||
== Pellet Stove Control Panel
|
|
||||||
== Author: Skylar Grant
|
|
||||||
== Version: v0.2.1
|
|
||||||
==
|
|
||||||
== Startup Time: 2023-01-03T15:56:33.395Z
|
|
||||||
==
|
|
||||||
== Environment variables:
|
|
||||||
== == ONTIME=500
|
|
||||||
== == OFFTIME=1500
|
|
||||||
== == PAUSETIME=3000
|
|
||||||
== == DEBUG=true
|
|
||||||
== == ONPI=false
|
|
||||||
[0.009] I: == GPIO Not Available
|
|
||||||
[0.01] I: Pausing for 3000ms
|
|
||||||
[3.013] I: Pausing for 3000ms
|
|
||||||
[6.015] I: Pausing for 3000ms
|
|
||||||
[9.017] I: Pausing for 3000ms
|
|
||||||
[12.019] I: Pausing for 3000ms
|
|
||||||
[15.022] I: Pausing for 3000ms
|
|
||||||
[18.025] I: Pausing for 3000ms
|
|
||||||
[21.027] I: Pausing for 3000ms
|
|
||||||
[24.028] I: Pausing for 3000ms
|
|
||||||
[27.03] I: Pausing for 3000ms
|
|
||||||
[30.032] I: Pausing for 3000ms
|
|
||||||
[33.034] I: Pausing for 3000ms
|
|
||||||
[36.037] I: Pausing for 3000ms
|
|
||||||
[39.039] I: Pausing for 3000ms
|
|
||||||
[42.041] I: Pausing for 3000ms
|
|
||||||
[45.042] I: Pausing for 3000ms
|
|
||||||
[48.044] I: Pausing for 3000ms
|
|
||||||
[51.046] I: Pausing for 3000ms
|
|
||||||
[52.551] I: Blower turned on.
|
|
||||||
[52.552] I: Igniter turned on.
|
|
||||||
[52.552] I: Auger enabled.
|
|
||||||
[54.047] I: The igniter is on. Started: 10:57:25. Stopping: 10:57:30.
|
|
||||||
[54.048] I: Auger disabled.
|
|
||||||
Shutdown has been initiated.
|
|
||||||
[54.048] I: Igniter turned off.
|
|
||||||
[54.048] I: Pausing for 3000ms
|
|
||||||
A shutdown has already been initiated and the blower is preventing shutdown.
|
|
||||||
[57.05] I: Pausing for 3000ms
|
|
||||||
[60.052] I: Blower can be turned off.
|
|
||||||
Shutting down...
|
|
||||||
[60.053] I: Pausing for 3000ms
|
|
||||||
Shutdown has been initiated.
|
|
||||||
[63.055] I: Pausing for 3000ms
|
|
||||||
[66.057] I: Blower can be turned off.
|
|
||||||
Shutting down...
|
|
||||||
[66.057] I: Pausing for 3000ms
|
|
||||||
Shutdown has been initiated.
|
|
||||||
[69.059] I: Pausing for 3000ms
|
|
||||||
[72.06] I: Blower can be turned off.
|
|
||||||
Shutting down...
|
|
||||||
[72.06] I: Pausing for 3000ms
|
|
233
main.js
233
main.js
|
@ -1,182 +1,91 @@
|
||||||
/* Pellet Stove Control Panel
|
const fn = require('./modules/functions.js').functions;
|
||||||
* Written by Skylar Grant
|
// Import the config file
|
||||||
* v0.2
|
var config = require('./templates/config.json');
|
||||||
*
|
// Database Functions
|
||||||
* TODO:
|
const dbfn = require('./modules/database.js');
|
||||||
* Update documentation
|
// Web Portal
|
||||||
* Move some of these functions to the functions file so they can be called from the web server
|
const portal = require('./modules/_server.js');
|
||||||
* Or move the web server here and remove the first init
|
portal.start();
|
||||||
* Or just remove the first init call and start the init elsewhere after main() is called
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Custom functions module to keep main script clean
|
dbfn.run(`UPDATE timestamps SET value = ${Date.now()} WHERE key = 'process_start'`).catch(err => console.error(`Error setting process start time: ${err}`));
|
||||||
const fn = require('./functions.js').functions;
|
|
||||||
|
|
||||||
// Config File
|
fn.commands.refreshConfig().then(res => {
|
||||||
const config = require('./config.json');
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||||
// Set the time we started execution, for time-aware logging
|
config = res.config;
|
||||||
config.timestamps.procStart = Date.now();
|
// Setup for use with the Pi's GPIO pins
|
||||||
|
switch (process.env.ONPI) {
|
||||||
// Environment Variables Importing
|
case 'true':
|
||||||
const dotenv = require('dotenv').config();
|
console.log(`== Running on a Raspberry Pi.`);
|
||||||
|
var gpio = require('rpi-gpio');
|
||||||
// Setup for use with the Pi's GPIO pins
|
|
||||||
// TODO: ONPI should be DEV_ENV or DEBUG
|
|
||||||
// Include something like LOGGING = "DEBUG"|"PRODUCTION"|"ETC"
|
|
||||||
if (process.env.ONPI == 'true') {
|
|
||||||
// TODO adjustable logging
|
|
||||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] == Running on a Raspberry Pi.`);
|
|
||||||
// Import the Node Raspberry Pi GPIO module
|
|
||||||
const gpio = require('rpi-gpio');
|
|
||||||
// Run the initialization function
|
|
||||||
fn.init(gpio).then((res) => {
|
fn.init(gpio).then((res) => {
|
||||||
// TODO: adjustable logging
|
|
||||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
// Invoke the first cycle of main, passing along the Functions module and GPIO module
|
main(gpio);
|
||||||
main(fn, gpio);
|
|
||||||
}).catch(rej => {
|
}).catch(rej => {
|
||||||
// TODO: This probably should end the process since we can't continue if the initialization fails
|
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
|
||||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${rej}`);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
} else if (process.env.ONPI == 'false') { // TODO ONPI change to DEV_ENV
|
break;
|
||||||
// TODO: adjustable logging
|
case 'false':
|
||||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Not running on a Raspberry Pi.`);
|
console.log(`I: Not running on a Raspberry Pi.`);
|
||||||
// Create a dummy gpio placeholder
|
var gpio = 'gpio';
|
||||||
const gpio = 'gpio';
|
|
||||||
// Run the initialization function, passing the fake GPIO module
|
|
||||||
fn.init(gpio).then(res => {
|
fn.init(gpio).then(res => {
|
||||||
// TODO: Adj. logging
|
|
||||||
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
// Invoke the first cycle of the main function, passing the Functions module and fake GPIO module
|
main(gpio);
|
||||||
main(fn, gpio);
|
|
||||||
}).catch(rej => {
|
}).catch(rej => {
|
||||||
// TODO: This probably should end the process since we can't continue if the initialization fails.
|
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
|
||||||
console.error(rej);
|
process.exit(1);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// TODO: This probably should end the process since we can't continue if the initialization fails.
|
|
||||||
console.error(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Add logic for other sensors
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Main function, turns the auger on, sleeps for the time given in environment variables, then turns the auger off, sleeps, repeats.
|
|
||||||
// TODO move these from environment variables to a config file, if possible.
|
|
||||||
async function main(fn, gpio) {
|
|
||||||
// Check for the existence of certain files
|
|
||||||
fn.files.check().then((res,rej) => {
|
|
||||||
// Log the result of the check if in debug mode
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: File Check: ${res}`);
|
|
||||||
// Choose what to do depending on the result of the check
|
|
||||||
switch (res) {
|
|
||||||
case "pause":
|
|
||||||
// Pause the script
|
|
||||||
fn.commands.pause().then(() => {
|
|
||||||
// Rerun this function once the pause has finished
|
|
||||||
main(fn, gpio);
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "reload":
|
|
||||||
// Reload the environment variables
|
|
||||||
fn.commands.reload().then(() => {
|
|
||||||
// Rerun this function once the reload has finished
|
|
||||||
main(fn, gpio);
|
|
||||||
}).catch(rej => {
|
|
||||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${rej}`);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "quit":
|
|
||||||
// Quit the script
|
|
||||||
console.log(fn.commands.shutdown(gpio));
|
|
||||||
statusCheck(fn, gpio);
|
|
||||||
break;
|
|
||||||
case "ignite":
|
|
||||||
// Start the ignite sequence
|
|
||||||
fn.commands.ignite(gpio).then(res => {
|
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
|
|
||||||
statusCheck(fn, gpio);
|
|
||||||
}).catch(rej => {
|
|
||||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${rej}`);
|
|
||||||
fn.commands.shutdown(gpio);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "start":
|
|
||||||
// Start the stove
|
|
||||||
fn.commands.startup(gpio).then(res => {
|
|
||||||
statusCheck(fn, gpio);
|
|
||||||
}).catch(rej => {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "none":
|
|
||||||
// If no special files are found, cycle the auger normally
|
|
||||||
if (config.status.auger == 1) {
|
|
||||||
fn.auger.cycle(gpio).then((res) => {
|
|
||||||
// Log the auger cycle results if in debug mode.
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
|
||||||
// Run the status check function
|
|
||||||
statusCheck(fn, gpio);
|
|
||||||
// Rerun this function once the cycle is complete
|
|
||||||
// main(fn, gpio);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger Status: ${config.status.auger}`);
|
|
||||||
|
|
||||||
fn.commands.pause().then(res => {
|
|
||||||
statusCheck(fn, gpio);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// If we don't get a result from the file check, or for some reason it's an unexpected response, log it and quit the script.
|
console.log(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
|
||||||
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: No result was received, something is wrong.\nres: ${res}`);
|
process.exit(1);
|
||||||
fn.commands.quit();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
}).catch(rej => {
|
||||||
}
|
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
function statusCheck(fn, gpio) {
|
function main(gpio) {
|
||||||
// Once per cycle, write the config variable to the file so it can be read by the web server
|
// If the auger is enabled
|
||||||
fn.commands.writeConfig();
|
if (config.status.auger == 1) {
|
||||||
if (config.status.shutdown == 1) {
|
// Run a cycle of the auger
|
||||||
console.log(fn.commands.shutdown(gpio) || 'Shutting down...');
|
fn.auger.cycle(gpio).then(res => {
|
||||||
main(fn, gpio);
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
return;
|
fn.checkForQuit().then(n => {
|
||||||
}
|
fn.commands.refreshConfig().then(res => {
|
||||||
if (config.status.igniter == 1) {
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||||
fn.tests.igniter(gpio).then((res) => {
|
config = res.config;
|
||||||
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||||
|
main(gpio);
|
||||||
|
}).catch(rej => {
|
||||||
|
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
|
||||||
|
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||||
|
main(gpio);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Check the vacuum switch, if the test returns true, the vacuum is sensed
|
|
||||||
// if it returns false, we will initiate a shutdown
|
|
||||||
// TODO this is messed up
|
|
||||||
fn.tests.vacuum(gpio).then(vacStatus => {
|
|
||||||
if (!vacStatus) {
|
|
||||||
console.error('No vacuum detected, beginning shutdown procedure.');
|
|
||||||
console.log(fn.commands.shutdown(gpio));
|
|
||||||
main(fn, gpio);
|
|
||||||
} else {
|
} else {
|
||||||
// Check the Proof of Fire Switch
|
// If the auger is disabled
|
||||||
fn.tests.pof(gpio).then(pofStatus => {
|
fn.commands.pause().then(res => {
|
||||||
// If the igniter has finished running and no proof of fire is seen, shutdown the stove
|
fn.checkForQuit().then(n => {
|
||||||
if (config.status.igniterFinished && (!pofStatus)) {
|
fn.commands.refreshConfig().then(res => {
|
||||||
console.error('No Proof of Fire after the igniter shut off, beginning shutdown procedure.');
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||||
console.log(fn.commands.shutdown(gpio));
|
config = res.config;
|
||||||
main(fn, gpio);
|
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||||
} else {
|
main(gpio);
|
||||||
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Vacuum and Proof of Fire OK.`);
|
}).catch(rej => {
|
||||||
main(fn, gpio);
|
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
|
||||||
}
|
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||||
|
main(gpio);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`);
|
||||||
|
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||||
|
main(gpio);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = main;
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
/* Pellet Stove Control Panel
|
||||||
|
* Web Configuration Server
|
||||||
|
* v0.0.0 by Skylar Grant
|
||||||
|
*
|
||||||
|
* TODOs:
|
||||||
|
* Implement Express to make it easier
|
||||||
|
* Add actual data into the responses
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const http = require('http');
|
||||||
|
const fn = require('./functions.js').functions;
|
||||||
|
var config;
|
||||||
|
fn.commands.refreshConfig().then(newConfig => {
|
||||||
|
config = newConfig.config;
|
||||||
|
});
|
||||||
|
const { dbfn } = require('./functions.js');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
app.use(express.urlencoded());
|
||||||
|
// Our root directory for the public web files
|
||||||
|
app.use(express.static(__dirname + '/../www/public'));
|
||||||
|
// Our directory for views used to render the pages
|
||||||
|
app.set('views', __dirname + '/../www/views');
|
||||||
|
// Set .html as the file extension for views
|
||||||
|
app.engine('html', require('ejs').renderFile);
|
||||||
|
app.set('view engine', 'html');
|
||||||
|
|
||||||
|
// A normal load of the root page
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${JSON.stringify(config)}`);
|
||||||
|
res.render('index', { config: JSON.stringify(config) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// A POST form submission to the root page
|
||||||
|
app.post('/', (req, response) => {
|
||||||
|
if (req.body.start != undefined) {
|
||||||
|
fn.commands.startup();
|
||||||
|
fn.commands.refreshConfig().then(res => {
|
||||||
|
config = res.config;
|
||||||
|
response.render('index', { config: JSON.stringify(config) });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (req.body.shutdown != undefined) {
|
||||||
|
fn.commands.shutdown();
|
||||||
|
fn.commands.refreshConfig().then(res => {
|
||||||
|
config = res.config;
|
||||||
|
response.render('index', { config: JSON.stringify(config) });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (req.body.reload != undefined) {
|
||||||
|
const updateAugerOffIntervalQuery = `UPDATE intervals SET value = '${2000 - req.body.feedRate}' WHERE key = 'auger_off'`;
|
||||||
|
const updateAugerOnIntervalQuery = `UPDATE intervals SET value = '${req.body.feedRate}' WHERE key = 'auger_on'`;
|
||||||
|
dbfn.run(updateAugerOffIntervalQuery).then(res => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger off interval updated: ${res.data.changes}`);
|
||||||
|
dbfn.run(updateAugerOnIntervalQuery).then(res => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Auger on interval updated: ${res.data.changes}`);
|
||||||
|
fn.commands.refreshConfig().then(res => {
|
||||||
|
config = res.config;
|
||||||
|
response.render('index', { config: JSON.stringify(config) });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}).catch(err => console.log(`E: ${err}`));
|
||||||
|
}).catch(err => console.log(`E: ${err}`));
|
||||||
|
}
|
||||||
|
if (req.body.quit != undefined) {
|
||||||
|
fn.commands.quit();
|
||||||
|
fn.commands.refreshConfig().then(res => {
|
||||||
|
config = res.config;
|
||||||
|
response.render('index', { config: JSON.stringify(config) });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
start: () => {
|
||||||
|
server.listen(8080, "0.0.0.0");
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,158 @@
|
||||||
|
const dbfn = require('../modules/database.js');
|
||||||
|
|
||||||
|
// Create `status` table
|
||||||
|
/*
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
| Field | Type | Null | Key | Default | Extra |
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
| key | varchar(100) | No | | | |
|
||||||
|
| value | varchar(1000) | No | | | |
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
|
||||||
|
+ ------------------- +
|
||||||
|
| igniter |
|
||||||
|
| blower |
|
||||||
|
| auger |
|
||||||
|
| igniter_finished |
|
||||||
|
| shutdown_initiated |
|
||||||
|
| vacuum |
|
||||||
|
| proof_of_fire |
|
||||||
|
| shutdown_next_cycle |
|
||||||
|
+ ------------------- +
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS status (
|
||||||
|
key varchar(100) NOT NULL,
|
||||||
|
value varchar(1000) NOT NULL
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
const createStatusTableQuery = "CREATE TABLE IF NOT EXISTS status (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
|
||||||
|
dbfn.run(createStatusTableQuery).then(res => {
|
||||||
|
console.log(res.status);
|
||||||
|
const statusEntries = {
|
||||||
|
igniter: 0,
|
||||||
|
blower: 0,
|
||||||
|
auger: 0,
|
||||||
|
igniter_finished: false,
|
||||||
|
shutdown_initiated: 0,
|
||||||
|
vacuum: 0,
|
||||||
|
proof_of_fire: 0,
|
||||||
|
shutdown_next_cycle: 0
|
||||||
|
};
|
||||||
|
for ( key in statusEntries ){
|
||||||
|
const insertStatusEntryQuery = `INSERT INTO status (key, value) VALUES ("${key}", "${statusEntries[key]}")`;
|
||||||
|
dbfn.run(insertStatusEntryQuery).then(res => {
|
||||||
|
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
const selectAllStatusEntriesQuery = "SELECT * FROM status";
|
||||||
|
dbfn.all(selectAllStatusEntriesQuery).then(res => {
|
||||||
|
console.log(res.status);
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create `timestamps` table
|
||||||
|
/*
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
| Field | Type | Null | Key | Default | Extra |
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
| key | varchar(100) | No | | | |
|
||||||
|
| value | varchar(1000) | No | | | |
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
|
||||||
|
+ ------------- +
|
||||||
|
| process_start |
|
||||||
|
| blower_on |
|
||||||
|
| blower_off |
|
||||||
|
| igniter_on |
|
||||||
|
| igniter_off |
|
||||||
|
+ ------------- +
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS timestamps (
|
||||||
|
key varchar(100) NOT NULL,
|
||||||
|
value varchar(1000) NOT NULL
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
const createTimestampsTableQuery = "CREATE TABLE IF NOT EXISTS timestamps (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
|
||||||
|
dbfn.run(createTimestampsTableQuery).then(res => {
|
||||||
|
console.log(res.status);
|
||||||
|
const timestampsEntries = {
|
||||||
|
process_start: 0,
|
||||||
|
blower_on: 0,
|
||||||
|
blower_off: 0,
|
||||||
|
igniter_on: 0,
|
||||||
|
igniter_off: 0
|
||||||
|
};
|
||||||
|
for ( key in timestampsEntries ){
|
||||||
|
const insertTimestampsEntryQuery = `INSERT INTO timestamps (key, value) VALUES ("${key}", "${timestampsEntries[key]}")`;
|
||||||
|
dbfn.run(insertTimestampsEntryQuery).then(res => {
|
||||||
|
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
const selectAllTimestampsEntriesQuery = "SELECT * FROM timestamps";
|
||||||
|
dbfn.all(selectAllTimestampsEntriesQuery).then(res => {
|
||||||
|
console.log(res.status);
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create `intervals` table
|
||||||
|
/*
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
| Field | Type | Null | Key | Default | Extra |
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
| key | varchar(100) | No | | | |
|
||||||
|
| value | varchar(1000) | No | | | |
|
||||||
|
+ ----- + ------------- + ---- + --- + ------- + -------------- +
|
||||||
|
|
||||||
|
+ ------------- +
|
||||||
|
| auger_on |
|
||||||
|
| auger_off |
|
||||||
|
| pause |
|
||||||
|
| igniter_start |
|
||||||
|
| blower_stop |
|
||||||
|
+ ------------- +
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS intervals (
|
||||||
|
key varchar(100) NOT NULL,
|
||||||
|
value varchar(1000) NOT NULL
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
const createIntervalsTableQuery = "CREATE TABLE IF NOT EXISTS intervals (key varchar(100) NOT NULL,value varchar(1000) NOT NULL);";
|
||||||
|
dbfn.run(createIntervalsTableQuery).then(res => {
|
||||||
|
console.log(res.status);
|
||||||
|
const intervalsEntries = {
|
||||||
|
auger_on: 600,
|
||||||
|
auger_off: 1400,
|
||||||
|
pause: 5000,
|
||||||
|
igniter_start: 420000,
|
||||||
|
blower_stop: 600000
|
||||||
|
};
|
||||||
|
for ( key in intervalsEntries ){
|
||||||
|
const insertIntervalsEntryQuery = `INSERT INTO intervals (key, value) VALUES ("${key}", "${intervalsEntries[key]}")`;
|
||||||
|
dbfn.run(insertIntervalsEntryQuery).then(res => {
|
||||||
|
console.log(`${res.status}: ${res.data.lastID}: ${res.data.changes} changes`);
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
const selectAllIntervalsEntriesQuery = "SELECT * FROM intervals";
|
||||||
|
dbfn.all(selectAllIntervalsEntriesQuery).then(res => {
|
||||||
|
console.log(res.status);
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the tables to confirm they were created properly:
|
||||||
|
|
||||||
|
dbfn.showTables().then(res => {
|
||||||
|
res.rows.forEach(row => {
|
||||||
|
console.log("Table: " + JSON.stringify(row));
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
|
@ -0,0 +1,53 @@
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
|
||||||
|
const db = new sqlite3.Database('./data/config.db', (err) => {
|
||||||
|
if (err) throw `E: DB Connection: ${err.message}`;
|
||||||
|
console.log(`I: Connected to the database.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
run(query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run(query, function(err) {
|
||||||
|
if (err) {
|
||||||
|
reject("Problem executing the query: " + err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve( { "status": "Query executed successfully: " + query, "data": this });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
all(query) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.serialize(() => {
|
||||||
|
db.all(query, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject("Problem executing the query: " + err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// [ { key: 'key_name', value: '0' }, { key: 'key_name', value: '0' } ]
|
||||||
|
let organizedRows = {};
|
||||||
|
rows.forEach(row => {
|
||||||
|
organizedRows[row.key] = row.value;
|
||||||
|
});
|
||||||
|
resolve({ "status": "Query executed successfully: " + query, "rows": organizedRows });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showTables() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.serialize(() => {
|
||||||
|
db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
reject("Problem executing the query: " + err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve({ "status": "Tables retreived successfully", "rows": rows });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,292 @@
|
||||||
|
// TODOs: Add tests for PoF and Vacuum switches, add delays for shutting down blower, test logic for igniter
|
||||||
|
// TODO: Move these to config
|
||||||
|
// Physical Pin numbers for GPIO
|
||||||
|
const augerPin = 7; // Pin for controlling the relay for the pellet auger motor.
|
||||||
|
|
||||||
|
// Require the package for pulling version numbers
|
||||||
|
const package = require('../package.json');
|
||||||
|
// Database Functions
|
||||||
|
const dbfn = require('./database.js');
|
||||||
|
|
||||||
|
|
||||||
|
// Get environment variables
|
||||||
|
const dotenv = require('dotenv').config();
|
||||||
|
// Module for working with files
|
||||||
|
const fs = require('fs');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
var config = require('../templates/config.json');
|
||||||
|
|
||||||
|
|
||||||
|
// The functions we'll export to be used in other files
|
||||||
|
const functions = {
|
||||||
|
auger: {
|
||||||
|
// Gets called once the Auger Pin has been setup by rpi-gpio
|
||||||
|
ready(err) {
|
||||||
|
if (err) throw err;
|
||||||
|
console.log('Auger GPIO Ready');
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
// Turns the auger on (Pin 7 high)
|
||||||
|
on(gpio) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.env.ONPI == 'true') {
|
||||||
|
gpio.write(augerPin, true, function (err) {
|
||||||
|
if (err) throw err;
|
||||||
|
resolve('Auger turned on.');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve('Simulated auger turned on.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
// Turns the auger off (pin 7 low)
|
||||||
|
off(gpio) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.env.ONPI == 'true') {
|
||||||
|
gpio.write(augerPin, false, function (err) {
|
||||||
|
if (err) throw err;
|
||||||
|
resolve('Auger turned off.');
|
||||||
|
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve('Simulated auger turned off.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
// Cycles the auger using the two functions above this one (functions.auger.on() and functions.auger.off())
|
||||||
|
// Sleeps in between cycles using functions.sleep()
|
||||||
|
cycle(gpio) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Turn the auger on
|
||||||
|
this.on(gpio).then((res) => {
|
||||||
|
// Log action if in debug mode
|
||||||
|
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
|
// Sleep for the time set in env variables
|
||||||
|
functions.sleep(config.intervals.augerOn).then((res) => {
|
||||||
|
// Log action if in debug mode
|
||||||
|
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
|
// Turn the auger off
|
||||||
|
this.off(gpio).then((res) => {
|
||||||
|
// Log action if in debug mode
|
||||||
|
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
|
// Sleep for the time set in env variables
|
||||||
|
functions.sleep(config.intervals.augerOff).then((res) => {
|
||||||
|
// Log action if in debug mode
|
||||||
|
// if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||||
|
// Resolve the promise, letting the main script know the cycle is complete
|
||||||
|
resolve(`Auger cycled (${config.intervals.augerOn}/${config.intervals.augerOff})`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
// Prepare the stove for starting
|
||||||
|
startup() {
|
||||||
|
// Basic startup just enables the auger
|
||||||
|
const enableAugerQuery = "UPDATE status SET value = 1 WHERE key = 'auger'";
|
||||||
|
dbfn.run(enableAugerQuery).then(res => {
|
||||||
|
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Auger enabled.`);
|
||||||
|
return;
|
||||||
|
}).catch(err => console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`));
|
||||||
|
},
|
||||||
|
shutdown() {
|
||||||
|
// Basic shutdown only needs to disable the auger
|
||||||
|
const disableAugerQuery = "UPDATE status SET value = 0 WHERE key = 'auger'";
|
||||||
|
dbfn.run(disableAugerQuery).then(res => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||||
|
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Auger disabled.`);
|
||||||
|
return;
|
||||||
|
}).catch(err => console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`));
|
||||||
|
},
|
||||||
|
// Pauses the script for the time defined in env variables
|
||||||
|
pause() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Pausing for ${config.intervals.pause}ms`);
|
||||||
|
|
||||||
|
functions.sleep(config.intervals.pause).then((res) => {
|
||||||
|
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Pause finished.`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Reload the environment variables on the fly
|
||||||
|
reload(envs) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// Re-require dotenv because inheritance in js sucks
|
||||||
|
const dotenv = require('dotenv').config({ override: true });
|
||||||
|
// Delete the reload file
|
||||||
|
fs.unlink('./reload', (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
if (process.env.DEBUG) console.log('Deleted reload file.');
|
||||||
|
});
|
||||||
|
// Print out the new environment variables
|
||||||
|
// This should be printed regardless of debug status, maybe prettied up TODO?
|
||||||
|
console.log('Reloaded environment variables.');
|
||||||
|
console.log(`ONTIME=${config.intervals.augerOn}\nOFFTIME=${config.intervals.augerOff}\nPAUSETIME=${config.intervals.pause}\nDEBUG=${process.env.DEBUG}\nONPI=${process.env.ONPI}`);
|
||||||
|
// Resolve the promise, letting the main script know we're done reloading the variables and the cycle can continue
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
refreshConfig() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// When the reload button is pressed, the call to this function will contain new config values
|
||||||
|
// {
|
||||||
|
// augerOff: 500,
|
||||||
|
// augerOn: 1500,
|
||||||
|
// pause: 5000
|
||||||
|
// }
|
||||||
|
// if (newSettings != undefined) {
|
||||||
|
// config.intervals.augerOff = newSettings.augerOff;
|
||||||
|
// config.intervals.augerOn = newSettings.augerOn;
|
||||||
|
// console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Intervals updated: (${newSettings.augerOn}/${newSettings.augerOff})`);
|
||||||
|
|
||||||
|
// }
|
||||||
|
// fs.writeFile('./config.json', JSON.stringify(config), (err) => {
|
||||||
|
// if (err) reject(err);
|
||||||
|
// resolve();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Get status
|
||||||
|
const selectStatusQuery = "SELECT * FROM status";
|
||||||
|
dbfn.all(selectStatusQuery).then(res => {
|
||||||
|
let { status } = config;
|
||||||
|
let { rows } = res;
|
||||||
|
status.auger = rows.auger;
|
||||||
|
status.blower = rows.blower;
|
||||||
|
status.igniter = rows.igniter;
|
||||||
|
status.igniterFinished = rows.igniter_finished;
|
||||||
|
status.pof = rows.proof_of_fire;
|
||||||
|
status.shutdownNextCycle = rows.shutdown_next_cycle;
|
||||||
|
status.vacuum = rows.vacuum;
|
||||||
|
|
||||||
|
// Get timestamps
|
||||||
|
const selectTimestampsQuery = "SELECT * FROM timestamps";
|
||||||
|
dbfn.all(selectTimestampsQuery).then(res => {
|
||||||
|
let { timestamps } = config;
|
||||||
|
let { rows } = res;
|
||||||
|
timestamps.blowerOff = rows.blower_off;
|
||||||
|
timestamps.blowerOn = rows.blower_on;
|
||||||
|
timestamps.igniterOff = rows.igniter_off;
|
||||||
|
timestamps.igniterOn = rows.igniter_on;
|
||||||
|
timestamps.procStart = rows.process_start;
|
||||||
|
|
||||||
|
// Get intervals
|
||||||
|
const selectIntervalsQuery = "SELECT * FROM intervals";
|
||||||
|
dbfn.all(selectIntervalsQuery).then(res => {
|
||||||
|
let { intervals } = config;
|
||||||
|
let { rows } = res;
|
||||||
|
intervals.augerOff = rows.auger_off;
|
||||||
|
intervals.augerOn = rows.auger_on;
|
||||||
|
intervals.blowerStop = rows.blower_stop;
|
||||||
|
intervals.igniterStart = rows.igniter_start;
|
||||||
|
intervals.pause = rows.pause;
|
||||||
|
resolve({ "status": "Refreshed the config", "config": config });
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
quit() {
|
||||||
|
functions.commands.shutdown();
|
||||||
|
functions.auger.off(gpio).then(res => {
|
||||||
|
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Exiting app...`);
|
||||||
|
process.exit(0);
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] E: Unable to shut off auger, rebooting Pi!`);
|
||||||
|
exec('shutdown -r 0');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Sleeps for any given milliseconds
|
||||||
|
sleep(ms) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// if (process.env.DEBUG) console.log(`Sleeping for ${ms}ms`);
|
||||||
|
// Function to be called when setTimeout finishes
|
||||||
|
const finish = () => {
|
||||||
|
// Resolve the promise
|
||||||
|
resolve(`Slept for ${ms}ms`);
|
||||||
|
};
|
||||||
|
// The actual sleep function, sleeps for ms then calls finish()
|
||||||
|
setTimeout(finish, ms);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Initializes rpi-gpio, or resolves if not on a raspberry pi
|
||||||
|
init(gpio) {
|
||||||
|
fs.readFile('./templates/config.json', (err, data) => {
|
||||||
|
fs.writeFile('./config.json', data, (err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
config = require('../config.json');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// TODO this boot splash needs updating
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Boot/About/Info
|
||||||
|
console.log(`== Lennox Winslow PS40
|
||||||
|
== Pellet Stove Control Panel
|
||||||
|
== Author: Skylar Grant
|
||||||
|
== Version: v${package.version}
|
||||||
|
==
|
||||||
|
== Startup Time: ${new Date().toISOString()}
|
||||||
|
==
|
||||||
|
== Environment variables:
|
||||||
|
== == ONTIME=${config.intervals.augerOn}
|
||||||
|
== == OFFTIME=${config.intervals.augerOff}
|
||||||
|
== == PAUSETIME=${config.intervals.pause}
|
||||||
|
== == DEBUG=${process.env.DEBUG}
|
||||||
|
== == ONPI=${process.env.ONPI}`);
|
||||||
|
// Set up GPIO 4 (pysical pin 7) as output, then call functions.auger.ready()
|
||||||
|
if (process.env.ONPI == 'true') {
|
||||||
|
// Init the Auger pin
|
||||||
|
gpio.setup(augerPin, gpio.DIR_OUT, (err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
if (process.env.DEBUG) console.log('== Auger pin initialized.');
|
||||||
|
// Resolve the promise now that all pins have been initialized
|
||||||
|
resolve('== GPIO Initialized.');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Resolve the promise
|
||||||
|
resolve('== GPIO Not Available');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
checkForQuit() {
|
||||||
|
if (config.status.shutdownNextCycle == 1) {
|
||||||
|
console.log(`[${(Date.now() - config.timestamps.procStart) / 1000}] I: Exiting Process!`);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (fs.existsSync('./quit')) {
|
||||||
|
fs.unlink('./quit', err => {
|
||||||
|
if (err) console.log('Error removing the quit file: ' + err);
|
||||||
|
config.status.shutdownNextCycle = 1;
|
||||||
|
config.status.auger = 0;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve('Not shutting down');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
time(stamp) {
|
||||||
|
const time = new Date(stamp);
|
||||||
|
return `${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the above object, functions, as a module
|
||||||
|
module.exports = { functions, dbfn };
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"debugMode": true,
|
|
||||||
"status": {
|
"status": {
|
||||||
"igniter": 0,
|
"igniter": 0,
|
||||||
"blower": 0,
|
"blower": 0,
|
||||||
|
@ -7,7 +6,8 @@
|
||||||
"igniterFinished": false,
|
"igniterFinished": false,
|
||||||
"shutdown": 0,
|
"shutdown": 0,
|
||||||
"vacuum": 0,
|
"vacuum": 0,
|
||||||
"pof": 0
|
"pof": 0,
|
||||||
|
"shutdownNextCycle": 0
|
||||||
},
|
},
|
||||||
"timestamps": {
|
"timestamps": {
|
||||||
"procStart": 0,
|
"procStart": 0,
|
||||||
|
@ -17,11 +17,11 @@
|
||||||
"igniterOff": 0
|
"igniterOff": 0
|
||||||
},
|
},
|
||||||
"intervals": {
|
"intervals": {
|
||||||
"augerOn": 500,
|
"augerOn": "600",
|
||||||
"augerOff": 1500,
|
"augerOff": "1400",
|
||||||
"pause": 3000,
|
"pause": "3000",
|
||||||
"igniterStart": 5000,
|
"igniterStart": "5000",
|
||||||
"blowerStop": 5000
|
"blowerStop": "5000"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
|
|
51
websvr.js
51
websvr.js
|
@ -1,51 +0,0 @@
|
||||||
/* Pellet Stove Control Panel
|
|
||||||
* Web Configuration Server
|
|
||||||
* v0.0.0 by Skylar Grant
|
|
||||||
*
|
|
||||||
* TODOs:
|
|
||||||
* Implement Express to make it easier
|
|
||||||
* Add actual data into the responses
|
|
||||||
*/
|
|
||||||
|
|
||||||
const express = require('express');
|
|
||||||
const app = express();
|
|
||||||
const http = require('http');
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const config = require('./config.json');
|
|
||||||
const fs = require('fs');
|
|
||||||
// const bodyParser = require('body-parser');
|
|
||||||
const core = require('./main.js');
|
|
||||||
const fn = require('./functions.js').functions;
|
|
||||||
const gpio = require('rpi-gpio');
|
|
||||||
|
|
||||||
app.use(express.urlencoded());
|
|
||||||
|
|
||||||
app.use(express.static(__dirname + '/www/public'));
|
|
||||||
app.set('views', __dirname + '/www/views');
|
|
||||||
app.engine('html', require('ejs').renderFile);
|
|
||||||
app.set('view engine', 'html');
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
fs.readFile(__dirname + '/config.json', (err, data) => {
|
|
||||||
// console.log(JSON.parse(data));
|
|
||||||
res.render('index', JSON.parse(data));
|
|
||||||
// res.send(200);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/', (req, res) => {
|
|
||||||
fs.readFile(__dirname + '/config.json', (err, data) => {
|
|
||||||
// console.log(JSON.parse(data));
|
|
||||||
res.render('index', JSON.parse(data));
|
|
||||||
if (req.body.start != undefined) {
|
|
||||||
fn.commands.ignite(gpio);
|
|
||||||
}
|
|
||||||
if (req.body.shutdown != undefined) {
|
|
||||||
fn.commands.shutdown(gpio);
|
|
||||||
}
|
|
||||||
// res.send(200);
|
|
||||||
});
|
|
||||||
// console.log(req.body);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(config.web.port, config.web.ip);
|
|
|
@ -1 +0,0 @@
|
||||||
../../config.json
|
|
|
@ -1 +0,0 @@
|
||||||
../../log.txt
|
|
|
@ -9,18 +9,20 @@ html, body {
|
||||||
#title {
|
#title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 35px;
|
font-size: 35px;
|
||||||
|
margin-top: 10px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#status {
|
#title a {
|
||||||
text-align: center;
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#safeties {
|
#safeties {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#controls-container {
|
.controls-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ html, body {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#controls-container input {
|
.controls-container input {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,13 +48,14 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#log-area {
|
#log-area {
|
||||||
width: 50%;
|
width: 100%;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
background-color: aqua;
|
background-color: aqua;
|
||||||
color: aqua;
|
color: aqua;
|
||||||
}
|
}
|
||||||
|
|
||||||
#trial {
|
#trial {
|
||||||
|
display:;
|
||||||
color: yellow;
|
color: yellow;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
background-color: red;
|
background-color: red;
|
||||||
|
@ -65,18 +68,9 @@ html, body {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-unselected {
|
.btn {
|
||||||
margin: 5px;
|
background-color: #333;
|
||||||
border-width: 0px;
|
color: aqua;
|
||||||
font-family: times;
|
|
||||||
font-size: 20px;
|
|
||||||
height: 35px;
|
|
||||||
width: 100px;
|
|
||||||
border-top-left-radius: 0px;
|
|
||||||
border-bottom-left-radius: 0px;
|
|
||||||
border-top-right-radius: 0px;
|
|
||||||
border-bottom-right-radius: 0px;
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-selected {
|
.button-selected {
|
||||||
|
@ -91,3 +85,14 @@ html, body {
|
||||||
border-top-right-radius: 0px;
|
border-top-right-radius: 0px;
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin: 0 auto;
|
||||||
|
color: aqua !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
|
@ -1,61 +0,0 @@
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function readJSON(path) {
|
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
request.open("GET", path, false);
|
|
||||||
request.send(null)
|
|
||||||
var JSONObj = JSON.parse(request.responseText);
|
|
||||||
return JSONObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseStatus(data) {
|
|
||||||
switch (data) {
|
|
||||||
case 0:
|
|
||||||
return "Off";
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
return "On";
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return "Error";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshData() {
|
|
||||||
const log = document.getElementById('log-area');
|
|
||||||
log.contentWindow.location.reload();
|
|
||||||
sleep(100).then(() => {
|
|
||||||
document.getElementById('log-area').contentWindow.scrollTo(0, 9999999);
|
|
||||||
});
|
|
||||||
|
|
||||||
const augerStatus = document.getElementById('auger-status');
|
|
||||||
// const augerOn = document.getElementById('auger-on');
|
|
||||||
// const augerOff = document.getElementById('auger-off');
|
|
||||||
const igniterStatus = document.getElementById('igniter-status');
|
|
||||||
const blowerStatus = document.getElementById('blower-status');
|
|
||||||
// const pauseInt = document.getElementById('pause-int');
|
|
||||||
const vacuumStatus = document.getElementById('vacuum-status');
|
|
||||||
const pofStatus = document.getElementById('pof-status');
|
|
||||||
|
|
||||||
const config = readJSON('./config.json');
|
|
||||||
|
|
||||||
augerStatus.innerHTML = parseStatus(config.status.auger);
|
|
||||||
// augerOn.innerHTML = parseStatus(config.intervals.augerOn);
|
|
||||||
// augerOff.innerHTML = parseStatus(config.intervals.augerOff);
|
|
||||||
igniterStatus.innerHTML = parseStatus(config.status.igniter);
|
|
||||||
blowerStatus.innerHTML = parseStatus(config.status.blower);
|
|
||||||
// pauseInt.innerHTML = parseStatus(config.intervals.pause);
|
|
||||||
vacuumStatus.innerHTML = parseStatus(config.status.vacuum);
|
|
||||||
pofStatus.innerHTML = parseStatus(config.status.pof);
|
|
||||||
|
|
||||||
sleep(2000).then(() => {
|
|
||||||
refreshData();
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -2,39 +2,149 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Hestia Web Portal</title>
|
<title>Hestia Web Portal</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<!-- Bootstrap -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||||
<link rel="stylesheet" href="/main.css">
|
<link rel="stylesheet" href="/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body onload="refreshData()">
|
<body onload="refreshData()" class="container">
|
||||||
|
<script>
|
||||||
|
// Get the config file
|
||||||
|
const config = <%- config %>;
|
||||||
|
console.log(<%- config %>);
|
||||||
|
</script>
|
||||||
<%- include('trial.html') -%>
|
<%- include('trial.html') -%>
|
||||||
<div id="title">Hestia Web Portal</div>
|
<div id="title" class="text-center mb-4">
|
||||||
<div id="status">Auger: <span id="auger-status"></span> | Igniter: <span id="igniter-status"></span> | Combustion Blower: <span id="blower-status"></span></div>
|
<a href='./'>Hestia Web Portal</a>
|
||||||
<div id="safeties">Vacuum: <span id="vacuum-status"></span> | Proof of Fire: <span id="pof-status"></span></div>
|
</div>
|
||||||
<div id="controls-container">
|
<div id="status" class="row">
|
||||||
|
<!--
|
||||||
|
| Auger | rows[0].cells[1] | On Time | rows[0].cells[3] |
|
||||||
|
| Feed Rate | rows[1].cells[1] | Off Time | rows[1].cells[3] |
|
||||||
|
-->
|
||||||
|
|
||||||
|
<table id="status-table" class="table table-bordered col-sm-12 col-md-6">
|
||||||
|
<tr>
|
||||||
|
<td>Auger</td>
|
||||||
|
<td></td>
|
||||||
|
<td>On Time</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Feed Rate</td>
|
||||||
|
<td></td>
|
||||||
|
<td>Off Time</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="controls-container">
|
||||||
<form action="/" method="post">
|
<form action="/" method="post">
|
||||||
<!-- Start | Shutdown | Reload Settings -->
|
<!-- Start | Shutdown | Reload Settings -->
|
||||||
<div class="button-container">
|
<div class="button-container d-flex justify-content-between">
|
||||||
<input class="button-unselected" type="submit" id="ignite" value="Start" name="start"><input class="button-unselected" type="submit" id="shutdown" value="Shutdown" name="shutdown"><input class="button-unselected" type="submit" id="reload" value="Reload" name="reload"><br>
|
<input class="btn btn-outline-secondary" type="submit" id="ignite" value="Enable Auger" name="start">
|
||||||
|
<input class="btn btn-outline-secondary" type="submit" id="shutdown" value="Disable Auger" name="shutdown">
|
||||||
</div>
|
</div>
|
||||||
<!-- Set feed rates -->
|
<!-- Set feed rates -->
|
||||||
<!-- <label for="augerOn">Auger On Interval: </label><input type="number" id="auger-on" name="augerOn" min="500" max="1000" value="<%= intervals.augerOn %>">ms<br> -->
|
<div class="form-group">
|
||||||
<!-- <label for="augerOff">Auger Off Interval: </label><input type="number" id="auger-off" name="augerOff" min="1000" max="2000" value="<%= intervals.augerOff %>">ms<br> -->
|
<label for="feedRate">Feed Rate: </label>
|
||||||
<!-- <label for="pauseInt">App Pause Interval: </label><input type="number" id="pause-int" name="pauseInt" min="1000" max="600000" value="<%= intervals.pause %>">ms<br> -->
|
<select name="feedRate" class="form-control" id="feed-rate-select">
|
||||||
|
<option value="600">Low</option>
|
||||||
|
<option value="800">Medium</option>
|
||||||
|
<option value="1000">High</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="button-container d-flex justify-content-end">
|
||||||
|
<input class="btn btn-outline-secondary" type="submit" id="reload" value="Set Feed Rate" name="reload">
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="subheading">
|
<!-- <div class="text-center my-4">
|
||||||
Pellet Feed Rate
|
<img src="./dancing_jesus.gif" class="img-fluid">
|
||||||
|
</div> -->
|
||||||
|
<div class="controls-container">
|
||||||
|
<form action="/" method="POST">
|
||||||
|
<input class="btn btn-danger" type="submit" id="quit" value="Quit!!" name="quit" style="visibility: hidden;">
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-container">
|
<!-- <script src="./main.js"></script> -->
|
||||||
<button class="button-unselected" id="low-level">LOW</button><button class="button-unselected" id="med-level">MED</button><button class="button-unselected" id="hi-level">HI</button>
|
<script>
|
||||||
</div>
|
function sleep(ms) {
|
||||||
<div class="button-container">
|
return new Promise((resolve, reject) => {
|
||||||
<img src="./dancing_jesus.gif">
|
setTimeout(() => {
|
||||||
</div>
|
resolve();
|
||||||
<div id="log-container">
|
}, ms);
|
||||||
<button id="refresh-log" onclick="refreshLog()">Refresh Log</button><br>
|
});
|
||||||
<!-- <textarea id="log-area"></textarea> -->
|
}
|
||||||
<iframe id="log-area" src="log.txt"></iframe>
|
|
||||||
</div>
|
function readJSON(path) {
|
||||||
<script src="./main.js"></script>
|
var request = new XMLHttpRequest();
|
||||||
|
request.open("GET", path, false);
|
||||||
|
request.send(null)
|
||||||
|
var JSONObj = JSON.parse(request.responseText);
|
||||||
|
return JSONObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStatus(data) {
|
||||||
|
switch (data) {
|
||||||
|
case "0":
|
||||||
|
return "Off";
|
||||||
|
break;
|
||||||
|
case "1":
|
||||||
|
return "On";
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return "Error: " + data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshData() {
|
||||||
|
// const log = document.getElementById('log-area');
|
||||||
|
// log.contentWindow.location.reload();
|
||||||
|
// sleep(100).then(() => {
|
||||||
|
// document.getElementById('log-area').contentWindow.scrollTo(0, 9999999999);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Get the elements we need to update
|
||||||
|
const statusTable = document.getElementById('status-table');
|
||||||
|
const augerStatus = statusTable.rows[0].cells[1];
|
||||||
|
const augerOn = statusTable.rows[0].cells[3];
|
||||||
|
const augerOff = statusTable.rows[1].cells[3];
|
||||||
|
const feedRate = statusTable.rows[1].cells[1];
|
||||||
|
const feedRateSelect = document.getElementById('feed-rate-select');
|
||||||
|
|
||||||
|
// console.log(config);
|
||||||
|
|
||||||
|
augerStatus.innerHTML = parseStatus(config.status.auger);
|
||||||
|
augerOn.innerHTML = config.intervals.augerOn;
|
||||||
|
augerOff.innerHTML = config.intervals.augerOff;
|
||||||
|
|
||||||
|
switch (config.intervals.augerOn) {
|
||||||
|
case '600':
|
||||||
|
feedRate.innerHTML = 'Low';
|
||||||
|
feedRateSelect.selectedIndex = 0;
|
||||||
|
break;
|
||||||
|
case '800':
|
||||||
|
feedRate.innerHTML = 'Medium';
|
||||||
|
feedRateSelect.selectedIndex = 1;
|
||||||
|
break;
|
||||||
|
case '1000':
|
||||||
|
feedRate.innerHTML = 'High';
|
||||||
|
feedRateSelect.selectedIndex = 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
feedRate.innerHTML = 'Unknown';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
feedRate.value = config.intervals.augerOn;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hestia Web Portal</title>
|
||||||
|
<link rel="stylesheet" href="/main.css">
|
||||||
|
</head>
|
||||||
|
<body onload="refreshData()">
|
||||||
|
<%- include('trial.html') -%>
|
||||||
|
<div id="title"><a href='./'>Hestia Web Portal</a></div>
|
||||||
|
<div id="status">
|
||||||
|
<!--
|
||||||
|
| Auger | rows[0].cells[1] | On Time | rows[0].cells[3] |
|
||||||
|
| Feed Rate | rows[1].cells[1] | Off Time | rows[1].cells[3] |
|
||||||
|
-->
|
||||||
|
|
||||||
|
<table id="status-table">
|
||||||
|
<tr>
|
||||||
|
<td>Auger</td>
|
||||||
|
<td></td>
|
||||||
|
<td>On Time</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Feed Rate</td>
|
||||||
|
<td></td>
|
||||||
|
<td>Off Time</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="controls-container">
|
||||||
|
<form action="/" method="post">
|
||||||
|
<!-- Start | Shutdown | Reload Settings -->
|
||||||
|
<div class="button-container">
|
||||||
|
<input class="button-unselected" type="submit" id="ignite" value="Enable Auger" name="start"><input class="button-unselected" type="submit" id="shutdown" value="Disable Auger" name="shutdown"><br>
|
||||||
|
</div>
|
||||||
|
<!-- Set feed rates -->
|
||||||
|
<label for="feedRate">Feed Rate: </label>
|
||||||
|
<select name="feedRate">
|
||||||
|
<option value="600">Low</option>
|
||||||
|
<option value="800">Medium</option>
|
||||||
|
<option value="1000">High</option>
|
||||||
|
</select>
|
||||||
|
<div class="button-container">
|
||||||
|
<input class="button-unselected" type="submit" id="reload" value="Reload" name="reload">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="button-container">
|
||||||
|
<img src="./dancing_jesus.gif">
|
||||||
|
</div>
|
||||||
|
<div id="log-container">
|
||||||
|
<iframe id="log-area" src="log.txt"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="controls-container">
|
||||||
|
<form action="/" method="POST">
|
||||||
|
<input class="button-unselected" type="submit" id="quit" value="Quit!!" name="quit" style="visibility: hidden;">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script src="./main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue