Text Editor
Adafruit recommends using the Mu editor for using your CircuitPython code with the Feather. You can get more info in this guide.
Alternatively, you can use any text editor that saves files.
CircuitPython Code
Copy the code below and paste it into Mu. Then, save it to your Feather RP2040's CIRCUITPY drive as code.py.
You'll need to download the .zip file in the GitHub link below and decompress the file to get all of the touch_deck_icons directory that's full of sample icons, as well as the touch_deck_layers.py file. Copy both of these to your Feather's CIRCUITPY drive.
# SPDX-FileCopyrightText: 2020 Tim C, written for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ This version runs on Feather RP2040 with a 3.5" FeatherWing """ import time import displayio import terminalio from adafruit_display_text import bitmap_label from adafruit_displayio_layout.layouts.grid_layout import GridLayout from adafruit_displayio_layout.widgets.icon_widget import IconWidget from adafruit_featherwing import tft_featherwing_35 import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS from adafruit_hid.consumer_control import ConsumerControl from touch_deck_layers import ( touch_deck_config, KEY, STRING, MEDIA, KEY_PRESS, KEY_RELEASE, CHANGE_LAYER, ) # seems to help the touchscreen not get stuck with chip not found time.sleep(3) # display and touchscreen initialization displayio.release_displays() tft_featherwing = tft_featherwing_35.TFTFeatherWing35() display = tft_featherwing.display touchscreen = tft_featherwing.touchscreen # HID setup kbd = Keyboard(usb_hid.devices) cc = ConsumerControl(usb_hid.devices) kbd_layout = KeyboardLayoutUS(kbd) # variables to enforce timout between icon presses COOLDOWN_TIME = 0.5 LAST_PRESS_TIME = -1 # 'mock' icon indexes for the layer buttons # used for debouncing PREV_LAYER_INDEX = -1 NEXT_LAYER_INDEX = -2 HOME_LAYER_INDEX = -3 # start on first layer current_layer = 0 # Make the main_group to hold everything main_group = displayio.Group() display.show(main_group) # loading screen loading_group = displayio.Group() # black background, screen size minus side buttons loading_background = displayio.Bitmap( (display.width - 40) // 20, display.height // 20, 1 ) loading_palette = displayio.Palette(1) loading_palette[0] = 0x0 # scaled group to match screen size minus side buttons loading_background_scale_group = displayio.Group(scale=20) loading_background_tilegrid = displayio.TileGrid( loading_background, pixel_shader=loading_palette ) loading_background_scale_group.append(loading_background_tilegrid) # loading screen label loading_label = bitmap_label.Label(terminalio.FONT, text="Loading...", scale=3) loading_label.anchor_point = (0.5, 0.5) loading_label.anchored_position = (display.width // 2, display.height // 2) # append background and label to the group loading_group.append(loading_background_scale_group) loading_group.append(loading_label) # GridLayout to hold the icons # size and location can be adjusted to fit # different sized screens. layout = GridLayout( x=20, y=20, width=420, height=290, grid_size=(4, 3), cell_padding=6, ) # list that holds the IconWidget objects for each icon. _icons = [] # list that holds indexes of currently pressed icons and layer buttons # used for debouncing _pressed_icons = [] # layer label at the top of the screen layer_label = bitmap_label.Label(terminalio.FONT) layer_label.anchor_point = (0.5, 0.0) layer_label.anchored_position = (display.width // 2, 4) main_group.append(layer_label) # right side layer buttons next_layer_btn = IconWidget("", "touch_deck_icons/layer_next.bmp", on_disk=True) next_layer_btn.x = display.width - 40 next_layer_btn.y = display.height - 100 next_layer_btn.resize = (40, 100) main_group.append(next_layer_btn) prev_layer_btn = IconWidget("", "touch_deck_icons/layer_prev.bmp", on_disk=True) prev_layer_btn.x = display.width - 40 prev_layer_btn.y = 110 prev_layer_btn.resize = (40, 100) main_group.append(prev_layer_btn) home_layer_btn = IconWidget("", "touch_deck_icons/layer_home.bmp", on_disk=True) home_layer_btn.x = display.width - 40 home_layer_btn.y = 0 home_layer_btn.resize = (40, 100) main_group.append(home_layer_btn) # helper method to laod icons for an index by its index in the # list of layers def load_layer(layer_index): # show the loading screen main_group.append(loading_group) time.sleep(0.05) # resets icon lists to empty global _icons _icons = [] layout._cell_content_list = [] # remove previous layer icons from the layout while len(layout) > 0: layout.pop() # set the layer labed at the top of the screen layer_label.text = touch_deck_config["layers"][layer_index]["name"] # loop over each shortcut and it's index for i, shortcut in enumerate(touch_deck_config["layers"][layer_index]["shortcuts"]): # create an icon for the current shortcut _new_icon = IconWidget(shortcut["label"], shortcut["icon"], on_disk=True) # add it to the list of icons _icons.append(_new_icon) # add it to the grid layout # calculate it's position from the index layout.add_content(_new_icon, grid_position=(i % 4, i // 4), cell_size=(1, 1)) # hide the loading screen time.sleep(0.05) main_group.pop() # append the grid layout to the main_group # so it gets shown on the display main_group.append(layout) # load the first layer to start load_layer(current_layer) # main loop while True: if touchscreen.touched: # loop over all data in touchscreen buffer while not touchscreen.buffer_empty: touches = touchscreen.touches # loop over all points touched for point in touches: if point: # current time, used for timeout between icon presses _now = time.monotonic() # if the timeout has passed if _now - LAST_PRESS_TIME > COOLDOWN_TIME: # print(point) # map the observed minimum and maximum touch values # to the screen size y = point["y"] - 250 x = 4096 - point["x"] - 250 y = y * display.width // (3820 - 250) x = x * display.height // (3820 - 250) # touch data is 90 degrees rotated # flip x, and y here to account for that p = (y, x) # print(p) # Next layer button pressed if ( next_layer_btn.contains(p) and NEXT_LAYER_INDEX not in _pressed_icons ): # increment layer current_layer += 1 # wrap back to beginning from end if current_layer >= len(touch_deck_config["layers"]): current_layer = 0 # load the new layer load_layer(current_layer) # save current time to check for timeout LAST_PRESS_TIME = _now # append this index to pressed icons for debouncing _pressed_icons.append(NEXT_LAYER_INDEX) # home layer button pressed if ( home_layer_btn.contains(p) and HOME_LAYER_INDEX not in _pressed_icons ): # 0 index is home layer current_layer = 0 # load the home layer load_layer(current_layer) # save current time to check for timeout LAST_PRESS_TIME = _now # append this index to pressed icons for debouncing _pressed_icons.append(HOME_LAYER_INDEX) # Previous layer button pressed if ( prev_layer_btn.contains(p) and PREV_LAYER_INDEX not in _pressed_icons ): # decrement layer current_layer -= 1 # wrap back to end from beginning if current_layer < 0: current_layer = len(touch_deck_config["layers"]) - 1 # load the new layer load_layer(current_layer) # save current time to check for timeout LAST_PRESS_TIME = _now # append this index to pressed icons for debouncing _pressed_icons.append(PREV_LAYER_INDEX) # loop over current layer icons and their indexes for index, icon_shortcut in enumerate(_icons): # if this icon was pressed if icon_shortcut.contains(p): # debounce logic, check that it wasn't already pressed if index not in _pressed_icons: # print("pressed {}".format(index)) # get actions for this icon from config object _cur_actions = touch_deck_config["layers"][ current_layer ]["shortcuts"][index]["actions"] # tuple means it's a single action if isinstance(_cur_actions, tuple): # put it in a list by itself _cur_actions = [_cur_actions] # loop over the actions for _action in _cur_actions: # HID keyboard keys if _action[0] == KEY: kbd.press(*_action[1]) kbd.release(*_action[1]) # String to write from layout elif _action[0] == STRING: kbd_layout.write(_action[1]) # Consumer control code elif _action[0] == MEDIA: cc.send(_action[1]) # Key press elif _action[0] == KEY_PRESS: kbd.press(*_action[1]) # Key release elif _action[0] == KEY_RELEASE: kbd.release(*_action[1]) # Change Layer elif _action[0] == CHANGE_LAYER: if isinstance( _action[1], int ) and 0 <= _action[1] < len( touch_deck_config["layers"] ): current_layer = _action[1] load_layer(_action[1]) # if there are multiple actions if len(_cur_actions) > 1: # small sleep to make sure # OS can respond to previous action time.sleep(0.2) # save current time to check for timeout LAST_PRESS_TIME = _now # append this index to pressed icons for debouncing _pressed_icons.append(index) else: # screen not touched # empty the pressed icons list _pressed_icons.clear()
Once you've pasted the project files onto your device, it will automatically reboot and begin running the touch deck app using the build-in example layers. Open up a text editor and click into it to give it focus. Press the "Play" icon at the top left of the screen. If everything working properly you should see "k" get typed into the text editor. If the touch deck app does not launch for you try opening the Mu Serial Console and check to see if there are any errors printed in there.
Hardware Driver
This project is made to work with the adafruit_featherwing
library contains a helper class that we use to initialize the touch overlay and display. Once we've done that, we're ready to start drawing things on the screen and reacting to user touches.
Graphical Interface
There are two main components used in the interface: GridLayout and IconWidget. Both of them are in the adafruit_displayio_layout
library. The IconWidget component contains an image, and optionally some text beneath it. All of the shortcut icons in the touch deck are IconWidget objects. The layer buttons on the right side are also IconWidgets, with a differently shaped image and no text. GridLayout is the widget we use to distribute our IconWidgets into a 4x3 grid. It does all the mathematical heavy lifting to size and position all of the widgets that we add to it.
Shortcut Sending
Whenever you press one of the touch deck icons, the code uses the adafruit_hid
library to send the keys, consumer codes, or strings specified by your layers configuration file.
Configuration
The code reads a configuration dictionary object from the file touch_deck_layers.py
. Within this file, you can define the layers of icons that you'd like to use, and set the keys and other shortcuts they'll send when you touch them.
Customizing the Layers File
Your layers file is where you define what icons you want to use and what actions they should do when you press them. These are grouped into lists known as layers. Each layer can hold up to 12 icons. Each layer contains a name
that will get shown at the top of the screen.
Each item in the layer has three properties: label
, icon
, and actions
. The icon
is a string filepath pointing to the bmp image that should be shown. All of the icons in this project are in the touch_deck_icons/
directory. The label
property is the text that will be shown under the icon image. Lastly, the actions
are the things to do when you press on the icon -- keys to press, or strings to write.
There are five types of actions that can be used on an icon. Each icon can have a single action, or a list of actions that will get run in sequence.
KEY
This will press and release a single key. It can also contain modifier keys that will be held down while the selected key is pressed.
{ "label": "Play", "icon": "touch_deck_icons/pr_play.bmp", "actions": (KEY, [Keycode.K]), }, { "label": "Fast", "icon": "touch_deck_icons/pr_fast.bmp", "actions": (KEY, [Keycode.RIGHT_SHIFT, Keycode.PERIOD]), }
In the example above, there is a "Play" shortcut that presses the "k" key, and a "Fast" shortcut that holds shift while pressing the period key, which would make ">" -- which is the fast forward shortcut for YouTube.
MEDIA
This action type will send the specified consumer control codes.
{ "label": "Vol +", "icon": "touch_deck_icons/pr_volup.bmp", "actions": (MEDIA, ConsumerControlCode.VOLUME_INCREMENT), },
This shortcut will press the volume up key.
STRING
The STRING action type will write a string using the US Keyboard Layout. You can use this to type words.
{ "label": "Blinka", "icon": "touch_deck_icons/af_blinka.bmp", "actions": (STRING, ":blinka:"), }
This shortcut will type the string ":blinka:" which gets interpreted by Discord as an emoji and gets replaced by a Blinka icon on the Adafruit Discord server.
KEY_PRESS
and KEY_RELEASE
The final 2 action types are meant to be used together. These allow you to hold a key down while pressing several other keys. These are helpful for PC shortcuts like "hold alt and type 1234". KEY_PRESS
will press and hold a key down. Then you can do some other actions. And finally KEY_RELEASE
will release the pressed key. KEY_RELEASE
will release the pressed key.
{ "label": "Test (L)", "icon": "touch_deck_icons/test48_icon.bmp", "actions": [ (KEY_PRESS, [Keycode.SHIFT]), (KEY, [Keycode.B]), (KEY, [Keycode.L]), (KEY, [Keycode.I]), (KEY, [Keycode.N]), (KEY, [Keycode.K]), (KEY, [Keycode.A]), (KEY_RELEASE, [Keycode.SHIFT]) ] },
This example holds the shift key down, and then presses the B, L, I, N, K, A keys in succession.
# SPDX-FileCopyrightText: 2021 foamyguy for Adafruit Industries # # SPDX-License-Identifier: MIT from adafruit_hid.keycode import Keycode from adafruit_hid.consumer_control_code import ConsumerControlCode MEDIA = 1 KEY = 2 STRING = 3 KEY_PRESS = 4 KEY_RELEASE = 5 CHANGE_LAYER = 6 touch_deck_config = { "layers": [ { "name": "Youtube Controls", "shortcuts": [ { "label": "Play", "icon": "touch_deck_icons/pr_play.bmp", "actions": (KEY, [Keycode.K]), }, { "label": "Pause", "icon": "touch_deck_icons/pr_pause.bmp", "actions": (KEY, [Keycode.K]), }, { "label": "Rewind", "icon": "touch_deck_icons/pr_rewind.bmp", "actions": (KEY, [Keycode.LEFT_ARROW]), }, { "label": "FastForward", "icon": "touch_deck_icons/pr_ffwd.bmp", "actions": (KEY, [Keycode.RIGHT_ARROW]), }, { "label": "Previous", "icon": "touch_deck_icons/pr_previous.bmp", "actions": (KEY, [Keycode.RIGHT_SHIFT, Keycode.P]), }, { "label": "Next", "icon": "touch_deck_icons/pr_next.bmp", "actions": (KEY, [Keycode.RIGHT_SHIFT, Keycode.N]), }, { "label": "Vol -", "icon": "touch_deck_icons/pr_voldown.bmp", "actions": (MEDIA, ConsumerControlCode.VOLUME_DECREMENT), }, { "label": "Vol +", "icon": "touch_deck_icons/pr_volup.bmp", "actions": (MEDIA, ConsumerControlCode.VOLUME_INCREMENT), }, { "label": "Fullscreen", "icon": "touch_deck_icons/pr_fullscreen.bmp", "actions": (KEY, [Keycode.F]), }, { "label": "Slow", "icon": "touch_deck_icons/pr_slow.bmp", "actions": (KEY, [Keycode.RIGHT_SHIFT, Keycode.COMMA]), }, { "label": "Fast", "icon": "touch_deck_icons/pr_fast.bmp", "actions": (KEY, [Keycode.RIGHT_SHIFT, Keycode.PERIOD]), }, { "label": "Mute", "icon": "touch_deck_icons/pr_mute.bmp", "actions": (KEY, [Keycode.M]), }, ], }, { "name": "Discord", "shortcuts": [ { "label": "Blinka", "icon": "touch_deck_icons/af_blinka.bmp", "actions": (STRING, ":blinka:"), }, { "label": "Adabot", "icon": "touch_deck_icons/af_adabot.bmp", "actions": (STRING, ":adabot:"), }, { "label": "Billie", "icon": "touch_deck_icons/af_billie.bmp", "actions": (STRING, ":billie:"), }, { "label": "Cappy", "icon": "touch_deck_icons/af_cappy.bmp", "actions": (STRING, ":cappy:"), }, { "label": "Connie", "icon": "touch_deck_icons/af_connie.bmp", "actions": (STRING, ":connie:"), }, { "label": "Gus", "icon": "touch_deck_icons/af_gus.bmp", "actions": (STRING, ":gus:"), }, { "label": "Hans", "icon": "touch_deck_icons/af_hans.bmp", "actions": (STRING, ":hans:"), }, { "label": "Mho", "icon": "touch_deck_icons/af_mho.bmp", "actions": (STRING, ":mho:"), }, { "label": "Minerva", "icon": "touch_deck_icons/af_minerva.bmp", "actions": (STRING, ":minerva:"), }, { "label": "NeoTrellis", "icon": "touch_deck_icons/af_neotrellis.bmp", "actions": (STRING, ":neotrellis:"), }, { "label": "Ruby", "icon": "touch_deck_icons/af_ruby.bmp", "actions": (STRING, ":ruby:"), }, { "label": "Sparky", "icon": "touch_deck_icons/af_sparky.bmp", "actions": (STRING, ":sparky:"), }, ], }, { "name": "Symbols", "shortcuts": [ { "label": "Infinity", # ∞ "icon": "touch_deck_icons/sy_infinity.bmp", "actions": (KEY, [Keycode.ALT, Keycode.FIVE]), }, { "label": "Degree", # º "icon": "touch_deck_icons/sy_degree.bmp", "actions": (KEY, [Keycode.ALT, Keycode.ZERO]), }, { "label": "Pi", # π "icon": "touch_deck_icons/sy_pi.bmp", "actions": (KEY, [Keycode.ALT, Keycode.P]), }, { "label": "Sigma", # ∑ "icon": "touch_deck_icons/sy_sigma.bmp", "actions": (KEY, [Keycode.ALT, Keycode.W]), }, { "label": "Partial diff", # "icon": "touch_deck_icons/sy_pdiff.bmp", "actions": (KEY, [Keycode.ALT, Keycode.D]), }, { "label": "Increment", # ∆ "icon": "touch_deck_icons/sy_increment.bmp", "actions": (KEY, [Keycode.ALT, Keycode.J]), }, { "label": "Omega", # Ω "icon": "touch_deck_icons/sy_omega.bmp", "actions": (KEY, [Keycode.ALT, Keycode.Z]), }, { "label": "Mu", # µ "icon": "touch_deck_icons/sy_micro.bmp", "actions": (KEY, [Keycode.ALT, Keycode.M]), }, { "label": "Rad O", # Ø "icon": "touch_deck_icons/sy_rado.bmp", "actions": (KEY, [Keycode.ALT, Keycode.SHIFT, Keycode.O]), }, { "label": "Square root", # √ "icon": "touch_deck_icons/sy_sqrrt.bmp", "actions": (KEY, [Keycode.ALT, Keycode.V]), }, { "label": "Approx", # ≈ "icon": "touch_deck_icons/sy_approx.bmp", "actions": (KEY, [Keycode.ALT, Keycode.X]), }, { "label": "Plus minus", # ± "icon": "touch_deck_icons/sy_plusminus.bmp", "actions": (KEY, [Keycode.ALT, Keycode.SHIFT, Keycode.EQUALS]), }, ], }, ] }