[{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/421/original/transform1.gif?1779215931","metadata":{}},{"element_type":"markdown","content":"\u003cp\u003eFirefox's browser engine, Gecko, added support for Web Serial in release 151. This opens up a fun new path for hardware-to-web experiments and experiences. To try it out, I built \u003cstrong\u003ePage Playground\u003c/strong\u003e: a device featuring a variety of novel input methods that allows me to point at elements on a web page and alter their colors, layout, text properties, and transforms in real time. It reveals the potential for powerful and intuitive user experiences integrating hardware with the web.\u003c/p\u003e\n","metadata":{"markdown":"Firefox's browser engine, Gecko, added support for Web Serial in release 151. This opens up a fun new path for hardware-to-web experiments and experiences. To try it out, I built **Page Playground**: a device featuring a variety of novel input methods that allows me to point at elements on a web page and alter their colors, layout, text properties, and transforms in real time. It reveals the potential for powerful and intuitive user experiences integrating hardware with the web."}},{"element_type":"embed","content":"https://www.youtube.com/watch?v=eo0TGsV4XC8"},{"element_type":"markdown","content":"\u003ch2\u003eIdea\u003c/h2\u003e\n\n\u003cp\u003eAs seen in the video, the Page Playground can alter various properties of elements on a webpage. It creates a new and fun way to interact with a page, but you can imagine a lot of professional use cases for such a tool, too (like web development, style tweaking, etc).\u003c/p\u003e\n\n\u003ch2\u003eAssembly\u003c/h2\u003e\n\n\u003cp\u003eAdafruit stocks a great set of essentially plug-n-play parts, so getting this off the ground didn't take much time at all! I decided to use the MACROPAD RP2040 as the base of the device functionality, since it has a nice screen, rotary encoder, and 12 keys with RGB LEDs. This was plenty to work with already, but I also added:\u003c/p\u003e\n\n\u003cul\u003e\n\u003cli\u003etwo more rotary encoders,\u003c/li\u003e\n\u003cli\u003ean IMU (BNO085),\u003c/li\u003e\n\u003cli\u003ea color/proximity sensor (APDS-9960),\u003c/li\u003e\n\u003cli\u003ea joystick, and\u003c/li\u003e\n\u003cli\u003ea slide potentiometer.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cp\u003eAll of these provide fun and different ways to interact with the webpage.\u003c/p\u003e\n\n\u003cp\u003eThe joystick and potentiometer are simple analog devices, so I needed to get another board to access those over I2C (because the MACROPAD doesn't expose anything other than I2C over a STEMMA QT port). Conveniently, my order was large enough that Adafruit provided a KB2040 for free :)\u003c/p\u003e\n\n\u003ch3\u003eMACROPAD\u003c/h3\u003e\n\n\u003cp\u003eThe MACROPAD was very easy to assemble; I just had to add the key switches and caps, and a rotary encoder top (I didn't realize the bare bones kit didn't come with one, so I came up short one top, whoops).\u003c/p\u003e\n","metadata":{"markdown":"## Idea\nAs seen in the video, the Page Playground can alter various properties of elements on a webpage. It creates a new and fun way to interact with a page, but you can imagine a lot of professional use cases for such a tool, too (like web development, style tweaking, etc).\n\n## Assembly\nAdafruit stocks a great set of essentially plug-n-play parts, so getting this off the ground didn't take much time at all! I decided to use the MACROPAD RP2040 as the base of the device functionality, since it has a nice screen, rotary encoder, and 12 keys with RGB LEDs. This was plenty to work with already, but I also added:\n- two more rotary encoders,\n- an IMU (BNO085),\n- a color/proximity sensor (APDS-9960),\n- a joystick, and\n- a slide potentiometer.\n\nAll of these provide fun and different ways to interact with the webpage.\n\nThe joystick and potentiometer are simple analog devices, so I needed to get another board to access those over I2C (because the MACROPAD doesn't expose anything other than I2C over a STEMMA QT port). Conveniently, my order was large enough that Adafruit provided a KB2040 for free :)\n\n### MACROPAD\nThe MACROPAD was very easy to assemble; I just had to add the key switches and caps, and a rotary encoder top (I didn't realize the bare bones kit didn't come with one, so I came up short one top, whoops)."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/422/original/PXL_20260514_171856118.jpg?1779216490","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/431/original/assembled.jpg?1779217698","metadata":{}},{"element_type":"markdown","content":"\u003ch3\u003eSoldering\u003c/h3\u003e\n\n\u003cp\u003eNext, I soldered all components that needed it (well, almost all of them: I took a second pass to add a few more things once my minimum feature set was complete). This was mostly attaching headers, which went quickly. The encoders also needed to be soldered to the I2C boards.\u003c/p\u003e\n","metadata":{"markdown":"### Soldering\nNext, I soldered all components that needed it (well, almost all of them: I took a second pass to add a few more things once my minimum feature set was complete). This was mostly attaching headers, which went quickly. The encoders also needed to be soldered to the I2C boards."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/424/original/pre-solder.jpg?1779217087","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/425/original/soldered.jpg?1779217112","metadata":{}},{"element_type":"markdown","content":"\u003ch3\u003eWiring\u003c/h3\u003e\n\n\u003cp\u003eNext, I wired everything on a small breadboard. This was pretty simple, because all the devices were I2C, so I just needed to chain them all together with the same 4 wires. Adafruit was unfortunately sold out of the STEMMA QT cables, but it was just as well, because putting everything on a breadboard made it easier to assemble as a prototype later.\u003c/p\u003e\n\n\u003cp\u003eNote that I didn't yet attach the analog components and the KB2040. I could get my first prototype to work without those extra inputs, so I omitted them.\u003c/p\u003e\n","metadata":{"markdown":"### Wiring\nNext, I wired everything on a small breadboard. This was pretty simple, because all the devices were I2C, so I just needed to chain them all together with the same 4 wires. Adafruit was unfortunately sold out of the STEMMA QT cables, but it was just as well, because putting everything on a breadboard made it easier to assemble as a prototype later.\n\nNote that I didn't yet attach the analog components and the KB2040. I could get my first prototype to work without those extra inputs, so I omitted them."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/426/original/wires.jpg?1779217139","metadata":{}},{"element_type":"markdown","content":"\u003cp\u003eBecause the STEMMA QT cables were sold out, I had to get a STEMMA QT to STEMMA cable and splice in wires to join to the breadboard. The threaded wires in the STEMMA QT connector are tiny, so I opted for twist wire connectors to reduce the possibility of them breaking.\u003c/p\u003e\n","metadata":{"markdown":"Because the STEMMA QT cables were sold out, I had to get a STEMMA QT to STEMMA cable and splice in wires to join to the breadboard. The threaded wires in the STEMMA QT connector are tiny, so I opted for twist wire connectors to reduce the possibility of them breaking."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/427/original/PXL_20260514_201035530.jpg?1779217173","metadata":{}},{"element_type":"markdown","content":"\u003cp\u003eAfter placing the components on the breadboard, I was happy to plug in the MACROPAD and see everything was lit up, with the I2C addresses enumerated on the MACROPAD screen.\u003c/p\u003e\n","metadata":{"markdown":"After placing the components on the breadboard, I was happy to plug in the MACROPAD and see everything was lit up, with the I2C addresses enumerated on the MACROPAD screen."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/428/original/PXL_20260514_201348703.jpg?1779217203","metadata":{}},{"element_type":"markdown","content":"\u003ch2\u003eProgramming\u003c/h2\u003e\n\n\u003cp\u003eI don't have any exciting pictures of programming, but I will outline how the code works and how I developed it.\u003c/p\u003e\n\n\u003cp\u003eIf curious, you can find the full source code on \u003ca href=\"https://github.com/afranchuk/page-playground\"\u003eGitHub\u003c/a\u003e.\u003c/p\u003e\n\n\u003ch3\u003eMessaging\u003c/h3\u003e\n\n\u003cp\u003eI chose to use \u003ca href=\"https://msgpack.org/\"\u003eMessagePack\u003c/a\u003e serialization because there was a readily-available \u003ca href=\"https://github.com/hideakitai/MsgPack\"\u003elibrary for Arduino (C++)\u003c/a\u003e, and the same developer has a nice \u003ca href=\"https://github.com/hideakitai/Packetizer\"\u003epacketizing library\u003c/a\u003e using \u003ca href=\"https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing\"\u003eCOBS\u003c/a\u003e that can prepend a byte to indicate the message type (a feature I used). The serial connection is fairly reliable, but the use of COBS to frame packets gave me flexibility to e.g. add extra data to packets without worrying about any other code handling them.\u003c/p\u003e\n\n\u003cp\u003eI didn't want to use text-based serialization like JSON because I wanted the modifications to be as fast and responsive as possible. The overhead of extra parsing and extra bytes over the interface may have been limiting. Yes, JSON may have worked (I didn't try, after all), but it was simple enough to just use MessagePack from the start.\u003c/p\u003e\n\n\u003ch3\u003eBrowser\u003c/h3\u003e\n\n\u003cp\u003eOn the browser side, I inject a GreaseMonkey script into every page using the \u003ca href=\"https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/\"\u003eTamperMonkey\u003c/a\u003e add-on. I found TamperMonkey a bit easier to use than the GreaseMonkey add-on, as the latter doesn't automatically prompt to install a script when you visit a \u003ccode class=\"inline\"\u003e.user.js\u003c/code\u003e URL, and to develop I needed to reload the script often.\u003c/p\u003e\n\n\u003cp\u003eThe script is built using \u003ca href=\"https://bun.com/\"\u003ebun\u003c/a\u003e. This was useful because it:\u003c/p\u003e\n\n\u003cul\u003e\n\u003cli\u003emade it trivial to add dependencies (like \u003ca href=\"https://github.com/SudoMaker/uCOBS\"\u003euCOBS\u003c/a\u003e and \u003ca href=\"https://github.com/msgpack/msgpack-javascript\"\u003emsgpack-javascript\u003c/a\u003e),\u003c/li\u003e\n\u003cli\u003eallowed me to use TypeScript (which caught many issues at compile/write time), and\u003c/li\u003e\n\u003cli\u003emade it easy to bundle static resources.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cp\u003eThe script adds a button in the bottom-right corner of the screen. When clicked, it opens a Web Serial connection (prompting the user to choose a device).\u003c/p\u003e\n\n\u003cp\u003e\u003cem\u003eNote\u003c/em\u003e: In Firefox 151, Web Serial access also involves Firefox’s site-permission flow before the usual device chooser appears, which enforces explicit user consent to connect to a device.\u003c/p\u003e\n\n\u003cp\u003eOnce connected, the script registers a \u003ccode class=\"inline\"\u003emousemove\u003c/code\u003e event handler. As the mouse moves to new elements in the DOM, various style properties of the element are sent in a message to the device. When those properties are modified by the device, it sends back messages indicating the changes. The script applies those changes to the current element. In this way, the script is \u003cem\u003efairly\u003c/em\u003e simple.\u003c/p\u003e\n\n\u003cp\u003eFor the effects, the images weren't readily accessible from network URIs, so I had to bundle images as base64 in the script (I can't e.g. access them as file URIs, either). The Kit GIF in particular (even when downsized and optimized) made the script magnitudes larger: it ended up around 1MB total.\u003c/p\u003e\n\n\u003ch3\u003eDevice\u003c/h3\u003e\n\n\u003cp\u003eI usually use cross-compile toolchains and device programmers directly, but I opted for the Arduino IDE because the Adafruit guides all give (quite good) instructions around it; it was the path of least resistance! I was able to add most libraries I needed using the built-in library manager. It has been quite a while since I used the Arduino IDE, and the library manager is a welcome addition.\u003c/p\u003e\n\n\u003cp\u003eOne issue I immediately encountered was that my laptop was auto-suspending the USB bus when there was no TX/RX for about 3 seconds. This would prevent the serial connection from working. It wouldn't disconnect, but it wouldn't be able to send from device to browser until the \u003cem\u003ebrowser\u003c/em\u003e sent something out. It took a while to debug this and come to the conclusion that autosuspend was to blame. I first disabled autosuspend for the entire system. When that worked, I looked closer at the TinyUSB functions (this is the USB software stack used in the RP2040), and found some functions to wake it back up. Now, before writing to the serial connection, I call:\u003c/p\u003e\n\u003ccode class=\"cpp\"\u003e\u003cpre\u003eif (tud_suspended()) tud_remote_wakeup();\n\u003c/pre\u003e\u003c/code\u003e\n\u003cp\u003eThankfully, I found all the Adafruit libraries Just Worked without any fiddling! It was very fast to get up and running. I set up peripherals and started programming the MACROPAD. I had already decided on how it was going to function: the rotary encoder would change the tool \"page\", and each page would have related properties.\u003c/p\u003e\n\n\u003ch4\u003eGlobal behavior\u003c/h4\u003e\n\n\u003cp\u003eThe device accepts two messages:\u003c/p\u003e\n\n\u003cul\u003e\n\u003cli\u003ea reset message to set the state back to initial state when first connecting to a new page, and\u003c/li\u003e\n\u003cli\u003ea style info message to give information about the selected element.\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cp\u003eEach page simply displays/updates the relevant information in the style info, though when changes are made there are individual messages to affect each change (rather than sending a big message with all the changes, and relying on the browser code to check for a delta or set everything).\u003c/p\u003e\n\n\u003cp\u003eThe MACROPAD encoder button can also be clicked to lock the selected element (not shown in the video), which prevents it from changing as the mouse changes. It is indicated by inverted colors in the element name on the OLED screen, and can be unlocked by clicking again.\u003c/p\u003e\n\n\u003ch4\u003eColors\u003c/h4\u003e\n\n\u003cp\u003eThe color page allows setting the foreground and background color of an element. At one point I imagined this could be extended to alter the color palette of the entire page, but I didn't have time to implement that.\u003c/p\u003e\n\n\u003cp\u003eYou can press and hold any of the keys to set the color using the two other rotary encoders (they alter HSL components). The color of the NeoPixel under the key reflects the associated color. You can also use the APDS-9960 color sensor to read a color, though I found that it really needs a bright input. It took a few iterations to refine the behavior, but it didn't take long to have something working. And dynamically changing the colors of elements on the screen was really cool! I was glad to see that everything was responsive and essentially instantaneous.\u003c/p\u003e\n\n\u003ch4\u003eTransform\u003c/h4\u003e\n\n\u003cp\u003eI worked on the transform page next, because it offered a very cool effect: you could move the device and have an element on the page move in a similar way! This uses the BNO085 IMU. I found the orientation data to be very accurate, so getting orientation to work wasn't too hard. Integrating the acceleration to movement was more tricky (as it always is), and I brought in some external \u003ca href=\"https://github.com/carrino/Quaternion\"\u003eQuaternion code\u003c/a\u003e to make transforming the acceleration vector simple. There's still some drift in the position, but I deemed it Good Enough.\u003c/p\u003e\n\n\u003cp\u003eLater on, I added an analog joystick and mapped the analog values to transforms, too (limited to XY).\u003c/p\u003e\n\n\u003ch4\u003eLayout\u003c/h4\u003e\n\n\u003cp\u003eThe layout tool was a little more involved, mainly because there were many values to show. There was enough room on the screen to display a little box model layout, showing margins, padding, and width/height. As you click the related keys on the MACROPAD (in a 3x3 grid), you can select the left/right/top/bottom (or all of those) sides, as well as width and height. Then, the rotary encoder can be used to adjust values (pressing down to adjust padding rather than margin).\u003c/p\u003e\n\n\u003cp\u003eI also added a feature to set a value using the APDS-9960 proximity sensor. The sensor doesn't have a long range, but it was still cool to set values in that way.\u003c/p\u003e\n\n\u003ch4\u003eText\u003c/h4\u003e\n\n\u003cp\u003eThe text page was one of the last I did, only because it was somewhat straightforward. I programmed the buttons to change to common font families, and each rotary encoder changes font size and line height.\u003c/p\u003e\n\n\u003ch4\u003eEffects\u003c/h4\u003e\n\n\u003cp\u003eThe effects on the device were trivial to implement: clicking a button sends a message with the numbered effect to perform.\u003c/p\u003e\n\n\u003ch2\u003eFinishing Touches\u003c/h2\u003e\n\n\u003ch3\u003eMore Inputs\u003c/h3\u003e\n\n\u003cp\u003eAfter I had all of the above working, I really wanted to use a slide potentiometer to enable (and potentially scale) the use of sensors for the relevant pages, and I thought it'd be cool to get a joystick in there, too. This required wiring in the KB2040 and setting it up for I2C, because there were no analog inputs on the MACROPAD. I was out of space on my breadboard (once the joystick was mounted there), so I soldered the wires directly to the KB2040 (the headers I had attached earlier sort of got in the way, actually).\u003c/p\u003e\n\n\u003cp\u003eI added the analog inputs, and programmed the KB2040 to act as an I2C peripheral that forwards the analog values (this was nearly trivial, and worked the first time!). It only took a few adjustments to use these new inputs in the MACROPAD code. The bulk of the change (which was still small) was using the joystick values on the transform page. I forgot to take a picture after connecting the new wires, but you can see most of it in the enclosure picture.\u003c/p\u003e\n","metadata":{"markdown":"## Programming\n\nI don't have any exciting pictures of programming, but I will outline how the code works and how I developed it.\n\nIf curious, you can find the full source code on [GitHub](https://github.com/afranchuk/page-playground).\n\n### Messaging\n\nI chose to use [MessagePack](https://msgpack.org/) serialization because there was a readily-available [library for Arduino (C++)](https://github.com/hideakitai/MsgPack), and the same developer has a nice [packetizing library](https://github.com/hideakitai/Packetizer) using [COBS](https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing) that can prepend a byte to indicate the message type (a feature I used). The serial connection is fairly reliable, but the use of COBS to frame packets gave me flexibility to e.g. add extra data to packets without worrying about any other code handling them.\n\nI didn't want to use text-based serialization like JSON because I wanted the modifications to be as fast and responsive as possible. The overhead of extra parsing and extra bytes over the interface may have been limiting. Yes, JSON may have worked (I didn't try, after all), but it was simple enough to just use MessagePack from the start.\n\n### Browser\n\nOn the browser side, I inject a GreaseMonkey script into every page using the [TamperMonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/) add-on. I found TamperMonkey a bit easier to use than the GreaseMonkey add-on, as the latter doesn't automatically prompt to install a script when you visit a `.user.js` URL, and to develop I needed to reload the script often.\n\nThe script is built using [bun](https://bun.com/). This was useful because it:\n- made it trivial to add dependencies (like [uCOBS](https://github.com/SudoMaker/uCOBS) and [msgpack-javascript](https://github.com/msgpack/msgpack-javascript)),\n- allowed me to use TypeScript (which caught many issues at compile/write time), and\n- made it easy to bundle static resources.\n\nThe script adds a button in the bottom-right corner of the screen. When clicked, it opens a Web Serial connection (prompting the user to choose a device).\n\n_Note_: In Firefox 151, Web Serial access also involves Firefox’s site-permission flow before the usual device chooser appears, which enforces explicit user consent to connect to a device.\n\nOnce connected, the script registers a `mousemove` event handler. As the mouse moves to new elements in the DOM, various style properties of the element are sent in a message to the device. When those properties are modified by the device, it sends back messages indicating the changes. The script applies those changes to the current element. In this way, the script is _fairly_ simple.\n\nFor the effects, the images weren't readily accessible from network URIs, so I had to bundle images as base64 in the script (I can't e.g. access them as file URIs, either). The Kit GIF in particular (even when downsized and optimized) made the script magnitudes larger: it ended up around 1MB total.\n\n### Device\n\nI usually use cross-compile toolchains and device programmers directly, but I opted for the Arduino IDE because the Adafruit guides all give (quite good) instructions around it; it was the path of least resistance! I was able to add most libraries I needed using the built-in library manager. It has been quite a while since I used the Arduino IDE, and the library manager is a welcome addition.\n\nOne issue I immediately encountered was that my laptop was auto-suspending the USB bus when there was no TX/RX for about 3 seconds. This would prevent the serial connection from working. It wouldn't disconnect, but it wouldn't be able to send from device to browser until the _browser_ sent something out. It took a while to debug this and come to the conclusion that autosuspend was to blame. I first disabled autosuspend for the entire system. When that worked, I looked closer at the TinyUSB functions (this is the USB software stack used in the RP2040), and found some functions to wake it back up. Now, before writing to the serial connection, I call:\n```cpp\nif (tud_suspended()) tud_remote_wakeup();\n```\n\nThankfully, I found all the Adafruit libraries Just Worked without any fiddling! It was very fast to get up and running. I set up peripherals and started programming the MACROPAD. I had already decided on how it was going to function: the rotary encoder would change the tool \"page\", and each page would have related properties.\n\n#### Global behavior\n\nThe device accepts two messages:\n- a reset message to set the state back to initial state when first connecting to a new page, and\n- a style info message to give information about the selected element.\n\nEach page simply displays/updates the relevant information in the style info, though when changes are made there are individual messages to affect each change (rather than sending a big message with all the changes, and relying on the browser code to check for a delta or set everything).\n\nThe MACROPAD encoder button can also be clicked to lock the selected element (not shown in the video), which prevents it from changing as the mouse changes. It is indicated by inverted colors in the element name on the OLED screen, and can be unlocked by clicking again.\n\n#### Colors\n\nThe color page allows setting the foreground and background color of an element. At one point I imagined this could be extended to alter the color palette of the entire page, but I didn't have time to implement that.\n\nYou can press and hold any of the keys to set the color using the two other rotary encoders (they alter HSL components). The color of the NeoPixel under the key reflects the associated color. You can also use the APDS-9960 color sensor to read a color, though I found that it really needs a bright input. It took a few iterations to refine the behavior, but it didn't take long to have something working. And dynamically changing the colors of elements on the screen was really cool! I was glad to see that everything was responsive and essentially instantaneous.\n\n#### Transform\n\nI worked on the transform page next, because it offered a very cool effect: you could move the device and have an element on the page move in a similar way! This uses the BNO085 IMU. I found the orientation data to be very accurate, so getting orientation to work wasn't too hard. Integrating the acceleration to movement was more tricky (as it always is), and I brought in some external [Quaternion code](https://github.com/carrino/Quaternion) to make transforming the acceleration vector simple. There's still some drift in the position, but I deemed it Good Enough.\n\nLater on, I added an analog joystick and mapped the analog values to transforms, too (limited to XY).\n\n#### Layout\n\nThe layout tool was a little more involved, mainly because there were many values to show. There was enough room on the screen to display a little box model layout, showing margins, padding, and width/height. As you click the related keys on the MACROPAD (in a 3x3 grid), you can select the left/right/top/bottom (or all of those) sides, as well as width and height. Then, the rotary encoder can be used to adjust values (pressing down to adjust padding rather than margin).\n\nI also added a feature to set a value using the APDS-9960 proximity sensor. The sensor doesn't have a long range, but it was still cool to set values in that way.\n\n#### Text\n\nThe text page was one of the last I did, only because it was somewhat straightforward. I programmed the buttons to change to common font families, and each rotary encoder changes font size and line height.\n\n#### Effects\n\nThe effects on the device were trivial to implement: clicking a button sends a message with the numbered effect to perform.\n\n## Finishing Touches\n\n### More Inputs\n\nAfter I had all of the above working, I really wanted to use a slide potentiometer to enable (and potentially scale) the use of sensors for the relevant pages, and I thought it'd be cool to get a joystick in there, too. This required wiring in the KB2040 and setting it up for I2C, because there were no analog inputs on the MACROPAD. I was out of space on my breadboard (once the joystick was mounted there), so I soldered the wires directly to the KB2040 (the headers I had attached earlier sort of got in the way, actually).\n\nI added the analog inputs, and programmed the KB2040 to act as an I2C peripheral that forwards the analog values (this was nearly trivial, and worked the first time!). It only took a few adjustments to use these new inputs in the MACROPAD code. The bulk of the change (which was still small) was using the joystick values on the transform page. I forgot to take a picture after connecting the new wires, but you can see most of it in the enclosure picture."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/429/original/more-inputs.jpg?1779217242","metadata":{}},{"element_type":"markdown","content":"\u003ch3\u003eEnclosure\u003c/h3\u003e\n\n\u003cp\u003eI picked up a plastic 6\"x4\" photo box as an enclosure. I used a drill and a utility knife to cut holes in the plastic for the hardware. It isn't the prettiest thing, but it keeps everything together as one unit and offers some modest protection.\u003c/p\u003e\n","metadata":{"markdown":"### Enclosure\n\nI picked up a plastic 6\"x4\" photo box as an enclosure. I used a drill and a utility knife to cut holes in the plastic for the hardware. It isn't the prettiest thing, but it keeps everything together as one unit and offers some modest protection."}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/002/430/original/complete.jpg?1779217265","metadata":{}},{"element_type":"markdown","content":"\u003ch2\u003eConclusion\u003c/h2\u003e\n\n\u003cp\u003eI had a lot of fun experimenting with Web Serial in Firefox. It made the browsing experience feel less like something shown to me, and more like something I can sculpt to my liking, reminding me of some of my earliest experiences with the Internet. It also excites me for what cool, interesting, and useful physical devices we may see in the future. Being able to use specialty hardware without installing special drivers or local software is liberating. This prototype was put together in a short period of time without much optimization, and even that was responsive enough to make changes on the page feel immediate and interactive. I can imagine many use cases for this class of device alone: design tweaking, accessibility development, live debugging, or just plain fun and unique experiences on the web. I can't wait to see what others come up with!\u003c/p\u003e\n","metadata":{"markdown":"## Conclusion\nI had a lot of fun experimenting with Web Serial in Firefox. It made the browsing experience feel less like something shown to me, and more like something I can sculpt to my liking, reminding me of some of my earliest experiences with the Internet. It also excites me for what cool, interesting, and useful physical devices we may see in the future. Being able to use specialty hardware without installing special drivers or local software is liberating. This prototype was put together in a short period of time without much optimization, and even that was responsive enough to make changes on the page feel immediate and interactive. I can imagine many use cases for this class of device alone: design tweaking, accessibility development, live debugging, or just plain fun and unique experiences on the web. I can't wait to see what others come up with!"}}]