├── .gitignore ├── LICENSE ├── README.md ├── demos └── demo1.py ├── examples ├── colour-picker.py ├── decorators.py ├── hid-keys-simple.py ├── rainbow.py └── reactive-press.py ├── rgbkeypad-1.jpg └── rgbkeypad.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .idea 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Angainor Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rgbkeypad-circuitpython 2 | CircuitPython library for the Pimoroni RGB Keypad base - PIM551 - Raspberry Pi Pico. 3 | This is a 16-key keypad with RGB backlit keys. Find out more about RGB Keypad at the link below. 4 | 5 | [Learn more about Pico RGB Keypad at pimoroni.com](https://shop.pimoroni.com/products/pico-rgb-keypad-base) 6 | 7 | This library is a fork of the awesome Keybow 2040 library by @SandyJMacDonald, adjusted for the RGB Keypad. 8 | 9 | [See the Original Keybow 2040 library](https://github.com/sandyjmacdonald/keybow2040-circuitpython) 10 | 11 | The library abstracts away most of the complexity of having to check pin states, 12 | interact with the LED driver library and exposes classes for 13 | individual keys and the whole Keypad (a collection of Key instances). 14 | 15 | ![RGB Keypad with backlit keys](rgbkeypad-1.jpg) 16 | 17 | # Getting started quickly! 18 | 19 | You'll need to grab the latest version of Adafruit's Pico CircuitPython firmware, from the link below. 20 | 21 | [Download the Adafruit CircuitPython binary for Raspberry Pico and follow install instructions](https://circuitpython.org/board/pimoroni_rgbkeypad/) 22 | 23 | The Adafruit Dotstar LED driver library for CircuitPython is a prerequisite for 24 | this RGB Keypad library, so you'll need to download it from GitHub at the link 25 | below, and then drop the `adafruit_dotstar.mpy` folder into the `lib` folder on 26 | your `CIRCUITPY` drive. 27 | 28 | [Download the Adafruit DotStar CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_DotStar/releases) 29 | This lib is part of the Adafruit Bundle. 30 | [Adafruit CircuitPython Bundle](https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases) 31 | 32 | Finally, drop the `rgbkeypad.py` file from this library into the `lib` folder 33 | on your `CIRCUITPY` drive also, and you're all set! 34 | 35 | Pick one of the [demos](demos), copy the 36 | code, and save it in the `code.py` file on your `CIRCUITPY` drive using your 37 | favourite text editor. As soon as you save the `code.py` file, or make any other 38 | changes, then it should load up and run the code! 39 | 40 | You can also check and just slightly adapt SandyJMacDonald's examples from his Keybow lib. 41 | Apart from the constructor (no need for an I2C port to be provided), all should be compatible. 42 | 43 | Some exemples have been converted for rgbkeypad, see [examples](examples) 44 | 45 | [Almost compatible Keybow examples](https://github.com/sandyjmacdonald/keybow2040-circuitpython/tree/master/examples) 46 | 47 | ## Index 48 | 49 | * [Library functionality](#library-functionality) 50 | * [Imports and setup](#imports-and-setup) 51 | * [The RgbKeypad class](#the-rgbkeypad-class) 52 | * [An interlude on timing!](#an-interlude-on-timing) 53 | * [Key presses](#key-presses) 54 | * [LEDs!](#leds) 55 | * [LED sleep](#led-sleep) 56 | * [Attaching functions to keys with decorators](#attaching-functions-to-keys-with-decorators) 57 | * [Key combos](#key-combos) 58 | * [USB HID](#usb-hid) 59 | * [Setup](#setup) 60 | * [Sending key presses](#sending-key-presses) 61 | * [Sending strings of text](#sending-strings-of-text) 62 | * [USB MIDI](#usb-midi) 63 | * [Sending MIDI notes](#sending-midi-notes) 64 | 65 | # Library functionality 66 | 67 | This section covers most of the functionality of the library itself, without 68 | delving into additional functions like USB MIDI or HID (they're both covered 69 | later!) 70 | 71 | ## Imports and setup 72 | 73 | All of your programs will need to start with the following: 74 | 75 | ``` 76 | from rgbkeypad import RgbKeypad 77 | 78 | keypad = RgbKeypad() 79 | ``` 80 | 81 | The `RgbKeypad()` class, imported from the `rgbkeypad` module, is instantiated. 82 | Instantiating this sets up all of the pins, keys and LEDs, and provides access to all of the attributes and methods associated with it. 83 | 84 | ## The RgbKeypad class 85 | 86 | The RgbKeypad class exposes a number of handy attributes and methods. The main one 87 | you'll be interested in is the `.keys` attribute, which is a list of `Key` 88 | class instances, one for each key. 89 | 90 | ``` 91 | keys = keypad.keys 92 | ``` 93 | 94 | The indices of the keys in that list correspond to their position on the keypad, 95 | staring from the bottom left corner (when the USB connector is at the top), 96 | which is key 0, going upwards in columns, and ending at the top right corner, 97 | which is key 15. 98 | 99 | More about the `Key` class later... 100 | 101 | A **super** important method of the `RgbKeypad` class is `.update()` method. It 102 | updates all of the keys, key states, and other attributes like the time of the 103 | last key press, and sleep state of the LEDs. 104 | 105 | **You need to call this method on your `RgbKeypad` class at the very start of each 106 | iteration of your program's main loop, as follows:** 107 | 108 | ``` 109 | while True: 110 | keypad.update() 111 | ``` 112 | 113 | ## An interlude on timing! 114 | 115 | Another **super** important thing is **not to include any `time.sleep()`s in 116 | your main loop!** Doing so will ruin the latency and mean that you'll miss key 117 | press events. Just don't do it. 118 | 119 | If you need introduce timed events, then you have to go about it in a slightly 120 | (!!) roundabout fashion, by using `time.monotonic()` a constantly incremented 121 | count of seconds elapsed, and use it to check the time elapsed since your last 122 | event, for example you could do this inside your `while True` loop: 123 | 124 | ``` 125 | time_interval = 10 126 | 127 | # An event just happened! 128 | 129 | time_last_fired = time.monotonic() 130 | time_elapsed = 0 131 | 132 | # ... some iterations later 133 | 134 | time_elapsed = time.monotonic() - time_last_fired 135 | 136 | if time_elapsed > time_interval: 137 | # Fire your event again! 138 | ``` 139 | 140 | There's a handy `keypad.time_of_last_press` attribute that allows you to quickly 141 | check if a certain amount of time has elapsed since any key press, and that 142 | attribute gets updated every time `keypad.update()` is called. 143 | 144 | ## Key presses 145 | 146 | There are a few ways that you can go about detecting key presses, some 147 | global methods on the `RgbKeypad` class instance, and some on the `Key` class 148 | instances themselves. 149 | 150 | ### RgbKeypad class methods for detecting presses and key states 151 | 152 | `keypad.get_states()` will return a list of the state of all of the keys, in 153 | order, with a state of `0` being not pressed, and `1` being pressed. You can 154 | then loop through that list to do whatever you like. 155 | 156 | `keypad.get_pressed()` will return a list of the key numbers (indices in the 157 | list of keys) that are currently pressed. If you only care about key presses, 158 | then this is an efficient way to do things, especially since you have all the 159 | key numbers in a list. 160 | 161 | `keypad.any_pressed()` returns a Boolean (`True`/`False`) that tells you whether 162 | any keys are currently being pressed. Handy if you want to attach a behaviour to 163 | all of the keys, which this is effectively a proxy for. 164 | 165 | `keypad.none_pressed()` is similar to `.any_pressed()`, in that it returns a 166 | Boolean also, but... you guessed it, it returns `True` if no keys are being 167 | pressed, and `False` if any keys are pressed. 168 | 169 | ### Key class methods for detecting key presses 170 | 171 | If we want to check whether key 0 is pressed, we can do so as follows: 172 | 173 | ``` 174 | keys = keypad.keys() 175 | 176 | while True: 177 | keypad.update() 178 | 179 | if keys[0].pressed: 180 | # Do something! 181 | ``` 182 | 183 | The `.pressed` attribute returns a Boolean that is `True` if the key is pressed 184 | and `False` if it is not pressed. 185 | 186 | `key.state` is another way to check the state of a key. It will equal `1` if the 187 | key is pressed and `0` if it is not pressed. 188 | 189 | If you want to attach an additional behaviour to your key, you can use 190 | `key.held` to check if a key is being key rather than being pressed and released 191 | quickly. It returns `True` if the key is held and `False` if it is not. 192 | 193 | The default hold time (after which `key.held` is `True`) for all of the keys is 194 | 0.75 seconds, but you can change `key.hold_time` to adjust this to your liking, 195 | on a per key basis. 196 | 197 | This means that we could extend the example above to be: 198 | 199 | ``` 200 | keys = keypad.keys() 201 | 202 | while True: 203 | keypad.update() 204 | 205 | if keys[0].pressed: 206 | # Do something! 207 | 208 | if keys[0].held: 209 | # Do something else! 210 | ``` 211 | 212 | The [reactive-press.py example](examples/reactive-press.py) shows in more detail 213 | how to handle key presses. 214 | 215 | ## LEDs! 216 | 217 | LEDs can be set either globally for all keys, using the `RgbKeypad` class instance, 218 | or on a per-key basis, either through the `RgbKeypad` class, or using a `Key` class 219 | instance. 220 | 221 | To set all of the keys to the same colour, you can use the `.set_all()` method 222 | of the `RgbKeypad` class, to which you pass three 0-255 integers for red, green, 223 | and blue. For example, to set all of the keys to magenta: 224 | 225 | ``` 226 | keypad.set_all(255, 0, 255) 227 | ``` 228 | 229 | To set an individal key through your `RgbKeypad` class instance, you can do as 230 | follows, to set key 0 to white: 231 | 232 | ``` 233 | keypad.set_led(0, 255, 255, 255) 234 | ``` 235 | 236 | To set the colour on the key itself, you could do as follows, again to set key 237 | 0 to white: 238 | 239 | ``` 240 | keypad.keys[0].set_led(255, 255, 255) 241 | ``` 242 | 243 | A key retains its RGB value, even if it is turned off, so once a key has its 244 | colour set with `key.rgb = (255, 0, 0)` for example, you can turn it off using 245 | `key.led_off()` or even `key.set_led(0, 0, 0)` and then when you turn it back on 246 | with `key.led_on()`, then it will still be red when it comes back on. 247 | 248 | As a convenience, and to avoid having to check `key.lit`, there is a 249 | `key.toggle_led()` method that will toggle the current state of the key's LED 250 | (on to off, and _vice versa_). 251 | 252 | There's a handy `hsv_to_rgb()` function that can be imported from the 253 | `rgbkeypad` module to convert an HSV colour (a tuple of floats from 0.0 to 1.0) 254 | to an RGB colour (a tuple of integers from 0 to 255), as follows: 255 | 256 | ``` 257 | from rgbkeypad import hsv_to_rgb 258 | 259 | h = 0.5 # Hue 260 | s = 1.0 # Saturation 261 | v = 1.0 # Value 262 | 263 | r, g, b = hsv_to_rgb(h, s, v) 264 | ``` 265 | 266 | The [rainbow.py example](examples/rainbow.py) shows a more complex example of 267 | how to animate the keys' LEDs, including the use of the `hsv_to_rgb()` function. 268 | 269 | ============== 270 | 271 | ## LED sleep 272 | 273 | The `RgbKeypad` class has an `.led_sleep_enabled` attribute that is disabled (set to 274 | `False`) by default, and an `.led_sleep_time` attribute (set to 60 seconds by 275 | default) that determines how many seconds need to elapse before LED sleep is 276 | triggered and the LEDs turn off. 277 | 278 | The time elapsed since the last key press is constantly updated when 279 | `keypad.update()` is called in your main loop, and if the `.led_sleep_time` is 280 | exceeded then LED sleep is triggered. 281 | 282 | Because keys retain their RGB values when toggled off, when asleep, a tap on any 283 | key will wake all of the LEDs up at their last state before sleep. 284 | 285 | Enabling LED sleep with a sleep time of 10 seconds could be done as simply as: 286 | 287 | ``` 288 | keypad.led_sleep_enabled = True 289 | keypad.led_sleep_time = 10 290 | ``` 291 | 292 | There's also a `.sleeping` attribute that returns a Boolean, that you can check 293 | to see whether the LEDs are sleeping or not. 294 | 295 | ## Attaching functions to keys with decorators 296 | 297 | There are three decorators that can be attached to functions to link that 298 | function to, i) a key press, ii) a key release, or iii) a key hold. 299 | 300 | Here's an example of how you could attach a decorator to a function that lights 301 | up that key yellow when it is pressed, turns all of the LEDs on when held, and 302 | turns them all off when released: 303 | 304 | ``` 305 | from rgbkeypad import RgbKeypad 306 | 307 | keypad = RgbKeypad() 308 | keys = keypad.keys 309 | 310 | key = keys[0] 311 | rgb = (255, 255, 0) 312 | key.rgb = rgb 313 | 314 | @keypad.on_press(key) 315 | def press_handler(key): 316 | key.led_on() 317 | 318 | @keypad.on_release(key) 319 | def release_handler(key): 320 | keypad.set_all(0, 0, 0) 321 | 322 | @keypad.on_hold(key) 323 | def hold_handler(key): 324 | keypad.set_all(*rgb) 325 | 326 | while True: 327 | keypad.update() 328 | ``` 329 | 330 | The [decorators.py example](examples/decorators.py) has another example of how 331 | to use the `.on_hold()` decorator to toggle LEDs on and off when a key is held. 332 | 333 | ## Key combos 334 | 335 | Key combos can provide a way to add additional behaviours to keys that only get 336 | triggered if a combination of keys is pressed. The best way to achieve this is 337 | using the `.held` attribute of a key, meaning that the key can also have a 338 | `.pressed` behaviour too. 339 | 340 | Here's a brief example of how you could do this inside your main loop, with key 341 | 0 as the modifier key, and key 1 as the action key: 342 | 343 | ``` 344 | keys = keypad.keys 345 | 346 | modifier_key = keys[0] 347 | action_key = keys[1] 348 | 349 | while True: 350 | keypad.update() 351 | 352 | if modifier_key.held and action_key.pressed: 353 | # Do something! 354 | ``` 355 | 356 | Of course, you could chain these together, to require two modifer keys to be 357 | held and a third to be pressed, and so on... 358 | 359 | The [colour-picker.py example](examples/colour-picker.py) has an example of 360 | using a modifier key to change the hue of the keys. 361 | 362 | # USB HID 363 | 364 | This covers setting up a USB HID keyboard and linking physical key presses to 365 | keyboard key presses on a connected computer. 366 | 367 | ## Setup 368 | 369 | USB HID requires the `adafruit_hid` CircuitPython library. Download it from the 370 | link below and drop the `adafruit_hid` folder into the `lib` folder on your 371 | `CIRCUITPY` drive. 372 | 373 | [Download the Adafruit HID CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_HID) 374 | 375 | You'll need to connect your RgbKeypad to a computer using a USB cable, just like 376 | you would with a regular USB keyboard. 377 | 378 | ## Sending key presses 379 | 380 | Here's an example of setting up a keyboard object and sending a `0` key press 381 | when key 0 is pressed, using an `.on_press()` decorator: 382 | 383 | ``` 384 | import board 385 | from rgbkeypad import RgbKeypad 386 | 387 | import usb_hid 388 | from adafruit_hid.keyboard import Keyboard 389 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 390 | from adafruit_hid.keycode import Keycode 391 | 392 | keypad = RgbKeypad() 393 | keys = keypad.keys 394 | 395 | keyboard = Keyboard(usb_hid.devices) 396 | layout = KeyboardLayoutUS(keyboard) 397 | 398 | key = keys[0] 399 | 400 | @keypad.on_press(key) 401 | def press_handler(key): 402 | keyboard.send(Keycode.ZERO) 403 | 404 | while True: 405 | keypad.update() 406 | ``` 407 | 408 | You can find a list of all of the keycodes available at the 409 | [HID CircuitPython library documentation here](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#adafruit-hid-keycode-keycode). 410 | 411 | As well as sending a single keypress, you can send multiple keypresses at once, 412 | simply by adding them as additional arguments to `keyboard.send()`, e.g. 413 | `keyboard.send(Keycode.A, Keycode.B)` and so on. 414 | 415 | ## Sending strings of text 416 | 417 | Rather than the inconvenience of sending multiple keycodes using 418 | `keyboard.send()`, there's a different method to send whole strings of text at 419 | once, using the `layout` object we created. 420 | 421 | ``` 422 | import board 423 | from rgbkeypad import RgbKeypad 424 | 425 | import usb_hid 426 | from adafruit_hid.keyboard import Keyboard 427 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 428 | from adafruit_hid.keycode import Keycode 429 | 430 | keypad = RgbKeypad() 431 | keys = keypad.keys 432 | 433 | keyboard = Keyboard(usb_hid.devices) 434 | layout = KeyboardLayoutUS(keyboard) 435 | 436 | key = keys[0] 437 | 438 | @keypad.on_press(key) 439 | def press_handler(key): 440 | layout.write("Pack my box with five dozen liquor jugs.") 441 | 442 | while True: 443 | keypad.update() 444 | ``` 445 | 446 | A press of key 0 will send that whole string of text at once! 447 | 448 | Be aware that strings sent like that take a little while to virtually "type", 449 | so you might want to incorporate a delay using `keypad.time_of_last_press`, 450 | and then check against a `time_elapsed` variable created with 451 | `time_elapsed = time.monotonic() - keypad.time_of_last_press`. 452 | 453 | Also, be aware that the Adafruit HID CircuitPython library only currently 454 | supports US Keyboard layouts, so you'll have to work around that and map any 455 | keycodes that differ from their US counterpart to whatever your is. 456 | 457 | There is ongoing work on that subject, see https://github.com/adafruit/Adafruit_CircuitPython_HID/pull/61 458 | 459 | # USB MIDI 460 | 461 | This covers basic MIDI note messages and how to link them to key presses. 462 | 463 | ## Setup 464 | 465 | USB MIDI requires the `adafruit_midi` CircuitPython library. Download it from 466 | the link below and then drop the `adafruit_midi` folder into the `lib` folder on 467 | your `CIRCUITPY` drive. 468 | 469 | [Download the Adafruit MIDI CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_MIDI) 470 | 471 | You'll need to connect your Keypad with a USB cable to a computer running a 472 | software synth or DAW like Ableton Live, to a hardware synth that accepts USB 473 | MIDI, or through a MIDI interface that will convert the USB MIDI messages to 474 | regular serial MIDI through a DIN connector. 475 | 476 | ## Sending MIDI notes 477 | 478 | Here's a complete, minimal example of how to send a single MIDI note (middle C, 479 | or MIDI note number 60) when key 0 is pressed, sending a note on message when 480 | pressed and a note off message when released. 481 | 482 | ``` 483 | from rgbkeypad import RgbKeypad 484 | 485 | import usb_midi 486 | import adafruit_midi 487 | from adafruit_midi.note_off import NoteOff 488 | from adafruit_midi.note_on import NoteOn 489 | 490 | keypad = RgbKeypad() 491 | keys = keypad.keys 492 | 493 | midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) 494 | 495 | key = keys[0] 496 | note = 60 497 | velocity = 127 498 | 499 | was_pressed = False 500 | 501 | while True: 502 | keypad.update() 503 | 504 | if key.pressed: 505 | midi.send(NoteOn(note, velocity)) 506 | was_pressed = True 507 | elif not key.pressed and was_pressed: 508 | midi.send(NoteOff(note, 0)) 509 | was_pressed = False 510 | ``` 511 | 512 | There'a more complete example of how to set up all of RgbKeypad's keys with 513 | associated MIDI notes using decorators in the original Keybow Library. 514 | 515 | -------------------------------------------------------------------------------- /demos/demo1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Random colors at every keypress 3 | 4 | Save as code.py on your circuitpy drive. 5 | """ 6 | 7 | from rgbkeypad import RgbKeypad 8 | 9 | 10 | def key_pressed(a_key): 11 | print(f"Pressed {a_key}") 12 | keypad.random_colors(x_range=4, y_range=4) 13 | 14 | 15 | if __name__ == "__main__": 16 | # No need for params, the object will instanciate its I2C and SPI port drivers. 17 | keypad = RgbKeypad() 18 | keypad.random_colors(x_range=4, y_range=4) 19 | 20 | # Attach the on press handlers 21 | for key in keypad.keys: 22 | @keypad.on_press(key) 23 | def press_handler(a_key): 24 | key_pressed(a_key.number) 25 | 26 | # Required loop so the lib scans for touch events 27 | while True: 28 | keypad.update() 29 | -------------------------------------------------------------------------------- /examples/colour-picker.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # This example demonstrates the use of a modifier key to pick the colour of the 6 | # keys' LEDs, as well as the LED sleep functionality. 7 | 8 | # Drop the rgbkeypad.py file into your `lib` folder on your `CIRCUITPY` drive. 9 | 10 | from rgbkeypad import RgbKeypad, hsv_to_rgb 11 | 12 | MODIFIER_KEY = 0 13 | 14 | # Set up keypad 15 | keypad = RgbKeypad() 16 | 17 | keys = keypad.keys 18 | 19 | # Enable LED sleep and set a time of 5 seconds before the LEDs turn off. 20 | # They'll turn back on with a tap of any key! 21 | keypad.led_sleep_enabled = True 22 | keypad.led_sleep_time = 5 23 | 24 | # Set up the modifier key. It's 0, or the bottom left key. 25 | modifier_key = keys[MODIFIER_KEY] 26 | modifier_key.modifier = True 27 | 28 | # The starting colour (black/off) 29 | rgb = (0, 0, 0) 30 | 31 | while True: 32 | # Always remember to call keybow.update()! 33 | keypad.update() 34 | 35 | # If the modifier key and any other key are pressed, then set all the 36 | # keys to the selected colour. The second key pressed picks the colour. 37 | if modifier_key.held and keypad.any_pressed: 38 | if len(keypad.get_pressed()) > 1: 39 | hue = max(keypad.get_pressed()) / 15.0 40 | rgb = hsv_to_rgb(hue, 1.0, 1.0) 41 | 42 | keypad.set_all(*rgb) 43 | -------------------------------------------------------------------------------- /examples/decorators.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # This example demonstrates attaching functions to keys using decorators, and 6 | # the ability to turn the LEDs off with led_sleep_enabled and led_sleep_time. 7 | 8 | # Drop the rgbkeypad.py file into your `lib` folder on your `CIRCUITPY` drive. 9 | 10 | from rgbkeypad import RgbKeypad 11 | 12 | # Set up keypad 13 | keypad = RgbKeypad() 14 | keys = keypad.keys 15 | 16 | # Enable LED sleep and set a time of 5 seconds before the LEDs turn off. 17 | # They'll turn back on with a tap of any key! 18 | keypad.led_sleep_enabled = True 19 | keypad.led_sleep_time = 5 20 | 21 | # Loop through the keys and set the RGB colour for the keys to magenta. 22 | for key in keys: 23 | key.rgb = (255, 0, 255) 24 | 25 | # Attach a `on_hold` decorator to the key that toggles the key's LED when 26 | # the key is held (the default hold time is 0.75 seconds). 27 | @keypad.on_hold(key) 28 | def hold_handler(key): 29 | key.toggle_led() 30 | 31 | while True: 32 | # Always remember to call keypad.update()! 33 | keypad.update() 34 | -------------------------------------------------------------------------------- /examples/hid-keys-simple.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Original file was done for Keybow 2040 6 | # Author: Sandy Macdonald 7 | 8 | # Adaptation for Pimoroni RGB Keypad 9 | # Author: Thierry Chantier 10 | 11 | # A simple example of how to set up a keymap and HID keyboard on Pimoroni RGB Keypad. 12 | 13 | # You'll need to connect Pimoroni RGB Keypad to a computer, as you would with a regular 14 | # USB keyboard. 15 | 16 | # Drop the rgbkeypad.py file into your `lib` folder on your `CIRCUITPY` drive. 17 | 18 | # NOTE! Requires the adafruit_hid CircuitPython library also! 19 | 20 | from rgbkeypad import RgbKeypad 21 | 22 | import usb_hid 23 | from adafruit_hid.keyboard import Keyboard 24 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 25 | from adafruit_hid.keycode import Keycode 26 | 27 | # Set up Keypad 28 | keypad = RgbKeypad() 29 | keys = keypad.keys 30 | 31 | # Set up the keyboard and layout 32 | keyboard = Keyboard(usb_hid.devices) 33 | layout = KeyboardLayoutUS(keyboard) 34 | 35 | # A map of keycodes that will be mapped sequentially to each of the keys, 0-15 36 | keymap = [Keycode.ZERO, 37 | Keycode.ONE, 38 | Keycode.TWO, 39 | Keycode.THREE, 40 | Keycode.FOUR, 41 | Keycode.FIVE, 42 | Keycode.SIX, 43 | Keycode.SEVEN, 44 | Keycode.EIGHT, 45 | Keycode.NINE, 46 | Keycode.A, 47 | Keycode.B, 48 | Keycode.C, 49 | Keycode.D, 50 | Keycode.E, 51 | Keycode.F] 52 | 53 | # The colour to set the keys when pressed, yellow. 54 | rgb = (255, 255, 0) 55 | 56 | # Attach handler functions to all of the keys 57 | for key in keys: 58 | # A press handler that sends the keycode and turns on the LED 59 | @keypad.on_press(key) 60 | def press_handler(key): 61 | keycode = keymap[key.number] 62 | print(keycode) 63 | keyboard.send(keycode) 64 | key.set_led(*rgb) 65 | 66 | # A release handler that turns off the LED 67 | @keypad.on_release(key) 68 | def release_handler(key): 69 | key.led_off() 70 | 71 | while True: 72 | # Always remember to call keypad.update() 73 | keypad.update() -------------------------------------------------------------------------------- /examples/rainbow.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # This example displays a rainbow animation on Keybow 2040's keys. 6 | 7 | # Drop the rgbkeypad.py file into your `lib` folder on your `CIRCUITPY` drive. 8 | 9 | import math 10 | from rgbkeypad import RgbKeypad, number_to_xy, hsv_to_rgb 11 | 12 | # Set up keypad 13 | keypad = RgbKeypad() 14 | keys = keypad.keys 15 | 16 | # Increment step to shift animation across keys. 17 | step = 0 18 | 19 | while True: 20 | # Always remember to call keybow.update() on every iteration of your loop! 21 | keypad.update() 22 | 23 | step += 1 24 | 25 | for i in range(16): 26 | # Convert the key number to an x/y coordinate to calculate the hue 27 | # in a matrix style-y. 28 | x, y = number_to_xy(i) 29 | 30 | # Calculate the hue. 31 | hue = (x + y + (step / 20)) / 8 32 | hue = hue - int(hue) 33 | hue = hue - math.floor(hue) 34 | 35 | # Convert the hue to RGB values. 36 | r, g, b = hsv_to_rgb(hue, 1, 1) 37 | 38 | # Display it on the key! 39 | keys[i].set_led(r, g, b) 40 | -------------------------------------------------------------------------------- /examples/reactive-press.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # This example demonstrates how to light keys when pressed. 6 | 7 | # Drop the rgbkeypad.py file into your `lib` folder on your `CIRCUITPY` drive. 8 | 9 | from rgbkeypad import RgbKeypad 10 | 11 | # Set up keypad 12 | keypad = RgbKeypad() 13 | keys = keypad.keys 14 | 15 | # Use cyan as the colour. 16 | rgb = (0, 255, 255) 17 | 18 | while True: 19 | # Always remember to call keypad.update() on every iteration of your loop! 20 | keypad.update() 21 | 22 | # Loop through the keys and set the LED to cyan if pressed, otherwise turn 23 | # it off (set it to black). 24 | for key in keys: 25 | if key.pressed: 26 | key.set_led(*rgb) 27 | else: 28 | key.set_led(0, 0, 0) 29 | -------------------------------------------------------------------------------- /rgbkeypad-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngainorDev/rgbkeypad-circuitpython/564f2f4bba3a335abe2c6316c32245b966d5eaa1/rgbkeypad-1.jpg -------------------------------------------------------------------------------- /rgbkeypad.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `Pimoroni's Pico RGB Keypad CircuitPython library` 7 | ==================================================== 8 | 9 | CircuitPython driver for the Pimoroni Pico RGB Keypad. 10 | From Sandy Macdonald's Keybow 2040 library. 11 | 12 | Drop the rgbkeypad.py file into your `lib` folder on your `CIRCUITPY` drive. 13 | 14 | * Author: Sandy Macdonald 15 | * Author: Angainor Dev 16 | 17 | Notes 18 | -------------------- 19 | 20 | **Hardware:** 21 | 22 | * Pimoroni Pico RGB Keypad 23 | _ 24 | 25 | **Software and Dependencies:** 26 | 27 | * Adafruit CircuitPython firmware for Raspberry Pi Pico: 28 | _ 29 | 30 | * Adafruit Dotstar circuit python library 31 | 32 | """ 33 | 34 | import time 35 | import board 36 | import busio 37 | from random import randint 38 | from digitalio import DigitalInOut, Direction, Pull 39 | from adafruit_bus_device.i2c_device import I2CDevice 40 | import adafruit_dotstar 41 | 42 | 43 | NUM_KEYS = 16 44 | 45 | # These are the 16 switches on keypad, with their value. 46 | _PINS = [2**i for i in range(NUM_KEYS)] 47 | 48 | 49 | class RgbKeypad(object): 50 | """ 51 | Represents a keypad and hence a set of Key instances with 52 | associated LEDs and key behaviours. 53 | 54 | 55 | """ 56 | def __init__(self): 57 | self.pins = _PINS 58 | self.cs = DigitalInOut(board.GP17) 59 | self.cs.direction = Direction.OUTPUT 60 | self.cs.value = 0 61 | 62 | self.pixels = adafruit_dotstar.DotStar(board.GP18, board.GP19, 16, brightness=0.1, auto_write=True) 63 | 64 | i2c = busio.I2C(board.GP5, board.GP4) 65 | self.expander = I2CDevice(i2c, 0x20) 66 | 67 | self.keys = [] 68 | self.time_of_last_press = time.monotonic() 69 | self.time_since_last_press = None 70 | self.led_sleep_enabled = False 71 | self.led_sleep_time = 60 72 | self.sleeping = False 73 | self.was_asleep = False 74 | self.last_led_states = None 75 | # self.rotation = 0 76 | self.full_state = [0] 77 | for i in range(len(self.pins)): 78 | _key = Key(i, self.pins[i], self.pixels, self.full_state) 79 | self.keys.append(_key) 80 | 81 | def update(self): 82 | # Call this in each iteration of your while loop to update 83 | # to update everything's state, e.g. `keybow.update()` 84 | 85 | with self.expander: 86 | self.expander.write(bytes([0x0])) 87 | result = bytearray(2) 88 | self.expander.readinto(result) 89 | self.full_state[0] = result[0] | result[1] << 8 90 | 91 | # print("u", self.full_state[0]) 92 | for _key in self.keys: 93 | _key.update() 94 | 95 | # Used to work out the sleep behaviour, by keeping track 96 | # of the time of the last key press. 97 | if self.any_pressed(): 98 | self.time_of_last_press = time.monotonic() 99 | self.sleeping = False 100 | 101 | self.time_since_last_press = time.monotonic() - self.time_of_last_press 102 | 103 | # If LED sleep is enabled, but not engaged, check if enough time 104 | # has elapsed to engage sleep. If engaged, record the state of the 105 | # LEDs, so it can be restored on wake. 106 | if self.led_sleep_enabled and not self.sleeping: 107 | if time.monotonic() - self.time_of_last_press > self.led_sleep_time: 108 | self.sleeping = True 109 | self.last_led_states = [k.rgb if k.lit else [0, 0, 0] for k in self.keys] 110 | self.set_all(0, 0, 0) 111 | self.was_asleep = True 112 | 113 | # If it was sleeping, but is no longer, then restore LED states. 114 | if not self.sleeping and self.was_asleep: 115 | for k in range(len(self.keys)): 116 | self.keys[k].set_led(*self.last_led_states[k]) 117 | self.was_asleep = False 118 | 119 | def set_led(self, number, r, g, b): 120 | # Set an individual key's LED to an RGB value by its number. 121 | 122 | self.keys[number].set_led(r, g, b) 123 | 124 | def set_all(self, r, g, b): 125 | # Set all of Keybow's LEDs to an RGB value. 126 | 127 | if not self.sleeping: 128 | for _key in self.keys: 129 | _key.set_led(r, g, b) 130 | else: 131 | for _key in self.keys: 132 | _key.led_off() 133 | 134 | def random_colors(self, x_range=3, y_range=3): 135 | for x in range(x_range): 136 | for y in range(y_range): 137 | i = x * 4 + y 138 | self.keys[i].set_led(randint(0, 255), randint(0, 255), randint(0, 255)) 139 | 140 | def clear_all(self): 141 | for _key in self.keys: 142 | _key.led_off() 143 | 144 | def get_states(self): 145 | # Returns a Boolean list of Keybow's key states 146 | # (0=not pressed, 1=pressed). 147 | 148 | _states = [_key.state for _key in self.keys] 149 | return _states 150 | 151 | def get_pressed(self): 152 | # Returns a list of key numbers currently pressed. 153 | 154 | _pressed = [_key.number for _key in self.keys if _key.state] 155 | return _pressed 156 | 157 | def any_pressed(self): 158 | # Returns True if any key is pressed, False if none are pressed. 159 | 160 | if any(self.get_states()): 161 | return True 162 | else: 163 | return False 164 | 165 | def none_pressed(self): 166 | # Returns True if none of the keys are pressed, False is any key 167 | # is pressed. 168 | 169 | if not any(self.get_states()): 170 | return True 171 | else: 172 | return False 173 | 174 | @staticmethod 175 | def on_press(_key, handler=None): 176 | # Attaches a press function to a key, via a decorator. This is stored as 177 | # `key.press_function` in the key's attributes, and run if necessary 178 | # as part of the key's update function (and hence Keybow's update 179 | # function). It can be attached as follows: 180 | 181 | # @keybow.on_press(key) 182 | # def press_handler(key, pressed): 183 | # if pressed: 184 | # do something 185 | # else: 186 | # do something else 187 | 188 | if _key is None: 189 | return 190 | 191 | def attach_handler(a_handler): 192 | _key.press_function = a_handler 193 | 194 | if handler is not None: 195 | attach_handler(handler) 196 | else: 197 | return attach_handler 198 | 199 | @staticmethod 200 | def on_release(_key, handler=None): 201 | # Attaches a release function to a key, via a decorator. This is stored 202 | # as `key.release_function` in the key's attributes, and run if 203 | # necessary as part of the key's update function (and hence Keybow's 204 | # update function). It can be attached as follows: 205 | 206 | # @keybow.on_release(key) 207 | # def release_handler(key): 208 | # do something 209 | 210 | if _key is None: 211 | return 212 | 213 | def attach_handler(a_handler): 214 | _key.release_function = a_handler 215 | 216 | if handler is not None: 217 | attach_handler(handler) 218 | else: 219 | return attach_handler 220 | 221 | @staticmethod 222 | def on_hold(_key, handler=None): 223 | # Attaches a hold unction to a key, via a decorator. This is stored as 224 | # `key.hold_function` in the key's attributes, and run if necessary 225 | # as part of the key's update function (and hence Keybow's update 226 | # function). It can be attached as follows: 227 | 228 | # @keybow.on_hold(key) 229 | # def hold_handler(key): 230 | # do something 231 | 232 | if _key is None: 233 | return 234 | 235 | def attach_handler(a_handler): 236 | _key.hold_function = a_handler 237 | 238 | if handler is not None: 239 | attach_handler(handler) 240 | else: 241 | return attach_handler 242 | 243 | 244 | class Key: 245 | """ 246 | Represents a key on Keypad, with associated value and 247 | LED behaviours. 248 | 249 | :param number: the key number (0-15) to associate with the key 250 | :param mask: the value when pressed (2**key number) 251 | :param pixels: the dotstar instance for the LEDs 252 | :param full_state: a list of the keypad full keys state (int) 253 | """ 254 | def __init__(self, number, mask, pixels, full_state): 255 | self.mask = mask 256 | self.number = number 257 | self.full_state = full_state 258 | 259 | self.state = 0 260 | self.pressed = 0 261 | self.last_state = None 262 | self.time_of_last_press = time.monotonic() 263 | self.time_since_last_press = None 264 | self.time_held_for = 0 265 | self.held = False 266 | self.hold_time = 0.75 267 | self.modifier = False 268 | self.rgb = [0, 0, 0] 269 | self.lit = False 270 | self.xy = self.get_xy() 271 | self.x, self.y = self.xy 272 | self.pixels = pixels 273 | self.led_off() 274 | self.press_function = None 275 | self.release_function = None 276 | self.hold_function = None 277 | self.press_func_fired = False 278 | self.hold_func_fired = False 279 | self.debounce = 0.125 280 | self.key_locked = False 281 | 282 | def get_state(self): 283 | # Returns the state of the key (0=not pressed, 1=pressed). 284 | res = 0 if self.full_state[0] & self.mask else 1 285 | return res 286 | 287 | def update(self): 288 | # Updates the state of the key and updates all of its 289 | # attributes. 290 | 291 | self.time_since_last_press = time.monotonic() - self.time_of_last_press 292 | 293 | # Keys get locked during the debounce time. 294 | if self.time_since_last_press < self.debounce: 295 | self.key_locked = True 296 | else: 297 | self.key_locked = False 298 | 299 | self.state = self.get_state() 300 | self.pressed = self.state 301 | update_time = time.monotonic() 302 | 303 | # If there's a `press_function` attached, then call it, 304 | # returning the key object and the pressed state. 305 | if self.press_function is not None and self.pressed and not self.press_func_fired and not self.key_locked: 306 | self.press_function(self) 307 | self.press_func_fired = True 308 | # time.sleep(0.05) # A little debounce 309 | 310 | # If the key has been pressed and releases, then call 311 | # the `release_function`, if one is attached. 312 | if not self.pressed and self.last_state: 313 | if self.release_function is not None: 314 | self.release_function(self) 315 | self.last_state = False 316 | self.press_func_fired = False 317 | 318 | if not self.pressed: 319 | self.time_held_for = 0 320 | self.last_state = False 321 | 322 | # If the key has just been pressed, then record the 323 | # `time_of_last_press`, and update last_state. 324 | elif self.pressed and not self.last_state: 325 | self.time_of_last_press = update_time 326 | self.last_state = True 327 | 328 | # If the key is pressed and held, then update the 329 | # `time_held_for` variable. 330 | elif self.pressed and self.last_state: 331 | self.time_held_for = update_time - self.time_of_last_press 332 | self.last_state = True 333 | 334 | # If the `hold_time` threshold is crossed, then call the 335 | # `hold_function` if one is attached. The `hold_func_fired` 336 | # ensures that the function is only called once. 337 | if self.time_held_for > self.hold_time: 338 | self.held = True 339 | if self.hold_function is not None and not self.hold_func_fired: 340 | self.hold_function(self) 341 | self.hold_func_fired = True 342 | else: 343 | self.held = False 344 | self.hold_func_fired = False 345 | 346 | def get_xy(self): 347 | # Returns the x/y coordinate of a key from 0,0 to 3,3. 348 | 349 | return number_to_xy(self.number) 350 | 351 | def get_number(self): 352 | # Returns the key number, from 0 to 15. 353 | 354 | return xy_to_number(self.x, self.y) 355 | 356 | def is_modifier(self): 357 | # Designates a modifier key, so you can hold the modifier 358 | # and tap another key to trigger additional behaviours. 359 | 360 | if self.modifier: 361 | return True 362 | else: 363 | return False 364 | 365 | def set_led(self, r, g, b): 366 | # Set this key's LED to an RGB value. 367 | 368 | if [r, g, b] == [0, 0, 0]: 369 | self.lit = False 370 | else: 371 | self.lit = True 372 | self.rgb = [r, g, b] 373 | 374 | self.pixels[self.number] = (r, g, b) 375 | 376 | def led_on(self): 377 | # Turn the LED on, using its current RGB value. 378 | 379 | r, g, b = self.rgb 380 | self.set_led(r, g, b) 381 | 382 | def led_off(self): 383 | # Turn the LED off. 384 | 385 | self.set_led(0, 0, 0) 386 | 387 | def led_state(self, state): 388 | # Set the LED's state (0=off, 1=on) 389 | 390 | state = int(state) 391 | 392 | if state == 0: 393 | self.led_off() 394 | elif state == 1: 395 | self.led_on() 396 | else: 397 | return 398 | 399 | def toggle_led(self, rgb=None): 400 | # Toggle the LED's state, retaining its RGB value for when it's toggled 401 | # back on. Can also be passed an RGB tuple to set the colour as part of 402 | # the toggle. 403 | 404 | if rgb is not None: 405 | self.rgb = rgb 406 | if self.lit: 407 | self.led_off() 408 | else: 409 | self.led_on() 410 | 411 | def __str__(self): 412 | # When printed, show the key's state (0 or 1). 413 | return self.state 414 | 415 | 416 | def xy_to_number(x, y): 417 | # Convert an x/y coordinate to key number. 418 | return x + (y * 4) 419 | 420 | 421 | def number_to_xy(number): 422 | # Convert a number to an x/y coordinate. 423 | x = number % 4 424 | y = number // 4 425 | return x, y 426 | 427 | 428 | def hsv_to_rgb(h, s, v): 429 | # Convert an HSV (0.0-1.0) colour to RGB (0-255) 430 | rgb = [v, v, v] # s = 0, default value 431 | 432 | i = int(h * 6.0) 433 | 434 | f = (h * 6.) - i 435 | p, q, t = v * (1. - s), v * (1. - s * f), v * (1. - s * (1. - f)) 436 | i %= 6 437 | 438 | if i == 0: 439 | rgb = [v, t, p] 440 | if i == 1: 441 | rgb = [q, v, p] 442 | if i == 2: 443 | rgb = [p, v, t] 444 | if i == 3: 445 | rgb = [p, q, v] 446 | if i == 4: 447 | rgb = [t, p, v] 448 | if i == 5: 449 | rgb = [v, p, q] 450 | 451 | rgb = tuple(int(c * 255) for c in rgb) 452 | 453 | return rgb 454 | --------------------------------------------------------------------------------