計測器に専用ソフトが無くても

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

一枚の画面

出力を入れて三つを読む。

BENCH — 3.295 volts VENDOR-A · 0.4119 amps VENDOR-B · 26.9 celsius VENDOR-C · 1.36 W

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 ボルトに上げる。

8.986 volts · 1.1233 amps · 40.5 celsius · 10.09 W

asked for 9 V -> meter reads 8.986 V, 40.5 C, 10.09 W

温度が 26.9 から 40.5 に上がった。温度ロガーがそう答えたからであって、画面が電力から計算したのではない。

つまみは計器ではない

ここがこの記事の要点だ。30 ボルトを要求した。

11.982 volts · 1.4977 amps · 55.2 celsius · 17.95 W — over the bench limit of 45.0 C

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 reading

3.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

持ち帰るもの

  1. 機器の宣言+パーサ三つ — _Instrument と exponent・plain・withUnit。四台目は行ひとつ
  2. 終端子を保つスプリッタ — ベンダの \r をコードに届かせる _NewlineOnlySplitter
  3. 方言の検査 — 三つの応答形式が実際に届いたかを見る verify.sh の三行

自分の機器に合わせるには — _Instrument に行をひとつ足し、その会社が数字をどう書くかのパーサをひとつ書く。画面は bench.json のフィールド名を合わせればいい。通信が USB-TMC でも LAN でも _Bench.ask の中の話だ。

練習課題

例にデバイスをもうひとつ追加してください。変更したファイルをすべて挙げてください。ホストがその中にあってはいけません。

関連記事When the Instrument Has No Dedicated Software