diff --git a/docs/api_boards.rst b/docs/api_boards.rst index 5268dcc..eb12754 100644 --- a/docs/api_boards.rst +++ b/docs/api_boards.rst @@ -18,7 +18,7 @@ individually. LED Board ========= -.. autoclass:: LEDBoard(\*pins, pwm=False) +.. autoclass:: LEDBoard(\*pins, pwm=False, active_high=True, initial_value=False) :inherited-members: :members: diff --git a/docs/api_exc.rst b/docs/api_exc.rst index 8e71f3d..40bdf79 100644 --- a/docs/api_exc.rst +++ b/docs/api_exc.rst @@ -34,10 +34,25 @@ so you can still do:: print('Bad value specified') +Errors +====== + .. autoexception:: GPIOZeroError +.. autoexception:: DeviceClosed + .. autoexception:: CompositeDeviceError +.. autoexception:: CompositeDeviceBadName + +.. autoexception:: EnergenieSocketMissing + +.. autoexception:: EnergenieBadSocket + +.. autoexception:: SPIError + +.. autoexception:: SPIBadArgs + .. autoexception:: GPIODeviceError .. autoexception:: GPIODeviceClosed @@ -80,3 +95,12 @@ so you can still do:: .. autoexception:: PinPWMFixedValue +Warnings +======== + +.. autoexception:: GPIOZeroWarning + +.. autoexception:: SPIWarning + +.. autoexception:: SPISoftwareFallback + diff --git a/docs/api_generic.rst b/docs/api_generic.rst index 67d9a12..5c6b51f 100644 --- a/docs/api_generic.rst +++ b/docs/api_generic.rst @@ -4,15 +4,22 @@ Generic Devices .. currentmodule:: gpiozero -The GPIO Zero class hierarchy is quite extensive. It contains a couple of base +The GPIO Zero class hierarchy is quite extensive. It contains several base classes: -* :class:`GPIODevice` for individual devices that attach to a single GPIO pin +* :class:`Device` is the root of the hierarchy, implementing base functionality + like :meth:`~Device.close` and context manager handlers. -* :class:`CompositeDevice` for devices composed of multiple other devices like - HATs +* :class:`GPIODevice` represents individual devices that attach to a single + GPIO pin -There are also a couple of `mixin classes`_: +* :class:`SPIDevice` represents devices that communicate over an SPI interface + (implemented as four GPIO pins) + +* :class:`CompositeDevice` represents devices composed of multiple other + devices like HATs + +There are also several `mixin classes`_: * :class:`ValuesMixin` which defines the ``values`` properties; there is rarely a need to use this as the base classes mentioned above both include it @@ -21,13 +28,27 @@ There are also a couple of `mixin classes`_: * :class:`SourceMixin` which defines the ``source`` property; this is generally included in novel output device classes +* :class:`SharedMixin` which causes classes to track their construction and + return existing instances when equivalent constructor arguments are passed + .. _mixin classes: https://en.wikipedia.org/wiki/Mixin The current class hierarchies are displayed below. For brevity, the mixin -classes are omitted: +classes (and some other details) are omitted, and the chart is broken into +pieces by base class. The lighter boxes represent classes that are "effectively +abstract". These classes aren't directly useful without sub-classing them and +adding bits. + +First, the classes below :class:`GPIODevice`: .. image:: images/gpio_device_hierarchy.* +Next, the classes below :class:`SPIDevice`: + +.. image:: images/spi_device_hierarchy.* + +Next, the classes below :class:`CompositeDevice`: + .. image:: images/composite_device_hierarchy.* Finally, for composite devices, the following chart shows which devices are @@ -38,12 +59,16 @@ composed of which other devices: Base Classes ============ +.. autoclass:: Device + :members: close, closed + .. autoclass:: GPIODevice(pin) - :inherited-members: :members: .. autoclass:: CompositeDevice - :inherited-members: + :members: + +.. autoclass:: SPIDevice :members: Input Devices @@ -61,19 +86,34 @@ Input Devices .. autoclass:: SmoothedInputDevice :members: -.. autoclass:: AnalogInputDevice - :members: - Output Devices ============== .. autoclass:: OutputDevice(pin, active_high=True, initial_value=False) :members: +.. autoclass:: DigitalOutputDevice(pin, active_high=True, initial_value=False) + :members: + .. autoclass:: PWMOutputDevice(pin, active_high=True, initial_value=0, frequency=100) :members: -.. autoclass:: DigitalOutputDevice(pin, active_high=True, initial_value=False) +SPI Devices +=========== + +.. autoclass:: SPIDevice + :members: + +.. autoclass:: AnalogInputDevice + :members: + +Composite Devices +================= + +.. autoclass:: CompositeOutputDevice + :members: + +.. autoclass:: LEDCollection :members: Mixin Classes @@ -85,3 +125,6 @@ Mixin Classes .. autoclass:: SourceMixin(...) :members: +.. autoclass:: SharedMixin(...) + :members: _shared_key + diff --git a/docs/api_input.rst b/docs/api_input.rst index b451c3e..fad1504 100644 --- a/docs/api_input.rst +++ b/docs/api_input.rst @@ -40,28 +40,3 @@ Distance Sensor (HC-SR04) .. autoclass:: DistanceSensor(echo, trigger, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False) :members: wait_for_in_range, wait_for_out_of_range, trigger, echo, when_in_range, when_out_of_range, max_distance, distance, threshold_distance - -Analog to Digital Converters (ADC) -================================== - -.. autoclass:: MCP3004 - :members: bus, device, channel, value, differential - -.. autoclass:: MCP3008 - :members: bus, device, channel, value, differential - -.. autoclass:: MCP3204 - :members: bus, device, channel, value, differential - -.. autoclass:: MCP3208 - :members: bus, device, channel, value, differential - -.. autoclass:: MCP3301 - :members: bus, device, value - -.. autoclass:: MCP3302 - :members: bus, device, channel, value, differential - -.. autoclass:: MCP3304 - :members: bus, device, channel, value, differential - diff --git a/docs/api_spi.rst b/docs/api_spi.rst new file mode 100644 index 0000000..000e186 --- /dev/null +++ b/docs/api_spi.rst @@ -0,0 +1,106 @@ +=========== +SPI Devices +=========== + +.. currentmodule:: gpiozero + +SPI stands for `Serial Peripheral Interface`_ and is a mechanism allowing +compatible devices to communicate with the Pi. SPI is a four-wire protocol +meaning it usually requires four pins to operate: + +* A "clock" pin which provides timing information. + +* A "MOSI" pin (Master Out, Slave In) which the Pi uses to send information + to the device. + +* A "MISO" pin (Master In, Slave Out) which the Pi uses to receive information + from the device. + +* A "select" pin which the Pi uses to indicate which device it's talking to. + This last pin is necessary because multiple devices can share the clock, + MOSI, and MISO pins, but only one device can be connected to each select + pin. + +The gpiozero library provides two SPI implementations: + +* A software based implementation. This is always available, can use any four + GPIO pins for SPI communication, but is rather slow and won't work with all + devices. + +* A hardware based implementation. This is only available when the SPI kernel + module is loaded, and the Python spidev library is available. It can only use + specific pins for SPI communication (GPIO11=clock, GPIO10=MOSI, GPIO9=MISO, + while GPIO8 is select for device 0 and GPIO7 is select for device 1). + However, it is extremely fast and works with all devices. + +.. _Serial Peripheral Interface: https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus + + +.. _spi_args: + +SPI keyword args +================ + +When constructing an SPI device the are two schemes for specifying which pins +it is connected to: + +* You can specify *port* and *device* keyword arguments. The *port* parameter + must be 0 (there is only one user-accessible hardware SPI interface on the Pi + using GPIO11 as the clock pin, GPIO10 as the MOSI pin, and GPIO9 as the MISO + pin), while the *device* parameter must be 0 or 1. If *device* is 0, the + select pin will be GPIO8. If *device* is 1, the select pin will be GPIO7. + +* Alternatively you can specify *clock_pin*, *mosi_pin*, *miso_pin*, and + *select_pin* keyword arguments. In this case the pins can be any 4 GPIO pins + (remember that SPI devices can share clock, MOSI, and MISO pins, but not + select pins - the gpiozero library will enforce this restriction). + +You cannot mix these two schemes, i.e. attempting to specify *port* and +*clock_pin* will result in :exc:`SPIBadArgs` being raised. However, you can +omit any arguments from either scheme. The defaults are: + +* *port* and *device* both default to 0. + +* *clock_pin* defaults to 11, *mosi_pin* defaults to 10, *miso_pin* defaults + to 9, and *select_pin* defaults to 8. + +Hence the following constructors are all equiavlent:: + + from gpiozero import MCP3008 + + MCP3008(channel=0) + MCP3008(channel=0, device=0) + MCP3008(channel=0, port=0, device=0) + MCP3008(channel=0, select_pin=8) + MCP3008(channel=0, clock_pin=11, mosi_pin=10, miso_pin=9, select_pin=8) + +Note that the defaults describe equivalent sets of pins and that these pins are +compatible with the hardware implementation. Regardless of which scheme you +use, gpiozero will attempt to use the hardware implementation if it is +available and if the selected pins are compatible, falling back to the software +implementation if not. + +Analog to Digital Converters (ADC) +================================== + +.. autoclass:: MCP3004 + :members: channel, value, differential + +.. autoclass:: MCP3008 + :members: channel, value, differential + +.. autoclass:: MCP3204 + :members: channel, value, differential + +.. autoclass:: MCP3208 + :members: channel, value, differential + +.. autoclass:: MCP3301 + :members: value + +.. autoclass:: MCP3302 + :members: channel, value, differential + +.. autoclass:: MCP3304 + :members: channel, value, differential + diff --git a/docs/conf.py b/docs/conf.py index ab831ff..fea2f1c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,7 @@ sys.modules['RPi'] = Mock() sys.modules['RPi.GPIO'] = sys.modules['RPi'].GPIO sys.modules['RPIO'] = Mock() sys.modules['RPIO.PWM'] = sys.modules['RPIO'].PWM +sys.modules['RPIO.Exceptions'] = sys.modules['RPIO'].Exceptions sys.modules['pigpio'] = Mock() sys.modules['w1thermsensor'] = Mock() sys.modules['spidev'] = Mock() diff --git a/docs/images/composed_devices.pdf b/docs/images/composed_devices.pdf index 1f464a0..40d4ed7 100644 Binary files a/docs/images/composed_devices.pdf and b/docs/images/composed_devices.pdf differ diff --git a/docs/images/composed_devices.png b/docs/images/composed_devices.png index c5830cb..e4ee206 100644 Binary files a/docs/images/composed_devices.png and b/docs/images/composed_devices.png differ diff --git a/docs/images/composite_device_hierarchy.dot b/docs/images/composite_device_hierarchy.dot index 214c4d9..ec8c0b3 100644 --- a/docs/images/composite_device_hierarchy.dot +++ b/docs/images/composite_device_hierarchy.dot @@ -2,32 +2,36 @@ digraph classes { graph [rankdir=BT]; - node [shape=rect, style=filled, color="#2980b9", fontname=Sans, fontcolor="#ffffff", fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; - AnalogInputDevice->CompositeDevice; - MCP3xxx->AnalogInputDevice; - MCP33xx->MCP3xxx; - MCP3004->MCP3xxx; - MCP3008->MCP3xxx; - MCP3204->MCP3xxx; - MCP3208->MCP3xxx; - MCP3301->MCP33xx; - MCP3302->MCP33xx; - MCP3304->MCP33xx; + /* Abstract classes */ + node [color="#9ec6e0", fontcolor="#000000"] + Device; + CompositeDevice; + CompositeOutputDevice; + LEDCollection; + + /* Concrete classes */ + node [color="#2980b9", fontcolor="#ffffff"]; + CompositeDevice->Device; + CompositeOutputDevice->CompositeDevice; + LEDCollection->CompositeOutputDevice; + + LEDBoard->LEDCollection; + LEDBarGraph->LEDCollection; - RGBLED->CompositeDevice; - Motor->CompositeDevice; - LEDBoard->CompositeDevice; PiLiter->LEDBoard; + PiLiterBarGraph->LEDBarGraph; TrafficLights->LEDBoard; PiTraffic->TrafficLights; - - TrafficLightsBuzzer->CompositeDevice; + TrafficLightsBuzzer->CompositeOutputDevice; FishDish->TrafficLightsBuzzer; TrafficHat->TrafficLightsBuzzer; - Robot->CompositeDevice; RyanteckRobot->Robot; CamJamKitRobot->Robot; + + RGBLED->CompositeDevice; + Motor->CompositeDevice; } diff --git a/docs/images/composite_device_hierarchy.pdf b/docs/images/composite_device_hierarchy.pdf index ffe3dbd..32f2d27 100644 Binary files a/docs/images/composite_device_hierarchy.pdf and b/docs/images/composite_device_hierarchy.pdf differ diff --git a/docs/images/composite_device_hierarchy.png b/docs/images/composite_device_hierarchy.png index 65e5e2f..da5096f 100644 Binary files a/docs/images/composite_device_hierarchy.png and b/docs/images/composite_device_hierarchy.png differ diff --git a/docs/images/composite_device_hierarchy.svg b/docs/images/composite_device_hierarchy.svg index 7edf1d0..01adcce 100644 --- a/docs/images/composite_device_hierarchy.svg +++ b/docs/images/composite_device_hierarchy.svg @@ -4,235 +4,185 @@ - - + + classes - - -AnalogInputDevice - -AnalogInputDevice + + +Device + +Device CompositeDevice - -CompositeDevice + +CompositeDevice - -AnalogInputDevice->CompositeDevice - - + +CompositeDevice->Device + + - -MCP3xxx - -MCP3xxx + +CompositeOutputDevice + +CompositeOutputDevice - -MCP3xxx->AnalogInputDevice - - + +CompositeOutputDevice->CompositeDevice + + - -MCP33xx - -MCP33xx + +LEDCollection + +LEDCollection - -MCP33xx->MCP3xxx - - - - -MCP3004 - -MCP3004 - - -MCP3004->MCP3xxx - - - - -MCP3008 - -MCP3008 - - -MCP3008->MCP3xxx - - - - -MCP3204 - -MCP3204 - - -MCP3204->MCP3xxx - - - - -MCP3208 - -MCP3208 - - -MCP3208->MCP3xxx - - - - -MCP3301 - -MCP3301 - - -MCP3301->MCP33xx - - - - -MCP3302 - -MCP3302 - - -MCP3302->MCP33xx - - - - -MCP3304 - -MCP3304 - - -MCP3304->MCP33xx - - - - -RGBLED - -RGBLED - - -RGBLED->CompositeDevice - - - - -Motor - -Motor - - -Motor->CompositeDevice - - + +LEDCollection->CompositeOutputDevice + + -LEDBoard - -LEDBoard +LEDBoard + +LEDBoard - -LEDBoard->CompositeDevice - - + +LEDBoard->LEDCollection + + + + +LEDBarGraph + +LEDBarGraph + + +LEDBarGraph->LEDCollection + + -PiLiter - -PiLiter +PiLiter + +PiLiter -PiLiter->LEDBoard - - +PiLiter->LEDBoard + + + + +PiLiterBarGraph + +PiLiterBarGraph + + +PiLiterBarGraph->LEDBarGraph + + -TrafficLights - -TrafficLights +TrafficLights + +TrafficLights -TrafficLights->LEDBoard - - +TrafficLights->LEDBoard + + -PiTraffic - -PiTraffic +PiTraffic + +PiTraffic -PiTraffic->TrafficLights - - +PiTraffic->TrafficLights + + -TrafficLightsBuzzer - -TrafficLightsBuzzer +TrafficLightsBuzzer + +TrafficLightsBuzzer - -TrafficLightsBuzzer->CompositeDevice - - + +TrafficLightsBuzzer->CompositeOutputDevice + + -FishDish - -FishDish +FishDish + +FishDish -FishDish->TrafficLightsBuzzer - - +FishDish->TrafficLightsBuzzer + + -TrafficHat - -TrafficHat +TrafficHat + +TrafficHat -TrafficHat->TrafficLightsBuzzer - - +TrafficHat->TrafficLightsBuzzer + + -Robot - -Robot +Robot + +Robot -Robot->CompositeDevice - - +Robot->CompositeDevice + + -RyanteckRobot - -RyanteckRobot +RyanteckRobot + +RyanteckRobot -RyanteckRobot->Robot - - +RyanteckRobot->Robot + + -CamJamKitRobot - -CamJamKitRobot +CamJamKitRobot + +CamJamKitRobot -CamJamKitRobot->Robot - - +CamJamKitRobot->Robot + + + + +RGBLED + +RGBLED + + +RGBLED->CompositeDevice + + + + +Motor + +Motor + + +Motor->CompositeDevice + + diff --git a/docs/images/gpio_device_hierarchy.dot b/docs/images/gpio_device_hierarchy.dot index 2461094..a1684ae 100644 --- a/docs/images/gpio_device_hierarchy.dot +++ b/docs/images/gpio_device_hierarchy.dot @@ -2,9 +2,20 @@ digraph classes { graph [rankdir=BT]; - node [shape=rect, style=filled, color="#2980b9", fontname=Sans, fontcolor="#ffffff", fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; + /* Abstract classes */ + node [color="#9ec6e0", fontcolor="#000000"] + Device; + GPIODevice; + WaitableInputDevice; + SmoothedInputDevice; + + /* Concrete classes */ + node [color="#2980b9", fontcolor="#ffffff"]; + + GPIODevice->Device; InputDevice->GPIODevice; WaitableInputDevice->InputDevice; DigitalInputDevice->WaitableInputDevice; diff --git a/docs/images/gpio_device_hierarchy.pdf b/docs/images/gpio_device_hierarchy.pdf index ff427f4..66ce3e1 100644 Binary files a/docs/images/gpio_device_hierarchy.pdf and b/docs/images/gpio_device_hierarchy.pdf differ diff --git a/docs/images/gpio_device_hierarchy.png b/docs/images/gpio_device_hierarchy.png index b66c47f..8af9cbd 100644 Binary files a/docs/images/gpio_device_hierarchy.png and b/docs/images/gpio_device_hierarchy.png differ diff --git a/docs/images/gpio_device_hierarchy.svg b/docs/images/gpio_device_hierarchy.svg index 0427f11..6624ab0 100644 --- a/docs/images/gpio_device_hierarchy.svg +++ b/docs/images/gpio_device_hierarchy.svg @@ -4,145 +4,155 @@ - - + + classes - - -InputDevice - -InputDevice + + +Device + +Device GPIODevice - -GPIODevice + +GPIODevice - -InputDevice->GPIODevice - - + +GPIODevice->Device + + WaitableInputDevice - -WaitableInputDevice + +WaitableInputDevice + + +InputDevice + +InputDevice -WaitableInputDevice->InputDevice - - - - -DigitalInputDevice - -DigitalInputDevice - - -DigitalInputDevice->WaitableInputDevice - - +WaitableInputDevice->InputDevice + + -SmoothedInputDevice - -SmoothedInputDevice +SmoothedInputDevice + +SmoothedInputDevice -SmoothedInputDevice->WaitableInputDevice - - +SmoothedInputDevice->WaitableInputDevice + + + + +InputDevice->GPIODevice + + + + +DigitalInputDevice + +DigitalInputDevice + + +DigitalInputDevice->WaitableInputDevice + + -Button - -Button +Button + +Button -Button->DigitalInputDevice - - +Button->DigitalInputDevice + + -MotionSensor - -MotionSensor +MotionSensor + +MotionSensor -MotionSensor->SmoothedInputDevice - - +MotionSensor->SmoothedInputDevice + + -LightSensor - -LightSensor +LightSensor + +LightSensor -LightSensor->SmoothedInputDevice - - +LightSensor->SmoothedInputDevice + + -OutputDevice - -OutputDevice +OutputDevice + +OutputDevice -OutputDevice->GPIODevice - - +OutputDevice->GPIODevice + + -DigitalOutputDevice - -DigitalOutputDevice +DigitalOutputDevice + +DigitalOutputDevice -DigitalOutputDevice->OutputDevice - - +DigitalOutputDevice->OutputDevice + + -LED - -LED +LED + +LED -LED->DigitalOutputDevice - - +LED->DigitalOutputDevice + + -Buzzer - -Buzzer +Buzzer + +Buzzer -Buzzer->DigitalOutputDevice - - +Buzzer->DigitalOutputDevice + + -PWMOutputDevice - -PWMOutputDevice +PWMOutputDevice + +PWMOutputDevice -PWMOutputDevice->OutputDevice - - +PWMOutputDevice->OutputDevice + + -PWMLED - -PWMLED +PWMLED + +PWMLED -PWMLED->PWMOutputDevice - - +PWMLED->PWMOutputDevice + + diff --git a/docs/images/led_button_bb.pdf b/docs/images/led_button_bb.pdf index 265f994..b9a1496 100644 Binary files a/docs/images/led_button_bb.pdf and b/docs/images/led_button_bb.pdf differ diff --git a/docs/images/led_button_bb.png b/docs/images/led_button_bb.png index a4d0ec7..bdb883c 100644 Binary files a/docs/images/led_button_bb.png and b/docs/images/led_button_bb.png differ diff --git a/docs/images/light_sensor_bb.pdf b/docs/images/light_sensor_bb.pdf index 5689d46..be5177d 100644 Binary files a/docs/images/light_sensor_bb.pdf and b/docs/images/light_sensor_bb.pdf differ diff --git a/docs/images/light_sensor_bb.png b/docs/images/light_sensor_bb.png index 746541c..ce22f27 100644 Binary files a/docs/images/light_sensor_bb.png and b/docs/images/light_sensor_bb.png differ diff --git a/docs/images/motion_sensor_bb.pdf b/docs/images/motion_sensor_bb.pdf index 92347a2..7bfe5c7 100644 Binary files a/docs/images/motion_sensor_bb.pdf and b/docs/images/motion_sensor_bb.pdf differ diff --git a/docs/images/motion_sensor_bb.png b/docs/images/motion_sensor_bb.png index bf2ff9b..614dcc4 100644 Binary files a/docs/images/motion_sensor_bb.png and b/docs/images/motion_sensor_bb.png differ diff --git a/docs/images/motor_bb.pdf b/docs/images/motor_bb.pdf index 7e26c48..90a0aae 100644 Binary files a/docs/images/motor_bb.pdf and b/docs/images/motor_bb.pdf differ diff --git a/docs/images/motor_bb.png b/docs/images/motor_bb.png index 016cdde..6eda1e6 100644 Binary files a/docs/images/motor_bb.png and b/docs/images/motor_bb.png differ diff --git a/docs/images/pin_layout.pdf b/docs/images/pin_layout.pdf index 25c889c..f3b6527 100644 Binary files a/docs/images/pin_layout.pdf and b/docs/images/pin_layout.pdf differ diff --git a/docs/images/pin_layout.png b/docs/images/pin_layout.png index ada4bef..1e5f379 100644 Binary files a/docs/images/pin_layout.png and b/docs/images/pin_layout.png differ diff --git a/docs/images/potentiometer_bb.pdf b/docs/images/potentiometer_bb.pdf index cc2e4d1..32b5d9b 100644 Binary files a/docs/images/potentiometer_bb.pdf and b/docs/images/potentiometer_bb.pdf differ diff --git a/docs/images/potentiometer_bb.png b/docs/images/potentiometer_bb.png index 4aa63b5..d55adc9 100644 Binary files a/docs/images/potentiometer_bb.png and b/docs/images/potentiometer_bb.png differ diff --git a/docs/images/reaction_game_bb.pdf b/docs/images/reaction_game_bb.pdf index ff5552a..9a8ce94 100644 Binary files a/docs/images/reaction_game_bb.pdf and b/docs/images/reaction_game_bb.pdf differ diff --git a/docs/images/rgb_led_bb.pdf b/docs/images/rgb_led_bb.pdf index ee83a65..410e513 100644 Binary files a/docs/images/rgb_led_bb.pdf and b/docs/images/rgb_led_bb.pdf differ diff --git a/docs/images/rgb_led_bb.png b/docs/images/rgb_led_bb.png index bd8bc8d..f85a305 100644 Binary files a/docs/images/rgb_led_bb.png and b/docs/images/rgb_led_bb.png differ diff --git a/docs/images/spi_device_hierarchy.dot b/docs/images/spi_device_hierarchy.dot new file mode 100644 index 0000000..430ecb7 --- /dev/null +++ b/docs/images/spi_device_hierarchy.dot @@ -0,0 +1,27 @@ +digraph classes { + graph [rankdir=BT]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; + edge []; + + /* Abstract classes */ + node [color="#9ec6e0", fontcolor="#000000"] + Device; + SPIDevice; + AnalogInputDevice; + MCP3xxx; + MCP33xx; + + /* Concrete classes */ + node [color="#2980b9", fontcolor="#ffffff"]; + SPIDevice->Device; + AnalogInputDevice->SPIDevice; + MCP3xxx->AnalogInputDevice; + MCP33xx->MCP3xxx; + MCP3004->MCP3xxx; + MCP3008->MCP3xxx; + MCP3204->MCP3xxx; + MCP3208->MCP3xxx; + MCP3301->MCP33xx; + MCP3302->MCP33xx; + MCP3304->MCP33xx; +} diff --git a/docs/images/spi_device_hierarchy.pdf b/docs/images/spi_device_hierarchy.pdf new file mode 100644 index 0000000..67b92df Binary files /dev/null and b/docs/images/spi_device_hierarchy.pdf differ diff --git a/docs/images/spi_device_hierarchy.png b/docs/images/spi_device_hierarchy.png new file mode 100644 index 0000000..14d63b0 Binary files /dev/null and b/docs/images/spi_device_hierarchy.png differ diff --git a/docs/images/spi_device_hierarchy.svg b/docs/images/spi_device_hierarchy.svg new file mode 100644 index 0000000..1fd031f --- /dev/null +++ b/docs/images/spi_device_hierarchy.svg @@ -0,0 +1,128 @@ + + + + + + +classes + + +Device + +Device + + +SPIDevice + +SPIDevice + + +SPIDevice->Device + + + + +AnalogInputDevice + +AnalogInputDevice + + +AnalogInputDevice->SPIDevice + + + + +MCP3xxx + +MCP3xxx + + +MCP3xxx->AnalogInputDevice + + + + +MCP33xx + +MCP33xx + + +MCP33xx->MCP3xxx + + + + +MCP3004 + +MCP3004 + + +MCP3004->MCP3xxx + + + + +MCP3008 + +MCP3008 + + +MCP3008->MCP3xxx + + + + +MCP3204 + +MCP3204 + + +MCP3204->MCP3xxx + + + + +MCP3208 + +MCP3208 + + +MCP3208->MCP3xxx + + + + +MCP3301 + +MCP3301 + + +MCP3301->MCP33xx + + + + +MCP3302 + +MCP3302 + + +MCP3302->MCP33xx + + + + +MCP3304 + +MCP3304 + + +MCP3304->MCP33xx + + + + + diff --git a/docs/images/traffic_lights_bb.pdf b/docs/images/traffic_lights_bb.pdf index 2f507f3..0f3dc11 100644 Binary files a/docs/images/traffic_lights_bb.pdf and b/docs/images/traffic_lights_bb.pdf differ diff --git a/docs/index.rst b/docs/index.rst index 4f16c12..2f5498f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ Table of Contents notes api_input api_output + api_spi api_boards api_generic api_pins diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index ddfb922..e031f7a 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -10,7 +10,13 @@ from .pins import ( ) from .exc import ( GPIOZeroError, + DeviceClosed, CompositeDeviceError, + CompositeDeviceBadName, + SPIError, + SPIBadArgs, + EnergenieSocketMissing, + EnergenieBadSocket, GPIODeviceError, GPIODeviceClosed, GPIOPinInUse, @@ -32,10 +38,15 @@ from .exc import ( PinPWMError, PinPWMUnsupported, PinPWMFixedValue, + GPIOZeroWarning, + SPIWarning, + SPISoftwareFallback, ) from .devices import ( + Device, GPIODevice, CompositeDevice, + SharedMixin, SourceMixin, ValuesMixin, ) @@ -44,12 +55,14 @@ from .input_devices import ( WaitableInputDevice, DigitalInputDevice, SmoothedInputDevice, - AnalogInputDevice, Button, LineSensor, MotionSensor, LightSensor, DistanceSensor, +) +from .spi_devices import ( + SPIDevice, AnalogInputDevice, MCP3001, MCP3002, @@ -74,6 +87,8 @@ from .output_devices import ( RGBLED, ) from .boards import ( + CompositeOutputDevice, + LEDCollection, LEDBoard, LEDBarGraph, PiLiter, @@ -86,4 +101,5 @@ from .boards import ( Robot, RyanteckRobot, CamJamKitRobot, + Energenie, ) diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 2c92afc..04e094d 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -12,36 +12,84 @@ except ImportError: from time import sleep from collections import namedtuple from itertools import repeat, cycle, chain +from threading import Lock -from .exc import InputDeviceError, OutputDeviceError +from .exc import ( + GPIOPinMissing, + EnergenieSocketMissing, + EnergenieBadSocket, + ) from .input_devices import Button -from .output_devices import LED, PWMLED, Buzzer, Motor -from .devices import GPIOThread, CompositeDevice, SourceMixin +from .output_devices import OutputDevice, LED, PWMLED, Buzzer, Motor +from .threads import GPIOThread +from .devices import Device, CompositeDevice, SharedMixin, SourceMixin -class LEDCollection(SourceMixin, CompositeDevice): +class CompositeOutputDevice(SourceMixin, CompositeDevice): """ - Abstract base class for :class:`LEDBoard` and :class:`LEDBarGraph`. + Extends :class:`CompositeDevice` with :meth:`on`, :meth:`off`, and + :meth:`toggle` methods for controlling subordinate output devices. Also + extends :attr:`value` to be writeable. """ - def __init__(self, *pins, **kwargs): - self._blink_thread = None - super(LEDCollection, self).__init__() - pwm = kwargs.get('pwm', False) - active_high = kwargs.get('active_high', True) - initial_value = kwargs.get('initial_value', False) - LEDClass = PWMLED if pwm else LED - self._leds = tuple( - LEDClass(pin, active_high, initial_value) for pin in pins - ) + def on(self): + """ + Turn all the output devices on. + """ + for device in self.all: + if isinstance(device, OutputDevice): + device.on() - def close(self): - for led in self.leds: - led.close() + def off(self): + """ + Turn all the output devices off. + """ + for device in self.all: + if isinstance(device, OutputDevice): + device.off() + + def toggle(self): + """ + Toggle all the output devices. For each device, if it's on, turn it + off; if it's off, turn it on. + """ + for device in self.all: + if isinstance(device, OutputDevice): + device.toggle() @property - def closed(self): - return all(led.closed for led in self.leds) + def value(self): + """ + A tuple containing a value for each subordinate device. This property + can also be set to update the state of all output subordinate devices. + """ + return super(CompositeOutputDevice, self).value + + @value.setter + def value(self, value): + for device, v in zip(self.all, value): + if isinstance(device, OutputDevice): + device.value = v + # Simply ignore values for non-output devices + + +class LEDCollection(CompositeOutputDevice): + """ + Extends :class:`CompositeOutputDevice`. Abstract base class for + :class:`LEDBoard` and :class:`LEDBarGraph`. + """ + + def __init__(self, *args, **kwargs): + self._blink_thread = None + pwm = kwargs.pop('pwm', False) + active_high = kwargs.pop('active_high', True) + initial_value = kwargs.pop('initial_value', False) + order = kwargs.pop('_order', None) + LEDClass = PWMLED if pwm else LED + super(LEDCollection, self).__init__( + *(LEDClass(pin, active_high, initial_value) for pin in args), + _order=order, + **{name: LEDClass(pin, active_high, initial_value) for name, pin in kwargs.items()}) @property def leds(self): @@ -49,35 +97,12 @@ class LEDCollection(SourceMixin, CompositeDevice): A tuple of all the :class:`LED` or :class:`PWMLED` objects contained by the instance. """ - return self._leds - - def on(self): - """ - Turn all the LEDs on. - """ - for led in self.leds: - led.on() - - def off(self): - """ - Turn all the LEDs off. - """ - for led in self.leds: - led.off() - - def toggle(self): - """ - Toggle all the LEDs. For each LED, if it's on, turn it off; if it's - off, turn it on. - """ - for led in self.leds: - led.toggle() - + return self.all class LEDBoard(LEDCollection): """ - Extends :class:`CompositeDevice` and represents a generic LED board or + Extends :class:`LEDCollection` and represents a generic LED board or collection of LEDs. The following example turns on all the LEDs on a board containing 5 LEDs @@ -96,26 +121,23 @@ class LEDBoard(LEDCollection): If ``True``, construct :class:`PWMLED` instances for each pin. If ``False`` (the default), construct regular :class:`LED` instances. This parameter can only be specified as a keyword parameter. + + :param bool active_high: + If ``True`` (the default), the :meth:`on` method will set all the + associates pins to HIGH. If ``False``, the :meth:`on` method will set + all pins to LOW (the :meth:`off` method always does the opposite). + + :param bool initial_value: + If ``False`` (the default), all LEDs will be off initially. If + ``None``, each device will be left in whatever state the pin is found + in when configured for output (warning: this can be on). The ``True``, + the device will be switched on initially. """ def close(self): self._stop_blink() super(LEDBoard, self).close() - @property - def value(self): - """ - A tuple containing a value for each LED on the board. This property can - also be set to update the state of all LEDs on the board. - """ - return tuple(led.value for led in self._leds) - - @value.setter - def value(self, value): - self._stop_blink() - for l, v in zip(self.leds, value): - l.value = v - def on(self): self._stop_blink() super(LEDBoard, self).on() @@ -266,8 +288,12 @@ class LEDBarGraph(LEDCollection): def __init__(self, *pins, **kwargs): super(LEDBarGraph, self).__init__(*pins, pwm=False) - initial_value = kwargs.get('initial_value', 0) - self.value = initial_value + try: + initial_value = kwargs.pop('initial_value', 0) + self.value = initial_value + except: + self.close() + raise @property def value(self): @@ -336,6 +362,7 @@ class PiLiter(LEDBoard): .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ + def __init__(self, pwm=False): super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25, pwm=pwm) @@ -360,14 +387,12 @@ class PiLiterBarGraph(LEDBarGraph): .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ + def __init__(self, initial_value=0): super(PiLiterBarGraph, self).__init__( 4, 17, 27, 18, 22, 23, 24, 25, initial_value=initial_value) -TrafficLightTuple = namedtuple('TrafficLightTuple', ('red', 'amber', 'green')) - - class TrafficLights(LEDBoard): """ Extends :class:`LEDBoard` for devices containing red, amber, and green @@ -397,44 +422,12 @@ class TrafficLights(LEDBoard): """ def __init__(self, red=None, amber=None, green=None, pwm=False): if not all([red, amber, green]): - raise OutputDeviceError( + raise GPIOPinMissing( 'red, amber and green pins must be provided' ) - super(TrafficLights, self).__init__(red, amber, green, pwm=pwm) - - @property - def value(self): - """ - A 3-tuple containing values for the red, amber, and green LEDs. This - property can also be set to alter the state of the LEDs. - """ - return TrafficLightTuple(*super(TrafficLights, self).value) - - @value.setter - def value(self, value): - # Eurgh, this is horrid but necessary (see #90) - super(TrafficLights, self.__class__).value.fset(self, value) - - @property - def red(self): - """ - The :class:`LED` or :class:`PWMLED` object representing the red LED. - """ - return self.leds[0] - - @property - def amber(self): - """ - The :class:`LED` or :class:`PWMLED` object representing the red LED. - """ - return self.leds[1] - - @property - def green(self): - """ - The :class:`LED` or :class:`PWMLED` object representing the green LED. - """ - return self.leds[2] + super(TrafficLights, self).__init__( + red=red, amber=amber, green=green, pwm=pwm, + _order=('red', 'amber', 'green')) class PiTraffic(TrafficLights): @@ -454,15 +447,12 @@ class PiTraffic(TrafficLights): To use the PI-TRAFFIC board when attached to a non-standard set of pins, simply use the parent class, :class:`TrafficLights`. """ + def __init__(self): super(PiTraffic, self).__init__(9, 10, 11) -TrafficLightsBuzzerTuple = namedtuple('TrafficLightsBuzzerTuple', ( - 'red', 'amber', 'green', 'buzzer')) - - -class TrafficLightsBuzzer(SourceMixin, CompositeDevice): +class TrafficLightsBuzzer(CompositeOutputDevice): """ Extends :class:`CompositeDevice` and is a generic class for HATs with traffic lights, a button and a buzzer. @@ -477,146 +467,11 @@ class TrafficLightsBuzzer(SourceMixin, CompositeDevice): :param Button button: An instance of :class:`Button` representing the button on the HAT. """ + def __init__(self, lights, buzzer, button): - self._blink_thread = None - super(TrafficLightsBuzzer, self).__init__() - self.lights = lights - self.buzzer = buzzer - self.button = button - self._all = self.lights.leds + (self.buzzer,) - - def close(self): - self.lights.close() - self.buzzer.close() - self.button.close() - - @property - def closed(self): - return all(o.closed for o in self.all) - - @property - def all(self): - """ - A tuple containing objects for all the items on the board (several - :class:`LED` objects, a :class:`Buzzer`, and a :class:`Button`). - """ - return self._all - - @property - def value(self): - """ - Returns a named-tuple containing values representing the states of - the LEDs, and the buzzer. This property can also be set to a 4-tuple - to update the state of all the board's components. - """ - return TrafficLightsBuzzerTuple( - self.lights.red.value, - self.lights.amber.value, - self.lights.green.value, - self.buzzer.value, - ) - - @value.setter - def value(self, value): - for i, v in zip(self.all, value): - i.value = v - - def on(self): - """ - Turn all the board's components on. - """ - for thing in self.all: - thing.on() - - def off(self): - """ - Turn all the board's components off. - """ - for thing in self.all: - thing.off() - - def toggle(self): - """ - Toggle all the board's components. For each component, if it's on, turn - it off; if it's off, turn it on. - """ - for thing in self.all: - thing.toggle() - - def blink( - self, on_time=1, off_time=1, fade_in_time=0, fade_out_time=0, - n=None, background=True): - """ - Make all the LEDs turn on and off repeatedly. - - :param float on_time: - Number of seconds on. Defaults to 1 second. - - :param float off_time: - Number of seconds off. Defaults to 1 second. - - :param float fade_in_time: - Number of seconds to spend fading in. Defaults to 0. Must be 0 if - ``pwm`` was ``False`` when the class was constructed - (:exc:`ValueError` will be raised if not). - - :param float fade_out_time: - Number of seconds to spend fading out. Defaults to 0. Must be 0 if - ``pwm`` was ``False`` when the class was constructed - (:exc:`ValueError` will be raised if not). - - :param int n: - Number of times to blink; ``None`` (the default) means forever. - - :param bool background: - If ``True``, start a background thread to continue blinking and - return immediately. If ``False``, only return when the blink is - finished (warning: the default value of *n* will result in this - method never returning). - """ - if isinstance(self.lights.leds[0], LED): - if fade_in_time: - raise ValueError('fade_in_time must be 0 with non-PWM LEDs') - if fade_out_time: - raise ValueError('fade_out_time must be 0 with non-PWM LEDs') - self._stop_blink() - self._blink_thread = GPIOThread( - target=self._blink_device, - args=(on_time, off_time, fade_in_time, fade_out_time, n) - ) - self._blink_thread.start() - if not background: - self._blink_thread.join() - self._blink_thread = None - - def _stop_blink(self): - if self._blink_thread: - self._blink_thread.stop() - self._blink_thread = None - - def _blink_device(self, on_time, off_time, fade_in_time, fade_out_time, n, fps=50): - sequence = [] - if fade_in_time > 0: - sequence += [ - (i * (1 / fps) / fade_in_time, 1 / fps) - for i in range(int(fps * fade_in_time)) - ] - sequence.append((1, on_time)) - if fade_out_time > 0: - sequence += [ - (1 - (i * (1 / fps) / fade_out_time), 1 / fps) - for i in range(int(fps * fade_out_time)) - ] - sequence.append((0, off_time)) - sequence = ( - cycle(sequence) if n is None else - chain.from_iterable(repeat(sequence, n)) - ) - for value, delay in sequence: - for thing in self._all: - thing.value = value - if self._blink_thread.stopping.wait(delay): - break + super(TrafficLightsBuzzer, self).__init__( + lights=lights, buzzer=buzzer, button=button, + _order=('lights', 'buzzer', 'button')) class FishDish(TrafficLightsBuzzer): @@ -639,6 +494,7 @@ class FishDish(TrafficLightsBuzzer): LED. If ``False`` (the default), construct regular :class:`LED` instances. """ + def __init__(self, pwm=False): super(FishDish, self).__init__( TrafficLights(9, 22, 4, pwm=pwm), @@ -667,6 +523,7 @@ class TrafficHat(TrafficLightsBuzzer): LED. If ``False`` (the default), construct regular :class:`LED` instances. """ + def __init__(self, pwm=False): super(TrafficHat, self).__init__( TrafficLights(24, 23, 22, pwm=pwm), @@ -701,9 +558,10 @@ class Robot(SourceMixin, CompositeDevice): A tuple of two GPIO pins representing the forward and backward inputs of the right motor's controller. """ + def __init__(self, left=None, right=None): if not all([left, right]): - raise OutputDeviceError( + raise GPIOPinMissing( 'left and right motor pins must be provided' ) super(Robot, self).__init__() @@ -822,6 +680,7 @@ class RyanteckRobot(Robot): robot = RyanteckRobot() robot.left() """ + def __init__(self): super(RyanteckRobot, self).__init__(left=(17, 18), right=(22, 23)) @@ -841,5 +700,111 @@ class CamJamKitRobot(Robot): .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ + def __init__(self): super(CamJamKitRobot, self).__init__(left=(9, 10), right=(7, 8)) + + +class _EnergenieMaster(SharedMixin, CompositeOutputDevice): + def __init__(self): + self._lock = Lock() + super(_EnergenieMaster, self).__init__( + *(OutputDevice(pin) for pin in (17, 22, 23, 27)), + mode=OutputDevice(24), enable=OutputDevice(25)) + + def close(self): + if self._lock: + with self._lock: + super(_EnergenieMaster, self).close() + self._lock = None + + @classmethod + def _shared_key(cls): + # There's only one Energenie master + return None + + def transmit(self, socket, enable): + with self._lock: + try: + code = (8 * bool(enable)) + (7 - socket) + for bit in self.all[:4]: + bit.value = (code & 1) + code >>= 1 + sleep(0.1) + self.enable.on() + sleep(0.25) + finally: + self.enable.off() + + +class Energenie(SourceMixin, Device): + """ + Extends :class:`Device` to represent an `Energenie socket`_ controller. + + This class is constructed with a socket number and an optional initial + state (defaults to ``False``, meaning off). Instances of this class can + be used to switch peripherals on and off. For example:: + + from gpiozero import Energenie + + lamp = Energenie(0) + lamp.on() + + :param int socket: + Which socket this instance should control. This is an integer number + between 0 and 3. + + :param bool initial_value: + The initial state of the socket. As Energenie sockets provide no + means of reading their state, you must provide an initial state for + the socket, which will be set upon construction. This defaults to + ``False`` which will switch the socket off. + + .. _Energenie socket: https://energenie4u.co.uk/index.php/catalogue/product/ENER002-2PI + """ + + def __init__(self, socket=None, initial_value=False): + if socket is None: + raise EnergenieSocketMissing('socket number must be provided') + if not (0 <= socket < 4): + raise EnergenieBadSocket('socket number must be between 0 and 3') + super(Energenie, self).__init__() + self._socket = socket + self._master = _EnergenieMaster() + if initial_value: + self.on() + else: + self.off() + + def close(self): + if self._master: + m = self._master + self._master = None + m.close() + + @property + def closed(self): + return self._master is None + + def __repr__(self): + try: + self._check_open() + return "" % self._socket + except DeviceClosed: + return "" + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._master.transmit(self._socket, bool(value)) + self._value = bool(value) + + def on(self): + self.value = True + + def off(self): + self.value = False + diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 119c018..bab53f7 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -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 "" % (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 "" % 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 - diff --git a/gpiozero/exc.py b/gpiozero/exc.py index b601163..5fb5a7f 100644 --- a/gpiozero/exc.py +++ b/gpiozero/exc.py @@ -10,14 +10,32 @@ str = type('') class GPIOZeroError(Exception): "Base class for all exceptions in GPIO Zero" +class DeviceClosed(GPIOZeroError): + "Error raised when an operation is attempted on a closed device" + class CompositeDeviceError(GPIOZeroError): "Base class for errors specific to the CompositeDevice hierarchy" +class CompositeDeviceBadName(CompositeDeviceError, ValueError): + "Error raised when a composite device is constructed with a reserved name" + +class EnergenieSocketMissing(CompositeDeviceError, ValueError): + "Error raised when socket number is not specified" + +class EnergenieBadSocket(CompositeDeviceError, ValueError): + "Error raised when an invalid socket number is passed to :class:`Energenie`" + +class SPIError(GPIOZeroError): + "Base class for errors related to the SPI implementation" + +class SPIBadArgs(SPIError, ValueError): + "Error raised when invalid arguments are given while constructing :class:`SPIDevice`" + class GPIODeviceError(GPIOZeroError): "Base class for errors specific to the GPIODevice hierarchy" class GPIODeviceClosed(GPIODeviceError): - "Error raised when an operation is attempted on a closed device" + "Deprecated descendent of :exc:`DeviceClosed`" class GPIOPinInUse(GPIODeviceError): "Error raised when attempting to use a pin already in use by another device" @@ -82,3 +100,12 @@ class PinPWMUnsupported(PinPWMError, AttributeError): class PinPWMFixedValue(PinPWMError, AttributeError): "Error raised when attempting to initialize PWM on an input pin" +class GPIOZeroWarning(Warning): + "Base class for all warnings in GPIO Zero" + +class SPIWarning(GPIOZeroWarning): + "Base class for warnings related to the SPI implementation" + +class SPISoftwareFallback(SPIWarning): + "Warning raised when falling back to the software implementation" + diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 51e1e34..8c0aeee 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -13,10 +13,9 @@ from functools import wraps from time import sleep, time from threading import Event -from spidev import SpiDev - from .exc import InputDeviceError, GPIODeviceError, GPIODeviceClosed -from .devices import GPIODevice, CompositeDevice, GPIOQueue +from .devices import GPIODevice, CompositeDevice +from .threads import GPIOQueue class InputDevice(GPIODevice): @@ -77,8 +76,10 @@ class WaitableInputDevice(InputDevice): state (:meth:`when_activated` and :meth:`when_deactivated`). These are aliased appropriately in various subclasses. - Note that this class provides no means of actually firing its events; it's - effectively an abstract base class. + .. note:: + + Note that this class provides no means of actually firing its events; + it's effectively an abstract base class. """ def __init__(self, pin=None, pull_up=False): super(WaitableInputDevice, self).__init__(pin, pull_up) @@ -244,6 +245,13 @@ class SmoothedInputDevice(WaitableInputDevice): threshold which is used to determine the state of the :attr:`is_active` property. + .. note:: + + The background queue is not automatically started upon construction. + This is to allow descendents to set up additional components before the + queue starts reading values. Effectively this is an abstract base + class. + This class is intended for use with devices which either exhibit analog behaviour (such as the charging time of a capacitor with an LDR), or those which exhibit "twitchy" behaviour (such as certain motion sensors). @@ -760,350 +768,3 @@ DistanceSensor.wait_for_out_of_range = DistanceSensor.wait_for_active DistanceSensor.wait_for_in_range = DistanceSensor.wait_for_inactive -class AnalogInputDevice(CompositeDevice): - """ - Represents an analog input device connected to SPI (serial interface). - - Typical analog input devices are `analog to digital converters`_ (ADCs). - Several classes are provided for specific ADC chips, including - :class:`MCP3004`, :class:`MCP3008`, :class:`MCP3204`, and :class:`MCP3208`. - - The following code demonstrates reading the first channel of an MCP3008 - chip attached to the Pi's SPI pins:: - - from gpiozero import MCP3008 - - pot = MCP3008(0) - print(pot.value) - - The :attr:`value` attribute is normalized such that its value is always - between 0.0 and 1.0 (or in special cases, such as differential sampling, - -1 to +1). Hence, you can use an analog input to control the brightness of - a :class:`PWMLED` like so:: - - from gpiozero import MCP3008, PWMLED - - pot = MCP3008(0) - led = PWMLED(17) - led.source = pot.values - - .. _analog to digital converters: https://en.wikipedia.org/wiki/Analog-to-digital_converter - """ - - def __init__(self, device=0, bits=None): - if bits is None: - raise InputDeviceError('you must specify the bit resolution of the device') - if device not in (0, 1): - raise InputDeviceError('device must be 0 or 1') - self._device = device - self._bits = bits - self._spi = SpiDev() - self._spi.open(0, self.device) - super(AnalogInputDevice, self).__init__() - - def close(self): - """ - Shut down the device and release all associated resources. - """ - if self._spi: - s = self._spi - self._spi = None - s.close() - super(AnalogInputDevice, self).close() - - @property - def bits(self): - """ - The bit-resolution of the device/channel. - """ - return self._bits - - @property - def bus(self): - """ - The SPI bus that the device is connected to. As the Pi only has a - single (user accessible) SPI bus, this always returns 0. - """ - return 0 - - @property - def device(self): - """ - The select pin that the device is connected to. The Pi has two select - pins so this will be 0 or 1. - """ - return self._device - - def _read(self): - raise NotImplementedError - - @property - def value(self): - """ - The current value read from the device, scaled to a value between 0 and - 1. - """ - return self._read() / (2**self.bits - 1) - - @property - def raw_value(self): - """ - The raw value as read from the device. - """ - return self._read() - - -class MCP3xxx(AnalogInputDevice): - """ - Extends :class:`AnalogInputDevice` to implement an interface for all ADC - chips with a protocol similar to the Microchip MCP3xxx series of devices. - """ - - def __init__(self, channel=0, device=0, bits=10, differential=False): - self._channel = channel - self._bits = bits - self._differential = bool(differential) - super(MCP3xxx, self).__init__(device, bits) - - @property - def channel(self): - """ - The channel to read data from. The MCP3008/3208/3304 have 8 channels - (0-7), while the MCP3004/3204/3302 have 4 channels (0-3), and the - MCP3301 only has 1 channel. - """ - return self._channel - - @property - def differential(self): - """ - If ``True``, the device is operated in pseudo-differential mode. In - this mode one channel (specified by the channel attribute) is read - relative to the value of a second channel (implied by the chip's - design). - - Please refer to the device data-sheet to determine which channel is - used as the relative base value (for example, when using an - :class:`MCP3008` in differential mode, channel 0 is read relative to - channel 1). - """ - return self._differential - - def _read(self): - # MCP3008/04 or MCP3208/04 protocol looks like the following: - # - # Byte 0 1 2 - # ==== ======== ======== ======== - # Tx 0001MCCC xxxxxxxx xxxxxxxx - # Rx xxxxxxxx x0RRRRRR RRRRxxxx for the 3004/08 - # Rx xxxxxxxx x0RRRRRR RRRRRRxx for the 3204/08 - # - # The transmit bits start with 3 preamble bits "000" (to warm up), a - # start bit "1" followed by the single/differential bit (M) which is 1 - # for single-ended read, and 0 for differential read, followed by - # 3-bits for the channel (C). The remainder of the transmission are - # "don't care" bits (x). - # - # The first byte received and the top 1 bit of the second byte are - # don't care bits (x). These are followed by a null bit (0), and then - # the result bits (R). 10 bits for the MCP300x, 12 bits for the - # MCP320x. - # - # XXX Differential mode still requires testing - data = self._spi.xfer2([16 + [8, 0][self.differential] + self.channel, 0, 0]) - return ((data[1] & 63) << (self.bits - 6)) | (data[2] >> (14 - self.bits)) - - -class MCP33xx(MCP3xxx): - """ - Extends :class:`MCP3xxx` with functionality specific to the MCP33xx family - of ADCs; specifically this handles the full differential capability of - these chips supporting the full 13-bit signed range of output values. - """ - - def __init__(self, channel=0, device=0, differential=False): - super(MCP33xx, self).__init__(channel, device, 12, differential) - - def _read(self): - # MCP3304/02 protocol looks like the following: - # - # Byte 0 1 2 - # ==== ======== ======== ======== - # Tx 0001MCCC xxxxxxxx xxxxxxxx - # Rx xxxxxxxx x0SRRRRR RRRRRRRx - # - # The transmit bits start with 3 preamble bits "000" (to warm up), a - # start bit "1" followed by the single/differential bit (M) which is 1 - # for single-ended read, and 0 for differential read, followed by - # 3-bits for the channel (C). The remainder of the transmission are - # "don't care" bits (x). - # - # The first byte received and the top 1 bit of the second byte are - # don't care bits (x). These are followed by a null bit (0), then the - # sign bit (S), and then the 12 result bits (R). - # - # In single read mode (the default) the sign bit is always zero and the - # result is effectively 12-bits. In differential mode, the sign bit is - # significant and the result is a two's-complement 13-bit value. - # - # The MCP3301 variant of the chip always operates in differential - # mode and effectively only has one channel (composed of an IN+ and - # IN-). As such it requires no input, just output. This is the reason - # we split out _send() below; so that MCP3301 can override it. - data = self._spi.xfer2(self._send()) - # Extract the last two bytes (again, for MCP3301) - data = data[-2:] - result = ((data[0] & 63) << 7) | (data[1] >> 1) - # Account for the sign bit - if self.differential and value > 4095: - result = -(8192 - result) - assert -4096 <= result < 4096 - return result - - def _send(self): - return [16 + [8, 0][self.differential] + self.channel, 0, 0] - - -class MCP3001(MCP3xxx): - """ - The `MCP3001`_ is a 10-bit analog to digital converter with 1 channel - - .. _MCP3001: http://www.farnell.com/datasheets/630400.pdf - """ - def __init__(self, device=0): - super(MCP3001, self).__init__(0, device, 10, differential=True) - - -class MCP3002(MCP3xxx): - """ - The `MCP3002`_ is a 10-bit analog to digital converter with 2 channels - (0-3). - - .. _MCP3002: http://www.farnell.com/datasheets/1599363.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 2: - raise InputDeviceError('channel must be 0 or 1') - super(MCP3002, self).__init__(channel, device, 10, differential) - - -class MCP3004(MCP3xxx): - """ - The `MCP3004`_ is a 10-bit analog to digital converter with 4 channels - (0-3). - - .. _MCP3004: http://www.farnell.com/datasheets/808965.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 4: - raise InputDeviceError('channel must be between 0 and 3') - super(MCP3004, self).__init__(channel, device, 10, differential) - - -class MCP3008(MCP3xxx): - """ - The `MCP3008`_ is a 10-bit analog to digital converter with 8 channels - (0-7). - - .. _MCP3008: http://www.farnell.com/datasheets/808965.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 8: - raise InputDeviceError('channel must be between 0 and 7') - super(MCP3008, self).__init__(channel, device, 10, differential) - - -class MCP3201(MCP3xxx): - """ - The `MCP3201`_ is a 12-bit analog to digital converter with 1 channel - - .. _MCP3201: http://www.farnell.com/datasheets/1669366.pdf - """ - def __init__(self, device=0): - super(MCP3201, self).__init__(0, device, 12, differential=True) - - -class MCP3202(MCP3xxx): - """ - The `MCP3202`_ is a 12-bit analog to digital converter with 2 channels - (0-1). - - .. _MCP3202: http://www.farnell.com/datasheets/1669376.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 2: - raise InputDeviceError('channel must be 0 or 1') - super(MCP3202, self).__init__(channel, device, 12, differential) - - -class MCP3204(MCP3xxx): - """ - The `MCP3204`_ is a 12-bit analog to digital converter with 4 channels - (0-3). - - .. _MCP3204: http://www.farnell.com/datasheets/808967.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 4: - raise InputDeviceError('channel must be between 0 and 3') - super(MCP3204, self).__init__(channel, device, 12, differential) - - -class MCP3208(MCP3xxx): - """ - The `MCP3208`_ is a 12-bit analog to digital converter with 8 channels - (0-7). - - .. _MCP3208: http://www.farnell.com/datasheets/808967.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 8: - raise InputDeviceError('channel must be between 0 and 7') - super(MCP3208, self).__init__(channel, device, 12, differential) - - -class MCP3301(MCP33xx): - """ - The `MCP3301`_ is a signed 13-bit analog to digital converter. Please note - that the MCP3301 always operates in differential mode between its two - channels and the output value is scaled from -1 to +1. - - .. _MCP3301: http://www.farnell.com/datasheets/1669397.pdf - """ - def __init__(self, device=0): - super(MCP3301, self).__init__(0, device, differential=True) - - def _send(self): - return [0, 0] - - -class MCP3302(MCP33xx): - """ - The `MCP3302`_ is a 12/13-bit analog to digital converter with 4 channels - (0-3). When operated in differential mode, the device outputs a signed - 13-bit value which is scaled from -1 to +1. When operated in single-ended - mode (the default), the device outputs an unsigned 12-bit value scaled from - 0 to 1. - - .. _MCP3302: http://www.farnell.com/datasheets/1486116.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 4: - raise InputDeviceError('channel must be between 0 and 4') - super(MCP3302, self).__init__(channel, device, differential) - - -class MCP3304(MCP33xx): - """ - The `MCP3304`_ is a 12/13-bit analog to digital converter with 8 channels - (0-7). When operated in differential mode, the device outputs a signed - 13-bit value which is scaled from -1 to +1. When operated in single-ended - mode (the default), the device outputs an unsigned 12-bit value scaled from - 0 to 1. - - .. _MCP3304: http://www.farnell.com/datasheets/1486116.pdf - """ - def __init__(self, channel=0, device=0, differential=False): - if not 0 <= channel < 8: - raise InputDeviceError('channel must be between 0 and 7') - super(MCP3304, self).__init__(channel, device, differential) diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index 3c4a66e..584afff 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -10,8 +10,9 @@ from time import sleep from threading import Lock from itertools import repeat, cycle, chain -from .exc import OutputDeviceBadValue, GPIOPinMissing, GPIODeviceClosed -from .devices import GPIODevice, GPIOThread, CompositeDevice, SourceMixin +from .exc import OutputDeviceBadValue, GPIOPinMissing +from .devices import GPIODevice, CompositeDevice, SourceMixin +from .threads import GPIOThread class OutputDevice(SourceMixin, GPIODevice): @@ -38,10 +39,8 @@ class OutputDevice(SourceMixin, GPIODevice): device will be switched on initially. """ def __init__(self, pin=None, active_high=True, initial_value=False): - self._active_high = active_high super(OutputDevice, self).__init__(pin) - self._active_state = True if active_high else False - self._inactive_state = False if active_high else True + self.active_high = active_high if initial_value is None: self.pin.function = 'output' elif initial_value: @@ -72,6 +71,10 @@ class OutputDevice(SourceMixin, GPIODevice): @property def value(self): + """ + Returns ``True`` if the device is currently active and ``False`` + otherwise. Setting this property changes the state of the device. + """ return super(OutputDevice, self).value @value.setter @@ -80,7 +83,22 @@ class OutputDevice(SourceMixin, GPIODevice): @property def active_high(self): - return self._active_high + """ + When ``True``, the :attr:`value` property is ``True`` when the device's + :attr:`pin` is high. When ``False`` the :attr:`value` property is + ``True`` when the device's pin is low (i.e. the value is inverted). + + This property can be set after construction; be warned that changing it + will invert :attr:`value` (i.e. changing this property doesn't change + the device's pin state - it just changes how that state is + interpreted). + """ + return self._active_state + + @active_high.setter + def active_high(self, value): + self._active_state = True if value else False + self._inactive_state = False if value else True def __repr__(self): try: diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py index d5e3389..1ec0f95 100644 --- a/gpiozero/pins/__init__.py +++ b/gpiozero/pins/__init__.py @@ -16,6 +16,9 @@ from ..exc import ( PINS_CLEANUP = [] +def _pins_shutdown(): + for routine in PINS_CLEANUP: + routine() class Pin(object): diff --git a/gpiozero/spi.py b/gpiozero/spi.py new file mode 100644 index 0000000..4574204 --- /dev/null +++ b/gpiozero/spi.py @@ -0,0 +1,421 @@ +from __future__ import ( + unicode_literals, + print_function, + absolute_import, + division, + ) +str = type('') + + +import warnings +import operator +from threading import RLock + +try: + from spidev import SpiDev +except ImportError: + SpiDev = None + +from .devices import Device, SharedMixin, _PINS, _PINS_LOCK +from .input_devices import InputDevice +from .output_devices import OutputDevice +from .exc import SPIBadArgs, SPISoftwareFallback, GPIOPinInUse, DeviceClosed + + +class SPIHardwareInterface(Device): + def __init__(self, port, device): + self._device = None + super(SPIHardwareInterface, self).__init__() + # XXX How can we detect conflicts with existing GPIO instances? This + # isn't ideal ... in fact, it's downright crap and doesn't guard + # against conflicts created *after* this instance, but it's all I can + # come up with right now ... + conflicts = (11, 10, 9, (8, 7)[device]) + with _PINS_LOCK: + for pin in _PINS: + if pin.number in conflicts: + raise GPIOPinInUse( + 'pin %r is already in use by another gpiozero object' % pin + ) + self._device_num = device + self._device = SpiDev() + self._device.open(port, device) + self._device.max_speed_hz = 500000 + + def close(self): + if self._device: + try: + self._device.close() + finally: + self._device = None + super(SPIHardwareInterface, self).close() + + @property + def closed(self): + return self._device is None + + def __repr__(self): + try: + self._check_open() + return ( + "hardware SPI on clock_pin=11, mosi_pin=10, miso_pin=9, " + "select_pin=%d" % ( + 8 if self._device_num == 0 else 7)) + except DeviceClosed: + return "hardware SPI closed" + + def read(self, n): + return self.transfer((0,) * n) + + def write(self, data): + return len(self.transfer(data)) + + def transfer(self, data): + """ + Writes data (a list of integer words where each word is assumed to have + :attr:`bits_per_word` bits or less) to the SPI interface, and reads an + equivalent number of words, returning them as a list of integers. + """ + return self._device.xfer2(data) + + def _get_clock_mode(self): + return self._device.mode + + def _set_clock_mode(self, value): + self._device.mode = value + + def _get_clock_polarity(self): + return bool(self.mode & 2) + + def _set_clock_polarity(self, value): + self.mode = self.mode & (~2) | (bool(value) << 1) + + def _get_clock_phase(self): + return bool(self.mode & 1) + + def _set_clock_phase(self, value): + self.mode = self.mode & (~1) | bool(value) + + def _get_lsb_first(self): + return self._device.lsbfirst + + def _set_lsb_first(self, value): + self._device.lsbfirst = bool(value) + + def _get_select_high(self): + return self._device.cshigh + + def _set_select_high(self, value): + self._device.cshigh = bool(value) + + def _get_bits_per_word(self): + return self._device.bits_per_word + + def _set_bits_per_word(self, value): + self._device.bits_per_word = value + + clock_polarity = property(_get_clock_polarity, _set_clock_polarity) + clock_phase = property(_get_clock_phase, _set_clock_phase) + clock_mode = property(_get_clock_mode, _set_clock_mode) + lsb_first = property(_get_lsb_first, _set_lsb_first) + select_high = property(_get_select_high, _set_select_high) + bits_per_word = property(_get_bits_per_word, _set_bits_per_word) + + +class SPISoftwareBus(SharedMixin, Device): + def __init__(self, clock_pin, mosi_pin, miso_pin): + self.lock = None + self.clock = None + self.mosi = None + self.miso = None + super(SPISoftwareBus, self).__init__() + self.lock = RLock() + self.clock_phase = False + self.lsb_first = False + self.bits_per_word = 8 + try: + self.clock = OutputDevice(clock_pin, active_high=True) + if mosi_pin is not None: + self.mosi = OutputDevice(mosi_pin) + if miso_pin is not None: + self.miso = InputDevice(miso_pin) + except: + self.close() + raise + + def close(self): + super(SPISoftwareBus, self).close() + if self.lock: + with self.lock: + if self.miso is not None: + self.miso.close() + self.miso = None + if self.mosi is not None: + self.mosi.close() + self.mosi = None + if self.clock is not None: + self.clock.close() + self.clock = None + self.lock = None + + @property + def closed(self): + return self.lock is None + + @classmethod + def _shared_key(self, clock_pin, mosi_pin, miso_pin): + return (clock_pin, mosi_pin, miso_pin) + + def read(self, n): + return self.transfer((0,) * n) + + def write(self, data): + return len(self.transfer(data)) + + def transfer(self, data): + """ + Writes data (a list of integer words where each word is assumed to have + :attr:`bits_per_word` bits or less) to the SPI interface, and reads an + equivalent number of words, returning them as a list of integers. + """ + result = [] + with self.lock: + shift = operator.lshift if self.lsb_first else operator.rshift + for write_word in data: + mask = 1 if self.lsb_first else 1 << (self.bits_per_word - 1) + read_word = 0 + for bit in range(self.bits_per_word): + if self.mosi is not None: + self.mosi.value = bool(write_word & mask) + self.clock.on() + if self.miso is not None and not self.clock_phase: + if self.miso.value: + read_word |= mask + self.clock.off() + if self.miso is not None and self.clock_phase: + if self.miso.value: + read_word |= mask + mask = shift(mask, 1) + result.append(read_word) + return result + + +class SPISoftwareInterface(OutputDevice): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin): + self._bus = None + super(SPISoftwareInterface, self).__init__(select_pin, active_high=False) + try: + self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin) + except: + self.close() + raise + + def close(self): + if self._bus: + self._bus.close() + self._bus = None + super(SPISoftwareInterface, self).close() + + def __repr__(self): + try: + self._check_open() + return ( + "software SPI on clock_pin=%d, mosi_pin=%d, miso_pin=%d, " + "select_pin=%d" % ( + self._bus.clock.pin.number, + self._bus.mosi.pin.number, + self._bus.miso.pin.number, + self.pin.number)) + except DeviceClosed: + return "software SPI closed" + + def read(self, n): + return self._bus.read(n) + + def write(self, data): + return self._bus.write(data) + + def transfer(self, data): + with self._bus.lock: + self.on() + try: + return self._bus.transfer(data) + finally: + self.off() + + def _get_clock_mode(self): + return (self.clock_polarity << 1) | self.clock_phase + + def _set_clock_mode(self, value): + value = int(value) + if not 0 <= value <= 3: + raise ValueError('clock_mode must be a value between 0 and 3 inclusive') + with self._bus.lock: + self._bus.clock.active_high = not (value & 2) + self._bus.clock.off() + self._bus.clock_phase = bool(value & 1) + + def _get_clock_polarity(self): + return not self._bus.clock.active_high + + def _set_clock_polarity(self, value): + with self._bus.lock: + self._bus.clock.active_high = not value + + def _get_clock_phase(self): + return self._bus.clock_phase + + def _set_clock_phase(self, value): + with self._bus.lock: + self._bus.clock_phase = bool(value) + + def _get_lsb_first(self): + return self._bus.lsb_first + + def _set_lsb_first(self, value): + with self._bus.lock: + self._bus.lsb_first = bool(value) + + def _get_bits_per_word(self): + return self._bus.bits_per_word + + def _set_bits_per_word(self, value): + if value < 1: + raise ValueError('bits_per_word must be positive') + with self._bus.lock: + self._bus.bits_per_word = int(value) + + def _get_select_high(self): + return self.active_high + + def _set_select_high(self, value): + with self._bus.lock: + self.active_high = value + self.off() + + clock_polarity = property(_get_clock_polarity, _set_clock_polarity) + clock_phase = property(_get_clock_phase, _set_clock_phase) + clock_mode = property(_get_clock_mode, _set_clock_mode) + lsb_first = property(_get_lsb_first, _set_lsb_first) + bits_per_word = property(_get_bits_per_word, _set_bits_per_word) + select_high = property(_get_select_high, _set_select_high) + + +class SharedSPIHardwareInterface(SharedMixin, SPIHardwareInterface): + @classmethod + def _shared_key(cls, port, device): + return (port, device) + + +class SharedSPISoftwareInterface(SharedMixin, SPISoftwareInterface): + @classmethod + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin): + return (clock_pin, mosi_pin, miso_pin, select_pin) + + +def extract_spi_args(**kwargs): + """ + Given a set of keyword arguments, splits it into those relevant to SPI + implementations and all the rest. SPI arguments are augmented with defaults + and converted into the pin format (from the port/device format) if + necessary. + + Returns a tuple of ``(spi_args, other_args)``. + """ + pin_defaults = { + 'clock_pin': 11, + 'mosi_pin': 10, + 'miso_pin': 9, + 'select_pin': 8, + } + dev_defaults = { + 'port': 0, + 'device': 0, + } + spi_args = { + key: value for (key, value) in kwargs.items() + if key in pin_defaults or key in dev_defaults + } + kwargs = { + key: value for (key, value) in kwargs.items() + if key not in spi_args + } + if not spi_args: + spi_args = pin_defaults + elif set(spi_args) <= set(pin_defaults): + spi_args = { + key: spi_args.get(key, default) + for key, default in pin_defaults.items() + } + elif set(spi_args) <= set(dev_defaults): + spi_args = { + key: spi_args.get(key, default) + for key, default in dev_defaults.items() + } + if spi_args['port'] != 0: + raise SPIBadArgs('port 0 is the only valid SPI port') + if spi_args['device'] not in (0, 1): + raise SPIBadArgs('device must be 0 or 1') + spi_args = { + key: value if key != 'select_pin' else (8, 7)[spi_args['device']] + for key, value in pin_defaults.items() + } + else: + raise SPIBadArgs( + 'you must either specify port and device, or clock_pin, mosi_pin, ' + 'miso_pin, and select_pin; combinations of the two schemes (e.g. ' + 'port and clock_pin) are not permitted') + return spi_args, kwargs + + +def SPI(**spi_args): + """ + Returns an SPI interface, for the specified SPI *port* and *device*, or for + the specified pins (*clock_pin*, *mosi_pin*, *miso_pin*, and *select_pin*). + Only one of the schemes can be used; attempting to mix *port* and *device* + with pin numbers will raise :exc:`SPIBadArgs`. + + If the pins specified match the hardware SPI pins (clock on GPIO11, MOSI on + GPIO10, MISO on GPIO9, and chip select on GPIO8 or GPIO7), and the spidev + module can be imported, a :class:`SPIHardwareInterface` instance will be + returned. Otherwise, a :class:`SPISoftwareInterface` will be returned which + will use simple bit-banging to communicate. + + Both interfaces have the same API, support clock polarity and phase + attributes, and can handle half and full duplex communications, but the + hardware interface is significantly faster (though for many things this + doesn't matter). + + Finally, the *shared* keyword argument specifies whether the resulting + SPI interface can be repeatedly created and used by multiple devices + (useful with multi-channel devices like numerous ADCs). + """ + spi_args, kwargs = extract_spi_args(**spi_args) + shared = kwargs.pop('shared', False) + if kwargs: + raise SPIBadArgs( + 'unrecognized keyword argument %s' % kwargs.popitem()[0]) + if all(( + SpiDev is not None, + spi_args['clock_pin'] == 11, + spi_args['mosi_pin'] == 10, + spi_args['miso_pin'] == 9, + spi_args['select_pin'] in (7, 8), + )): + try: + if shared: + return SharedSPIHardwareInterface( + port=0, device={8: 0, 7: 1}[spi_args['select_pin']]) + else: + return SPIHardwareInterface( + port=0, device={8: 0, 7: 1}[spi_args['select_pin']]) + except Exception as e: + warnings.warn( + SPISoftwareFallback( + 'failed to initialize hardware SPI, falling back to ' + 'software (error was: %s)' % str(e))) + if shared: + return SharedSPISoftwareInterface(**spi_args) + else: + return SPISoftwareInterface(**spi_args) + diff --git a/gpiozero/spi_devices.py b/gpiozero/spi_devices.py new file mode 100644 index 0000000..8dad97c --- /dev/null +++ b/gpiozero/spi_devices.py @@ -0,0 +1,361 @@ +from __future__ import ( + unicode_literals, + print_function, + absolute_import, + division, + ) +str = type('') + + +from .exc import DeviceClosed +from .devices import Device +from .spi import extract_spi_args, SPI + + +class SPIDevice(Device): + """ + Extends :class:`Device`. Represents a device that communicates via the SPI + protocol. + + See :ref:`spi_args` for information on the keyword arguments that can be + specified with the constructor. + """ + def __init__(self, **spi_args): + self._spi = SPI(**spi_args) + + def close(self): + if self._spi: + s = self._spi + self._spi = None + s.close() + super(SPIDevice, self).close() + + @property + def closed(self): + return self._spi is None + + def __repr__(self): + try: + self._check_open() + return "" % (self.__class__.__name__, self._spi) + except DeviceClosed: + return "" % self.__class__.__name__ + + +class AnalogInputDevice(SPIDevice): + """ + Represents an analog input device connected to SPI (serial interface). + + Typical analog input devices are `analog to digital converters`_ (ADCs). + Several classes are provided for specific ADC chips, including + :class:`MCP3004`, :class:`MCP3008`, :class:`MCP3204`, and :class:`MCP3208`. + + The following code demonstrates reading the first channel of an MCP3008 + chip attached to the Pi's SPI pins:: + + from gpiozero import MCP3008 + + pot = MCP3008(0) + print(pot.value) + + The :attr:`value` attribute is normalized such that its value is always + between 0.0 and 1.0 (or in special cases, such as differential sampling, + -1 to +1). Hence, you can use an analog input to control the brightness of + a :class:`PWMLED` like so:: + + from gpiozero import MCP3008, PWMLED + + pot = MCP3008(0) + led = PWMLED(17) + led.source = pot.values + + .. _analog to digital converters: https://en.wikipedia.org/wiki/Analog-to-digital_converter + """ + + def __init__(self, bits=None, **spi_args): + if bits is None: + raise InputDeviceError('you must specify the bit resolution of the device') + self._bits = bits + super(AnalogInputDevice, self).__init__(shared=True, **spi_args) + + @property + def bits(self): + """ + The bit-resolution of the device/channel. + """ + return self._bits + + def _read(self): + raise NotImplementedError + + @property + def value(self): + """ + The current value read from the device, scaled to a value between 0 and + 1 (or -1 to +1 for devices operating in differential mode). + """ + return self._read() / (2**self.bits - 1) + + @property + def raw_value(self): + """ + The raw value as read from the device. + """ + return self._read() + + +class MCP3xxx(AnalogInputDevice): + """ + Extends :class:`AnalogInputDevice` to implement an interface for all ADC + chips with a protocol similar to the Microchip MCP3xxx series of devices. + """ + + def __init__(self, channel=0, bits=10, differential=False, **spi_args): + self._channel = channel + self._bits = bits + self._differential = bool(differential) + super(MCP3xxx, self).__init__(bits, **spi_args) + + @property + def channel(self): + """ + The channel to read data from. The MCP3008/3208/3304 have 8 channels + (0-7), while the MCP3004/3204/3302 have 4 channels (0-3), and the + MCP3301 only has 1 channel. + """ + return self._channel + + @property + def differential(self): + """ + If ``True``, the device is operated in pseudo-differential mode. In + this mode one channel (specified by the channel attribute) is read + relative to the value of a second channel (implied by the chip's + design). + + Please refer to the device data-sheet to determine which channel is + used as the relative base value (for example, when using an + :class:`MCP3008` in differential mode, channel 0 is read relative to + channel 1). + """ + return self._differential + + def _read(self): + # MCP3008/04 or MCP3208/04 protocol looks like the following: + # + # Byte 0 1 2 + # ==== ======== ======== ======== + # Tx 0001MCCC xxxxxxxx xxxxxxxx + # Rx xxxxxxxx x0RRRRRR RRRRxxxx for the 3004/08 + # Rx xxxxxxxx x0RRRRRR RRRRRRxx for the 3204/08 + # + # The transmit bits start with 3 preamble bits "000" (to warm up), a + # start bit "1" followed by the single/differential bit (M) which is 1 + # for single-ended read, and 0 for differential read, followed by + # 3-bits for the channel (C). The remainder of the transmission are + # "don't care" bits (x). + # + # The first byte received and the top 1 bit of the second byte are + # don't care bits (x). These are followed by a null bit (0), and then + # the result bits (R). 10 bits for the MCP300x, 12 bits for the + # MCP320x. + # + # XXX Differential mode still requires testing + data = self._spi.transfer([16 + [8, 0][self.differential] + self.channel, 0, 0]) + return ((data[1] & 63) << (self.bits - 6)) | (data[2] >> (14 - self.bits)) + + +class MCP33xx(MCP3xxx): + """ + Extends :class:`MCP3xxx` with functionality specific to the MCP33xx family + of ADCs; specifically this handles the full differential capability of + these chips supporting the full 13-bit signed range of output values. + """ + + def __init__(self, channel=0, differential=False, **spi_args): + super(MCP33xx, self).__init__(channel, 12, differential, **spi_args) + + def _read(self): + # MCP3304/02 protocol looks like the following: + # + # Byte 0 1 2 + # ==== ======== ======== ======== + # Tx 0001MCCC xxxxxxxx xxxxxxxx + # Rx xxxxxxxx x0SRRRRR RRRRRRRx + # + # The transmit bits start with 3 preamble bits "000" (to warm up), a + # start bit "1" followed by the single/differential bit (M) which is 1 + # for single-ended read, and 0 for differential read, followed by + # 3-bits for the channel (C). The remainder of the transmission are + # "don't care" bits (x). + # + # The first byte received and the top 1 bit of the second byte are + # don't care bits (x). These are followed by a null bit (0), then the + # sign bit (S), and then the 12 result bits (R). + # + # In single read mode (the default) the sign bit is always zero and the + # result is effectively 12-bits. In differential mode, the sign bit is + # significant and the result is a two's-complement 13-bit value. + # + # The MCP3301 variant of the chip always operates in differential + # mode and effectively only has one channel (composed of an IN+ and + # IN-). As such it requires no input, just output. This is the reason + # we split out _send() below; so that MCP3301 can override it. + data = self._spi.transfer(self._send()) + # Extract the last two bytes (again, for MCP3301) + data = data[-2:] + result = ((data[0] & 63) << 7) | (data[1] >> 1) + # Account for the sign bit + if self.differential and value > 4095: + result = -(8192 - result) + assert -4096 <= result < 4096 + return result + + def _send(self): + return [16 + [8, 0][self.differential] + self.channel, 0, 0] + + +class MCP3001(MCP3xxx): + """ + The `MCP3001`_ is a 10-bit analog to digital converter with 1 channel + + .. _MCP3001: http://www.farnell.com/datasheets/630400.pdf + """ + def __init__(self, **spi_args): + super(MCP3001, self).__init__(0, 10, differential=True, **spi_args) + + +class MCP3002(MCP3xxx): + """ + The `MCP3002`_ is a 10-bit analog to digital converter with 2 channels + (0-3). + + .. _MCP3002: http://www.farnell.com/datasheets/1599363.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 2: + raise InputDeviceError('channel must be 0 or 1') + super(MCP3002, self).__init__(channel, 10, differential, **spi_args) + + +class MCP3004(MCP3xxx): + """ + The `MCP3004`_ is a 10-bit analog to digital converter with 4 channels + (0-3). + + .. _MCP3004: http://www.farnell.com/datasheets/808965.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 4: + raise InputDeviceError('channel must be between 0 and 3') + super(MCP3004, self).__init__(channel, 10, differential, **spi_args) + + +class MCP3008(MCP3xxx): + """ + The `MCP3008`_ is a 10-bit analog to digital converter with 8 channels + (0-7). + + .. _MCP3008: http://www.farnell.com/datasheets/808965.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 8: + raise InputDeviceError('channel must be between 0 and 7') + super(MCP3008, self).__init__(channel, 10, differential, **spi_args) + + +class MCP3201(MCP3xxx): + """ + The `MCP3201`_ is a 12-bit analog to digital converter with 1 channel + + .. _MCP3201: http://www.farnell.com/datasheets/1669366.pdf + """ + def __init__(self, **spi_args): + super(MCP3201, self).__init__(0, 12, differential=True, **spi_args) + + +class MCP3202(MCP3xxx): + """ + The `MCP3202`_ is a 12-bit analog to digital converter with 2 channels + (0-1). + + .. _MCP3202: http://www.farnell.com/datasheets/1669376.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 2: + raise InputDeviceError('channel must be 0 or 1') + super(MCP3202, self).__init__(channel, 12, differential, **spi_args) + + +class MCP3204(MCP3xxx): + """ + The `MCP3204`_ is a 12-bit analog to digital converter with 4 channels + (0-3). + + .. _MCP3204: http://www.farnell.com/datasheets/808967.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 4: + raise InputDeviceError('channel must be between 0 and 3') + super(MCP3204, self).__init__(channel, 12, differential, **spi_args) + + +class MCP3208(MCP3xxx): + """ + The `MCP3208`_ is a 12-bit analog to digital converter with 8 channels + (0-7). + + .. _MCP3208: http://www.farnell.com/datasheets/808967.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 8: + raise InputDeviceError('channel must be between 0 and 7') + super(MCP3208, self).__init__(channel, 12, differential, **spi_args) + + +class MCP3301(MCP33xx): + """ + The `MCP3301`_ is a signed 13-bit analog to digital converter. Please note + that the MCP3301 always operates in differential mode between its two + channels and the output value is scaled from -1 to +1. + + .. _MCP3301: http://www.farnell.com/datasheets/1669397.pdf + """ + def __init__(self, **spi_args): + super(MCP3301, self).__init__(0, differential=True, **spi_args) + + def _send(self): + return [0, 0] + + +class MCP3302(MCP33xx): + """ + The `MCP3302`_ is a 12/13-bit analog to digital converter with 4 channels + (0-3). When operated in differential mode, the device outputs a signed + 13-bit value which is scaled from -1 to +1. When operated in single-ended + mode (the default), the device outputs an unsigned 12-bit value scaled from + 0 to 1. + + .. _MCP3302: http://www.farnell.com/datasheets/1486116.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 4: + raise InputDeviceError('channel must be between 0 and 4') + super(MCP3302, self).__init__(channel, differential, **spi_args) + + +class MCP3304(MCP33xx): + """ + The `MCP3304`_ is a 12/13-bit analog to digital converter with 8 channels + (0-7). When operated in differential mode, the device outputs a signed + 13-bit value which is scaled from -1 to +1. When operated in single-ended + mode (the default), the device outputs an unsigned 12-bit value scaled from + 0 to 1. + + .. _MCP3304: http://www.farnell.com/datasheets/1486116.pdf + """ + def __init__(self, channel=0, differential=False, **spi_args): + if not 0 <= channel < 8: + raise InputDeviceError('channel must be between 0 and 7') + super(MCP3304, self).__init__(channel, differential, **spi_args) + diff --git a/gpiozero/threads.py b/gpiozero/threads.py new file mode 100644 index 0000000..e52d237 --- /dev/null +++ b/gpiozero/threads.py @@ -0,0 +1,92 @@ +from __future__ import ( + unicode_literals, + print_function, + absolute_import, + division, + ) +str = type('') + +import weakref +from collections import deque +from threading import Thread, Event, RLock +try: + from statistics import median, mean +except ImportError: + from .compat import median, mean + +from .exc import ( + GPIOBadQueueLen, + GPIOBadSampleWait, + ) + + +_THREADS = set() +def _threads_shutdown(): + while _THREADS: + for t in _THREADS.copy(): + t.stop() + + +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 + diff --git a/setup.py b/setup.py index 21e2bc3..b4628de 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ __keywords__ = [ ] __requires__ = [ - 'spidev', ] __extra_requires__ = {