Compare commits

...

84 Commits

Author SHA1 Message Date
Skylar Grant 5c278e9dd5 Better logging 2024-09-04 20:48:25 -04:00
Skylar Grant 94af045e0c semicolon 2024-09-02 13:16:52 -04:00
Skylar Grant 2a756065e7 Try new startup init 2024-09-02 13:16:21 -04:00
Skylar Grant 0a90a93269 Fix call for init 2024-09-02 12:11:51 -04:00
Skylar Grant 49d855e306 Change calls to pinMap 2024-09-02 11:53:12 -04:00
Skylar Grant 00e892c181 Add dev board pinout 2024-09-01 15:14:24 -04:00
Skylar Grant ff9dfb1f43 Merge branch 'v2-back' of https://git.vfsh.dev/voidf1sh/hestia into v2-back 2024-09-01 13:22:47 -04:00
Skylar Grant a7a736bf9c Updated pinmap 2024-09-01 13:22:07 -04:00
Skylar Grant ae4f3c7507 stash the pinmap in the process 2024-08-23 21:17:04 -04:00
Skylar Grant a0a0804754 Add states for commands 2024-08-22 22:15:33 -04:00
Skylar Grant bdeb3152d2 Ready to test startup and shutdown 2024-08-22 22:08:10 -04:00
Skylar Grant 7fa0339791 Set prod 2024-08-22 20:59:04 -04:00
Skylar Grant 42b5576be5 Minor fixes 2024-08-22 20:34:17 -04:00
Skylar Grant 6276a51f05 Mostly working, reconnects after every detected change 2024-08-22 20:15:14 -04:00
Skylar Grant 236faaefbe Logic and comms ready for testing 2024-08-22 20:07:05 -04:00
Skylar Grant 0c1c1483b1 Add missing closing bracket 2024-08-22 19:17:38 -04:00
Skylar Grant 594d177348 Testing improved init logic 2024-08-22 19:16:15 -04:00
Skylar Grant 8a68c00f01 idk 2024-08-21 22:26:20 -04:00
Skylar Grant a0b0d16c73 Boop 2024-08-21 22:18:38 -04:00
Skylar Grant 4e4bf9f7ca Change togglePin to Promise+fix calling of toggle 2024-08-21 22:16:32 -04:00
Skylar Grant 4f35efcf79 Fix events I hope 2024-08-21 22:11:57 -04:00
Skylar Grant a987800cd7 Tentative test for control toggle 2024-08-21 22:02:23 -04:00
Skylar Grant 5fa3d5bb9c Comms working!! 2024-08-21 21:43:56 -04:00
Skylar Grant ccc84173cb Fix import of config and pins 2024-08-21 21:19:46 -04:00
Skylar Grant 24470fe6f2 Reconfigure module 2024-08-21 21:18:43 -04:00
Skylar Grant 9e543989b8 Update config to match frontend 2024-08-21 21:16:48 -04:00
Skylar Grant 673aa2faf5 Logic and MQTT Basics added not tested 2024-08-21 21:01:55 -04:00
Skylar Grant 459008d372 Added more logic flow stuff. 2024-08-20 18:54:34 -04:00
Skylar Grant f0687e4c2b Not really tested 2024-08-19 20:56:39 -04:00
Skylar Grant 178bc1e115 Merge branch 'v2-back' of https://git.vfsh.dev/voidf1sh/hestia into v2-back 2024-08-19 16:10:51 -04:00
Skylar Grant fd4092dbd3 Add package-lock to gitignore 2024-08-19 16:10:08 -04:00
Skylar Grant 6f4d94c17b Logic flow started 2024-08-18 21:14:42 -04:00
Skylar Grant 4666d84905 GPIO working reliably. 2024-08-18 18:52:40 -04:00
Skylar Grant c41016a055 Change exhaust default to off 2024-08-18 18:32:09 -04:00
Skylar Grant f861b1a145 Another reference error 2024-08-18 14:39:07 -04:00
Skylar Grant e7ada41977 Removing more vestigials 2024-08-18 14:37:59 -04:00
Skylar Grant da06ebc49e derp 2024-08-18 14:37:19 -04:00
Skylar Grant 2a4393df73 Maybe fixed now 2024-08-18 14:36:49 -04:00
Skylar Grant 768e9faae5 Semi fixed... 2024-08-18 14:34:23 -04:00
Skylar Grant 1eb1dc8270 Implement custom logging 2024-08-18 14:26:28 -04:00
Skylar Grant 02b2f77d4d Polling working, remove vistgial stuff 2024-08-18 14:12:40 -04:00
Skylar Grant 4d6e058afb Maybe fixed polling? 2024-08-18 14:11:32 -04:00
Skylar Grant 38a110fa8f z 2024-08-18 14:07:07 -04:00
Skylar Grant 2759b0f9c0 Stop poll 2024-08-18 14:06:26 -04:00
Skylar Grant 992ae238eb Reimagined polling 2024-08-18 14:02:14 -04:00
Skylar Grant c3e70ab9c7 Testing polling loop 2024-08-18 13:59:11 -04:00
Skylar Grant 486c3aaa62 Move away from forEach loop 2024-08-18 13:53:55 -04:00
Skylar Grant 1932913396 Didn't actually callback lol 2024-08-18 13:51:40 -04:00
Skylar Grant f0ad09fadd Try not returning but calling? 2024-08-18 13:50:54 -04:00
Skylar Grant 27cc6bafe5 Add more detailed logging 2024-08-18 13:49:41 -04:00
Skylar Grant fee1e0e88e Remove erroneous space 2024-08-18 13:46:51 -04:00
Skylar Grant 7afc662ba7 Move timeout to a promise 2024-08-18 13:46:10 -04:00
Skylar Grant 32893467bb 3.4 almost working 2024-08-18 13:19:29 -04:00
Skylar Grant 3f6f9c3e73 3.3 2024-08-18 13:18:54 -04:00
Skylar Grant 582c05cdc3 3.2 2024-08-18 13:15:44 -04:00
Skylar Grant f16ccbeaa6 3.1 2024-08-18 13:14:57 -04:00
Skylar Grant f596ea1bda 3 2024-08-18 13:14:28 -04:00
Skylar Grant 1413decbe0 2 2024-08-18 13:10:10 -04:00
Skylar Grant 170cbb7792 Restructure logic and test 2024-08-18 12:31:29 -04:00
Skylar Grant 09b524d35b Change pin numbering 2024-08-18 10:21:03 -04:00
Skylar Grant 00a52d0bf5 Fix file references 2024-08-16 20:53:12 -04:00
Skylar Grant 1ce82270f5 Testing better 2024-08-16 20:52:22 -04:00
Skylar Grant 9d084ee37a Testing gpio interface 2024-08-16 20:41:12 -04:00
Skylar Grant c0fe29a204 Update todo 2024-08-15 12:09:55 -04:00
Skylar Grant 805e5e28ab Tentatively got State and Communicator working 2024-08-15 09:30:36 -04:00
Skylar Grant bb3f0cfad5 Import classes 2024-08-15 08:40:20 -04:00
Skylar Grant 288d19e95b Blank mostly 2024-08-15 06:53:26 -04:00
Skylar Grant c7ffc0c296 Update styling 2024-08-14 20:34:31 -04:00
Skylar Grant 470d00f468 Consolidate buttons 2024-08-14 20:31:22 -04:00
Skylar Grant 538775d726 Hoping and praying 2024-08-14 20:29:22 -04:00
Skylar Grant 6fab8e4101 7 2024-08-14 20:27:24 -04:00
Skylar Grant 1e55c058b9 6 2024-08-14 20:26:40 -04:00
Skylar Grant b30caa1c60 Testing 5 2024-08-14 19:18:01 -04:00
Skylar Grant ca30189c64 Testing 4 2024-08-14 19:15:55 -04:00
Skylar Grant 26c2f0a87d revert config 2024-08-14 19:12:49 -04:00
Skylar Grant 1ce103d39c silly brackets 2024-08-14 19:12:01 -04:00
Skylar Grant 8bea9b55e4 I want to scream 2024-08-14 19:11:09 -04:00
Skylar Grant e24a70053d Add config.json, nothing is sensitive in it, use .env 2024-08-14 19:08:06 -04:00
Skylar Grant 76e66c8818 Fix mqtt import 2024-08-14 19:05:55 -04:00
Skylar Grant 6ba5766a30 Re-fix imports 2024-08-14 19:03:32 -04:00
Skylar Grant b8038e7847 Fix import I hope 2024-08-14 19:02:03 -04:00
Skylar Grant 63ef0da043 Testing 2 2024-08-14 19:00:58 -04:00
Skylar Grant 180723e4f5 Testing 1 2024-08-14 16:25:15 -04:00
Skylar Grant 928b1d36da Init, blank 2024-08-14 14:32:46 -04:00
29 changed files with 749 additions and 4925 deletions

BIN
.DS_Store vendored

Binary file not shown.

2
.gitignore vendored
View File

@ -9,11 +9,11 @@ lerna-debug.log*
.VSCodeCounter
.vscode
.vscode/*
config.json
log.txt
nohup.out
data/config.db
config.db
package-lock.json
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

2
.vscode/launch.json vendored
View File

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

View File

@ -2,22 +2,67 @@
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.
This project seeks to replace the OEM control panel on a Lennox Winslow PS40 pellet stove with a Raspberry Pi. I am utilizing a Raspberry Pi 3 Model B+, relays, and temperature snap switches installed on the pellet stove. I chose a Pi 3 for 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 to handle high-level logic and communications, calling Python scripts to handle interacting with GPIO. I'm now in the process of rewriting this project to a more modern v2. Previously I had settled on using deprecated versions of Node.js to maintain compatibility with various Pi GPIO modules, now I've decided to split the GPIO controls off to a small Python interface script.
# Logic Flow
### Boot
* Server starts
* Call python to check pin states and reset them
* Establish connection to MQTT
* Listen for messages published to MQTT
### Auto-Start
* Check pin states for safety
* Power on igniter, 30 second pre-heat
* Power on exhaust
* Power on auger
* Wait X minutes
* Check Proof of Fire switch
* No Fire:
* Shutdown
* Alert
* Fire:
*Power off igniter
### Shutdown
* Check pin states
* Power off auger
* Wait X minutes
* Check Proof of Fire switch
* No Fire:
* Wait X minutes
* Power off exhaust
* Fire:
* Wait X minutes
* Repeat PoF switch check
# GPIO
Three GPIO pins are used along with a common ground to control three relays, supplying 120VAC power to the igniter and combustion blower when appropriate, and supplying power to the auger motor in pulses. Two more GPIO pins are used to detect open/closed status of a temperature-controlled snap switch and a vacuum switch. Another temperature-controlled snap switch is used to supply power to the convection motor when the pellet stove has reached a suitable temperature. A final temperature-controlled snap switch us used to interrupt the circuit for the auger motor to shut the stove off when an over-temperature condition is met. I will be utilizing a OneWire DS18B20 temperature sensor to detect the temperature of air exiting the stove vents.
| Pi Pin | Function | Direction | Wire Color |
| ------:| -------- | --------- | ---------- |
7 | Auger Relay | Out | Blue
13 | Igniter Relay | Out | Blue/White
15 | Combustion Blower Relay | Out | Orange
16 | Proof of Fire Switch | In | Orange/White
18 | OneWire Temp Sensor | In | Brown
22 | Vacuum Switch | In | Brown/White
4 | +5VDC for Switches | N/A | Green
6 | GND for Relays | N/A | Green/White
| Board Pin | BCM Pin | Function | Direction | Wire Color |
| ------:| -------- | -------- | --------- | ---------- |
7 | 4 | Auger Relay | Out | Or/W
13 | 27 | Igniter Relay | Out | Orange
15 | 22 | Exhaust Relay | Out | Gr/W
16 | 23 | Proof of Fire Switch | In | Blue
~~18~~ | ~~24~~ | ~~OneWire Temp Sensor~~ | ~~In~~ | ~~Blue/W~~
22 | 25 | Vacuum Switch | In | Green
4 | N/A | +5VDC for Switches | N/A | Br/W
6 | N/A | GND for Relays | N/A | Brown
### Dev Board Guide
8-Pin DIP:
1: Exhaust
2: Igniter
3: Auger
4: N/A
5: Vacuum
6: PoF
7: Passthrough to 220 Ohm DIP; 7-seg common cathode
8: Passthrough to 220 Ohm DIP; 7-seg common cathode
# Schematics
## The Current Setup
![Current Schematic](/assets/currentschem.png)

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.

15
TODO.md Normal file
View File

@ -0,0 +1,15 @@
# In Progress
1. ~~Strip to bones~~
2. Better commenting
3. Add startup and shutdown logic (implemented, not tested)
4. Connect to MQTT
# Done
1. GPIO Interface
1. Duplicate and adapt `HestiaClasses` from branch `v2-front`
# Immediate To-Do
1. Connect to MQTT
# Roadmap
1. Add wiring and sensors for safeties

View File

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

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

91
main.js
View File

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

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

3770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,19 @@
{
"name": "pscontrolpanel",
"version": "0.2.1",
"requires": true,
"packages": {},
"dependencies": {
"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"
}
"name": "hestia",
"version": "1.0.0",
"description": "Hestia Pellet Stove Controller by Skylar Grant",
"main": "src/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.vfsh.dev/voidf1sh/hestia"
},
"author": "Skylar Grant",
"license": "MIT",
"dependencies": {
"dotenv": "^16.4.5",
"mqtt": "^5.10.0"
}
}

View File

@ -0,0 +1,159 @@
const EventEmitter = require('events');
const mqtt = require('mqtt');
module.exports = {
// State class
State: class State {
constructor(config) {
this.publisher = 'backend';
this.igniter = {
on: false,
name: "igniter",
topic: config.mqtt.topics.igniter,
publisher: this.publisher,
power: (communicator, pinState) => {
// Set the power based on the desired pinState
this.igniter.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.igniter, JSON.stringify(this.igniter));
}
};
this.exhaust = {
on: false,
name: "exhaust",
topic: config.mqtt.topics.exhaust,
publisher: this.publisher,
power: (communicator, pinState) => {
// Set the power based on the desired pinState
this.exhaust.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.exhaust, JSON.stringify(this.exhaust));
}
};
this.auger = {
on: false,
name: "auger",
feedRate: 500,
topic: config.mqtt.topics.auger,
publisher: this.publisher,
power: (communicator, pinState) => {
// Set the power based on the desired pinState
this.auger.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.auger, JSON.stringify(this.auger));
}
};
this.pof = {
on: false,
name: "pof",
topic: config.mqtt.topics.pof,
publisher: this.publisher,
power: (communicator, pinState) => {
// Set the power based on the desired pinState
this.pof.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.pof, JSON.stringify(this.pof));
}
};
this.vacuum = {
on: false,
name: "vacuum",
topic: config.mqtt.topics.vacuum,
publisher: this.publisher,
power: (communicator, pinState) => {
// Set the power based on the desired pinState
this.vacuum.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.vacuum, JSON.stringify(this.vacuum));
}
};
this.startup = {
topic: config.mqtt.topics.startup
};
this.shutdown = {
topic: config.mqtt.topics.shutdown
};
console.log(`State initialized.`)
};
},
// Communicator class
Communicator: class Communicator extends EventEmitter {
constructor(state) {
super();
this.publisher = state.publisher;
}
init(state, config) {
// Connect to the MQTT Broker
console.log(`Attempting MQTT connection to broker: ${config.mqtt.address}, with username: ${config.mqtt.username}`);
this.client = mqtt.connect(config.mqtt.address, {
username: config.mqtt.username,
password: config.mqtt.password
});
const { client } = this;
client.on('connect', () => {
console.log('Connected to MQTT broker');
// Subscribe to status topics
config.states.elements.forEach(element => {
client.subscribe(state[element].topic, (err) => {
if (!err) {
console.log(`Subscribed to ${state[element].topic}`);
}
});
});
});
client.on('disconnect', () => {
console.log('Disconnected from MQTT broker');
});
// Handle when the Broker sends us a message
client.on('message', (topic, message) => {
if (topic.startsWith('hestia/status')) {
// Save the existing state
const oldState = JSON.parse(JSON.stringify(state));
// The message is a buffer which will need to be converted to string
const msgStr = message.toString();
// Since the message is a JSON object, we can parse it
const msgJson = JSON.parse(msgStr);
// Log the message
// console.log(`Message received on topic ${topic}:`);
// console.log(msgJson);
// Check if the message is from the backend
if (msgJson.publisher === this.publisher) {
// console.log('Message is from the backend, ignoring');
return;
}
// console.log('Message is from the frontend, updating state');
// Update the state
state[msgJson.name].on = msgJson.on;
// Emit the state change
this.emit('stateChange', oldState, state);
} else if (topic === 'hestia/command/startup') {
// Empty block for 'hestia/command' topics
this.emit('startup');
} else if (topic === 'hestia/command/shutdown') {
// Empty block for 'hestia/command' topics
this.emit('shutdown');
} else {
console.log(`Unknown topic: ${topic}`);
}
});
}
// Publish a message to the MQTT Broker
send(topic, message) {
// Publish with retain flag set to true
this.client.publish(topic, message, { retain: true }, (err) => {
if (err) {
console.error('Failed to publish message:', err);
} else {
console.log('Message published and retained on topic:', topic);
}
});
}
}
};

View File

@ -0,0 +1,39 @@
const { exec } = require('child_process');
const config = require('./config.json');
module.exports = {
// Calls the GPIO Interface script to toggle a pin's state opposite of its current state
// togglePin(pin) {
// return new Promise((resolve, reject) => {
// exec(`python3 ${config.gpioScript} toggle ${pin}`, (error, stdout, stderr) => {
// if (error) reject(error);
// if (stderr) reject(new Error(stderr));
// resolve();
// });
// });
// },
// Calls the GPIO Interface script to read a pin's state
readPin(pin) {
return new Promise((resolve, reject) => {
exec(`python3 ${config.gpioScript} read ${pin}`, (error, stdout, stderr) => {
if (error) reject(error);
if (stderr) reject(new Error(stderr));
resolve(stdout.trim());
});
});
},
// Calls the GPIO Interface script to set a pin's state regardless of its current state
setPin(pin, state) {
return new Promise((resolve, reject) => {
exec(`python3 ${config.gpioScript} set ${pin} ${state}`, (error, stdout, stderr) => {
if (error) {
reject(error);
}
if (stderr) {
reject(new Error(stderr));
}
resolve();
})
});
}
}

View File

@ -0,0 +1,73 @@
{
"mqtt": {
"address": "wss://mqtt.3411.one",
"username": "hestia",
"password": "hestia",
"topics": {
"igniter": "hestia/status/igniter",
"exhaust": "hestia/status/exhaust",
"auger": "hestia/status/auger",
"pof": "hestia/status/pof",
"vacuum": "hestia/status/vacuum",
"startup": "hestia/command/startup",
"shutdown": "hestia/command/shutdown"
}
},
"states": {
"elements": [
"igniter",
"exhaust",
"auger",
"pof",
"vacuum",
"startup",
"shutdown"
]
},
"pins": [
{
"key": "igniter",
"board": 13,
"bcm": 27,
"mode": "OUT",
"defaultState": 0
},
{
"key": "exhaust",
"board": 15,
"bcm": 22,
"mode": "OUT",
"defaultState": 0
},
{
"key": "auger",
"board": 7,
"bcm": 4,
"mode": "OUT",
"defaultState": 0
},
{
"key": "pof",
"board": 16,
"bcm": 23,
"mode": "IN"
},
{
"key": "vacuum",
"board": 22,
"bcm": 25,
"mode": "IN"
}
],
"power": {
"start": {
"exhaustDelay": 5000,
"igniterPreheat": 60000,
"igniterDelay": 420000
},
"stop": {
"exhaustDelay": 600000
}
},
"gpioScript": "src/python/gpio_interface.py"
}

View File

@ -0,0 +1,185 @@
const dotenv = require('dotenv').config();
const debug = process.env.DEBUG === "TRUE";
const config = require('./config.json');
const { pins } = config;
const gpio = require('./VoidGPIO.js');
process.pinMap = new Map();
for (const pin of pins) {
process.pinMap.set(pin.key, pin);
}
module.exports = {
log(message) {
if (debug) {
console.log(message);
}
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
gpio: {
setDefaults(communicator, state) {
return new Promise((resolve, reject) => {
let stateChanges = [];
const promises = pins.map(pin => {
if (pin.mode === 'OUT') {
return gpio.setPin(pin.board, pin.defaultState).then(() => {
stateChanges.push(`Set ${pin.key} pin to ${pin.defaultState}.`);
state[pin.key].power(communicator, pin.defaultState);
}).catch(e => console.error(e));
} else if (pin.mode === 'IN') {
return gpio.readPin(pin.board).then(pinState => {
const boolState = pinState === '1' ? true : false;
state[pin.key].on = boolState;
communicator.send(config.mqtt.topics[pin.key], JSON.stringify(state[pin.key]));
stateChanges.push(`${pin.key}: ${state}`);
}).catch(e => console.error(e));
}
});
Promise.all(promises).then(() => {
const changes = stateChanges.join('\n');
resolve(changes);
}).catch(reject);
});
},
async init(communicator, state) {
module.exports.log('Resetting all output pins.');
module.exports.gpio.setDefaults(communicator, state).then((changes) => {
module.exports.log(changes);
}).catch(e => console.error(e));
}
},
power: {
start: {
init(communicator, state) {
return new Promise(async (resolve, reject) => {
// TODO: Check pin states?
// Set pins to default states
module.exports.gpio.setDefaults(communicator, state).then(changes => {
module.exports.log(changes);
}).catch(e => console.error(e));
// Start the exhaust
this.exhaust().then((res) => {
module.exports.log(res);
// Check for vacuum
this.vacuum().then((res) => {
module.exports.log(res);
// Start the auger
this.auger().then((res) => {
module.exports.log(res);
resolve('Startup sequence complete.');
}).catch(e => console.error(e));
// Preheat the igniter
this.igniter().then((res) => {
module.exports.log(res);
module.exports.sleep(config.power.start.igniterDelay).then((res) => {
module.exports.log(res);
// Check for fire
this.fire().then((res) => {
module.exports.log(res);
}).catch(e => console.error(e));
});
}).catch(e => console.error(e));
}).catch(e => console.error(e));
}).catch(e => console.error(e));
});
},
exhaust() {
return new Promise(async (resolve, reject) => {
// Start exhaust
gpio.setPin(process.pinMap.get('exhaust').board, 1).then(() => {
// Wait to resolve
module.exports.sleep(config.power.start.exhaustDelay).then(() => {
resolve('Exhaust started.');
});
}).catch(e => console.error(e));
});
},
vacuum() {
return new Promise(async (resolve, reject) => {
// Check for vacuum
gpio.readPin(process.pinMap.get('vacuum').board).then(state => {
if (state === '0') {
reject(new Error('Vacuum failure.'));
} else {
resolve('Vacuum established.');
}
}).catch(e => console.error(e));
});
},
igniter() {
return new Promise(async (resolve, reject) => {
// Start igniter
gpio.setPin(process.pinMap.get('igniter').board, 1).then(() => {
// Wait to resolve
module.exports.sleep(config.power.start.igniterPreheat).then(() => {
resolve('Igniter preheated.');
});
}).catch(e => console.error(e));
});
},
auger() {
return new Promise(async (resolve, reject) => {
// Start auger
gpio.setPin(process.pinMap.get('auger').board, 1).then(() => {
resolve('Auger started.');
}).catch(e => console.error(e));
});
},
fire() {
return new Promise(async (resolve, reject) => {
// Check for fire
gpio.readPin(process.pinMap.get('pof').board).then(state => {
if (state === '0') {
reject(new Error('Failed ignition.'));
} else {
resolve('Successful ignition.');
}
}).catch(e => console.error(e));
});
}
},
stop: {
init() {
return new Promise(async (resolve, reject) => {
// Power off auger
gpio.setPin(process.pinMap.auger.board, 0).then(async () => {
// Wait for exhaust shutdown delay
await module.exports.sleep(config.power.stop.exhaustDelay);
// Power off exhaust
gpio.setPin(process.pinMap.exhaust.board, 0).then(() => {
// Report successful shutdown
resolve('Successful shutdown.');
}).catch(e => console.error(e));
}).catch(e => console.error(e));
});
}
}
},
handlers: {
stateChange(oldState, state) {
const promises = pins.map(pin => {
if (oldState[pin.key].on !== state[pin.key].on) {
if (state[pin.key].on) {
return gpio.setPin(pin.board, 1).then(() => {
console.log(`${pin.key} powered on.`);
}).catch(e => console.error(e));
} else {
return gpio.setPin(pin.board, 0).then(() => {
console.log(`${pin.key} powered off.`);
}).catch(e => console.error(e));
}
}
});
Promise.all(promises).then(() => {
console.log('State change complete.');
}).catch(e => console.error(e));
}
}
}

43
src/main.js Normal file
View File

@ -0,0 +1,43 @@
// Import modules
const gpio = require('./custom_modules/VoidGPIO.js');
const config = require('./custom_modules/config.json');
const fn = require('./custom_modules/functions.js');
const { State, Communicator } = require('./custom_modules/HestiaClasses.js');
const psState = new State(config);
const comms = new Communicator(psState);
comms.init(psState, config);
fn.gpio.init(comms, psState);
// Sensor detection loop
setInterval(() => {
for (const pin of config.pins) {
if (pin.mode === 'IN') {
gpio.readPin(pin.board).then(state => {
const boolState = state === '1' ? true : false;
if (boolState !== psState[pin.key].on) {
psState[pin.key].on = boolState;
comms.send(config.mqtt.topics[pin.key], JSON.stringify(psState[pin.key]));
fn.log(`${pin.key}: ${state}`);
}
}).catch(e => console.error(e));
}
}
}, 1000);
comms.on('stateChange', (oldState, state) => {
console.log(`State change detected.`);
fn.handlers.stateChange(oldState, state);
});
comms.on('startup', () => {
console.log(`Startup detected.`);
fn.power.start.init(comms, psState).catch(e => console.error(e));
});
comms.on('shutdown', () => {
console.log(`Shutdown detected.`);
fn.power.stop.init(comms, psState).catch(e => console.error(e));
});

View File

@ -0,0 +1,75 @@
import sys
# Define the state of the pins
PIN_STATES = {
7: 0,
13: 0,
15: 0,
16: 0,
22: 0
}
# Define a function to get the pin state from the array
def get_pin_state(pin):
if pin in PIN_STATES:
return PIN_STATES[pin]
else:
print(f"Invalid pin: {pin}")
return -1
# Modify the read_pin function to use the get_pin_state function
def read_pin(pin):
try:
# Read pin state
state = get_pin_state(pin)
# Return 1 if pin is HIGH, 0 if LOW
return 1 if state == 1 else 0
except Exception as e:
# Handle errors
print(f"Error reading pin {pin}: {e}")
# Return -1 on error
return -1
# Modify the set_pin_state function to return success always
def set_pin_state(pin, state):
try:
# Set the pin state to either HIGH (1) or LOW (0)
# Exit with 0 for success
return 0
except Exception as e:
# Handle errors
print(f"Error setting pin {pin} to state {state}: {e}")
# Exit with 1 for failure
return 1
def main():
if len(sys.argv) < 3:
print("Usage: python3 gpio_interface.py <command> <pin> [<state>]")
sys.exit(1)
command = sys.argv[1].lower()
pin = int(sys.argv[2])
if command == "toggle":
result = toggle_pin(pin)
sys.exit(result)
elif command == "read":
result = read_pin(pin)
print(result)
sys.exit(0 if result >= 0 else 1)
elif command == "set":
if len(sys.argv) < 4:
print("Usage: python3 gpio_interface.py set <pin> <state>")
sys.exit(1)
state = int(sys.argv[3])
if state not in [0, 1]:
print("Invalid state. Use 0 for LOW or 1 for HIGH.")
sys.exit(1)
result = set_pin_state(pin, state)
sys.exit(result)
else:
print("Invalid command. Use 'toggle', 'read', or 'set'.")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,85 @@
import RPi.GPIO as GPIO
import sys
# Initialize GPIO using Board mode for pin numbering
GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
# Define pin modes and states
def setup_pin(pin, mode):
if mode == 'OUT':
GPIO.setup(pin, GPIO.OUT)
elif mode == 'IN':
GPIO.setup(pin, GPIO.IN)
def toggle_pin(pin):
setup_pin(pin, 'OUT')
current_state = GPIO.input(pin)
try:
# Toggle pin state
GPIO.output(pin, GPIO.LOW if current_state == GPIO.HIGH else GPIO.HIGH)
# Exit with 0 for success
return 0
except Exception as e:
# Handle errors
print(f"Error toggling pin {pin}: {e}")
# Exit with 1 for failure
return 1
def read_pin(pin):
setup_pin(pin, 'IN')
try:
# Read pin state
state = GPIO.input(pin)
# Return 1 if pin is HIGH, 0 if LOW
return 1 if state == GPIO.HIGH else 0
except Exception as e:
# Handle errors
print(f"Error reading pin {pin}: {e}")
# Return -1 on error
return -1
def set_pin_state(pin, state):
setup_pin(pin, 'OUT')
try:
# Set the pin state to either HIGH (1) or LOW (0)
GPIO.output(pin, GPIO.HIGH if state == 1 else GPIO.LOW)
# Exit with 0 for success
return 0
except Exception as e:
# Handle errors
print(f"Error setting pin {pin} to state {state}: {e}")
# Exit with 1 for failure
return 1
def main():
if len(sys.argv) < 3:
print("Usage: python3 gpio_interface.py <command> <pin> [<state>]")
sys.exit(1)
command = sys.argv[1].lower()
pin = int(sys.argv[2])
if command == "toggle":
result = toggle_pin(pin)
sys.exit(result)
elif command == "read":
result = read_pin(pin)
print(result)
sys.exit(0 if result >= 0 else 1)
elif command == "set":
if len(sys.argv) < 4:
print("Usage: python3 gpio_interface.py set <pin> <state>")
sys.exit(1)
state = int(sys.argv[3])
if state not in [0, 1]:
print("Invalid state. Use 0 for LOW or 1 for HIGH.")
sys.exit(1)
result = set_pin_state(pin, state)
sys.exit(result)
else:
print("Invalid command. Use 'toggle', 'read', or 'set'.")
sys.exit(1)
if __name__ == "__main__":
main()

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

View File

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

View File

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

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>

View File

@ -1,3 +0,0 @@
<body>
<marquee id="trial">YOUR FREE TRIAL HAS ENDED, PLEASE PURCHASE A PELLET STOVE SUBSCRIPTION</marquee>
</body>