From c2d94822186382ffe526f350aa6192e45dfde74f Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 30 Oct 2024 00:58:06 -0700 Subject: [PATCH 01/10] Running `gpiod` but removed support for other GPIO mechanisms --- README.md | 4 +++ hdc.py | 60 ++++++++++++++++++++++++---------------- hdc_config.secmon00.json | 40 +++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 hdc_config.secmon00.json diff --git a/README.md b/README.md index 829a453..ab1b9fd 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ HALDOR collects data from *doors*, *PIR*, and one-wire *Temperature Sensors.* +## Branch Description +This branch exists to edit the haldor GPIO data collection methods to include +the now-default and cross-platform `gpiod`. + ## Description HALDOR collects data from the sources listed above. The collected data is reported via MQTT in two different ways: interrupt and checkup. Interrupts are posted on `/event` and trigger automatically when any door or PIR changes state. Checkups are requested via `reporter/checkup_req` and include all sensors when posted on `/checkup`. On a configurable number of checkups, there is a long checkup which includes a configurable system information report. diff --git a/hdc.py b/hdc.py index 6ea87c4..852e28d 100755 --- a/hdc.py +++ b/hdc.py @@ -153,17 +153,14 @@ def on_disconnect(self, client, userdata, rc): # HDC functions def enable_gpio(self): - global GPIO - if not self.config.gpio_path: - logging.debug("Configuring GPIOs") - import orangepi.one - import OPi.GPIO as GPIO - GPIO.setmode(orangepi.one.BOARD) - GPIO.setwarnings(False) - else: + global GPIO, Direction, Bias, Value + if self.config.gpio_path.startswith("/dev/gpiochip"): logging.debug("Configuring GPIOs at: " + self.config.gpio_path) - import RPi.GPIO as GPIO - GPIO.setmode(GPIO.BCM) + import gpiod as GPIO + from gpiod.line import Direction, Bias, Value + self._gpiodict = {} + else: + raise KeyError("This HDC implementation does not support the selected GPIO method.") # sort GPIOs self.runtime = type("Runtime", (object, ), {}) @@ -174,23 +171,21 @@ def enable_gpio(self): self.runtime.ct_ios = {} self.runtime.temp_channels = {} self.runtime.temp_power_sm = {} + logging.debug("Running through I/O configuration.") for acq in self.config.acq_io: if acq.acType == "SW": logging.debug("Configuring Switch: " + str(acq.acObject)) - GPIO.setup(acq.acObject, GPIO.IN, pull_up_down=GPIO.PUD_UP) self.runtime.switch_channels.update({acq.name : acq.acObject}) - self.runtime.ct_ios.update({acq.name : confirmation_threshold(GPIO.input(acq.acObject),3)}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "SW_INV": logging.debug("Configuring invSwitch: " + str(acq.acObject)) - GPIO.setup(acq.acObject, GPIO.IN, pull_up_down=GPIO.PUD_UP) self.runtime.flip_channels.update({acq.name : acq.acObject}) - self.runtime.ct_ios.update({acq.name : confirmation_threshold(not GPIO.input(acq.acObject),3)}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "PIR": logging.debug("Configuring PIR Sensor: " + str(acq.acObject)) - GPIO.setup(acq.acObject, GPIO.IN, pull_up_down=GPIO.PUD_UP) self.runtime.pir_channels.update({acq.name : acq.acObject}) - self.runtime.ct_ios.update({acq.name : confirmation_threshold(GPIO.input(acq.acObject),3)}) self.runtime.last_pir_state.update({acq.name : 0}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "TEMP": logging.debug("Configuring Temperature Sensor: " + str(acq.acObject)) self.runtime.temp_channels.update({acq.name : acq.acObject}) @@ -202,8 +197,7 @@ def enable_gpio(self): except AttributeError: logging.debug("Configuring Temperature Power Fault: " + str(acq.acObject)) self.runtime.temp_fault = acq.acObject - GPIO.setup(acq.acObject, GPIO.IN, pull_up_down=GPIO.PUD_UP) - self.runtime.temp_fault_sm = confirmation_threshold(not GPIO.input(acq.acObject),3) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "TEMP_EN": try: self.runtime.temp_en @@ -211,13 +205,31 @@ def enable_gpio(self): except AttributeError: logging.debug("Configuring Temperature Power Enable: " + str(acq.acObject)) self.runtime.temp_en = acq.acObject - GPIO.setup(acq.acObject, GPIO.OUT) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT)}) self.runtime.temp_power_commanded = True self.runtime.temp_power_on = True self.runtime.temp_power_last = True else: raise KeyError('"' + acq.acType + '"' + " is not a valid acquisition type") + logging.debug(f"GPIO configuration generated: {self._gpiodict}") + logging.debug("Applying configuration.") + self._gpioreq = GPIO.request_lines(self.config.gpio_path ,consumer=self.config.name ,config=self._gpiodict) + + logging.debug("Starting debouncing.") + # Switches + for name, line in self.runtime.switch_channels.items(): + self.runtime.ct_ios.update({name : confirmation_threshold(1 if self._gpioreq.get_value(line) == Value.ACTIVE else 0, 3)}) + # Inverted Switches + for name, line in self.runtime.flip_channels.items(): + self.runtime.ct_ios.update({name : confirmation_threshold(0 if self._gpioreq.get_value(line) == Value.ACTIVE else 1, 3)}) + # PIR sensors + for name, line in self.runtime.pir_channels.items(): + self.runtime.ct_ios.update({name : confirmation_threshold(1 if self._gpioreq.get_value(line) == Value.ACTIVE else 0, 3)}) + # Temperature Fault + if hasattr(self.runtime, "temp_fault"): + self.runtime.temp_fault_sm = confirmation_threshold(0 if self._gpioreq.get_value(self.runtime.temp_en) == Value.ACTIVE else 1, 3) + def notify(self, path, params, retain=False): params['time'] = str(time.time()) logging.debug(params) @@ -315,7 +327,7 @@ def checkup(self): logging.warn("Temp sensor \"" + ts_name + "down!") self.runtime.temp_power_last = self.runtime.temp_power_on checks["Temp Power"] = int(self.runtime.temp_power_on) - GPIO.output(self.runtime.temp_en, self.runtime.temp_power_on) + self._gpioreq.set_value(self.runtime.temp_en, self.runtime.temp_power_on) self.notify('checkup', checks) @@ -328,16 +340,16 @@ def io_check(self): self.io_check_count += 1 logging.debug("IO check " + str(self.io_check_count)) for name, chan in self.runtime.switch_channels.items(): - result = self.runtime.ct_ios[name].update(GPIO.input(chan)) + result = self.runtime.ct_ios[name].update(1 if self._gpioreq.get_value(chan) == Value.ACTIVE else 0) # value confirmed if result[0]: checks[name] = result[1] for name, chan in self.runtime.flip_channels.items(): - result = self.runtime.ct_ios[name].update(int (not GPIO.input(chan))) + result = self.runtime.ct_ios[name].update(0 if self._gpioreq.get_value(chan) == Value.ACTIVE else 1) if result[0]: checks[name] = result[1] for name, chan in self.runtime.pir_channels.items(): - result = self.runtime.ct_ios[name].update(GPIO.input(chan)) + result = self.runtime.ct_ios[name].update(1 if self._gpioreq.get_value(chan) == Value.ACTIVE else 0) # PIR's are special because they like to be on and are only turned off during # timed checkups if result[0] and result[1] and not self.runtime.last_pir_state[name]: @@ -346,7 +358,7 @@ def io_check(self): # don't run the temperature power control if there is no such thing. try: - result = self.runtime.temp_fault_sm.update(not GPIO.input(self.runtime.temp_fault)) + result = self.runtime.temp_fault_sm.update(0 if self._gpioreq.get_value(self.runtime.temp_fault) == Value.ACTIVE else 1) if result[0]: checks["Temp Power Fault"] = result[1] except AttributeError: diff --git a/hdc_config.secmon00.json b/hdc_config.secmon00.json new file mode 100644 index 0000000..b192a1e --- /dev/null +++ b/hdc_config.secmon00.json @@ -0,0 +1,40 @@ +{ + "name": "secmon00", + "description": "This is the security monitor configuration file.", + "boot_check_list": { }, + "acq_io":[ + { + "name": "TestPIR0", + "acType": "PIR", + "acObject": 20 + }, + { + "name": "TestPIR1", + "acType": "PIR", + "acObject": 10 + }, + { + "name": "TestPIR2", + "acType": "PIR", + "acObject": 9 + }, + { + "name": "TestPIR3", + "acType": "PIR", + "acObject": 8 + }, + { + "name": "TestPIR4", + "acType": "PIR", + "acObject": 19 + } + ], + "long_checkup_freq": 100, + "long_checkup_leng": 0, + "pidfile":"/tmp/daisy.pid", + "gpio_path": "/dev/gpiochip0", + "mqtt_broker": "hal", + "mqtt_port": 1883, + "mqtt_timeout": 60, + "loglevel": "DEBUG" +} From c89560227ee7d8d808a47f2772a81424cfcc75ae Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 5 Dec 2024 06:49:52 -0800 Subject: [PATCH 02/10] Updated for PIR testing. Changed IO thread to report every 1s, PIR confirmation threshold to be 1 time, and PIR reporting to be on change. --- hdc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hdc.py b/hdc.py index 852e28d..bb005e1 100755 --- a/hdc.py +++ b/hdc.py @@ -225,7 +225,7 @@ def enable_gpio(self): self.runtime.ct_ios.update({name : confirmation_threshold(0 if self._gpioreq.get_value(line) == Value.ACTIVE else 1, 3)}) # PIR sensors for name, line in self.runtime.pir_channels.items(): - self.runtime.ct_ios.update({name : confirmation_threshold(1 if self._gpioreq.get_value(line) == Value.ACTIVE else 0, 3)}) + self.runtime.ct_ios.update({name : confirmation_threshold(1 if self._gpioreq.get_value(line) == Value.ACTIVE else 0, 1)}) # Temperature Fault if hasattr(self.runtime, "temp_fault"): self.runtime.temp_fault_sm = confirmation_threshold(0 if self._gpioreq.get_value(self.runtime.temp_en) == Value.ACTIVE else 1, 3) @@ -352,7 +352,7 @@ def io_check(self): result = self.runtime.ct_ios[name].update(1 if self._gpioreq.get_value(chan) == Value.ACTIVE else 0) # PIR's are special because they like to be on and are only turned off during # timed checkups - if result[0] and result[1] and not self.runtime.last_pir_state[name]: + if result[0] and result[1] != self.runtime.last_pir_state[name]: checks[name] = result[1] self.runtime.last_pir_state[name] = result[1] @@ -391,7 +391,7 @@ def run(self): self.connect(self.config.mqtt_broker, self.config.mqtt_port, self.config.mqtt_timeout) atexit.register(self.disconnect) self.notify_bootup() - self.ioPolling = MultiTimer(interval=5, function=self.io_check) + self.ioPolling = MultiTimer(interval=1, function=self.io_check) self.ioPolling.start() atexit.register(self.ioPolling.stop) break From 4d675f5018fad24ea105b0afe72aa0e1d2f584aa Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 15 Dec 2024 04:01:21 -0800 Subject: [PATCH 03/10] Added groundwork for an output channel. Still to add a token and HMAC mechanism. --- hdc.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/hdc.py b/hdc.py index bb005e1..9a35f87 100755 --- a/hdc.py +++ b/hdc.py @@ -171,6 +171,7 @@ def enable_gpio(self): self.runtime.ct_ios = {} self.runtime.temp_channels = {} self.runtime.temp_power_sm = {} + self.runtime.output_channels = {} logging.debug("Running through I/O configuration.") for acq in self.config.acq_io: if acq.acType == "SW": @@ -198,6 +199,10 @@ def enable_gpio(self): logging.debug("Configuring Temperature Power Fault: " + str(acq.acObject)) self.runtime.temp_fault = acq.acObject self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) + elif acq.acType == "OUT": + logging.debug("Configuring Output: " + str(acq.acObject)) + self.runtime.output_channels.update({acq.name : acq.acObject}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT)}) elif acq.acType == "TEMP_EN": try: self.runtime.temp_en @@ -355,7 +360,9 @@ def io_check(self): if result[0] and result[1] != self.runtime.last_pir_state[name]: checks[name] = result[1] self.runtime.last_pir_state[name] = result[1] - + for name, chan in self.runtime.output_channels.items(): + logging.debug(f"Channel {chan} outputting value {self.runtime.output_values[chan]}") + self._gpioreq.set_value(chan, self.runtime.output_values[chan]) # don't run the temperature power control if there is no such thing. try: result = self.runtime.temp_fault_sm.update(0 if self._gpioreq.get_value(self.runtime.temp_fault) == Value.ACTIVE else 1) From efd48ee9b2c2baa6cf066dfc880daa009ac6298a Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 18 Dec 2024 01:36:32 -0800 Subject: [PATCH 04/10] Modifications to stage for GPIO output. Logging upgrades. HMAC message authentication. --- hdc.py | 121 ++++++++++++++++++++-------------- maglab_crypto/__init__.py | 135 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 maglab_crypto/__init__.py diff --git a/hdc.py b/hdc.py index 9a35f87..688b781 100755 --- a/hdc.py +++ b/hdc.py @@ -11,8 +11,12 @@ from typing import * from multitimer import MultiTimer from confirmation_threshold import confirmation_threshold +from maglab_crypto import MAGToken from threading import Event +def conv_value(my_int): + return Value.Inactive if my_int == 0 else Value.Active + class HDCDaemon(Daemon): def run(self): h_datacollector = HDC() @@ -31,7 +35,7 @@ def run(self): class Acquisition: name: str acType: str - acObject: Union[List[str], int] + acObject: Union[List[str], int, List[int]] # state machine for temperature sensor power network restart # should probably add the state machine diagram in ascii art here @@ -98,44 +102,49 @@ class config: mqtt_port: int mqtt_timeout: int temp_max_restart: int = 3 + tokens: Optional[List[str]] = None loglevel: Optional[str] = None # overloaded MQTT functions from (mqtt.Client) def on_log(self, client, userdata, level, buff): if level == mqtt.MQTT_LOG_DEBUG: - logging.debug("PAHO MQTT DEBUG: " + buff) + self.log.debug("PAHO MQTT DEBUG: " + buff) elif level == mqtt.MQTT_LOG_INFO: - logging.info("PAHO MQTT INFO: " + buff) + self.log.info("PAHO MQTT INFO: " + buff) elif level == mqtt.MQTT_LOG_NOTICE: - logging.info("PAHO MQTT NOTICE: " + buff) + self.log.info("PAHO MQTT NOTICE: " + buff) elif level == mqtt.MQTT_LOG_WARNING: - logging.warning("PAHO MQTT WARN: " + buff) + self.log.warning("PAHO MQTT WARN: " + buff) else: - logging.error("PAHO MQTT ERROR: " + buff) + self.log.error("PAHO MQTT ERROR: " + buff) def on_connect(self, client, userdata, flags, rc): - logging.info("Connected: " + str(rc)) + self.log.info("Connected: " + str(rc)) self.subscribe("reporter/checkup_req") self.subscribe(self.config.name + "/temp_power") + self.subscribe(f"{self.config.name}/cmd") def on_message(self, client, userdata, message): if (message.topic == "reporter/checkup_req"): - logging.info("Checkup received.") + self.log.info("Checkup received.") self.checkup() elif (message.topic == self.config.name + "/temp_power"): decoded = message.payload.decode('utf-8') - logging.debug("Temperature sensor power command received: " + decoded) + self.log.debug("Temperature sensor power command received: " + decoded) if (decoded.lower() == "false" or decoded == "0"): - logging.info("Temperature sensor power commanded off") + self.log.info("Temperature sensor power commanded off") self.runtime.temp_power_commanded = False else: - logging.info("Temperature sensor power commanded on") + self.log.info("Temperature sensor power commanded on") self.runtime.temp_power_commanded = True + elif message.topic == f"{self.config.name}/cmd": + if self.mag_token: + commands = self.mag_token.cmd_msg_auth(message.payload.decode("utf-8"), 7200) def on_disconnect(self, client, userdata, rc): - logging.warning("Disconnected: " + str(rc)) + self.log.warning("Disconnected: " + str(rc)) if rc != 0: - logging.error("Unexpected disconnection. Attempting reconnection.") + self.log.error("Unexpected disconnection. Attempting reconnection.") reconnect_count = 0 while (reconnect_count < 10): try: @@ -143,19 +152,19 @@ def on_disconnect(self, client, userdata, rc): self.reconnect() break except OSError: - logging.error("Connection error while trying to reconnect.") - logging.error(traceback.format_exc()) - logging.error("Waiting to restart.") + self.log.error("Connection error while trying to reconnect.") + self.log.error(traceback.format_exc()) + self.log.error("Waiting to restart.") self.tEvent.wait(30) if reconnect_count >= 10: - logging.critical("Too many reconnect tries. Exiting.") + self.log.critical("Too many reconnect tries. Exiting.") os._exit(1) # HDC functions def enable_gpio(self): global GPIO, Direction, Bias, Value if self.config.gpio_path.startswith("/dev/gpiochip"): - logging.debug("Configuring GPIOs at: " + self.config.gpio_path) + self.log.debug("Configuring GPIOs at: " + self.config.gpio_path) import gpiod as GPIO from gpiod.line import Direction, Bias, Value self._gpiodict = {} @@ -172,23 +181,23 @@ def enable_gpio(self): self.runtime.temp_channels = {} self.runtime.temp_power_sm = {} self.runtime.output_channels = {} - logging.debug("Running through I/O configuration.") + self.log.debug("Running through I/O configuration.") for acq in self.config.acq_io: if acq.acType == "SW": - logging.debug("Configuring Switch: " + str(acq.acObject)) + self.log.debug("Configuring Switch: " + str(acq.acObject)) self.runtime.switch_channels.update({acq.name : acq.acObject}) self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "SW_INV": - logging.debug("Configuring invSwitch: " + str(acq.acObject)) + self.log.debug("Configuring invSwitch: " + str(acq.acObject)) self.runtime.flip_channels.update({acq.name : acq.acObject}) self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "PIR": - logging.debug("Configuring PIR Sensor: " + str(acq.acObject)) + self.log.debug("Configuring PIR Sensor: " + str(acq.acObject)) self.runtime.pir_channels.update({acq.name : acq.acObject}) self.runtime.last_pir_state.update({acq.name : 0}) self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "TEMP": - logging.debug("Configuring Temperature Sensor: " + str(acq.acObject)) + self.log.debug("Configuring Temperature Sensor: " + str(acq.acObject)) self.runtime.temp_channels.update({acq.name : acq.acObject}) self.runtime.temp_power_sm.update({acq.name : TempSensorPower(self.config.temp_max_restart)}) elif acq.acType == "TEMP_FAULT": @@ -196,19 +205,25 @@ def enable_gpio(self): self.runtime.temp_fault raise KeyError("Temperature sensor fault channel already allocated") except AttributeError: - logging.debug("Configuring Temperature Power Fault: " + str(acq.acObject)) + self.log.debug("Configuring Temperature Power Fault: " + str(acq.acObject)) self.runtime.temp_fault = acq.acObject self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP)}) elif acq.acType == "OUT": - logging.debug("Configuring Output: " + str(acq.acObject)) - self.runtime.output_channels.update({acq.name : acq.acObject}) - self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT)}) + self.log.debug("Configuring Output: " + str(acq.acObject)) + try: + self.runtime.output_channels.update({acq.name : acq.acObject[0]}) + self.runtime.output_values.update({acq.name : acq.acObject[1]}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acQbject[1]))}) + except TypeError: + self.runtime.output_channels.update({acq.name : acq.acObject}) + self.runtime.output_values.update({acq.name : 0}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=Value.Inactive)}) elif acq.acType == "TEMP_EN": try: self.runtime.temp_en raise KeyError("Temperature sensor enable channel already allocated") except AttributeError: - logging.debug("Configuring Temperature Power Enable: " + str(acq.acObject)) + self.log.debug("Configuring Temperature Power Enable: " + str(acq.acObject)) self.runtime.temp_en = acq.acObject self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT)}) self.runtime.temp_power_commanded = True @@ -217,11 +232,11 @@ def enable_gpio(self): else: raise KeyError('"' + acq.acType + '"' + " is not a valid acquisition type") - logging.debug(f"GPIO configuration generated: {self._gpiodict}") - logging.debug("Applying configuration.") + self.log.debug(f"GPIO configuration generated: {self._gpiodict}") + self.log.debug("Applying configuration.") self._gpioreq = GPIO.request_lines(self.config.gpio_path ,consumer=self.config.name ,config=self._gpiodict) - logging.debug("Starting debouncing.") + self.log.debug("Starting debouncing.") # Switches for name, line in self.runtime.switch_channels.items(): self.runtime.ct_ios.update({name : confirmation_threshold(1 if self._gpioreq.get_value(line) == Value.ACTIVE else 0, 3)}) @@ -237,16 +252,16 @@ def enable_gpio(self): def notify(self, path, params, retain=False): params['time'] = str(time.time()) - logging.debug(params) + self.log.debug(params) topic = self.config.name + '/' + path self.publish(topic, json.dumps(params), retain=retain) - logging.info("Published " + topic) + self.log.info("Published " + topic) def notify_bootup(self): boot_checks = {} - logging.debug("Bootup:") + self.log.debug("Bootup:") for bc_name, bc_cmd in self.config.boot_check_list.items(): boot_checks[bc_name] = subprocess.check_output( @@ -283,7 +298,7 @@ def check_temp(self, temp_path): def signal_handler(self, signum, frame): # so far, we only need to handle signals that make the program exit. - logging.warning("Caught a deadly signal: " + str(signum) + "!") + self.log.warning("Caught a deadly signal: " + str(signum) + "!") if self.ioPolling: self.ioPolling.stop() self.running = False @@ -327,9 +342,9 @@ def checkup(self): self.runtime.temp_power_on = self.runtime.temp_power_sm[ts_name].run(self.runtime.temp_power_last, self.runtime.temp_power_on, received, self.runtime.temp_power_fault) if self.runtime.temp_power_sm[ts_name].broke: if self.runtime.temp_power_sm[ts_name].state == TempSensorPower.PowerState.RESTART: - logging.warn("Temp sensor \"" + ts_name + "down and causing one-wire network restart!") + self.log.warn("Temp sensor \"" + ts_name + "down and causing one-wire network restart!") else: - logging.warn("Temp sensor \"" + ts_name + "down!") + self.log.warn("Temp sensor \"" + ts_name + "down!") self.runtime.temp_power_last = self.runtime.temp_power_on checks["Temp Power"] = int(self.runtime.temp_power_on) self._gpioreq.set_value(self.runtime.temp_en, self.runtime.temp_power_on) @@ -343,7 +358,7 @@ def io_check(self): self.io_check_count = 0 else: self.io_check_count += 1 - logging.debug("IO check " + str(self.io_check_count)) + self.log.debug("IO check " + str(self.io_check_count)) for name, chan in self.runtime.switch_channels.items(): result = self.runtime.ct_ios[name].update(1 if self._gpioreq.get_value(chan) == Value.ACTIVE else 0) # value confirmed @@ -361,7 +376,7 @@ def io_check(self): checks[name] = result[1] self.runtime.last_pir_state[name] = result[1] for name, chan in self.runtime.output_channels.items(): - logging.debug(f"Channel {chan} outputting value {self.runtime.output_values[chan]}") + self.log.debug(f"Channel {chan} outputting value {self.runtime.output_values[chan]}") self._gpioreq.set_value(chan, self.runtime.output_values[chan]) # don't run the temperature power control if there is no such thing. try: @@ -374,21 +389,25 @@ def io_check(self): if checks: self.notify('event', checks) else: - logging.debug("Noting changed between timed io checks") + self.log.debug("Noting changed between timed io checks") def run(self): + self.log = logging.getLogger(__name__) self.tEvent = Event() self.running = True startup_count = 0 self.io_check_count = 0 self.loop_count = 0 + self.mag_token = None + if self.config.tokens: + self.mag_token = MAGToken(self.config.tokens) try: if type(logging.getLevelName(self.config.loglevel.upper())) is int: logging.basicConfig(level=self.config.loglevel.upper()) else: - logging.warning("Log level not configured. Defaulting to WARNING.") + self.log.warning("Log level not configured. Defaulting to WARNING.") except (KeyError, AttributeError) as e: - logging.warning("Log level not configured. Defaulting to WARNING. Caught: " + str(e)) + self.log.warning("Log level not configured. Defaulting to WARNING. Caught: " + str(e)) self.bootup() while startup_count < 10: @@ -403,16 +422,16 @@ def run(self): atexit.register(self.ioPolling.stop) break except OSError: - logging.error("Error connecting on bootup.") - logging.error(traceback.format_exc()) - logging.error("Waiting to reconnect...") + self.log.error("Error connecting on bootup.") + self.log.error(traceback.format_exc()) + self.log.error("Waiting to reconnect...") self.tEvent.wait(30) if startup_count >= 10: - logging.critical("Too many startup tries. Exiting.") + self.log.critical("Too many startup tries. Exiting.") os._exit(1) - logging.info("Startup success.") + self.log.info("Startup success.") self.reconnect_me = False self.inner_reconnect_try = 0 while self.running and (self.inner_reconnect_try < 10): @@ -432,11 +451,11 @@ def run(self): except (socket.timeout, TimeoutError, ConnectionError): self.inner_reconnect_try += 1 self.reconnect_me = True - logging.error("MQTT loop error. Attempting to reconnect: " + inner_reconnect_try + "/10") + self.log.error("MQTT loop error. Attempting to reconnect: " + inner_reconnect_try + "/10") except: - logging.critical("Exception in MQTT loop.") - logging.critical(traceback.format_exc()) - logging.critical("Exiting.") + self.log.critical("Exception in MQTT loop.") + self.log.critical(traceback.format_exc()) + self.log.critical("Exiting.") exit(2) if self.inner_reconnect_try >= 10: exit(1) diff --git a/maglab_crypto/__init__.py b/maglab_crypto/__init__.py new file mode 100644 index 0000000..5a5954b --- /dev/null +++ b/maglab_crypto/__init__.py @@ -0,0 +1,135 @@ +# mag laboratory cryptography library +# currently only contains the token class + +import re +import json +import base64 +import zlib +import hashlib +import hmac +import time +import logging + +class MAGBase64: + @staticmethod + def b64enc(obj): + """ encode in base 64 (and without padding) """ + return base64.b64encode(obj).decode("utf-8").rstrip('=') + + @staticmethod + def b64pad(line): + """ pad for the python b64 library """ + num = (4 - len(line) % 4) % 4 + return f"{line}{'=' * num}" + + + +# class containing token utility +class MAGToken: + MINCTLEN = 2 # minimum central token length + B64CRCLEN = 6 # base 64 encoded CRC length + _tokens = [] + log = None + + def __init__(self, tokens, start = "magld_"): + self.log = logging.getLogger(__name__) + # set the prefix that we are supposed to decode + self.start = start + # call token decode function + self.tokens_decode(tokens) + + def tokens_decode(self, tokens): + idx = 0 + for token in tokens: + try: + idx += 1 + self._tokens.append(self.token_decode(token)) + except AssertionError: + self.log.error(f"Token {idx} not recognized!") + + if self._tokens: + self.log.debug("Tokens decoded") + else: + self.log.critical("No tokens accepted.") + + @staticmethod + def token_decode(token): + """ + decodes and validates the token + returns a byte array with the central token when decoded + """ + token = token.rstrip() + # length verification + assert len(token) >= len(MAGToken.start) + MAGToken.MINCTLEN + MAGToken.B64CRCLEN + # header verification + assert token[0:len(MAGToken.start)].lower() == MAGToken.start + # retrieve token in byte array form + # pad token with magical number of pad characters to make the base64 decode happy + central_token = MAGBase64.b64pad(token[len(MAGToken.start):-MAGToken.B64CRCLEN]) + central_token = base64.base64decode(str.encode(central_token)) + + # retrieve the precalculated checksum inside the token + end_checksum = token[-MAGToken.B64CRCLEN:] + # although the default is big endian for most libraries, we use little endian here to keep + # consistent with the encoding schemes used by other famous token systems... + calc_checksum = MAGBase64.b64enc(zlib.crc32(central_token).to_bytes(4, "little")) + # checksum verification + assert calc_checksum == end_checksum + + return central_token + + @staticmethod + def wr_hmac(msg, token): + """ calculate the HMAC based on a token and the message """ + log = logging.getLogger(__name__) + log.debug(f"HMAC calculation utility called with: {msg} and {token}") + obj = hmac.new(token, msg=str.encode(msg), digestmod=hashlib.sha265) + return MAGBase64.b64enc(obj.digest()) + + def hmac_auth(self, msg, code): + """ message authentication function """ + self.log.debug(f"msg_auth called with: {msg} and {cdoe}") + match = False + for token in self._tokens: + calc = MAGToken.wr_hmac(msg, token) + logging.debug(f"Calculated hmac as: {calc}") + if calc == code: + match = True + break + # throw an assertion if there are no matches + assert match + + def cmd_msg_auth(self, raw_msg, max_time): + """ + The JSON and HMAC code are contained in a `pair` from Kotlin and two-element `tuple` + we run this text output through this regex to decode the values within. + + The HMAC here is base64 encoded. + + Function returns None if there are no matches and a dictionary if it matches + """ + # assume unaccepted by default + retval = None + self.log.debug(f"Received in command channel: {raw_msg}") + # regex to break down the pair or tuple + matches = re.fullmatch(r"\([\"\']?(\{.+\})[\"\']?\, [\"\']?(.*?)[\"\']?\)", raw_msg) + if matches is not None: + self.log.debug(f"The split strings are: {matches[1]} and {matches[2]}") + try: + data = json.loads(matches[1]) + # validate message time + current_time = time.time() + sent_time = data["time"] + diff_time = current_time - sent_time + self.log.debug(f"Message time validation; Current: {current_time}, "\ + f"Sent: {sent_time}, Diff: {diff_time}") + assert abs(diff_time) <= max_time + + self.hmac_auth(matches[1], matches[2]) + + retval = data + except (json.JSONDecodeError, AttributeError, AssertionError) as exc: + self.log.error(str(exc)) + + return retval + From 8fcaae14b1091d78cf0c6fca6a0deacc5ed59626 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 24 Dec 2024 02:17:28 -0800 Subject: [PATCH 05/10] Completed IO output code; yet to test. Debugged token library code: decoding tokens. --- hdc.py | 34 ++++++++++++++++++++++++---------- maglab_crypto/__init__.py | 17 ++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/hdc.py b/hdc.py index 688b781..77980fc 100755 --- a/hdc.py +++ b/hdc.py @@ -15,7 +15,7 @@ from threading import Event def conv_value(my_int): - return Value.Inactive if my_int == 0 else Value.Active + return Value.INACTIVE if my_int == 0 or my_int == False or (type(my_int) == str and my_int.lower() == "off") else Value.ACTIVE class HDCDaemon(Daemon): def run(self): @@ -138,8 +138,21 @@ def on_message(self, client, userdata, message): self.log.info("Temperature sensor power commanded on") self.runtime.temp_power_commanded = True elif message.topic == f"{self.config.name}/cmd": - if self.mag_token: - commands = self.mag_token.cmd_msg_auth(message.payload.decode("utf-8"), 7200) + if self.mag_token: + commands = self.mag_token.cmd_msg_auth(message.payload.decode("utf-8"), 7200) + if commands: + line_values = {} + for name, value in commands.items(): + if name in self.runtime.output_channels.keys(): + if conv_value(value) == Value.ACTIVE: + line_values.update({self.runtime.output_channels[name]:Value.ACTIVE}) + self.runtime.output_channels.update({name:Value.ACTIVE}) + else: + line_values.update({self.runtime.output_channels[name]:Value.INACTIVE}) + self.runtime.output_channels.update({name:Value.INACTIVE}) + self._gpioreq.set_values(line_values) + + def on_disconnect(self, client, userdata, rc): self.log.warning("Disconnected: " + str(rc)) @@ -212,12 +225,12 @@ def enable_gpio(self): self.log.debug("Configuring Output: " + str(acq.acObject)) try: self.runtime.output_channels.update({acq.name : acq.acObject[0]}) - self.runtime.output_values.update({acq.name : acq.acObject[1]}) - self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acQbject[1]))}) + self.runtime.output_values.update({acq.name : conv_value(acq.acObject[1])}) + self._gpiodict.update({acq.acObject[0] : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acQbject[1]))}) except TypeError: self.runtime.output_channels.update({acq.name : acq.acObject}) - self.runtime.output_values.update({acq.name : 0}) - self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=Value.Inactive)}) + self.runtime.output_values.update({acq.name : Value.INACTIVE}) + self._gpiodict.update({acq.acObject : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE)}) elif acq.acType == "TEMP_EN": try: self.runtime.temp_en @@ -377,7 +390,7 @@ def io_check(self): self.runtime.last_pir_state[name] = result[1] for name, chan in self.runtime.output_channels.items(): self.log.debug(f"Channel {chan} outputting value {self.runtime.output_values[chan]}") - self._gpioreq.set_value(chan, self.runtime.output_values[chan]) + self._gpioreq.set_value(chan, self.runtime.output_values[name]) # don't run the temperature power control if there is no such thing. try: result = self.runtime.temp_fault_sm.update(0 if self._gpioreq.get_value(self.runtime.temp_fault) == Value.ACTIVE else 1) @@ -399,8 +412,6 @@ def run(self): self.io_check_count = 0 self.loop_count = 0 self.mag_token = None - if self.config.tokens: - self.mag_token = MAGToken(self.config.tokens) try: if type(logging.getLevelName(self.config.loglevel.upper())) is int: logging.basicConfig(level=self.config.loglevel.upper()) @@ -409,6 +420,9 @@ def run(self): except (KeyError, AttributeError) as e: self.log.warning("Log level not configured. Defaulting to WARNING. Caught: " + str(e)) + if self.config.tokens: + self.mag_token = MAGToken(self.config.tokens) + self.bootup() while startup_count < 10: try: diff --git a/maglab_crypto/__init__.py b/maglab_crypto/__init__.py index 5a5954b..46337e1 100644 --- a/maglab_crypto/__init__.py +++ b/maglab_crypto/__init__.py @@ -31,7 +31,7 @@ class MAGToken: _tokens = [] log = None - def __init__(self, tokens, start = "magld_"): + def __init__(self, tokens, start = "magls_"): self.log = logging.getLogger(__name__) # set the prefix that we are supposed to decode self.start = start @@ -42,6 +42,7 @@ def tokens_decode(self, tokens): idx = 0 for token in tokens: try: + self.log.debug(f"Decoding token {idx}...") idx += 1 self._tokens.append(self.token_decode(token)) except AssertionError: @@ -52,21 +53,22 @@ def tokens_decode(self, tokens): else: self.log.critical("No tokens accepted.") - @staticmethod - def token_decode(token): + def token_decode(self, token): """ decodes and validates the token returns a byte array with the central token when decoded """ token = token.rstrip() # length verification - assert len(token) >= len(MAGToken.start) + MAGToken.MINCTLEN + MAGToken.B64CRCLEN + self.log.debug("Checking token length.") + assert len(token) >= len(self.start) + MAGToken.MINCTLEN + MAGToken.B64CRCLEN # header verification - assert token[0:len(MAGToken.start)].lower() == MAGToken.start + self.log.debug("Checking token header.") + assert token[0:len(self.start)].lower() == self.start # retrieve token in byte array form # pad token with magical number of pad characters to make the base64 decode happy - central_token = MAGBase64.b64pad(token[len(MAGToken.start):-MAGToken.B64CRCLEN]) - central_token = base64.base64decode(str.encode(central_token)) + central_token = MAGBase64.b64pad(token[len(self.start):-MAGToken.B64CRCLEN]) + central_token = base64.b64decode(str.encode(central_token)) # retrieve the precalculated checksum inside the token end_checksum = token[-MAGToken.B64CRCLEN:] @@ -74,6 +76,7 @@ def token_decode(token): # consistent with the encoding schemes used by other famous token systems... calc_checksum = MAGBase64.b64enc(zlib.crc32(central_token).to_bytes(4, "little")) # checksum verification + self.log.debug("Checking token checksum.") assert calc_checksum == end_checksum return central_token From 0309ccdebebfcb9ab85b012398ba35f498879879 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 24 Dec 2024 19:35:12 -0800 Subject: [PATCH 06/10] Debugged IO output. --- hdc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hdc.py b/hdc.py index 77980fc..6844e5b 100755 --- a/hdc.py +++ b/hdc.py @@ -194,6 +194,7 @@ def enable_gpio(self): self.runtime.temp_channels = {} self.runtime.temp_power_sm = {} self.runtime.output_channels = {} + self.runtime.output_values = {} self.log.debug("Running through I/O configuration.") for acq in self.config.acq_io: if acq.acType == "SW": @@ -226,7 +227,7 @@ def enable_gpio(self): try: self.runtime.output_channels.update({acq.name : acq.acObject[0]}) self.runtime.output_values.update({acq.name : conv_value(acq.acObject[1])}) - self._gpiodict.update({acq.acObject[0] : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acQbject[1]))}) + self._gpiodict.update({acq.acObject[0] : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acObject[1]))}) except TypeError: self.runtime.output_channels.update({acq.name : acq.acObject}) self.runtime.output_values.update({acq.name : Value.INACTIVE}) @@ -389,7 +390,7 @@ def io_check(self): checks[name] = result[1] self.runtime.last_pir_state[name] = result[1] for name, chan in self.runtime.output_channels.items(): - self.log.debug(f"Channel {chan} outputting value {self.runtime.output_values[chan]}") + self.log.debug(f"IO {name} outputting value {self.runtime.output_values[name]}") self._gpioreq.set_value(chan, self.runtime.output_values[name]) # don't run the temperature power control if there is no such thing. try: From b37d257b8d982aa37554b6e810a0ff39597778ae Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 25 Dec 2024 02:04:23 -0800 Subject: [PATCH 07/10] Changed the gpiod interface to have the name of the python program. --- hdc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hdc.py b/hdc.py index 6844e5b..9b8ec89 100755 --- a/hdc.py +++ b/hdc.py @@ -248,7 +248,7 @@ def enable_gpio(self): self.log.debug(f"GPIO configuration generated: {self._gpiodict}") self.log.debug("Applying configuration.") - self._gpioreq = GPIO.request_lines(self.config.gpio_path ,consumer=self.config.name ,config=self._gpiodict) + self._gpioreq = GPIO.request_lines(self.config.gpio_path, consumer=f"{self.config.name}:hdc.py", config=self._gpiodict) self.log.debug("Starting debouncing.") # Switches From 9c028ab0e80f19d16058d8b29eef0a8e344f1eb3 Mon Sep 17 00:00:00 2001 From: Brandon Date: Sun, 29 Dec 2024 23:17:00 -0800 Subject: [PATCH 08/10] Adding test PIR toggle script which toggles the LEDs according to the PIRs --- led_change.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100755 led_change.py diff --git a/led_change.py b/led_change.py new file mode 100755 index 0000000..b33c568 --- /dev/null +++ b/led_change.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import logging +import json +import paho.mqtt.publish as publish +import paho.mqtt.client as mqtt +from maglab_crypto import MAGToken + +class LED_RELAY(mqtt.Client): + "Main class which toggles LEDs when PIR modules detect motion" + def __init__(self, token): + self.log = logging.getLogger(__name__) + self.token = token + + mqtt.Client.__init__(self, mqtt.CallbackAPIVersion.VERSION2) + + def on_log(self, _, __, level, buff): + """ Overloaded MQTT log function """ + if level == mqtt.MQTT_LOG_DEBUG: + self.log.debug(f"PAHO: {buff}") + elif level == mqtt.MQTT_LOG_INFO: + self.log.info(f"PAHO: {buff}") + elif level == mqtt.MQTT_LOG_NOTICE: + self.log.info(f"PAHO: {buff}") + else: + self.log.error(f"PAHO: {buff}") + + def on_connect(self, _, __, ___, reason, ____): + self.log.info(f"MQTT connected: {reason}") + self.subscribe("secmon00/+") + + def on_message(self, _, __, msg): + if msg.topic.startswith("secmon00/"): + out_d = {} + decoded = msg.payload.decode('utf-8') + self.log.debug(f"Motion message received: {decoded}") + try: + data = json.loads(decoded) + for i in range(5): + if f"TestPIR{i}" in data: + out_d.update({f"LEDPIR{i}" : data[f"TestPIR{i}"]}) + if len(out_d) > 0: + cmd_msg = str(MAGToken.cmd_msg_gen(out_d, self.token)) + self.publish("secmon00/cmd", cmd_msg) + except json.JSONDecodeError as exc: + self.log.info(str(exc)) + + def main(self): + self.connect("hal.maglab", 1883, 60) + self.loop_forever() + + +if __name__ == "__main__": + logging.basicConfig(level="DEBUG") + token = "magls_NXQmv+RixRJnH3gbUq2Ttp/85Zd9qantr7DrZQV6DMWw" + token = MAGToken.token_decode("magls_", token) + + relay = LED_RELAY(token) + relay.main() From dd462cc953af61313e52f91542957baa15c96be9 Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 30 Dec 2024 01:30:47 -0800 Subject: [PATCH 09/10] Debugged token library. Refined GPIO output code. --- hdc.py | 12 +++++----- maglab_crypto/__init__.py | 46 +++++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/hdc.py b/hdc.py index 9b8ec89..95a4d5f 100755 --- a/hdc.py +++ b/hdc.py @@ -138,18 +138,18 @@ def on_message(self, client, userdata, message): self.log.info("Temperature sensor power commanded on") self.runtime.temp_power_commanded = True elif message.topic == f"{self.config.name}/cmd": + self.log.info(f"Received command: {message.payload.decode('utf-8')}") if self.mag_token: commands = self.mag_token.cmd_msg_auth(message.payload.decode("utf-8"), 7200) if commands: line_values = {} for name, value in commands.items(): if name in self.runtime.output_channels.keys(): - if conv_value(value) == Value.ACTIVE: - line_values.update({self.runtime.output_channels[name]:Value.ACTIVE}) - self.runtime.output_channels.update({name:Value.ACTIVE}) - else: - line_values.update({self.runtime.output_channels[name]:Value.INACTIVE}) - self.runtime.output_channels.update({name:Value.INACTIVE}) + output = conv_value(value) + channel = self.runtime.output_channels[name] + line_values.update({channel:output}) + self.runtime.output_values.update({name:output}) + self.log.info(f"{name} on {channel} set to {output}") self._gpioreq.set_values(line_values) diff --git a/maglab_crypto/__init__.py b/maglab_crypto/__init__.py index 46337e1..3c60626 100644 --- a/maglab_crypto/__init__.py +++ b/maglab_crypto/__init__.py @@ -44,7 +44,7 @@ def tokens_decode(self, tokens): try: self.log.debug(f"Decoding token {idx}...") idx += 1 - self._tokens.append(self.token_decode(token)) + self._tokens.append(MAGToken.token_decode(self.start, token)) except AssertionError: self.log.error(f"Token {idx} not recognized!") @@ -53,21 +53,23 @@ def tokens_decode(self, tokens): else: self.log.critical("No tokens accepted.") - def token_decode(self, token): + @staticmethod + def token_decode(start, token): + log = logging.getLogger(__name__) """ decodes and validates the token returns a byte array with the central token when decoded """ token = token.rstrip() # length verification - self.log.debug("Checking token length.") - assert len(token) >= len(self.start) + MAGToken.MINCTLEN + MAGToken.B64CRCLEN + log.debug("Checking token length.") + assert len(token) >= len(start) + MAGToken.MINCTLEN + MAGToken.B64CRCLEN # header verification - self.log.debug("Checking token header.") - assert token[0:len(self.start)].lower() == self.start + log.debug("Checking token header.") + assert token[0:len(start)].lower() == start # retrieve token in byte array form # pad token with magical number of pad characters to make the base64 decode happy - central_token = MAGBase64.b64pad(token[len(self.start):-MAGToken.B64CRCLEN]) + central_token = MAGBase64.b64pad(token[len(start):-MAGToken.B64CRCLEN]) central_token = base64.b64decode(str.encode(central_token)) # retrieve the precalculated checksum inside the token @@ -76,7 +78,7 @@ def token_decode(self, token): # consistent with the encoding schemes used by other famous token systems... calc_checksum = MAGBase64.b64enc(zlib.crc32(central_token).to_bytes(4, "little")) # checksum verification - self.log.debug("Checking token checksum.") + log.debug("Checking token checksum.") assert calc_checksum == end_checksum return central_token @@ -86,12 +88,12 @@ def wr_hmac(msg, token): """ calculate the HMAC based on a token and the message """ log = logging.getLogger(__name__) log.debug(f"HMAC calculation utility called with: {msg} and {token}") - obj = hmac.new(token, msg=str.encode(msg), digestmod=hashlib.sha265) + obj = hmac.new(token, msg=str.encode(msg), digestmod=hashlib.sha256) return MAGBase64.b64enc(obj.digest()) def hmac_auth(self, msg, code): """ message authentication function """ - self.log.debug(f"msg_auth called with: {msg} and {cdoe}") + self.log.debug(f"msg_auth called with: {msg} and {code}") match = False for token in self._tokens: calc = MAGToken.wr_hmac(msg, token) @@ -113,13 +115,15 @@ def cmd_msg_auth(self, raw_msg, max_time): """ # assume unaccepted by default retval = None + IDX_MSG = 1 + IDX_CODE = 2 self.log.debug(f"Received in command channel: {raw_msg}") # regex to break down the pair or tuple matches = re.fullmatch(r"\([\"\']?(\{.+\})[\"\']?\, [\"\']?(.*?)[\"\']?\)", raw_msg) if matches is not None: - self.log.debug(f"The split strings are: {matches[1]} and {matches[2]}") + self.log.debug(f"The split strings are: {matches[IDX_MSG]} and {matches[IDX_CODE]}") try: - data = json.loads(matches[1]) + data = json.loads(matches[IDX_MSG]) # validate message time current_time = time.time() sent_time = data["time"] @@ -127,8 +131,11 @@ def cmd_msg_auth(self, raw_msg, max_time): self.log.debug(f"Message time validation; Current: {current_time}, "\ f"Sent: {sent_time}, Diff: {diff_time}") assert abs(diff_time) <= max_time - - self.hmac_auth(matches[1], matches[2]) + # a possible way to prevent repeat attacks is to store old message codes since + # commands will always have a time attached to ensure uniqueness + + # validate message code + self.hmac_auth(matches[IDX_MSG], matches[IDX_CODE]) retval = data except (json.JSONDecodeError, AttributeError, AssertionError) as exc: @@ -136,3 +143,14 @@ def cmd_msg_auth(self, raw_msg, max_time): return retval + @staticmethod + def cmd_msg_gen(msg, token): + """ + The message is as a dictionary. + + The token is as a byte array + """ + msg["time"] = time.time() + msg_out = json.dumps(msg) + code = MAGToken.wr_hmac(msg_out, token) + return (msg_out, code) From 8f1ad33e05479905ac24b58de5215df3a59e37ad Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 18 Feb 2025 22:46:59 -0800 Subject: [PATCH 10/10] Added conversion function and debugged conversion function. --- hdc.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/hdc.py b/hdc.py index 95a4d5f..35d3927 100755 --- a/hdc.py +++ b/hdc.py @@ -14,8 +14,16 @@ from maglab_crypto import MAGToken from threading import Event -def conv_value(my_int): - return Value.INACTIVE if my_int == 0 or my_int == False or (type(my_int) == str and my_int.lower() == "off") else Value.ACTIVE +def conv_value(myVal, trueVal, falseVal): + rValue = trueVal + if myVal == 0: + rValue = falseVal + if myVal == False: + rValue = falseVal + if type(myVal) == str: + if myVal.lower() == "off" or myVal.lower() == "false": + rValue = falseVal + return rValue class HDCDaemon(Daemon): def run(self): @@ -144,8 +152,10 @@ def on_message(self, client, userdata, message): if commands: line_values = {} for name, value in commands.items(): + if name == "temp_power": + self.log.debug(f"Temperature sensor power command received: {decoded}") if name in self.runtime.output_channels.keys(): - output = conv_value(value) + output = conv_value(value, Value.ACTIVE, Value.INACTIVE) channel = self.runtime.output_channels[name] line_values.update({channel:output}) self.runtime.output_values.update({name:output}) @@ -226,8 +236,8 @@ def enable_gpio(self): self.log.debug("Configuring Output: " + str(acq.acObject)) try: self.runtime.output_channels.update({acq.name : acq.acObject[0]}) - self.runtime.output_values.update({acq.name : conv_value(acq.acObject[1])}) - self._gpiodict.update({acq.acObject[0] : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acObject[1]))}) + self.runtime.output_values.update({acq.name : conv_value(acq.acObject[1], Value.ACTIVE, Value.INACTIVE)}) + self._gpiodict.update({acq.acObject[0] : GPIO.LineSettings(direction=Direction.OUTPUT, output_value=conv_value(acq.acObject[1], Value.ACTIVE, Value.INACTIVE))}) except TypeError: self.runtime.output_channels.update({acq.name : acq.acObject}) self.runtime.output_values.update({acq.name : Value.INACTIVE}) @@ -403,7 +413,7 @@ def io_check(self): if checks: self.notify('event', checks) else: - self.log.debug("Noting changed between timed io checks") + self.log.debug("Nothing changed between timed io checks") def run(self): self.log = logging.getLogger(__name__)