Merge branch 'v2-back' of https://git.vfsh.dev/voidf1sh/hestia into v2-back

This commit is contained in:
Skylar Grant 2024-09-01 13:22:47 -04:00
commit ff9dfb1f43
7 changed files with 240 additions and 84 deletions

2
.vscode/launch.json vendored
View File

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

View File

@ -5,14 +5,16 @@ module.exports = {
// State class // State class
State: class State { State: class State {
constructor(config) { constructor(config) {
this.publisher = 'backend';
this.igniter = { this.igniter = {
on: false, on: false,
name: "igniter", name: "igniter",
topic: config.mqtt.topics.igniter, topic: config.mqtt.topics.igniter,
publisher: 'front', publisher: this.publisher,
power: (communicator) => { power: (communicator, pinState) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true // Set the power based on the desired pinState
this.igniter.on ? this.igniter.on = false : this.igniter.on = true; this.igniter.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.igniter, JSON.stringify(this.igniter)); communicator.send(config.mqtt.topics.igniter, JSON.stringify(this.igniter));
} }
}; };
@ -21,10 +23,10 @@ module.exports = {
on: false, on: false,
name: "exhaust", name: "exhaust",
topic: config.mqtt.topics.exhaust, topic: config.mqtt.topics.exhaust,
publisher: 'front', publisher: this.publisher,
power: (communicator) => { power: (communicator, pinState) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true // Set the power based on the desired pinState
this.exhaust.on ? this.exhaust.on = false : this.exhaust.on = true; this.exhaust.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.exhaust, JSON.stringify(this.exhaust)); communicator.send(config.mqtt.topics.exhaust, JSON.stringify(this.exhaust));
} }
}; };
@ -34,13 +36,45 @@ module.exports = {
name: "auger", name: "auger",
feedRate: 500, feedRate: 500,
topic: config.mqtt.topics.auger, topic: config.mqtt.topics.auger,
publisher: 'front', publisher: this.publisher,
power: (communicator) => { power: (communicator, pinState) => {
// This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true // Set the power based on the desired pinState
this.auger.on ? this.auger.on = false : this.auger.on = true; this.auger.on = pinState === 1 ? true : false;
communicator.send(config.mqtt.topics.auger, JSON.stringify(this.auger)); 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.`) console.log(`State initialized.`)
}; };
}, },
@ -70,21 +104,43 @@ module.exports = {
} }
}); });
}); });
});
client.on('disconnect', () => {
console.log('Disconnected from MQTT broker');
}); });
// Handle when the Broker sends us a message // Handle when the Broker sends us a message
client.on('message', (topic, message) => { client.on('message', (topic, message) => {
const msgStr = message.toString(); if (topic.startsWith('hestia/status')) {
const msgJson = JSON.parse(msgStr); // Save the existing state
console.log(`Message received on topic ${topic}: ${msgStr}`); const oldState = JSON.parse(JSON.stringify(state));
console.log(msgJson); // The message is a buffer which will need to be converted to string
state[msgJson.name].on = msgJson.on; const msgStr = message.toString();
const change = { // Since the message is a JSON object, we can parse it
name: msgJson.name, const msgJson = JSON.parse(msgStr);
on: msgJson.on // Log the message
}; // console.log(`Message received on topic ${topic}:`);
this.emit('stateChange', change); // 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}`);
}
}); });
} }

View File

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

View File

@ -6,14 +6,22 @@
"topics": { "topics": {
"igniter": "hestia/status/igniter", "igniter": "hestia/status/igniter",
"exhaust": "hestia/status/exhaust", "exhaust": "hestia/status/exhaust",
"auger": "hestia/status/auger" "auger": "hestia/status/auger",
"pof": "hestia/status/pof",
"vacuum": "hestia/status/vacuum",
"startup": "hestia/command/startup",
"shutdown": "hestia/command/shutdown"
} }
}, },
"states": { "states": {
"elements": [ "elements": [
"igniter", "igniter",
"exhaust", "exhaust",
"auger" "auger",
"pof",
"vacuum",
"startup",
"shutdown"
] ]
}, },
"pins": [ "pins": [
@ -60,5 +68,6 @@
"stop": { "stop": {
"exhaustDelay": 600000 "exhaustDelay": 600000
} }
} },
"gpioScript": "src/python/gpio_interface.py"
} }

View File

@ -3,10 +3,10 @@ const debug = process.env.DEBUG === "TRUE";
const config = require('./config.json'); const config = require('./config.json');
const { pins } = config; const { pins } = config;
const gpio = require('./VoidGPIO.js'); const gpio = require('./VoidGPIO.js');
const pinMap = new Map(); process.pinMap = new Map();
for (const pin of pins) { for (const pin of pins) {
pinMap.set(pin.key, pin); process.pinMap.set(pin.key, pin);
} }
module.exports = { module.exports = {
@ -19,7 +19,7 @@ module.exports = {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
}, },
gpio: { gpio: {
setDefaults() { setDefaults(communicator, state) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let stateChanges = []; let stateChanges = [];
@ -27,6 +27,14 @@ module.exports = {
if (pin.mode === 'OUT') { if (pin.mode === 'OUT') {
return gpio.setPin(pin.board, pin.defaultState).then(() => { return gpio.setPin(pin.board, pin.defaultState).then(() => {
stateChanges.push(`Set ${pin.key} pin to ${pin.defaultState}.`); 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)); }).catch(e => console.error(e));
} }
}); });
@ -37,33 +45,11 @@ module.exports = {
}).catch(reject); }).catch(reject);
}); });
}, },
// Boot up sanity check during debug mode async init(communicator, state) {
async debugInit() {
module.exports.log('Resetting all output pins.'); module.exports.log('Resetting all output pins.');
module.exports.gpio.setDefaults().then((changes) => { module.exports.gpio.setDefaults(communicator, state).then((changes) => {
module.exports.log(changes); module.exports.log(changes);
}).catch(e => console.error(e)); }).catch(e => console.error(e));
for (const pin of pins) {
switch (pin.mode) {
case 'OUT':
gpio.togglePin(pin.board).then(() => {
module.exports.log(`Toggled ${pin.key}`);
}).catch(e => console.error(e));
// Wait 1000ms before toggling again.
await module.exports.sleep(1000);
gpio.togglePin(pin.board).then(() => {
module.exports.log(`Toggled ${pin.key}`);
}).catch(e => console.error(e));
break;
case 'IN':
gpio.readPin(pin.board).then(state => {
module.exports.log(`${pin.key} state: ${state}`);
}).catch(e => console.error(e));
default:
break;
}
};
} }
}, },
power: { power: {
@ -78,45 +64,45 @@ module.exports = {
}).catch(e => console.error(e)); }).catch(e => console.error(e));
// Power on igniter // Power on igniter
gpio.setPin(pinMap.igniter.board, 1).then(async () => { gpio.setPin(process.pinMap.igniter.board, 1).then(async () => {
// Wait for igniter preheat // Wait for igniter preheat
await module.exports.sleep(config.power.start.exhaustDelay); await module.exports.sleep(config.power.start.exhaustDelay);
}).catch(e => console.error(e)); }).catch(e => console.error(e));
// Start exhaust // Start exhaust
gpio.setPin(pinMap.exhaust.board, 1).then(async () => { gpio.setPin(process.pinMap.exhaust.board, 1).then(async () => {
// Finish igniter preheat // Finish igniter preheat
await module.exports.sleep(config.power.start.augerDelay); await module.exports.sleep(config.power.start.augerDelay);
}).catch(e => console.error(e)); }).catch(e => console.error(e));
// Check for vacuum // Check for vacuum
gpio.readPin(pinMap.vacuum.board).then(state => { gpio.readPin(process.pinMap.vacuum.board).then(state => {
if (state === '0') { if (state === '0') {
// Power off exhaust // Power off exhaust
gpio.setPin(pinMap.exhaust.board, 0).then(() => { gpio.setPin(process.pinMap.exhaust.board, 0).then(() => {
// Report vacuum failure // Report vacuum failure
reject(new Error('Vacuum failure.')); reject(new Error('Vacuum failure.'));
return; return;
}).catch(e => console.error(e)); }).catch(e => console.error(e));
} else { } else {
// Start auger // Start auger
gpio.setPin(pinMap.auger.board, 1).then(async () => { gpio.setPin(process.pinMap.auger.board, 1).then(async () => {
// Wait for fire // Wait for fire
await module.exports.sleep(config.power.start.fireCheckDelay); await module.exports.sleep(config.power.start.fireCheckDelay);
// Power off igniter // Power off igniter
gpio.setPin(pinMap.igniter.board, 0).then(() => { gpio.setPin(process.pinMap.igniter.board, 0).then(() => {
// Check for fire on pof // Check for fire on pof
gpio.readPin(pinMap.pof.board).then(state => { gpio.readPin(process.pinMap.pof.board).then(state => {
if (state === '0') { if (state === '0') {
// Power off auger // Power off auger
gpio.setPin(pinMap.auger.board, 0).then(() => { gpio.setPin(process.pinMap.auger.board, 0).then(() => {
// Report failed ignition // Report failed ignition
reject(new Error('Failed ignition.')); reject(new Error('Failed ignition.'));
}).catch(e => console.error(e)); }).catch(e => console.error(e));
} else { } else {
// Power off auger // Power off auger
gpio.setPin(pinMap.auger.board, 0).then(() => { gpio.setPin(process.pinMap.auger.board, 0).then(() => {
// Report successful ignition // Report successful ignition
resolve('Successful ignition.'); resolve('Successful ignition.');
}).catch(e => console.error(e)); }).catch(e => console.error(e));
@ -133,11 +119,11 @@ module.exports = {
init() { init() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// Power off auger // Power off auger
gpio.setPin(pinMap.auger.board, 0).then(async () => { gpio.setPin(process.pinMap.auger.board, 0).then(async () => {
// Wait for exhaust shutdown delay // Wait for exhaust shutdown delay
await module.exports.sleep(config.power.stop.exhaustDelay); await module.exports.sleep(config.power.stop.exhaustDelay);
// Power off exhaust // Power off exhaust
gpio.setPin(pinMap.exhaust.board, 0).then(() => { gpio.setPin(process.pinMap.exhaust.board, 0).then(() => {
// Report successful shutdown // Report successful shutdown
resolve('Successful shutdown.'); resolve('Successful shutdown.');
}).catch(e => console.error(e)); }).catch(e => console.error(e));
@ -147,9 +133,23 @@ module.exports = {
} }
}, },
handlers: { handlers: {
stateChange(change) { stateChange(oldState, state) {
gpio.togglePin(pinMap.get(change.name).board).then(() => { const promises = pins.map(pin => {
console.log(`${change.name} toggled.`); 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)); }).catch(e => console.error(e));
} }
} }

View File

@ -9,20 +9,35 @@ const comms = new Communicator(psState);
comms.init(psState, config); comms.init(psState, config);
fn.gpio.debugInit(); fn.gpio.init(comms, psState);
// Sensor detection loop // Sensor detection loop
setInterval(() => { setInterval(() => {
for (const pin of config.pins) { for (const pin of config.pins) {
if (pin.mode === 'IN') { if (pin.mode === 'IN') {
gpio.readPin(pin.board).then(state => { gpio.readPin(pin.board).then(state => {
fn.log(`${pin.key}: ${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)); }).catch(e => console.error(e));
} }
} }
}, 1000); }, 1000);
comms.on('stateChange', (change) => { comms.on('stateChange', (oldState, state) => {
console.log(`State change detected: ${change.name}`); console.log(`State change detected.`);
fn.handlers.stateChange(change); fn.handlers.stateChange(oldState, state);
});
comms.on('startup', () => {
console.log(`Startup detected.`);
fn.power.start.init().catch(e => console.error(e));
});
comms.on('shutdown', () => {
console.log(`Shutdown detected.`);
fn.power.stop.init().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()