Requirements
- Node.js ≥ 20. SerialPilot uses native bindings compiled against modern N-API.
- Linux, macOS, or Windows. Prebuilt binaries ship for the common architectures; on anything exotic, the package falls back to a source build (needs a C++ toolchain).
- OS-level access. On Linux that means your user is in the
dialoutgroup; on macOS and Windows there's nothing to configure.
Install
$ npm install serialpilot
That single package pulls in the native bindings, the stream wrapper, and every parser. If you want to hand-pick what you depend on:
$ npm install @serialpilot/bindings-cpp @serialpilot/stream
$ npm install @serialpilot/parser-readline
Hello, port
The shortest useful program — list available ports, open one, write a byte, and read whatever comes back as lines:
import { SerialPilot, ReadlineParser } from 'serialpilot'// 1. discover const ports = await SerialPilot.list() console.log(ports)
// 2. open const port = new SerialPilot({ path: ‘/dev/tty.usbmodem1421’, baudRate: 115200, })
// 3. parse incoming bytes into lines const lines = port.pipe(new ReadlineParser({ delimiter: ‘\n’ })) lines.on(‘data’, line => console.log(’<-’, line))
// 4. write port.write(‘PING\n’)
That's the whole shape of the library — every other feature wraps these four moves: discover, open, parse, write.
Anatomy of a port
A SerialPilot instance is a Node.js Duplex stream that wraps a binding:
┌──────────────────────────────────────────────┐
│ your code │
│ port.write(...) port.on('data',...) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ SerialPilot (Duplex stream) │ │
│ │ high-water marks · backpressure · ↑↓ │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────────────────┐ │
│ │ Binding (bindings-cpp / mock / rust) │ │
│ │ open · close · read · write · drain │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ operating system │
└──────────────────────────────────────────────┘
You almost never reach for the binding directly — but knowing it's there explains how mocking works, why prebuilds matter, and where to look when an error mentions a system call.
Try it without hardware
You can run the example above with no device plugged in by swapping the binding for the mock:
import { SerialPilotMock, ReadlineParser } from 'serialpilot' import { MockBinding } from '@serialpilot/binding-mock'MockBinding.createPort(‘/dev/ROBOT’, { echo: true })
const port = new SerialPilotMock({ path: ‘/dev/ROBOT’, baudRate: 9600 }) const lines = port.pipe(new ReadlineParser({ delimiter: ‘\n’ })) lines.on(‘data’, line => console.log(line.toString()))
port.write(‘hello\n’) // echo: lines emits ‘hello’
The same parsers work against a mock and against silicon — that's the whole point of the abstraction. See Testing & mocking for the deeper version.
Discover ports from your shell
Sometimes the fastest path is the terminal. serialpilot ships three small CLIs:
$ npx serialpilot-list --format json [{"path":"/dev/tty.usbmodem1421","manufacturer":"Arduino LLC"}]$ npx serialpilot-terminal -p /dev/tty.usbmodem1421 -b 115200 # interactive console — type, see live response
$ npx serialpilot-repl -p /dev/tty.usbmodem1421 # scriptable Node REPL withportandSerialPilotin scope
When things don't work
| Symptom | Likely cause | Fix |
|---|---|---|
PortNotFoundError | Path is wrong or device unplugged | SerialPilot.list() to enumerate; or use findPorts({ vendorId: '…' }) |
PermissionDeniedError | Linux user not in dialout | sudo usermod -aG dialout $USER & log out / back in |
PortBusyError | Arduino IDE / PuTTY / screen has the port | Close the other app; only one process can hold the port |
| Garbled bytes | Baud rate mismatch | 9600 / 115200 are the usual; consult the device datasheet |
| Native build fails on install | No prebuild for your arch + no toolchain | Install Xcode CLT / build-essential / Visual Studio C++ Build Tools |
write() seems to vanish, pipe through ReadyParser and wait for the boot banner before you start sending.