diff --git a/README.rst b/README.md similarity index 99% rename from README.rst rename to README.md index 1e0cc13..afcb8b7 100644 --- a/README.rst +++ b/README.md @@ -60,6 +60,7 @@ Contributors * `Serge Schneider`_ * `Dave Jones`_ * `Tyler Laws`_ +* `George Boukeas`_ Open Source =========== diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..8d6d2af --- /dev/null +++ b/debian/changelog @@ -0,0 +1,36 @@ +python-sense-hat (2.3.1~test0) UNRELEASED; urgency=medium + + * v2.3.1 alpha + + -- George Boukeas Wed, 21 Jul 2021 15:11:00 +0000 + +python-sense-hat (2.3.0~test0) UNRELEASED; urgency=medium + + * v2.3.0 alpha + + -- Serge Schneider Fri, 11 Dec 2020 14:41:32 +0000 + +python-sense-hat (2.2.0-1) jessie; urgency=medium + + * v2.2.0 release + + -- Serge Schneider Sun, 07 Aug 2016 14:32:16 +0100 + +python-sense-hat (2.1.0-1) jessie; urgency=low + + * v2.1.0 release + + -- Serge Schneider Tue, 25 Aug 2015 05:19:02 +0100 + +python-sense-hat (2.0.0-1) jessie; urgency=low + + * v2.0.0 release + * Rename to python-sense-hat + + -- Serge Schneider Fri, 21 Aug 2015 19:36:23 +0100 + +astropi (1.1.5-1) jessie; urgency=low + + * Initial release + + -- Serge Schneider Fri, 03 Jul 2015 03:24:45 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..33e0070 --- /dev/null +++ b/debian/control @@ -0,0 +1,32 @@ +Source: python-sense-hat +Section: python +Priority: optional +Maintainer: Serge Schneider +Build-Depends: debhelper (>= 9~), dh-python, python-all (>= 2.6.6-3~), + python-setuptools, python3-all, python3-setuptools +Standards-Version: 4.3.0 +Homepage: https://github.com/RPi-Distro/python-sense-hat +Vcs-Git: git://github.com/RPi-Distro/python-sense-hat.git -b debian +Vcs-Browser: https://github.com/RPi-Distro/python-sense-hat/tree/debian + +Package: python-sense-hat +Architecture: all +Depends: ${misc:Depends}, ${python:Depends}, python-rtimulib, python-pil, + python-numpy +Description: Sense HAT python library (Python 2) + Python module to control the Sense HAT for the Raspberry Pi used + in the Astro Pi mission - an education outreach programme for UK schools + sending code experiments to the International Space Station + . + This package installs the library for Python 2. + +Package: python3-sense-hat +Architecture: all +Depends: ${misc:Depends}, ${python3:Depends}, python3-rtimulib, python3-pil, + python3-numpy, python3-smbus +Description: Sense HAT python library (Python 3) + Python module to control the Sense HAT for the Raspberry Pi used + in the Astro Pi mission - an education outreach programme for UK schools + sending code experiments to the International Space Station + . + This package installs the library for Python 3. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..09392d9 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,32 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: python-sense-hat +Source: https://github.com/RPi-Distro/python-sense-hat + +Files: * +Copyright: 2015 Raspberry Pi Foundation +License: BSD-3-Clause + +License: BSD-3-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..016681d --- /dev/null +++ b/debian/rules @@ -0,0 +1,13 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#DH_VERBOSE = 1 + +DPKG_EXPORT_BUILDFLAGS = 1 +include /usr/share/dpkg/default.mk + + +export PYBUILD_NAME = sense-hat + +%: + dh $@ --with python2,python3 --buildsystem=pybuild diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/docs/api.md b/docs/api.md index 3b1bad2..ef6a9e0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -774,3 +774,131 @@ Note that the `direction_any` event is always called *after* all other events making it an ideal hook for things like display refreshing (as in the example above). +- - - +## Light and colour sensor + +The v2 Sense HAT includes a TCS34725 colour sensor that is capable of measuring the amount of Red, Green and Blue (RGB) in the incident light, as well as providing a Clear light (brightness) reading. + +You can interact with the colour sensor through the `colour` (or `color`) attribute of the Sense HAT, which corresponds to a `ColourSensor` object. + +The example below serves as an overview of how the colour sensor can be used, while the sections that follow provide additional details and explanations. + +```python +from sense_hat import SenseHat +from time import sleep + +sense = SenseHat() +sense.color.gain = 4 +sense.color.integration_cycles = 64 + +while True: + sleep(2 * sense.colour.integration_time) + red, green, blue, clear = sense.colour.colour # readings scaled to 0-256 + print(f"R: {red}, G: {green}, B: {blue}, C: {clear}") +``` + +--- +### Obtaining RGB and Clear light readings + +The `colour` (or `color`) property of the `ColourSensor` object is a 4-tuple containing the measured values for Red, Green and Blue (RGB), along with a Clear light value, which is a measure of brightness. Individual colour and light readings can also be obtained through the `red`, `green`, `blue` and `clear` properties of the `ColourSensor` object. + +`ColourSensor` property | Returned type | Explanation +--- | --- | --- +`red` | int | The amount of incident red light, scaled to 0-256 +`green` | int | The amount of incident green light, scaled to 0-256 +`blue` | int | The amount of incident blue light, scaled to 0-256 +`clear` | int | The amount of incident light (brightness), scaled to 0-256 +`colour` | tuple | A 4-tuple containing the RGBC (Red, Green, Blue and Clear) sensor readings, each scaled to 0-256 + +These are all read-only properties; they cannot be set. + +Note that, in the current implementation, the four values accessed through the `colour` property are retrieved through a single sensor reading. Obtaining these values through the `red`, `green`, `blue` and `clear` properties would require four separate readings. + +--- +### Gain + +In sensors, the term "gain" can be understood as being synonymous to _sensitivity_. A higher gain setting means the output values will be greater for the same input. + +There are four possible gain values for the colour sensor: `1`, `4`, `16` and `60`, with the default value being `1`. You can get or set the sensor gain through the `gain` property of the `ColourSensor` object. An attempt to set the gain to a value that is not valid will result in an `InvalidGainError` exception being raised. + +```python +from sense_hat import SenseHAT +from time import sleep + +sense = SenseHat() +sense.colour.gain = 1 +sleep(1) +print(f"Gain: {sense.colour.gain}") +print(f"RGBC: {sense.colour.colour}") + +sense.colour.gain = 16 +sleep(1) +print(f"Gain: {sense.colour.gain}") +print(f"RGBC: {sense.colour.colour}") +``` + +Under the same lighting conditions, the RGBC values should be considerably higher when the gain setting is increased. + +When there is very little ambient light and the RGBC values are low, it makes sense to use a higher gain setting. Conversely, when there is too much light and the RGBC values are maximal, the sensor is saturated and the gain should be set to lower values. + +--- +### Integration cycles and the interval between measurements + +You can specify the number of _integration cycles_ required to generate a new set of sensor readings. Each integration cycle is 2.4 milliseconds long, so the number of integration cycles determines the _minimum_ amount of time required between consecutive readings. + +You can set the number of integration cycles to any integer between `1` and `256`, through the `integration_cycles` property of the `ColourSensor` object. The default value is `1`. An attempt to set the number of integration cycles to a value that is not valid will result in a `InvalidIntegrationCyclesError` or `TypeError` exception being raised. + +```python +from sense_hat import SenseHAT +from time import sleep + +sense = SenseHat() +sense.colour.integration_cycles = 100 +print(f"Integration cycles: {sense.colour.integration_cycles}") +print(f"Minimum wait time between measurements: {sense.colour.integration_time} seconds") +``` + +--- +### Integration cycles and raw values + +The values of the `colour`, `red`, `green`, `blue` and `clear` properties are integers between 0 and 256. However, these are not the actual _raw_ values obtained from the sensor; they have been scaled down to this range for convenience. + +The range of the raw values depends on the number of integration cycles: + +`integration_cycles` | maximum raw value (`max_raw`) +--- | --- +1 - 64 | 1024 * `integration_cycles` +\> 64 | 65536 + +What this really means is that the _accuracy_ of the sensor is affected by the number of integration cycles, i.e. the time required by the sensor to obtain a reading. A longer integration time will result in more reliable readings that fall into a wider range of values, being able to more accurately distinguish between similar lighting conditions. + +The following properties of the `ColourSensor` object provide direct access to the raw values measured by the sensor. + +`ColourSensor` property | Returned type | Explanation +--- | --- | --- +`red_raw` | int | The amount of incident red light, between 0 and `max_raw` +`green_raw` | int | The amount of incident green light, between 0 and `max_raw` +`blue_raw` | int | The amount of incident blue light, between 0 and `max_raw` +`clear_raw` | int | The amount of incident light (brightness), between 0 and `max_raw` +`colour_raw` | tuple | A 4-tuple containing the RGBC (Red, Green, Blue and Clear) raw sensor readings, each between 0 and `max_raw` + +Here is an example comparing raw values to the corresponding scaled ones, for a given number of integration cycles. + +``` +from sense_hat import SenseHAT +from time import sleep + +sense = SenseHat() +sense.colour.integration_cycles = 64 +print(f"Minimum time between readings: {sense.colour.integration_time} seconds") +print(f"Maximum raw sensor reading: {sense.colour.max_raw}") +sleep(sense.colour.integration_time + 0.1) # try omitting this +print(f"Current raw sensor readings: {sense.colour.colour_raw}") +print(f"Scaled values: {sense.colour.colour}") +``` + +## Exceptions + +Custom Sense HAT exceptions are statically defined in the `sense_hat.exceptions` module. +The exceptions relate to problems encountered while initialising the colour chip or due to setting invalid parameters. +Each exception includes a message describing the issue encountered, and is subclassed from the base class `SenseHatException`. \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 71ee382..8813ae7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ ## v2 +### 2.3.x + +- Added support for the light/colour sensor in the v2 Sense HAT + ### 2.2.0 - Added new stick interface for the joystick diff --git a/examples/README.md b/docs/examples/README.md similarity index 100% rename from examples/README.md rename to docs/examples/README.md diff --git a/examples/colour_cycle.py b/docs/examples/colour_cycle.py similarity index 100% rename from examples/colour_cycle.py rename to docs/examples/colour_cycle.py diff --git a/examples/compass.py b/docs/examples/compass.py similarity index 100% rename from examples/compass.py rename to docs/examples/compass.py diff --git a/examples/evdev_joystick.py b/docs/examples/evdev_joystick.py similarity index 100% rename from examples/evdev_joystick.py rename to docs/examples/evdev_joystick.py diff --git a/examples/pygame_joystick.py b/docs/examples/pygame_joystick.py similarity index 100% rename from examples/pygame_joystick.py rename to docs/examples/pygame_joystick.py diff --git a/examples/rainbow.py b/docs/examples/rainbow.py similarity index 100% rename from examples/rainbow.py rename to docs/examples/rainbow.py diff --git a/examples/rotation.py b/docs/examples/rotation.py similarity index 100% rename from examples/rotation.py rename to docs/examples/rotation.py diff --git a/examples/space_invader.png b/docs/examples/space_invader.png similarity index 100% rename from examples/space_invader.png rename to docs/examples/space_invader.png diff --git a/examples/space_invader.py b/docs/examples/space_invader.py similarity index 100% rename from examples/space_invader.py rename to docs/examples/space_invader.py diff --git a/examples/text_scroll.py b/docs/examples/text_scroll.py similarity index 100% rename from examples/text_scroll.py rename to docs/examples/text_scroll.py diff --git a/docs/index.md b/docs/index.md index 1a71daa..6c16084 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ The Sense HAT features an 8x8 RGB LED matrix, a mini joystick and the following - Temperature - Humidity - Barometric pressure +- Light and colour ## Install @@ -35,10 +36,10 @@ sense = SenseHat() sense.show_message("Hello world!") ``` -See the [API reference](api.md) for full documentation of the library's functions. See [examples](https://github.com/RPi-Distro/python-sense-hat/blob/master/examples/README.md). +See the [API reference](api.md) for full documentation of the library's functions. See [examples](examples/README.md). ## Development -This library is maintained by the Raspberry Pi Foundation on GitHub at [github.com/RPi-Distro/python-sense-hat](https://github.com/RPi-Distro/python-sense-hat) +This library is maintained by the Raspberry Pi Foundation on GitHub at [github.com/astro-pi/python-sense-hat](https://github.com/astro-pi/python-sense-hat) See the [changelog](changelog.md). diff --git a/mkdocs.yml b/mkdocs.yml index e2fb0b6..22eb7e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,8 +5,9 @@ repo_url: https://github.com/RPi-Distro/python-sense-hat site_description: Python module to control the Raspberry Pi Sense HAT used in the Astro Pi mission site_author: David Honess site_dir: pythonhosted -google_analytics: ['UA-46270871-5', 'pythonhosted.org/sense-hat'] -pages: +#google_analytics: ['UA-46270871-5', 'pythonhosted.org/sense-hat'] +nav: - 'Home': 'index.md' - 'API Reference': 'api.md' +- 'Examples': 'examples/README.md' - 'Changelog': 'changelog.md' diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index 631f9b8..2143620 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -13,4 +13,4 @@ ACTION_HELD, ) -__version__ = '2.2.0' +__version__ = '2.3.1' diff --git a/sense_hat/colour.py b/sense_hat/colour.py new file mode 100644 index 0000000..610905a --- /dev/null +++ b/sense_hat/colour.py @@ -0,0 +1,358 @@ +""" +Python library for the TCS34725 Color Sensor +Documentation (including datasheet): https://ams.com/tcs34725#tab/documents +""" + +from time import sleep +from .exceptions import ColourSensorInitialisationError, InvalidGainError, \ + InvalidIntegrationCyclesError + + +class HardwareInterface: + """ + `HardwareInterface` is the abstract class that sits between the + `ColourSensor` class (providing the TCS34725 sensor API) and the + actual hardware. Using this intermediate layer of abstraction, a + `ColourSensor` object interacts with the hardware without being + aware of how this interaction is implemented. + + Different subclasses of the `HardwareInterface` class can provide + access to the hardware through e.g. I2C, `libiio` and its system + files or even a hardware emulator. + """ + + GAIN_VALUES = (1, 4, 16, 60) + CLOCK_STEP = 0.0024 # the clock step is 2.4ms + + @staticmethod + def max_value(integration_cycles): + """ + The maximum raw value for the RBGC channels depends on the number + of integration cycles. + """ + return 2**16 if integration_cycles >= 64 else 1024*integration_cycles + + def get_enabled(self): + """ + Return True if the sensor is enabled and False otherwise + """ + raise NotImplementedError + + def set_enabled(self, status): + """ + Enable or disable the sensor, depending on the boolean `status` flag + """ + raise NotImplementedError + + def get_gain(self): + """ + Return the current value of the sensor gain. + See GAIN_VALUES for the set of possible values. + """ + raise NotImplementedError + + def set_gain(self, gain): + """ + Set the value for the sensor `gain`. + See GAIN_VALUES for the set of possible values. + """ + raise NotImplementedError + + def get_integration_cycles(self): + """ + Return the current number of integration_cycles (1-256). + It takes `integration_cycles` * CLOCK_STEP to obtain a new + sensor reading. + """ + raise NotImplementedError + + def set_integration_cycles(self, integration_cycles): + """ + Set the current number of integration_cycles (1-256). + It takes `integration_cycles` * CLOCK_STEP to obtain a new + sensor reading. + """ + raise NotImplementedError + + def get_raw(self): + """ + Return a tuple containing the raw values of the RGBC channels. + The maximum for these raw values depends on the number of + integration cycles and can be computed using `max_value`. + """ + raise NotImplementedError + + def get_red(self): + """ + Return the raw value of the R (red) channel. + The maximum for this raw value depends on the number of + integration cycles and can be computed using `max_value`. + """ + raise NotImplementedError + + def get_green(self): + """ + Return the raw value of the G (green) channel. + The maximum for this raw value depends on the number of + integration cycles and can be computed using `max_value`. + """ + raise NotImplementedError + + def get_blue(self): + """ + Return the raw value of the B (blue) channel. + The maximum for this raw value depends on the number of + integration cycles and can be computed using `max_value`. + """ + raise NotImplementedError + + def get_clear(self): + """ + Return the raw value of the C (clear light) channel. + The maximum for this raw value depends on the number of + integration cycles and can be computed using `max_value`. + """ + raise NotImplementedError + + +### An I2C implementation of the abstract colour sensor `HardwareInterface` + +def _raw_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`. + The RGBC readings are all retrieved from the sensor in an identical + fashion. This is a factory function that implements this retrieval method. + """ + def get_raw_register(self): + return self._read(register) + return get_raw_register + +class I2C(HardwareInterface): + """ + An implementation of the `HardwareInterface` for the TCS34725 sensor + that uses I2C to control the sensor and retrieve measurements. + + Use the datasheet as a reference: https://ams.com/tcs34725#tab/documents + """ + + # device-specific constants + BUS = 1 + ADDR = 0x29 + + COMMAND_BIT = 0x80 + + # control registers + ENABLE = 0x00 | COMMAND_BIT + ATIME = 0x01 | COMMAND_BIT + CONTROL = 0x0F | COMMAND_BIT + ID = 0x12 | COMMAND_BIT + STATUS = 0x13 | COMMAND_BIT + # (if a register is described in the datasheet but missing here + # it means the corresponding functionality is not provided) + + # data registers + CDATA = 0x14 | COMMAND_BIT + RDATA = 0x16 | COMMAND_BIT + GDATA = 0x18 | COMMAND_BIT + BDATA = 0x1A | COMMAND_BIT + + # bit positions + OFF = 0x00 + PON = 0x01 + AEN = 0x02 + ON = (PON | AEN) + AVALID = 0x01 + + GAIN_REG_VALUES = (0x00, 0x01, 0x02, 0x03) + # map gain values to register values and vice-versa + GAIN_TO_REG = dict(zip(HardwareInterface.GAIN_VALUES, GAIN_REG_VALUES)) + REG_TO_GAIN = dict(zip(GAIN_REG_VALUES, HardwareInterface.GAIN_VALUES)) + + def __init__(self): + + import smbus + import glob + + try: + self.bus = smbus.SMBus(self.BUS) + except Exception as e: + explanation = "(I2C is not enabled)" if not self.i2c_enabled() else "" + raise ColourSensorInitialisationError(explanation=explanation) from e + try: + id = self._read(self.ID) + except Exception as e: + explanation = "(sensor not present)" + raise ColourSensorInitialisationError(explanation=explanation) from e + if id != 0x44: + explanation = f" (different device id detected: {id})" + raise ColourSensorInitialisationError(explanation=explanation) from e + @staticmethod + def i2c_enabled(): + """Returns True if I2C is enabled or False otherwise.""" + return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None + + def _read(self, attribute): + """ + Read and return the value of a specific register (`attribute`) of the + TCS34725 colour sensor. + """ + return self.bus.read_byte_data(self.ADDR, attribute) + + def _write(self, attribute, value): + """ + Write a value in a specific register (`attribute`) of the + TCS34725 colour sensor. + """ + self.bus.write_byte_data(self.ADDR, attribute, value) + + def get_enabled(self): + """ + Return True if the sensor is enabled and False otherwise + """ + return self._read(self.ENABLE) == (PON | AEN) + + def set_enabled(self, status): + """ + Enable or disable the sensor, depending on the boolean `status` flag + """ + if status: + self._write(self.ENABLE, self.PON) + sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." + self._write(self.ENABLE, self.ON) + else: + self._write(self.ENABLE, self.OFF) + sleep(self.CLOCK_STEP) + + def get_gain(self): + """ + Return the current value of the sensor gain. + See GAIN_VALUES for the set of possible values. + """ + register_value = self._read(self.CONTROL) + # map the register value to an actual gain value + return self.REG_TO_GAIN[register_value] + + def set_gain(self, gain): + """ + Set the value for the sensor `gain`. + See GAIN_VALUES for the set of possible values. + """ + # map the specified value for `gain` to a register value + register_value = self.GAIN_TO_REG[gain] + self._write(self.CONTROL, register_value) + + def get_integration_cycles(self): + """ + Return the current number of integration_cycles (1-256). + It takes `integration_cycles` * CLOCK_STEP to obtain a new + sensor reading. + """ + return 256 - self._read(self.ATIME) + + def set_integration_cycles(self, integration_cycles): + """ + Set the current number of integration_cycles (1-256). + It takes `integration_cycles` * CLOCK_STEP to obtain a new + sensor reading. + """ + self._write(self.ATIME, 256-integration_cycles) + + def get_raw(self): + """ + Return a tuple containing the raw values of the RGBC channels. + The maximum for these raw values depends on the number of + integration cycles and can be computed using `max_value`. + """ + # The 4-tuple is retrieved using a *single read*. + block = self.bus.read_i2c_block_data(self.ADDR, self.CDATA, 8) + return ( + (block[3] << 8) + block[2], + (block[5] << 8) + block[4], + (block[7] << 8) + block[6], + (block[1] << 8) + block[0] + ) + + """ + The methods below return the raw value of the R, G, B or Clear channels. + The maximum for these raw value depends on the number of integration + cycles and can be computed using `max_value`. + Use these methods if you only make use of one channel reading per iteration. + Otherwise, you are probably better off using `get_raw`, to retrieve all + channels in a single read. + """ + get_red = _raw_wrapper(RDATA) + get_green = _raw_wrapper(GDATA) + get_blue = _raw_wrapper(BDATA) + get_clear = _raw_wrapper(CDATA) + + +class ColourSensor: + + def __init__(self, gain=1, integration_cycles=1, interface=I2C): + self.interface = interface() + self.gain = gain + self.integration_cycles = integration_cycles + self.enabled = 1 + + @property + def enabled(self): + return self.interface.get_enabled() + + @enabled.setter + def enabled(self, status): + self.interface.set_enabled(status) + + @property + def gain(self): + return self.interface.get_gain() + + @gain.setter + def gain(self, gain): + if gain in self.interface.GAIN_VALUES: + self.interface.set_gain(gain) + else: + raise InvalidGainError(gain=gain, values=self.interface.GAIN_VALUES) + + @property + def integration_cycles(self): + return self.interface.get_integration_cycles() + + @integration_cycles.setter + def integration_cycles(self, integration_cycles): + if 1 <= integration_cycles <= 256: + self.interface.set_integration_cycles(integration_cycles) + sleep(self.interface.CLOCK_STEP) + else: + raise InvalidIntegrationCyclesError(integration_cycles=integration_cycles) + + @property + def integration_time(self): + return self.integration_cycles * self.interface.CLOCK_STEP + + @property + def max_raw(self): + return self.interface.max_value(self.integration_cycles) + + @property + def colour_raw(self): + return self.interface.get_raw() + + color_raw = colour_raw + red_raw = property(lambda self: self.interface.get_red()) + green_raw = property(lambda self: self.interface.get_green()) + blue_raw = property(lambda self: self.interface.get_blue()) + clear_raw = property(lambda self: self.interface.get_clear()) + + @property + def _scaling(self): + return self.max_raw // 256 + + @property + def colour(self): + return tuple(reading // self._scaling for reading in self.colour_raw) + + color = colour + red = property(lambda self: self.red_raw // self._scaling ) + green = property(lambda self: self.green_raw // self._scaling ) + blue = property(lambda self: self.blue_raw // self._scaling ) + clear = property(lambda self: self.clear_raw // self._scaling ) diff --git a/sense_hat/exceptions.py b/sense_hat/exceptions.py new file mode 100644 index 0000000..d6f3d18 --- /dev/null +++ b/sense_hat/exceptions.py @@ -0,0 +1,22 @@ +class SenseHatException(Exception): + """ + The base exception class for all SenseHat exceptions. + """ + fmt = 'An unspecified error occurred' + + def __init__(self, **kwargs): + msg = self.fmt.format(**kwargs) + Exception.__init__(self, msg) + self.kwargs = kwargs + + +class ColourSensorInitialisationError(SenseHatException): + fmt = "Failed to initialise TCS34725 colour sensor. {explanation}" + + +class InvalidGainError(SenseHatException): + fmt = "Cannot set gain to '{gain}'. Values: {values}" + + +class InvalidIntegrationCyclesError(SenseHatException): + fmt = "Cannot set integration cycles to {integration_cycles} (1-256)" diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 30597b9..9c5cb83 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -1,4 +1,5 @@ #!/usr/bin/python +import logging import struct import os import sys @@ -15,7 +16,8 @@ from copy import deepcopy from .stick import SenseStick - +from .colour import ColourSensor +from .exceptions import ColourSensorInitialisationError class SenseHat(object): @@ -91,6 +93,13 @@ def __init__( self._accel_enabled = False self._stick = SenseStick() + # initialise the TCS34725 colour sensor (if possible) + try: + self._colour = ColourSensor() + except Exception as e: + logging.warning(e) + pass + #### # Text assets #### @@ -191,6 +200,29 @@ def _get_fb_device(self): def stick(self): return self._stick + #### + # Colour sensor + #### + + @property + def colour(self): + try: + return self._colour + except AttributeError as e: + raise ColourSensorInitialisationError( + explanation="This Sense HAT" + + " does not have a color sensor") from e + + color = colour + + def has_colour_sensor(self): + try: + self._colour + except: + return False + else: + return True + #### # LED Matrix #### @@ -504,7 +536,7 @@ def gamma(self): @gamma.setter def gamma(self, buffer): - if len(buffer) is not 32: + if len(buffer) != 32: raise ValueError('Gamma array must be of length 32') if not all(b <= 31 for b in buffer): diff --git a/setup.py b/setup.py index a9e94a3..0ad5b9c 100644 --- a/setup.py +++ b/setup.py @@ -7,18 +7,18 @@ def read(fname): setup( name="sense-hat", - version="2.2.0", + version="2.3.1", author="Dave Honess", author_email="dave@raspberrypi.org", - description="Python module to control the Raspberry Pi Sense HAT used in the Astro Pi mission", - long_description=read('README.rst'), + description="Python module to control the Raspberry Pi Sense HAT, originally used in the Astro Pi mission", + long_description=read('README.md'), license="BSD", keywords=[ "sense hat", "raspberrypi", "astro pi", ], - url="https://github.com/RPi-Distro/python-sense-hat", + url="https://github.com/astro-pi/python-sense-hat", packages=find_packages(), package_data={ "txt": ['sense_hat_text.txt'],