Compare commits

...

2 Commits

4 changed files with 154 additions and 15 deletions

View File

@ -4,6 +4,49 @@ Node.js Raspberry Pi Pellet Stove Controller, named after the Greek virgin godde
# 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.
# 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
### Boot

View File

@ -61,7 +61,7 @@
],
"power": {
"start": {
"exhaustDelay": 5000,
"vacuumCheckDelay": 5000,
"igniterPreheat": 60000,
"igniterDelay": 420000
},
@ -69,5 +69,6 @@
"exhaustDelay": 600000
}
},
"gpioScript": "src/python/gpio_interface.py"
"gpioScript": "src/python/gpio_interface.py",
"augerTotalCycleTime": 2000
}

View File

@ -52,16 +52,108 @@ module.exports = {
}).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: {
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: {
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) => {

View File

@ -4,12 +4,12 @@ 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);
process.psState = new State(config);
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
setInterval(() => {
@ -17,9 +17,9 @@ setInterval(() => {
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]));
if (boolState !== process.psState[pin.key].on) {
process.psState[pin.key].on = boolState;
comms.send(config.mqtt.topics[pin.key], JSON.stringify(process.psState[pin.key]));
fn.log(`${pin.key}: ${state}`);
}
}).catch(e => console.error(e));
@ -27,6 +27,9 @@ setInterval(() => {
}
}, 1000);
// Auger feed loop
setInterval(fn.routines.cycleAuger, config.augerTotalCycleTime);
comms.on('stateChange', (oldState, state) => {
console.log(`State change detected.`);
fn.handlers.stateChange(oldState, state);
@ -34,10 +37,10 @@ comms.on('stateChange', (oldState, state) => {
comms.on('startup', () => {
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', () => {
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));
});