I Wanted to Watch Every Pin On My Arduino At Once
So I Built a Single-File HTML Console to do Just That
A few weekends back I was trying to debug a sensor wired into an Uno R4 on my bench. Voltage looked fine on the multimeter. The serial monitor was spitting numbers but I could not see the shape of them. Was the signal drifting? Glitching? Picking up hum from the bench supply? I had no way to look at twenty seconds of pin behavior at a glance without writing a custom script for that specific problem.
This is a thing every embedded developer has lived through. You want something between printf statements and pulling out the proper bench oscilloscope. You want a window into the board that shows you everything it is doing right now, calibrated, time-aligned, with a strip chart, an FFT if you need one, alerts you can set, an I2C scanner when the bus goes quiet for no reason. A field instrument for the microcontroller itself.
What I had was the Arduino IDE Serial Monitor, which is a tiny window showing numbers scrolling by.
That mild irritation turned into a six-week build that I now use every time I touch a board. It is one HTML file. No build step, no framework, no install. I am calling it PinScope, and the source is up at:
https://github.com/mbparks/pinscope
The docs site is at https://mbparks.github.io/pinscope/.
What it does
PinScope is a browser console for any Arduino-compatible board that can speak a tiny JSON line protocol. You flash one of the firmware sketches that ship with the repo (serial, BLE, or MQTT variants). You open pinscope.html in Chrome or Edge. You click a transport button. The page connects and you get this:
A live map of every digital and analog pin. Click a pin’s mode badge to cycle through input, input-pullup, output, PWM, or an interrupt-counted frequency mode for pulses and tachometers. Outputs toggle on click; PWM pins get a duty slider. Analog channels stream into a strip chart that holds up to 14 traces with per-channel stats, calibration in real units, threshold alerts, and oscilloscope-style triggering on rising or falling edges. There is an FFT tab and a waterfall spectrogram for the frequency-domain stuff, an XY scatter for Lissajous-style plots, an I2C scanner with four polling slots that feed virtual channels at up to 50 Hz, and a cross-pin math engine that lets you derive any channel as an expression of any other channels.
You can save the whole device’s configuration as a JSON file and restore it later. You can capture a run and replay it. You can stream samples straight to a CSV file as they arrive. There is a small scripted automation sandbox if you want to ramp a PWM pin and log the response. There is a sandboxed plugin system if you want to add your own panel that shows something I have not thought of.
All of it is one file. Once the page loads it makes zero further network requests. You can serve it from a Raspberry Pi, drop it on a USB stick, run it from a thumb drive in the field, embed it inside a CI pipeline, or pin it to localhost. It is the only file there is.
The hard parts were the boring parts
The interesting-looking pieces of this project (strip chart, FFT, spectrogram) were almost free. There are good algorithms for that stuff, JavaScript on a modern laptop can comfortably FFT a few thousand samples sixty times a second, and the visuals are just <canvas> work. None of that was where the weeks went.
The weeks went into the seams.
I needed the wire protocol to be small enough that any embedded person could speak it from a fresh sketch in an hour. That meant JSON over newline-delimited lines, hand-parsed on the firmware side without pulling in a JSON library. Each command and response had to be a single object on a single line, short enough to fit on a 9600-baud link if it had to.
I needed transports to be interchangeable. The same browser-side device card had to work whether bytes were coming over Web Serial, a WebSocket to a custom firmware, an MQTT topic, or a Bluetooth GATT characteristic. The transports each have their own quirks, and the device code had to not care which one was underneath.
I needed calibration to behave. A two-point fit wizard sounds simple until you realize the user might apply the same reference for both captures, and the math now divides by zero. The wizard refuses to fit in that case and tells you why, instead of silently producing a calibration that pegs every reading to one value.
I needed the plugin system to be safe. Anyone running PinScope can paste arbitrary JavaScript into the plugin manager and load it. That JavaScript runs inside an iframe sandboxed to allow-scripts only, with no same-origin access, no network, and a randomized postMessage bridge id so plugins cannot impersonate each other. The plugin source gets embedded into the sandbox via JSON.stringify and eval’d, which prevents template-literal breakout attacks on the host. There is a two-line dance around splitting <scr + ipt> tags so the host HTML parser does not terminate the script block early when a plugin’s srcdoc contains the closing tag as a string. Tiny detail, total silent failure if you do not handle it.
I needed the firmware to identify itself. The Arduino Uno R3 has a 10-bit ADC. The Uno R4, Nano 33 IoT, and Uno Q all have 12-bit ADCs. The firmware detects the host MCU at compile time and reports its adcMax value in the hello packet, so the browser-side scaling lines up with reality without the user ever having to set it.
None of these are interesting in isolation. All of them were necessary. This is the part of every project that nobody writes blog posts about, and it is where most of the actual craft lives.
What I keep relearning
The gap between “I can imagine the tool” and “the tool works reliably on the real-world inputs people will throw at it” is wider than it ever feels at the start. I wrote about this same gap a few months ago in the Gerber Viewer post, and here it is again. Two completely different projects, same lesson.
The first half of any maker project is the part you can picture in your head. The second half is the part the real world hands you that you had no way of picturing. The 1200-bps touch-to-reset that some Arduino bootloaders need. The fact that browsers terminate inline scripts on the first literal </script> they see, including ones inside strings. The way Google Fonts loads asynchronously, which means your hero font is fine online and looks wrong in any offline test environment. The 50 Hz upper cap on the wire protocol that comes from how many bytes will actually fit per state packet at 115200 baud, not from any clean theoretical reason.
Plan for that second half. Budget for it. Do not be surprised by it. Half the time it is the second half that turns a project into something worth keeping.
Where to find it
PinScope is open source under GPL-3.0.
Source, install instructions, and the full feature reference: https://github.com/mbparks/pinscope
Docs site with screenshots and the bring-up walkthrough: https://mbparks.github.io/pinscope/
The hardware bring-up checklist (Arduino Uno Q on macOS, phase-by-phase): https://mbparks.github.io/pinscope/bringup.html
The firmware compiles for classic Arduino Uno, Uno R4 WiFi, Nano 33 IoT, and (experimentally) Uno Q. Real bench-side verification on Uno Q is the next thing on my list. If you give it a try and something breaks, the bug-report shape that helps me most is which bring-up phase you got to, what the pass criterion was, and what actually happened.
If you want me to build something like this for your project, hardware diagnostics or otherwise, that is what Green Shoe Garage does for a living. Reach out.





