mirror of
				https://github.com/KevinMidboe/python-gpiozero.git
				synced 2025-10-29 17:50:37 +00:00 
			
		
		
		
	This PR adds a software SPI implementation. Firstly this removes the absolute necessity for spidev (#140), which also means when it's not present things still work (effectively fixes #185), and also enables any four pins to be used for SPI devices (which don't require the hardware implementation). The software implementation is simplistic but still supports clock polarity and phase, select-high, and variable bits per word. However it doesn't allow precise speeds to be implemented because it just wibbles the clock as fast as it can (which being pure Python isn't actually that fast). Finally, because this PR involves creating a framework for "shared" devices (like SPI devices with multiple channels), it made sense to bung Energenie (#69) in as wells as this is a really simple shared device.
This commit is contained in:
		| @@ -9,20 +9,17 @@ str = type('') | ||||
|  | ||||
| import atexit | ||||
| import weakref | ||||
| from threading import Thread, Event, RLock | ||||
| from collections import deque | ||||
| from collections import namedtuple | ||||
| from itertools import chain | ||||
| from types import FunctionType | ||||
| try: | ||||
|     from statistics import median, mean | ||||
| except ImportError: | ||||
|     from .compat import median, mean | ||||
| from threading import RLock | ||||
|  | ||||
| from .threads import GPIOThread, _threads_shutdown | ||||
| from .exc import ( | ||||
|     DeviceClosed, | ||||
|     GPIOPinMissing, | ||||
|     GPIOPinInUse, | ||||
|     GPIODeviceClosed, | ||||
|     GPIOBadQueueLen, | ||||
|     GPIOBadSampleWait, | ||||
|     GPIOBadSourceDelay, | ||||
|     ) | ||||
|  | ||||
| @@ -30,7 +27,7 @@ from .exc import ( | ||||
| # as it supports PWM, and all Pi revisions. If no third-party libraries are | ||||
| # available, however, we fall back to a pure Python implementation which | ||||
| # supports platforms like PyPy | ||||
| from .pins import PINS_CLEANUP | ||||
| from .pins import _pins_shutdown | ||||
| try: | ||||
|     from .pins.rpigpio import RPiGPIOPin | ||||
|     DefaultPin = RPiGPIOPin | ||||
| @@ -47,24 +44,17 @@ except ImportError: | ||||
|             DefaultPin = NativePin | ||||
|  | ||||
|  | ||||
| _THREADS = set() | ||||
| _PINS = set() | ||||
| # Due to interactions between RPi.GPIO cleanup and the GPIODevice.close() | ||||
| # method the same thread may attempt to acquire this lock, leading to deadlock | ||||
| # unless the lock is re-entrant | ||||
| _PINS_LOCK = RLock() | ||||
| _PINS_LOCK = RLock() # Yes, this needs to be re-entrant | ||||
|  | ||||
| def _shutdown(): | ||||
|     while _THREADS: | ||||
|         for t in _THREADS.copy(): | ||||
|             t.stop() | ||||
|     _threads_shutdown() | ||||
|     with _PINS_LOCK: | ||||
|         while _PINS: | ||||
|             _PINS.pop().close() | ||||
|     # Any cleanup routines registered by pins libraries must be called *after* | ||||
|     # cleanup of pin objects used by devices | ||||
|     for routine in PINS_CLEANUP: | ||||
|         routine() | ||||
|     _pins_shutdown() | ||||
|  | ||||
| atexit.register(_shutdown) | ||||
|  | ||||
| @@ -75,9 +65,9 @@ class GPIOMeta(type): | ||||
|     def __new__(mcls, name, bases, cls_dict): | ||||
|         # Construct the class as normal | ||||
|         cls = super(GPIOMeta, mcls).__new__(mcls, name, bases, cls_dict) | ||||
|         # If there's a method in the class which has no docstring, search | ||||
|         # the base classes recursively for a docstring to copy | ||||
|         for attr_name, attr in cls_dict.items(): | ||||
|             # If there's a method in the class which has no docstring, search | ||||
|             # the base classes recursively for a docstring to copy | ||||
|             if isinstance(attr, FunctionType) and not attr.__doc__: | ||||
|                 for base_cls in cls.__mro__: | ||||
|                     if hasattr(base_cls, attr_name): | ||||
| @@ -87,17 +77,45 @@ class GPIOMeta(type): | ||||
|                             break | ||||
|         return cls | ||||
|  | ||||
|     def __call__(mcls, *args, **kwargs): | ||||
|         # Construct the instance as normal and ensure it's an instance of | ||||
|         # GPIOBase (defined below with a custom __setattrs__) | ||||
|         result = super(GPIOMeta, mcls).__call__(*args, **kwargs) | ||||
|         assert isinstance(result, GPIOBase) | ||||
|     def __call__(cls, *args, **kwargs): | ||||
|         # Make sure cls has GPIOBase somewhere in its ancestry (otherwise | ||||
|         # setting __attrs__ below will be rather pointless) | ||||
|         assert issubclass(cls, GPIOBase) | ||||
|         if issubclass(cls, SharedMixin): | ||||
|             # If SharedMixin appears in the class' ancestry, convert the | ||||
|             # constructor arguments to a key and check whether an instance | ||||
|             # already exists. Only construct the instance if the key's new. | ||||
|             key = cls._shared_key(*args, **kwargs) | ||||
|             try: | ||||
|                 self = cls._INSTANCES[key] | ||||
|                 self._refs += 1 | ||||
|             except (KeyError, ReferenceError) as e: | ||||
|                 self = super(GPIOMeta, cls).__call__(*args, **kwargs) | ||||
|                 self._refs = 1 | ||||
|                 # Replace the close method with one that merely decrements | ||||
|                 # the refs counter and calls the original close method when | ||||
|                 # it reaches zero | ||||
|                 old_close = self.close | ||||
|                 def close(): | ||||
|                     self._refs = max(0, self._refs - 1) | ||||
|                     if not self._refs: | ||||
|                         try: | ||||
|                             old_close() | ||||
|                         finally: | ||||
|                             del cls._INSTANCES[key] | ||||
|                 self.close = close | ||||
|                 cls._INSTANCES[key] = weakref.proxy(self) | ||||
|         else: | ||||
|             # Construct the instance as normal | ||||
|             self = super(GPIOMeta, cls).__call__(*args, **kwargs) | ||||
|         # At this point __new__ and __init__ have all been run. We now fix the | ||||
|         # set of attributes on the class by dir'ing the instance and creating a | ||||
|         # frozenset of the result called __attrs__ (which is queried by | ||||
|         # GPIOBase.__setattr__) | ||||
|         result.__attrs__ = frozenset(dir(result)) | ||||
|         return result | ||||
|         # GPIOBase.__setattr__). An exception is made for SharedMixin devices | ||||
|         # which can be constructed multiple times, returning the same instance | ||||
|         if not issubclass(cls, SharedMixin) or self._refs == 1: | ||||
|             self.__attrs__ = frozenset(dir(self)) | ||||
|         return self | ||||
|  | ||||
|  | ||||
| # Cross-version compatible method of using a metaclass | ||||
| @@ -119,13 +137,47 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})): | ||||
|         self.close() | ||||
|  | ||||
|     def close(self): | ||||
|         """ | ||||
|         Shut down the device and release all associated resources. This method | ||||
|         can be called on an already closed device without raising an exception. | ||||
|  | ||||
|         This method is primarily intended for interactive use at the command | ||||
|         line. It disables the device and releases its pin(s) for use by another | ||||
|         device. | ||||
|  | ||||
|         You can attempt to do this simply by deleting an object, but unless | ||||
|         you've cleaned up all references to the object this may not work (even | ||||
|         if you've cleaned up all references, there's still no guarantee the | ||||
|         garbage collector will actually delete the object at that point).  By | ||||
|         contrast, the close method provides a means of ensuring that the object | ||||
|         is shut down. | ||||
|  | ||||
|         For example, if you have a breadboard with a buzzer connected to pin | ||||
|         16, but then wish to attach an LED instead: | ||||
|  | ||||
|             >>> from gpiozero import * | ||||
|             >>> bz = Buzzer(16) | ||||
|             >>> bz.on() | ||||
|             >>> bz.off() | ||||
|             >>> bz.close() | ||||
|             >>> led = LED(16) | ||||
|             >>> led.blink() | ||||
|  | ||||
|         :class:`Device` descendents can also be used as context managers using | ||||
|         the :keyword:`with` statement. For example: | ||||
|  | ||||
|             >>> from gpiozero import * | ||||
|             >>> with Buzzer(16) as bz: | ||||
|             ...     bz.on() | ||||
|             ... | ||||
|             >>> with LED(16) as led: | ||||
|             ...     led.on() | ||||
|             ... | ||||
|         """ | ||||
|         # This is a placeholder which is simply here to ensure close() can be | ||||
|         # safely called from subclasses without worrying whether super-class' | ||||
|         # have it (which in turn is useful in conjunction with the SourceMixin | ||||
|         # class). | ||||
|         """ | ||||
|         Shut down the device and release all associated resources. | ||||
|         """ | ||||
|         pass | ||||
|  | ||||
|     @property | ||||
| @@ -137,6 +189,11 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})): | ||||
|         """ | ||||
|         return False | ||||
|  | ||||
|     def _check_open(self): | ||||
|         if self.closed: | ||||
|             raise DeviceClosed( | ||||
|                 '%s is closed or uninitialized' % self.__class__.__name__) | ||||
|  | ||||
|     def __enter__(self): | ||||
|         return self | ||||
|  | ||||
| @@ -145,7 +202,14 @@ class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})): | ||||
|  | ||||
|  | ||||
| class ValuesMixin(object): | ||||
|     # NOTE Use this mixin *first* in the parent list | ||||
|     """ | ||||
|     Adds a :attr:`values` property to the class which returns an infinite | ||||
|     generator of readings from the :attr:`value` property. | ||||
|  | ||||
|     .. note:: | ||||
|  | ||||
|         Use this mixin *first* in the parent class list. | ||||
|     """ | ||||
|  | ||||
|     @property | ||||
|     def values(self): | ||||
| @@ -160,7 +224,14 @@ class ValuesMixin(object): | ||||
|  | ||||
|  | ||||
| class SourceMixin(object): | ||||
|     # NOTE Use this mixin *first* in the parent list | ||||
|     """ | ||||
|     Adds a :attr:`source` property to the class which, given an iterable, | ||||
|     sets :attr:`value` to each member of that iterable until it is exhausted. | ||||
|  | ||||
|     .. note:: | ||||
|  | ||||
|         Use this mixin *first* in the parent class list. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self._source = None | ||||
| @@ -214,18 +285,121 @@ class SourceMixin(object): | ||||
|             self._source_thread.start() | ||||
|  | ||||
|  | ||||
| class CompositeDevice(ValuesMixin, GPIOBase): | ||||
| class SharedMixin(object): | ||||
|     """ | ||||
|     Represents a device composed of multiple GPIO devices like simple HATs, | ||||
|     H-bridge motor controllers, robots composed of multiple motors, etc. | ||||
|     This mixin marks a class as "shared". In this case, the meta-class | ||||
|     (GPIOMeta) will use :meth:`_shared_key` to convert the constructor | ||||
|     arguments to an immutable key, and will check whether any existing | ||||
|     instances match that key. If they do, they will be returned by the | ||||
|     constructor instead of a new instance. An internal reference counter is | ||||
|     used to determine how many times an instance has been "constructed" in this | ||||
|     way. | ||||
|  | ||||
|     When :meth:`close` is called, an internal reference counter will be | ||||
|     decremented and the instance will only close when it reaches zero. | ||||
|     """ | ||||
|     _INSTANCES = {} | ||||
|  | ||||
|     def __del__(self): | ||||
|         self._refs = 0 | ||||
|         super(SharedMixin, self).__del__() | ||||
|  | ||||
|     @classmethod | ||||
|     def _shared_key(cls, *args, **kwargs): | ||||
|         """ | ||||
|         Given the constructor arguments, returns an immutable key representing | ||||
|         the instance. The default simply assumes all positional arguments are | ||||
|         immutable. | ||||
|         """ | ||||
|         return args | ||||
|  | ||||
|  | ||||
| class Device(ValuesMixin, GPIOBase): | ||||
|     """ | ||||
|     Represents a single device of any type; GPIO-based, SPI-based, I2C-based, | ||||
|     etc. This is the base class of the device hierarchy. | ||||
|     """ | ||||
|     def __repr__(self): | ||||
|         return "<gpiozero.%s object>" % (self.__class__.__name__) | ||||
|  | ||||
|  | ||||
| class GPIODevice(ValuesMixin, GPIOBase): | ||||
| class CompositeDevice(Device): | ||||
|     """ | ||||
|     Represents a generic GPIO device. | ||||
|     Extends :class:`Device`. Represents a device composed of multiple devices | ||||
|     like simple HATs, H-bridge motor controllers, robots composed of multiple | ||||
|     motors, etc. | ||||
|  | ||||
|     The constructor accepts subordinate devices as positional or keyword | ||||
|     arguments.  Positional arguments form unnamed devices accessed via the | ||||
|     :attr:`all` attribute, while keyword arguments are added to the device | ||||
|     as named (read-only) attributes. | ||||
|  | ||||
|     :param list _order: | ||||
|         If specified, this is the order of named items specified by keyword | ||||
|         arguments (to ensure that the :attr:`value` tuple is constructed with a | ||||
|         specific order). All keyword arguments *must* be included in the | ||||
|         collection. If omitted, an arbitrary order will be selected for keyword | ||||
|         arguments. | ||||
|     """ | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         self._all = () | ||||
|         self._named = {} | ||||
|         self._tuple = None | ||||
|         self._order = kwargs.pop('_order', None) | ||||
|         if self._order is None: | ||||
|             self._order = kwargs.keys() | ||||
|         self._order = tuple(self._order) | ||||
|         for missing_name in set(self._order) - set(kwargs.keys()): | ||||
|             raise ValueError('%s missing from _order' % missing_name) | ||||
|         super(CompositeDevice, self).__init__() | ||||
|         for name in set(self._order) & set(dir(self)): | ||||
|             raise CompositeDeviceBadName('%s is a reserved name' % name) | ||||
|         self._all = args + tuple(kwargs[v] for v in self._order) | ||||
|         self._named = kwargs | ||||
|         self._tuple = namedtuple('CompositeDeviceValue', chain( | ||||
|             (str(i) for i in range(len(args))), self._order), | ||||
|             rename=True) | ||||
|  | ||||
|     def __getattr__(self, name): | ||||
|         # if _named doesn't exist yet, pretend it's an empty dict | ||||
|         if name == '_named': | ||||
|             return {} | ||||
|         try: | ||||
|             return self._named[name] | ||||
|         except KeyError: | ||||
|             raise AttributeError("no such attribute %s" % name) | ||||
|  | ||||
|     def __setattr__(self, name, value): | ||||
|         # make named components read-only properties | ||||
|         if name in self._named: | ||||
|             raise AttributeError("can't set attribute %s" % name) | ||||
|         return super(CompositeDevice, self).__setattr__(name, value) | ||||
|  | ||||
|     @property | ||||
|     def all(self): | ||||
|         return self._all | ||||
|  | ||||
|     def close(self): | ||||
|         for device in self._all: | ||||
|             device.close() | ||||
|         self._all = () | ||||
|  | ||||
|     @property | ||||
|     def closed(self): | ||||
|         return bool(self._all) | ||||
|  | ||||
|     @property | ||||
|     def tuple(self): | ||||
|         return self._tuple | ||||
|  | ||||
|     @property | ||||
|     def value(self): | ||||
|         return self.tuple(*(device.value for device in self._all)) | ||||
|  | ||||
|  | ||||
| class GPIODevice(Device): | ||||
|     """ | ||||
|     Extends :class:`Device`. Represents a generic GPIO device. | ||||
|  | ||||
|     This is the class at the root of the gpiozero class hierarchy. It handles | ||||
|     ensuring that two GPIO devices do not share the same pin, and provides | ||||
| @@ -267,48 +441,7 @@ class GPIODevice(ValuesMixin, GPIOBase): | ||||
|     def _fire_events(self): | ||||
|         pass | ||||
|  | ||||
|     def _check_open(self): | ||||
|         if self.closed: | ||||
|             raise GPIODeviceClosed( | ||||
|                 '%s is closed or uninitialized' % self.__class__.__name__) | ||||
|  | ||||
|     def close(self): | ||||
|         """ | ||||
|         Shut down the device and release all associated resources. | ||||
|  | ||||
|         This method is primarily intended for interactive use at the command | ||||
|         line. It disables the device and releases its pin for use by another | ||||
|         device. | ||||
|  | ||||
|         You can attempt to do this simply by deleting an object, but unless | ||||
|         you've cleaned up all references to the object this may not work (even | ||||
|         if you've cleaned up all references, there's still no guarantee the | ||||
|         garbage collector will actually delete the object at that point).  By | ||||
|         contrast, the close method provides a means of ensuring that the object | ||||
|         is shut down. | ||||
|  | ||||
|         For example, if you have a breadboard with a buzzer connected to pin | ||||
|         16, but then wish to attach an LED instead: | ||||
|  | ||||
|             >>> from gpiozero import * | ||||
|             >>> bz = Buzzer(16) | ||||
|             >>> bz.on() | ||||
|             >>> bz.off() | ||||
|             >>> bz.close() | ||||
|             >>> led = LED(16) | ||||
|             >>> led.blink() | ||||
|  | ||||
|         :class:`GPIODevice` descendents can also be used as context managers | ||||
|         using the :keyword:`with` statement. For example: | ||||
|  | ||||
|             >>> from gpiozero import * | ||||
|             >>> with Buzzer(16) as bz: | ||||
|             ...     bz.on() | ||||
|             ... | ||||
|             >>> with LED(16) as led: | ||||
|             ...     led.on() | ||||
|             ... | ||||
|         """ | ||||
|         super(GPIODevice, self).close() | ||||
|         with _PINS_LOCK: | ||||
|             pin = self._pin | ||||
| @@ -321,6 +454,13 @@ class GPIODevice(ValuesMixin, GPIOBase): | ||||
|     def closed(self): | ||||
|         return self._pin is None | ||||
|  | ||||
|     def _check_open(self): | ||||
|         try: | ||||
|             super(GPIODevice, self)._check_open() | ||||
|         except DeviceClosed as e: | ||||
|             # For backwards compatibility; GPIODeviceClosed is deprecated | ||||
|             raise GPIODeviceClosed(str(e)) | ||||
|  | ||||
|     @property | ||||
|     def pin(self): | ||||
|         """ | ||||
| @@ -349,66 +489,3 @@ class GPIODevice(ValuesMixin, GPIOBase): | ||||
|             return "<gpiozero.%s object closed>" % self.__class__.__name__ | ||||
|  | ||||
|  | ||||
| class GPIOThread(Thread): | ||||
|     def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): | ||||
|         super(GPIOThread, self).__init__(group, target, name, args, kwargs) | ||||
|         self.stopping = Event() | ||||
|         self.daemon = True | ||||
|  | ||||
|     def start(self): | ||||
|         self.stopping.clear() | ||||
|         _THREADS.add(self) | ||||
|         super(GPIOThread, self).start() | ||||
|  | ||||
|     def stop(self): | ||||
|         self.stopping.set() | ||||
|         self.join() | ||||
|  | ||||
|     def join(self): | ||||
|         super(GPIOThread, self).join() | ||||
|         _THREADS.discard(self) | ||||
|  | ||||
|  | ||||
| class GPIOQueue(GPIOThread): | ||||
|     def __init__( | ||||
|             self, parent, queue_len=5, sample_wait=0.0, partial=False, | ||||
|             average=median): | ||||
|         assert isinstance(parent, GPIODevice) | ||||
|         assert callable(average) | ||||
|         super(GPIOQueue, self).__init__(target=self.fill) | ||||
|         if queue_len < 1: | ||||
|             raise GPIOBadQueueLen('queue_len must be at least one') | ||||
|         if sample_wait < 0: | ||||
|             raise GPIOBadSampleWait('sample_wait must be 0 or greater') | ||||
|         self.queue = deque(maxlen=queue_len) | ||||
|         self.partial = partial | ||||
|         self.sample_wait = sample_wait | ||||
|         self.full = Event() | ||||
|         self.parent = weakref.proxy(parent) | ||||
|         self.average = average | ||||
|  | ||||
|     @property | ||||
|     def value(self): | ||||
|         if not self.partial: | ||||
|             self.full.wait() | ||||
|         try: | ||||
|             return self.average(self.queue) | ||||
|         except ZeroDivisionError: | ||||
|             # No data == inactive value | ||||
|             return 0.0 | ||||
|  | ||||
|     def fill(self): | ||||
|         try: | ||||
|             while (not self.stopping.wait(self.sample_wait) and | ||||
|                     len(self.queue) < self.queue.maxlen): | ||||
|                 self.queue.append(self.parent._read()) | ||||
|                 if self.partial: | ||||
|                     self.parent._fire_events() | ||||
|             self.full.set() | ||||
|             while not self.stopping.wait(self.sample_wait): | ||||
|                 self.queue.append(self.parent._read()) | ||||
|                 self.parent._fire_events() | ||||
|         except ReferenceError: | ||||
|             # Parent is dead; time to die! | ||||
|             pass | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user