계측기에 전용 소프트웨어가 없어도
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? 를 1초마다 묻는 화면은 운영자가 보는 계기판이지 고속 계측 경로가 아니다. 마이크로초 트리거로 파형을 잡는 일을 정의 층으로 옮기는 건 어리석다. 단단해야 하는 측정은 단단한 계측기에 둔다.
실행·검증
$ 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 안쪽 일이다.
확인 과제
예제에 장치를 하나 더 붙여 보세요. 고친 파일을 모두 적어 보세요. 호스트가 그 목록에 있으면 안 됩니다.