Compare commits

..

1 Commits
main ... web

Author SHA1 Message Date
Skylar Grant f4985ac11d Commenting 2023-09-21 19:46:50 -04:00
23 changed files with 928 additions and 1080 deletions

6
.gitignore vendored
View File

@ -8,12 +8,6 @@ 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

2
.vscode/launch.json vendored
View File

@ -11,7 +11,7 @@
"skipFiles": [ "skipFiles": [
"<node_internals>/**" "<node_internals>/**"
], ],
"program": "${workspaceFolder}/main.js" "program": "${workspaceFolder}/websvr.js"
} }
] ]
} }

View File

@ -16,10 +16,10 @@
// "description": "Log output to console" // "description": "Log output to console"
// } // }
"Hestia Debug Log": { "Log if in Debug mode": {
"scope": "javascript", "scope": "javascript",
"prefix": "log", "prefix": "log",
"body": "if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: $1`);$0", "body": "if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: $1`);\n$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,17 +27,5 @@
"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"
},
} }

View File

@ -54,53 +54,3 @@ 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

8
TODO
View File

@ -1,8 +0,0 @@
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
config.json Normal file
View File

@ -0,0 +1 @@
{"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"}}

View File

@ -1,5 +0,0 @@
{
"database": {
"createConfigTable": ""
}
}

526
functions.js Normal file
View File

@ -0,0 +1,526 @@
// 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 };

View File

@ -1,93 +0,0 @@
#!/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 Normal file
View File

@ -0,0 +1,56 @@
[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

265
main.js
View File

@ -1,91 +1,182 @@
const fn = require('./modules/functions.js').functions; /* Pellet Stove Control Panel
// Import the config file * Written by Skylar Grant
var config = require('./templates/config.json'); * v0.2
// Database Functions *
const dbfn = require('./modules/database.js'); * TODO:
// Web Portal * Update documentation
const portal = require('./modules/_server.js'); * Move some of these functions to the functions file so they can be called from the web server
portal.start(); * Or move the web server here and remove the first init
* Or just remove the first init call and start the init elsewhere after main() is called
*/
dbfn.run(`UPDATE timestamps SET value = ${Date.now()} WHERE key = 'process_start'`).catch(err => console.error(`Error setting process start time: ${err}`)); // Custom functions module to keep main script clean
const fn = require('./functions.js').functions;
fn.commands.refreshConfig().then(res => { // Config File
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`); const config = require('./config.json');
config = res.config; // Set the time we started execution, for time-aware logging
// Setup for use with the Pi's GPIO pins config.timestamps.procStart = Date.now();
switch (process.env.ONPI) {
case 'true':
console.log(`== Running on a Raspberry Pi.`);
var gpio = require('rpi-gpio');
fn.init(gpio).then((res) => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
main(gpio);
}).catch(rej => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
process.exit(1);
});
break;
case 'false':
console.log(`I: Not running on a Raspberry Pi.`);
var gpio = 'gpio';
fn.init(gpio).then(res => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
main(gpio);
}).catch(rej => {
console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
process.exit(1);
});
break;
default:
console.log(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
process.exit(1);
break;
}
}).catch(rej => {
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
process.exit(1);
});
function main(gpio) { // Environment Variables Importing
// If the auger is enabled const dotenv = require('dotenv').config();
if (config.status.auger == 1) {
// Run a cycle of the auger // Setup for use with the Pi's GPIO pins
fn.auger.cycle(gpio).then(res => { // TODO: ONPI should be DEV_ENV or DEBUG
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`); // Include something like LOGGING = "DEBUG"|"PRODUCTION"|"ETC"
fn.checkForQuit().then(n => { if (process.env.ONPI == 'true') {
fn.commands.refreshConfig().then(res => { // TODO adjustable logging
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`); console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] == Running on a Raspberry Pi.`);
config = res.config; // Import the Node Raspberry Pi GPIO module
// Recursion ecursion cursion ursion rsion sion ion on n const gpio = require('rpi-gpio');
main(gpio); // Run the initialization function
}).catch(rej => { fn.init(gpio).then((res) => {
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`); // TODO: adjustable logging
// Recursion ecursion cursion ursion rsion sion ion on n console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
main(gpio); // Invoke the first cycle of main, passing along the Functions module and GPIO module
}); main(fn, gpio);
}); }).catch(rej => {
}).catch(err => { // TODO: This probably should end the process since we can't continue if the initialization fails
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`); console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${rej}`);
}); });
} else { } else if (process.env.ONPI == 'false') { // TODO ONPI change to DEV_ENV
// If the auger is disabled // TODO: adjustable logging
fn.commands.pause().then(res => { console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Not running on a Raspberry Pi.`);
fn.checkForQuit().then(n => { // Create a dummy gpio placeholder
fn.commands.refreshConfig().then(res => { const gpio = 'gpio';
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`); // Run the initialization function, passing the fake GPIO module
config = res.config; fn.init(gpio).then(res => {
// Recursion ecursion cursion ursion rsion sion ion on n // TODO: Adj. logging
main(gpio); console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
}).catch(rej => { // Invoke the first cycle of the main function, passing the Functions module and fake GPIO module
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`); main(fn, gpio);
// Recursion ecursion cursion ursion rsion sion ion on n }).catch(rej => {
main(gpio); // TODO: This probably should end the process since we can't continue if the initialization fails.
}); console.error(rej);
}); });
}).catch(err => { } else {
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: ${err}`); // TODO: This probably should end the process since we can't continue if the initialization fails.
// Recursion ecursion cursion ursion rsion sion ion on n console.error(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
main(gpio);
});
}
} }
// 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;
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:
// 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.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: No result was received, something is wrong.\nres: ${res}`);
fn.commands.quit();
break;
}
});
}
function statusCheck(fn, gpio) {
// Once per cycle, write the config variable to the file so it can be read by the web server
fn.commands.writeConfig();
if (config.status.shutdown == 1) {
console.log(fn.commands.shutdown(gpio) || 'Shutting down...');
main(fn, gpio);
return;
}
if (config.status.igniter == 1) {
fn.tests.igniter(gpio).then((res) => {
if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
});
}
// 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 {
// Check the Proof of Fire Switch
fn.tests.pof(gpio).then(pofStatus => {
// If the igniter has finished running and no proof of fire is seen, shutdown the stove
if (config.status.igniterFinished && (!pofStatus)) {
console.error('No Proof of Fire after the igniter shut off, beginning shutdown procedure.');
console.log(fn.commands.shutdown(gpio));
main(fn, gpio);
} else {
// if (config.debugMode) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: Vacuum and Proof of Fire OK.`);
main(fn, gpio);
}
});
}
});
}
module.exports = main;

View File

@ -1,83 +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 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");
}
};

View File

@ -1,158 +0,0 @@
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);
});

View File

@ -1,53 +0,0 @@
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 });
});
});
});
}
};

View File

@ -1,292 +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 = 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 };

View File

@ -1,4 +1,5 @@
{ {
"debugMode": true,
"status": { "status": {
"igniter": 0, "igniter": 0,
"blower": 0, "blower": 0,
@ -6,8 +7,7 @@
"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": "600", "augerOn": 500,
"augerOff": "1400", "augerOff": 1500,
"pause": "3000", "pause": 3000,
"igniterStart": "5000", "igniterStart": 5000,
"blowerStop": "5000" "blowerStop": 5000
}, },
"web": { "web": {
"port": 8080, "port": 8080,

51
websvr.js Normal file
View File

@ -0,0 +1,51 @@
/* 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
www/public/config.json Symbolic link
View File

@ -0,0 +1 @@
../../config.json

1
www/public/log.txt Symbolic link
View File

@ -0,0 +1 @@
../../log.txt

View File

@ -9,20 +9,18 @@ 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;
} }
#title a { #status {
text-decoration: none; text-align: center;
color: inherit;
} }
#safeties { #safeties {
text-align: center; text-align: center;
} }
.controls-container { #controls-container {
text-align: center; text-align: center;
} }
@ -33,7 +31,7 @@ html, body {
height: 30px; height: 30px;
} }
.controls-container input { #controls-container input {
margin: 5px; margin: 5px;
} }
@ -48,14 +46,13 @@ html, body {
} }
#log-area { #log-area {
width: 100%; width: 50%;
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;
@ -68,9 +65,18 @@ html, body {
text-align: center; text-align: center;
} }
.btn { .button-unselected {
background-color: #333; margin: 5px;
color: aqua; border-width: 0px;
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 {
@ -85,14 +91,3 @@ 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;
}

61
www/public/main.js Normal file
View File

@ -0,0 +1,61 @@
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();
});
};

View File

@ -2,149 +2,39 @@
<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()" class="container"> <body onload="refreshData()">
<script>
// Get the config file
const config = <%- config %>;
console.log(<%- config %>);
</script>
<%- include('trial.html') -%> <%- include('trial.html') -%>
<div id="title" class="text-center mb-4"> <div id="title">Hestia Web Portal</div>
<a href='./'>Hestia Web Portal</a> <div id="status">Auger: <span id="auger-status"></span> | Igniter: <span id="igniter-status"></span> | Combustion Blower: <span id="blower-status"></span></div>
</div> <div id="safeties">Vacuum: <span id="vacuum-status"></span> | Proof of Fire: <span id="pof-status"></span></div>
<div id="status" class="row"> <div id="controls-container">
<!--
| 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 d-flex justify-content-between"> <div class="button-container">
<input class="btn btn-outline-secondary" type="submit" id="ignite" value="Enable Auger" name="start"> <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="shutdown" value="Disable Auger" name="shutdown">
</div> </div>
<!-- Set feed rates --> <!-- Set feed rates -->
<div class="form-group"> <!-- <label for="augerOn">Auger On Interval: </label><input type="number" id="auger-on" name="augerOn" min="500" max="1000" value="<%= intervals.augerOn %>">ms<br> -->
<label for="feedRate">Feed Rate: </label> <!-- <label for="augerOff">Auger Off Interval: </label><input type="number" id="auger-off" name="augerOff" min="1000" max="2000" value="<%= intervals.augerOff %>">ms<br> -->
<select name="feedRate" class="form-control" id="feed-rate-select"> <!-- <label for="pauseInt">App Pause Interval: </label><input type="number" id="pause-int" name="pauseInt" min="1000" max="600000" value="<%= intervals.pause %>">ms<br> -->
<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="text-center my-4"> <div class="subheading">
<img src="./dancing_jesus.gif" class="img-fluid"> Pellet Feed Rate
</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>
<!-- <script src="./main.js"></script> --> <div class="button-container">
<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>
function sleep(ms) { </div>
return new Promise((resolve, reject) => { <div class="button-container">
setTimeout(() => { <img src="./dancing_jesus.gif">
resolve(); </div>
}, ms); <div id="log-container">
}); <button id="refresh-log" onclick="refreshLog()">Refresh Log</button><br>
} <!-- <textarea id="log-area"></textarea> -->
<iframe id="log-area" src="log.txt"></iframe>
function readJSON(path) { </div>
var request = new XMLHttpRequest(); <script src="./main.js"></script>
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>

View File

@ -1,65 +0,0 @@
<!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>