Skip to main content
Glama
kakuteki

owon-spe6103-mcp

by kakuteki
README.md
# owon-spe6103-mcp

An MCP server that lets an AI assistant read and control an **OWON SPE6103**
programmable DC power supply (60 V / 10 A / 300 W) over its USB serial port.

Built and verified against real hardware.

```
*IDN?  ->  OWON,SPE6103,24371986,FV:V5.2.0
```

## Why

Bench work often means "set 16 V, limit 0.5 A, turn it on, watch the current".
Doing that by hand breaks your flow, and it is exactly the kind of thing an
assistant can do while you keep both hands on the hardware. This server exposes
the supply as five small tools, with the dangerous one clearly marked.

## Tools

| Tool | What it does |
|---|---|
| `psu_status` | Model, port, setpoints, live measurement, output state, protection limits. Read only. |
| `psu_measure` | Live voltage / current / power only. Cheap, for polling. |
| `psu_set(volt, curr)` | Sets output voltage and current limit. **Does not change the output on/off state.** |
| `psu_output_on` | **Actually energises the terminals.** Reads back immediately and warns if the current is pinned at the limit (short or reversed polarity). |
| `psu_output_off` | Turns the output off. Setpoints are kept. |

### Safety design

- Anything above **60 V** or **10 A** is rejected **before it is sent to the instrument**.
- `psu_set` never flips the output on. You set values first, then energise — in that order.
- `psu_output_on` says so in its own description, so an assistant reading the tool
  list knows it is not a read-only call.
- After energising, the measured current is compared against the limit. Pinned at
  the limit means a short or reversed polarity, and you get told.
- The serial port is opened and closed per call, so this does not fight with other
  tools for the port.
- If the remembered port number is gone, the call **rescans and retries within the
  same call**, so the first `psu_output_off` after a replug still works.

This does **not** replace a physical means of cutting power. Software paths fail.

## Requirements

- Python 3.10+
- `pyserial`
- `mcp` (the official Python SDK; `FastMCP` ships inside it)

```
pip install pyserial mcp
```

## Connection

The SPE6103 appears as a **CH340** USB serial device (VID:PID `1A86:7523`).
The server finds it automatically. To pin it down:

```
PSU_PORT=COM8      # Windows
PSU_PORT=/dev/ttyUSB0
```

Wire protocol, confirmed on hardware: **SCPI, 115200 8N1**, commands terminated
with `\n`, replies with `\r\n`.

## Install

```
claude mcp add --scope user psu -- python /path/to/server.py
```

Then restart your client so the tools appear.

## Verified SCPI commands

| Command | Meaning | Example reply |
|---|---|---|
| `*IDN?` | Identify | `OWON,SPE6103,24371986,FV:V5.2.0` |
| `VOLT?` / `VOLT <v>` | Voltage setpoint | `16.000` |
| `CURR?` / `CURR <a>` | Current limit setpoint | `0.500` |
| `VOLT:LIM?` | Over-voltage protection | `40.000` |
| `CURR:LIM?` | Over-current protection | `10.200` |
| `MEAS:VOLT?` `MEAS:CURR?` `MEAS:POW?` | Live measurement | `0.000` |
| `MEAS:ALL?` | Live voltage and current | `0.000,0.000` |
| `OUTP?` / `OUTP ON` / `OUTP OFF` | Output state | `OFF` |

`SYST:STAT?` returns `ERR` on this firmware. Do not use it.

## Three gotchas, all measured on hardware

**1. Read replies with `read_until(b"\n")`, not a fixed byte count.** A fixed-size
read waits out the full timeout on every query, which turns a 5 ms exchange into
an 800 ms one — enough to break a one-second sampling loop.

**2. Do not measure immediately after `OUTP ON` or a setpoint change.** The output
ramps, and the instrument's own measurement only refreshes about every 0.3 s.
Asking for 16 V and reading straight away gives you 2.5 V. Measured ramp, no load:

```
0.11s  0.000      0.92s  12.010
0.27s  0.160      1.08s  15.690
0.43s  0.160      1.40s  15.960
0.59s  6.150      1.56s  16.000   <- settled
0.75s  6.150
```

Note the repeated values. Because the reading refreshes slower than you can poll,
*two identical samples do not mean settled* — a naive "wait until two reads match"
detector latches onto `0.160` or `6.150` and reports a mid-ramp value as final.
This server waits 1.2 s, then polls slower than the refresh (0.35 s) and requires
**three** consecutive matching samples.

This matters beyond cosmetics: with a capacitive load, inrush during the ramp will
sit at the current limit, and a naive reader will report it as a short.

**3. The COM number changes every time you replug, so recover inside the call, not
on the next one.** On this bench it went `COM18 -> COM10 -> COM9 -> COM11`. Caching
the port and only forgetting it *after* an open fails is not good enough: the call
that discovers the change always fails, and the user learns that "the first call
errors and the second one works". That is merely annoying for a read, and dangerous
for `psu_output_off`, which is what you reach for as an emergency stop.

So a transaction that cannot open its remembered port rescans immediately and
retries once, in the same call. If the rescan finds nothing, you get the same
clear "not found" error as before. When a rescan did happen, the return value says
so, including the old and new port numbers — a silent recovery would hide a
flaky cable.

The rescan still identifies every candidate with `*IDN?` and demands `SPE6103`.
Grabbing whichever CH340 happens to be free is exactly the accident this guards
against, and a port that vanished is *more* likely to have been renumbered onto a
neighbour, not less. For the same reason, a port that opens but does not answer is
treated as gone rather than as a slow instrument, and write-only commands
(`OUTP OFF` and friends) send `*IDN?` first in the same transaction: a write gets
no reply, so without that check a renumbered port could take an `OUTP OFF` meant
for the supply and hand it to an ESP32.

### Why `psu_output_off` has no priority lock

It would look sensible to let the emergency stop jump the queue. It would not help.
MCP tool calls are handled serially by the client, so `psu_output_off` waits in
that queue long before it reaches this server's lock — measured earlier at 2.88 s
behind a slow call. A lock priority cannot shorten a wait that happens upstream of
the lock. The real remedy is the one already in place: every tool returns quickly,
the port is held for one transaction at a time, and no tool sleeps while holding it.
Adding a second locking scheme would add deadlock risk for no measurable gain.

Other OWON SPE/SPM models most likely speak the same dialect, but only the
SPE6103 has been tested.

---

# 日本語

**OWON SPE6103**(60V / 10A / 300W のプログラマブル直流電源)を、USB のシリアル経由で
読み書きするための MCP サーバーです。実機で確認しながら作りました。

## 道具

| 道具 | 内容 |
|---|---|
| `psu_status` | 機種・ポート・設定値・実測・出力の入切・保護値。読むだけ |
| `psu_measure` | 実測(電圧・電流・電力)だけ。軽いので見張りに使う |
| `psu_set(volt, curr)` | 電圧と電流制限を設定する。**出力の入切は変えない** |
| `psu_output_on` | **実際に電気が出る。**直後に実測を読み、電流が制限に張り付いていれば短絡・逆接の疑いとして警告する |
| `psu_output_off` | 出力を切る。設定値は残る |

## 安全のきまり

- **60V / 10A を超える指示は、機器に送る前に断る**
- `psu_set` は出力を入れない。値を決めてから入れる、という順序を守れる
- `psu_output_on` は説明文の先頭で「実際に電気が出る」と明示している
- 入れた直後に実測を読み、電流が制限に張り付いていれば警告する
- ポートは呼び出しごとに開閉するので、他の道具と取り合わない
- 覚えているポート番号が消えていたら、**その呼び出しの中で探し直してもう一度試す**。
  挿し直した直後の1回目の `psu_output_off` も通る

**これは電源を物理的に切る手段の代わりにはなりません。** ソフトの経路は落ちます。

## 接続

CH340 の仮想シリアル(VID:PID `1A86:7523`)として見えます。自動で探しますが、
`PSU_PORT` で固定もできます。通信は **SCPI / 115200bps 8N1**、送りは `\n`、返しは `\r\n`。

## 罠(どちらも実機で測って分かったこと)

**1. 応答は `read_until(b"\n")` で読むこと。**バイト数を決めて読むと、その数が来ないので
毎回タイムアウトを待ち切ります。5ms で済む問い合わせが 800ms になり、
1秒ごとの記録が崩れます。

**2. 出力を入れた直後・設定を変えた直後に、そのまま実測を読まないこと。**
出力はゆっくり立ち上がり、しかも**機器側の実測の更新が約0.3秒ごと**です。
16V を指示した直後に読むと 2.5V が返ります。無負荷での立ち上がりの実測:

```
0.11s  0.000      0.92s  12.010
0.27s  0.160      1.08s  15.690
0.43s  0.160      1.40s  15.960
0.59s  6.150      1.56s  16.000   ← 到達
0.75s  6.150
```

同じ値が2回続いているところに注意してください。更新より速く読むとこうなるので、
**「2回同じなら落ち着いた」という判定は途中の値を掴みます**(`0.160` や `6.150` で止まる)。
このサーバーは 1.2 秒待ってから、更新より遅い 0.35 秒間隔で、**3回**続けて一致を見ています。

これは見た目の問題では済みません。容量のある負荷では、立ち上がり中の突入電流が
電流制限に張り付くので、**素直に読むと「短絡」と誤判定します**。

**3. COM 番号は挿し直すたびに変わる。復帰は「次の呼び出し」ではなく「その呼び出しの中」で。**
この机では `COM18 → COM10 → COM9 → COM11` と変わりました。ポートを覚えておいて、
開けなくなって**から**覚えを捨てる作りでは足りません。変化に気づいた呼び出しは必ず失敗するので、
利用者から見ると「1回目は必ずエラー、2回目で直る」になります。読むだけならただ煩わしいだけですが、
緊急停止に使う `psu_output_off` でこれが起きるのは危険です。

そこで、覚えている番号で開けなかったやりとりは、その場で探し直して1回だけやり直します。
探し直しても見つからなければ、従来どおりの分かりやすい「見つからない」エラーを返します。
探し直したときは、元の番号と新しい番号を返り値に書きます。黙って復帰すると、
線の接触不良を見逃すからです。

探し直しでも `*IDN?` の確認は必ず通し、`SPE6103` と名乗らないものは掴みません。
空いていた CH340 を掴むのは、まさにこの確認が防いでいる事故です。消えた番号は
**隣の機器に振り直されている可能性がむしろ高い**ので、ここで確認を省く理由はありません。
同じ理由で、開けたのに答えないポートは「遅い機器」ではなく「もういない」とみなします。
また、答えの返らない書き込み(`OUTP OFF` など)は、同じやりとりの中でまず `*IDN?` を確かめてから
送ります。書き込みは答えが返らないので、この確認が無いと、電源に向けたつもりの `OUTP OFF` が
ESP32 に届いても気づけません。

### `psu_output_off` に優先権を付けなかった理由

緊急停止だけ列に割り込ませたくなりますが、効きません。MCP の呼び出しは**利用者側で直列に処理される**ので、
`psu_output_off` はこのサーバーの鍵にたどり着くずっと前に、その列で待たされます
(以前の実測で 2.88 秒)。鍵の優先度では、鍵より手前で起きている待ちは縮められません。
効く手は既に入っている方、つまり「どの道具も素早く返す」「ポートは1回のやりとりの間だけ握る」
「握ったまま眠らない」です。二つ目の鍵の仕組みを足すと、得るものが無いまま
すくみ(デッドロック)の危険だけが増えます。

`SYST:STAT?` はこの版では `ERR` を返すので使いません。

## ライセンス

MIT