計測器に専用ソフトが無くても
7 分実験台の上に電源、マルチメータ、温度ロガーがある。三社の製品だ。
この三つを一枚の画面にまとめる仕事は、いつも噛み合わない。各社が自社機器用の GUI をくれるが、それは 自社の一台のためのものだ。他社のマルチメータをその画面に一緒に載せるのは、そのソフトの仕事ではない。だから結局自分で書く — ドライバを入れ、通信コードを書き、値を描く画面をまた作り、実験が変わればその全部を直す。
ところが 計測器はすでに話せる。 発明するものではなく、包むものがあるだけだ。
三社が三通りで答える
*IDN? を投げれば三つとも答える。
IDN VENDOR-A,PSU-3010,SN421337,1.4
IDN VENDOR-B,DMM-71,SN090210,2.0
IDN VENDOR-C,TL-4,SN005150,0.9ここまでは同じだ。値を問うと分かれる。
psu1:MEAS:VOLT? -> +3.29505E+00
dmm1:MEAS:CURR? -> 0.4119\r
temp1:MEAS:TEMP? -> 26.9 C三行とも数字ひとつを言っているのに、三通りだ。
- 電源は 指数表記で答える
- マルチメータは普通の十進だが キャリッジリターンを付けて来る
- 温度ロガーは 単位を値にくっつけて送る
三つとも SCPI だ。それでもこれだけ違う。そしてこれが計測コードが汚くなる本当の理由だ — プロトコルが難しいからではなく、同じプロトコルを三社が少しずつ違う使い方をしているからだ。
吸収する場所をひとつに
機器ごとに 数字を読む関数ひとつを置く。それがその会社の癖を知る唯一の場所だ。
/// `+3.30000E+00` — exponent notation. Dart parses it directly.
static double exponent(String raw) => double.parse(raw.trim());
/// `0.4125\r\n` — plain, but with a carriage return that will silently
/// break a naive parse if it is not trimmed.
static double plain(String raw) => double.parse(raw.trim());
/// `24.8 C` — the unit is glued to the value. Split it off, and keep the
/// unit out of the number rather than out of the record.
static double withUnit(String raw) => double.parse(raw.trim().split(' ').first);機器の宣言がこの関数をひとつずつ咥えている。
static const _psu = _Instrument('psu1', 'VENDOR-A', _Instrument.exponent);
static const _dmm = _Instrument('dmm1', 'VENDOR-B', _Instrument.plain);
static const _tmp = _Instrument('temp1', 'VENDOR-C', _Instrument.withUnit);四社目の機器が入れば行がひとつ増える。 分岐がコードのあちこちに広がらない。
方言がコードに届く前に消えかけた
作りながら出たことだ。最初は Dart 既定の LineSplitter で行を切っていたが、それがキャリッジリターンを先に食べてしまった。 マルチメータの \r が plain() に届きさえしなかった。
動作には問題がない。ところが その会社の癖を知る場所が、実際には何もしていない状態になる。 次の機器が \r\r\n を送ってきたときにそこで壊れ、誰もどこを見ればいいか分からない。
だから改行だけを切って渡す。
/// Lines are split on the newline only, deliberately. Dart's LineSplitter
/// would swallow a vendor's carriage return before this code ever sees it,
/// and then the one place that is supposed to know about that vendor's habit
/// would never be exercised. Terminators are a property of the instrument, so
/// they arrive intact and get dealt with once.検証が三つの方言が実際に届いたかを見る。機器が均一になれば、このサンプルは何も見せられないからだ。
idn = [s.call("bench.read")["wire"] for _ in range(3)]
assert len(idn) == 3, idn # exponent · CR-terminated · unit-attached一枚の画面
出力を入れて三つを読む。

after output on: 3.295 V · 0.4119 A · 26.9 C · 1.36 W
verbatim: psu1:MEAS:VOLT? -> +3.29505E+00 | dmm1:MEAS:CURR? -> 0.4119\r | temp1:MEAS:TEMP? -> 26.9 C画面には三社の名前が各数字の下に付いている。どの値がどの箱から来たかが画面に残る — 値三つがひとつの形になったあとは出所が消えやすいのに、実験中に数字がおかしいときまず問うべきなのがそれだ。
9 ボルトに上げる。

asked for 9 V -> meter reads 8.986 V, 40.5 C, 10.09 W温度が 26.9 から 40.5 に上がった。温度ロガーがそう答えたからであって、画面が電力から計算したのではない。
つまみは計器ではない
ここがこの記事の要点だ。30 ボルトを要求した。

asked for 30 V -> meter reads 11.982 V (the supply is rated to 12)画面の下には "asked the supply for 30.0V"、大きな数字には 11.982。
電源が自分の定格で切ったのだ。
/* The supply clamps to its own rating whatever it is told.
* The screen above may ask for 30 V; this box goes to 12. */
if (v > 12.0) v = 12.0;要求した値をそのまま大きな字で出す画面を作るのは簡単だ。そしてそれは嘘になる。計測画面が存在する理由は つまみを回した値と実際に出る値が違うからなのに、要求値を見せるならその画面を見る理由が無い。
検証がそれも守る。
thirty = s.call("supply.set_volts", {"volts": 30})
assert float(thirty["volts"]) < 12.0, "the bench limit must clamp the supply"
assert thirty["over"], thirty asked 30 V, screen shows 11.982 V — the knob is not the reading3.295 ボルトも同じだ。3.3 を設定したのに計器は 3.295 を読んだ — 負荷が掛かると電源が少し沈むからで、その 0.005 ボルトを見るためにマルチメータを付ける。
限界は実験台のものだ
55.2 度で赤い線が出た。その 45 度はどこから来たのか。
// The limit is the bench's, not the instrument's. A box does not know what
// the experiment considers too hot.
const tempLimit = 45.0;温度ロガーは 55.2 度が熱いかどうかを知らない。 ただ 55.2 と言うだけだ。熱いという判断はこの実験が何かを知る人のもので、だからその数字は機器ではなく実験台の側にある。
計測器が依然としてやる仕事
このやり方が測定を代わりにやるわけではない。確度、校正、速いサンプリング、トリガのタイミング — それは計測器ファームウェアの本業であり、そうであるべきだ。
MEAS:VOLT? を一秒ごとに問う画面は オペレータが見る計器盤であって、高速計測の経路ではない。マイクロ秒トリガで波形を捉える仕事を定義層に移すのは愚かだ。堅くなければならない測定は堅い計測器に置く。
実行・検証
$ bash verify.sh
[1/2] bench_server (dart analyze)
No issues found!
[2/2] open in AppPlayer, drive it, capture
bench-instruments: three instruments read, 9 V taken, 30 V clamped by the bench自分で動かす
cd instruments && cc -O2 -o instruments instruments.c && cd ..
( cd bench_server && dart pub get )
# In AppPlayer: add a server app — command `dart`, arguments `run bin/server.dart`,
# working directory `bench_server/`.
bash verify.sh持ち帰るもの
- 機器の宣言+パーサ三つ —
_Instrumentとexponent・plain・withUnit。四台目は行ひとつ - 終端子を保つスプリッタ — ベンダの
\rをコードに届かせる_NewlineOnlySplitter - 方言の検査 — 三つの応答形式が実際に届いたかを見る
verify.shの三行
自分の機器に合わせるには — _Instrument に行をひとつ足し、その会社が数字をどう書くかのパーサをひとつ書く。画面は bench.json のフィールド名を合わせればいい。通信が USB-TMC でも LAN でも _Bench.ask の中の話だ。
練習課題
例にデバイスをもうひとつ追加してください。変更したファイルをすべて挙げてください。ホストがその中にあってはいけません。