← All work

Browser-to-firmware hardware tool

WebHID Controller Configurator

Configuring a game controller normally means downloading a desktop app. This does it in a browser tab - remapping buttons, tuning sticks and triggers, managing profiles, and flashing firmware, all over WebHID. It is built on Xuezhou Dai's open-source DualSense Tester; the work added the Xero controller as a first-class device, which meant a new device layer, its own configuration surface, and a firmware update path.

Role
Full stack engineering
Delivered
Delivered at Hybrid MediaWorks
Period
May 2025 - Present

Stack: Vue 3, TypeScript, WebHID API, Pinia, Vite

Visit the live product

~3,000 lines

of device layer between the browser tab and the firmware

Starting from someone else's app

The base project already tested DualSense controllers in the browser and had a sensible bones for it: a device router where each supported controller registers its own HID filters and panels, so a connected device is matched to its handler and the interface composes itself from there. Extending that beat rewriting it.

Adding Xero meant registering it as another device in that system, then building everything behind it - the protocol layer that actually talks to the hardware, and the configuration screens that sit on top. Working inside an existing architecture is its own constraint: the additions had to look like they belonged, not like a second app sharing a repository.

Talking to firmware from a browser tab

WebHID gives you feature reports and very little else. Everything above that is yours to build: a command and response layer with an explicit state machine - idle, sending, receiving, error - because a half-finished exchange with a device is not something you want to guess about.

Controller settings live in a binary configuration block in flash, so the app reads and writes a packed structure rather than JSON. That block is versioned and carries a magic header, which matters more than it sounds: it lets the app recognise a layout it does not understand and refuse to write, instead of corrupting a device that a user cannot recover from a web page. The mapping tables were mirrored from an existing native implementation so both tools agree on what a given byte means.

Settings that can ruin a controller

The configuration surface covers button remapping and profiles, stick sensitivity, trigger deadzones, recoil reduction, and rumble. Some combinations are genuinely bad - settings that leave a controller feeling broken to someone who will not connect the two events - so unsafe values get flagged and highlighted before anything is committed.

Every settings page has an explicit save-and-exit rather than writing as you drag a slider. Live-updating on input is right for a form and wrong for flash memory, which has a finite write budget and does not enjoy being driven by a mouse gesture.

Firmware update is the part that must not fail

Flashing firmware from a web page is the highest-consequence thing in the app. The update reports staged progress rather than a spinner, verifies what was written by hashing it back off the device instead of trusting the write, and restarts the controller cleanly at the end.

It also enforces a minimum firmware version, so a controller cannot be left sitting on a build the app can no longer talk to. That check eventually replaced the manual update button entirely - given the choice between letting people opt into an upgrade and making the safe path the only path, the second is the right call for hardware.

Want something like this built for your team?

Start a project