diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..6fd8be7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,14 @@ +version: 2 + +build: + os: "ubuntu-22.04" + tools: + python: "3.9" + +mkdocs: + configuration: mkdocs.yml + fail_on_warning: false + +python: + install: + - requirements: docs/requirements.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 498eafd..5a53a6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Issues -Please report bugs and other issues as [GitHub issues](https://github.com/RPi-Distro/python-sense-hat/issues) ensuring to give as much detail about your problem as possible. +Please report bugs and other issues as [GitHub issues](https://github.com/astro-pi/python-sense-hat/issues) ensuring to give as much detail about your problem as possible. ## Pull Requests diff --git a/README.md b/README.md new file mode 100644 index 0000000..71d101b --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Sense HAT + +Python module to control the `Raspberry Pi` Sense HAT used in the `Astro Pi` mission - an education outreach programme for UK schools sending code experiments to the International Space Station. + +Hardware +======== + +The Sense HAT features an 8x8 RGB LED matrix, a mini joystick and the following sensors: + +* Gyroscope +* Accelerometer +* Magnetometer +* Temperature +* Humidity +* Barometric pressure + +Buy +=== + +Buy the Sense HAT from: + +* `The Pi Hut` +* `Pimoroni` +* `Amazon (UK)` +* `element14` +* `adafruit` +* `Amazon (USA)` + + +Installation +============ + +To install the Sense HAT software, enter the following commands in a terminal:: + + sudo apt-get update + sudo apt-get install sense-hat + sudo reboot + +Usage +===== + +Import the sense_hat module and instantiate a SenseHat object:: + + from sense_hat import SenseHat + + sense = SenseHat() + +Documentation +============= + +Comprehensive documentation is available at `https://sense-hat.readthedocs.io/en/latest/`. + +Contributors +============ + +* `Dave Honess` +* `Ben Nuttall` +* `Serge Schneider` +* `Dave Jones` +* `Tyler Laws` +* `George Boukeas` + +Open Source +=========== + +* The code is licensed under the `BSD Licence` +* The project source code is hosted on `GitHub` +* Please use `GitHub issues` to submit bugs and report issues + +URLs +===== + +* Raspberry Pi: https://www.raspberrypi.org/ +* Astro Pi: http://www.astro-pi.org/ +* sense-hat.readthedocs.io: https://sense-hat.readthedocs.io/en/latest/ +* Dave Honess: https://github.com/davidhoness +* Ben Nuttall: https://github.com/bennuttall +* Serge Schneider: https://github.com/XECDesign +* Dave Jones: https://github.com/waveform80 +* Tyler Laws: https://github.com/tyler-laws +* George Boukeas: https://github.com/boukeas +* BSD Licence: http://opensource.org/licenses/BSD-3-Clause +* GitHub: https://github.com/astro-pi/python-sense-hat +* GitHub Issues: https://github.com/astro-pi/python-sense-hat/issues +* `The Pi Hut`: http://thepihut.com/products/raspberry-pi-sense-hat-astro-pi +* `Pimoroni`: https://shop.pimoroni.com/products/raspberry-pi-sense-hat +* `Amazon (UK)`: http://www.amazon.co.uk/Raspberry-Pi-2483095-Sense-HAT/dp/B014T2IHQ8/ +* element14: https://www.element14.com/community/docs/DOC-78155/l/raspberry-pi-sense-hat +* adafruit: https://www.adafruit.com/products/2738 +* Amazon (USA): http://www.amazon.com/Raspberry-Pi-Sense-HAT-AstroPi/dp/B014HDG74S diff --git a/README.rst b/README.rst deleted file mode 100644 index 1e0cc13..0000000 --- a/README.rst +++ /dev/null @@ -1,87 +0,0 @@ -========= -Sense HAT -========= - -Python module to control the `Raspberry Pi`_ Sense HAT used in the `Astro Pi`_ mission - an education outreach programme for UK schools sending code experiments to the International Space Station. - -Hardware -======== - -The Sense HAT features an 8x8 RGB LED matrix, a mini joystick and the following sensors: - -* Gyroscope -* Accelerometer -* Magnetometer -* Temperature -* Humidity -* Barometric pressure - -Buy -=== - -Buy the Sense HAT from: - -* `The Pi Hut`_ -* `Pimoroni`_ -* `Amazon (UK)`_ -* `element14`_ -* `adafruit`_ -* `Amazon (USA)`_ - - -Installation -============ - -To install the Sense HAT software, enter the following commands in a terminal:: - - sudo apt-get update - sudo apt-get install sense-hat - sudo reboot - -Usage -===== - -Import the sense_hat module and instantiate a SenseHat object:: - - from sense_hat import SenseHat - - sense = SenseHat() - -Documentation -============= - -Comprehensive documentation is available at `pythonhosted.org/sense-hat`_ - -Contributors -============ - -* `Dave Honess`_ -* `Ben Nuttall`_ -* `Serge Schneider`_ -* `Dave Jones`_ -* `Tyler Laws`_ - -Open Source -=========== - -* The code is licensed under the `BSD Licence`_ -* The project source code is hosted on `GitHub`_ -* Please use `GitHub issues`_ to submit bugs and report issues - -.. _Raspberry Pi: https://www.raspberrypi.org/ -.. _Astro Pi: http://www.astro-pi.org/ -.. _pythonhosted.org/sense-hat: http://pythonhosted.org/sense-hat/ -.. _Dave Honess: https://github.com/davidhoness -.. _Ben Nuttall: https://github.com/bennuttall -.. _Serge Schneider: https://github.com/XECDesign -.. _Dave Jones: https://github.com/waveform80 -.. _Tyler Laws: https://github.com/tyler-laws -.. _BSD Licence: http://opensource.org/licenses/BSD-3-Clause -.. _GitHub: https://github.com/RPi-Distro/python-sense-hat -.. _GitHub Issues: https://github.com/RPi-Distro/python-sense-hat/issues -.. _`The Pi Hut`: http://thepihut.com/products/raspberry-pi-sense-hat-astro-pi -.. _`Pimoroni`: https://shop.pimoroni.com/products/raspberry-pi-sense-hat -.. _`Amazon (UK)`: http://www.amazon.co.uk/Raspberry-Pi-2483095-Sense-HAT/dp/B014T2IHQ8/ -.. _element14: https://www.element14.com/community/docs/DOC-78155/l/raspberry-pi-sense-hat -.. _adafruit: https://www.adafruit.com/products/2738 -.. _Amazon (USA): http://www.amazon.com/Raspberry-Pi-Sense-HAT-AstroPi/dp/B014HDG74S diff --git a/docs/api.md b/docs/api.md index 3b1bad2..c2d2ea1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -774,3 +774,133 @@ 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` +`rgb` | tuple | A 3-tuple containing the RGB raw sensor readings, each between 0 and `max_raw`. +`brightness` | int | An alias to the `clear_raw` property - the amount of incident light, 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`. diff --git a/docs/changelog.md b/docs/changelog.md index 71ee382..6f2d06c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,14 @@ ## v2 +### 2.4.0 +- Added `rgb` method to allow for easy reuse of sense hat colour sensor values +- Added `brightness` method alias for the sense hat colour sensor + +### 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..e21b39b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Sense HAT -Python module to control the [Raspberry Pi Sense HAT](https://www.raspberrypi.org/products/sense-hat/) +Python module to control the [Raspberry Pi Sense HAT](https://www.raspberrypi.com/products/sense-hat/) ## Features @@ -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/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..e517ad5 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +mkdocs==1.4.2 diff --git a/mkdocs.yml b/mkdocs.yml index e2fb0b6..1739d93 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,12 +1,13 @@ site_name: Sense HAT theme: readthedocs -site_url: https://pythonhosted.org/sense-hat/ -repo_url: https://github.com/RPi-Distro/python-sense-hat +site_url: https://sense-hat.readthedocs.io/en/latest/ +repo_url: https://github.com/astro-pi/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: +site_author: Raspberry Pi Foundation +site_dir: readthedocs +#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..d8aa831 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -13,4 +13,4 @@ ACTION_HELD, ) -__version__ = '2.2.0' +__version__ = '2.6.1' diff --git a/sense_hat/colour.py b/sense_hat/colour.py new file mode 100644 index 0000000..430b31f --- /dev/null +++ b/sense_hat/colour.py @@ -0,0 +1,393 @@ +""" +Python library for the TCS3472x and TCS340x Color Sensors +Documentation (including datasheet): https://ams.com/tcs34725#tab/documents + https://ams.com/tcs3400#tab/documents +The sense hat for AstroPi on the ISS uses the TCS34725. +The sense hat v2 uses the TCS3400 the successor of the TCS34725. +The TCS34725 is not available any more. It was discontinued by ams in 2021. +""" + +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/TCS3400 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. + """ + + @staticmethod + def max_value(integration_cycles): + """ + The maximum raw value for the RBGC channels depends on the number + of integration cycles. + """ + return 65535 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): + block = self.bus.read_i2c_block_data(self.ADDR, register, 2) + return (block[0] + (block[1] << 8)) + return get_raw_register + +class I2C(HardwareInterface): + """ + An implementation of the `HardwareInterface` for the TCS34725/TCS3400 + sensor that uses I2C to control the sensor and retrieve measurements. + Use the datasheets as a reference. + """ + + # device-specific constants + BUS = 1 + + # control registers + ENABLE = 0x80 + ATIME = 0x81 + CONTROL = 0x8F + ID = 0x92 + STATUS = 0x93 + # (if a register is described in the datasheet but missing here + # it means the corresponding functionality is not provided) + + # data registers + CDATA = 0x94 + RDATA = 0x96 + GDATA = 0x98 + BDATA = 0x9A + + # bit positions + OFF = 0x00 + PON = 0x01 + AEN = 0x02 + ON = (PON | AEN) + AVALID = 0x01 + + GAIN_REG_VALUES = (0x00, 0x01, 0x02, 0x03) + # Assume TCS34725 as on the ISS AstroPi + # Adjust for TCS3400 after the detection of the sensor type. + ADDR = 0x29 + GAIN_VALUES = (1, 4, 16, 60) + CLOCK_STEP = 0.0024 # 2.4ms + GAIN_TO_REG = dict(zip(GAIN_VALUES, GAIN_REG_VALUES)) + REG_TO_GAIN = dict(zip(GAIN_REG_VALUES, 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 + + # Test for sensor at I2C addresses 0x29 or 0x39 + # Both sensors have variants at 0x29 and 0x39 (See data sheets) + addr1 = addr2 = False + try: + self.bus.write_quick(0x29) + addr1 = True + except: + pass + try: + self.bus.write_quick(0x39) + addr2 = True + except: + pass + + if addr2: + self.ADDR = 0x39 + if addr1 or addr2: + # get sensor id + id = self._read(self.ID) + if (id & 0xf8) == 0x90: + sensor = 'TCS340x' + elif (id & 0xf4) == 0x44: + sensor = 'TCS3472x' + else: + explanation = "(Sensor not present)" + raise ColourSensorInitialisationError(explanation=explanation) + + # Set type specific constants + # Assume TCS3472x as in AstroPi + sensor == 'TCS3472x' + if sensor == 'TCS340x': + self.GAIN_VALUES = (1, 4, 16, 64) + self.CLOCK_STEP = 0.00275 # 2.75ms + self.GAIN_TO_REG = dict(zip(self.GAIN_VALUES, self.GAIN_REG_VALUES)) + self.REG_TO_GAIN = dict(zip(self.GAIN_REG_VALUES, self.GAIN_VALUES)) + + @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/TCS3400 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/TCS3400 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()) + brightness = clear_raw + + @property + def _scaling(self): + return self.max_raw // 256 + + @property + def colour(self): + return tuple(reading // self._scaling for reading in self.colour_raw) + + @property + def rgb(self): + return tuple(reading // self._scaling for reading in self.colour_raw)[0:3] + + 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..4c69290 --- /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 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..68aa698 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.debug(e) + pass + #### # Text assets #### @@ -191,6 +200,27 @@ def _get_fb_device(self): def stick(self): return self._stick + #### + # Colour sensor + #### + + @property + def colour(self): + try: + return self._colour + except AttributeError: + print('This Sense Hat does not have a colour sensor') + + color = colour + + def has_colour_sensor(self): + try: + self._colour + except: + return False + else: + return True + #### # LED Matrix #### @@ -272,15 +302,15 @@ def flip_v(self, redraw=True): def set_pixels(self, pixel_list): """ Accepts a list containing 64 smaller lists of [R,G,B] pixels and - updates the LED matrix. R,G,B elements must intergers between 0 + updates the LED matrix. R,G,B elements must integers between 0 and 255 """ - if len(pixel_list) != 64: + if not hasattr(pixel_list, '__len__') or len(pixel_list) != 64: raise ValueError('Pixel lists must have 64 elements') for index, pix in enumerate(pixel_list): - if len(pix) != 3: + if not hasattr(pix, '__len__') or len(pix) != 3: raise ValueError('Pixel at index %d is invalid. Pixels must contain 3 elements: Red, Green and Blue' % index) for element in pix: @@ -321,16 +351,15 @@ def set_pixel(self, x, y, *args): ap.set_pixel(x, y, pixel) """ - pixel_error = 'Pixel arguments must be given as (r, g, b) or r, g, b' - if len(args) == 1: pixel = args[0] - if len(pixel) != 3: - raise ValueError(pixel_error) elif len(args) == 3: pixel = args else: - raise ValueError(pixel_error) + pixel = None + + if not hasattr(pixel, '__len__') or len(pixel) != 3: + raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') if x > 7 or x < 0: raise ValueError('X position must be between 0 and 7') @@ -504,7 +533,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..be4c3db 100644 --- a/setup.py +++ b/setup.py @@ -7,18 +7,19 @@ def read(fname): setup( name="sense-hat", - version="2.2.0", + version="2.6.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'), + long_description_content_type="text/markdown", 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'],