Compare commits

..

115 Commits

Author SHA1 Message Date
Skylar Grant
0b0a09942d Remove redundant self-call 2023-01-22 23:18:36 -05:00
Skylar Grant
67769232cb Change log viewer 2023-01-22 23:16:11 -05:00
Skylar Grant
38c4a6a644 refresh config on every response 2023-01-22 23:12:21 -05:00
Skylar Grant
e6b64b340e Sure 2023-01-22 23:10:22 -05:00
Skylar Grant
754150a91b Ignore config.db 2023-01-22 23:10:07 -05:00
Skylar Grant
1e1d3488c2 Fix ordering of items before responding to request 2023-01-22 23:09:32 -05:00
Skylar Grant
2bb376868d Fix up the front end JS 2023-01-22 23:09:18 -05:00
Skylar Grant
057b2a6b3a Ignore config.db as it can be regend 2023-01-22 22:56:22 -05:00
Skylar Grant
87631c8aa7 Add db setup 2023-01-22 22:52:05 -05:00
Skylar Grant
7dfafc95d9 Fix process ID 2023-01-22 22:50:09 -05:00
Skylar Grant
3ea70b0898 Change log viewer 2023-01-22 22:48:26 -05:00
Skylar Grant
8f4bf90334 Fix POST response 2023-01-22 22:47:55 -05:00
Skylar Grant
5c852f2c97 Blah 2023-01-22 22:46:14 -05:00
Skylar Grant
f4cfbb71cf Add update option 2023-01-22 22:43:31 -05:00
Skylar Grant
38b767034d Update web portal to use SQLite 2023-01-22 22:42:23 -05:00
Skylar Grant
a7f9a7b6fb Remove db reference 2023-01-22 13:20:31 -05:00
Skylar Grant
a673b15ab9 Move variable 2023-01-22 13:18:43 -05:00
Skylar Grant
9f5811d90f More migration, ready for testing 2023-01-22 11:27:58 -05:00
Skylar Grant
299a8b2efa Refactoring for SQLite 2023-01-21 21:33:52 -05:00
Skylar Grant
6ee01cc214 Begin db migration 2023-01-21 10:48:16 -05:00
Skylar Grant
53e3042940 Remove laggy elements of the portal 2023-01-19 17:48:04 -05:00
Skylar Grant
afce8a68a7 Bug fixes 2023-01-12 19:51:41 -05:00
Skylar Grant
da745e212f New tests 2023-01-12 18:34:58 -05:00
Skylar Grant
44104f11c1 Formatting 2023-01-11 14:09:59 -05:00
Skylar Grant
5393f93a97 . 2023-01-10 13:42:14 -05:00
Skylar Grant
f26090e79a 1 2023-01-10 13:40:44 -05:00
Skylar Grant
df229db09e . 2023-01-10 13:17:06 -05:00
Skylar Grant
6c0d5b5b04 remove original stylesheet 2023-01-10 13:14:54 -05:00
Skylar Grant
72923d94ca Try ChatGPT-generated mobile friendly page 2023-01-10 12:48:19 -05:00
Skylar Grant
7ff3ceab96 Hide quit button 2023-01-06 22:41:16 -05:00
Skylar Grant
eee6d27375 Finesse 2023-01-06 21:53:54 -05:00
Skylar Grant
d8a6ea9577 Moving Thigns Around 2023-01-06 21:33:12 -05:00
Skylar Grant
5b59d6f38f Remove unnecessary refresh buttton 2023-01-06 21:31:14 -05:00
Skylar Grant
cbcc182e9c Forgot a class 2023-01-06 21:30:14 -05:00
Skylar Grant
21f076f403 Minor QoL 2023-01-06 21:29:28 -05:00
Skylar Grant
df349afede Move quit button to prevent accidents 2023-01-06 21:27:35 -05:00
Skylar Grant
69e41e6f0d Add shutdown ability 2023-01-06 21:20:17 -05:00
Skylar Grant
f37ff53bb6 Add way to exit script 2023-01-06 21:19:57 -05:00
Skylar Grant
d7a32e18ce Fix feed rate detection 2023-01-06 20:26:51 -05:00
Skylar Grant
dd2dc0ac65 Remove pause int selection add feed rate reading 2023-01-06 18:53:58 -05:00
Skylar Grant
7cf4677791 remove pause int selection 2023-01-06 18:51:00 -05:00
Skylar Grant
81d34723f1 remove pause int selection 2023-01-06 18:50:03 -05:00
Skylar Grant
d2769fad5e remove PauseInt selection 2023-01-06 18:48:33 -05:00
Skylar Grant
1532d6e3d3 Fix dropdown 2023-01-06 18:47:38 -05:00
Skylar Grant
4daec6513d Load current feed rate 2023-01-06 18:36:12 -05:00
Skylar Grant
e6bcf90741 Change how feed rate is controlled 2023-01-06 18:33:44 -05:00
Skylar Grant
7f58dc87ac Fix auger pin assignment 2023-01-06 18:21:24 -05:00
Skylar Grant
c505c858dc Better logging 2023-01-06 17:32:29 -05:00
Skylar Grant
88206c8208 uhh 2023-01-06 17:32:19 -05:00
Skylar Grant
8a4ab13b0c Update 2023-01-06 17:32:07 -05:00
Skylar Grant
7c19ee0bdc Disable debug mode 2023-01-06 17:26:52 -05:00
Skylar Grant
c982cd535a Ignore created log and config files as they change 2023-01-06 17:24:46 -05:00
Skylar Grant
d177706bb1 remove files 2023-01-06 17:24:25 -05:00
Skylar Grant
72f814b016 Change error handling to show in web log 2023-01-06 17:23:03 -05:00
Skylar Grant
85bf00a35a Set auger off time autometically 2023-01-06 17:12:27 -05:00
Skylar Grant
5da303a24a Fix what to update 2023-01-06 17:09:58 -05:00
Skylar Grant
dfb56ab68c Move status displays away from input 2023-01-06 17:09:09 -05:00
Skylar Grant
283f963e38 bug fixes 2023-01-06 17:04:46 -05:00
Skylar Grant
b7f9bacfa1 simplifying to temp launch 2023-01-06 15:58:42 -05:00
Skylar Grant
f66f705e58 Oops I forgot to commit 2023-01-03 17:01:23 -05:00
Skylar Grant
0f99b092f7 Fix script link 2022-12-22 22:22:35 -05:00
Skylar Grant
5b3a4038b4 Moving stuff around 2022-12-22 22:19:24 -05:00
Skylar Grant
82fe402b2e touching up logic 2022-12-22 22:19:18 -05:00
Skylar Grant
7ddb65783c Adding data for safeties 2022-12-21 21:56:29 -05:00
Skylar Grant
aef7e760a4 apt install --fix-missing 2022-12-21 21:31:59 -05:00
Skylar Grant
626b1d0214 Tweaking of values 2022-12-21 21:17:59 -05:00
Skylar Grant
f0ef52d5c0 Implementing Web Portal 2022-12-21 21:07:46 -05:00
Skylar Grant
8890e8acbd Renaming 2022-12-21 14:16:29 -05:00
Skylar Grant
595d46dda2 Ready for testing on psdev 2022-12-21 13:50:09 -05:00
Skylar Grant
1de8ba78a7 web server sugma 2022-12-20 21:12:22 -05:00
Skylar Grant
c4b29be20f Ignore vsCode files 2022-12-20 21:11:58 -05:00
Skylar Grant
d92815cfc6 afsd 2022-12-20 15:53:57 -05:00
Skylar Grant
040bba5770 adfs 2022-12-20 15:53:25 -05:00
Skylar Grant
5711214734 kjkjkj 2022-12-20 15:53:16 -05:00
Skylar Grant
6d9a4fe2b9 a 2022-12-20 15:52:18 -05:00
Skylar Grant
ac9c8751b8 ignore dsstore 2022-12-20 15:20:18 -05:00
Skylar Grant
b758529d79 Merge branch 'dev' into web 2022-12-20 15:19:38 -05:00
Skylar Grant
0e9b36ead1 fds 2022-12-20 15:17:38 -05:00
Skylar Grant
68306ad224 who knows anymore 2022-12-20 15:17:30 -05:00
Skylar Grant
3e0a78b905 Safeties, Startup, Shutdown 2022-12-20 15:04:37 -05:00
Skylar Grant
89504f0f2d idk anymore 2022-12-20 14:18:24 -05:00
Skylar Grant
84c8ff3708 sobbing 2022-12-19 21:55:50 -05:00
Skylar Grant
096d1739ac struggles 2022-12-19 21:25:17 -05:00
Skylar Grant
7f1a6d51b2 Add foundation for web portal 2022-12-19 13:07:36 -05:00
Skylar Grant
2a19a8499d idk 2022-12-18 21:51:15 -05:00
Skylar Grant
8506492a64 asdf 2022-12-18 21:42:47 -05:00
Skylar Grant
1ead555940 sadf 2022-12-18 21:39:07 -05:00
Skylar Grant
0be6df1172 Fix for quitting hang, I think? 2022-12-18 21:38:13 -05:00
Skylar Grant
5f97b8858a Start blower with igniter 2022-12-18 21:29:50 -05:00
Skylar Grant
9fc783d1f5 Shutdown gracefully now 2022-12-18 20:31:39 -05:00
Skylar Grant
74b8ee69cc Documenting testing procedure 2022-12-18 20:30:25 -05:00
Skylar Grant
ca333e5603 Finish changing config file updates 2022-12-18 13:14:34 -05:00
Skylar Grant
d1ac00f737 Adding sensor test calls 2022-12-18 12:47:03 -05:00
Skylar Grant
a4d93e4757 Change structure of config file to be easier read 2022-12-18 12:46:36 -05:00
Skylar Grant
4e042e9776 Add a roadmap to keep plans in place 2022-12-18 12:45:48 -05:00
Skylar Grant
7a8b037e06 i want to cry 2022-12-16 22:30:12 -05:00
Skylar Grant
8a35bb0328 Moving pins 2022-12-08 13:18:24 -05:00
Skylar Grant
55f0466ead Forgot to loop 2022-12-08 12:54:20 -05:00
Skylar Grant
8d947e5ffa Turn auger on with igniter 2022-12-08 12:53:04 -05:00
Skylar Grant
0309ffa615 Forgot to break switch 2022-12-08 12:51:39 -05:00
Skylar Grant
c7ff04faea Remove temperature pin init 2022-12-08 12:47:50 -05:00
Skylar Grant
b2a04b35af Properly handle rejections 2022-12-08 12:45:46 -05:00
Skylar Grant
16f0b4e9fc Adding startup and shutdown functions 2022-12-08 12:37:37 -05:00
Skylar Grant
67dc1bfdb6 Updating snippets 2022-12-08 12:37:21 -05:00
Skylar Grant
d7a071301d Increment version number 2022-12-06 17:10:33 -05:00
Skylar Grant
223833a52d Moving things from .env to config.json 2022-12-06 17:08:47 -05:00
Skylar Grant
f85d9ab928 Snippets for the code 2022-12-06 17:08:32 -05:00
Skylar Grant
579a570d93 Move some things to config.json from .env 2022-12-06 17:08:23 -05:00
Skylar Grant
8b53d66109 Adding logic for igniter, and improving logging 2022-12-06 17:07:47 -05:00
Skylar Grant
fd93c2d78e On -> Off on log 2022-12-06 00:32:26 -05:00
Skylar Grant
8936a96af7 Matching log style 2022-12-06 00:31:21 -05:00
Skylar Grant
8111fafbad Cleanup 2022-12-06 00:29:03 -05:00
Skylar Grant
5f4e1cfb41 Fancy logging 2022-12-06 00:28:14 -05:00
Skylar Grant
6a0d9eeadc Formatting log & fixing dependencies 2022-12-06 00:20:41 -05:00
Skylar Grant
4d682ae984 Adding new gpio assignments 2022-12-06 00:05:35 -05:00
23 changed files with 5011 additions and 259 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

9
.gitignore vendored
View File

@ -5,6 +5,15 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.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) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

17
.vscode/launch.json vendored Normal file
View 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
View 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"
},
}

View File

@ -1,5 +1,5 @@
# pscontrolpanel # Hestia
Node.js Raspberry Pi Pellet Stove Controller Node.js Raspberry Pi Pellet Stove Controller, named after the Greek virgin goddess of the hearth.
# About # 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 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. * 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. * 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. * 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
View 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
View File

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

View File

@ -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
View 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
View File

@ -1,81 +1,91 @@
// Custom functions module to keep main script clean const fn = require('./modules/functions.js').functions;
const fn = require('./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 dbfn.run(`UPDATE timestamps SET value = ${Date.now()} WHERE key = 'process_start'`).catch(err => console.error(`Error setting process start time: ${err}`));
const dotenv = require('dotenv').config();
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 // Setup for use with the Pi's GPIO pins
if (process.env.ONPI == 'true') { switch (process.env.ONPI) {
console.log('Running on a Raspberry Pi.'); case 'true':
const gpio = require('rpi-gpio'); console.log(`== Running on a Raspberry Pi.`);
fn.init(gpio).then((res, rej) => { var gpio = require('rpi-gpio');
if (res != undefined) { fn.init(gpio).then((res) => {
console.log(res); console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] I: ${res}`);
main(fn, gpio); main(gpio);
} else { }).catch(rej => {
console.error(rej); console.log(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Error during initialization: ${rej}`);
} process.exit(1);
});
} 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);
}); });
break; break;
case "reload": case 'false':
// Reload the environment variables console.log(`I: Not running on a Raspberry Pi.`);
fn.commands.reload().then(() => { var gpio = 'gpio';
// Rerun this function once the reload has finished fn.init(gpio).then(res => {
main(fn, gpio); 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; 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: default:
// If we don't get a result from the file check, or for some reason it's an unexpected response, log it and quit the script. console.log(`[${Date.now() - config.timestamps.procStart}] E: Problem with ENV file.`);
console.error(`No result was received, something is wrong.\nres: ${res}`); process.exit(1);
fn.commands.quit();
break; break;
} }
}).catch(rej => {
console.error(`[${(Date.now() - config.timestamps.procStart)/1000}] E: Problem refreshing the config: ${rej}`);
process.exit(1);
});
function 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,15 @@
{ {
"name": "pscontrolpanel", "name": "pscontrolpanel",
"version": "0.1.0", "version": "0.2.1",
"requires": true, "requires": true,
"packages": {}, "packages": {},
"dependencies": { "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
View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

98
www/public/main.css Normal file
View 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
View 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
View 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
View File

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