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

$curl -o ~/.claude/skills/serialport/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/hardware-iot/serial-monitor/skills/serialport/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/serialport/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How serialport Compares

Feature / AgentserialportStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/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

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

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

7
from heldernoid/agentic-build-templates

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

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose

Skill: Pastebin Core

7
from heldernoid/agentic-build-templates

## Purpose