Best use case

serial-monitor is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Teams using serial-monitor 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/serial-monitor/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/hardware-iot/serial-monitor/skills/serial-monitor/SKILL.md"

Manual Installation

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

How serial-monitor Compares

Feature / Agentserial-monitorStandard 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: serial-monitor

## What this skill knows

This skill covers the serial-monitor application: a pnpm-workspaces monorepo with a Node.js/Express backend and a React/Vite frontend. It covers the server-side serial port management, WebSocket broadcast layer, REST API routes, and the React store and hook architecture.

## Monorepo layout

```
serial-monitor/
  packages/
    server/src/
      index.ts        - Express + WS server entry point
      ports.ts        - SerialPort.list() wrapper
      connection.ts   - single active port management
      websocket.ts    - ws.Server + broadcast helper
      parser.ts       - parseNumeric from data lines
      routes/
        ports.ts      - GET /api/ports
        connect.ts    - POST /api/connect, DELETE /api/connect
        send.ts       - POST /api/send
        status.ts     - GET /api/status
    client/src/
      components/
        PortSelector/
        SerialOutput/
        LineChart/
        SendBar/
        ConnectionStatus/
      pages/MonitorPage.tsx
      store/useSerialStore.ts
      hooks/useWebSocket.ts
```

## Environment variables

```
PORT=3001            # Express server port
WS_PORT=3002         # WebSocket server port
ALLOWED_ORIGIN=http://localhost:5173
```

Boolean env vars: `0` = false, `1` = true.

## API routes

| Method | Path | Body | Response |
|--------|------|------|----------|
| GET | `/api/ports` | - | `PortInfo[]` |
| POST | `/api/connect` | `{ port, baudRate }` | `200 OK` or `400` |
| DELETE | `/api/connect` | - | `200 OK` |
| POST | `/api/send` | `{ data }` | `200 OK` or `400` |
| GET | `/api/status` | - | `ConnectionInfo` |

## WebSocket message types

All messages are JSON with `{ type, payload }` shape.

```ts
type WsMessage =
  | { type: 'data';   payload: { line: string; ts: number } }
  | { type: 'status'; payload: ConnectionInfo }
  | { type: 'ports';  payload: PortInfo[] }
  | { type: 'error';  payload: { message: string } };
```

On new client connect: server immediately sends `status` and `ports` messages.

## connection.ts - key rules

- Only one active `SerialPort` instance at a time. Opening a new port closes the current one first.
- Use `ReadlineParser` with `delimiter: '\r\n'` to emit complete lines.
- `sendData` appends `\n` to the string before writing to the port.
- `broadcast` is imported from `websocket.ts` - it must be called from within `connection.ts` on data and error events.

```ts
// Correct open pattern
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() } });
});
await activePort.open();
```

## parser.ts

```ts
export function parseNumeric(line: string): number | null {
  const match = line.match(/-?\d+(\.\d+)?/);
  if (!match) return null;
  const n = parseFloat(match[0]);
  return isNaN(n) ? null : n;
}
```

This extracts the first integer or float from the line. It handles:
- `"42.5"` -> `42.5`
- `"temp: 24.5"` -> `24.5`
- `"x=99"` -> `99`
- `"SENSOR,24.5,1013"` -> `24.5`
- `"hello world"` -> `null`

## useSerialStore.ts (Zustand)

```ts
interface SerialStore {
  connection: ConnectionInfo;
  lines: DataLine[];        // capped at 500
  chartValues: number[];    // capped at 200
  ports: PortInfo[];
  selectedPort: string;
  selectedBaudRate: number;
  setConnection(info: ConnectionInfo): void;
  addLine(line: DataLine): void;
  setChartValues(values: number[]): void;
  setPorts(ports: PortInfo[]): void;
  setSelectedPort(port: string): void;
  setSelectedBaudRate(rate: number): void;
  clearLines(): void;
}
```

Capping logic for `addLine`:

```ts
addLine: (line) => set((s) => {
  const lines = [...s.lines, line].slice(-500);
  const chartValues = line.numeric !== null
    ? [...s.chartValues, line.numeric].slice(-200)
    : s.chartValues;
  return { lines, chartValues };
}),
```

## useWebSocket.ts

- Connects to `ws://localhost:${WS_PORT}` on mount.
- On `onclose`: schedules reconnect after 2000ms.
- On unmount: calls `ws.close()` and clears the reconnect timer.
- Message handler dispatches to store:
  - `data` -> `addLine` + numeric value appended to chart
  - `status` -> `setConnection`
  - `ports` -> `setPorts`
  - `error` -> `addLine` with `direction: 'rx'` and a prefixed error text

```ts
function handleMessage(msg: WsMessage) {
  const store = useSerialStore.getState();
  if (msg.type === 'data') {
    const { line, ts } = msg.payload;
    const numeric = parseNumeric(line); // client-side parse for chart
    store.addLine({ id: crypto.randomUUID(), timestamp: ts, text: line, numeric, direction: 'rx' });
  } else if (msg.type === 'status') {
    store.setConnection(msg.payload);
  } else if (msg.type === 'ports') {
    store.setPorts(msg.payload);
  } else if (msg.type === 'error') {
    store.addLine({ id: crypto.randomUUID(), timestamp: Date.now(), text: `ERROR: ${msg.payload.message}`, numeric: null, direction: 'rx' });
  }
}
```

## SerialOutput auto-scroll

Track scroll state with a ref. Only auto-scroll if the user has not manually scrolled up.

```ts
const containerRef = useRef<HTMLDivElement>(null);
const userScrolled = useRef(false);

function onScroll() {
  const el = containerRef.current;
  if (!el) return;
  const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
  userScrolled.current = !atBottom;
}

useEffect(() => {
  if (!userScrolled.current && containerRef.current) {
    containerRef.current.scrollTop = containerRef.current.scrollHeight;
  }
}, [lines]);
```

Show a "Jump to bottom" button when `userScrolled.current === true`.

## LineChart configuration (Chart.js)

```ts
const options: ChartOptions<'line'> = {
  animation: false,          // required for real-time performance
  responsive: true,
  maintainAspectRatio: false,
  scales: {
    x: { display: false },
    y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#6b7280', font: { size: 11 } } },
  },
  datasets: {
    line: { pointRadius: 0, borderColor: '#d97706', borderWidth: 2,
            backgroundColor: 'rgba(217,119,6,0.15)', fill: true },
  },
};
```

Update data without full re-render:

```ts
useEffect(() => {
  if (!chartRef.current) return;
  chartRef.current.data.labels = chartValues.map((_, i) => i);
  chartRef.current.data.datasets[0].data = chartValues;
  chartRef.current.update('none'); // 'none' skips animation
}, [chartValues]);
```

## SendBar - POST /api/send

```ts
async function handleSend() {
  if (!value.trim()) return;
  const res = await fetch('/api/send', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ data: value }),
  });
  if (!res.ok) {
    // show error toast
    return;
  }
  // add sent line to store directly
  store.addLine({ id: crypto.randomUUID(), timestamp: Date.now(), text: value, numeric: null, direction: 'tx' });
  setValue('');
}
```

## Port polling

```ts
useEffect(() => {
  async function poll() {
    const res = await fetch('/api/ports');
    if (res.ok) store.setPorts(await res.json());
  }
  poll();
  const id = setInterval(poll, 5000);
  return () => clearInterval(id);
}, []);
```

## Common baud rates

9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600. Default: 9600.

## Common mistakes

- Not calling `activePort.pipe(parser)` before `activePort.open()` - parser events will not fire.
- Calling `broadcast` before the WebSocket server is initialized. Initialize `ws.Server` in `index.ts` and export `broadcast` before the server starts.
- Storing `chartValues` as mutable array in Zustand and mutating it in place. Always use `.slice(-200)` to return a new array.
- Using `ws.send` inside the `connection` event before checking `ws.readyState === WebSocket.OPEN`.
- Vite proxy: in `vite.config.ts`, proxy `/api` to `http://localhost:3001` and `/ws` or add `ws: true` to the proxy config for the WebSocket.

Related Skills

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

serialport

7
from heldernoid/agentic-build-templates

No description provided.

ssl-cert-monitor

7
from heldernoid/agentic-build-templates

Operate ssl-cert-monitor -- add hosts, configure alert rules, trigger checks, review history, and deploy the stack.

backup-monitor

7
from heldernoid/agentic-build-templates

Track backup jobs via heartbeat pings, alert on missed or failed backups. Use when you need to monitor scheduled backup scripts, get alerted when a backup misses its window, or track backup execution history. Triggers include "backup monitoring", "backup alerts", "missed backup", "backup heartbeat", "backup job tracking", or any task involving backup reliability verification.

cron-monitor

7
from heldernoid/agentic-build-templates

Send heartbeat pings to cron-monitor after cron job completion, check job status, and register new jobs. Use when you need to confirm a scheduled task ran successfully, check if a cron job is healthy, or add monitoring to a new cron script. Triggers include "ping cron-monitor", "check job status", "register cron job", "heartbeat", "cron health check", or any task involving scheduled job monitoring.

database-size-monitor

7
from heldernoid/agentic-build-templates

Dashboard for monitoring PostgreSQL and MySQL table sizes over time, with growth tracking, threshold alerts, and snapshot comparison

data-pipeline-monitor

7
from heldernoid/agentic-build-templates

Track ETL and data pipeline jobs with success/failure status, duration tracking, heartbeat monitoring, and dependency visualization. Use when you need to monitor scheduled jobs, detect failures, track pipeline health over time, or visualize ETL step dependencies. Triggers include "pipeline monitoring", "job tracking", "ETL status", "cron job health", "heartbeat monitor", "pipeline failed", or any task involving monitoring data workflows.

process-monitor

7
from heldernoid/agentic-build-templates

Monitor system processes for resource usage using process-tree watch mode. Use when tracking CPU or memory usage over time, finding resource hogs, or watching a specific process. Triggers include "monitor processes", "watch cpu usage", "process monitor", "top processes", "resource usage", "ptree watch".

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.