[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/695/original/hero.jpeg?1742088004","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eOverview\u003c/h1\u003e\n\u003cp\u003eOne of the coolest things about\u0026nbsp;\u003ca href=\"https://www.solder.party/\"\u003eSolder Party's\u003c/a\u003e \u003ca href=\"https://www.solder.party/docs/keyboard-featherwing/\"\u003eKeyboard Featherwing\u003c/a\u003e is its versatility. While it offers a fully-featured hardware package all on its own, the ability to pair it with any Feather board allows it to be extended to do pretty incredible things. In this project, I'm pairing it with a Feather nRF52840 Express to create a universal Bluetooth Low Energy (BLE) keyboard/mouse/remote control, programmed in CircuitPython.\u003c/p\u003e\n\u003cp\u003eUsing the nRF52840, CircuitPython has the ability to send BLE keyboard, mouse, and consumer control HID events to control a host device, such as a computer. This firmware combines the ability to send all 3 event types from a single device in an easily configurable way, adding another layer of versatility!\u003c/p\u003e\n\u003cp\u003eSome possible applications:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eControl a laptop connected to a TV remotely, with physical keyboard and full mouse support\u003c/li\u003e\n\u003cli\u003eControl an Apple TV, including navigation. Use a physical keyboard when searching!\u003c/li\u003e\n\u003cli\u003eRemotely trigger the camera shutter on iPhone/iPad\u003c/li\u003e\n\u003cli\u003eUse the 9 programmable buttons to make a BLE \"macro pad\"\u003c/li\u003e\n\u003cli\u003ePractically anything that could benefit from a BLE keyboard, mouse, and/or consumer control remote\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAnother nice thing about the nRF52840 for this application is its excellent energy efficiency. When using a 2000mAh LiPo battery, I've been able to get between 70-80 hours of active use on a single charge! The Keyboard Featherwing's built-in on/off switch allows extending the battery life even further.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eFirmware Features\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eCompact physical keyboard makes it easy to type wirelessly on device like laptops/desktops, Raspberry Pis, or anything else that can connect to BLE keyboards.\u003c/li\u003e\n\u003cli\u003eTouch screen behaves as a trackpad/mouse when connected to a host device that supports pointer devices.\u003c/li\u003e\n\u003cli\u003e4 activity presets, selectable through an on-screen preferences menu. Further configuration is possible by connecting device to a computer via USB and editing the configuration files in the \u003cstrong\u003euser\u003c/strong\u003e directory.\n\u003cul\u003e\n\u003cli\u003eEach activity allows you to customize the 4 function buttons and the 5-way direction button (\u003cstrong\u003eD-BUTTON\u003c/strong\u003e) to send keyboard, consumer control, and mouse button events\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eAdjustable display/keyboard/NeoPixel brightness\u003c/li\u003e\n\u003cli\u003eAdjustable primary user interface color\u003c/li\u003e\n\u003cli\u003ePower-saving feature: auto-dim keyboard, screen, and NeoPixel after 30 seconds of inactivity, significantly increasing battery life.\u003c/li\u003e\n\u003cli\u003eBattery voltage monitoring and \"needs charge\" indicator.\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eHardware\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003ca href=\"https://www.solder.party/docs/keyboard-featherwing/rev2/\"\u003eKeyboard Featherwing Rev2\u003c/a\u003e by Solder Party. In theory the \u003ca href=\"https://www.solder.party/docs/keyboard-featherwing/rev1/\"\u003eRev1\u003c/a\u003e should work too, but I don't have a way to test it. If you do and are willing to, feel free to ping me on Bluesky (\u003ca href=\"https://bsky.app/profile/apendley.bsky.social\"\u003eapendley.bsky.social)\u003c/a\u003e or Discord (squid.jpg) and let me know if it works or if there are any issues (you can also\u0026nbsp;\u003ca href=\"https://github.com/apendley/keyboard-featherwing-ble-remote/issues\"\u003efile an issue\u003c/a\u003e on the project github).\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.adafruit.com/product/4062\"\u003eFeather nRF52840 Express\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://www.adafruit.com/product/2011\"\u003e3.7V 2000 mAh LiPo battery\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003eMicroSD card, any size\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003e3D-Printed Case\u003c/h1\u003e\n\u003cp\u003eMost of my Keyboard Featherwing projects use a 3D printed case, including this one. You can find links to the 3D files from my Doomsday Messenger article \u003ca href=\"https://adafruit-playground.com/u/squid_jpg/pages/keyboard-featherwing-messenger-powered-by-lvgl#3d-printing-3185197\"\u003ehere on Adafruit Playground\u003c/a\u003e.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/707/original/stand.jpeg?1742142321","metadata":{"caption":""}},{"element_type":"text","content":"\n        \u003ch1\u003eKnown Issues\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003eCurrently, as far as I'm aware, there is no way to force an unpairing from the remote (Keyboard Featherwing) side. You must force unpair/forget from the host side if you wish to pair the Keyboard Featherwing remote with another device.\u003c/li\u003e\n\u003cli\u003eLong startup time: it takes ~3 seconds before our program gains control from the boot loader, and another 5-7 seconds to load and initialize libraries.\u003c/li\u003e\n\u003cli\u003eSince there is no way to shut off power to the display, touch screen, and keyboard in deep sleep mode, the lowest power draw possible is around ~20 mA. Also, there seems to be a bug causing the inputs to cease being sent over the BLE connection after waking from deep sleep. As such, deep sleep is not implemented.\u003c/li\u003e\n\u003cli\u003eLimited amount of RAM on the nRF52840 puts a lot of pressure on the garbage collector, which can result in slightly hitchy mouse movement when using the touch screen as a trackpad/mouse.\u003c/li\u003e\n\u003cli\u003eThere seems to be an issue where the name we assign the device in CircuitPython doesn't always \"stick\". Sometimes it appears to other devices as something similar to \"CIRCUITPYc930\".\u003c/li\u003e\n\u003cli\u003eThe shift and alt keys on the Keyboard Featherwing's keyboard do not report shift and alt key presses to CircuitPython (this is by design). However, you can assign the shift, alt, or other inaccesible keys to one of the configurable buttons, and they will be sent as keyboard events.\u003c/li\u003e\n\u003c/ul\u003e\n      ","metadata":{}},{"element_type":"text","content":"\n  \n        \u003ch1\u003eInstallation\u003c/h1\u003e\n\u003cp\u003eFirst, you'll need to install CircuitPython onto your Feather. You can start by install the latest version of CircuitPython 9 according to the instructions\u0026nbsp;\u003ca href=\"https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather/circuitpython\"\u003ehere\u003c/a\u003e. If your Feather was purchased awhile ago, you may need to \u003ca href=\"https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather/update-bootloader\"\u003eupdate the bootloader\u003c/a\u003e before installing CircuitPython.\u003c/p\u003e\n\u003ch2\u003eFirmware source\u003c/h2\u003e\n\u003cp\u003eNext, you'll need to install the remote firmware source onto your Feather. There are two options: you can use the pre-compiled source, or the raw source. Using the pre-compiled \u003cstrong\u003e.mpy\u003c/strong\u003e files reduces the firmware startup time by as much as 1.5 seconds vs. uncompiled files. The tradeoff is that you can't tinker with the core code as easily if you'd like to do more advanced hacking of the firmware. If you just want to \u003cstrong\u003euse\u003c/strong\u003e the firmware, and aren't interested in the inner workings, this is probably the option you want.\u003c/p\u003e\n      \n","metadata":{}},{"element_type":"alert","content":"\u003cp\u003eNOTE: both options still allow for customization, since the files in the \u003cstrong\u003euser\u003c/strong\u003e folder are user-editable and are not pre-compiled.\u003c/p\u003e","metadata":{"class":"element alert-element build-alert alert-primary","markdown":"NOTE: both options still allow for customization, since the files in the **user** folder are user-editable and are not pre-compiled.","alert_type":"primary"}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n        \u003ch3\u003eTo install pre-compiled source:\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003eDelete all files on your Feather's CIRCUITPY drive.\u003c/li\u003e\n\u003cli\u003eIf you haven't done so already, download or clone the \u003ca href=\"https://github.com/apendley/keyboard-featherwing-ble-remote\"\u003egit repository\u003c/a\u003e for this firmware.\u003c/li\u003e\n\u003cli\u003eOnce downloaded, copy the contents of the downloaded \u003cstrong\u003eCOMPILED\u003c/strong\u003e directory onto your Feather's CIRCUITPY drive. Overwrite any existing files if necessary.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3\u003eTo install raw source\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003eDelete all files on your Feather's CIRCUITPY drive.\u003c/li\u003e\n\u003cli\u003eIf you haven't done so already, download or clone the\u0026nbsp;\u003ca href=\"https://github.com/apendley/keyboard-featherwing-ble-remote\"\u003egit repository\u003c/a\u003e for this firmware.\u003c/li\u003e\n\u003cli\u003eOnce downloaded, copy the contents of the downloaded \u003cstrong\u003eCIRCUITPY\u003c/strong\u003e directory onto your Feather's CIRCUITPY drive. Overwrite any existing files if necessary.\u003c/li\u003e\n\u003c/ol\u003e\n      \n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eUsage\u003c/h1\u003e\n\u003cp\u003eThe Keyboard Featherwing Remote uses \"\u003cstrong\u003eactivities\u003c/strong\u003e\" to assign commands/events to the 4 function buttons (labeled \u003cstrong\u003eL1\u003c/strong\u003e, \u003cstrong\u003eL2\u003c/strong\u003e, \u003cstrong\u003eR1\u003c/strong\u003e, and \u003cstrong\u003eR2\u003c/strong\u003e below) and the 5-way directional button (\u003cstrong\u003eD-BUTTON\u003c/strong\u003e). The main UI consists of the battery indicator and two sub-screens: the pairing/connecting screen, and the remote control screen.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/696/original/buttons-labeled.jpeg?1742088442","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eBattery indicator\u003c/h2\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThe Feather nRF52840 provides a voltage divider connected to a GPIO pin, allowing us to monitor the battery voltage from our program. The battery indicator is in the top-right corner of the screen. It displays the current battery voltage, and has 2 states: \"OK\" and \"Needs charging\". The \"OK\" state is indicated by the \"full battery\" icon, whereas the \"Needs charging\" state is indicated by the \"low battery\" icon. Additionally, in the \"Needs charging\" state, the battery icon and voltage text are colored red, and an exclamation point is appended to the voltage text.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/697/original/ss-mac.jpeg?1742088485","metadata":{"caption":"Note the battery voltage monitor in the top-left of the screen. Currently the reading 4.2 volts, and the \"full battery\" icon is displayed."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/701/original/ss-low-battery.jpeg?1742088811","metadata":{"caption":"When the voltage dips below 3.7 volts, the \"low battery\" icon is displayed, an exclamation point is appended to the voltage readout, and the icon and text are colored red."}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cblockquote\u003e\n\u003ch2\u003ePairing/Connecting\u003c/h2\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAfter booting, if the remote has been previously paired, it will immediately go to the remote control screen, ready to control the paired device. Otherwise, an animation of the Bluetooth logo will display and the NeoPixel will slowly blink blue. In this state, the remote is advertising that it is available to pair. The remote will remain in this state until a host device is paired.\u003c/p\u003e\n\u003cp\u003eOn the host device you would like to pair with, go to the bluetooth settings and look for a device named \"Keyboard Featherwing Remote\". Select it, and if prompted, pair with it.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"alert","content":"\u003cp\u003eSometimes the remote may appear with a name like \"CIRCUITPYc930\". I'm not sure why this happens, but selecting this device for pairing should still work.\u003c/p\u003e","metadata":{"class":"element alert-element build-alert alert-info","markdown":"Sometimes the remote may appear with a name like \"CIRCUITPYc930\". I'm not sure why this happens, but selecting this device for pairing should still work.","alert_type":"info"}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/699/original/ss-connecting.jpeg?1742088694","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eRemote Control\u003c/h2\u003e\n\u003cp\u003eThis is the main remote control screen. Once connected/paired to a host device, the NeoPixel will stop blinking and remain solid blue. In the top-left corner of the screen shows the name of the current remote activity.\u003c/p\u003e\n\u003cp\u003eAt the bottom of the screen there are 4 icons representing the events mapped to each of the 4 function buttons below them. Above that there are icons arranged in a \"plus\" configuration representing the events mapped to the \u003cstrong\u003eD-BUTTON\u003c/strong\u003e.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/702/original/ss-media.jpeg?1742088902","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eFor host devices that support it, the touch screen will behave as a trackpad/mouse. In other words, touching and moving your finger or a stylus to the screen will move the mouse cursor on the host device.\u003c/p\u003e\n\u003cp\u003eOne other thing about that activity label in the top-left corner of the screen: it is the only touch-screen button in the firmware. If you tap this label, you'll notice that it becomes highlighted. If you continue to hold the button for about 1.5 seconds, the remote transitions to the preferences/activity select screen.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/703/original/ss-config-button-idle.jpeg?1742088928","metadata":{"caption":"The activity title text, \"Media\" here, is actually a touch-screen button."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/704/original/ss-config-button-pressed.jpeg?1742088976","metadata":{"caption":"When the activity title label is tapped/held, it will be highlighted. Holding for 1.5 seconds will launch the preferences screen."}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eBecause I chose to use the touch screen primarily as a trackpad/mouse, I generally didn't want to implement touch screen buttons-like controls for this firmware. I made an exception here because I did not want to sacrifice any of the physical buttons/keys that could be used to control the host device. Hopefully the title label is out-of-the-way enough that its presence won't interfere with the trackpad/mouse capability.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003ePreferences/Activity select\u003c/h2\u003e\n\u003cp\u003eIn this screen, you can choose which activity to assign to the remote screen, as well as adjust the display/keyboard/NeoPixel brightness and the main UI color.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/705/original/ss-config.jpeg?1742089117","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cul\u003e\n\u003cli\u003eChange brightness\n\u003cul\u003e\n\u003cli\u003ePressing the \u003cstrong\u003eUP\u003c/strong\u003e/\u003cstrong\u003eDOWN\u003c/strong\u003e directions on the \u003cstrong\u003eD-BUTTON\u003c/strong\u003e will increase/decrease the system brightness, respectively. This affects the display brightness, the keyboard brightness, and the overall brightness of the NeoPixel.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eChange UI Color\n\u003cul\u003e\n\u003cli\u003ePressing the \u003cstrong\u003eLEFT\u003c/strong\u003e/\u003cstrong\u003eRIGHT\u003c/strong\u003e directions on the \u003cstrong\u003eD-BUTTON\u003c/strong\u003e will cycle through the available UI colors.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eSelect Activity\n\u003cul\u003e\n\u003cli\u003eEach of the \u003cstrong\u003e4 function buttons\u003c/strong\u003e below the screen are labeled with the numbers 1-4. Above that is a list of activities, also labeled 1-4. Pressing any of the 4 function buttons will select the corresponding activity, save the configuration (including brightness and color settings) to the SD card, and return to the remote screen configured to use the selected activity.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003e\"Auto-Sleep\"\u003c/h2\u003e\n\u003cp\u003eWhile I didn't implement deep sleep as mentioned in the \u003ca href=\"https://adafruit-playground.com/u/squid_jpg/pages/universal-ble-remote-with-keyboard-featherwing#known-issues-3195122\"\u003eKnown Issues\u003c/a\u003e section, it was still worthwhile to adopt another power-saving technique: turn the display and keyboard backlights off when the remote is \"idle\". We consider \"idle\" to mean that the user has not touched the screen or pressed any keys/buttons for about 30 seconds. Once \"asleep\", if the user touches the touch screen or presses a key/button, the backlight LEDs are turned back on.\u003c/p\u003e\n\u003cp\u003eWith the display brightness at 50% and keyboard brightness at10%, the remote uses ~50 mA. With those backlights turned completely off, the power draw is reduced to ~25 mA. A significant reduction!\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eCustomization - Activities\u003c/h1\u003e\n\u003cp\u003eThe remote firmware comes with 4 pre-configured activities:\u0026nbsp;\u003cstrong\u003eMedia\u003c/strong\u003e, \u003cstrong\u003eApple TV,\u003c/strong\u003e \u003cstrong\u003eMac\u003c/strong\u003e, and \u003cstrong\u003eWindows\u003c/strong\u003e. These activities can be renamed, and partially or completely customized from their original versions.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/706/original/ss-default-modes.jpeg?1742140016","metadata":{"caption":""}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eBefore we get into how to configure the activities, it's helpful to explain a few other concepts first. To start, an \"activity\" is simple the mapping of the 9 configurable buttons (4 function buttons + D-BUTTON) to \"actions\". An action consists of 3 pieces of information:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eThe \u003cstrong\u003eHID event type\u003c/strong\u003e the button will emit (defined in \u003cstrong\u003efunction_key.py\u003c/strong\u003e). This can be one of four types:\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHID_KEYBOARD\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHID_KEYBOARD_LAYOUT\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHID_CONSUMER_CONTROL\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHID_MOUSE\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eA compatible \u003cstrong\u003eevent code\u003c/strong\u003e for that HID event to emit upon pressing/releasing the button. The value of the code depends on the type of HID event:\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eHID_KEYBOARD\u003c/strong\u003e: The code should be a \u003ca href=\"https://docs.circuitpython.org/projects/hid/en/latest/api.html#adafruit-hid-keycode-keycode\"\u003eKeycode\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eHID_KEYBOARD_LAYOUT\u003c/strong\u003e: The code should be a typeable character, e.g. 'A', 'g', or '3'\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eHID_CONSUMER_CONTROL\u003c/strong\u003e: The code should be a \u003ca href=\"https://docs.circuitpython.org/projects/hid/en/latest/api.html#adafruit_hid.consumer_control_code.ConsumerControlCode\"\u003eConsumerControlCode\u003c/a\u003e. For an extended list of consumer control codes, see \u003ca href=\"https://www.usb.org/sites/default/files/hut1_21_0.pdf#page=118\"\u003ethis page\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eHID_MOUSE\u003c/strong\u003e: One of the mouse buttons \u003ca href=\"https://docs.circuitpython.org/projects/hid/en/latest/api.html#adafruit_hid.mouse.Mouse\"\u003edefined here\u003c/a\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eAn \u003cstrong\u003eicon\u003c/strong\u003e to visually represent the action for the button on the Keyboard Featherwing remote display. In the context of an activity, an \"icon\" can either be an integer value representing a sprite index into the icons sprite sheet, or, if suitable icon does not exist in the sprite sheet, a string that will be displayed as black text on an inverted background. As a handy convenience, all of the sprites in the icons sprite sheet have constants defined in \u003cstrong\u003esprites.py\u003c/strong\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eHere is an example of defining an action for an activity for when the \"L1\" key is pressed/released that simulates clicking the left mouse button:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"L1=Action(\n    hid_type=HID_MOUSE, \n    code=Mouse.LEFT_BUTTON, \n    icon=LEFT_MOUSE\n)","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eThis example assigns the \"Volume increment\" consumer control code to the \"UP\" key on the D-BUTTON:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"UP=Action(\n    hid_type=HID_CONSUMER_CONTROL, \n    code=ConsumerControlCode.VOLUME_INCREMENT, \n    icon=SPEAKER_PLUS\n)","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eFinally, we get to the \u003cstrong\u003eActivityList\u003c/strong\u003e, which is the overall list of the 4 activities selectable via preferences screen at run-time. An activity list contains an \u003cstrong\u003eAction\u003c/strong\u003e (as shown above) for each customizable button, as well as two other pieces of information:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eThe activity's name, e.g. \"Media\" or \"Apple TV\"\u003c/li\u003e\n\u003cli\u003eWhether or not we should display the message indicating that the touch screen will behave as a mouse. When paired with some devices that do not use mouse control, it can be helpful to disable this message (note however that currently the mouse events are still sent. This may change in a future update).\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eEach of the 4 activities can be configured by editing the \u003cstrong\u003euser/activities.py\u003c/strong\u003e file on the Feather's \u003cstrong\u003eCIRCUITPY\u003c/strong\u003e drive.\u003c/p\u003e\n\u003cp\u003eA fully-configured activity might look something like this:\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"ACTIVITY_2=Activity(\n    name=\"Apple TV\",\n    show_mouse_message=False,\n\n    L1=Action(\n        hid_type=HID_CONSUMER_CONTROL, \n        code=ConsumerControlCode.VOLUME_DECREMENT, \n        icon=SPEAKER_MINUS\n    ),\n\n    L2=Action(\n        hid_type=HID_CONSUMER_CONTROL, \n        code=ConsumerControlCode.VOLUME_INCREMENT, \n        icon=SPEAKER_PLUS\n    ),\n\n    R1=Action(\n        hid_type=HID_KEYBOARD, \n        code=Keycode.ESCAPE, \n        icon=BACK\n    ),\n\n    R2=Action(\n        hid_type=HID_CONSUMER_CONTROL, \n        code=CC_MENU, \n        icon=HOME\n    ),\n\n    UP=Action(\n        hid_type=HID_KEYBOARD, \n        code=Keycode.UP_ARROW, \n        icon=UP_ARROW\n    ),\n\n    DOWN=Action(\n        hid_type=HID_KEYBOARD, \n        code=Keycode.DOWN_ARROW, \n        icon=DOWN_ARROW\n    ),\n\n    LEFT=Action(\n        hid_type=HID_KEYBOARD, \n        code=Keycode.LEFT_ARROW, \n        icon=LEFT_ARROW\n    ),\n    \n    RIGHT=Action(\n        hid_type=HID_KEYBOARD, \n        code=Keycode.RIGHT_ARROW, \n        icon=RIGHT_ARROW\n    ),\n    \n    SELECT=Action(\n        hid_type=HID_KEYBOARD, \n        code=Keycode.RETURN, \n        icon=RETURN\n    )\n),","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003cp\u003eNotice that in the case of the Apple TV, standard keyboard key codes are used for directional navigation (the arrow keys), and the use of the return key for selection. This is part of what makes the Keyboard Featherwing remote so versatile!\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eCustomization - Color list\u003c/h1\u003e\n\u003cp\u003eOne other simple point of customization for the remote is the ability to add/remove/edit the colors that can be selected in the preferences screen. Many default named colors are provided, as well as an array of ALL colors. You can customize this list of colors by editing the \u003cstrong\u003euser/activities.py\u003c/strong\u003e file on the Feather's \u003cstrong\u003eCIRCUITPY\u003c/strong\u003e drive.\u003c/p\u003e\n\u003cp\u003eNote that you should not remove the built-in named colors, as there is code elsewhere in the firmware that may depend on them. You may, however:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eChange the actual color value for the built-in colors\u003c/li\u003e\n\u003cli\u003eAdd additional named colors\u003c/li\u003e\n\u003cli\u003eAdd/remove colors to the ALL array, which defines the colors that can be selected from the preferences screen.\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# Any color in this array is selectable to be the\n# primary UI color from the preferences screen\nALL = [\n    WHITE,\n    ORANGE, \n    AMBER,\n    YELLOW,\n    GREEN,\n    CYAN,\n    BLUE,\n    PURPLE,\n    MAGENTA,\n    RED,    \n]","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eCustomization - Device settings\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003euser/config.py\u003c/strong\u003e contains just a few more settings that allow you to configure the firmware without needing to re-compile the core files, if you're using the pre-compiled code. Those are:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eble_name\u003c/strong\u003e: The name the device will use for it's BLE advertising name. By default, this is \"Keyboard Featherwing Remote\"\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eneopixel_color\u003c/strong\u003e: Color value to use for the BLE NeoPixel indicator. Blue by default, but you can change it here if you want.\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eneopixel_max_brightness\u003c/strong\u003e: You can adjust the overall brightness of the NeoPixel with this if you'd like it brighter/dimmer.\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003elow_voltage_warning\u003c/strong\u003e: At or below this voltage, the battery indicator in the top-right corner of the screen will enter the \"Needs charging\" state.\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003etouch_calibration\u003c/strong\u003e: Just in case you need to modify the touch calibration or change whether the touch y-axis is inverted, you can do that here.\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"code","content":"# Feel free to edit these seetings as needed/desired.\nCONFIG = DeviceConfig(\n\tble_name=\"Keyboard Featherwing Remote\",\n\tneopixel_color=0x0000FF,\n\tneopixel_max_brightness=0.04,\n\tlow_voltage_warning=3.6,\n\n\ttouch_calibration=TouchCalibration(\n    \tmin_x=200,\n    \tmax_x=3600,\n    \tmin_y=250,\n    \tmax_y=3700,    \n    \tinvert_y=True\t\t\n\t)\n)","metadata":{"language":"python","linenums":false}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eSource code walkthrough\u003c/h1\u003e\n\u003cp\u003eIf you came across this and thought \"Hey, I have a Keyboard Featherwing and an nRF52840 Feather, lemme try this out!\", and do not care at all about the code in more detail, you can stop here. You know everything you need to know to use the firmware as-designed! Otherwise, for some of the gory details, read on.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003ecode.py\u003c/h2\u003e\n\u003cp\u003eFor this project, code.py is basically just used to \"boot strap\" our main program, which is defined in ...\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003erun.py\u003c/h2\u003e\n\u003cp\u003erun.py is responsible for importing and setting up the hardware drivers and other libraries we need for the rest of the firmware to do its job. Normally this code might live in code.py, but since a lot of work happens here, I wanted to be able to pre-compile this process into an .mpy file.\u003c/p\u003e\n\u003cp\u003eOne perhaps interesting thing about this file is that we don't import all of the libraries at the top of the file as is common in many CircuitPython programs. Instead, run.py lays out a startup process that tries to prioritize things like gaining control of the system's backlight LEDs as soon as possible, and getting something on the display quickly after booting. Importing large libraries can take a significant amount of time, so if we import them all at once at the top of the file, it could be several seconds before our firmware actually gets control of the program flow.\u003c/p\u003e\n\u003cp\u003erun.py is also responsible for setting up and managing:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCreation of the DisplayController object, used for most tasks involving the display. This is explained in further detail in the display_controller.py section below.\u003c/li\u003e\n\u003cli\u003eCreation of the Device object, which is used by other objects in the application to interface with the remote's hardware and other systems. The Device object is explained in a little more detail in the device.py section below.\u003c/li\u003e\n\u003cli\u003eApplication flow. This is explained further detail below in the mode.py section below.\u003c/li\u003e\n\u003c/ul\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003edisplay_controller.py\u003c/h2\u003e\n\u003cp\u003eThe DisplayController object is the main interface to the display for other objects. It defines handy constants for things like UI layout, methods to access the underlying display hardware and the displayio root group, and methods that make it simple to create and add sprites and labels to UI groups.\u003c/p\u003e\n\u003ch3\u003eSprite Sheet\u003c/h3\u003e\n\u003cp\u003eThe display controller contains the main sprite sheet, which is used to create sprites/icons by our firmware. The SpriteSheet object is defined in sprite_sheet.py. All sprites are 32x32 pixels wide, as defined by the constants \u003cstrong\u003eSpriteSheet.SPRITE_WIDTH\u003c/strong\u003e and \u003cstrong\u003e.SPRITE_HEIGHT.\u003c/strong\u003e The sprite sheet also provides the add_sprite method, which creates a sprite using the given sprite index, and adds it to the specified group.\u003c/p\u003e\n\u003cp\u003eAs a convenience, named constants for the sprite indices can be found in \u003cstrong\u003esprites.py\u003c/strong\u003e. This allows our code to reference the sprite indices by memorable names, rather than arbitrary integer values.\u003c/p\u003e\n\u003ch3\u003ePalette\u003c/h3\u003e\n\u003cp\u003eAll of the graphics in this firmware are effectively 1-bit palettized images and text. This means we only have 2 colors, 0 and 1. The actual values that these colors display on the screen is defined by a Palette. The actual colors used are customizable, and can be found in \u003cstrong\u003euser/colors.py\u003c/strong\u003e. For this firmware, color 0 is generally always black, and color 1 is the \"primary\" color.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003euser/colors.py\u003c/strong\u003e contains constants for common colors, as well as an array named \u003cstrong\u003eALL\u003c/strong\u003e which contains a list of colors. Any elements in the \u003cstrong\u003eALL\u003c/strong\u003e array are available to be selected as the primary UI color in the remote's preferences screen.\u003c/p\u003e\n\u003ch3 class=\"heading-element\"\u003eAlignment\u003c/h3\u003e\n\u003cp\u003eTo make basic layout more convenient, DisplayController's \u003cstrong\u003eadd_sprite\u003c/strong\u003e, \u003cstrong\u003eadd_label\u003c/strong\u003e, and \u003cstrong\u003eadd_icon\u003c/strong\u003e methods all accept two alignment parameters, \u003cstrong\u003eh_align\u003c/strong\u003e and \u003cstrong\u003ev_align\u003c/strong\u003e. There are 3 possible values for these parameters:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cstrong\u003eALIGN_LEADING\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eWhen assigned to \u003cstrong\u003eh_align\u003c/strong\u003e, this indicates to position the element so that its left edge is the reference for the x position when laying out the element.\u003c/li\u003e\n\u003cli\u003eWhen assigned to \u003cstrong\u003ev_align\u003c/strong\u003e, this indicates to position the element such that its top edge is the reference for the y position when layout out the element.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eALIGN_CENTER\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eWhen assigned to both \u003cstrong\u003eh_align\u003c/strong\u003e and \u003cstrong\u003ev_align\u003c/strong\u003e, this indicates to position the element such that its x/y position is centered long the horiztonal/vertical axis, respectively\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eALIGN_TRAILING\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eWhen assigned to \u003cstrong\u003eh_align\u003c/strong\u003e, this indicates to position the element so that its right edge is the reference for the x position when laying out the element.\u003c/li\u003e\n\u003cli\u003eWhen assigned to \u003cstrong\u003ev_align\u003c/strong\u003e, this indicates to position the element such that its bottom edge is the reference for the y position when layout out the element.\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003etouch_screen.py\u003c/h2\u003e\n\u003cp\u003eThe TouchScreen object provides a handy abstraction for easy access to the information our firmware needs from the touch screen, namely:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIs the touch screen being touched right now?\u003c/li\u003e\n\u003cli\u003eWhere it is being touched?\u003c/li\u003e\n\u003cli\u003eHow much has the touch point moved since the last time we checked?\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3\u003eCalibration\u003c/h3\u003e\n\u003cp\u003eThe default touch screen calibration values are probably okay, but if you need to refine them, or change whether the touch-screen y-axis is inverted, you can update the calibration values in \u003ca href=\"https://adafruit-playground.com/u/squid_jpg/pages/universal-ble-remote-with-keyboard-featherwing#customization-device-settings-3195329\"\u003euser/config.py\u003c/a\u003e.\u003c/p\u003e\n\u003ch3\u003eFiltering/Smoothing\u003c/h3\u003e\n\u003cp\u003eOne of the other handy things TouchScreen provides is filtering, as touch screen input tends to be pretty noisy. You can find the code for the filters in filters.py. A more in-depth explanation of the filters used can be found \u003ca href=\"https://dlbeer.co.nz/articles/tsf.html\"\u003ehere\u003c/a\u003e, but the short version is that we are applying a combination of 2 filters to compensate for the noise in the touch screen input. The first is a median filter, which smooths out the worst spikes, and the second is an IIR (infinite impulse response) filter to remove low-level noise over a longer sample period. The TouchScreen object also handles a form of debouncing the touch screen. It seems when first touched, the next sample read from the tsc2004 touch screen can be wildly innaccurate. So...we just ignore it, and only process the subsequent samples. With these layers of processing applied, the touch screen now becomes more reliable as a simple track pad.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003edevice.py\u003c/h2\u003e\n\u003cp\u003eOnce run.py has finished setting up all of the hardware and libraries, the Device object is created. From this point on, the Device object is the main interface for the rest of our firmware to access device- and hardware- specific functionality. The Device object exposes properties for all hardware like the keyboard, touch screen, display controller, neopixel, etc. It is also responsible for some more global functionality, such as setting up the BLE stack, managing the NeoPixel to display BLE status, reading the battery voltage monitor/updating the battery UI, controlling the device backlight brightness, etc.\u003c/p\u003e\n\u003cp\u003eBy itself the Device object doesn't do much. Most of the remaining program logic is contained one of two types of Mode object.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003emode.py\u003c/h2\u003e\n\u003cp\u003eIn the context of this firmware, generally speaking a \"Mode\" is a UI \"screen\". More specifically, each mode can be though of as a state in a finite state machine. In our case, our state machine contains two states, or modes, that determine how the program behaves:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\"Remote control\" mode\u003c/li\u003e\n\u003cli\u003e\"Preferences\" mode\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe Mode class defined in mode.py is used as the superclass for our two modes. Its initializer takes the device object as a parameter. Without access to the device object, our modes/states can't really do much.\u003c/p\u003e\n\u003cp\u003eThe Mode class also provides our concrete subclasses with a common lifetime interface. The 3 key pieces are the \u003cstrong\u003eenter(),\u003c/strong\u003e \u003cstrong\u003eupdate()\u003c/strong\u003e , and the \u003cstrong\u003eexit()\u003c/strong\u003e methods.\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003eenter()\u003c/strong\u003e method is responsible for setting up the mode right before it becomes active. This includes intitializing its UI, etc.\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003eupdate()\u003c/strong\u003e method is resposible for processing input and taking appropriate action, such as sending HID events, as well as updating the UI to reflect the current device state.\u003c/p\u003e\n\u003cp\u003eThe \u003cstrong\u003eexit()\u003c/strong\u003e method is responsible for cleaning up right before the mode is about to become inactive. Here we tear down our UI and perform any other necessary cleanup tasks.\u003c/p\u003e\n\u003cp\u003eThe two concrete mode implementations are discussed below.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch2\u003eremote_mode.py\u003c/h2\u003e\n\u003cp\u003eThe RemoteMode object contains the core remote control logic for the firmware.\u003c/p\u003e\n\u003cp\u003eWhen not paired with a host BLE device, the UI consists of an animated Bluetooth icon.\u003c/p\u003e\n\u003cp\u003eOnce a connection is made, the \"remote control\" UI is shown, which displays a mapping of the 9 customizable buttons and their actions. When connected, pressing keys on the keyboard, pressing the customizable buttons, and touching the touch screen all send HID events to the paired host, based on the selected activity. The activity label in the top-left corner also behaves as a touch screen \"button\" that, when held for about 1.5 seconds, will transition the firmware to PreferencesMode.\u003c/p\u003e\n\u003ch2 class=\"heading-element\"\u003epreferences_mode.py\u003c/h2\u003e\n\u003cp\u003eThe PreferencesMode object is responsible for selecting the current activity from the 4 activity presets, as well as adjusting the device brightness and primary UI color. Once an activity selection is made, control is returned back to the RemoteMode.\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n  \n        \u003ch1\u003eSpecial thanks\u003c/h1\u003e\n\u003ch2\u003e\u003ca href=\"https://kenney.nl/\"\u003eKenney Assets\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMany of the sprites used in this firmware are from the excellent \u003ca href=\"https://kenney.nl/assets/1-bit-input-prompts-pixel-16\"\u003e1-bit input prompts\u003c/a\u003e asset pack from Kenney.\u003c/p\u003e\n\u003ch2\u003e\u003ca href=\"https://www.solder.party/\"\u003eSolder party\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThanks for both the excellent Keyboard Featherwing hardware, as well as for the image from the documentation that I borrowed to frame my screenshots. You can also find them on Bluesky at \u003ca href=\"https://bsky.app/profile/solder.party\"\u003e@solder.party\u003c/a\u003e\u003c/p\u003e\n      \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n","metadata":{}}]