From 6c262033f652e37ca389d6cfe9f6a6591925c7ea Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 8 Dec 2020 10:09:31 +0000 Subject: [PATCH 01/37] Add code to drive colour sensor --- sense_hat/sense_hat.py | 11 ++- sense_hat/tcs34725.py | 151 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 sense_hat/tcs34725.py diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 30597b9..783d703 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -15,7 +15,7 @@ from copy import deepcopy from .stick import SenseStick - +from .tcs34725 import TCS34725 as ColourSensor class SenseHat(object): @@ -90,6 +90,7 @@ def __init__( self._gyro_enabled = False self._accel_enabled = False self._stick = SenseStick() + self._colour = ColourSensor() #### # Text assets @@ -191,6 +192,14 @@ def _get_fb_device(self): def stick(self): return self._stick + #### + # Colour sensor + #### + + @property + def colour(self): + return self._colour + #### # LED Matrix #### diff --git a/sense_hat/tcs34725.py b/sense_hat/tcs34725.py new file mode 100644 index 0000000..5bbf52d --- /dev/null +++ b/sense_hat/tcs34725.py @@ -0,0 +1,151 @@ +""" +Python library for the TCS34725 Color Sensor + +Documentation (including datasheet): https://ams.com/tcs34725#tab/documents +""" + +import smbus +import glob +from time import sleep + +def i2c_enabled(): + return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None + +class TCS34725: + + # 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 + PON = 0x01 + AEN = 0x02 + AVALID = 0x01 + + GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values + GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) + + CLOCK_STEP = 0.0024 # the clock step is 2.4ms + + def __init__(self, gain=1, integration_cycles=1): + try: + self.bus = smbus.SMBus(self.BUS) + except Exception: + explanation = " (I2C is not enabled)" if not i2c_enabled() else "" + raise RuntimeError(f'Failed to initialise TCS34725 colour sensor.{explanation}') + else: + if self._id != 0x44: + raise RuntimeError(f'Not connected to TCS34725 (id: {self._id})') + self.gain = gain + self.integration_cycles=integration_cycles + self.enabled= 1 + + @property + def _id(self): + return self.bus.read_byte_data(self.ADDR, self.ID) + + @property + def enabled(self): + return self.bus.read_byte_data(self.ADDR, self.ENABLE) == (self.PON | self.AEN) + + @enabled.setter + def enabled(self, status): + if status: + self.bus.write_byte_data(self.ADDR, (self.ENABLE), self.PON) + sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." + self.bus.write_byte_data(self.ADDR, (self.ENABLE), (self.PON | self.AEN)) + else: + self.bus.write_byte_data(self.ADDR, (self.ENABLE), 0x00) + sleep(self.CLOCK_STEP) + + @property + def gain(self): + return self.GAIN_INV[self.bus.read_byte_data(self.ADDR, self.CONTROL)] + + @gain.setter + def gain(self, value): + if value in self.GAIN_MAP: + self.bus.write_byte_data(self.ADDR, self.CONTROL, self.GAIN_MAP[value]) + sleep(self.CLOCK_STEP) + else: + raise RuntimeError(f'Cannot set gain to {value}. {tuple(self.GAIN_MAP.keys())}') + + @property + def integration_cycles(self): + return 256 - self.bus.read_byte_data(self.ADDR, self.ATIME) + + @integration_cycles.setter + def integration_cycles(self, cycles): + if 1 <= cycles <= 256: + self.bus.write_byte_data(self.ADDR, self.ATIME, 256-cycles) + self.integration_time = cycles * self.CLOCK_STEP + self.max_value = 2**16 if cycles >= 64 else 1024*cycles + self._scaling = self.max_value // 256 + sleep(self.CLOCK_STEP) + else: + raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') + + @property + def colour_raw(self): + 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] + ) + + @property + def colour(self): + return tuple(reading // self._scaling for reading in self.colour_raw) + + @staticmethod + def _raw_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_raw(self): + value = self.bus.read_word_data(self.ADDR, register) + return value + return get_raw + + clear_raw = property(_raw_wrapper(CDATA)) + red_raw = property(_raw_wrapper(RDATA)) + green_raw = property(_raw_wrapper(GDATA)) + blue_raw = property(_raw_wrapper(BDATA)) + + @staticmethod + def _byte_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_byte(self): + value = self.bus.read_word_data(self.ADDR, register) // self._scaling + return value + return get_byte + + clear = property(_byte_wrapper(CDATA)) + red = property(_byte_wrapper(RDATA)) + green = property(_byte_wrapper(GDATA)) + blue = property(_byte_wrapper(BDATA)) From 8162442c19ead02bcfa280795777f5539a0bb072 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 8 Dec 2020 10:10:30 +0000 Subject: [PATCH 02/37] Update version info --- sense_hat/__init__.py | 4 ++-- setup.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index 631f9b8..a4dc164 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -12,5 +12,5 @@ ACTION_RELEASED, ACTION_HELD, ) - -__version__ = '2.2.0' +from .tcs34725 import TCS34725, TCS34725 as ColourSensor +__version__ = '2.3.0' diff --git a/setup.py b/setup.py index a9e94a3..d93f4a7 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.0", 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'], From 280e0a552c70450bcf4f124ea1ae5b84ef72016d Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 8 Dec 2020 11:36:42 +0000 Subject: [PATCH 03/37] Fix static wrappers --- sense_hat/tcs34725.py | 47 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/sense_hat/tcs34725.py b/sense_hat/tcs34725.py index 5bbf52d..3852cf2 100644 --- a/sense_hat/tcs34725.py +++ b/sense_hat/tcs34725.py @@ -11,6 +11,29 @@ def i2c_enabled(): return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None +def _raw_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_raw(self): + value = self.bus.read_word_data(self.ADDR, register) + return value + return get_raw + +def _byte_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_byte(self): + value = self.bus.read_word_data(self.ADDR, register) // self._scaling + return value + return get_byte + + class TCS34725: # device-specific constants @@ -116,35 +139,11 @@ def colour_raw(self): def colour(self): return tuple(reading // self._scaling for reading in self.colour_raw) - @staticmethod - def _raw_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_raw(self): - value = self.bus.read_word_data(self.ADDR, register) - return value - return get_raw - clear_raw = property(_raw_wrapper(CDATA)) red_raw = property(_raw_wrapper(RDATA)) green_raw = property(_raw_wrapper(GDATA)) blue_raw = property(_raw_wrapper(BDATA)) - @staticmethod - def _byte_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_byte(self): - value = self.bus.read_word_data(self.ADDR, register) // self._scaling - return value - return get_byte - clear = property(_byte_wrapper(CDATA)) red = property(_byte_wrapper(RDATA)) green = property(_byte_wrapper(GDATA)) From 10eb5650250d9e626c850fbc241948d5e95c29f7 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Wed, 9 Dec 2020 07:54:09 +0000 Subject: [PATCH 04/37] Rename everything tcs34725 to colour --- sense_hat/__init__.py | 2 +- sense_hat/{tcs34725.py => colour.py} | 2 +- sense_hat/sense_hat.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename sense_hat/{tcs34725.py => colour.py} (99%) diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index a4dc164..f4eb03e 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -12,5 +12,5 @@ ACTION_RELEASED, ACTION_HELD, ) -from .tcs34725 import TCS34725, TCS34725 as ColourSensor +from .colour import ColourSensor __version__ = '2.3.0' diff --git a/sense_hat/tcs34725.py b/sense_hat/colour.py similarity index 99% rename from sense_hat/tcs34725.py rename to sense_hat/colour.py index 3852cf2..c638bbf 100644 --- a/sense_hat/tcs34725.py +++ b/sense_hat/colour.py @@ -34,7 +34,7 @@ def get_byte(self): return get_byte -class TCS34725: +class ColourSensor: # device-specific constants BUS = 1 diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 783d703..783b6ab 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -15,7 +15,7 @@ from copy import deepcopy from .stick import SenseStick -from .tcs34725 import TCS34725 as ColourSensor +from .colour import ColourSensor class SenseHat(object): From ca6949ae4aa0620c245b59040381ed3299124794 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Mon, 4 Jan 2021 11:22:16 +0000 Subject: [PATCH 05/37] Allow 'color' variants (along with 'colour') --- sense_hat/__init__.py | 2 +- sense_hat/sense_hat.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index f4eb03e..d723807 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -12,5 +12,5 @@ ACTION_RELEASED, ACTION_HELD, ) -from .colour import ColourSensor + __version__ = '2.3.0' diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 783b6ab..7a143d6 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -90,6 +90,8 @@ def __init__( self._gyro_enabled = False self._accel_enabled = False self._stick = SenseStick() + + # initialise the TCS34725 colour sensor (if possible) self._colour = ColourSensor() #### @@ -200,6 +202,10 @@ def stick(self): def colour(self): return self._colour + @property + def color(self): + return self._colour + #### # LED Matrix #### From 7baa96ca0b32afafa69947bc7628e85d7bcd05fd Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Mon, 4 Jan 2021 11:58:47 +0000 Subject: [PATCH 06/37] Handle previous versions of the SenseHat (i.e. no colour sensor) --- sense_hat/sense_hat.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 7a143d6..8494309 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -92,7 +92,10 @@ def __init__( self._stick = SenseStick() # initialise the TCS34725 colour sensor (if possible) - self._colour = ColourSensor() + try: + self._colour = ColourSensor() + except: + pass #### # Text assets @@ -206,6 +209,14 @@ def colour(self): def color(self): return self._colour + def has_colour_sensor(self): + try: + self._colour + except: + return False + else: + return True + #### # LED Matrix #### From d2998173875adb99cc8d160ac18c61c9f5a7f5cd Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 5 Jan 2021 12:09:47 +0000 Subject: [PATCH 07/37] Add (temporary) debian packaging files --- README.rst => README.md | 0 .../installed-by-dh_installdocs | 0 .../installed-by-dh_installdocs | 0 debian/changelog | 30 + debian/compat | 1 + debian/control | 32 + debian/copyright | 32 + debian/files | 3 + debian/python-sense-hat.debhelper.log | 19 + debian/python-sense-hat.postinst.debhelper | 7 + debian/python-sense-hat.prerm.debhelper | 12 + debian/python-sense-hat.substvars | 5 + debian/python-sense-hat/DEBIAN/control | 15 + debian/python-sense-hat/DEBIAN/md5sums | 12 + debian/python-sense-hat/DEBIAN/postinst | 9 + debian/python-sense-hat/DEBIAN/prerm | 14 + .../dist-packages/sense_hat/__init__.py | 16 + .../dist-packages/sense_hat/colour.py | 150 +++ .../dist-packages/sense_hat/sense_hat.py | 883 ++++++++++++++++++ .../sense_hat/sense_hat_text.png | Bin 0 -> 908 bytes .../sense_hat/sense_hat_text.txt | 1 + .../dist-packages/sense_hat/stick.py | 308 ++++++ .../share/doc/python-sense-hat/changelog.gz | Bin 0 -> 310 bytes .../usr/share/doc/python-sense-hat/copyright | 32 + debian/python3-sense-hat.debhelper.log | 19 + debian/python3-sense-hat.postinst.debhelper | 10 + debian/python3-sense-hat.prerm.debhelper | 10 + debian/python3-sense-hat.substvars | 3 + debian/python3-sense-hat/DEBIAN/control | 16 + debian/python3-sense-hat/DEBIAN/md5sums | 12 + debian/python3-sense-hat/DEBIAN/postinst | 12 + debian/python3-sense-hat/DEBIAN/prerm | 12 + .../dist-packages/sense_hat/__init__.py | 16 + .../python3/dist-packages/sense_hat/colour.py | 150 +++ .../dist-packages/sense_hat/sense_hat.py | 883 ++++++++++++++++++ .../sense_hat/sense_hat_text.png | Bin 0 -> 908 bytes .../sense_hat/sense_hat_text.txt | 1 + .../python3/dist-packages/sense_hat/stick.py | 308 ++++++ .../share/doc/python3-sense-hat/changelog.gz | Bin 0 -> 310 bytes .../usr/share/doc/python3-sense-hat/copyright | 32 + debian/rules | 13 + debian/source/format | 1 + 42 files changed, 3079 insertions(+) rename README.rst => README.md (100%) create mode 100644 debian/.debhelper/generated/python-sense-hat/installed-by-dh_installdocs create mode 100644 debian/.debhelper/generated/python3-sense-hat/installed-by-dh_installdocs create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/files create mode 100644 debian/python-sense-hat.debhelper.log create mode 100644 debian/python-sense-hat.postinst.debhelper create mode 100644 debian/python-sense-hat.prerm.debhelper create mode 100644 debian/python-sense-hat.substvars create mode 100644 debian/python-sense-hat/DEBIAN/control create mode 100644 debian/python-sense-hat/DEBIAN/md5sums create mode 100755 debian/python-sense-hat/DEBIAN/postinst create mode 100755 debian/python-sense-hat/DEBIAN/prerm create mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py create mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py create mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py create mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png create mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.txt create mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py create mode 100644 debian/python-sense-hat/usr/share/doc/python-sense-hat/changelog.gz create mode 100644 debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright create mode 100644 debian/python3-sense-hat.debhelper.log create mode 100644 debian/python3-sense-hat.postinst.debhelper create mode 100644 debian/python3-sense-hat.prerm.debhelper create mode 100644 debian/python3-sense-hat.substvars create mode 100644 debian/python3-sense-hat/DEBIAN/control create mode 100644 debian/python3-sense-hat/DEBIAN/md5sums create mode 100755 debian/python3-sense-hat/DEBIAN/postinst create mode 100755 debian/python3-sense-hat/DEBIAN/prerm create mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py create mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py create mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py create mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png create mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.txt create mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py create mode 100644 debian/python3-sense-hat/usr/share/doc/python3-sense-hat/changelog.gz create mode 100644 debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright create mode 100755 debian/rules create mode 100644 debian/source/format diff --git a/README.rst b/README.md similarity index 100% rename from README.rst rename to README.md diff --git a/debian/.debhelper/generated/python-sense-hat/installed-by-dh_installdocs b/debian/.debhelper/generated/python-sense-hat/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/debian/.debhelper/generated/python3-sense-hat/installed-by-dh_installdocs b/debian/.debhelper/generated/python3-sense-hat/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..d67c286 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,30 @@ +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..04b031f --- /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 +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/files b/debian/files new file mode 100644 index 0000000..62a4de2 --- /dev/null +++ b/debian/files @@ -0,0 +1,3 @@ +python-sense-hat_2.3.0~test0_all.deb python optional +python-sense-hat_2.3.0~test0_armhf.buildinfo python optional +python3-sense-hat_2.3.0~test0_all.deb python optional diff --git a/debian/python-sense-hat.debhelper.log b/debian/python-sense-hat.debhelper.log new file mode 100644 index 0000000..de33f70 --- /dev/null +++ b/debian/python-sense-hat.debhelper.log @@ -0,0 +1,19 @@ +dh_update_autotools_config +dh_auto_configure +dh_auto_build +dh_auto_test +dh_prep +dh_auto_install +dh_installdocs +dh_installchangelogs +dh_installinit +dh_perl +dh_link +dh_strip_nondeterminism +dh_compress +dh_fixperms +dh_missing +dh_installdeb +dh_gencontrol +dh_md5sums +dh_builddeb diff --git a/debian/python-sense-hat.postinst.debhelper b/debian/python-sense-hat.postinst.debhelper new file mode 100644 index 0000000..e415945 --- /dev/null +++ b/debian/python-sense-hat.postinst.debhelper @@ -0,0 +1,7 @@ + +# Automatically added by dh_python2: +if which pycompile >/dev/null 2>&1; then + pycompile -p python-sense-hat +fi + +# End automatically added section diff --git a/debian/python-sense-hat.prerm.debhelper b/debian/python-sense-hat.prerm.debhelper new file mode 100644 index 0000000..7150f5e --- /dev/null +++ b/debian/python-sense-hat.prerm.debhelper @@ -0,0 +1,12 @@ + +# Automatically added by dh_python2: +if which pyclean >/dev/null 2>&1; then + pyclean -p python-sense-hat +else + dpkg -L python-sense-hat | grep '\.py$' | while read file + do + rm -f "${file}"[co] >/dev/null + done +fi + +# End automatically added section diff --git a/debian/python-sense-hat.substvars b/debian/python-sense-hat.substvars new file mode 100644 index 0000000..467868f --- /dev/null +++ b/debian/python-sense-hat.substvars @@ -0,0 +1,5 @@ +python:Versions=2.7 +python:Provides=python2.7-sense-hat +python:Depends=python-numpy, python-pil, python:any, python:any (<< 2.8), python:any (>= 2.7~) +misc:Depends= +misc:Pre-Depends= diff --git a/debian/python-sense-hat/DEBIAN/control b/debian/python-sense-hat/DEBIAN/control new file mode 100644 index 0000000..44348c7 --- /dev/null +++ b/debian/python-sense-hat/DEBIAN/control @@ -0,0 +1,15 @@ +Package: python-sense-hat +Version: 2.3.0~test0 +Architecture: all +Maintainer: Serge Schneider +Installed-Size: 67 +Depends: python-numpy, python-pil, python:any (<< 2.8), python:any (>= 2.7~), python-rtimulib +Section: python +Priority: optional +Homepage: https://github.com/RPi-Distro/python-sense-hat +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. diff --git a/debian/python-sense-hat/DEBIAN/md5sums b/debian/python-sense-hat/DEBIAN/md5sums new file mode 100644 index 0000000..dda61e3 --- /dev/null +++ b/debian/python-sense-hat/DEBIAN/md5sums @@ -0,0 +1,12 @@ +35dd286b437b2916c40cdc4ed7075e50 usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/dependency_links.txt +d41d8cd98f00b204e9800998ecf8427e usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/requires.txt +d485d30aa3a3f58b8b0db6e71f19ce20 usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/top_level.txt +6f29e2883acf35bf03d73cc70c5e5db7 usr/lib/python2.7/dist-packages/sense_hat/__init__.py +776c985173201b7c38f016a3320cb8d6 usr/lib/python2.7/dist-packages/sense_hat/colour.py +48c3612fdc876097daccd0cad726ed91 usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py +a90fdb250c9803bf822190b12636c73e usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png +ea665cb6f674eea5244a3e39051e45cf usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.txt +aecf59111307ec7eafb6192e8fbd1429 usr/lib/python2.7/dist-packages/sense_hat/stick.py +68832ab63c7ca094f1d1e31f84be94c8 usr/share/doc/python-sense-hat/changelog.gz +ca593700544765aae69f76ecf48ec56b usr/share/doc/python-sense-hat/copyright diff --git a/debian/python-sense-hat/DEBIAN/postinst b/debian/python-sense-hat/DEBIAN/postinst new file mode 100755 index 0000000..240f649 --- /dev/null +++ b/debian/python-sense-hat/DEBIAN/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python2: +if which pycompile >/dev/null 2>&1; then + pycompile -p python-sense-hat +fi + +# End automatically added section diff --git a/debian/python-sense-hat/DEBIAN/prerm b/debian/python-sense-hat/DEBIAN/prerm new file mode 100755 index 0000000..ee6c986 --- /dev/null +++ b/debian/python-sense-hat/DEBIAN/prerm @@ -0,0 +1,14 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python2: +if which pyclean >/dev/null 2>&1; then + pyclean -p python-sense-hat +else + dpkg -L python-sense-hat | grep '\.py$' | while read file + do + rm -f "${file}"[co] >/dev/null + done +fi + +# End automatically added section diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py new file mode 100644 index 0000000..d723807 --- /dev/null +++ b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +from .sense_hat import SenseHat, SenseHat as AstroPi +from .stick import ( + SenseStick, + InputEvent, + DIRECTION_UP, + DIRECTION_DOWN, + DIRECTION_LEFT, + DIRECTION_RIGHT, + DIRECTION_MIDDLE, + ACTION_PRESSED, + ACTION_RELEASED, + ACTION_HELD, + ) + +__version__ = '2.3.0' diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py new file mode 100644 index 0000000..c638bbf --- /dev/null +++ b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py @@ -0,0 +1,150 @@ +""" +Python library for the TCS34725 Color Sensor + +Documentation (including datasheet): https://ams.com/tcs34725#tab/documents +""" + +import smbus +import glob +from time import sleep + +def i2c_enabled(): + return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None + +def _raw_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_raw(self): + value = self.bus.read_word_data(self.ADDR, register) + return value + return get_raw + +def _byte_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_byte(self): + value = self.bus.read_word_data(self.ADDR, register) // self._scaling + return value + return get_byte + + +class ColourSensor: + + # 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 + PON = 0x01 + AEN = 0x02 + AVALID = 0x01 + + GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values + GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) + + CLOCK_STEP = 0.0024 # the clock step is 2.4ms + + def __init__(self, gain=1, integration_cycles=1): + try: + self.bus = smbus.SMBus(self.BUS) + except Exception: + explanation = " (I2C is not enabled)" if not i2c_enabled() else "" + raise RuntimeError(f'Failed to initialise TCS34725 colour sensor.{explanation}') + else: + if self._id != 0x44: + raise RuntimeError(f'Not connected to TCS34725 (id: {self._id})') + self.gain = gain + self.integration_cycles=integration_cycles + self.enabled= 1 + + @property + def _id(self): + return self.bus.read_byte_data(self.ADDR, self.ID) + + @property + def enabled(self): + return self.bus.read_byte_data(self.ADDR, self.ENABLE) == (self.PON | self.AEN) + + @enabled.setter + def enabled(self, status): + if status: + self.bus.write_byte_data(self.ADDR, (self.ENABLE), self.PON) + sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." + self.bus.write_byte_data(self.ADDR, (self.ENABLE), (self.PON | self.AEN)) + else: + self.bus.write_byte_data(self.ADDR, (self.ENABLE), 0x00) + sleep(self.CLOCK_STEP) + + @property + def gain(self): + return self.GAIN_INV[self.bus.read_byte_data(self.ADDR, self.CONTROL)] + + @gain.setter + def gain(self, value): + if value in self.GAIN_MAP: + self.bus.write_byte_data(self.ADDR, self.CONTROL, self.GAIN_MAP[value]) + sleep(self.CLOCK_STEP) + else: + raise RuntimeError(f'Cannot set gain to {value}. {tuple(self.GAIN_MAP.keys())}') + + @property + def integration_cycles(self): + return 256 - self.bus.read_byte_data(self.ADDR, self.ATIME) + + @integration_cycles.setter + def integration_cycles(self, cycles): + if 1 <= cycles <= 256: + self.bus.write_byte_data(self.ADDR, self.ATIME, 256-cycles) + self.integration_time = cycles * self.CLOCK_STEP + self.max_value = 2**16 if cycles >= 64 else 1024*cycles + self._scaling = self.max_value // 256 + sleep(self.CLOCK_STEP) + else: + raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') + + @property + def colour_raw(self): + 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] + ) + + @property + def colour(self): + return tuple(reading // self._scaling for reading in self.colour_raw) + + clear_raw = property(_raw_wrapper(CDATA)) + red_raw = property(_raw_wrapper(RDATA)) + green_raw = property(_raw_wrapper(GDATA)) + blue_raw = property(_raw_wrapper(BDATA)) + + clear = property(_byte_wrapper(CDATA)) + red = property(_byte_wrapper(RDATA)) + green = property(_byte_wrapper(GDATA)) + blue = property(_byte_wrapper(BDATA)) diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py new file mode 100644 index 0000000..8494309 --- /dev/null +++ b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py @@ -0,0 +1,883 @@ +#!/usr/bin/python +import struct +import os +import sys +import math +import time +import numpy as np +import shutil +import glob +import RTIMU # custom version +import pwd +import array +import fcntl +from PIL import Image # pillow +from copy import deepcopy + +from .stick import SenseStick +from .colour import ColourSensor + +class SenseHat(object): + + SENSE_HAT_FB_NAME = 'RPi-Sense FB' + SENSE_HAT_FB_FBIOGET_GAMMA = 61696 + SENSE_HAT_FB_FBIOSET_GAMMA = 61697 + SENSE_HAT_FB_FBIORESET_GAMMA = 61698 + SENSE_HAT_FB_GAMMA_DEFAULT = 0 + SENSE_HAT_FB_GAMMA_LOW = 1 + SENSE_HAT_FB_GAMMA_USER = 2 + SETTINGS_HOME_PATH = '.config/sense_hat' + + def __init__( + self, + imu_settings_file='RTIMULib', + text_assets='sense_hat_text' + ): + + self._fb_device = self._get_fb_device() + if self._fb_device is None: + raise OSError('Cannot detect %s device' % self.SENSE_HAT_FB_NAME) + + if not glob.glob('/dev/i2c*'): + raise OSError('Cannot access I2C. Please ensure I2C is enabled in raspi-config') + + # 0 is With B+ HDMI port facing downwards + pix_map0 = np.array([ + [0, 1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13, 14, 15], + [16, 17, 18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29, 30, 31], + [32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47], + [48, 49, 50, 51, 52, 53, 54, 55], + [56, 57, 58, 59, 60, 61, 62, 63] + ], int) + + pix_map90 = np.rot90(pix_map0) + pix_map180 = np.rot90(pix_map90) + pix_map270 = np.rot90(pix_map180) + + self._pix_map = { + 0: pix_map0, + 90: pix_map90, + 180: pix_map180, + 270: pix_map270 + } + + self._rotation = 0 + + # Load text assets + dir_path = os.path.dirname(__file__) + self._load_text_assets( + os.path.join(dir_path, '%s.png' % text_assets), + os.path.join(dir_path, '%s.txt' % text_assets) + ) + + # Load IMU settings and calibration data + self._imu_settings = self._get_settings_file(imu_settings_file) + self._imu = RTIMU.RTIMU(self._imu_settings) + self._imu_init = False # Will be initialised as and when needed + self._pressure = RTIMU.RTPressure(self._imu_settings) + self._pressure_init = False # Will be initialised as and when needed + self._humidity = RTIMU.RTHumidity(self._imu_settings) + self._humidity_init = False # Will be initialised as and when needed + self._last_orientation = {'pitch': 0, 'roll': 0, 'yaw': 0} + raw = {'x': 0, 'y': 0, 'z': 0} + self._last_compass_raw = deepcopy(raw) + self._last_gyro_raw = deepcopy(raw) + self._last_accel_raw = deepcopy(raw) + self._compass_enabled = False + self._gyro_enabled = False + self._accel_enabled = False + self._stick = SenseStick() + + # initialise the TCS34725 colour sensor (if possible) + try: + self._colour = ColourSensor() + except: + pass + + #### + # Text assets + #### + + # Text asset files are rotated right through 90 degrees to allow blocks of + # 40 contiguous pixels to represent one 5 x 8 character. These are stored + # in a 8 x 640 pixel png image with characters arranged adjacently + # Consequently we must rotate the pixel map left through 90 degrees to + # compensate when drawing text + + def _load_text_assets(self, text_image_file, text_file): + """ + Internal. Builds a character indexed dictionary of pixels used by the + show_message function below + """ + + text_pixels = self.load_image(text_image_file, False) + with open(text_file, 'r') as f: + loaded_text = f.read() + self._text_dict = {} + for index, s in enumerate(loaded_text): + start = index * 40 + end = start + 40 + char = text_pixels[start:end] + self._text_dict[s] = char + + def _trim_whitespace(self, char): # For loading text assets only + """ + Internal. Trims white space pixels from the front and back of loaded + text characters + """ + + psum = lambda x: sum(sum(x, [])) + if psum(char) > 0: + is_empty = True + while is_empty: # From front + row = char[0:8] + is_empty = psum(row) == 0 + if is_empty: + del char[0:8] + is_empty = True + while is_empty: # From back + row = char[-8:] + is_empty = psum(row) == 0 + if is_empty: + del char[-8:] + return char + + def _get_settings_file(self, imu_settings_file): + """ + Internal. Logic to check for a system wide RTIMU ini file. This is + copied to the home folder if one is not already found there. + """ + + ini_file = '%s.ini' % imu_settings_file + + home_dir = pwd.getpwuid(os.getuid())[5] + home_path = os.path.join(home_dir, self.SETTINGS_HOME_PATH) + if not os.path.exists(home_path): + os.makedirs(home_path) + + home_file = os.path.join(home_path, ini_file) + home_exists = os.path.isfile(home_file) + system_file = os.path.join('/etc', ini_file) + system_exists = os.path.isfile(system_file) + + if system_exists and not home_exists: + shutil.copyfile(system_file, home_file) + + return RTIMU.Settings(os.path.join(home_path, imu_settings_file)) # RTIMU will add .ini internally + + def _get_fb_device(self): + """ + Internal. Finds the correct frame buffer device for the sense HAT + and returns its /dev name. + """ + + device = None + + for fb in glob.glob('/sys/class/graphics/fb*'): + name_file = os.path.join(fb, 'name') + if os.path.isfile(name_file): + with open(name_file, 'r') as f: + name = f.read() + if name.strip() == self.SENSE_HAT_FB_NAME: + fb_device = fb.replace(os.path.dirname(fb), '/dev') + if os.path.exists(fb_device): + device = fb_device + break + + return device + + #### + # Joystick + #### + + @property + def stick(self): + return self._stick + + #### + # Colour sensor + #### + + @property + def colour(self): + return self._colour + + @property + def color(self): + return self._colour + + def has_colour_sensor(self): + try: + self._colour + except: + return False + else: + return True + + #### + # LED Matrix + #### + + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, r): + self.set_rotation(r, True) + + def set_rotation(self, r=0, redraw=True): + """ + Sets the LED matrix rotation for viewing, adjust if the Pi is upside + down or sideways. 0 is with the Pi HDMI port facing downwards + """ + + if r in self._pix_map.keys(): + if redraw: + pixel_list = self.get_pixels() + self._rotation = r + if redraw: + self.set_pixels(pixel_list) + else: + raise ValueError('Rotation must be 0, 90, 180 or 270 degrees') + + def _pack_bin(self, pix): + """ + Internal. Encodes python list [R,G,B] into 16 bit RGB565 + """ + + r = (pix[0] >> 3) & 0x1F + g = (pix[1] >> 2) & 0x3F + b = (pix[2] >> 3) & 0x1F + bits16 = (r << 11) + (g << 5) + b + return struct.pack('H', bits16) + + def _unpack_bin(self, packed): + """ + Internal. Decodes 16 bit RGB565 into python list [R,G,B] + """ + + output = struct.unpack('H', packed) + bits16 = output[0] + r = (bits16 & 0xF800) >> 11 + g = (bits16 & 0x7E0) >> 5 + b = (bits16 & 0x1F) + return [int(r << 3), int(g << 2), int(b << 3)] + + def flip_h(self, redraw=True): + """ + Flip LED matrix horizontal + """ + + pixel_list = self.get_pixels() + flipped = [] + for i in range(8): + offset = i * 8 + flipped.extend(reversed(pixel_list[offset:offset + 8])) + if redraw: + self.set_pixels(flipped) + return flipped + + def flip_v(self, redraw=True): + """ + Flip LED matrix vertical + """ + + pixel_list = self.get_pixels() + flipped = [] + for i in reversed(range(8)): + offset = i * 8 + flipped.extend(pixel_list[offset:offset + 8]) + if redraw: + self.set_pixels(flipped) + return flipped + + 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 + and 255 + """ + + if len(pixel_list) != 64: + raise ValueError('Pixel lists must have 64 elements') + + for index, pix in enumerate(pixel_list): + if 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: + if element > 255 or element < 0: + raise ValueError('Pixel at index %d is invalid. Pixel elements must be between 0 and 255' % index) + + with open(self._fb_device, 'wb') as f: + map = self._pix_map[self._rotation] + for index, pix in enumerate(pixel_list): + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[index // 8][index % 8] * 2) # row, column + f.write(self._pack_bin(pix)) + + def get_pixels(self): + """ + Returns a list containing 64 smaller lists of [R,G,B] pixels + representing what is currently displayed on the LED matrix + """ + + pixel_list = [] + with open(self._fb_device, 'rb') as f: + map = self._pix_map[self._rotation] + for row in range(8): + for col in range(8): + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[row][col] * 2) # row, column + pixel_list.append(self._unpack_bin(f.read(2))) + return pixel_list + + def set_pixel(self, x, y, *args): + """ + Updates the single [R,G,B] pixel specified by x and y on the LED matrix + Top left = 0,0 Bottom right = 7,7 + + e.g. ap.set_pixel(x, y, r, g, b) + or + pixel = (r, g, b) + 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) + + if x > 7 or x < 0: + raise ValueError('X position must be between 0 and 7') + + if y > 7 or y < 0: + raise ValueError('Y position must be between 0 and 7') + + for element in pixel: + if element > 255 or element < 0: + raise ValueError('Pixel elements must be between 0 and 255') + + with open(self._fb_device, 'wb') as f: + map = self._pix_map[self._rotation] + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[y][x] * 2) # row, column + f.write(self._pack_bin(pixel)) + + def get_pixel(self, x, y): + """ + Returns a list of [R,G,B] representing the pixel specified by x and y + on the LED matrix. Top left = 0,0 Bottom right = 7,7 + """ + + if x > 7 or x < 0: + raise ValueError('X position must be between 0 and 7') + + if y > 7 or y < 0: + raise ValueError('Y position must be between 0 and 7') + + pix = None + + with open(self._fb_device, 'rb') as f: + map = self._pix_map[self._rotation] + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[y][x] * 2) # row, column + pix = self._unpack_bin(f.read(2)) + + return pix + + def load_image(self, file_path, redraw=True): + """ + Accepts a path to an 8 x 8 image file and updates the LED matrix with + the image + """ + + if not os.path.exists(file_path): + raise IOError('%s not found' % file_path) + + img = Image.open(file_path).convert('RGB') + pixel_list = list(map(list, img.getdata())) + + if redraw: + self.set_pixels(pixel_list) + + return pixel_list + + def clear(self, *args): + """ + Clears the LED matrix with a single colour, default is black / off + + e.g. ap.clear() + or + ap.clear(r, g, b) + or + colour = (r, g, b) + ap.clear(colour) + """ + + black = (0, 0, 0) # default + + if len(args) == 0: + colour = black + elif len(args) == 1: + colour = args[0] + elif len(args) == 3: + colour = args + else: + raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') + + self.set_pixels([colour] * 64) + + def _get_char_pixels(self, s): + """ + Internal. Safeguards the character indexed dictionary for the + show_message function below + """ + + if len(s) == 1 and s in self._text_dict.keys(): + return list(self._text_dict[s]) + else: + return list(self._text_dict['?']) + + def show_message( + self, + text_string, + scroll_speed=.1, + text_colour=[255, 255, 255], + back_colour=[0, 0, 0] + ): + """ + Scrolls a string of text across the LED matrix using the specified + speed and colours + """ + + # We must rotate the pixel map left through 90 degrees when drawing + # text, see _load_text_assets + previous_rotation = self._rotation + self._rotation -= 90 + if self._rotation < 0: + self._rotation = 270 + dummy_colour = [None, None, None] + string_padding = [dummy_colour] * 64 + letter_padding = [dummy_colour] * 8 + # Build pixels from dictionary + scroll_pixels = [] + scroll_pixels.extend(string_padding) + for s in text_string: + scroll_pixels.extend(self._trim_whitespace(self._get_char_pixels(s))) + scroll_pixels.extend(letter_padding) + scroll_pixels.extend(string_padding) + # Recolour pixels as necessary + coloured_pixels = [ + text_colour if pixel == [255, 255, 255] else back_colour + for pixel in scroll_pixels + ] + # Shift right by 8 pixels per frame to scroll + scroll_length = len(coloured_pixels) // 8 + for i in range(scroll_length - 8): + start = i * 8 + end = start + 64 + self.set_pixels(coloured_pixels[start:end]) + time.sleep(scroll_speed) + self._rotation = previous_rotation + + def show_letter( + self, + s, + text_colour=[255, 255, 255], + back_colour=[0, 0, 0] + ): + """ + Displays a single text character on the LED matrix using the specified + colours + """ + + if len(s) > 1: + raise ValueError('Only one character may be passed into this method') + # We must rotate the pixel map left through 90 degrees when drawing + # text, see _load_text_assets + previous_rotation = self._rotation + self._rotation -= 90 + if self._rotation < 0: + self._rotation = 270 + dummy_colour = [None, None, None] + pixel_list = [dummy_colour] * 8 + pixel_list.extend(self._get_char_pixels(s)) + pixel_list.extend([dummy_colour] * 16) + coloured_pixels = [ + text_colour if pixel == [255, 255, 255] else back_colour + for pixel in pixel_list + ] + self.set_pixels(coloured_pixels) + self._rotation = previous_rotation + + @property + def gamma(self): + buffer = array.array('B', [0]*32) + with open(self._fb_device) as f: + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOGET_GAMMA, buffer) + return list(buffer) + + @gamma.setter + def gamma(self, buffer): + if len(buffer) is not 32: + raise ValueError('Gamma array must be of length 32') + + if not all(b <= 31 for b in buffer): + raise ValueError('Gamma values must be bewteen 0 and 31') + + if not isinstance(buffer, array.array): + buffer = array.array('B', buffer) + + with open(self._fb_device) as f: + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOSET_GAMMA, buffer) + + def gamma_reset(self): + """ + Resets the LED matrix gamma correction to default + """ + + with open(self._fb_device) as f: + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, self.SENSE_HAT_FB_GAMMA_DEFAULT) + + @property + def low_light(self): + return self.gamma == [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10] + + @low_light.setter + def low_light(self, value): + with open(self._fb_device) as f: + cmd = self.SENSE_HAT_FB_GAMMA_LOW if value else self.SENSE_HAT_FB_GAMMA_DEFAULT + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, cmd) + + #### + # Environmental sensors + #### + + def _init_humidity(self): + """ + Internal. Initialises the humidity sensor via RTIMU + """ + + if not self._humidity_init: + self._humidity_init = self._humidity.humidityInit() + if not self._humidity_init: + raise OSError('Humidity Init Failed') + + def _init_pressure(self): + """ + Internal. Initialises the pressure sensor via RTIMU + """ + + if not self._pressure_init: + self._pressure_init = self._pressure.pressureInit() + if not self._pressure_init: + raise OSError('Pressure Init Failed') + + def get_humidity(self): + """ + Returns the percentage of relative humidity + """ + + self._init_humidity() # Ensure humidity sensor is initialised + humidity = 0 + data = self._humidity.humidityRead() + if (data[0]): # Humidity valid + humidity = data[1] + return humidity + + @property + def humidity(self): + return self.get_humidity() + + def get_temperature_from_humidity(self): + """ + Returns the temperature in Celsius from the humidity sensor + """ + + self._init_humidity() # Ensure humidity sensor is initialised + temp = 0 + data = self._humidity.humidityRead() + if (data[2]): # Temp valid + temp = data[3] + return temp + + def get_temperature_from_pressure(self): + """ + Returns the temperature in Celsius from the pressure sensor + """ + + self._init_pressure() # Ensure pressure sensor is initialised + temp = 0 + data = self._pressure.pressureRead() + if (data[2]): # Temp valid + temp = data[3] + return temp + + def get_temperature(self): + """ + Returns the temperature in Celsius + """ + + return self.get_temperature_from_humidity() + + @property + def temp(self): + return self.get_temperature_from_humidity() + + @property + def temperature(self): + return self.get_temperature_from_humidity() + + def get_pressure(self): + """ + Returns the pressure in Millibars + """ + + self._init_pressure() # Ensure pressure sensor is initialised + pressure = 0 + data = self._pressure.pressureRead() + if (data[0]): # Pressure valid + pressure = data[1] + return pressure + + @property + def pressure(self): + return self.get_pressure() + + #### + # IMU Sensor + #### + + def _init_imu(self): + """ + Internal. Initialises the IMU sensor via RTIMU + """ + + if not self._imu_init: + self._imu_init = self._imu.IMUInit() + if self._imu_init: + self._imu_poll_interval = self._imu.IMUGetPollInterval() * 0.001 + # Enable everything on IMU + self.set_imu_config(True, True, True) + else: + raise OSError('IMU Init Failed') + + def set_imu_config(self, compass_enabled, gyro_enabled, accel_enabled): + """ + Enables and disables the gyroscope, accelerometer and/or magnetometer + input to the orientation functions + """ + + # If the consuming code always calls this just before reading the IMU + # the IMU consistently fails to read. So prevent unnecessary calls to + # IMU config functions using state variables + + self._init_imu() # Ensure imu is initialised + + if (not isinstance(compass_enabled, bool) + or not isinstance(gyro_enabled, bool) + or not isinstance(accel_enabled, bool)): + raise TypeError('All set_imu_config parameters must be of boolean type') + + if self._compass_enabled != compass_enabled: + self._compass_enabled = compass_enabled + self._imu.setCompassEnable(self._compass_enabled) + + if self._gyro_enabled != gyro_enabled: + self._gyro_enabled = gyro_enabled + self._imu.setGyroEnable(self._gyro_enabled) + + if self._accel_enabled != accel_enabled: + self._accel_enabled = accel_enabled + self._imu.setAccelEnable(self._accel_enabled) + + def _read_imu(self): + """ + Internal. Tries to read the IMU sensor three times before giving up + """ + + self._init_imu() # Ensure imu is initialised + + attempts = 0 + success = False + + while not success and attempts < 3: + success = self._imu.IMURead() + attempts += 1 + time.sleep(self._imu_poll_interval) + + return success + + def _get_raw_data(self, is_valid_key, data_key): + """ + Internal. Returns the specified raw data from the IMU when valid + """ + + result = None + + if self._read_imu(): + data = self._imu.getIMUData() + if data[is_valid_key]: + raw = data[data_key] + result = { + 'x': raw[0], + 'y': raw[1], + 'z': raw[2] + } + + return result + + def get_orientation_radians(self): + """ + Returns a dictionary object to represent the current orientation in + radians using the aircraft principal axes of pitch, roll and yaw + """ + + raw = self._get_raw_data('fusionPoseValid', 'fusionPose') + + if raw is not None: + raw['roll'] = raw.pop('x') + raw['pitch'] = raw.pop('y') + raw['yaw'] = raw.pop('z') + self._last_orientation = raw + + return deepcopy(self._last_orientation) + + @property + def orientation_radians(self): + return self.get_orientation_radians() + + def get_orientation_degrees(self): + """ + Returns a dictionary object to represent the current orientation + in degrees, 0 to 360, using the aircraft principal axes of + pitch, roll and yaw + """ + + orientation = self.get_orientation_radians() + for key, val in orientation.items(): + deg = math.degrees(val) # Result is -180 to +180 + orientation[key] = deg + 360 if deg < 0 else deg + return orientation + + def get_orientation(self): + return self.get_orientation_degrees() + + @property + def orientation(self): + return self.get_orientation_degrees() + + def get_compass(self): + """ + Gets the direction of North from the magnetometer in degrees + """ + + self.set_imu_config(True, False, False) + orientation = self.get_orientation_degrees() + if type(orientation) is dict and 'yaw' in orientation.keys(): + return orientation['yaw'] + else: + return None + + @property + def compass(self): + return self.get_compass() + + def get_compass_raw(self): + """ + Magnetometer x y z raw data in uT (micro teslas) + """ + + raw = self._get_raw_data('compassValid', 'compass') + + if raw is not None: + self._last_compass_raw = raw + + return deepcopy(self._last_compass_raw) + + @property + def compass_raw(self): + return self.get_compass_raw() + + def get_gyroscope(self): + """ + Gets the orientation in degrees from the gyroscope only + """ + + self.set_imu_config(False, True, False) + return self.get_orientation_degrees() + + @property + def gyro(self): + return self.get_gyroscope() + + @property + def gyroscope(self): + return self.get_gyroscope() + + def get_gyroscope_raw(self): + """ + Gyroscope x y z raw data in radians per second + """ + + raw = self._get_raw_data('gyroValid', 'gyro') + + if raw is not None: + self._last_gyro_raw = raw + + return deepcopy(self._last_gyro_raw) + + @property + def gyro_raw(self): + return self.get_gyroscope_raw() + + @property + def gyroscope_raw(self): + return self.get_gyroscope_raw() + + def get_accelerometer(self): + """ + Gets the orientation in degrees from the accelerometer only + """ + + self.set_imu_config(False, False, True) + return self.get_orientation_degrees() + + @property + def accel(self): + return self.get_accelerometer() + + @property + def accelerometer(self): + return self.get_accelerometer() + + def get_accelerometer_raw(self): + """ + Accelerometer x y z raw data in Gs + """ + + raw = self._get_raw_data('accelValid', 'accel') + + if raw is not None: + self._last_accel_raw = raw + + return deepcopy(self._last_accel_raw) + + @property + def accel_raw(self): + return self.get_accelerometer_raw() + + @property + def accelerometer_raw(self): + return self.get_accelerometer_raw() diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png new file mode 100644 index 0000000000000000000000000000000000000000..747ed9cbe233d9dfe6d5a08bba122965791b81b4 GIT binary patch literal 908 zcmV;719SX|P)z3l1qDCPoV#0000PbVXQnLvL+uWo~o;Lvm$dbY)~9 zcWHEJAV*0}P*;Ht7XSbQ21!IgRA}DqTFG+5APhAc{{NThp_$kQEl)^^J5%Y@nqcj_ zkK<3k7~}dcNxy6BZ(D0yuX^<0M(ac|&xfCL6$k&@XU+_0V~p0C!F&uFORI*%e?!zq zV}*0RD&xkDO8#KFq~c(2SZE-#IGUg)3G5#{Nf3{+*RE)P|EBk^L_Vf7ATk&t z0(9>8UkS%C#$eBgWm^-;EWX<@k6+RtNw1~r)0u<`r*;O|?&>(0pKI?qsaOeRIENgk zU&U}Wwb>FGLMP^FWzC>U6|Jq>8y`Haz8uZi_576mW3OgladeSU5huhjaTd?enyz^2jBVE3*0R}>W88RA{ zKHDfd88k+rTUkxKL3x#n-`WYHJ<-o^Sq<7or-audVW}7Igl+EwZiJ1&xy6m=^H^fT znri6l1Yo-qFIXfE!ejNsj^&Mmc5$HX3M^e{*AG;E;d)oKPpusDNKB(jTx|tf8Gm7M z>!Y;ZF(x#)KMj250|ypnDm>259De0F6`Ala|t)J=mU8$7!|e>C8@RSPU96Pw2P(?Bh{n^lj3f`WpAf`WpA if`WpAf`WpAe+ob22BS=Py=(XY0000<0123456789.=)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?,;:|@%[&_']\~ diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py new file mode 100644 index 0000000..896a9f8 --- /dev/null +++ b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py @@ -0,0 +1,308 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +native_str = str +str = type('') + +import io +import os +import glob +import errno +import struct +import select +import inspect +from functools import wraps +from collections import namedtuple +from threading import Thread, Event + + +DIRECTION_UP = 'up' +DIRECTION_DOWN = 'down' +DIRECTION_LEFT = 'left' +DIRECTION_RIGHT = 'right' +DIRECTION_MIDDLE = 'middle' + +ACTION_PRESSED = 'pressed' +ACTION_RELEASED = 'released' +ACTION_HELD = 'held' + + +InputEvent = namedtuple('InputEvent', ('timestamp', 'direction', 'action')) + + +class SenseStick(object): + """ + Represents the joystick on the Sense HAT. + """ + SENSE_HAT_EVDEV_NAME = 'Raspberry Pi Sense HAT Joystick' + EVENT_FORMAT = native_str('llHHI') + EVENT_SIZE = struct.calcsize(EVENT_FORMAT) + + EV_KEY = 0x01 + + STATE_RELEASE = 0 + STATE_PRESS = 1 + STATE_HOLD = 2 + + KEY_UP = 103 + KEY_LEFT = 105 + KEY_RIGHT = 106 + KEY_DOWN = 108 + KEY_ENTER = 28 + + def __init__(self): + self._stick_file = io.open(self._stick_device(), 'rb', buffering=0) + self._callbacks = {} + self._callback_thread = None + self._callback_event = Event() + + def close(self): + if self._stick_file: + self._callbacks.clear() + self._start_stop_thread() + self._stick_file.close() + self._stick_file = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + + def _stick_device(self): + """ + Discovers the filename of the evdev device that represents the Sense + HAT's joystick. + """ + for evdev in glob.glob('/sys/class/input/event*'): + try: + with io.open(os.path.join(evdev, 'device', 'name'), 'r') as f: + if f.read().strip() == self.SENSE_HAT_EVDEV_NAME: + return os.path.join('/dev', 'input', os.path.basename(evdev)) + except IOError as e: + if e.errno != errno.ENOENT: + raise + raise RuntimeError('unable to locate SenseHAT joystick device') + + def _read(self): + """ + Reads a single event from the joystick, blocking until one is + available. Returns `None` if a non-key event was read, or an + `InputEvent` tuple describing the event otherwise. + """ + event = self._stick_file.read(self.EVENT_SIZE) + (tv_sec, tv_usec, type, code, value) = struct.unpack(self.EVENT_FORMAT, event) + if type == self.EV_KEY: + return InputEvent( + timestamp=tv_sec + (tv_usec / 1000000), + direction={ + self.KEY_UP: DIRECTION_UP, + self.KEY_DOWN: DIRECTION_DOWN, + self.KEY_LEFT: DIRECTION_LEFT, + self.KEY_RIGHT: DIRECTION_RIGHT, + self.KEY_ENTER: DIRECTION_MIDDLE, + }[code], + action={ + self.STATE_PRESS: ACTION_PRESSED, + self.STATE_RELEASE: ACTION_RELEASED, + self.STATE_HOLD: ACTION_HELD, + }[value]) + else: + return None + + def _wait(self, timeout=None): + """ + Waits *timeout* seconds until an event is available from the + joystick. Returns `True` if an event became available, and `False` + if the timeout expired. + """ + r, w, x = select.select([self._stick_file], [], [], timeout) + return bool(r) + + def _wrap_callback(self, fn): + # Shamelessley nicked (with some variation) from GPIO Zero :) + @wraps(fn) + def wrapper(event): + return fn() + + if fn is None: + return None + elif not callable(fn): + raise ValueError('value must be None or a callable') + elif inspect.isbuiltin(fn): + # We can't introspect the prototype of builtins. In this case we + # assume that the builtin has no (mandatory) parameters; this is + # the most reasonable assumption on the basis that pre-existing + # builtins have no knowledge of InputEvent, and the sole parameter + # we would pass is an InputEvent + return wrapper + else: + # Try binding ourselves to the argspec of the provided callable. + # If this works, assume the function is capable of accepting no + # parameters and that we have to wrap it to ignore the event + # parameter + try: + inspect.getcallargs(fn) + return wrapper + except TypeError: + try: + # If the above fails, try binding with a single tuple + # parameter. If this works, return the callback as is + inspect.getcallargs(fn, ()) + return fn + except TypeError: + raise ValueError( + 'value must be a callable which accepts up to one ' + 'mandatory parameter') + + def _start_stop_thread(self): + if self._callbacks and not self._callback_thread: + self._callback_event.clear() + self._callback_thread = Thread(target=self._callback_run) + self._callback_thread.daemon = True + self._callback_thread.start() + elif not self._callbacks and self._callback_thread: + self._callback_event.set() + self._callback_thread.join() + self._callback_thread = None + + def _callback_run(self): + while not self._callback_event.wait(0): + event = self._read() + if event: + callback = self._callbacks.get(event.direction) + if callback: + callback(event) + callback = self._callbacks.get('*') + if callback: + callback(event) + + def wait_for_event(self, emptybuffer=False): + """ + Waits until a joystick event becomes available. Returns the event, as + an `InputEvent` tuple. + + If *emptybuffer* is `True` (it defaults to `False`), any pending + events will be thrown away first. This is most useful if you are only + interested in "pressed" events. + """ + if emptybuffer: + while self._wait(0): + self._read() + while self._wait(): + event = self._read() + if event: + return event + + def get_events(self): + """ + Returns a list of all joystick events that have occurred since the last + call to `get_events`. The list contains events in the order that they + occurred. If no events have occurred in the intervening time, the + result is an empty list. + """ + result = [] + while self._wait(0): + event = self._read() + if event: + result.append(event) + return result + + @property + def direction_up(self): + """ + The function to be called when the joystick is pushed up. The function + can either take a parameter which will be the `InputEvent` tuple that + has occurred, or the function can take no parameters at all. + """ + return self._callbacks.get(DIRECTION_UP) + + @direction_up.setter + def direction_up(self, value): + self._callbacks[DIRECTION_UP] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_down(self): + """ + The function to be called when the joystick is pushed down. The + function can either take a parameter which will be the `InputEvent` + tuple that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_DOWN) + + @direction_down.setter + def direction_down(self, value): + self._callbacks[DIRECTION_DOWN] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_left(self): + """ + The function to be called when the joystick is pushed left. The + function can either take a parameter which will be the `InputEvent` + tuple that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_LEFT) + + @direction_left.setter + def direction_left(self, value): + self._callbacks[DIRECTION_LEFT] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_right(self): + """ + The function to be called when the joystick is pushed right. The + function can either take a parameter which will be the `InputEvent` + tuple that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_RIGHT) + + @direction_right.setter + def direction_right(self, value): + self._callbacks[DIRECTION_RIGHT] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_middle(self): + """ + The function to be called when the joystick middle click is pressed. The + function can either take a parameter which will be the `InputEvent` tuple + that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_MIDDLE) + + @direction_middle.setter + def direction_middle(self, value): + self._callbacks[DIRECTION_MIDDLE] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_any(self): + """ + The function to be called when the joystick is used. The function + can either take a parameter which will be the `InputEvent` tuple that + has occurred, or the function can take no parameters at all. + + This event will always be called *after* events associated with a + specific action. Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get('*') + + @direction_any.setter + def direction_any(self, value): + self._callbacks['*'] = self._wrap_callback(value) + self._start_stop_thread() + diff --git a/debian/python-sense-hat/usr/share/doc/python-sense-hat/changelog.gz b/debian/python-sense-hat/usr/share/doc/python-sense-hat/changelog.gz new file mode 100644 index 0000000000000000000000000000000000000000..f4127305a6dce1d896937eca51583fc02ac611f8 GIT binary patch literal 310 zcmV-60m=R!iwFP!000021D(>}O2aS|2Jrhn#kt(vT27L6V*G=HO;E&(RUaUBhc(is zB}tXu`0Qq5Q;}`zaup6A;m3E%$81%F&-uwOs~O)|w&UDNNdB zK(X-(CAbWi0kVB4wi{K}Z=WJ|wCJ&=A@Fo7N&!>J&Lr|fBK%#1-$bj?oQ$dnd<{Pz z{3m$5SI)k`K`X|Ri!E3=Z|g=wkA+&&gU(x{y;kLs)vYBc;I1t<)d|VNCrdYy5224? IbNm7T00rlh)Bpeg literal 0 HcmV?d00001 diff --git a/debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright b/debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright new file mode 100644 index 0000000..09392d9 --- /dev/null +++ b/debian/python-sense-hat/usr/share/doc/python-sense-hat/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/python3-sense-hat.debhelper.log b/debian/python3-sense-hat.debhelper.log new file mode 100644 index 0000000..de33f70 --- /dev/null +++ b/debian/python3-sense-hat.debhelper.log @@ -0,0 +1,19 @@ +dh_update_autotools_config +dh_auto_configure +dh_auto_build +dh_auto_test +dh_prep +dh_auto_install +dh_installdocs +dh_installchangelogs +dh_installinit +dh_perl +dh_link +dh_strip_nondeterminism +dh_compress +dh_fixperms +dh_missing +dh_installdeb +dh_gencontrol +dh_md5sums +dh_builddeb diff --git a/debian/python3-sense-hat.postinst.debhelper b/debian/python3-sense-hat.postinst.debhelper new file mode 100644 index 0000000..00683f3 --- /dev/null +++ b/debian/python3-sense-hat.postinst.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3: +if which py3compile >/dev/null 2>&1; then + py3compile -p python3-sense-hat +fi +if which pypy3compile >/dev/null 2>&1; then + pypy3compile -p python3-sense-hat || true +fi + +# End automatically added section diff --git a/debian/python3-sense-hat.prerm.debhelper b/debian/python3-sense-hat.prerm.debhelper new file mode 100644 index 0000000..632db81 --- /dev/null +++ b/debian/python3-sense-hat.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3: +if which py3clean >/dev/null 2>&1; then + py3clean -p python3-sense-hat +else + dpkg -L python3-sense-hat | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/debian/python3-sense-hat.substvars b/debian/python3-sense-hat.substvars new file mode 100644 index 0000000..3015a68 --- /dev/null +++ b/debian/python3-sense-hat.substvars @@ -0,0 +1,3 @@ +python3:Depends=python3-numpy, python3-pil, python3:any +misc:Depends= +misc:Pre-Depends= diff --git a/debian/python3-sense-hat/DEBIAN/control b/debian/python3-sense-hat/DEBIAN/control new file mode 100644 index 0000000..93297ca --- /dev/null +++ b/debian/python3-sense-hat/DEBIAN/control @@ -0,0 +1,16 @@ +Package: python3-sense-hat +Source: python-sense-hat +Version: 2.3.0~test0 +Architecture: all +Maintainer: Serge Schneider +Installed-Size: 67 +Depends: python3-numpy, python3-pil, python3:any, python3-rtimulib +Section: python +Priority: optional +Homepage: https://github.com/RPi-Distro/python-sense-hat +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/python3-sense-hat/DEBIAN/md5sums b/debian/python3-sense-hat/DEBIAN/md5sums new file mode 100644 index 0000000..b66791d --- /dev/null +++ b/debian/python3-sense-hat/DEBIAN/md5sums @@ -0,0 +1,12 @@ +35dd286b437b2916c40cdc4ed7075e50 usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/dependency_links.txt +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/requires.txt +d485d30aa3a3f58b8b0db6e71f19ce20 usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/top_level.txt +6f29e2883acf35bf03d73cc70c5e5db7 usr/lib/python3/dist-packages/sense_hat/__init__.py +776c985173201b7c38f016a3320cb8d6 usr/lib/python3/dist-packages/sense_hat/colour.py +48c3612fdc876097daccd0cad726ed91 usr/lib/python3/dist-packages/sense_hat/sense_hat.py +a90fdb250c9803bf822190b12636c73e usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png +ea665cb6f674eea5244a3e39051e45cf usr/lib/python3/dist-packages/sense_hat/sense_hat_text.txt +aecf59111307ec7eafb6192e8fbd1429 usr/lib/python3/dist-packages/sense_hat/stick.py +68832ab63c7ca094f1d1e31f84be94c8 usr/share/doc/python3-sense-hat/changelog.gz +ca593700544765aae69f76ecf48ec56b usr/share/doc/python3-sense-hat/copyright diff --git a/debian/python3-sense-hat/DEBIAN/postinst b/debian/python3-sense-hat/DEBIAN/postinst new file mode 100755 index 0000000..c10ee09 --- /dev/null +++ b/debian/python3-sense-hat/DEBIAN/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3: +if which py3compile >/dev/null 2>&1; then + py3compile -p python3-sense-hat +fi +if which pypy3compile >/dev/null 2>&1; then + pypy3compile -p python3-sense-hat || true +fi + +# End automatically added section diff --git a/debian/python3-sense-hat/DEBIAN/prerm b/debian/python3-sense-hat/DEBIAN/prerm new file mode 100755 index 0000000..e9db4dd --- /dev/null +++ b/debian/python3-sense-hat/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3: +if which py3clean >/dev/null 2>&1; then + py3clean -p python3-sense-hat +else + dpkg -L python3-sense-hat | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py new file mode 100644 index 0000000..d723807 --- /dev/null +++ b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +from .sense_hat import SenseHat, SenseHat as AstroPi +from .stick import ( + SenseStick, + InputEvent, + DIRECTION_UP, + DIRECTION_DOWN, + DIRECTION_LEFT, + DIRECTION_RIGHT, + DIRECTION_MIDDLE, + ACTION_PRESSED, + ACTION_RELEASED, + ACTION_HELD, + ) + +__version__ = '2.3.0' diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py new file mode 100644 index 0000000..c638bbf --- /dev/null +++ b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py @@ -0,0 +1,150 @@ +""" +Python library for the TCS34725 Color Sensor + +Documentation (including datasheet): https://ams.com/tcs34725#tab/documents +""" + +import smbus +import glob +from time import sleep + +def i2c_enabled(): + return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None + +def _raw_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_raw(self): + value = self.bus.read_word_data(self.ADDR, register) + return value + return get_raw + +def _byte_wrapper(register): + """ + Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. + The CRGB readings are all retrieved from the sensor in an identical fashion. + This is a factory function that implements this retrieval method. + """ + def get_byte(self): + value = self.bus.read_word_data(self.ADDR, register) // self._scaling + return value + return get_byte + + +class ColourSensor: + + # 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 + PON = 0x01 + AEN = 0x02 + AVALID = 0x01 + + GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values + GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) + + CLOCK_STEP = 0.0024 # the clock step is 2.4ms + + def __init__(self, gain=1, integration_cycles=1): + try: + self.bus = smbus.SMBus(self.BUS) + except Exception: + explanation = " (I2C is not enabled)" if not i2c_enabled() else "" + raise RuntimeError(f'Failed to initialise TCS34725 colour sensor.{explanation}') + else: + if self._id != 0x44: + raise RuntimeError(f'Not connected to TCS34725 (id: {self._id})') + self.gain = gain + self.integration_cycles=integration_cycles + self.enabled= 1 + + @property + def _id(self): + return self.bus.read_byte_data(self.ADDR, self.ID) + + @property + def enabled(self): + return self.bus.read_byte_data(self.ADDR, self.ENABLE) == (self.PON | self.AEN) + + @enabled.setter + def enabled(self, status): + if status: + self.bus.write_byte_data(self.ADDR, (self.ENABLE), self.PON) + sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." + self.bus.write_byte_data(self.ADDR, (self.ENABLE), (self.PON | self.AEN)) + else: + self.bus.write_byte_data(self.ADDR, (self.ENABLE), 0x00) + sleep(self.CLOCK_STEP) + + @property + def gain(self): + return self.GAIN_INV[self.bus.read_byte_data(self.ADDR, self.CONTROL)] + + @gain.setter + def gain(self, value): + if value in self.GAIN_MAP: + self.bus.write_byte_data(self.ADDR, self.CONTROL, self.GAIN_MAP[value]) + sleep(self.CLOCK_STEP) + else: + raise RuntimeError(f'Cannot set gain to {value}. {tuple(self.GAIN_MAP.keys())}') + + @property + def integration_cycles(self): + return 256 - self.bus.read_byte_data(self.ADDR, self.ATIME) + + @integration_cycles.setter + def integration_cycles(self, cycles): + if 1 <= cycles <= 256: + self.bus.write_byte_data(self.ADDR, self.ATIME, 256-cycles) + self.integration_time = cycles * self.CLOCK_STEP + self.max_value = 2**16 if cycles >= 64 else 1024*cycles + self._scaling = self.max_value // 256 + sleep(self.CLOCK_STEP) + else: + raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') + + @property + def colour_raw(self): + 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] + ) + + @property + def colour(self): + return tuple(reading // self._scaling for reading in self.colour_raw) + + clear_raw = property(_raw_wrapper(CDATA)) + red_raw = property(_raw_wrapper(RDATA)) + green_raw = property(_raw_wrapper(GDATA)) + blue_raw = property(_raw_wrapper(BDATA)) + + clear = property(_byte_wrapper(CDATA)) + red = property(_byte_wrapper(RDATA)) + green = property(_byte_wrapper(GDATA)) + blue = property(_byte_wrapper(BDATA)) diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py new file mode 100644 index 0000000..8494309 --- /dev/null +++ b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py @@ -0,0 +1,883 @@ +#!/usr/bin/python +import struct +import os +import sys +import math +import time +import numpy as np +import shutil +import glob +import RTIMU # custom version +import pwd +import array +import fcntl +from PIL import Image # pillow +from copy import deepcopy + +from .stick import SenseStick +from .colour import ColourSensor + +class SenseHat(object): + + SENSE_HAT_FB_NAME = 'RPi-Sense FB' + SENSE_HAT_FB_FBIOGET_GAMMA = 61696 + SENSE_HAT_FB_FBIOSET_GAMMA = 61697 + SENSE_HAT_FB_FBIORESET_GAMMA = 61698 + SENSE_HAT_FB_GAMMA_DEFAULT = 0 + SENSE_HAT_FB_GAMMA_LOW = 1 + SENSE_HAT_FB_GAMMA_USER = 2 + SETTINGS_HOME_PATH = '.config/sense_hat' + + def __init__( + self, + imu_settings_file='RTIMULib', + text_assets='sense_hat_text' + ): + + self._fb_device = self._get_fb_device() + if self._fb_device is None: + raise OSError('Cannot detect %s device' % self.SENSE_HAT_FB_NAME) + + if not glob.glob('/dev/i2c*'): + raise OSError('Cannot access I2C. Please ensure I2C is enabled in raspi-config') + + # 0 is With B+ HDMI port facing downwards + pix_map0 = np.array([ + [0, 1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13, 14, 15], + [16, 17, 18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29, 30, 31], + [32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47], + [48, 49, 50, 51, 52, 53, 54, 55], + [56, 57, 58, 59, 60, 61, 62, 63] + ], int) + + pix_map90 = np.rot90(pix_map0) + pix_map180 = np.rot90(pix_map90) + pix_map270 = np.rot90(pix_map180) + + self._pix_map = { + 0: pix_map0, + 90: pix_map90, + 180: pix_map180, + 270: pix_map270 + } + + self._rotation = 0 + + # Load text assets + dir_path = os.path.dirname(__file__) + self._load_text_assets( + os.path.join(dir_path, '%s.png' % text_assets), + os.path.join(dir_path, '%s.txt' % text_assets) + ) + + # Load IMU settings and calibration data + self._imu_settings = self._get_settings_file(imu_settings_file) + self._imu = RTIMU.RTIMU(self._imu_settings) + self._imu_init = False # Will be initialised as and when needed + self._pressure = RTIMU.RTPressure(self._imu_settings) + self._pressure_init = False # Will be initialised as and when needed + self._humidity = RTIMU.RTHumidity(self._imu_settings) + self._humidity_init = False # Will be initialised as and when needed + self._last_orientation = {'pitch': 0, 'roll': 0, 'yaw': 0} + raw = {'x': 0, 'y': 0, 'z': 0} + self._last_compass_raw = deepcopy(raw) + self._last_gyro_raw = deepcopy(raw) + self._last_accel_raw = deepcopy(raw) + self._compass_enabled = False + self._gyro_enabled = False + self._accel_enabled = False + self._stick = SenseStick() + + # initialise the TCS34725 colour sensor (if possible) + try: + self._colour = ColourSensor() + except: + pass + + #### + # Text assets + #### + + # Text asset files are rotated right through 90 degrees to allow blocks of + # 40 contiguous pixels to represent one 5 x 8 character. These are stored + # in a 8 x 640 pixel png image with characters arranged adjacently + # Consequently we must rotate the pixel map left through 90 degrees to + # compensate when drawing text + + def _load_text_assets(self, text_image_file, text_file): + """ + Internal. Builds a character indexed dictionary of pixels used by the + show_message function below + """ + + text_pixels = self.load_image(text_image_file, False) + with open(text_file, 'r') as f: + loaded_text = f.read() + self._text_dict = {} + for index, s in enumerate(loaded_text): + start = index * 40 + end = start + 40 + char = text_pixels[start:end] + self._text_dict[s] = char + + def _trim_whitespace(self, char): # For loading text assets only + """ + Internal. Trims white space pixels from the front and back of loaded + text characters + """ + + psum = lambda x: sum(sum(x, [])) + if psum(char) > 0: + is_empty = True + while is_empty: # From front + row = char[0:8] + is_empty = psum(row) == 0 + if is_empty: + del char[0:8] + is_empty = True + while is_empty: # From back + row = char[-8:] + is_empty = psum(row) == 0 + if is_empty: + del char[-8:] + return char + + def _get_settings_file(self, imu_settings_file): + """ + Internal. Logic to check for a system wide RTIMU ini file. This is + copied to the home folder if one is not already found there. + """ + + ini_file = '%s.ini' % imu_settings_file + + home_dir = pwd.getpwuid(os.getuid())[5] + home_path = os.path.join(home_dir, self.SETTINGS_HOME_PATH) + if not os.path.exists(home_path): + os.makedirs(home_path) + + home_file = os.path.join(home_path, ini_file) + home_exists = os.path.isfile(home_file) + system_file = os.path.join('/etc', ini_file) + system_exists = os.path.isfile(system_file) + + if system_exists and not home_exists: + shutil.copyfile(system_file, home_file) + + return RTIMU.Settings(os.path.join(home_path, imu_settings_file)) # RTIMU will add .ini internally + + def _get_fb_device(self): + """ + Internal. Finds the correct frame buffer device for the sense HAT + and returns its /dev name. + """ + + device = None + + for fb in glob.glob('/sys/class/graphics/fb*'): + name_file = os.path.join(fb, 'name') + if os.path.isfile(name_file): + with open(name_file, 'r') as f: + name = f.read() + if name.strip() == self.SENSE_HAT_FB_NAME: + fb_device = fb.replace(os.path.dirname(fb), '/dev') + if os.path.exists(fb_device): + device = fb_device + break + + return device + + #### + # Joystick + #### + + @property + def stick(self): + return self._stick + + #### + # Colour sensor + #### + + @property + def colour(self): + return self._colour + + @property + def color(self): + return self._colour + + def has_colour_sensor(self): + try: + self._colour + except: + return False + else: + return True + + #### + # LED Matrix + #### + + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, r): + self.set_rotation(r, True) + + def set_rotation(self, r=0, redraw=True): + """ + Sets the LED matrix rotation for viewing, adjust if the Pi is upside + down or sideways. 0 is with the Pi HDMI port facing downwards + """ + + if r in self._pix_map.keys(): + if redraw: + pixel_list = self.get_pixels() + self._rotation = r + if redraw: + self.set_pixels(pixel_list) + else: + raise ValueError('Rotation must be 0, 90, 180 or 270 degrees') + + def _pack_bin(self, pix): + """ + Internal. Encodes python list [R,G,B] into 16 bit RGB565 + """ + + r = (pix[0] >> 3) & 0x1F + g = (pix[1] >> 2) & 0x3F + b = (pix[2] >> 3) & 0x1F + bits16 = (r << 11) + (g << 5) + b + return struct.pack('H', bits16) + + def _unpack_bin(self, packed): + """ + Internal. Decodes 16 bit RGB565 into python list [R,G,B] + """ + + output = struct.unpack('H', packed) + bits16 = output[0] + r = (bits16 & 0xF800) >> 11 + g = (bits16 & 0x7E0) >> 5 + b = (bits16 & 0x1F) + return [int(r << 3), int(g << 2), int(b << 3)] + + def flip_h(self, redraw=True): + """ + Flip LED matrix horizontal + """ + + pixel_list = self.get_pixels() + flipped = [] + for i in range(8): + offset = i * 8 + flipped.extend(reversed(pixel_list[offset:offset + 8])) + if redraw: + self.set_pixels(flipped) + return flipped + + def flip_v(self, redraw=True): + """ + Flip LED matrix vertical + """ + + pixel_list = self.get_pixels() + flipped = [] + for i in reversed(range(8)): + offset = i * 8 + flipped.extend(pixel_list[offset:offset + 8]) + if redraw: + self.set_pixels(flipped) + return flipped + + 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 + and 255 + """ + + if len(pixel_list) != 64: + raise ValueError('Pixel lists must have 64 elements') + + for index, pix in enumerate(pixel_list): + if 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: + if element > 255 or element < 0: + raise ValueError('Pixel at index %d is invalid. Pixel elements must be between 0 and 255' % index) + + with open(self._fb_device, 'wb') as f: + map = self._pix_map[self._rotation] + for index, pix in enumerate(pixel_list): + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[index // 8][index % 8] * 2) # row, column + f.write(self._pack_bin(pix)) + + def get_pixels(self): + """ + Returns a list containing 64 smaller lists of [R,G,B] pixels + representing what is currently displayed on the LED matrix + """ + + pixel_list = [] + with open(self._fb_device, 'rb') as f: + map = self._pix_map[self._rotation] + for row in range(8): + for col in range(8): + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[row][col] * 2) # row, column + pixel_list.append(self._unpack_bin(f.read(2))) + return pixel_list + + def set_pixel(self, x, y, *args): + """ + Updates the single [R,G,B] pixel specified by x and y on the LED matrix + Top left = 0,0 Bottom right = 7,7 + + e.g. ap.set_pixel(x, y, r, g, b) + or + pixel = (r, g, b) + 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) + + if x > 7 or x < 0: + raise ValueError('X position must be between 0 and 7') + + if y > 7 or y < 0: + raise ValueError('Y position must be between 0 and 7') + + for element in pixel: + if element > 255 or element < 0: + raise ValueError('Pixel elements must be between 0 and 255') + + with open(self._fb_device, 'wb') as f: + map = self._pix_map[self._rotation] + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[y][x] * 2) # row, column + f.write(self._pack_bin(pixel)) + + def get_pixel(self, x, y): + """ + Returns a list of [R,G,B] representing the pixel specified by x and y + on the LED matrix. Top left = 0,0 Bottom right = 7,7 + """ + + if x > 7 or x < 0: + raise ValueError('X position must be between 0 and 7') + + if y > 7 or y < 0: + raise ValueError('Y position must be between 0 and 7') + + pix = None + + with open(self._fb_device, 'rb') as f: + map = self._pix_map[self._rotation] + # Two bytes per pixel in fb memory, 16 bit RGB565 + f.seek(map[y][x] * 2) # row, column + pix = self._unpack_bin(f.read(2)) + + return pix + + def load_image(self, file_path, redraw=True): + """ + Accepts a path to an 8 x 8 image file and updates the LED matrix with + the image + """ + + if not os.path.exists(file_path): + raise IOError('%s not found' % file_path) + + img = Image.open(file_path).convert('RGB') + pixel_list = list(map(list, img.getdata())) + + if redraw: + self.set_pixels(pixel_list) + + return pixel_list + + def clear(self, *args): + """ + Clears the LED matrix with a single colour, default is black / off + + e.g. ap.clear() + or + ap.clear(r, g, b) + or + colour = (r, g, b) + ap.clear(colour) + """ + + black = (0, 0, 0) # default + + if len(args) == 0: + colour = black + elif len(args) == 1: + colour = args[0] + elif len(args) == 3: + colour = args + else: + raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') + + self.set_pixels([colour] * 64) + + def _get_char_pixels(self, s): + """ + Internal. Safeguards the character indexed dictionary for the + show_message function below + """ + + if len(s) == 1 and s in self._text_dict.keys(): + return list(self._text_dict[s]) + else: + return list(self._text_dict['?']) + + def show_message( + self, + text_string, + scroll_speed=.1, + text_colour=[255, 255, 255], + back_colour=[0, 0, 0] + ): + """ + Scrolls a string of text across the LED matrix using the specified + speed and colours + """ + + # We must rotate the pixel map left through 90 degrees when drawing + # text, see _load_text_assets + previous_rotation = self._rotation + self._rotation -= 90 + if self._rotation < 0: + self._rotation = 270 + dummy_colour = [None, None, None] + string_padding = [dummy_colour] * 64 + letter_padding = [dummy_colour] * 8 + # Build pixels from dictionary + scroll_pixels = [] + scroll_pixels.extend(string_padding) + for s in text_string: + scroll_pixels.extend(self._trim_whitespace(self._get_char_pixels(s))) + scroll_pixels.extend(letter_padding) + scroll_pixels.extend(string_padding) + # Recolour pixels as necessary + coloured_pixels = [ + text_colour if pixel == [255, 255, 255] else back_colour + for pixel in scroll_pixels + ] + # Shift right by 8 pixels per frame to scroll + scroll_length = len(coloured_pixels) // 8 + for i in range(scroll_length - 8): + start = i * 8 + end = start + 64 + self.set_pixels(coloured_pixels[start:end]) + time.sleep(scroll_speed) + self._rotation = previous_rotation + + def show_letter( + self, + s, + text_colour=[255, 255, 255], + back_colour=[0, 0, 0] + ): + """ + Displays a single text character on the LED matrix using the specified + colours + """ + + if len(s) > 1: + raise ValueError('Only one character may be passed into this method') + # We must rotate the pixel map left through 90 degrees when drawing + # text, see _load_text_assets + previous_rotation = self._rotation + self._rotation -= 90 + if self._rotation < 0: + self._rotation = 270 + dummy_colour = [None, None, None] + pixel_list = [dummy_colour] * 8 + pixel_list.extend(self._get_char_pixels(s)) + pixel_list.extend([dummy_colour] * 16) + coloured_pixels = [ + text_colour if pixel == [255, 255, 255] else back_colour + for pixel in pixel_list + ] + self.set_pixels(coloured_pixels) + self._rotation = previous_rotation + + @property + def gamma(self): + buffer = array.array('B', [0]*32) + with open(self._fb_device) as f: + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOGET_GAMMA, buffer) + return list(buffer) + + @gamma.setter + def gamma(self, buffer): + if len(buffer) is not 32: + raise ValueError('Gamma array must be of length 32') + + if not all(b <= 31 for b in buffer): + raise ValueError('Gamma values must be bewteen 0 and 31') + + if not isinstance(buffer, array.array): + buffer = array.array('B', buffer) + + with open(self._fb_device) as f: + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOSET_GAMMA, buffer) + + def gamma_reset(self): + """ + Resets the LED matrix gamma correction to default + """ + + with open(self._fb_device) as f: + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, self.SENSE_HAT_FB_GAMMA_DEFAULT) + + @property + def low_light(self): + return self.gamma == [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10] + + @low_light.setter + def low_light(self, value): + with open(self._fb_device) as f: + cmd = self.SENSE_HAT_FB_GAMMA_LOW if value else self.SENSE_HAT_FB_GAMMA_DEFAULT + fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, cmd) + + #### + # Environmental sensors + #### + + def _init_humidity(self): + """ + Internal. Initialises the humidity sensor via RTIMU + """ + + if not self._humidity_init: + self._humidity_init = self._humidity.humidityInit() + if not self._humidity_init: + raise OSError('Humidity Init Failed') + + def _init_pressure(self): + """ + Internal. Initialises the pressure sensor via RTIMU + """ + + if not self._pressure_init: + self._pressure_init = self._pressure.pressureInit() + if not self._pressure_init: + raise OSError('Pressure Init Failed') + + def get_humidity(self): + """ + Returns the percentage of relative humidity + """ + + self._init_humidity() # Ensure humidity sensor is initialised + humidity = 0 + data = self._humidity.humidityRead() + if (data[0]): # Humidity valid + humidity = data[1] + return humidity + + @property + def humidity(self): + return self.get_humidity() + + def get_temperature_from_humidity(self): + """ + Returns the temperature in Celsius from the humidity sensor + """ + + self._init_humidity() # Ensure humidity sensor is initialised + temp = 0 + data = self._humidity.humidityRead() + if (data[2]): # Temp valid + temp = data[3] + return temp + + def get_temperature_from_pressure(self): + """ + Returns the temperature in Celsius from the pressure sensor + """ + + self._init_pressure() # Ensure pressure sensor is initialised + temp = 0 + data = self._pressure.pressureRead() + if (data[2]): # Temp valid + temp = data[3] + return temp + + def get_temperature(self): + """ + Returns the temperature in Celsius + """ + + return self.get_temperature_from_humidity() + + @property + def temp(self): + return self.get_temperature_from_humidity() + + @property + def temperature(self): + return self.get_temperature_from_humidity() + + def get_pressure(self): + """ + Returns the pressure in Millibars + """ + + self._init_pressure() # Ensure pressure sensor is initialised + pressure = 0 + data = self._pressure.pressureRead() + if (data[0]): # Pressure valid + pressure = data[1] + return pressure + + @property + def pressure(self): + return self.get_pressure() + + #### + # IMU Sensor + #### + + def _init_imu(self): + """ + Internal. Initialises the IMU sensor via RTIMU + """ + + if not self._imu_init: + self._imu_init = self._imu.IMUInit() + if self._imu_init: + self._imu_poll_interval = self._imu.IMUGetPollInterval() * 0.001 + # Enable everything on IMU + self.set_imu_config(True, True, True) + else: + raise OSError('IMU Init Failed') + + def set_imu_config(self, compass_enabled, gyro_enabled, accel_enabled): + """ + Enables and disables the gyroscope, accelerometer and/or magnetometer + input to the orientation functions + """ + + # If the consuming code always calls this just before reading the IMU + # the IMU consistently fails to read. So prevent unnecessary calls to + # IMU config functions using state variables + + self._init_imu() # Ensure imu is initialised + + if (not isinstance(compass_enabled, bool) + or not isinstance(gyro_enabled, bool) + or not isinstance(accel_enabled, bool)): + raise TypeError('All set_imu_config parameters must be of boolean type') + + if self._compass_enabled != compass_enabled: + self._compass_enabled = compass_enabled + self._imu.setCompassEnable(self._compass_enabled) + + if self._gyro_enabled != gyro_enabled: + self._gyro_enabled = gyro_enabled + self._imu.setGyroEnable(self._gyro_enabled) + + if self._accel_enabled != accel_enabled: + self._accel_enabled = accel_enabled + self._imu.setAccelEnable(self._accel_enabled) + + def _read_imu(self): + """ + Internal. Tries to read the IMU sensor three times before giving up + """ + + self._init_imu() # Ensure imu is initialised + + attempts = 0 + success = False + + while not success and attempts < 3: + success = self._imu.IMURead() + attempts += 1 + time.sleep(self._imu_poll_interval) + + return success + + def _get_raw_data(self, is_valid_key, data_key): + """ + Internal. Returns the specified raw data from the IMU when valid + """ + + result = None + + if self._read_imu(): + data = self._imu.getIMUData() + if data[is_valid_key]: + raw = data[data_key] + result = { + 'x': raw[0], + 'y': raw[1], + 'z': raw[2] + } + + return result + + def get_orientation_radians(self): + """ + Returns a dictionary object to represent the current orientation in + radians using the aircraft principal axes of pitch, roll and yaw + """ + + raw = self._get_raw_data('fusionPoseValid', 'fusionPose') + + if raw is not None: + raw['roll'] = raw.pop('x') + raw['pitch'] = raw.pop('y') + raw['yaw'] = raw.pop('z') + self._last_orientation = raw + + return deepcopy(self._last_orientation) + + @property + def orientation_radians(self): + return self.get_orientation_radians() + + def get_orientation_degrees(self): + """ + Returns a dictionary object to represent the current orientation + in degrees, 0 to 360, using the aircraft principal axes of + pitch, roll and yaw + """ + + orientation = self.get_orientation_radians() + for key, val in orientation.items(): + deg = math.degrees(val) # Result is -180 to +180 + orientation[key] = deg + 360 if deg < 0 else deg + return orientation + + def get_orientation(self): + return self.get_orientation_degrees() + + @property + def orientation(self): + return self.get_orientation_degrees() + + def get_compass(self): + """ + Gets the direction of North from the magnetometer in degrees + """ + + self.set_imu_config(True, False, False) + orientation = self.get_orientation_degrees() + if type(orientation) is dict and 'yaw' in orientation.keys(): + return orientation['yaw'] + else: + return None + + @property + def compass(self): + return self.get_compass() + + def get_compass_raw(self): + """ + Magnetometer x y z raw data in uT (micro teslas) + """ + + raw = self._get_raw_data('compassValid', 'compass') + + if raw is not None: + self._last_compass_raw = raw + + return deepcopy(self._last_compass_raw) + + @property + def compass_raw(self): + return self.get_compass_raw() + + def get_gyroscope(self): + """ + Gets the orientation in degrees from the gyroscope only + """ + + self.set_imu_config(False, True, False) + return self.get_orientation_degrees() + + @property + def gyro(self): + return self.get_gyroscope() + + @property + def gyroscope(self): + return self.get_gyroscope() + + def get_gyroscope_raw(self): + """ + Gyroscope x y z raw data in radians per second + """ + + raw = self._get_raw_data('gyroValid', 'gyro') + + if raw is not None: + self._last_gyro_raw = raw + + return deepcopy(self._last_gyro_raw) + + @property + def gyro_raw(self): + return self.get_gyroscope_raw() + + @property + def gyroscope_raw(self): + return self.get_gyroscope_raw() + + def get_accelerometer(self): + """ + Gets the orientation in degrees from the accelerometer only + """ + + self.set_imu_config(False, False, True) + return self.get_orientation_degrees() + + @property + def accel(self): + return self.get_accelerometer() + + @property + def accelerometer(self): + return self.get_accelerometer() + + def get_accelerometer_raw(self): + """ + Accelerometer x y z raw data in Gs + """ + + raw = self._get_raw_data('accelValid', 'accel') + + if raw is not None: + self._last_accel_raw = raw + + return deepcopy(self._last_accel_raw) + + @property + def accel_raw(self): + return self.get_accelerometer_raw() + + @property + def accelerometer_raw(self): + return self.get_accelerometer_raw() diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png new file mode 100644 index 0000000000000000000000000000000000000000..747ed9cbe233d9dfe6d5a08bba122965791b81b4 GIT binary patch literal 908 zcmV;719SX|P)z3l1qDCPoV#0000PbVXQnLvL+uWo~o;Lvm$dbY)~9 zcWHEJAV*0}P*;Ht7XSbQ21!IgRA}DqTFG+5APhAc{{NThp_$kQEl)^^J5%Y@nqcj_ zkK<3k7~}dcNxy6BZ(D0yuX^<0M(ac|&xfCL6$k&@XU+_0V~p0C!F&uFORI*%e?!zq zV}*0RD&xkDO8#KFq~c(2SZE-#IGUg)3G5#{Nf3{+*RE)P|EBk^L_Vf7ATk&t z0(9>8UkS%C#$eBgWm^-;EWX<@k6+RtNw1~r)0u<`r*;O|?&>(0pKI?qsaOeRIENgk zU&U}Wwb>FGLMP^FWzC>U6|Jq>8y`Haz8uZi_576mW3OgladeSU5huhjaTd?enyz^2jBVE3*0R}>W88RA{ zKHDfd88k+rTUkxKL3x#n-`WYHJ<-o^Sq<7or-audVW}7Igl+EwZiJ1&xy6m=^H^fT znri6l1Yo-qFIXfE!ejNsj^&Mmc5$HX3M^e{*AG;E;d)oKPpusDNKB(jTx|tf8Gm7M z>!Y;ZF(x#)KMj250|ypnDm>259De0F6`Ala|t)J=mU8$7!|e>C8@RSPU96Pw2P(?Bh{n^lj3f`WpAf`WpA if`WpAf`WpAe+ob22BS=Py=(XY0000<0123456789.=)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?,;:|@%[&_']\~ diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py new file mode 100644 index 0000000..896a9f8 --- /dev/null +++ b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py @@ -0,0 +1,308 @@ +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, + ) +native_str = str +str = type('') + +import io +import os +import glob +import errno +import struct +import select +import inspect +from functools import wraps +from collections import namedtuple +from threading import Thread, Event + + +DIRECTION_UP = 'up' +DIRECTION_DOWN = 'down' +DIRECTION_LEFT = 'left' +DIRECTION_RIGHT = 'right' +DIRECTION_MIDDLE = 'middle' + +ACTION_PRESSED = 'pressed' +ACTION_RELEASED = 'released' +ACTION_HELD = 'held' + + +InputEvent = namedtuple('InputEvent', ('timestamp', 'direction', 'action')) + + +class SenseStick(object): + """ + Represents the joystick on the Sense HAT. + """ + SENSE_HAT_EVDEV_NAME = 'Raspberry Pi Sense HAT Joystick' + EVENT_FORMAT = native_str('llHHI') + EVENT_SIZE = struct.calcsize(EVENT_FORMAT) + + EV_KEY = 0x01 + + STATE_RELEASE = 0 + STATE_PRESS = 1 + STATE_HOLD = 2 + + KEY_UP = 103 + KEY_LEFT = 105 + KEY_RIGHT = 106 + KEY_DOWN = 108 + KEY_ENTER = 28 + + def __init__(self): + self._stick_file = io.open(self._stick_device(), 'rb', buffering=0) + self._callbacks = {} + self._callback_thread = None + self._callback_event = Event() + + def close(self): + if self._stick_file: + self._callbacks.clear() + self._start_stop_thread() + self._stick_file.close() + self._stick_file = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.close() + + def _stick_device(self): + """ + Discovers the filename of the evdev device that represents the Sense + HAT's joystick. + """ + for evdev in glob.glob('/sys/class/input/event*'): + try: + with io.open(os.path.join(evdev, 'device', 'name'), 'r') as f: + if f.read().strip() == self.SENSE_HAT_EVDEV_NAME: + return os.path.join('/dev', 'input', os.path.basename(evdev)) + except IOError as e: + if e.errno != errno.ENOENT: + raise + raise RuntimeError('unable to locate SenseHAT joystick device') + + def _read(self): + """ + Reads a single event from the joystick, blocking until one is + available. Returns `None` if a non-key event was read, or an + `InputEvent` tuple describing the event otherwise. + """ + event = self._stick_file.read(self.EVENT_SIZE) + (tv_sec, tv_usec, type, code, value) = struct.unpack(self.EVENT_FORMAT, event) + if type == self.EV_KEY: + return InputEvent( + timestamp=tv_sec + (tv_usec / 1000000), + direction={ + self.KEY_UP: DIRECTION_UP, + self.KEY_DOWN: DIRECTION_DOWN, + self.KEY_LEFT: DIRECTION_LEFT, + self.KEY_RIGHT: DIRECTION_RIGHT, + self.KEY_ENTER: DIRECTION_MIDDLE, + }[code], + action={ + self.STATE_PRESS: ACTION_PRESSED, + self.STATE_RELEASE: ACTION_RELEASED, + self.STATE_HOLD: ACTION_HELD, + }[value]) + else: + return None + + def _wait(self, timeout=None): + """ + Waits *timeout* seconds until an event is available from the + joystick. Returns `True` if an event became available, and `False` + if the timeout expired. + """ + r, w, x = select.select([self._stick_file], [], [], timeout) + return bool(r) + + def _wrap_callback(self, fn): + # Shamelessley nicked (with some variation) from GPIO Zero :) + @wraps(fn) + def wrapper(event): + return fn() + + if fn is None: + return None + elif not callable(fn): + raise ValueError('value must be None or a callable') + elif inspect.isbuiltin(fn): + # We can't introspect the prototype of builtins. In this case we + # assume that the builtin has no (mandatory) parameters; this is + # the most reasonable assumption on the basis that pre-existing + # builtins have no knowledge of InputEvent, and the sole parameter + # we would pass is an InputEvent + return wrapper + else: + # Try binding ourselves to the argspec of the provided callable. + # If this works, assume the function is capable of accepting no + # parameters and that we have to wrap it to ignore the event + # parameter + try: + inspect.getcallargs(fn) + return wrapper + except TypeError: + try: + # If the above fails, try binding with a single tuple + # parameter. If this works, return the callback as is + inspect.getcallargs(fn, ()) + return fn + except TypeError: + raise ValueError( + 'value must be a callable which accepts up to one ' + 'mandatory parameter') + + def _start_stop_thread(self): + if self._callbacks and not self._callback_thread: + self._callback_event.clear() + self._callback_thread = Thread(target=self._callback_run) + self._callback_thread.daemon = True + self._callback_thread.start() + elif not self._callbacks and self._callback_thread: + self._callback_event.set() + self._callback_thread.join() + self._callback_thread = None + + def _callback_run(self): + while not self._callback_event.wait(0): + event = self._read() + if event: + callback = self._callbacks.get(event.direction) + if callback: + callback(event) + callback = self._callbacks.get('*') + if callback: + callback(event) + + def wait_for_event(self, emptybuffer=False): + """ + Waits until a joystick event becomes available. Returns the event, as + an `InputEvent` tuple. + + If *emptybuffer* is `True` (it defaults to `False`), any pending + events will be thrown away first. This is most useful if you are only + interested in "pressed" events. + """ + if emptybuffer: + while self._wait(0): + self._read() + while self._wait(): + event = self._read() + if event: + return event + + def get_events(self): + """ + Returns a list of all joystick events that have occurred since the last + call to `get_events`. The list contains events in the order that they + occurred. If no events have occurred in the intervening time, the + result is an empty list. + """ + result = [] + while self._wait(0): + event = self._read() + if event: + result.append(event) + return result + + @property + def direction_up(self): + """ + The function to be called when the joystick is pushed up. The function + can either take a parameter which will be the `InputEvent` tuple that + has occurred, or the function can take no parameters at all. + """ + return self._callbacks.get(DIRECTION_UP) + + @direction_up.setter + def direction_up(self, value): + self._callbacks[DIRECTION_UP] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_down(self): + """ + The function to be called when the joystick is pushed down. The + function can either take a parameter which will be the `InputEvent` + tuple that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_DOWN) + + @direction_down.setter + def direction_down(self, value): + self._callbacks[DIRECTION_DOWN] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_left(self): + """ + The function to be called when the joystick is pushed left. The + function can either take a parameter which will be the `InputEvent` + tuple that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_LEFT) + + @direction_left.setter + def direction_left(self, value): + self._callbacks[DIRECTION_LEFT] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_right(self): + """ + The function to be called when the joystick is pushed right. The + function can either take a parameter which will be the `InputEvent` + tuple that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_RIGHT) + + @direction_right.setter + def direction_right(self, value): + self._callbacks[DIRECTION_RIGHT] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_middle(self): + """ + The function to be called when the joystick middle click is pressed. The + function can either take a parameter which will be the `InputEvent` tuple + that has occurred, or the function can take no parameters at all. + + Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get(DIRECTION_MIDDLE) + + @direction_middle.setter + def direction_middle(self, value): + self._callbacks[DIRECTION_MIDDLE] = self._wrap_callback(value) + self._start_stop_thread() + + @property + def direction_any(self): + """ + The function to be called when the joystick is used. The function + can either take a parameter which will be the `InputEvent` tuple that + has occurred, or the function can take no parameters at all. + + This event will always be called *after* events associated with a + specific action. Assign `None` to prevent this event from being fired. + """ + return self._callbacks.get('*') + + @direction_any.setter + def direction_any(self, value): + self._callbacks['*'] = self._wrap_callback(value) + self._start_stop_thread() + diff --git a/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/changelog.gz b/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/changelog.gz new file mode 100644 index 0000000000000000000000000000000000000000..f4127305a6dce1d896937eca51583fc02ac611f8 GIT binary patch literal 310 zcmV-60m=R!iwFP!000021D(>}O2aS|2Jrhn#kt(vT27L6V*G=HO;E&(RUaUBhc(is zB}tXu`0Qq5Q;}`zaup6A;m3E%$81%F&-uwOs~O)|w&UDNNdB zK(X-(CAbWi0kVB4wi{K}Z=WJ|wCJ&=A@Fo7N&!>J&Lr|fBK%#1-$bj?oQ$dnd<{Pz z{3m$5SI)k`K`X|Ri!E3=Z|g=wkA+&&gU(x{y;kLs)vYBc;I1t<)d|VNCrdYy5224? IbNm7T00rlh)Bpeg literal 0 HcmV?d00001 diff --git a/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright b/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright new file mode 100644 index 0000000..09392d9 --- /dev/null +++ b/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/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) From ac0b20e8365d0a553c2f6e3d54c47ecd5fbec5b3 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 5 Jan 2021 13:03:42 +0000 Subject: [PATCH 08/37] Tidy up and improve error-handling --- sense_hat/colour.py | 149 ++++++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 66 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index c638bbf..2d9b4fb 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -1,6 +1,5 @@ """ Python library for the TCS34725 Color Sensor - Documentation (including datasheet): https://ams.com/tcs34725#tab/documents """ @@ -8,7 +7,42 @@ import glob from time import sleep + +# 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 +PON = 0x01 +AEN = 0x02 +AVALID = 0x01 + +GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values +GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) + +CLOCK_STEP = 0.0024 # the clock step is 2.4ms + +_error_str = "Failed to initialise TCS34725 colour sensor." + 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 _raw_wrapper(register): @@ -18,7 +52,7 @@ def _raw_wrapper(register): This is a factory function that implements this retrieval method. """ def get_raw(self): - value = self.bus.read_word_data(self.ADDR, register) + value = self.bus.read_word_data(ADDR, register) return value return get_raw @@ -29,105 +63,86 @@ def _byte_wrapper(register): This is a factory function that implements this retrieval method. """ def get_byte(self): - value = self.bus.read_word_data(self.ADDR, register) // self._scaling + value = self.bus.read_word_data(ADDR, register) // self._scaling return value return get_byte class ColourSensor: - # 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 - PON = 0x01 - AEN = 0x02 - AVALID = 0x01 - - GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values - GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) - - CLOCK_STEP = 0.0024 # the clock step is 2.4ms - def __init__(self, gain=1, integration_cycles=1): try: - self.bus = smbus.SMBus(self.BUS) - except Exception: + self.bus = smbus.SMBus(BUS) + except Exception as e: explanation = " (I2C is not enabled)" if not i2c_enabled() else "" - raise RuntimeError(f'Failed to initialise TCS34725 colour sensor.{explanation}') - else: - if self._id != 0x44: - raise RuntimeError(f'Not connected to TCS34725 (id: {self._id})') - self.gain = gain - self.integration_cycles=integration_cycles - self.enabled= 1 + raise RuntimeError(f'{_error_str}{explanation}') from e + + try: + id = self.bus.read_byte_data(ADDR, ID) + except Exception as e: + explanation = " (sensor not present)" + raise RuntimeError(f'{_error_str}{explanation}') from e - @property - def _id(self): - return self.bus.read_byte_data(self.ADDR, self.ID) + if id != 0x44: + explanation = f" (different device id detected: {id})" + raise RuntimeError(f'{_error_str}{explanation}') + + self.gain = gain + self.integration_cycles=integration_cycles + self.enabled = 1 @property def enabled(self): - return self.bus.read_byte_data(self.ADDR, self.ENABLE) == (self.PON | self.AEN) + return self.bus.read_byte_data(ADDR, ENABLE) == (PON | AEN) @enabled.setter def enabled(self, status): if status: - self.bus.write_byte_data(self.ADDR, (self.ENABLE), self.PON) - sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." - self.bus.write_byte_data(self.ADDR, (self.ENABLE), (self.PON | self.AEN)) + self.bus.write_byte_data(ADDR, ENABLE, PON) + sleep(CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." + self.bus.write_byte_data(ADDR, ENABLE, (PON | AEN)) else: - self.bus.write_byte_data(self.ADDR, (self.ENABLE), 0x00) - sleep(self.CLOCK_STEP) + self.bus.write_byte_data(ADDR, ENABLE, 0x00) + sleep(CLOCK_STEP) @property def gain(self): - return self.GAIN_INV[self.bus.read_byte_data(self.ADDR, self.CONTROL)] + return GAIN_INV[self.bus.read_byte_data(ADDR, CONTROL)] @gain.setter def gain(self, value): - if value in self.GAIN_MAP: - self.bus.write_byte_data(self.ADDR, self.CONTROL, self.GAIN_MAP[value]) - sleep(self.CLOCK_STEP) + if value in GAIN_MAP: + self.bus.write_byte_data(ADDR, CONTROL, GAIN_MAP[value]) + sleep(CLOCK_STEP) else: - raise RuntimeError(f'Cannot set gain to {value}. {tuple(self.GAIN_MAP.keys())}') + raise RuntimeError(f'Cannot set gain to {value}. Values: {tuple(GAIN_MAP.keys())}') @property def integration_cycles(self): - return 256 - self.bus.read_byte_data(self.ADDR, self.ATIME) + return 256 - self.bus.read_byte_data(ADDR, ATIME) @integration_cycles.setter def integration_cycles(self, cycles): if 1 <= cycles <= 256: - self.bus.write_byte_data(self.ADDR, self.ATIME, 256-cycles) - self.integration_time = cycles * self.CLOCK_STEP - self.max_value = 2**16 if cycles >= 64 else 1024*cycles - self._scaling = self.max_value // 256 - sleep(self.CLOCK_STEP) + self.bus.write_byte_data(ADDR, ATIME, 256-cycles) + self._integration_time = cycles * CLOCK_STEP + self._max_value = 2**16 if cycles >= 64 else 1024*cycles + self._scaling = self._max_value // 256 + sleep(CLOCK_STEP) else: raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') + @property + def integration_time(self): + return self._integration_time + + @property + def max_raw(self): + return self._max_value + @property def colour_raw(self): - block = self.bus.read_i2c_block_data(self.ADDR, self.CDATA, 8) + block = self.bus.read_i2c_block_data(ADDR, CDATA, 8) return ( (block[3] << 8) + block[2], (block[5] << 8) + block[4], @@ -139,12 +154,14 @@ def colour_raw(self): def colour(self): return tuple(reading // self._scaling for reading in self.colour_raw) - clear_raw = property(_raw_wrapper(CDATA)) + color_raw = colour_raw red_raw = property(_raw_wrapper(RDATA)) green_raw = property(_raw_wrapper(GDATA)) blue_raw = property(_raw_wrapper(BDATA)) + clear_raw = property(_raw_wrapper(CDATA)) - clear = property(_byte_wrapper(CDATA)) + color = colour red = property(_byte_wrapper(RDATA)) green = property(_byte_wrapper(GDATA)) blue = property(_byte_wrapper(BDATA)) + clear = property(_byte_wrapper(CDATA)) From d56e9cca2f65b029480295fd97c4e47383bdc0a7 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Thu, 24 Jun 2021 10:49:13 +0100 Subject: [PATCH 09/37] Raise an informative error when there is no colour sensor to access --- sense_hat/sense_hat.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 8494309..e60a2d4 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -203,11 +203,12 @@ def stick(self): @property def colour(self): - return self._colour + try: + return self._colour + except AttributeError as e: + raise RuntimeError('This Sense HAT does not have a color sensor') from e - @property - def color(self): - return self._colour + color = colour def has_colour_sensor(self): try: From 564da24e01e4c0b60626696b9aa2e706cc0cf66a Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Thu, 24 Jun 2021 16:52:44 +0100 Subject: [PATCH 10/37] Retrieve all colour sensor values through a `HardwareInterface` --- sense_hat/colour.py | 249 ++++++++++++++++++++++++++++++-------------- 1 file changed, 169 insertions(+), 80 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 2d9b4fb..a09f3f1 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -7,43 +7,46 @@ import glob from time import sleep +_error_str = "Failed to initialise TCS34725 colour sensor." -# device-specific constants -BUS = 1 -ADDR = 0x29 +class HardwareInterface: -COMMAND_BIT = 0x80 + GAIN_VALUES = (1, 4, 16, 60) + CLOCK_STEP = 0.0024 # the clock step is 2.4ms -# 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) + def get_enabled(self): + raise NotImplementedError -# data registers -CDATA = 0x14 | COMMAND_BIT -RDATA = 0x16 | COMMAND_BIT -GDATA = 0x18 | COMMAND_BIT -BDATA = 0x1A | COMMAND_BIT + def set_enabled(self, value): + raise NotImplementedError -# bit positions -PON = 0x01 -AEN = 0x02 -AVALID = 0x01 + def get_gain(self): + raise NotImplementedError -GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values -GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) + def set_gain(self, value): + raise NotImplementedError -CLOCK_STEP = 0.0024 # the clock step is 2.4ms + def get_integration_cycles(self): + raise NotImplementedError -_error_str = "Failed to initialise TCS34725 colour sensor." + def set_integration_cycles(self, value): + raise NotImplementedError + + def get_all(self): + raise NotImplementedError + + def get_red(self): + raise NotImplementedError + + def get_green(self): + raise NotImplementedError + + def get_blue(self): + raise NotImplementedError + + def get_clear(self): + raise NotImplementedError -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 _raw_wrapper(register): """ @@ -52,33 +55,53 @@ def _raw_wrapper(register): This is a factory function that implements this retrieval method. """ def get_raw(self): - value = self.bus.read_word_data(ADDR, register) - return value + return self.read(register) return get_raw -def _byte_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_byte(self): - value = self.bus.read_word_data(ADDR, register) // self._scaling - return value - return get_byte +class I2C(HardwareInterface): + # 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) + GAIN_TO_REG = dict(zip(HardwareInterface.GAIN_VALUES, GAIN_REG_VALUES)) # maps gain values to register values + REG_TO_GAIN = dict(zip(GAIN_REG_VALUES, HardwareInterface.GAIN_VALUES)) + + def __init__(self): -class ColourSensor: - - def __init__(self, gain=1, integration_cycles=1): try: - self.bus = smbus.SMBus(BUS) + self.bus = smbus.SMBus(self.BUS) except Exception as e: - explanation = " (I2C is not enabled)" if not i2c_enabled() else "" + explanation = " (I2C is not enabled)" if not self.i2c_enabled() else "" raise RuntimeError(f'{_error_str}{explanation}') from e try: - id = self.bus.read_byte_data(ADDR, ID) + id = self.read(self.ID) except Exception as e: explanation = " (sensor not present)" raise RuntimeError(f'{_error_str}{explanation}') from e @@ -87,48 +110,97 @@ def __init__(self, gain=1, integration_cycles=1): explanation = f" (different device id detected: {id})" raise RuntimeError(f'{_error_str}{explanation}') + @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): + return self.bus.read_byte_data(self.ADDR, attribute) + + def write(self, attribute, value): + self.bus.write_byte_data(self.ADDR, attribute, value) + + def get_enabled(self): + return self.read(self.ENABLE) == (PON | AEN) + + def set_enabled(self, value): + if value: + 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): + register_value = self.read(self.CONTROL) + return self.REG_TO_GAIN[register_value] + + def set_gain(self, value): + register_value = self.GAIN_TO_REG[value] + self.write(self.CONTROL, register_value) + + def get_integration_cycles(self): + return 256 - self.read(self.ATIME) + + def set_integration_cycles(self, value): + self.write(self.ATIME, 256-value) + + def get_all(self): + 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] + ) + + 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.integration_cycles = integration_cycles self.enabled = 1 @property def enabled(self): - return self.bus.read_byte_data(ADDR, ENABLE) == (PON | AEN) + return self.interface.get_enabled() @enabled.setter def enabled(self, status): - if status: - self.bus.write_byte_data(ADDR, ENABLE, PON) - sleep(CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." - self.bus.write_byte_data(ADDR, ENABLE, (PON | AEN)) - else: - self.bus.write_byte_data(ADDR, ENABLE, 0x00) - sleep(CLOCK_STEP) + self.interface.set_enabled(status) @property def gain(self): - return GAIN_INV[self.bus.read_byte_data(ADDR, CONTROL)] + return self.interface.get_gain() @gain.setter def gain(self, value): - if value in GAIN_MAP: - self.bus.write_byte_data(ADDR, CONTROL, GAIN_MAP[value]) - sleep(CLOCK_STEP) + if value in self.interface.GAIN_VALUES: + self.interface.set_gain(value) else: - raise RuntimeError(f'Cannot set gain to {value}. Values: {tuple(GAIN_MAP.keys())}') + raise RuntimeError(f'Cannot set gain to {value}. Values: {self.interface.GAIN_VALUES}') @property def integration_cycles(self): - return 256 - self.bus.read_byte_data(ADDR, ATIME) + return self.interface.get_integration_cycles() @integration_cycles.setter def integration_cycles(self, cycles): if 1 <= cycles <= 256: - self.bus.write_byte_data(ADDR, ATIME, 256-cycles) - self._integration_time = cycles * CLOCK_STEP + self.interface.set_integration_cycles(cycles) + self._integration_time = cycles * self.interface.CLOCK_STEP self._max_value = 2**16 if cycles >= 64 else 1024*cycles self._scaling = self._max_value // 256 - sleep(CLOCK_STEP) + sleep(self.interface.CLOCK_STEP) else: raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') @@ -142,26 +214,43 @@ def max_raw(self): @property def colour_raw(self): - block = self.bus.read_i2c_block_data(ADDR, CDATA, 8) - return ( - (block[3] << 8) + block[2], - (block[5] << 8) + block[4], - (block[7] << 8) + block[6], - (block[1] << 8) + block[0] - ) + return self.interface.get_all() @property def colour(self): return tuple(reading // self._scaling for reading in self.colour_raw) color_raw = colour_raw - red_raw = property(_raw_wrapper(RDATA)) - green_raw = property(_raw_wrapper(GDATA)) - blue_raw = property(_raw_wrapper(BDATA)) - clear_raw = property(_raw_wrapper(CDATA)) - color = colour - red = property(_byte_wrapper(RDATA)) - green = property(_byte_wrapper(GDATA)) - blue = property(_byte_wrapper(BDATA)) - clear = property(_byte_wrapper(CDATA)) + + @property + def red_raw(self): + return self.interface.get_red() + + @property + def green_raw(self): + return self.interface.get_green() + + @property + def blue_raw(self): + return self.interface.get_blue() + + @property + def clear_raw(self): + return self.interface.get_clear() + + @property + def red(self): + return self.red_raw // self._scaling + + @property + def green(self): + return self.green_raw // self._scaling + + @property + def blue(self): + return self.blue_raw // self._scaling + + @property + def clear(self): + return self.clear_raw // self._scaling \ No newline at end of file From 3a57f316d9563dad53789da6c45420ddb78fae7b Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Fri, 25 Jun 2021 16:50:13 +0100 Subject: [PATCH 11/37] Add a `HardwareInterface` for the emulator: `StatusFile` --- sense_hat/colour.py | 108 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index a09f3f1..7900838 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -5,6 +5,12 @@ import smbus import glob + +import io, os, sys, errno +import mmap +from struct import Struct +from collections import namedtuple + from time import sleep _error_str = "Failed to initialise TCS34725 colour sensor." @@ -162,6 +168,105 @@ def get_all(self): get_clear = _raw_wrapper(CDATA) +class StatusFile(HardwareInterface): + + COLOUR_DATA = Struct(str( + '@' # native mode + '?' # enabled + '4I' # RGBC + 'H' # gain + 'H' # integration cycles + )) + + ColourData = namedtuple( + 'ColourData', ( + 'enabled', 'R', 'G', 'B', 'C', 'gain', 'integration_cycles')) + + def __init__(self): + self.init_file() + + @staticmethod + def filename(): + """ + Return the filename used to represent the state of the emulated sense HAT's + co sensor. On UNIX we try ``/dev/shm`` then fall back to ``/tmp``; on + Windows we use whatever ``%TEMP%`` contains + """ + fname = 'rpi-sense-emu-colour' + if sys.platform.startswith('win'): + # just use a temporary file on Windows + return os.path.join(os.environ['TEMP'], fname) + else: + if os.path.exists('/dev/shm'): + return os.path.join('/dev/shm', fname) + else: + return os.path.join('/tmp', fname) + + def init_file(self): + """ + Opens the file representing the state of the humidity sensor. The + file-like object is returned. + If the file already exists we simply make sure it is the right size. If + the file does not already exist, it is created and zeroed. + """ + try: + # Attempt to open the colour sensor's file and ensure it's the right size + fd = io.open(self.filename(), 'r+b', buffering=0) + fd.seek(self.COLOUR_DATA.size) + fd.truncate() + except IOError as e: + # If the colour sensor device file doesn't exist, zero it into existence + if e.errno == errno.ENOENT: + fd = io.open(self.filename(), 'w+b', buffering=0) + fd.write(b'\x00' * self.COLOUR_DATA.size) + else: + raise IOError from e + else: + self._fd = fd + self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_WRITE) + + def read(self): + return self.ColourData(*self.COLOUR_DATA.unpack_from(self._map)) + + def write(self, value): + self.COLOUR_DATA.pack_into(self._map, 0, *value) + + def get_enabled(self): + return self.read().enabled + + def set_enabled(self, value): + self.write(self.read()._replace(enabled=value)) + + def get_gain(self): + return self.read().gain + + def set_gain(self, value): + self.write(self.read()._replace(gain=value)) + + def get_integration_cycles(self): + return self.read().integration_cycles + + def set_integration_cycles(self, value): + self.write(self.read()._replace(integration_cycles=value)) + + def get_all(self): + _, R, G, B, C, _, _ = self.read() + return (R, G, B, C) + + def get_red(self): + print(f"DEBUG: {self.read()}") + return self.read().R + + def get_green(self): + return self.read().G + + def get_blue(self): + return self.read().B + + def get_clear(self): + return self.read().C + + class ColourSensor: def __init__(self, gain=1, integration_cycles=1, interface=I2C): @@ -223,6 +328,9 @@ def colour(self): color_raw = colour_raw color = colour + # For the following, could also use something like: + # red_raw = property(lambda self: self.interface.get_red()) + @property def red_raw(self): return self.interface.get_red() From 40c700762ece39b8310314f5e43c57c6b17b6c53 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Mon, 28 Jun 2021 14:57:12 +0100 Subject: [PATCH 12/37] Improve documentation. Move the `StatusFile` interface to the emulator. --- sense_hat/colour.py | 312 ++++++++++++++++++++++++-------------------- setup.py | 2 +- 2 files changed, 168 insertions(+), 146 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 7900838..1119c7c 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -3,68 +3,137 @@ Documentation (including datasheet): https://ams.com/tcs34725#tab/documents """ -import smbus -import glob - -import io, os, sys, errno -import mmap -from struct import Struct -from collections import namedtuple - from time import sleep _error_str = "Failed to initialise TCS34725 colour sensor." + 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, value): + 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, value): + 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, value): + 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_all(self): + 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 a 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 a 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 a 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 a 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 CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. + 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(self): - return self.read(register) - return get_raw + 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 @@ -95,11 +164,15 @@ class I2C(HardwareInterface): AVALID = 0x01 GAIN_REG_VALUES = (0x00, 0x01, 0x02, 0x03) - GAIN_TO_REG = dict(zip(HardwareInterface.GAIN_VALUES, GAIN_REG_VALUES)) # maps gain values to register values + # 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: @@ -107,7 +180,7 @@ def __init__(self): raise RuntimeError(f'{_error_str}{explanation}') from e try: - id = self.read(self.ID) + id = self._read(self.ID) except Exception as e: explanation = " (sensor not present)" raise RuntimeError(f'{_error_str}{explanation}') from e @@ -121,39 +194,79 @@ 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): + 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): + 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 self.read(self.ENABLE) == (PON | AEN) + """ + Return True if the sensor is enabled and False otherwise + """ + return self._read(self.ENABLE) == (PON | AEN) - def set_enabled(self, value): - if value: - self.write(self.ENABLE, self.PON) + 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) + self._write(self.ENABLE, self.ON) else: - self.write(self.ENABLE, self.OFF) + self._write(self.ENABLE, self.OFF) sleep(self.CLOCK_STEP) def get_gain(self): - register_value = self.read(self.CONTROL) + """ + 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, value): - register_value = self.GAIN_TO_REG[value] - self.write(self.CONTROL, 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 256 - self.read(self.ATIME) + """ + 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, value): - self.write(self.ATIME, 256-value) + 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_all(self): + 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], @@ -162,111 +275,20 @@ def get_all(self): (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 StatusFile(HardwareInterface): - - COLOUR_DATA = Struct(str( - '@' # native mode - '?' # enabled - '4I' # RGBC - 'H' # gain - 'H' # integration cycles - )) - - ColourData = namedtuple( - 'ColourData', ( - 'enabled', 'R', 'G', 'B', 'C', 'gain', 'integration_cycles')) - - def __init__(self): - self.init_file() - - @staticmethod - def filename(): - """ - Return the filename used to represent the state of the emulated sense HAT's - co sensor. On UNIX we try ``/dev/shm`` then fall back to ``/tmp``; on - Windows we use whatever ``%TEMP%`` contains - """ - fname = 'rpi-sense-emu-colour' - if sys.platform.startswith('win'): - # just use a temporary file on Windows - return os.path.join(os.environ['TEMP'], fname) - else: - if os.path.exists('/dev/shm'): - return os.path.join('/dev/shm', fname) - else: - return os.path.join('/tmp', fname) - - def init_file(self): - """ - Opens the file representing the state of the humidity sensor. The - file-like object is returned. - If the file already exists we simply make sure it is the right size. If - the file does not already exist, it is created and zeroed. - """ - try: - # Attempt to open the colour sensor's file and ensure it's the right size - fd = io.open(self.filename(), 'r+b', buffering=0) - fd.seek(self.COLOUR_DATA.size) - fd.truncate() - except IOError as e: - # If the colour sensor device file doesn't exist, zero it into existence - if e.errno == errno.ENOENT: - fd = io.open(self.filename(), 'w+b', buffering=0) - fd.write(b'\x00' * self.COLOUR_DATA.size) - else: - raise IOError from e - else: - self._fd = fd - self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_WRITE) - - def read(self): - return self.ColourData(*self.COLOUR_DATA.unpack_from(self._map)) - - def write(self, value): - self.COLOUR_DATA.pack_into(self._map, 0, *value) - - def get_enabled(self): - return self.read().enabled - - def set_enabled(self, value): - self.write(self.read()._replace(enabled=value)) - - def get_gain(self): - return self.read().gain - - def set_gain(self, value): - self.write(self.read()._replace(gain=value)) - - def get_integration_cycles(self): - return self.read().integration_cycles - - def set_integration_cycles(self, value): - self.write(self.read()._replace(integration_cycles=value)) - - def get_all(self): - _, R, G, B, C, _, _ = self.read() - return (R, G, B, C) - - def get_red(self): - print(f"DEBUG: {self.read()}") - return self.read().R - - def get_green(self): - return self.read().G - - def get_blue(self): - return self.read().B - - def get_clear(self): - return self.read().C - - class ColourSensor: def __init__(self, gain=1, integration_cycles=1, interface=I2C): @@ -288,26 +310,26 @@ def gain(self): return self.interface.get_gain() @gain.setter - def gain(self, value): - if value in self.interface.GAIN_VALUES: - self.interface.set_gain(value) + def gain(self, gain): + if gain in self.interface.GAIN_VALUES: + self.interface.set_gain(gain) else: - raise RuntimeError(f'Cannot set gain to {value}. Values: {self.interface.GAIN_VALUES}') + raise RuntimeError(f'Cannot set gain to {gain}. Values: {self.interface.GAIN_VALUES}') @property def integration_cycles(self): return self.interface.get_integration_cycles() @integration_cycles.setter - def integration_cycles(self, cycles): - if 1 <= cycles <= 256: - self.interface.set_integration_cycles(cycles) - self._integration_time = cycles * self.interface.CLOCK_STEP - self._max_value = 2**16 if cycles >= 64 else 1024*cycles + def integration_cycles(self, integration_cycles): + if 1 <= integration_cycles <= 256: + self.interface.set_integration_cycles(integration_cycles) + self._integration_time = integration_cycles * self.interface.CLOCK_STEP + self._max_value = self.interface.max_value(integration_cycles) self._scaling = self._max_value // 256 sleep(self.interface.CLOCK_STEP) else: - raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') + raise RuntimeError(f'Cannot set integration cycles to {integration_cycles} (1-256)') @property def integration_time(self): @@ -319,7 +341,7 @@ def max_raw(self): @property def colour_raw(self): - return self.interface.get_all() + return self.interface.get_raw() @property def colour(self): diff --git a/setup.py b/setup.py index d93f4a7..0ad5b9c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(fname): setup( name="sense-hat", - version="2.3.0", + version="2.3.1", author="Dave Honess", author_email="dave@raspberrypi.org", description="Python module to control the Raspberry Pi Sense HAT, originally used in the Astro Pi mission", From 2b5829c758e1d1d4539a8a53cdc4170520634e0b Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 29 Jun 2021 12:13:44 +0100 Subject: [PATCH 13/37] Raise `ValueError` instead of `RuntimeError` --- sense_hat/colour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 1119c7c..73f7ce1 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -314,7 +314,7 @@ def gain(self, gain): if gain in self.interface.GAIN_VALUES: self.interface.set_gain(gain) else: - raise RuntimeError(f'Cannot set gain to {gain}. Values: {self.interface.GAIN_VALUES}') + raise ValueError(f'Cannot set gain to {gain}. Values: {self.interface.GAIN_VALUES}') @property def integration_cycles(self): @@ -329,7 +329,7 @@ def integration_cycles(self, integration_cycles): self._scaling = self._max_value // 256 sleep(self.interface.CLOCK_STEP) else: - raise RuntimeError(f'Cannot set integration cycles to {integration_cycles} (1-256)') + raise ValueError(f'Cannot set integration cycles to {integration_cycles} (1-256)') @property def integration_time(self): From a180341ec78af9036450841f2060943c62988f73 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Tue, 29 Jun 2021 12:14:48 +0100 Subject: [PATCH 14/37] Update `docs/` to include `ColourSensor` examples and explanations --- docs/api.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++ docs/changelog.md | 4 ++ docs/index.md | 5 +- 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index 3b1bad2..a75135e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -774,3 +774,125 @@ 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 a `ValueError` 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 `ValueError` 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}") +``` \ 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/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). From 543f9e689729d318cf19802e1b7fdcf9383015b1 Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Thu, 8 Jul 2021 09:30:04 +0100 Subject: [PATCH 15/37] Compute all properties "live" (no pre-computed attributes) --- sense_hat/colour.py | 55 +++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 73f7ce1..641aea4 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -324,63 +324,38 @@ def integration_cycles(self): def integration_cycles(self, integration_cycles): if 1 <= integration_cycles <= 256: self.interface.set_integration_cycles(integration_cycles) - self._integration_time = integration_cycles * self.interface.CLOCK_STEP - self._max_value = self.interface.max_value(integration_cycles) - self._scaling = self._max_value // 256 sleep(self.interface.CLOCK_STEP) else: raise ValueError(f'Cannot set integration cycles to {integration_cycles} (1-256)') @property def integration_time(self): - return self._integration_time + return self.integration_cycles * self.interface.CLOCK_STEP @property def max_raw(self): - return self._max_value + return self.interface.max_value(self.integration_cycles) @property def colour_raw(self): return self.interface.get_raw() - @property - def colour(self): - return tuple(reading // self._scaling for reading in self.colour_raw) - color_raw = colour_raw - color = colour - - # For the following, could also use something like: - # red_raw = property(lambda self: self.interface.get_red()) - - @property - def red_raw(self): - return self.interface.get_red() - - @property - def green_raw(self): - return self.interface.get_green() + 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 blue_raw(self): - return self.interface.get_blue() - - @property - def clear_raw(self): - return self.interface.get_clear() - - @property - def red(self): - return self.red_raw // self._scaling + def _scaling(self): + return self.max_raw // 256 @property - def green(self): - return self.green_raw // self._scaling - - @property - def blue(self): - return self.blue_raw // self._scaling + def colour(self): + return tuple(reading // self._scaling for reading in self.colour_raw) - @property - def clear(self): - return self.clear_raw // self._scaling \ No newline at end of file + 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 ) From df46a8631723056c980c695ab2a57c5d1c6344ad Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Wed, 21 Jul 2021 15:01:54 +0100 Subject: [PATCH 16/37] Remove generated files from `debian` --- debian/python-sense-hat.debhelper.log | 19 - debian/python-sense-hat.postinst.debhelper | 7 - debian/python-sense-hat.prerm.debhelper | 12 - debian/python-sense-hat.substvars | 5 - debian/python-sense-hat/DEBIAN/control | 15 - debian/python-sense-hat/DEBIAN/md5sums | 12 - debian/python-sense-hat/DEBIAN/postinst | 9 - debian/python-sense-hat/DEBIAN/prerm | 14 - .../dist-packages/sense_hat/__init__.py | 16 - .../dist-packages/sense_hat/colour.py | 150 --- .../dist-packages/sense_hat/sense_hat.py | 883 ------------------ .../sense_hat/sense_hat_text.png | Bin 908 -> 0 bytes .../sense_hat/sense_hat_text.txt | 1 - .../dist-packages/sense_hat/stick.py | 308 ------ .../share/doc/python-sense-hat/changelog.gz | Bin 310 -> 0 bytes .../usr/share/doc/python-sense-hat/copyright | 32 - debian/python3-sense-hat.debhelper.log | 19 - debian/python3-sense-hat.postinst.debhelper | 10 - debian/python3-sense-hat.prerm.debhelper | 10 - debian/python3-sense-hat.substvars | 3 - debian/python3-sense-hat/DEBIAN/control | 16 - debian/python3-sense-hat/DEBIAN/md5sums | 12 - debian/python3-sense-hat/DEBIAN/postinst | 12 - debian/python3-sense-hat/DEBIAN/prerm | 12 - .../dist-packages/sense_hat/__init__.py | 16 - .../python3/dist-packages/sense_hat/colour.py | 150 --- .../dist-packages/sense_hat/sense_hat.py | 883 ------------------ .../sense_hat/sense_hat_text.png | Bin 908 -> 0 bytes .../sense_hat/sense_hat_text.txt | 1 - .../python3/dist-packages/sense_hat/stick.py | 308 ------ .../share/doc/python3-sense-hat/changelog.gz | Bin 310 -> 0 bytes .../usr/share/doc/python3-sense-hat/copyright | 32 - 32 files changed, 2967 deletions(-) delete mode 100644 debian/python-sense-hat.debhelper.log delete mode 100644 debian/python-sense-hat.postinst.debhelper delete mode 100644 debian/python-sense-hat.prerm.debhelper delete mode 100644 debian/python-sense-hat.substvars delete mode 100644 debian/python-sense-hat/DEBIAN/control delete mode 100644 debian/python-sense-hat/DEBIAN/md5sums delete mode 100755 debian/python-sense-hat/DEBIAN/postinst delete mode 100755 debian/python-sense-hat/DEBIAN/prerm delete mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py delete mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py delete mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py delete mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png delete mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.txt delete mode 100644 debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py delete mode 100644 debian/python-sense-hat/usr/share/doc/python-sense-hat/changelog.gz delete mode 100644 debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright delete mode 100644 debian/python3-sense-hat.debhelper.log delete mode 100644 debian/python3-sense-hat.postinst.debhelper delete mode 100644 debian/python3-sense-hat.prerm.debhelper delete mode 100644 debian/python3-sense-hat.substvars delete mode 100644 debian/python3-sense-hat/DEBIAN/control delete mode 100644 debian/python3-sense-hat/DEBIAN/md5sums delete mode 100755 debian/python3-sense-hat/DEBIAN/postinst delete mode 100755 debian/python3-sense-hat/DEBIAN/prerm delete mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py delete mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py delete mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py delete mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png delete mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.txt delete mode 100644 debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py delete mode 100644 debian/python3-sense-hat/usr/share/doc/python3-sense-hat/changelog.gz delete mode 100644 debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright diff --git a/debian/python-sense-hat.debhelper.log b/debian/python-sense-hat.debhelper.log deleted file mode 100644 index de33f70..0000000 --- a/debian/python-sense-hat.debhelper.log +++ /dev/null @@ -1,19 +0,0 @@ -dh_update_autotools_config -dh_auto_configure -dh_auto_build -dh_auto_test -dh_prep -dh_auto_install -dh_installdocs -dh_installchangelogs -dh_installinit -dh_perl -dh_link -dh_strip_nondeterminism -dh_compress -dh_fixperms -dh_missing -dh_installdeb -dh_gencontrol -dh_md5sums -dh_builddeb diff --git a/debian/python-sense-hat.postinst.debhelper b/debian/python-sense-hat.postinst.debhelper deleted file mode 100644 index e415945..0000000 --- a/debian/python-sense-hat.postinst.debhelper +++ /dev/null @@ -1,7 +0,0 @@ - -# Automatically added by dh_python2: -if which pycompile >/dev/null 2>&1; then - pycompile -p python-sense-hat -fi - -# End automatically added section diff --git a/debian/python-sense-hat.prerm.debhelper b/debian/python-sense-hat.prerm.debhelper deleted file mode 100644 index 7150f5e..0000000 --- a/debian/python-sense-hat.prerm.debhelper +++ /dev/null @@ -1,12 +0,0 @@ - -# Automatically added by dh_python2: -if which pyclean >/dev/null 2>&1; then - pyclean -p python-sense-hat -else - dpkg -L python-sense-hat | grep '\.py$' | while read file - do - rm -f "${file}"[co] >/dev/null - done -fi - -# End automatically added section diff --git a/debian/python-sense-hat.substvars b/debian/python-sense-hat.substvars deleted file mode 100644 index 467868f..0000000 --- a/debian/python-sense-hat.substvars +++ /dev/null @@ -1,5 +0,0 @@ -python:Versions=2.7 -python:Provides=python2.7-sense-hat -python:Depends=python-numpy, python-pil, python:any, python:any (<< 2.8), python:any (>= 2.7~) -misc:Depends= -misc:Pre-Depends= diff --git a/debian/python-sense-hat/DEBIAN/control b/debian/python-sense-hat/DEBIAN/control deleted file mode 100644 index 44348c7..0000000 --- a/debian/python-sense-hat/DEBIAN/control +++ /dev/null @@ -1,15 +0,0 @@ -Package: python-sense-hat -Version: 2.3.0~test0 -Architecture: all -Maintainer: Serge Schneider -Installed-Size: 67 -Depends: python-numpy, python-pil, python:any (<< 2.8), python:any (>= 2.7~), python-rtimulib -Section: python -Priority: optional -Homepage: https://github.com/RPi-Distro/python-sense-hat -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. diff --git a/debian/python-sense-hat/DEBIAN/md5sums b/debian/python-sense-hat/DEBIAN/md5sums deleted file mode 100644 index dda61e3..0000000 --- a/debian/python-sense-hat/DEBIAN/md5sums +++ /dev/null @@ -1,12 +0,0 @@ -35dd286b437b2916c40cdc4ed7075e50 usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/PKG-INFO -68b329da9893e34099c7d8ad5cb9c940 usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/dependency_links.txt -d41d8cd98f00b204e9800998ecf8427e usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/requires.txt -d485d30aa3a3f58b8b0db6e71f19ce20 usr/lib/python2.7/dist-packages/sense_hat-2.3.0.egg-info/top_level.txt -6f29e2883acf35bf03d73cc70c5e5db7 usr/lib/python2.7/dist-packages/sense_hat/__init__.py -776c985173201b7c38f016a3320cb8d6 usr/lib/python2.7/dist-packages/sense_hat/colour.py -48c3612fdc876097daccd0cad726ed91 usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py -a90fdb250c9803bf822190b12636c73e usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png -ea665cb6f674eea5244a3e39051e45cf usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.txt -aecf59111307ec7eafb6192e8fbd1429 usr/lib/python2.7/dist-packages/sense_hat/stick.py -68832ab63c7ca094f1d1e31f84be94c8 usr/share/doc/python-sense-hat/changelog.gz -ca593700544765aae69f76ecf48ec56b usr/share/doc/python-sense-hat/copyright diff --git a/debian/python-sense-hat/DEBIAN/postinst b/debian/python-sense-hat/DEBIAN/postinst deleted file mode 100755 index 240f649..0000000 --- a/debian/python-sense-hat/DEBIAN/postinst +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -e - -# Automatically added by dh_python2: -if which pycompile >/dev/null 2>&1; then - pycompile -p python-sense-hat -fi - -# End automatically added section diff --git a/debian/python-sense-hat/DEBIAN/prerm b/debian/python-sense-hat/DEBIAN/prerm deleted file mode 100755 index ee6c986..0000000 --- a/debian/python-sense-hat/DEBIAN/prerm +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh -set -e - -# Automatically added by dh_python2: -if which pyclean >/dev/null 2>&1; then - pyclean -p python-sense-hat -else - dpkg -L python-sense-hat | grep '\.py$' | while read file - do - rm -f "${file}"[co] >/dev/null - done -fi - -# End automatically added section diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py deleted file mode 100644 index d723807..0000000 --- a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import absolute_import -from .sense_hat import SenseHat, SenseHat as AstroPi -from .stick import ( - SenseStick, - InputEvent, - DIRECTION_UP, - DIRECTION_DOWN, - DIRECTION_LEFT, - DIRECTION_RIGHT, - DIRECTION_MIDDLE, - ACTION_PRESSED, - ACTION_RELEASED, - ACTION_HELD, - ) - -__version__ = '2.3.0' diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py deleted file mode 100644 index c638bbf..0000000 --- a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/colour.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -Python library for the TCS34725 Color Sensor - -Documentation (including datasheet): https://ams.com/tcs34725#tab/documents -""" - -import smbus -import glob -from time import sleep - -def i2c_enabled(): - return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None - -def _raw_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_raw(self): - value = self.bus.read_word_data(self.ADDR, register) - return value - return get_raw - -def _byte_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_byte(self): - value = self.bus.read_word_data(self.ADDR, register) // self._scaling - return value - return get_byte - - -class ColourSensor: - - # 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 - PON = 0x01 - AEN = 0x02 - AVALID = 0x01 - - GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values - GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) - - CLOCK_STEP = 0.0024 # the clock step is 2.4ms - - def __init__(self, gain=1, integration_cycles=1): - try: - self.bus = smbus.SMBus(self.BUS) - except Exception: - explanation = " (I2C is not enabled)" if not i2c_enabled() else "" - raise RuntimeError(f'Failed to initialise TCS34725 colour sensor.{explanation}') - else: - if self._id != 0x44: - raise RuntimeError(f'Not connected to TCS34725 (id: {self._id})') - self.gain = gain - self.integration_cycles=integration_cycles - self.enabled= 1 - - @property - def _id(self): - return self.bus.read_byte_data(self.ADDR, self.ID) - - @property - def enabled(self): - return self.bus.read_byte_data(self.ADDR, self.ENABLE) == (self.PON | self.AEN) - - @enabled.setter - def enabled(self, status): - if status: - self.bus.write_byte_data(self.ADDR, (self.ENABLE), self.PON) - sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." - self.bus.write_byte_data(self.ADDR, (self.ENABLE), (self.PON | self.AEN)) - else: - self.bus.write_byte_data(self.ADDR, (self.ENABLE), 0x00) - sleep(self.CLOCK_STEP) - - @property - def gain(self): - return self.GAIN_INV[self.bus.read_byte_data(self.ADDR, self.CONTROL)] - - @gain.setter - def gain(self, value): - if value in self.GAIN_MAP: - self.bus.write_byte_data(self.ADDR, self.CONTROL, self.GAIN_MAP[value]) - sleep(self.CLOCK_STEP) - else: - raise RuntimeError(f'Cannot set gain to {value}. {tuple(self.GAIN_MAP.keys())}') - - @property - def integration_cycles(self): - return 256 - self.bus.read_byte_data(self.ADDR, self.ATIME) - - @integration_cycles.setter - def integration_cycles(self, cycles): - if 1 <= cycles <= 256: - self.bus.write_byte_data(self.ADDR, self.ATIME, 256-cycles) - self.integration_time = cycles * self.CLOCK_STEP - self.max_value = 2**16 if cycles >= 64 else 1024*cycles - self._scaling = self.max_value // 256 - sleep(self.CLOCK_STEP) - else: - raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') - - @property - def colour_raw(self): - 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] - ) - - @property - def colour(self): - return tuple(reading // self._scaling for reading in self.colour_raw) - - clear_raw = property(_raw_wrapper(CDATA)) - red_raw = property(_raw_wrapper(RDATA)) - green_raw = property(_raw_wrapper(GDATA)) - blue_raw = property(_raw_wrapper(BDATA)) - - clear = property(_byte_wrapper(CDATA)) - red = property(_byte_wrapper(RDATA)) - green = property(_byte_wrapper(GDATA)) - blue = property(_byte_wrapper(BDATA)) diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py deleted file mode 100644 index 8494309..0000000 --- a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat.py +++ /dev/null @@ -1,883 +0,0 @@ -#!/usr/bin/python -import struct -import os -import sys -import math -import time -import numpy as np -import shutil -import glob -import RTIMU # custom version -import pwd -import array -import fcntl -from PIL import Image # pillow -from copy import deepcopy - -from .stick import SenseStick -from .colour import ColourSensor - -class SenseHat(object): - - SENSE_HAT_FB_NAME = 'RPi-Sense FB' - SENSE_HAT_FB_FBIOGET_GAMMA = 61696 - SENSE_HAT_FB_FBIOSET_GAMMA = 61697 - SENSE_HAT_FB_FBIORESET_GAMMA = 61698 - SENSE_HAT_FB_GAMMA_DEFAULT = 0 - SENSE_HAT_FB_GAMMA_LOW = 1 - SENSE_HAT_FB_GAMMA_USER = 2 - SETTINGS_HOME_PATH = '.config/sense_hat' - - def __init__( - self, - imu_settings_file='RTIMULib', - text_assets='sense_hat_text' - ): - - self._fb_device = self._get_fb_device() - if self._fb_device is None: - raise OSError('Cannot detect %s device' % self.SENSE_HAT_FB_NAME) - - if not glob.glob('/dev/i2c*'): - raise OSError('Cannot access I2C. Please ensure I2C is enabled in raspi-config') - - # 0 is With B+ HDMI port facing downwards - pix_map0 = np.array([ - [0, 1, 2, 3, 4, 5, 6, 7], - [8, 9, 10, 11, 12, 13, 14, 15], - [16, 17, 18, 19, 20, 21, 22, 23], - [24, 25, 26, 27, 28, 29, 30, 31], - [32, 33, 34, 35, 36, 37, 38, 39], - [40, 41, 42, 43, 44, 45, 46, 47], - [48, 49, 50, 51, 52, 53, 54, 55], - [56, 57, 58, 59, 60, 61, 62, 63] - ], int) - - pix_map90 = np.rot90(pix_map0) - pix_map180 = np.rot90(pix_map90) - pix_map270 = np.rot90(pix_map180) - - self._pix_map = { - 0: pix_map0, - 90: pix_map90, - 180: pix_map180, - 270: pix_map270 - } - - self._rotation = 0 - - # Load text assets - dir_path = os.path.dirname(__file__) - self._load_text_assets( - os.path.join(dir_path, '%s.png' % text_assets), - os.path.join(dir_path, '%s.txt' % text_assets) - ) - - # Load IMU settings and calibration data - self._imu_settings = self._get_settings_file(imu_settings_file) - self._imu = RTIMU.RTIMU(self._imu_settings) - self._imu_init = False # Will be initialised as and when needed - self._pressure = RTIMU.RTPressure(self._imu_settings) - self._pressure_init = False # Will be initialised as and when needed - self._humidity = RTIMU.RTHumidity(self._imu_settings) - self._humidity_init = False # Will be initialised as and when needed - self._last_orientation = {'pitch': 0, 'roll': 0, 'yaw': 0} - raw = {'x': 0, 'y': 0, 'z': 0} - self._last_compass_raw = deepcopy(raw) - self._last_gyro_raw = deepcopy(raw) - self._last_accel_raw = deepcopy(raw) - self._compass_enabled = False - self._gyro_enabled = False - self._accel_enabled = False - self._stick = SenseStick() - - # initialise the TCS34725 colour sensor (if possible) - try: - self._colour = ColourSensor() - except: - pass - - #### - # Text assets - #### - - # Text asset files are rotated right through 90 degrees to allow blocks of - # 40 contiguous pixels to represent one 5 x 8 character. These are stored - # in a 8 x 640 pixel png image with characters arranged adjacently - # Consequently we must rotate the pixel map left through 90 degrees to - # compensate when drawing text - - def _load_text_assets(self, text_image_file, text_file): - """ - Internal. Builds a character indexed dictionary of pixels used by the - show_message function below - """ - - text_pixels = self.load_image(text_image_file, False) - with open(text_file, 'r') as f: - loaded_text = f.read() - self._text_dict = {} - for index, s in enumerate(loaded_text): - start = index * 40 - end = start + 40 - char = text_pixels[start:end] - self._text_dict[s] = char - - def _trim_whitespace(self, char): # For loading text assets only - """ - Internal. Trims white space pixels from the front and back of loaded - text characters - """ - - psum = lambda x: sum(sum(x, [])) - if psum(char) > 0: - is_empty = True - while is_empty: # From front - row = char[0:8] - is_empty = psum(row) == 0 - if is_empty: - del char[0:8] - is_empty = True - while is_empty: # From back - row = char[-8:] - is_empty = psum(row) == 0 - if is_empty: - del char[-8:] - return char - - def _get_settings_file(self, imu_settings_file): - """ - Internal. Logic to check for a system wide RTIMU ini file. This is - copied to the home folder if one is not already found there. - """ - - ini_file = '%s.ini' % imu_settings_file - - home_dir = pwd.getpwuid(os.getuid())[5] - home_path = os.path.join(home_dir, self.SETTINGS_HOME_PATH) - if not os.path.exists(home_path): - os.makedirs(home_path) - - home_file = os.path.join(home_path, ini_file) - home_exists = os.path.isfile(home_file) - system_file = os.path.join('/etc', ini_file) - system_exists = os.path.isfile(system_file) - - if system_exists and not home_exists: - shutil.copyfile(system_file, home_file) - - return RTIMU.Settings(os.path.join(home_path, imu_settings_file)) # RTIMU will add .ini internally - - def _get_fb_device(self): - """ - Internal. Finds the correct frame buffer device for the sense HAT - and returns its /dev name. - """ - - device = None - - for fb in glob.glob('/sys/class/graphics/fb*'): - name_file = os.path.join(fb, 'name') - if os.path.isfile(name_file): - with open(name_file, 'r') as f: - name = f.read() - if name.strip() == self.SENSE_HAT_FB_NAME: - fb_device = fb.replace(os.path.dirname(fb), '/dev') - if os.path.exists(fb_device): - device = fb_device - break - - return device - - #### - # Joystick - #### - - @property - def stick(self): - return self._stick - - #### - # Colour sensor - #### - - @property - def colour(self): - return self._colour - - @property - def color(self): - return self._colour - - def has_colour_sensor(self): - try: - self._colour - except: - return False - else: - return True - - #### - # LED Matrix - #### - - @property - def rotation(self): - return self._rotation - - @rotation.setter - def rotation(self, r): - self.set_rotation(r, True) - - def set_rotation(self, r=0, redraw=True): - """ - Sets the LED matrix rotation for viewing, adjust if the Pi is upside - down or sideways. 0 is with the Pi HDMI port facing downwards - """ - - if r in self._pix_map.keys(): - if redraw: - pixel_list = self.get_pixels() - self._rotation = r - if redraw: - self.set_pixels(pixel_list) - else: - raise ValueError('Rotation must be 0, 90, 180 or 270 degrees') - - def _pack_bin(self, pix): - """ - Internal. Encodes python list [R,G,B] into 16 bit RGB565 - """ - - r = (pix[0] >> 3) & 0x1F - g = (pix[1] >> 2) & 0x3F - b = (pix[2] >> 3) & 0x1F - bits16 = (r << 11) + (g << 5) + b - return struct.pack('H', bits16) - - def _unpack_bin(self, packed): - """ - Internal. Decodes 16 bit RGB565 into python list [R,G,B] - """ - - output = struct.unpack('H', packed) - bits16 = output[0] - r = (bits16 & 0xF800) >> 11 - g = (bits16 & 0x7E0) >> 5 - b = (bits16 & 0x1F) - return [int(r << 3), int(g << 2), int(b << 3)] - - def flip_h(self, redraw=True): - """ - Flip LED matrix horizontal - """ - - pixel_list = self.get_pixels() - flipped = [] - for i in range(8): - offset = i * 8 - flipped.extend(reversed(pixel_list[offset:offset + 8])) - if redraw: - self.set_pixels(flipped) - return flipped - - def flip_v(self, redraw=True): - """ - Flip LED matrix vertical - """ - - pixel_list = self.get_pixels() - flipped = [] - for i in reversed(range(8)): - offset = i * 8 - flipped.extend(pixel_list[offset:offset + 8]) - if redraw: - self.set_pixels(flipped) - return flipped - - 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 - and 255 - """ - - if len(pixel_list) != 64: - raise ValueError('Pixel lists must have 64 elements') - - for index, pix in enumerate(pixel_list): - if 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: - if element > 255 or element < 0: - raise ValueError('Pixel at index %d is invalid. Pixel elements must be between 0 and 255' % index) - - with open(self._fb_device, 'wb') as f: - map = self._pix_map[self._rotation] - for index, pix in enumerate(pixel_list): - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[index // 8][index % 8] * 2) # row, column - f.write(self._pack_bin(pix)) - - def get_pixels(self): - """ - Returns a list containing 64 smaller lists of [R,G,B] pixels - representing what is currently displayed on the LED matrix - """ - - pixel_list = [] - with open(self._fb_device, 'rb') as f: - map = self._pix_map[self._rotation] - for row in range(8): - for col in range(8): - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[row][col] * 2) # row, column - pixel_list.append(self._unpack_bin(f.read(2))) - return pixel_list - - def set_pixel(self, x, y, *args): - """ - Updates the single [R,G,B] pixel specified by x and y on the LED matrix - Top left = 0,0 Bottom right = 7,7 - - e.g. ap.set_pixel(x, y, r, g, b) - or - pixel = (r, g, b) - 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) - - if x > 7 or x < 0: - raise ValueError('X position must be between 0 and 7') - - if y > 7 or y < 0: - raise ValueError('Y position must be between 0 and 7') - - for element in pixel: - if element > 255 or element < 0: - raise ValueError('Pixel elements must be between 0 and 255') - - with open(self._fb_device, 'wb') as f: - map = self._pix_map[self._rotation] - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[y][x] * 2) # row, column - f.write(self._pack_bin(pixel)) - - def get_pixel(self, x, y): - """ - Returns a list of [R,G,B] representing the pixel specified by x and y - on the LED matrix. Top left = 0,0 Bottom right = 7,7 - """ - - if x > 7 or x < 0: - raise ValueError('X position must be between 0 and 7') - - if y > 7 or y < 0: - raise ValueError('Y position must be between 0 and 7') - - pix = None - - with open(self._fb_device, 'rb') as f: - map = self._pix_map[self._rotation] - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[y][x] * 2) # row, column - pix = self._unpack_bin(f.read(2)) - - return pix - - def load_image(self, file_path, redraw=True): - """ - Accepts a path to an 8 x 8 image file and updates the LED matrix with - the image - """ - - if not os.path.exists(file_path): - raise IOError('%s not found' % file_path) - - img = Image.open(file_path).convert('RGB') - pixel_list = list(map(list, img.getdata())) - - if redraw: - self.set_pixels(pixel_list) - - return pixel_list - - def clear(self, *args): - """ - Clears the LED matrix with a single colour, default is black / off - - e.g. ap.clear() - or - ap.clear(r, g, b) - or - colour = (r, g, b) - ap.clear(colour) - """ - - black = (0, 0, 0) # default - - if len(args) == 0: - colour = black - elif len(args) == 1: - colour = args[0] - elif len(args) == 3: - colour = args - else: - raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') - - self.set_pixels([colour] * 64) - - def _get_char_pixels(self, s): - """ - Internal. Safeguards the character indexed dictionary for the - show_message function below - """ - - if len(s) == 1 and s in self._text_dict.keys(): - return list(self._text_dict[s]) - else: - return list(self._text_dict['?']) - - def show_message( - self, - text_string, - scroll_speed=.1, - text_colour=[255, 255, 255], - back_colour=[0, 0, 0] - ): - """ - Scrolls a string of text across the LED matrix using the specified - speed and colours - """ - - # We must rotate the pixel map left through 90 degrees when drawing - # text, see _load_text_assets - previous_rotation = self._rotation - self._rotation -= 90 - if self._rotation < 0: - self._rotation = 270 - dummy_colour = [None, None, None] - string_padding = [dummy_colour] * 64 - letter_padding = [dummy_colour] * 8 - # Build pixels from dictionary - scroll_pixels = [] - scroll_pixels.extend(string_padding) - for s in text_string: - scroll_pixels.extend(self._trim_whitespace(self._get_char_pixels(s))) - scroll_pixels.extend(letter_padding) - scroll_pixels.extend(string_padding) - # Recolour pixels as necessary - coloured_pixels = [ - text_colour if pixel == [255, 255, 255] else back_colour - for pixel in scroll_pixels - ] - # Shift right by 8 pixels per frame to scroll - scroll_length = len(coloured_pixels) // 8 - for i in range(scroll_length - 8): - start = i * 8 - end = start + 64 - self.set_pixels(coloured_pixels[start:end]) - time.sleep(scroll_speed) - self._rotation = previous_rotation - - def show_letter( - self, - s, - text_colour=[255, 255, 255], - back_colour=[0, 0, 0] - ): - """ - Displays a single text character on the LED matrix using the specified - colours - """ - - if len(s) > 1: - raise ValueError('Only one character may be passed into this method') - # We must rotate the pixel map left through 90 degrees when drawing - # text, see _load_text_assets - previous_rotation = self._rotation - self._rotation -= 90 - if self._rotation < 0: - self._rotation = 270 - dummy_colour = [None, None, None] - pixel_list = [dummy_colour] * 8 - pixel_list.extend(self._get_char_pixels(s)) - pixel_list.extend([dummy_colour] * 16) - coloured_pixels = [ - text_colour if pixel == [255, 255, 255] else back_colour - for pixel in pixel_list - ] - self.set_pixels(coloured_pixels) - self._rotation = previous_rotation - - @property - def gamma(self): - buffer = array.array('B', [0]*32) - with open(self._fb_device) as f: - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOGET_GAMMA, buffer) - return list(buffer) - - @gamma.setter - def gamma(self, buffer): - if len(buffer) is not 32: - raise ValueError('Gamma array must be of length 32') - - if not all(b <= 31 for b in buffer): - raise ValueError('Gamma values must be bewteen 0 and 31') - - if not isinstance(buffer, array.array): - buffer = array.array('B', buffer) - - with open(self._fb_device) as f: - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOSET_GAMMA, buffer) - - def gamma_reset(self): - """ - Resets the LED matrix gamma correction to default - """ - - with open(self._fb_device) as f: - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, self.SENSE_HAT_FB_GAMMA_DEFAULT) - - @property - def low_light(self): - return self.gamma == [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10] - - @low_light.setter - def low_light(self, value): - with open(self._fb_device) as f: - cmd = self.SENSE_HAT_FB_GAMMA_LOW if value else self.SENSE_HAT_FB_GAMMA_DEFAULT - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, cmd) - - #### - # Environmental sensors - #### - - def _init_humidity(self): - """ - Internal. Initialises the humidity sensor via RTIMU - """ - - if not self._humidity_init: - self._humidity_init = self._humidity.humidityInit() - if not self._humidity_init: - raise OSError('Humidity Init Failed') - - def _init_pressure(self): - """ - Internal. Initialises the pressure sensor via RTIMU - """ - - if not self._pressure_init: - self._pressure_init = self._pressure.pressureInit() - if not self._pressure_init: - raise OSError('Pressure Init Failed') - - def get_humidity(self): - """ - Returns the percentage of relative humidity - """ - - self._init_humidity() # Ensure humidity sensor is initialised - humidity = 0 - data = self._humidity.humidityRead() - if (data[0]): # Humidity valid - humidity = data[1] - return humidity - - @property - def humidity(self): - return self.get_humidity() - - def get_temperature_from_humidity(self): - """ - Returns the temperature in Celsius from the humidity sensor - """ - - self._init_humidity() # Ensure humidity sensor is initialised - temp = 0 - data = self._humidity.humidityRead() - if (data[2]): # Temp valid - temp = data[3] - return temp - - def get_temperature_from_pressure(self): - """ - Returns the temperature in Celsius from the pressure sensor - """ - - self._init_pressure() # Ensure pressure sensor is initialised - temp = 0 - data = self._pressure.pressureRead() - if (data[2]): # Temp valid - temp = data[3] - return temp - - def get_temperature(self): - """ - Returns the temperature in Celsius - """ - - return self.get_temperature_from_humidity() - - @property - def temp(self): - return self.get_temperature_from_humidity() - - @property - def temperature(self): - return self.get_temperature_from_humidity() - - def get_pressure(self): - """ - Returns the pressure in Millibars - """ - - self._init_pressure() # Ensure pressure sensor is initialised - pressure = 0 - data = self._pressure.pressureRead() - if (data[0]): # Pressure valid - pressure = data[1] - return pressure - - @property - def pressure(self): - return self.get_pressure() - - #### - # IMU Sensor - #### - - def _init_imu(self): - """ - Internal. Initialises the IMU sensor via RTIMU - """ - - if not self._imu_init: - self._imu_init = self._imu.IMUInit() - if self._imu_init: - self._imu_poll_interval = self._imu.IMUGetPollInterval() * 0.001 - # Enable everything on IMU - self.set_imu_config(True, True, True) - else: - raise OSError('IMU Init Failed') - - def set_imu_config(self, compass_enabled, gyro_enabled, accel_enabled): - """ - Enables and disables the gyroscope, accelerometer and/or magnetometer - input to the orientation functions - """ - - # If the consuming code always calls this just before reading the IMU - # the IMU consistently fails to read. So prevent unnecessary calls to - # IMU config functions using state variables - - self._init_imu() # Ensure imu is initialised - - if (not isinstance(compass_enabled, bool) - or not isinstance(gyro_enabled, bool) - or not isinstance(accel_enabled, bool)): - raise TypeError('All set_imu_config parameters must be of boolean type') - - if self._compass_enabled != compass_enabled: - self._compass_enabled = compass_enabled - self._imu.setCompassEnable(self._compass_enabled) - - if self._gyro_enabled != gyro_enabled: - self._gyro_enabled = gyro_enabled - self._imu.setGyroEnable(self._gyro_enabled) - - if self._accel_enabled != accel_enabled: - self._accel_enabled = accel_enabled - self._imu.setAccelEnable(self._accel_enabled) - - def _read_imu(self): - """ - Internal. Tries to read the IMU sensor three times before giving up - """ - - self._init_imu() # Ensure imu is initialised - - attempts = 0 - success = False - - while not success and attempts < 3: - success = self._imu.IMURead() - attempts += 1 - time.sleep(self._imu_poll_interval) - - return success - - def _get_raw_data(self, is_valid_key, data_key): - """ - Internal. Returns the specified raw data from the IMU when valid - """ - - result = None - - if self._read_imu(): - data = self._imu.getIMUData() - if data[is_valid_key]: - raw = data[data_key] - result = { - 'x': raw[0], - 'y': raw[1], - 'z': raw[2] - } - - return result - - def get_orientation_radians(self): - """ - Returns a dictionary object to represent the current orientation in - radians using the aircraft principal axes of pitch, roll and yaw - """ - - raw = self._get_raw_data('fusionPoseValid', 'fusionPose') - - if raw is not None: - raw['roll'] = raw.pop('x') - raw['pitch'] = raw.pop('y') - raw['yaw'] = raw.pop('z') - self._last_orientation = raw - - return deepcopy(self._last_orientation) - - @property - def orientation_radians(self): - return self.get_orientation_radians() - - def get_orientation_degrees(self): - """ - Returns a dictionary object to represent the current orientation - in degrees, 0 to 360, using the aircraft principal axes of - pitch, roll and yaw - """ - - orientation = self.get_orientation_radians() - for key, val in orientation.items(): - deg = math.degrees(val) # Result is -180 to +180 - orientation[key] = deg + 360 if deg < 0 else deg - return orientation - - def get_orientation(self): - return self.get_orientation_degrees() - - @property - def orientation(self): - return self.get_orientation_degrees() - - def get_compass(self): - """ - Gets the direction of North from the magnetometer in degrees - """ - - self.set_imu_config(True, False, False) - orientation = self.get_orientation_degrees() - if type(orientation) is dict and 'yaw' in orientation.keys(): - return orientation['yaw'] - else: - return None - - @property - def compass(self): - return self.get_compass() - - def get_compass_raw(self): - """ - Magnetometer x y z raw data in uT (micro teslas) - """ - - raw = self._get_raw_data('compassValid', 'compass') - - if raw is not None: - self._last_compass_raw = raw - - return deepcopy(self._last_compass_raw) - - @property - def compass_raw(self): - return self.get_compass_raw() - - def get_gyroscope(self): - """ - Gets the orientation in degrees from the gyroscope only - """ - - self.set_imu_config(False, True, False) - return self.get_orientation_degrees() - - @property - def gyro(self): - return self.get_gyroscope() - - @property - def gyroscope(self): - return self.get_gyroscope() - - def get_gyroscope_raw(self): - """ - Gyroscope x y z raw data in radians per second - """ - - raw = self._get_raw_data('gyroValid', 'gyro') - - if raw is not None: - self._last_gyro_raw = raw - - return deepcopy(self._last_gyro_raw) - - @property - def gyro_raw(self): - return self.get_gyroscope_raw() - - @property - def gyroscope_raw(self): - return self.get_gyroscope_raw() - - def get_accelerometer(self): - """ - Gets the orientation in degrees from the accelerometer only - """ - - self.set_imu_config(False, False, True) - return self.get_orientation_degrees() - - @property - def accel(self): - return self.get_accelerometer() - - @property - def accelerometer(self): - return self.get_accelerometer() - - def get_accelerometer_raw(self): - """ - Accelerometer x y z raw data in Gs - """ - - raw = self._get_raw_data('accelValid', 'accel') - - if raw is not None: - self._last_accel_raw = raw - - return deepcopy(self._last_accel_raw) - - @property - def accel_raw(self): - return self.get_accelerometer_raw() - - @property - def accelerometer_raw(self): - return self.get_accelerometer_raw() diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/sense_hat_text.png deleted file mode 100644 index 747ed9cbe233d9dfe6d5a08bba122965791b81b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 908 zcmV;719SX|P)z3l1qDCPoV#0000PbVXQnLvL+uWo~o;Lvm$dbY)~9 zcWHEJAV*0}P*;Ht7XSbQ21!IgRA}DqTFG+5APhAc{{NThp_$kQEl)^^J5%Y@nqcj_ zkK<3k7~}dcNxy6BZ(D0yuX^<0M(ac|&xfCL6$k&@XU+_0V~p0C!F&uFORI*%e?!zq zV}*0RD&xkDO8#KFq~c(2SZE-#IGUg)3G5#{Nf3{+*RE)P|EBk^L_Vf7ATk&t z0(9>8UkS%C#$eBgWm^-;EWX<@k6+RtNw1~r)0u<`r*;O|?&>(0pKI?qsaOeRIENgk zU&U}Wwb>FGLMP^FWzC>U6|Jq>8y`Haz8uZi_576mW3OgladeSU5huhjaTd?enyz^2jBVE3*0R}>W88RA{ zKHDfd88k+rTUkxKL3x#n-`WYHJ<-o^Sq<7or-audVW}7Igl+EwZiJ1&xy6m=^H^fT znri6l1Yo-qFIXfE!ejNsj^&Mmc5$HX3M^e{*AG;E;d)oKPpusDNKB(jTx|tf8Gm7M z>!Y;ZF(x#)KMj250|ypnDm>259De0F6`Ala|t)J=mU8$7!|e>C8@RSPU96Pw2P(?Bh{n^lj3f`WpAf`WpA if`WpAf`WpAe+ob22BS=Py=(XY0000<0123456789.=)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?,;:|@%[&_']\~ diff --git a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py b/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py deleted file mode 100644 index 896a9f8..0000000 --- a/debian/python-sense-hat/usr/lib/python2.7/dist-packages/sense_hat/stick.py +++ /dev/null @@ -1,308 +0,0 @@ -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -native_str = str -str = type('') - -import io -import os -import glob -import errno -import struct -import select -import inspect -from functools import wraps -from collections import namedtuple -from threading import Thread, Event - - -DIRECTION_UP = 'up' -DIRECTION_DOWN = 'down' -DIRECTION_LEFT = 'left' -DIRECTION_RIGHT = 'right' -DIRECTION_MIDDLE = 'middle' - -ACTION_PRESSED = 'pressed' -ACTION_RELEASED = 'released' -ACTION_HELD = 'held' - - -InputEvent = namedtuple('InputEvent', ('timestamp', 'direction', 'action')) - - -class SenseStick(object): - """ - Represents the joystick on the Sense HAT. - """ - SENSE_HAT_EVDEV_NAME = 'Raspberry Pi Sense HAT Joystick' - EVENT_FORMAT = native_str('llHHI') - EVENT_SIZE = struct.calcsize(EVENT_FORMAT) - - EV_KEY = 0x01 - - STATE_RELEASE = 0 - STATE_PRESS = 1 - STATE_HOLD = 2 - - KEY_UP = 103 - KEY_LEFT = 105 - KEY_RIGHT = 106 - KEY_DOWN = 108 - KEY_ENTER = 28 - - def __init__(self): - self._stick_file = io.open(self._stick_device(), 'rb', buffering=0) - self._callbacks = {} - self._callback_thread = None - self._callback_event = Event() - - def close(self): - if self._stick_file: - self._callbacks.clear() - self._start_stop_thread() - self._stick_file.close() - self._stick_file = None - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_tb): - self.close() - - def _stick_device(self): - """ - Discovers the filename of the evdev device that represents the Sense - HAT's joystick. - """ - for evdev in glob.glob('/sys/class/input/event*'): - try: - with io.open(os.path.join(evdev, 'device', 'name'), 'r') as f: - if f.read().strip() == self.SENSE_HAT_EVDEV_NAME: - return os.path.join('/dev', 'input', os.path.basename(evdev)) - except IOError as e: - if e.errno != errno.ENOENT: - raise - raise RuntimeError('unable to locate SenseHAT joystick device') - - def _read(self): - """ - Reads a single event from the joystick, blocking until one is - available. Returns `None` if a non-key event was read, or an - `InputEvent` tuple describing the event otherwise. - """ - event = self._stick_file.read(self.EVENT_SIZE) - (tv_sec, tv_usec, type, code, value) = struct.unpack(self.EVENT_FORMAT, event) - if type == self.EV_KEY: - return InputEvent( - timestamp=tv_sec + (tv_usec / 1000000), - direction={ - self.KEY_UP: DIRECTION_UP, - self.KEY_DOWN: DIRECTION_DOWN, - self.KEY_LEFT: DIRECTION_LEFT, - self.KEY_RIGHT: DIRECTION_RIGHT, - self.KEY_ENTER: DIRECTION_MIDDLE, - }[code], - action={ - self.STATE_PRESS: ACTION_PRESSED, - self.STATE_RELEASE: ACTION_RELEASED, - self.STATE_HOLD: ACTION_HELD, - }[value]) - else: - return None - - def _wait(self, timeout=None): - """ - Waits *timeout* seconds until an event is available from the - joystick. Returns `True` if an event became available, and `False` - if the timeout expired. - """ - r, w, x = select.select([self._stick_file], [], [], timeout) - return bool(r) - - def _wrap_callback(self, fn): - # Shamelessley nicked (with some variation) from GPIO Zero :) - @wraps(fn) - def wrapper(event): - return fn() - - if fn is None: - return None - elif not callable(fn): - raise ValueError('value must be None or a callable') - elif inspect.isbuiltin(fn): - # We can't introspect the prototype of builtins. In this case we - # assume that the builtin has no (mandatory) parameters; this is - # the most reasonable assumption on the basis that pre-existing - # builtins have no knowledge of InputEvent, and the sole parameter - # we would pass is an InputEvent - return wrapper - else: - # Try binding ourselves to the argspec of the provided callable. - # If this works, assume the function is capable of accepting no - # parameters and that we have to wrap it to ignore the event - # parameter - try: - inspect.getcallargs(fn) - return wrapper - except TypeError: - try: - # If the above fails, try binding with a single tuple - # parameter. If this works, return the callback as is - inspect.getcallargs(fn, ()) - return fn - except TypeError: - raise ValueError( - 'value must be a callable which accepts up to one ' - 'mandatory parameter') - - def _start_stop_thread(self): - if self._callbacks and not self._callback_thread: - self._callback_event.clear() - self._callback_thread = Thread(target=self._callback_run) - self._callback_thread.daemon = True - self._callback_thread.start() - elif not self._callbacks and self._callback_thread: - self._callback_event.set() - self._callback_thread.join() - self._callback_thread = None - - def _callback_run(self): - while not self._callback_event.wait(0): - event = self._read() - if event: - callback = self._callbacks.get(event.direction) - if callback: - callback(event) - callback = self._callbacks.get('*') - if callback: - callback(event) - - def wait_for_event(self, emptybuffer=False): - """ - Waits until a joystick event becomes available. Returns the event, as - an `InputEvent` tuple. - - If *emptybuffer* is `True` (it defaults to `False`), any pending - events will be thrown away first. This is most useful if you are only - interested in "pressed" events. - """ - if emptybuffer: - while self._wait(0): - self._read() - while self._wait(): - event = self._read() - if event: - return event - - def get_events(self): - """ - Returns a list of all joystick events that have occurred since the last - call to `get_events`. The list contains events in the order that they - occurred. If no events have occurred in the intervening time, the - result is an empty list. - """ - result = [] - while self._wait(0): - event = self._read() - if event: - result.append(event) - return result - - @property - def direction_up(self): - """ - The function to be called when the joystick is pushed up. The function - can either take a parameter which will be the `InputEvent` tuple that - has occurred, or the function can take no parameters at all. - """ - return self._callbacks.get(DIRECTION_UP) - - @direction_up.setter - def direction_up(self, value): - self._callbacks[DIRECTION_UP] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_down(self): - """ - The function to be called when the joystick is pushed down. The - function can either take a parameter which will be the `InputEvent` - tuple that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_DOWN) - - @direction_down.setter - def direction_down(self, value): - self._callbacks[DIRECTION_DOWN] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_left(self): - """ - The function to be called when the joystick is pushed left. The - function can either take a parameter which will be the `InputEvent` - tuple that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_LEFT) - - @direction_left.setter - def direction_left(self, value): - self._callbacks[DIRECTION_LEFT] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_right(self): - """ - The function to be called when the joystick is pushed right. The - function can either take a parameter which will be the `InputEvent` - tuple that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_RIGHT) - - @direction_right.setter - def direction_right(self, value): - self._callbacks[DIRECTION_RIGHT] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_middle(self): - """ - The function to be called when the joystick middle click is pressed. The - function can either take a parameter which will be the `InputEvent` tuple - that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_MIDDLE) - - @direction_middle.setter - def direction_middle(self, value): - self._callbacks[DIRECTION_MIDDLE] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_any(self): - """ - The function to be called when the joystick is used. The function - can either take a parameter which will be the `InputEvent` tuple that - has occurred, or the function can take no parameters at all. - - This event will always be called *after* events associated with a - specific action. Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get('*') - - @direction_any.setter - def direction_any(self, value): - self._callbacks['*'] = self._wrap_callback(value) - self._start_stop_thread() - diff --git a/debian/python-sense-hat/usr/share/doc/python-sense-hat/changelog.gz b/debian/python-sense-hat/usr/share/doc/python-sense-hat/changelog.gz deleted file mode 100644 index f4127305a6dce1d896937eca51583fc02ac611f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmV-60m=R!iwFP!000021D(>}O2aS|2Jrhn#kt(vT27L6V*G=HO;E&(RUaUBhc(is zB}tXu`0Qq5Q;}`zaup6A;m3E%$81%F&-uwOs~O)|w&UDNNdB zK(X-(CAbWi0kVB4wi{K}Z=WJ|wCJ&=A@Fo7N&!>J&Lr|fBK%#1-$bj?oQ$dnd<{Pz z{3m$5SI)k`K`X|Ri!E3=Z|g=wkA+&&gU(x{y;kLs)vYBc;I1t<)d|VNCrdYy5224? IbNm7T00rlh)Bpeg diff --git a/debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright b/debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright deleted file mode 100644 index 09392d9..0000000 --- a/debian/python-sense-hat/usr/share/doc/python-sense-hat/copyright +++ /dev/null @@ -1,32 +0,0 @@ -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/python3-sense-hat.debhelper.log b/debian/python3-sense-hat.debhelper.log deleted file mode 100644 index de33f70..0000000 --- a/debian/python3-sense-hat.debhelper.log +++ /dev/null @@ -1,19 +0,0 @@ -dh_update_autotools_config -dh_auto_configure -dh_auto_build -dh_auto_test -dh_prep -dh_auto_install -dh_installdocs -dh_installchangelogs -dh_installinit -dh_perl -dh_link -dh_strip_nondeterminism -dh_compress -dh_fixperms -dh_missing -dh_installdeb -dh_gencontrol -dh_md5sums -dh_builddeb diff --git a/debian/python3-sense-hat.postinst.debhelper b/debian/python3-sense-hat.postinst.debhelper deleted file mode 100644 index 00683f3..0000000 --- a/debian/python3-sense-hat.postinst.debhelper +++ /dev/null @@ -1,10 +0,0 @@ - -# Automatically added by dh_python3: -if which py3compile >/dev/null 2>&1; then - py3compile -p python3-sense-hat -fi -if which pypy3compile >/dev/null 2>&1; then - pypy3compile -p python3-sense-hat || true -fi - -# End automatically added section diff --git a/debian/python3-sense-hat.prerm.debhelper b/debian/python3-sense-hat.prerm.debhelper deleted file mode 100644 index 632db81..0000000 --- a/debian/python3-sense-hat.prerm.debhelper +++ /dev/null @@ -1,10 +0,0 @@ - -# Automatically added by dh_python3: -if which py3clean >/dev/null 2>&1; then - py3clean -p python3-sense-hat -else - dpkg -L python3-sense-hat | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' - find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir -fi - -# End automatically added section diff --git a/debian/python3-sense-hat.substvars b/debian/python3-sense-hat.substvars deleted file mode 100644 index 3015a68..0000000 --- a/debian/python3-sense-hat.substvars +++ /dev/null @@ -1,3 +0,0 @@ -python3:Depends=python3-numpy, python3-pil, python3:any -misc:Depends= -misc:Pre-Depends= diff --git a/debian/python3-sense-hat/DEBIAN/control b/debian/python3-sense-hat/DEBIAN/control deleted file mode 100644 index 93297ca..0000000 --- a/debian/python3-sense-hat/DEBIAN/control +++ /dev/null @@ -1,16 +0,0 @@ -Package: python3-sense-hat -Source: python-sense-hat -Version: 2.3.0~test0 -Architecture: all -Maintainer: Serge Schneider -Installed-Size: 67 -Depends: python3-numpy, python3-pil, python3:any, python3-rtimulib -Section: python -Priority: optional -Homepage: https://github.com/RPi-Distro/python-sense-hat -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/python3-sense-hat/DEBIAN/md5sums b/debian/python3-sense-hat/DEBIAN/md5sums deleted file mode 100644 index b66791d..0000000 --- a/debian/python3-sense-hat/DEBIAN/md5sums +++ /dev/null @@ -1,12 +0,0 @@ -35dd286b437b2916c40cdc4ed7075e50 usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/PKG-INFO -68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/dependency_links.txt -d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/requires.txt -d485d30aa3a3f58b8b0db6e71f19ce20 usr/lib/python3/dist-packages/sense_hat-2.3.0.egg-info/top_level.txt -6f29e2883acf35bf03d73cc70c5e5db7 usr/lib/python3/dist-packages/sense_hat/__init__.py -776c985173201b7c38f016a3320cb8d6 usr/lib/python3/dist-packages/sense_hat/colour.py -48c3612fdc876097daccd0cad726ed91 usr/lib/python3/dist-packages/sense_hat/sense_hat.py -a90fdb250c9803bf822190b12636c73e usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png -ea665cb6f674eea5244a3e39051e45cf usr/lib/python3/dist-packages/sense_hat/sense_hat_text.txt -aecf59111307ec7eafb6192e8fbd1429 usr/lib/python3/dist-packages/sense_hat/stick.py -68832ab63c7ca094f1d1e31f84be94c8 usr/share/doc/python3-sense-hat/changelog.gz -ca593700544765aae69f76ecf48ec56b usr/share/doc/python3-sense-hat/copyright diff --git a/debian/python3-sense-hat/DEBIAN/postinst b/debian/python3-sense-hat/DEBIAN/postinst deleted file mode 100755 index c10ee09..0000000 --- a/debian/python3-sense-hat/DEBIAN/postinst +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -e - -# Automatically added by dh_python3: -if which py3compile >/dev/null 2>&1; then - py3compile -p python3-sense-hat -fi -if which pypy3compile >/dev/null 2>&1; then - pypy3compile -p python3-sense-hat || true -fi - -# End automatically added section diff --git a/debian/python3-sense-hat/DEBIAN/prerm b/debian/python3-sense-hat/DEBIAN/prerm deleted file mode 100755 index e9db4dd..0000000 --- a/debian/python3-sense-hat/DEBIAN/prerm +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -e - -# Automatically added by dh_python3: -if which py3clean >/dev/null 2>&1; then - py3clean -p python3-sense-hat -else - dpkg -L python3-sense-hat | perl -ne 's,/([^/]*)\.py$,/__pycache__/\1.*, or next; unlink $_ or die $! foreach glob($_)' - find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir -fi - -# End automatically added section diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py deleted file mode 100644 index d723807..0000000 --- a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import absolute_import -from .sense_hat import SenseHat, SenseHat as AstroPi -from .stick import ( - SenseStick, - InputEvent, - DIRECTION_UP, - DIRECTION_DOWN, - DIRECTION_LEFT, - DIRECTION_RIGHT, - DIRECTION_MIDDLE, - ACTION_PRESSED, - ACTION_RELEASED, - ACTION_HELD, - ) - -__version__ = '2.3.0' diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py deleted file mode 100644 index c638bbf..0000000 --- a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/colour.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -Python library for the TCS34725 Color Sensor - -Documentation (including datasheet): https://ams.com/tcs34725#tab/documents -""" - -import smbus -import glob -from time import sleep - -def i2c_enabled(): - return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None - -def _raw_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_raw(self): - value = self.bus.read_word_data(self.ADDR, register) - return value - return get_raw - -def _byte_wrapper(register): - """ - Returns a function that retrieves the sensor reading at `register`, scaled to 0-255. - The CRGB readings are all retrieved from the sensor in an identical fashion. - This is a factory function that implements this retrieval method. - """ - def get_byte(self): - value = self.bus.read_word_data(self.ADDR, register) // self._scaling - return value - return get_byte - - -class ColourSensor: - - # 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 - PON = 0x01 - AEN = 0x02 - AVALID = 0x01 - - GAIN_MAP = {1: 0x00, 4: 0x01, 16: 0x02, 60: 0x03} # maps gain values to register values - GAIN_INV = dict(zip(GAIN_MAP.values(), GAIN_MAP.keys())) - - CLOCK_STEP = 0.0024 # the clock step is 2.4ms - - def __init__(self, gain=1, integration_cycles=1): - try: - self.bus = smbus.SMBus(self.BUS) - except Exception: - explanation = " (I2C is not enabled)" if not i2c_enabled() else "" - raise RuntimeError(f'Failed to initialise TCS34725 colour sensor.{explanation}') - else: - if self._id != 0x44: - raise RuntimeError(f'Not connected to TCS34725 (id: {self._id})') - self.gain = gain - self.integration_cycles=integration_cycles - self.enabled= 1 - - @property - def _id(self): - return self.bus.read_byte_data(self.ADDR, self.ID) - - @property - def enabled(self): - return self.bus.read_byte_data(self.ADDR, self.ENABLE) == (self.PON | self.AEN) - - @enabled.setter - def enabled(self, status): - if status: - self.bus.write_byte_data(self.ADDR, (self.ENABLE), self.PON) - sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." - self.bus.write_byte_data(self.ADDR, (self.ENABLE), (self.PON | self.AEN)) - else: - self.bus.write_byte_data(self.ADDR, (self.ENABLE), 0x00) - sleep(self.CLOCK_STEP) - - @property - def gain(self): - return self.GAIN_INV[self.bus.read_byte_data(self.ADDR, self.CONTROL)] - - @gain.setter - def gain(self, value): - if value in self.GAIN_MAP: - self.bus.write_byte_data(self.ADDR, self.CONTROL, self.GAIN_MAP[value]) - sleep(self.CLOCK_STEP) - else: - raise RuntimeError(f'Cannot set gain to {value}. {tuple(self.GAIN_MAP.keys())}') - - @property - def integration_cycles(self): - return 256 - self.bus.read_byte_data(self.ADDR, self.ATIME) - - @integration_cycles.setter - def integration_cycles(self, cycles): - if 1 <= cycles <= 256: - self.bus.write_byte_data(self.ADDR, self.ATIME, 256-cycles) - self.integration_time = cycles * self.CLOCK_STEP - self.max_value = 2**16 if cycles >= 64 else 1024*cycles - self._scaling = self.max_value // 256 - sleep(self.CLOCK_STEP) - else: - raise RuntimeError(f'Cannot set integration cycles to {cycles} (1-256)') - - @property - def colour_raw(self): - 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] - ) - - @property - def colour(self): - return tuple(reading // self._scaling for reading in self.colour_raw) - - clear_raw = property(_raw_wrapper(CDATA)) - red_raw = property(_raw_wrapper(RDATA)) - green_raw = property(_raw_wrapper(GDATA)) - blue_raw = property(_raw_wrapper(BDATA)) - - clear = property(_byte_wrapper(CDATA)) - red = property(_byte_wrapper(RDATA)) - green = property(_byte_wrapper(GDATA)) - blue = property(_byte_wrapper(BDATA)) diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py deleted file mode 100644 index 8494309..0000000 --- a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat.py +++ /dev/null @@ -1,883 +0,0 @@ -#!/usr/bin/python -import struct -import os -import sys -import math -import time -import numpy as np -import shutil -import glob -import RTIMU # custom version -import pwd -import array -import fcntl -from PIL import Image # pillow -from copy import deepcopy - -from .stick import SenseStick -from .colour import ColourSensor - -class SenseHat(object): - - SENSE_HAT_FB_NAME = 'RPi-Sense FB' - SENSE_HAT_FB_FBIOGET_GAMMA = 61696 - SENSE_HAT_FB_FBIOSET_GAMMA = 61697 - SENSE_HAT_FB_FBIORESET_GAMMA = 61698 - SENSE_HAT_FB_GAMMA_DEFAULT = 0 - SENSE_HAT_FB_GAMMA_LOW = 1 - SENSE_HAT_FB_GAMMA_USER = 2 - SETTINGS_HOME_PATH = '.config/sense_hat' - - def __init__( - self, - imu_settings_file='RTIMULib', - text_assets='sense_hat_text' - ): - - self._fb_device = self._get_fb_device() - if self._fb_device is None: - raise OSError('Cannot detect %s device' % self.SENSE_HAT_FB_NAME) - - if not glob.glob('/dev/i2c*'): - raise OSError('Cannot access I2C. Please ensure I2C is enabled in raspi-config') - - # 0 is With B+ HDMI port facing downwards - pix_map0 = np.array([ - [0, 1, 2, 3, 4, 5, 6, 7], - [8, 9, 10, 11, 12, 13, 14, 15], - [16, 17, 18, 19, 20, 21, 22, 23], - [24, 25, 26, 27, 28, 29, 30, 31], - [32, 33, 34, 35, 36, 37, 38, 39], - [40, 41, 42, 43, 44, 45, 46, 47], - [48, 49, 50, 51, 52, 53, 54, 55], - [56, 57, 58, 59, 60, 61, 62, 63] - ], int) - - pix_map90 = np.rot90(pix_map0) - pix_map180 = np.rot90(pix_map90) - pix_map270 = np.rot90(pix_map180) - - self._pix_map = { - 0: pix_map0, - 90: pix_map90, - 180: pix_map180, - 270: pix_map270 - } - - self._rotation = 0 - - # Load text assets - dir_path = os.path.dirname(__file__) - self._load_text_assets( - os.path.join(dir_path, '%s.png' % text_assets), - os.path.join(dir_path, '%s.txt' % text_assets) - ) - - # Load IMU settings and calibration data - self._imu_settings = self._get_settings_file(imu_settings_file) - self._imu = RTIMU.RTIMU(self._imu_settings) - self._imu_init = False # Will be initialised as and when needed - self._pressure = RTIMU.RTPressure(self._imu_settings) - self._pressure_init = False # Will be initialised as and when needed - self._humidity = RTIMU.RTHumidity(self._imu_settings) - self._humidity_init = False # Will be initialised as and when needed - self._last_orientation = {'pitch': 0, 'roll': 0, 'yaw': 0} - raw = {'x': 0, 'y': 0, 'z': 0} - self._last_compass_raw = deepcopy(raw) - self._last_gyro_raw = deepcopy(raw) - self._last_accel_raw = deepcopy(raw) - self._compass_enabled = False - self._gyro_enabled = False - self._accel_enabled = False - self._stick = SenseStick() - - # initialise the TCS34725 colour sensor (if possible) - try: - self._colour = ColourSensor() - except: - pass - - #### - # Text assets - #### - - # Text asset files are rotated right through 90 degrees to allow blocks of - # 40 contiguous pixels to represent one 5 x 8 character. These are stored - # in a 8 x 640 pixel png image with characters arranged adjacently - # Consequently we must rotate the pixel map left through 90 degrees to - # compensate when drawing text - - def _load_text_assets(self, text_image_file, text_file): - """ - Internal. Builds a character indexed dictionary of pixels used by the - show_message function below - """ - - text_pixels = self.load_image(text_image_file, False) - with open(text_file, 'r') as f: - loaded_text = f.read() - self._text_dict = {} - for index, s in enumerate(loaded_text): - start = index * 40 - end = start + 40 - char = text_pixels[start:end] - self._text_dict[s] = char - - def _trim_whitespace(self, char): # For loading text assets only - """ - Internal. Trims white space pixels from the front and back of loaded - text characters - """ - - psum = lambda x: sum(sum(x, [])) - if psum(char) > 0: - is_empty = True - while is_empty: # From front - row = char[0:8] - is_empty = psum(row) == 0 - if is_empty: - del char[0:8] - is_empty = True - while is_empty: # From back - row = char[-8:] - is_empty = psum(row) == 0 - if is_empty: - del char[-8:] - return char - - def _get_settings_file(self, imu_settings_file): - """ - Internal. Logic to check for a system wide RTIMU ini file. This is - copied to the home folder if one is not already found there. - """ - - ini_file = '%s.ini' % imu_settings_file - - home_dir = pwd.getpwuid(os.getuid())[5] - home_path = os.path.join(home_dir, self.SETTINGS_HOME_PATH) - if not os.path.exists(home_path): - os.makedirs(home_path) - - home_file = os.path.join(home_path, ini_file) - home_exists = os.path.isfile(home_file) - system_file = os.path.join('/etc', ini_file) - system_exists = os.path.isfile(system_file) - - if system_exists and not home_exists: - shutil.copyfile(system_file, home_file) - - return RTIMU.Settings(os.path.join(home_path, imu_settings_file)) # RTIMU will add .ini internally - - def _get_fb_device(self): - """ - Internal. Finds the correct frame buffer device for the sense HAT - and returns its /dev name. - """ - - device = None - - for fb in glob.glob('/sys/class/graphics/fb*'): - name_file = os.path.join(fb, 'name') - if os.path.isfile(name_file): - with open(name_file, 'r') as f: - name = f.read() - if name.strip() == self.SENSE_HAT_FB_NAME: - fb_device = fb.replace(os.path.dirname(fb), '/dev') - if os.path.exists(fb_device): - device = fb_device - break - - return device - - #### - # Joystick - #### - - @property - def stick(self): - return self._stick - - #### - # Colour sensor - #### - - @property - def colour(self): - return self._colour - - @property - def color(self): - return self._colour - - def has_colour_sensor(self): - try: - self._colour - except: - return False - else: - return True - - #### - # LED Matrix - #### - - @property - def rotation(self): - return self._rotation - - @rotation.setter - def rotation(self, r): - self.set_rotation(r, True) - - def set_rotation(self, r=0, redraw=True): - """ - Sets the LED matrix rotation for viewing, adjust if the Pi is upside - down or sideways. 0 is with the Pi HDMI port facing downwards - """ - - if r in self._pix_map.keys(): - if redraw: - pixel_list = self.get_pixels() - self._rotation = r - if redraw: - self.set_pixels(pixel_list) - else: - raise ValueError('Rotation must be 0, 90, 180 or 270 degrees') - - def _pack_bin(self, pix): - """ - Internal. Encodes python list [R,G,B] into 16 bit RGB565 - """ - - r = (pix[0] >> 3) & 0x1F - g = (pix[1] >> 2) & 0x3F - b = (pix[2] >> 3) & 0x1F - bits16 = (r << 11) + (g << 5) + b - return struct.pack('H', bits16) - - def _unpack_bin(self, packed): - """ - Internal. Decodes 16 bit RGB565 into python list [R,G,B] - """ - - output = struct.unpack('H', packed) - bits16 = output[0] - r = (bits16 & 0xF800) >> 11 - g = (bits16 & 0x7E0) >> 5 - b = (bits16 & 0x1F) - return [int(r << 3), int(g << 2), int(b << 3)] - - def flip_h(self, redraw=True): - """ - Flip LED matrix horizontal - """ - - pixel_list = self.get_pixels() - flipped = [] - for i in range(8): - offset = i * 8 - flipped.extend(reversed(pixel_list[offset:offset + 8])) - if redraw: - self.set_pixels(flipped) - return flipped - - def flip_v(self, redraw=True): - """ - Flip LED matrix vertical - """ - - pixel_list = self.get_pixels() - flipped = [] - for i in reversed(range(8)): - offset = i * 8 - flipped.extend(pixel_list[offset:offset + 8]) - if redraw: - self.set_pixels(flipped) - return flipped - - 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 - and 255 - """ - - if len(pixel_list) != 64: - raise ValueError('Pixel lists must have 64 elements') - - for index, pix in enumerate(pixel_list): - if 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: - if element > 255 or element < 0: - raise ValueError('Pixel at index %d is invalid. Pixel elements must be between 0 and 255' % index) - - with open(self._fb_device, 'wb') as f: - map = self._pix_map[self._rotation] - for index, pix in enumerate(pixel_list): - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[index // 8][index % 8] * 2) # row, column - f.write(self._pack_bin(pix)) - - def get_pixels(self): - """ - Returns a list containing 64 smaller lists of [R,G,B] pixels - representing what is currently displayed on the LED matrix - """ - - pixel_list = [] - with open(self._fb_device, 'rb') as f: - map = self._pix_map[self._rotation] - for row in range(8): - for col in range(8): - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[row][col] * 2) # row, column - pixel_list.append(self._unpack_bin(f.read(2))) - return pixel_list - - def set_pixel(self, x, y, *args): - """ - Updates the single [R,G,B] pixel specified by x and y on the LED matrix - Top left = 0,0 Bottom right = 7,7 - - e.g. ap.set_pixel(x, y, r, g, b) - or - pixel = (r, g, b) - 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) - - if x > 7 or x < 0: - raise ValueError('X position must be between 0 and 7') - - if y > 7 or y < 0: - raise ValueError('Y position must be between 0 and 7') - - for element in pixel: - if element > 255 or element < 0: - raise ValueError('Pixel elements must be between 0 and 255') - - with open(self._fb_device, 'wb') as f: - map = self._pix_map[self._rotation] - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[y][x] * 2) # row, column - f.write(self._pack_bin(pixel)) - - def get_pixel(self, x, y): - """ - Returns a list of [R,G,B] representing the pixel specified by x and y - on the LED matrix. Top left = 0,0 Bottom right = 7,7 - """ - - if x > 7 or x < 0: - raise ValueError('X position must be between 0 and 7') - - if y > 7 or y < 0: - raise ValueError('Y position must be between 0 and 7') - - pix = None - - with open(self._fb_device, 'rb') as f: - map = self._pix_map[self._rotation] - # Two bytes per pixel in fb memory, 16 bit RGB565 - f.seek(map[y][x] * 2) # row, column - pix = self._unpack_bin(f.read(2)) - - return pix - - def load_image(self, file_path, redraw=True): - """ - Accepts a path to an 8 x 8 image file and updates the LED matrix with - the image - """ - - if not os.path.exists(file_path): - raise IOError('%s not found' % file_path) - - img = Image.open(file_path).convert('RGB') - pixel_list = list(map(list, img.getdata())) - - if redraw: - self.set_pixels(pixel_list) - - return pixel_list - - def clear(self, *args): - """ - Clears the LED matrix with a single colour, default is black / off - - e.g. ap.clear() - or - ap.clear(r, g, b) - or - colour = (r, g, b) - ap.clear(colour) - """ - - black = (0, 0, 0) # default - - if len(args) == 0: - colour = black - elif len(args) == 1: - colour = args[0] - elif len(args) == 3: - colour = args - else: - raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') - - self.set_pixels([colour] * 64) - - def _get_char_pixels(self, s): - """ - Internal. Safeguards the character indexed dictionary for the - show_message function below - """ - - if len(s) == 1 and s in self._text_dict.keys(): - return list(self._text_dict[s]) - else: - return list(self._text_dict['?']) - - def show_message( - self, - text_string, - scroll_speed=.1, - text_colour=[255, 255, 255], - back_colour=[0, 0, 0] - ): - """ - Scrolls a string of text across the LED matrix using the specified - speed and colours - """ - - # We must rotate the pixel map left through 90 degrees when drawing - # text, see _load_text_assets - previous_rotation = self._rotation - self._rotation -= 90 - if self._rotation < 0: - self._rotation = 270 - dummy_colour = [None, None, None] - string_padding = [dummy_colour] * 64 - letter_padding = [dummy_colour] * 8 - # Build pixels from dictionary - scroll_pixels = [] - scroll_pixels.extend(string_padding) - for s in text_string: - scroll_pixels.extend(self._trim_whitespace(self._get_char_pixels(s))) - scroll_pixels.extend(letter_padding) - scroll_pixels.extend(string_padding) - # Recolour pixels as necessary - coloured_pixels = [ - text_colour if pixel == [255, 255, 255] else back_colour - for pixel in scroll_pixels - ] - # Shift right by 8 pixels per frame to scroll - scroll_length = len(coloured_pixels) // 8 - for i in range(scroll_length - 8): - start = i * 8 - end = start + 64 - self.set_pixels(coloured_pixels[start:end]) - time.sleep(scroll_speed) - self._rotation = previous_rotation - - def show_letter( - self, - s, - text_colour=[255, 255, 255], - back_colour=[0, 0, 0] - ): - """ - Displays a single text character on the LED matrix using the specified - colours - """ - - if len(s) > 1: - raise ValueError('Only one character may be passed into this method') - # We must rotate the pixel map left through 90 degrees when drawing - # text, see _load_text_assets - previous_rotation = self._rotation - self._rotation -= 90 - if self._rotation < 0: - self._rotation = 270 - dummy_colour = [None, None, None] - pixel_list = [dummy_colour] * 8 - pixel_list.extend(self._get_char_pixels(s)) - pixel_list.extend([dummy_colour] * 16) - coloured_pixels = [ - text_colour if pixel == [255, 255, 255] else back_colour - for pixel in pixel_list - ] - self.set_pixels(coloured_pixels) - self._rotation = previous_rotation - - @property - def gamma(self): - buffer = array.array('B', [0]*32) - with open(self._fb_device) as f: - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOGET_GAMMA, buffer) - return list(buffer) - - @gamma.setter - def gamma(self, buffer): - if len(buffer) is not 32: - raise ValueError('Gamma array must be of length 32') - - if not all(b <= 31 for b in buffer): - raise ValueError('Gamma values must be bewteen 0 and 31') - - if not isinstance(buffer, array.array): - buffer = array.array('B', buffer) - - with open(self._fb_device) as f: - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOSET_GAMMA, buffer) - - def gamma_reset(self): - """ - Resets the LED matrix gamma correction to default - """ - - with open(self._fb_device) as f: - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, self.SENSE_HAT_FB_GAMMA_DEFAULT) - - @property - def low_light(self): - return self.gamma == [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10] - - @low_light.setter - def low_light(self, value): - with open(self._fb_device) as f: - cmd = self.SENSE_HAT_FB_GAMMA_LOW if value else self.SENSE_HAT_FB_GAMMA_DEFAULT - fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, cmd) - - #### - # Environmental sensors - #### - - def _init_humidity(self): - """ - Internal. Initialises the humidity sensor via RTIMU - """ - - if not self._humidity_init: - self._humidity_init = self._humidity.humidityInit() - if not self._humidity_init: - raise OSError('Humidity Init Failed') - - def _init_pressure(self): - """ - Internal. Initialises the pressure sensor via RTIMU - """ - - if not self._pressure_init: - self._pressure_init = self._pressure.pressureInit() - if not self._pressure_init: - raise OSError('Pressure Init Failed') - - def get_humidity(self): - """ - Returns the percentage of relative humidity - """ - - self._init_humidity() # Ensure humidity sensor is initialised - humidity = 0 - data = self._humidity.humidityRead() - if (data[0]): # Humidity valid - humidity = data[1] - return humidity - - @property - def humidity(self): - return self.get_humidity() - - def get_temperature_from_humidity(self): - """ - Returns the temperature in Celsius from the humidity sensor - """ - - self._init_humidity() # Ensure humidity sensor is initialised - temp = 0 - data = self._humidity.humidityRead() - if (data[2]): # Temp valid - temp = data[3] - return temp - - def get_temperature_from_pressure(self): - """ - Returns the temperature in Celsius from the pressure sensor - """ - - self._init_pressure() # Ensure pressure sensor is initialised - temp = 0 - data = self._pressure.pressureRead() - if (data[2]): # Temp valid - temp = data[3] - return temp - - def get_temperature(self): - """ - Returns the temperature in Celsius - """ - - return self.get_temperature_from_humidity() - - @property - def temp(self): - return self.get_temperature_from_humidity() - - @property - def temperature(self): - return self.get_temperature_from_humidity() - - def get_pressure(self): - """ - Returns the pressure in Millibars - """ - - self._init_pressure() # Ensure pressure sensor is initialised - pressure = 0 - data = self._pressure.pressureRead() - if (data[0]): # Pressure valid - pressure = data[1] - return pressure - - @property - def pressure(self): - return self.get_pressure() - - #### - # IMU Sensor - #### - - def _init_imu(self): - """ - Internal. Initialises the IMU sensor via RTIMU - """ - - if not self._imu_init: - self._imu_init = self._imu.IMUInit() - if self._imu_init: - self._imu_poll_interval = self._imu.IMUGetPollInterval() * 0.001 - # Enable everything on IMU - self.set_imu_config(True, True, True) - else: - raise OSError('IMU Init Failed') - - def set_imu_config(self, compass_enabled, gyro_enabled, accel_enabled): - """ - Enables and disables the gyroscope, accelerometer and/or magnetometer - input to the orientation functions - """ - - # If the consuming code always calls this just before reading the IMU - # the IMU consistently fails to read. So prevent unnecessary calls to - # IMU config functions using state variables - - self._init_imu() # Ensure imu is initialised - - if (not isinstance(compass_enabled, bool) - or not isinstance(gyro_enabled, bool) - or not isinstance(accel_enabled, bool)): - raise TypeError('All set_imu_config parameters must be of boolean type') - - if self._compass_enabled != compass_enabled: - self._compass_enabled = compass_enabled - self._imu.setCompassEnable(self._compass_enabled) - - if self._gyro_enabled != gyro_enabled: - self._gyro_enabled = gyro_enabled - self._imu.setGyroEnable(self._gyro_enabled) - - if self._accel_enabled != accel_enabled: - self._accel_enabled = accel_enabled - self._imu.setAccelEnable(self._accel_enabled) - - def _read_imu(self): - """ - Internal. Tries to read the IMU sensor three times before giving up - """ - - self._init_imu() # Ensure imu is initialised - - attempts = 0 - success = False - - while not success and attempts < 3: - success = self._imu.IMURead() - attempts += 1 - time.sleep(self._imu_poll_interval) - - return success - - def _get_raw_data(self, is_valid_key, data_key): - """ - Internal. Returns the specified raw data from the IMU when valid - """ - - result = None - - if self._read_imu(): - data = self._imu.getIMUData() - if data[is_valid_key]: - raw = data[data_key] - result = { - 'x': raw[0], - 'y': raw[1], - 'z': raw[2] - } - - return result - - def get_orientation_radians(self): - """ - Returns a dictionary object to represent the current orientation in - radians using the aircraft principal axes of pitch, roll and yaw - """ - - raw = self._get_raw_data('fusionPoseValid', 'fusionPose') - - if raw is not None: - raw['roll'] = raw.pop('x') - raw['pitch'] = raw.pop('y') - raw['yaw'] = raw.pop('z') - self._last_orientation = raw - - return deepcopy(self._last_orientation) - - @property - def orientation_radians(self): - return self.get_orientation_radians() - - def get_orientation_degrees(self): - """ - Returns a dictionary object to represent the current orientation - in degrees, 0 to 360, using the aircraft principal axes of - pitch, roll and yaw - """ - - orientation = self.get_orientation_radians() - for key, val in orientation.items(): - deg = math.degrees(val) # Result is -180 to +180 - orientation[key] = deg + 360 if deg < 0 else deg - return orientation - - def get_orientation(self): - return self.get_orientation_degrees() - - @property - def orientation(self): - return self.get_orientation_degrees() - - def get_compass(self): - """ - Gets the direction of North from the magnetometer in degrees - """ - - self.set_imu_config(True, False, False) - orientation = self.get_orientation_degrees() - if type(orientation) is dict and 'yaw' in orientation.keys(): - return orientation['yaw'] - else: - return None - - @property - def compass(self): - return self.get_compass() - - def get_compass_raw(self): - """ - Magnetometer x y z raw data in uT (micro teslas) - """ - - raw = self._get_raw_data('compassValid', 'compass') - - if raw is not None: - self._last_compass_raw = raw - - return deepcopy(self._last_compass_raw) - - @property - def compass_raw(self): - return self.get_compass_raw() - - def get_gyroscope(self): - """ - Gets the orientation in degrees from the gyroscope only - """ - - self.set_imu_config(False, True, False) - return self.get_orientation_degrees() - - @property - def gyro(self): - return self.get_gyroscope() - - @property - def gyroscope(self): - return self.get_gyroscope() - - def get_gyroscope_raw(self): - """ - Gyroscope x y z raw data in radians per second - """ - - raw = self._get_raw_data('gyroValid', 'gyro') - - if raw is not None: - self._last_gyro_raw = raw - - return deepcopy(self._last_gyro_raw) - - @property - def gyro_raw(self): - return self.get_gyroscope_raw() - - @property - def gyroscope_raw(self): - return self.get_gyroscope_raw() - - def get_accelerometer(self): - """ - Gets the orientation in degrees from the accelerometer only - """ - - self.set_imu_config(False, False, True) - return self.get_orientation_degrees() - - @property - def accel(self): - return self.get_accelerometer() - - @property - def accelerometer(self): - return self.get_accelerometer() - - def get_accelerometer_raw(self): - """ - Accelerometer x y z raw data in Gs - """ - - raw = self._get_raw_data('accelValid', 'accel') - - if raw is not None: - self._last_accel_raw = raw - - return deepcopy(self._last_accel_raw) - - @property - def accel_raw(self): - return self.get_accelerometer_raw() - - @property - def accelerometer_raw(self): - return self.get_accelerometer_raw() diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/sense_hat_text.png deleted file mode 100644 index 747ed9cbe233d9dfe6d5a08bba122965791b81b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 908 zcmV;719SX|P)z3l1qDCPoV#0000PbVXQnLvL+uWo~o;Lvm$dbY)~9 zcWHEJAV*0}P*;Ht7XSbQ21!IgRA}DqTFG+5APhAc{{NThp_$kQEl)^^J5%Y@nqcj_ zkK<3k7~}dcNxy6BZ(D0yuX^<0M(ac|&xfCL6$k&@XU+_0V~p0C!F&uFORI*%e?!zq zV}*0RD&xkDO8#KFq~c(2SZE-#IGUg)3G5#{Nf3{+*RE)P|EBk^L_Vf7ATk&t z0(9>8UkS%C#$eBgWm^-;EWX<@k6+RtNw1~r)0u<`r*;O|?&>(0pKI?qsaOeRIENgk zU&U}Wwb>FGLMP^FWzC>U6|Jq>8y`Haz8uZi_576mW3OgladeSU5huhjaTd?enyz^2jBVE3*0R}>W88RA{ zKHDfd88k+rTUkxKL3x#n-`WYHJ<-o^Sq<7or-audVW}7Igl+EwZiJ1&xy6m=^H^fT znri6l1Yo-qFIXfE!ejNsj^&Mmc5$HX3M^e{*AG;E;d)oKPpusDNKB(jTx|tf8Gm7M z>!Y;ZF(x#)KMj250|ypnDm>259De0F6`Ala|t)J=mU8$7!|e>C8@RSPU96Pw2P(?Bh{n^lj3f`WpAf`WpA if`WpAf`WpAe+ob22BS=Py=(XY0000<0123456789.=)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?,;:|@%[&_']\~ diff --git a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py b/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py deleted file mode 100644 index 896a9f8..0000000 --- a/debian/python3-sense-hat/usr/lib/python3/dist-packages/sense_hat/stick.py +++ /dev/null @@ -1,308 +0,0 @@ -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -native_str = str -str = type('') - -import io -import os -import glob -import errno -import struct -import select -import inspect -from functools import wraps -from collections import namedtuple -from threading import Thread, Event - - -DIRECTION_UP = 'up' -DIRECTION_DOWN = 'down' -DIRECTION_LEFT = 'left' -DIRECTION_RIGHT = 'right' -DIRECTION_MIDDLE = 'middle' - -ACTION_PRESSED = 'pressed' -ACTION_RELEASED = 'released' -ACTION_HELD = 'held' - - -InputEvent = namedtuple('InputEvent', ('timestamp', 'direction', 'action')) - - -class SenseStick(object): - """ - Represents the joystick on the Sense HAT. - """ - SENSE_HAT_EVDEV_NAME = 'Raspberry Pi Sense HAT Joystick' - EVENT_FORMAT = native_str('llHHI') - EVENT_SIZE = struct.calcsize(EVENT_FORMAT) - - EV_KEY = 0x01 - - STATE_RELEASE = 0 - STATE_PRESS = 1 - STATE_HOLD = 2 - - KEY_UP = 103 - KEY_LEFT = 105 - KEY_RIGHT = 106 - KEY_DOWN = 108 - KEY_ENTER = 28 - - def __init__(self): - self._stick_file = io.open(self._stick_device(), 'rb', buffering=0) - self._callbacks = {} - self._callback_thread = None - self._callback_event = Event() - - def close(self): - if self._stick_file: - self._callbacks.clear() - self._start_stop_thread() - self._stick_file.close() - self._stick_file = None - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, exc_tb): - self.close() - - def _stick_device(self): - """ - Discovers the filename of the evdev device that represents the Sense - HAT's joystick. - """ - for evdev in glob.glob('/sys/class/input/event*'): - try: - with io.open(os.path.join(evdev, 'device', 'name'), 'r') as f: - if f.read().strip() == self.SENSE_HAT_EVDEV_NAME: - return os.path.join('/dev', 'input', os.path.basename(evdev)) - except IOError as e: - if e.errno != errno.ENOENT: - raise - raise RuntimeError('unable to locate SenseHAT joystick device') - - def _read(self): - """ - Reads a single event from the joystick, blocking until one is - available. Returns `None` if a non-key event was read, or an - `InputEvent` tuple describing the event otherwise. - """ - event = self._stick_file.read(self.EVENT_SIZE) - (tv_sec, tv_usec, type, code, value) = struct.unpack(self.EVENT_FORMAT, event) - if type == self.EV_KEY: - return InputEvent( - timestamp=tv_sec + (tv_usec / 1000000), - direction={ - self.KEY_UP: DIRECTION_UP, - self.KEY_DOWN: DIRECTION_DOWN, - self.KEY_LEFT: DIRECTION_LEFT, - self.KEY_RIGHT: DIRECTION_RIGHT, - self.KEY_ENTER: DIRECTION_MIDDLE, - }[code], - action={ - self.STATE_PRESS: ACTION_PRESSED, - self.STATE_RELEASE: ACTION_RELEASED, - self.STATE_HOLD: ACTION_HELD, - }[value]) - else: - return None - - def _wait(self, timeout=None): - """ - Waits *timeout* seconds until an event is available from the - joystick. Returns `True` if an event became available, and `False` - if the timeout expired. - """ - r, w, x = select.select([self._stick_file], [], [], timeout) - return bool(r) - - def _wrap_callback(self, fn): - # Shamelessley nicked (with some variation) from GPIO Zero :) - @wraps(fn) - def wrapper(event): - return fn() - - if fn is None: - return None - elif not callable(fn): - raise ValueError('value must be None or a callable') - elif inspect.isbuiltin(fn): - # We can't introspect the prototype of builtins. In this case we - # assume that the builtin has no (mandatory) parameters; this is - # the most reasonable assumption on the basis that pre-existing - # builtins have no knowledge of InputEvent, and the sole parameter - # we would pass is an InputEvent - return wrapper - else: - # Try binding ourselves to the argspec of the provided callable. - # If this works, assume the function is capable of accepting no - # parameters and that we have to wrap it to ignore the event - # parameter - try: - inspect.getcallargs(fn) - return wrapper - except TypeError: - try: - # If the above fails, try binding with a single tuple - # parameter. If this works, return the callback as is - inspect.getcallargs(fn, ()) - return fn - except TypeError: - raise ValueError( - 'value must be a callable which accepts up to one ' - 'mandatory parameter') - - def _start_stop_thread(self): - if self._callbacks and not self._callback_thread: - self._callback_event.clear() - self._callback_thread = Thread(target=self._callback_run) - self._callback_thread.daemon = True - self._callback_thread.start() - elif not self._callbacks and self._callback_thread: - self._callback_event.set() - self._callback_thread.join() - self._callback_thread = None - - def _callback_run(self): - while not self._callback_event.wait(0): - event = self._read() - if event: - callback = self._callbacks.get(event.direction) - if callback: - callback(event) - callback = self._callbacks.get('*') - if callback: - callback(event) - - def wait_for_event(self, emptybuffer=False): - """ - Waits until a joystick event becomes available. Returns the event, as - an `InputEvent` tuple. - - If *emptybuffer* is `True` (it defaults to `False`), any pending - events will be thrown away first. This is most useful if you are only - interested in "pressed" events. - """ - if emptybuffer: - while self._wait(0): - self._read() - while self._wait(): - event = self._read() - if event: - return event - - def get_events(self): - """ - Returns a list of all joystick events that have occurred since the last - call to `get_events`. The list contains events in the order that they - occurred. If no events have occurred in the intervening time, the - result is an empty list. - """ - result = [] - while self._wait(0): - event = self._read() - if event: - result.append(event) - return result - - @property - def direction_up(self): - """ - The function to be called when the joystick is pushed up. The function - can either take a parameter which will be the `InputEvent` tuple that - has occurred, or the function can take no parameters at all. - """ - return self._callbacks.get(DIRECTION_UP) - - @direction_up.setter - def direction_up(self, value): - self._callbacks[DIRECTION_UP] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_down(self): - """ - The function to be called when the joystick is pushed down. The - function can either take a parameter which will be the `InputEvent` - tuple that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_DOWN) - - @direction_down.setter - def direction_down(self, value): - self._callbacks[DIRECTION_DOWN] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_left(self): - """ - The function to be called when the joystick is pushed left. The - function can either take a parameter which will be the `InputEvent` - tuple that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_LEFT) - - @direction_left.setter - def direction_left(self, value): - self._callbacks[DIRECTION_LEFT] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_right(self): - """ - The function to be called when the joystick is pushed right. The - function can either take a parameter which will be the `InputEvent` - tuple that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_RIGHT) - - @direction_right.setter - def direction_right(self, value): - self._callbacks[DIRECTION_RIGHT] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_middle(self): - """ - The function to be called when the joystick middle click is pressed. The - function can either take a parameter which will be the `InputEvent` tuple - that has occurred, or the function can take no parameters at all. - - Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get(DIRECTION_MIDDLE) - - @direction_middle.setter - def direction_middle(self, value): - self._callbacks[DIRECTION_MIDDLE] = self._wrap_callback(value) - self._start_stop_thread() - - @property - def direction_any(self): - """ - The function to be called when the joystick is used. The function - can either take a parameter which will be the `InputEvent` tuple that - has occurred, or the function can take no parameters at all. - - This event will always be called *after* events associated with a - specific action. Assign `None` to prevent this event from being fired. - """ - return self._callbacks.get('*') - - @direction_any.setter - def direction_any(self, value): - self._callbacks['*'] = self._wrap_callback(value) - self._start_stop_thread() - diff --git a/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/changelog.gz b/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/changelog.gz deleted file mode 100644 index f4127305a6dce1d896937eca51583fc02ac611f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmV-60m=R!iwFP!000021D(>}O2aS|2Jrhn#kt(vT27L6V*G=HO;E&(RUaUBhc(is zB}tXu`0Qq5Q;}`zaup6A;m3E%$81%F&-uwOs~O)|w&UDNNdB zK(X-(CAbWi0kVB4wi{K}Z=WJ|wCJ&=A@Fo7N&!>J&Lr|fBK%#1-$bj?oQ$dnd<{Pz z{3m$5SI)k`K`X|Ri!E3=Z|g=wkA+&&gU(x{y;kLs)vYBc;I1t<)d|VNCrdYy5224? IbNm7T00rlh)Bpeg diff --git a/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright b/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright deleted file mode 100644 index 09392d9..0000000 --- a/debian/python3-sense-hat/usr/share/doc/python3-sense-hat/copyright +++ /dev/null @@ -1,32 +0,0 @@ -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. From afb680e6f2d1ea72371b573d1c67090c35a466fb Mon Sep 17 00:00:00 2001 From: George Boukeas Date: Wed, 21 Jul 2021 15:14:07 +0100 Subject: [PATCH 17/37] Prepare debian packaging for 2.3.1~test0 (for the kit OS) --- .../generated/python-sense-hat/installed-by-dh_installdocs | 0 .../generated/python3-sense-hat/installed-by-dh_installdocs | 0 debian/changelog | 6 ++++++ debian/control | 2 +- debian/files | 3 --- sense_hat/__init__.py | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 debian/.debhelper/generated/python-sense-hat/installed-by-dh_installdocs delete mode 100644 debian/.debhelper/generated/python3-sense-hat/installed-by-dh_installdocs delete mode 100644 debian/files diff --git a/debian/.debhelper/generated/python-sense-hat/installed-by-dh_installdocs b/debian/.debhelper/generated/python-sense-hat/installed-by-dh_installdocs deleted file mode 100644 index e69de29..0000000 diff --git a/debian/.debhelper/generated/python3-sense-hat/installed-by-dh_installdocs b/debian/.debhelper/generated/python3-sense-hat/installed-by-dh_installdocs deleted file mode 100644 index e69de29..0000000 diff --git a/debian/changelog b/debian/changelog index d67c286..8d6d2af 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +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 diff --git a/debian/control b/debian/control index 04b031f..33e0070 100644 --- a/debian/control +++ b/debian/control @@ -23,7 +23,7 @@ Description: Sense HAT python library (Python 2) Package: python3-sense-hat Architecture: all Depends: ${misc:Depends}, ${python3:Depends}, python3-rtimulib, python3-pil, - python3-numpy + 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 diff --git a/debian/files b/debian/files deleted file mode 100644 index 62a4de2..0000000 --- a/debian/files +++ /dev/null @@ -1,3 +0,0 @@ -python-sense-hat_2.3.0~test0_all.deb python optional -python-sense-hat_2.3.0~test0_armhf.buildinfo python optional -python3-sense-hat_2.3.0~test0_all.deb python optional diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index d723807..2143620 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -13,4 +13,4 @@ ACTION_HELD, ) -__version__ = '2.3.0' +__version__ = '2.3.1' From aace39a401346cd62724b78410a03b8216342ae7 Mon Sep 17 00:00:00 2001 From: ZionChris1 <30394618+ZionChris1@users.noreply.github.com> Date: Fri, 31 Dec 2021 12:31:53 -0800 Subject: [PATCH 18/37] Fix typo in sense_hat.py --- sense_hat/sense_hat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 30597b9..7003dbe 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -272,7 +272,7 @@ 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 """ From 9a67533d643271130be87a59737a924ea5625ab0 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Wed, 1 Jun 2022 12:04:03 +0100 Subject: [PATCH 19/37] Small improvements to lib - Fix mkdocs warnings - Fix Python SyntaxWarning - Add static exceptions for colour sensor - Log warning when colour chip not present --- README.md | 1 + docs/api.md | 12 ++++++-- {examples => docs/examples}/README.md | 0 {examples => docs/examples}/colour_cycle.py | 0 {examples => docs/examples}/compass.py | 0 {examples => docs/examples}/evdev_joystick.py | 0 .../examples}/pygame_joystick.py | 0 {examples => docs/examples}/rainbow.py | 0 {examples => docs/examples}/rotation.py | 0 {examples => docs/examples}/space_invader.png | Bin {examples => docs/examples}/space_invader.py | 0 {examples => docs/examples}/text_scroll.py | 0 mkdocs.yml | 5 +-- sense_hat/colour.py | 29 ++++++++---------- sense_hat/exceptions.py | 22 +++++++++++++ sense_hat/sense_hat.py | 11 +++++-- 16 files changed, 56 insertions(+), 24 deletions(-) rename {examples => docs/examples}/README.md (100%) rename {examples => docs/examples}/colour_cycle.py (100%) rename {examples => docs/examples}/compass.py (100%) rename {examples => docs/examples}/evdev_joystick.py (100%) rename {examples => docs/examples}/pygame_joystick.py (100%) rename {examples => docs/examples}/rainbow.py (100%) rename {examples => docs/examples}/rotation.py (100%) rename {examples => docs/examples}/space_invader.png (100%) rename {examples => docs/examples}/space_invader.py (100%) rename {examples => docs/examples}/text_scroll.py (100%) create mode 100644 sense_hat/exceptions.py diff --git a/README.md b/README.md index 1e0cc13..afcb8b7 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Contributors * `Serge Schneider`_ * `Dave Jones`_ * `Tyler Laws`_ +* `George Boukeas`_ Open Source =========== diff --git a/docs/api.md b/docs/api.md index a75135e..ef6a9e0 100644 --- a/docs/api.md +++ b/docs/api.md @@ -819,7 +819,7 @@ Note that, in the current implementation, the four values accessed through the ` 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 a `ValueError` exception being raised. +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 @@ -846,7 +846,7 @@ When there is very little ambient light and the RGBC values are low, it makes se 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 `ValueError` or `TypeError` exception being raised. +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 @@ -895,4 +895,10 @@ 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}") -``` \ No newline at end of file +``` + +## 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/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/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/colour.py b/sense_hat/colour.py index 641aea4..610905a 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -4,8 +4,8 @@ """ from time import sleep - -_error_str = "Failed to initialise TCS34725 colour sensor." +from .exceptions import ColourSensorInitialisationError, InvalidGainError, \ + InvalidIntegrationCyclesError class HardwareInterface: @@ -84,7 +84,7 @@ def get_raw(self): def get_red(self): """ - Return a the raw value of the R (red) channel. + 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`. """ @@ -92,7 +92,7 @@ def get_red(self): def get_green(self): """ - Return a the raw value of the G (green) channel. + 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`. """ @@ -100,7 +100,7 @@ def get_green(self): def get_blue(self): """ - Return a the raw value of the B (blue) channel. + 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`. """ @@ -108,7 +108,7 @@ def get_blue(self): def get_clear(self): """ - Return a the raw value of the C (clear light) channel. + 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`. """ @@ -176,19 +176,16 @@ def __init__(self): try: self.bus = smbus.SMBus(self.BUS) except Exception as e: - explanation = " (I2C is not enabled)" if not self.i2c_enabled() else "" - raise RuntimeError(f'{_error_str}{explanation}') from 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 RuntimeError(f'{_error_str}{explanation}') from e - + explanation = "(sensor not present)" + raise ColourSensorInitialisationError(explanation=explanation) from e if id != 0x44: explanation = f" (different device id detected: {id})" - raise RuntimeError(f'{_error_str}{explanation}') - + raise ColourSensorInitialisationError(explanation=explanation) from e @staticmethod def i2c_enabled(): """Returns True if I2C is enabled or False otherwise.""" @@ -314,7 +311,7 @@ def gain(self, gain): if gain in self.interface.GAIN_VALUES: self.interface.set_gain(gain) else: - raise ValueError(f'Cannot set gain to {gain}. Values: {self.interface.GAIN_VALUES}') + raise InvalidGainError(gain=gain, values=self.interface.GAIN_VALUES) @property def integration_cycles(self): @@ -326,7 +323,7 @@ def integration_cycles(self, integration_cycles): self.interface.set_integration_cycles(integration_cycles) sleep(self.interface.CLOCK_STEP) else: - raise ValueError(f'Cannot set integration cycles to {integration_cycles} (1-256)') + raise InvalidIntegrationCyclesError(integration_cycles=integration_cycles) @property def integration_time(self): 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 e60a2d4..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 @@ -16,6 +17,7 @@ from .stick import SenseStick from .colour import ColourSensor +from .exceptions import ColourSensorInitialisationError class SenseHat(object): @@ -94,7 +96,8 @@ def __init__( # initialise the TCS34725 colour sensor (if possible) try: self._colour = ColourSensor() - except: + except Exception as e: + logging.warning(e) pass #### @@ -206,7 +209,9 @@ def colour(self): try: return self._colour except AttributeError as e: - raise RuntimeError('This Sense HAT does not have a color sensor') from e + raise ColourSensorInitialisationError( + explanation="This Sense HAT" + + " does not have a color sensor") from e color = colour @@ -531,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): From 34076414240bada4505d15036fe19dfb0a3ff042 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Fri, 17 Jun 2022 10:40:46 +0100 Subject: [PATCH 20/37] Add long_description_content_type Set it to markdown so that the README passes twine check --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0ad5b9c..ce74ca0 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ def read(fname): author_email="dave@raspberrypi.org", 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", From c89405740d9fd4a3c7e8aaef426df81532a51582 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Fri, 17 Jun 2022 10:55:34 +0100 Subject: [PATCH 21/37] Reformat README --- README.md | 76 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index afcb8b7..8a46b62 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -========= -Sense HAT -========= +#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. +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 ======== @@ -21,12 +19,12 @@ Buy Buy the Sense HAT from: -* `The Pi Hut`_ -* `Pimoroni`_ -* `Amazon (UK)`_ -* `element14`_ -* `adafruit`_ -* `Amazon (USA)`_ +* `The Pi Hut` +* `Pimoroni` +* `Amazon (UK)` +* `element14` +* `adafruit` +* `Amazon (USA)` Installation @@ -55,34 +53,38 @@ Comprehensive documentation is available at `pythonhosted.org/sense-hat`_ Contributors ============ -* `Dave Honess`_ -* `Ben Nuttall`_ -* `Serge Schneider`_ -* `Dave Jones`_ -* `Tyler Laws`_ -* `George Boukeas`_ +* `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 - -.. _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 +* 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/ +* 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 +* George Boukeas: https://github.com/boukeas +* 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 From 58b0fbba603f4d69ce78c870cf6f5ef561632330 Mon Sep 17 00:00:00 2001 From: Serge Schneider Date: Fri, 17 Jun 2022 15:08:40 +0100 Subject: [PATCH 22/37] Keep debian packaging in a separate branch --- debian/changelog | 36 ------------------------------------ debian/compat | 1 - debian/control | 32 -------------------------------- debian/copyright | 32 -------------------------------- debian/rules | 13 ------------- debian/source/format | 1 - 6 files changed, 115 deletions(-) delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100755 debian/rules delete mode 100644 debian/source/format diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 8d6d2af..0000000 --- a/debian/changelog +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index ec63514..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control deleted file mode 100644 index 33e0070..0000000 --- a/debian/control +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 09392d9..0000000 --- a/debian/copyright +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100755 index 016681d..0000000 --- a/debian/rules +++ /dev/null @@ -1,13 +0,0 @@ -#!/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 deleted file mode 100644 index 89ae9db..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) From caa41a2f79c52cb8bc4d34e54fa6477e1ebf4ec9 Mon Sep 17 00:00:00 2001 From: snake48 Date: Sun, 7 Aug 2022 14:28:23 +0100 Subject: [PATCH 23/37] Update colour.py --- sense_hat/colour.py | 52 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 610905a..6a9751e 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -1,3 +1,35 @@ +Skip to content +Search or jump to… +Pull requests +Issues +Marketplace +Explore + +@snake48 +astro-pi +/ +python-sense-hat +Public +Code +Issues +25 +Pull requests +9 +Actions +Projects +Wiki +Security +Insights +python-sense-hat/sense_hat/colour.py / +@G3zz +G3zz Small improvements to lib +… +Latest commit 9a67533 on 1 Jun + History + 2 contributors +@boukeas@G3zz +358 lines (301 sloc) 11.6 KB + """ Python library for the TCS34725 Color Sensor Documentation (including datasheet): https://ams.com/tcs34725#tab/documents @@ -15,7 +47,6 @@ class HardwareInterface: 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. @@ -131,7 +162,6 @@ 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 """ @@ -351,8 +381,26 @@ def _scaling(self): 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 ) +Footer +© 2022 GitHub, Inc. +Footer navigation +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About From 7a92bbe900ce4731f4f5cebf461973e33708f31b Mon Sep 17 00:00:00 2001 From: snake48 Date: Sun, 7 Aug 2022 16:45:06 +0100 Subject: [PATCH 24/37] Update colour.py --- sense_hat/colour.py | 46 --------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 6a9751e..5be1ca9 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -1,35 +1,3 @@ -Skip to content -Search or jump to… -Pull requests -Issues -Marketplace -Explore - -@snake48 -astro-pi -/ -python-sense-hat -Public -Code -Issues -25 -Pull requests -9 -Actions -Projects -Wiki -Security -Insights -python-sense-hat/sense_hat/colour.py / -@G3zz -G3zz Small improvements to lib -… -Latest commit 9a67533 on 1 Jun - History - 2 contributors -@boukeas@G3zz -358 lines (301 sloc) 11.6 KB - """ Python library for the TCS34725 Color Sensor Documentation (including datasheet): https://ams.com/tcs34725#tab/documents @@ -390,17 +358,3 @@ def rgb(self): 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 ) -Footer -© 2022 GitHub, Inc. -Footer navigation -Terms -Privacy -Security -Status -Docs -Contact GitHub -Pricing -API -Training -Blog -About From dd335ffa21d7a2bcdf8b35a79c56d5de31590938 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Tue, 9 Aug 2022 12:27:29 +0100 Subject: [PATCH 25/37] Misc changes * Update docs * Bump minor version number * Add brightness alias --- docs/api.md | 4 +++- docs/changelog.md | 4 ++++ sense_hat/__init__.py | 2 +- sense_hat/colour.py | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index ef6a9e0..c2d2ea1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -881,6 +881,8 @@ The following properties of the `ColourSensor` object provide direct access to t `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. @@ -901,4 +903,4 @@ print(f"Scaled values: {sense.colour.colour}") 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 +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 8813ae7..6f2d06c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ ## 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 diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index 2143620..ec44f1c 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -13,4 +13,4 @@ ACTION_HELD, ) -__version__ = '2.3.1' +__version__ = '2.4.0' diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 5be1ca9..7b5f2f4 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -340,6 +340,7 @@ def colour_raw(self): 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): From c5b81c6a34163deb572fd76102a615b7cdc107bb Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Tue, 9 Aug 2022 15:47:55 +0100 Subject: [PATCH 26/37] Bump minor version in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ce74ca0..9f59aba 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(fname): setup( name="sense-hat", - version="2.3.1", + version="2.4.0", author="Dave Honess", author_email="dave@raspberrypi.org", description="Python module to control the Raspberry Pi Sense HAT, originally used in the Astro Pi mission", From f3dd9ac2b82e4c6915e32877cd7e035b899524f7 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Mon, 21 Nov 2022 12:42:42 +0000 Subject: [PATCH 27/37] Setup readthedocs --- .readthedocs.yaml | 14 ++++++++++++++ docs/requirements.txt | 1 + mkdocs.yml | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt 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/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 22eb7e0..8f94c64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,7 +3,7 @@ theme: readthedocs site_url: https://pythonhosted.org/sense-hat/ 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_author: Raspberry Pi Foundation site_dir: pythonhosted #google_analytics: ['UA-46270871-5', 'pythonhosted.org/sense-hat'] nav: From 1923122162a10928100d4d500509b7cd3575973a Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Mon, 21 Nov 2022 15:24:10 +0000 Subject: [PATCH 28/37] Change logging level of warning This was too confusing for users, so downgrade the level to debug. --- sense_hat/sense_hat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index b800ee4..87bd6cf 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -97,7 +97,7 @@ def __init__( try: self._colour = ColourSensor() except Exception as e: - logging.warning(e) + logging.debug(e) pass #### From ad91951473d33910d8de020bf15df3a0e0766ee9 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Mon, 21 Nov 2022 15:59:05 +0000 Subject: [PATCH 29/37] Update documentation URL --- README.md | 4 ++-- mkdocs.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8a46b62..769e64a 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Import the sense_hat module and instantiate a SenseHat object:: Documentation ============= -Comprehensive documentation is available at `pythonhosted.org/sense-hat`_ +Comprehensive documentation is available at `https://sense-hat.readthedocs.io/en/latest/`. Contributors ============ @@ -72,7 +72,7 @@ URLs * Raspberry Pi: https://www.raspberrypi.org/ * Astro Pi: http://www.astro-pi.org/ -* pythonhosted.org/sense-hat: http://pythonhosted.org/sense-hat/ +* 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 diff --git a/mkdocs.yml b/mkdocs.yml index 8f94c64..10bea90 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,10 @@ site_name: Sense HAT theme: readthedocs -site_url: https://pythonhosted.org/sense-hat/ +site_url: https://sense-hat.readthedocs.io/en/latest/ 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: Raspberry Pi Foundation -site_dir: pythonhosted +site_dir: readthedocs #google_analytics: ['UA-46270871-5', 'pythonhosted.org/sense-hat'] nav: - 'Home': 'index.md' From d86a1cdc01c6bb1f390eeb8e6013864b9059215f Mon Sep 17 00:00:00 2001 From: Ton van Overbeek Date: Sat, 26 Nov 2022 22:25:07 +0100 Subject: [PATCH 30/37] TCS34725 and TCS3400 support Detects the used sensor. so the same code supports both. TCS3400 is used for the Sense Hat V2 while the (discontinued) TCS34725 is used on the AstroPi sense hat for the ISS. ALso a bug fix for the computation of the red_raw, green_raw, blue_raw and clear_raw values. The original code ony fetched the LSB. --- sense_hat/colour.py | 102 ++++++++++++++++++++++++++-------------- sense_hat/exceptions.py | 2 +- sense_hat/sense_hat.py | 6 +-- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/sense_hat/colour.py b/sense_hat/colour.py index 7b5f2f4..430b31f 100644 --- a/sense_hat/colour.py +++ b/sense_hat/colour.py @@ -1,6 +1,10 @@ """ -Python library for the TCS34725 Color Sensor +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 @@ -11,25 +15,22 @@ 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 + `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. """ - 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 + return 65535 if integration_cycles >= 64 else 1024*integration_cycles def get_enabled(self): """ @@ -123,36 +124,34 @@ def _raw_wrapper(register): fashion. This is a factory function that implements this retrieval method. """ def get_raw_register(self): - return self._read(register) + 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 sensor - that uses I2C to control the sensor and retrieve measurements. - Use the datasheet as a reference: https://ams.com/tcs34725#tab/documents + 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 - 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 + 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 = 0x14 | COMMAND_BIT - RDATA = 0x16 | COMMAND_BIT - GDATA = 0x18 | COMMAND_BIT - BDATA = 0x1A | COMMAND_BIT + CDATA = 0x94 + RDATA = 0x96 + GDATA = 0x98 + BDATA = 0x9A # bit positions OFF = 0x00 @@ -162,9 +161,13 @@ class I2C(HardwareInterface): 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)) + # 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): @@ -176,14 +179,43 @@ def __init__(self): 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) - 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 + 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.""" @@ -192,14 +224,14 @@ def i2c_enabled(): def _read(self, attribute): """ Read and return the value of a specific register (`attribute`) of the - TCS34725 colour sensor. + 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 colour sensor. + TCS34725/TCS3400 colour sensor. """ self.bus.write_byte_data(self.ADDR, attribute, value) diff --git a/sense_hat/exceptions.py b/sense_hat/exceptions.py index d6f3d18..4c69290 100644 --- a/sense_hat/exceptions.py +++ b/sense_hat/exceptions.py @@ -11,7 +11,7 @@ def __init__(self, **kwargs): class ColourSensorInitialisationError(SenseHatException): - fmt = "Failed to initialise TCS34725 colour sensor. {explanation}" + fmt = "Failed to initialise colour sensor. {explanation}" class InvalidGainError(SenseHatException): diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 87bd6cf..a639dc6 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -208,10 +208,8 @@ def stick(self): 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 + except AttributeError: + print('This Sense Hat does not have a colour sensor') color = colour From f5ecdae3f2e623cf819a8089cf938662b8b6a3bd Mon Sep 17 00:00:00 2001 From: Andrew Scheller Date: Wed, 25 Jan 2023 14:11:17 +0000 Subject: [PATCH 31/37] Update product URL --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 6c16084..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 From 4ee53c8880801d8e07a23ea3edabf92cf9a25cab Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Wed, 11 Oct 2023 12:12:02 +0100 Subject: [PATCH 32/37] Bump version numbers --- sense_hat/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index ec44f1c..3c5e8b2 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -13,4 +13,4 @@ ACTION_HELD, ) -__version__ = '2.4.0' +__version__ = '2.6.0' diff --git a/setup.py b/setup.py index 9f59aba..6f9898c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(fname): setup( name="sense-hat", - version="2.4.0", + version="2.6.0", author="Dave Honess", author_email="dave@raspberrypi.org", description="Python module to control the Raspberry Pi Sense HAT, originally used in the Astro Pi mission", From d2942773a5c740a4cf23211d6bc9a3a695439e25 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Wed, 11 Oct 2023 12:30:00 +0100 Subject: [PATCH 33/37] Update repo url --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- mkdocs.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 index 769e64a..13aaa0d 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ URLs * 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/RPi-Distro/python-sense-hat -* GitHub Issues: https://github.com/RPi-Distro/python-sense-hat/issues +* 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/ diff --git a/mkdocs.yml b/mkdocs.yml index 10bea90..1739d93 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Sense HAT theme: readthedocs site_url: https://sense-hat.readthedocs.io/en/latest/ -repo_url: https://github.com/RPi-Distro/python-sense-hat +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: Raspberry Pi Foundation site_dir: readthedocs From b035b96b5b6fddab31aa048c34061d19e73282c2 Mon Sep 17 00:00:00 2001 From: Greg Annandale Date: Tue, 6 Jan 2026 16:50:30 +0000 Subject: [PATCH 34/37] Fix formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a46b62..2ffeacb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Sense HAT +# 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. From 61a0ee91d536cfe2d800358de319ecf4ddccd071 Mon Sep 17 00:00:00 2001 From: Patrick Cherry Date: Fri, 27 Feb 2026 08:50:03 +0000 Subject: [PATCH 35/37] Ensure pixels have __len__ before checking with len() In Astro Pi we were getting TypeErrors when checking len() on non-iterable variables, e.g. when a user calls set_pixel() with just three arguments. This PR ensures that the pixel has a __len__ attribute before testing its length. Also fixes checking the pixel list in set_pixels. --- sense_hat/sense_hat.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sense_hat/sense_hat.py b/sense_hat/sense_hat.py index 87bd6cf..cccc419 100644 --- a/sense_hat/sense_hat.py +++ b/sense_hat/sense_hat.py @@ -308,11 +308,11 @@ def set_pixels(self, pixel_list): 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: @@ -353,16 +353,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') From a06eea096e9cf0ca3dbae09896f33973129e2890 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Mon, 2 Mar 2026 11:02:18 +0000 Subject: [PATCH 36/37] chore: bump patch version --- sense_hat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sense_hat/__init__.py b/sense_hat/__init__.py index 3c5e8b2..d8aa831 100644 --- a/sense_hat/__init__.py +++ b/sense_hat/__init__.py @@ -13,4 +13,4 @@ ACTION_HELD, ) -__version__ = '2.6.0' +__version__ = '2.6.1' From a4dfcf51d54832b102302a39071ee1de4329eaa9 Mon Sep 17 00:00:00 2001 From: Geraint Ballinger Date: Mon, 23 Mar 2026 16:44:05 +0000 Subject: [PATCH 37/37] chore: bump patch version in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6f9898c..be4c3db 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ def read(fname): setup( name="sense-hat", - version="2.6.0", + version="2.6.1", author="Dave Honess", author_email="dave@raspberrypi.org", description="Python module to control the Raspberry Pi Sense HAT, originally used in the Astro Pi mission",