From 41909ed02c3f6082684f448aa6e67bd2d19764a2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 21 Nov 2023 13:46:52 +0000 Subject: [PATCH] Begin port to gpiod. --- examples/7color/buttons.py | 55 ++++++++++++---------- inky/inky.py | 59 +++++++++++++----------- inky/inky_ssd1608.py | 57 ++++++++++++----------- inky/inky_ssd1683.py | 51 +++++++++++---------- inky/inky_uc8159.py | 94 ++++++++++++++++++++------------------ 5 files changed, 170 insertions(+), 146 deletions(-) diff --git a/examples/7color/buttons.py b/examples/7color/buttons.py index 9559d38..4cbe8e3 100755 --- a/examples/7color/buttons.py +++ b/examples/7color/buttons.py @@ -1,46 +1,53 @@ #!/usr/bin/env python3 -import signal - -import RPi.GPIO as GPIO +import gpiod +import gpiodevice +from gpiod.line import Bias, Direction, Edge, Value print("""buttons.py - Detect which button has been pressed This example should demonstrate how to: - 1. set up RPi.GPIO to read buttons, + 1. set up gpiod to read buttons, 2. determine which button has been pressed Press Ctrl+C to exit! """) -# Gpio pins for each button (from top to bottom) -BUTTONS = [5, 6, 16, 24] +# GPIO pins for each button (from top to bottom) +# These will vary depending on platform and the ones +# below should be correct for Raspberry Pi 5. +# Run "gpioinfo" to find out what yours might be +BUTTONS = ["PIN29", "PIN31", "PIN36", "PIN18"] # These correspond to buttons A, B, C and D respectively LABELS = ["A", "B", "C", "D"] -# Set up RPi.GPIO with the "BCM" numbering scheme -GPIO.setmode(GPIO.BCM) +# Create settings for all the input pins, we want them to be inputs +# with a pull-up and a falling edge detection. +INPUT = gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP, edge_detection=Edge.FALLING) -# Buttons connect to ground when pressed, so we should set them up -# with a "PULL UP", which weakly pulls the input signal to 3.3V. -GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP) +# Find the gpiochip device we need, we'll use +# gpiodevice for this, since it knows the right device +# for its supported platforms. +chip = gpiodevice.find_chip_by_platform() +# Build our config for each pin/line we want to use +OFFSETS = [chip.line_offset_from_id(id) for id in BUTTONS] +line_config = dict.fromkeys(OFFSETS, INPUT) -# "handle_button" will be called every time a button is pressed -# It receives one argument: the associated input pin. -def handle_button(pin): - label = LABELS[BUTTONS.index(pin)] - print("Button press detected on pin: {} label: {}".format(pin, label)) +# Request the lines, *whew* +request = chip.request_lines(consumer="inky7-buttons", config=line_config) +# "handle_button" will be called every time a button is pressed +# It receives one argument: the associated gpiod event object. +def handle_button(event): + index = OFFSETS.index(event.line_offset) + pin = BUTTONS[index] + label = LABELS[index] + print(f"Button press detected on pin: {pin} label: {label}") -# Loop through out buttons and attach the "handle_button" function to each -# We're watching the "FALLING" edge (transition from 3.3V to Ground) and -# picking a generous bouncetime of 250ms to smooth out button presses. -for pin in BUTTONS: - GPIO.add_event_detect(pin, GPIO.FALLING, handle_button, bouncetime=250) -# Finally, since button handlers don't require a "while True" loop, -# we pause the script to prevent it exiting immediately. -signal.pause() +while True: + for event in request.read_edge_events(): + handle_button(event) diff --git a/inky/inky.py b/inky/inky.py index 5ade68b..32b75e0 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -1,13 +1,14 @@ """Inky e-Ink Display Driver.""" import struct import time +from datetime import timedelta -from . import eeprom +import gpiod +import gpiodevice +import numpy +from gpiod.line import Bias, Direction, Edge, Value -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") +from . import eeprom __version__ = "1.5.0" @@ -17,9 +18,9 @@ RED = YELLOW = 2 # GPIO pins required by BCM number -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 # In addition the following pins are used for SPI # CS_PIN = 8 @@ -71,8 +72,7 @@ def __init__(self, resolution=(400, 300), colour="black", cs_channel=CS0, dc_pin :type spi_bus: :class:`spidev.SpiDev` :param i2c_bus: SMB object. If `None` then :class:`smbus2.SMBus(1)` is used. :type i2c_bus: :class:`smbus2.SMBus` - :param gpio: GPIO module. If `None` then `RPi.GPIO` is imported. Default: `None`. - :type gpio: :class:`RPi.GPIO` + :param gpio: deprecated """ self._spi_bus = spi_bus self._i2c_bus = i2c_bus @@ -222,16 +222,22 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + + if gpiodevice.check_pins_available(self._gpio, { + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.dc_pin = self._gpio.line_offset_from_id(self.dc_pin) + self.reset_pin = self._gpio.line_offset_from_id(self.reset_pin) + self.busy_pin = self._gpio.line_offset_from_id(self.busy_pin) + + self._gpio = self._gpio.request_lines(consumer="inky", config={ + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING) + }) if self._spi_bus is None: import spidev @@ -242,18 +248,19 @@ def setup(self): self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio_request.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio_request.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.1) self._send_command(0x12) # Soft Reset self._busy_wait() - def _busy_wait(self): + def _busy_wait(self, timeout=30.0): """Wait for busy/wait pin.""" - while self._gpio.input(self.busy_pin) != self._gpio.LOW: - time.sleep(0.01) + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -376,7 +383,7 @@ def _spi_write(self, dc, values): :param dc: whether to write as data or command :param values: list of values to write """ - self._gpio.output(self.dc_pin, dc) + self._gpio_request.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) except AttributeError: diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 80d21f8..a46ec9d 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -1,22 +1,22 @@ """Inky e-Ink Display Driver.""" import time +from datetime import timedelta +import gpiod +import gpiodevice +import numpy +from gpiod.line import Bias, Direction, Edge, Value from PIL import Image from . import eeprom, ssd1608 -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") - WHITE = 0 BLACK = 1 RED = YELLOW = 2 -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 MOSI_PIN = 10 SCLK_PIN = 11 @@ -126,17 +126,22 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev @@ -148,9 +153,9 @@ def setup(self): self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.5) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.5) self._send_command(0x12) # Soft Reset @@ -159,11 +164,9 @@ def setup(self): def _busy_wait(self, timeout=5.0): """Wait for busy/wait pin.""" - t_start = time.time() - while self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - raise RuntimeError("Timeout waiting for busy signal to clear.") + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -269,7 +272,7 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) except AttributeError: diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index f299be1..7b857b4 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -1,15 +1,15 @@ """Inky e-Ink Display Driver.""" import time +from datetime import timedelta +import gpiod +import gpiodevice +import numpy +from gpiod.line import Bias, Direction, Edge, Value from PIL import Image from . import eeprom, ssd1683 -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") - WHITE = 0 BLACK = 1 RED = YELLOW = 2 @@ -114,17 +114,22 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev @@ -136,9 +141,9 @@ def setup(self): self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.5) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.5) self._send_command(0x12) # Soft Reset @@ -147,11 +152,9 @@ def setup(self): def _busy_wait(self, timeout=5.0): """Wait for busy/wait pin.""" - t_start = time.time() - while self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - raise RuntimeError("Timeout waiting for busy signal to clear.") + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -257,7 +260,7 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) except AttributeError: diff --git a/inky/inky_uc8159.py b/inky/inky_uc8159.py index bd453cc..1189b4a 100644 --- a/inky/inky_uc8159.py +++ b/inky/inky_uc8159.py @@ -2,19 +2,16 @@ import struct import time import warnings +from datetime import timedelta -try: - from PIL import Image -except ImportError: - Image = None +import gpiod +import gpiodevice +import numpy +from gpiod.line import Bias, Direction, Edge, Value +from PIL import Image from . import eeprom -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") - BLACK = 0 WHITE = 1 GREEN = 2 @@ -46,9 +43,9 @@ [255, 255, 255] ] -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 MOSI_PIN = 10 SCLK_PIN = 11 @@ -157,7 +154,7 @@ def __init__(self, resolution=None, colour="multi", cs_pin=CS0_PIN, dc_pin=DC_PI resolution = _RESOLUTION_5_7_INCH if resolution not in _RESOLUTION.keys(): - raise ValueError("Resolution {}x{} not supported!".format(*resolution)) + raise ValueError(f"Resolution {resolution[0]}x{resolution[1]} not supported!") self.resolution = resolution self.width, self.height = resolution @@ -166,7 +163,7 @@ def __init__(self, resolution=None, colour="multi", cs_pin=CS0_PIN, dc_pin=DC_PI self.cols, self.rows, self.rotation, self.offset_x, self.offset_y, self.resolution_setting = _RESOLUTION[resolution] if colour not in ("multi"): - raise ValueError("Colour {} is not supported!".format(colour)) + raise ValueError(f"Colour {colour} is not supported!") self.colour = colour self.lut = colour @@ -209,31 +206,42 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.cs_pin, self._gpio.OUT, initial=self._gpio.HIGH) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.RISING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev self._spi_bus = spidev.SpiDev() self._spi_bus.open(0, self.cs_channel) - self._spi_bus.no_cs = True + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") self._spi_bus.max_speed_hz = 3000000 self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) self._busy_wait(1.0) @@ -319,19 +327,18 @@ def _busy_wait(self, timeout=40.0): # If the busy_pin is *high* (pulled up by host) # then assume we're not getting a signal from inky # and wait the timeout period to be safe. - if self._gpio.input(self.busy_pin): - warnings.warn("Busy Wait: Held high. Waiting for {:0.2f}s".format(timeout)) + if self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + warnings.warn(f"Busy Wait: Held high. Waiting for {timeout:0.2f}s") time.sleep(timeout) return - # If the busy_pin is *low* (pulled down by inky) - # then wait for it to high. - t_start = time.time() - while not self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - warnings.warn("Busy Wait: Timed out after {:0.2f}s".format(time.time() - t_start)) - return + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + warnings.warn(f"Busy Wait: Timed out after {timeout:0.2f}s") + return + + for event in self._gpio.read_edge_events(): + print(timeout, event) def _update(self, buf): """Update display. @@ -343,7 +350,6 @@ def _update(self, buf): """ self.setup() - self._send_command(UC8159_DTM1, buf) self._send_command(UC8159_PON) @@ -401,10 +407,8 @@ def set_image(self, image, saturation=0.5): """ if not image.size == (self.width, self.height): - raise ValueError("Image must be ({}x{}) pixels!".format(self.width, self.height)) + raise ValueError(f"Image must be ({self.width}x{self.height}) pixels!") if not image.mode == "P": - if Image is None: - raise RuntimeError("PIL is required for converting images: sudo apt install python-pil python3-pil") palette = self._palette_blend(saturation) # Image size doesn't matter since it's just the palette we're using palette_image = Image.new("P", (1, 1)) @@ -422,8 +426,8 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.cs_pin, 0) - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.cs_pin, Value.INACTIVE) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) if isinstance(values, str): values = [ord(c) for c in values] @@ -434,7 +438,7 @@ def _spi_write(self, dc, values): for x in range(((len(values) - 1) // _SPI_CHUNK_SIZE) + 1): offset = x * _SPI_CHUNK_SIZE self._spi_bus.xfer(values[offset : offset + _SPI_CHUNK_SIZE]) - self._gpio.output(self.cs_pin, 1) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) def _send_command(self, command, data=None): """Send command over SPI.