Best use case
serialport is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Teams using serialport should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/serialport/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How serialport Compares
| Feature / Agent | serialport | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
This skill provides specific capabilities for your AI agent. See the About section for full details.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Skill: serialport
## What this skill knows
This skill covers the `serialport` npm package v12 for Node.js. It covers port discovery, opening and closing connections, reading data with parsers, writing data, and error handling patterns used in the serial-monitor project.
## Installation
```bash
pnpm add serialport
pnpm add -D @types/serialport # not needed in v12 - types are bundled
```
serialport v12 ships its own TypeScript types. Do not install `@types/serialport`.
## Named imports (v12)
```ts
import { SerialPort } from 'serialport';
import { ReadlineParser } from '@serialport/parser-readline';
```
In v12, all imports use named exports. The old default export `const SerialPort = require('serialport')` is no longer valid.
## Listing available ports
```ts
import { SerialPort } from 'serialport';
const ports = await SerialPort.list();
// ports: Array<PortInfo>
// Each PortInfo has: path, manufacturer?, serialNumber?, vendorId?, productId?, pnpId?, locationId?, friendlyName?
```
`SerialPort.list()` always resolves with an array. It never rejects. If there are no ports, it resolves with `[]`.
On Linux/macOS, paths look like `/dev/ttyUSB0`, `/dev/ttyACM0`, `/dev/tty.usbserial-*`.
On Windows, paths look like `COM1`, `COM3`, etc.
## Opening a port
### autoOpen: false (recommended for async usage)
```ts
const port = new SerialPort({
path: '/dev/ttyUSB0',
baudRate: 9600,
autoOpen: false, // do not open immediately
});
await port.open(); // returns Promise<void>, throws on error
```
### autoOpen: true (default - synchronous-style open)
```ts
const port = new SerialPort({
path: '/dev/ttyUSB0',
baudRate: 9600,
// autoOpen defaults to true
});
port.on('open', () => {
console.log('Port opened');
});
port.on('error', (err) => {
console.error(err.message);
});
```
For the serial-monitor project, use `autoOpen: false` and `await port.open()` so errors can be caught in a try/catch block.
## Constructor options
```ts
new SerialPort({
path: string; // required - device path
baudRate: number; // required
dataBits?: 5 | 6 | 7 | 8; // default 8
stopBits?: 1 | 1.5 | 2; // default 1
parity?: 'none' | 'even' | 'odd' | 'mark' | 'space'; // default 'none'
autoOpen?: boolean; // default true
lock?: boolean; // default true - lock the port
})
```
## Closing a port
```ts
await port.close();
// port.isOpen is false after this
```
Always check `port.isOpen` before closing to avoid "Port is not open" errors:
```ts
if (port.isOpen) await port.close();
```
## Checking if open
```ts
port.isOpen; // boolean
```
## Reading data - ReadlineParser
The `ReadlineParser` buffers incoming bytes and emits one event per complete line.
```ts
import { ReadlineParser } from '@serialport/parser-readline';
const parser = port.pipe(new ReadlineParser({ delimiter: '\r\n' }));
parser.on('data', (line: string) => {
// line is a complete line without the delimiter
console.log(line);
});
```
The `delimiter` defaults to `'\n'` if not specified. For most microcontrollers (Arduino, ESP32) use `'\r\n'`.
### Other parsers available
| Parser | Package | Use case |
|--------|---------|---------|
| ReadlineParser | @serialport/parser-readline | Line-delimited text |
| ByteLengthParser | @serialport/parser-byte-length | Fixed-length packets |
| DelimiterParser | @serialport/parser-delimiter | Custom byte delimiter |
| InterByteTimeoutParser | @serialport/parser-inter-byte-timeout | Silence-delimited packets |
| PacketLengthParser | @serialport/parser-packet-length | Length-prefixed packets |
## Writing data
```ts
// Write a string - automatically encoded as UTF-8
port.write('AT+RST\r\n');
// Write a Buffer
port.write(Buffer.from([0xff, 0x01, 0x00]));
// Write with callback (fires after OS write buffer is flushed)
port.write('ping\n', (err) => {
if (err) console.error(err.message);
});
// Drain: wait until all written data has been transmitted
await new Promise<void>((resolve, reject) => {
port.drain((err) => err ? reject(err) : resolve());
});
```
For the serial-monitor project, `sendData` appends `\n` to the string:
```ts
export function sendData(data: string): void {
if (!activePort?.isOpen) throw new Error('Port not open');
activePort.write(`${data}\n`);
}
```
## Error handling
Common errors:
| Error message | Cause |
|--------------|-------|
| `Permission denied` | User not in `dialout` group (Linux) or no access (macOS) |
| `Port is not open` | Writing to a closed port |
| `No such file or directory` | Device disconnected or wrong path |
| `Access denied` | Port in use by another process (Windows) |
Port errors fire the `error` event:
```ts
port.on('error', (err: Error) => {
console.error('Port error:', err.message);
});
```
`openPort` errors on `await port.open()` are thrown synchronously and should be caught:
```ts
try {
await port.open();
} catch (err: any) {
// err.message contains the OS error
broadcast({ type: 'error', payload: { message: err.message } });
}
```
## Port disconnect detection
When a USB device is physically removed while open, serialport emits `close` with `disconnected: true`:
```ts
port.on('close', ({ disconnected }: { disconnected: boolean }) => {
if (disconnected) {
broadcast({ type: 'status', payload: { state: 'error', ... } });
}
});
```
## Data event on the port itself (raw bytes)
Reading data directly from the port (without a parser) gives `Buffer` chunks:
```ts
port.on('data', (chunk: Buffer) => {
console.log(chunk.toString('hex')); // for hex display mode
});
```
Use the raw `data` event when implementing the hex display mode to see individual bytes before line assembly.
## Pipe pattern
`port.pipe(parser)` returns the parser, not the port. Store the parser reference separately:
```ts
const parser = port.pipe(new ReadlineParser({ delimiter: '\r\n' }));
// parser is a Readable stream
// port is the original SerialPort instance
```
Piping does not affect `port.write` - writes still go directly to the port.
## isOpen guard pattern
```ts
export function isOpen(): boolean {
return activePort?.isOpen ?? false;
}
export function getActivePort(): string | null {
return activePort?.path ?? null;
}
```
## Testing with a mock port
For unit tests without real hardware, use `@serialport/binding-mock`:
```ts
import { SerialPort } from 'serialport';
import { MockBinding } from '@serialport/binding-mock';
SerialPort.Binding = MockBinding;
MockBinding.createPort('/dev/ttyUSB0', { echo: true, record: true });
const port = new SerialPort({ path: '/dev/ttyUSB0', baudRate: 9600, autoOpen: false });
await port.open();
// Simulate incoming data
MockBinding.getInstance('/dev/ttyUSB0').emitData(Buffer.from('temp: 24.5\r\n'));
```
## Complete openPort implementation
```ts
import { SerialPort } from 'serialport';
import { ReadlineParser } from '@serialport/parser-readline';
import { broadcast } from './websocket';
let activePort: SerialPort | null = null;
let activeParser: ReadlineParser | null = null;
export async function openPort(path: string, baudRate: number): Promise<void> {
if (activePort?.isOpen) await activePort.close();
activePort = new SerialPort({ path, baudRate, autoOpen: false });
activeParser = activePort.pipe(new ReadlineParser({ delimiter: '\r\n' }));
activeParser.on('data', (line: string) => {
broadcast({ type: 'data', payload: { line, ts: Date.now() } });
});
activePort.on('error', (err: Error) => {
broadcast({ type: 'error', payload: { message: err.message } });
});
activePort.on('close', ({ disconnected }: { disconnected: boolean }) => {
if (disconnected) {
broadcast({ type: 'status', payload: { state: 'error', port: path, baudRate, errorMessage: 'Device disconnected' } });
}
});
await activePort.open(); // throws on permission denied / no such file
broadcast({ type: 'status', payload: { state: 'connected', port: path, baudRate, errorMessage: null } });
}
```
## Common mistakes
- Importing `SerialPort` as a default export. In v12, always use `import { SerialPort } from 'serialport'`.
- Using `port.pipe(parser)` after `port.open()` - the pipe must be set up before opening so no data events are missed. With `autoOpen: false`, pipe before calling `open()`.
- Calling `port.close()` without checking `isOpen`. Closing an already-closed port throws.
- Not handling the `close` event - port can be closed by OS events (device unplugged) without calling `closePort()`.
- `SerialPort.list()` returns objects with `path`, not `comName`. v9 and earlier used `comName`; v10+ uses `path`.
- On macOS, `/dev/tty.*` ports require no DTR assertion to open. `/dev/cu.*` counterparts are caller-initiated. Prefer `/dev/cu.*` on macOS to avoid blocking on open.Related Skills
Skill: Uptime Monitoring
## Overview
Skill: Status Page
## Overview
Skill: unit-conversion
## Overview
Skill: recipe-scaler
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".
poll-builder
Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.
Skill: personal-finance
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose
Skill: Pastebin Core
## Purpose