Compare commits
115 Commits
37a8c9486f
...
0b0a09942d
Author | SHA1 | Date | |
---|---|---|---|
|
0b0a09942d | ||
|
67769232cb | ||
|
38c4a6a644 | ||
|
e6b64b340e | ||
|
754150a91b | ||
|
1e1d3488c2 | ||
|
2bb376868d | ||
|
057b2a6b3a | ||
|
87631c8aa7 | ||
|
7dfafc95d9 | ||
|
3ea70b0898 | ||
|
8f4bf90334 | ||
|
5c852f2c97 | ||
|
f4cfbb71cf | ||
|
38b767034d | ||
|
a7f9a7b6fb | ||
|
a673b15ab9 | ||
|
9f5811d90f | ||
|
299a8b2efa | ||
|
6ee01cc214 | ||
|
53e3042940 | ||
|
afce8a68a7 | ||
|
da745e212f | ||
|
44104f11c1 | ||
|
5393f93a97 | ||
|
f26090e79a | ||
|
df229db09e | ||
|
6c0d5b5b04 | ||
|
72923d94ca | ||
|
7ff3ceab96 | ||
|
eee6d27375 | ||
|
d8a6ea9577 | ||
|
5b59d6f38f | ||
|
cbcc182e9c | ||
|
21f076f403 | ||
|
df349afede | ||
|
69e41e6f0d | ||
|
f37ff53bb6 | ||
|
d7a32e18ce | ||
|
dd2dc0ac65 | ||
|
7cf4677791 | ||
|
81d34723f1 | ||
|
d2769fad5e | ||
|
1532d6e3d3 | ||
|
4daec6513d | ||
|
e6bcf90741 | ||
|
7f58dc87ac | ||
|
c505c858dc | ||
|
88206c8208 | ||
|
8a4ab13b0c | ||
|
7c19ee0bdc | ||
|
c982cd535a | ||
|
d177706bb1 | ||
|
72f814b016 | ||
|
85bf00a35a | ||
|
5da303a24a | ||
|
dfb56ab68c | ||
|
283f963e38 | ||
|
b7f9bacfa1 | ||
|
f66f705e58 | ||
|
0f99b092f7 | ||
|
5b3a4038b4 | ||
|
82fe402b2e | ||
|
7ddb65783c | ||
|
aef7e760a4 | ||
|
626b1d0214 | ||
|
f0ef52d5c0 | ||
|
8890e8acbd | ||
|
595d46dda2 | ||
|
1de8ba78a7 | ||
|
c4b29be20f | ||
|
d92815cfc6 | ||
|
040bba5770 | ||
|
5711214734 | ||
|
6d9a4fe2b9 | ||
|
ac9c8751b8 | ||
|
b758529d79 | ||
|
0e9b36ead1 | ||
|
68306ad224 | ||
|
3e0a78b905 | ||
|
89504f0f2d | ||
|
84c8ff3708 | ||
|
096d1739ac | ||
|
7f1a6d51b2 | ||
|
2a19a8499d | ||
|
8506492a64 | ||
|
1ead555940 | ||
|
0be6df1172 | ||
|
5f97b8858a | ||
|
9fc783d1f5 | ||
|
74b8ee69cc | ||
|
ca333e5603 | ||
|
d1ac00f737 | ||
|
a4d93e4757 | ||
|
4e042e9776 | ||
|
7a8b037e06 | ||
|
8a35bb0328 | ||
|
55f0466ead | ||
|
8d947e5ffa | ||
|
0309ffa615 | ||
|
c7ff04faea | ||
|
b2a04b35af | ||
|
16f0b4e9fc | ||
|
67dc1bfdb6 | ||
|
d7a071301d | ||
|
223833a52d | ||
|
f85d9ab928 | ||
|
579a570d93 | ||
|
8b53d66109 | ||
|
fd93c2d78e | ||
|
8936a96af7 | ||
|
8111fafbad | ||
|
5f4e1cfb41 | ||
|
6a0d9eeadc | ||
|
4d682ae984 |
9
.gitignore
vendored
9
.gitignore
vendored
@ -5,6 +5,15 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.DS_Store
|
||||
.VSCodeCounter
|
||||
.vscode
|
||||
.vscode/*
|
||||
config.json
|
||||
log.txt
|
||||
nohup.out
|
||||
data/config.db
|
||||
config.db
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/main.js"
|
||||
}
|
||||
]
|
||||
}
|
43
.vscode/pssnippets.code-snippets
vendored
Normal file
43
.vscode/pssnippets.code-snippets
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
// Place your pscontrolpanel workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
|
||||
"Hestia Debug Log": {
|
||||
"scope": "javascript",
|
||||
"prefix": "log",
|
||||
"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"
|
||||
},
|
||||
"Run only on Pi": {
|
||||
"scope": "javascript",
|
||||
"prefix": "onpi",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
}
|
69
README.md
69
README.md
@ -1,5 +1,5 @@
|
||||
# pscontrolpanel
|
||||
Node.js Raspberry Pi Pellet Stove Controller
|
||||
# Hestia
|
||||
Node.js Raspberry Pi Pellet Stove Controller, named after the Greek virgin goddess of the hearth.
|
||||
|
||||
# About
|
||||
This project seeks to replace the OEM control panel on a Lennox Winslow PS40 pellet stove with a Raspberry Pi. I will be utilizing a Raspberry Pi Zero W, relays, and switches already installed on the pellet stove. I chose a Pi Zero W for its small form factor, the lack of pre-installed headers, its wireless connectivity, and familiarity with the platform. I had previously used an Arduino Nano for a much more rudimentary version of this project and found great difficulty in making adjustments to the code. Additionally the Raspberry Pi platform will allow for expansion in the future to include IoT controls and logging of usage utilizing networked databases. The project will be written using Node.js and the rpi-gpio Node module. I've chosen Node.js for its familiarity as well as ease of implementation of a web server for future expansion.
|
||||
@ -39,3 +39,68 @@ For ease of adaption, connection, and prototyping I've decided to use Cat 5 ethe
|
||||
* Pause the script by creating a file named `pause` in the root directory.
|
||||
* Reload the environment variables by creating a file named `reload` in the root directory.
|
||||
* Quit the script by creating a file named `quit` in the root directory.
|
||||
|
||||
# Roadmap
|
||||
* v0.1 - Get the pellet stove operating at a basic level. Only implements the auger relay and no safeties.
|
||||
* v0.2 - Implement safety switches and put the igniter and combustion blowers on relays controlled by the Pi.
|
||||
* v0.3 - Implement the HTTP module to allow controlling the stove from the LAN.
|
||||
* v0.4 - Implement usage logging with a SQL database.
|
||||
|
||||
# Testing Procedure
|
||||
1. Launch app, check startup messages, check that it idles and pauses properly.
|
||||
2. Provide ignite command, observe if the igniter, blower, and auger get turned on. Make sure the igniter turns off after the pre-set time.
|
||||
3. Test that the following conditions cause a shutdown:
|
||||
* Proof of Fire Switch OPEN after igniter shutoff.
|
||||
* Vacuum Switch OPEN after igniter start.
|
||||
4. Test manipulation of feed rates.
|
||||
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
Normal file
8
TODO
Normal file
@ -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.
|
5
data/strings.json
Normal file
5
data/strings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"database": {
|
||||
"createConfigTable": ""
|
||||
}
|
||||
}
|
178
functions.js
178
functions.js
@ -1,178 +0,0 @@
|
||||
// Get environment variables
|
||||
const dotenv = require('dotenv').config();
|
||||
// Module for working with files
|
||||
const fs = require('fs');
|
||||
// Promises I think?
|
||||
const { resolve } = require('path');
|
||||
|
||||
|
||||
// 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(7, true, function(err) {
|
||||
if (err) throw err;
|
||||
resolve('Auger turned on.');
|
||||
});
|
||||
} else {
|
||||
console.log('NOPI Auger turned on.');
|
||||
resolve('NOPI Auger turned on.');
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
// Turns the auger off (pin 7 low)
|
||||
off(gpio) {
|
||||
return new Promise((resolve) => {
|
||||
if (process.env.ONPI == 'true') {
|
||||
gpio.write(7, false, function(err) {
|
||||
if (err) throw err;
|
||||
resolve('Auger turned on.');
|
||||
|
||||
});
|
||||
} else {
|
||||
console.log('NOPI Auger turned off.');
|
||||
resolve('NOPI 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 == 'true') console.log(res);
|
||||
// Sleep for the time set in env variables
|
||||
functions.sleep(process.env.ONTIME).then((res) => {
|
||||
// Log action if in debug mode
|
||||
if (process.env.DEBUG == 'true') console.log(res);
|
||||
// Turn the auger off
|
||||
this.off(gpio).then((res) => {
|
||||
// Log action if in debug mode
|
||||
if (process.env.DEBUG == 'true') console.log(res);
|
||||
// Sleep for the time set in env variables
|
||||
functions.sleep(process.env.OFFTIME).then((res) => {
|
||||
// Log action if in debug mode
|
||||
if (process.env.DEBUG == 'true') console.log(res);
|
||||
// Resolve the promise, letting the main script know the cycle is complete
|
||||
resolve("Cycle complete.");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
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");
|
||||
}
|
||||
// Resolve the promise, letting the main script know what we found (nothing)
|
||||
resolve("none");
|
||||
});
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
// Pauses the script for the time defined in env variables
|
||||
pause() {
|
||||
return new Promise((resolve) => {
|
||||
functions.sleep(process.env.PAUSETIME).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 (process.env.DEBUG == 'true') 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=${process.env.ONTIME}\nOFFTIME=${process.env.OFFTIME}\nPAUSETIME=${process.env.PAUSETIME}\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();
|
||||
});
|
||||
|
||||
},
|
||||
// Shutdown the script gracefully
|
||||
quit() {
|
||||
// Delete the quit file
|
||||
fs.unlink('./quit', (err) => {
|
||||
if (err) throw err;
|
||||
if (process.env.DEBUG == 'true') console.log('Removed quit file.');
|
||||
});
|
||||
// Print out that the script is quitting
|
||||
console.log('Quitting...');
|
||||
// Quit the script
|
||||
process.exit();
|
||||
},
|
||||
},
|
||||
// Sleeps for any given milliseconds, call with await
|
||||
sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
if (process.env.DEBUG == "true") 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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Write the current env vars to console
|
||||
console.log('Environment variables:');
|
||||
console.log(`ONTIME=${process.env.ONTIME}\nOFFTIME=${process.env.OFFTIME}\nPAUSETIME=${process.env.PAUSETIME}\nDEBUG=${process.env.DEBUG}\nONPI=${process.env.ONPI}`);
|
||||
// Set up GPIO 4 (pysical pin 7) as output, then call functions.auger.ready()
|
||||
if (process.env.ONPI == 'true') {
|
||||
gpio.setup(7, gpio.DIR_OUT, (err) => {
|
||||
if (err) reject(err);
|
||||
// Resolve the promise
|
||||
resolve('GPIO Initialized');
|
||||
});
|
||||
} else {
|
||||
// Resolve the promise
|
||||
resolve('GPIO Not Available');
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// Export the above object, functions, as a module
|
||||
module.exports = { functions };
|
93
hestia.sh
Executable file
93
hestia.sh
Executable file
@ -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
|
148
main.js
148
main.js
@ -1,81 +1,91 @@
|
||||
// Custom functions module to keep main script clean
|
||||
const fn = require('./functions.js').functions;
|
||||
const fn = require('./modules/functions.js').functions;
|
||||
// Import the config file
|
||||
var config = require('./templates/config.json');
|
||||
// Database Functions
|
||||
const dbfn = require('./modules/database.js');
|
||||
// Web Portal
|
||||
const portal = require('./modules/_server.js');
|
||||
portal.start();
|
||||
|
||||
// Environment Variables Importing
|
||||
const dotenv = require('dotenv').config();
|
||||
dbfn.run(`UPDATE timestamps SET value = ${Date.now()} WHERE key = 'process_start'`).catch(err => console.error(`Error setting process start time: ${err}`));
|
||||
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
config = res.config;
|
||||
// Setup for use with the Pi's GPIO pins
|
||||
if (process.env.ONPI == 'true') {
|
||||
console.log('Running on a Raspberry Pi.');
|
||||
const gpio = require('rpi-gpio');
|
||||
fn.init(gpio).then((res, rej) => {
|
||||
if (res != undefined) {
|
||||
console.log(res);
|
||||
main(fn, gpio);
|
||||
} else {
|
||||
console.error(rej);
|
||||
}
|
||||
});
|
||||
} else if (process.env.ONPI == 'false') {
|
||||
console.log('Not running on a Raspberry Pi.');
|
||||
const gpio = 'gpio';
|
||||
fn.init(gpio).then((res, rej) => {
|
||||
if (res != undefined) {
|
||||
console.log(res);
|
||||
main(fn, gpio);
|
||||
} else {
|
||||
console.error(rej);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('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.
|
||||
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 (process.env.DEBUG == 'true') console.log('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);
|
||||
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 "reload":
|
||||
// Reload the environment variables
|
||||
fn.commands.reload().then(() => {
|
||||
// Rerun this function once the reload has finished
|
||||
main(fn, gpio);
|
||||
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;
|
||||
case "quit":
|
||||
// Quit the script
|
||||
fn.commands.quit();
|
||||
break;
|
||||
case "none":
|
||||
// If no special files are found, cycle the auger normally
|
||||
fn.auger.cycle(gpio).then((res) => {
|
||||
// Log the auger cycle results if in debug mode.
|
||||
if (process.env.DEBUG == 'true') console.log(res);
|
||||
// Rerun this function once the cycle is complete
|
||||
main(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(`No result was received, something is wrong.\nres: ${res}`);
|
||||
fn.commands.quit();
|
||||
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) {
|
||||
// If the auger is enabled
|
||||
if (config.status.auger == 1) {
|
||||
// Run a cycle of the auger
|
||||
fn.auger.cycle(gpio).then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
|
||||
fn.checkForQuit().then(n => {
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
config = res.config;
|
||||
// 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}`);
|
||||
});
|
||||
} else {
|
||||
// If the auger is disabled
|
||||
fn.commands.pause().then(res => {
|
||||
fn.checkForQuit().then(n => {
|
||||
fn.commands.refreshConfig().then(res => {
|
||||
if (process.env.DEBUG) console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res.status}`);
|
||||
config = res.config;
|
||||
// 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}`);
|
||||
// Recursion ecursion cursion ursion rsion sion ion on n
|
||||
main(gpio);
|
||||
});
|
||||
}
|
||||
}
|
83
modules/_server.js
Normal file
83
modules/_server.js
Normal file
@ -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");
|
||||
}
|
||||
};
|
158
modules/_setupdb.js
Normal file
158
modules/_setupdb.js
Normal file
@ -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);
|
||||
});
|
53
modules/database.js
Normal file
53
modules/database.js
Normal file
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
292
modules/functions.js
Normal file
292
modules/functions.js
Normal file
@ -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 };
|
3744
package-lock.json
generated
3744
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -1,9 +1,15 @@
|
||||
{
|
||||
"name": "pscontrolpanel",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"requires": true,
|
||||
"packages": {},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3"
|
||||
"body-parser": "^1.20.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "^4.18.2",
|
||||
"rpi-gpio": "^2.1.7",
|
||||
"sequelize": "^6.28.0",
|
||||
"sqlite3": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
30
templates/config.json
Normal file
30
templates/config.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"status": {
|
||||
"igniter": 0,
|
||||
"blower": 0,
|
||||
"auger": 0,
|
||||
"igniterFinished": false,
|
||||
"shutdown": 0,
|
||||
"vacuum": 0,
|
||||
"pof": 0,
|
||||
"shutdownNextCycle": 0
|
||||
},
|
||||
"timestamps": {
|
||||
"procStart": 0,
|
||||
"blowerOn": 0,
|
||||
"blowerOff": 0,
|
||||
"igniterOn": 0,
|
||||
"igniterOff": 0
|
||||
},
|
||||
"intervals": {
|
||||
"augerOn": "600",
|
||||
"augerOff": "1400",
|
||||
"pause": "3000",
|
||||
"igniterStart": "5000",
|
||||
"blowerStop": "5000"
|
||||
},
|
||||
"web": {
|
||||
"port": 8080,
|
||||
"ip": "0.0.0.0"
|
||||
}
|
||||
}
|
BIN
www/.DS_Store
vendored
Normal file
BIN
www/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
www/public/dancing_jesus.gif
Normal file
BIN
www/public/dancing_jesus.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
98
www/public/main.css
Normal file
98
www/public/main.css
Normal file
@ -0,0 +1,98 @@
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: #333;
|
||||
color: aqua;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#title {
|
||||
text-align: center;
|
||||
font-size: 35px;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
#title a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#safeties {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 20px 5px;
|
||||
font-size: 20px;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.controls-container input {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#log-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#log-area {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background-color: aqua;
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
#trial {
|
||||
display:;
|
||||
color: yellow;
|
||||
font-size: 20px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 0;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #333;
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
.button-selected {
|
||||
margin: 5px;
|
||||
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;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 auto;
|
||||
color: aqua !important;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid;
|
||||
border-collapse: collapse;
|
||||
padding: 3px;
|
||||
}
|
150
www/views/index.html
Normal file
150
www/views/index.html
Normal file
@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<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">
|
||||
</head>
|
||||
<body onload="refreshData()" class="container">
|
||||
<script>
|
||||
// Get the config file
|
||||
const config = <%- config %>;
|
||||
console.log(<%- config %>);
|
||||
</script>
|
||||
<%- include('trial.html') -%>
|
||||
<div id="title" class="text-center mb-4">
|
||||
<a href='./'>Hestia Web Portal</a>
|
||||
</div>
|
||||
<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">
|
||||
<!-- Start | Shutdown | Reload Settings -->
|
||||
<div class="button-container d-flex justify-content-between">
|
||||
<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>
|
||||
<!-- Set feed rates -->
|
||||
<div class="form-group">
|
||||
<label for="feedRate">Feed Rate: </label>
|
||||
<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>
|
||||
</div>
|
||||
<!-- <div class="text-center my-4">
|
||||
<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>
|
||||
<!-- <script src="./main.js"></script> -->
|
||||
<script>
|
||||
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: " + 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>
|
||||
</html>
|
||||
|
65
www/views/index.html.bak
Normal file
65
www/views/index.html.bak
Normal file
@ -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>
|
3
www/views/trial.html
Normal file
3
www/views/trial.html
Normal file
@ -0,0 +1,3 @@
|
||||
<body>
|
||||
<marquee id="trial">YOUR FREE TRIAL HAS ENDED, PLEASE PURCHASE A PELLET STOVE SUBSCRIPTION</marquee>
|
||||
</body>
|
Loading…
Reference in New Issue
Block a user