Source code for ekfsm.devices.gpio

from abc import ABC, abstractmethod
import os
import re
from pathlib import Path

import gpiod
from gpiod.chip import LineSettings
from gpiod.line import Direction, Value
from more_itertools import first_true

from ekfsm.core.components import SysTree
from ekfsm.exceptions import GPIOError
from ekfsm.log import ekfsm_logger

from .generic import Device
from ..core.probe import ProbeableDevice

gpio_pat = re.compile(r"gpiochip\d+")


[docs] def get_gpio_major_minor(path: Path) -> tuple[int, int]: for d in path.iterdir(): if gpio_pat.match(d.name): dev = d / "dev" if dev.exists(): content = dev.read_text().strip() major, minor = map(int, content.split(":")) return major, minor raise GPIOError( GPIOError.ErrorType.NO_MAJOR_MINOR, f"No minor/major number found for GPIO device at {path}", )
[docs] def find_gpio_dev_with_major_minor(major: int, minor: int) -> Path | None: for dev in Path("/dev").iterdir(): if gpio_pat.match(dev.name): stat_info = dev.stat() cmaj = os.major(stat_info.st_rdev) cmin = os.minor(stat_info.st_rdev) if cmaj == major and cmin == minor: return dev raise GPIOError( GPIOError.ErrorType.NO_MATCHING_DEVICE, f"Failed to find GPIO device with major {major} and minor {minor}", )
[docs] class GPIO(Device): def __init__( self, name: str, parent: SysTree | None = None, *args, **kwargs, ): super().__init__( name, parent, None, *args, **kwargs, ) self.logger = ekfsm_logger("GPIODevice" + name) major, minor = self._find_gpio_dev(parent, *args, **kwargs) self.gpio = find_gpio_dev_with_major_minor(major, minor) assert self.gpio is not None match = re.search(r"\d+", self.gpio.name) if match: self.number: int = int(match.group().strip()) else: raise GPIOError( GPIOError.ErrorType.NO_MATCHING_DEVICE, "Failed to find matching device" ) self.init_dev() def _find_gpio_dev( self, parent: SysTree | None = None, *args, **kwargs, ) -> tuple[int, int]: self.addr = self.get_i2c_chip_addr() self.logger.debug(f"GPIO: {self.addr}") self.sysfs_device = self.get_i2c_sysfs_device(self.addr) return get_gpio_major_minor(self.sysfs_device.path)
[docs] def init_dev(self): if self.gpio: try: self.dev = gpiod.Chip(str(self.gpio)) self.initialized = True except FileNotFoundError: raise FileNotFoundError(f"{self.gpio} does not exist")
[docs] def num_lines(self) -> int: """ Get number of GPIO lines available on the device. """ return self.dev.get_info().num_lines
[docs] def set_pin(self, pin: int, value: bool) -> None: """ Set the value of a GPIO pin. Parameters ---------- pin : int The pin number. value : bool The value to set. """ v = Value.ACTIVE if value else Value.INACTIVE with self.dev.request_lines( consumer="set-pin", config={pin: gpiod.LineSettings()}, ) as request: request.set_value(pin, v)
[docs] def get_pin(self, pin: int) -> bool: """ Get the value of a GPIO pin. Parameters ---------- pin : int The pin number. Returns ------- bool The value of the pin. """ with self.dev.request_lines( consumer="get-pin", config={pin: gpiod.LineSettings()}, ) as req: value = req.get_value(pin) return value == Value.ACTIVE
[docs] def get_lines(self, lines: list[int]): if ( invalid_pin := first_true( lines, pred=lambda line: line < 0 or line >= self.num_lines(), default=None, ) ) is not None: raise GPIOError( GPIOError.ErrorType.INVALID_PIN, f"GPIO {invalid_pin} is invalid." )
[docs] def set_direction(self, pin: int, direction: bool) -> None: """ Set the direction of a GPIO pin. Parameters ---------- pin : int The pin number. direction : bool The direction to set. True for output, False for input. """ dir = Direction.OUTPUT if direction else Direction.INPUT self.dev.request_lines( consumer="set-direction", config={pin: LineSettings(direction=dir)}, )
def __str__(self) -> str: return ( f"GPIO - Number: {self.number}; " f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''} " f"(dev: {self.gpio if self.gpio else 'No matching device found'})" )
[docs] class GPIOExpander(GPIO): def __init__( self, name: str, parent: SysTree | None, *args, **kwargs, ): super().__init__(name, parent, None, *args, **kwargs) def __str__(self) -> str: return ( f"GPIOExpander - Number: {self.number}; " f"sysfs_path: {self.sysfs_device.path if self.sysfs_device else ''}" )
[docs] class EKFIdentificationIOExpander(GPIOExpander, ProbeableDevice): def __init__( self, name: str, parent: SysTree | None, *args, **kwargs, ): super().__init__(name, parent, None, *args, **kwargs)
[docs] def probe(self, *args, **kwargs) -> bool: from ekfsm.core import HwModule assert isinstance(self.hw_module, HwModule) id, _ = self.read_board_id_rev() self.logger.debug(f"Probing EKFIdentificationIOExpander: {id}") return self.hw_module.id == id
[docs] def read_board_id_rev(self) -> tuple[int, int]: for pin in range(6, 8): self.set_direction(pin, True) self.set_pin(pin, False) for pin in range(0, 6): self.set_direction(pin, False) v_gnd = self.read_id_gpio_inputs() self.set_pin(7, True) v_7h = self.read_id_gpio_inputs() self.set_pin(6, True) v_6h = self.read_id_gpio_inputs() readings = [v_gnd, v_7h, v_6h] bit_sums = [] for bit in range(5, -1, -1): s = 0 for reading in readings: s += (reading >> bit) & 1 bit_sums.append(s) return ( sum(val * (4**i) for i, val in enumerate(reversed(bit_sums[2:]))), self._get_board_rev(bit_sums[:2]), # board_rev )
[docs] def revision(self) -> str: id, rev = self.read_board_id_rev() return str(rev)
[docs] def read_id_gpio_inputs(self) -> int: value = 0 for pin in range(6): if self.get_pin(pin): value |= 1 << pin return value
[docs] @staticmethod def _get_board_rev(bits: list[int]) -> int: """Convert 2-bit sum values to board revision number.""" rev_map = { (0, 0): 0, (0, 1): 1, (0, 2): 2, (1, 0): 3, (1, 1): 4, (1, 2): 5, (2, 0): 6, (2, 1): 7, (2, 2): 8, } return rev_map.get((bits[0], bits[1]), -1)
def __str__(self) -> str: return ( f"EKFIdentificationIOExpander - Number: {self.number}; " f'sysfs_path: {self.sysfs_device.path if self.sysfs_device else ""}' )
[docs] class SimGpio(ABC):
[docs] @abstractmethod def num_lines(self) -> int: pass
[docs] @abstractmethod def set_pin(self, pin: int, value: bool) -> None: pass
[docs] @abstractmethod def get_pin(self, pin: int) -> bool: pass
[docs] @abstractmethod def set_direction(self, pin: int, direction: bool) -> None: pass
[docs] class EKFIdSimGpio(SimGpio): def __init__(self, coding_gnd, coding_vcc, coding_6, coding_7) -> None: self._coding_gnd = coding_gnd self._coding_vcc = coding_vcc self._coding_6 = coding_6 self._coding_7 = coding_7 self._dir = 0 self._out = 0 self._in = 0
[docs] def num_lines(self) -> int: return 8
[docs] def set_pin(self, pin: int, value: bool) -> None: mask = 1 << pin if value not in [0, 1]: raise RuntimeError("value must be 0 or 1") if self._dir & mask: self._out = (self._out & ~mask) | (value << pin) else: raise RuntimeError("pin not set as output")
[docs] def get_pin(self, pin: int) -> bool: mask = 1 << pin if self._coding_gnd & mask: return False if self._coding_vcc & mask: return True if self._coding_6 & mask: return True if self._out & (1 << 6) else False if self._coding_7 & mask: return True if self._out & (1 << 7) else False return False
[docs] def set_direction(self, pin: int, direction: bool) -> None: if direction == 1 and (pin != 6 and pin != 7): raise RuntimeError("only pins 6 and 7 supported as output") mask = 1 << pin if direction: self._dir |= mask else: self._dir &= ~mask