├── .gitignore ├── .readthedocs.yaml ├── CONTRIBUTING.md ├── LICENCE.txt ├── MANIFEST.in ├── README.md ├── docs ├── api.md ├── changelog.md ├── examples │ ├── README.md │ ├── colour_cycle.py │ ├── compass.py │ ├── evdev_joystick.py │ ├── pygame_joystick.py │ ├── rainbow.py │ ├── rotation.py │ ├── space_invader.png │ ├── space_invader.py │ └── text_scroll.py ├── index.md └── requirements.txt ├── mkdocs.yml ├── sense_hat ├── __init__.py ├── colour.py ├── exceptions.py ├── sense_hat.py ├── sense_hat_text.png ├── sense_hat_text.txt └── stick.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | build/ 4 | dist/ 5 | pythonhosted/ 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.9" 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | fail_on_warning: false 11 | 12 | python: 13 | install: 14 | - requirements: docs/requirements.txt 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | 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. 6 | 7 | ## Pull Requests 8 | 9 | Please create a new pull request for each change recommendation. 10 | 11 | ## Policy 12 | 13 | - Python 2/3 compatability 14 | - PEP8-compliance (with exceptions) 15 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015- Raspberry Pi Foundation 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | * Neither the name of the copyright holder nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CONTRIBUTING.md 3 | include LICENCE.txt 4 | include sense_hat/sense_hat_text.png 5 | include sense_hat/sense_hat_text.txt 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Sense HAT 2 | 3 | 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. 4 | 5 | Hardware 6 | ======== 7 | 8 | The Sense HAT features an 8x8 RGB LED matrix, a mini joystick and the following sensors: 9 | 10 | * Gyroscope 11 | * Accelerometer 12 | * Magnetometer 13 | * Temperature 14 | * Humidity 15 | * Barometric pressure 16 | 17 | Buy 18 | === 19 | 20 | Buy the Sense HAT from: 21 | 22 | * `The Pi Hut` 23 | * `Pimoroni` 24 | * `Amazon (UK)` 25 | * `element14` 26 | * `adafruit` 27 | * `Amazon (USA)` 28 | 29 | 30 | Installation 31 | ============ 32 | 33 | To install the Sense HAT software, enter the following commands in a terminal:: 34 | 35 | sudo apt-get update 36 | sudo apt-get install sense-hat 37 | sudo reboot 38 | 39 | Usage 40 | ===== 41 | 42 | Import the sense_hat module and instantiate a SenseHat object:: 43 | 44 | from sense_hat import SenseHat 45 | 46 | sense = SenseHat() 47 | 48 | Documentation 49 | ============= 50 | 51 | Comprehensive documentation is available at `https://sense-hat.readthedocs.io/en/latest/`. 52 | 53 | Contributors 54 | ============ 55 | 56 | * `Dave Honess` 57 | * `Ben Nuttall` 58 | * `Serge Schneider` 59 | * `Dave Jones` 60 | * `Tyler Laws` 61 | * `George Boukeas` 62 | 63 | Open Source 64 | =========== 65 | 66 | * The code is licensed under the `BSD Licence` 67 | * The project source code is hosted on `GitHub` 68 | * Please use `GitHub issues` to submit bugs and report issues 69 | 70 | URLs 71 | ===== 72 | 73 | * Raspberry Pi: https://www.raspberrypi.org/ 74 | * Astro Pi: http://www.astro-pi.org/ 75 | * sense-hat.readthedocs.io: https://sense-hat.readthedocs.io/en/latest/ 76 | * Dave Honess: https://github.com/davidhoness 77 | * Ben Nuttall: https://github.com/bennuttall 78 | * Serge Schneider: https://github.com/XECDesign 79 | * Dave Jones: https://github.com/waveform80 80 | * Tyler Laws: https://github.com/tyler-laws 81 | * George Boukeas: https://github.com/boukeas 82 | * BSD Licence: http://opensource.org/licenses/BSD-3-Clause 83 | * GitHub: https://github.com/astro-pi/python-sense-hat 84 | * GitHub Issues: https://github.com/astro-pi/python-sense-hat/issues 85 | * `The Pi Hut`: http://thepihut.com/products/raspberry-pi-sense-hat-astro-pi 86 | * `Pimoroni`: https://shop.pimoroni.com/products/raspberry-pi-sense-hat 87 | * `Amazon (UK)`: http://www.amazon.co.uk/Raspberry-Pi-2483095-Sense-HAT/dp/B014T2IHQ8/ 88 | * element14: https://www.element14.com/community/docs/DOC-78155/l/raspberry-pi-sense-hat 89 | * adafruit: https://www.adafruit.com/products/2738 90 | * Amazon (USA): http://www.amazon.com/Raspberry-Pi-Sense-HAT-AstroPi/dp/B014HDG74S 91 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Sense HAT API Reference 2 | 3 | ## LED Matrix 4 | 5 | ### set_rotation 6 | 7 | If you're using the Pi upside down or sideways you can use this function to correct the orientation of the image being shown. 8 | 9 | Parameter | Type | Valid values | Explanation 10 | --- | --- | --- | --- 11 | `r` | Integer | `0` `90` `180` `270` | The angle to rotate the LED matrix though. `0` is with the Raspberry Pi HDMI port facing downwards. 12 | `redraw` | Boolean | `True` `False` | Whether or not to redraw what is already being displayed on the LED matrix. Defaults to `True` 13 | 14 | Returned type | Explanation 15 | --- | --- 16 | None | 17 | 18 | ```python 19 | from sense_hat import SenseHat 20 | 21 | sense = SenseHat() 22 | sense.set_rotation(180) 23 | # alternatives 24 | sense.rotation = 180 25 | ``` 26 | 27 | - - - 28 | ### flip_h 29 | 30 | Flips the image on the LED matrix horizontally. 31 | 32 | Parameter | Type | Valid values | Explanation 33 | --- | --- | --- | --- 34 | `redraw` | Boolean | `True` `False` | Whether or not to redraw what is already being displayed on the LED matrix. Defaults to `True` 35 | 36 | Returned type | Explanation 37 | --- | --- 38 | List | A list containing 64 smaller lists of `[R, G, B]` pixels (red, green, blue) representing the flipped image. 39 | 40 | ```python 41 | from sense_hat import SenseHat 42 | 43 | sense = SenseHat() 44 | sense.flip_h() 45 | ``` 46 | 47 | - - - 48 | ### flip_v 49 | 50 | Flips the image on the LED matrix vertically. 51 | 52 | Parameter | Type | Valid values | Explanation 53 | --- | --- | --- | --- 54 | `redraw` | Boolean | `True` `False` | Whether or not to redraw what is already being displayed on the LED matrix when flipped. Defaults to `True` 55 | 56 | Returned type | Explanation 57 | --- | --- 58 | List | A list containing 64 smaller lists of `[R, G, B]` pixels (red, green, blue) representing the flipped image. 59 | 60 | ```python 61 | from sense_hat import SenseHat 62 | 63 | sense = SenseHat() 64 | sense.flip_v() 65 | ``` 66 | 67 | - - - 68 | ### set_pixels 69 | 70 | Updates the entire LED matrix based on a 64 length list of pixel values. 71 | 72 | Parameter | Type | Valid values | Explanation 73 | --- | --- | --- | --- 74 | `pixel_list` | List | `[[R, G, B] * 64]` | A list containing 64 smaller lists of `[R, G, B]` pixels (red, green, blue). Each R-G-B element must be an integer between 0 and 255. 75 | 76 | Returned type | Explanation 77 | --- | --- 78 | None | 79 | 80 | ```python 81 | from sense_hat import SenseHat 82 | 83 | sense = SenseHat() 84 | 85 | X = [255, 0, 0] # Red 86 | O = [255, 255, 255] # White 87 | 88 | question_mark = [ 89 | O, O, O, X, X, O, O, O, 90 | O, O, X, O, O, X, O, O, 91 | O, O, O, O, O, X, O, O, 92 | O, O, O, O, X, O, O, O, 93 | O, O, O, X, O, O, O, O, 94 | O, O, O, X, O, O, O, O, 95 | O, O, O, O, O, O, O, O, 96 | O, O, O, X, O, O, O, O 97 | ] 98 | 99 | sense.set_pixels(question_mark) 100 | ``` 101 | 102 | - - - 103 | ### get_pixels 104 | 105 | Returned type | Explanation 106 | --- | --- 107 | List | A list containing 64 smaller lists of `[R, G, B]` pixels (red, green, blue) representing the currently displayed image. 108 | 109 | ```python 110 | from sense_hat import SenseHat 111 | 112 | sense = SenseHat() 113 | pixel_list = sense.get_pixels() 114 | ``` 115 | 116 | Note: You will notice that the pixel values you pass into `set_pixels` sometimes change when you read them back with `get_pixels`. This is because we specify each pixel element as 8 bit numbers (0 to 255) but when they're passed into the Linux frame buffer for the LED matrix the numbers are bit shifted down to fit into RGB 565. 5 bits for red, 6 bits for green and 5 bits for blue. The loss of binary precision when performing this conversion (3 bits lost for red, 2 for green and 3 for blue) accounts for the discrepancies you see. 117 | 118 | The `get_pixels` function provides a correct representation of how the pixels end up in frame buffer memory after you've called `set_pixels`. 119 | 120 | - - - 121 | ### set_pixel 122 | 123 | Sets an individual LED matrix pixel at the specified X-Y coordinate to the specified colour. 124 | 125 | Parameter | Type | Valid values | Explanation 126 | --- | --- | --- | --- 127 | `x` | Integer | `0 - 7` | 0 is on the left, 7 on the right. 128 | `y` | Integer | `0 - 7` | 0 is at the top, 7 at the bottom. 129 | Colour can either be passed as an RGB tuple: ||| 130 | `pixel` | Tuple or List | `(r, g, b)` | Each element must be an integer between 0 and 255. 131 | Or three separate values for red, green and blue: ||| 132 | `r` | Integer | `0 - 255` | The Red element of the pixel. 133 | `g` | Integer | `0 - 255` | The Green element of the pixel. 134 | `b` | Integer | `0 - 255` | The Blue element of the pixel. 135 | 136 | Returned type | Explanation 137 | --- | --- 138 | None | 139 | 140 | ```python 141 | from sense_hat import SenseHat 142 | 143 | sense = SenseHat() 144 | 145 | # examples using (x, y, r, g, b) 146 | sense.set_pixel(0, 0, 255, 0, 0) 147 | sense.set_pixel(0, 7, 0, 255, 0) 148 | sense.set_pixel(7, 0, 0, 0, 255) 149 | sense.set_pixel(7, 7, 255, 0, 255) 150 | 151 | red = (255, 0, 0) 152 | green = (0, 255, 0) 153 | blue = (0, 0, 255) 154 | 155 | # examples using (x, y, pixel) 156 | sense.set_pixel(0, 0, red) 157 | sense.set_pixel(0, 0, green) 158 | sense.set_pixel(0, 0, blue) 159 | ``` 160 | 161 | - - - 162 | ### get_pixel 163 | 164 | Parameter | Type | Valid values | Explanation 165 | --- | --- | --- | --- 166 | `x` | Integer | `0 - 7` | 0 is on the left, 7 on the right. 167 | `y` | Integer | `0 - 7` | 0 is at the top, 7 at the bottom. 168 | 169 | Returned type | Explanation 170 | --- | --- 171 | List | Returns a list of `[R, G, B]` representing the colour of an individual LED matrix pixel at the specified X-Y coordinate. 172 | 173 | ```python 174 | from sense_hat import SenseHat 175 | 176 | sense = SenseHat() 177 | top_left_pixel = sense.get_pixel(0, 0) 178 | ``` 179 | 180 | Note: Please read the note under `get_pixels` 181 | 182 | - - - 183 | ### load_image 184 | 185 | Loads an image file, converts it to RGB format and displays it on the LED matrix. The image must be 8 x 8 pixels in size. 186 | 187 | Parameter | Type | Valid values | Explanation 188 | --- | --- | --- | --- 189 | `file_path` | String | Any valid file path. | The file system path to the image file to load. 190 | `redraw` | Boolean | `True` `False` | Whether or not to redraw the loaded image file on the LED matrix. Defaults to `True` 191 | 192 | ```python 193 | from sense_hat import SenseHat 194 | 195 | sense = SenseHat() 196 | sense.load_image("space_invader.png") 197 | ``` 198 | 199 | Returned type | Explanation 200 | --- | --- 201 | List | A list containing 64 smaller lists of `[R, G, B]` pixels (red, green, blue) representing the loaded image after RGB conversion. 202 | 203 | ```python 204 | from sense_hat import SenseHat 205 | 206 | sense = SenseHat() 207 | invader_pixels = sense.load_image("space_invader.png", redraw=False) 208 | ``` 209 | 210 | - - - 211 | ### clear 212 | 213 | Sets the entire LED matrix to a single colour, defaults to blank / off. 214 | 215 | Parameter | Type | Valid values | Explanation 216 | --- | --- | --- | --- 217 | `colour` | Tuple or List | `(r, g, b)` | A tuple or list containing the RGB (red, green, blue) values of the colour. Each element must be an integer between 0 and 255. Defaults to `(0, 0, 0)`. 218 | Alternatively, the RGB values can be passed individually:||| 219 | `r` | Integer | `0 - 255` | The Red element of the colour. 220 | `g` | Integer | `0 - 255` | The Green element of the colour. 221 | `b` | Integer | `0 - 255` | The Blue element of the colour. 222 | 223 | ```python 224 | from sense_hat import SenseHat 225 | from time import sleep 226 | 227 | sense = SenseHat() 228 | 229 | red = (255, 0, 0) 230 | 231 | sense.clear() # no arguments defaults to off 232 | sleep(1) 233 | sense.clear(red) # passing in an RGB tuple 234 | sleep(1) 235 | sense.clear(255, 255, 255) # passing in r, g and b values of a colour 236 | ``` 237 | 238 | - - - 239 | ### show_message 240 | 241 | Scrolls a text message from right to left across the LED matrix and at the specified speed, in the specified colour and background colour. 242 | 243 | Parameter | Type | Valid values | Explanation 244 | --- | --- | --- | --- 245 | `text_string` | String | Any text string. | The message to scroll. 246 | `scroll_speed` | Float | Any floating point number. | The speed at which the text should scroll. This value represents the time paused for between shifting the text to the left by one column of pixels. Defaults to `0.1` 247 | `text_colour` | List | `[R, G, B]` | A list containing the R-G-B (red, green, blue) colour of the text. Each R-G-B element must be an integer between 0 and 255. Defaults to `[255, 255, 255]` white. 248 | `back_colour` | List | `[R, G, B]` | A list containing the R-G-B (red, green, blue) colour of the background. Each R-G-B element must be an integer between 0 and 255. Defaults to `[0, 0, 0]` black / off. 249 | 250 | Returned type | Explanation 251 | --- | --- 252 | None | 253 | 254 | ```python 255 | from sense_hat import SenseHat 256 | 257 | sense = SenseHat() 258 | sense.show_message("One small step for Pi!", text_colour=[255, 0, 0]) 259 | ``` 260 | 261 | - - - 262 | ### show_letter 263 | 264 | Displays a single text character on the LED matrix. 265 | 266 | Parameter | Type | Valid values | Explanation 267 | --- | --- | --- | --- 268 | `s` | String | A text string of length 1. | The letter to show. 269 | `text_colour` | List | `[R, G, B]` | A list containing the R-G-B (red, green, blue) colour of the letter. Each R-G-B element must be an integer between 0 and 255. Defaults to `[255, 255, 255]` white. 270 | `back_colour` | List | `[R, G, B]` | A list containing the R-G-B (red, green, blue) colour of the background. Each R-G-B element must be an integer between 0 and 255. Defaults to `[0, 0, 0]` black / off. 271 | 272 | Returned type | Explanation 273 | --- | --- 274 | None | 275 | 276 | ```python 277 | import time 278 | from sense_hat import SenseHat 279 | 280 | sense = SenseHat() 281 | 282 | for i in reversed(range(0,10)): 283 | sense.show_letter(str(i)) 284 | time.sleep(1) 285 | ``` 286 | 287 | ### low_light 288 | 289 | Toggles the LED matrix low light mode, useful if the Sense HAT is being used in a dark environment. 290 | 291 | ```python 292 | import time 293 | from sense_hat import SenseHat 294 | 295 | sense = SenseHat() 296 | sense.clear(255, 255, 255) 297 | sense.low_light = True 298 | time.sleep(2) 299 | sense.low_light = False 300 | ``` 301 | 302 | ### gamma 303 | 304 | For advanced users. Most users will just need the `low_light` Boolean property above. The Sense HAT python API uses 8 bit (0 to 255) colours for R, G, B. When these are written to the Linux frame buffer they're bit shifted into RGB 5 6 5. The driver then converts them to RGB 5 5 5 before it passes them over to the ATTiny88 AVR for writing to the LEDs. 305 | 306 | The gamma property allows you to specify a gamma lookup table for the [final 5](http://en.battlestarwiki.org/wiki/Final_Five) bits of colour used. The lookup table is a list of 32 numbers that must be between 0 and 31. The value of the incoming 5 bit colour is used to index the lookup table and the value found at that position is then written to the LEDs. 307 | 308 | Type | Valid values | Explanation 309 | --- | --- | --- 310 | Tuple or List | Tuple or List of length 32 containing Integers between 0 and 31 | Gamma lookup table for the final 5 bits of colour 311 | 312 | ```python 313 | import time 314 | from sense_hat import SenseHat 315 | 316 | sense = SenseHat() 317 | sense.clear(255, 127, 0) 318 | 319 | print(sense.gamma) 320 | time.sleep(2) 321 | 322 | sense.gamma = list(reversed(sense.gamma)) 323 | print(sense.gamma) 324 | time.sleep(2) 325 | 326 | sense.low_light = True 327 | print(sense.gamma) 328 | time.sleep(2) 329 | 330 | sense.low_light = False 331 | ``` 332 | 333 | ### gamma_reset 334 | 335 | A function to reset the gamma lookup table to default, ideal if you've been messing with it and want to get it back to a default state. 336 | 337 | Returned type | Explanation 338 | --- | --- 339 | None | 340 | 341 | ```python 342 | import time 343 | from sense_hat import SenseHat 344 | 345 | sense = SenseHat() 346 | sense.clear(255, 127, 0) 347 | time.sleep(2) 348 | sense.gamma = [0] * 32 # Will turn the LED matrix off 349 | time.sleep(2) 350 | sense.gamma_reset() 351 | ``` 352 | 353 | - - - 354 | ## Environmental sensors 355 | 356 | ### get_humidity 357 | 358 | Gets the percentage of relative humidity from the humidity sensor. 359 | 360 | Returned type | Explanation 361 | --- | --- 362 | Float | The percentage of relative humidity. 363 | 364 | ```python 365 | from sense_hat import SenseHat 366 | 367 | sense = SenseHat() 368 | humidity = sense.get_humidity() 369 | print("Humidity: %s %%rH" % humidity) 370 | 371 | # alternatives 372 | print(sense.humidity) 373 | ``` 374 | 375 | - - - 376 | ### get_temperature 377 | 378 | Calls `get_temperature_from_humidity` below. 379 | 380 | ```python 381 | from sense_hat import SenseHat 382 | 383 | sense = SenseHat() 384 | temp = sense.get_temperature() 385 | print("Temperature: %s C" % temp) 386 | 387 | # alternatives 388 | print(sense.temp) 389 | print(sense.temperature) 390 | ``` 391 | 392 | - - - 393 | ### get_temperature_from_humidity 394 | 395 | Gets the current temperature in degrees Celsius from the humidity sensor. 396 | 397 | Returned type | Explanation 398 | --- | --- 399 | Float | The current temperature in degrees Celsius. 400 | 401 | ```python 402 | from sense_hat import SenseHat 403 | 404 | sense = SenseHat() 405 | temp = sense.get_temperature_from_humidity() 406 | print("Temperature: %s C" % temp) 407 | ``` 408 | 409 | - - - 410 | ### get_temperature_from_pressure 411 | 412 | Gets the current temperature in degrees Celsius from the pressure sensor. 413 | 414 | Returned type | Explanation 415 | --- | --- 416 | Float | The current temperature in degrees Celsius. 417 | 418 | ```python 419 | from sense_hat import SenseHat 420 | 421 | sense = SenseHat() 422 | temp = sense.get_temperature_from_pressure() 423 | print("Temperature: %s C" % temp) 424 | ``` 425 | 426 | - - - 427 | ### get_pressure 428 | 429 | Gets the current pressure in Millibars from the pressure sensor. 430 | 431 | Returned type | Explanation 432 | --- | --- 433 | Float | The current pressure in Millibars. 434 | 435 | ```python 436 | from sense_hat import SenseHat 437 | 438 | sense = SenseHat() 439 | pressure = sense.get_pressure() 440 | print("Pressure: %s Millibars" % pressure) 441 | 442 | # alternatives 443 | print(sense.pressure) 444 | ``` 445 | 446 | - - - 447 | ## IMU Sensor 448 | 449 | The IMU (inertial measurement unit) sensor is a combination of three sensors, each with an x, y and z axis. For this reason it's considered to be a 9 dof (degrees of freedom) sensor. 450 | 451 | - Gyroscope 452 | - Accelerometer 453 | - Magnetometer (compass) 454 | 455 | This API allows you to use these sensors in any combination to measure orientation or as individual sensors in their own right. 456 | 457 | ### set_imu_config 458 | 459 | Enables and disables the gyroscope, accelerometer and/or magnetometer contribution to the get orientation functions below. 460 | 461 | Parameter | Type | Valid values | Explanation 462 | --- | --- | --- | --- 463 | `compass_enabled` | Boolean | `True` `False` | Whether or not the compass should be enabled. 464 | `gyro_enabled` | Boolean | `True` `False` | Whether or not the gyroscope should be enabled. 465 | `accel_enabled` | Boolean | `True` `False` | Whether or not the accelerometer should be enabled. 466 | 467 | Returned type | Explanation 468 | --- | --- 469 | None | 470 | 471 | ```python 472 | from sense_hat import SenseHat 473 | 474 | sense = SenseHat() 475 | sense.set_imu_config(False, True, False) # gyroscope only 476 | ``` 477 | 478 | - - - 479 | ### get_orientation_radians 480 | 481 | Gets the current orientation in radians using the aircraft principal axes of pitch, roll and yaw. 482 | 483 | Returned type | Explanation 484 | --- | --- 485 | Dictionary | A dictionary object indexed by the strings `pitch`, `roll` and `yaw`. The values are Floats representing the angle of the axis in radians. 486 | 487 | ```python 488 | from sense_hat import SenseHat 489 | 490 | sense = SenseHat() 491 | orientation_rad = sense.get_orientation_radians() 492 | print("p: {pitch}, r: {roll}, y: {yaw}".format(**orientation_rad)) 493 | 494 | # alternatives 495 | print(sense.orientation_radians) 496 | ``` 497 | 498 | - - - 499 | ### get_orientation_degrees 500 | 501 | Gets the current orientation in degrees using the aircraft principal axes of pitch, roll and yaw. 502 | 503 | Returned type | Explanation 504 | --- | --- 505 | Dictionary | A dictionary object indexed by the strings `pitch`, `roll` and `yaw`. The values are Floats representing the angle of the axis in degrees. 506 | 507 | ```python 508 | from sense_hat import SenseHat 509 | 510 | sense = SenseHat() 511 | orientation = sense.get_orientation_degrees() 512 | print("p: {pitch}, r: {roll}, y: {yaw}".format(**orientation)) 513 | ``` 514 | 515 | - - - 516 | ### get_orientation 517 | 518 | Calls `get_orientation_degrees` above. 519 | 520 | ```python 521 | from sense_hat import SenseHat 522 | 523 | sense = SenseHat() 524 | orientation = sense.get_orientation() 525 | print("p: {pitch}, r: {roll}, y: {yaw}".format(**orientation)) 526 | 527 | # alternatives 528 | print(sense.orientation) 529 | ``` 530 | 531 | - - - 532 | ### get_compass 533 | 534 | Calls `set_imu_config` to disable the gyroscope and accelerometer then gets the direction of North from the magnetometer in degrees. 535 | 536 | Returned type | Explanation 537 | --- | --- 538 | Float | The direction of North. 539 | 540 | ```python 541 | from sense_hat import SenseHat 542 | 543 | sense = SenseHat() 544 | north = sense.get_compass() 545 | print("North: %s" % north) 546 | 547 | # alternatives 548 | print(sense.compass) 549 | ``` 550 | 551 | - - - 552 | ### get_compass_raw 553 | 554 | Gets the raw x, y and z axis magnetometer data. 555 | 556 | Returned type | Explanation 557 | --- | --- 558 | Dictionary | A dictionary object indexed by the strings `x`, `y` and `z`. The values are Floats representing the magnetic intensity of the axis in **microteslas** (µT). 559 | 560 | ```python 561 | from sense_hat import SenseHat 562 | 563 | sense = SenseHat() 564 | raw = sense.get_compass_raw() 565 | print("x: {x}, y: {y}, z: {z}".format(**raw)) 566 | 567 | # alternatives 568 | print(sense.compass_raw) 569 | ``` 570 | 571 | - - - 572 | ### get_gyroscope 573 | 574 | Calls `set_imu_config` to disable the magnetometer and accelerometer then gets the current orientation from the gyroscope only. 575 | 576 | Returned type | Explanation 577 | --- | --- 578 | Dictionary | A dictionary object indexed by the strings `pitch`, `roll` and `yaw`. The values are Floats representing the angle of the axis in degrees. 579 | 580 | ```python 581 | from sense_hat import SenseHat 582 | 583 | sense = SenseHat() 584 | gyro_only = sense.get_gyroscope() 585 | print("p: {pitch}, r: {roll}, y: {yaw}".format(**gyro_only)) 586 | 587 | # alternatives 588 | print(sense.gyro) 589 | print(sense.gyroscope) 590 | ``` 591 | 592 | - - - 593 | ### get_gyroscope_raw 594 | 595 | Gets the raw x, y and z axis gyroscope data. 596 | 597 | Returned type | Explanation 598 | --- | --- 599 | Dictionary | A dictionary object indexed by the strings `x`, `y` and `z`. The values are Floats representing the rotational intensity of the axis in **radians per second**. 600 | 601 | ```python 602 | from sense_hat import SenseHat 603 | 604 | sense = SenseHat() 605 | raw = sense.get_gyroscope_raw() 606 | print("x: {x}, y: {y}, z: {z}".format(**raw)) 607 | 608 | # alternatives 609 | print(sense.gyro_raw) 610 | print(sense.gyroscope_raw) 611 | ``` 612 | 613 | - - - 614 | ### get_accelerometer 615 | 616 | Calls `set_imu_config` to disable the magnetometer and gyroscope then gets the current orientation from the accelerometer only. 617 | 618 | Returned type | Explanation 619 | --- | --- 620 | Dictionary | A dictionary object indexed by the strings `pitch`, `roll` and `yaw`. The values are Floats representing the angle of the axis in degrees. 621 | 622 | ```python 623 | from sense_hat import SenseHat 624 | 625 | sense = SenseHat() 626 | accel_only = sense.get_accelerometer() 627 | print("p: {pitch}, r: {roll}, y: {yaw}".format(**accel_only)) 628 | 629 | # alternatives 630 | print(sense.accel) 631 | print(sense.accelerometer) 632 | ``` 633 | 634 | - - - 635 | ### get_accelerometer_raw 636 | 637 | Gets the raw x, y and z axis accelerometer data. 638 | 639 | Returned type | Explanation 640 | --- | --- 641 | Dictionary | A dictionary object indexed by the strings `x`, `y` and `z`. The values are Floats representing the acceleration intensity of the axis in **Gs**. 642 | 643 | ```python 644 | from sense_hat import SenseHat 645 | 646 | sense = SenseHat() 647 | raw = sense.get_accelerometer_raw() 648 | print("x: {x}, y: {y}, z: {z}".format(**raw)) 649 | 650 | # alternatives 651 | print(sense.accel_raw) 652 | print(sense.accelerometer_raw) 653 | ``` 654 | 655 | - - - 656 | ## Joystick 657 | 658 | ### InputEvent 659 | 660 | A tuple describing a joystick event. Contains three named parameters: 661 | 662 | * `timestamp` - The time at which the event occurred, as a fractional number of seconds (the same format as the built-in `time` function) 663 | 664 | * `direction` - The direction the joystick was moved, as a string (`"up"`, `"down"`, `"left"`, `"right"`, `"middle"`) 665 | 666 | * `action` - The action that occurred, as a string (`"pressed"`, `"released"`, `"held"`) 667 | 668 | This tuple type is used by several joystick methods either as the return type 669 | or the type of a parameter. 670 | 671 | - - - 672 | ### wait_for_event 673 | 674 | Blocks execution until a joystick event occurs, then returns an `InputEvent` 675 | representing the event that occurred. 676 | 677 | ```python 678 | from sense_hat import SenseHat 679 | from time import sleep 680 | 681 | sense = SenseHat() 682 | event = sense.stick.wait_for_event() 683 | print("The joystick was {} {}".format(event.action, event.direction)) 684 | sleep(0.1) 685 | event = sense.stick.wait_for_event() 686 | print("The joystick was {} {}".format(event.action, event.direction)) 687 | ``` 688 | 689 | In the above example, if you briefly push the joystick in a single direction 690 | you should see two events output: a pressed action and a released action. 691 | The optional *emptybuffer* can be used to flush any pending events before 692 | waiting for new events. Try the following script to see the difference: 693 | 694 | ```python 695 | from sense_hat import SenseHat 696 | from time import sleep 697 | 698 | sense = SenseHat() 699 | event = sense.stick.wait_for_event() 700 | print("The joystick was {} {}".format(event.action, event.direction)) 701 | sleep(0.1) 702 | event = sense.stick.wait_for_event(emptybuffer=True) 703 | print("The joystick was {} {}".format(event.action, event.direction)) 704 | ``` 705 | 706 | - - - 707 | ### get_events 708 | 709 | Returns a list of `InputEvent` tuples representing all events that have 710 | occurred since the last call to `get_events` or `wait_for_event`. 711 | 712 | ```python 713 | from sense_hat import SenseHat 714 | 715 | sense = SenseHat() 716 | while True: 717 | for event in sense.stick.get_events(): 718 | print("The joystick was {} {}".format(event.action, event.direction)) 719 | ``` 720 | 721 | - - - 722 | ### direction_up, direction_left, direction_right, direction_down, direction_middle, direction_any 723 | 724 | These attributes can be assigned a function which will be called whenever the 725 | joystick is pushed in the associated direction (or in any direction in the case 726 | of `direction_any`). The function assigned must either take no parameters or 727 | must take a single parameter which will be passed the associated `InputEvent`. 728 | 729 | ```python 730 | from sense_hat import SenseHat, ACTION_PRESSED, ACTION_HELD, ACTION_RELEASED 731 | from signal import pause 732 | 733 | x = 3 734 | y = 3 735 | sense = SenseHat() 736 | 737 | def clamp(value, min_value=0, max_value=7): 738 | return min(max_value, max(min_value, value)) 739 | 740 | def pushed_up(event): 741 | global y 742 | if event.action != ACTION_RELEASED: 743 | y = clamp(y - 1) 744 | 745 | def pushed_down(event): 746 | global y 747 | if event.action != ACTION_RELEASED: 748 | y = clamp(y + 1) 749 | 750 | def pushed_left(event): 751 | global x 752 | if event.action != ACTION_RELEASED: 753 | x = clamp(x - 1) 754 | 755 | def pushed_right(event): 756 | global x 757 | if event.action != ACTION_RELEASED: 758 | x = clamp(x + 1) 759 | 760 | def refresh(): 761 | sense.clear() 762 | sense.set_pixel(x, y, 255, 255, 255) 763 | 764 | sense.stick.direction_up = pushed_up 765 | sense.stick.direction_down = pushed_down 766 | sense.stick.direction_left = pushed_left 767 | sense.stick.direction_right = pushed_right 768 | sense.stick.direction_any = refresh 769 | refresh() 770 | pause() 771 | ``` 772 | 773 | Note that the `direction_any` event is always called *after* all other events 774 | making it an ideal hook for things like display refreshing (as in the example 775 | above). 776 | 777 | - - - 778 | ## Light and colour sensor 779 | 780 | 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. 781 | 782 | You can interact with the colour sensor through the `colour` (or `color`) attribute of the Sense HAT, which corresponds to a `ColourSensor` object. 783 | 784 | 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. 785 | 786 | ```python 787 | from sense_hat import SenseHat 788 | from time import sleep 789 | 790 | sense = SenseHat() 791 | sense.color.gain = 4 792 | sense.color.integration_cycles = 64 793 | 794 | while True: 795 | sleep(2 * sense.colour.integration_time) 796 | red, green, blue, clear = sense.colour.colour # readings scaled to 0-256 797 | print(f"R: {red}, G: {green}, B: {blue}, C: {clear}") 798 | ``` 799 | 800 | --- 801 | ### Obtaining RGB and Clear light readings 802 | 803 | 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. 804 | 805 | `ColourSensor` property | Returned type | Explanation 806 | --- | --- | --- 807 | `red` | int | The amount of incident red light, scaled to 0-256 808 | `green` | int | The amount of incident green light, scaled to 0-256 809 | `blue` | int | The amount of incident blue light, scaled to 0-256 810 | `clear` | int | The amount of incident light (brightness), scaled to 0-256 811 | `colour` | tuple | A 4-tuple containing the RGBC (Red, Green, Blue and Clear) sensor readings, each scaled to 0-256 812 | 813 | These are all read-only properties; they cannot be set. 814 | 815 | 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. 816 | 817 | --- 818 | ### Gain 819 | 820 | 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. 821 | 822 | 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. 823 | 824 | ```python 825 | from sense_hat import SenseHAT 826 | from time import sleep 827 | 828 | sense = SenseHat() 829 | sense.colour.gain = 1 830 | sleep(1) 831 | print(f"Gain: {sense.colour.gain}") 832 | print(f"RGBC: {sense.colour.colour}") 833 | 834 | sense.colour.gain = 16 835 | sleep(1) 836 | print(f"Gain: {sense.colour.gain}") 837 | print(f"RGBC: {sense.colour.colour}") 838 | ``` 839 | 840 | Under the same lighting conditions, the RGBC values should be considerably higher when the gain setting is increased. 841 | 842 | 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. 843 | 844 | --- 845 | ### Integration cycles and the interval between measurements 846 | 847 | 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. 848 | 849 | 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. 850 | 851 | ```python 852 | from sense_hat import SenseHAT 853 | from time import sleep 854 | 855 | sense = SenseHat() 856 | sense.colour.integration_cycles = 100 857 | print(f"Integration cycles: {sense.colour.integration_cycles}") 858 | print(f"Minimum wait time between measurements: {sense.colour.integration_time} seconds") 859 | ``` 860 | 861 | --- 862 | ### Integration cycles and raw values 863 | 864 | 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. 865 | 866 | The range of the raw values depends on the number of integration cycles: 867 | 868 | `integration_cycles` | maximum raw value (`max_raw`) 869 | --- | --- 870 | 1 - 64 | 1024 * `integration_cycles` 871 | \> 64 | 65536 872 | 873 | 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. 874 | 875 | The following properties of the `ColourSensor` object provide direct access to the raw values measured by the sensor. 876 | 877 | `ColourSensor` property | Returned type | Explanation 878 | --- | --- | --- 879 | `red_raw` | int | The amount of incident red light, between 0 and `max_raw` 880 | `green_raw` | int | The amount of incident green light, between 0 and `max_raw` 881 | `blue_raw` | int | The amount of incident blue light, between 0 and `max_raw` 882 | `clear_raw` | int | The amount of incident light (brightness), between 0 and `max_raw` 883 | `colour_raw` | tuple | A 4-tuple containing the RGBC (Red, Green, Blue and Clear) raw sensor readings, each between 0 and `max_raw` 884 | `rgb` | tuple | A 3-tuple containing the RGB raw sensor readings, each between 0 and `max_raw`. 885 | `brightness` | int | An alias to the `clear_raw` property - the amount of incident light, between 0 and `max_raw` 886 | 887 | Here is an example comparing raw values to the corresponding scaled ones, for a given number of integration cycles. 888 | 889 | ``` 890 | from sense_hat import SenseHAT 891 | from time import sleep 892 | 893 | sense = SenseHat() 894 | sense.colour.integration_cycles = 64 895 | print(f"Minimum time between readings: {sense.colour.integration_time} seconds") 896 | print(f"Maximum raw sensor reading: {sense.colour.max_raw}") 897 | sleep(sense.colour.integration_time + 0.1) # try omitting this 898 | print(f"Current raw sensor readings: {sense.colour.colour_raw}") 899 | print(f"Scaled values: {sense.colour.colour}") 900 | ``` 901 | 902 | ## Exceptions 903 | 904 | Custom Sense HAT exceptions are statically defined in the `sense_hat.exceptions` module. 905 | The exceptions relate to problems encountered while initialising the colour chip or due to setting invalid parameters. 906 | Each exception includes a message describing the issue encountered, and is subclassed from the base class `SenseHatException`. 907 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Sense HAT Changelog 2 | 3 | ## v2 4 | 5 | ### 2.4.0 6 | - Added `rgb` method to allow for easy reuse of sense hat colour sensor values 7 | - Added `brightness` method alias for the sense hat colour sensor 8 | 9 | ### 2.3.x 10 | 11 | - Added support for the light/colour sensor in the v2 Sense HAT 12 | 13 | ### 2.2.0 14 | 15 | - Added new stick interface for the joystick 16 | 17 | ### 2.1.0 18 | 19 | - Added gamma, low light and other properties 20 | 21 | ### 2.0.0 22 | 23 | - Library renamed from `astro_pi` to `sense_hat` 24 | - Class renamed from `AstroPi` to `SenseHat` 25 | - API otherwise unchanged 26 | 27 | ## v1 28 | 29 | ### 1.1.6 30 | 31 | - Updated IMU settings file path 32 | 33 | ### 1.1.5 34 | 35 | - Introduced IMU settings file 36 | 37 | ### 1.1.4 38 | 39 | - Made _get_char_pixels return cloned lists 40 | 41 | ### 1.1.0 42 | 43 | - Fixed bug in `show_letter` 44 | 45 | ### 1.0.0 46 | 47 | - API design confirmed 48 | 49 | ## v0 50 | 51 | ### 0.0.0 52 | 53 | - Alpha status 54 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Sense HAT examples 2 | 3 | - [Colour cycle](colour_cycle.py) 4 | - [Compass](compass.py) 5 | - [PyGame Joystick](pygame_joystick.py) 6 | - [Rainbow](rainbow.py) 7 | - [Rotation](rotation.py) 8 | - [Space Invader](space_invader.py) 9 | - [Text scroll](text_scroll.py) 10 | -------------------------------------------------------------------------------- /docs/examples/colour_cycle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import time 3 | from sense_hat import SenseHat 4 | 5 | sense = SenseHat() 6 | 7 | r = 255 8 | g = 0 9 | b = 0 10 | 11 | msleep = lambda x: time.sleep(x / 1000.0) 12 | 13 | 14 | def next_colour(): 15 | global r 16 | global g 17 | global b 18 | 19 | if (r == 255 and g < 255 and b == 0): 20 | g += 1 21 | 22 | if (g == 255 and r > 0 and b == 0): 23 | r -= 1 24 | 25 | if (g == 255 and b < 255 and r == 0): 26 | b += 1 27 | 28 | if (b == 255 and g > 0 and r == 0): 29 | g -= 1 30 | 31 | if (b == 255 and r < 255 and g == 0): 32 | r += 1 33 | 34 | if (r == 255 and b > 0 and g == 0): 35 | b -= 1 36 | 37 | while True: 38 | sense.clear([r, g, b]) 39 | msleep(2) 40 | next_colour() 41 | -------------------------------------------------------------------------------- /docs/examples/compass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | from sense_hat import SenseHat 4 | 5 | # To get good results with the magnetometer you must first calibrate it using 6 | # the program in RTIMULib/Linux/RTIMULibCal 7 | # The calibration program will produce the file RTIMULib.ini 8 | # Copy it into the same folder as your Python code 9 | 10 | led_loop = [4, 5, 6, 7, 15, 23, 31, 39, 47, 55, 63, 62, 61, 60, 59, 58, 57, 56, 48, 40, 32, 24, 16, 8, 0, 1, 2, 3] 11 | 12 | sense = SenseHat() 13 | sense.set_rotation(0) 14 | sense.clear() 15 | 16 | prev_x = 0 17 | prev_y = 0 18 | 19 | led_degree_ratio = len(led_loop) / 360.0 20 | 21 | while True: 22 | dir = sense.get_compass() 23 | dir_inverted = 360 - dir # So LED appears to follow North 24 | led_index = int(led_degree_ratio * dir_inverted) 25 | offset = led_loop[led_index] 26 | 27 | y = offset // 8 # row 28 | x = offset % 8 # column 29 | 30 | if x != prev_x or y != prev_y: 31 | sense.set_pixel(prev_x, prev_y, 0, 0, 0) 32 | 33 | sense.set_pixel(x, y, 0, 0, 255) 34 | 35 | prev_x = x 36 | prev_y = y 37 | -------------------------------------------------------------------------------- /docs/examples/evdev_joystick.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import time 4 | from sense_hat import SenseHat 5 | from evdev import InputDevice, list_devices, ecodes 6 | 7 | print("Press Ctrl-C to quit") 8 | time.sleep(1) 9 | 10 | sense = SenseHat() 11 | sense.clear() # Blank the LED matrix 12 | 13 | found = False; 14 | devices = [InputDevice(fn) for fn in list_devices()] 15 | for dev in devices: 16 | if dev.name == 'Raspberry Pi Sense HAT Joystick': 17 | found = True; 18 | break 19 | 20 | if not(found): 21 | print('Raspberry Pi Sense HAT Joystick not found. Aborting ...') 22 | sys.exit() 23 | 24 | # 0, 0 = Top left 25 | # 7, 7 = Bottom right 26 | UP_PIXELS = [[3, 0], [4, 0]] 27 | DOWN_PIXELS = [[3, 7], [4, 7]] 28 | LEFT_PIXELS = [[0, 3], [0, 4]] 29 | RIGHT_PIXELS = [[7, 3], [7, 4]] 30 | CENTRE_PIXELS = [[3, 3], [4, 3], [3, 4], [4, 4]] 31 | 32 | 33 | def set_pixels(pixels, col): 34 | for p in pixels: 35 | sense.set_pixel(p[0], p[1], col[0], col[1], col[2]) 36 | 37 | 38 | def handle_code(code, colour): 39 | if code == ecodes.KEY_DOWN: 40 | set_pixels(DOWN_PIXELS, colour) 41 | elif code == ecodes.KEY_UP: 42 | set_pixels(UP_PIXELS, colour) 43 | elif code == ecodes.KEY_LEFT: 44 | set_pixels(LEFT_PIXELS, colour) 45 | elif code == ecodes.KEY_RIGHT: 46 | set_pixels(RIGHT_PIXELS, colour) 47 | elif code == ecodes.KEY_ENTER: 48 | set_pixels(CENTRE_PIXELS, colour) 49 | 50 | 51 | BLACK = [0, 0, 0] 52 | WHITE = [255, 255, 255] 53 | 54 | try: 55 | for event in dev.read_loop(): 56 | if event.type == ecodes.EV_KEY: 57 | if event.value == 1: # key down 58 | handle_code(event.code, WHITE) 59 | if event.value == 0: # key up 60 | handle_code(event.code, BLACK) 61 | except KeyboardInterrupt: 62 | sys.exit() 63 | -------------------------------------------------------------------------------- /docs/examples/pygame_joystick.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from sense_hat import SenseHat 3 | import os 4 | import time 5 | import pygame # See http://www.pygame.org/docs 6 | from pygame.locals import * 7 | 8 | 9 | print("Press Escape to quit") 10 | time.sleep(1) 11 | 12 | pygame.init() 13 | pygame.display.set_mode((640, 480)) 14 | 15 | sense = SenseHat() 16 | sense.clear() # Blank the LED matrix 17 | 18 | # 0, 0 = Top left 19 | # 7, 7 = Bottom right 20 | UP_PIXELS = [[3, 0], [4, 0]] 21 | DOWN_PIXELS = [[3, 7], [4, 7]] 22 | LEFT_PIXELS = [[0, 3], [0, 4]] 23 | RIGHT_PIXELS = [[7, 3], [7, 4]] 24 | CENTRE_PIXELS = [[3, 3], [4, 3], [3, 4], [4, 4]] 25 | 26 | 27 | def set_pixels(pixels, col): 28 | for p in pixels: 29 | sense.set_pixel(p[0], p[1], col[0], col[1], col[2]) 30 | 31 | 32 | def handle_event(event, colour): 33 | if event.key == pygame.K_DOWN: 34 | set_pixels(DOWN_PIXELS, colour) 35 | elif event.key == pygame.K_UP: 36 | set_pixels(UP_PIXELS, colour) 37 | elif event.key == pygame.K_LEFT: 38 | set_pixels(LEFT_PIXELS, colour) 39 | elif event.key == pygame.K_RIGHT: 40 | set_pixels(RIGHT_PIXELS, colour) 41 | elif event.key == pygame.K_RETURN: 42 | set_pixels(CENTRE_PIXELS, colour) 43 | 44 | 45 | running = True 46 | 47 | BLACK = [0, 0, 0] 48 | WHITE = [255, 255, 255] 49 | 50 | while running: 51 | for event in pygame.event.get(): 52 | if event.type == pygame.QUIT: 53 | running = False 54 | if event.type == KEYDOWN: 55 | if event.key == K_ESCAPE: 56 | running = False 57 | handle_event(event, WHITE) 58 | if event.type == KEYUP: 59 | handle_event(event, BLACK) 60 | -------------------------------------------------------------------------------- /docs/examples/rainbow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import time 3 | from sense_hat import SenseHat 4 | 5 | sense = SenseHat() 6 | 7 | pixels = [ 8 | [255, 0, 0], [255, 0, 0], [255, 87, 0], [255, 196, 0], [205, 255, 0], [95, 255, 0], [0, 255, 13], [0, 255, 122], 9 | [255, 0, 0], [255, 96, 0], [255, 205, 0], [196, 255, 0], [87, 255, 0], [0, 255, 22], [0, 255, 131], [0, 255, 240], 10 | [255, 105, 0], [255, 214, 0], [187, 255, 0], [78, 255, 0], [0, 255, 30], [0, 255, 140], [0, 255, 248], [0, 152, 255], 11 | [255, 223, 0], [178, 255, 0], [70, 255, 0], [0, 255, 40], [0, 255, 148], [0, 253, 255], [0, 144, 255], [0, 34, 255], 12 | [170, 255, 0], [61, 255, 0], [0, 255, 48], [0, 255, 157], [0, 243, 255], [0, 134, 255], [0, 26, 255], [83, 0, 255], 13 | [52, 255, 0], [0, 255, 57], [0, 255, 166], [0, 235, 255], [0, 126, 255], [0, 17, 255], [92, 0, 255], [201, 0, 255], 14 | [0, 255, 66], [0, 255, 174], [0, 226, 255], [0, 117, 255], [0, 8, 255], [100, 0, 255], [210, 0, 255], [255, 0, 192], 15 | [0, 255, 183], [0, 217, 255], [0, 109, 255], [0, 0, 255], [110, 0, 255], [218, 0, 255], [255, 0, 183], [255, 0, 74] 16 | ] 17 | 18 | msleep = lambda x: time.sleep(x / 1000.0) 19 | 20 | 21 | def next_colour(pix): 22 | r = pix[0] 23 | g = pix[1] 24 | b = pix[2] 25 | 26 | if (r == 255 and g < 255 and b == 0): 27 | g += 1 28 | 29 | if (g == 255 and r > 0 and b == 0): 30 | r -= 1 31 | 32 | if (g == 255 and b < 255 and r == 0): 33 | b += 1 34 | 35 | if (b == 255 and g > 0 and r == 0): 36 | g -= 1 37 | 38 | if (b == 255 and r < 255 and g == 0): 39 | r += 1 40 | 41 | if (r == 255 and b > 0 and g == 0): 42 | b -= 1 43 | 44 | pix[0] = r 45 | pix[1] = g 46 | pix[2] = b 47 | 48 | while True: 49 | for pix in pixels: 50 | next_colour(pix) 51 | 52 | sense.set_pixels(pixels) 53 | msleep(2) 54 | -------------------------------------------------------------------------------- /docs/examples/rotation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | import time 4 | from sense_hat import SenseHat 5 | 6 | X = (255, 0, 0) 7 | O = (255, 255, 255) 8 | 9 | question_mark = [ 10 | O, O, O, X, X, O, O, O, 11 | O, O, X, O, O, X, O, O, 12 | O, O, O, O, O, X, O, O, 13 | O, O, O, O, X, O, O, O, 14 | O, O, O, X, O, O, O, O, 15 | O, O, O, X, O, O, O, O, 16 | O, O, O, O, O, O, O, O, 17 | O, O, O, X, O, O, O, O 18 | ] 19 | 20 | sense = SenseHat() 21 | 22 | sense.set_pixels(question_mark) 23 | 24 | sense.set_pixel(0, 0, 255, 0, 0) 25 | sense.set_pixel(0, 7, 0, 255, 0) 26 | sense.set_pixel(7, 0, 0, 0, 255) 27 | sense.set_pixel(7, 7, 255, 0, 255) 28 | 29 | while True: 30 | for r in [0, 90, 180, 270]: 31 | sense.set_rotation(r) 32 | time.sleep(0.3) 33 | -------------------------------------------------------------------------------- /docs/examples/space_invader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astro-pi/python-sense-hat/d2942773a5c740a4cf23211d6bc9a3a695439e25/docs/examples/space_invader.png -------------------------------------------------------------------------------- /docs/examples/space_invader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from sense_hat import SenseHat 3 | 4 | sense = SenseHat() 5 | sense.clear() 6 | sense.load_image("space_invader.png") 7 | -------------------------------------------------------------------------------- /docs/examples/text_scroll.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from sense_hat import SenseHat 3 | 4 | sense = SenseHat() 5 | sense.set_rotation(180) 6 | red = (255, 0, 0) 7 | sense.show_message("One small step for Pi!", text_colour=red) 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Sense HAT 2 | 3 | Python module to control the [Raspberry Pi Sense HAT](https://www.raspberrypi.com/products/sense-hat/) 4 | 5 | ## Features 6 | 7 | The Sense HAT features an 8x8 RGB LED matrix, a mini joystick and the following sensors: 8 | 9 | - Gyroscope 10 | - Accelerometer 11 | - Magnetometer 12 | - Temperature 13 | - Humidity 14 | - Barometric pressure 15 | - Light and colour 16 | 17 | ## Install 18 | 19 | Install the Sense HAT software by opening a Terminal window and entering the following commands (while connected to the Internet): 20 | 21 | ```bash 22 | sudo apt-get update 23 | sudo apt-get install sense-hat 24 | sudo reboot 25 | ``` 26 | 27 | ## Usage 28 | 29 | Hello world example: 30 | 31 | ```python 32 | from sense_hat import SenseHat 33 | 34 | sense = SenseHat() 35 | 36 | sense.show_message("Hello world!") 37 | ``` 38 | 39 | See the [API reference](api.md) for full documentation of the library's functions. See [examples](examples/README.md). 40 | 41 | ## Development 42 | 43 | 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) 44 | 45 | See the [changelog](changelog.md). 46 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs==1.4.2 2 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Sense HAT 2 | theme: readthedocs 3 | site_url: https://sense-hat.readthedocs.io/en/latest/ 4 | repo_url: https://github.com/astro-pi/python-sense-hat 5 | site_description: Python module to control the Raspberry Pi Sense HAT used in the Astro Pi mission 6 | site_author: Raspberry Pi Foundation 7 | site_dir: readthedocs 8 | #google_analytics: ['UA-46270871-5', 'pythonhosted.org/sense-hat'] 9 | nav: 10 | - 'Home': 'index.md' 11 | - 'API Reference': 'api.md' 12 | - 'Examples': 'examples/README.md' 13 | - 'Changelog': 'changelog.md' 14 | -------------------------------------------------------------------------------- /sense_hat/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .sense_hat import SenseHat, SenseHat as AstroPi 3 | from .stick import ( 4 | SenseStick, 5 | InputEvent, 6 | DIRECTION_UP, 7 | DIRECTION_DOWN, 8 | DIRECTION_LEFT, 9 | DIRECTION_RIGHT, 10 | DIRECTION_MIDDLE, 11 | ACTION_PRESSED, 12 | ACTION_RELEASED, 13 | ACTION_HELD, 14 | ) 15 | 16 | __version__ = '2.6.0' 17 | -------------------------------------------------------------------------------- /sense_hat/colour.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python library for the TCS3472x and TCS340x Color Sensors 3 | Documentation (including datasheet): https://ams.com/tcs34725#tab/documents 4 | https://ams.com/tcs3400#tab/documents 5 | The sense hat for AstroPi on the ISS uses the TCS34725. 6 | The sense hat v2 uses the TCS3400 the successor of the TCS34725. 7 | The TCS34725 is not available any more. It was discontinued by ams in 2021. 8 | """ 9 | 10 | from time import sleep 11 | from .exceptions import ColourSensorInitialisationError, InvalidGainError, \ 12 | InvalidIntegrationCyclesError 13 | 14 | 15 | class HardwareInterface: 16 | """ 17 | `HardwareInterface` is the abstract class that sits between the 18 | `ColourSensor` class (providing the TCS34725/TCS3400 sensor API) 19 | and the actual hardware. Using this intermediate layer of abstraction, 20 | a `ColourSensor` object interacts with the hardware without being 21 | aware of how this interaction is implemented. 22 | Different subclasses of the `HardwareInterface` class can provide 23 | access to the hardware through e.g. I2C, `libiio` and its system 24 | files or even a hardware emulator. 25 | """ 26 | 27 | @staticmethod 28 | def max_value(integration_cycles): 29 | """ 30 | The maximum raw value for the RBGC channels depends on the number 31 | of integration cycles. 32 | """ 33 | return 65535 if integration_cycles >= 64 else 1024*integration_cycles 34 | 35 | def get_enabled(self): 36 | """ 37 | Return True if the sensor is enabled and False otherwise 38 | """ 39 | raise NotImplementedError 40 | 41 | def set_enabled(self, status): 42 | """ 43 | Enable or disable the sensor, depending on the boolean `status` flag 44 | """ 45 | raise NotImplementedError 46 | 47 | def get_gain(self): 48 | """ 49 | Return the current value of the sensor gain. 50 | See GAIN_VALUES for the set of possible values. 51 | """ 52 | raise NotImplementedError 53 | 54 | def set_gain(self, gain): 55 | """ 56 | Set the value for the sensor `gain`. 57 | See GAIN_VALUES for the set of possible values. 58 | """ 59 | raise NotImplementedError 60 | 61 | def get_integration_cycles(self): 62 | """ 63 | Return the current number of integration_cycles (1-256). 64 | It takes `integration_cycles` * CLOCK_STEP to obtain a new 65 | sensor reading. 66 | """ 67 | raise NotImplementedError 68 | 69 | def set_integration_cycles(self, integration_cycles): 70 | """ 71 | Set the current number of integration_cycles (1-256). 72 | It takes `integration_cycles` * CLOCK_STEP to obtain a new 73 | sensor reading. 74 | """ 75 | raise NotImplementedError 76 | 77 | def get_raw(self): 78 | """ 79 | Return a tuple containing the raw values of the RGBC channels. 80 | The maximum for these raw values depends on the number of 81 | integration cycles and can be computed using `max_value`. 82 | """ 83 | raise NotImplementedError 84 | 85 | def get_red(self): 86 | """ 87 | Return the raw value of the R (red) channel. 88 | The maximum for this raw value depends on the number of 89 | integration cycles and can be computed using `max_value`. 90 | """ 91 | raise NotImplementedError 92 | 93 | def get_green(self): 94 | """ 95 | Return the raw value of the G (green) channel. 96 | The maximum for this raw value depends on the number of 97 | integration cycles and can be computed using `max_value`. 98 | """ 99 | raise NotImplementedError 100 | 101 | def get_blue(self): 102 | """ 103 | Return the raw value of the B (blue) channel. 104 | The maximum for this raw value depends on the number of 105 | integration cycles and can be computed using `max_value`. 106 | """ 107 | raise NotImplementedError 108 | 109 | def get_clear(self): 110 | """ 111 | Return the raw value of the C (clear light) channel. 112 | The maximum for this raw value depends on the number of 113 | integration cycles and can be computed using `max_value`. 114 | """ 115 | raise NotImplementedError 116 | 117 | 118 | ### An I2C implementation of the abstract colour sensor `HardwareInterface` 119 | 120 | def _raw_wrapper(register): 121 | """ 122 | Returns a function that retrieves the sensor reading at `register`. 123 | The RGBC readings are all retrieved from the sensor in an identical 124 | fashion. This is a factory function that implements this retrieval method. 125 | """ 126 | def get_raw_register(self): 127 | block = self.bus.read_i2c_block_data(self.ADDR, register, 2) 128 | return (block[0] + (block[1] << 8)) 129 | return get_raw_register 130 | 131 | class I2C(HardwareInterface): 132 | """ 133 | An implementation of the `HardwareInterface` for the TCS34725/TCS3400 134 | sensor that uses I2C to control the sensor and retrieve measurements. 135 | Use the datasheets as a reference. 136 | """ 137 | 138 | # device-specific constants 139 | BUS = 1 140 | 141 | # control registers 142 | ENABLE = 0x80 143 | ATIME = 0x81 144 | CONTROL = 0x8F 145 | ID = 0x92 146 | STATUS = 0x93 147 | # (if a register is described in the datasheet but missing here 148 | # it means the corresponding functionality is not provided) 149 | 150 | # data registers 151 | CDATA = 0x94 152 | RDATA = 0x96 153 | GDATA = 0x98 154 | BDATA = 0x9A 155 | 156 | # bit positions 157 | OFF = 0x00 158 | PON = 0x01 159 | AEN = 0x02 160 | ON = (PON | AEN) 161 | AVALID = 0x01 162 | 163 | GAIN_REG_VALUES = (0x00, 0x01, 0x02, 0x03) 164 | # Assume TCS34725 as on the ISS AstroPi 165 | # Adjust for TCS3400 after the detection of the sensor type. 166 | ADDR = 0x29 167 | GAIN_VALUES = (1, 4, 16, 60) 168 | CLOCK_STEP = 0.0024 # 2.4ms 169 | GAIN_TO_REG = dict(zip(GAIN_VALUES, GAIN_REG_VALUES)) 170 | REG_TO_GAIN = dict(zip(GAIN_REG_VALUES, GAIN_VALUES)) 171 | 172 | def __init__(self): 173 | 174 | import smbus 175 | import glob 176 | 177 | try: 178 | self.bus = smbus.SMBus(self.BUS) 179 | except Exception as e: 180 | explanation = "(I2C is not enabled)" if not self.i2c_enabled() else "" 181 | raise ColourSensorInitialisationError(explanation=explanation) from e 182 | 183 | # Test for sensor at I2C addresses 0x29 or 0x39 184 | # Both sensors have variants at 0x29 and 0x39 (See data sheets) 185 | addr1 = addr2 = False 186 | try: 187 | self.bus.write_quick(0x29) 188 | addr1 = True 189 | except: 190 | pass 191 | try: 192 | self.bus.write_quick(0x39) 193 | addr2 = True 194 | except: 195 | pass 196 | 197 | if addr2: 198 | self.ADDR = 0x39 199 | if addr1 or addr2: 200 | # get sensor id 201 | id = self._read(self.ID) 202 | if (id & 0xf8) == 0x90: 203 | sensor = 'TCS340x' 204 | elif (id & 0xf4) == 0x44: 205 | sensor = 'TCS3472x' 206 | else: 207 | explanation = "(Sensor not present)" 208 | raise ColourSensorInitialisationError(explanation=explanation) 209 | 210 | # Set type specific constants 211 | # Assume TCS3472x as in AstroPi 212 | sensor == 'TCS3472x' 213 | if sensor == 'TCS340x': 214 | self.GAIN_VALUES = (1, 4, 16, 64) 215 | self.CLOCK_STEP = 0.00275 # 2.75ms 216 | self.GAIN_TO_REG = dict(zip(self.GAIN_VALUES, self.GAIN_REG_VALUES)) 217 | self.REG_TO_GAIN = dict(zip(self.GAIN_REG_VALUES, self.GAIN_VALUES)) 218 | 219 | @staticmethod 220 | def i2c_enabled(): 221 | """Returns True if I2C is enabled or False otherwise.""" 222 | return next(glob.iglob('/sys/bus/i2c/devices/*'), None) is not None 223 | 224 | def _read(self, attribute): 225 | """ 226 | Read and return the value of a specific register (`attribute`) of the 227 | TCS34725/TCS3400 colour sensor. 228 | """ 229 | return self.bus.read_byte_data(self.ADDR, attribute) 230 | 231 | def _write(self, attribute, value): 232 | """ 233 | Write a value in a specific register (`attribute`) of the 234 | TCS34725/TCS3400 colour sensor. 235 | """ 236 | self.bus.write_byte_data(self.ADDR, attribute, value) 237 | 238 | def get_enabled(self): 239 | """ 240 | Return True if the sensor is enabled and False otherwise 241 | """ 242 | return self._read(self.ENABLE) == (PON | AEN) 243 | 244 | def set_enabled(self, status): 245 | """ 246 | Enable or disable the sensor, depending on the boolean `status` flag 247 | """ 248 | if status: 249 | self._write(self.ENABLE, self.PON) 250 | sleep(self.CLOCK_STEP) # From datasheet: "there is a 2.4 ms warm-up delay if PON is enabled." 251 | self._write(self.ENABLE, self.ON) 252 | else: 253 | self._write(self.ENABLE, self.OFF) 254 | sleep(self.CLOCK_STEP) 255 | 256 | def get_gain(self): 257 | """ 258 | Return the current value of the sensor gain. 259 | See GAIN_VALUES for the set of possible values. 260 | """ 261 | register_value = self._read(self.CONTROL) 262 | # map the register value to an actual gain value 263 | return self.REG_TO_GAIN[register_value] 264 | 265 | def set_gain(self, gain): 266 | """ 267 | Set the value for the sensor `gain`. 268 | See GAIN_VALUES for the set of possible values. 269 | """ 270 | # map the specified value for `gain` to a register value 271 | register_value = self.GAIN_TO_REG[gain] 272 | self._write(self.CONTROL, register_value) 273 | 274 | def get_integration_cycles(self): 275 | """ 276 | Return the current number of integration_cycles (1-256). 277 | It takes `integration_cycles` * CLOCK_STEP to obtain a new 278 | sensor reading. 279 | """ 280 | return 256 - self._read(self.ATIME) 281 | 282 | def set_integration_cycles(self, integration_cycles): 283 | """ 284 | Set the current number of integration_cycles (1-256). 285 | It takes `integration_cycles` * CLOCK_STEP to obtain a new 286 | sensor reading. 287 | """ 288 | self._write(self.ATIME, 256-integration_cycles) 289 | 290 | def get_raw(self): 291 | """ 292 | Return a tuple containing the raw values of the RGBC channels. 293 | The maximum for these raw values depends on the number of 294 | integration cycles and can be computed using `max_value`. 295 | """ 296 | # The 4-tuple is retrieved using a *single read*. 297 | block = self.bus.read_i2c_block_data(self.ADDR, self.CDATA, 8) 298 | return ( 299 | (block[3] << 8) + block[2], 300 | (block[5] << 8) + block[4], 301 | (block[7] << 8) + block[6], 302 | (block[1] << 8) + block[0] 303 | ) 304 | 305 | """ 306 | The methods below return the raw value of the R, G, B or Clear channels. 307 | The maximum for these raw value depends on the number of integration 308 | cycles and can be computed using `max_value`. 309 | Use these methods if you only make use of one channel reading per iteration. 310 | Otherwise, you are probably better off using `get_raw`, to retrieve all 311 | channels in a single read. 312 | """ 313 | get_red = _raw_wrapper(RDATA) 314 | get_green = _raw_wrapper(GDATA) 315 | get_blue = _raw_wrapper(BDATA) 316 | get_clear = _raw_wrapper(CDATA) 317 | 318 | 319 | class ColourSensor: 320 | 321 | def __init__(self, gain=1, integration_cycles=1, interface=I2C): 322 | self.interface = interface() 323 | self.gain = gain 324 | self.integration_cycles = integration_cycles 325 | self.enabled = 1 326 | 327 | @property 328 | def enabled(self): 329 | return self.interface.get_enabled() 330 | 331 | @enabled.setter 332 | def enabled(self, status): 333 | self.interface.set_enabled(status) 334 | 335 | @property 336 | def gain(self): 337 | return self.interface.get_gain() 338 | 339 | @gain.setter 340 | def gain(self, gain): 341 | if gain in self.interface.GAIN_VALUES: 342 | self.interface.set_gain(gain) 343 | else: 344 | raise InvalidGainError(gain=gain, values=self.interface.GAIN_VALUES) 345 | 346 | @property 347 | def integration_cycles(self): 348 | return self.interface.get_integration_cycles() 349 | 350 | @integration_cycles.setter 351 | def integration_cycles(self, integration_cycles): 352 | if 1 <= integration_cycles <= 256: 353 | self.interface.set_integration_cycles(integration_cycles) 354 | sleep(self.interface.CLOCK_STEP) 355 | else: 356 | raise InvalidIntegrationCyclesError(integration_cycles=integration_cycles) 357 | 358 | @property 359 | def integration_time(self): 360 | return self.integration_cycles * self.interface.CLOCK_STEP 361 | 362 | @property 363 | def max_raw(self): 364 | return self.interface.max_value(self.integration_cycles) 365 | 366 | @property 367 | def colour_raw(self): 368 | return self.interface.get_raw() 369 | 370 | color_raw = colour_raw 371 | red_raw = property(lambda self: self.interface.get_red()) 372 | green_raw = property(lambda self: self.interface.get_green()) 373 | blue_raw = property(lambda self: self.interface.get_blue()) 374 | clear_raw = property(lambda self: self.interface.get_clear()) 375 | brightness = clear_raw 376 | 377 | @property 378 | def _scaling(self): 379 | return self.max_raw // 256 380 | 381 | @property 382 | def colour(self): 383 | return tuple(reading // self._scaling for reading in self.colour_raw) 384 | 385 | @property 386 | def rgb(self): 387 | return tuple(reading // self._scaling for reading in self.colour_raw)[0:3] 388 | 389 | color = colour 390 | red = property(lambda self: self.red_raw // self._scaling ) 391 | green = property(lambda self: self.green_raw // self._scaling ) 392 | blue = property(lambda self: self.blue_raw // self._scaling ) 393 | clear = property(lambda self: self.clear_raw // self._scaling ) 394 | -------------------------------------------------------------------------------- /sense_hat/exceptions.py: -------------------------------------------------------------------------------- 1 | class SenseHatException(Exception): 2 | """ 3 | The base exception class for all SenseHat exceptions. 4 | """ 5 | fmt = 'An unspecified error occurred' 6 | 7 | def __init__(self, **kwargs): 8 | msg = self.fmt.format(**kwargs) 9 | Exception.__init__(self, msg) 10 | self.kwargs = kwargs 11 | 12 | 13 | class ColourSensorInitialisationError(SenseHatException): 14 | fmt = "Failed to initialise colour sensor. {explanation}" 15 | 16 | 17 | class InvalidGainError(SenseHatException): 18 | fmt = "Cannot set gain to '{gain}'. Values: {values}" 19 | 20 | 21 | class InvalidIntegrationCyclesError(SenseHatException): 22 | fmt = "Cannot set integration cycles to {integration_cycles} (1-256)" 23 | -------------------------------------------------------------------------------- /sense_hat/sense_hat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import logging 3 | import struct 4 | import os 5 | import sys 6 | import math 7 | import time 8 | import numpy as np 9 | import shutil 10 | import glob 11 | import RTIMU # custom version 12 | import pwd 13 | import array 14 | import fcntl 15 | from PIL import Image # pillow 16 | from copy import deepcopy 17 | 18 | from .stick import SenseStick 19 | from .colour import ColourSensor 20 | from .exceptions import ColourSensorInitialisationError 21 | 22 | class SenseHat(object): 23 | 24 | SENSE_HAT_FB_NAME = 'RPi-Sense FB' 25 | SENSE_HAT_FB_FBIOGET_GAMMA = 61696 26 | SENSE_HAT_FB_FBIOSET_GAMMA = 61697 27 | SENSE_HAT_FB_FBIORESET_GAMMA = 61698 28 | SENSE_HAT_FB_GAMMA_DEFAULT = 0 29 | SENSE_HAT_FB_GAMMA_LOW = 1 30 | SENSE_HAT_FB_GAMMA_USER = 2 31 | SETTINGS_HOME_PATH = '.config/sense_hat' 32 | 33 | def __init__( 34 | self, 35 | imu_settings_file='RTIMULib', 36 | text_assets='sense_hat_text' 37 | ): 38 | 39 | self._fb_device = self._get_fb_device() 40 | if self._fb_device is None: 41 | raise OSError('Cannot detect %s device' % self.SENSE_HAT_FB_NAME) 42 | 43 | if not glob.glob('/dev/i2c*'): 44 | raise OSError('Cannot access I2C. Please ensure I2C is enabled in raspi-config') 45 | 46 | # 0 is With B+ HDMI port facing downwards 47 | pix_map0 = np.array([ 48 | [0, 1, 2, 3, 4, 5, 6, 7], 49 | [8, 9, 10, 11, 12, 13, 14, 15], 50 | [16, 17, 18, 19, 20, 21, 22, 23], 51 | [24, 25, 26, 27, 28, 29, 30, 31], 52 | [32, 33, 34, 35, 36, 37, 38, 39], 53 | [40, 41, 42, 43, 44, 45, 46, 47], 54 | [48, 49, 50, 51, 52, 53, 54, 55], 55 | [56, 57, 58, 59, 60, 61, 62, 63] 56 | ], int) 57 | 58 | pix_map90 = np.rot90(pix_map0) 59 | pix_map180 = np.rot90(pix_map90) 60 | pix_map270 = np.rot90(pix_map180) 61 | 62 | self._pix_map = { 63 | 0: pix_map0, 64 | 90: pix_map90, 65 | 180: pix_map180, 66 | 270: pix_map270 67 | } 68 | 69 | self._rotation = 0 70 | 71 | # Load text assets 72 | dir_path = os.path.dirname(__file__) 73 | self._load_text_assets( 74 | os.path.join(dir_path, '%s.png' % text_assets), 75 | os.path.join(dir_path, '%s.txt' % text_assets) 76 | ) 77 | 78 | # Load IMU settings and calibration data 79 | self._imu_settings = self._get_settings_file(imu_settings_file) 80 | self._imu = RTIMU.RTIMU(self._imu_settings) 81 | self._imu_init = False # Will be initialised as and when needed 82 | self._pressure = RTIMU.RTPressure(self._imu_settings) 83 | self._pressure_init = False # Will be initialised as and when needed 84 | self._humidity = RTIMU.RTHumidity(self._imu_settings) 85 | self._humidity_init = False # Will be initialised as and when needed 86 | self._last_orientation = {'pitch': 0, 'roll': 0, 'yaw': 0} 87 | raw = {'x': 0, 'y': 0, 'z': 0} 88 | self._last_compass_raw = deepcopy(raw) 89 | self._last_gyro_raw = deepcopy(raw) 90 | self._last_accel_raw = deepcopy(raw) 91 | self._compass_enabled = False 92 | self._gyro_enabled = False 93 | self._accel_enabled = False 94 | self._stick = SenseStick() 95 | 96 | # initialise the TCS34725 colour sensor (if possible) 97 | try: 98 | self._colour = ColourSensor() 99 | except Exception as e: 100 | logging.debug(e) 101 | pass 102 | 103 | #### 104 | # Text assets 105 | #### 106 | 107 | # Text asset files are rotated right through 90 degrees to allow blocks of 108 | # 40 contiguous pixels to represent one 5 x 8 character. These are stored 109 | # in a 8 x 640 pixel png image with characters arranged adjacently 110 | # Consequently we must rotate the pixel map left through 90 degrees to 111 | # compensate when drawing text 112 | 113 | def _load_text_assets(self, text_image_file, text_file): 114 | """ 115 | Internal. Builds a character indexed dictionary of pixels used by the 116 | show_message function below 117 | """ 118 | 119 | text_pixels = self.load_image(text_image_file, False) 120 | with open(text_file, 'r') as f: 121 | loaded_text = f.read() 122 | self._text_dict = {} 123 | for index, s in enumerate(loaded_text): 124 | start = index * 40 125 | end = start + 40 126 | char = text_pixels[start:end] 127 | self._text_dict[s] = char 128 | 129 | def _trim_whitespace(self, char): # For loading text assets only 130 | """ 131 | Internal. Trims white space pixels from the front and back of loaded 132 | text characters 133 | """ 134 | 135 | psum = lambda x: sum(sum(x, [])) 136 | if psum(char) > 0: 137 | is_empty = True 138 | while is_empty: # From front 139 | row = char[0:8] 140 | is_empty = psum(row) == 0 141 | if is_empty: 142 | del char[0:8] 143 | is_empty = True 144 | while is_empty: # From back 145 | row = char[-8:] 146 | is_empty = psum(row) == 0 147 | if is_empty: 148 | del char[-8:] 149 | return char 150 | 151 | def _get_settings_file(self, imu_settings_file): 152 | """ 153 | Internal. Logic to check for a system wide RTIMU ini file. This is 154 | copied to the home folder if one is not already found there. 155 | """ 156 | 157 | ini_file = '%s.ini' % imu_settings_file 158 | 159 | home_dir = pwd.getpwuid(os.getuid())[5] 160 | home_path = os.path.join(home_dir, self.SETTINGS_HOME_PATH) 161 | if not os.path.exists(home_path): 162 | os.makedirs(home_path) 163 | 164 | home_file = os.path.join(home_path, ini_file) 165 | home_exists = os.path.isfile(home_file) 166 | system_file = os.path.join('/etc', ini_file) 167 | system_exists = os.path.isfile(system_file) 168 | 169 | if system_exists and not home_exists: 170 | shutil.copyfile(system_file, home_file) 171 | 172 | return RTIMU.Settings(os.path.join(home_path, imu_settings_file)) # RTIMU will add .ini internally 173 | 174 | def _get_fb_device(self): 175 | """ 176 | Internal. Finds the correct frame buffer device for the sense HAT 177 | and returns its /dev name. 178 | """ 179 | 180 | device = None 181 | 182 | for fb in glob.glob('/sys/class/graphics/fb*'): 183 | name_file = os.path.join(fb, 'name') 184 | if os.path.isfile(name_file): 185 | with open(name_file, 'r') as f: 186 | name = f.read() 187 | if name.strip() == self.SENSE_HAT_FB_NAME: 188 | fb_device = fb.replace(os.path.dirname(fb), '/dev') 189 | if os.path.exists(fb_device): 190 | device = fb_device 191 | break 192 | 193 | return device 194 | 195 | #### 196 | # Joystick 197 | #### 198 | 199 | @property 200 | def stick(self): 201 | return self._stick 202 | 203 | #### 204 | # Colour sensor 205 | #### 206 | 207 | @property 208 | def colour(self): 209 | try: 210 | return self._colour 211 | except AttributeError: 212 | print('This Sense Hat does not have a colour sensor') 213 | 214 | color = colour 215 | 216 | def has_colour_sensor(self): 217 | try: 218 | self._colour 219 | except: 220 | return False 221 | else: 222 | return True 223 | 224 | #### 225 | # LED Matrix 226 | #### 227 | 228 | @property 229 | def rotation(self): 230 | return self._rotation 231 | 232 | @rotation.setter 233 | def rotation(self, r): 234 | self.set_rotation(r, True) 235 | 236 | def set_rotation(self, r=0, redraw=True): 237 | """ 238 | Sets the LED matrix rotation for viewing, adjust if the Pi is upside 239 | down or sideways. 0 is with the Pi HDMI port facing downwards 240 | """ 241 | 242 | if r in self._pix_map.keys(): 243 | if redraw: 244 | pixel_list = self.get_pixels() 245 | self._rotation = r 246 | if redraw: 247 | self.set_pixels(pixel_list) 248 | else: 249 | raise ValueError('Rotation must be 0, 90, 180 or 270 degrees') 250 | 251 | def _pack_bin(self, pix): 252 | """ 253 | Internal. Encodes python list [R,G,B] into 16 bit RGB565 254 | """ 255 | 256 | r = (pix[0] >> 3) & 0x1F 257 | g = (pix[1] >> 2) & 0x3F 258 | b = (pix[2] >> 3) & 0x1F 259 | bits16 = (r << 11) + (g << 5) + b 260 | return struct.pack('H', bits16) 261 | 262 | def _unpack_bin(self, packed): 263 | """ 264 | Internal. Decodes 16 bit RGB565 into python list [R,G,B] 265 | """ 266 | 267 | output = struct.unpack('H', packed) 268 | bits16 = output[0] 269 | r = (bits16 & 0xF800) >> 11 270 | g = (bits16 & 0x7E0) >> 5 271 | b = (bits16 & 0x1F) 272 | return [int(r << 3), int(g << 2), int(b << 3)] 273 | 274 | def flip_h(self, redraw=True): 275 | """ 276 | Flip LED matrix horizontal 277 | """ 278 | 279 | pixel_list = self.get_pixels() 280 | flipped = [] 281 | for i in range(8): 282 | offset = i * 8 283 | flipped.extend(reversed(pixel_list[offset:offset + 8])) 284 | if redraw: 285 | self.set_pixels(flipped) 286 | return flipped 287 | 288 | def flip_v(self, redraw=True): 289 | """ 290 | Flip LED matrix vertical 291 | """ 292 | 293 | pixel_list = self.get_pixels() 294 | flipped = [] 295 | for i in reversed(range(8)): 296 | offset = i * 8 297 | flipped.extend(pixel_list[offset:offset + 8]) 298 | if redraw: 299 | self.set_pixels(flipped) 300 | return flipped 301 | 302 | def set_pixels(self, pixel_list): 303 | """ 304 | Accepts a list containing 64 smaller lists of [R,G,B] pixels and 305 | updates the LED matrix. R,G,B elements must integers between 0 306 | and 255 307 | """ 308 | 309 | if len(pixel_list) != 64: 310 | raise ValueError('Pixel lists must have 64 elements') 311 | 312 | for index, pix in enumerate(pixel_list): 313 | if len(pix) != 3: 314 | raise ValueError('Pixel at index %d is invalid. Pixels must contain 3 elements: Red, Green and Blue' % index) 315 | 316 | for element in pix: 317 | if element > 255 or element < 0: 318 | raise ValueError('Pixel at index %d is invalid. Pixel elements must be between 0 and 255' % index) 319 | 320 | with open(self._fb_device, 'wb') as f: 321 | map = self._pix_map[self._rotation] 322 | for index, pix in enumerate(pixel_list): 323 | # Two bytes per pixel in fb memory, 16 bit RGB565 324 | f.seek(map[index // 8][index % 8] * 2) # row, column 325 | f.write(self._pack_bin(pix)) 326 | 327 | def get_pixels(self): 328 | """ 329 | Returns a list containing 64 smaller lists of [R,G,B] pixels 330 | representing what is currently displayed on the LED matrix 331 | """ 332 | 333 | pixel_list = [] 334 | with open(self._fb_device, 'rb') as f: 335 | map = self._pix_map[self._rotation] 336 | for row in range(8): 337 | for col in range(8): 338 | # Two bytes per pixel in fb memory, 16 bit RGB565 339 | f.seek(map[row][col] * 2) # row, column 340 | pixel_list.append(self._unpack_bin(f.read(2))) 341 | return pixel_list 342 | 343 | def set_pixel(self, x, y, *args): 344 | """ 345 | Updates the single [R,G,B] pixel specified by x and y on the LED matrix 346 | Top left = 0,0 Bottom right = 7,7 347 | 348 | e.g. ap.set_pixel(x, y, r, g, b) 349 | or 350 | pixel = (r, g, b) 351 | ap.set_pixel(x, y, pixel) 352 | """ 353 | 354 | pixel_error = 'Pixel arguments must be given as (r, g, b) or r, g, b' 355 | 356 | if len(args) == 1: 357 | pixel = args[0] 358 | if len(pixel) != 3: 359 | raise ValueError(pixel_error) 360 | elif len(args) == 3: 361 | pixel = args 362 | else: 363 | raise ValueError(pixel_error) 364 | 365 | if x > 7 or x < 0: 366 | raise ValueError('X position must be between 0 and 7') 367 | 368 | if y > 7 or y < 0: 369 | raise ValueError('Y position must be between 0 and 7') 370 | 371 | for element in pixel: 372 | if element > 255 or element < 0: 373 | raise ValueError('Pixel elements must be between 0 and 255') 374 | 375 | with open(self._fb_device, 'wb') as f: 376 | map = self._pix_map[self._rotation] 377 | # Two bytes per pixel in fb memory, 16 bit RGB565 378 | f.seek(map[y][x] * 2) # row, column 379 | f.write(self._pack_bin(pixel)) 380 | 381 | def get_pixel(self, x, y): 382 | """ 383 | Returns a list of [R,G,B] representing the pixel specified by x and y 384 | on the LED matrix. Top left = 0,0 Bottom right = 7,7 385 | """ 386 | 387 | if x > 7 or x < 0: 388 | raise ValueError('X position must be between 0 and 7') 389 | 390 | if y > 7 or y < 0: 391 | raise ValueError('Y position must be between 0 and 7') 392 | 393 | pix = None 394 | 395 | with open(self._fb_device, 'rb') as f: 396 | map = self._pix_map[self._rotation] 397 | # Two bytes per pixel in fb memory, 16 bit RGB565 398 | f.seek(map[y][x] * 2) # row, column 399 | pix = self._unpack_bin(f.read(2)) 400 | 401 | return pix 402 | 403 | def load_image(self, file_path, redraw=True): 404 | """ 405 | Accepts a path to an 8 x 8 image file and updates the LED matrix with 406 | the image 407 | """ 408 | 409 | if not os.path.exists(file_path): 410 | raise IOError('%s not found' % file_path) 411 | 412 | img = Image.open(file_path).convert('RGB') 413 | pixel_list = list(map(list, img.getdata())) 414 | 415 | if redraw: 416 | self.set_pixels(pixel_list) 417 | 418 | return pixel_list 419 | 420 | def clear(self, *args): 421 | """ 422 | Clears the LED matrix with a single colour, default is black / off 423 | 424 | e.g. ap.clear() 425 | or 426 | ap.clear(r, g, b) 427 | or 428 | colour = (r, g, b) 429 | ap.clear(colour) 430 | """ 431 | 432 | black = (0, 0, 0) # default 433 | 434 | if len(args) == 0: 435 | colour = black 436 | elif len(args) == 1: 437 | colour = args[0] 438 | elif len(args) == 3: 439 | colour = args 440 | else: 441 | raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') 442 | 443 | self.set_pixels([colour] * 64) 444 | 445 | def _get_char_pixels(self, s): 446 | """ 447 | Internal. Safeguards the character indexed dictionary for the 448 | show_message function below 449 | """ 450 | 451 | if len(s) == 1 and s in self._text_dict.keys(): 452 | return list(self._text_dict[s]) 453 | else: 454 | return list(self._text_dict['?']) 455 | 456 | def show_message( 457 | self, 458 | text_string, 459 | scroll_speed=.1, 460 | text_colour=[255, 255, 255], 461 | back_colour=[0, 0, 0] 462 | ): 463 | """ 464 | Scrolls a string of text across the LED matrix using the specified 465 | speed and colours 466 | """ 467 | 468 | # We must rotate the pixel map left through 90 degrees when drawing 469 | # text, see _load_text_assets 470 | previous_rotation = self._rotation 471 | self._rotation -= 90 472 | if self._rotation < 0: 473 | self._rotation = 270 474 | dummy_colour = [None, None, None] 475 | string_padding = [dummy_colour] * 64 476 | letter_padding = [dummy_colour] * 8 477 | # Build pixels from dictionary 478 | scroll_pixels = [] 479 | scroll_pixels.extend(string_padding) 480 | for s in text_string: 481 | scroll_pixels.extend(self._trim_whitespace(self._get_char_pixels(s))) 482 | scroll_pixels.extend(letter_padding) 483 | scroll_pixels.extend(string_padding) 484 | # Recolour pixels as necessary 485 | coloured_pixels = [ 486 | text_colour if pixel == [255, 255, 255] else back_colour 487 | for pixel in scroll_pixels 488 | ] 489 | # Shift right by 8 pixels per frame to scroll 490 | scroll_length = len(coloured_pixels) // 8 491 | for i in range(scroll_length - 8): 492 | start = i * 8 493 | end = start + 64 494 | self.set_pixels(coloured_pixels[start:end]) 495 | time.sleep(scroll_speed) 496 | self._rotation = previous_rotation 497 | 498 | def show_letter( 499 | self, 500 | s, 501 | text_colour=[255, 255, 255], 502 | back_colour=[0, 0, 0] 503 | ): 504 | """ 505 | Displays a single text character on the LED matrix using the specified 506 | colours 507 | """ 508 | 509 | if len(s) > 1: 510 | raise ValueError('Only one character may be passed into this method') 511 | # We must rotate the pixel map left through 90 degrees when drawing 512 | # text, see _load_text_assets 513 | previous_rotation = self._rotation 514 | self._rotation -= 90 515 | if self._rotation < 0: 516 | self._rotation = 270 517 | dummy_colour = [None, None, None] 518 | pixel_list = [dummy_colour] * 8 519 | pixel_list.extend(self._get_char_pixels(s)) 520 | pixel_list.extend([dummy_colour] * 16) 521 | coloured_pixels = [ 522 | text_colour if pixel == [255, 255, 255] else back_colour 523 | for pixel in pixel_list 524 | ] 525 | self.set_pixels(coloured_pixels) 526 | self._rotation = previous_rotation 527 | 528 | @property 529 | def gamma(self): 530 | buffer = array.array('B', [0]*32) 531 | with open(self._fb_device) as f: 532 | fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOGET_GAMMA, buffer) 533 | return list(buffer) 534 | 535 | @gamma.setter 536 | def gamma(self, buffer): 537 | if len(buffer) != 32: 538 | raise ValueError('Gamma array must be of length 32') 539 | 540 | if not all(b <= 31 for b in buffer): 541 | raise ValueError('Gamma values must be bewteen 0 and 31') 542 | 543 | if not isinstance(buffer, array.array): 544 | buffer = array.array('B', buffer) 545 | 546 | with open(self._fb_device) as f: 547 | fcntl.ioctl(f, self.SENSE_HAT_FB_FBIOSET_GAMMA, buffer) 548 | 549 | def gamma_reset(self): 550 | """ 551 | Resets the LED matrix gamma correction to default 552 | """ 553 | 554 | with open(self._fb_device) as f: 555 | fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, self.SENSE_HAT_FB_GAMMA_DEFAULT) 556 | 557 | @property 558 | def low_light(self): 559 | 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] 560 | 561 | @low_light.setter 562 | def low_light(self, value): 563 | with open(self._fb_device) as f: 564 | cmd = self.SENSE_HAT_FB_GAMMA_LOW if value else self.SENSE_HAT_FB_GAMMA_DEFAULT 565 | fcntl.ioctl(f, self.SENSE_HAT_FB_FBIORESET_GAMMA, cmd) 566 | 567 | #### 568 | # Environmental sensors 569 | #### 570 | 571 | def _init_humidity(self): 572 | """ 573 | Internal. Initialises the humidity sensor via RTIMU 574 | """ 575 | 576 | if not self._humidity_init: 577 | self._humidity_init = self._humidity.humidityInit() 578 | if not self._humidity_init: 579 | raise OSError('Humidity Init Failed') 580 | 581 | def _init_pressure(self): 582 | """ 583 | Internal. Initialises the pressure sensor via RTIMU 584 | """ 585 | 586 | if not self._pressure_init: 587 | self._pressure_init = self._pressure.pressureInit() 588 | if not self._pressure_init: 589 | raise OSError('Pressure Init Failed') 590 | 591 | def get_humidity(self): 592 | """ 593 | Returns the percentage of relative humidity 594 | """ 595 | 596 | self._init_humidity() # Ensure humidity sensor is initialised 597 | humidity = 0 598 | data = self._humidity.humidityRead() 599 | if (data[0]): # Humidity valid 600 | humidity = data[1] 601 | return humidity 602 | 603 | @property 604 | def humidity(self): 605 | return self.get_humidity() 606 | 607 | def get_temperature_from_humidity(self): 608 | """ 609 | Returns the temperature in Celsius from the humidity sensor 610 | """ 611 | 612 | self._init_humidity() # Ensure humidity sensor is initialised 613 | temp = 0 614 | data = self._humidity.humidityRead() 615 | if (data[2]): # Temp valid 616 | temp = data[3] 617 | return temp 618 | 619 | def get_temperature_from_pressure(self): 620 | """ 621 | Returns the temperature in Celsius from the pressure sensor 622 | """ 623 | 624 | self._init_pressure() # Ensure pressure sensor is initialised 625 | temp = 0 626 | data = self._pressure.pressureRead() 627 | if (data[2]): # Temp valid 628 | temp = data[3] 629 | return temp 630 | 631 | def get_temperature(self): 632 | """ 633 | Returns the temperature in Celsius 634 | """ 635 | 636 | return self.get_temperature_from_humidity() 637 | 638 | @property 639 | def temp(self): 640 | return self.get_temperature_from_humidity() 641 | 642 | @property 643 | def temperature(self): 644 | return self.get_temperature_from_humidity() 645 | 646 | def get_pressure(self): 647 | """ 648 | Returns the pressure in Millibars 649 | """ 650 | 651 | self._init_pressure() # Ensure pressure sensor is initialised 652 | pressure = 0 653 | data = self._pressure.pressureRead() 654 | if (data[0]): # Pressure valid 655 | pressure = data[1] 656 | return pressure 657 | 658 | @property 659 | def pressure(self): 660 | return self.get_pressure() 661 | 662 | #### 663 | # IMU Sensor 664 | #### 665 | 666 | def _init_imu(self): 667 | """ 668 | Internal. Initialises the IMU sensor via RTIMU 669 | """ 670 | 671 | if not self._imu_init: 672 | self._imu_init = self._imu.IMUInit() 673 | if self._imu_init: 674 | self._imu_poll_interval = self._imu.IMUGetPollInterval() * 0.001 675 | # Enable everything on IMU 676 | self.set_imu_config(True, True, True) 677 | else: 678 | raise OSError('IMU Init Failed') 679 | 680 | def set_imu_config(self, compass_enabled, gyro_enabled, accel_enabled): 681 | """ 682 | Enables and disables the gyroscope, accelerometer and/or magnetometer 683 | input to the orientation functions 684 | """ 685 | 686 | # If the consuming code always calls this just before reading the IMU 687 | # the IMU consistently fails to read. So prevent unnecessary calls to 688 | # IMU config functions using state variables 689 | 690 | self._init_imu() # Ensure imu is initialised 691 | 692 | if (not isinstance(compass_enabled, bool) 693 | or not isinstance(gyro_enabled, bool) 694 | or not isinstance(accel_enabled, bool)): 695 | raise TypeError('All set_imu_config parameters must be of boolean type') 696 | 697 | if self._compass_enabled != compass_enabled: 698 | self._compass_enabled = compass_enabled 699 | self._imu.setCompassEnable(self._compass_enabled) 700 | 701 | if self._gyro_enabled != gyro_enabled: 702 | self._gyro_enabled = gyro_enabled 703 | self._imu.setGyroEnable(self._gyro_enabled) 704 | 705 | if self._accel_enabled != accel_enabled: 706 | self._accel_enabled = accel_enabled 707 | self._imu.setAccelEnable(self._accel_enabled) 708 | 709 | def _read_imu(self): 710 | """ 711 | Internal. Tries to read the IMU sensor three times before giving up 712 | """ 713 | 714 | self._init_imu() # Ensure imu is initialised 715 | 716 | attempts = 0 717 | success = False 718 | 719 | while not success and attempts < 3: 720 | success = self._imu.IMURead() 721 | attempts += 1 722 | time.sleep(self._imu_poll_interval) 723 | 724 | return success 725 | 726 | def _get_raw_data(self, is_valid_key, data_key): 727 | """ 728 | Internal. Returns the specified raw data from the IMU when valid 729 | """ 730 | 731 | result = None 732 | 733 | if self._read_imu(): 734 | data = self._imu.getIMUData() 735 | if data[is_valid_key]: 736 | raw = data[data_key] 737 | result = { 738 | 'x': raw[0], 739 | 'y': raw[1], 740 | 'z': raw[2] 741 | } 742 | 743 | return result 744 | 745 | def get_orientation_radians(self): 746 | """ 747 | Returns a dictionary object to represent the current orientation in 748 | radians using the aircraft principal axes of pitch, roll and yaw 749 | """ 750 | 751 | raw = self._get_raw_data('fusionPoseValid', 'fusionPose') 752 | 753 | if raw is not None: 754 | raw['roll'] = raw.pop('x') 755 | raw['pitch'] = raw.pop('y') 756 | raw['yaw'] = raw.pop('z') 757 | self._last_orientation = raw 758 | 759 | return deepcopy(self._last_orientation) 760 | 761 | @property 762 | def orientation_radians(self): 763 | return self.get_orientation_radians() 764 | 765 | def get_orientation_degrees(self): 766 | """ 767 | Returns a dictionary object to represent the current orientation 768 | in degrees, 0 to 360, using the aircraft principal axes of 769 | pitch, roll and yaw 770 | """ 771 | 772 | orientation = self.get_orientation_radians() 773 | for key, val in orientation.items(): 774 | deg = math.degrees(val) # Result is -180 to +180 775 | orientation[key] = deg + 360 if deg < 0 else deg 776 | return orientation 777 | 778 | def get_orientation(self): 779 | return self.get_orientation_degrees() 780 | 781 | @property 782 | def orientation(self): 783 | return self.get_orientation_degrees() 784 | 785 | def get_compass(self): 786 | """ 787 | Gets the direction of North from the magnetometer in degrees 788 | """ 789 | 790 | self.set_imu_config(True, False, False) 791 | orientation = self.get_orientation_degrees() 792 | if type(orientation) is dict and 'yaw' in orientation.keys(): 793 | return orientation['yaw'] 794 | else: 795 | return None 796 | 797 | @property 798 | def compass(self): 799 | return self.get_compass() 800 | 801 | def get_compass_raw(self): 802 | """ 803 | Magnetometer x y z raw data in uT (micro teslas) 804 | """ 805 | 806 | raw = self._get_raw_data('compassValid', 'compass') 807 | 808 | if raw is not None: 809 | self._last_compass_raw = raw 810 | 811 | return deepcopy(self._last_compass_raw) 812 | 813 | @property 814 | def compass_raw(self): 815 | return self.get_compass_raw() 816 | 817 | def get_gyroscope(self): 818 | """ 819 | Gets the orientation in degrees from the gyroscope only 820 | """ 821 | 822 | self.set_imu_config(False, True, False) 823 | return self.get_orientation_degrees() 824 | 825 | @property 826 | def gyro(self): 827 | return self.get_gyroscope() 828 | 829 | @property 830 | def gyroscope(self): 831 | return self.get_gyroscope() 832 | 833 | def get_gyroscope_raw(self): 834 | """ 835 | Gyroscope x y z raw data in radians per second 836 | """ 837 | 838 | raw = self._get_raw_data('gyroValid', 'gyro') 839 | 840 | if raw is not None: 841 | self._last_gyro_raw = raw 842 | 843 | return deepcopy(self._last_gyro_raw) 844 | 845 | @property 846 | def gyro_raw(self): 847 | return self.get_gyroscope_raw() 848 | 849 | @property 850 | def gyroscope_raw(self): 851 | return self.get_gyroscope_raw() 852 | 853 | def get_accelerometer(self): 854 | """ 855 | Gets the orientation in degrees from the accelerometer only 856 | """ 857 | 858 | self.set_imu_config(False, False, True) 859 | return self.get_orientation_degrees() 860 | 861 | @property 862 | def accel(self): 863 | return self.get_accelerometer() 864 | 865 | @property 866 | def accelerometer(self): 867 | return self.get_accelerometer() 868 | 869 | def get_accelerometer_raw(self): 870 | """ 871 | Accelerometer x y z raw data in Gs 872 | """ 873 | 874 | raw = self._get_raw_data('accelValid', 'accel') 875 | 876 | if raw is not None: 877 | self._last_accel_raw = raw 878 | 879 | return deepcopy(self._last_accel_raw) 880 | 881 | @property 882 | def accel_raw(self): 883 | return self.get_accelerometer_raw() 884 | 885 | @property 886 | def accelerometer_raw(self): 887 | return self.get_accelerometer_raw() 888 | -------------------------------------------------------------------------------- /sense_hat/sense_hat_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astro-pi/python-sense-hat/d2942773a5c740a4cf23211d6bc9a3a695439e25/sense_hat/sense_hat_text.png -------------------------------------------------------------------------------- /sense_hat/sense_hat_text.txt: -------------------------------------------------------------------------------- 1 | +-*/!"#$><0123456789.=)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?,;:|@%[&_']\~ 2 | -------------------------------------------------------------------------------- /sense_hat/stick.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | unicode_literals, 3 | absolute_import, 4 | print_function, 5 | division, 6 | ) 7 | native_str = str 8 | str = type('') 9 | 10 | import io 11 | import os 12 | import glob 13 | import errno 14 | import struct 15 | import select 16 | import inspect 17 | from functools import wraps 18 | from collections import namedtuple 19 | from threading import Thread, Event 20 | 21 | 22 | DIRECTION_UP = 'up' 23 | DIRECTION_DOWN = 'down' 24 | DIRECTION_LEFT = 'left' 25 | DIRECTION_RIGHT = 'right' 26 | DIRECTION_MIDDLE = 'middle' 27 | 28 | ACTION_PRESSED = 'pressed' 29 | ACTION_RELEASED = 'released' 30 | ACTION_HELD = 'held' 31 | 32 | 33 | InputEvent = namedtuple('InputEvent', ('timestamp', 'direction', 'action')) 34 | 35 | 36 | class SenseStick(object): 37 | """ 38 | Represents the joystick on the Sense HAT. 39 | """ 40 | SENSE_HAT_EVDEV_NAME = 'Raspberry Pi Sense HAT Joystick' 41 | EVENT_FORMAT = native_str('llHHI') 42 | EVENT_SIZE = struct.calcsize(EVENT_FORMAT) 43 | 44 | EV_KEY = 0x01 45 | 46 | STATE_RELEASE = 0 47 | STATE_PRESS = 1 48 | STATE_HOLD = 2 49 | 50 | KEY_UP = 103 51 | KEY_LEFT = 105 52 | KEY_RIGHT = 106 53 | KEY_DOWN = 108 54 | KEY_ENTER = 28 55 | 56 | def __init__(self): 57 | self._stick_file = io.open(self._stick_device(), 'rb', buffering=0) 58 | self._callbacks = {} 59 | self._callback_thread = None 60 | self._callback_event = Event() 61 | 62 | def close(self): 63 | if self._stick_file: 64 | self._callbacks.clear() 65 | self._start_stop_thread() 66 | self._stick_file.close() 67 | self._stick_file = None 68 | 69 | def __enter__(self): 70 | return self 71 | 72 | def __exit__(self, exc_type, exc_value, exc_tb): 73 | self.close() 74 | 75 | def _stick_device(self): 76 | """ 77 | Discovers the filename of the evdev device that represents the Sense 78 | HAT's joystick. 79 | """ 80 | for evdev in glob.glob('/sys/class/input/event*'): 81 | try: 82 | with io.open(os.path.join(evdev, 'device', 'name'), 'r') as f: 83 | if f.read().strip() == self.SENSE_HAT_EVDEV_NAME: 84 | return os.path.join('/dev', 'input', os.path.basename(evdev)) 85 | except IOError as e: 86 | if e.errno != errno.ENOENT: 87 | raise 88 | raise RuntimeError('unable to locate SenseHAT joystick device') 89 | 90 | def _read(self): 91 | """ 92 | Reads a single event from the joystick, blocking until one is 93 | available. Returns `None` if a non-key event was read, or an 94 | `InputEvent` tuple describing the event otherwise. 95 | """ 96 | event = self._stick_file.read(self.EVENT_SIZE) 97 | (tv_sec, tv_usec, type, code, value) = struct.unpack(self.EVENT_FORMAT, event) 98 | if type == self.EV_KEY: 99 | return InputEvent( 100 | timestamp=tv_sec + (tv_usec / 1000000), 101 | direction={ 102 | self.KEY_UP: DIRECTION_UP, 103 | self.KEY_DOWN: DIRECTION_DOWN, 104 | self.KEY_LEFT: DIRECTION_LEFT, 105 | self.KEY_RIGHT: DIRECTION_RIGHT, 106 | self.KEY_ENTER: DIRECTION_MIDDLE, 107 | }[code], 108 | action={ 109 | self.STATE_PRESS: ACTION_PRESSED, 110 | self.STATE_RELEASE: ACTION_RELEASED, 111 | self.STATE_HOLD: ACTION_HELD, 112 | }[value]) 113 | else: 114 | return None 115 | 116 | def _wait(self, timeout=None): 117 | """ 118 | Waits *timeout* seconds until an event is available from the 119 | joystick. Returns `True` if an event became available, and `False` 120 | if the timeout expired. 121 | """ 122 | r, w, x = select.select([self._stick_file], [], [], timeout) 123 | return bool(r) 124 | 125 | def _wrap_callback(self, fn): 126 | # Shamelessley nicked (with some variation) from GPIO Zero :) 127 | @wraps(fn) 128 | def wrapper(event): 129 | return fn() 130 | 131 | if fn is None: 132 | return None 133 | elif not callable(fn): 134 | raise ValueError('value must be None or a callable') 135 | elif inspect.isbuiltin(fn): 136 | # We can't introspect the prototype of builtins. In this case we 137 | # assume that the builtin has no (mandatory) parameters; this is 138 | # the most reasonable assumption on the basis that pre-existing 139 | # builtins have no knowledge of InputEvent, and the sole parameter 140 | # we would pass is an InputEvent 141 | return wrapper 142 | else: 143 | # Try binding ourselves to the argspec of the provided callable. 144 | # If this works, assume the function is capable of accepting no 145 | # parameters and that we have to wrap it to ignore the event 146 | # parameter 147 | try: 148 | inspect.getcallargs(fn) 149 | return wrapper 150 | except TypeError: 151 | try: 152 | # If the above fails, try binding with a single tuple 153 | # parameter. If this works, return the callback as is 154 | inspect.getcallargs(fn, ()) 155 | return fn 156 | except TypeError: 157 | raise ValueError( 158 | 'value must be a callable which accepts up to one ' 159 | 'mandatory parameter') 160 | 161 | def _start_stop_thread(self): 162 | if self._callbacks and not self._callback_thread: 163 | self._callback_event.clear() 164 | self._callback_thread = Thread(target=self._callback_run) 165 | self._callback_thread.daemon = True 166 | self._callback_thread.start() 167 | elif not self._callbacks and self._callback_thread: 168 | self._callback_event.set() 169 | self._callback_thread.join() 170 | self._callback_thread = None 171 | 172 | def _callback_run(self): 173 | while not self._callback_event.wait(0): 174 | event = self._read() 175 | if event: 176 | callback = self._callbacks.get(event.direction) 177 | if callback: 178 | callback(event) 179 | callback = self._callbacks.get('*') 180 | if callback: 181 | callback(event) 182 | 183 | def wait_for_event(self, emptybuffer=False): 184 | """ 185 | Waits until a joystick event becomes available. Returns the event, as 186 | an `InputEvent` tuple. 187 | 188 | If *emptybuffer* is `True` (it defaults to `False`), any pending 189 | events will be thrown away first. This is most useful if you are only 190 | interested in "pressed" events. 191 | """ 192 | if emptybuffer: 193 | while self._wait(0): 194 | self._read() 195 | while self._wait(): 196 | event = self._read() 197 | if event: 198 | return event 199 | 200 | def get_events(self): 201 | """ 202 | Returns a list of all joystick events that have occurred since the last 203 | call to `get_events`. The list contains events in the order that they 204 | occurred. If no events have occurred in the intervening time, the 205 | result is an empty list. 206 | """ 207 | result = [] 208 | while self._wait(0): 209 | event = self._read() 210 | if event: 211 | result.append(event) 212 | return result 213 | 214 | @property 215 | def direction_up(self): 216 | """ 217 | The function to be called when the joystick is pushed up. The function 218 | can either take a parameter which will be the `InputEvent` tuple that 219 | has occurred, or the function can take no parameters at all. 220 | """ 221 | return self._callbacks.get(DIRECTION_UP) 222 | 223 | @direction_up.setter 224 | def direction_up(self, value): 225 | self._callbacks[DIRECTION_UP] = self._wrap_callback(value) 226 | self._start_stop_thread() 227 | 228 | @property 229 | def direction_down(self): 230 | """ 231 | The function to be called when the joystick is pushed down. The 232 | function can either take a parameter which will be the `InputEvent` 233 | tuple that has occurred, or the function can take no parameters at all. 234 | 235 | Assign `None` to prevent this event from being fired. 236 | """ 237 | return self._callbacks.get(DIRECTION_DOWN) 238 | 239 | @direction_down.setter 240 | def direction_down(self, value): 241 | self._callbacks[DIRECTION_DOWN] = self._wrap_callback(value) 242 | self._start_stop_thread() 243 | 244 | @property 245 | def direction_left(self): 246 | """ 247 | The function to be called when the joystick is pushed left. The 248 | function can either take a parameter which will be the `InputEvent` 249 | tuple that has occurred, or the function can take no parameters at all. 250 | 251 | Assign `None` to prevent this event from being fired. 252 | """ 253 | return self._callbacks.get(DIRECTION_LEFT) 254 | 255 | @direction_left.setter 256 | def direction_left(self, value): 257 | self._callbacks[DIRECTION_LEFT] = self._wrap_callback(value) 258 | self._start_stop_thread() 259 | 260 | @property 261 | def direction_right(self): 262 | """ 263 | The function to be called when the joystick is pushed right. The 264 | function can either take a parameter which will be the `InputEvent` 265 | tuple that has occurred, or the function can take no parameters at all. 266 | 267 | Assign `None` to prevent this event from being fired. 268 | """ 269 | return self._callbacks.get(DIRECTION_RIGHT) 270 | 271 | @direction_right.setter 272 | def direction_right(self, value): 273 | self._callbacks[DIRECTION_RIGHT] = self._wrap_callback(value) 274 | self._start_stop_thread() 275 | 276 | @property 277 | def direction_middle(self): 278 | """ 279 | The function to be called when the joystick middle click is pressed. The 280 | function can either take a parameter which will be the `InputEvent` tuple 281 | that has occurred, or the function can take no parameters at all. 282 | 283 | Assign `None` to prevent this event from being fired. 284 | """ 285 | return self._callbacks.get(DIRECTION_MIDDLE) 286 | 287 | @direction_middle.setter 288 | def direction_middle(self, value): 289 | self._callbacks[DIRECTION_MIDDLE] = self._wrap_callback(value) 290 | self._start_stop_thread() 291 | 292 | @property 293 | def direction_any(self): 294 | """ 295 | The function to be called when the joystick is used. The function 296 | can either take a parameter which will be the `InputEvent` tuple that 297 | has occurred, or the function can take no parameters at all. 298 | 299 | This event will always be called *after* events associated with a 300 | specific action. Assign `None` to prevent this event from being fired. 301 | """ 302 | return self._callbacks.get('*') 303 | 304 | @direction_any.setter 305 | def direction_any(self, value): 306 | self._callbacks['*'] = self._wrap_callback(value) 307 | self._start_stop_thread() 308 | 309 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | setup( 9 | name="sense-hat", 10 | version="2.6.0", 11 | author="Dave Honess", 12 | author_email="dave@raspberrypi.org", 13 | description="Python module to control the Raspberry Pi Sense HAT, originally used in the Astro Pi mission", 14 | long_description=read('README.md'), 15 | long_description_content_type="text/markdown", 16 | license="BSD", 17 | keywords=[ 18 | "sense hat", 19 | "raspberrypi", 20 | "astro pi", 21 | ], 22 | url="https://github.com/astro-pi/python-sense-hat", 23 | packages=find_packages(), 24 | package_data={ 25 | "txt": ['sense_hat_text.txt'], 26 | "png": ['sense_hat_text.png'] 27 | }, 28 | include_package_data=True, 29 | install_requires=[ 30 | "pillow", 31 | "numpy" 32 | ], 33 | classifiers=[ 34 | "Development Status :: 5 - Production/Stable", 35 | "Programming Language :: Python :: 2", 36 | "Programming Language :: Python :: 3", 37 | "Topic :: Scientific/Engineering :: Astronomy", 38 | "Topic :: Scientific/Engineering :: Atmospheric Science", 39 | "Topic :: Education", 40 | "Intended Audience :: Education", 41 | "Intended Audience :: Science/Research", 42 | "License :: OSI Approved :: BSD License", 43 | ], 44 | ) 45 | --------------------------------------------------------------------------------