6502tinymachines

QA の装置

ベンチの目は下敷き板の上に組んだ金属のフレームに固定してある。このファイルは、その「固定」が何を意味するかだ: どれがどこにあるかを書いておけば、動いたカメラや板は、地図を読み直す相手ではなく、照らし合わせる数字になる。寸法はユーザーから、2026-09-15。画素スケールは名指しのフレームで実測した。tools/board-overlay.py --read は既知の姿勢のフレームから穴の地図を読む。docs/board-map.json が、それを読んだ姿勢を名指しする。

下敷き板とブレッドボード

もの寸法どこに
下敷き板12 by 18 inすべての下。フレームの支柱はその長辺に
ブレッドボードのブロック6.5 by 10 in (三枚のボードとレールのストリップを横に並べたもの)下敷き板の右の短辺にぴったり付け、中央に。上下に板が 3 in ずつ
チップのボード三枚のうちの一枚下敷き板の端に最も近いもの (写真のままの位置: フレームから読み取る、docs/board-map.json の band)

ブレッドボードの穴のピッチは 0.1 in で、以下のすべての画素スケールはそれに対して測られている。

カメラ

カメラデバイス (by-id)役目取り付けフレームスケール
Logitech BRIOusb-046d_Logitech_BRIO_1C8D6975ボードの目: 穴の地図、名前付きの近接撮影、定時のボードの取り込みブレッドボードの上、フレームの横棒に真下向き。2026-09-15 に持ち上げて水平を取り、ガムテープとマジックテープで留めた1920 by 1080、MJPG (USB 2 なので 4K は出ない)固定 2026-09-15 (captures/b15-all.jpg): ズーム 100 で 1 穴 11.1 px、列方向も行方向も同じ (水平が取れている: 短縮が無い)、ズーム 250 で 28、ズーム 500 で 53。フォーカス 18。下敷き板の全体がフレームに入る。地図はズーム 250 のフレームからボードごとに読む (docs/board-map.json が各ボードのフレーム、狙い、band を名指しする)。現物どおりの写真はそれらのフレームの上に描かれる
Logitech QuickCam Pro 9000usb-046d_0990_08DF0A45ボードを横切る横目: 三つのチップと UNO のリード線を Pi 側から横顔で下敷き板の Pi 側、低く、ブレッドボードを越えてコンソールの方を見る1600 by 1200、MJPG固定が外れた 2026-09-24: 一度外し、pad-ble の作業を写すために Communicate の場所へ戻したので、ここのスケールはどれも今のものではない。その晩はポート 1-1.4 で列挙を拒んだ (can't set config #1, error -71 が二度。ハブの電力から二つの機器を外しても変わらず)。電源を切っての再起動で消え、それ以来きれいに動いている。古い固定は古い姿勢についてだけ: 固定 2026-09-15 (docs/lab/rig-side-eye-9000.jpg): チップはフレームの幅の 3 分の 1 ほど。ハウジングのピンは横顔で 1 本あたり 4 px ほどで見える
Logitech QuickCam Communicate Deluxe (2026-09-24 に装置から外した)usb-046d_09a2_ABAD8310二つ目の横目だった。チップのボードのレール側を、ケーブル端から沿って見る: プローブのクリップ、コンソールのケーブルのハウジング、U3 のレール側の着地点ケーブル端のフレームの支柱に、低く1280 by 960、MJPG固定が外れた 2026-09-24: pad-ble の作業を写すために動かし、まだ測り直していないので、ここのスケールはどれも今のものではない。動かした後の最初のフレーム (captures/side-eye-2026-09-24.jpg、コミットしていない) では被写体が左端に寄り、自動露出はその後ろの壁で測っている。古い固定は古い姿勢についてだけ: 固定 2026-09-15 (docs/lab/rig-side-eye-communicate.jpg): 列番号が列 25 あたりまで読める。Q の線の弧とハウジングが横顔で見える
Roxio の取り込み (em28xx)usb-1b80_Roxio_Video_Capture_USB_...コンソールの絵スコープの CH3 と一緒に分岐器に720 by 480 NTSCカメラではない

ソフトウェアで向きを変えられるカメラはどれか、測って

2026-09-24 に一度まちがって言い切り、それから測った。まちがった答えを根拠にカメラを一台入れ替えてしまったからだ。その日の再起動の後、それぞれで v4l2-ctl --list-ctrls を走らせた:

カメラパン / チルトズームフォーカスフレーム
Logitech BRIOpan_absolute、tilt_absolutezoom_absolutefocus_absolute、focus_automatic_continuous1920 by 1080
QuickCam Pro 9000なしなしなし1600 by 1200
QuickCam Communicate Deluxeなしなしなし1280 by 960

触れずに向きを変えられるのは BRIO だけ。だから eye.py は BRIO だけを相手に書かれ、ズーム、パン、チルトのプリセットを持っている。二つの横目はどちらも手で構図を決めるもので、露出、ゲイン、ホワイトバランス、シャープネス、逆光補正のほかには何も出さない。Communicate Deluxe には privacy が加わり、二台の違いはそれだけだ。

だから横目は届く範囲ではなくセンサーで選ぶ: 9000 は 1600 by 1200、Communicate は 1280 by 960 で、どちらもここからは向けられない。

ベンチ、写真で

固定の後、2026-09-15 に撮った装置全体の電話の写真が四枚 (電話のメタデータは削除。サイトは商用ゲームのスクリーンショットを載せないので、テレビの絵はぼかしてある):

ベンチを正面から: 左にコンソールとそのテレビ、下敷き板の上にフレーム、その奥にスコープ

フレーム: 横棒にベンチライト、その下の中央に BRIO、下のレールに横目

コンソールの横: カートリッジの入ったメイン基板、変調器、プローブのクリップ、その先のブレッドボード

真上から: BRIO が見るままの下敷き板の全体、下に UNO と Pi、右に QuickCam 9000

姿勢、固定した

三台のカメラとボードは 2026-09-15 の夜にガムテープとマジックテープで留め、BRIO は水平を取った (最初に持ち上げた姿勢の傾き、列方向の 11.1 に対して行方向が 1 穴 9.2 px だったものは無くなり、どちらも 11.1 と読む)。名前付きの近接撮影はどれも狙いに当たり、地図の輪は両方のボードにわたって列 56 まで穴の上に座る (captures/views-20260915T152127)。これが基準だ: 動いたものは、captures/b15-all.jpg からセンサー雑音より大きく違うフレームとして現れ (定時の取り込みのそれに対する最悪の 40 px ブロック)、直し方はもう一度 --read で、新しいツールではない。

ある姿勢が払う代価と買うもの

以前の高さでは BRIO の近接撮影は 1 穴に 120 px を置き、線の端が列番号の横で苦もなく読めた。持ち上げた高さでは 1 穴に 53 px、その半分になり、列番号はまだ読める。持ち上げた姿勢が買うのは装置の全体が一つのフレームに入ることだ: UNO、Pi、コンソールのメイン基板、変調器。定時の取り込みが記録として欲しいのはそれである。代価は、二列にまたがるハウジングの細かい読みで、そちらは横目の役目だ。

アドレスはどこにあるか

三つの鍵が、ツールの隣の .env にある: BRIDGE (Pi のシリアルの橋を通した UNO、host:port)、SCOPE (計器、ポートは無し)、PI (あちら側で何かを走らせるツールのための user@host)。このファイルは bench.local.md と同じく gitignore されている。.env.example がその形を持ち、コミットされている。フラグが環境に勝ち、環境がファイルに勝ち、ファイルが bench.local.md に勝つので、tools/rig-check.py と tools/bench-check.py は、ファイルを持つ機械の上ではアドレスを一つも取らなくなった。Pi 自身の上では BRIDGE はループバックだ。そこでは橋がローカルにあるからだ。

変更のたびに: 回帰の検査

python3 tools/rig-check.py --pi HOST

六つの検査があり、それぞれ PASS か FAIL を、そう決めた数字と一緒に出す。light (明るさ。ボード全体のフレームの水準が基準のものの近くにあること、露出がカメラの上限の下にあること: 暗い部屋はそれを 312 に張り付ける。2026-09-15 に、動くバーが明かりを一緒に持って行ったときに見えた)、still (静止。フレームが基準のフレームからセンサー雑音のぶんだけしか違わないこと: 最悪の 40 px ブロックが 60 未満。雑音は約 20 と測れ、動いたボードやカメラは 120 以上になる。FAIL はその領域を地図の座標で名指しする)、board right と board middle (右のボードと中央のボード。それぞれ、地図が記録する狙いでズーム 250 のフレームから新たに読み、地図に対して半穴以内に押さえる。FAIL は地図が古いという意味だ)、そして二つの side eyes (横目。点いていて、自分の基準のフレームと 0.85 以上で相関していること)。どれか一つでも FAIL なら終了コード 1。撮ったフレームは captures/rig/ にあるので、FAIL は目で見られる。

変更が意図されたものだったとき (ボードを足した、カメラを滑らせた) の順序はこうだ。まず検査 (失敗し、どこかを言う)、次に、いま撮ったばかりのフレームから地図を読み直す (tools/board-overlay.py --read-boards --frames right=captures/rig/right.jpg,middle=captures/rig/middle.jpg。ボードがもう自分のフレームに収まっていないときは --aims と --bands を付ける)、それから重ね描き (tools/board-overlay.py captures/rig/all.jpg) を、輪が穴の上に乗っているか目で見る。そして最後にもう一度検査を --baseline 付きで。これは、基準になるはずのフレームでどれかの検査が失敗すれば拒む。基準の測定値は docs/rig-baseline.json へ行き、日付が付く。そのフレームは captures/rig/baseline-*.jpg へ。どちらもまだ無い: 基準はまだ取っていない (2026-09-28)。

信号の経路も、同じやり方で

scripts/hands-head.sh          # Pi の手。コンソールのスイッチは OFF
scripts/hands-manual.sh        # あなたの手をパネルに。スイッチは ON
python3 tools/bench-check.py   # 同じツール、手は無し

二つのスクリプトは同じ検査で、ボタンに置く手が違うだけだ。橋とスコープのアドレスを .env から、次に bench.local.md から読み、始める前にどちらのスイッチ位置が欲しいかを言い、それ以外は素通しで渡す。手動のほうは --walk を与えないかぎりレジスタの歩き回りを飛ばす。頭の側の走行がそれを覆うからだ。

目の検査は電子について何も言わない。こちらは、立ち上げとレジスタの歩き回りが押さえた測定を走らせる: 橋が STATUS に応え、スコープが *IDN? に応え、コンソールのポーリングが毎秒およそ 60 回、そのどれにもクロックが 8 つ入って来て (B0 の検査 1)、TRIG 20 がスコープを CH1 で止め、レジスタに入れた 15 バイトが八つの中間スロットで D0 から読み返され、押された状態は LOW で、どれも入れたとおりである。これにはコンソールが点いていて、ゲームがポーリングしていることが要る。ポーリングが無ければ、それを必要とする検査はその理由を付けて SKIP され、失敗にはならない。スコープの設定は先に読んで後で戻す。違ったバイトは数に入れる前にもう一度取る。ゲームはどのポーリングでも同じようにクロックを出すわけではないからだ (15 回のポーリングに 1 回、八つのクロックが普通の 98 us に対して 68 us で来た、2026-09-15)。取り直したことは報告される。スクリーンショットは captures/bench/ に。初回の走行は 2026-09-15 23:35: 橋、スコープ、ポーリング (20 s で 1212 回、クロックはすべて八つ)、トリガ、歩き回り 15 のうち 15、回帰なし。

--hands manual か --hands head を付けると、二つの手、リセットと電源も、ポーリングの流れによって検査する: CPU が押さえられている間や電源が切れている間、ゲームはポーリングを止め、その後また始める。ポーリングは 4 分の 1 秒ごとに数える (Pi の橋は行を 100 ms ほどのまとまりで渡すので、到着時刻からは 17 ms の刻みが見えない)。manual はあなたの手を前面パネルに、ボタンを 2 秒押し、スイッチを三つ数えるあいだ切る。head は Pi の GPIO17 を OK1 経由、GPIO27 を K1 経由で、同じ二つの測定をする。前面パネル自身のボタンは OK1 と並列に、そのスイッチは K1 と並列に配線されたままだ (J3 の茶と赤、2026-09-17 にメーターで確認)。だから手ではうまくいって頭からはうまくいかない配線は、まさにそう見える。manual はリレーを開いたまま休ませて走り、head は前面のスイッチを切って走り、聞き始める前に K1 を閉じる。リレーのモジュールの入力はアクティブ LOW だ: 頭のデーモンはそう駆動する (2026-09-16 に修正)。そして Pi の GPIO27 は、何かがそれを主張するまでプルダウン付きの入力として休む。それはリレーが ON ということだ。Pi の config.txt の gpio=27=op,dh の行が治療で、リレーを入れてその休み状態を測ったら一度だけ設定する。リレーは 2026-09-17 から入って駆動されている。あの行を設定したかどうかはここには記録が無い (あるとすれば bench.local.md だ)。

何かが動いたとき

  1. python3 tools/eye.py sweep NAME --pi HOST --from 10 --to 40 --step 4: フォーカス。
  2. python3 tools/eye.py grab bN-all --pi HOST --preset board: フレーム。
  3. python3 tools/board-overlay.py --read captures/bN-all.jpg: 地図と視野。姿勢が装置のもの (真下向き、frame_rotate が設定され、ボードが自分の band に入っている) であれば。そうでなければ先に band を。
  4. python3 tools/eye.py views --pi HOST と、その一組に対する tools/view-rings.py: 輪は穴の上にあるか。
  5. python3 tools/board-overlay.py captures/bN-all.jpg: 現物どおりの写真。

ビルド時に nes-bench/docs/rig.md から取り込まれる。写しはリポジトリの一つだけだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。