Hey everyone, Alex here. Welcome back to another edition of Coding with Alex at sysseder.com.
If you're anything like me, your desk is probably a graveyard of hardware. Maybe you've got an old mechanical keyboard, a couple of Arduino boards, or, if you're musically inclined, a dusty hardware synthesizer from the late 1900s (yes, the 90s) sitting in the corner. For the longest time, interacting with these devices required heavy, native, platform-specific desktop software. You had to download a sketchy .exe or .dmg file from an unencrypted FTP server just to update a firmware or back up your patches.
But the web has quietly been undergoing a massive revolution. Recently, a project hit the top of Hacker News: a developer built a browser-native SysEx librarian for 80s/90s synthesizers. For the uninitiated, SysEx (System Exclusive) is a MIDI protocol used to transmit raw data dumps—like synthesizer presets, custom waveforms, or firmware—between a computer and hardware.
This got me thinking: as web developers, we often restrict our thinking to HTTP requests, database transactions, and DOM rendering. But the modern web browser is now fully capable of acting as a direct interface to physical hardware via APIs like WebMIDI, WebHID, and WebUSB.
Today, we're going to dive deep into how these browser-native hardware APIs work, why they are a massive win for user experience and security, and how you can write code to talk to physical hardware directly from a standard web page.
The Evolution: Why Hardware is Moving to the Browser
Traditionally, writing hardware-interfacing software meant dealing with the nightmare of cross-platform desktop development. You had to choose a framework like Electron (which bloats your app's footprint to 150MB+), manage native C++ bindings for USB/MIDI libraries, handle driver installations (looking at you, Windows COM ports), and sign your installers so OS-level gatekeepers wouldn't block your users.
Web APIs change the paradigm entirely. By moving hardware configuration to the browser, you get:
- Zero Installation: Users just visit a URL. No drivers, no installers, no bloat.
- Instant Cross-Platform Support: If the browser supports the API, it works on Windows, macOS, Linux, and ChromeOS.
- Sandboxed Security: Browsers act as gatekeepers. Pages cannot silently access connected hardware; they require explicit, user-initiated permission prompts.
Let's look under the hood at how we can actually build one of these systems using the WebMIDI API to send and receive binary data over a hardware port.
Understanding WebMIDI and SysEx
MIDI (Musical Instrument Digital Interface) isn't just about playing notes. It’s a 31.25 kbps serial protocol developed in 1983 that is still the industry standard today. While standard MIDI messages control things like "note on" or "pitch bend," SysEx (System Exclusive) messages allow manufacturers to send arbitrary binary payloads. This is how we perform backups, restore patches, and execute firmware updates on hardware.
A SysEx message is structured as a sequence of bytes:
0xF0- The Start of Exclusive (SOX) status byte.Manufacturer ID- One to three bytes identifying the hardware maker (e.g., Roland, Korg, or a development ID).Data bytes- The payload (traditionally restricted to 7-bit bytes, meaning values from0x00to0x7F).0xF7- The End of Exclusive (EOX) status byte.
Let's write a JavaScript utility to access connected MIDI devices and transmit a system exclusive dump.
Hands-On: Accessing MIDI and Sending SysEx in JavaScript
To use WebMIDI, you must request permission from the user. Because SysEx messages can potentially modify device firmware, browsers treat them as a privileged operation. We must explicitly request SysEx access when invoking navigator.requestMIDIAccess().
Step 1: Requesting Access
async function initializeMIDI() {
if (!navigator.requestMIDIAccess) {
console.error("WebMIDI is not supported in this browser.");
return null;
}
try {
// We must pass sysex: true to enable raw binary transmissions
const midiAccess = await navigator.requestMIDIAccess({ sysex: true });
console.log("MIDI Access granted!");
return midiAccess;
} catch (error) {
console.error("Access to MIDI devices denied by user.", error);
return null;
}
}
Step 2: Listing Connected Devices
Once access is granted, we can inspect midiAccess.inputs and midiAccess.outputs. These are map-like structures containing the hardware interfaces detected by the operating system.
function logDevices(midiAccess) {
console.log("--- Inputs ---");
midiAccess.inputs.forEach((input) => {
console.log(`Input Port [ID: ${input.id}] Name: ${input.name} Vendor: ${input.manufacturer}`);
});
console.log("--- Outputs ---");
midiAccess.outputs.forEach((output) => {
console.log(`Output Port [ID: ${output.id}] Name: ${output.name}`);
});
}
Step 3: Receiving Data from Hardware
To read patch dumps or real-time control changes from our hardware, we bind a listener to the onmidimessage property of an input port. The incoming message is exposed as a Uint8Array via event.data.
function startListening(midiAccess, targetInputId) {
const input = midiAccess.inputs.get(targetInputId);
if (!input) {
console.error("Input device not found.");
return;
}
input.onmidimessage = (event) => {
const data = event.data;
console.log(`Received ${data.length} bytes from hardware:`, data);
// Check if it's a SysEx message
if (data[0] === 0xF0 && data[data.length - 1] === 0xF7) {
console.log("Valid SysEx packet received! Processing payload...");
processSysExPayload(data);
}
};
console.log(`Listening on ${input.name}...`);
}
function processSysExPayload(data) {
// Extract manufacturer, model, and data payload
const manufacturerId = data[1];
const payload = data.slice(2, -1); // Strip F0 and F7
console.log(`Manufacturer ID: 0x${manufacturerId.toString(16)}`);
console.log("Payload data: ", payload);
}
Step 4: Writing (Sending) a SysEx Packet to the Device
Now, let's write data back to the machine. Let's say we want to trigger a patch backup request. We construct our binary packet using a Uint8Array and send it directly to the output port.
async function sendSysExBackupRequest(midiAccess, targetOutputId) {
const output = midiAccess.outputs.get(targetOutputId);
if (!output) {
console.error("Output device not found.");
return;
}
// Example: Generic SysEx packet structure
// 0xF0 (Start), 0x7E (Non-Realtime ID), 0x06 (General Info request), 0xF7 (End)
const identityRequest = new Uint8Array([0xF0, 0x7E, 0x7F, 0x06, 0x01, 0xF7]);
console.log(`Sending Identity Request to ${output.name}...`);
output.send(identityRequest);
}
The Security Architecture of Browser-Hardware APIs
You might be thinking: *"Wait a minute. If any website can talk directly to connected USB or MIDI devices, isn't that a massive security hazard?"*
Absolutely. If malicious scripts could arbitrarily query your USB bus or flood your MIDI controller with high-voltage firmware updates, it would be a disaster. Because of this, the W3C and browser vendors (principally Chromium) have designed strict security guardrails around WebMIDI, WebHID, and WebUSB:
1. Secure Contexts (HTTPS)
These APIs are strictly restricted to Secure Contexts. You cannot access navigator.requestMIDIAccess or navigator.hid on an unencrypted HTTP connection (with the standard exception of localhost for development).
2. Explicit User Consent and Transient Activation
You cannot trigger a hardware request programmatically on page load. The browser requires **transient user activation** (like a click on a "Connect Device" button). When triggered, the browser pops up a native UI permission prompt listing only the devices the user explicitly selects.
3. Blocklists
WebUSB and WebHID maintain a strict blocklist of device classes. For instance, the browser will block access to USB mass storage devices, keyboard/mouse human interface devices (to prevent malicious keystroke injection/keylogging), and smart card readers. You can't just hijack a user's primary keyboard; you must target specific, non-critical vendor/product IDs.
Going Beyond MIDI: WebHID and WebUSB
While the Hacker News project showcased WebMIDI, the exact same architectural principles apply to other hardware devices using WebHID (Human Interface Device) and WebUSB.
For example, if you are building an interface to configure custom macro keys on a mechanical keyboard, or change RGB lighting profiles on a stream controller, you would use WebHID. Here's a brief look at how WebHID establishes a secure channel to write a feature report:
async function configureHidDevice() {
try {
// Request permission to access a specific custom controller
const devices = await navigator.hid.requestDevice({
filters: [{ vendorId: 0x1234, productId: 0xabcd }]
});
if (devices.length === 0) return;
const device = devices[0];
await device.open();
console.log(`Connected to HID device: ${device.productName}`);
// Write an 8-byte command to update RGB status (Report ID: 0x01)
const reportId = 0x01;
const rState = new Uint8Array([0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
await device.sendReport(reportId, rState);
console.log("RGB state configuration sent!");
} catch (error) {
console.error("HID Communication failed", error);
}
}
Wrapping Up: The Browser is the New Desktop
The boundary between native desktop applications and web applications is disintegrating. Projects like the browser-native SysEx librarian prove that with WebMIDI, WebHID, and WebUSB, we can deliver highly specialized, zero-install, secure hardware configuration utilities straight to our users via standard web pages.
Next time you find yourself building a companion app for an IoT project, an internal physical automation system, or an interface for custom media controllers, step back from native desktop frameworks. The web platform might already have everything you need built right in.
What are your thoughts? Have you built anything that bridges the gap between hardware and the web? Or are you still wary of browsers accessing the USB stack? Let me know in the comments below!
Until next time, keep coding.
— Alex