├── README.md ├── community-documentation └── raspberry_pi_os_lite_install.md ├── community-scripts ├── Lumicube Interface │ ├── LumicubeInterface.py │ └── example │ │ └── example.py ├── Resistor clock.py ├── cube_runner.py ├── cylon.py ├── digital_clock_v1.py ├── digital_rain.py ├── display_helpers.py ├── game_of_life_clock.py ├── game_of_life_phased.py ├── gameday.py ├── glitter.py ├── google_api.py ├── identify_panels v2.py ├── identify_panels.py ├── iss-tracker-lumi.py ├── minecraft_pic.py ├── minecraft_random_pic.py ├── mlb.py ├── nest_thermostat.py ├── pacman.py ├── vesuvius.py ├── weather.py └── yagol.py ├── examples ├── autumn_scene.py ├── binary_clock.py ├── button.py ├── chiptune.py ├── conways_game_of_life.py ├── equaliser.py ├── land_grab.py ├── lava_lamp.py ├── pi_status_screen.py ├── rain.py ├── rainbow.py ├── ripples.py ├── scrolling_clock.py ├── tapping_ripples.py ├── voice_recognition.py ├── water_level.py └── windmill.py └── official-documentation └── api.txt /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |    5 | 6 | 7 | 8 |

9 | 10 | # LumiCube 11 | 12 | This repository contains some example code and documentation for the LumiCube: 13 | * https://www.kickstarter.com/projects/1202256831/lumicube-an-led-cube-kit-for-the-raspberry-pi 14 | * https://www.indiegogo.com/projects/lumicube-an-led-cube-kit-for-the-raspberry-pi 15 | 16 | More detailed information can be found in our project booklet: 17 | * https://abstractfoundry.com/lumicube/manual.pdf 18 | 19 | For getting started head to our resources page: 20 | * https://abstractfoundry.com/lumicube/resources 21 | 22 | (Note: the only supported operating system at the moment is the full 32bit Raspberry Pi OS.) 23 | 24 | The source code for the software running on the Raspberry Pi, including our web-based IDE, is available in a separate project: 25 | * https://github.com/abstractfoundry/lumicube-daemon 26 | 27 | ## Community Contributions 28 | 29 | Documentation: 30 | * https://github.com/abstractfoundry/lumicube/tree/main/community-documentation 31 | 32 | Scripts: 33 | * https://github.com/abstractfoundry/lumicube/tree/main/community-scripts 34 | 35 | ## Simulator 36 | 37 | It is also possible to run _some_ of these examples projects using our online simulator: 38 | * https://abstractfoundry.com/lumicube/simulator 39 | 40 | ## Example projects 41 | 42 | The “examples” folder contains a number of example projects, some of which we used in our Kickstarter and Indiegogo video: 43 | 44 | ### Level 1 (easiest) 45 | 46 | #### button.py 47 | Use the button to switch the cube’s colour between red and blue. 48 | 49 | #### chiptune.py 50 | Play a randomly generated tune. 51 | 52 | #### pi_status_screen.py 53 | Display some Raspberry Pi stats on the screen: CPU temperature, disk usage, etc. 54 | 55 | #### rainbow.py 56 | Continually change the cube's colour. 57 | 58 | #### scrolling_clock.py 59 | Every 10 seconds scroll the time across the LEDs. 60 | 61 | #### voice_recognition.py 62 | Basic voice recognition and text-to-speech. 63 | 64 | ### Level 2 (after you’ve learnt the basics) 65 | 66 | #### autumn_scene.py 67 | Autumn animation (leaves falling from a pixel-art tree). 68 | 69 | #### binary_clock.py 70 | A simple binary clock (see https://en.wikipedia.org/wiki/Binary_clock). 71 | 72 | #### conways_game_of_life.py 73 | Run Conway’s Game of Life on the LEDs (see https://en.wikipedia.org/wiki/Conway's_Game_of_Life). 74 | 75 | #### equaliser.py 76 | Colourful equaliser using the microphone. 77 | 78 | #### land_grab.py 79 | Several computer players randomly walk the LED grid, colouring squares until they get stuck. 80 | 81 | #### lava_lamp.py 82 | Use OpenSimplex noise to generate a lava lamp like effect (see https://en.wikipedia.org/wiki/Simplex_noise). 83 | 84 | #### rain.py 85 | Rain animation. 86 | 87 | #### ripples.py 88 | Random circular ripple animation. 89 | 90 | #### tapping_ripples.py 91 | Ripple animation which responds to tapping or moving the cube. 92 | 93 | #### water_level.py 94 | Virtual water-level effect which responds to the cube's orientation. 95 | 96 | #### windmill.py 97 | Blow at the back of the cube to make the windmill animation turn. 98 | 99 | ## Documentation 100 | 101 | ### Python API 102 | 103 | See the LumiCube manual for a detailed explanation of the modules, methods and fields: 104 | * https://abstractfoundry.com/lumicube/manual.pdf 105 | 106 | A brief summary of the API is also given here: 107 | * https://github.com/abstractfoundry/lumicube/blob/main/documentation/api.txt 108 | 109 | ### REST API 110 | 111 | The API is broken down into: 112 | - modules (e.g. speaker, microphone, screen) 113 | - methods (e.g. play_tone(), draw_rectangle()) 114 | - fields (e.g. volume, brightness) 115 | 116 | All of the modules, methods, and fields in the Python API (see the product manual, linked above) also exist in the REST API. 117 | 118 | #### Getting or setting a field 119 | 120 | ``` 121 | [GET/POST] http:///api/v1/modules//fields/ { "value": } 122 | ``` 123 | 124 | For example, to set the brightness of the display to 50%: 125 | 126 | ``` 127 | curl -X POST -H "Content-Type: application/json" -d '{"value": 50}' http:///api/v1/modules/display/fields/brightness 128 | ``` 129 | 130 | #### Calling a method 131 | 132 | ``` 133 | [POST] http:///api/v1/modules//methods/ { "arguments": } 134 | ``` 135 | 136 | For example, to set one LED at x=1, y=0 to colour=255 (blue, #0000ff in hex): 137 | 138 | ``` 139 | curl -X POST -H "Content-Type: application/json" -d '{"arguments": [1, 0, 255]}' http:///api/v1/modules/display/methods/set_led 140 | ``` 141 | 142 | #### Executing a Python script 143 | 144 | ``` 145 | [POST] http:///api/v1/scripts/main/methods/start { "body": "" } 146 | ``` 147 | 148 | For example, to set all the LEDs to red using a Python script: 149 | 150 | ``` 151 | curl -X POST -H "Content-Type: application/json" -d '{"body": "display.set_all(red)"}' http:///api/v1/scripts/main/methods/start 152 | ``` 153 | 154 | #### Exceptional cases 155 | 156 | `set_leds(), set_3d()` 157 | 158 | In Python these methods take a dictionary mapping coordinates to colours, where the coordinates are a tuple of (x, y) or (x, y, z) values. JSON doesn't support tuples, so instead coordinates are represented as a comma-separated string. 159 | 160 | For example, to set the LED at (0,0) to 0x000080 and the LED at (1,1) to 0x0000FF: 161 | 162 | ``` 163 | curl -X POST -H "Content-Type: application/json" -d '{"arguments": [{"0,0": 128, "1,1": 255}]}' http:///api/v1/modules/display/methods/set_leds 164 | ``` 165 | -------------------------------------------------------------------------------- /community-documentation/raspberry_pi_os_lite_install.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi OS Lite install 2 | 3 | If you would rather use the Raspberry Pi OS Lite image for your lumicube you need to do a few extra steps. 4 | 5 | ### Download the image 6 | 7 | Only use the official repo: https://www.raspberrypi.com/software/operating-systems/ 8 | 9 | You want the 32-bit image called "Raspberry Pi OS Lite" as the java daemon is not built for arm64. 10 | 11 | Get this flashed onto your SD card using your favorite tool. If you would rather use the official Raspberry Pi Imager tool (https://www.raspberrypi.com/software/) do pay close attention when choosing the OS. 12 | 13 | ### Extra install steps 14 | 15 | Once the image is on the SD card and you have fired up your raspberry with keyboard, mouse and monitor, added your user and password - the following needs to be done: 16 | 17 | 1. Get the WiFi working 18 | 19 | > sudo raspi-config 20 | 21 | This will open a menu. Go into System Options. Select Wireless LAN and follow the instructions. 22 | 23 | 2. Update and install needed packages 24 | 25 | > sudo apt-get update && sudo apt-get upgrade -y && sudo apt install pulseaudio pulseaudio-utils python3-psutil 26 | 27 | 3. Install the lumicube resources according to https://www.abstractfoundry.com/lumicube/resources/ 28 | 29 | > python3 <(curl -fL https://www.abstractfoundry.com/lumicube/download/install.py) 30 | 31 | 4. Make sure the daemon starts after reboot with your raspberry user 32 | 33 | > sudo loginctl enable-linger 34 | 35 | 5. Reboot 36 | 37 | ### Do not forget 38 | 39 | Using the lumicube will be done via the http interface, however if at a later point you want to ssh into your raspi to update or add something you will need to enable this (as it is disabled by default). 40 | 41 | > sudo systemctl enable ssh && sudo systemctl start ssh 42 | 43 | 44 | -------------------------------------------------------------------------------- /community-scripts/Lumicube Interface/LumicubeInterface.py: -------------------------------------------------------------------------------- 1 | from requests import Session 2 | from typing import List 3 | 4 | """ 5 | A Python Module for the Lumicube HTTP Interface. 6 | So you can interact with the Lumicube from a different Python Script 7 | """ 8 | 9 | 10 | class LumicubeInterface: 11 | """ 12 | A Class to interface with the Lumicube API 13 | For more information: 14 | https://github.com/abstractfoundry/lumicube 15 | """ 16 | def __init__(self, api_version: str = 'v1', url: str = 'localhost') -> None: 17 | """ 18 | 19 | :param api_version: Api Version (default v1) 20 | :param url: url of the Lumicube (default localhost) 21 | """ 22 | self._baseUrl = 'http://' + url + '/api/' + api_version 23 | self.session = Session() 24 | self.modules = Modules(self._baseUrl, self.session) 25 | 26 | 27 | # region Modules 28 | 29 | 30 | class Modules: 31 | def __init__(self, baseurl, session): 32 | self.__url = baseurl + '/modules' 33 | self.buttons = Buttons(self.__url, session) 34 | self.display = Display(self.__url, session) 35 | self.light_sensor = LightSensor(self.__url, session) 36 | self.imu = IMU(self.__url, session) 37 | self.microphone = Microphone(self.__url, session) 38 | self.screen = Screen(self.__url, session) 39 | self.speaker = Speaker(self.__url, session) 40 | # self.pi = Pi(self.__url) WIP 41 | 42 | 43 | class Buttons: 44 | def __init__(self, url, session): 45 | self.__fullUrl = url + '/buttons' 46 | self.session = session 47 | 48 | def get_next_action(self, timeout: int): 49 | """ 50 | Returns the next button pressed by the user, either "top", "middle", or "bottom". 51 | If more than "timeout" seconds elapse before a button is pressed, None is returned. 52 | 53 | :param timeout: maximum time to wait in seconds 54 | """ 55 | args = {"arguments": [timeout]} 56 | ret = self.session.post(url=self.__fullUrl + '/methods/get_next_action', json=args) 57 | ret.raise_for_status() 58 | 59 | @property 60 | def top_pressed(self): 61 | """ 62 | Returns the current top button state 63 | 64 | :return: current top button state 65 | """ 66 | ret = self.session.post(url=self.__fullUrl + '/fields/top_pressed') 67 | ret.raise_for_status() 68 | data = ret.json() 69 | return data['value'] 70 | 71 | @property 72 | def top_pressed_count(self): 73 | """ 74 | Returns amount of times the top button was pressed 75 | 76 | :return: amount of times the top button was pressed 77 | """ 78 | ret = self.session.get(url=self.__fullUrl + '/fields/top_pressed_count') 79 | ret.raise_for_status() 80 | data = ret.json() 81 | return data['value'] 82 | 83 | @top_pressed_count.setter 84 | def top_pressed_count(self, value: int): 85 | """ 86 | Sets the amount of times the top button was pressed 87 | 88 | :param value: amount of times the top button was pressed 89 | """ 90 | args = {"value": value} 91 | ret = self.session.post(url=self.__fullUrl + '/fields/top_pressed_count', json=args) 92 | ret.raise_for_status() 93 | 94 | @property 95 | def middle_pressed(self) -> int: 96 | """ 97 | Returns the current middle button state 98 | 99 | :return: current middle button state 100 | """ 101 | ret = self.session.post(url=self.__fullUrl + '/fields/middle_pressed') 102 | ret.raise_for_status() 103 | data = ret.json() 104 | return data['value'] 105 | 106 | @property 107 | def middle_pressed_count(self): 108 | """ 109 | Returns amount of times the middle button was pressed 110 | 111 | :return: amount of times the middle button was pressed 112 | """ 113 | ret = self.session.get(url=self.__fullUrl + '/fields/middle_pressed_count') 114 | ret.raise_for_status() 115 | data = ret.json() 116 | return data['value'] 117 | 118 | @middle_pressed_count.setter 119 | def middle_pressed_count(self, value: int): 120 | """ 121 | Sets the amount of times the middle button was pressed 122 | 123 | :param value: amount of times the middle button was pressed 124 | """ 125 | args = {"value": value} 126 | ret = self.session.post(url=self.__fullUrl + '/fields/middle_pressed_count', json=args) 127 | ret.raise_for_status() 128 | 129 | @property 130 | def bottom_pressed(self): 131 | ret = self.session.post(url=self.__fullUrl + '/fields/bottom_pressed') 132 | ret.raise_for_status() 133 | data = ret.json() 134 | return data['value'] 135 | 136 | @property 137 | def bottom_pressed_count(self): 138 | """ 139 | Returns amount of times the bottom button was pressed 140 | 141 | :return: amount of times the bottom button was pressed 142 | """ 143 | ret = self.session.get(url=self.__fullUrl + '/fields/bottom_pressed_count') 144 | ret.raise_for_status() 145 | data = ret.json() 146 | return data['value'] 147 | 148 | @bottom_pressed_count.setter 149 | def bottom_pressed_count(self, value: int): 150 | """ 151 | Sets the amount of times the bottom button was pressed 152 | 153 | :param value: amount of times the bottom button was pressed 154 | """ 155 | args = {"value": value} 156 | ret = self.session.post(url=self.__fullUrl + '/fields/bottom_pressed_count', json=args) 157 | ret.raise_for_status() 158 | 159 | class Display: 160 | def __init__(self, url, session): 161 | self.__fullUrl = url + '/display' 162 | self.session = session 163 | 164 | def set_all(self, color: int): 165 | """ 166 | Set the colour of all the LEDs. 167 | 168 | :param color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 169 | """ 170 | args = {"arguments": [color]} 171 | ret = self.session.post(url=self.__fullUrl + '/methods/set_all', json=args, timeout=5) 172 | ret.raise_for_status() 173 | 174 | def set_led(self, x: int, y: int, color: int): 175 | """ 176 | Set a single LED to a colour using x and y coordinates. 177 | The bottom left panel is described by: x in range 0 to 7, y in range 0 to 7 178 | The bottom right panel is described by: x in range 8 to 15, y in range 0 to 7 179 | The top panel is described by: x in range 0 to 7, y in range 8 to 15 180 | Coordinates outside this range will be ignored. 181 | e.g. 182 | display.set_led(0, 0, red) would change the bottom left pixel to red 183 | display.set_led(15, 15, pink) would do nothing as it is out of range 184 | 185 | :param x: x coordinate, range 0 to 15 186 | :param y: y coordinate, range 0 to 15 187 | :param color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 188 | """ 189 | args = {"arguments": [x, y, color]} 190 | ret = self.session.post(url=self.__fullUrl + '/methods/set_led', json=args) 191 | ret.raise_for_status() 192 | 193 | def set_leds(self, x_y_to_colour_dict: dict): 194 | """ 195 | Set multiple LEDs all at once using x and y coordinates. 196 | For more information see display.set_led. 197 | 198 | :param x_y_to_colour_dict: dictionary with (x, y) coordinates as keys and colours as values 199 | """ 200 | 201 | def __2d_dic_to_css(dictionary: dict): 202 | """ 203 | JSON doesn't support tuples, so instead coordinates are represented as a comma-separated string. 204 | This function converts the tuples to a comma-separated string. 205 | 206 | :param dictionary: dictionary with 2 tuples as key value 207 | :return: dictionary with comma-separated string as key value 208 | """ 209 | comma_separate_string = [] 210 | temp_dict = {} 211 | for item in dictionary: 212 | temp_dict[f"{item[0]}, {item[1]}"] = dictionary[item[0], item[1]] 213 | comma_separate_string.append(temp_dict) 214 | return comma_separate_string 215 | 216 | param = __2d_dic_to_css(x_y_to_colour_dict) 217 | args = {"arguments": param} 218 | ret = self.session.post(url=self.__fullUrl + '/methods/set_leds', json=args) 219 | ret.raise_for_status() 220 | 221 | def set_3d(self, x_y_to_colour_dict: dict): 222 | """ 223 | Description: Set multiple LEDs to a colour using x, y and z coordinates. 224 | 225 | The left panel is described by: x, y when z = 8 226 | The right panel is described by: y, z when x = 8 227 | The top panel is described by: x, z when y = 8 228 | Coordinates outside this range will be ignored. 229 | 230 | :param x_y_to_colour_dict: dictionary with (x, y, z) as keys and colours as values 231 | """ 232 | 233 | def __3d_dic_to_css(dictionary: dict) -> object: 234 | """ 235 | JSON doesn't support tuples, so instead coordinates are represented as a comma-separated string. 236 | This function converts the tuples to a comma-separated string. 237 | 238 | :param dictionary: dictionary with 3 tuples as key value 239 | :return: dictionary with comma-separated string as key value 240 | """ 241 | comma_separate_string = [] 242 | temp_dict = {} 243 | for item in dictionary: 244 | temp_dict[f"{item[0]}, {item[1]}, {item[2]}"] = dictionary[item[0], item[1], item[2]] 245 | comma_separate_string.append(temp_dict) 246 | return comma_separate_string 247 | 248 | param = __3d_dic_to_css(x_y_to_colour_dict) 249 | args2 = {"arguments": param} 250 | ret = self.session.post(url=self.__fullUrl + '/methods/set_3d', json=args2) 251 | ret.raise_for_status() 252 | 253 | def set_panel(self, panel: str, color_list: List[List[int]]): 254 | """ 255 | Set all the LEDs on one panel. 256 | Takes 8 lists each containing 8 colours. 257 | The first list is the top row of LEDs. 258 | The first colour in that list is the top left LED colour. 259 | 260 | :param panel: panel name, either "top", "left" or "right" 261 | :param color_list: 8 by 8 list of colours 262 | """ 263 | args = {"arguments": [panel, color_list]} 264 | ret = self.session.post(url=self.__fullUrl + '/methods/set_panel', json=args) 265 | ret.raise_for_status() 266 | 267 | def clear_panel(self, panel: str): 268 | """ 269 | Clears a complete LED panel (sets LEDs to black) 270 | 271 | :param panel: panel name, either "top", "left" or "right" 272 | """ 273 | sub_list = [0, 0, 0, 0, 0, 0, 0, 0] 274 | main_list = [] 275 | for x in range(8): 276 | main_list.append(sub_list) 277 | args = {"arguments": [panel, main_list]} 278 | ret = self.session.post(url=self.__fullUrl + '/methods/set_panel', json=args) 279 | ret.raise_for_status() 280 | 281 | def scroll_text(self, text: str, color: int = 0xFFFFFF, background_color: int = 0x0, speed: float = 1): 282 | """ 283 | Display text on the LEDs scrolling from right to left. 284 | 285 | :param text: text to display 286 | :param color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 287 | :param background_color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 288 | :param speed: floating-point value 289 | """ 290 | args = {"arguments": [text, color, background_color, speed]} 291 | ret = self.session.post(url=self.__fullUrl + '/methods/scroll_text', json=args) 292 | ret.raise_for_status() 293 | 294 | @property 295 | def brightness(self) -> int: 296 | """ 297 | Return the current LED brightness 298 | 299 | :return: brightness of the LEDs 300 | """ 301 | ret = self.session.get(url=self.__fullUrl + '/fields/brightness') 302 | ret.raise_for_status() 303 | data = ret.json() 304 | return data['value'] 305 | 306 | @brightness.setter 307 | def brightness(self, brightness: int): 308 | """ 309 | Sets the LED brightness 310 | 311 | :param brightness: brightness of the LEDs 312 | """ 313 | args = {"value": brightness} 314 | ret = self.session.post(url=self.__fullUrl + '/fields/brightness', json=args) 315 | ret.raise_for_status() 316 | 317 | @property 318 | def refresh_period(self) -> int: 319 | """ 320 | Returns the refresh period of the LEDs 321 | 322 | :return: refresh Period 323 | """ 324 | ret = self.session.get(url=self.__fullUrl + '/fields/refresh_period') 325 | ret.raise_for_status() 326 | data = ret.json() 327 | return data['value'] 328 | 329 | @refresh_period.setter 330 | def refresh_period(self, refresh: int): 331 | """ 332 | Sets the refresh Period of the LEDs 333 | 334 | :param refresh: refresh Period 335 | """ 336 | args = {"value": refresh} 337 | ret = self.session.post(url=self.__fullUrl + '/fields/refresh_period', json=args) 338 | ret.raise_for_status() 339 | 340 | @property 341 | def get_estimated_current(self): 342 | """ 343 | Return the estimated current drawn by the LEDs 344 | 345 | :return: estimated current drawn 346 | """ 347 | ret = self.session.get(url=self.__fullUrl + '/fields/estimated_current') 348 | ret.raise_for_status() 349 | data = ret.json() 350 | return data['value'] 351 | 352 | @property 353 | def max_current(self) -> int: 354 | """ 355 | Returns the maximum current drawn by the LEDs 356 | 357 | :return: maximum current drawn 358 | """ 359 | ret = self.session.get(url=self.__fullUrl + '/fields/max_current') 360 | ret.raise_for_status() 361 | data = ret.json() 362 | return data['value'] 363 | 364 | @max_current.setter 365 | def max_current(self, max_current: int): 366 | """ 367 | Sets the maximum current drawn by the LEDs 368 | :param max_current: maximum current drawn 369 | """ 370 | args = {"value": max_current} 371 | ret = self.session.post(url=self.__fullUrl + '/fields/max_current', json=args) 372 | ret.raise_for_status() 373 | 374 | 375 | class LightSensor: 376 | def __init__(self, url, session): 377 | self.__fullUrl = url + '/light_sensor' 378 | self.session = session 379 | 380 | def next_gesture(self, timeout: int): 381 | """ 382 | Returns the next gesture performed by the user, either "up", "down", "left" or "right". 383 | If more than "timeout" seconds elapse before a gesture is performed, None is returned. 384 | 385 | :param timeout: maximum time to wait in seconds 386 | """ 387 | args = {"arguments": [timeout]} 388 | ret = self.session.post(url=self.__fullUrl + '/methods/get_next_gesture', json=args) 389 | ret.raise_for_status() 390 | 391 | @property 392 | def ambient_light(self): 393 | """ 394 | Returns the ambient light intensity 395 | 396 | :return: ambient light intensity 397 | """ 398 | ret = self.session.get(url=self.__fullUrl + '/fields/ambient_light') 399 | ret.raise_for_status() 400 | data = ret.json() 401 | return data['value'] 402 | 403 | @property 404 | def red(self): 405 | """ 406 | Returns red value of the RGB sensor 407 | 408 | :return: red value 409 | """ 410 | ret = self.session.get(url=self.__fullUrl + '/fields/red') 411 | ret.raise_for_status() 412 | data = ret.json() 413 | return data['value'] 414 | 415 | @property 416 | def green(self): 417 | """ 418 | Returns green value of the RGB sensor 419 | 420 | :return: green value 421 | """ 422 | ret = self.session.get(url=self.__fullUrl + '/fields/green') 423 | ret.raise_for_status() 424 | data = ret.json() 425 | return data['value'] 426 | 427 | @property 428 | def blue(self): 429 | """ 430 | Returns blue value of the RGB sensor 431 | 432 | :return: blue value 433 | """ 434 | ret = self.session.get(url=self.__fullUrl + '/fields/blue') 435 | ret.raise_for_status() 436 | data = ret.json() 437 | return data['value'] 438 | 439 | @property 440 | def last_gesture(self): 441 | """ 442 | Returns last gesture direction 443 | 444 | :return: last gesture 445 | """ 446 | ret = self.session.get(url=self.__fullUrl + '/fields/last_gesture') 447 | ret.raise_for_status() 448 | data = ret.json() 449 | return data['value'] 450 | 451 | @property 452 | def num_gesture(self): 453 | """ 454 | Returns amount of gestures 455 | 456 | :return: amount of gestures 457 | """ 458 | ret = self.session.get(url=self.__fullUrl + '/fields/num_gestures') 459 | ret.raise_for_status() 460 | data = ret.json() 461 | return data['value'] 462 | 463 | @num_gesture.setter 464 | def num_gesture(self, value: int): 465 | """ 466 | Sets the amount of gestures 467 | 468 | :param value: amount of gestures 469 | """ 470 | args = {"value": value} 471 | ret = self.session.post(url=self.__fullUrl + '/fields/num_gestures', json=args) 472 | ret.raise_for_status() 473 | 474 | @property 475 | def within_proximity(self): 476 | """ 477 | Returns current state of the proximity Sensor 478 | 479 | :return: True = in proximity || False = not in proximity 480 | """ 481 | ret = self.session.get(url=self.__fullUrl + '/fields/within_proximity') 482 | ret.raise_for_status() 483 | data = ret.json() 484 | return data['value'] 485 | 486 | @property 487 | def num_times_within_proximity(self): 488 | """ 489 | Returns the amount of times within the proximity sensor 490 | 491 | :return: amount of times within proximity 492 | """ 493 | ret = self.session.get(url=self.__fullUrl + '/fields/num_times_within_proximity') 494 | ret.raise_for_status() 495 | data = ret.json() 496 | return data['value'] 497 | 498 | @num_times_within_proximity.setter 499 | def num_times_within_proximity(self, value: int): 500 | """ 501 | Sets the amount of times within the proximity sensor 502 | 503 | :return: amount of times within proximity 504 | """ 505 | args = {"value": value} 506 | ret = self.session.post(url=self.__fullUrl + '/fields/num_times_within_proximity', json=args) 507 | ret.raise_for_status() 508 | 509 | 510 | class Microphone: 511 | def __init__(self, url, session): 512 | self.__fullUrl = url + '/microphone' 513 | self.session = session 514 | 515 | def start_recording(self, path: str): 516 | """ 517 | Start recording microphone audio to the specified file. The file name must end in either ".wav" 518 | or ".mp3". If a file already exists at the given path, it is replaced. 519 | 520 | :param path: path to the output file (must end in ".wav" or ".mp3") 521 | """ 522 | args = {"arguments": [path]} 523 | ret = self.session.post(url=self.__fullUrl + '/methods/start_recording', json=args) 524 | ret.raise_for_status() 525 | 526 | def stop_recording(self): 527 | """ 528 | Stop a previously started recording. 529 | """ 530 | args = {"arguments": []} 531 | ret = self.session.post(url=self.__fullUrl + '/methods/stop_recording', json=args) 532 | ret.raise_for_status() 533 | 534 | def start_voice_recognition(self): 535 | """ 536 | Start the voice recognition service. 537 | """ 538 | args = {"arguments": []} 539 | ret = self.session.post(url=self.__fullUrl + '/methods/start_voice_recognition', json=args) 540 | ret.raise_for_status() 541 | 542 | def wait_for_sentence(self, timeout: float): 543 | """ 544 | Wait some number of seconds for a sentence to be spoken and translated into text. 545 | The sentence must be preceded by the wake word "Hey Mycroft". 546 | 547 | :param timeout: floating-point value specifying how many seconds to wait 548 | """ 549 | args = {"arguments": [timeout]} 550 | ret = self.session.post(url=self.__fullUrl + '/methods/wait_for_sentence', json=args) 551 | ret.raise_for_status() 552 | 553 | def stop_voice_recognition(self): 554 | """ 555 | Stop the voice recognition service. 556 | """ 557 | args = {"arguments": []} 558 | ret = self.session.post(url=self.__fullUrl + '/methods/stop_voice_recognition', json=args) 559 | ret.raise_for_status() 560 | 561 | def start_recording_for_frequency_analysis(self): 562 | """ 563 | Start recording audio for frequency analysis. 564 | """ 565 | args = {"arguments": []} 566 | ret = self.session.post(url=self.__fullUrl + '/methods/start_recording_for_frequency_analysis', json=args) 567 | ret.raise_for_status() 568 | 569 | def get_frequency_buckets(self, num_buckets: int = 8, min_hz: int = 0, max_hz: int = 4000): 570 | """ 571 | Process the audio data collected since the last call to get_frequency_buckets. 572 | Returns a dictionary of frequencies mapped to magnitudes. 573 | 574 | :param num_buckets: the number of buckets to divide the frequencies amongst 575 | :param min_hz: the minimum frequency to process 576 | :param max_hz: the maximum frequency to process 577 | """ 578 | args = {"arguments": [num_buckets, min_hz, max_hz]} 579 | ret = self.session.post(url=self.__fullUrl + '/methods/get_frequency_buckets', json=args) 580 | ret.raise_for_status() 581 | 582 | def enable(self, enable: bool = True): 583 | """ 584 | Enables the microphone 585 | 586 | :param enable: True = enable || False = disable 587 | """ 588 | args = {"arguments": [enable]} 589 | ret = self.session.post(url=self.__fullUrl + '/methods/get_frequency_buckets', json=args) 590 | ret.raise_for_status() 591 | 592 | 593 | class IMU: 594 | def __init__(self, url, session): 595 | self.__fullUrl = url + '/imu' 596 | self.session = session 597 | 598 | @property 599 | def get_pitch(self): 600 | """ 601 | WIP 602 | 603 | :return: 604 | """ 605 | ret = self.session.get(url=self.__fullUrl + '/fields/pitch') 606 | ret.raise_for_status() 607 | data = ret.json() 608 | return data['value'] 609 | 610 | @property 611 | def get_roll(self): 612 | """ 613 | WIP 614 | 615 | :return: 616 | """ 617 | ret = self.session.get(url=self.__fullUrl + '/fields/roll') 618 | ret.raise_for_status() 619 | data = ret.json() 620 | return data['value'] 621 | 622 | @property 623 | def get_yaw(self): 624 | """ 625 | WIP 626 | 627 | :return: 628 | """ 629 | ret = self.session.get(url=self.__fullUrl + '/fields/yaw') 630 | if not ret.status_code == 200: 631 | log(ret) 632 | return 633 | data = ret.json() 634 | return data['value'] 635 | 636 | @property 637 | def get_acc_x(self): 638 | """ 639 | WIP 640 | 641 | :return: 642 | """ 643 | ret = self.session.get(url=self.__fullUrl + '/fields/acceleration_x') 644 | ret.raise_for_status() 645 | data = ret.json() 646 | return data['value'] 647 | 648 | @property 649 | def get_acc_y(self): 650 | """ 651 | WIP 652 | 653 | :return: 654 | """ 655 | ret = self.session.get(url=self.__fullUrl + '/fields/acceleration_y') 656 | ret.raise_for_status() 657 | data = ret.json() 658 | return data['value'] 659 | 660 | @property 661 | def get_acc_z(self): 662 | """ 663 | WIP 664 | 665 | :return: 666 | """ 667 | ret = self.session.get(url=self.__fullUrl + '/fields/acceleration_z') 668 | ret.raise_for_status() 669 | data = ret.json() 670 | return data['value'] 671 | 672 | @property 673 | def get_ang_vel_x(self): 674 | """ 675 | WIP 676 | 677 | :return: 678 | """ 679 | ret = self.session.get(url=self.__fullUrl + '/fields/angular_velocity_x') 680 | ret.raise_for_status() 681 | data = ret.json() 682 | return data['value'] 683 | 684 | @property 685 | def get_ang_vel_y(self): 686 | """ 687 | WIP 688 | 689 | :return: 690 | """ 691 | ret = self.session.get(url=self.__fullUrl + '/fields/angular_velocity_y') 692 | ret.raise_for_status() 693 | data = ret.json() 694 | return data['value'] 695 | 696 | @property 697 | def get_ang_vel_z(self): 698 | """ 699 | WIP 700 | 701 | :return: 702 | """ 703 | ret = self.session.get(url=self.__fullUrl + '/fields/angular_velocity_z') 704 | ret.raise_for_status() 705 | data = ret.json() 706 | return data['value'] 707 | 708 | @property 709 | def get_gravity_x(self): 710 | """ 711 | WIP 712 | 713 | :return: 714 | """ 715 | ret = self.session.get(url=self.__fullUrl + '/fields/gravity_x') 716 | ret.raise_for_status() 717 | data = ret.json() 718 | return data['value'] 719 | 720 | @property 721 | def get_gravity_y(self): 722 | """ 723 | WIP 724 | 725 | :return: 726 | """ 727 | ret = self.session.get(url=self.__fullUrl + '/fields/gravity_y') 728 | ret.raise_for_status() 729 | data = ret.json() 730 | return data['value'] 731 | 732 | @property 733 | def get_gravity_z(self): 734 | """ 735 | WIP 736 | 737 | :return: 738 | """ 739 | ret = self.session.get(url=self.__fullUrl + '/fields/gravity_z') 740 | ret.raise_for_status() 741 | data = ret.json() 742 | return data['value'] 743 | 744 | 745 | class Screen: 746 | def __init__(self, url, session): 747 | self.__fullUrl = url + '/screen' 748 | self.session = session 749 | 750 | def set_pixel(self, x: int, y: int, color: int): 751 | """ 752 | Set the colour of a pixel given an x and y coordinate. 753 | 754 | :param x: integer between 0 and 319 755 | :param y: integer between 0 and 239 756 | :param color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 757 | """ 758 | args = {"arguments": [x, y, color]} 759 | ret = self.session.post(url=self.__fullUrl + '/methods/set_pixel', json=args) 760 | ret.raise_for_status() 761 | 762 | def set_pixels(self, x: int, y: int, width: int, height: int, pixels: List[int]): 763 | """ 764 | Set the colours of a rectangular region of pixels, from (x, y) to (x+width, y+height). 765 | The length of the "pixels" list must be exactly width*height, and is read left to right, 766 | top down. 767 | 768 | :param x: integer between 0 and 319 769 | :param y: integer between 0 and 239 770 | :param width: integer between 0 and 320 771 | :param height: integer between 0 and 240 772 | :param pixels: list of 24-bit colours (length width*height) 773 | """ 774 | args = {"arguments": [x, y, width, height, pixels]} 775 | ret = self.session.post(url=self.__fullUrl + '/methods/set_pixels', json=args) 776 | ret.raise_for_status() 777 | 778 | def draw_rectangle(self, x: int, y: int, width: int, height: int, color: int): 779 | """ 780 | Draw a coloured rectangle starting at x and y coordinates. 781 | 782 | :param x: integer between 0 and 319 783 | :param y: integer between 0 and 239 784 | :param width: integer between 0 and 320 785 | :param height: integer between 0 and 240 786 | :param color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 787 | """ 788 | args = {"arguments": [x, y, width, height, color]} 789 | ret = self.session.post(url=self.__fullUrl + '/methods/draw_rectangle', json=args) 790 | ret.raise_for_status() 791 | 792 | def write_text(self, x: int, y: int, text: str, size: int, color: int, background_color: int): 793 | """ 794 | Write some text to the screen starting at x and y coordinates. 795 | 796 | :param x: integer between 0 and 319 797 | :param y: integer between 0 and 239 798 | :param text: text to be displayed 799 | :param size: integer representing the font size, must be greater than 0 800 | :param color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 801 | :param background_color: 24-bit colour, e.g. black, red, hsv_colour(0.5, 1, 1), 0xFF0000 802 | """ 803 | args = {"arguments": [x, y, text, size, color, background_color]} 804 | ret = self.session.post(url=self.__fullUrl + '/methods/write_text', json=args) 805 | ret.raise_for_status() 806 | 807 | def draw_image(self, path: str, x: int = 0, y: int = 0, width: int = 320, height: int = 240): 808 | """ 809 | Draw an image to the screen, scaled to fit the rectangular region from (x, y) to (x+width, y+height). 810 | Paths are taken relative to the desktop (or can be specified absolutely). 811 | 812 | :param path: path to the image file 813 | :param x: integer between 0 and 319 814 | :param y: integer between 0 and 239 815 | :param width: integer between 0 and 320 816 | :param height: integer between 0 and 240 817 | """ 818 | args = {"arguments": [path, x, y, width, height]} 819 | ret = self.session.post(url=self.__fullUrl + '/methods/draw_image', json=args) 820 | ret.raise_for_status() 821 | 822 | def resolution_scaling(self, scaling: int): 823 | """ 824 | Scale the resolution 825 | 826 | :param scaling: N/A 827 | """ 828 | args = {"value": scaling} 829 | ret = self.session.post(url=self.__fullUrl + '/fields/resolution_scaling', json=args) 830 | ret.raise_for_status() 831 | 832 | def invert_colours(self, state: int): 833 | """ 834 | Invert colors of the screen 835 | 836 | :param state: True = inverted || False = normal 837 | """ 838 | args = {"value": state} 839 | ret = self.session.post(url=self.__fullUrl + '/fields/invert_colours', json=args) 840 | ret.raise_for_status() 841 | 842 | 843 | class Speaker: 844 | def __init__(self, url, session): 845 | self.__fullUrl = url + '/speaker' 846 | self.session = session 847 | 848 | def play(self, path: str): 849 | """ 850 | Play an audio file (non-absolute paths are taken relative to the Desktop directory). 851 | 852 | :param path: string specifying the path to the audio file 853 | """ 854 | args = {"arguments": [path]} 855 | ret = self.session.post(url=self.__fullUrl + '/methods/play', json=args) 856 | ret.raise_for_status() 857 | 858 | def stop(self): 859 | """ 860 | Stops all currently playing audio files. 861 | """ 862 | args = {"arguments": []} 863 | ret = self.session.post(url=self.__fullUrl + '/methods/stop', json=args) 864 | ret.raise_for_status() 865 | 866 | def say(self, text: str): 867 | """ 868 | Convert text to speech. 869 | 870 | :param text: Text to speach 871 | """ 872 | args = {"arguments": [text]} 873 | ret = self.session.post(url=self.__fullUrl + '/methods/say', json=args) 874 | ret.raise_for_status() 875 | 876 | def tone(self, freq: float, duration: float, volume: float, function: str): 877 | """ 878 | Play a tone of a certain frequency. 879 | 880 | :param freq: frequency in Hz 881 | :param duration: time in seconds 882 | :param volume: number between 0.0 and 1.0 specifying the volume 883 | :param function: sine_wave, square_wave, white_noise 884 | """ 885 | args = {"arguments": [freq, duration, volume, function]} 886 | ret = self.session.post(url=self.__fullUrl + '/methods/tone', json=args) 887 | ret.raise_for_status() 888 | 889 | def set_volume(self, volume: float): 890 | """ 891 | Set the speaker volume 892 | 893 | :param volume: volume of the speaker 894 | """ 895 | args = {"value": volume} 896 | ret = self.session.post(url=self.__fullUrl + '/fields/volume', json=args) 897 | ret.raise_for_status() 898 | 899 | 900 | """ WIP 901 | class Pi: 902 | def __init__(self, url, session): 903 | self.__fullUrl = url + '/pi' 904 | self.session = session 905 | 906 | def get_ip_address(self): 907 | ret = self.session.get(url=self.__fullUrl + '/fields/ip_address') 908 | ret.raise_for_status() 909 | data = ret.json() 910 | return data['value'] 911 | 912 | def get_cpu_temp(self): 913 | ret = self.session.get(url=self.__fullUrl + '/fields/cpu_temp') 914 | ret.raise_for_status() 915 | data = ret.json() 916 | return data['value'] 917 | 918 | def get_cpu_percentage(self): 919 | ret = self.session.get(url=self.__fullUrl + '/fields/cpu_percent') 920 | ret.raise_for_status() 921 | data = ret.json() 922 | return data['value'] 923 | 924 | def get_ram_percent_used(self): 925 | ret = self.session.get(url=self.__fullUrl + '/fields/ram_percent_used') 926 | ret.raise_for_status() 927 | data = ret.json() 928 | return data['value'] 929 | 930 | def get_disk_percent(self): 931 | ret = self.session.get(url=self.__fullUrl + '/fields/disk_percent') 932 | ret.raise_for_status() 933 | data = ret.json() 934 | return data['value'] 935 | """ 936 | # endregion 937 | -------------------------------------------------------------------------------- /community-scripts/Lumicube Interface/example/example.py: -------------------------------------------------------------------------------- 1 | from LumicubeInterface import LumicubeInterface 2 | 3 | 4 | def main(): 5 | #Create instance of the Interface (make sure to change the IP-Adress 6 | #Parameters are optional default are: 7 | #api_version = v1 8 | #url = 127.0.0.1 (localhost) 9 | lc_interface = LumicubeInterface(api_version='v1', url='192.168.178.10') 10 | 11 | #exeptional cases: 12 | #set_leds() and set_3d() refer the github documentation for further information 13 | lc_interface.modules.display.set_led(1, 1, 0xFF000) 14 | 15 | 16 | if __name__ == '__main__': 17 | main() 18 | -------------------------------------------------------------------------------- /community-scripts/Resistor clock.py: -------------------------------------------------------------------------------- 1 | # Display a clock in resistor colour code 2 | # By Sean Kelly 3 | 4 | def fill_panel(panel,n1,n2): 5 | display.set_panel(panel, [[b, b, b, b, b, b, b, b], 6 | [b, n1, n1, n1, n2, n2, n2, b], 7 | [b, n1, n1, n1, n2, n2, n2, b], 8 | [wires, n1, n1, n1, n2, n2, n2, wires], 9 | [wires, n1, n1, n1, n2, n2, n2, wires], 10 | [b, n1, n1, n1, n2, n2, n2, b], 11 | [b, n1, n1, n1, n2, n2, n2, b], 12 | [b, b, b, b, b, b, b, b]]) 13 | 14 | # initialise 15 | import datetime 16 | display.set_all(black) 17 | lastminute = 99 18 | lasthour = 25 19 | 20 | # number and colour definitions 21 | brown = 0xAB784E 22 | violet = 0x9B26B6 23 | border = 0xDED1A6 24 | b = border 25 | wires = 0x444444 26 | dimblack = 0x101010 27 | colourlist = [dimblack,brown,red,orange,yellow,green,blue,violet,grey,white] 28 | 29 | # main loop 30 | while True: 31 | time_now = datetime.datetime.now() 32 | seconds = int(format(time_now.second)) 33 | minutes = int(format(time_now.minute)) 34 | hours = int(format(time_now.hour)) 35 | fill_panel("top",colourlist[int(seconds/10)],colourlist[seconds % 10]) 36 | if lastminute != minutes: 37 | fill_panel("right",colourlist[int(minutes/10)],colourlist[minutes % 10]) 38 | lastminute = minutes 39 | if hours != lasthour: 40 | fill_panel("left",colourlist[int(hours/10)],colourlist[hours % 10]) 41 | lasthour = hours 42 | time.sleep(1) 43 | -------------------------------------------------------------------------------- /community-scripts/cube_runner.py: -------------------------------------------------------------------------------- 1 | # Script Runner 2 | # Simple script runner for lumicube 3 | # Author: Kevin Haney 4 | # Date: 12/9/2022 5 | # 6 | # Scripts are assumed to be in /home/pi/AbstractFoundry/Daemon/Scripts. Change the path if necessary. 7 | # Note that if you want to use any of the example scripts, you'll need to save them to a file (they're not on disk, but in the web code). 8 | # the sample script below us using two cubes - "cubes" array can have just one (or more) as necessary. cube value should be the dns name 9 | # or ip address of the lumicube pi host. 10 | # in cubes, a cube can specify a filename, or random : True to pick from a list of random file. 11 | # delay is in seconds 12 | # to run, place the script on the pi and run it (e.g. python cube_runner.py). 13 | # You may want to comment out the print statements, they are useful for debugging though. 14 | import requests 15 | import json 16 | import time 17 | import random 18 | import traceback 19 | 20 | config = { 21 | 'random' : [ 22 | 'vesuvius.py', 23 | 'cylon.py', 24 | 'yagol.py', 25 | 'ripples.py', 26 | 'Lava lamp 2.py', 27 | 'digital-rain-v2.py', 28 | 'pong.py' 29 | ], 30 | 'groups' : [ 31 | { 32 | 'name' : '1', 33 | 'delay' : 300, 34 | 'cubes' : 35 | [ 36 | { 'cube' : 'lumipi.lan', 'random' : True }, 37 | { 'cube' : 'lumipi2.lan', 'random' : True } 38 | ] 39 | }, 40 | { 41 | 'name' : '2', 42 | 'delay' : 300, 43 | 'cubes' : 44 | [ 45 | { 'cube' : 'lumipi.lan', 'filename' : 'nest_thermostat.py' }, 46 | { 'cube' : 'lumipi2.lan', 'filename' : 'yagol.py' } 47 | ] 48 | }, 49 | { 50 | 'name' : '3', 51 | 'delay' : 300, 52 | 'cubes' : 53 | [ 54 | { 'cube' : 'lumipi2.lan', 'random' : True } 55 | ] 56 | }, 57 | { 58 | 'name' : '4', 59 | 'delay' : 300, 60 | 'cubes' : 61 | [ 62 | { 'cube' : 'lumipi.lan', 'random' : True }, 63 | { 'cube' : 'lumipi2.lan', 'random' : True } 64 | ] 65 | }, 66 | { 67 | 'name' : '5', 68 | 'delay' : 300, 69 | 'cubes' : 70 | [ 71 | { 'cube' : 'lumipi.lan', 'filename' : 'nest_thermostat.py' }, 72 | { 'cube' : 'lumipi2.lan', 'random' : True } 73 | ] 74 | } 75 | ] 76 | } 77 | 78 | if __name__ == "__main__": 79 | workingDirectory = '/home/pi/AbstractFoundry/Daemon/Scripts/' 80 | print(workingDirectory) 81 | 82 | # save config to file 83 | #with open(workingDirectory + 'config','w') as config_file: 84 | # config_file.write(json.dumps(config, indent=4)) 85 | 86 | while True: 87 | random_files = config['random'] 88 | for group in config['groups']: 89 | name = group['name'] 90 | delay = group['delay'] 91 | print(f'Group {name}, delay {delay}') 92 | for cube in group['cubes']: 93 | cubename = cube['cube'] 94 | if 'random' in cube: 95 | filename = random_files[random.randrange(len(random_files))] 96 | else: 97 | filename = cube['filename'] 98 | with open(workingDirectory + filename, 'r') as file: 99 | script = file.read() 100 | 101 | try: 102 | url = "http://" + cubename + "/api/v1/scripts/main/methods/stop" 103 | response = requests.post(url, timeout=30) 104 | print(f'sending stop to {cubename}') 105 | if response.status_code != 200: 106 | print(f'Unexpected response {response.status_code}') 107 | print(response) 108 | 109 | headers = { 110 | 'Content-Type': 'application/json' 111 | } 112 | data = { 113 | 'body' : script 114 | } 115 | print(f'sending start of script {filename} to {cubename}') 116 | url = "http://" + cubename + "/api/v1/scripts/main/methods/start" 117 | response = requests.post(url, headers=headers, data=json.dumps(data), timeout=30) 118 | if response.status_code != 200: 119 | print(f'Unexpected response {response.status_code}') 120 | print(response) 121 | except OSError as ose: 122 | print(f'OSError exception talking to {cubename}') 123 | print(getattr(ose, 'message', str(ose))) 124 | except Exception as e: 125 | print(f'Unexpected exception talking to {cubename}') 126 | print(traceback.format_exc()) 127 | time.sleep(delay) -------------------------------------------------------------------------------- /community-scripts/cylon.py: -------------------------------------------------------------------------------- 1 | # BSG Cylon with ever scanning eye 2 | # best viewed from the front with left and right panels evently visible left and right to see the cylon "face" 3 | # Author : Kevin Haney 4 | # Date : 10/13/2022 5 | # v1.0 6 | 7 | import time 8 | 9 | display.set_all(black) 10 | 11 | m0 = hsv_colour(.198, .058, .46) 12 | m1 = hsv_colour(.198, .058, .36) 13 | m2 = hsv_colour(.198, .058, .26) 14 | m3 = hsv_colour(.198, .058, .16) 15 | m4 = hsv_colour(.198, .058, .06) 16 | m5 = hsv_colour(.198, .058, .02) 17 | 18 | bright_red = hsv_colour(0.0, .91, .86) 19 | fade_red = hsv_colour(0.0, .91, .40) 20 | pulse_red = hsv_colour(0.0, .30, .82) 21 | 22 | leds = {} 23 | 24 | delay = .07 25 | longdelay = .25 26 | 27 | # pre-build a dictionary in a nice order, and update it later - the cube doesn't like randomly ordered data for some reason 28 | helmet = {} 29 | for x in range(9): 30 | for y in range(9): 31 | helmet[(x,y,8)] = black 32 | for x in range(9): 33 | for z in range(9): 34 | helmet[(x,8,z)] = black 35 | for y in range(9): 36 | for z in range(9): 37 | helmet[(8,y,z)] = black 38 | 39 | # the static helmet data 40 | data = { 41 | #left 42 | (0,7,8) : m3, (1,7,8) : m1, (2,7,8) : m0, (3,7,8) : m0, (4,7,8) : m0, (5,7,8) : m0, (6,7,8) : m0, (7,7,8) : m0, 43 | (0,6,8) : m2, 44 | (0,5,8) : m3, (1,5,8) : m1, (2,5,8) : m0, (3,5,8) : m0, (4,5,8) : m0, (5,5,8) : m0, (6,5,8) : m0, (7,5,8) : m0, 45 | (0,4,8) : m5, (1,4,8) : m4, (2,4,8) : m2, (3,4,8) : m2, (4,4,8) : m2, (5,4,8) : m3, (6,4,8) : m4, (7,4,8) : m1, 46 | (0,3,8) : m5, (1,3,8) : m4, (2,3,8) : m2, (3,3,8) : m2, (4,3,8) : m2, (5,3,8) : m3, (6,3,8) : m1, (7,3,8) : black, 47 | (0,2,8) : m5, (1,2,8) : m4, (2,2,8) : m3, (3,2,8) : m2, (4,2,8) : m2, (5,2,8) : m3, (6,2,8) : m1, (7,2,8) : black, 48 | (0,1,8) : black, (1,1,8) : m5, (2,1,8) : m3, (3,1,8) : m2, (4,1,8) : m3, (5,1,8) : m1, (6,1,8) : black, (7,1,8) : black, 49 | (0,0,8) : black, (1,0,8) : m5, (2,0,8) : m3, (3,0,8) : m2, (4,0,8) : m3, (5,0,8) : m1, (6,0,8) : black, (7,0,8) : black, 50 | #right 51 | (8,7,0) : m3, (8,7,1) : m1, (8,7,2) : m0, (8,7,3) : m0, (8,7,4) : m0, (8,7,5) : m0, (8,7,6) : m0, (8,7,7) : m0, 52 | (8,6,0) : m2, 53 | (8,5,0) : m3, (8,5,1) : m1, (8,5,2) : m0, (8,5,3) : m0, (8,5,4) : m0, (8,5,5) : m0, (8,5,6) : m0, (8,5,7) : m0, 54 | (0,4,8) : m5, (8,4,1) : m4, (8,4,2) : m2, (8,4,3) : m2, (8,4,4) : m2, (8,4,5) : m3, (8,4,6) : m4, (8,4,7) : m1, 55 | (0,3,8) : m5, (8,3,1) : m4, (8,3,2) : m2, (8,3,3) : m2, (8,3,4) : m2, (8,3,5) : m3, (8,3,6) : m1, (8,3,7) : black, 56 | (0,2,8) : m5, (8,2,1) : m4, (8,2,2) : m3, (8,2,3) : m2, (8,2,4) : m2, (8,2,5) : m3, (8,2,6) : m1, (8,2,7) : black, 57 | (0,1,8) : black, (8,1,1) : m5, (8,1,2) : m3, (8,1,3) : m2, (8,1,4) : m3, (8,1,5) : m1, (8,1,6) : black, (8,1,7) : black, 58 | (0,0,8) : black, (8,0,1) : m5, (8,0,2) : m3, (8,0,3) : m2, (8,0,4) : m3, (8,0,5) : m1, (8,0,6) : black, (8,0,7) : black, 59 | #top 60 | (0,8,7) : m3, (0,8,6) : m4, 61 | (1,8,7) : m2, (1,8,6) : m2, (1,8,5) : m2, (1,8,4) : m1, (1,8,3) : m3, 62 | (2,8,7) : m1, (2,8,6) : m1, (2,8,5) : m1, (2,8,4) : m0, (2,8,3) : m0, (2,8,2) : m0, 63 | (3,8,7) : m1, (3,8,6) : m1, (3,8,5) : m0, (3,8,4) : m0, (3,8,3) : m0, (3,8,2) : m0, (3,8,1) : m2, 64 | (4,8,7) : m1, (4,8,6) : m0, (4,8,5) : m0, (4,8,4) : m0, (4,8,3) : m0, (4,8,2) : m0, (4,8,1) : m1, 65 | (5,8,7) : m1, (5,8,6) : m1, (5,8,5) : m0, (5,8,4) : m0, (5,8,3) : m0, (5,8,2) : m1, (5,8,1) : m1, 66 | (6,8,7) : m2, (6,8,6) : m2, (6,8,5) : m1, (6,8,4) : m0, (6,8,3) : m1, (6,8,2) : m1, (6,8,1) : m2, (6,8,0) : m4, 67 | (7,8,7) : m3, (7,8,6) : m2, (7,8,5) : m1, (7,8,4) : m1, (7,8,3) : m0, (7,8,2) : m1, (7,8,1) : m2, (7,8,0) : m3, 68 | (0,8,0) : black 69 | } 70 | 71 | for d in data: 72 | helmet[d] = data[d] 73 | 74 | display.set_3d(helmet) 75 | 76 | # scanning eye code. I may put this into method that just handles the left and right on a flat plane, then use a map to convert o the cube 77 | while True: 78 | for x in range(1,9): 79 | leds[(x,6,8)] = bright_red 80 | if x > 1: 81 | leds[(x-1,6,8)] = fade_red 82 | if x > 2: 83 | leds[(x-2,6,8)] = black 84 | display.set_3d(leds) 85 | time.sleep(delay) 86 | 87 | if x == 8: 88 | leds[(7,6,8)] = black 89 | 90 | for z in reversed(range(1,8)): 91 | leds[(8,6,z)] = bright_red 92 | if z < 7: 93 | leds[(8,6,z+1)] = fade_red 94 | if z < 6: 95 | leds[(8,6,z+2)] = black 96 | display.set_3d(leds) 97 | last = leds 98 | time.sleep(delay) 99 | 100 | leds[(8,6,2)] = black 101 | leds[(8,6,1)] = pulse_red 102 | display.set_3d(leds) 103 | 104 | time.sleep(longdelay) 105 | 106 | for z in range(1,9): 107 | leds[(8,6,z)] = bright_red 108 | if z > 1: 109 | leds[(8,6,z-1)] = fade_red 110 | if z > 2: 111 | leds[(8,6,z-2)] = black 112 | display.set_3d(leds) 113 | last = leds 114 | time.sleep(delay) 115 | if z == 7: 116 | leds[(8,6,6)] = black 117 | 118 | for x in reversed(range(1,9)): 119 | leds[(x,6,8)] = bright_red 120 | if x == 8: 121 | leds[(8,6,7)] = black 122 | if x < 8: 123 | leds[(x+1,6,8)] = fade_red 124 | if x < 7: 125 | leds[(x+2,6,8)] = black 126 | display.set_3d(leds) 127 | time.sleep(delay) 128 | 129 | leds[(2,6,8)] = black 130 | leds[(1,6,8)] = pulse_red 131 | display.set_3d(leds) 132 | 133 | time.sleep(longdelay) 134 | -------------------------------------------------------------------------------- /community-scripts/digital_clock_v1.py: -------------------------------------------------------------------------------- 1 | # credit to @arustleund#0019 (Discord) for the original script 2 | # modified by @darkalexwang 10-01-2022 3 | 4 | # ToDo: have the second dots come to life in a spiral as an optional mode 5 | # ToDo: develop a menu for the rear screen so the config below can be changed on the fly (up/down/enter buttons) 6 | # ToDo: load and save config to file 7 | 8 | import datetime 9 | import traceback 10 | from pathlib import Path 11 | import pyttsx3 12 | 13 | import requests 14 | 15 | # Digital Clock 16 | 17 | # config 18 | enable_check_temperature_button = True 19 | chime_on_hour = True 20 | chime_on_half_hour = True 21 | chime_disabled_hours = range(8) # iterable containing hours by which clock shouldn't chime 22 | chime_file_path = "Cuckoo.mp3" # http://sfxcontent.s3.amazonaws.com/soundfx/Cartoon-Cuckoo1.mp3 23 | #chime_file_path = "CharlieSheen-001.mp3" 24 | dictate_on_hour = True 25 | dictate_on_half_hour = True 26 | hour_color_hue = 0.8 27 | hour_color_saturation = 1 28 | min_ambient_light_for_max_brightness = 3000 # the minimum ambient light level to display pixels at full brightness 29 | min_brightness = 0.4 # range 0-1 30 | minute_color_hue = 0 31 | minute_color_saturation = 0 32 | prefer_double_digits = True 33 | use_random_second_colors = True 34 | use_random_minute_colors = True 35 | use_random_hour_colors = True 36 | refresh_rate = 1/20 37 | second_color_hue = 0.6 38 | second_color_saturation = 0.8 39 | speaker.volume = 10 40 | use_random_dots_for_seconds = True # fill in the top with dots in a random pattern else display numbers for seconds 41 | use_24_hour_clock = True 42 | 43 | # weather config 44 | openweathermap_api_key = "" 45 | params = { 46 | "appid": openweathermap_api_key, 47 | "id": "", # city id as per openweathermap.org; search your city and take ID from URL 48 | "units": "metric" 49 | } 50 | current_weather_url = "https://api.openweathermap.org/data/2.5/weather?" 51 | # disable weather functionality if no api key 52 | # ToDo: Add functionality for the env_sensor when it is delivered as an alternative to the API 53 | if not openweathermap_api_key: 54 | enable_check_temperature_button = False 55 | 56 | # engine initiate for text to speach 57 | engine = pyttsx3.init() 58 | voices = engine.getProperty('voices') 59 | engine.setProperty('voice', 'english_rp+f3') 60 | 61 | def ampm_hour(hour: int) -> int: 62 | if hour > 11: 63 | hour -= 12 64 | return 12 if hour == 0 else hour 65 | 66 | 67 | def chime(hour): 68 | if hour not in chime_disabled_hours: 69 | if Path(chime_file_path).exists(): 70 | speaker.play(chime_file_path) 71 | 72 | 73 | def draw_number(leds: dict, number: int, x_offset: int, y_offset: int, color: int) -> None: 74 | mask = pow(2, 9 - number) 75 | for y in range(8): 76 | for x in range(3): 77 | tf_index = (21 - (y * 3)) + (x % 3) 78 | tf_value = tall_font[tf_index] 79 | bit_on = bool(mask & tf_value) 80 | leds[x + x_offset, y + y_offset] = color if bit_on else 0 81 | 82 | 83 | def draw_double_digit_number(leds: dict, number: int, x_offset: int, y_offset: int, color: int) -> None: 84 | draw_number(leds, number // 10, x_offset, y_offset, color) 85 | draw_number(leds, number % 10, x_offset + 4, y_offset, color) 86 | 87 | 88 | def get_seconds_colors() -> list[tuple[float, float]]: 89 | if use_random_second_colors: 90 | return [random_color() for _ in range(64)] 91 | return [(second_color_hue, second_color_saturation) for _ in range(64)] 92 | 93 | 94 | def get_temperature() -> None: 95 | global displaying_weather, temperature 96 | if displaying_weather == 0: 97 | display.set_panel("top", [(black,)*8]*8) 98 | try: 99 | # note: doesn't handle negative or > 100 100 | # really need a scrolling text function for a single panel! 101 | temperature = int(requests.get(current_weather_url, params).json()['main']['temp']) 102 | except: 103 | temperature = 0 104 | 105 | displaying_weather += refresh_rate 106 | if displaying_weather >= 5: # slight pause before clearing screen 107 | temperature = 0 108 | displaying_weather = 0 109 | display.set_panel("top", [(black,)*8]*8) 110 | 111 | 112 | def random_color() -> tuple[float, float]: 113 | return (random.randint(0, 100)/100, random.randint(0, 100)/100) 114 | 115 | 116 | 117 | tall_font = [ 118 | 0b1011111111, 0b1111011111, 0b1011111111, 119 | 0b1100111011, 0b0100000000, 0b1011100111, 120 | 0b1000111011, 0b0100000000, 0b1011100111, 121 | 0b1011111011, 0b0111111011, 0b1011111111, 122 | 0b1010001010, 0b0100000000, 0b1001111111, 123 | 0b1010001010, 0b0100000000, 0b1001111111, 124 | 0b1010001010, 0b0100000000, 0b1001111111, 125 | 0b1111011011, 0b1111011011, 0b1111111111 126 | ] 127 | big_font = [ 128 | [ 129 | [0,1,1,1,1,1,1,0], 130 | [1,1,1,1,1,1,1,1], 131 | [1,1,1,0,0,1,1,1], 132 | [1,1,1,0,0,1,1,1], 133 | [1,1,1,0,0,1,1,1], 134 | [1,1,1,0,0,1,1,1], 135 | [1,1,1,1,1,1,1,1], 136 | [0,1,1,1,1,1,1,0], 137 | ], 138 | [ 139 | [0,0,0,1,1,1,0,0], 140 | [0,0,1,1,1,1,0,0], 141 | [0,1,1,1,1,1,0,0], 142 | [0,0,0,1,1,1,0,0], 143 | [0,0,0,1,1,1,0,0], 144 | [0,0,0,1,1,1,0,0], 145 | [0,0,0,1,1,1,0,0], 146 | [0,0,0,1,1,1,0,0], 147 | ], 148 | [ 149 | [0,1,1,1,1,1,1,0], 150 | [1,1,1,1,1,1,1,1], 151 | [1,1,1,0,0,1,1,1], 152 | [0,0,0,0,1,1,1,0], 153 | [0,0,1,1,1,0,0,0], 154 | [1,1,1,0,0,0,0,0], 155 | [1,1,1,1,1,1,1,1], 156 | [1,1,1,1,1,1,1,1], 157 | ], 158 | [ 159 | [1,1,1,1,1,1,1,0], 160 | [1,1,1,1,1,1,1,1], 161 | [0,0,0,0,0,1,1,1], 162 | [1,1,1,1,1,1,1,0], 163 | [1,1,1,1,1,1,1,0], 164 | [0,0,0,0,0,1,1,1], 165 | [1,1,1,1,1,1,1,1], 166 | [1,1,1,1,1,1,1,0], 167 | ], 168 | [ 169 | [1,1,1,0,1,1,1,0], 170 | [1,1,1,0,1,1,1,0], 171 | [1,1,1,0,1,1,1,0], 172 | [1,1,1,1,1,1,1,1], 173 | [1,1,1,1,1,1,1,1], 174 | [0,0,0,0,1,1,1,0], 175 | [0,0,0,0,1,1,1,0], 176 | [0,0,0,0,1,1,1,0], 177 | ], 178 | [ 179 | [1,1,1,1,1,1,1,1], 180 | [1,1,1,1,1,1,1,1], 181 | [1,1,1,0,0,0,0,0], 182 | [1,1,1,1,1,1,1,0], 183 | [1,1,1,1,1,1,1,1], 184 | [0,0,0,0,0,1,1,1], 185 | [1,1,1,1,1,1,1,1], 186 | [0,1,1,1,1,1,1,0], 187 | ], 188 | [ 189 | [0,1,1,1,1,1,1,0], 190 | [1,1,1,1,1,1,1,1], 191 | [1,1,1,0,0,0,0,0], 192 | [1,1,1,1,1,1,1,0], 193 | [1,1,1,1,1,1,1,1], 194 | [1,1,1,0,0,1,1,1], 195 | [1,1,1,1,1,1,1,1], 196 | [0,1,1,1,1,1,1,0], 197 | ], 198 | [ 199 | [1,1,1,1,1,1,1,1], 200 | [1,1,1,1,1,1,1,1], 201 | [0,0,0,0,0,1,1,1], 202 | [0,0,0,0,1,1,1,0], 203 | [0,0,0,1,1,1,0,0], 204 | [0,0,1,1,1,0,0,0], 205 | [0,0,1,1,1,0,0,0], 206 | [0,0,1,1,1,0,0,0], 207 | ], 208 | [ 209 | [0,1,1,1,1,1,1,0], 210 | [1,1,1,1,1,1,1,1], 211 | [1,1,1,0,0,1,1,1], 212 | [0,1,1,1,1,1,1,0], 213 | [0,1,1,1,1,1,1,0], 214 | [1,1,1,0,0,1,1,1], 215 | [1,1,1,1,1,1,1,1], 216 | [0,1,1,1,1,1,1,0], 217 | ], 218 | [ 219 | [0,1,1,1,1,1,1,0], 220 | [1,1,1,1,1,1,1,1], 221 | [1,1,1,0,0,1,1,1], 222 | [1,1,1,1,1,1,1,1], 223 | [0,1,1,1,1,1,1,1], 224 | [0,0,0,0,0,1,1,1], 225 | [1,1,1,1,1,1,1,1], 226 | [0,1,1,1,1,1,1,0], 227 | ] 228 | ] 229 | 230 | 231 | display.set_all(black) 232 | rand_indexes = [x for x in range(64)] 233 | random.shuffle(rand_indexes) 234 | colors = get_seconds_colors() 235 | reset_indexes = False 236 | displaying_weather = 0 237 | temperature = 0 238 | time_now = datetime.datetime.now() 239 | 240 | recent_hour = (time_now.hour - 1, time_now.hour, time_now.hour - 12) # starting hours to not chime on 241 | recent_minute = time_now.minute - 1 242 | try: 243 | while True: 244 | if buttons.middle_pressed: 245 | use_random_dots_for_seconds = not use_random_dots_for_seconds 246 | if buttons.bottom_pressed: 247 | use_24_hour_clock = not use_24_hour_clock 248 | 249 | leds = {(x, y):0 for x in range(16) for y in range(16)} 250 | max_brightness = max(min_brightness, min(1, light_sensor.ambient_light / min_ambient_light_for_max_brightness)) 251 | 252 | time_now = datetime.datetime.now() 253 | 254 | # set hour digits for left panel 255 | latest_hour = ampm_hour(time_now.hour) if not use_24_hour_clock else time_now.hour 256 | # change colours of hour if optioned and hour has changed 257 | if latest_hour not in recent_hour: 258 | if chime_on_hour: 259 | chime(time_now.hour) 260 | if dictate_on_hour: 261 | engine.say(f"{ampm_hour(time_now.hour)} o clock") 262 | engine.runAndWait() 263 | #speaker.say(f"{ampm_hour(time_now.hour)} o clock") 264 | 265 | if use_random_hour_colors: 266 | hour_color_hue, hour_color_saturation = random_color() 267 | recent_hour = (latest_hour, latest_hour - 12) # record both, don't want it to chime when changing to/from 24hrs 268 | 269 | if not prefer_double_digits and latest_hour < 10: 270 | for y in range(8): 271 | for x in range(8): 272 | pixel_on = big_font[latest_hour][7 - y][x] == 1 273 | leds[x, y] = hsv_colour(hour_color_hue, hour_color_saturation, max_brightness) if pixel_on else 0 274 | else: 275 | draw_double_digit_number(leds, latest_hour, 1, 0, hsv_colour(hour_color_hue, hour_color_saturation, max_brightness)) 276 | 277 | # set minutes digits for right panel 278 | # change colours of minute if optioned and minute has changed 279 | if time_now.minute != recent_minute: 280 | if time_now.minute == 30: 281 | if chime_on_half_hour: 282 | chime(time_now.hour) 283 | if dictate_on_half_hour: 284 | engine.say(f"{ampm_hour(time_now.hour)} thirty") 285 | engine.runAndWait() 286 | #speaker.say(f"{ampm_hour(time_now.hour)} thirty") 287 | 288 | if use_random_minute_colors: 289 | minute_color_hue, minute_color_saturation = random_color() 290 | recent_minute = time_now.minute 291 | draw_double_digit_number(leds, time_now.minute, 9, 0, hsv_colour(minute_color_hue, minute_color_saturation, max_brightness)) 292 | 293 | # handle button press - if pressed, get weather and set the top panel digits to temperature 294 | if enable_check_temperature_button and (buttons.top_pressed or displaying_weather != 0): 295 | get_temperature() 296 | draw_double_digit_number(leds, temperature, 1, 8, hsv_colour(second_color_hue, second_color_saturation, max_brightness)) 297 | 298 | # if top panel isn't currently displaying weather, set seconds for top panel 299 | if not displaying_weather: 300 | if not use_random_dots_for_seconds: 301 | draw_double_digit_number(leds, time_now.second, 1, 8, hsv_colour(second_color_hue, second_color_saturation, max_brightness)) 302 | else: 303 | if (time_now.second == 0 and not reset_indexes): 304 | random.shuffle(rand_indexes) 305 | reset_indexes = True 306 | colors = get_seconds_colors() 307 | elif time_now.second != 0: 308 | reset_indexes = False 309 | 310 | percent_seconds = ((time_now.second * 1000000) + time_now.microsecond) / 60000000 311 | how_lit = (percent_seconds * 64) 312 | 313 | for y in range(8): 314 | for x in range(8): 315 | led_index = (y * 8) + x 316 | hue, saturation = colors[led_index] 317 | if how_lit >= led_index: 318 | diff = how_lit - led_index 319 | plot_x = rand_indexes[led_index] % 8 320 | plot_y = rand_indexes[led_index] // 8 321 | percent_lit = min(1, diff) 322 | adjusted_lit = max_brightness * percent_lit 323 | leds[plot_x, plot_y + 8] = hsv_colour(hue, saturation, adjusted_lit) 324 | 325 | display.set_leds(leds) 326 | time.sleep(refresh_rate) 327 | except Exception: 328 | print(traceback.format_exc()) 329 | screen.draw_rectangle(0, 0, 320, 240, black) 330 | screen.write_text(0, 0, traceback.format_exc(), 1, black, white) 331 | -------------------------------------------------------------------------------- /community-scripts/digital_rain.py: -------------------------------------------------------------------------------- 1 | # Digital Rain (matrix screen effect) 2 | # v2.0 3 | # builds 8 random streams which "rain" down from the top to the sides. 4 | # supports v1 mode (rain down from the top to the left side, overflow from left onto right) or 5 | # v2 mode (rain down diagonally from the top, over both sides) 6 | # set Diagonally to true or false as you prefer. 7 | # Author: Kevin Haney 8 | # Date: 10/10/2022 9 | # history: 10 | # - 1.0 - original (v1) 11 | # - 2.0 - diagonal scrolling added (v2) 12 | # - 2.1 - some code cleanup and fixed right scrolling 13 | 14 | import random 15 | 16 | # set to True to rain down diagonally from the top (v2 mode), False to rain down to the left (v1) 17 | Diagonally = True 18 | 19 | class Point: 20 | def __init__(self, column, row, hue, saturation, brightness): 21 | self.column = column 22 | self.row = row 23 | self.hue = self._check(hue) 24 | self.color = black 25 | self.saturation = self._check(saturation) 26 | self.brightness = self._check(brightness) 27 | 28 | def _check(self,value): 29 | if value > 1: 30 | return 1 31 | elif value < 0: 32 | return 0 33 | else: 34 | return value 35 | 36 | class Stream: 37 | def __init__(self, column, saturation, brightness, speed, length): 38 | self.column = column 39 | self.saturation = saturation 40 | self.brightness = brightness 41 | self.position = 0 42 | self.speed = speed 43 | self.length = length 44 | self.pause = 0 45 | 46 | self.points = [] 47 | 48 | # brightness tapers off after 1/2 49 | bdelta = brightness / (self.length / 2) 50 | 51 | # front pad a random number of cells so they don't all "start" at the same time 52 | pad = random.randrange(5,12) 53 | 54 | for row in reversed(range (0,self.length)): 55 | sat = self.saturation + random.randrange(-15,16)/100 56 | if row > (self.length / 2) - 2: 57 | adjust = (row/2)*bdelta 58 | bright = self.brightness - adjust 59 | if bright < 0: 60 | bright = 0 61 | else: 62 | bright = self.brightness + random.randrange(-25,26)/100 63 | hue = .3 # green 64 | if (row == pad): 65 | hue = .15 66 | if row < pad: 67 | bright = 0 68 | 69 | self.points.append(Point(column,row,hue,sat,bright)) 70 | 71 | # grab a window of 16/24 points to display, moving from bottom to top 72 | # not crazy how this abruplty ends the stream though - I'd like those to scroll off too 73 | def rain(self): 74 | update = False 75 | if self.pause == 0: 76 | self.position += 1 77 | self.pause = self.speed 78 | update = True 79 | else: 80 | self.pause -= 1 81 | if self.position >= self.length: 82 | self.position = 1 83 | first = self.length - self.position 84 | last = first + self.position - 1 85 | lastRow = 23 86 | if Diagonally: 87 | lastRow = 15 88 | if last - first > lastRow: 89 | last = first + lastRow 90 | if update: 91 | row = 0 92 | for i in range(first,last+1): 93 | p = self.points[i] 94 | p.row = row 95 | # random adjustment of sat and brighness? given the motion it seems like overkill 96 | sat = p.saturation # + random.randrange(-15,16)/100 97 | bright = p.brightness # + random.randrange(-25,26)/100 98 | p.color = hsv_colour(p.hue, sat, bright) 99 | row += 1 100 | 101 | return self.points[first:last+1] 102 | 103 | def map(point): 104 | x = point.column 105 | y = 15-point.row 106 | # left screen overflow onto right screen 107 | if point.row > 15: 108 | x = x + 8 109 | y = y + 8 110 | return x, y 111 | 112 | def mapV2(point,lookup): 113 | # left 114 | if (point.column < 7 or point.column == 15) and point.row > 7: 115 | x = point.column + 1 116 | if (point.column == 15): 117 | x = 0 118 | y = 15 - point.row 119 | # right 120 | elif (point.column > 6 and point.column < 15) and point.row > 7: 121 | x = point.column 122 | if x == 7: 123 | x += 8 # throw the center stream over to the far right 124 | y = 15 - point.row 125 | else: 126 | if (point.column, point.row) in lookup: 127 | l = lookup[(point.column,point.row)] 128 | #print(l) 129 | x = l[0] 130 | y = l[1] 131 | else: 132 | x = -1 133 | y = -1 134 | 135 | return x, y 136 | 137 | def BuildLookup(): 138 | lookup = { 139 | (0,7) : (0,8), (1,6) : (0,9), (2,5) : (0,10), (3,4) : (0,11), (4,3) : (0,12), (5,2) : (0,13), (6,1) : (0,14), (7,0) : (0,15), 140 | (1,7) : (1,8), (2,6) : (1,9), (3,5) : (1,10), (4,4) : (1,11), (5,3) : (1,12), (6,2) : (1,13), (7,1) : (1,14), (8,1) : (1,15), 141 | (2,7) : (2,8), (3,6) : (2,9), (4,5) : (2,10), (5,4) : (2,11), (6,3) : (2,12), (7,2) : (2,13), (8,2) : (2,14), (9,2) : (2,15), 142 | (3,7) : (3,8), (4,6) : (3,9), (5,5) : (3,10), (6,4) : (3,11), (7,3) : (3,12), (8,3) : (3,13), (9,3) : (3,14), (10,3) : (3,15), 143 | (4,7) : (4,8), (5,6) : (4,9), (6,5) : (4,10), (7,4) : (4,11), (8,4) : (4,12), (9,4) : (4,13), (10,4) : (4,14), (11,4) : (4,15), 144 | (5,7) : (5,8), (6,6) : (5,9), (7,5) : (5,10), (8,5) : (5,11), (9,5) : (5,12), (10,5) : (5,13), (11,5) : (5,14), (12,5) : (5,15), 145 | (6,7) : (6,8), (7,6) : (6,9), (8,6) : (6,10), (9,7) : (6,11), (10,6) : (6,12), (11,6) : (6,13), (12,6) : (6,14), (13,6) : (6,15), 146 | (7,7) : (7,8), (8,7) : (7,9), (9,7) : (7,10), (10,7) : (7,11), (11,7) : (7,12), (12,7) : (7,13), (13,7) : (7,14), (14,7) : (7,15) 147 | } 148 | return lookup 149 | 150 | 151 | def LedDictionary(): 152 | leds = {} 153 | for x in range(0,16): 154 | for y in range(0,16): 155 | if x < 8 or (x > 7 and y < 8): 156 | leds[x,y] = 0 157 | return leds 158 | 159 | # main code 160 | 161 | display.set_all(black) 162 | 163 | lookup = BuildLookup() 164 | 165 | maxStreams = 8 166 | if Diagonally: 167 | maxStreams = 16 168 | 169 | streams = [] 170 | for s in range(0,maxStreams): 171 | streams.append(Stream(s, random.randrange(45,66)/100, random.randrange(35,86)/100, random.randrange(0,3), random.randrange(32,43))) 172 | 173 | while True: 174 | # need a pre-ordered dictionary of points because I'm not setting them in an order the cube code likes (unordered = crash!) 175 | leds = LedDictionary() 176 | for stream in streams: 177 | drops = stream.rain() 178 | for d in drops: 179 | if Diagonally: 180 | m = mapV2(d,lookup) 181 | else: 182 | m = map(d) 183 | if m[0] != -1: 184 | #print ("v2 map from",d.column,d.row,"to",m[0],m[1]) 185 | leds[m[0], m[1]] = d.color 186 | 187 | display.set_leds(leds) 188 | time.sleep(.25) -------------------------------------------------------------------------------- /community-scripts/display_helpers.py: -------------------------------------------------------------------------------- 1 | # Display helper code 2 | # Author : Kevin Haney 3 | # Date : 11/23/2922 4 | # Version : 1.0 5 | # 6 | from string import whitespace 7 | 8 | # digits are defined as bits on in a 3x5 grid, 0,0 =bottomleft 9 | digitAll = [ (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2), (3,0), (3,1), (3,2), (4,0), (4,1), (4,2)] 10 | digit = [[ (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,2), (3,0), (3,2), (4,0), (4,1), (4,2)], 11 | [ (0,1), (1,1), (2,1), (3,1), (4,1) ], 12 | [ (0,0), (0,1), (0,2), (1,0), (2,0), (2,1), (2,2), (3,2), (4,0), (4,1), (4,2)], 13 | [ (0,0), (0,1), (0,2), (1,2), (2,0), (2,1), (2,2), (3,2), (4,0), (4,1), (4,2)], 14 | [ (0,2), (1,2), (2,0), (2,1), (2,2), (3,0), (3,2), (4,0), (4,2)], 15 | [ (0,0), (0,1), (0,2), (1,2), (2,0), (2,1), (2,2), (3,0), (4,0), (4,1), (4,2)], 16 | [ (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2), (3,0), (4,0) ], 17 | [ (0,2), (1,2), (2,2), (3,2), (4,0), (4,1), (4,2)], 18 | [ (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2), (3,0), (3,2), (4,0), (4,1), (4,2)], 19 | [ (0,2), (1,2), (2,0), (2,1), (2,2), (3,0), (3,2), (4,0), (4,1), (4,2)]] 20 | # x and y from bottom left 21 | leftOnesLeds = [[(5,2),(6,2),(7,2)],[(5,3),(6,3),(7,3)],[(5,4),(6,4),(7,4)],[(5,5),(6,5),(7,5)],[(5,6),(6,6),(7,6)]] 22 | leftTensLeds = [[(1,2),(2,2),(3,2)],[(1,3),(2,3),(3,3)],[(1,4),(2,4),(3,4)],[(1,5),(2,5),(3,5)],[(1,6),(2,6),(3,6)]] 23 | # z and y from bottom left 24 | rightOnesLeds = [[(2,2),(1,2),(0,2)],[(2,3),(1,3),(0,3)],[(2,4),(1,4),(0,4)],[(2,5),(1,5),(0,5)],[(2,6),(1,6),(0,6)]] 25 | rightTensLeds = [[(6,2),(5,2),(4,2)],[(6,3),(5,3),(4,3)],[(6,4),(5,4),(4,4)],[(6,5),(5,5),(4,5)],[(6,6),(5,6),(4,6)]] 26 | 27 | def clear_left(all_leds, background_color): 28 | for x in range(9): 29 | for y in range(9): 30 | all_leds[(x,y,8)] = background_color 31 | 32 | def clear_right(all_leds, background_color): 33 | for y in range(9): 34 | for z in range(9): 35 | all_leds[(8,y,z)] = background_color 36 | 37 | def set_left(all_leds, value_leds, value_color, background_color, clear=False): 38 | if clear: 39 | clear_left(all_leds, background_color) 40 | for v in value_leds: 41 | all_leds[(v[0],v[1],v[2])] = value_color 42 | 43 | def set_right(all_leds, value_leds, value_color, background_color, clear=False): 44 | if clear: 45 | clear_right(all_leds, background_color) 46 | for v in value_leds: 47 | all_leds[(v[0],v[1],v[2])] = value_color 48 | 49 | def clear_digits(all_leds, side, background): 50 | if side == 'right': 51 | clear_right(all_leds, background) 52 | else: 53 | clear_left(all_leds, background) 54 | 55 | def set_digits(all_leds, side, total, color, background): 56 | clear_digits(all_leds, side, background) 57 | if total == None: 58 | return 59 | 60 | ones = total % 10 61 | tens = total // 10 62 | shift = 0 63 | if tens == 0: 64 | shift = 1 65 | 66 | if side == 'right': 67 | onesLeds = rightOnesLeds 68 | tensLeds = rightTensLeds 69 | else: 70 | onesLeds = leftOnesLeds 71 | tensLeds = leftTensLeds 72 | 73 | for point in range(len(digit[ones])): 74 | pair = onesLeds[digit[ones][point][0]][digit[ones][point][1]] 75 | h = pair[0] 76 | v = pair[1] 77 | if side == 'right': 78 | all_leds[(8,v,h+shift)] = color 79 | else: 80 | all_leds[(h-shift,v,8)] = color 81 | 82 | if tens > 0: 83 | for point in range(len(digit[tens])): 84 | pair = tensLeds[digit[tens][point][0]][digit[tens][point][1]] 85 | h = pair[0] 86 | v = pair[1] 87 | if side == 'right': 88 | all_leds[(8,v,h+shift)] = color 89 | else: 90 | all_leds[(h-shift,v,8)] = color 91 | 92 | if __name__ == "__main__": 93 | display.set_all(black) 94 | 95 | # initialize our led dictionary 96 | leds = {} 97 | for x in range(9): 98 | for y in range(9): 99 | for z in range(9): 100 | leds[(x,y,z)] = black 101 | 102 | display.set_3d(leds) 103 | 104 | for t in range(100): 105 | set_digits(leds,'right',t,white,orange) 106 | set_digits(leds,'left',99-t,white,blue) 107 | display.set_3d(leds) 108 | time.sleep(.1) 109 | -------------------------------------------------------------------------------- /community-scripts/game_of_life_clock.py: -------------------------------------------------------------------------------- 1 | # Every minute display the time - then run Conway's game of life on the digits 2 | 3 | import random 4 | import datetime 5 | 6 | 7 | tall_font = [ 8 | 0b1011111111, 0b1111011111, 0b1011111111, 9 | 0b1100111011, 0b0100000000, 0b1011100111, 10 | 0b1000111011, 0b0100000000, 0b1011100111, 11 | 0b1011111011, 0b0111111011, 0b1011111111, 12 | 0b1010001010, 0b0100000000, 0b1001111111, 13 | 0b1010001010, 0b0100000000, 0b1001111111, 14 | 0b1010001010, 0b0100000000, 0b1001111111, 15 | 0b1111011011, 0b1111011011, 0b1111111111 16 | ] 17 | 18 | def draw_number(leds: dict, number: int, x_offset: int, y_offset: int, color: int) -> None: 19 | mask = pow(2, 9 - number) 20 | for y in range(8): 21 | for x in range(3): 22 | tf_index = (21 - (y * 3)) + (x % 3) 23 | tf_value = tall_font[tf_index] 24 | bit_on = bool(mask & tf_value) 25 | leds[x + x_offset, y + y_offset] = color if bit_on else 0 26 | 27 | 28 | def draw_double_digit_number(leds: dict, number: int, x_offset: int, y_offset: int, color: int) -> None: 29 | draw_number(leds, number // 10, x_offset, y_offset, color) 30 | draw_number(leds, number % 10, x_offset + 4, y_offset, color) 31 | 32 | 33 | while True: 34 | max_turns = 50 35 | colour = random_colour() 36 | num_turns = 0 37 | 38 | # Set the digits 39 | time_now = datetime.datetime.now() 40 | display.set_all(black) 41 | leds = {} 42 | draw_double_digit_number(leds, time_now.hour, 1,0, colour) 43 | display.set_leds(leds) 44 | leds2 = {} 45 | draw_double_digit_number(leds2, time_now.minute, 9,0, colour) 46 | display.set_leds(leds2) 47 | time.sleep(5) 48 | 49 | alive_cells = [] 50 | for pos in leds.keys(): 51 | alive_cells.append(pos) 52 | for pos in leds2.keys(): 53 | alive_cells.append(pos) 54 | 55 | def num_alive_neighbours(x, y): 56 | num_neighbours = 0 57 | for x2 in [x-1, x, x+1]: 58 | for y2 in [y-1, y, y+1]: 59 | if (x2, y2) != (x, y): 60 | neighbour = (x2, y2) 61 | # Account for 3D nature of panels 62 | if x2 == 8 and y2 >= 8: 63 | neighbour = (y2, 7) 64 | elif y2 == 8 and x2 >= 8: 65 | neighbour = (7, x2) 66 | if neighbour in alive_cells: 67 | num_neighbours += 1 68 | return num_neighbours 69 | 70 | # Run Game of Life 71 | 72 | seconds = 0 73 | while (seconds < 60.0): 74 | next_cells = [] 75 | leds = {} 76 | for x in range(0,16): 77 | for y in range(0,16): 78 | if x < 8 or y < 8: 79 | alive = (x, y) in alive_cells 80 | neighbours = num_alive_neighbours(x, y) 81 | # Remains alive 82 | if alive and (neighbours == 2 83 | or neighbours == 3): 84 | next_cells.append((x, y)) 85 | leds[x, y] = colour 86 | # Becomes alive 87 | elif not alive and neighbours == 3: 88 | next_cells.append((x, y)) 89 | leds[x, y] = colour 90 | # Else dead 91 | else: 92 | leds[x, y] = black 93 | alive_cells = next_cells 94 | display.set_leds(leds) 95 | time.sleep(0.3) 96 | seconds += 0.3 97 | num_turns += 1 98 | -------------------------------------------------------------------------------- /community-scripts/game_of_life_phased.py: -------------------------------------------------------------------------------- 1 | # Run Conway's Game of Life (https://en.wikipedia.org/wiki/Conway's_Game_of_Life) 2 | # based on the AbstractFoundry code 3 | # modified by Kirk Carlson 4 | # phased transitions between generations 5 | # allow generations to proceed to annihilation, stability or oscillation 6 | # while emphasizing the end 7 | 8 | 9 | #### IMPORTS #### 10 | import random 11 | 12 | #### FUNCTIONS #### 13 | 14 | # determine the number of alive neighbors 15 | def num_alive_neighbours(x, y): 16 | num_neighbours = 0 17 | for x2 in [x-1, x, x+1]: 18 | for y2 in [y-1, y, y+1]: 19 | if (x2, y2) != (x, y): 20 | neighbour = (x2, y2) 21 | # Account for 3D nature of panels 22 | if x2 == 8 and y2 >= 8: 23 | neighbour = (y2, 7) 24 | elif y2 == 8 and x2 >= 8: 25 | neighbour = (7, x2) 26 | if neighbour in alive_cells: 27 | num_neighbours += 1 28 | return num_neighbours 29 | 30 | 31 | # determine if two lists are equivalent 32 | def is_list_equal( list1, list2): 33 | if len( list1) != len( list2): 34 | return False 35 | for item in list1: 36 | if item not in list2: 37 | return False 38 | return True 39 | 40 | 41 | 42 | #### CONFIGURATION CONSTANTS #### 43 | starting_live_ratio = 0.4 44 | guard_hue = 0.1 # portion of full circle 45 | allowed_loops = 3 46 | maximum_generations = 500 47 | base_saturation = 1 48 | base_luminance = 0.5 49 | 50 | 51 | #### CONSTANTS #### 52 | num_transitions = 5 # between state 53 | num_phases = 2 * num_transitions 54 | dead = 0 55 | alive = num_transitions 56 | dying = alive + 1 57 | spark = dead + 1 58 | 59 | #### MAIN LOOP #### 60 | # VARIABLES 61 | base_hue = 0 62 | num_generations = 0 63 | num_cells_start = 0 64 | num_cells_left = 0 65 | loop_length = 0 66 | past_loop_length = 0 67 | loop_count = 0 68 | while True: 69 | # loop initialization 70 | if loop_length > 0: 71 | print ("hue: %4.2f start cells %2s, generations: %3s, cells at end: %2s, loop length: %2s" % ( base_hue, num_cells_start, num_generations, num_cells_left, loop_length) ) 72 | base_hue = (base_hue + guard_hue + ( 1 - 2 * guard_hue) * random.random() ) % 1 73 | phaseColors = [ hsv_colour( base_hue, base_saturation, 0 * base_luminance), # dead 74 | hsv_colour( base_hue, base_saturation, 0.2 * base_luminance), # spark 75 | hsv_colour( base_hue, base_saturation, 0.4 * base_luminance), 76 | hsv_colour( base_hue, base_saturation, 0.6 * base_luminance), 77 | hsv_colour( base_hue, base_saturation, 0.8 * base_luminance), 78 | 79 | hsv_colour( base_hue, base_saturation, 1 * base_luminance), # alive 80 | hsv_colour( base_hue, base_saturation, 0.8 * base_luminance), # dying 81 | hsv_colour( base_hue, base_saturation, 0.6 * base_luminance), 82 | hsv_colour( base_hue, base_saturation, 0.4 * base_luminance), 83 | hsv_colour( base_hue, base_saturation, 0.2 * base_luminance) 84 | ] 85 | 86 | # seed the starting cells and build current_phases list of lists 87 | alive_cells = [] 88 | current_phases = [] 89 | for x in range( 16): 90 | column = [] 91 | for y in range( 16): 92 | if (x < 8 or y < 8) and random.random() < starting_live_ratio: 93 | alive_cells.append( (x,y)) 94 | column.append( spark) 95 | else: 96 | column.append( dead) 97 | current_phases.append( column) 98 | num_cells_start = len( alive_cells) 99 | 100 | num_generations = 0 101 | past_alives = [] 102 | is_looping = False 103 | loop_count = 0 104 | past_loop_length = 0 105 | fail_safe_count = maximum_generations 106 | while not is_looping: 107 | 108 | # transition from current phase to final phase of current state 109 | leds = {} 110 | for t in range ( num_transitions): 111 | for x in range( 16): 112 | for y in range( 16): 113 | if x < 8 or y < 8: 114 | phase = current_phases[ x][ y] 115 | leds[ x,y] = phaseColors [ phase] 116 | if phase != dead and phase != alive: 117 | current_phases[ x][ y] = (phase + 1) % num_phases 118 | display.set_leds( leds) 119 | time.sleep( 0.2) 120 | 121 | # determine if pattern is looping (too much) 122 | for count, past_alive_cells in enumerate( past_alives): 123 | if is_list_equal( alive_cells, past_alive_cells): # seen this before? 124 | loop_length = len( past_alives) - count 125 | if past_loop_length == 0: 126 | past_loop_length = loop_length 127 | loop_count = 1 128 | elif loop_length == past_loop_length: 129 | loop_count += 1 130 | if loop_count >= allowed_loops: 131 | is_looping = True # now we've seen enough 132 | if len( past_alives) == 0 or past_loop_length == 0: # first pass or no loop detected 133 | past_alives.append( alive_cells) 134 | num_generations += 1 135 | 136 | # determine the state and phase for the next generation 137 | next_cells = [] 138 | for x in range( 16): 139 | for y in range( 16): 140 | if x < 8 or y < 8: 141 | is_alive = (x, y) in alive_cells 142 | neighbours = num_alive_neighbours(x, y) 143 | 144 | if is_alive: 145 | if ( neighbours == 2 or neighbours == 3): 146 | next_cells.append( ( x, y)) 147 | phase = alive 148 | else: 149 | phase = dying 150 | elif neighbours == 3: 151 | phase = spark 152 | next_cells.append( ( x,y)) 153 | else: # dead 154 | phase = dead 155 | current_phases[ x][y] = phase 156 | 157 | num_cells_left = len( alive_cells) 158 | alive_cells = next_cells 159 | 160 | # stop VERY long non-repeating patterns 161 | fail_safe_count -= 1 162 | if fail_safe_count <= 0: 163 | print ("Fail safe triggered") 164 | is_looping = True 165 | -------------------------------------------------------------------------------- /community-scripts/gameday.py: -------------------------------------------------------------------------------- 1 | # Use the MLB api to "watch" a live game 2 | # Author : Kevin Haney 3 | # Date : 10/23/2922 4 | # Version : 1.0 5 | # 6 | from datetime import datetime, timedelta 7 | from readline import get_current_history_length 8 | from string import whitespace 9 | import sys 10 | sys.path.insert(0, '/home/pi/AbstractFoundry/Daemon/Scripts') 11 | import mlb 12 | 13 | ballLeds = [ (1,8,4), (2,8,3), (3,8,2), (4,8,1) ] 14 | strikeLeds = [ (3,8,5), (4,8,4), (5,8,3) ] 15 | outLeds = [ (5,8,7), (6,8,6), (7,8,5) ] 16 | inningBotLeds = [ (0,0,8),(1,0,8),(2,0,8),(3,0,8),(4,0,8),(5,0,8),(6,0,8),(7,0,8),(8,0,7),(8,0,6),(8,0,5),(8,0,4),(8,0,3),(8,0,2),(8,0,1),(8,0,0) ] # up to 16 17 | inningTopLeds = [ (0,1,8),(1,1,8),(2,1,8),(3,1,8),(4,1,8),(5,1,8),(6,1,8),(7,1,8),(8,1,7),(8,1,6),(8,1,5),(8,1,4),(8,1,3),(8,1,2),(8,1,1),(8,1,0) ] # up to 16 18 | firstBaseLeds = [ (6,8,0), (6,8,1), (7,8,0), (7,8,1) ] 19 | secondBaseLeds = [ (0,8,0), (0,8,1), (1,8,0), (1,8,1) ] 20 | thirdBaseLeds = [ (0,8,6), (0,8,7), (1,8,6), (1,8,7) ] 21 | # digits are defined as bits on in a 3x5 grid, 0,0 =bottomleft 22 | digitAll = [ (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2), (3,0), (3,1), (3,2), (4,0), (4,1), (4,2)] 23 | digit = [[ (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,2), (3,0), (3,2), (4,0), (4,1), (4,2)], 24 | [ (0,1), (1,1), (2,1), (3,1), (4,1) ], 25 | [ (0,0), (0,1), (0,2), (1,0), (2,0), (2,1), (2,2), (3,2), (4,0), (4,1), (4,2)], 26 | [ (0,0), (0,1), (0,2), (1,2), (2,0), (2,1), (2,2), (3,2), (4,0), (4,1), (4,2)], 27 | [ (0,2), (1,2), (2,0), (2,1), (2,2), (3,0), (3,2), (4,0), (4,2)], 28 | [ (0,0), (0,1), (0,2), (1,2), (2,0), (2,1), (2,2), (3,0), (4,0), (4,1), (4,2)], 29 | [ (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2), (3,0), (4,0) ], 30 | [ (0,2), (1,2), (2,2), (3,2), (4,0), (4,1), (4,2)], 31 | [ (0,0), (0,1), (0,2), (1,0), (1,2), (2,0), (2,1), (2,2), (3,0), (3,2), (4,0), (4,1), (4,2)], 32 | [ (0,2), (1,2), (2,0), (2,1), (2,2), (3,0), (3,2), (4,0), (4,1), (4,2)]] 33 | # x and y from bottom left 34 | leftScoreOnesLeds = [[(4,3),(5,3),(6,3)],[(4,4),(5,4),(6,4)],[(4,5),(5,5),(6,5)],[(4,6),(5,6),(6,6)],[(4,7),(5,7),(6,7)]] 35 | leftScoreTensLeds = [[(0,3),(1,3),(2,3)],[(0,4),(1,4),(2,4)],[(0,5),(1,5),(2,5)],[(0,6),(1,6),(2,6)],[(0,7),(1,7),(2,7)]] 36 | # z and y from bottom left 37 | rightScoreOnesLeds = [[(3,3),(2,3),(1,3)],[(3,4),(2,4),(1,4)],[(3,5),(2,5),(1,5)],[(3,6),(2,6),(1,6)],[(3,7),(2,7),(1,7)]] 38 | rightScoreTensLeds = [[(7,3),(6,3),(5,3)],[(7,4),(6,4),(5,4)],[(7,5),(6,5),(5,5)],[(7,6),(6,6),(5,6)],[(7,7),(6,7),(5,7)]] 39 | 40 | baseOff = white 41 | baseOn = hsv_colour(.15,1,1) 42 | ballOff = hsv_colour(.6,.5,.35) 43 | ballOn = hsv_colour(.6,1,1) 44 | strikeOff = hsv_colour(0,.5,.35) 45 | strikeOn = hsv_colour(0,1,1) 46 | outOff = hsv_colour(.15,.5,.35) 47 | outOn = hsv_colour(.15,1,1) 48 | 49 | def clear_score(leds, home): 50 | if home: 51 | for y in range(3,8): 52 | for z in range(8): 53 | leds[(8,y,z)] = black 54 | else: 55 | for x in range(8): 56 | for y in range(3,8): 57 | leds[(x,y,8)] = black 58 | 59 | def set_score(leds, home, score): 60 | clear_score(leds, home) 61 | 62 | ones = score % 10 63 | tens = score // 10 64 | shift = 0 65 | if tens == 0: 66 | shift = 1 67 | 68 | if home: 69 | onesLeds = rightScoreOnesLeds 70 | tensLeds = rightScoreTensLeds 71 | else: 72 | onesLeds = leftScoreOnesLeds 73 | tensLeds = leftScoreTensLeds 74 | 75 | for point in range(len(digit[ones])): 76 | pair = onesLeds[digit[ones][point][0]][digit[ones][point][1]] 77 | h = pair[0] 78 | v = pair[1] 79 | if home: 80 | leds[(8,v,h+shift)] = white 81 | else: 82 | leds[(h-shift,v,8)] = white 83 | 84 | if tens > 0: 85 | for point in range(len(digit[tens])): 86 | pair = tensLeds[digit[tens][point][0]][digit[tens][point][1]] 87 | h = pair[0] 88 | v = pair[1] 89 | if home: 90 | leds[(8,v,h+shift)] = white 91 | else: 92 | leds[(h-shift,v,8)] = white 93 | 94 | def initialize_display(leds): 95 | #bases 96 | for x in range(0,2): 97 | for z in range(0,2): 98 | leds[(x,8,z)] = baseOff 99 | for z in range(6,8): 100 | leds[(x,8,z)] = baseOff 101 | for x in range(6,8): 102 | for z in range(0,2): 103 | leds[(x,8,z)] = baseOff 104 | #balls ("off") 105 | for led in ballLeds: 106 | leds[led] = ballOff 107 | #strikes ("off") 108 | for led in strikeLeds: 109 | leds[led] = strikeOff 110 | #outs ("off") 111 | for led in outLeds: 112 | leds[led] = outOff 113 | #inning ("off") 114 | for led in inningTopLeds: 115 | leds[led] = black 116 | for led in inningBotLeds: 117 | leds[led] = black 118 | 119 | display.set_3d(leds) 120 | 121 | def score_to_color(score): 122 | if score == 0: 123 | return white 124 | elif score == 1: 125 | return yellow 126 | elif score == 2: 127 | return orange 128 | elif score == 3: 129 | return green 130 | else: 131 | return blue 132 | 133 | def set_game_state(liveData, leds): 134 | if liveData.status == "In Progress": 135 | #balls 136 | for b in range(len(ballLeds)): 137 | leds[ballLeds[b]] = ballOff 138 | for b in range(liveData.balls): 139 | leds[ballLeds[b]] = ballOn 140 | #strikes 141 | for s in range(len(strikeLeds)): 142 | leds[strikeLeds[s]] = strikeOff 143 | for s in range(liveData.strikes): 144 | leds[strikeLeds[s]] = strikeOn 145 | #outs 146 | for o in range(len(outLeds)): 147 | leds[outLeds[o]] = outOff 148 | for o in range(liveData.outs): 149 | leds[outLeds[o]] = outOn 150 | #bases 151 | for b in range(4): 152 | color = baseOn if liveData.onFirst else baseOff 153 | leds[firstBaseLeds[b]] = color 154 | color = baseOn if liveData.onSecond else baseOff 155 | leds[secondBaseLeds[b]] = color 156 | color = baseOn if liveData.onThird else baseOff 157 | leds[thirdBaseLeds[b]] = color 158 | #box score 159 | lineScore = liveData.lineScore 160 | #print(lineScore) 161 | for i in range(len(lineScore.innings)): 162 | inning = lineScore.innings[i] 163 | leds[inningTopLeds[i]] = score_to_color(inning.awayScore) 164 | botColor = score_to_color(inning.homeScore) 165 | if i+1 == lineScore.currentInning and lineScore.isTopInning: 166 | botColor = black 167 | leds[inningBotLeds[i]] = botColor 168 | 169 | #scores 170 | set_score(leds,False,liveData.awayScore) 171 | set_score(leds,True,liveData.homeScore) 172 | 173 | display.set_3d(leds) 174 | 175 | if __name__ == "__main__": 176 | date = datetime.now().strftime("%m/%d/%Y") 177 | #can try different dates to see upcoming games, or past games (will show final score and boxscore) 178 | #date = (datetime.now() + timedelta(days=-1)).strftime("%m/%d/%Y") 179 | #date = "10/23/2022" # last NLCS ALCS games 180 | #date = "10/29/2022" 181 | 182 | #To watch a game, put the name here (should match what is listed in the games listing) If they're playing, it'll watch the game when it starts 183 | watchTeam = 'Philadelphia Phillies' 184 | watchPk = 0 185 | 186 | display.set_all(black) 187 | 188 | # initialize our led dictionary 189 | leds = {} 190 | for x in range(9): 191 | for y in range(9): 192 | for z in range(9): 193 | leds[(x,y,z)] = black 194 | 195 | initialize_display(leds) 196 | 197 | games = mlb.get_games_on_date(date) 198 | if len(games) == 0: 199 | message = "No games scheduled for " + date 200 | print(message) 201 | display.scroll_text(message) 202 | 203 | for gamePk, game in games.items(): 204 | startTime = datetime.strftime(game.startTime,'%-I:%m') 205 | print(game.away.name,"at",game.home.name,startTime) 206 | liveData = mlb.LiveData(gamePk) 207 | print(liveData) 208 | if game.home.name == watchTeam or game.away.name == watchTeam: 209 | watchPk = gamePk 210 | 211 | if watchPk > 0: 212 | while liveData.status != "Final": 213 | liveData = mlb.LiveData(watchPk) 214 | if liveData.status == "Scheduled" or liveData.status == "Pre-Game" or liveData.status == "Warmup": 215 | message = liveData.status + " " + game.away.abbreviation + " @ " + game.home.abbreviation + " " + startTime 216 | print(message) 217 | display.scroll_text(message, speed=.5) 218 | time.sleep(300) 219 | if liveData.status == "In Progress": 220 | #uncomment this to see updates in the console during play 221 | #print(liveData) 222 | set_game_state(liveData, leds) 223 | #try not to make this too low. 10 is reasonable. abusing the API may get you blocked! 224 | time.sleep(15) 225 | 226 | if liveData.status == "Final": 227 | set_game_state(liveData, leds) 228 | print("done") 229 | time.sleep(10) 230 | -------------------------------------------------------------------------------- /community-scripts/glitter.py: -------------------------------------------------------------------------------- 1 | # Note: make sure to put the brightness back down after running this script 2 | display.brightness = 250 3 | 4 | brightness = {} 5 | def glitter_shader(x, y, frame): 6 | if x %2 == 0 or y % 2 == 0: 7 | return 0 8 | if (x,y) not in brightness: 9 | brightness[(x,y)] = 0 10 | if brightness[x,y] > 0: 11 | brightness[x,y] = brightness[x,y] - 0.05 12 | if brightness[x,y] < 0.1: 13 | brightness[x,y] = 0 14 | else: 15 | if random.random() < 1/50: 16 | brightness[x,y] = 1.0 17 | return hsv_colour(0.10, 0.8, brightness[x,y]) 18 | 19 | frame = 0; 20 | while True: 21 | led_colours = {} 22 | for y in range(0,16): 23 | for x in range(0,16): 24 | if (x < 8) or (y < 8): 25 | led_colours[x,y] = glitter_shader(x,y,frame) 26 | display.set_leds(led_colours) 27 | frame+=1 28 | time.sleep(1/20) 29 | 30 | -------------------------------------------------------------------------------- /community-scripts/google_api.py: -------------------------------------------------------------------------------- 1 | # Google API 2 | # code to access the google api fetch nest thermostat information 3 | # this module can be run without the display code, and will dump results to the console. 4 | # 5 | # you WILL need a google cloud account, and will need to 6 | # - create a new google cloud project - https://console.cloud.google.com/ 7 | # - enable the Smart Device Management API on that project 8 | # - create a Web Application type credential - this will give you the oauth client_id and client_secret 9 | # - on the oauth consent screen add a google user that has permission to access the project 10 | # - create a device access project in the device access console - https://console.nest.google.com/device-access/project-list 11 | # you'll need to provide the oauth client id you got in the previous step 12 | # after this project is created, you'll have the project_id you'll need for configuration 13 | # The program will look for it's configuration in the google folder in the home directory (usually /home/pi/Desktop) 14 | # - create the google folder in the home folder 15 | # - create a file called .nest-config in the google folder, with the folling format (add your project and client id values): 16 | # { 17 | # "project_id" : "", 18 | # "client_id" : "", 19 | # "client_secret" : "" 20 | # } 21 | # 22 | # when you run the first time, the lumicube will show a lock and question mark on screens. 23 | # look at the console for a url 24 | # - copy that url to a browser 25 | # - you'll need to log in with an authorized google account, probably the one you used to create the project with, or another that you have authorized 26 | # - allow the lumicube project to access the data 27 | # - if all goes well, you should be redirected to the redirect_url that you configured - IN that url will be code. copy that code from the URL 28 | # check the console logs again, it should be asking you to put the code in a specific file 29 | # - create that file and add the code (I usually use echo to output to the file) 30 | # - note that I would recommend editing a file of a different name, and renaming it when done, or just use echo " 31 | # if you open the final file in the editor the code may see it and try to read it immediately before you've saved it. 32 | # the code will be used to fetch a refresh token and access token, which it will used to access the device api. Even if you restart it will use the saved token. 33 | # I've found that the token is good for about a week, after which you'll see the lock and question mark display - look at the console for instructions. 34 | # 35 | # Useful links: 36 | # - controlling a nest thermostat with python : https://www.wouternieuwerth.nl/controlling-a-google-nest-thermostat-with-python/ 37 | # - Device Access Guides : https://developers.google.com/nest/device-access/registration 38 | # - Thermostat API - https://developers.google.com/nest/device-access/api/thermostat 39 | # 40 | # Author: Kevin Haney 41 | # Date: 12/9/2022 42 | # v1.0 43 | # history: 44 | # - 1.0 - original (v1) 45 | 46 | import os 47 | import time 48 | import requests 49 | import json 50 | from datetime import datetime, timedelta 51 | 52 | class Thermostat: 53 | def __init__(self, t): 54 | self.device_id = '' 55 | self.name = '' 56 | self.humidity = 0 57 | self.connectivity = '' 58 | self.mode = '' 59 | self.eco_mode = False 60 | self.fan_on = False 61 | self.setpoint_eco_heat = 0 62 | self.setpoint_eco_cool = 0 63 | self.status = '' 64 | self.scale = 'F' 65 | self.setpoint_heat = 0 66 | self.setpoint_cool = 0 67 | self.ambient_temperature = 0 68 | 69 | self.device_id = t['name'] 70 | if 'traits' in t: 71 | traits = t['traits'] 72 | self.name = traits['sdm.devices.traits.Info']['customName'] 73 | scale = traits['sdm.devices.traits.Settings']['temperatureScale'] 74 | if scale == 'FAHRENHEIT': 75 | self.scale = 'F' 76 | else: 77 | self.scale = 'C' 78 | self.humidity = traits['sdm.devices.traits.Humidity']['ambientHumidityPercent'] 79 | self.connectivity = traits['sdm.devices.traits.Connectivity']['status'] 80 | self.mode = traits['sdm.devices.traits.ThermostatMode']['mode'] 81 | self.fan_on = traits['sdm.devices.traits.Fan']['timerMode'] != 'OFF' 82 | self.eco_mode = traits['sdm.devices.traits.ThermostatEco']['mode'] == 'MANUAL_ECO' 83 | self.setpoint_eco_heat = traits['sdm.devices.traits.ThermostatEco']['heatCelsius'] 84 | if self.scale == 'F': 85 | self.setpoint_eco_heat = Thermostat.cToF(self.setpoint_eco_heat) 86 | self.setpoint_eco_cool = traits['sdm.devices.traits.ThermostatEco']['coolCelsius'] 87 | if self.scale == 'F': 88 | self.setpoint_eco_cool = Thermostat.cToF(self.setpoint_eco_cool) 89 | self.status = traits['sdm.devices.traits.ThermostatHvac']['status'] 90 | setpoint = traits['sdm.devices.traits.ThermostatTemperatureSetpoint'] 91 | if 'heatCelsius' in setpoint: 92 | self.setpoint_heat = setpoint['heatCelsius'] 93 | if self.scale == 'F': 94 | self.setpoint_heat = Thermostat.cToF(self.setpoint_heat) 95 | if 'coolCelsius' in setpoint: 96 | self.setpoint_cool = setpoint['coolCelsius'] 97 | if self.scale == 'F': 98 | self.setpoint_cool = Thermostat.cToF(self.setpoint_cool) 99 | self.ambient_temperature = traits['sdm.devices.traits.Temperature']['ambientTemperatureCelsius'] 100 | if self.scale == 'F': 101 | self.ambient_temperature = Thermostat.cToF(self.ambient_temperature) 102 | 103 | 104 | def __str__(self): 105 | fan = 'off' 106 | if self.fan_on: 107 | fan = 'on' 108 | info = f'{self.name} -- mode: {self.mode}, status: {self.status}, fan: {fan}, ' 109 | if self.eco_mode: 110 | info += 'eco set: ' + f'{self.setpoint_eco_heat:3.1f} {self.scale} to {self.setpoint_eco_cool:3.1f} {self.scale}, ' 111 | else: 112 | if self.mode == 'HEAT': 113 | info += 'set: ' + f'{self.setpoint_heat:3.1f} {self.scale}, ' 114 | elif self.mode == 'COOL': 115 | info += 'set: ' + f'{self.setpoint_cool:3.1f} {self.scale}, ' 116 | elif self.mode == 'HEATCOOL': 117 | info += 'set: ' + f'{self.setpoint_heat:3.1f} {self.scale} to {self.setpoint_cool:3.1f} {self.scale}, ' 118 | info += 'current ' + f'{self.ambient_temperature:3.1f} {self.scale}, ' 119 | info += f'humidity {self.humidity:3.1f} %' 120 | return info 121 | 122 | @staticmethod 123 | def cToF(celsius): 124 | return ((9/5)*celsius) + 32 125 | 126 | @staticmethod 127 | def fToC(farenheit): 128 | return (farenheit-32)*5/9 129 | 130 | class GoogleApi: 131 | ACCESS_TOKEN_LIFETIME_MINUTES = 30 132 | 133 | def __init__(self, working_directory): 134 | self.working_directory = working_directory 135 | self._config_file_name = self.working_directory+'/google/.nest-config' 136 | try: 137 | with open(self._config_file_name,'r') as config_file: 138 | config_json = json.load(config_file) 139 | print('found config') 140 | self.project_id = config_json['project_id'] 141 | self.client_id = config_json['client_id'] 142 | self.client_secret = config_json['client_secret'] 143 | 144 | except FileNotFoundError: 145 | print('did not find config file at',self._config_file_name) 146 | self.refresh_token = '' 147 | return 148 | 149 | self.access_token = '' 150 | self.access_token_expires = datetime.now() 151 | self.working_directory = working_directory 152 | self._token_file_name = self.working_directory+'/google/.refreshtoken' 153 | 154 | self.refresh_token = '' 155 | 156 | try: 157 | tokenFile = open(self._token_file_name,'r') 158 | self.refresh_token = tokenFile.read() 159 | tokenFile.close() 160 | print('found token') 161 | #print(self.refresh_token) 162 | except FileNotFoundError: 163 | print('no token file found in',self._token_file_name) 164 | self.refresh_token = '' 165 | 166 | if self.refresh_token != '': 167 | self.access_token = self.fetch_access_token() 168 | 169 | def is_permissioned(self): 170 | return self.refresh_token != '' 171 | 172 | def get_permission_url(self): 173 | redirect_uri = 'https://www.google.com' 174 | return 'https://nestservices.google.com/partnerconnections/'+self.project_id+'/auth?redirect_uri='+redirect_uri+'&access_type=offline&prompt=consent&client_id='+self.client_id+'&response_type=code&scope=https://www.googleapis.com/auth/sdm.service' 175 | 176 | def get_permission(self): 177 | redirect_uri = 'https://www.google.com' 178 | url = 'https://nestservices.google.com/partnerconnections/'+self.project_id+'/auth?redirect_uri='+redirect_uri+'&access_type=offline&prompt=consent&client_id='+self.client_id+'&response_type=code&scope=https://www.googleapis.com/auth/sdm.service' 179 | 180 | print("Go to this URL to log in:", flush=True) 181 | print(url) 182 | 183 | code = '' 184 | code_file_name = self.working_directory + '/google/.code' 185 | if os.path.exists(code_file_name): 186 | os.remove(code_file_name) 187 | 188 | while code == '': 189 | print(f'waiting for code in {code_file_name}',flush=True) 190 | try: 191 | code_file = open(code_file_name,'r') 192 | code = code_file.read() 193 | except FileNotFoundError: 194 | time.sleep(10) 195 | 196 | print(f'got code{code}',flush=True) 197 | self.fetch_refresh_token(code) 198 | return self.is_permissioned() 199 | 200 | def fetch_refresh_token(self,code): 201 | redirect_uri = 'https://www.google.com' 202 | print('fetch refresh token') 203 | 204 | params = ( 205 | ('client_id', self.client_id), 206 | ('client_secret', self.client_secret), 207 | ('code', code), 208 | ('grant_type', 'authorization_code'), 209 | # get a new access token 210 | ('redirect_uri', redirect_uri), 211 | ) 212 | response = requests.post('https://www.googleapis.com/oauth2/v4/token', params=params) 213 | response_json = response.json() 214 | 215 | self.access_token = response_json['token_type'] + ' ' + str(response_json['access_token']) 216 | self.access_token_expires = datetime.now() + timedelta(minutes=self.ACCESS_TOKEN_LIFETIME_MINUTES) 217 | self.refresh_token = response_json['refresh_token'] 218 | print('created token') 219 | #save it 220 | refresh_file = open(self._token_file_name,'w') 221 | refresh_file.write(self.refresh_token) 222 | refresh_file.close() 223 | print('saved token to',self._token_file_name) 224 | return self.refresh_token 225 | 226 | def fetch_access_token(self): 227 | # get a new access token 228 | params = ( 229 | ('client_id', self.client_id), 230 | ('client_secret', self.client_secret), 231 | ('refresh_token', self.refresh_token), 232 | ('grant_type', 'refresh_token'), 233 | ) 234 | 235 | response = requests.post('https://www.googleapis.com/oauth2/v4/token', params=params) 236 | if response.status_code != 200: 237 | print(response,response.status_code,response.reason) 238 | if response.status_code == 400: 239 | self.refresh_token = '' 240 | self.access_token = '' 241 | return '' 242 | 243 | response_json = response.json() 244 | # print(response_json) 245 | self.access_token = response_json['token_type'] + ' ' + response_json['access_token'] 246 | self.access_token_expires = datetime.now() + timedelta(minutes=self.ACCESS_TOKEN_LIFETIME_MINUTES) 247 | #print('new Access token: ' + self.access_token,'expires',self.access_token_expires) 248 | return self.access_token 249 | 250 | def check_access_token(self): 251 | if self.access_token == '' or datetime.now() > self.access_token_expires: 252 | print('refreshing access token') 253 | self.fetch_access_token() 254 | 255 | return self.access_token != '' 256 | 257 | def fetch_thermostats(self): 258 | if self.check_access_token() == False: 259 | print('could not refresh access token') 260 | return [] 261 | 262 | # Get devices 263 | url_get_devices = 'https://smartdevicemanagement.googleapis.com/v1/enterprises/' + self.project_id + '/devices' 264 | headers = { 265 | 'Content-Type': 'application/json', 266 | 'Authorization': self.access_token, 267 | } 268 | 269 | response = requests.get(url_get_devices, headers=headers) 270 | if response.status_code != 200: 271 | print(response,response.status_code,response.reason) 272 | response_json = response.json() 273 | 274 | #print(response_json) 275 | thermostats = [] 276 | devices = response_json['devices'] 277 | #print('found',len(devices),'devices') 278 | for d in devices: 279 | if 'type' in d and d['type'] == 'sdm.devices.types.THERMOSTAT': 280 | traits = d['traits'] 281 | #print(traits) 282 | # print('device :',traits['sdm.devices.traits.Info']['customName']) 283 | # for t in traits: 284 | # for x in traits[t]: 285 | # print(t,x,traits[t][x]) 286 | thermostats.append(Thermostat(d)) 287 | return thermostats 288 | 289 | def fetch_thermostat(self, t): 290 | if self.check_access_token() == False: 291 | print('could not refresh access token') 292 | return None 293 | 294 | # Get devices 295 | url_get_device = 'https://smartdevicemanagement.googleapis.com/v1/' + t.device_id 296 | headers = { 297 | 'Content-Type': 'application/json', 298 | 'Authorization': self.access_token, 299 | } 300 | 301 | success = False 302 | while not success: 303 | response = requests.get(url_get_device, headers=headers) 304 | if response.status_code != 200: 305 | print(response,response.status_code,response.reason) 306 | time.sleep(30) 307 | else: 308 | success = True 309 | response_json = response.json() 310 | #print(response,response_json) 311 | 312 | d = response_json 313 | #print(d) 314 | if 'type' in d and d['type'] == 'sdm.devices.types.THERMOSTAT': 315 | traits = d['traits'] 316 | #print(traits) 317 | # print('device :',traits['sdm.devices.traits.Info']['customName']) 318 | # for t in traits: 319 | # for x in traits[t]: 320 | # print(t,x,traits[t][x]) 321 | return Thermostat(d) 322 | 323 | def change_setpoint(self, change, thermostat, heat): 324 | if self.check_access_token() == False: 325 | print('could not refresh access token') 326 | return False 327 | 328 | current_setpoint = None 329 | params = None 330 | if thermostat.eco_mode: 331 | print('eco mode not supported yet for change_setpoint()',flush=True) 332 | return 333 | elif thermostat.mode == 'HEATCOOL': 334 | if thermostat.eco_mode: 335 | setpoint_heat = thermostat.setpoint_eco_heat 336 | setpoint_cool = thermostat.setpoint_eco_cool 337 | else: 338 | setpoint_heat = thermostat.setpoint_heat 339 | setpoint_cool = thermostat.setpoint_cool 340 | 341 | if heat: 342 | new_setpoint_heat = change + setpoint_heat 343 | new_setpoint_cool = setpoint_cool 344 | else: 345 | new_setpoint_heat = setpoint_heat 346 | new_setpoint_cool = change + setpoint_cool 347 | 348 | print(f'changing range from {setpoint_heat:3.1f} {thermostat.scale} - {setpoint_cool:3.1f} {thermostat.scale} to {new_setpoint_heat:3.1f} {thermostat.scale} - {new_setpoint_cool:3.1f} {thermostat.scale}') 349 | if thermostat.scale == 'F': 350 | new_setpoint_heat = Thermostat.fToC(new_setpoint_heat) 351 | new_setpoint_cool = Thermostat.fToC(new_setpoint_cool) 352 | command = 'SetRange' 353 | params = { 'heatCelsius': new_setpoint_heat , 'coolCelsius' : new_setpoint_cool } 354 | elif thermostat.mode == 'HEAT': 355 | command = 'SetHeat' 356 | point = 'heatCelsius' 357 | current_setpoint = thermostat.setpoint_heat 358 | elif thermostat.mode == 'COOL': 359 | command = 'SetCool' 360 | point = 'coolCelsius' 361 | current_setpoint = thermostat.setpoint_cool 362 | elif thermostat.mode == 'OFF': 363 | return 364 | else: 365 | print(thermostat.mode,'not supported yet for change_setpoint()',flush=True) 366 | return 367 | 368 | if params == None: 369 | new_setpoint = change + current_setpoint 370 | print(f'changing from {current_setpoint:3.1f} {thermostat.scale} to {new_setpoint:3.1f} {thermostat.scale}',flush=True) 371 | if thermostat.scale == 'F': 372 | new_setpoint = Thermostat.fToC(new_setpoint) 373 | params = { point : new_setpoint } 374 | 375 | url_exec_command = 'https://smartdevicemanagement.googleapis.com/v1/' + thermostat.device_id + ':executeCommand' 376 | headers = { 377 | 'Content-Type': 'application/json', 378 | 'Authorization': self.access_token, 379 | } 380 | 381 | data = {'command': 'sdm.devices.commands.ThermostatTemperatureSetpoint.' + command, 382 | 'params': params } 383 | #print(data) 384 | response = requests.post(url_exec_command, headers=headers, data=json.dumps(data)) 385 | if response.status_code != 200: 386 | print(response,response.status_code,response.reason,flush=True) 387 | return False 388 | return True 389 | 390 | def change_mode(self, new_mode, thermostat): 391 | if new_mode == 'ECO': 392 | command = 'ThermostatEco.SetMode' 393 | params = { 'mode' : 'MANUAL_ECO' } 394 | elif new_mode == 'FAN': 395 | command = 'Fan.SetTimer' 396 | # fan is a toggle - if it was on, turn it off, it if was off, turn it on 397 | if thermostat.fan_on: 398 | params = { 'timerMode' : 'OFF' } 399 | else: 400 | params = { 'timerMode' : 'ON' } 401 | else: 402 | command = 'ThermostatMode.SetMode' 403 | params = { 'mode' : new_mode } 404 | 405 | url_exec_command = 'https://smartdevicemanagement.googleapis.com/v1/' + thermostat.device_id + ':executeCommand' 406 | headers = { 407 | 'Content-Type': 'application/json', 408 | 'Authorization': self.access_token, 409 | } 410 | 411 | data = {'command': 'sdm.devices.commands.' + command, 412 | 'params': params } 413 | #print(data) 414 | response = requests.post(url_exec_command, headers=headers, data=json.dumps(data)) 415 | if response.status_code != 200: 416 | print(response,response.status_code,response.reason,flush=True) 417 | return False 418 | return True 419 | 420 | 421 | if __name__ == "__main__": 422 | workingDirectory = os.getcwd() 423 | print(workingDirectory) 424 | 425 | api = GoogleApi(workingDirectory) 426 | if api.is_permissioned() == False: 427 | if api.get_permission() == False: 428 | print('could not get permision') 429 | 430 | thermostats = api.fetch_thermostats() 431 | t = thermostats[0] 432 | print(t) 433 | print('temp down') 434 | api.change_setpoint(-1,t, True) 435 | time.sleep(5) 436 | t = api.fetch_thermostat(t) 437 | print(t) 438 | print('temp up') 439 | api.change_setpoint(1,t, False) 440 | time.sleep(5) 441 | print(t) 442 | 443 | last = '' 444 | while True: 445 | for d in thermostats: 446 | t = api.fetch_thermostat(d) 447 | text = f'{t}' 448 | if last != text: 449 | print(text) 450 | last = text 451 | 452 | 453 | time.sleep(30) 454 | -------------------------------------------------------------------------------- /community-scripts/identify_panels v2.py: -------------------------------------------------------------------------------- 1 | # Identify panels: left, red; right, green; top blue; and shared corners 2 | 3 | display.set_panel("left", [[grey, red, red, red, red, red, red, white], 4 | [red, red, cyan, red, red, red, red, red], 5 | [red, red, cyan, red, red, red, red, red], 6 | [red, red, cyan, red, red, red, red, red], 7 | [red, red, cyan, red, red, red, red, red], 8 | [red, red, cyan, red, red, red, red, red], 9 | [red, red, cyan, cyan, cyan, cyan, red, red], 10 | [red, red, red, red, red, red, red, orange]]) 11 | display.set_panel("right", [[white, green, green, green, green, green, green, pink], 12 | [green, green, magenta, magenta, magenta, green, green, green], 13 | [green, green, magenta, green, green, magenta, green, green], 14 | [green, green, magenta, green, green, magenta, green, green], 15 | [green, green, magenta, magenta, magenta, green, green, green], 16 | [green, green, magenta, green, green, magenta, green, green], 17 | [green, green, magenta, green, green, magenta, green, green], 18 | [orange, green, green, green, green, green, green, green]]) 19 | display.set_panel("top", [[blue, blue, blue, blue, blue, blue, blue, pink], 20 | [blue, blue, yellow, yellow, yellow, blue, blue, blue], 21 | [blue, blue, blue, yellow, blue, blue, blue, blue], 22 | [blue, blue, blue, yellow, blue, blue, blue, blue], 23 | [blue, blue, blue, yellow, blue, blue, blue, blue], 24 | [blue, blue, blue, yellow, blue, blue, blue, blue], 25 | [blue, blue, blue, yellow, blue, blue, blue, blue], 26 | [grey, blue, blue, blue, blue, blue, blue, white]]) 27 | # Display status on screen 28 | 29 | def to_text(value): 30 | return str("{:.1f}".format(value)) 31 | 32 | screen.draw_rectangle(0, 0, 320, 240, black) 33 | height = 36 34 | while True: 35 | text = ("IP address: " + pi.ip_address() + "\n" 36 | + "CPU temp : " + to_text(pi.cpu_temp()) + "\n" 37 | + "CPU usage : " + to_text(pi.cpu_percent()) + "\n" 38 | + "RAM usage : " + to_text(pi.ram_percent_used()) +"\n" 39 | + "Disk usage: " + to_text(pi.disk_percent()) + "\n") 40 | screen.write_text(10, 18, text, 1, white, black) 41 | time.sleep(5) 42 | -------------------------------------------------------------------------------- /community-scripts/identify_panels.py: -------------------------------------------------------------------------------- 1 | # Identify panels: left, red; right, green; top blue. 2 | 3 | display.set_panel("left", [[red, red, red, red, red, red, red, red], 4 | [red, red, cyan, red, red, red, red, red], 5 | [red, red, cyan, red, red, red, red, red], 6 | [red, red, cyan, red, red, red, red, red], 7 | [red, red, cyan, red, red, red, red, red], 8 | [red, red, cyan, red, red, red, red, red], 9 | [red, red, cyan, cyan, cyan, cyan, red, red], 10 | [red, red, red, red, red, red, red, red]]) 11 | display.set_panel("right", [[green, green, green, green, green, green, green, green], 12 | [green, green, magenta, magenta, magenta, green, green, green], 13 | [green, green, magenta, green, green, magenta, green, green], 14 | [green, green, magenta, green, green, magenta, green, green], 15 | [green, green, magenta, magenta, magenta, green, green, green], 16 | [green, green, magenta, green, green, magenta, green, green], 17 | [green, green, magenta, green, green, magenta, green, green], 18 | [green, green, green, green, green, green, green, green]]) 19 | display.set_panel("top", [[blue, blue, blue, blue, blue, blue, blue, blue], 20 | [blue, blue, yellow, yellow, yellow, blue, blue, blue], 21 | [blue, blue, blue, yellow, blue, blue, blue, blue], 22 | [blue, blue, blue, yellow, blue, blue, blue, blue], 23 | [blue, blue, blue, yellow, blue, blue, blue, blue], 24 | [blue, blue, blue, yellow, blue, blue, blue, blue], 25 | [blue, blue, blue, yellow, blue, blue, blue, blue], 26 | [blue, blue, blue, blue, blue, blue, blue, blue]]) 27 | # Display status on screen 28 | 29 | def to_text(value): 30 | return str("{:.1f}".format(value)) 31 | 32 | screen.draw_rectangle(0, 0, 320, 240, black) 33 | height = 36 34 | while True: 35 | text = ("IP address: " + pi.ip_address() + "\n" 36 | + "CPU temp : " + to_text(pi.cpu_temp()) + "\n" 37 | + "CPU usage : " + to_text(pi.cpu_percent()) + "\n" 38 | + "RAM usage : " + to_text(pi.ram_percent_used()) +"\n" 39 | + "Disk usage: " + to_text(pi.disk_percent()) + "\n") 40 | screen.write_text(10, 18, text, 1, white, black) 41 | time.sleep(5) 42 | 43 | -------------------------------------------------------------------------------- /community-scripts/iss-tracker-lumi.py: -------------------------------------------------------------------------------- 1 | """ 2 | iss-tracker-lumi.py 3 | 4 | Track the International Space Station. 5 | 6 | Additional Installation Packages 7 | 8 | You will need to pip install the following packages: 9 | pip install requests==2.25.1 10 | pip install geopy==2.2.0 11 | 12 | Display 8-bit ISS, it's current location (latitude / longitude) and distance from a given location. 13 | 14 | This project was inspired by my son. He has a large interest in space and wanted a light system to track the ISS. 15 | The original version of this will light up a strip of 20 leds. This version was built when a co-worker sent me a 16 | link the the LumiCube. 17 | 18 | You will need to set the latitude, longitude, sound_file, and sound_file_length in the current_properties dictionary. 19 | 20 | You can get your Latitude and Longitude from: https://www.latlong.net 21 | 22 | latitude and longitude are float values 23 | sound_file is a string and the file will need to be in the Desktop/ directory of the user you created when setting up 24 | your LumiCube. 25 | sound_length is an integer specifying the length of the sound file in seconds 26 | 27 | Properties you can set: 28 | latitude : Float of latitude of location you are at. i.e. 51.507351 29 | longitude : Float of longitude of location you are at. i.e. -0.127758 30 | radius : If the ISS is withing this float value it will either be Overhead or Visible 31 | The default is 1300 (miles). If you change from mi to km, you need to change this to 2092 (kilometers) 32 | distance_measure : mi for miles, km for kilometers 33 | do_not_disturb : 34 | start : Hour you want the do not disturb to start in 24 hour format. i.e. 22 35 | end : Hour you want the do not disturb to end in 24 hour format. i.e. 9 36 | sound_file : The name of the file in Desktop/ directory that will be played when Overhead or Visible 37 | sound_file_length : The length in seconds of the sound file. 38 | """ 39 | __author__ = "Rick Feldmann" 40 | __email__ = "wrfeldmann@icloud.com" 41 | __status__ = "Production" 42 | __version__ = "1.0" 43 | 44 | import logging.handlers 45 | import os 46 | import requests 47 | import time 48 | 49 | from datetime import datetime 50 | from geopy import distance 51 | 52 | properties = dict() 53 | properties["latitude"] = 39.2383 54 | properties["longitude"] = -77.4511 55 | properties["radius"] = 1300.0 56 | properties["distance_measure"] = "mi" 57 | properties["do_not_disturb"] = dict() 58 | properties["do_not_disturb"]["start"] = 22 59 | properties["do_not_disturb"]["end"] = 9 60 | properties["sound_file"] = "2001_Space_Odyssey.mp3" 61 | properties["sound_file_length"] = 78 62 | 63 | 64 | class BuildPanel(object): 65 | def __init__(self): 66 | return 67 | 68 | def create_iss_panel(self, color, rotate): 69 | """ 70 | Creates an 8-bit representation of the ISS. 71 | 72 | @param: LumiCube Color 73 | @param: Boolean True or False to build the ISS rotated 90 degrees 74 | 75 | @return: 8x8 list with the appropriate colors to represent the ISS 76 | No rotation Rotated 90 degrees 77 | 78 | X X X X XXXXXXXX 79 | X X X X XX 80 | X X X X XXXXXXXX 81 | XXXXXXXX XX 82 | XXXXXXXX XX 83 | X X X X XXXXXXXX 84 | X X X X XX 85 | X X X X XXXXXXXX 86 | """ 87 | panel = list() 88 | if rotate is True: 89 | for row_index in range(0, 8, 1): 90 | row = list() 91 | for cell_index in range(0, 8, 1): 92 | if row_index in [0, 2, 5, 7] and cell_index in [0, 1, 2, 5, 6, 7]: 93 | row.append(color) 94 | if row_index in [1, 3, 4, 6] and cell_index in [0, 1, 2, 5, 6, 7]: 95 | row.append(black) 96 | if row_index in [0, 1, 2, 3, 4, 5, 6, 7] and cell_index in [3, 4]: 97 | row.append(color) 98 | panel.append(row) 99 | else: 100 | for row_index in range(0, 8, 1): 101 | row = list() 102 | for cell_index in range(0, 8, 1): 103 | if row_index in [0, 1, 2, 5, 6, 7] and cell_index in [0, 2, 5, 7]: 104 | row.append(color) 105 | if row_index in [0, 1, 2, 5, 6, 7] and cell_index in [1, 3, 4, 6]: 106 | row.append(black) 107 | if row_index in [3, 4] and cell_index in [0, 1, 2, 3, 4, 5, 6, 7]: 108 | row.append(color) 109 | panel.append(row) 110 | return panel 111 | 112 | 113 | class CheckDoNotDisturb(object): 114 | def __init__(self): 115 | return 116 | 117 | def check_do_not_disturb(self, current_datetime): 118 | """Check to see if it the run is during the do not disturb time.""" 119 | return_value = False 120 | current_hour = current_datetime.hour 121 | if current_hour > properties["do_not_disturb"]["start"]: 122 | return_value = True 123 | elif current_hour < properties["do_not_disturb"]["end"]: 124 | return_value = True 125 | return return_value 126 | 127 | 128 | class ISSUtils(object): 129 | def __init__(self): 130 | return 131 | 132 | def current_ISS_location(self): 133 | """Get the current ISS Location.""" 134 | try: 135 | response = requests.get(url="http://api.open-notify.org/iss-now.json") 136 | data = response.json() 137 | current_iss_latitude = float(data["iss_position"]["latitude"]) 138 | current_iss_longitude = float(data["iss_position"]["longitude"]) 139 | except: 140 | current_iss_latitude = 0.0 141 | current_iss_longitude = 0.0 142 | return current_iss_latitude, current_iss_longitude 143 | 144 | def iss_distance(self, current_iss_latitude, current_iss_longitude): 145 | """Get the distance you are from the ISS.""" 146 | my_location = (properties["latitude"], properties["longitude"]) 147 | iss_location = (current_iss_latitude, current_iss_longitude) 148 | if properties["distance_measure"] == "mi": 149 | return_distance = distance.distance(my_location, iss_location).miles 150 | else: 151 | return_distance = distance.distance(my_location, iss_location).km 152 | return return_distance 153 | 154 | def near_ISS(self, distance): 155 | """Calculate if we are near enough that it is Overhead.""" 156 | message = "Far Away" 157 | true_false = False 158 | if distance <= properties["radius"]: 159 | message = "Overhead" 160 | true_false = True 161 | return true_false, message 162 | 163 | 164 | class LoggingUtils(object): 165 | def __init__(self): 166 | """ 167 | Initialize logging. 168 | 169 | File is Desktop/loggs/iss-tracker.log 170 | 171 | I use this file for homebridge integration with Apple HomeKit so I can trigger additional Apple Home devices 172 | to perform various things. 173 | """ 174 | self.log_dir = "logs/" 175 | self.log_file = "{0}/{1}{2}".format(os.getcwd(), self.log_dir, "iss-tracker.log") 176 | self.logger = logging.getLogger("issLogger") 177 | if not os.path.exists(self.log_dir): 178 | os.makedirs(self.log_dir) 179 | 180 | def configureLogging(self, level="INFO"): 181 | """Configure and return the logger.""" 182 | self.logger.setLevel(logging.INFO) 183 | rotating_log_file_handler = logging.handlers.RotatingFileHandler(self.log_file, 184 | maxBytes=1048576, 185 | backupCount=5) 186 | rotating_log_file_formatter = logging.Formatter("%(message)s") 187 | rotating_log_file_handler.setFormatter(rotating_log_file_formatter) 188 | self.logger.addHandler(rotating_log_file_handler) 189 | return self.logger 190 | 191 | 192 | class SoundUtils(object): 193 | def __init__(self): 194 | return 195 | 196 | def check_playing_sound(self, play_sound, message): 197 | """ 198 | Say the message passed in and, if not playing, play the sound. 199 | 200 | :param play_sound: 201 | :param message: 202 | :return: 203 | """ 204 | speaker.say("The International Space Station is {0}".format(message)) 205 | if not play_sound and os.path.exists(properties["sound_file"]): 206 | self.visible_sound_start_time = datetime.now() 207 | speaker.play(properties["sound_file"]) 208 | play_sound = True 209 | time.sleep(1.0) 210 | elif play_sound: 211 | current_time = datetime.now() 212 | time_difference = abs(current_time - self.visible_sound_start_time) 213 | if time_difference.seconds > properties["sound_file_length"]: 214 | play_sound = False 215 | return play_sound 216 | 217 | 218 | class SunriseSunsetUtils(object): 219 | def __init__(self): 220 | return 221 | 222 | def getTimestamp(self, dt): 223 | return datetime.timestamp(datetime.fromisoformat(dt)) 224 | 225 | def is_twilight(self, current_time): 226 | """ 227 | Determines if the ISS is visible. 228 | 229 | :param current_time: 230 | :param args: 231 | :return: 232 | """ 233 | sunrise_sunset_base_url = "https://api.sunrise-sunset.org/json" 234 | sunrise_sunset_url = "{0}?lat={1}&lng={2}&formatted=0".format(sunrise_sunset_base_url, 235 | properties["latitude"], 236 | properties["longitude"]) 237 | try: 238 | response = requests.get(sunrise_sunset_url) 239 | data = response.json() 240 | astronomical_twilight_begin_timestamp = self.getTimestamp( 241 | data["results"]["astronomical_twilight_begin"]) 242 | sunrise_timestamp = self.getTimestamp(data["results"]["sunrise"]) 243 | sunset_timestamp = self.getTimestamp(data["results"]["sunset"]) 244 | astronomical_twilight_end_timestamp = self.getTimestamp(data["results"]["astronomical_twilight_end"]) 245 | except: 246 | astronomical_twilight_begin_timestamp = 0 247 | sunrise_timestamp = 0 248 | sunset_timestamp = 0 249 | astronomical_twilight_end_timestamp = 0 250 | timezone_offset = float(current_time.isoformat().split("T")[1][-6:].replace(":", ".")) 251 | time_now = self.getTimestamp(current_time.isoformat()) 252 | astronomical_twilight_begin_timestamp = astronomical_twilight_begin_timestamp + (timezone_offset * 3600) 253 | sunrise_timestamp = sunrise_timestamp + (timezone_offset * 3600) 254 | sunset_timestamp = sunset_timestamp + (timezone_offset * 3600) 255 | astronomical_twilight_end_timestamp = astronomical_twilight_end_timestamp + (timezone_offset * 3600) 256 | true_false = False 257 | if (time_now >= astronomical_twilight_begin_timestamp and time_now <= sunrise_timestamp) or \ 258 | (time_now >= sunset_timestamp and time_now <= astronomical_twilight_end_timestamp): 259 | true_false = True 260 | return true_false 261 | 262 | 263 | build_panel = BuildPanel() 264 | check_do_not_disturb = CheckDoNotDisturb() 265 | iss_utils = ISSUtils() 266 | logging_utils = LoggingUtils() 267 | sound_utils = SoundUtils() 268 | sunrise_sunset_utils = SunriseSunsetUtils() 269 | 270 | # Configure and return a logger from logging_utils 271 | logger = logging_utils.configureLogging() 272 | 273 | # Create the panels necessary 274 | blue_panel_1 = build_panel.create_iss_panel(blue, False) 275 | blue_panel_2 = build_panel.create_iss_panel(blue, True) 276 | green_panel_1 = build_panel.create_iss_panel(green, False) 277 | green_panel_2 = build_panel.create_iss_panel(green, True) 278 | grey_panel_1 = build_panel.create_iss_panel(grey, False) 279 | grey_panel_2 = build_panel.create_iss_panel(grey, True) 280 | black_panel = build_panel.create_iss_panel(black, False) 281 | 282 | # Initialize other variables 283 | panel_cycle = False 284 | heading_printed = False 285 | playing_sound = False 286 | 287 | display.set_all(black) 288 | 289 | """Fun little startup to show the ISS.""" 290 | for i in range(1, 5, 1): 291 | display.set_panel("top", grey_panel_1) 292 | display.set_panel("left", green_panel_1) 293 | display.set_panel("right", blue_panel_1) 294 | time.sleep(.5) 295 | display.set_panel("top", grey_panel_2) 296 | display.set_panel("left", green_panel_2) 297 | display.set_panel("right", blue_panel_2) 298 | time.sleep(.5) 299 | 300 | display.set_all(black) 301 | 302 | """Infinite loop to keep tracking the ISS.""" 303 | while True: 304 | now = datetime.now().astimezone() 305 | display_now = now.strftime("%Y-%m-%d %H:%M:%S %Z") 306 | iss_latitude, iss_longitude = iss_utils.current_ISS_location() 307 | distance_iss = iss_utils.iss_distance(iss_latitude, iss_longitude) 308 | is_near, message = iss_utils.near_ISS(distance_iss) 309 | is_visible = sunrise_sunset_utils.is_twilight(now) 310 | if is_visible and is_near: 311 | message = "Visible" 312 | str_iss_latitude = "{0:.2f}N".format(iss_latitude) 313 | if iss_latitude < 0: 314 | str_iss_latitude = "{0:.2f}S".format(abs(iss_latitude)) 315 | str_iss_longitude = "{0:.2f}E".format(iss_longitude) 316 | if iss_longitude < 0: 317 | str_iss_longitude = "{0:.2f}W".format(abs(iss_longitude)) 318 | str_distance_iss = "{0:.0f}".format(distance_iss) 319 | do_not_disturb = check_do_not_disturb.check_do_not_disturb(now) 320 | speaker.volume = 25 321 | if not heading_printed: 322 | heading_line = "| {0} | {1} | {2} | {3} | {4} |".format("Current Time".ljust(23, " "), 323 | "ISS Latitude".rjust(12, " "), 324 | "ISS Longitude".rjust(13, " "), 325 | "Distance".rjust(8, " "), 326 | "Message".ljust(8, " ")) 327 | print(heading_line) 328 | logger.info(heading_line) 329 | data_line = "| {0} | {1} | {2} | {3} | {4} |".format(display_now.ljust(23, " "), 330 | str_iss_latitude.rjust(12, " "), 331 | str_iss_longitude.rjust(13, " "), 332 | str_distance_iss.rjust(8, " "), 333 | message.ljust(8, " ")) 334 | print(data_line) 335 | logger.info(data_line) 336 | heading_printed = True 337 | lumi_message = "{0} - {1} - {2}{3} - {4}".format(str_iss_latitude, 338 | str_iss_longitude, 339 | str_distance_iss, 340 | properties["distance_measure"], 341 | message) 342 | if is_visible and is_near: 343 | display.set_panel("top", green_panel_1) 344 | if not do_not_disturb: 345 | playing_sound = sound_utils.check_playing_sound(playing_sound, message) 346 | display.scroll_text(lumi_message, green, black, .5) 347 | display.set_panel("left", green_panel_1) 348 | display.set_panel("right", green_panel_1) 349 | elif is_near: 350 | display.set_panel("top", blue_panel_1) 351 | if not do_not_disturb: 352 | playing_sound = sound_utils.check_playing_sound(playing_sound, message) 353 | display.scroll_text(lumi_message, blue, black, .5) 354 | display.set_panel("left", blue_panel_1) 355 | display.set_panel("right", blue_panel_1) 356 | else: 357 | display.set_panel("top", black_panel) 358 | if panel_cycle: 359 | display.set_panel("top", grey_panel_1) 360 | panel_cycle = False 361 | else: 362 | display.set_panel("top", grey_panel_2) 363 | panel_cycle = True 364 | lumi_message = "{0} - {1} - {2}{3}".format(str_iss_latitude, 365 | str_iss_longitude, 366 | str_distance_iss, 367 | properties["distance_measure"]) 368 | display.scroll_text(lumi_message, grey, black, .5) 369 | display.set_panel("left", grey_panel_1) 370 | display.set_panel("right", grey_panel_1) 371 | time.sleep(4) 372 | -------------------------------------------------------------------------------- /community-scripts/minecraft_pic.py: -------------------------------------------------------------------------------- 1 | # Draw some minecraft 2 | 3 | # Predefined colors 4 | 5 | B = black 6 | G = grey 7 | w = white 8 | r = red 9 | o = orange 10 | y = yellow 11 | g = green 12 | c = cyan 13 | b = blue 14 | m = magenta 15 | p = pink 16 | P = purple 17 | 18 | # Image definitions 19 | 20 | enderman = [ 21 | [B,B,B,B,B,B,B,B], 22 | [B,B,B,B,B,B,B,B], 23 | [B,B,B,B,B,B,B,B], 24 | [B,B,B,B,B,B,B,B], 25 | [m,P,m,B,B,m,P,m], 26 | [B,B,B,B,B,B,B,B], 27 | [B,B,B,B,B,B,B,B], 28 | [B,B,B,B,B,B,B,B], 29 | ] 30 | 31 | creeper = [ 32 | [g,g,g,g,g,g,g,g], 33 | [g,g,g,g,g,g,g,g], 34 | [g,B,B,g,g,B,B,g], 35 | [g,B,B,g,g,B,B,g], 36 | [g,g,g,B,B,g,g,g], 37 | [g,g,B,B,B,B,g,g], 38 | [g,g,B,g,g,B,g,g], 39 | [g,g,B,g,g,B,g,g], 40 | ] 41 | 42 | mushroomcow = [ 43 | [r,r,r,w,w,w,G,r], 44 | [r,r,r,w,w,w,r,r], 45 | [B,B,r,w,w,r,B,B], 46 | [B,B,r,G,r,r,B,B], 47 | [r,r,r,r,r,r,r,r], 48 | [r,r,w,w,w,w,r,r], 49 | [r,w,B,G,G,B,w,r], 50 | [r,w,G,B,B,G,w,r], 51 | ] 52 | 53 | # Display images 54 | 55 | display.set_panel("left", alex) 56 | display.set_panel("right", creeper) 57 | display.set_panel("top", mushroomcow) 58 | 59 | -------------------------------------------------------------------------------- /community-scripts/minecraft_random_pic.py: -------------------------------------------------------------------------------- 1 | # Definition of time = https://docs.python.org/3/library/time.html 2 | # Definition of random = https://docs.python.org/3/library/random.html 3 | 4 | import time 5 | import random 6 | 7 | # Define some hex colors 8 | 9 | black = 0x000000 10 | brown = 0xA38000 11 | grey = 0x808080 12 | lightgrey = 0xEFEDED 13 | mediumgrey = 0xBBBBCC 14 | white = 0xFFFFFF 15 | red = 0xFF0000 16 | orange = 0xFF8C00 17 | yellow = 0xFFFF00 18 | green = 0x00FF00 19 | cyan = 0x00FFFF 20 | blue = 0x0000FF 21 | magenta = 0xFF00FF 22 | pink = 0xFF007F 23 | skin = 0xF4CCCC 24 | purple = 0x800080 25 | 26 | # Define shorts for the colors for easier handling 27 | 28 | B = black 29 | Br = brown 30 | G = grey 31 | lG = lightgrey 32 | mG = mediumgrey 33 | w = white 34 | r = red 35 | o = orange 36 | y = yellow 37 | g = green 38 | c = cyan 39 | b = blue 40 | m = magenta 41 | p = pink 42 | P = purple 43 | s = skin 44 | 45 | # define the pixel/led images 46 | # 47 | # top 48 | # r 49 | # l i 50 | # e g 51 | # f h 52 | # t t 53 | # bottom 54 | 55 | sheep = [ 56 | [w,w,w,w,w,w,w,w], 57 | [w,lG,lG,mG,mG,mG,mG,w], 58 | [w,Br,Br,Br,Br,Br,Br,w], 59 | [w,B,w,Br,Br,w,B,w], 60 | [w,Br,Br,w,w,Br,Br,w], 61 | [w,lG,Br,s,s,Br,lG,w], 62 | [w,lG,Br,s,s,Br,lG,w], 63 | [w,w,w,w,w,w,w,w], 64 | ] 65 | 66 | enderman = [ 67 | [B,B,B,B,B,B,B,B], 68 | [B,B,B,B,B,B,B,B], 69 | [B,B,B,B,B,B,B,B], 70 | [B,B,B,B,B,B,B,B], 71 | [m,P,m,B,B,m,P,m], 72 | [B,B,B,B,B,B,B,B], 73 | [B,B,B,B,B,B,B,B], 74 | [B,B,B,B,B,B,B,B], 75 | ] 76 | 77 | alex = [ 78 | [o,o,o,o,o,o,o,o], 79 | [o,o,o,o,o,o,o,o], 80 | [o,o,o,o,s,s,o,o], 81 | [o,o,o,s,s,s,s,o], 82 | [s,B,w,s,s,B,w,s], 83 | [s,s,s,s,s,s,s,s], 84 | [s,s,s,m,m,s,s,s], 85 | [s,s,s,s,s,s,s,s], 86 | ] 87 | 88 | creeper = [ 89 | [g,g,g,g,g,g,g,g], 90 | [g,g,g,g,g,g,g,g], 91 | [g,B,B,g,g,B,B,g], 92 | [g,B,B,g,g,B,B,g], 93 | [g,g,g,B,B,g,g,g], 94 | [g,g,B,B,B,B,g,g], 95 | [g,g,B,g,g,B,g,g], 96 | [g,g,B,g,g,B,g,g], 97 | ] 98 | 99 | mushroomcow = [ 100 | [r,r,r,w,w,w,mG,r], 101 | [r,r,r,w,w,lG,r,r], 102 | [B,B,r,w,lG,r,B,B], 103 | [B,B,r,mG,r,r,B,B], 104 | [r,r,r,r,r,r,r,r], 105 | [r,r,w,w,w,w,r,r], 106 | [r,w,B,G,G,B,w,r], 107 | [r,w,G,B,B,G,w,r], 108 | ] 109 | 110 | cow = [ 111 | [Br,Br,Br,w,w,G,G,Br], 112 | [Br,Br,Br,Br,Br,Br,Br,Br], 113 | [lG,lG,Br,Br,Br,Br,Br,Br], 114 | [B,w,Br,Br,Br,Br,w,B], 115 | [Br,Br,Br,Br,Br,Br,Br,Br], 116 | [Br,Br,Br,Br,Br,Br,Br,Br], 117 | [Br,Br,Br,Br,Br,Br,Br,Br], 118 | [Br,Br,Br,Br,Br,Br,Br,Br], 119 | ] 120 | 121 | cow = [ 122 | [Br,Br,Br,Br,B,lG,Br,Br], 123 | [w,w,Br,Br,w,lG,Br,Br], 124 | [lG,B,w,Br,Br,Br,Br,Br], 125 | [lG,G,w,Br,Br,Br,w,w], 126 | [lG,G,w,Br,Br,Br,w,w], 127 | [lG,B,w,Br,Br,Br,lG,G], 128 | [w,w,Br,Br,w,lG,Br,G], 129 | [Br,Br,Br,Br,B,lG,Br,Br], 130 | ] 131 | 132 | # Make a list named "items" of the images 133 | # https://docs.python.org/3/tutorial/datastructures.html 134 | 135 | items = [sheep, enderman, alex, creeper, cow, mushroomcow] 136 | 137 | # Use "while" to create a loop 138 | # https://docs.python.org/3/reference/compound_stmts.html?#while 139 | 140 | # Take a random item out of the list 141 | # https://docs.python.org/3/library/random.html?#random.choice 142 | 143 | while True: 144 | random_item = random.choice(items) 145 | display.set_panel("left", random_item) 146 | random_item = random.choice(items) 147 | display.set_panel("right", random_item) 148 | random_item = random.choice(items) 149 | display.set_panel("top", random_item) 150 | time.sleep(10) 151 | 152 | # Sleep for 10 seconds before returning to the beginning of the loop again 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /community-scripts/mlb.py: -------------------------------------------------------------------------------- 1 | # classes and method useful for getting data from the mlb api 2 | # see mlb copyright information here http://gdx.mlb.com/components/copyright.txt 3 | # Author : Kevin Haney 4 | # Date : 10/23/2922 5 | # Version : 1.0 6 | # 7 | from datetime import datetime, timedelta 8 | from dateutil.parser import parse 9 | import requests 10 | import time 11 | 12 | class Team: 13 | def __init__(self, teamNode): 14 | self.name = "" 15 | self.abbreviation = "" 16 | if 'name' in teamNode: 17 | self.name = teamNode['name'] 18 | if 'abbreviation' in teamNode: 19 | self.abbreviation = teamNode['abbreviation'] 20 | 21 | class Inning: 22 | def __init__(self, awayScore, homeScore): 23 | self.awayScore = awayScore 24 | self.homeScore = homeScore 25 | 26 | class LineScore: 27 | def __init__(self, liveData): 28 | self.innings = [] 29 | self.currentInning = 0 30 | self.isTopInning = False 31 | if 'linescore' in liveData: 32 | lineScore = liveData['linescore'] 33 | if 'currentInning' in lineScore: 34 | self.currentInning = lineScore['currentInning'] 35 | self.isTopInning = lineScore['isTopInning'] 36 | for inning in lineScore['innings']: 37 | awayRuns = 0 38 | homeRuns = 0 39 | if 'runs' in inning['away']: 40 | awayRuns = inning['away']['runs'] 41 | if 'runs' in inning['home']: 42 | homeRuns = inning['home']['runs'] 43 | self.innings.append(Inning(awayRuns, homeRuns)) 44 | 45 | class LiveData: 46 | def __init__(self, gamePk): 47 | self.gamePk = gamePk 48 | self.indicator = "---" 49 | self.inning = 0 50 | self.balls = 0 51 | self.strikes = 0 52 | self.outs = 0 53 | self.score = " " 54 | self.awayScore = 0 55 | self.homeScore = 0 56 | self.onFirst = False 57 | self.onSecond = False 58 | self.onThird = False 59 | self.lineScore = None 60 | 61 | #live = requests.get("https://statsapi.mlb.com/api/v1.1/game/"+str(gamePk)+"/feed/live").json() 62 | live = {} 63 | url = "https://statsapi.mlb.com/api/v1.1/game/"+str(gamePk)+"/feed/live" 64 | try: 65 | live = requests.get(url).json() 66 | except Exception as e: 67 | print("Exception getting",url,"\n",e) 68 | self.status = "Error" 69 | self.awayTeam = Team({}) 70 | self.homeTeam = Team({}) 71 | self.lineScore = {} 72 | 73 | if 'gameData' in live: 74 | gameData = live['gameData'] 75 | away = gameData['teams']['away'] 76 | home = gameData['teams']['home'] 77 | self.status = gameData['status']['detailedState'] 78 | 79 | self.awayTeam = Team(away) 80 | self.homeTeam = Team(home) 81 | 82 | currentPlay = {} 83 | liveData = live['liveData'] 84 | if 'currentPlay' in liveData['plays']: 85 | currentPlay = liveData['plays']['currentPlay'] 86 | self._set_game_data(currentPlay) 87 | self.lineScore = LineScore(liveData) 88 | 89 | def _set_game_data(self, currentPlay): 90 | if 'result' in currentPlay: 91 | self.awayScore = currentPlay['result']['awayScore'] 92 | self.homeScore = currentPlay['result']['homeScore'] 93 | self.score = str(self.awayScore) + "-" + str(self.homeScore) 94 | 95 | if self.status == "In Progress": 96 | about = currentPlay['about'] 97 | count = currentPlay['count'] 98 | 99 | isTop = about['isTopInning'] 100 | if isTop == "true": 101 | self.indicator = "top" 102 | else: 103 | self.indicator = "bot" 104 | 105 | self.balls = count['balls'] 106 | self.strikes = count['strikes'] 107 | self.outs = count['outs'] 108 | 109 | self.inning = about['inning'] 110 | if self.outs == 3: 111 | #print(currentPlay) 112 | #print(self.outs,self.indicator) 113 | if self.indicator == "top": 114 | self.indicator = "mid" 115 | else: 116 | self.indicator = "end" 117 | self.outs = 0 118 | self.balls = 0 119 | self.strikes = 0 120 | 121 | self.onFirst = False 122 | self.onSecond = False 123 | self.onThird = False 124 | 125 | if 'matchup' in currentPlay: 126 | matchup = currentPlay['matchup'] 127 | #print(matchup) 128 | if 'postOnFirst' in matchup: 129 | self.onFirst = True 130 | if 'postOnSecond' in matchup: 131 | self.onSecond = True 132 | if 'postOnThird' in matchup: 133 | self.onThird = True 134 | 135 | def __str__(self): 136 | inning = " " + self.indicator + " " + str(self.inning) 137 | outs = " " + str(self.outs) + " outs" 138 | count = " " + str(self.balls) + " balls, " + str(self.strikes) + " strikes" 139 | score = " " + self.score 140 | boxScore = "" 141 | header = "" 142 | topScore = "" 143 | botScore = "" 144 | if self.status == "In Progress": 145 | for i in range(len(self.lineScore.innings)): 146 | header += str(i+1) + " " 147 | topScore += str(self.lineScore.innings[i].awayScore) + " " 148 | botScore += str(self.lineScore.innings[i].homeScore) + " " 149 | boxScore = "\n" + header + "\n" + topScore + "\n" + botScore 150 | 151 | if self.status != "In Progress": 152 | if self.indicator == "---" or self.indicator == "mid" or self.indicator == "end": 153 | inning = "" 154 | outs = "" 155 | count = "" 156 | 157 | if self.status == "Scheduled" or self.status == "Pre-Game" or self.status == "Delayed Start": 158 | score = "" 159 | innings = "" 160 | 161 | return self.status + score + inning + outs + count + boxScore 162 | 163 | class Game: 164 | def datetime_from_utc_to_local(utc_datetime): 165 | now_timestamp = time.time() 166 | offset = datetime.fromtimestamp(now_timestamp) - datetime.utcfromtimestamp(now_timestamp) 167 | return utc_datetime + offset 168 | 169 | def __init__(self, homeTeam, awayTeam, startTime): 170 | self.home = homeTeam 171 | self.away = awayTeam 172 | self.startTime = Game.datetime_from_utc_to_local(startTime) 173 | 174 | def get_games_on_date(date): 175 | results = {} 176 | games = {} 177 | url = "http://statsapi.mlb.com/api/v1/schedule/games?sportId=1&date=" + date 178 | try: 179 | games = requests.get(url).json() 180 | except Exception as e: 181 | print("Exception getting",url,"\n",e) 182 | 183 | if 'dates' in games: 184 | for d in games['dates']: 185 | for g in d['games']: 186 | gamePk = g['gamePk'] 187 | 188 | liveData = LiveData(gamePk) 189 | 190 | results[gamePk] = Game(liveData.homeTeam, liveData.awayTeam, parse(g['gameDate'])) 191 | 192 | return results 193 | 194 | 195 | if __name__ == "__main__": 196 | date = datetime.now().strftime("%m/%d/%Y") 197 | #date = (datetime.now() + timedelta(days=-1)).strftime("%m/%d/%Y") 198 | #date = "10/23/2022" 199 | #date = "10/28/2022" 200 | 201 | games = get_games_on_date(date) 202 | if len(games) == 0: 203 | message = "No games scheduled for" + date 204 | print(message) 205 | 206 | for gamePk, game in games.items(): 207 | print(gamePk, game.away.name,"at",game.home.name,datetime.strftime(game.startTime,'%I:%m')) 208 | liveData = LiveData(gamePk) 209 | print(liveData) 210 | if liveData.status == "In Progress": 211 | while liveData.status == "In Progress": 212 | liveData = LiveData(gamePk) 213 | print(liveData) 214 | time.sleep(5) -------------------------------------------------------------------------------- /community-scripts/nest_thermostat.py: -------------------------------------------------------------------------------- 1 | # Nest Thermostat 2 | # uses the google_api.py and display_helpers.py modules to communicate with google to access nest thermostat information. 3 | # Display current set point (or range for eco and HEATCOOL modes) and humidity. 4 | # Will display in Celsius or Farenheit base on how the thermostat is configured. 5 | # the top display will show the current mode (orange waves for heat mode, blue for cool mode. other modes are "eco", fan and off). 6 | # if the lumicube has buttons, the top and bottom buttons can be used to increase or decrease the current setpoint. 7 | # the middle button will change the mode - how change mode works: 8 | # - press the middle button, the mode icon on top will flash. You have 30 seconds to change the mode with the top and bottom buttons. 9 | # - Each time you press the top or bottom button, it'll scroll forward or back in the mode options, changing the top display to the next possible mode. 10 | # - if you do select a different mode, do change to that mode, you must hit the center button again. The mode will be changed and the top display 11 | # will update to the new mode. 12 | # setpoint color on the left will change to orange if currently heating, and blue if currently cooling. 13 | # if in "range" mode (eco mode or heatcool mode) there will be an orange or blue indicator to indicate which range we're seeing. 14 | # 15 | # you WILL need a google cloud account, and will need to 16 | # - create a new google cloud project - https://console.cloud.google.com/ 17 | # - enable the Smart Device Management API on that project 18 | # - create a Web Application type credential - this will give you the oauth client_id and client_secret 19 | # - on the oauth consent screen add a google user that has permission to access the project 20 | # - create a device access project in the device access console - https://console.nest.google.com/device-access/project-list 21 | # you'll need to provide the oauth client id you got in the previous step 22 | # after this project is created, you'll have the project_id you'll need for configuration 23 | # The program will look for it's configuration in the google folder in the home directory (usually /home/pi/Desktop) 24 | # - create the google folder in the home folder 25 | # - create a file called .nest-config in the google folder, with the folling format (add your project and client id values): 26 | # { 27 | # "project_id" : "", 28 | # "client_id" : "", 29 | # "client_secret" : "" 30 | # } 31 | # 32 | # when you run the first time, the lumicube will show a lock and question mark on screens. 33 | # look at the console for a url 34 | # - copy that url to a browser 35 | # - you'll need to log in with an authorized google account, probably the one you used to create the project with, or another that you have authorized 36 | # - allow the lumicube project to access the data 37 | # - if all goes well, you should be redirected to the redirect_url that you configured - IN that url will be code. copy that code from the URL 38 | # check the console logs again, it should be asking you to put the code in a specific file 39 | # - create that file and add the code (I usually use echo to output to the file) 40 | # - note that I would recommend editing a file of a different name, and renaming it when done, or just use echo " 41 | # if you open the final file in the editor the code may see it and try to read it immediately before you've saved it. 42 | # the code will be used to fetch a refresh token and access token, which it will used to access the device api. Even if you restart it will use the saved token. 43 | # I've found that the token is good for about a week, after which you'll see the lock and question mark display - look at the console for instructions. 44 | # 45 | # Useful links: 46 | # - controlling a nest thermostat with python : https://www.wouternieuwerth.nl/controlling-a-google-nest-thermostat-with-python/ 47 | # - Device Access Guides : https://developers.google.com/nest/device-access/registration 48 | # - Thermostat API - https://developers.google.com/nest/device-access/api/thermostat 49 | # 50 | # Author: Kevin Haney 51 | # Date: 12/9/2022 52 | # v1.0 53 | # history: 54 | # - 1.0 - original (v1) 55 | 56 | import os 57 | import time 58 | import requests 59 | import json 60 | import threading 61 | 62 | import sys 63 | sys.path.insert(0, '/home/pi/AbstractFoundry/Daemon/Scripts') 64 | import display_helpers as helpers 65 | import google_api as google 66 | 67 | # right panel 68 | question = [(8,6,1),(8,5,1),(8,4,1), (8,7,2),(8,6,2),(8,5,2),(8,4,2),(8,3,2), (8,7,3),(8,6,3),(8,3,3),(8,2,3),(8,0,3), (8,7,4),(8,6,4),(8,3,4),(8,2,4),(8,0,4), 69 | (8,7,5),(8,6,5),(8,5,5), (8,6,6),(8,5,6)] 70 | # left panel(6,0,8), 71 | lock = [(1,0,8),(1,1,8),(1,2,8),(1,3,8),(1,4,8), (2,0,8),(2,1,8),(2,2,8),(2,3,8),(2,4,8),(2,5,8),(2,6,8),(2,7,8), (3,0,8),(3,1,8),(3,2,8),(3,3,8),(3,4,8),(3,7,8), 72 | (4,0,8),(4,1,8),(4,2,8),(4,3,8),(4,4,8),(4,7,8), (5,0,8),(5,1,8),(5,2,8),(5,3,8),(5,4,8),(5,5,8),(5,6,8),(5,7,8), (6,0,8),(6,1,8),(6,2,8),(6,3,8),(6,4,8)] 73 | lockShade = [(2,1,8),(2,3,8), (3,1,8),(3,3,8), (4,1,8),(4,3,8), (5,1,8),(5,3,8)] 74 | 75 | #top panel 76 | fan = [(0,8,0),(1,8,0),(6,8,0),(7,8,0), (0,8,1),(1,8,1),(2,8,1),(5,8,1),(6,8,1),(7,8,1), (1,8,2),(2,8,2),(5,8,2),(6,8,2), 77 | (3,8,3),(4,8,3), (3,8,4),(4,8,4), (1,8,5),(2,8,5),(5,8,5),(6,8,5), (0,8,6),(1,8,6),(2,8,6),(5,8,6),(6,8,6),(7,8,6), 78 | (0,8,7),(1,8,7),(6,8,7),(7,8,7)] 79 | 80 | topWave = [(0,8,2),(0,8,3),(0,8,6),(0,8,7), (1,8,1),(1,8,2),(1,8,5), (2,8,0),(2,8,1),(2,8,4),(2,8,5), (3,8,0),(3,8,3), (4,8,2),(4,8,3), (5,8,1), (6,8,0),(6,8,1)] 81 | 82 | botWave = [(1,8,6),(1,8,7), (2,8,6), (3,8,4),(3,8,5), (4,8,4),(4,8,7), (5,8,2),(5,8,3),(5,8,6),(5,8,7), (6,8,2),(6,8,5),(6,8,6), (7,8,0),(7,8,1),(7,8,4),(7,8,5)] 83 | 84 | eco1 = [(1,8,3),(1,8,4),(1,8,5),(1,8,6), (2,8,2),(2,8,3),(2,8,4),(2,8,5),(2,8,6), (3,8,2),(3,8,3),(3,8,4),(3,8,5),(3,8,6), 85 | (4,8,1),(4,8,2),(4,8,3),(4,8,4),(4,8,5),(4,8,6), (5,8,1),(5,8,2),(5,8,3),(5,8,4),(5,8,5), (6,8,1),(6,8,2),(6,8,3)] 86 | eco2 = [(2,8,5),(3,8,4),(4,8,3),(5,8,2)] 87 | 88 | def set_display_mode(leds, status): 89 | for x in range(8): 90 | for z in range(8): 91 | leds[(x,8,z)] = black 92 | 93 | if status == None: 94 | display.set_3d(leds) 95 | return 96 | 97 | if status == 'FAN': 98 | for p in fan: 99 | leds[p] = white 100 | elif status == 'HEAT': 101 | for p in topWave: 102 | leds[p] = orange 103 | for p in botWave: 104 | leds[p] = orange 105 | elif status == 'COOL': 106 | for p in topWave: 107 | leds[p] = blue 108 | for p in botWave: 109 | leds[p] = blue 110 | elif status == 'HEATCOOL': 111 | for p in topWave: 112 | leds[p] = orange 113 | for p in botWave: 114 | leds[p] = blue 115 | elif status == 'ECO': 116 | for p in eco1: 117 | leds[p] = green 118 | for p in eco2: 119 | leds[p] = hsv_colour(.225,.88,.392) # 0c2264 decimal 12, 34, 100 hsv 225 88 39.2 120 | else: 121 | for p in topWave: 122 | leds[p] = grey 123 | for p in botWave: 124 | leds[p] = grey 125 | display.set_3d(leds) 126 | 127 | def wait_on_buttons(delay): 128 | global button_warned 129 | action = None 130 | try: 131 | action = buttons.get_next_action(delay) 132 | except: 133 | if not button_warned: 134 | print('This lumicube does not appear to have buttons?') 135 | button_warned = True 136 | time.sleep(delay) 137 | 138 | return action 139 | 140 | def get_current_mode(thermostat): 141 | if thermostat.fan_on: 142 | return 'FAN' 143 | elif thermostat.eco_mode: 144 | return 'ECO' 145 | return thermostat.mode 146 | 147 | def blink_thread(leds,mode): 148 | while True: 149 | set_display_mode(leds,None) 150 | time.sleep(1) 151 | set_display_mode(leds,mode) 152 | time.sleep(1) 153 | 154 | if stop_blink_thread: 155 | break 156 | 157 | def get_next_mode(mode,forward=True): 158 | modes = ['OFF','HEAT','COOL','HEATCOOL','ECO','FAN'] 159 | 160 | for i in range(len(modes)): 161 | if modes[i] == mode: 162 | break 163 | 164 | if forward: 165 | i += 1 166 | if i == len(modes): 167 | i=0 168 | else: 169 | i -= 1 170 | if i < 0: 171 | i = len(modes)-1 172 | 173 | return modes[i] 174 | 175 | # once they press the middle button, we're in wait mode. 176 | # we'll flash the current selected mode with a thread. 177 | # we'll then wait for 30 seconds for another button to be pressed. 178 | # if they push top or bottom button, we'll tell the blink thread to stop and wait, 179 | # change the display mode to the next available mode, and start that blinking 180 | # if they pressed the middle button, we'll stop the blink, then change the mode of the thermostat to the currently selected mode and return. 181 | # if 30 seconds elapses before they press a button, we exit without any changes 182 | def change_mode(leds,thermostat): 183 | global stop_blink_thread 184 | current_mode = get_current_mode(thermostat) 185 | new_mode = current_mode 186 | waiting = True 187 | while waiting: 188 | stop_blink_thread = False 189 | t1 = threading.Thread(target=blink_thread, args=(leds,new_mode)) 190 | t1.start() 191 | 192 | button = wait_on_buttons(10) 193 | stop_blink_thread = True 194 | t1.join() 195 | 196 | if button == None: 197 | print('no selection') 198 | set_display_mode(leds,current_mode) 199 | waiting = False 200 | elif button == 'top': 201 | new_mode = get_next_mode(new_mode,True) 202 | set_display_mode(leds,new_mode) 203 | elif button == 'bottom': 204 | new_mode = get_next_mode(new_mode,False) 205 | elif button == 'middle': 206 | if current_mode != new_mode or new_mode == 'FAN': 207 | print(f'changing mode from {current_mode} to {new_mode}') 208 | api.change_mode(new_mode,thermostat) 209 | else: 210 | print('no change') 211 | waiting = False 212 | 213 | def handle_action(leds,action,thermostat,heat): 214 | if action == None: 215 | return False 216 | if action == 'top': 217 | api.change_setpoint(1,thermostat,heat) 218 | elif action == 'bottom': 219 | api.change_setpoint(-1,thermostat,heat) 220 | elif action == 'middle': 221 | change_mode(leds,thermostat) 222 | time.sleep(1) 223 | return True 224 | 225 | def display_test(leds): 226 | set_display_mode(leds,'HEAT') 227 | time.sleep(3) 228 | set_display_mode(leds,'COOL') 229 | time.sleep(3) 230 | set_display_mode(leds,'HEATCOOL') 231 | time.sleep(3) 232 | set_display_mode(leds,'FAN') 233 | time.sleep(3) 234 | set_display_mode(leds,'ECO') 235 | time.sleep(3) 236 | set_display_mode(leds,'OFF') 237 | time.sleep(3) 238 | 239 | def show_security(leds): 240 | display.set_all(black) 241 | helpers.set_left(leds, lock, yellow, black, True) 242 | lockshadow = hsv_colour(.14,.063,.812) # cfc532, decimal 207, 197, 50 hsv 14 6.3 81.2 243 | helpers.set_left(leds, lockShade, lockshadow, black, False) 244 | helpers.set_right(leds, question, red, black, True) 245 | display.set_3d(leds) 246 | 247 | def get_set_temperature(t, heat): 248 | if t.mode == 'OFF': 249 | return None 250 | 251 | if t.eco_mode: 252 | if heat: 253 | return t.setpoint_eco_heat 254 | else: 255 | return t.setpoint_eco_cool 256 | if t.mode == 'HEAT': 257 | return t.setpoint_heat 258 | if t.mode == 'COOL': 259 | return t.setpoint_cool 260 | if t.mode == 'HEATCOOL': 261 | if t.status == 'HEATING': 262 | return t.setpoint_heat 263 | elif t.status == 'COOLING': 264 | return t.setpoint_cool 265 | else: 266 | if heat: 267 | return t.setpoint_heat 268 | else: 269 | return t.setpoint_cool 270 | 271 | return None 272 | 273 | def get_font_color(t): 274 | font = grey 275 | if t.status == 'HEATING': 276 | font = orange 277 | elif t.status == 'COOLING': 278 | font = blue 279 | 280 | return font 281 | 282 | def get_mode(t): 283 | mode = t.mode 284 | if t.fan_on: 285 | mode = 'FAN' 286 | if t.eco_mode: 287 | mode = 'ECO' 288 | 289 | return mode 290 | 291 | class Settings: 292 | def __init__(self, delay, humidity_every): 293 | # amount of time between updates 294 | self.delay = delay 295 | # normally show ambient temperature on the right, but every 'humidity_every' cycle show humidity 296 | self.humidity_every = humidity_every 297 | 298 | if __name__ == "__main__": 299 | # if they don't have buttons, print a warning but just once 300 | button_warned = False 301 | settings = Settings(delay=10,humidity_every=12) 302 | humidity_count = 0 303 | 304 | display.set_all(black) 305 | 306 | working_directory = os.getcwd() 307 | print('working directory is ',working_directory) 308 | 309 | # initialize our led dictionary^M 310 | leds = {} 311 | for x in range(9): 312 | for y in range(9): 313 | for z in range(9): 314 | leds[(x,y,z)] = black 315 | 316 | api = google.GoogleApi(working_directory) 317 | #print(api.refresh_token,api.access_token,api.is_permissioned()) 318 | if api.is_permissioned() == False: 319 | show_security(leds); 320 | 321 | if api.get_permission() == False: 322 | #show some indication that it failed? 323 | print('could not get permission') 324 | exit() 325 | 326 | # show some indication that it was successful? 327 | display.set_all(black) 328 | 329 | #display_test(leds) 330 | 331 | thermostats = api.fetch_thermostats() 332 | if len(thermostats) == 0: 333 | print('no thermostats found!') 334 | exit() 335 | 336 | last = '' 337 | heat = True 338 | while True: 339 | background = black 340 | font = white 341 | set_temp = 0 342 | for d in thermostats: 343 | t = api.fetch_thermostat(d) 344 | if t == None: 345 | print('couldn''t get thermostat - did credentials expire?') 346 | if api.is_permissioned() == False: 347 | print('restart the program and re-permission the application') 348 | show_security(leds); 349 | exit() 350 | time.sleep(10) 351 | else: 352 | #print(t) 353 | setTemp = get_set_temperature(t,heat) 354 | ambientTemp = t.ambient_temperature 355 | font = get_font_color(t) 356 | mode = get_mode(t) 357 | 358 | set_display_mode(leds, mode) 359 | 360 | text = f'{t}' 361 | if last != text: 362 | print(text) 363 | last = text 364 | 365 | if setTemp is not None: 366 | setTemp = int(round(setTemp,0)) 367 | helpers.set_digits(leds,'left',setTemp,font,background) 368 | humidity_count +=1 369 | if humidity_count > settings.humidity_every: 370 | humidity_count = 0 371 | helpers.set_digits(leds,'right',int(round(t.humidity,0)),yellow,background) 372 | else: 373 | helpers.set_digits(leds,'right',int(round(ambientTemp,0)),green,background) 374 | 375 | if (t.eco_mode or t.mode == 'HEATCOOL') and t.status == 'OFF': 376 | if heat: 377 | leds[(4,0,8)] = orange 378 | else: 379 | leds[(4,0,8)] = blue 380 | 381 | display.set_3d(leds) 382 | 383 | action = wait_on_buttons(settings.delay) 384 | if action != None: 385 | if handle_action(leds,action,t,heat): 386 | #toggling heat because if they changed temp during HEATCOOL cycle I want to show the same cycle again 387 | heat = not heat 388 | 389 | heat = not heat -------------------------------------------------------------------------------- /community-scripts/pacman.py: -------------------------------------------------------------------------------- 1 | c = cyan 2 | y = yellow 3 | pacman_and_ghost = [ 4 | [0,c,c,c,c,c,0,0, 0,0,y,y,y,y,0,0,], 5 | [c,c,c,c,c,c,c,0, 0,y,y,y,y,y,y,0,], 6 | [c,0,c,c,0,c,c,0, y,y,0,y,y,y,y,0,], 7 | [c,0,0,c,0,0,c,0, y,y,y,y,y,y,0,0,], 8 | [c,c,c,c,c,c,c,0, y,y,y,y,y,0,0,0,], 9 | [c,c,c,c,c,c,c,0, y,y,y,y,y,y,0,0,], 10 | [c,c,c,c,c,c,c,0, 0,y,y,y,y,y,y,0,], 11 | [c,0,c,0,c,0,c,0, 0,0,y,y,y,y,0,0,], 12 | ] 13 | 14 | while True: 15 | display.set_all(black) 16 | leds = {} 17 | for frame in range(0,32): 18 | for y in range(0, 8): 19 | for x in range(0, 16): 20 | x1 = x - frame + 15 21 | if x1 >= 0 and x1 < 16: 22 | leds[x, y] = pacman_and_ghost[7 - y][x1] 23 | else: 24 | leds[x, y] = black 25 | display.set_leds(leds) 26 | time.sleep(1/10) 27 | 28 | -------------------------------------------------------------------------------- /community-scripts/vesuvius.py: -------------------------------------------------------------------------------- 1 | # Volcano simulation 2 | # Bubbling magma, random lava flows, random projectiles, earthquakes... 3 | # Author : Kevin Haney 4 | # Date : 10/21/2022 5 | # v1.0 6 | 7 | import time 8 | 9 | display.set_all(black) 10 | 11 | b0 = hsv_colour(.0, .0, .750) 12 | b1 = hsv_colour(.0, .0, .650) 13 | b2 = hsv_colour(.0, .0, .550) 14 | b3 = hsv_colour(.0, .0, .450) 15 | b4 = hsv_colour(.0, .0, .350) 16 | b5 = hsv_colour(.0, .0, .250) 17 | b6 = hsv_colour(.0, .0, .150) 18 | 19 | cone = { 20 | #top 21 | (1,8,5) : b6, (1,8,4) : b5, (1,8,3) : b6, (2,8,2) : b6, 22 | (2,8,6) : b5, (2,8,5) : b3, (2,8,4) : b3, (2,8,3) : b4, (2,8,2) : b6, 23 | (3,8,7) : b5, (3,8,6) : b5, (3,8,5) : b5, (3,8,3) : b5, (3,8,2) : b4, (3,8,1) : b6, 24 | (4,8,7) : b5, (4,8,6) : b4, (4,8,2) : b3, (4,8,1) : b5, 25 | (5,8,7) : b3, (5,8,6) : b3, (5,8,5) : b5, (5,8,3) : b5, (5,8,2) : b3, (5,8,1) : b6, 26 | (6,8,7) : b2, (6,8,6) : b2, (6,8,5) : b3, (6,8,4) : b4, (6,8,3) : b3, (6,8,2) : b5, 27 | (7,8,7) : b3, (7,8,6) : b2, (7,8,5) : b2, (7,8,4) : b5, (7,8,3) : b6, 28 | #left 29 | (7,7,8) : b2, (7,6,8) : b1, (7,5,8) : b1, (7,4,8) : b1, (7,3,8) : b2, (7,2,8) : b3, (7,1,8) : b0, (7,0,8) : b0, 30 | (6,7,8) : b5, (6,6,8) : b2, (6,5,8) : b1, (6,4,8) : b1, (6,3,8) : b1, (6,2,8) : b0, (6,1,8) : b2, (6,0,8) : b3, 31 | (5,7,8) : b4, (5,6,8) : b5, (5,5,8) : b4, (5,4,8) : b1, (5,3,8) : b1, (5,2,8) : b1, (5,1,8) : b0, (5,0,8) : b0, 32 | (4,7,8) : b5, (4,6,8) : b4, (4,5,8) : b3, (4,4,8) : b4, (4,3,8) : b4, (4,2,8) : b3, (4,1,8) : b1, (4,0,8) : b0, 33 | (3,7,8) : b6, (3,6,8) : b5, (3,5,8) : b4, (3,4,8) : b3, (3,3,8) : b2, (3,2,8) : b1, (3,1,8) : b3, (3,0,8) : b0, 34 | (2,5,8) : b6, (2,4,8) : b5, (2,3,8) : b4, (2,2,8) : b2, (2,1,8) : b1, (2,0,8) : b3, 35 | (1,4,8) : b6, (1,3,8) : b5, (1,2,8) : b3, (1,1,8) : b2, (1,0,8) : b1, 36 | (0,3,8) : b6, (0,2,8) : b5, (0,1,8) : b3, (0,0,8) : b2, 37 | #right 38 | (8,7,7) : b2, (8,6,7) : b1, (8,5,7) : b1, (8,4,7) : b1, (8,3,7) : b2, (8,2,7) : b3, (8,1,7) : b0, (8,0,7) : b0, 39 | (8,7,6) : b5, (8,6,6) : b2, (8,5,6) : b1, (8,4,6) : b1, (8,3,6) : b1, (8,2,6) : b0, (8,1,6) : b2, (8,0,6) : b3, 40 | (8,7,5) : b4, (8,6,5) : b5, (8,5,5) : b4, (8,4,5) : b1, (8,3,5) : b1, (8,2,5) : b1, (8,1,5) : b0, (8,0,5) : b0, 41 | (8,7,4) : b5, (8,6,4) : b4, (8,5,4) : b3, (8,4,4) : b4, (8,3,4) : b4, (8,2,4) : b3, (8,1,4) : b1, (8,0,4) : b0, 42 | (8,7,3) : b6, (8,6,3) : b5, (8,5,3) : b4, (8,4,3) : b3, (8,3,3) : b2, (8,2,3) : b1, (8,1,3) : b3, (8,0,3) : b0, 43 | (8,5,2) : b6, (8,4,2) : b5, (8,3,2) : b4, (8,2,2) : b2, (8,1,2) : b1, (8,0,2) : b3, 44 | (8,4,1) : b6, (8,3,1) : b5, (8,2,1) : b3, (8,1,1) : b2, (8,0,1) : b1, 45 | (8,3,0) : b6, (8,2,0) : b5, (8,1,0) : b3, (8,0,0) : b2 46 | } 47 | 48 | lava0 = [ 49 | (3,8,5), (2,8,6), (2,8,7) , (2,7,8), (2,6,8), (2,5,8), (1,5,8), (2,5,8), (1,5,8), (1,4,8), (1,4,8), (0,4,8), (0,4,8), (0,3,8) 50 | ] 51 | 52 | lava1 = [ 53 | (4,8,6), (5,8,6), (4,8,7) , (5,8,7), (4,7,8), (5,7,8), (4,6,8), (4,5,8), (3,4,8), (5,4,8), 54 | (3,3,8), (5,3,8), (3,2,8) , (5,2,8), (2,1,8), (5,1,8), (1,0,8), (5,0,8), (0,0,8), (4,0,8), (4,0,8), (4,1,8), (3,0,8), (4,0,8) 55 | ] 56 | 57 | lava2 = [ 58 | (5,8,5), (6,8,6), (5,8,6), (6,8,5), (6,8,6), (6,8,7), (7,8,6), (7,8,7), (7,7,8), (7,7,8), (7,6,8), (8,7,7), (7,5,8), (8,6,7), 59 | (7,4,8), (8,5,7), (7,3,8), (8,4,7), (7,3,8), (8,3,7), (6,3,8), (8,3,7), (6,3,8), (8,3,6), (6,2,8), (8,2,6), (5,2,8), (8,2,6), 60 | (5,1,8), (8,2,5), (6,1,8), (8,1,5), (5,0,8), (8,1,6), (4,0,8), (8,0,6), (4,1,8), (8,0,5), (4,0,8), (8,0,4), (3,0,8), (8,0,7) 61 | ] 62 | 63 | lava3 = [ 64 | (6,8,5), (6,8,4), (7,8,5) , (7,8,4), (8,7,5), (8,7,4), (8,6,5), (8,6,4), (8,5,4), (8,5,3), 65 | (8,4,4), (8,4,3), (8,3,3) , (8,3,2), (8,2,3), (8,2,2), (8,1,2), (8,1,1), (8,0,3), (8,0,1), (8,0,2), (8,0,0) 66 | ] 67 | 68 | lava4 = [ 69 | (5,8,3), (6,8,2), (7,8,8) , (8,7,2), (8,6,2), (8,5,2), (8,5,1), (8,5,2), (8,5,1), (8,4,1), (8,4,1), (8,4,0), (8,4,0), (8,3,0) 70 | ] 71 | 72 | flows = [ 73 | lava0, 74 | lava1, 75 | lava2, 76 | lava3, 77 | lava4 78 | ] 79 | 80 | fireball0 = [ 81 | (4,8,5), (4,8,5), (3,8,4), (2,8,3), (1,8,2), (1,8,2), (0,8,3), (1,8,4), (2,8,5), (3,8,6), (4,8,7), (5,7,8), (5,6,8), (5,5,8), 82 | (5,5,8), (4,6,8), (3,5,8), (3,4,8), (3,3,8), (3,2,8), (3,2,8), (2,3,8), (1,2,8), (1,1,8), (1,0,8) 83 | ] 84 | 85 | fireball1 = [ 86 | (4,8,5), (4,8,5), (3,8,4), (2,8,3), (1,8,2), (1,8,2), (0,8,3), (1,8,4), (2,8,5), (3,8,6), (4,8,7), (5,7,8), (5,6,8), (5,5,8), 87 | (5,5,8), (6,6,8), (7,5,8), (7,4,8), (7,3,8), (7,2,8), (7,2,8), (5,3,8), (4,2,8), (4,1,8), (4,0,8) 88 | ] 89 | 90 | fireball2 = [ 91 | (5,8,4), (5,8,4), (4,8,3), (3,8,2), (2,8,1), (2,8,1), (3,8,0), (4,8,1), (5,8,2), (6,8,3), (7,8,4), (8,7,5), (8,6,5), (8,5,5), 92 | (8,5,5), (8,6,4), (8,5,3), (8,4,3), (8,3,3), (8,2,3), (8,2,3), (8,3,2), (8,2,1), (8,1,1), (8,0,1) 93 | ] 94 | 95 | fireball3 = [ 96 | (5,8,4), (5,8,4), (4,8,3), (3,8,2), (2,8,1), (2,8,1), (3,8,0), (4,8,1), (5,8,2), (6,8,3), (7,8,4), (8,7,5), (8,6,5), (8,5,5), 97 | (8,5,5), (8,6,4), (8,5,3), (8,4,3), (8,3,3), (8,2,3), (8,2,3), (8,3,4), (8,2,5), (8,1,5), (8,0,5) 98 | ] 99 | 100 | fireballs = [ 101 | fireball0, 102 | fireball1, 103 | fireball2, 104 | fireball3 105 | ] 106 | 107 | 108 | def LedDictionary(): 109 | leds = {} 110 | for x in range(9): 111 | for y in range(9): 112 | for z in range(9): 113 | leds[x,y,z] = blue 114 | return leds 115 | 116 | def Shift(direction, leds): 117 | if direction == "left": 118 | #left side 119 | for x in range(0,7): 120 | for y in range(8): 121 | leds[x,y,8] = leds[x+1,y,8] 122 | #right side 123 | for y in range(8): 124 | for z in reversed(range(1,8)): 125 | leds[8,y,z] = leds[8,y,z-1] 126 | #fill in on right 127 | leds[8,0,0] = b5 128 | leds[8,1,0] = b5 129 | leds[8,2,0] = blue 130 | leds[8,3,0] = blue 131 | #top 132 | for x in range(8): 133 | for z in reversed(range(1,7)): 134 | leds[x,8,z] = leds[x,8,z-1] 135 | else: 136 | #left side 137 | for x in reversed(range(1,8)): 138 | for y in range(8): 139 | leds[x,y,8] = leds[x-1,y,8] 140 | #right side 141 | for y in range(8): 142 | for z in (range(0,7)): 143 | leds[8,y,z] = leds[8,y,z+1] 144 | #fill in on the left 145 | leds[0,0,8] = b5 146 | leds[0,1,8] = b5 147 | leds[0,2,8] = blue 148 | leds[0,3,8] = blue 149 | #top 150 | for x in reversed(range(1,8)): 151 | for z in range(8): 152 | leds[x,8,z] = leds[x-1,8,z] 153 | 154 | return leds 155 | 156 | def Shake(leds): 157 | left = Shift("left",leds.copy()) 158 | right = Shift("right",leds.copy()) 159 | 160 | for i in range(7): 161 | display.set_3d(left) 162 | time.sleep(random.randrange(1,3)/10) 163 | display.set_3d(leds) 164 | time.sleep(random.randrange(1,3)/10) 165 | display.set_3d(right) 166 | time.sleep(random.randrange(1,3)/10) 167 | 168 | display.set_3d(leds) 169 | 170 | def RandomBubbleColor(): 171 | return hsv_colour(random.randrange(0,5)/100, random.randrange(50,100)/100, random.randrange(60,100)/100) 172 | 173 | def RandomFlowColor(): 174 | return hsv_colour(random.randrange(0,5)/100, random.randrange(60,100)/100, random.randrange(80,100)/100) 175 | 176 | def Bubble(count, leds, sleep): 177 | for c in range(count): 178 | for x in range(3,6): 179 | for z in range(3,6): 180 | leds[x,8,z] = RandomBubbleColor() 181 | display.set_3d(leds) 182 | if sleep: 183 | time.sleep(random.randrange(5,7)/10) 184 | 185 | def Flow(leds, lava): 186 | copy = leds.copy() 187 | Bubble(1,copy,False) 188 | pairs = int(len(lava)/2) 189 | # flow them on 190 | for p in range(pairs+1): 191 | # draw 2 at a time 192 | for l in range(p*2): 193 | copy[lava[l]] = RandomFlowColor() 194 | Bubble(1,copy,False) 195 | display.set_3d(copy) 196 | time.sleep(.9) 197 | # refresh all colors a few times 198 | for i in range(6): 199 | for l in range(len(lava)): 200 | copy[lava[l]] = RandomFlowColor() 201 | Bubble(1,copy,False) 202 | display.set_3d(copy) 203 | time.sleep(.9) 204 | # flow them off 205 | for p in range(1,pairs+1): 206 | copy = leds.copy() 207 | for l in range(p*2,pairs*2): 208 | copy[lava[l]] = RandomFlowColor() 209 | Bubble(1,copy,False) 210 | display.set_3d(copy) 211 | time.sleep(.9) 212 | 213 | def Fireball(leds, fb): 214 | count = len(fb) 215 | for i in range(count): 216 | copy = leds.copy() 217 | Bubble(1,copy,False) 218 | if i > 0: 219 | copy[fb[i-1]] = hsv_colour(.05, .7, .7) 220 | copy[fb[i]] = hsv_colour(.05, 1, 1) 221 | 222 | display.set_3d(copy); 223 | time.sleep(.1) 224 | 225 | def main(): 226 | leds = LedDictionary() 227 | 228 | for key, value in cone.items(): 229 | leds[key] = value 230 | 231 | display.set_3d(leds) 232 | 233 | shakeEvery = 8 234 | fireballEvery = 2 235 | shake = 0 236 | fireball = 0 237 | 238 | while True: 239 | fireball += 1 240 | if fireball >= fireballEvery: 241 | fireball = 0 242 | fb = fireballs[random.randrange(0,len(fireballs))] 243 | Fireball(leds,fb) 244 | 245 | Bubble(random.randrange(5,8), leds, True) 246 | shake += 1 247 | if shake >= shakeEvery: 248 | shake = 0 249 | Shake(leds) 250 | flow = random.randrange(0,len(flows)) 251 | Flow(leds, flows[flow]) 252 | 253 | if __name__ == "__main__": 254 | main() -------------------------------------------------------------------------------- /community-scripts/weather.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from foundry_api.standard_library import * 4 | 5 | 6 | # Predefined colors 7 | 8 | D = 0x5A5A5A # dark grey 9 | G = grey 10 | w = white 11 | r = red 12 | o = orange 13 | y = yellow 14 | g = green 15 | c = cyan 16 | b = blue 17 | m = magenta 18 | p = pink 19 | P = purple 20 | 21 | icon_sun = [ [0,o,0,o,o,0,o,0], [o,0,y,y,y,y,0,o], [0,y,y,y,y,y,y,0], [o,y,y,y,y,y,y,o], [o,y,y,y,y,y,y,o], [0,y,y,y,y,y,y,0], [o,0,y,y,y,y,0,o], [0,o,0,o,o,0,o,0] ] 22 | ##icon_clear_day = [ [0,0,y,0,0,y,0,0], [0,0,0,0,0,0,0,0], [y,0,y,y,y,y,0,y], [0,0,y,y,y,y,0,0], [0,0,y,y,y,y,0,0], [y,0,y,y,y,y,0,y], [0,0,0,0,0,0,0,0], [0,0,y,0,0,y,0,0] ] 23 | icon_moon = [ [0,0,G,w,w,w,0,0], [0,G,w,G,0,0,G,0], [G,w,w,0,0,0,0,0], [w,w,w,0,0,0,0,0], [w,w,w,0,0,0,0,0], [G,w,w,G,0,0,0,G], [0,G,w,w,w,w,G,0], [0,0,G,G,G,G,0,0] ] 24 | ##icon_clear_night = [ [0,0,b,0,0,b,0,0], [0,0,0,0,0,0,0,0], [b,0,b,b,b,b,0,b], [0,0,b,b,b,b,0,0], [0,0,b,b,b,b,0,0], [b,0,b,b,b,b,0,b], [0,0,0,0,0,0,0,0], [0,0,b,0,0,b,0,0] ] 25 | icon_clouds = [ [0,0,o,o,o,0,0,0], [0,o,y,y,y,o,0,0], [o,y,y,y,D,D,D,0], [o,y,y,D,G,G,G,D], [o,y,y,D,G,G,G,G], [o,o,D,G,G,G,G,G], [0,D,G,G,G,G,G,G], [D,G,G,G,G,G,G,G] ] 26 | icon_rain = [ [0,0,0,G,G,G,0,0], [0,0,G,G,G,G,G,0], [0,0,G,G,G,G,G,0], [G,G,G,G,G,G,G,G], [0,0,0,0,0,0,0,0], [b,0,0,b,0,0,b,0], [0,0,0,0,0,0,0,0], [0,b,0,0,b,0,0,b] ] 27 | icon_raindrop = [ [0,0,0,b,0,0,0,0], [0,0,0,b,0,0,0,0], [0,0,w,b,b,0,0,0], [0,w,w,b,b,b,0,0], [b,w,b,b,b,b,b,0], [b,b,b,b,b,b,b,0], [b,w,b,b,b,b,b,0], [0,b,b,b,b,b,0,0] ] 28 | icon_thunder = [ [0,0,o,y,y,o,0,0], [0,0,y,y,o,0,0,0], [0,o,y,o,0,0,0,0], [0,y,y,0,0,0,0,0], [o,y,y,y,y,y,o,0], [0,0,0,0,y,o,0,0], [0,0,0,y,o,0,0,0], [0,0,0,o,0,0,0,0] ] 29 | icon_snow = [ [0,0,0,w,0,0,0,0], [0,0,w,w,w,0,0,0], [0,0,0,w,0,0,w,0], [0,w,0,w,w,w,w,w], [w,w,w,w,w,0,w,0], [0,w,0,0,w,0,0,0], [0,0,0,w,w,w,0,0], [0,0,0,0,w,0,0,0] ] 30 | icon_question_mark = [ [0,r,r,r,r,r,r,0], [r,r,r,r,r,r,r,r], [r,r,0,0,0,0,r,r], [0,0,0,0,0,0,r,r], [0,0,0,r,r,r,r,0], [0,0,0,r,r,0,0,0], [0,0,0,0,0,0,0,0], [0,0,0,r,r,0,0,0] ] 31 | 32 | 33 | # import required modules 34 | import requests, json 35 | 36 | # Enter your API key here 37 | api_key = "" # Create an account and put the API key here 38 | 39 | # base_url variable to store url 40 | base_url = "http://api.openweathermap.org/data/2.5/weather?" 41 | 42 | # Give city id 43 | city_id = # Look up your city on openweathermap.org 44 | 45 | # complete_url variable to store 46 | # complete url address 47 | complete_url = base_url + "appid=" + api_key + "&id=" + city_id 48 | 49 | # get method of requests module 50 | # return response object 51 | response = requests.get(complete_url) 52 | 53 | # json method of response object 54 | # convert json format data into 55 | # python format data 56 | x = response.json() 57 | 58 | # Now x contains list of nested dictionaries 59 | # Check the value of "cod" key is equal to 60 | # "404", means city is found otherwise, 61 | # city is not found 62 | if x["cod"] != "404": 63 | 64 | # store the value of "main" 65 | # key in variable y 66 | y = x["main"] 67 | 68 | # store the value corresponding 69 | # to the "temp" key of y 70 | current_temperature = y["temp"] 71 | celsius_temperature = int(current_temperature - 273.15) 72 | 73 | # store the value corresponding 74 | # to the "pressure" key of y 75 | current_pressure = y["pressure"] 76 | 77 | # store the value corresponding 78 | # to the "humidity" key of y 79 | current_humidity = y["humidity"] 80 | 81 | # store the value of "weather" 82 | # key in variable w 83 | w = x["weather"] 84 | 85 | # store the value corresponding 86 | # to the "description" key at 87 | # the 0th index of z 88 | weather_description = w[0]["description"] 89 | 90 | # colour of the text based on temperature 91 | if celsius_temperature >= 40: colour = red 92 | elif celsius_temperature >= 30 and celsius_temperature < 40: colour = orange 93 | elif celsius_temperature >= 20 and celsius_temperature < 30: colour = yellow 94 | elif celsius_temperature >= 10 and celsius_temperature < 20: colour = green 95 | elif celsius_temperature >= 0 and celsius_temperature < 10: colour = cyan 96 | elif celsius_temperature >= -10 and celsius_temperature < 0: colour = blue 97 | else: colour = white 98 | 99 | z = colour # because i use 'z' in the number icons 100 | 101 | # number icons 102 | 103 | number_one = [ [0,0,0,0,0,0,0,0], [z,z,z,z,0,0,0,0], [0,0,0,z,0,0,0,0], [0,0,0,z,0,0,0,0], [0,0,0,z,0,0,0,0], [0,0,0,z,0,0,0,0], [0,0,0,z,0,0,0,0], [z,z,z,z,z,z,z,0] ] 104 | number_two = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,0,0], [z,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0] ] 105 | number_three = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0] ] 106 | number_four = [ [0,0,0,0,0,0,0,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0] ] 107 | number_five = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,0,0], [z,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0] ] 108 | number_six = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,0,0], [z,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0] ] 109 | number_seven = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0] ] 110 | number_eight = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0] ] 111 | number_nine = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0], [0,0,0,0,0,0,z,0], [0,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0] ] 112 | number_zero = [ [0,0,0,0,0,0,0,0], [z,z,z,z,z,z,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,0,0,0,0,0,z,0], [z,z,z,z,z,z,z,0] ] 113 | 114 | # text based on celsius temperature 115 | strtemp = str(celsius_temperature) 116 | firstnumber = strtemp[:1] 117 | secondnumber = strtemp[1:2] 118 | 119 | if firstnumber == "1": left_number = number_one 120 | elif firstnumber == "2": left_number = number_two 121 | elif firstnumber == "3": left_number = number_three 122 | elif firstnumber == "4": left_number = number_four 123 | elif firstnumber == "5": left_number = number_five 124 | elif firstnumber == "6": left_number = number_six 125 | elif firstnumber == "7": left_number = number_seven 126 | elif firstnumber == "8": left_number = number_eight 127 | elif firstnumber == "9": left_number = number_nine 128 | elif firstnumber == "0": left_number = number_zero 129 | else: left_number = icon_question_mark 130 | 131 | if secondnumber == "1": right_number = number_one 132 | elif secondnumber == "2": right_number = number_two 133 | elif secondnumber == "3": right_number = number_three 134 | elif secondnumber == "4": right_number = number_four 135 | elif secondnumber == "5": right_number = number_five 136 | elif secondnumber == "6": right_number = number_six 137 | elif secondnumber == "7": right_number = number_seven 138 | elif secondnumber == "8": right_number = number_eight 139 | elif secondnumber == "9": right_number = number_nine 140 | elif secondnumber == "0": right_number = number_zero 141 | else: right_number = icon_question_mark 142 | 143 | 144 | # weather icon based on openweathermap icon 145 | 146 | weather_icon = w[0]["icon"] 147 | 148 | if weather_icon == "01d": icon = icon_sun 149 | elif weather_icon == "01n": icon = icon_moon 150 | elif weather_icon == "02d": icon = icon_clouds 151 | elif weather_icon == "02n": icon = icon_clouds 152 | elif weather_icon == "03d": icon = icon_clouds 153 | elif weather_icon == "03n": icon = icon_clouds 154 | elif weather_icon == "04d": icon = icon_clouds 155 | elif weather_icon == "04n": icon = icon_clouds 156 | elif weather_icon == "10d": icon = icon_rain 157 | elif weather_icon == "10n": icon = icon_rain 158 | elif weather_icon == "09d": icon = icon_raindrop 159 | elif weather_icon == "09n": icon = icon_raindrop 160 | 161 | else: icon = icon_question_mark 162 | 163 | # first set lumicube display to black 164 | display.set_all(black) 165 | # lumicube display 166 | display.set_panel("top", icon) 167 | display.set_panel("left", left_number) 168 | display.set_panel("right", right_number) 169 | 170 | else: 171 | print(" Wrong city ID ") 172 | -------------------------------------------------------------------------------- /community-scripts/yagol.py: -------------------------------------------------------------------------------- 1 | # Yet Another Game of Life variant. 2 | # Conway's Game of Life (https://en.wikipedia.org/wiki/Conway's_Game_of_Life). 3 | # modified to show new cells as green, old cells as blue, new dead cell as red 4 | # will run for as long as 5 minutes, automatically checking for stopped or loop states, when it will pause for a few seconds then restart 5 | # Author : Kevin Haney 6 | # Date : 10/9/2022 7 | # Version : 1.0 8 | 9 | import random 10 | 11 | fresh = green 12 | old = blue 13 | dead = red 14 | 15 | while True: 16 | max_turns = 300 17 | starting_live_ratio = 0.4 18 | # colour = random_colour() 19 | colour = fresh 20 | num_turns = 0 21 | alive_cells = [] 22 | for x in range(0,16): 23 | for y in range(0,16): 24 | if x < 8 or y < 8: 25 | if random.random() < starting_live_ratio: 26 | alive_cells.append((x,y)) 27 | 28 | def num_alive_neighbours(x, y): 29 | num_neighbours = 0 30 | for x2 in [x-1, x, x+1]: 31 | for y2 in [y-1, y, y+1]: 32 | if (x2, y2) != (x, y): 33 | neighbour = (x2, y2) 34 | # Account for 3D nature of panels 35 | if x2 == 8 and y2 >= 8: 36 | neighbour = (y2, 7) 37 | elif y2 == 8 and x2 >= 8: 38 | neighbour = (7, x2) 39 | if neighbour in alive_cells: 40 | num_neighbours += 1 41 | return num_neighbours 42 | 43 | leds = {} 44 | last_cells = [] # keep track of last state for when things stop changing 45 | prev_cells = [] # keep track of two states ago go catch when we're stuck in a repeating loop 46 | loop_waits = 0 47 | 48 | while (num_turns < max_turns): 49 | next_cells = [] 50 | prev_cells = last_cells 51 | last_cells = alive_cells 52 | prev_leds = leds 53 | leds = {} 54 | for x in range(0,16): 55 | for y in range(0,16): 56 | if x < 8 or y < 8: 57 | alive = (x, y) in alive_cells 58 | neighbours = num_alive_neighbours(x, y) 59 | # Remains alive 60 | if alive and (neighbours == 2 61 | or neighbours == 3): 62 | next_cells.append((x, y)) 63 | leds[x, y] = old #colour 64 | # Becomes alive 65 | elif not alive and neighbours == 3: 66 | next_cells.append((x, y)) 67 | leds[x, y] = fresh #colour 68 | # Else dead 69 | else: 70 | if alive: 71 | k = x, y 72 | if prev_leds.get((x,y)) == old: 73 | leds[x,y] = dead 74 | else: 75 | leds[x,y] = orange 76 | else: 77 | leds[x, y] = black 78 | display.set_leds(leds) 79 | alive_cells = next_cells 80 | if (alive_cells==last_cells): 81 | time.sleep(5.0) 82 | break 83 | if (alive_cells==prev_cells): 84 | loop_waits += 1 85 | if (loop_waits > 20): 86 | loop_waits = 0 87 | break 88 | time.sleep(1.0) 89 | num_turns += 1 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/autumn_scene.py: -------------------------------------------------------------------------------- 1 | # Autumn animation (tree, moon, and falling leaves). 2 | 3 | y = 0xffc000 4 | o = 0xff9000 5 | r = hsv_colour(0.04, 1, 1) 6 | b = hsv_colour(0.04, 0.7, 0.3) 7 | w = hsv_colour(0, 0, 1) 8 | top = [ 9 | [0,w,0,0,0,0,0,0], 10 | [w,0,0,0,0,0,0,0], 11 | [w,0,0,0,0,0,o,r], 12 | [0,w,0,0,o,o,o,o], 13 | [0,0,0,o,o,r,o,o], 14 | [0,0,0,o,o,o,o,o], 15 | [0,0,o,o,r,o,y,o], 16 | [0,0,o,o,o,o,o,o], 17 | ] 18 | left = [ 19 | [0,0,o,o,y,o,r,o], 20 | [0,0,0,r,o,o,o,o], 21 | [0,0,0,0,0,o,o,y], 22 | [0,0,0,0,0,0,0,b], 23 | [0,0,0,0,0,0,0,b], 24 | [0,0,0,0,0,0,0,b], 25 | [0,0,0,0,0,0,0,b], 26 | [0,0,0,0,0,0,b,b], 27 | ] 28 | right = [ 29 | [o,r,o,y,o,o,0,0], 30 | [o,o,o,o,r,0,0,0], 31 | [y,o,o,0,0,0,0,0], 32 | [b,0,0,0,0,0,0,0], 33 | [b,0,0,0,0,0,0,0], 34 | [b,0,0,0,0,0,0,0], 35 | [b,0,0,0,0,0,0,0], 36 | [b,b,0,0,0,0,0,0], 37 | ] 38 | display.set_panel('left', left) 39 | display.set_panel('right', right) 40 | display.set_panel('top', top) 41 | 42 | # Animation of leaves falling to the floor 43 | leaves = {} 44 | while True: 45 | # 30% chance of creating a new falling leaf 46 | if random.random() < 0.3: 47 | # Start at a random point on the tree 48 | y = 7 49 | x = random.randint(2,6) 50 | if (random.random() < 0.5): 51 | x += 8 52 | leaves[(x,y)] = hsv_colour(0.04 + 0.12*random.random(), 1, 1) 53 | 54 | # Move all the falling leaves 55 | leds = {} 56 | new_leaves = {} 57 | for (x,y), colour in leaves.items(): 58 | if y > 0: 59 | # If the leaf has moved, set the LED back to the original image 60 | if x < 8: 61 | leds[(x,y)] = left[7-y][x] 62 | else: 63 | leds[(x,y)] = right[7-y][x-8] 64 | # Move the leaf side to side as it falls 65 | if y % 2 == 0: 66 | x = x + 1 67 | else: 68 | x = x - 1 69 | # Move the leaf down 70 | new_leaves[(x,y-1)] = colour 71 | 72 | leaves = new_leaves 73 | leds.update(leaves) 74 | display.set_leds(leds) 75 | time.sleep(0.2) 76 | -------------------------------------------------------------------------------- /examples/binary_clock.py: -------------------------------------------------------------------------------- 1 | # Display a binary clock (https://en.wikipedia.org/wiki/Binary_clock). 2 | 3 | def draw_column(decimal_digit, x, colour): 4 | # Convert digit to four digit binary 5 | binary = list(format(decimal_digit, '04b')) 6 | # Start at the bottom with the least significant digit 7 | binary.reverse() 8 | leds = {} 9 | for i, value in enumerate(binary): 10 | # Set all the leds in a 2x2 square 11 | pixel = colour if value == '1' else black 12 | y = i * 2 13 | leds[x, y ] = pixel 14 | leds[x, y+1] = pixel 15 | leds[x+1, y ] = pixel 16 | leds[x+1, y+1] = pixel 17 | display.set_leds(leds) 18 | 19 | import datetime 20 | display.set_all(black) 21 | while True: 22 | time_now = datetime.datetime.now() 23 | seconds = format(time_now.second, '02') 24 | minutes = format(time_now.minute, '02') 25 | hours = format(time_now.hour, '02') 26 | draw_column(int(hours[0]), 0, pink) 27 | draw_column(int(hours[1]), 2, pink) 28 | draw_column(int(minutes[0]), 4, purple) 29 | draw_column(int(minutes[1]), 6, purple) 30 | draw_column(int(seconds[0]), 8, cyan) 31 | draw_column(int(seconds[1]), 10, cyan) 32 | time.sleep(1/10) 33 | -------------------------------------------------------------------------------- /examples/button.py: -------------------------------------------------------------------------------- 1 | # If the top button has been pressed an even number of times set the cube red, 2 | # otherwise set it blue. 3 | 4 | while True: 5 | if buttons.top_pressed_count % 2 == 0: 6 | display.set_all(red) 7 | else: 8 | display.set_all(blue) 9 | time.sleep(1 / 20) 10 | -------------------------------------------------------------------------------- /examples/chiptune.py: -------------------------------------------------------------------------------- 1 | # Play a randomly generated tune. 2 | 3 | while True: 4 | # Play a rising piece 5 | for frequency in range(500, 2000, 100): 6 | speaker.tone(frequency, 0.01) 7 | # Play a beat and then another beat 8 | time.sleep(0.02) 9 | speaker.tone(500, 0.1, 0.1, function=white_noise) 10 | time.sleep(0.05) 11 | speaker.tone(500, 0.1, 0.1, function=white_noise) 12 | # Play 3 different tones 13 | speaker.tone(500 + 500 * random.random(), 0.1) 14 | speaker.tone(500 + 500 * random.random(), 0.1) 15 | speaker.tone(500 + 500 * random.random(), 0.1) 16 | # Play another rising piece 17 | for frequency in range(200, 1000, 10): 18 | speaker.tone(frequency, 0.003) 19 | -------------------------------------------------------------------------------- /examples/conways_game_of_life.py: -------------------------------------------------------------------------------- 1 | # Run Conway's Game of Life (https://en.wikipedia.org/wiki/Conway's_Game_of_Life). 2 | 3 | import random 4 | max_turns = 50 5 | starting_live_ratio = 0.4 6 | colour = random_colour() 7 | num_turns = 0 8 | alive_cells = [] 9 | for x in range(0,16): 10 | for y in range(0,16): 11 | if x < 8 or y < 8: 12 | if random.random() < starting_live_ratio: 13 | alive_cells.append((x,y)) 14 | 15 | def num_alive_neighbours(x, y): 16 | num_neighbours = 0 17 | for x2 in [x-1, x, x+1]: 18 | for y2 in [y-1, y, y+1]: 19 | if (x2, y2) != (x, y): 20 | neighbour = (x2, y2) 21 | # Account for 3D nature of panels 22 | if x2 == 8 and y2 >= 8: 23 | neighbour = (y2, 7) 24 | elif y2 == 8 and x2 >= 8: 25 | neighbour = (7, x2) 26 | if neighbour in alive_cells: 27 | num_neighbours += 1 28 | return num_neighbours 29 | 30 | while (num_turns < max_turns): 31 | next_cells = [] 32 | leds = {} 33 | for x in range(0,16): 34 | for y in range(0,16): 35 | if x < 8 or y < 8: 36 | alive = (x, y) in alive_cells 37 | neighbours = num_alive_neighbours(x, y) 38 | # Remains alive 39 | if alive and (neighbours == 2 40 | or neighbours == 3): 41 | next_cells.append((x, y)) 42 | leds[x, y] = colour 43 | # Becomes alive 44 | elif not alive and neighbours == 3: 45 | next_cells.append((x, y)) 46 | leds[x, y] = colour 47 | # Else dead 48 | else: 49 | leds[x, y] = black 50 | alive_cells = next_cells 51 | display.set_leds(leds) 52 | time.sleep(0.3) 53 | num_turns += 1 54 | -------------------------------------------------------------------------------- /examples/equaliser.py: -------------------------------------------------------------------------------- 1 | # Records microphone audio, processes it into frequency buckets, 2 | # and then displays the levels of each bucket on the LEDs. 3 | 4 | num_buckets = 8 5 | 6 | display.set_all(black) 7 | pixels_per_bucket = max(1, 16/num_buckets) 8 | buckets = [0 for i in range(num_buckets)] 9 | last_time = time.time() 10 | max_magnitude = 1 # Dynamically adjust the max as we go along 11 | 12 | # Start recording audio samples 13 | microphone.start_recording_for_frequency_analysis() 14 | 15 | while 1: 16 | # Get new bucket levels, converting the audio samples into 8 frequency buckets between 0Hz and 1000Hz 17 | new_buckets = list(microphone.get_frequency_buckets(num_buckets, 0, 800).values()) 18 | 19 | # Process data, keeping track of max level 20 | for bi, magnitude in enumerate(new_buckets): 21 | buckets[bi] = max(buckets[bi], magnitude) 22 | max_magnitude = max(max_magnitude, magnitude) 23 | 24 | # Every 10th of a second update the LEDs 25 | # Note: This is done separately from the data processing because we don't want the LEDs to change too fast 26 | if time.time() - last_time > 1/10: 27 | last_time = time.time() 28 | colours = {} 29 | for y in range(0,8): 30 | for x in range(0,16): 31 | bi = int(x/pixels_per_bucket) 32 | if y/8.0 <= buckets[bi] / max_magnitude or y == 0: 33 | colours[(x,y)] = hsv_colour(1 - bi/num_buckets, 1, 1) 34 | else: 35 | colours[(x,y)] = 0 36 | display.set_leds(colours) 37 | # Reset the bucket levels 38 | buckets = [0 for i in range(num_buckets)] 39 | # Over time gradually decay the max 40 | max_magnitude *= 0.99 41 | -------------------------------------------------------------------------------- /examples/land_grab.py: -------------------------------------------------------------------------------- 1 | # Several automated players mark neighbouring LEDs with their colour, 2 | # until they get stuck. Can you make the players more intelligent? 3 | 4 | def adjacent_positions(x, y, include_diagonal=False): 5 | positions = [(x-1, y), (x+1, y), (x, y-1), (x, y+1)] 6 | if include_diagonal: 7 | positions = positions + [(x-1, y-1), (x-1, y+1), (x+1, y-1), (x+1, y+1)] 8 | mapped_positions = [] 9 | for (x,y) in positions: 10 | # Account for the 3D nature of the panels 11 | if x > 7 and y > 7: 12 | if x > 7 and y == 8: 13 | mapped_positions.append((7, x)) 14 | elif x == 8 and y > 7: 15 | mapped_positions.append((y, 7)) 16 | # Make sure x and y are valid positions 17 | elif (x < 16 and x >= 0) and (y < 16 and y >= 0): 18 | mapped_positions.append((x,y)) 19 | return mapped_positions 20 | 21 | class Player: 22 | def __init__(self, pos, colour): 23 | self.pos = pos 24 | self.colour = colour 25 | 26 | def move(self, leds): 27 | (x,y) = self.pos 28 | # Get a list of all valid positions to move to 29 | valid_positions = [] 30 | for neighbour in adjacent_positions(x, y): 31 | if neighbour not in leds: 32 | valid_positions.append(neighbour) 33 | # If there are no valid positions the player is finished, so return False 34 | if len(valid_positions) == 0: 35 | return False 36 | # Choose a random valid position 37 | pos = valid_positions[random.randrange(len(valid_positions))] 38 | (x,y) = pos 39 | self.pos = pos 40 | leds[x, y] = self.colour 41 | display.set_led(x, y, self.colour) 42 | return True 43 | 44 | num_players = 4 45 | while True: 46 | # Start a new game 47 | players = [] 48 | leds = {} 49 | display.set_all(black) 50 | # Generate all the players with random starting positions and colours 51 | for i in range(0, num_players): 52 | (x,y) = (random.randrange(16), random.randrange(16)) 53 | if x >= 8 and y >= 8: 54 | y -= 8 55 | players.append(Player((x,y), random_colour())) 56 | # Keep allowing players to move until there are no players left 57 | while len(players) > 0: 58 | died = [] 59 | # Move all the players 60 | for player in players: 61 | if player.move(leds) == False: 62 | died.append(player) 63 | # Remove finished players 64 | for player in died: 65 | players.remove(player) 66 | time.sleep(0.1) 67 | # Wait before starting a new game 68 | time.sleep(1) 69 | -------------------------------------------------------------------------------- /examples/lava_lamp.py: -------------------------------------------------------------------------------- 1 | # Generate a lava lamp effect using OpenSimplex noise. 2 | 3 | def lava_colour(x, y, z, t): 4 | scale = 0.10 5 | speed = 0.05 6 | hue = noise_4d(scale * x, scale * y, scale * z, speed * t) 7 | return hsv_colour(hue, 1, 1) 8 | 9 | def paint_cube(t): 10 | colours = {} 11 | for x in range(9): 12 | for y in range(9): 13 | for z in range(9): 14 | if x == 8 or y == 8 or z == 8: 15 | colour = lava_colour(x, y, z, t) 16 | colours[x,y,z] = colour 17 | display.set_3d(colours) 18 | 19 | t = 0 20 | while True: 21 | paint_cube(t) 22 | time.sleep(1/30) 23 | t += 1 24 | -------------------------------------------------------------------------------- /examples/pi_status_screen.py: -------------------------------------------------------------------------------- 1 | # Write some statistics about the Pi to the the screen. 2 | 3 | def to_text(value): 4 | return str("{:.1f}".format(value)) 5 | 6 | screen.draw_rectangle(0, 0, 320, 240, black) 7 | height = 36 8 | while True: 9 | text = ("IP address: " + pi.ip_address() + "\n" 10 | + "CPU temp : " + to_text(pi.cpu_temp()) + "\n" 11 | + "CPU usage : " + to_text(pi.cpu_percent()) + "\n" 12 | + "RAM usage : " + to_text(pi.ram_percent_used()) +"\n" 13 | + "Disk usage: " + to_text(pi.disk_percent()) + "\n") 14 | screen.write_text(10, 18, text, 1, white, black) 15 | time.sleep(5) 16 | -------------------------------------------------------------------------------- /examples/rain.py: -------------------------------------------------------------------------------- 1 | # Rain animation. 2 | 3 | display.set_all(black) 4 | rows = [[0 for x in range(16)] for y in range(8)] 5 | while True: 6 | # Shift all rows down 7 | rows.pop(0) 8 | # Create a new row 9 | top_row = rows[-1] 10 | new_top_row = [] 11 | for prev_pixel in top_row: 12 | new_pixel = 0 13 | # If the previous pixel was the start of a drop, create the 14 | # droplet tail by reducing the brightness for the new pixel 15 | if prev_pixel > 0: 16 | new_pixel = prev_pixel - 0.4 17 | new_pixel = max(new_pixel, 0.0) 18 | # Sometimes generate a new droplet 19 | elif random.random() < 0.1: 20 | new_pixel = 1 21 | new_top_row.append(new_pixel) 22 | rows.append(new_top_row) 23 | # Convert the brightness values to LED colours 24 | leds = {} 25 | for y in range(0,8): 26 | for x in range(0,16): 27 | leds[(x, y)] = hsv_colour(0.6, 1, rows[y][x]) 28 | display.set_leds(leds) 29 | time.sleep(1/15) 30 | -------------------------------------------------------------------------------- /examples/rainbow.py: -------------------------------------------------------------------------------- 1 | # Continually change the cube's colour. 2 | 3 | hue = 0 4 | while True: 5 | hue += 0.01 6 | if hue > 1: 7 | hue = 0 8 | display.set_all(hsv_colour(hue, 1, 1)) 9 | time.sleep(1 / 30) 10 | -------------------------------------------------------------------------------- /examples/ripples.py: -------------------------------------------------------------------------------- 1 | # Animate ripples across the cube's LEDs. 2 | 3 | class Ripple: 4 | def __init__(self, start_x, start_y, start_z, hsv, velocity=10, wavefront_size=1.3): 5 | self.start_x = start_x 6 | self.start_y = start_y 7 | self.start_z = start_z 8 | self.hsv = hsv 9 | self.velocity = velocity 10 | self.wavefront_size = wavefront_size 11 | self.start_time = time.time() 12 | self.radius = 0 13 | 14 | def draw(self, leds): 15 | self.radius = self.velocity * (time.time() - self.start_time) 16 | for x in range(0,9): 17 | for y in range(0,9): 18 | for z in range(0,9): 19 | if x == 8 or y == 8 or z == 8: 20 | # Calculate how far this pixel is from the ripple line 21 | distance = (abs(self.start_x - x) ** 2 + abs(self.start_y - y) ** 2 + abs(self.start_z - z) ** 2) ** (1/2) 22 | dist_diff = abs(self.radius - distance) 23 | if (dist_diff < self.wavefront_size): 24 | # Set the brightness based on how close the pixel is to the ripple line 25 | pos_brightness = math.cos(math.pi*dist_diff/(2*self.wavefront_size)) 26 | hue, sat, value = self.hsv 27 | brightness = value*pos_brightness 28 | if brightness > 0.05: # Set any low brightness LEDs black 29 | leds[(x,y,z)] = hsv_colour(hue, sat, brightness) 30 | 31 | def finished(self): 32 | if (self.radius > 16): 33 | return True 34 | return False 35 | 36 | display.set_all(black) 37 | ripples = [] 38 | count = 0 39 | 40 | while True: 41 | 42 | # Every 30 iterations create a new ripple 43 | if count % 30 == 0: 44 | # Pick a random 3D coordinate to start from 45 | (x, y, z) = [random.randint(0,7) for i in range(0,3)] 46 | # Make it start on one of the faces 47 | panel = random.randint(0,3) 48 | if panel == 0: 49 | z = 0 50 | elif panel == 1: 51 | x = 8 52 | else: 53 | y = 8 54 | hsv = (random.random(), 1, 1) 55 | ripples.append(Ripple(x, y, z, hsv)) 56 | 57 | # Initialise the LEDs to black 58 | leds = {} 59 | for x in range(0,9): 60 | for y in range(0,9): 61 | for z in range(0,9): 62 | if x == 8 or y == 8 or z == 8: 63 | leds[(x,y,z)] = 0; 64 | 65 | # Draw all the ripples 66 | for r in ripples: 67 | r.draw(leds) 68 | 69 | # Remove any ripples that have finished 70 | to_remove = [] 71 | for r in ripples: 72 | if r.finished(): 73 | to_remove.append(r) 74 | for r in to_remove: 75 | ripples.remove(r) 76 | 77 | display.set_3d(leds, True) 78 | time.sleep(1/20) 79 | count += 1 80 | -------------------------------------------------------------------------------- /examples/scrolling_clock.py: -------------------------------------------------------------------------------- 1 | # Every 20 seconds scroll the time across the cube. 2 | 3 | import datetime 4 | display.set_all(black) 5 | while True: 6 | time_text = datetime.datetime.now().strftime("%H:%M") 7 | display.scroll_text(time_text, orange) 8 | time.sleep(20) 9 | -------------------------------------------------------------------------------- /examples/tapping_ripples.py: -------------------------------------------------------------------------------- 1 | # Animate ripples across the cube's LEDs, which respond 2 | # to tapping the cube (or accelerating it in some way). 3 | # Note: Requires the advanced kit IMU add-on. 4 | 5 | import threading 6 | 7 | class Ripple: 8 | def __init__(self, start_x, start_y, start_z, hsv, velocity=10, wavefront_size=1.3): 9 | self.start_x = start_x 10 | self.start_y = start_y 11 | self.start_z = start_z 12 | self.hsv = hsv 13 | self.velocity = velocity 14 | self.wavefront_size = wavefront_size 15 | self.start_time = time.time() 16 | self.radius = 0 17 | 18 | def draw(self, leds): 19 | self.radius = self.velocity * (time.time() - self.start_time) 20 | for x in range(0,9): 21 | for y in range(0,9): 22 | for z in range(0,9): 23 | if x == 8 or y == 8 or z == 8: 24 | # Calculate how far this pixel is from the ripple line 25 | distance = (abs(self.start_x - x) ** 2 + abs(self.start_y - y) ** 2 + abs(self.start_z - z) ** 2) ** (1/2) 26 | dist_diff = abs(self.radius - distance) 27 | if (dist_diff < self.wavefront_size): 28 | # Set the brightness based on how close the pixel is to the ripple line 29 | pos_brightness = math.cos(math.pi*dist_diff/(2*self.wavefront_size)) 30 | hue, sat, value = self.hsv 31 | brightness = value*pos_brightness 32 | if brightness > 0.05: # Set any low brightness LEDs black 33 | leds[(x,y,z)] = hsv_colour(hue, sat, brightness) 34 | 35 | def finished(self): 36 | if (self.radius > 16): 37 | return True 38 | return False 39 | 40 | # Check the acceleration in a separate thread to ensure we sample it at regular intervals 41 | acc_direction = None 42 | wait_count = 0 43 | def worker(): 44 | global acc_direction, wait_count 45 | avg_acc_x, avg_acc_y, avg_acc_z = 0, 0, 0 46 | while True: 47 | acc_x, acc_y, acc_z = (abs(imu.acceleration_x), abs(imu.acceleration_y), abs(imu.acceleration_z)) 48 | avg_acc_x = avg_acc_x * 0.95 + acc_x * 0.05 49 | avg_acc_y = avg_acc_y * 0.95 + acc_y * 0.05 50 | avg_acc_z = avg_acc_z * 0.95 + acc_z * 0.05 51 | acc_x -= avg_acc_x 52 | acc_y -= avg_acc_y 53 | acc_z -= avg_acc_z 54 | max_acc = max(acc_x, acc_y, acc_z) 55 | if wait_count > 0: 56 | wait_count -= 1 57 | if max_acc > 0.15 and wait_count == 0: 58 | wait_count = 5 59 | if max_acc == acc_x: 60 | acc_direction = "x" 61 | elif max_acc == acc_y: 62 | acc_direction = "y" 63 | else: 64 | acc_direction = "z" 65 | time.sleep(0.02) 66 | threading.Thread(target=worker, daemon=True).start() 67 | 68 | display.set_all(black) 69 | ripples = [] 70 | 71 | while True: 72 | # If an acceleration is detected create new ripple 73 | if acc_direction != None: 74 | # Make it start in the middle of the accelerated face 75 | if acc_direction == "x": 76 | x, y, z = 8, 4, 4 77 | elif acc_direction == "y": 78 | x, y, z = 4, 8, 4 79 | else: 80 | x, y, z = 4, 4, 0 81 | hsv = (random.random(), 1, 1) 82 | ripples.append(Ripple(x, y, z, hsv)) 83 | # Reset the acceleration direction 84 | acc_direction = None 85 | 86 | # Initialise the LEDs to black 87 | leds = {} 88 | for x in range(0,9): 89 | for y in range(0,9): 90 | for z in range(0,9): 91 | if x == 8 or y == 8 or z == 8: 92 | leds[(x,y,z)] = 0; 93 | 94 | # Draw all the ripples 95 | for r in ripples: 96 | r.draw(leds) 97 | 98 | # Remove any ripples that have finished 99 | to_remove = [] 100 | for r in ripples: 101 | if r.finished(): 102 | to_remove.append(r) 103 | for r in to_remove: 104 | ripples.remove(r) 105 | 106 | display.set_3d(leds, True) 107 | time.sleep(1/20) 108 | -------------------------------------------------------------------------------- /examples/voice_recognition.py: -------------------------------------------------------------------------------- 1 | # Say "Hey Mycroft" to wake up the voice recognition, 2 | # and then say a sentence. Your sentence will then be 3 | # repeated back to you. 4 | 5 | microphone.start_voice_recognition() 6 | while (True): 7 | # Wait up to 1000 seconds for some speech 8 | sentence = microphone.wait_for_sentence(1000.0) 9 | if sentence: 10 | # Convert text back to speech 11 | speaker.say('You said ' + sentence) 12 | -------------------------------------------------------------------------------- /examples/water_level.py: -------------------------------------------------------------------------------- 1 | # Pick up the cube and rotate it - this project makes it look like there is water inside. 2 | # Note: Requires the advanced kit IMU add-on. 3 | 4 | def dot_product(vector1, vector2): 5 | (x1,y1,z1) = vector1 6 | (x2,y2,z2) = vector2 7 | return (x1 * x2) + (y1 * y2) + (z1 * z2) 8 | 9 | def led_below_water_level(x, y, z, gravity): 10 | mid_point_height = dot_product((4.5, 4.5, 4.5), gravity) 11 | led_bottom = (x, y, z) 12 | led_top = (x+1, y+1, z+1) 13 | max_height = max(dot_product(led_bottom, gravity), \ 14 | dot_product(led_top, gravity)) 15 | return (max_height < mid_point_height) 16 | 17 | while True: 18 | gravity = (imu.gravity_x, imu.gravity_y, imu.gravity_z) 19 | leds = {} 20 | for x in range(9): 21 | for y in range(9): 22 | for z in range(9): 23 | if x == 8 or y == 8 or z == 8: 24 | if led_below_water_level(x,y,z, gravity): 25 | leds[x, y, z] = cyan 26 | else: 27 | leds[x, y, z] = black 28 | time.sleep(0.05) 29 | display.set_3d(leds) 30 | -------------------------------------------------------------------------------- /examples/windmill.py: -------------------------------------------------------------------------------- 1 | # Blow on the back of the cube to make the windmill animation turn. 2 | 3 | import threading 4 | 5 | humidity = 100 6 | 7 | def worker(): 8 | global humidity 9 | while True: 10 | humidity = env_sensor.humidity 11 | time.sleep(0.05) 12 | 13 | threading.Thread(target=worker, daemon=True).start() 14 | 15 | def windmill_shader(x, y, blade_angle): 16 | theta = 360 * (0.5 + math.atan2(y, x) / (2 * math.pi)) 17 | modulo = (3 * theta) % 360 18 | difference = abs(modulo - blade_angle) 19 | return hsv_colour(0, 0, 500 / difference ** 2 if difference > 0 else 1) 20 | 21 | decay = 0.07 22 | sample_period = 0.025 23 | threshold = 0.15 24 | previous_sample = 100 25 | next_sample_time = time.monotonic() + sample_period 26 | accumulator = 0 27 | blade_angle = 0 28 | rotational_velocity = 0 29 | max_velocity = 70 30 | while True: 31 | now = time.monotonic() 32 | if time.monotonic() > next_sample_time: 33 | sample = humidity 34 | if sample > previous_sample + threshold: 35 | accumulator = 3 36 | previous_sample = sample 37 | next_sample_time = now + sample_period 38 | rotational_velocity = max_velocity * min(accumulator, 1) 39 | blade_angle = (blade_angle + rotational_velocity) % 360 40 | canvas = {} 41 | for x in range(9): 42 | for y in range(9): 43 | for z in range(9): 44 | if x == 8 or y == 8 or z == 8: 45 | projected_x = (x - z) 46 | projected_y = (2 * y - x - z) / (3 ** 0.5) 47 | canvas[(x,y,z)] = windmill_shader(projected_x, projected_y, blade_angle) 48 | display.set_3d(canvas, True) 49 | accumulator *= (1 - decay) 50 | time.sleep(1 / 25) 51 | -------------------------------------------------------------------------------- /official-documentation/api.txt: -------------------------------------------------------------------------------- 1 | =================================== 2 | Pre-defined variables and functions 3 | =================================== 4 | 5 | black = 0x000000 6 | grey = 0x808080 7 | white = 0xFFFFFF 8 | red = 0xFF0000 9 | orange = 0xFF8C00 10 | yellow = 0xFFFF00 11 | green = 0x00FF00 12 | cyan = 0x00FFFF 13 | blue = 0x0000FF 14 | magenta = 0xFF00FF 15 | pink = 0xFF007F 16 | purple = 0x800080 17 | 18 | hsv_colour(hue, saturation, value) 19 | random_colour() 20 | noise_2d(x, y) 21 | noise_3d(x, y, z) 22 | noise_4d(x, y, z, w) 23 | run_async(task, *args, **kwargs) 24 | LumiCube(ip_address) 25 | 26 | 27 | ======================== 28 | Default imported modules 29 | ======================== 30 | 31 | import time 32 | import math 33 | import random 34 | 35 | 36 | =========== 37 | LED display 38 | =========== 39 | 40 | display.brightness # 0 - 100 41 | 42 | display.set_all(colour) 43 | display.set_led(x, y, colour) 44 | display.set_leds(x_y_to_colour_dict) # { (x1, y1): colour1, (x2, y2): colour2, ... } 45 | display.set_panel(panel, 2d_colour_list) # 2d_colour_list is a list of 8 lists, each containing 8 colours 46 | display.scroll_text(text, colour=white, background_colour=black, speed=1) 47 | display.set_3d(x_y_z_to_colour_dict) # { (x1, y1, z1): colour1, (x2, y2, z2): colour2, ... } 48 | 49 | 3D coordinate system (looking at the 3 LED panels, the origin is at the back corner of the cube): 50 | 51 | y 52 | | 53 | | 54 | _-` `-_ 55 | z _-` `-_ x 56 | 57 | - The left panel is described by: x, y when z = 8 58 | - The right panel is described by: y, z when x = 8 59 | - The top panel is described by: x, z when y = 8 60 | - Coordinates outside this range will be ignored 61 | 62 | 63 | ========== 64 | Microphone 65 | ========== 66 | 67 | microphone.enable # 0 or 1 68 | 69 | microphone.start_recording(file) 70 | microphone.stop_recording() 71 | microphone.start_voice_recognition() 72 | microphone.wait_for_sentence(timeout) # Returns the sentence spoken as a string 73 | microphone.stop_voice_recognition() 74 | microphone.start_recording_for_frequency_analysis() 75 | microphone.get_frequency_buckets(num_buckets=8, min_hz=0, max_hz=4000) 76 | 77 | 78 | ======= 79 | Speaker 80 | ======= 81 | 82 | speaker.volume # 0 - 200 83 | 84 | speaker.play(path) 85 | speaker.stop() 86 | speaker.say(text) 87 | speaker.tone(frequency=261.626, duration=0.5, amplitude=0.25, function=sine_wave) 88 | 89 | 90 | ====== 91 | Screen 92 | ====== 93 | 94 | screen.set_pixel(x, y, colour) 95 | screen.set_pixels(x, y, width, height, pixels) 96 | screen.draw_rectangle(x, y, width, height, colour) 97 | screen.write_text(x, y, text, size, colour, background_colour) 98 | screen.draw_image(path, x, y, width, height) 99 | 100 | 101 | ======= 102 | Buttons 103 | ======= 104 | 105 | buttons.top_pressed 106 | buttons.middle_pressed 107 | buttons.bottom_pressed 108 | buttons.top_pressed_count 109 | buttons.middle_pressed_count 110 | buttons.bottom_pressed_count 111 | 112 | buttons.get_next_action(timeout) # Returns either 'top', 'middle', or 'bottom' 113 | 114 | 115 | ============ 116 | Light sensor 117 | ============ 118 | 119 | buttons.get_next_gesture(timeout) # Returns either 'up', 'down', 'left' or 'right' 120 | 121 | light_sensor.ambient_light 122 | light_sensor.red 123 | light_sensor.green 124 | light_sensor.blue 125 | light_sensor.last_gesture 126 | light_sensor.num_gestures 127 | light_sensor.within_proximity 128 | light_sensor.num_times_within_proximity 129 | 130 | 131 | ======================== 132 | Orientation sensor (IMU) 133 | ======================== 134 | 135 | imu.pitch 136 | imu.roll 137 | imu.yaw 138 | imu.acceleration_x 139 | imu.acceleration_y 140 | imu.acceleration_z 141 | imu.angular_velocity_x 142 | imu.angular_velocity_y 143 | imu.angular_velocity_z 144 | imu.gravity_x 145 | imu.gravity_y 146 | imu.gravity_z 147 | 148 | 149 | ================== 150 | Environment sensor 151 | ================== 152 | 153 | env_sensor.temperature 154 | env_sensor.pressure 155 | env_sensor.humidity 156 | 157 | 158 | ============ 159 | Raspberry Pi 160 | ============ 161 | 162 | pi.ip_address() 163 | pi.cpu_temp() 164 | pi.cpu_percent() 165 | pi.ram_percent_used() 166 | pi.disk_percent() 167 | 168 | 169 | ======================== 170 | Using multiple LumiCubes 171 | ======================== 172 | 173 | Technically the objects above are also available on an object called "cube", so: 174 | 175 | display.set_led(0, 0, red) 176 | 177 | is the same as: 178 | 179 | cube.display.set_led(0, 0, red) 180 | 181 | If you want to interact with a different LumiCube, create another cube object using the IP address of the other cube. 182 | You can then access all the fields and methods of the remote cube in the same way: 183 | 184 | cube2 = Cube("192.168.0.14") 185 | cube2.display.set_led(0, 0, red) 186 | 187 | Note: It will be communicating over a WiFi network, so the responsiveness will depend on your network speed and reliability. 188 | --------------------------------------------------------------------------------