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]
@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