From cf72567ee25fd361450a5164d0dad6a70251b37c Mon Sep 17 00:00:00 2001 From: "Bryan T. Richardson" Date: Fri, 2 Aug 2024 06:07:25 -0600 Subject: [PATCH] feat(rpi-gpio): support GPIO inputs and add module README --- src/python/otsim/rpi_gpio/README.md | 37 +++++++++++++++++++++ src/python/otsim/rpi_gpio/rpi_gpio.py | 48 +++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/python/otsim/rpi_gpio/README.md diff --git a/src/python/otsim/rpi_gpio/README.md b/src/python/otsim/rpi_gpio/README.md new file mode 100644 index 0000000..f747467 --- /dev/null +++ b/src/python/otsim/rpi_gpio/README.md @@ -0,0 +1,37 @@ +# Raspberry Pi GPIO Module + +This OT-sim module leverages the [RPi.GPIO](https://pypi.org/project/RPi.GPIO/) +Python module to associate OT-sim tags with input and output pins. + +Please note the following about this module: + +1. This module will only work when OT-sim is run on a Raspberry Pi. To avoid +long compilation times, the suggested way to do so is to run OT-sim on a +Raspberry Pi using the OT-sim Docker image, which is a multi-architecture build. +1. Each input and output is used to setup the corresponding GPIO channel, +defined by the `pin` assignment. +1. This module only supports boolean inputs and outputs (`PWM` is not +supported). +1. This module only subscribes to `update` messages from the OT-sim message bus, +and only publishes `status` messages to the OT-sim message bus. GPIO inputs are +mapped to input tags, and output tags are mapped to GPIO outputs. +1. On exit, `GPIO.cleanup()` is called, which will set all used channels back to +inputs with no pull up/down. This may cause anything physically connected to the +channels to be affected. + +## Example Configuration + +``` + + 1 + + led-11 + + + relay-1-3 + + + switch-17 + + +``` \ No newline at end of file diff --git a/src/python/otsim/rpi_gpio/rpi_gpio.py b/src/python/otsim/rpi_gpio/rpi_gpio.py index 6ea41ad..5bcfc3b 100644 --- a/src/python/otsim/rpi_gpio/rpi_gpio.py +++ b/src/python/otsim/rpi_gpio/rpi_gpio.py @@ -1,11 +1,12 @@ from __future__ import annotations -import json, logging, signal, sys, threading, typing +import logging, signal, sys, threading, time, typing import otsim.msgbus.envelope as envelope import xml.etree.ElementTree as ET -from otsim.msgbus.envelope import Envelope +from otsim.msgbus.envelope import Envelope, Point +from otsim.msgbus.pusher import Pusher from otsim.msgbus.subscriber import Subscriber import RPi.GPIO as GPIO @@ -13,12 +14,18 @@ class RPiGPIO: def __init__(self: RPiGPIO, pub: str, pull: str, el: ET.Element): + # map pin numbers --> tag + self.inputs: typing.Dict[int, str] = {} # map tags --> pin number self.outputs: typing.Dict[str, int] = {} + self.monitoring = True + self.name = el.get('name', default='ot-sim-rpi-gpio') mode = el.get('mode', default='BOARD') + GPIO.setwarnings(False) + if mode.upper() == 'BOARD': GPIO.setmode(GPIO.BOARD) elif mode.upper() == 'BCM': @@ -27,8 +34,20 @@ def __init__(self: RPiGPIO, pub: str, pull: str, el: ET.Element): print(f"unknown GPIO mode '{mode}' - defaulting to 'BOARD' mode") GPIO.setmode(GPIO.BOARD) - pub_endpoint = el.findtext('pub-endpoint', default=pub) + pub_endpoint = el.findtext('pub-endpoint', default=pub) + pull_endpoint = el.findtext('pull-endpoint', default=pull) + self.subscriber = Subscriber(pub_endpoint) + self.pusher = Pusher(pull_endpoint) + + self.period = float(el.findtext('period', default=5)) + + for o in el.findall('input'): + pin = int(o.get('pin')) + tag = o.findtext('tag') + + self.inputs[pin] = tag + GPIO.setup(pin, GPIO.IN) for o in el.findall('output'): pin = int(o.get('pin')) @@ -43,12 +62,35 @@ def __init__(self: RPiGPIO, pub: str, pull: str, el: ET.Element): def start(self: RPiGPIO): self.subscriber.start('RUNTIME') + # run GPIO monitor in a thread + self.monitor_thread = threading.Thread(target=self.monitor, daemon=True).start() + def stop(self: RPiGPIO): + self.monitoring = False + self.monitor_thread.join(self.period) + GPIO.cleanup() self.subscriber.stop() + def monitor(self: RPiGPIO): + if len(self.inputs) == 0: + return + + while self.monitoring: + points: typing.List[Point] = [] + + for pin, tag in self.inputs.items(): + val = GPIO.input(pin) + points.append({'tag': tag, 'value': float(val), 'ts': 0}) + + env = envelope.new_status_envelope(self.name, {'measurements': points}) + self.pusher.push('RUNTIME', env) + + time.sleep(self.period) + + def handle_msgbus_update(self: RPiGPIO, env: Envelope): update = envelope.update_from_envelope(env)