├── .gitignore ├── LICENSE ├── README.md ├── examples ├── colour-picker.py ├── decorator-key-test.py ├── decorators.py ├── hid-keypad-fifteen-layers.py ├── hid-keys-advanced.py ├── hid-keys-simple.py ├── midi-arp.py ├── midi-keys.py ├── midi-sequencer.py ├── obs-studio-toggle-and-mutex.py ├── rainbow.py └── reactive-press.py ├── keybow-2040-github-1.jpg └── lib └── pmk ├── __init__.py └── platform ├── __init__.py ├── display ├── __init__.py ├── dotstar.py └── keybow2040.py ├── keybow2040.py ├── rgbkeypadbase.py └── switches ├── __init__.py ├── gpio.py └── tca9555.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | .DS_Store 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sandy Macdonald 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 | # PMK - Pimoroni Mechanical/Mushy Keypad - CircuitPython 2 | 3 | The library abstracts away most of the complexity of having to check pin states, 4 | and interact with the LED driver library, and exposes classes for 5 | individual keys and the whole PMK class (a collection of Key instances). 6 | 7 | ## Supported Devices 8 | 9 | * The [RP2040-powered Keybow 2040 from Pimoroni](https://shop.pimoroni.com/products/keybow-2040), 10 | a 16-key mini mechanical keyboard with RGB backlit keys. 11 | * A Raspberry Pi Pico mounted on the [RGB Keypad Base from Pimoroni](https://shop.pimoroni.com/products/pico-rgb-keypad-base) 12 | a 16-key mini rubber keyboard with RGB backlit keys. 13 | 14 | ![Keybow 2040 with backlit keys on marble background](keybow-2040-github-1.jpg) 15 | 16 | # Index 17 | 18 | - [Getting started quickly!](#getting-started-quickly) 19 | - [Preparing Your Device](#preparing-your-device) 20 | - [Keybow 2040](#keybow-2040) 21 | - [Pico RGB Keypad](#pico-rgb-keypad) 22 | - [Installing PMK](#installing-pmk) 23 | - [Library functionality](#library-functionality) 24 | - [Imports and setup](#imports-and-setup) 25 | - [The PMK class](#the-pmk-class) 26 | - [An interlude on timing!](#an-interlude-on-timing) 27 | - [Key presses](#key-presses) 28 | - [PMK class methods for detecting presses and key states](#pmk-class-methods-for-detecting-presses-and-key-states) 29 | - [Key class methods for detecting key presses](#key-class-methods-for-detecting-key-presses) 30 | - [LEDs!](#leds) 31 | - [LED sleep](#led-sleep) 32 | - [Attaching functions to keys with decorators](#attaching-functions-to-keys-with-decorators) 33 | - [Key combos](#key-combos) 34 | - [USB HID](#usb-hid) 35 | - [Setup](#setup) 36 | - [Sending key presses](#sending-key-presses) 37 | - [Sending strings of text](#sending-strings-of-text) 38 | - [USB MIDI](#usb-midi) 39 | - [Setup](#setup-1) 40 | - [Sending MIDI notes](#sending-midi-notes) 41 | - [Other Resources](#other-resources) 42 | 43 | # Getting started quickly! 44 | 45 | For a more verbose installation guide with screenshots, check out our Learn guide: 46 | 47 | [CircuitPython and Keybow 2040](https://learn.pimoroni.com/article/circuitpython-and-keybow-2040) 48 | 49 | ## Preparing Your Device 50 | 51 | ### Keybow 2040 52 | 53 | You'll need to grab the latest version of Adafruit's Keybow 2040-flavoured 54 | CircuitPython, from the link below. 55 | 56 | [Download the Adafruit CircuitPython binary for Keybow 2040](https://circuitpython.org/board/pimoroni_keybow2040/) 57 | 58 | Unplug your Keybow 2040's USB-C cable, press and hold the BOOTSEL button while plugging the USB-C cable back into your computer to mount 59 | it as a drive (it should show up as `RPI-RP2` or something similar). The BOOTSEL button is to the right of the USB-C port, assuming your Keybow is oriented with keys pointing upwards and with the USB-C port at the top edge. 60 | 61 | Drag and drop the `adafruit-circuitpython-pimoroni_keybow2040-en_US-XXXXX.uf2` 62 | file that you downloaded onto the drive and it should reboot and load the 63 | CircuitPython firmware. The drive should now show up as `CIRCUITPY`. 64 | 65 | The Adafruit IS31FL3731 LED driver library for CircuitPython is a prerequisite for 66 | this Keybow 2040 library, so you'll need to download it from GitHub at the link 67 | below, and then drop the `adafruit_is31fl3731` folder into the `lib` folder on 68 | your `CIRCUITPY` drive. 69 | 70 | [Download the Adafruit IS31FL3731 CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_IS31FL3731) 71 | 72 | ### Pico RGB Keypad 73 | 74 | You'll need to grab the latest version of Adafruit's Raspberry Pi Pico-flavoured 75 | CircuitPython, from the link below. 76 | 77 | [Download the Adafruit CircuitPython binary for Raspberry Pi Pico](https://circuitpython.org/board/raspberry_pi_pico/) 78 | 79 | Unplug your Pi Pico's micro USB cable, press and hold the BOOTSEL button on the top 80 | of Pi Pico while plugging the micro USB cable back into your computer to mount 81 | it as a drive (it should show up as `RPI-RP2` or something similar). 82 | 83 | Drag and drop the `adafruit-circuitpython-raspberry_pi_pico-en_US-XXXXX.uf2` 84 | file that you downloaded onto the drive and it should reboot and load the 85 | CircuitPython firmware. The drive should now show up as `CIRCUITPY`. 86 | 87 | The Adafruit DotStar LED driver library for CircuitPython is a prerequisite for 88 | this Keybow 2040 library, so you'll need to download it from GitHub at the link 89 | below, and then drop the `adafruit_dotstar.py` file into the `lib` folder on 90 | your `CIRCUITPY` drive. 91 | 92 | [Download the Adafruit DotStar CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_DotStar) 93 | 94 | ## Installing PMK 95 | 96 | Drop the `lib` contents (the `pmk` folder) from this library into the `lib` folder 97 | on your `CIRCUITPY` drive also, and you're all set! 98 | 99 | Pick one of the [examples](examples) (I'd suggest the 100 | [reactive.press.py](examples/reactive-press.py) example to begin), copy the 101 | code, and save it in the `code.py` file on your `CIRCUITPY` drive using your 102 | favourite text editor. As soon as you save the `code.py` file, or make any other 103 | changes, then it should load up and run the code! 104 | 105 | Examples are by default using Keybow 2040 hardware, if you want to run them 106 | on Pico RGB Keypad, you need to change the hardware. Comment out the line: 107 | 108 | ``` 109 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 110 | ``` 111 | 112 | and uncomment the line: 113 | 114 | ``` 115 | from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware 116 | ``` 117 | 118 | # Library functionality 119 | 120 | This section covers most of the functionality of the library itself, without 121 | delving into additional functions like USB MIDI or HID (they're both covered 122 | later!) 123 | 124 | ## Imports and setup 125 | 126 | All of your programs will need to start with the following: 127 | 128 | ```python 129 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 130 | from pmk import PMK 131 | 132 | hardware = Hardware() 133 | pmk = PMK(hardware) 134 | ``` 135 | 136 | First, this imports a hardware object representing the board. A hardware object 137 | hides technical details on how keys and LEDs are connected and exposes them 138 | via uniform interface. You need to choose the correct hardware object for 139 | your hardware. If you're curious, hardware differences are explained below, 140 | but all you need to know is that for Keybow 2040 you need an import: 141 | 142 | ```python 143 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 144 | ``` 145 | 146 | and for Pico RGB Keypad Base: 147 | 148 | ```python 149 | from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware 150 | ``` 151 | 152 | On Keybow 2040 (`Keybow2040`) keys are read directly via GPIO, and LEDs are set 153 | via IS31FL3731 LED driver connected over I2C bus. 154 | 155 | On Pico RGB Keypad Base (`RGBKeypadBase`) keys are connected via TCA9555 GPIO extender 156 | connected over I2C bus and LEDs are DotStar LEDs connected via SPI bus. 157 | 158 | Since both boards use I2C bus, hardware object also exposes it in case you 159 | need to access it (Keybow 2040 has even I2C connecting pads exposed): 160 | i2c = hardware.i2c() 161 | 162 | In the rest of this file examples of the code will use `Keybow2040` hardware object. 163 | If you're running them on Pico RGB Keypad Base, don't forget to change it accordingly. 164 | 165 | The `PMK()` class, imported from the `pmk` module, is instantiated 166 | and passed the hardware object. Instantiating this sets up all of the pins, keys, 167 | and LEDs, and provides access to all of the attributes and methods associated 168 | with it. 169 | 170 | ## The PMK class 171 | 172 | The PMK class exposes a number of handy attributes and methods. The main one 173 | you'll be interested in is the `.keys` attribute, which is a list of `Key` 174 | class instances, one for each key. 175 | 176 | ```python 177 | keys = pmk.keys 178 | ``` 179 | 180 | The indices of the keys in that list correspond to their position on the keypad, 181 | staring from the bottom left corner (when the USB connector is at the top), 182 | which is key 0, going upwards in columns, and ending at the top right corner, 183 | which is key 15. 184 | 185 | More about the `Key` class later... 186 | 187 | A **super** important method of the `PMK` class is `.update()` method. It 188 | updates all of the keys, key states, and other attributes like the time of the 189 | last key press, and sleep state of the LEDs. 190 | 191 | **You need to call this method on your `PMK` class at the very start of each 192 | iteration of your program's main loop, as follows:** 193 | 194 | ```python 195 | while True: 196 | pmk.update() 197 | ``` 198 | 199 | ## Rotation 200 | 201 | If you're using your keypad in a different orientation, you can use the `.rotate()` method to make sure your key numbers are in the logical order. 202 | 203 | The default PMK key arrangement doesn't match the Pico RGB Keypad Base (`RGBKeypadBase`) circuitboard labels. You can fix it by rotating the keys 90 degrees 204 | 205 | ``` 206 | pmk = PMK(Hardware()) 207 | pmk.rotate(90) 208 | ``` 209 | 210 | 211 | ## An interlude on timing! 212 | 213 | Another **super** important thing is **not to include any `time.sleep()`s in 214 | your main loop!** Doing so will ruin the latency and mean that you'll miss key 215 | press events. Just don't do it. 216 | 217 | If you need introduce timed events, then you have to go about it in a slightly 218 | (!!) roundabout fashion, by using `time.monotonic()` a constantly incremented 219 | count of seconds elapsed, and use it to check the time elapsed since your last 220 | event, for example you could do this inside your `while True` loop: 221 | 222 | ```python 223 | time_interval = 10 224 | 225 | # An event just happened! 226 | 227 | time_last_fired = time.monotonic() 228 | time_elapsed = 0 229 | 230 | # ... some iterations later 231 | 232 | time_elapsed = time.monotonic() - time_last_fired 233 | 234 | if time_elapsed > time_interval: 235 | # Fire your event again! 236 | ``` 237 | 238 | There's a handy `pmk.time_of_last_press` attribute that allows you to quickly 239 | check if a certain amount of time has elapsed since any key press, and that 240 | attribute gets updated every time `pmk.update()` is called. 241 | 242 | ## Key presses 243 | 244 | There are a few ways that you can go about detecting key presses, some 245 | global methods on the `PMK` class instance, and some on the `Key` class 246 | instances themselves. 247 | 248 | ### PMK class methods for detecting presses and key states 249 | 250 | `pmk.get_states()` will return a list of the state of all of the keys, in 251 | order, with a state of `0` being not pressed, and `1` being pressed. You can 252 | then loop through that list to do whatever you like. 253 | 254 | `pmk.get_pressed()` will return a list of the key numbers (indices in the 255 | list of keys) that are currently pressed. If you only care about key presses, 256 | then this is an efficient way to do things, especially since you have all the 257 | key numbers in a list. 258 | 259 | `pmk.any_pressed()` returns a Boolean (`True`/`False`) that tells you whether 260 | any keys are currently being pressed. Handy if you want to attach a behaviour to 261 | all of the keys, which this is effectively a proxy for. 262 | 263 | `pmk.none_pressed()` is similar to `.any_pressed()`, in that it returns a 264 | Boolean also, but... you guessed it, it returns `True` if no keys are being 265 | pressed, and `False` if any keys are pressed. 266 | 267 | ### Key class methods for detecting key presses 268 | 269 | If we want to check whether key 0 is pressed, we can do so as follows: 270 | 271 | ```python 272 | keys = pmk.keys() 273 | 274 | while True: 275 | pmk.update() 276 | 277 | if keys[0].pressed: 278 | # Do something! 279 | ``` 280 | 281 | The `.pressed` attribute returns a Boolean that is `True` if the key is pressed 282 | and `False` if it is not pressed. 283 | 284 | `key.state` is another way to check the state of a key. It will equal `1` if the 285 | key is pressed and `0` if it is not pressed. 286 | 287 | If you want to attach an additional behaviour to your key, you can use 288 | `key.held` to check if a key is being key rather than being pressed and released 289 | quickly. It returns `True` if the key is held and `False` if it is not. 290 | 291 | The default hold time (after which `key.held` is `True`) for all of the keys is 292 | 0.75 seconds, but you can change `key.hold_time` to adjust this to your liking, 293 | on a per key basis. 294 | 295 | This means that we could extend the example above to be: 296 | 297 | ```python 298 | keys = pmk.keys() 299 | 300 | while True: 301 | pmk.update() 302 | 303 | if keys[0].pressed: 304 | # Do something! 305 | 306 | if keys[0].held: 307 | # Do something else! 308 | ``` 309 | 310 | The [reactive-press.py example](examples/reactive-press.py) shows in more detail 311 | how to handle key presses. 312 | 313 | ## LEDs! 314 | 315 | LEDs can be set either globally for all keys, using the `PMK` class instance, 316 | or on a per-key basis, either through the `PMK` class, or using a `Key` class 317 | instance. 318 | 319 | To set all of the keys to the same colour, you can use the `.set_all()` method 320 | of the `PMK` class, to which you pass three 0-255 integers for red, green, 321 | and blue. For example, to set all of the keys to magenta: 322 | 323 | ``` 324 | pmk.set_all(255, 0, 255) 325 | ``` 326 | 327 | To set an individual key through your `PMK` class instance, you can do as 328 | follows, to set key 0 to white: 329 | 330 | ``` 331 | pmk.set_led(0, 255, 255, 255) 332 | ``` 333 | 334 | To set the colour on the key itself, you could do as follows, again to set key 335 | 0 to white: 336 | 337 | ``` 338 | pmk.keys[0].set_led(255, 255, 255) 339 | ``` 340 | 341 | A key retains its RGB value, even if it is turned off, so once a key has its 342 | colour set with `key.rgb = (255, 0, 0)` for example, you can turn it off using 343 | `key.led_off()` or even `key.set_led(0, 0, 0)` and then when you turn it back on 344 | with `key.led_on()`, then it will still be red when it comes back on. 345 | 346 | As a convenience, and to avoid having to check `key.lit`, there is a 347 | `key.toggle_led()` method that will toggle the current state of the key's LED 348 | (on to off, and _vice versa_). 349 | 350 | There's a handy `hsv_to_rgb()` function that can be imported from the 351 | `pmk` module to convert an HSV colour (a tuple of floats from 0.0 to 1.0) 352 | to an RGB colour (a tuple of integers from 0 to 255), as follows: 353 | 354 | ``` 355 | from pmk import hsv_to_rgb 356 | 357 | h = 0.5 # Hue 358 | s = 1.0 # Saturation 359 | v = 1.0 # Value 360 | 361 | r, g, b = hsv_to_rgb(h, s, v) 362 | ``` 363 | 364 | The [rainbow.py example](examples/rainbow.py) shows a more complex example of 365 | how to animate the keys' LEDs, including the use of the `hsv_to_rgb()` function. 366 | 367 | ## LED sleep 368 | 369 | The `PMK` class has an `.led_sleep_enabled` attribute that is disabled (set to 370 | `False`) by default, and an `.led_sleep_time` attribute (set to 60 seconds by 371 | default) that determines how many seconds need to elapse before LED sleep is 372 | triggered and the LEDs turn off. 373 | 374 | The time elapsed since the last key press is constantly updated when 375 | `pmk.update()` is called in your main loop, and if the `.led_sleep_time` is 376 | exceeded then LED sleep is triggered. 377 | 378 | Because keys retain their RGB values when toggled off, when asleep, a tap on any 379 | key will wake all of the LEDs up at their last state before sleep. 380 | 381 | Enabling LED sleep with a sleep time of 10 seconds could be done as simply as: 382 | 383 | ```python 384 | pmk.led_sleep_enabled = True 385 | pmk.led_sleep_time = 10 386 | ``` 387 | 388 | There's also a `.sleeping` attribute that returns a Boolean, that you can check 389 | to see whether the LEDs are sleeping or not. 390 | 391 | ## Attaching functions to keys with decorators 392 | 393 | There are three decorators that can be attached to functions to link that 394 | function to, i) a key press, ii) a key release, or iii) a key hold. 395 | 396 | Here's an example of how you could attach a decorator to a function that lights 397 | up that key yellow when it is pressed, turns all of the LEDs on when held, and 398 | turns them all off when released: 399 | 400 | ```python 401 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 402 | from pmk import PMK 403 | 404 | pmk = PMK(Hardware()) 405 | keys = pmk.keys 406 | 407 | key = keys[0] 408 | rgb = (255, 255, 0) 409 | key.rgb = rgb 410 | 411 | @pmk.on_press(key) 412 | def press_handler(key): 413 | key.led_on() 414 | 415 | @pmk.on_release(key) 416 | def release_handler(key): 417 | pmk.set_all(0, 0, 0) 418 | 419 | @pmk.on_hold(key) 420 | def hold_handler(key): 421 | pmk.set_all(*rgb) 422 | 423 | while True: 424 | pmk.update() 425 | ``` 426 | 427 | The [decorators.py example](examples/decorators.py) has another example of how 428 | to use the `.on_hold()` decorator to toggle LEDs on and off when a key is held. 429 | 430 | ## Key combos 431 | 432 | Key combos can provide a way to add additional behaviours to keys that only get 433 | triggered if a combination of keys is pressed. The best way to achieve this is 434 | using the `.held` attribute of a key, meaning that the key can also have a 435 | `.pressed` behaviour too. 436 | 437 | Here's a brief example of how you could do this inside your main loop, with key 438 | 0 as the modifier key, and key 1 as the action key: 439 | 440 | ``` 441 | keys = pmk.keys 442 | 443 | modifier_key = keys[0] 444 | action_key = keys[1] 445 | 446 | while True: 447 | pmk.update() 448 | 449 | if modifier_key.held and action_key.pressed: 450 | # Do something! 451 | ``` 452 | 453 | Of course, you could chain these together, to require two modifer keys to be 454 | held and a third to be pressed, and so on... 455 | 456 | The [colour-picker.py example](examples/colour-picker.py) has an example of 457 | using a modifier key to change the hue of the keys. 458 | 459 | # USB HID 460 | 461 | This covers setting up a USB HID keyboard and linking physical key presses to 462 | keyboard key presses on a connected computer. 463 | 464 | ## Setup 465 | 466 | USB HID requires the `adafruit_hid` CircuitPython library. Download it from the 467 | link below and drop the `adafruit_hid` folder into the `lib` folder on your 468 | `CIRCUITPY` drive. 469 | 470 | [Download the Adafruit HID CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_HID) 471 | 472 | You'll need to connect your Keybow or Pico + RGB Keypad Base to a computer using a USB cable, just like 473 | you would with a regular USB keyboard. 474 | 475 | ## Sending key presses 476 | 477 | Here's an example of setting up a keyboard object and sending a `0` key press 478 | when key 0 is pressed, using an `.on_press()` decorator: 479 | 480 | ```python 481 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 482 | from pmk import PMK 483 | 484 | import usb_hid 485 | from adafruit_hid.keyboard import Keyboard 486 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 487 | from adafruit_hid.keycode import Keycode 488 | 489 | pmk = PMK(Hardware()) 490 | keys = pmk.keys 491 | 492 | keyboard = Keyboard(usb_hid.devices) 493 | layout = KeyboardLayoutUS(keyboard) 494 | 495 | key = keys[0] 496 | 497 | @pmk.on_press(key) 498 | def press_handler(key): 499 | keyboard.send(Keycode.ZERO) 500 | 501 | while True: 502 | pmk.update() 503 | ``` 504 | 505 | You can find a list of all of the keycodes available at the 506 | [HID CircuitPython library documentation here](https://circuitpython.readthedocs.io/projects/hid/en/latest/api.html#adafruit-hid-keycode-keycode). 507 | 508 | If you wanted to take this a bit further and make a full keymap for your 509 | keyboard, then you could create a list of 16 different keycodes and then use the 510 | number of the key press registered by the `press_handler` function as an index 511 | into your keymap to get the keycode to send for each key. 512 | 513 | ```python 514 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 515 | from pmk import PMK 516 | 517 | import usb_hid 518 | from adafruit_hid.keyboard import Keyboard 519 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 520 | from adafruit_hid.keycode import Keycode 521 | 522 | pmk = PMK(Hardware()) 523 | keys = pmk.keys 524 | 525 | keyboard = Keyboard(usb_hid.devices) 526 | layout = KeyboardLayoutUS(keyboard) 527 | 528 | keymap = [Keycode.ZERO, 529 | Keycode.ONE, 530 | Keycode.TWO, 531 | Keycode.THREE, 532 | Keycode.FOUR, 533 | Keycode.FIVE, 534 | Keycode.SIX, 535 | Keycode.SEVEN, 536 | Keycode.EIGHT, 537 | Keycode.NINE, 538 | Keycode.A, 539 | Keycode.B, 540 | Keycode.C, 541 | Keycode.D, 542 | Keycode.E, 543 | Keycode.F] 544 | 545 | for key in keys: 546 | @pmk.on_press(key) 547 | def press_handler(key): 548 | keycode = keymap[key.number] 549 | keyboard.send(keycode) 550 | 551 | while True: 552 | pmk.update() 553 | ``` 554 | 555 | This code is available in the 556 | [hid-keys-simple.py example](examples/hid-keys-simple.py). 557 | 558 | As well as sending a single keypress, you can send multiple keypresses at once, 559 | simply by adding them as additional arguments to `keyboard.send()`, e.g. 560 | `keyboard.send(Keycode.A, Keycode.B)` and so on. 561 | 562 | ## Sending strings of text 563 | 564 | Rather than the inconvenience of sending multiple keycodes using 565 | `keyboard.send()`, there's a different method to send whole strings of text at 566 | once, using the `layout` object we created. 567 | 568 | ```python 569 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 570 | from pmk import PMK 571 | 572 | import usb_hid 573 | from adafruit_hid.keyboard import Keyboard 574 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 575 | from adafruit_hid.keycode import Keycode 576 | 577 | pmk = PMK(Hardware()) 578 | keys = pmk.keys 579 | 580 | keyboard = Keyboard(usb_hid.devices) 581 | layout = KeyboardLayoutUS(keyboard) 582 | 583 | key = keys[0] 584 | 585 | @pmk.on_press(key) 586 | def press_handler(key): 587 | layout.write("Pack my box with five dozen liquor jugs.") 588 | 589 | while True: 590 | pmk.update() 591 | ``` 592 | 593 | A press of key 0 will send that whole string of text at once! 594 | 595 | Be aware that strings sent like that take a little while to virtually "type", 596 | so you might want to incorporate a delay using `pmk.time_of_last_press`, 597 | and then check against a `time_elapsed` variable created with 598 | `time_elapsed = time.monotonic() - pmk.time_of_last_press`. 599 | 600 | Also, be aware that the Adafruit HID CircuitPython library only currently 601 | supports US Keyboard layouts, so you'll have to work around that and map any 602 | keycodes that differ from their US counterpart to whatever your is. 603 | 604 | # USB MIDI 605 | 606 | This covers basic MIDI note messages and how to link them to key presses. 607 | 608 | ## Setup 609 | 610 | USB MIDI requires the `adafruit_midi` CircuitPython library. Download it from 611 | the link below and then drop the `adafruit_midi` folder into the `lib` folder on 612 | your `CIRCUITPY` drive. 613 | 614 | [Download the Adafruit MIDI CircuitPython library](https://github.com/adafruit/Adafruit_CircuitPython_MIDI) 615 | 616 | You'll need to connect your Keybow 2040 with a USB cable to a computer running a 617 | software synth or DAW like Ableton Live, to a hardware synth that accepts USB 618 | MIDI, or through a MIDI interface that will convert the USB MIDI messages to 619 | regular serial MIDI through a DIN connector. 620 | 621 | Using USB MIDI, Keybow 2040 shows up as a device with the name 622 | `Keybow 2040 (CircuitPython usb midi.ports[1])` 623 | 624 | In my testing, Keybow 2040 works with the Teenage Engineering OP-Z quite nicely. 625 | 626 | ## Sending MIDI notes 627 | 628 | Here's a complete, minimal example of how to send a single MIDI note (middle C, 629 | or MIDI note number 60) when key 0 is pressed, sending a note on message when 630 | pressed and a note off message when released. 631 | 632 | ```python 633 | from pmk.platform.keybow2040 import Keybow2040 as Hardware 634 | from pmk import PMK 635 | 636 | import usb_midi 637 | import adafruit_midi 638 | from adafruit_midi.note_off import NoteOff 639 | from adafruit_midi.note_on import NoteOn 640 | 641 | pmk = PMK(Hardware()) 642 | keys = pmk.keys 643 | 644 | midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) 645 | 646 | key = keys[0] 647 | note = 60 648 | velocity = 127 649 | 650 | was_pressed = False 651 | 652 | while True: 653 | pmk.update() 654 | 655 | if key.pressed: 656 | midi.send(NoteOn(note, velocity)) 657 | was_pressed = True 658 | elif not key.pressed and was_pressed: 659 | midi.send(NoteOff(note, 0)) 660 | was_pressed = False 661 | ``` 662 | 663 | There'a more complete example of how to set up all of Keybow's keys with 664 | associated MIDI notes using decorators in the 665 | [midi-keys.py example](examples/midi-keys.py). 666 | 667 | The example above, and the `midi-keys.py` example both send notes on MIDI 668 | channel 0 (all channels), but you can set this to a specific channel, if you 669 | like, by changing `out_channel=` when you instantiate your `midi` object. 670 | 671 | # Other Resources 672 | 673 | Here are some cool community projects and resources that you might find useful / inspirational! Note that code at the links below has not been tested by us and we're not able to offer support with it. 674 | 675 | - :link: [OBS Controller using Raspberry Pi Pico and Pimoroni RGB Keypad](https://vimeo.com/802443378) 676 | -------------------------------------------------------------------------------- /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 `pmk` folder 9 | # into your `lib` folder on your `CIRCUITPY` drive. 10 | 11 | from pmk import PMK, hsv_to_rgb 12 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 13 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 14 | 15 | MODIFIER_KEY = 0 16 | 17 | # Set up Keybow 18 | keybow = PMK(Hardware()) 19 | keys = keybow.keys 20 | 21 | # Enable LED sleep and set a time of 5 seconds before the LEDs turn off. 22 | # They'll turn back on with a tap of any key! 23 | keybow.led_sleep_enabled = True 24 | keybow.led_sleep_time = 5 25 | 26 | # Set up the modifier key. It's 0, or the bottom left key. 27 | modifier_key = keys[MODIFIER_KEY] 28 | modifier_key.modifier = True 29 | 30 | # The starting colour (black/off) 31 | rgb = (0, 0, 0) 32 | 33 | while True: 34 | # Always remember to call keybow.update()! 35 | keybow.update() 36 | 37 | # If the modifier key and any other key are pressed, then set all the 38 | # keys to the selected colour. The second key pressed picks the colour. 39 | if modifier_key.held and keybow.any_pressed: 40 | if len(keybow.get_pressed()) > 1: 41 | hue = max(keybow.get_pressed()) / 15.0 42 | rgb = hsv_to_rgb(hue, 1.0, 1.0) 43 | 44 | keybow.set_all(*rgb) 45 | -------------------------------------------------------------------------------- /examples/decorator-key-test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Philip Howard 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # This example allows you to test each key and LED in turn 6 | # 1. At startup all LEDs should be white 7 | # 2. Press a key and it will turn blue 8 | # 3. Release that key and it will turn white 9 | # 4. *Hold* a key and it will turn red 10 | # 5. Release a *held* key and it will turn green. 11 | # If you can turn all your keys blue -> red -> green, they're good! 12 | 13 | # Drop the `pmk` folder 14 | # into your `lib` folder on your `CIRCUITPY` drive. 15 | 16 | from pmk import PMK 17 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 18 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 19 | import time 20 | 21 | keybow = PMK(Hardware()) 22 | keys = keybow.keys 23 | 24 | keybow.set_all(64, 64, 64) 25 | 26 | for key in keys: 27 | @keybow.on_press(key) 28 | def press_handler(key): 29 | print("Key {} pressed".format(key.number)) 30 | key.set_led(0, 0, 255) 31 | 32 | @keybow.on_release(key) 33 | def release_handler(key): 34 | print("Key {} released".format(key.number)) 35 | if key.rgb == [255, 0, 0]: 36 | key.set_led(0, 255, 0) 37 | else: 38 | key.set_led(64, 64, 64) 39 | 40 | @keybow.on_hold(key) 41 | def hold_handler(key): 42 | print("Key {} held".format(key.number)) 43 | key.set_led(255, 0, 0) 44 | 45 | while True: 46 | keybow.update() 47 | time.sleep(1.0 / 60) 48 | -------------------------------------------------------------------------------- /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 `pmk` folder 9 | # into your `lib` folder on your `CIRCUITPY` drive. 10 | 11 | from pmk import PMK 12 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 13 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 14 | 15 | # Set up Keybow 16 | keybow = PMK(Hardware()) 17 | keys = keybow.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 | keybow.led_sleep_enabled = True 22 | keybow.led_sleep_time = 5 23 | 24 | # Loop through the keys and set the RGB colour for the keys to magenta. 25 | for key in keys: 26 | key.rgb = (255, 0, 255) 27 | 28 | # Attach a `on_hold` decorator to the key that toggles the key's LED when 29 | # the key is held (the default hold time is 0.75 seconds). 30 | @keybow.on_hold(key) 31 | def hold_handler(key): 32 | key.toggle_led() 33 | 34 | while True: 35 | # Always remember to call keybow.update()! 36 | keybow.update() 37 | -------------------------------------------------------------------------------- /examples/hid-keypad-fifteen-layers.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | 3 | # Modified by Frank Smith 2021 all the keys bar the modifier key 4 | # can now be used as layer select and input keys 5 | # prints debug messages via debug serial port (USB) 6 | # sudo cat /dev/ttyACM0 7 | 8 | # SPDX-License-Identifier: MIT 9 | 10 | # An advanced example of how to set up a HID keyboard. 11 | 12 | # There are four layers defined out of fifteen possible, 13 | # selected by pressing and holding key 0 (bottom left), 14 | # then tapping one of the coloured layer selector keys to switch layer. 15 | 16 | # The defined layer colours are as follows: 17 | 18 | # * layer 1: pink: numpad-style keys, 0-9, delete, and enter. 19 | # * layer 2: blue: sends strings on each key press 20 | # * layer 3: yellow: media controls, rev, play/pause, fwd on row one, vol. down, mute, 21 | # vol. up on row two 22 | # * layer 4: white: sends mixxx controls 23 | 24 | import time 25 | from pmk import PMK 26 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 27 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 28 | 29 | import usb_hid 30 | from adafruit_hid.keyboard import Keyboard 31 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 32 | from adafruit_hid.keycode import Keycode 33 | 34 | from adafruit_hid.consumer_control import ConsumerControl 35 | from adafruit_hid.consumer_control_code import ConsumerControlCode 36 | 37 | # Set up Keybow 38 | keybow = PMK(Hardware()) 39 | keys = keybow.keys 40 | 41 | # Set up the keyboard and layout 42 | keyboard = Keyboard(usb_hid.devices) 43 | layout = KeyboardLayoutUS(keyboard) 44 | 45 | # Set up consumer control (used to send media key presses) 46 | consumer_control = ConsumerControl(usb_hid.devices) 47 | 48 | # Our layers. The key of item in the layer dictionary is the key number on 49 | # Keybow to map to, and the value is the key press to send. 50 | 51 | # Note that key 0 is reserved as the modifier 52 | 53 | # purple - numeric keypad 54 | layer_1 = {4: Keycode.ZERO, 55 | 5: Keycode.ONE, 56 | 6: Keycode.FOUR, 57 | 7: Keycode.SEVEN, 58 | 8: Keycode.DELETE, 59 | 9: Keycode.TWO, 60 | 10: Keycode.FIVE, 61 | 11: Keycode.EIGHT, 62 | 12: Keycode.ENTER, 63 | 13: Keycode.THREE, 64 | 14: Keycode.SIX, 65 | 15: Keycode.NINE} 66 | 67 | # blue - words 68 | layer_2 = {7: "pack ", 69 | 11: "my ", 70 | 15: "box ", 71 | 6: "with ", 72 | 10: "five ", 73 | 14: "dozen ", 74 | 5: "liquor ", 75 | 9: "jugs "} 76 | 77 | # yellow - media controls 78 | layer_3 = {6: ConsumerControlCode.VOLUME_DECREMENT, 79 | 7: ConsumerControlCode.SCAN_PREVIOUS_TRACK, 80 | 10: ConsumerControlCode.MUTE, 81 | 11: ConsumerControlCode.PLAY_PAUSE, 82 | 14: ConsumerControlCode.VOLUME_INCREMENT, 83 | 15: ConsumerControlCode.SCAN_NEXT_TRACK} 84 | 85 | # white - mixxx 86 | layer_4 = {2: Keycode.X, 87 | 5: Keycode.D, 88 | 7: Keycode.T, 89 | 8: Keycode.SPACE, 90 | 13: Keycode.L, 91 | 15: Keycode.Y} 92 | 93 | layers = {1: layer_1, 94 | 2: layer_2, 95 | 3: layer_3, 96 | 4: layer_4} 97 | 98 | selectors = {1: keys[1], 99 | 2: keys[2], 100 | 3: keys[3], 101 | 4: keys[4]} 102 | 103 | # Define the modifier key and layer selector keys 104 | modifier = keys[0] 105 | 106 | # Start on layer 1 107 | current_layer = 1 108 | 109 | # The colours for each layer 110 | colours = {1: (255, 0, 255), 111 | 2: (0, 255, 255), 112 | 3: (255, 255, 0), 113 | 4: (128, 128, 128)} 114 | 115 | layer_keys = range(0, 16) 116 | 117 | # dictionary of sets (sets cannot be changed but can be replaced) 118 | LEDs = {0: (64, 0, 0), 119 | 1: (128, 0, 0), 120 | 2: (196, 0, 0), 121 | 3: (255, 0, 0), 122 | 4: (0, 4, 0), 123 | 5: (0, 128, 0), 124 | 6: (0, 12, 0), 125 | 7: (0, 196, 0), 126 | 8: (0, 0, 64), 127 | 9: (0, 0, 128), 128 | 10: (0, 0, 196), 129 | 11: (0, 0, 255), 130 | 12: (64, 64, 0), 131 | 13: (128, 128, 0), 132 | 14: (196, 196, 0), 133 | 15: (255, 255, 0)} 134 | 135 | # Set the LEDs for each key in the current layer 136 | for k in layers[current_layer].keys(): 137 | keys[k].set_led(*colours[current_layer]) 138 | 139 | print("Starting!") 140 | mode = 0 141 | count = 0 142 | 143 | # To prevent the strings (as opposed to single key presses) that are sent from 144 | # refiring on a single key press, the debounce time for the strings has to be 145 | # longer. 146 | 147 | short_debounce = 0.03 148 | long_debounce = 0.15 149 | debounce = 0.03 150 | fired = False 151 | 152 | while True: 153 | # Always remember to call keybow.update() 154 | keybow.update() 155 | 156 | # if no key is pressed ensure not locked in layer change mode 157 | if ((mode == 2) & keybow.none_pressed()): 158 | mode = 0 159 | 160 | if modifier.held: 161 | # set to looking to change the keypad layer 162 | for layer in layers.keys(): 163 | # If the modifier key is held, light up the layer selector keys 164 | if mode == 1: 165 | print("Looking for layer select") 166 | # Set the LEDs for each key in selectors 167 | for k in layer_keys: 168 | keys[k].set_led(0, 0, 0) 169 | for k in selectors.keys(): 170 | keys[k].set_led(*colours[k]) 171 | keys[0].set_led(0, 255, 0) 172 | mode = 2 173 | 174 | # Change current layer if layer key is pressed 175 | if selectors[layer].pressed: 176 | if mode >= 1: 177 | mode = 0 178 | current_layer = layer 179 | print("Layer Changed:", current_layer) 180 | # Set the LEDs for each key in the current layer 181 | for k in layer_keys: 182 | keys[k].set_led(0, 0, 0) 183 | for k in layers[current_layer].keys(): 184 | keys[k].set_led(*colours[current_layer]) 185 | else: 186 | # set to look for a key presses 187 | if mode == 0: 188 | print("Looking for key press on layer:", current_layer) 189 | mode = 1 190 | 191 | # Set the LEDs for each key in the current layer 192 | for k in layer_keys: 193 | keys[k].set_led(0, 0, 0) 194 | for k in layers[current_layer].keys(): 195 | keys[k].set_led(*colours[current_layer]) 196 | 197 | # Loop through all of the keys in the layer and if they're pressed, get the 198 | # key code from the layer's key map 199 | for k in layers[current_layer].keys(): 200 | if keys[k].pressed: 201 | key_press = layers[current_layer][k] 202 | 203 | # If the key hasn't just fired (prevents refiring) 204 | if not fired: 205 | fired = True 206 | 207 | # Send the right sort of key press and set debounce for each 208 | # layer accordingly (layer 2 needs a long debounce) 209 | if current_layer == 1 or current_layer == 4: 210 | debounce = short_debounce 211 | keyboard.send(key_press) 212 | elif current_layer == 2: 213 | debounce = long_debounce 214 | layout.write(key_press) 215 | elif current_layer == 3: 216 | debounce = short_debounce 217 | consumer_control.send(key_press) 218 | 219 | # If enough time has passed, reset the fired variable 220 | if fired and time.monotonic() - keybow.time_of_last_press > debounce: 221 | fired = False 222 | -------------------------------------------------------------------------------- /examples/hid-keys-advanced.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # An advanced example of how to set up a HID keyboard. 6 | 7 | # There are three layers, selected by pressing and holding key 0 (bottom left), 8 | # then tapping one of the coloured layer selector keys above it to switch layer. 9 | 10 | # The layer colours are as follows: 11 | 12 | # * layer 1: pink: numpad-style keys, 0-9, delete, and enter. 13 | # * layer 2: blue: sends strings on each key press 14 | # * layer 3: media controls, rev, play/pause, fwd on row one, vol. down, mute, 15 | # vol. up on row two 16 | 17 | # You'll need to connect Keybow 2040 to a computer, as you would with a regular 18 | # USB keyboard. 19 | 20 | # Drop the `pmk` folder 21 | # into your `lib` folder on your `CIRCUITPY` drive. 22 | 23 | # NOTE! Requires the adafruit_hid CircuitPython library also! 24 | 25 | import time 26 | from pmk import PMK 27 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 28 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 29 | 30 | import usb_hid 31 | from adafruit_hid.keyboard import Keyboard 32 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 33 | from adafruit_hid.keycode import Keycode 34 | 35 | from adafruit_hid.consumer_control import ConsumerControl 36 | from adafruit_hid.consumer_control_code import ConsumerControlCode 37 | 38 | # Set up Keybow 39 | keybow = PMK(Hardware()) 40 | keys = keybow.keys 41 | 42 | # Set up the keyboard and layout 43 | keyboard = Keyboard(usb_hid.devices) 44 | layout = KeyboardLayoutUS(keyboard) 45 | 46 | # Set up consumer control (used to send media key presses) 47 | consumer_control = ConsumerControl(usb_hid.devices) 48 | 49 | # Our layers. The key of item in the layer dictionary is the key number on 50 | # Keybow to map to, and the value is the key press to send. 51 | 52 | # Note that keys 0-3 are reserved as the modifier and layer selector keys 53 | # respectively. 54 | 55 | layer_1 = {4: Keycode.ZERO, 56 | 5: Keycode.ONE, 57 | 6: Keycode.FOUR, 58 | 7: Keycode.SEVEN, 59 | 8: Keycode.DELETE, 60 | 9: Keycode.TWO, 61 | 10: Keycode.FIVE, 62 | 11: Keycode.EIGHT, 63 | 12: Keycode.ENTER, 64 | 13: Keycode.THREE, 65 | 14: Keycode.SIX, 66 | 15: Keycode.NINE} 67 | 68 | layer_2 = {7: "pack ", 69 | 11: "my ", 70 | 15: "box ", 71 | 6: "with ", 72 | 10: "five ", 73 | 14: "dozen ", 74 | 5: "liquor ", 75 | 9: "jugs "} 76 | 77 | layer_3 = {6: ConsumerControlCode.VOLUME_DECREMENT, 78 | 7: ConsumerControlCode.SCAN_PREVIOUS_TRACK, 79 | 10: ConsumerControlCode.MUTE, 80 | 11: ConsumerControlCode.PLAY_PAUSE, 81 | 14: ConsumerControlCode.VOLUME_INCREMENT, 82 | 15: ConsumerControlCode.SCAN_NEXT_TRACK} 83 | 84 | layers = {1: layer_1, 85 | 2: layer_2, 86 | 3: layer_3} 87 | 88 | # Define the modifier key and layer selector keys 89 | modifier = keys[0] 90 | 91 | selectors = {1: keys[1], 92 | 2: keys[2], 93 | 3: keys[3]} 94 | 95 | # Start on layer 1 96 | current_layer = 1 97 | 98 | # The colours for each layer 99 | colours = {1: (255, 0, 255), 100 | 2: (0, 255, 255), 101 | 3: (255, 255, 0)} 102 | 103 | layer_keys = range(4, 16) 104 | 105 | # Set the LEDs for each key in the current layer 106 | for k in layers[current_layer].keys(): 107 | keys[k].set_led(*colours[current_layer]) 108 | 109 | # To prevent the strings (as opposed to single key presses) that are sent from 110 | # refiring on a single key press, the debounce time for the strings has to be 111 | # longer. 112 | short_debounce = 0.03 113 | long_debounce = 0.15 114 | debounce = 0.03 115 | fired = False 116 | 117 | while True: 118 | # Always remember to call keybow.update()! 119 | keybow.update() 120 | 121 | # This handles the modifier and layer selector behaviour 122 | if modifier.held: 123 | # Give some visual feedback for the modifier key 124 | keys[0].led_off() 125 | 126 | # If the modifier key is held, light up the layer selector keys 127 | for layer in layers.keys(): 128 | keys[layer].set_led(*colours[layer]) 129 | 130 | # Change layer if layer key is pressed 131 | if current_layer != layer: 132 | if selectors[layer].pressed: 133 | current_layer = layer 134 | 135 | # Set the key LEDs first to off, then to their layer colour 136 | for k in layer_keys: 137 | keys[k].set_led(0, 0, 0) 138 | 139 | for k in layers[layer].keys(): 140 | keys[k].set_led(*colours[layer]) 141 | 142 | # Turn off the layer selector LEDs if the modifier isn't held 143 | else: 144 | for layer in layers.keys(): 145 | keys[layer].led_off() 146 | 147 | # Give some visual feedback for the modifier key 148 | keys[0].set_led(0, 255, 25) 149 | 150 | # Loop through all of the keys in the layer and if they're pressed, get the 151 | # key code from the layer's key map 152 | for k in layers[current_layer].keys(): 153 | if keys[k].pressed: 154 | key_press = layers[current_layer][k] 155 | 156 | # If the key hasn't just fired (prevents refiring) 157 | if not fired: 158 | fired = True 159 | 160 | # Send the right sort of key press and set debounce for each 161 | # layer accordingly (layer 2 needs a long debounce) 162 | if current_layer == 1: 163 | debounce = short_debounce 164 | keyboard.send(key_press) 165 | elif current_layer == 2: 166 | debounce = long_debounce 167 | layout.write(key_press) 168 | elif current_layer == 3: 169 | debounce = short_debounce 170 | consumer_control.send(key_press) 171 | 172 | # If enough time has passed, reset the fired variable 173 | if fired and time.monotonic() - keybow.time_of_last_press > debounce: 174 | fired = False 175 | -------------------------------------------------------------------------------- /examples/hid-keys-simple.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # A simple example of how to set up a keymap and HID keyboard on Keybow 2040. 6 | 7 | # You'll need to connect Keybow 2040 to a computer, as you would with a regular 8 | # USB keyboard. 9 | 10 | # Drop the `pmk` folder 11 | # into your `lib` folder on your `CIRCUITPY` drive. 12 | 13 | # NOTE! Requires the adafruit_hid CircuitPython library also! 14 | 15 | from pmk import PMK 16 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 17 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 18 | 19 | import usb_hid 20 | from adafruit_hid.keyboard import Keyboard 21 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 22 | from adafruit_hid.keycode import Keycode 23 | 24 | # Set up Keybow 25 | keybow = PMK(Hardware()) 26 | keys = keybow.keys 27 | 28 | # Set up the keyboard and layout 29 | keyboard = Keyboard(usb_hid.devices) 30 | layout = KeyboardLayoutUS(keyboard) 31 | 32 | # A map of keycodes that will be mapped sequentially to each of the keys, 0-15 33 | keymap = [Keycode.ZERO, 34 | Keycode.ONE, 35 | Keycode.TWO, 36 | Keycode.THREE, 37 | Keycode.FOUR, 38 | Keycode.FIVE, 39 | Keycode.SIX, 40 | Keycode.SEVEN, 41 | Keycode.EIGHT, 42 | Keycode.NINE, 43 | Keycode.A, 44 | Keycode.B, 45 | Keycode.C, 46 | Keycode.D, 47 | Keycode.E, 48 | Keycode.F] 49 | 50 | # The colour to set the keys when pressed, yellow. 51 | rgb = (255, 255, 0) 52 | 53 | # Attach handler functions to all of the keys 54 | for key in keys: 55 | # A press handler that sends the keycode and turns on the LED 56 | @keybow.on_press(key) 57 | def press_handler(key): 58 | keycode = keymap[key.number] 59 | keyboard.send(keycode) 60 | key.set_led(*rgb) 61 | 62 | # A release handler that turns off the LED 63 | @keybow.on_release(key) 64 | def release_handler(key): 65 | key.led_off() 66 | 67 | while True: 68 | # Always remember to call keybow.update()! 69 | keybow.update() 70 | -------------------------------------------------------------------------------- /examples/midi-arp.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # A MIDI arpeggiator, with three different styles: up, down, or up-down. BPM and 6 | # note length are both configurable, and LEDs cycle with the currently-played 7 | # key/note to give some visual feedback. 8 | 9 | # You'll need to connect Keybow 2040 to a computer running a DAW like Ableton, 10 | # or other software synth, or to a hardware synth that accepts USB MIDI. 11 | 12 | # Drop the `pmk` folder 13 | # into your `lib` folder on your `CIRCUITPY` drive. 14 | 15 | # NOTE! Requires the adafruit_midi CircuitPython library also! 16 | 17 | import time 18 | from pmk import PMK 19 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 20 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 21 | 22 | import usb_midi 23 | import adafruit_midi 24 | from adafruit_midi.note_off import NoteOff 25 | from adafruit_midi.note_on import NoteOn 26 | 27 | # Set up Keybow 28 | keybow = PMK(Hardware()) 29 | keys = keybow.keys 30 | 31 | # Set USB MIDI up on channel 0. 32 | midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=5) 33 | 34 | # The colour to set the keys when pressed, orange-y. 35 | rgb = (255, 50, 0) 36 | 37 | # MIDI velocity. 38 | start_note = 36 39 | velocity = 100 40 | 41 | # Beats per minute 42 | bpm = 80 43 | 44 | # Play 16th notes 45 | note_length = 1 / 16 46 | 47 | # Assumes BPM is calculated on quarter notes 48 | note_time = 60 / bpm * (note_length * 4) 49 | 50 | # Arpeggio style: 51 | # 0 = up 52 | # 1 = down 53 | # 2 = up-down 54 | arp_style = 2 55 | 56 | # Start the arp in a forwards direction (1) if the style is up or up-down, or 57 | # or backwards (-1) if the style is down. 58 | if arp_style == 0 or arp_style == 2: 59 | direction = 1 60 | elif arp_style == 1: 61 | direction = -1 62 | 63 | # Keep track of time of last note played and last keys pressed 64 | last_played = None 65 | last_pressed = [] 66 | 67 | while True: 68 | # Always remember to call keybow.update()! 69 | keybow.update() 70 | 71 | # If any keys are pressed, go through shenanigans 72 | if keybow.any_pressed(): 73 | # Fetch a list of pressed keys 74 | pressed = keybow.get_pressed() 75 | 76 | # If the keys pressed have changed... 77 | if pressed != last_pressed: 78 | # Keys that were pressed, but are no longer 79 | missing = [k for k in last_pressed if k not in pressed] 80 | 81 | # Any keys that were pressed, but are no longer, turn LED off 82 | # and send MIDI note off for the respective note. 83 | for k in missing: 84 | note = start_note + k 85 | midi.send(NoteOff(note, 0)) 86 | keys[k].set_led(0, 0, 0) 87 | 88 | # Calculate MIDI note numbers 89 | notes = [start_note + k for k in pressed] 90 | last_pressed = pressed 91 | 92 | # If going forward (up or starting up-down), start at 0, 93 | # otherwise start at the end of the list of notes. 94 | if arp_style == 0 or arp_style == 2: 95 | this_note = 0 96 | elif arp_style == 1: 97 | this_note = len(notes) - 1 98 | 99 | # Send MIDI note on message for current note and turn LED on 100 | midi.send(NoteOn(notes[this_note], velocity)) 101 | keys[pressed[this_note]].set_led(*rgb) 102 | 103 | # Update last_played time, set elapsed to 0, and update current and 104 | # last note indices. 105 | last_played = time.monotonic() 106 | elapsed = 0 107 | last_note = this_note 108 | this_note += direction 109 | 110 | # If the currently pressed notes are the same as the last loop, then... 111 | else: 112 | if notes != []: 113 | # Check time elapsed since last note played 114 | elapsed = time.monotonic() - last_played 115 | 116 | # If the note time has elapsed, then... 117 | if elapsed > note_time: 118 | # Reset at the end or start of the notes list 119 | if this_note == len(notes) and direction == 1: 120 | this_note = 0 121 | elif this_note < 0: 122 | this_note = len(notes) - 1 123 | 124 | # Send a MIDI note off for the last note, turn off LED 125 | midi.send(NoteOff(notes[last_note], 0)) 126 | keys[pressed[last_note]].set_led(0, 0, 0) 127 | 128 | # Send a MIDI note on for the next note, turn on LED 129 | midi.send(NoteOn(notes[this_note], velocity)) 130 | keys[pressed[this_note]].set_led(*rgb) 131 | 132 | # Update time last_played, make this note last note 133 | last_played = time.monotonic() 134 | last_note = this_note 135 | 136 | # For the up-down style, switch direction at either end 137 | if arp_style == 2 and this_note == len(notes) - 1: 138 | direction = -1 139 | elif arp_style == 2 and this_note == 0: 140 | direction = 1 141 | 142 | # Increment note 143 | this_note += direction 144 | 145 | # If nothing is now pressed, but was last time, then send MIDI note off 146 | # for every note, and turn all the LEDs off. 147 | elif len(last_pressed) and keybow.none_pressed(): 148 | for note in range(128): 149 | midi.send(NoteOff(note, 0)) 150 | for key in keys: 151 | key.set_led(0, 0, 0) 152 | 153 | # Nothing is pressed, so reset last_pressed list 154 | last_pressed = [] 155 | -------------------------------------------------------------------------------- /examples/midi-keys.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Demonstrates how to send MIDI notes by attaching handler functions to key 6 | # presses with decorators. 7 | 8 | # You'll need to connect Keybow 2040 to a computer running a DAW like Ableton, 9 | # or other software synth, or to a hardware synth that accepts USB MIDI. 10 | 11 | # Drop the `pmk` folder 12 | # into your `lib` folder on your `CIRCUITPY` drive. 13 | 14 | # NOTE! Requires the adafruit_midi CircuitPython library also! 15 | 16 | from pmk import PMK 17 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 18 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 19 | 20 | import usb_midi 21 | import adafruit_midi 22 | from adafruit_midi.note_off import NoteOff 23 | from adafruit_midi.note_on import NoteOn 24 | 25 | # Set up Keybow 26 | keybow = PMK(Hardware()) 27 | keys = keybow.keys 28 | 29 | # Set USB MIDI up on channel 0. 30 | midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) 31 | 32 | # The colour to set the keys when pressed. 33 | rgb = (0, 255, 50) 34 | 35 | # Initial values for MIDI note and velocity. 36 | start_note = 36 37 | velocity = 127 38 | 39 | # Loop through keys and attach decorators. 40 | for key in keys: 41 | # If pressed, send a MIDI note on command and light key. 42 | @keybow.on_press(key) 43 | def press_handler(key): 44 | note = start_note + key.number 45 | key.set_led(*rgb) 46 | midi.send(NoteOn(note, velocity)) 47 | 48 | # If released, send a MIDI note off command and turn off LED. 49 | @keybow.on_release(key) 50 | def release_handler(key): 51 | note = start_note + key.number 52 | key.set_led(0, 0, 0) 53 | midi.send(NoteOff(note, 0)) 54 | 55 | while True: 56 | # Always remember to call keybow.update()! 57 | keybow.update() 58 | -------------------------------------------------------------------------------- /examples/midi-sequencer.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # A MIDI step sequencer, with four tracks and eight steps per track. 6 | 7 | # The eight steps are on the top two rows of keys. Steps can be toggled on by 8 | # tapping a step's key. Active steps are indicated with a brighter LED, and the 9 | # currently playing step in the sequence is shown with a moving LED across the 10 | # eight steps. 11 | 12 | # Each track is colour-coded: track 1 is orange, track 2 blue, track 3 is pink, 13 | # and track 4 is green. Tracks can be selected by pressing and holding the 14 | # bottom left orange track select key and then tapping one of the four track 15 | # keys on the row above. The currently focussed track's track select key (on the 16 | # second bottom row) is highlighted in a brighter colour. 17 | 18 | # A track can be toggled on or off (no notes are sent from that track, but notes 19 | # are not deleted) by tapping the track's track select key. The track select LED 20 | # for a track toggled off will not be lit. 21 | 22 | # The sequencer is started and stopped by tapping the bottom right key, which is 23 | # red when the sequencer is stopped, and green when it is playing. 24 | 25 | # The sequencer can be cleared by holding the track selector key (orange, bottom 26 | # left) and then holding the start/stop key (red/green, bottom right). 27 | 28 | # A single track can be cleared by holding the track selector key, the track 29 | # select key (on the second bottom row) for the track you want to clear, and 30 | # then holding the start/stop key. 31 | 32 | # Tempo can be increased or decreased by holding the tempo selector key (blue, 33 | # second from left, on the bottom row) and then tapping blue key on the row 34 | # above to shift tempo down, or the pink key to shift it up. Tempo is increased/ 35 | # decreased by 5 BPM on each press. 36 | 37 | # If an active step is held down, the second bottom row of keys lights to allow 38 | # the note to be shifted down/up (the left two keys, decremented/incremented by 39 | # one each time) and the note velocity to be shifted down/up (the right two keys 40 | # decremented/incremented by four each time). 41 | 42 | # You'll need to connect Keybow 2040 to a computer running a DAW like Ableton, 43 | # or other software synth, or to a hardware synth that accepts USB MIDI. 44 | 45 | # Tracks' notes are sent on MIDI channels 1-4. 46 | 47 | # Drop the `pmk` folder 48 | # into your `lib` folder on your `CIRCUITPY` drive. 49 | 50 | # NOTE! Requires the adafruit_midi CircuitPython library also! 51 | 52 | import time 53 | from pmk import PMK 54 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 55 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 56 | 57 | import usb_midi 58 | import adafruit_midi 59 | from adafruit_midi.note_off import NoteOff 60 | from adafruit_midi.note_on import NoteOn 61 | 62 | 63 | # CONSTANTS. Change these to change the look and feel of the sequencer. 64 | 65 | # These are the key numbers that represent each step in a track (the top two 66 | # rows of four keys) 67 | TRACK_KEYS = [3, 7, 11, 15, 2, 6, 10, 14] 68 | 69 | ORANGE = (255, 255, 0) 70 | BLUE = (0, 255, 175) 71 | PINK = (255, 0, 255) 72 | GREEN = (0, 255, 0) 73 | RED = (255, 0, 0) 74 | 75 | # The colours for the LEDs on each track: orange, blue, pink, green 76 | TRACK_COLOURS = [ORANGE, BLUE, PINK, GREEN] 77 | 78 | # The MIDI channels for each track in turn: 1, 2, 3, 4 79 | MIDI_CHANNELS = [0, 1, 2, 3] 80 | 81 | # The bottom left key, orange. When pressed, it brings up the track selector 82 | # keys, the four keys on the row above it. 83 | TRACK_SELECTOR = 0 84 | TRACK_SELECTOR_KEYS = [1, 5, 9, 13] 85 | TRACK_SELECTOR_COLOUR = ORANGE 86 | 87 | # The bottom right key. When pressed, it toggles the sequencer on or off. Green 88 | # indicates that it is currently playing, red that it is stopped. 89 | START_STOP = 12 90 | START_COLOUR = GREEN 91 | STOP_COLOUR = RED 92 | 93 | # The key second from left on the bottom row, blue. When pressed, it brings up 94 | # the tempo down/up buttons on the row above it. The left blue key shifts the 95 | # tempo down, the right pink key shifts the tempo up. 96 | TEMPO_SELECTOR = 4 97 | TEMPO_SELECTOR_COLOUR = BLUE 98 | TEMPO_DOWN = 1 99 | TEMPO_DOWN_COLOUR = BLUE 100 | TEMPO_UP = 5 101 | TEMPO_UP_COLOUR = PINK 102 | 103 | NOTE_DOWN = 1 104 | NOTE_DOWN_COLOUR = BLUE 105 | NOTE_UP = 5 106 | NOTE_UP_COLOUR = PINK 107 | 108 | # When an active step is held down, the second bottom row of keys lights to 109 | # allow the note to be shifted down/up (the left two keys) and the note velocity 110 | # to be shifted down/up (the right two keys). 111 | VELOCITY_DOWN = 9 112 | VELOCITY_DOWN_COLOUR = BLUE 113 | VELOCITY_UP = 13 114 | VELOCITY_UP_COLOUR = PINK 115 | 116 | # The default starting BPM. 117 | BPM = 85 118 | MAX_BPM = 200 119 | 120 | # Dictates the time after which a key is "held". 121 | KEY_HOLD_TIME = 0.25 122 | 123 | # LED brightness settings for the track steps. 124 | HIGH_BRIGHTNESS = 1.0 125 | MID_BRIGHTNESS = 0.2 126 | LOW_BRIGHTNESS = 0.05 127 | 128 | # Start on middle C and a reasonably high velocity. 129 | DEFAULT_NOTE = 60 130 | DEFAULT_VELOCITY = 99 131 | MAX_VELOCITY = 127 132 | MAX_NOTE = 127 133 | VELOCITY_STEP = 4 134 | 135 | 136 | class Sequencer(PMK): 137 | """ 138 | Represents the sequencer, with a set of Track instances, which in turn have 139 | a set of Step instances. This class is a subclass of the PMK class, 140 | so it inherits all of its methods and key methods. 141 | 142 | :param hardware: object representing a board hardware 143 | """ 144 | def __init__(self, *args, **kwargs): 145 | super(Sequencer, self).__init__(*args, **kwargs) 146 | 147 | # Holds the list of MIDI channels for the tracks. 148 | self.midi_channels = [] 149 | 150 | # Set the MIDI channels up. 151 | for channel in MIDI_CHANNELS: 152 | midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=channel) 153 | self.midi_channels.append(midi) 154 | 155 | # These keys represent the steps on the tracks. 156 | self.track_keys = [] 157 | 158 | for i in range(len(TRACK_KEYS)): 159 | track_key = self.keys[TRACK_KEYS[i]] 160 | track_key.index = i 161 | self.track_keys.append(track_key) 162 | 163 | # These keys select and change the current track. 164 | self.track_select_keys = [] 165 | 166 | for i in range(len(TRACK_SELECTOR_KEYS)): 167 | track_select_key = self.keys[TRACK_SELECTOR_KEYS[i]] 168 | track_select_key.rgb = TRACK_COLOURS[i] 169 | self.track_select_keys.append(track_select_key) 170 | 171 | self.track_select_keys_held = [False, False, False, False] 172 | self.track_select_active = False 173 | 174 | # Holds the list of tracks, a set of Track instances. 175 | self.tracks = [] 176 | 177 | # Set the tracks up. 178 | for i in range(4): 179 | track = Track(self, i, i, TRACK_COLOURS[i]) 180 | self.tracks.append(track) 181 | 182 | # Speed attributes. 183 | self.bpm = BPM 184 | self.tempo_selector = self.keys[TEMPO_SELECTOR] 185 | self.tempo_selector.set_led(*TEMPO_SELECTOR_COLOUR) 186 | self.tempo_select_active = False 187 | self.tempo_down = self.keys[TEMPO_DOWN] 188 | self.tempo_up = self.keys[TEMPO_UP] 189 | 190 | # Step related stuff 191 | self.num_steps = 8 192 | self.this_step_num = 0 193 | self.last_step_num = 0 194 | self.steps_held = [] 195 | 196 | # Note change attributes. 197 | self.note_down = self.keys[NOTE_DOWN] 198 | self.note_up = self.keys[NOTE_UP] 199 | self.velocity_down = self.keys[VELOCITY_DOWN] 200 | self.velocity_up = self.keys[VELOCITY_UP] 201 | 202 | # Is the sequencer running? 203 | self.running = False 204 | 205 | # Step time assumes the BPM is based on quarter notes. 206 | self.step_time = 60.0 / self.bpm / (self.num_steps / 2) 207 | self.last_step_time = time.monotonic() 208 | 209 | # Set the default starting track to track 0 210 | self.current_track = 0 211 | 212 | # The start stop key. 213 | self.start_stop = self.keys[START_STOP] 214 | self.start_stop.set_led(*STOP_COLOUR) 215 | self.start_stop_held = False 216 | 217 | # The track selector key. 218 | self.track_selector = self.keys[TRACK_SELECTOR] 219 | self.track_selector.set_led(*TRACK_SELECTOR_COLOUR) 220 | 221 | # Set the key hold time for all the keys. A little shorter than the 222 | # default for Keybow. Makes controlling the sequencer a bit more fluid. 223 | for key in self.keys: 224 | key.hold_time = KEY_HOLD_TIME 225 | 226 | # Attach step_select function to keys in track steps. If pressed it 227 | # toggles the state of the step. 228 | for key in self.track_keys: 229 | @self.on_release(key) 230 | def step_select(key): 231 | if self.tracks[self.current_track].active: 232 | if not key.held: 233 | step = self.tracks[self.current_track].steps[key.index] 234 | step.toggle() 235 | if not step.active: 236 | current_note = step.note 237 | self.midi_channels[track.channel].send(NoteOff(current_note, 0)) 238 | step.note = DEFAULT_NOTE 239 | step.velocity = DEFAULT_VELOCITY 240 | else: 241 | self.steps_held.remove(key.index) 242 | self.note_down.led_off() 243 | self.note_up.led_off() 244 | self.velocity_down.led_off() 245 | self.velocity_up.led_off() 246 | 247 | self.update_track_select_keys(True) 248 | 249 | # When step held, toggle on the note and velocity up/down keys. 250 | @self.on_hold(key) 251 | def step_change(key): 252 | if self.tracks[self.current_track].active: 253 | self.steps_held.append(key.index) 254 | self.note_down.set_led(*NOTE_DOWN_COLOUR) 255 | self.note_up.set_led(*NOTE_UP_COLOUR) 256 | self.velocity_down.set_led(*VELOCITY_DOWN_COLOUR) 257 | self.velocity_up.set_led(*VELOCITY_UP_COLOUR) 258 | 259 | self.update_track_select_keys(False) 260 | 261 | # Attach hold function to track selector key that sets it active and 262 | # lights the track select keys. 263 | @self.on_hold(self.track_selector) 264 | def track_selector_hold(key): 265 | self.track_select_active = True 266 | 267 | for track in self.tracks: 268 | track.update_track_select_key = True 269 | 270 | # Attach release function to track selector key that sets it inactive 271 | # and turns track select LEDs off. 272 | @self.on_release(self.track_selector) 273 | def track_selector_release(key): 274 | self.track_select_active = False 275 | self.update_track_select_keys(True) 276 | 277 | # Handles track select/mute, tempo down/up, note down/up. 278 | # 279 | # If the tempo selector key (second from left, blue, on the bottom row) 280 | # is held, pressing the tempo keys (the left two keys on the second 281 | # bottom row, lit blue and pink) shifts the tempo down or up by 282 | # 5 bpm each time it is pressed, with a lower limit of 5 BPM and upper 283 | # limit of 200 BPM. 284 | # 285 | # If notes are held, then the four track select keys allow the held 286 | # notes MIDI note number to be shifted down/up (track select keys 0 287 | # and 1 respectively), or MIDI velocity to be shifted down/up (track 288 | # select keys 2 and 3 respectively). 289 | # 290 | # If the track selector is not held, tapping this track button toggles 291 | # the track on/off. 292 | for key in self.track_select_keys: 293 | 294 | @self.on_press(key) 295 | def track_select_press(key): 296 | index = TRACK_SELECTOR_KEYS.index(key.number) 297 | if self.track_select_active: 298 | self.current_track = index 299 | elif self.tempo_select_active: 300 | if index == 0: 301 | if self.bpm > 5: 302 | self.bpm -= 5 303 | elif index == 1: 304 | if self.bpm < 200: 305 | self.bpm += 5 306 | elif len(self.steps_held): 307 | for i in self.steps_held: 308 | step = self.tracks[self.current_track].steps[i] 309 | if index == 0 or index == 1: 310 | step.last_notes.append(step.note) 311 | step.note_changed = True 312 | if index == 0: 313 | if step.note > 0: 314 | step.note -= 1 315 | elif index == 1: 316 | if step.note < MAX_NOTE: 317 | step.note += 1 318 | elif index == 2: 319 | if step.velocity > 0 + VELOCITY_STEP: 320 | step.velocity -= VELOCITY_STEP 321 | elif index == 3: 322 | if step.velocity <= MAX_VELOCITY - VELOCITY_STEP: 323 | step.velocity += VELOCITY_STEP 324 | else: 325 | self.tracks[index].active = not self.tracks[index].active 326 | self.tracks[index].update_track_select_key = True 327 | 328 | # Handlers to hold held states of track select keys. 329 | for key in self.track_select_keys: 330 | @self.on_hold(key) 331 | def track_select_key_hold(key): 332 | index = TRACK_SELECTOR_KEYS.index(key.number) 333 | self.track_select_keys_held[index] = True 334 | 335 | @self.on_release(key) 336 | def track_select_key_release(key): 337 | index = TRACK_SELECTOR_KEYS.index(key.number) 338 | self.track_select_keys_held[index] = False 339 | 340 | # Attach press function to start/stop key that toggles whether the 341 | # sequencer is running and toggles its colour between green (running) 342 | # and red (not running). 343 | @self.on_press(self.start_stop) 344 | def start_stop_toggle(key): 345 | if not self.track_select_active: 346 | if self.running: 347 | self.running = False 348 | key.set_led(*STOP_COLOUR) 349 | else: 350 | self.running = True 351 | key.set_led(*START_COLOUR) 352 | 353 | # Attach hold function, so that when the track selector key is held and 354 | # the start/stop key is also held, clear all of the steps on all of the 355 | # tracks. If a track select key is held, then clear just that track. 356 | @self.on_hold(self.start_stop) 357 | def start_stop_hold(key): 358 | self.start_stop_held = True 359 | 360 | if self.track_select_active: 361 | if not any(self.track_select_keys_held): 362 | self.clear_tracks() 363 | for track in self.tracks: 364 | track.midi_panic() 365 | else: 366 | for i, state in enumerate(self.track_select_keys_held): 367 | if state: 368 | self.tracks[i].clear_steps() 369 | 370 | @self.on_release(self.start_stop) 371 | def start_stop_release(key): 372 | self.start_stop_held = False 373 | 374 | # Attach hold function that lights the tempo down/up keys when the 375 | # tempo selector key is held. 376 | @self.on_hold(self.tempo_selector) 377 | def tempo_selector_hold(key): 378 | self.tempo_select_active = True 379 | self.tempo_down.set_led(*TEMPO_DOWN_COLOUR) 380 | self.tempo_up.set_led(*TEMPO_UP_COLOUR) 381 | self.track_select_keys[2].led_off() 382 | self.track_select_keys[3].led_off() 383 | self.update_track_select_keys(False) 384 | 385 | # Attach release function that furns off the tempo down/up LEDs. 386 | @self.on_release(self.tempo_selector) 387 | def tempo_selector_release(key): 388 | self.tempo_select_active = False 389 | self.tempo_down.led_off() 390 | self.tempo_up.led_off() 391 | self.update_track_select_keys(True) 392 | 393 | def update(self): 394 | # Update the superclass (PMK). 395 | super(Sequencer, self).update() 396 | 397 | if self.running: 398 | # Keep track of current time. 399 | current_time = time.monotonic() 400 | 401 | # If a step has elapsed... 402 | if current_time - self.last_step_time > self.step_time: 403 | for track in self.tracks: 404 | if track.active: 405 | # Turn last step off. 406 | last_step = track.steps[self.last_step_num] 407 | last_step.playing = False 408 | last_step.update() 409 | last_note = last_step.note 410 | 411 | # Helps prevent stuck notes. 412 | if last_step.note_changed: 413 | for note in last_step.last_notes: 414 | self.midi_channels[track.channel].send(NoteOff(note, 0)) 415 | last_step.note_changed = False 416 | last_step.last_notes = [] 417 | 418 | # If last step is active, send MIDI note off message. 419 | if last_step.active: 420 | self.midi_channels[track.channel].send(NoteOff(last_note, 0)) 421 | 422 | # Turn this step on. 423 | this_step = track.steps[self.this_step_num] 424 | this_step.playing = True 425 | this_step.update() 426 | this_note = this_step.note 427 | this_vel = this_step.velocity 428 | 429 | # Helps prevent stuck notes 430 | if this_step.note_changed: 431 | for note in this_step.last_notes: 432 | self.midi_channels[track.channel].send(NoteOff(note, 0)) 433 | this_step.note_changed = False 434 | this_step.last_notes = [] 435 | 436 | # If this step is active, send MIDI note on message. 437 | if this_step.active: 438 | self.midi_channels[track.channel].send(NoteOn(this_note, this_vel)) 439 | 440 | # If track is not active, send note off for last note and this note. 441 | else: 442 | last_note = track.steps[self.last_step_num].note 443 | this_note = track.steps[self.this_step_num].note 444 | self.midi_channels[track.channel].send(NoteOff(last_note, 0)) 445 | self.midi_channels[track.channel].send(NoteOff(this_note, 0)) 446 | 447 | # This step is now the last step! 448 | last_step = this_step 449 | self.last_step_num = self.this_step_num 450 | self.this_step_num += 1 451 | 452 | # If we get to the end of the sequence, go back to the start. 453 | if self.this_step_num == self.num_steps: 454 | self.this_step_num = 0 455 | 456 | # Keep track of last step time. 457 | self.last_step_time = current_time 458 | 459 | # Update the tracks. 460 | for track in self.tracks: 461 | track.update() 462 | 463 | # Update the step_time, in case the BPM has been changed. 464 | self.step_time = 60.0 / self.bpm / (self.num_steps / 2) 465 | 466 | def clear_tracks(self): 467 | # Clears the steps on all tracks. 468 | for track in self.tracks: 469 | track.clear_steps() 470 | 471 | def update_track_select_keys(self, state): 472 | # Updates all of the track select keys' states in one go. 473 | for track in self.tracks: 474 | track.update_track_select_key = state 475 | 476 | 477 | class Track: 478 | """ 479 | Represents a track on the sequencer. 480 | 481 | :param sequencer: the parent sequencer instance 482 | :param index: the index of the track, integer 483 | :param channel: the MIDI channel, integer 484 | :param rgb: the RGB colour of the track, tuple of R, G, B, 0-255. 485 | """ 486 | def __init__(self, sequencer, index, channel, rgb): 487 | self.index = index 488 | self.rgb = rgb 489 | self.channel = channel 490 | self.steps = [] 491 | self.sequencer = sequencer 492 | self.track_keys = self.sequencer.track_keys 493 | self.update_track_leds = False 494 | self.update_track_select_key = True 495 | self.select_key = self.sequencer.track_select_keys[self.index] 496 | 497 | # For each key in the track, create a Step instance and add to 498 | # self.steps. 499 | for i, key in enumerate(self.track_keys): 500 | step = Step(i, key, self) 501 | self.steps.append(step) 502 | 503 | # Default to having the track active. 504 | self.active = True 505 | self.focussed = False 506 | 507 | def set_on(self): 508 | # Toggle the track on. 509 | self.active = True 510 | 511 | def set_off(self): 512 | # Toggle the track off. 513 | self.active = False 514 | 515 | def update(self): 516 | # Make the current track focussed and update its steps. 517 | if sequencer.current_track == self.index: 518 | self.focussed = True 519 | self.update_steps() 520 | else: 521 | self.focussed = False 522 | 523 | r, g, b = TRACK_COLOURS[self.index] 524 | 525 | # Only update these keys if required, as it affects the BPM when 526 | # constantly updating them. Light the focussed track in a bright colour. 527 | # Turn the LED off for tracks that aren't active. 528 | if self.update_track_select_key: 529 | if not self.sequencer.track_select_active: 530 | if self.active: 531 | if not self.focussed: 532 | r, g, b = rgb_with_brightness(r, g, b, brightness=LOW_BRIGHTNESS) 533 | self.select_key.set_led(r, g, b) 534 | else: 535 | r, g, b = rgb_with_brightness(r, g, b, brightness=HIGH_BRIGHTNESS) 536 | self.select_key.set_led(r, g, b) 537 | else: 538 | self.select_key.led_off() 539 | self.update_track_select_key = False 540 | else: 541 | r, g, b = rgb_with_brightness(r, g, b, brightness=HIGH_BRIGHTNESS) 542 | self.select_key.set_led(r, g, b) 543 | self.update_track_select_key = False 544 | 545 | def update_steps(self): 546 | # Update a tracks steps. 547 | for step in self.steps: 548 | step.update() 549 | 550 | def clear_steps(self): 551 | # Clear a track's steps by setting them all to inactive. 552 | for step in self.steps: 553 | step.active = False 554 | 555 | def midi_panic(self): 556 | # Send note off messages for every note on this track's channel. 557 | for i in range(128): 558 | self.sequencer.midi_channels[self.channel].send(NoteOff(i, 0)) 559 | 560 | 561 | class Step: 562 | """ 563 | Represents a step on a track. 564 | 565 | :param index: the index of the step, integer 566 | :param key: the key attached to this step, integer 567 | :param track: the track this step belongs to, Track instance. 568 | """ 569 | def __init__(self, index, key, track): 570 | self.index = index 571 | self.key = key 572 | self.track = track 573 | self.active = False 574 | self.playing = False 575 | self.held = False 576 | self.velocity = DEFAULT_VELOCITY 577 | self.note = DEFAULT_NOTE 578 | self.last_notes = [] 579 | self.note_changed = False 580 | self.rgb = self.track.rgb 581 | self.sequencer = self.track.sequencer 582 | 583 | def toggle(self): 584 | # Toggle the step between active and inactive. 585 | self.active = not self.active 586 | 587 | def state(self): 588 | # Returns the state of the track (active/inactve). 589 | return self.active 590 | 591 | def set_led(self, r, g, b, brightness): 592 | # Set the step's LED. Has an additional brightness parameter from 0.0 593 | # (off) to 1.0 (full brightness for the colour). 594 | r, g, b = [int(c * brightness) for c in (r, g, b)] 595 | self.key.set_led(r, g, b) 596 | 597 | def update(self): 598 | # Update the step. Pretty much just handles the LEDs. 599 | r, g, b = self.rgb 600 | 601 | # If this step's track is focussed... 602 | if self.track.focussed: 603 | # Only update the LEDs when the sequencer is running. 604 | if self.sequencer.running: 605 | if self.track.active: 606 | # Make an active step that is currently being played full 607 | # brightness. 608 | if self.playing and self.active: 609 | self.set_led(r, g, b, HIGH_BRIGHTNESS) 610 | 611 | # Make an inactive step that is "playing" (the current step) 612 | # the dimmest brightness, but bright enough to indicate the 613 | # step the sequencer is on. 614 | if self.playing and not self.active: 615 | self.set_led(r, g, b, LOW_BRIGHTNESS) 616 | 617 | # Make an active step that is not playing a low-medium 618 | # brightness to indicate that it is toggled active. 619 | if not self.playing and self.active: 620 | self.set_led(r, g, b, MID_BRIGHTNESS) 621 | 622 | # Turn not playing, not active steps off. 623 | if not self.playing and not self.active: 624 | self.set_led(0, 0, 0, 0) 625 | else: 626 | self.set_led(0, 0, 0, 0) 627 | 628 | # If the sequencer is not running, still show the active steps. 629 | elif not self.sequencer.running: 630 | if self.active: 631 | self.set_led(r, g, b, 0.3) 632 | else: 633 | self.set_led(0, 0, 0, 0) 634 | 635 | 636 | def rgb_with_brightness(r, g, b, brightness=1.0): 637 | # Allows an RGB value to be altered with a brightness 638 | # value from 0.0 to 1.0. 639 | r, g, b = (int(c * brightness) for c in (r, g, b)) 640 | return r, g, b 641 | 642 | 643 | # Instantiate the sequencer. 644 | sequencer = Sequencer(Hardware()) 645 | 646 | while True: 647 | # Always remember to call sequencer.update() on every iteration of the main 648 | # loop, otherwise NOTHING WILL WORK! 649 | sequencer.update() 650 | -------------------------------------------------------------------------------- /examples/obs-studio-toggle-and-mutex.py: -------------------------------------------------------------------------------- 1 | 2 | # SPDX-FileCopyrightText: 2021 Philip Howard 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | # This example gives you eight toggle and eight "mutex" key bindings for OBS studio 7 | # 8 | # MUTEX 9 | # Use the top eight buttons (nearest the USB connector) to bind your scenes. 10 | # The light on these is mutually exclusive- the one you last pressed should light up, 11 | # and this is the scene you should be broadcasting. 12 | # 13 | # TOGGLE 14 | # The bottom eight buttons will toggle on/off, emitting a slightly different keycode 15 | # for each state. This means they will *always* indicate the toggle state. 16 | # Bind these to Mute/Unmute audio by pressing the key once in Mute and once again in Unmute. 17 | # 18 | # Keep OBS focussed when using these... to avoid weirdness! 19 | 20 | # Drop the `pmk` folder 21 | # into your `lib` folder on your `CIRCUITPY` drive. 22 | 23 | import math 24 | from pmk import PMK, number_to_xy, hsv_to_rgb 25 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 26 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 27 | 28 | import usb_hid 29 | from adafruit_hid.keyboard import Keyboard 30 | from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS 31 | from adafruit_hid.keycode import Keycode 32 | 33 | # Pick your keycodes here, these are chosen to - mostly - stay out of the way 34 | # and use Numpad and regular numbers. 35 | # Toggle keybinds (indicated by a Tuple with True) will send: 36 | # * CONTROL + SHIFT + KEYCODE - when toggled on 37 | # * CONTROL + ALT + KEYCODE - when toggled off 38 | keycodes = [ 39 | (Keycode.KEYPAD_FIVE, True), # Bottom 1 40 | (Keycode.KEYPAD_ONE, True), # Bottom 1 41 | Keycode.FIVE, 42 | Keycode.ONE, 43 | (Keycode.KEYPAD_SIX, True), # Bottom 2 44 | (Keycode.KEYPAD_TWO, True), # Bottom 2 45 | Keycode.SIX, 46 | Keycode.TWO, 47 | (Keycode.KEYPAD_SEVEN, True), # Bottom 3 48 | (Keycode.KEYPAD_THREE, True), # Bottom 3 49 | Keycode.SEVEN, 50 | Keycode.THREE, 51 | (Keycode.KEYPAD_EIGHT, True), # Bottom 4 52 | (Keycode.KEYPAD_FOUR, True), # Bottom 4 53 | Keycode.EIGHT, 54 | Keycode.FOUR 55 | ] 56 | 57 | # Set up the keyboard and layout 58 | keyboard = Keyboard(usb_hid.devices) 59 | layout = KeyboardLayoutUS(keyboard) 60 | 61 | # Set up Keybow 62 | keybow = PMK(Hardware()) 63 | keys = keybow.keys 64 | 65 | states = [False for _ in keys] 66 | 67 | # Increment step to shift animation across keys. 68 | step = 0 69 | active = -1 70 | 71 | for key in keys: 72 | @keybow.on_press(key) 73 | def press_handler(key): 74 | global active 75 | print("{} pressed".format(key.number)) 76 | binding = keycodes[key.number] 77 | if binding is None: 78 | return 79 | if type(binding) is tuple: 80 | binding, _ = binding 81 | states[key.number] = not states[key.number] 82 | if states[key.number]: 83 | keyboard.press(Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, binding) 84 | else: 85 | keyboard.press(Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, Keycode.LEFT_ALT, binding) 86 | else: 87 | keyboard.press(Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, binding) 88 | active = key.number 89 | 90 | @keybow.on_release(key) 91 | def release_handler(key): 92 | global active 93 | print("{} released".format(key.number)) 94 | binding = keycodes[key.number] 95 | if binding is None: 96 | return 97 | if type(binding) is tuple: 98 | binding, _ = binding 99 | if states[key.number]: 100 | keyboard.release(Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, binding) 101 | else: 102 | keyboard.release(Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, Keycode.LEFT_ALT, binding) 103 | else: 104 | keyboard.release(Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, binding) 105 | 106 | @keybow.on_hold(key) 107 | def hold_handler(key): 108 | pass 109 | 110 | 111 | while True: 112 | # Always remember to call keybow.update() on every iteration of your loop! 113 | keybow.update() 114 | 115 | step += 1 116 | 117 | for i in range(16): 118 | # Convert the key number to an x/y coordinate to calculate the hue 119 | # in a matrix style-y. 120 | x, y = number_to_xy(i) 121 | 122 | # Calculate the hue. 123 | hue = (x + y + (step / 20)) / 8 124 | hue = hue - int(hue) 125 | hue = hue - math.floor(hue) 126 | 127 | # Convert the hue to RGB values. 128 | r, g, b = hsv_to_rgb(hue, 1, 1) 129 | 130 | # Display it on the key! 131 | if i == active or states[i]: 132 | keys[i].set_led(r, g, b) 133 | else: 134 | keys[i].set_led(int(r / 10.0), int(g / 10.0), int(b / 10.0)) 135 | -------------------------------------------------------------------------------- /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 `pmk` folder 8 | # into your `lib` folder on your `CIRCUITPY` drive. 9 | 10 | import math 11 | from pmk import PMK, number_to_xy, hsv_to_rgb 12 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 13 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 14 | 15 | # Set up Keybow 16 | keybow = PMK(Hardware()) 17 | keys = keybow.keys 18 | 19 | # Increment step to shift animation across keys. 20 | step = 0 21 | 22 | while True: 23 | # Always remember to call keybow.update() on every iteration of your loop! 24 | keybow.update() 25 | 26 | step += 1 27 | 28 | for i in range(16): 29 | # Convert the key number to an x/y coordinate to calculate the hue 30 | # in a matrix style-y. 31 | x, y = number_to_xy(i) 32 | 33 | # Calculate the hue. 34 | hue = (x + y + (step / 20)) / 8 35 | hue = hue - int(hue) 36 | hue = hue - math.floor(hue) 37 | 38 | # Convert the hue to RGB values. 39 | r, g, b = hsv_to_rgb(hue, 1, 1) 40 | 41 | # Display it on the key! 42 | keys[i].set_led(r, g, b) 43 | -------------------------------------------------------------------------------- /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 `pmk` folder 8 | # into your `lib` folder on your `CIRCUITPY` drive. 9 | 10 | 11 | from pmk import PMK 12 | from pmk.platform.keybow2040 import Keybow2040 as Hardware # for Keybow 2040 13 | # from pmk.platform.rgbkeypadbase import RGBKeypadBase as Hardware # for Pico RGB Keypad Base 14 | 15 | # Set up Keybow 16 | keybow = PMK(Hardware()) 17 | keys = keybow.keys 18 | 19 | # Use cyan as the colour. 20 | rgb = (0, 255, 255) 21 | 22 | while True: 23 | # Always remember to call keybow.update() on every iteration of your loop! 24 | keybow.update() 25 | 26 | # Loop through the keys and set the LED to cyan if pressed, otherwise turn 27 | # it off (set it to black). 28 | for key in keys: 29 | if key.pressed: 30 | key.set_led(*rgb) 31 | else: 32 | key.set_led(0, 0, 0) 33 | -------------------------------------------------------------------------------- /keybow-2040-github-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimoroni/pmk-circuitpython/476b5bfb00e24d90a5bfb9c5472506cd1b7c922f/keybow-2040-github-1.jpg -------------------------------------------------------------------------------- /lib/pmk/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Sandy Macdonald 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """ 6 | `PMK CircuitPython library` 7 | ==================================================== 8 | 9 | PMK: Pimoroni Mechanical/Mushy Keypads 10 | 11 | CircuitPython driver for the Pimoroni Keybow 2040 and Pico RGB Keypad Base. 12 | 13 | Drop the `lib` contents (`pmk.py` file and `pmk_platform` folder) 14 | into your `lib` folder on your `CIRCUITPY` drive. 15 | 16 | * Authors: Sandy Macdonald, Maciej Sokolowski 17 | 18 | Notes 19 | -------------------- 20 | 21 | **Hardware:** 22 | 23 | One of: 24 | 25 | * Pimoroni Keybow 2040 26 | _ 27 | 28 | * Pimoroni Pico RGB Keypad Base 29 | _ 30 | 31 | **Software and Dependencies:** 32 | 33 | For Keybow 2040: 34 | 35 | * Adafruit CircuitPython firmware for Keybow 2040: 36 | _ 37 | 38 | * Adafruit CircuitPython IS31FL3731 library: 39 | _ 40 | 41 | For Pico RGB Keypad Base: 42 | 43 | * Adafruit CircuitPython firmware for Raspberry Pi Pico: 44 | _ 45 | 46 | * Adafruit CircuitPython DotStar library: 47 | _ 48 | 49 | """ 50 | 51 | import time 52 | 53 | class PMK(object): 54 | """ 55 | Represents a set of Key instances with 56 | associated LEDs and key behaviours. 57 | 58 | :param hardware: object representing a board hardware 59 | """ 60 | def __init__(self, hardware): 61 | self.hardware = hardware 62 | self.keys = [] 63 | self.time_of_last_press = time.monotonic() 64 | self.time_since_last_press = None 65 | self.led_sleep_enabled = False 66 | self.led_sleep_time = 60 67 | self.sleeping = False 68 | self.was_asleep = False 69 | self.last_led_states = None 70 | self.rotation = 0 71 | 72 | for i in range(self.hardware.num_keys()): 73 | _key = Key(i, self.hardware) 74 | self.keys.append(_key) 75 | 76 | def update(self): 77 | # Call this in each iteration of your while loop to update 78 | # to update everything's state, e.g. `keybow.update()` 79 | 80 | for _key in self.keys: 81 | _key.update() 82 | 83 | # Used to work out the sleep behaviour, by keeping track 84 | # of the time of the last key press. 85 | if self.any_pressed(): 86 | self.time_of_last_press = time.monotonic() 87 | self.sleeping = False 88 | 89 | self.time_since_last_press = time.monotonic() - self.time_of_last_press 90 | 91 | # If LED sleep is enabled, but not engaged, check if enough time 92 | # has elapsed to engage sleep. If engaged, record the state of the 93 | # LEDs, so it can be restored on wake. 94 | if self.led_sleep_enabled and not self.sleeping: 95 | if time.monotonic() - self.time_of_last_press > self.led_sleep_time: 96 | self.sleeping = True 97 | self.last_led_states = [k.rgb if k.lit else [0, 0, 0] for k in self.keys] 98 | self.set_all(0, 0, 0) 99 | self.was_asleep = True 100 | 101 | # If it was sleeping, but is no longer, then restore LED states. 102 | if not self.sleeping and self.was_asleep: 103 | for k in range(len(self.keys)): 104 | self.keys[k].set_led(*self.last_led_states[k]) 105 | self.was_asleep = False 106 | 107 | def set_led(self, number, r, g, b): 108 | # Set an individual key's LED to an RGB value by its number. 109 | 110 | self.keys[number].set_led(r, g, b) 111 | 112 | def set_all(self, r, g, b): 113 | # Set all of Keybow's LEDs to an RGB value. 114 | 115 | if not self.sleeping: 116 | for _key in self.keys: 117 | _key.set_led(r, g, b) 118 | else: 119 | for _key in self.keys: 120 | _key.led_off() 121 | 122 | def get_states(self): 123 | # Returns a Boolean list of Keybow's key states 124 | # (0=not pressed, 1=pressed). 125 | 126 | _states = [_key.state for _key in self.keys] 127 | return _states 128 | 129 | def get_pressed(self): 130 | # Returns a list of key numbers currently pressed. 131 | 132 | _pressed = [_key.number for _key in self.keys if _key.state == True] 133 | return _pressed 134 | 135 | def any_pressed(self): 136 | # Returns True if any key is pressed, False if none are pressed. 137 | 138 | if any(self.get_states()): 139 | return True 140 | else: 141 | return False 142 | 143 | def none_pressed(self): 144 | # Returns True if none of the keys are pressed, False is any key 145 | # is pressed. 146 | 147 | if not any(self.get_states()): 148 | return True 149 | else: 150 | return False 151 | 152 | def on_press(self, _key, handler=None): 153 | # Attaches a press function to a key, via a decorator. This is stored as 154 | # `key.press_function` in the key's attributes, and run if necessary 155 | # as part of the key's update function (and hence Keybow's update 156 | # function). It can be attached as follows: 157 | 158 | # @keybow.on_press(key) 159 | # def press_handler(key, pressed): 160 | # if pressed: 161 | # do something 162 | # else: 163 | # do something else 164 | 165 | if _key is None: 166 | return 167 | 168 | def attach_handler(handler): 169 | _key.press_function = handler 170 | 171 | if handler is not None: 172 | attach_handler(handler) 173 | else: 174 | return attach_handler 175 | 176 | def on_release(self, _key, handler=None): 177 | # Attaches a release function to a key, via a decorator. This is stored 178 | # as `key.release_function` in the key's attributes, and run if 179 | # necessary as part of the key's update function (and hence Keybow's 180 | # update function). It can be attached as follows: 181 | 182 | # @keybow.on_release(key) 183 | # def release_handler(key): 184 | # do something 185 | 186 | if _key is None: 187 | return 188 | 189 | def attach_handler(handler): 190 | _key.release_function = handler 191 | 192 | if handler is not None: 193 | attach_handler(handler) 194 | else: 195 | return attach_handler 196 | 197 | def on_hold(self, _key, handler=None): 198 | # Attaches a hold unction to a key, via a decorator. This is stored as 199 | # `key.hold_function` in the key's attributes, and run if necessary 200 | # as part of the key's update function (and hence Keybow's update 201 | # function). It can be attached as follows: 202 | 203 | # @keybow.on_hold(key) 204 | # def hold_handler(key): 205 | # do something 206 | 207 | if _key is None: 208 | return 209 | 210 | def attach_handler(handler): 211 | _key.hold_function = handler 212 | 213 | if handler is not None: 214 | attach_handler(handler) 215 | else: 216 | return attach_handler 217 | 218 | def rotate(self, degrees): 219 | # Rotates all of Keybow's keys by a number of degrees, clamped to 220 | # the closest multiple of 90 degrees. Because it shuffles the order 221 | # of the Key instances, all of the associated attributes of the key 222 | # are retained. The x/y coordinate of the keys are rotated also. It 223 | # also handles negative degrees, e.g. -90 to rotate 90 degrees anti- 224 | # clockwise. 225 | 226 | # Rotate as follows: `keybow.rotate(270)` 227 | 228 | old_rotation = self.rotation 229 | self.rotation += degrees 230 | num_rotations = (round(self.rotation / 90) - round(old_rotation / 90)) % 4 231 | 232 | if num_rotations == 0: 233 | return 234 | 235 | matrix = [[(x * 4) + y for y in range(4)] for x in range(4)] 236 | 237 | for r in range(num_rotations): 238 | matrix = zip(*matrix[::-1]) 239 | matrix = [list(x) for x in list(matrix)] 240 | 241 | flat_matrix = [x for y in matrix for x in y] 242 | 243 | for i in range(len(self.keys)): 244 | self.keys[i].number = flat_matrix[i] 245 | self.keys[i].update_xy() 246 | 247 | self.keys = sorted(self.keys, key=lambda x:x.number) 248 | 249 | 250 | class Key: 251 | """ 252 | Represents a key on Keybow 2040, with associated switch and 253 | LED behaviours. 254 | 255 | :param number: the key number (0-15) to associate with the key 256 | :param hardware: object representing a board hardware 257 | """ 258 | def __init__(self, number, hardware): 259 | self.hardware = hardware 260 | self.number = number 261 | self.hw_number = number 262 | self.state = 0 263 | self.pressed = 0 264 | self.last_state = None 265 | self.time_of_last_press = time.monotonic() 266 | self.time_since_last_press = None 267 | self.time_held_for = 0 268 | self.held = False 269 | self.hold_time = 0.75 270 | self.modifier = False 271 | self.rgb = [0, 0, 0] 272 | self.lit = False 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 | self.update_xy() 282 | 283 | def get_state(self): 284 | # Returns the state of the key (0=not pressed, 1=pressed). 285 | 286 | return int(self.hardware.switch_state(self.hw_number)) 287 | 288 | def update(self): 289 | # Updates the state of the key and updates all of its 290 | # attributes. 291 | 292 | self.time_since_last_press = time.monotonic() - self.time_of_last_press 293 | 294 | # Keys get locked during the debounce time. 295 | if self.time_since_last_press < self.debounce: 296 | self.key_locked = True 297 | else: 298 | self.key_locked = False 299 | 300 | self.state = self.get_state() 301 | self.pressed = self.state 302 | update_time = time.monotonic() 303 | 304 | # If there's a `press_function` attached, then call it, 305 | # returning the key object and the pressed state. 306 | if self.press_function is not None and self.pressed and not self.press_func_fired and not self.key_locked: 307 | self.press_function(self) 308 | self.press_func_fired = True 309 | # time.sleep(0.05) # A little debounce 310 | 311 | # If the key has been pressed and releases, then call 312 | # the `release_function`, if one is attached. 313 | if not self.pressed and self.last_state == True: 314 | if self.release_function is not None: 315 | self.release_function(self) 316 | self.last_state = False 317 | self.press_func_fired = False 318 | 319 | if not self.pressed: 320 | self.time_held_for = 0 321 | self.last_state = False 322 | 323 | # If the key has just been pressed, then record the 324 | # `time_of_last_press`, and update last_state. 325 | elif self.pressed and self.last_state == False: 326 | self.time_of_last_press = update_time 327 | self.last_state = True 328 | 329 | # If the key is pressed and held, then update the 330 | # `time_held_for` variable. 331 | elif self.pressed and self.last_state == True: 332 | self.time_held_for = update_time - self.time_of_last_press 333 | self.last_state = True 334 | 335 | # If the `hold_time` theshold is crossed, then call the 336 | # `hold_function` if one is attached. The `hold_func_fired` 337 | # ensures that the function is only called once. 338 | if self.time_held_for > self.hold_time: 339 | self.held = True 340 | if self.hold_function is not None and not self.hold_func_fired: 341 | self.hold_function(self) 342 | self.hold_func_fired = True 343 | else: 344 | self.held = False 345 | self.hold_func_fired = False 346 | 347 | def update_xy(self): 348 | self.xy = self.get_xy() 349 | self.x, self.y = self.xy 350 | 351 | def get_xy(self): 352 | # Returns the x/y coordinate of a key from 0,0 to 3,3. 353 | 354 | return number_to_xy(self.number) 355 | 356 | def get_number(self): 357 | # Returns the key number, from 0 to 15. 358 | 359 | return self.number 360 | 361 | def is_modifier(self): 362 | # Designates a modifier key, so you can hold the modifier 363 | # and tap another key to trigger additional behaviours. 364 | 365 | if self.modifier: 366 | return True 367 | else: 368 | return False 369 | 370 | def set_led(self, r, g, b): 371 | # Set this key's LED to an RGB value. 372 | 373 | if [r, g, b] == [0, 0, 0]: 374 | self.lit = False 375 | else: 376 | self.lit = True 377 | self.rgb = [r, g, b] 378 | 379 | self.hardware.set_pixel(self.hw_number, r, g, b) 380 | 381 | def led_on(self): 382 | # Turn the LED on, using its current RGB value. 383 | 384 | r, g, b = self.rgb 385 | self.set_led(r, g, b) 386 | 387 | def led_off(self): 388 | # Turn the LED off. 389 | 390 | self.set_led(0, 0, 0) 391 | 392 | def led_state(self, state): 393 | # Set the LED's state (0=off, 1=on) 394 | 395 | state = int(state) 396 | 397 | if state == 0: 398 | self.led_off() 399 | elif state == 1: 400 | self.led_on() 401 | else: 402 | return 403 | 404 | def toggle_led(self, rgb=None): 405 | # Toggle the LED's state, retaining its RGB value for when it's toggled 406 | # back on. Can also be passed an RGB tuple to set the colour as part of 407 | # the toggle. 408 | 409 | if rgb is not None: 410 | self.rgb = rgb 411 | if self.lit: 412 | self.led_off() 413 | else: 414 | self.led_on() 415 | 416 | def __str__(self): 417 | # When printed, show the key's state (0 or 1). 418 | return self.state 419 | 420 | def xy_to_number(x, y): 421 | # Convert an x/y coordinate to key number. 422 | return x + (y * 4) 423 | 424 | def number_to_xy(number): 425 | # Convert a number to an x/y coordinate. 426 | x = number % 4 427 | y = number // 4 428 | 429 | return (x, y) 430 | 431 | def hsv_to_rgb(h, s, v): 432 | # Convert an HSV (0.0-1.0) colour to RGB (0-255) 433 | if s == 0.0: 434 | rgb = [v, v, v] 435 | 436 | i = int(h * 6.0) 437 | 438 | f = (h*6.)-i; p,q,t = v*(1.-s), v*(1.-s*f), v*(1.-s*(1.-f)); i%=6 439 | 440 | if i == 0: 441 | rgb = [v, t, p] 442 | if i == 1: 443 | rgb = [q, v, p] 444 | if i == 2: 445 | rgb = [p, v, t] 446 | if i == 3: 447 | rgb = [p, q, v] 448 | if i == 4: 449 | rgb = [t, p, v] 450 | if i == 5: 451 | rgb = [v, p, q] 452 | 453 | rgb = tuple(int(c * 255) for c in rgb) 454 | 455 | return rgb 456 | -------------------------------------------------------------------------------- /lib/pmk/platform/__init__.py: -------------------------------------------------------------------------------- 1 | class PMK: 2 | """ 3 | Abstract class providing common interface to RGB-backlit keyboard 4 | Subclasses should fill _switches and _display properties. 5 | Filling _i2c is optional, unless you want to use i2c() accessor. 6 | """ 7 | 8 | def set_pixel(self, idx, r, g, b): 9 | self._display.set_pixel(idx, r, g, b) 10 | 11 | def num_keys(self): 12 | return self._switches.num_switches() 13 | 14 | def switch_state(self, idx): 15 | return self._switches.switch_state(idx) 16 | 17 | def i2c(self): 18 | return self._i2c 19 | -------------------------------------------------------------------------------- /lib/pmk/platform/display/__init__.py: -------------------------------------------------------------------------------- 1 | class Display: 2 | """ 3 | Abstract class providing common interface to the set of pixels 4 | """ 5 | def set_pixel(self, idx, r, g, b): 6 | raise NotImplementedError 7 | -------------------------------------------------------------------------------- /lib/pmk/platform/display/dotstar.py: -------------------------------------------------------------------------------- 1 | import adafruit_dotstar 2 | 3 | from . import Display 4 | 5 | class Dotstar(Display): 6 | """ 7 | Display consisting of dotstars 8 | """ 9 | def __init__(self, clock, data, count): 10 | self._pixels = adafruit_dotstar.DotStar(clock, data, count) 11 | 12 | def set_pixel(self, idx, r, g, b): 13 | self._pixels[idx] = (r, g, b) 14 | -------------------------------------------------------------------------------- /lib/pmk/platform/display/keybow2040.py: -------------------------------------------------------------------------------- 1 | from adafruit_is31fl3731.keybow2040 import Keybow2040 as Pixels 2 | 3 | from . import Display 4 | 5 | class Keybow2040(Display): 6 | """ 7 | Keybow 2040 4x4 display 8 | """ 9 | def __init__(self, i2c): 10 | self._pixels = Pixels(i2c) 11 | 12 | def set_pixel(self, idx, r, g, b): 13 | self._pixels.pixelrgb(idx % 4, idx // 4, r, g, b) 14 | -------------------------------------------------------------------------------- /lib/pmk/platform/keybow2040.py: -------------------------------------------------------------------------------- 1 | import board 2 | 3 | from .switches.gpio import GPIO as Switches 4 | from .display.keybow2040 import Keybow2040 as Display 5 | 6 | from . import PMK 7 | 8 | # These are the 16 switches on Keybow, with their board-defined names. 9 | _PINS = [board.SW0, 10 | board.SW1, 11 | board.SW2, 12 | board.SW3, 13 | board.SW4, 14 | board.SW5, 15 | board.SW6, 16 | board.SW7, 17 | board.SW8, 18 | board.SW9, 19 | board.SW10, 20 | board.SW11, 21 | board.SW12, 22 | board.SW13, 23 | board.SW14, 24 | board.SW15] 25 | 26 | class Keybow2040(PMK): 27 | def __init__(self): 28 | self._i2c = board.I2C() 29 | self._switches = Switches(_PINS) 30 | self._display = Display(self._i2c) 31 | -------------------------------------------------------------------------------- /lib/pmk/platform/rgbkeypadbase.py: -------------------------------------------------------------------------------- 1 | import board 2 | import busio 3 | from digitalio import DigitalInOut, Direction 4 | 5 | from .switches.tca9555 import TCA9555 as Switches 6 | from .display.dotstar import Dotstar as Display 7 | 8 | from . import PMK 9 | 10 | NUM_KEYS = 16 11 | 12 | # Let's match Keybow2040 orientation 13 | _ROTATED = { 14 | 0: 12, 1: 8, 2: 4, 3: 0, 15 | 4: 13, 5: 9, 6: 5, 7: 1, 16 | 8: 14, 9: 10, 10: 6, 11: 2, 17 | 12: 15, 13: 11, 14: 7, 15: 3, 18 | } 19 | 20 | class RGBKeypadBase(PMK): 21 | def __init__(self): 22 | self._i2c = busio.I2C(board.GP5, board.GP4) 23 | self._switches = Switches(self._i2c, NUM_KEYS) 24 | self._display = Display(board.GP18, board.GP19, NUM_KEYS) 25 | self._cs = DigitalInOut(board.GP17) 26 | self._cs.direction = Direction.OUTPUT 27 | self._cs.value = 1 28 | 29 | def set_pixel(self, idx, r, g, b): 30 | # https://github.com/pimoroni/pimoroni-pico/blob/main/libraries/pico_rgb_keypad/pico_rgb_keypad.cpp#L20-L45 31 | # code above sets CS only for the time of updating LEDs, so let's do the same 32 | self._cs.value = 0 33 | super().set_pixel(_ROTATED[idx], r, g, b) 34 | self._cs.value = 1 35 | 36 | def switch_state(self, idx): 37 | return super().switch_state(_ROTATED[idx]) 38 | -------------------------------------------------------------------------------- /lib/pmk/platform/switches/__init__.py: -------------------------------------------------------------------------------- 1 | class Switches: 2 | """ 3 | Abstract class providing common interface to the set of switches 4 | """ 5 | def num_switches(self): 6 | raise NotImplementedError 7 | 8 | def switch_state(self, idx): 9 | raise NotImplementedError 10 | -------------------------------------------------------------------------------- /lib/pmk/platform/switches/gpio.py: -------------------------------------------------------------------------------- 1 | from digitalio import DigitalInOut, Direction, Pull 2 | 3 | from . import Switches 4 | 5 | class GPIO(Switches): 6 | """ 7 | Switches connected directly to GPIO 8 | """ 9 | def __init__(self, pins): 10 | self._switches = [DigitalInOut(pin) for pin in pins] 11 | for switch in self._switches: 12 | switch.direction = Direction.INPUT 13 | switch.pull = Pull.UP 14 | 15 | def num_switches(self): 16 | return len(self._switches) 17 | 18 | def switch_state(self, idx): 19 | return not self._switches[idx].value 20 | -------------------------------------------------------------------------------- /lib/pmk/platform/switches/tca9555.py: -------------------------------------------------------------------------------- 1 | from . import Switches 2 | 3 | class TCA9555(Switches): 4 | """ 5 | Switches connected via TCA9555 IO expander on i2c 6 | """ 7 | def __init__(self, i2c, count): 8 | self._count = count 9 | self._i2c = i2c 10 | 11 | def num_switches(self): 12 | return self._count 13 | 14 | def switch_state(self, idx): 15 | buffer = bytearray(self._count // 8) 16 | buffer[0] = 0x0 17 | while not self._i2c.try_lock(): 18 | pass 19 | self._i2c.writeto_then_readfrom(0x20, buffer, buffer, out_end=1) 20 | self._i2c.unlock() 21 | b = buffer[0] | buffer[1] << 8 # up to 16 buttons supported now 22 | return not (1 << idx) & b 23 | --------------------------------------------------------------------------------