Tool: The Web Serial plotter
Plug an Arduino-class board into this page and see your signal live, with a spectrum, and record it to CSV. Or run the demo without hardware to learn the tool first.
You are skimming: the title, the first figure, and the short version. Switch to Read in the header for the full page, or Deep to open every deep dive.
Every hardware project on this site ends the same way: numbers stream out of a microcontroller over USB and you want to see them. This page is the plotter. It uses the browser’s Web Serial API, so nothing is installed; you plug in a board, click Connect, choose it, and the trace appears. It shows every channel, estimates the sample rate, shows a live spectrum of one channel, and records to a CSV file you can open in Python.
What the board has to send
One line per sample. Numbers separated by commas. A newline at the end. That is the whole protocol.
512,498
513,499
509,497
Two numbers per line means two channels. Eight means eight. The plotter does not care what the numbers mean; it plots them. Send raw ADC counts, or send microvolts if your firmware has already scaled them.
The Arduino sketch
Download serial_stream.ino. It reads one or two analog pins at a fixed 250 samples per second using a microsecond scheduler, so the sample rate does not wobble with how long printing takes, and prints them as a line. Change SAMPLE_US for a different rate; 1000 gives 1 kHz, which is what EMG wants. At 1 kHz with two channels, 115200 baud is enough; for eight channels at 1 kHz use 500000.
Browser support
Web Serial exists in Chrome and Edge on Windows, macOS, Linux, and Android. Firefox and Safari do not implement it. If you use one of those, download the Python plotter, which does the same job with pyserial and matplotlib:
pip install pyserial numpy matplotlib
python serial_plot.py /dev/ttyUSB0 250
On Linux you may need to add yourself to the dialout group to open the port. On macOS the port looks like /dev/cu.usbserial-XXXX. On Windows it is COM3 or similar.
Recording
Press Record. Press it again to stop and a CSV downloads with a timestamp column in milliseconds and one column per channel. Open it in Python:
import numpy as np
d = np.loadtxt('recording.csv', delimiter=',', skiprows=1)
t, ch1 = d[:, 0] / 1000, d[:, 1]
fs = 1 / np.median(np.diff(t))
print(f'{len(t)} samples at about {fs:.0f} Hz')
The Python for signals skill page takes it from there: plotting, filtering, and a spectrum with scipy.signal.welch.
Reading the spectrum panel
The spectrum is Welch’s estimate over the last two seconds of the selected channel, in decibels. It appears once the plotter has a sample-rate estimate, which takes a second. The horizontal axis stops at half the sample rate or 80 Hz, whichever is lower. A tall spike at 60 Hz is hum. A hump around 10 Hz with eyes closed is alpha. Everything else is in the artifact bestiary.