diff --git a/.vscode/launch.json b/.vscode/launch.json index c52eeff..c9e16ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "skipFiles": [ "/**" ], - "program": "${workspaceFolder}/main.js" + "program": "${workspaceFolder}/src/main.js" } ] } \ No newline at end of file diff --git a/src/custom_modules/HestiaClasses.js b/src/custom_modules/HestiaClasses.js index ad768e6..369baac 100644 --- a/src/custom_modules/HestiaClasses.js +++ b/src/custom_modules/HestiaClasses.js @@ -5,14 +5,16 @@ module.exports = { // State class State: class State { constructor(config) { + this.publisher = 'backend'; + this.igniter = { on: false, name: "igniter", topic: config.mqtt.topics.igniter, - publisher: 'front', - power: (communicator) => { - // This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true - this.igniter.on ? this.igniter.on = false : this.igniter.on = true; + publisher: this.publisher, + power: (communicator, pinState) => { + // Set the power based on the desired pinState + this.igniter.on = pinState === 1 ? true : false; communicator.send(config.mqtt.topics.igniter, JSON.stringify(this.igniter)); } }; @@ -21,10 +23,10 @@ module.exports = { on: false, name: "exhaust", topic: config.mqtt.topics.exhaust, - publisher: 'front', - power: (communicator) => { - // This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true - this.exhaust.on ? this.exhaust.on = false : this.exhaust.on = true; + publisher: this.publisher, + power: (communicator, pinState) => { + // Set the power based on the desired pinState + this.exhaust.on = pinState === 1 ? true : false; communicator.send(config.mqtt.topics.exhaust, JSON.stringify(this.exhaust)); } }; @@ -34,13 +36,45 @@ module.exports = { name: "auger", feedRate: 500, topic: config.mqtt.topics.auger, - publisher: 'front', - power: (communicator) => { - // This *should* toggle the state, asks if state is true, if it is set it false, otherwise set it true - this.auger.on ? this.auger.on = false : this.auger.on = true; + publisher: this.publisher, + power: (communicator, pinState) => { + // Set the power based on the desired pinState + this.auger.on = pinState === 1 ? true : false; communicator.send(config.mqtt.topics.auger, JSON.stringify(this.auger)); } }; + + this.pof = { + on: false, + name: "pof", + topic: config.mqtt.topics.pof, + publisher: this.publisher, + power: (communicator, pinState) => { + // Set the power based on the desired pinState + this.pof.on = pinState === 1 ? true : false; + communicator.send(config.mqtt.topics.pof, JSON.stringify(this.pof)); + } + }; + + this.vacuum = { + on: false, + name: "vacuum", + topic: config.mqtt.topics.vacuum, + publisher: this.publisher, + power: (communicator, pinState) => { + // Set the power based on the desired pinState + this.vacuum.on = pinState === 1 ? true : false; + communicator.send(config.mqtt.topics.vacuum, JSON.stringify(this.vacuum)); + } + }; + + this.startup = { + topic: config.mqtt.topics.startup + }; + + this.shutdown = { + topic: config.mqtt.topics.shutdown + }; console.log(`State initialized.`) }; }, @@ -70,21 +104,43 @@ module.exports = { } }); }); - + }); + + client.on('disconnect', () => { + console.log('Disconnected from MQTT broker'); }); // Handle when the Broker sends us a message client.on('message', (topic, message) => { - const msgStr = message.toString(); - const msgJson = JSON.parse(msgStr); - console.log(`Message received on topic ${topic}: ${msgStr}`); - console.log(msgJson); - state[msgJson.name].on = msgJson.on; - const change = { - name: msgJson.name, - on: msgJson.on - }; - this.emit('stateChange', change); + if (topic.startsWith('hestia/status')) { + // Save the existing state + const oldState = JSON.parse(JSON.stringify(state)); + // The message is a buffer which will need to be converted to string + const msgStr = message.toString(); + // Since the message is a JSON object, we can parse it + const msgJson = JSON.parse(msgStr); + // Log the message + // console.log(`Message received on topic ${topic}:`); + // console.log(msgJson); + // Check if the message is from the backend + if (msgJson.publisher === this.publisher) { + // console.log('Message is from the backend, ignoring'); + return; + } + // console.log('Message is from the frontend, updating state'); + // Update the state + state[msgJson.name].on = msgJson.on; + // Emit the state change + this.emit('stateChange', oldState, state); + } else if (topic === 'hestia/command/startup') { + // Empty block for 'hestia/command' topics + this.emit('startup'); + } else if (topic === 'hestia/command/shutdown') { + // Empty block for 'hestia/command' topics + this.emit('shutdown'); + } else { + console.log(`Unknown topic: ${topic}`); + } }); } diff --git a/src/custom_modules/VoidGPIO.js b/src/custom_modules/VoidGPIO.js index b52909c..f83f272 100644 --- a/src/custom_modules/VoidGPIO.js +++ b/src/custom_modules/VoidGPIO.js @@ -1,20 +1,21 @@ const { exec } = require('child_process'); +const config = require('./config.json'); module.exports = { // Calls the GPIO Interface script to toggle a pin's state opposite of its current state - togglePin(pin) { - return new Promise((resolve, reject) => { - exec(`python3 src/python/gpio_interface.py toggle ${pin}`, (error, stdout, stderr) => { - if (error) reject(error); - if (stderr) reject(new Error(stderr)); - resolve(); - }); - }); - }, + // togglePin(pin) { + // return new Promise((resolve, reject) => { + // exec(`python3 ${config.gpioScript} toggle ${pin}`, (error, stdout, stderr) => { + // if (error) reject(error); + // if (stderr) reject(new Error(stderr)); + // resolve(); + // }); + // }); + // }, // Calls the GPIO Interface script to read a pin's state readPin(pin) { return new Promise((resolve, reject) => { - exec(`python3 src/python/gpio_interface.py read ${pin}`, (error, stdout, stderr) => { + exec(`python3 ${config.gpioScript} read ${pin}`, (error, stdout, stderr) => { if (error) reject(error); if (stderr) reject(new Error(stderr)); 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 setPin(pin, state) { 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) { reject(error); } diff --git a/src/custom_modules/config.json b/src/custom_modules/config.json index e7a7233..58e42c9 100644 --- a/src/custom_modules/config.json +++ b/src/custom_modules/config.json @@ -6,14 +6,22 @@ "topics": { "igniter": "hestia/status/igniter", "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": { "elements": [ "igniter", "exhaust", - "auger" + "auger", + "pof", + "vacuum", + "startup", + "shutdown" ] }, "pins": [ @@ -60,5 +68,6 @@ "stop": { "exhaustDelay": 600000 } - } + }, + "gpioScript": "src/python/gpio_interface.py" } \ No newline at end of file diff --git a/src/custom_modules/functions.js b/src/custom_modules/functions.js index 0e3fc87..fcaf638 100644 --- a/src/custom_modules/functions.js +++ b/src/custom_modules/functions.js @@ -3,10 +3,10 @@ const debug = process.env.DEBUG === "TRUE"; const config = require('./config.json'); const { pins } = config; const gpio = require('./VoidGPIO.js'); -const pinMap = new Map(); +process.pinMap = new Map(); for (const pin of pins) { - pinMap.set(pin.key, pin); + process.pinMap.set(pin.key, pin); } module.exports = { @@ -19,7 +19,7 @@ module.exports = { return new Promise(resolve => setTimeout(resolve, ms)); }, gpio: { - setDefaults() { + setDefaults(communicator, state) { return new Promise((resolve, reject) => { let stateChanges = []; @@ -27,6 +27,14 @@ module.exports = { if (pin.mode === 'OUT') { return gpio.setPin(pin.board, pin.defaultState).then(() => { stateChanges.push(`Set ${pin.key} pin to ${pin.defaultState}.`); + state[pin.key].power(communicator, pin.defaultState); + }).catch(e => console.error(e)); + } else if (pin.mode === 'IN') { + return gpio.readPin(pin.board).then(pinState => { + const boolState = pinState === '1' ? true : false; + state[pin.key].on = boolState; + communicator.send(config.mqtt.topics[pin.key], JSON.stringify(state[pin.key])); + stateChanges.push(`${pin.key}: ${state}`); }).catch(e => console.error(e)); } }); @@ -37,33 +45,11 @@ module.exports = { }).catch(reject); }); }, - // Boot up sanity check during debug mode - async debugInit() { + async init(communicator, state) { 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); }).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: { @@ -78,45 +64,45 @@ module.exports = { }).catch(e => console.error(e)); // Power on igniter - gpio.setPin(pinMap.igniter.board, 1).then(async () => { + gpio.setPin(process.pinMap.igniter.board, 1).then(async () => { // Wait for igniter preheat await module.exports.sleep(config.power.start.exhaustDelay); }).catch(e => console.error(e)); // Start exhaust - gpio.setPin(pinMap.exhaust.board, 1).then(async () => { + gpio.setPin(process.pinMap.exhaust.board, 1).then(async () => { // Finish igniter preheat await module.exports.sleep(config.power.start.augerDelay); }).catch(e => console.error(e)); // Check for vacuum - gpio.readPin(pinMap.vacuum.board).then(state => { + gpio.readPin(process.pinMap.vacuum.board).then(state => { if (state === '0') { // Power off exhaust - gpio.setPin(pinMap.exhaust.board, 0).then(() => { + gpio.setPin(process.pinMap.exhaust.board, 0).then(() => { // Report vacuum failure reject(new Error('Vacuum failure.')); return; }).catch(e => console.error(e)); } else { // Start auger - gpio.setPin(pinMap.auger.board, 1).then(async () => { + gpio.setPin(process.pinMap.auger.board, 1).then(async () => { // Wait for fire await module.exports.sleep(config.power.start.fireCheckDelay); // Power off igniter - gpio.setPin(pinMap.igniter.board, 0).then(() => { + gpio.setPin(process.pinMap.igniter.board, 0).then(() => { // Check for fire on pof - gpio.readPin(pinMap.pof.board).then(state => { + gpio.readPin(process.pinMap.pof.board).then(state => { if (state === '0') { // Power off auger - gpio.setPin(pinMap.auger.board, 0).then(() => { + gpio.setPin(process.pinMap.auger.board, 0).then(() => { // Report failed ignition reject(new Error('Failed ignition.')); }).catch(e => console.error(e)); } else { // Power off auger - gpio.setPin(pinMap.auger.board, 0).then(() => { + gpio.setPin(process.pinMap.auger.board, 0).then(() => { // Report successful ignition resolve('Successful ignition.'); }).catch(e => console.error(e)); @@ -133,11 +119,11 @@ module.exports = { init() { return new Promise(async (resolve, reject) => { // 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 await module.exports.sleep(config.power.stop.exhaustDelay); // Power off exhaust - gpio.setPin(pinMap.exhaust.board, 0).then(() => { + gpio.setPin(process.pinMap.exhaust.board, 0).then(() => { // Report successful shutdown resolve('Successful shutdown.'); }).catch(e => console.error(e)); @@ -147,9 +133,23 @@ module.exports = { } }, handlers: { - stateChange(change) { - gpio.togglePin(pinMap.get(change.name).board).then(() => { - console.log(`${change.name} toggled.`); + stateChange(oldState, state) { + const promises = pins.map(pin => { + if (oldState[pin.key].on !== state[pin.key].on) { + if (state[pin.key].on) { + return gpio.setPin(pin.board, 1).then(() => { + console.log(`${pin.key} powered on.`); + }).catch(e => console.error(e)); + } else { + return gpio.setPin(pin.board, 0).then(() => { + console.log(`${pin.key} powered off.`); + }).catch(e => console.error(e)); + } + } + }); + + Promise.all(promises).then(() => { + console.log('State change complete.'); }).catch(e => console.error(e)); } } diff --git a/src/main.js b/src/main.js index 4f7595a..396f2f1 100644 --- a/src/main.js +++ b/src/main.js @@ -9,20 +9,35 @@ const comms = new Communicator(psState); comms.init(psState, config); -fn.gpio.debugInit(); +fn.gpio.init(comms, psState); // Sensor detection loop setInterval(() => { for (const pin of config.pins) { if (pin.mode === 'IN') { gpio.readPin(pin.board).then(state => { - 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)); } } }, 1000); -comms.on('stateChange', (change) => { - console.log(`State change detected: ${change.name}`); - fn.handlers.stateChange(change); +comms.on('stateChange', (oldState, state) => { + console.log(`State change detected.`); + fn.handlers.stateChange(oldState, state); +}); + +comms.on('startup', () => { + console.log(`Startup detected.`); + fn.power.start.init().catch(e => console.error(e)); +}); + +comms.on('shutdown', () => { + console.log(`Shutdown detected.`); + fn.power.stop.init().catch(e => console.error(e)); }); \ No newline at end of file diff --git a/src/python/fake_gpio_interface.py b/src/python/fake_gpio_interface.py new file mode 100644 index 0000000..5d5658c --- /dev/null +++ b/src/python/fake_gpio_interface.py @@ -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 []") + 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 ") + 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()