Compare commits
2 Commits
9a820741c0
...
f2b0f99941
Author | SHA1 | Date | |
---|---|---|---|
f2b0f99941 | |||
0a349d31e4 |
43
README.md
43
README.md
@ -4,6 +4,49 @@ Node.js Raspberry Pi Pellet Stove Controller, named after the Greek virgin godde
|
|||||||
# About
|
# About
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
# Setting Up the Pi
|
||||||
|
```bash
|
||||||
|
# Update and upgrade the system
|
||||||
|
sudo apt update && sudo apt upgrade -y
|
||||||
|
# Install Node.js and npm along with Python and pip
|
||||||
|
sudo apt install nodejs npm python3 python3-pip
|
||||||
|
# Install PM2 to manage the Node.js process
|
||||||
|
sudo npm install -g pm2
|
||||||
|
# Set up a new user to run the process with home /srv/hestia
|
||||||
|
sudo useradd -m -s /usr/bin/fish hestia
|
||||||
|
# Set hestia home to /srv/hestia
|
||||||
|
sudo usermod -d /srv/hestia hestia
|
||||||
|
# Create a runners group
|
||||||
|
sudo groupadd runners
|
||||||
|
# Add the new user to the runners group
|
||||||
|
sudo usermod -aG runners hestia
|
||||||
|
# Add hestia to the gpio group
|
||||||
|
sudo usermod -aG gpio hestia
|
||||||
|
# Create a directory for the project
|
||||||
|
sudo mkdir /srv/hestia
|
||||||
|
# Change ownership of the directory to the new user
|
||||||
|
sudo chown hestia:runners /srv/hestia -R
|
||||||
|
# Set permissions on the directory
|
||||||
|
sudo chmod 775 /srv/hestia -R
|
||||||
|
# Change to the new user
|
||||||
|
sudo su hestia
|
||||||
|
# Pull the project
|
||||||
|
git clone https://git.vfsh.dev/voidf1sh/hestia.git /srv/hestia
|
||||||
|
# Change to the project directory
|
||||||
|
cd /srv/hestia
|
||||||
|
# Checkout the backend branch
|
||||||
|
git checkout v2-back
|
||||||
|
# Install the project dependencies
|
||||||
|
npm install
|
||||||
|
# Test run
|
||||||
|
node src/main.js
|
||||||
|
# Start with PM2
|
||||||
|
pm2 start src/main.js --name hestia
|
||||||
|
# Save the process list
|
||||||
|
pm2 save
|
||||||
|
# Setup PM2 daemon
|
||||||
|
pm2 startup
|
||||||
|
```
|
||||||
# Logic Flow
|
# Logic Flow
|
||||||
|
|
||||||
### Boot
|
### Boot
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
],
|
],
|
||||||
"power": {
|
"power": {
|
||||||
"start": {
|
"start": {
|
||||||
"exhaustDelay": 5000,
|
"vacuumCheckDelay": 5000,
|
||||||
"igniterPreheat": 60000,
|
"igniterPreheat": 60000,
|
||||||
"igniterDelay": 420000
|
"igniterDelay": 420000
|
||||||
},
|
},
|
||||||
@ -69,5 +69,6 @@
|
|||||||
"exhaustDelay": 600000
|
"exhaustDelay": 600000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"gpioScript": "src/python/gpio_interface.py"
|
"gpioScript": "src/python/gpio_interface.py",
|
||||||
|
"augerTotalCycleTime": 2000
|
||||||
}
|
}
|
@ -52,16 +52,108 @@ module.exports = {
|
|||||||
}).catch(e => console.error(e));
|
}).catch(e => console.error(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
routines: {
|
||||||
|
startup(communicator, state) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const mod = module.exports;
|
||||||
|
// Set pins to default states
|
||||||
|
mod.gpio.setDefaults(communicator, state).then(changes => {
|
||||||
|
mod.log(changes);
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
|
||||||
|
// Turn on the exhaust
|
||||||
|
mod.power.exhaust.on(communicator, state).then((res) => {
|
||||||
|
// Wait for vacuum
|
||||||
|
mod.sleep(config.power.start.vacuumCheckDelay).then(() => {
|
||||||
|
// Vacuum switch is in series with auger so no need for logic check
|
||||||
|
// Turn on the auger
|
||||||
|
mod.power.auger.on(communicator, state).then((res) => {
|
||||||
|
// Wait for auger to start
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
});
|
||||||
|
}).catch(e => console.error(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
shutdown(communicator, state) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cycleAuger() {
|
||||||
|
const mod = module.exports;
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// Check if the auger is enabled
|
||||||
|
if (process.psState.auger.on) {
|
||||||
|
mod.power.auger.on().then((res) => {
|
||||||
|
// Sleep while auger is feeding
|
||||||
|
mod.sleep(process.psState.auger.feedRate).then(() => {
|
||||||
|
// Turn off auger
|
||||||
|
mod.power.auger.off().then((res) => {
|
||||||
|
resolve('Auger cycle complete.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
} else {
|
||||||
|
resolve('Auger is disabled.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
power: {
|
power: {
|
||||||
|
auger: {
|
||||||
|
on() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
gpio.setPin(process.pinMap.get('auger').board, 1).then(() => {
|
||||||
|
resolve('Auger powered on.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
off() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
gpio.setPin(process.pinMap.get('auger').board, 0).then(() => {
|
||||||
|
resolve('Auger powered off.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exhaust: {
|
||||||
|
on() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
gpio.setPin(process.pinMap.get('exhaust').board, 1).then(() => {
|
||||||
|
resolve('Exhaust powered on.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
off() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
gpio.setPin(process.pinMap.get('exhaust').board, 0).then(() => {
|
||||||
|
resolve('Exhaust powered off.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
igniter: {
|
||||||
|
on() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
gpio.setPin(process.pinMap.get('igniter').board, 1).then(() => {
|
||||||
|
resolve('Igniter powered on.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
off() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
gpio.setPin(process.pinMap.get('igniter').board, 0).then(() => {
|
||||||
|
resolve('Igniter powered off.');
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
start: {
|
start: {
|
||||||
init(communicator, state) {
|
init(communicator, state) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
// TODO: Check pin states?
|
// 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
|
// Start the exhaust
|
||||||
this.exhaust().then((res) => {
|
this.exhaust().then((res) => {
|
||||||
|
21
src/main.js
21
src/main.js
@ -4,12 +4,12 @@ const config = require('./custom_modules/config.json');
|
|||||||
const fn = require('./custom_modules/functions.js');
|
const fn = require('./custom_modules/functions.js');
|
||||||
const { State, Communicator } = require('./custom_modules/HestiaClasses.js');
|
const { State, Communicator } = require('./custom_modules/HestiaClasses.js');
|
||||||
|
|
||||||
const psState = new State(config);
|
process.psState = new State(config);
|
||||||
const comms = new Communicator(psState);
|
const comms = new Communicator(process.psState);
|
||||||
|
|
||||||
comms.init(psState, config);
|
comms.init(process.psState, config);
|
||||||
|
|
||||||
fn.gpio.init(comms, psState);
|
fn.gpio.init(comms, process.psState);
|
||||||
|
|
||||||
// Sensor detection loop
|
// Sensor detection loop
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@ -17,9 +17,9 @@ setInterval(() => {
|
|||||||
if (pin.mode === 'IN') {
|
if (pin.mode === 'IN') {
|
||||||
gpio.readPin(pin.board).then(state => {
|
gpio.readPin(pin.board).then(state => {
|
||||||
const boolState = state === '1' ? true : false;
|
const boolState = state === '1' ? true : false;
|
||||||
if (boolState !== psState[pin.key].on) {
|
if (boolState !== process.psState[pin.key].on) {
|
||||||
psState[pin.key].on = boolState;
|
process.psState[pin.key].on = boolState;
|
||||||
comms.send(config.mqtt.topics[pin.key], JSON.stringify(psState[pin.key]));
|
comms.send(config.mqtt.topics[pin.key], JSON.stringify(process.psState[pin.key]));
|
||||||
fn.log(`${pin.key}: ${state}`);
|
fn.log(`${pin.key}: ${state}`);
|
||||||
}
|
}
|
||||||
}).catch(e => console.error(e));
|
}).catch(e => console.error(e));
|
||||||
@ -27,6 +27,9 @@ setInterval(() => {
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
// Auger feed loop
|
||||||
|
setInterval(fn.routines.cycleAuger, config.augerTotalCycleTime);
|
||||||
|
|
||||||
comms.on('stateChange', (oldState, state) => {
|
comms.on('stateChange', (oldState, state) => {
|
||||||
console.log(`State change detected.`);
|
console.log(`State change detected.`);
|
||||||
fn.handlers.stateChange(oldState, state);
|
fn.handlers.stateChange(oldState, state);
|
||||||
@ -34,10 +37,10 @@ comms.on('stateChange', (oldState, state) => {
|
|||||||
|
|
||||||
comms.on('startup', () => {
|
comms.on('startup', () => {
|
||||||
console.log(`Startup detected.`);
|
console.log(`Startup detected.`);
|
||||||
fn.power.start.init(comms, psState).catch(e => console.error(e));
|
fn.power.start.init(comms, process.psState).catch(e => console.error(e));
|
||||||
});
|
});
|
||||||
|
|
||||||
comms.on('shutdown', () => {
|
comms.on('shutdown', () => {
|
||||||
console.log(`Shutdown detected.`);
|
console.log(`Shutdown detected.`);
|
||||||
fn.power.stop.init(comms, psState).catch(e => console.error(e));
|
fn.power.stop.init(comms, process.psState).catch(e => console.error(e));
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user