"""GPIO utility functions for Raspberry Pi BCM pin control.
Wraps ``RPi.GPIO`` to provide a simplified interface for pin setup, digital
read/write, cleanup, and I2C multiplexer channel selection. Tracks pin modes
internally so that pins are auto-configured on first use.
"""
import time
import RPi.GPIO as GPIO
from utils.oizom_logger import OizomLogger
# -----------------------------------------------------------------------------
# Configure logging
# -----------------------------------------------------------------------------
basic_logger = OizomLogger(__name__).get()
context_logger = OizomLogger(__name__)
# Constants
IN: str = "in"
OUT: str = "out"
LOW: int = 0
HIGH: int = 1
# Set up default mode
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
# Track mode per pin
_pin_modes = {}
[docs]
def setup(pin: int, mode: str, pullup: bool | None = None, initial: bool = False) -> None:
"""Configure a GPIO pin as input or output.
Args:
pin: BCM GPIO pin number.
mode: ``"in"`` for input or ``"out"`` for output.
pullup: If not None, enable pull-up (True) or pull-down (False) for inputs.
initial: Initial output level when mode is ``"out"`` (True = HIGH).
"""
try:
if mode == IN:
if pullup is not None:
pud = GPIO.PUD_UP if pullup else GPIO.PUD_DOWN
GPIO.setup(pin, GPIO.IN, pull_up_down=pud)
else:
GPIO.setup(pin, GPIO.IN)
elif mode == OUT:
GPIO.setup(pin, GPIO.OUT, initial=GPIO.HIGH if initial else GPIO.LOW)
else:
msg = f"Invalid mode: {mode}"
raise ValueError(msg)
_pin_modes[pin] = mode
except Exception as e:
context_logger.error_with_context("GPIO", f"Failed to setup pin {pin} as {mode}: {e}")
[docs]
def set(pin: int, value: bool) -> None:
"""Set a GPIO pin HIGH or LOW, auto-configuring as output if needed.
Args:
pin: BCM GPIO pin number.
value: True for HIGH, False for LOW.
"""
try:
if _pin_modes.get(pin) != OUT:
setup(pin, OUT)
GPIO.output(pin, GPIO.HIGH if value else GPIO.LOW)
except Exception as e:
context_logger.error_with_context("GPIO", f"Failed to set pin {pin} to {value}: {e}")
[docs]
def read(pin: int) -> bool:
"""Read the logic level of a GPIO pin, auto-configuring as input if needed.
Args:
pin: BCM GPIO pin number.
Returns:
bool: True if HIGH, False if LOW.
"""
try:
if _pin_modes.get(pin) != IN:
setup(pin, IN)
return GPIO.input(pin)
except Exception as e:
context_logger.error_with_context("GPIO", f"Failed to read pin {pin}: {e}")
[docs]
def output(pin: int, value: bool) -> None:
"""Alias for ``set()`` -- set a GPIO pin HIGH or LOW.
Args:
pin: BCM GPIO pin number.
value: True for HIGH, False for LOW.
"""
return set(pin, value)
[docs]
def mode(pin: int) -> str | None:
"""Return the current configured mode of a pin.
Args:
pin: BCM GPIO pin number.
Returns:
str | None: ``"in"``, ``"out"``, or None if the pin has not been set up.
"""
return _pin_modes.get(pin, None)
[docs]
def cleanup(pin: int | None = None, assert_exists: bool = False) -> None:
"""Release GPIO resources for a specific pin or all pins.
Args:
pin: BCM GPIO pin number, or None to clean up all pins.
assert_exists: Unused; kept for API compatibility.
"""
try:
if pin is None:
GPIO.cleanup()
_pin_modes.clear()
else:
GPIO.cleanup(pin)
_pin_modes.pop(pin, None)
except Exception as e:
context_logger.warning_with_context("GPIO", f"Failed to cleanup pin {pin}: {e}")
[docs]
def setwarnings(value: bool) -> None:
"""Enable or disable RPi.GPIO runtime warnings.
Args:
value: True to enable warnings, False to suppress.
"""
GPIO.setwarnings(value)
[docs]
def setmode(value: int) -> None:
"""Set the GPIO pin numbering scheme (BCM or BOARD).
Args:
value: ``GPIO.BCM`` or ``GPIO.BOARD``.
"""
GPIO.setmode(value)
[docs]
def select_I2C(pn: int = 24, ch: int = -1) -> None:
"""Select an I2C multiplexer channel via GPIO pins 24 and 25.
Maps a part-number or channel identifier to the two-bit address lines
that control the Oizom I2C mux. A 100 ms settling delay is applied
after switching.
Args:
pn: Part number or logical channel identifier.
ch: Explicit channel override; if not -1, overrides ``pn``.
"""
try:
context_logger.debug_with_context("GPIO", f"Selecting I2C {pn}")
if ch != -1:
pn = ch
if pn in [0, 18, 23, 24, 29, 53, 54]:
set(24, 0)
set(25, 0)
elif pn in [1, 32, 33, 34, 35, 36, 38, 39]:
set(24, 1)
set(25, 0)
elif pn in [2, 25, 26, 27, 28]:
set(24, 0)
set(25, 1)
elif pn in [3, 4]:
set(24, 1)
set(25, 1)
time.sleep(0.1)
except Exception as e:
context_logger.error_with_context("GPIO", f"Failed to select I2C channel {pn}: {e}")