pax_global_header00006660000000000000000000000064146177133650014527gustar00rootroot0000000000000052 comment=2debec30e78fca89abc83186b4d841f04c38930f rpi-lgpio-release-0.6/000077500000000000000000000000001461771336500147145ustar00rootroot00000000000000rpi-lgpio-release-0.6/.gitignore000066400000000000000000000000371461771336500167040ustar00rootroot00000000000000*.pyc *.egg-info/ build/ dist/ rpi-lgpio-release-0.6/.readthedocs.yaml000066400000000000000000000003271461771336500201450ustar00rootroot00000000000000version: 2 formats: all python: install: - method: pip path: . extra_requirements: - doc build: os: ubuntu-22.04 apt_packages: - swig - liblgpio-dev tools: python: "3" rpi-lgpio-release-0.6/LICENSE.txt000066400000000000000000000021521461771336500165370ustar00rootroot00000000000000SPDX-License-Identifier: MIT The MIT License (MIT) Copyright (c) 2022 Dave Jones Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rpi-lgpio-release-0.6/Makefile000066400000000000000000000065151461771336500163630ustar00rootroot00000000000000# vim: set noet sw=4 ts=4 fileencoding=utf-8: # External utilities PYTHON ?= python3 PIP ?= pip3 PYTEST ?= pytest TWINE ?= twine PYFLAGS ?= DEST_DIR ?= / # Calculate the base names of the distribution, the location of all source, # documentation, packaging, icon, and executable script files NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name) WHEEL_NAME:=$(subst -,_,$(NAME)) VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version) PY_SOURCES:=$(shell \ $(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \ cat $(WHEEL_NAME).egg-info/SOURCES.txt | grep -v "\.egg-info" | grep -v "\.mo$$") DOC_SOURCES:=docs/conf.py \ $(wildcard docs/*.png) \ $(wildcard docs/*.svg) \ $(wildcard docs/*.dot) \ $(wildcard docs/*.mscgen) \ $(wildcard docs/*.gpi) \ $(wildcard docs/*.rst) \ $(wildcard docs/*.pdf) SUBDIRS:= # Calculate the name of all outputs DIST_WHEEL=dist/$(WHEEL_NAME)-$(VER)-py3-none-any.whl DIST_TAR=dist/$(NAME)-$(VER).tar.gz DIST_ZIP=dist/$(NAME)-$(VER).zip MAN_PAGES= # Default target all: @echo "make install - Install on local system" @echo "make develop - Install symlinks for development" @echo "make test - Run tests" @echo "make doc - Generate HTML and PDF documentation" @echo "make preview - Preview HTML documentation with local server" @echo "make source - Create source package" @echo "make wheel - Generate a PyPI wheel package" @echo "make zip - Generate a source zip package" @echo "make tar - Generate a source tar package" @echo "make dist - Generate all packages" @echo "make clean - Get rid of all generated files" @echo "make release - Create and tag a new release" @echo "make upload - Upload the new release to repositories" install: $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py install --root $(DEST_DIR) doc: $(DOC_SOURCES) $(MAKE) -C docs clean $(MAKE) -C docs html $(MAKE) -C docs epub $(MAKE) -C docs latexpdf $(MAKE) $(MAN_PAGES) preview: $(MAKE) -C docs preview source: $(DIST_TAR) $(DIST_ZIP) wheel: $(DIST_WHEEL) zip: $(DIST_ZIP) tar: $(DIST_TAR) dist: $(DIST_WHEEL) $(DIST_TAR) $(DIST_ZIP) develop: @# These have to be done separately to avoid a cockup... $(PIP) install -U setuptools $(PIP) install -U pip $(PIP) install -U twine $(PIP) install -U tox $(PIP) install -e .[doc,test] test: $(PYTEST) clean: rm -fr build/ dist/ man/ .pytest_cache/ .mypy_cache/ $(WHEEL_NAME).egg-info/ tags .coverage* for dir in docs $(SUBDIRS); do \ $(MAKE) -C $$dir clean; \ done find $(CURDIR) -name "*.pyc" -delete find $(CURDIR) -name "__pycache__" -delete tags: $(PY_SOURCES) ctags -R --languages="Python" $(PY_SOURCES) lint: $(PY_SOURCES) pylint $(WHEEL_NAME) $(SUBDIRS): $(MAKE) -C $@ $(MAN_PAGES): $(DOC_SOURCES) $(MAKE) -C docs man mkdir -p man/ cp build/man/*.[0-9] man/ $(DIST_TAR): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats gztar $(DIST_ZIP): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats zip $(DIST_WHEEL): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py bdist_wheel release: $(MAKE) clean test -z "$(shell git status --porcelain)" git tag -s release-$(VER) -m "Release $(VER)" git push origin release-$(VER) upload: $(DIST_TAR) $(DIST_WHEEL) $(TWINE) check $(DIST_TAR) $(DIST_WHEEL) $(TWINE) upload $(DIST_TAR) $(DIST_WHEEL) .PHONY: all install develop test doc source wheel zip tar dist clean tags release upload $(SUBDIRS) rpi-lgpio-release-0.6/README.rst000066400000000000000000000021621461771336500164040ustar00rootroot00000000000000====== README ====== rpi-lgpio is a compatibility package intended to provide compatibility with the `rpi-gpio`_ (aka RPi.GPIO) library, on top of kernels that only support the `gpiochip device`_ (and which have removed the `deprecated sysfs GPIO interface`_). .. warning:: You *cannot* install rpi-lgpio and `rpi-gpio`_ (aka RPi.GPIO, the library it emulates) at the same time, in the same Python environment. Both packages attempt to install a module named ``RPi.GPIO`` and obviously this will not work. Useful Links ============ * `Source code `_ * `Bug reports `_ * `Documentation `_ * `Ubuntu packaging `_ .. * `Debian packaging `_ .. _rpi-gpio: https://pypi.org/project/RPi.GPIO/ .. _gpiochip device: https://embeddedbits.org/new-linux-kernel-gpio-user-space-interface/ .. _deprecated sysfs GPIO interface: https://waldorf.waveform.org.uk/2021/the-pins-they-are-a-changin.html rpi-lgpio-release-0.6/RPi/000077500000000000000000000000001461771336500154065ustar00rootroot00000000000000rpi-lgpio-release-0.6/RPi/GPIO/000077500000000000000000000000001461771336500161445ustar00rootroot00000000000000rpi-lgpio-release-0.6/RPi/GPIO/__init__.py000066400000000000000000000724041461771336500202640ustar00rootroot00000000000000# Copyright (c) 2022-2023 Dave Jones # # SPDX-License-Identifier: MIT import os import sys import struct import warnings from time import sleep from pathlib import Path from threading import Event from weakref import WeakValueDictionary import lgpio try: # Patch several constants which changed incompatibly between 0.1.6.0 # (jammy) and 0.2.0.0 (kinetic) lgpio.SET_PULL_NONE except AttributeError: lgpio.SET_PULL_NONE = lgpio.SET_BIAS_DISABLE lgpio.SET_PULL_UP = lgpio.SET_BIAS_PULL_UP lgpio.SET_PULL_DOWN = lgpio.SET_BIAS_PULL_DOWN # This is *not* the version of rpi-lgpio, but is the version of RPi.GPIO we # seek to emulate VERSION = '0.7.2' UNKNOWN = -1 BOARD = 10 BCM = 11 PUD_OFF = 20 PUD_DOWN = 21 PUD_UP = 22 OUT = 0 IN = 1 LOW = 0 HIGH = 1 RISING = 31 FALLING = 32 BOTH = 33 SERIAL = 40 SPI = 41 I2C = 42 HARD_PWM = 43 # Note the nuance of the early boards (in which GPIO0/1 and GPIO2/3 were # switched) is not represented here. This library (currently) has no intention # of supporting the early Pi boards. As such, this mapping only represents the # Pi Model B+ onwards _BOARD_MAP = { 3: 2, 5: 3, 7: 4, 8: 14, 10: 15, 11: 17, 12: 18, 13: 27, 15: 22, 16: 23, 18: 24, 19: 10, 21: 9, 22: 25, 23: 11, 24: 8, 26: 7, 29: 5, 31: 6, 32: 12, 33: 13, 35: 19, 36: 16, 37: 26, 38: 20, 40: 21, } _BCM_MAP = {channel: gpio for (gpio, channel) in _BOARD_MAP.items()} # LG mode constants _LG_INPUT = 0x100 _LG_OUTPUT = 0x200 _LG_ALERT = 0x400 _LG_GROUP = 0x800 _LG_MODES = (_LG_INPUT | _LG_OUTPUT | _LG_ALERT | _LG_GROUP) _LG_PULL_UP = 0x20 _LG_PULL_DOWN = 0x40 _LG_PULL_NONE = 0x80 _LG_PULLS = (_LG_PULL_UP | _LG_PULL_DOWN | _LG_PULL_NONE) _mode = UNKNOWN _chip = None _warnings = True # Mapping of GPIO number to _Alert instances _alerts = {} class _Alert: """ A trivial class encapsulating a single GPIO set for alerts. Stores the edge (which we override with the gpiochip API2 result, if available), the bouncetime (which we can't get from anywhere else), the default tally callback, and the list of user callbacks. """ __slots__ = ( 'gpio', '_edge', 'bouncetime', 'callbacks', '_detected', '_callback') def __init__(self, gpio, edge, bouncetime=None): self.gpio = gpio self._edge = edge self.bouncetime = bouncetime self.callbacks = [] self._detected = False if bouncetime is not None: _check(lgpio.gpio_set_debounce_micros( _chip, gpio, bouncetime * 1000)) self._callback = lgpio.callback(_chip, gpio, func=self._call) def __repr__(self): return f'_Alert({self.gpio}, {self.edge}, {self.bouncetime})' def close(self): self._callback.cancel() def _call(self, chip, gpio, level, timestamp): if level == 2: # Watchdog timeout; this *shouldn't* happen as we never use this # part of lgpio but if there's something else messing with the API # other than this shim it's a possibility return self._detected = True for cb in self.callbacks: try: cb(_from_gpio(gpio)) except Exception as exc: # Bug compatibility: this is how RPi.GPIO operates print(exc, file=sys.stderr) @property def edge(self): # Attempt to determine the edges for this alert from gpiochip API2. If # this results in no edges, we're on gpiochip API1, so use the stored # value. mode = (lgpio.gpio_get_mode(_chip, self.gpio) >> 17) & 3 try: return { 1: lgpio.RISING_EDGE, 2: lgpio.FALLING_EDGE, 3: lgpio.BOTH_EDGES, }[mode] except KeyError: return self._edge @property def detected(self): if self._detected: self._detected = False return True return False _pwms = WeakValueDictionary() class PWM: """ Initializes and controls software-based PWM (Pulse Width Modulation) on the specified *channel* at *frequency* (in Hz). Call :meth:`start` and :meth:`stop` to generate and stop the actual output respectively. :meth:`ChangeFrequency` and :meth:`ChangeDutyCycle` can also be used to control the output. .. note:: Letting the :class:`PWM` object go out of scope (and be garbage collected) will implicitly stop the PWM. .. _PWM: https://en.wikipedia.org/wiki/Pulse-width-modulation """ __slots__ = ('_gpio', '_frequency', '_dc', '_running', '__weakref__') def __init__(self, channel, frequency): self._gpio = _to_gpio(channel) if self._gpio in _pwms: raise RuntimeError( 'A PWM object already exists for this GPIO channel') _check_output(lgpio.gpio_get_mode(_chip, self._gpio)) _pwms[self._gpio] = self self._frequency = None self._dc = None self._running = False self.ChangeFrequency(frequency) if self._frequency <= 0.0: raise ValueError('frequency must be greater than 0.0') def __del__(self): self.stop() def start(self, dc): """ Starts outputting a wave on the assigned pin with a duty-cycle (which must be between 0 and 100) given by *dc*. :param float dc: The duty-cycle (the percentage of time the pin is "on") """ self.ChangeDutyCycle(dc) self._running = True lgpio.tx_pwm(_chip, self._gpio, self._frequency, dc) def stop(self): """ Stops outputting a wave on the assigned pin, and sets the pin's state to off. """ # We do not care about errors in stop; __del__ methods (from which this # is called) should generally avoid exceptions but moreover, it should # be idempotent on outputs try: lgpio.tx_pwm(_chip, self._gpio, 0, 0) except lgpio.error: pass lgpio.gpio_write(_chip, self._gpio, 0) self._running = False def ChangeDutyCycle(self, dc): """ Changes the duty cycle (percentage of the time that the pin is "on") to *dc*. """ self._dc = float(dc) if not 0 <= self._dc <= 100: raise ValueError('dutycycle must have a value from 0.0 to 100.0') if self._running: lgpio.tx_pwm(_chip, self._gpio, self._frequency, self._dc) def ChangeFrequency(self, frequency): """ Changes the *frequency* of rising edges output by the pin. """ self._frequency = float(frequency) if self._frequency <= 0.0: raise ValueError('frequency must be greater than 0.0') if self._running: lgpio.tx_pwm(_chip, self._gpio, self._frequency, self._dc) def _check(result): """ Many lgpio functions return <0 on error; this simple function just converts any *result* less than zero to the appropriate :exc:`RuntimeError` message and passes non-negative results back to the caller. """ if result < 0: raise RuntimeError(lgpio.error_text(result)) return result def _check_input(mode, msg='You must setup() the GPIO channel as an input first'): """ Raises :exc:`RuntimeError` if *mode* (as returned by :func:`lgpio.gpio_get_mode`) does not indicate that the GPIO is configured for "Input" or "Alert". """ if not mode & (_LG_INPUT | _LG_ALERT): raise RuntimeError(msg) def _check_output(mode, msg='You must setup() the GPIO channel as an output first'): """ Raises :exc:`RuntimeError` if *mode* (as returned by :func:`lgpio.gpio_get_mode`) does not indicate that the GPIO is configured for "Output". """ if not mode & _LG_OUTPUT: raise RuntimeError(msg) def _check_edge(edge): """ Checks *edge* is a valid value. """ if edge not in (FALLING, RISING, BOTH): raise ValueError('The edge must be set to RISING, FALLING or BOTH') def _check_bounce(bouncetime): """ Checks *bouncetime* is :data:`None` or a positive value. """ # The value -666 is special in RPi.GPIO, and is used internally as the # default of no bouncetime; we convert this to None (which is lgpio's # Python binding's default) if bouncetime == -666: bouncetime = None if bouncetime is not None and bouncetime <= 0: raise ValueError('Bouncetime must be greater than 0') return bouncetime def _get_alert(gpio, mode, edge, bouncetime): """ Returns the :class:`_Alert` object for the specified *gpio* (which has the specifed *mode*, as returned by :func:`lgpio.gpio_get_mode`), but only if it has compatible *edge* and *bouncetime* settings. If no alerts are set, :exc:`KeyError` is raised. If alerts are set, but with incompatible *edge* or *bouncetime* values, :exc:`RuntimeError` is raised. """ if not mode & _LG_ALERT: raise KeyError(gpio) alert = _alerts[gpio] if alert.edge != edge or alert.bouncetime != bouncetime: raise RuntimeError( 'Conflicting edge detection already enabled for this GPIO ' 'channel') return alert def _set_alert(gpio, mode, edge, bouncetime): """ Set up alerts on a *gpio*. The *mode* is the current GPIO mode as returned by :func:`lgpio.gpio_get_mode`. The *edge* is the desired edge detection, and *bouncetime* the desired debounce delay. """ _check(lgpio.gpio_claim_alert(_chip, gpio, { RISING: lgpio.RISING_EDGE, FALLING: lgpio.FALLING_EDGE, BOTH: lgpio.BOTH_EDGES, }[edge], mode & _LG_PULLS)) if bouncetime is not None: _check(lgpio.gpio_set_debounce_micros( _chip, gpio, bouncetime * 1000)) alert = _Alert(gpio, edge, bouncetime) _alerts[gpio] = alert return alert def _unset_alert(gpio): """ Remove alerts on *gpio*. This doesn't actually remove the claimed alert status (in lgpio parlance), but it does cancel all associated callbacks and remove the relevant :class:`_Alert` instance. """ try: alert = _alerts.pop(gpio) except KeyError: pass else: alert.close() def _retry(func, *args, _count=3, _delay=0.001, **kwargs): """ Under certain circumstances (usually multiple concurrent processes accessing the same GPIO device), GPIO functions can return "GPIO_BUSY". In this case the operation should simply be retried after a delay. """ for i in range(_count): result = func(*args, **kwargs) if result != lgpio.GPIO_BUSY: return _check(result) sleep(_delay) raise RuntimeError(lgpio.error_text(lgpio.GPIO_BUSY)) def _to_gpio(channel): """ Converts *channel* to a GPIO number, according to the globally set :data:`_mode`. """ if _mode == UNKNOWN: raise RuntimeError( 'Please set pin numbering mode using GPIO.setmode(GPIO.BOARD) or ' 'GPIO.setmode(GPIO.BCM)') elif _mode == BCM: if not 0 <= channel < 54: raise ValueError('The channel sent is invalid on a Raspberry Pi') return channel elif _mode == BOARD: try: return _BOARD_MAP[channel] except KeyError: raise ValueError('The channel sent is invalid on a Raspberry Pi') else: assert False, 'Invalid channel mode' def _from_gpio(gpio): """ Converts *gpio* to a channel number, according to the globally set :data:`_mode`. """ if _mode == BCM: return gpio elif _mode == BOARD: return _BCM_MAP[gpio] else: raise RuntimeError( 'Please set pin numbering mode using GPIO.setmode(GPIO.BOARD) or ' 'GPIO.setmode(GPIO.BCM)') def _gpio_list(chanlist): """ Convert *chanlist* which may be an iterable, or an int, to a tuple of integers """ try: return tuple(_to_gpio(int(channel)) for channel in chanlist) except TypeError: try: return (_to_gpio(int(chanlist)),) except TypeError: raise ValueError( 'Channel must be an integer or list/tuple of integers') def _in_use(gpio): """ Returns :data:`True` if the GPIO has been "claimed" by lgpio. lgpio mode bits (256, 512, 1024, 2048) are only set if the calling process owns the GPIO. """ return bool(_check(lgpio.gpio_get_mode(_chip, gpio)) & _LG_MODES) def _get_rpi_info(): """ Queries the device-tree for the board revision, throwing :exc:`RuntimeError` if it cannot be found, then returns a :class:`dict` containing information about the board. """ try: revision = int(os.environ['RPI_LGPIO_REVISION'], base=16) except KeyError: try: with open('/proc/device-tree/system/linux,revision', 'rb') as f: revision = struct.unpack('>I', f.read(4))[0] if not revision: raise OSError() except OSError: raise RuntimeError('This module can only be run on a Raspberry Pi!') if not (revision >> 23 & 0x1): raise NotImplementedError( 'This module does not understand old-style revision codes') return { 'P1_REVISION': { 0x00: 2, 0x01: 2, 0x06: 0, 0x0a: 0, 0x10: 0, 0x14: 0, }.get(revision >> 4 & 0xff, 3), 'REVISION': hex(revision)[2:], 'TYPE': { 0x00: 'Model A', 0x01: 'Model B', 0x02: 'Model A+', 0x03: 'Model B+', 0x04: 'Pi 2 Model B', 0x05: 'Alpha', 0x06: 'Compute Module 1', 0x08: 'Pi 3 Model B', 0x09: 'Zero', 0x0a: 'Compute Module 3', 0x0c: 'Zero W', 0x0d: 'Pi 3 Model B+', 0x0e: 'Pi 3 Model A+', 0x10: 'Compute Module 3+', 0x11: 'Pi 4 Model B', 0x12: 'Zero 2 W', 0x13: 'Pi 400', 0x14: 'Compute Module 4', 0x17: 'Pi 5 Model B', }.get(revision >> 4 & 0xff, 'Unknown'), 'MANUFACTURER': { 0: 'Sony UK', 1: 'Egoman', 2: 'Embest', 3: 'Sony Japan', 4: 'Embest', 5: 'Stadium', }.get(revision >> 16 & 0xf, 'Unknown'), 'PROCESSOR': { 0: 'BCM2835', 1: 'BCM2836', 2: 'BCM2837', 3: 'BCM2711', 4: 'BCM2712', }.get(revision >> 12 & 0xf, 'Unknown'), 'RAM': { 0: '256M', 1: '512M', 2: '1GB', 3: '2GB', 4: '4GB', 5: '8GB', 6: '16GB', }.get(revision >> 20 & 0x7, 'Unknown'), } def _get_gpiochip_num(): """ Determines the number of the GPIO chip device to access. If :envvar:`RPI_LGPIO_CHIP` is found in the environment, it will be used. Otherwise, the routine will attempt to query sysfs to find a GPIO chip device with a driver known to be used for userspace GPIO access. If none can be found, returns 0 as a fallback. """ try: return int(os.environ['RPI_LGPIO_CHIP']) except KeyError: # The following are the driver names used for the GPIO chip devices # intended for userspace control user_gpio_drivers = frozenset(( 'raspberrypi,rp1-gpio', 'raspberrypi,bcm2835-gpio', 'raspberrypi,bcm2711-gpio', )) for dev in Path('/sys/bus/gpio/devices').glob('gpiochip*'): compatible = (dev / 'of_node/compatible') try: drivers = set(compatible.read_text().split('\0')) except FileNotFoundError: continue if drivers & user_gpio_drivers: return int(dev.name[len('gpiochip'):]) return 0 def getmode(): """ Get the numbering mode used for the pins on the board. Returns :data:`BOARD`, :data:`BCM` or :data:`None`. """ if _mode == UNKNOWN: return None else: return _mode def setmode(new_mode): """ Set up the numbering mode to use for the pins on the board. The options for *new_mode* are: * :data:`BOARD` - Use Raspberry Pi board numbers * :data:`BCM` - Use Broadcom GPIO 00..nn numbers If a numbering mode has already been set, and *new_mode* is not the same as the result of :func:`getmode`, a :exc:`ValueError` is raised. :param int new_mode: The new numbering mode to apply """ global _mode, _chip if _mode != UNKNOWN and new_mode != _mode: raise ValueError('A different mode has already been set!') if new_mode not in (BOARD, BCM): raise ValueError('An invalid mode was passed to setmode()') if _chip is None: _chip = _check(lgpio.gpiochip_open(_get_gpiochip_num())) _mode = new_mode def setwarnings(value): """ Enable or disable warning messages. These are mostly produced when calling :func:`setup` or :func:`cleanup` to change channel modes. """ global _warnings _warnings = bool(value) def gpio_function(channel): """ Return the current GPIO function (:data:`IN`, :data:`OUT`, :data:`HARD_PWM`, :data:`SERIAL`, :data:`I2C`, :data:`SPI`) for the specified *channel*. .. note:: This function will only return :data:`IN` or :data:`OUT` under rpi-lgpio as the underlying kernel device cannot report the alt-mode of GPIO pins. :param int channel: The board pin number or BCM number depending on :func:`setmode` """ gpio = _to_gpio(channel) mode = _check(lgpio.gpio_get_mode(_chip, gpio)) if mode & 0x2: return OUT else: return IN def cleanup(chanlist=None): """ Reset the specified GPIO channels (or all channels if none are specified) to INPUT with no pull-up / pull-down and no event detection. :type chanlist: list or tuple or int or None :param chanlist: The channel, or channels to clean up """ global _chip, _mode if _chip is None: return # If we're cleaning up everything we need to close the chip handle too, # and reset the GPIO mode. But first... close = chanlist is None if chanlist is None: # Bug compatibility: it's awfully tempting to just re-initialize here, # but that doesn't reset pins to inputs, and users may be relying upon # this side-effect result, gpios, *tail = lgpio.gpio_get_chip_info(_chip) _check(result) chanlist = [gpio for gpio in range(gpios) if _in_use(gpio)] else: chanlist = _gpio_list(chanlist) if chanlist: for gpio in chanlist: # As this is cleanup we ignore all errors (no _check calls); if we # didn't own the GPIO, we don't care _unset_alert(gpio) lgpio.gpio_claim_input(_chip, gpio, lgpio.SET_PULL_NONE) lgpio.gpio_free(_chip, gpio) elif _warnings: warnings.warn(Warning( 'No channels have been set up yet - nothing to clean up! Try ' 'cleaning up at the end of your program instead!')) if close: lgpio.gpiochip_close(_chip) _chip = None _mode = UNKNOWN assert not _alerts def setup(chanlist, direction, pull_up_down=PUD_OFF, initial=None): """ Set up a GPIO channel or iterable of channels with a direction and (optionally, for inputs) pull/up down control, or (optionally, for outputs) and initial state. The GPIOs to affect are listed in *chanlist* which may be any iterable. The *direction* is either :data:`IN` or :data:`OUT`. If *direction* is :data:`IN`, then *pull_up_down* may specify one of the values :data:`PUD_UP` to set the internal pull-up resistor, :data:`PUD_DOWN` to set the internal pull-down resistor, or the default :data:`PUD_OFF` which disables the internal pulls. If *direction* is :data:`OUT`, then *initial* may specify zero or one to indicate the initial state of the output. :type chanlist: list or tuple or int :param chanlist: The list of GPIO channels to setup :param int direction: Whether the channels should act as inputs or outputs :type pull_up_down: int or None :param pull_up_down: The internal pull resistor (if any) to enable for inputs :type initial: bool or int or None :param initial: The initial state of an output """ if direction == OUT: if pull_up_down != PUD_OFF: raise ValueError('pull_up_down parameter is not valid for outputs') if initial is not None: initial = bool(initial) elif direction == IN: if initial is not None: raise ValueError('initial parameter is not valid for inputs') if pull_up_down not in (PUD_UP, PUD_DOWN, PUD_OFF): raise ValueError( 'Invalid value for pull_up_down - should be either PUD_OFF, ' 'PUD_UP or PUD_DOWN') else: raise ValueError('An invalid direction was passed to setup()') for gpio in _gpio_list(chanlist): # We don't bother with warnings about GPIOs already in use here because # if we try to *use* a GPIO already in use, things are going to blow up # shortly anyway. We do deal with the pull-up warning, but only for # GPIO2 and GPIO3 because we're not supporting the original RPi so we # don't need to worry about the GPIO0 and GPIO1 discrepancy if _warnings and gpio in (2, 3) and pull_up_down in (PUD_UP, PUD_DOWN): warnings.warn(Warning( 'A physical pull up resistor is fitted on this channel!')) if direction == IN: # This gpio_free may seem redundant, but is required when changing # the line-flags of an already acquired input line try: lgpio.gpio_free(_chip, gpio) except lgpio.error: pass _check(lgpio.gpio_claim_input(_chip, gpio, { PUD_OFF: lgpio.SET_PULL_NONE, PUD_DOWN: lgpio.SET_PULL_DOWN, PUD_UP: lgpio.SET_PULL_UP, }[pull_up_down])) elif direction == OUT: _unset_alert(gpio) if initial is None: initial = _check(lgpio.gpio_read(_chip, gpio)) _check(lgpio.gpio_claim_output( _chip, gpio, initial, lgpio.SET_PULL_NONE)) else: assert False, 'Invalid direction' def input(channel): """ Input from a GPIO *channel*. Returns 1 or 0. This can also be called on a GPIO output, in which case the value returned will be the last state set on the GPIO. :param int channel: The board pin number or BCM number depending on :func:`setmode` """ gpio = _to_gpio(channel) if not _in_use(gpio): raise RuntimeError('You must setup() the GPIO channel first') return _check(lgpio.gpio_read(_chip, gpio)) def output(channel, value): """ Output to a GPIO *channel* or list of channels. The *value* can be the integer :data:`LOW` or :data:`HIGH`, or a list of integers. If a list of channels is specified, with a single integer for the *value* then it is applied to all channels. Otherwise, the length of the two lists must match. :type channel: list or tuple or int :param channel: The GPIO channel, or list of GPIO channels to output to :type value: list or tuple or int :param value: The value, or list of values to output """ gpios = _gpio_list(channel) try: values = tuple(bool(item) for item in value) except TypeError: try: values = (bool(value),) except TypeError: raise ValueError( 'Value must be an integer/boolean or list/tuple of ' 'integers/booleans') if len(gpios) != len(values): if len(gpios) > 1 and len(values) == 1: values = values * len(gpios) else: raise RuntimeError('Number of channels != number of values') for gpio, value in zip(gpios, values): mode = lgpio.gpio_get_mode(_chip, gpio) _check_output(mode, 'The GPIO channel has not been set up as an OUTPUT') _check(lgpio.gpio_write(_chip, gpio, value)) def wait_for_edge(channel, edge, bouncetime=None, timeout=None): """ Wait for an *edge* on the specified *channel*. Returns *channel* or :data:`None` if *timeout* elapses before the specified edge occurs. .. note:: Debounce works significantly differently in rpi-lgpio than it does in rpi-gpio; please see :ref:`debounce` for more information on the differences. :param int channel: The board pin number or BCM number depending on :func:`setmode` to watch for changes :param int edge: One of the constants :data:`RISING`, :data:`FALLING`, or :data:`BOTH` :type bouncetime: int or None :param bouncetime: Time (in ms) used to debounce signals :type timeout: int or None :param timeout: Maximum time (in ms) to wait for the edge """ gpio = _to_gpio(channel) mode = _check(lgpio.gpio_get_mode(_chip, gpio)) _check_input(mode) _check_edge(edge) bouncetime = _check_bounce(bouncetime) if timeout is not None and timeout <= 0: raise ValueError('Timeout must be greater than 0') try: alert = _get_alert(gpio, mode, edge, bouncetime) except KeyError: unset = True alert = _set_alert(gpio, mode, edge, bouncetime) else: unset = False # Bug compatibility: this is how RPi.GPIO operates if alert.callbacks: raise RuntimeError( 'Conflicting edge detection already enabled for this GPIO ' 'channel') evt = Event() alert.callbacks.append(lambda i: evt.set()) if timeout is not None: timeout /= 1000 if evt.wait(timeout): result = channel else: result = None if unset: _unset_alert(gpio) return result def add_event_detect(channel, edge, callback=None, bouncetime=None): """ Start background *edge* detection on the specified GPIO *channel*. If *callback* is specified, it must be a callable that will be executed when the specified *edge* is seen on the GPIO *channel*. The callable must accept a single parameter: the channel on which the edge was detected. .. note:: Debounce works significantly differently in rpi-lgpio than it does in rpi-gpio; please see :ref:`debounce` for more information on the differences. :param int channel: The board pin number or BCM number depending on :func:`setmode` to watch for changes :param int edge: One of the constants :data:`RISING`, :data:`FALLING`, or :data:`BOTH` :type callback: callable or None :param callback: The callback to run when an edge is detected; must take a single integer parameter of the channel on which the edge was detected :type bouncetime: int or None :param bouncetime: Time (in ms) used to debounce signals """ if callback is not None and not callable(callback): raise TypeError('Parameter must be callable') gpio = _to_gpio(channel) mode = _check(lgpio.gpio_get_mode(_chip, gpio)) _check_input(mode) _check_edge(edge) bouncetime = _check_bounce(bouncetime) try: alert = _get_alert(gpio, mode, edge, bouncetime) except KeyError: alert = _set_alert(gpio, mode, edge, bouncetime) if callback is not None: alert.callbacks.append(callback) def add_event_callback(channel, callback): """ Add a *callback* to the specified GPIO *channel* which must already have been set for background edge detection with :func:`add_event_detect`. :param int channel: The board pin number or BCM number depending on :func:`setmode` to watch for changes :param callback: The callback to run when an edge is detected; must take a single integer parameter of the channel on which the edge was detected """ if not callable(callback): raise TypeError('Parameter must be callable') gpio = _to_gpio(channel) mode = _check(lgpio.gpio_get_mode(_chip, gpio)) _check_input(mode) try: alert = _alerts[gpio] except KeyError: raise RuntimeError( 'Add event detection using add_event_detect first before adding ' 'a callback') else: alert.callbacks.append(callback) def remove_event_detect(channel): """ Remove background event detection for the specified *channel*. :param int channel: The board pin number or BCM number depending on :func:`setmode` to watch for changes """ _unset_alert(_to_gpio(channel)) def event_detected(channel): """ Returns :data:`True` if an edge has occurred on the specified *channel* since the last query of the channel (if any). Querying this will also reset the internal edge detected flag for this channel. The *channel* must previously have had edge detection enabled with :func:`add_event_detect`. :param int channel: The board pin number or BCM number depending on :func:`setmode` """ try: return _alerts[_to_gpio(channel)].detected except KeyError: return False RPI_INFO = _get_rpi_info() RPI_REVISION = RPI_INFO['P1_REVISION'] rpi-lgpio-release-0.6/RPi/__init__.py000066400000000000000000000000001461771336500175050ustar00rootroot00000000000000rpi-lgpio-release-0.6/docs/000077500000000000000000000000001461771336500156445ustar00rootroot00000000000000rpi-lgpio-release-0.6/docs/Makefile000066400000000000000000000127751461771336500173200ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build PY_SOURCES := $(wildcard ../RPi/*.py ../RPi/*/*.py) DOT_DIAGRAMS := $(wildcard images/*.dot) MSC_DIAGRAMS := $(wildcard images/*.mscgen) GPI_DIAGRAMS := $(wildcard images/*.gpi) SVG_IMAGES := $(wildcard images/*.svg) $(DOT_DIAGRAMS:%.dot=%.svg) $(MSC_DIAGRAMS:%.mscgen=%.svg) PNG_IMAGES := $(wildcard images/*.png) $(GPI_DIAGRAMS:%.gpi=%.png) $(SVG_IMAGES:%.svg=%.png) PDF_IMAGES := $(SVG_IMAGES:%.svg=%.pdf) $(GPI_DIAGRAMS:%.gpi=%.pdf) $(DOT_DIAGRAMS:%.dot=%.pdf) $(MSC_DIAGRAMS:%.mscgen=%.pdf) INKSCAPE_VER := $(shell DISPLAY= inkscape --version | sed -ne '/^Inkscape/ s/^Inkscape \([0-9]\+\)\..*$$/\1/p') # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " preview to start a web-server that watches for file changes" @echo " and re-builds the docs when required" @echo " json to make JSON files" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." preview: ../scripts/previewer $(BUILDDIR)/html images/types.dot: $(PY_SOURCES) ../scripts/class_graph -i Type -i Stats -x Fields -x SourcesList -x Redo > $@ %.svg: %.mscgen mscgen -T svg -o $@ $< %.svg: %.dot dot -T svg -o $@ $< %.png: %.gpi gnuplot -e "set term pngcairo transparent size 400,400" $< > $@ ifeq ($(INKSCAPE_VER),0) %.png: %.svg DISPLAY= inkscape --export-dpi 150 -e $@ $< %.pdf: %.svg DISPLAY= inkscape -A $@ $< else %.png: %.svg DISPLAY= inkscape --export-dpi 150 --export-type png -o $@ $< %.pdf: %.svg DISPLAY= inkscape --export-type pdf -o $@ $< endif %.pdf: %.gpi gnuplot -e "set term pdfcairo size 5cm,5cm" $< > $@ %.pdf: %.mscgen mscgen -T eps -o - $< | ps2pdf -dEPSCrop - $@ .PHONY: help clean html preview json epub latex latexpdf text man changes linkcheck doctest gettext rpi-lgpio-release-0.6/docs/_static/000077500000000000000000000000001461771336500172725ustar00rootroot00000000000000rpi-lgpio-release-0.6/docs/_static/style_override.css000066400000000000000000000034561461771336500230530ustar00rootroot00000000000000/* override table width restrictions */ @media screen and (min-width: 767px) { .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } } /* Sort out RTD's lacking code captions */ .rst-content div.code-block-caption { /* Copied from pre... */ font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; font-size: 14px; font-weight: bold; font-style: normal; text-align: left; background-color: #ddd; padding: 0.5em; } .rst-content div[class^="highlight"] { border: 0 none; margin-top: 0; } .rst-content div.chart pre { line-height: 1; } .rst-content div.code-block-caption + div.highlight-python3 { margin-top: 0; } .rst-content div.code-block-caption a.headerlink { visibility: hidden; } .rst-content div.code-block-caption a.headerlink::after { font-family: FontAwesome; font-size: 12px; content: ""; visibility: hidden; } .rst-content div.code-block-caption:hover a.headerlink::after { visibility: visible; } /* Make highlighting color a little less ugly */ .rst-content div.highlight span.hll { background-color: #ddddff; } /* Custom style for exercises */ .rst-content div.admonition-exercise { background: #e7fae8; } .rst-content div.admonition-exercise p.admonition-title { background: #6ade78; } /* Equal spacing around content images */ .rst-content .document img { margin-bottom: 24px; } rpi-lgpio-release-0.6/docs/api.rst000066400000000000000000000070131461771336500171500ustar00rootroot00000000000000.. Copyright (c) 2022 Dave Jones .. .. SPDX-License-Identifier: MIT ============= API Reference ============= .. module:: RPi.GPIO The API of rpi-lgpio (naturally) follows that of rpi-gpio (aka RPi.GPIO) as closely as possible. As such the following is simply a re-iteration of that API. Initialization ============== .. autofunction:: setmode .. autofunction:: getmode .. autofunction:: setup .. autofunction:: cleanup Pin Usage ========= .. autofunction:: input .. autofunction:: output Edge Detection ============== .. autofunction:: wait_for_edge .. autofunction:: add_event_detect .. autofunction:: add_event_callback .. autofunction:: event_detected Miscellaneous ============= .. autofunction:: gpio_function .. autofunction:: setwarnings PWM === .. autoclass:: PWM Constants ========= .. data:: RPI_INFO A dictionary that provides information about the model of Raspberry Pi that the library is loaded onto. Includes the following keys: P1_REVISION The revision of the P1 header. 0 indicates no P1 header (typical on the compute module range), 1 and 2 vary on the oldest Raspberry Pi models, and 3 is the typical 40-pin header present on all modern Raspberry Pis. REVISION The hex `board revision code`_ as a :class:`str`. TYPE The name of the Pi model, e.g. "Pi 4 Model B" MANUFACTURER The name of the board manufacturer, e.g. "Sony UK" PROCESSOR The name of the SoC used on the board, e.g. "BCM2711" RAM The amount of RAM installed on the board, e.g. "4GB" The board revision can be overridden with the ``RPI_LGPIO_REVISION`` environment variable; see :ref:`revision` for further details. .. data:: RPI_REVISION The same as the ``P1_REVISION`` key in :data:`RPI_INFO` .. _board revision code: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#new-style-revision-codes .. data:: BOARD Indicates to :func:`setmode` that physical board numbering is requested .. data:: BCM Indicates to :func:`setmode` that GPIO numbering is requested .. data:: PUD_OFF Used with :func:`setup` to disable internal pull resistors on an input .. data:: PUD_DOWN Used with :func:`setup` to enable the internal pull-down resistor on an input .. data:: PUD_UP Used with :func:`setup` to enable the internal pull-up resistor on an input .. data:: OUT Used with :func:`setup` to set a GPIO to an output, and :func:`gpio_function` to report a GPIO is an output .. data:: IN Used with :func:`setup` to set a GPIO to an input, and :func:`gpio_function` to report a GPIO is an input .. data:: HARD_PWM .. data:: SERIAL .. data:: I2C .. data:: SPI Used with :func:`gpio_function` to indicate "alternate" modes of certain GPIO pins. .. note:: In rpi-lgpio these values will never be returned as the kernel device cannot report if pins are in alternate modes. .. data:: LOW :value: 0 Used with :func:`output` to turn an output GPIO off .. data:: HIGH :value: 1 Used with :func:`output` to turn an output GPIO on .. data:: RISING Used with :func:`wait_for_edge` and :func:`add_event_detect` to specify that rising edges only should be sampled .. data:: FALLING Used with :func:`wait_for_edge` and :func:`add_event_detect` to specify that falling edges only should be sampled .. data:: BOTH Used with :func:`wait_for_edge` and :func:`add_event_detect` to specify that all edges should be sampled rpi-lgpio-release-0.6/docs/changelog.rst000066400000000000000000000031251461771336500203260ustar00rootroot00000000000000.. Copyright (c) 2022-2023 Dave Jones .. .. SPDX-License-Identifier: MIT ========== Changelog ========== .. currentmodule:: RPi.GPIO Release 0.6 (2024-05-11) ======================== * Use a smarter algorithm for determining the gpiochip device to open (`#10`_) * Add a minimum compatible version to the lgpio dependency .. _#10: https://github.com/waveform80/rpi-lgpio/issues/10 Release 0.5 (2024-04-12) ======================== * Fix setting pull on GPIO2 & 3 (`#8`_) * Added some bits to the Differences chapter on determining which GPIOs are reserved * Added more information on the supported models of Raspberry Pi (`#6`_) .. _#6: https://github.com/waveform80/rpi-lgpio/issues/6 .. _#8: https://github.com/waveform80/rpi-lgpio/pull/8 Release 0.4 (2023-10-03) ======================== * Add compatibility with Raspberry Pi 5 (auto-selection of correct gpiochip device) * Add ability to override gpiochip selection; see :ref:`gpio_chip` * Convert bouncetime -666 to :data:`None` (bug compatibility, which also ensures this should work with GPIO Zero's rpigpio pin driver) * Fix ``pull_up_down`` default on :func:`setup` * Fix changing ``pull_up_down`` of already-acquired input * Ensure :meth:`PWM.stop` is idempotent Release 0.3 (2022-10-14) ======================== * Permit override of Pi revision code; see :ref:`revision` * Document alternate pin modes in :doc:`differences` Release 0.2 (2022-10-14) ======================== * Add support for :data:`RPI_REVISION` and :data:`RPI_INFO` globals Release 0.1 (2022-10-14) ======================== * Initial release rpi-lgpio-release-0.6/docs/conf.py000066400000000000000000000062541461771336500171520ustar00rootroot00000000000000#!/usr/bin/env python3 # vim: set et sw=4 sts=4 fileencoding=utf-8: # # Copyright (c) 2022 Dave Jones # # SPDX-License-Identifier: MIT import sys import os import configparser from datetime import datetime from pathlib import Path on_rtd = os.environ.get('READTHEDOCS', '').lower() == 'true' config = configparser.ConfigParser() config.read([Path(__file__).parent / '..' / 'setup.cfg']) metadata = config['metadata'] # -- Project information ----------------------------------------------------- project = metadata['name'] author = metadata['author'] copyright = '{now:%Y} {author}'.format(now=datetime.now(), author=author) release = metadata['version'] version = release # -- General configuration --------------------------------------------------- needs_sphinx = '1.4.0' extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinx.ext.imgmath', ] if on_rtd: tags.add('rtd') imgmath_image_format = 'svg' templates_path = ['_templates'] master_doc = 'index' exclude_patterns = ['_build'] pygments_style = 'sphinx' # -- Autodoc options --------------------------------------------------------- autodoc_member_order = 'groupwise' autodoc_default_options = {'members': True} autodoc_mock_imports = ['lgpio'] # Set a fake revision so docs can build on non-Pi platforms os.environ['RPI_LGPIO_REVISION'] = 'd03114' # -- Intersphinx options ----------------------------------------------------- intersphinx_mapping = { 'python': ('http://docs.python.org/3/', None), } # -- Options for HTML output ---------------------------------------------- html_theme = 'sphinx_rtd_theme' pygments_style = 'default' html_title = '{project} {version} Documentation'.format( project=project, version=version) html_static_path = ['_static'] manpages_url = 'https://manpages.ubuntu.com/manpages/noble/en/man{section}/{page}.{section}.html' # Hack to make wide tables work properly in RTD # See https://github.com/snide/sphinx_rtd_theme/issues/117 for details def setup(app): app.add_css_file('style_override.css') # -- Options for LaTeX output ------------------------------------------------ latex_engine = 'xelatex' latex_elements = { 'papersize': 'a4paper', 'pointsize': '10pt', 'preamble': r'\def\thempfootnote{\arabic{mpfootnote}}', # workaround sphinx issue #2530 } latex_documents = [ ( 'index', # source start file project + '.tex', # target filename html_title, # title author, # author 'manual', # documentclass True, # documents ref'd from toctree only ), ] latex_show_pagerefs = True latex_show_urls = 'footnote' # -- Options for epub output ------------------------------------------------- epub_basename = project epub_author = author epub_identifier = 'https://{metadata[name]}.readthedocs.io/'.format(metadata=metadata) epub_show_urls = 'no' # -- Options for manual page output ------------------------------------------ man_pages = [] man_show_urls = True # -- Options for linkcheck builder ------------------------------------------- linkcheck_retries = 3 linkcheck_workers = 20 linkcheck_anchors = True rpi-lgpio-release-0.6/docs/differences.rst000066400000000000000000000473021461771336500206610ustar00rootroot00000000000000.. Copyright (c) 2022-2023 Dave Jones .. .. SPDX-License-Identifier: MIT ======================= Differences ======================= .. currentmodule:: RPi.GPIO Many of the assumptions underlying `RPi.GPIO`_ -- that it has complete access to, and control over, the registers controlling the GPIO pins -- do not work when applied to the Linux gpiochip devices. To that end, while the library strives as far as possible to be "bug compatible" with RPi.GPIO, there *are* differences in behaviour that may result in incompatibility. Bug Compatible? =============== What does being "bug compatible" mean? It is not enough for the library to implement the `RPi.GPIO`_ API. It must also: * Act, as far as possible, in the same way to the same calls with the same values * Raise the same exception types, with the same messages, in the same circumstances * Break (i.e. fail to operate correctly) in the same way, as far as possible This last point may sound silly, but a library is *always* used in unexpected or undocumented ways by *some* applications. Thus anything that tries to take the place of that library must do more than simply operate the same as the "documented surface" would suggest. That said, given that the underlying assumptions are fundamentally different this will not always be possible... .. _revision: Pi Revision =========== The RPi.GPIO module attempts to determine the revision of Raspberry Pi board that it is running on when the module is imported by querying :file:`/proc/cpuinfo`, raising :exc:`RuntimeError` at import time if it finds it is not running on a Raspberry Pi. rpi-lgpio emulates this behaviour, but this can be inconvenient for certain situations including testing, and usage of rpi-lgpio on other single board computers. To that end rpi-lgpio permits a Raspberry Pi `revision code`_ to be manually specified via the environment in the ``RPI_LGPIO_REVISION`` value (when this is set, :file:`/proc/cpuinfo` is not read at all). For example: .. code-block:: console $ RPI_LGPIO_REVISION="c03114" python3 Python 3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from RPi import GPIO >>> GPIO.RPI_INFO {'P1_REVISION': 3, 'REVISION': 'c03114', 'TYPE': 'Pi 4 Model B', 'MANUFACTURER': 'Sony UK', 'PROCESSOR': 'BCM2711', 'RAM': '4GB'} >>> exit() $ RPI_LGPIO_REVISION="902120" python3 Python 3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from RPi import GPIO >>> GPIO.RPI_INFO {'P1_REVISION': 3, 'REVISION': '902120', 'TYPE': 'Zero 2 W', 'MANUFACTURER': 'Sony UK', 'PROCESSOR': 'BCM2837', 'RAM': '512M'} >>> exit() At present, rpi-lgpio only interprets "`new-style`_" (6 hex-digit) revision codes, as found in the "Revision" field of :file:`/proc/cpuinfo`. The `old-style`_ (4 hex-digit) revision codes found on the original model B, A, A+, B+, and Compute Module 1 are not supported. If there is significant demand, this can be added but for the time being only boards made since the launch of the 2B (which introduced the new-style revision codes) are supported. Specifically, this includes the following models: * Zero * Zero W * Zero 2W * 2B * 3B * Compute Module 3 * 3A+ * 3B+ * Compute Module 3+ * 4B * 400 * Compute Module 4 * 5B A workaround for use on old-style boards is to use ``RPI_LGPIO_REVISION`` to fake the revision code. For example, 0004 (an old-style model B rev 2) can also be represented by 800012 in the new-style. .. code-block:: console $ RPI_LGPIO_REVISION="800012" python3 Python 3.12.2 (main, Apr 2 2024, 18:40:52) [GCC 13.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from RPi import GPIO >>> GPIO.RPI_INFO {'P1_REVISION': 2, 'REVISION': '800012', 'TYPE': 'Model B', 'MANUFACTURER': 'Sony UK', 'PROCESSOR': 'BCM2835', 'RAM': '256M'} >>> exit() .. _revision code: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#new-style-revision-codes .. _new-style: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#new-style-revision-codes .. _old-style: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#old-style-revision-codes .. _gpio_chip: GPIO Chip ========= The lgpio library needs to know the number of the ``/dev/gpiochip`` device it should open. By default this will be calculated from the reported :ref:`revision` (which may be customized as detailed in that section). In practice this means the chip defaults to "4" on the Raspberry Pi Model 5B, and "0" on all other boards. You may also specify the chip manually using the ``RPI_LGPIO_CHIP`` environment variable. For example: .. code-block:: console $ ls /dev/gpiochip* crw------- 1 root root 254, 0 Oct 1 15:00 /dev/gpiochip0 crw------- 1 root root 254, 1 Oct 1 15:00 /dev/gpiochip1 crw------- 1 root root 254, 2 Oct 1 15:00 /dev/gpiochip2 crw------- 1 root root 254, 3 Oct 1 15:00 /dev/gpiochip3 crw-rw----+ 1 root dialout 254, 4 Oct 1 15:00 /dev/gpiochip4 crw------- 1 root root 254, 5 Oct 1 15:00 /dev/gpiochip5 $ RPI_LGPIO_CHIP=5 python3 Python 3.11.5 (main, Aug 29 2023, 15:31:31) [GCC 13.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3/dist-packages/RPi/GPIO/__init__.py", line 513, in setmode _chip = _check(lgpio.gpiochip_open(int(chip_num))) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3/dist-packages/lgpio.py", line 645, in gpiochip_open return _u2i(handle) ^^^^^^^^^^^^ File "/usr/lib/python3/dist-packages/lgpio.py", line 458, in _u2i raise error(error_text(v)) lgpio.error: 'can not open gpiochip' This is primarily useful for other boards where the correct gpiochip device is something other than 0. Alternate Pin Modes =================== The :func:`gpio_function` function can be used to report the current mode of a pin. In RPi.GPIO this may return several "alternate" mode values including :data:`SPI`, :data:`I2C`, and :data:`HARD_PWM`. rpi-lgpio will only ever return the basic :data:`IN` and :data:`OUT` values however, as the underlying gpiochip device cannot report alternate modes. For example, under RPi.GPIO: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.gpio_function(2) == GPIO.I2C True Under rpi-lgpio: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.gpio_function(2) == GPIO.I2C False >>> GPIO.gpio_function(2) == GPIO.IN True Stack Traces ============ While every effort has been made to raise the same exceptions with the same messages as RPi.GPIO, rpi-lgpio does raise the exceptions from pure Python so the exceptions will generally include a larger stack trace than under RPi.GPIO. For example, under RPi.GPIO: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.setup(26, GPIO.IN) >>> GPIO.output(26, 1) Traceback (most recent call last): File "", line 1, in RuntimeError: The GPIO channel has not been set up as an OUTPUT Under rpi-lgpio: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.setup(26, GPIO.IN) >>> GPIO.output(26, 1) Traceback (most recent call last): File "", line 1, in File "/home/dave/projects/rpi-lgpio/rpi-lgpio/RPi/GPIO.py", line 626, in output _check_output(mode, 'The GPIO channel has not been set up as an OUTPUT') File "/home/dave/projects/rpi-lgpio/rpi-lgpio/RPi/GPIO.py", line 242, in _check_output raise RuntimeError(msg) RuntimeError: The GPIO channel has not been set up as an OUTPUT Simultaneous Access =================== Two processes using RPi.GPIO can happily control the same pin. This is simply not permitted by the Linux gpiochip device and will fail under rpi-lgpio. For example, if another process has reserved GPIO26, and our script also tries to allocate it: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.setup(26, GPIO.OUT) Traceback (most recent call last): File "", line 1, in File "/home/dave/projects/rpi-lgpio/rpi-lgpio/RPi/GPIO.py", line 569, in setup initial = _check(lgpio.gpio_read(_chip, gpio)) File "/home/dave/envs/rpi-lgpio/lib/python3.10/site-packages/lgpio.py", line 894, in gpio_read return _u2i(_lgpio._gpio_read(handle&0xffff, gpio)) File "/home/dave/envs/rpi-lgpio/lib/python3.10/site-packages/lgpio.py", line 461, in _u2i raise error(error_text(v)) lgpio.error: 'GPIO not allocated' How can you tell if a GPIO is reserved by another process? Use the :manpage:`gpioinfo(1)` tool, which is part of the ``gpiod`` package. By default this attempts to read GPIO chip 0, which is fine on all Pi's *except* the Pi 5 where you will need to read GPIO chip 4 specifically: .. code-block:: console $ gpioinfo 4 gpiochip4 - 54 lines: line 0: "ID_SDA" unused input active-high line 1: "ID_SCL" unused input active-high line 2: "GPIO2" unused input active-high line 3: "GPIO3" unused input active-high line 4: "GPIO4" unused input active-high line 5: "GPIO5" unused input active-high line 6: "GPIO6" unused input active-high line 7: "GPIO7" "spi0 CS1" output active-low [used] line 8: "GPIO8" "spi0 CS0" output active-low [used] line 9: "GPIO9" unused input active-high line 10: "GPIO10" unused input active-high line 11: "GPIO11" unused input active-high line 12: "GPIO12" unused input active-high line 13: "GPIO13" unused input active-high line 14: "GPIO14" unused input active-high line 15: "GPIO15" unused input active-high line 16: "GPIO16" unused input active-high line 17: "GPIO17" unused input active-high line 18: "GPIO18" unused input active-high line 19: "GPIO19" unused input active-high line 20: "GPIO20" unused input active-high line 21: "GPIO21" unused input active-high line 22: "GPIO22" unused input active-high line 23: "GPIO23" unused input active-high line 24: "GPIO24" unused input active-high line 25: "GPIO25" unused input active-high line 26: "GPIO26" unused input active-high line 27: "GPIO27" unused input active-high line 28: "PCIE_RP1_WAKE" unused output active-high line 29: "FAN_TACH" unused input active-high line 30: "HOST_SDA" unused input active-high line 31: "HOST_SCL" unused input active-high line 32: "ETH_RST_N" "phy-reset" output active-low [used] line 33: "-" unused input active-high line 34: "CD0_IO0_MICCLK" "cam0_reg" output active-high [used] line 35: "CD0_IO0_MICDAT0" unused input active-high line 36: "RP1_PCIE_CLKREQ_N" unused input active-high line 37: "-" unused input active-high line 38: "CD0_SDA" unused input active-high line 39: "CD0_SCL" unused input active-high line 40: "CD1_SDA" unused input active-high line 41: "CD1_SCL" unused input active-high line 42: "USB_VBUS_EN" unused output active-high line 43: "USB_OC_N" unused input active-high line 44: "RP1_STAT_LED" "PWR" output active-low [used] line 45: "FAN_PWM" unused output active-high line 46: "CD1_IO0_MICCLK" "cam1_reg" output active-high [used] line 47: "2712_WAKE" unused input active-high line 48: "CD1_IO1_MICDAT1" unused input active-high line 49: "EN_MAX_USB_CUR" unused output active-high line 50: "-" unused input active-high line 51: "-" unused input active-high line 52: "-" unused input active-high line 53: "-" unused input active-high The ``[used]`` suffixes indicate which GPIOs are reserved by other processes. In the output above we can see that GPIOs 7 and 8 are reserved. As it happens, these are reserved by the kernel because we have ``dtparam=spi=on`` in our boot configuration to enable the kernel SPI devices (:file:`/dev/spidev0.0` and :file:`/dev/spidev0.1`). As a result, these GPIOs *cannot* be used by rpi-lgpio because the kernel will not let anything else reserve them. They can only be used for SPI via those kernel devices, and the only way to release those GPIOs would be to change our kernel / boot configuration. In other cases we may find that a GPIO is temporarily reserved by a process. For example, the following trivial script will reserve GPIO21. .. code-block:: python3 from time import sleep from RPi import GPIO GPIO.setmode(GPIO.BCM) GPIO.setup(21, GPIO.OUT) while True: sleep(1) If we again query :manpage:`gpioinfo(1)` while it is running we will see the following: .. code-block:: console $ gpioinfo 4 | grep GPIO21 line 21: "GPIO21" "lg" output active-high [used bias-disabled] However, this reservation will disappear when the process dies. .. note:: If you receive the ``GPIO not allocated`` error in your script, please check the output of :manpage:`gpioinfo(1)` to see if the GPIO you want to use is reserved by something else. .. _debounce: Debounce ======== Debouncing of signals works fundamentally differently in RPi.GPIO, and in `lgpio`_ (the library underlying rpi-lgpio). Rather than attempt to add more complexity in between users and lgpio, which would also inevitably slow down edge detection (with all the attendant timing issues for certain applications) it is likely preferable to just live with this difference, but document it thoroughly. RPi.GPIO debounces signals by tracking the last timestamp at which it saw a specified edge and suppressing reports of edges that occur within the specified number of milliseconds after that. lgpio (and thus rpi-lgpio) debounces by waiting for a signal to be stable for the specified number of milliseconds before reporting the edge. For some applications, there will be little/no difference other than rpi-lgpio reporting an edge a few milliseconds later than RPi.GPIO would (specifically, by the amount of debounce requsted). The following diagram shows the waveform from a "bouncy" switch being pressed once, along with the points in time where RPi.GPIO and rpi-lgpio would report the rising edge when debounce of 3ms is requested: .. code-block:: :class: chart 0ms 2ms 4ms 6ms 8ms | | | | | | ┌─┐ ┌─┐ ┌─────────────────┐ | │ │ │ │ │ : │ | │ │ │ │ │ : │ ───────┘ └─┘ └─┘ : └──────────────────────── : : : : RPi.GPIO rpi-lgpio RPi.GPIO reports the edge at 2ms, then suppresses the edges at 3ms and 4ms because they are within 3ms of the last edge. By contrast, rpi-lgpio ignores the first and second rising edges (because they didn't stay stable for 3ms) and only reports the third edge at 7ms (after it's spent 3ms stable). However, consider this same scenario if debounce of 2ms is requested: .. code-block:: :class: chart 0ms 2ms 4ms 6ms 8ms | | | | | | ┌─┐ ┌─┐ ┌─────────────────┐ | │ │ │ │ │ : │ | │ │ │ │ │ : │ ───────┘ └─┘ └─┘ : └──────────────────────── : : : : : : RPi.GPIO RPi.GPIO rpi-lgpio In this case, RPi.GPIO reports the switch *twice* because the third edge is at least 2ms after the first edge. However, rpi-lgpio only reports the switch *once* because only one edge stayed stable for 2ms. Also note in this case, that rpi-lgpio's report time has moved back to 6ms because it's not waiting as long for stability. .. note:: This implies that you may find shorter debounce periods preferable when working with rpi-lgpio, than with RPi.GPIO. They will still debounce effectively, but will reduce the delay in reporting edges. One final scenario to consider is a waveform of equally spaced, repeating pulses (like PWM) every 2ms: .. code-block:: :class: chart 0ms 2ms 4ms 6ms 8ms 10ms 12ms | | | | | | | | ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌── | │ │ │ │ │ │ │ │ │ │ │ │ │ | │ │ │ │ │ │ │ │ │ │ │ │ │ ───────┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ : : : : : : : : RPi.GPIO RPi.GPIO RPi.GPIO RPi.GPIO If we request rising edge detection with a debounce of 3ms, RPi.GPIO reports half of the edges; it's suppressing every other edge as they occur within 3ms of the edge preceding them. rpi-lgpio, on the other hand, reports *no* edges at all because none of them stay stable for 3ms. PWM on inputs ============= RPi.GPIO (probably erroneously) permits PWM objects to continue operating on pins that are switched to inputs: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.setup(26, GPIO.OUT) >>> p = GPIO.PWM(26, 1000) >>> p.start(75) >>> GPIO.setup(26, GPIO.IN) >>> p.stop() >>> p.start(75) >>> p.stop() This will not work under rpi-lgpio: .. code-block:: pycon >>> from RPi import GPIO >>> GPIO.setmode(GPIO.BCM) >>> GPIO.setup(26, GPIO.OUT) >>> p = GPIO.PWM(26, 1000) >>> p.start(75) >>> GPIO.setup(26, GPIO.IN) >>> p.stop() Traceback (most recent call last): File "", line 1, in File "/home/dave/projects/rpi-lgpio/rpi-lgpio/RPi/GPIO.py", line 190, in stop lgpio.tx_pwm(_chip, self._gpio, 0, 0) File "/home/dave/envs/rpi-lgpio/lib/python3.10/site-packages/lgpio.py", line 1074, in tx_pwm return _u2i(_lgpio._tx_pwm( File "/home/dave/envs/rpi-lgpio/lib/python3.10/site-packages/lgpio.py", line 461, in _u2i raise error(error_text(v)) lgpio.error: 'bad PWM micros' Though note that the error occurs when the :class:`PWM` object is *next* acted upon, rather than at the point when the GPIO is switched to an input. .. _RPi.GPIO: https://pypi.org/project/RPi.GPIO/ .. _lgpio: https://abyz.me.uk/lg/py_lgpio.html rpi-lgpio-release-0.6/docs/index.rst000066400000000000000000000004271461771336500175100ustar00rootroot00000000000000.. Copyright (c) 2022 Dave Jones .. .. SPDX-License-Identifier: MIT .. include:: ../README.rst Contents ======== .. toctree:: :maxdepth: 1 install differences api changelog license Indexes ======= * :ref:`genindex` * :ref:`search` rpi-lgpio-release-0.6/docs/install.rst000066400000000000000000000061501461771336500200460ustar00rootroot00000000000000.. Copyright (c) 2022 David Vescovi .. Copyright (c) 2022 Dave Jones .. .. SPDX-License-Identifier: MIT ============ Installation ============ rpi-lgpio is distributed in several formats. The following sections detail installation from a variety of formats. But first a warning: .. warning:: You *cannot* install rpi-lgpio and rpi-gpio (aka RPi.GPIO, the library it emulates) at the same time, in the same Python environment. Both packages attempt to install a module named ``RPi.GPIO`` and obviously this will not work. apt/deb package =============== If your distribution includes rpi-lgpio in its archive of apt packages, then you can simply: .. code-block:: console $ sudo apt install python3-rpi-lgpio If you wish to go back to rpi-gpio: .. code-block:: console $ sudo apt remove python3-rpi-lgpio $ sudo apt install python3-rpi.gpio wheel package ============= If your distribution does not include a "native" packaging of rpi-lgpio, you can also install `rpi-lgpio from PyPI `_ using pip. Please note that rpi-lgpio does still depend on `lgpio `_ so you will need that installed as a dependency. .. note:: It is strongly recommended that you install in a virtualenv if persuing this method, in which case you have a choice as to whether lgpio is provided by a system package (such as apt), or another wheel. The following sections demonstrate installing from a wheel in a variety of scenarios. in venv without system packages ------------------------------- Construct a "clean" virutalenv with no access to system packages, then install rpi-lgpio as a wheel within that virtualenv, trusting it to pull an appropriate lgpio dependency from PyPI as another wheel: .. code-block:: console $ python3 -m venv cleanvenv $ source cleanvenv/bin/activate (cleanvenv) $ pip3 install rpi-lgpio in venv with system packages ---------------------------- Install the lgpio dependency as a system package, construct a virtualenv with access to system packages, and install rpi-lgpio as a wheel within that virtualenv: .. code-block:: console $ sudo apt install python3-lgpio $ sudo apt remove python3-rpi.gpio $ python3 -m venv --system-site-packages sysvenv $ source sysvenv/bin/activate (sysvenv) $ pip3 install rpi-lgpio Note that in this case we also ensure that we remove any system-level RPi.GPIO installation that may interfere. outside venv (system-wide) -------------------------- If you wish to install system-wide with pip, you may need to place ``sudo`` in front of the ``pip`` (or ``pip3``) commands too. Please be aware that on modern versions of pip you will need to explicitly accept the risk of trying to co-exist ``apt`` and ``pip`` packages as follows: .. code-block:: console $ sudo pip3 install --break-system-packages rpi-lgpio .. warning:: This is not an advised mode of installation, unless you are quite certain that you know what pip is going to pull in. Upgrading such an installation is also particularly risky. rpi-lgpio-release-0.6/docs/license.rst000066400000000000000000000021541461771336500200220ustar00rootroot00000000000000========= License ========= The MIT License (MIT) Copyright (c) 2022 `Dave Jones `_ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rpi-lgpio-release-0.6/scripts/000077500000000000000000000000001461771336500164035ustar00rootroot00000000000000rpi-lgpio-release-0.6/scripts/copyrights000077500000000000000000000362601461771336500205330ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: GPL-2.0-or-later """ This script updates the copyright headers on all files project-wide. It derives the authorship and copyright years information from the git history of the project; hence, this script must be run within a git clone of the project's repository. Options are available to specify the license text file, the files to edit, and files to exclude. Default options can be specified in the containing project's setup.cfg under [{SETUP_SECTION}] """ from __future__ import annotations import os import sys assert sys.version_info >= (3, 6), 'Script requires Python 3.6+' import tempfile import typing as t from argparse import ArgumentParser, Namespace from configparser import ConfigParser from operator import attrgetter from itertools import groupby from datetime import datetime from subprocess import Popen, PIPE, DEVNULL from pathlib import Path from fnmatch import fnmatch PROJECT_ROOT = (Path(__file__).parent / '..').resolve() SETUP_SECTION = str(Path(__file__).name) + ':settings' SPDX_PREFIX = 'SPDX-License-Identifier:' COPYRIGHT_PREFIX = 'Copyright (c)' def main(args: t.List[str] = None): if args is None: args = sys.argv[1:] config = get_config(args) writer = CopyWriter.from_config(config) for path, copyrights in get_copyrights(config.include, config.exclude): print(f'Re-writing {path}...') copyrights = sorted( copyrights, reverse=True, key=lambda c: (max(c.years), c.author)) with AtomicReplaceFile(path, encoding='utf-8') as target: with path.open('r') as source: for chunk in writer.transform(source, copyrights): target.write(chunk) def get_config(args: t.List[str]) -> Namespace: config = ConfigParser( defaults={ 'include': '**/*', 'exclude': '', 'license': 'LICENSE.txt', 'preamble': '', 'strip_preamble': 'false', 'spdx_prefix': SPDX_PREFIX, 'copy_prefix': COPYRIGHT_PREFIX, }, delimiters=('=',), default_section=SETUP_SECTION, empty_lines_in_values=False, interpolation=None, converters={'list': lambda s: s.strip().splitlines()}) config.read(PROJECT_ROOT / 'setup.cfg') sect = config[SETUP_SECTION] # Resolve license default relative to setup.cfg if sect['license']: sect['license'] = str(PROJECT_ROOT / sect['license']) parser = ArgumentParser(description=__doc__.format(**globals())) parser.add_argument( '-i', '--include', action='append', metavar='GLOB', default=sect.getlist('include'), help="The set of patterns that a file must match to be included in " "the set of files to re-write. Can be specified multiple times to " "add several patterns. Default: %(default)r") parser.add_argument( '-e', '--exclude', action='append', metavar='GLOB', default=sect.getlist('exclude'), help="The set of patterns that a file must *not* match to be included " "in the set of files to re-write. Can be specified multiple times to " "add several patterns. Default: %(default)r") parser.add_argument( '-l', '--license', action='store', type=Path, metavar='PATH', default=sect['license'], help="The file containing the project's license text. If this file " "contains a SPDX-License-Identifier line (in addition to the license " "text itself), then matching license text found in source files will " "be replaced by the SPDX-License-Identifier line (appropriately " "commented). Default: %(default)s") parser.add_argument( '-p', '--preamble', action='append', metavar='STR', default=sect.getlist('preamble'), help="The line(s) of text to insert before the copyright attributions " "in source files. This is typically a brief description of the " "project. Can be specified multiple times to add several lines. " "Default: %(default)r") parser.add_argument( '-S', '--spdx-prefix', action='store', metavar='STR', default=sect['spdx_prefix'], help="The prefix on the line in the license file, and within comments " "of source files that identifies the appropriate license from the " "SPDX list. Default: %(default)r") parser.add_argument( '-C', '--copy-prefix', action='store', metavar='STR', default=sect['copy_prefix'], help="The prefix before copyright attributions in source files. " "Default: %(default)r") parser.add_argument( '--no-strip-preamble', action='store_false', dest='strip_preamble') parser.add_argument( '--strip-preamble', action='store_true', default=sect.getboolean('strip-preamble'), help="If enabled, any existing preamble matching that specified " "by --preamble will be removed. This can be used to change the " "preamble text in files by first specifying the old preamble with " "this option, then running a second time with the new preamble") ns = parser.parse_args(args) ns.include = set(ns.include) ns.exclude = set(ns.exclude) return ns class Copyright(t.NamedTuple): author: str email: str years: t.Set[int] def __str__(self): if len(self.years) > 1: years = f'{min(self.years)}-{max(self.years)}' else: years = f'{min(self.years)}' return f'{years} {self.author} <{self.email}>' def get_copyrights(include: t.Set[str], exclude: t.Set[str])\ -> t.Iterator[t.Tuple[Path, t.Iterable[Copyright]]]: sorted_blame = sorted( get_contributions(include, exclude), key=lambda c: (c.path, c.author, c.email) ) blame_by_file = { path: list(file_contributions) for path, file_contributions in groupby( sorted_blame, key=attrgetter('path') ) } for path, file_contributors in blame_by_file.items(): it = groupby(file_contributors, key=lambda c: (c.author, c.email)) copyrights = [ Copyright(author, email, {y.year for y in years}) for (author, email), years in it ] yield path, copyrights class Contribution(t.NamedTuple): author: str email: str year: int path: Path def get_contributions(include: t.Set[str], exclude: t.Set[str])\ -> t.Iterator[Contribution]: for path in get_source_paths(include, exclude): blame = Popen( ['git', 'blame', '--line-porcelain', 'HEAD', '--', str(path)], stdout=PIPE, stderr=PIPE, universal_newlines=True ) author = email = year = None if blame.stdout is not None: for line in blame.stdout: if line.startswith('author '): author = line.split(' ', 1)[1].rstrip() elif line.startswith('author-mail '): email = line.split(' ', 1)[1].rstrip() email = email.lstrip('<').rstrip('>') elif line.startswith('author-time '): # Forget the timezone; we only want the year anyway timestamp = int(line.split(' ', 1)[1].strip()) year = datetime.fromtimestamp(timestamp).year elif line.startswith('filename '): assert author is not None assert email is not None assert year is not None yield Contribution( author=author, email=email, year=year, path=path) author = email = year = None blame.wait() assert blame.returncode == 0 def get_source_paths(include: t.Set[str], exclude: t.Set[str])\ -> t.Iterator[Path]: ls_tree = Popen( ['git', 'ls-tree', '-r', '--name-only', 'HEAD'], stdout=PIPE, stderr=DEVNULL, universal_newlines=True) if not include: include = {'*'} if ls_tree.stdout is not None: for filename in ls_tree.stdout: filename = filename.strip() if any(fnmatch(filename, pattern) for pattern in exclude): continue if any(fnmatch(filename, pattern) for pattern in include): yield Path(filename) ls_tree.wait() assert ls_tree.returncode == 0 class License(t.NamedTuple): ident: t.Optional[str] text: t.List[str] def get_license(path: Path, *, spdx_prefix: str = SPDX_PREFIX) -> License: with open(path, 'r') as f: lines = f.read().splitlines() idents = [ line.rstrip() for line in lines if line.startswith(spdx_prefix) ] ident = None if len(idents) > 1: raise RuntimeError(f'More than one {spdx_prefix} line in {path}!') elif len(idents) == 1: ident = idents[0] body = [ line.rstrip() for line in lines if not line.startswith(spdx_prefix) ] while not body[0]: del body[0] while not body[-1]: del body[-1] return License(ident, body) class CopyWriter: """ Transformer for the copyright header in source files. The :meth:`transform` method can be called with a file-like object as the *source* and will yield chunks of replacement data to be written to the replacement. """ # The script's kinda dumb at this point - only handles straight-forward # line-based comments, not multi-line delimited styles like /*..*/ COMMENTS = { '': '#', '.c': '//', '.cpp': '//', '.js': '//', '.py': '#', '.rst': '..', '.sh': '#', '.sql': '--', } def __init__(self, license: Path=Path('LICENSE.txt'), preamble: t.List[str]=None, spdx_prefix: str=SPDX_PREFIX, copy_prefix: str=COPYRIGHT_PREFIX): if preamble is None: preamble = [] self.license = get_license(license, spdx_prefix=spdx_prefix) self.preamble = preamble self.spdx_prefix = spdx_prefix self.copy_prefix = copy_prefix @classmethod def from_config(cls, config: Namespace) -> CopyWriter: return cls( config.license, config.preamble, config.spdx_prefix, config.copy_prefix) def transform(self, source: t.TextIO, copyrights: t.List[Copyright], *, comment_prefix: str=None) -> t.Iterator[str]: if comment_prefix is None: comment_prefix = self.COMMENTS[Path(source.name).suffix] license_start = self.license.text[0] license_end = self.license.text[-1] state = 'header' empty = True for linenum, line in enumerate(source, start=1): if state == 'header': if linenum == 1 and line.startswith('#!'): yield line empty = False elif linenum < 3 and ( 'fileencoding=' in line or '-*- coding:' in line): yield line empty = False elif line.rstrip() == comment_prefix: pass # skip blank comment lines elif line.startswith(f'{comment_prefix} {self.spdx_prefix}'): pass # skip existing SPDX ident elif line.startswith(f'{comment_prefix} {self.copy_prefix}'): pass # skip existing copyright lines elif any(line.startswith(f'{comment_prefix} {pre_line}') for pre_line in self.preamble): pass # skip existing preamble elif line.startswith(f'{comment_prefix} {license_start}'): state = 'license' # skip existing license lines else: yield from self._generate_header( copyrights, comment_prefix, empty) state = 'blank' elif state == 'license': if line.startswith(f'{comment_prefix} {license_end}'): yield from self._generate_header( copyrights, comment_prefix, empty) state = 'blank' continue if state == 'blank': # Ensure there's a blank line between license and start of the # source body if line.strip(): yield '\n' yield line state = 'body' elif state == 'body': yield line def _generate_header(self, copyrights: t.Iterable[Copyright], comment_prefix: str, empty: bool) -> t.Iterator[str]: if not empty: yield comment_prefix + '\n' for line in self.preamble: yield f'{comment_prefix} {line}\n' if self.preamble: yield comment_prefix + '\n' for copyright in copyrights: yield f'{comment_prefix} {self.copy_prefix} {copyright!s}\n' yield comment_prefix + '\n' if self.license.ident: yield f'{comment_prefix} {self.license.ident}\n' else: for line in self.license.text: if line: yield f'{comment_prefix} {line}\n' else: yield comment_prefix + '\n' class AtomicReplaceFile: """ A context manager for atomically replacing a target file. Uses :class:`tempfile.NamedTemporaryFile` to construct a temporary file in the same directory as the target file. The associated file-like object is returned as the context manager's variable; you should write the content you wish to this object. When the context manager exits, if no exception has occurred, the temporary file will be renamed over the target file atomically (after copying permissions from the target file). If an exception occurs during the context manager's block, the temporary file will be deleted leaving the original target file unaffected and the exception will be re-raised. :param pathlib.Path path: The full path and filename of the target file. This is expected to be an absolute path. :param str encoding: If ``None`` (the default), the temporary file will be opened in binary mode. Otherwise, this specifies the encoding to use with text mode. """ def __init__(self, path: t.Union[str, Path], encoding: str = None): if isinstance(path, str): path = Path(path) self._path = path self._tempfile = tempfile.NamedTemporaryFile( mode='wb' if encoding is None else 'w', dir=str(self._path.parent), encoding=encoding, delete=False) self._withfile = None def __enter__(self): self._withfile = self._tempfile.__enter__() return self._withfile def __exit__(self, exc_type, exc_value, exc_tb): os.fchmod(self._withfile.file.fileno(), self._path.stat().st_mode) result = self._tempfile.__exit__(exc_type, exc_value, exc_tb) if exc_type is None: os.rename(self._withfile.name, str(self._path)) else: os.unlink(self._withfile.name) return result if __name__ == '__main__': sys.exit(main()) rpi-lgpio-release-0.6/scripts/previewer000077500000000000000000000165211461771336500203460ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: GPL-2.0-or-later """ This script builds the HTML documentation of the containing project, and serves it from a trivial built-in web-server. It then watches the project source code for changes, and rebuilds the documentation as necessary. Options are available to specify the build output directory, the build command, and the paths to watch for changes. Default options can be specified in the containing project's setup.cfg under [{SETUP_SECTION}] """ from __future__ import annotations import os import sys assert sys.version_info >= (3, 6), 'Script requires Python 3.6+' import time import shlex import socket import traceback import typing as t import subprocess as sp import multiprocessing as mp from pathlib import Path from functools import partial from configparser import ConfigParser from argparse import ArgumentParser, Namespace from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler PROJECT_ROOT = Path(__file__).parent / '..' SETUP_SECTION = str(Path(__file__).name) + ':settings' def main(args: t.List[str] = None): if args is None: args = sys.argv[1:] config = get_config(args) queue: mp.Queue = mp.Queue() builder_proc = mp.Process(target=builder, args=(config, queue), daemon=True) server_proc = mp.Process(target=server, args=(config, queue), daemon=True) builder_proc.start() server_proc.start() exc, value, tb = queue.get() server_proc.terminate() builder_proc.terminate() traceback.print_exception(exc, value, tb) def get_config(args: t.List[str]) -> Namespace: config = ConfigParser( defaults={ 'command': 'make doc', 'html': 'build/html', 'watch': '', 'ignore': '\n'.join(['*.swp', '*.bak', '*~', '.*']), 'bind': '0.0.0.0', 'port': '8000', }, delimiters=('=',), default_section=SETUP_SECTION, empty_lines_in_values=False, interpolation=None, converters={'list': lambda s: s.strip().splitlines()}) config.read(PROJECT_ROOT / 'setup.cfg') sect = config[SETUP_SECTION] # Resolve html and watch defaults relative to setup.cfg if sect['html']: sect['html'] = str(PROJECT_ROOT / sect['html']) if sect['watch']: sect['watch'] = '\n'.join( str(PROJECT_ROOT / watch) for watch in sect.getlist('watch') ) parser = ArgumentParser(description=__doc__.format(**globals())) parser.add_argument( 'html', default=sect['html'], type=Path, nargs='?', help="The base directory (relative to the project's root) which you " "wish to server over HTTP. Default: %(default)s") parser.add_argument( '-c', '--command', default=sect['command'], help="The command to run (relative to the project root) to regenerate " "the HTML documentation. Default: %(default)s") parser.add_argument( '-w', '--watch', action='append', default=sect.getlist('watch'), help="Can be specified multiple times to append to the list of source " "patterns (relative to the project's root) to watch for changes. " "Default: %(default)s") parser.add_argument( '-i', '--ignore', action='append', default=sect.getlist('ignore'), help="Can be specified multiple times to append to the list of " "patterns to ignore. Default: %(default)s") parser.add_argument( '--bind', metavar='ADDR', default=sect['bind'], help="The address to listen on. Default: %(default)s") parser.add_argument( '--port', metavar='PORT', default=sect['port'], help="The port to listen on. Default: %(default)s") ns = parser.parse_args(args) ns.command = shlex.split(ns.command) if not ns.watch: parser.error('You must specify at least one --watch') ns.watch = [ str(Path(watch).relative_to(Path.cwd())) for watch in ns.watch ] return ns class DevRequestHandler(SimpleHTTPRequestHandler): server_version = 'DocsPreview/1.0' protocol_version = 'HTTP/1.0' class DevServer(ThreadingHTTPServer): allow_reuse_address = True base_path = None def get_best_family(host: t.Union[str, None], port: t.Union[str, int, None])\ -> t.Tuple[ socket.AddressFamily, t.Union[t.Tuple[str, int], t.Tuple[str, int, int, int]] ]: infos = socket.getaddrinfo( host, port, type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE) family, type, proto, canonname, sockaddr = next(iter(infos)) return family, sockaddr def server(config: Namespace, queue: mp.Queue = None): try: DevServer.address_family, addr = get_best_family(config.bind, config.port) handler = partial(DevRequestHandler, directory=str(config.html)) with DevServer(addr[:2], handler) as httpd: host, port = httpd.socket.getsockname()[:2] hostname = socket.gethostname() print(f'Serving {config.html} HTTP on {host} port {port}') print(f'http://{hostname}:{port}/ ...') # XXX Wait for queue message to indicate time to start? httpd.serve_forever() except: if queue is not None: queue.put(sys.exc_info()) raise def get_stats(config: Namespace) -> t.Dict[Path, os.stat_result]: return { path: path.stat() for watch_pattern in config.watch for path in Path('.').glob(watch_pattern) if not any(path.match(ignore_pattern) for ignore_pattern in config.ignore) } def get_changes(old_stats: t.Dict[Path, os.stat_result], new_stats: t.Dict[Path, os.stat_result])\ -> t.Tuple[t.Set[Path], t.Set[Path], t.Set[Path]]: # Yes, this is crude and could be more efficient but it's fast enough on a # Pi so it'll be fast enough on anything else return ( new_stats.keys() - old_stats.keys(), # new old_stats.keys() - new_stats.keys(), # deleted { # modified filepath for filepath in old_stats.keys() & new_stats.keys() if new_stats[filepath].st_mtime > old_stats[filepath].st_mtime } ) def rebuild(config: Namespace) -> t.Dict[Path, os.stat_result]: print('Rebuilding...') sp.run(config.command, cwd=PROJECT_ROOT) return get_stats(config) def builder(config: Namespace, queue: mp.Queue = None): try: old_stats = rebuild(config) print('Watching for changes in:') print('\n'.join(config.watch)) # XXX Add some message to the queue to indicate first build done and # webserver can start? while True: new_stats = get_stats(config) created, deleted, modified = get_changes(old_stats, new_stats) if created or deleted or modified: for filepath in created: print(f'New file, {filepath}') for filepath in deleted: print(f'Deleted file, {filepath}') for filepath in modified: print(f'Changed detected in {filepath}') old_stats = rebuild(config) else: time.sleep(0.5) # make sure we're not a busy loop except: if queue is not None: queue.put(sys.exc_info()) raise if __name__ == '__main__': sys.exit(main()) rpi-lgpio-release-0.6/setup.cfg000066400000000000000000000031401461771336500165330ustar00rootroot00000000000000[metadata] name = rpi-lgpio version = 0.6 description = A compatibility shim between RPi.GPIO and lgpio long_description = file:README.rst author = Dave Jones author_email = dave@waveform.org.uk url = https://rpi-lgpio.readthedocs.io/ project_urls = Documentation = https://rpi-lgpio.readthedocs.io/ Source Code = https://github.com/waveform80/rpi-lgpio Issue Tracker = https://github.com/waveform80/rpi-lgpio/issues keywords = raspberrypi gpio lgpio rpi-gpio license = BSD-3-Clause classifiers = Development Status :: 3 - Alpha Intended Audience :: Developers Topic :: System :: Hardware License :: OSI Approved :: BSD License Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: Implementation :: PyPy [options] packages = find: python_requires = >=3.7 install_requires = lgpio>=0.1.0.1 [options.extras_require] test = pytest pytest-cov doc = sphinx sphinx-rtd-theme [tool:pytest] addopts = -rsx --cov --tb=short testpaths = tests [coverage:run] source = RPi branch = true [coverage:report] ignore_errors = true show_missing = true exclude_lines = pragma: no cover assert False raise NotImplementedError pass [copyrights:settings] include = **/*.py **/*.rst exclude = docs/license.rst license = LICENSE.txt [previewer:settings] command = make -C docs html html = build/html watch = RPi/*.py docs/*.rst rpi-lgpio-release-0.6/setup.py000066400000000000000000000000461461771336500164260ustar00rootroot00000000000000from setuptools import setup setup()