この記事は IBM Bob(AI コーディングアシスタント)と実際にペアプログラミングして感熱プリンタの USB 印刷アプリを作った体験記です。うまくいったこと、全くうまくいかなかったこと、人間が悩んで試行錯誤した過程を包み隠さず書きます。
なぜこんなことをしたのか
発端は、知人の自動車修理工場(宇都宮市)を訪れたときのことです。
その工場では SII(セイコーインスツルメント)の感熱プリンタ DPU-S445 を Bluetooth 接続で使っていました。用途は、車両の各種センサー状況や車検情報を表示する診断端末から、必要な情報をその場で印刷すること。有線だと取り回しが悪く、Bluetooth による無線接続が現場では不可欠です。
感熱プリンタ自体は数年おきに新品に替えるほどのものでもなく、壊れるまで使い続けるのが現場としては妥当な判断です。DPU-S445(01A) はすでに製造終了した機種ですが、印字できる限り現役です。
ところがいつしかBluetooth でまったく繋がらなくなってしまいました。
車の作業時間、今回はエンジンフラッシングとオイル交換なので3時間弱、は暇です。
長らくPCに詳しいということで、その場で1時間ほど試行錯誤しました。
確かに接続できません。どうしてもPINコードを受け付けない、原因不明のまま手詰まりになりました。
もちろん初期化操作もしました、2回ほど。しかし症状は解消しません。初期化はPINコードは含まれないのか?なんというセキュリティなのか・・故障の方が濃厚です。
「USB で有線接続すればPINのリセットができ、使えるかもしれない」——可能性を見出したものの、DPU-S445 の接続端子は MINI USB(今となっては珍しい規格)。ケーブルが手元になかったため、プリンタごと自宅に持ち帰ることになりました。
自宅で MINI USB ケーブルを用意し、Mac に繋いでテキストを印刷したい。ただそれだけです。
しかしことはそう簡単ではありませんでした。USB ドライバ、ESC/POS コマンド体系、文字コード、感熱紙の特性——ハードウェアが絡む開発は「ドキュメントがない」「動かしてみないと分からない」の連続で、AI アシスタントが得意とする「コード生成」だけでは到底解決できません。
そこで思いました。IBM Bob はこういう泥臭い場面でも相棒になれるのか?
結論から言うと——USB 印刷の完成まで約5時間、Bluetooth は翌日さらに約2時間で完成しました。
構成とアーキテクチャ
最終的に出来上がったアプリの技術スタックはこうなりました。
| 技術 | バージョン | 役割 |
|---|---|---|
| Java | 21 | JVM |
| Quarkus | 3.36.3 | CDI コンテナ・ライフサイクル管理 |
| JNA | 5.19.1 | libusb ネイティブバインディング |
| libusb | 1.0.29 | USB デバイスアクセス |
| Java Swing | JDK 同梱 | デスクトップ GUI |
なぜ Quarkus か? Web アプリでもないのに、と思われるかもしれません。理由は CDI(依存性注入)です。USB ハンドル、Bluetooth ハンドル、印刷サービス、UI ウィンドウ——それぞれを @ApplicationScoped Bean として管理し、@Inject でつなぐことで、接続方式(USB / Bluetooth)をランタイムに切り替えられるクリーンな設計が自然に実現しました。Quarkus は「超高速起動の Web フレームワーク」という印象が強いですが、デスクトップアプリの軽量 CDI コンテナとしても優秀だと分かりました。
[Swing UI (EDT)]
│ USB or Bluetooth ラジオボタン切替
▼
[PrinterService @ApplicationScoped]
│ buildPrintJob() — USB / BT 共通
├─ UsbDeviceHandle (@ApplicationScoped)
│ │ JNA 経由
│ ▼ libusb_bulk_transfer()
│ [libusb-1.0.dylib]
│ ▼
│ [DPU-S445 USB Bulk OUT EP:0x01]
│
└─ BluetoothDeviceHandle (@ApplicationScoped)
│ ESC/POS バイト列を一時ファイルに書き出し
▼ サブプロセス起動
[bt_rfcomm_send.py(PyObjC + IOBluetooth)]
│ ACL接続 → SDP照会 → openRFCOMMChannelSync
▼ writeSync_length_()
[DPU-S445 Bluetooth RFCOMM ch.1]
Bob との作業の流れ
Phase 1:USB デバイス接続の確認
最初に Bob にお願いしたのは「USB でプリンタが見えているか確認して」という一言です。
Bob は macOS のコマンドでデバイスを調査しました。
# macOS の USB デバイス一覧
system_profiler SPUSBDataType
# → DPU-S445 発見、VID=0x0619 / PID=0x0119 を確認
# エンドポイント構成を pyusb で確認
python3 -c "import usb.core; d=usb.core.find(idVendor=0x0619,idProduct=0x0119); print(d)"
# → Bulk OUT EP:0x01 / Bulk IN EP:0x82 を確認
さらに DC2 l(SWDIP 読み取り)コマンドで本体の設定情報も取得できました。
(最終的には、USB 接続時・Bluetooth 接続時、どちらからでも同じコマンドで読み取れることが確認できました)
DC2 l レスポンス(実機取得値):
SWDIP1 = 01001011 → ボーレート:115200bps / データ長:8bit / パリティ:なし / ストップ:1bit
SWDIP2 = 10010111 → 漢字コード:Shift-JIS / フォント:16dot / 通信モード:Bluetooth/USB
SWDIP3 = 11111101 → 印字濃度:110% / 感熱紙モード:ロール紙
SWDIP4 = 10111110 → BT LinkKey:無効 / AC自動ON:有効
⏱ 人間なら時間がかかるところ:
system_profiler・ioreg・pyusb を使いこなして VID/PID とエンドポイント構成を一気に確認し、さらに SWDIP の読み取りと解析まで行うのは、経験がある人でも1〜2時間かかる作業です。Bob はこれを数分で完了しました。SWDIP の手作業での設定変更がいかに大変かを補足しておきます。このプリンタにはモニターがなく、設定の確認・変更はすべて印刷で行います。YesかNoかの選択が FEED スイッチと POWER スイッチに割り当てられており、都度ボタンを押すと結果が紙に印字される、という繰り返しです。
さらにビット値の入力は「8桁を一気に押す」方式で、例えば
0100なら「POWER → FEED → POWER → POWER」の順に押していきます。この操作に慣れるまでにロール紙を1m近く消費しました。当初「適当に押したら勝手に設定が確定して印字されてしまった」という事故もあり、設定をやり直す羽目になりました。全 SWDIP を確認・変更し終えるまでに、紙を3m近く使いながら1時間かかる作業です。Bob にDC2 lのレスポンスを貼るだけで即座に解釈が返ってくるのとは、大違いです。
しかし libusb4java(Maven Central の Java libusb バインディング)は macOS aarch64(Apple Silicon)未対応だと判明しました。Bob はすぐに代替案を提示しました。
「libusb4java は macOS aarch64 に対応していません。JNA で libusb を直接バインドする方式に切り替えます」
LibUsb.java(JNA インターフェース)、UsbDeviceHandle.java(open/close/bulk 転送)を一気に実装し、libusb_set_auto_detach_kernel_driver() で macOS の CUPS ドライバ自動デタッチも忘れずに対応しました。
Phase 2:Swing GUI — ボタンが出ない問題
アプリを起動したらボタンが表示されないという問題が発生しました。
「印刷ボタンが出ないので、出るようにして」と伝えると、Bob は即座に原因を特定しました。
macOS のシステム L&F(AquaLAF)は
JButton.setBackground()を無視してネイティブ描画するため、白背景に白文字でボタンが見えない状態になっている
しかし修正が何度やっても効きませんでした。setOpaque(true) → 効かない。UIManager.setLookAndFeel() を EDT 外で呼ぶ → 初期化タイミングで無視される。
試行錯誤の末、JVM 起動引数で最初から Metal L&F を指定するのが唯一確実な方法だと分かりました。
java -Dswing.defaultlaf=javax.swing.plaf.metal.MetalLookAndFeel -jar "$JAR"
Bob も「コード側での対処には限界がある」と認め、run.sh に起動引数を追加する形で解決しました。「コードを書くだけが答えではない」というのは AI にとっても重要な判断です。
Phase 3:漢字・カタカナが薄い問題(最大の難所)
アプリから印刷すると漢字とカタカナだけが薄くて読めない。アルファベットは濃く出ます。
ここが今回の作業で一番迷走した場所でした。
Bob の最初の見立て: 「Swing のテキストエリアのフォントが薄い」→ 画面表示の問題と勘違い
「アプリのウィンドウの話ではなく、印刷が薄いのだが?」と指摘するまで、Bob はひたすらヒラギノフォントの設定をいじり続けました。これは完全に私のコミュニケーションミスでもあります。
正しい問いに直したら Bob の動きが変わりました。
試みた解決策と結果:
| 試した方法 | 結果 |
|---|---|
DC2 # 6(印字濃度コマンド) |
効果なし(非対応コマンド) |
ESC 7(印字エネルギー) |
効果なし(非対応) |
| SWDIP3 を 110% に変更(本体操作) | 効果なし |
ESC E 1(Epson 形式の強調) |
効果なし(SII は ESC E、パラメータなし) |
GS !(文字拡大) |
効果なし(非対応) |
FS S(漢字スペース設定) |
誤作動:文字間に大きなスペースが開いた |
最終的に SII の公式スマホアプリでは濃く印刷できると分かりました。BobはHEX Dump モードで公式アプリのコマンドを解析する方針に切り替えると言い出しました。
ちょっとまて!膨大な印刷を紙を出すのか?まだやれることはあるだろう??
ここで人間が動きます。SII の公式サイトから技術説明書(PDF)を探し、これが正しいドキュメントであることを確認してから Bob に渡したのです。
なぜ「確認してから渡す」のか。他の生成 AI はネットを勝手に検索し、それらしい PDF を自動で読み込んでくれます。しかし DPU-S445 は古い機種で、類似モデルも多く、バージョン違いの仕様書や非公式の互換資料がネット上に散在しています。
0xAシリーズと、0xB,Cシリーズがあります。さらのBluetooth搭載・非搭載
誤った仕様書を信じたまま実装が進んだ場合の手戻りコストは、人間が一度確認する手間より遥かに大きい。 今回は人間が「これは正しい SII 公式の技術説明書か」を目視で確認し、その上で Bob に渡すという判断をしました。
PDF を渡した直後、Bob はコマンド表を解析して正解のコマンド列を特定しました。
最終的に有効だった ESC/POS コマンド列:
1B 40 ESC @ プリンタ初期化
1C 43 01 FS C 1 漢字コード = Shift-JIS(JIS ではなく)
12 46 01 DC2 F 1 フォントサイズ = 24ドット(16ドットより大きく濃い)
12 7E 82 DC2 ~ 130 印字濃度 130%(SII 独自コマンド、仕様上限付近)
1B 45 ESC E 強調印字 ON(※パラメータなし!Epson と異なる)
1B 21 18 ESC ! 0x18 半角: 強調ビット + 二重印字ビット
ここで分かった重要な事実:
-
ESC E n(Epson)とESC E(SII)は別物。パラメータを付けると余分なバイトになります -
DC2 ~ nは SII 独自の濃度コマンドで、有効範囲は 70〜130% -
GS V(Epson のカット)は DPU-S445 ではAという文字として印字されます -
CR(0x0D)はLFと同じ動作をするため重ね打ちには使えません
Phase 4:Bluetooth 接続
USB での印刷が安定したところで、Bluetooth SPP(シリアルポートプロファイル)対応に着手しました。
最初に試したのは、macOS がペアリング後に出すシリアルデバイスへの書き込みです。BlueCove は Apple Silicon 未対応なので、/dev/tty.DPU-S445(および後に cu.*)へ FileOutputStream で ESC/POS を流す方式を実装しました。stty で 115200 に合わせ、接続維持用の hold() も足しました。
// Day 1 に試した方式(最終的には断念)
stream = new FileOutputStream("/dev/tty.DPU-S445");
stream.write(printJob);
stream.flush();
しかし印字されません。 ポートは開けて「送信成功」に見えても、RFCOMM が半開きのままデータがプリンタに届かない。システム設定上の「接続済み」もすぐ切れます。Day 1 後半はこの /dev/tty.* 方式を約1時間試して断念しました。
Day 2 に仕切り直し、PyObjC の IOBluetooth で ACL 接続 → openRFCOMMChannelSync → writeSync するヘルパー(bt_rfcomm_send.py)へ切り替えました。Java 側はジョブを一時ファイルに書き、このスクリプトを起動するだけです。ここで初めて Bluetooth 印刷が安定しました。
[BluetoothDeviceHandle]
│ 一時ファイルに ESC/POS を書き出し
▼
[bt_rfcomm_send.py(PyObjC + IOBluetooth)]
│ openConnection → openRFCOMMChannelSync
▼ writeSync_length_()
[DPU-S445 RFCOMM ch.1]
Bob が得意だったこと
- 既知の技術領域の実装:JNA バインディング、Swing UI、Quarkus CDI 設計——コードの品質が高く、一発で動くことが多かったです
- エラーの原因特定:ログを見せると「これは macOS AquaLAF の既知の挙動」「このバイト列は正しく Shift-JIS になっている」と即座に分析してくれました
- ドキュメント解析:PDF の仕様書を読ませると SWDIP スイッチの表を正確に解釈し、正しい設定値を計算してくれました(人間はスイッチ番号が上下逆に記載されていることを気づかず作業しうまくいきませんでした。なんでここ上下逆に記載があるのか・・・)
- 代替案の提示:「これは動かない、代わりにこうする」という切り替えが早いです
Bob が苦手だったこと(正直に書きます)
- 問題の文脈を誤解する:「漢字が薄い」を画面表示の問題と勘違いし、しばらく Swing フォントをいじり続けました。「印刷が薄い」と明示するまで気づきませんでした
-
非公開仕様の推測:SII 独自コマンドを推測で送り、誤作動を起こしました(
FS Sでスペースが開いた等)。仕様書がない領域では推測で実装するリスクがあることを認識しておく必要があります - ハードウェアの実機確認は人間がやるしかない:コマンドを送っても反応がない→原因が「コマンドが間違い」か「接続が確立していない」か「紙が逆向き」かは、実機を見ている人間にしか判断できません
人間の役割
Bob と作業して気づいたのは、「正しい質問をする」のが人間の最大の仕事だということです。
「漢字が薄い」→ Bob は Swing フォントを修正
「印刷した紙の漢字が薄い」→ Bob は ESC/POS コマンドを調査
この一言の差が、何十分もの無駄な試行錯誤を生みます。AI は「聞かれたことに答える」のが得意で、「問題の本質を勝手に再定義する」のは苦手です。
また、実機で試す・紙を見る・ランプを確認する——これは人間にしかできません。Bob はターミナルのログとコードしか見えません。現実世界との接点は常に人間が担います。
タイムライン
| セッション | 内容 | 所要時間 |
|---|---|---|
| Day 1 前半 | USB デバイス確認・JNA 実装・GUI・ESC/POS コマンド調整・USB 印刷完成 | 約5時間(試行錯誤含む) |
| Day 1 後半 | Bluetooth /dev/tty.* 方式を試みるも断念 |
約1時間 |
| Day 2 | PyObjC + IOBluetooth 方式に切り替え・Bluetooth 印刷完成 | 約2時間 |
| 合計 | USB + Bluetooth 両対応アプリ完成 | 約8時間 |
Day 1 の5時間の中には「漢字が薄い」問題での迷走・仕様書の取り寄せ・コマンド総当たりも含まれています。Bob がいなければ USB 印刷だけで1〜2日かかっていたと思います。Bluetooth は「翌日に仕切り直す」判断が転機でした。
逆説:古い技術こそ AI が活きる
今回使った技術を並べてみると、DPU-S445(2008年頃の製品)、ESC/POS コマンド(1980年代からある規格)、libusb(2000年代初頭)、Java Swing(1998年〜)、Bluetooth SPP(2003年頃)——どれも「枯れた技術」です。
ところが Bob はこれらをすらすらと扱いました。理由は単純で、枯れた技術は訓練データに十分含まれているからです。新しいフレームワークや最新の API は情報が少なく AI が苦手とする場合がありますが、20年前の仕様書に書かれた内容は、むしろ豊富な文献がインターネット上に蓄積されています。
ここで一つ問いを立てたいと思います。今の若いエンジニアは、こういった「古い知識」を再取得できるでしょうか?
TCP/IP の仕組みも、IoT デバイスの基板も、RS-232C のピン配置も、USB のバルク転送も——最近は目にする機会がほとんどありません。クラウドと API が当たり前になった世界では、物理的な接続レイヤーを意識する場面が減っています。それは効率的な反面、いざ古い機器と向き合ったとき「どこから手をつければいいか分からない」という壁になります。
AI はこの壁を低くしてくれます。 「USB のバルク転送って何?」「ESC/POS の強調コマンドはどう送る?」——経験がなくても、正しい問いさえ立てられれば Bob が答えてくれます。古い知識を持つ人間が減っていく時代に、AI はその知識の代替として機能する可能性があります。
ただし、それは「問いを立てる力」と「答えを検証する力」を人間が持っていることが前提です。本記事で繰り返し書いてきた「実機で確かめる」「正しいドキュメントを選ぶ」——これらは AI には代替できません。
まとめ:ハード相手でも相棒になれる
Bob は「ソフトウェアだけの世界」でしか使えないと思っていましたが、今回の経験でその認識が変わりました。
ハードウェア開発での Bob の使い方:
- USB デバイス識別・インターフェース解析:ioreg / pyusb の使い方、VID/PID の確認
- 低レベルライブラリの実装:JNA バインディング、バイト列の組み立て
- 仕様書の解析:PDF を読ませてコマンド表を解釈、SWDIP 設定値の計算
- デバッグ支援:ログのバイト列を解析して「このコマンドは正しい/間違い」を判定
- GUI の実装:プラットフォーム固有の問題(L&F など)への対処
人間がやるべきこと:
- 問題を正確に言語化して Bob に伝える
- 実機で動作確認する
- 「動かない」の原因がソフト/ハード/設定のどれかを切り分ける
- 正しい仕様書を選別・確認して Bob に渡す(類似機種・旧バージョンが混在する中で「これが正解のドキュメント」と判断できるのは人間だけ。AI に自動検索させると誤った資料を信じたまま実装が進むリスクがある)
コード
このアプリのソースコードは以下の構成で実装しています。
printer-app/
├── src/main/java/com/example/printer/
│ ├── Main.java # Quarkus エントリポイント
│ ├── service/
│ │ ├── PrinterService.java # USB/BT 共通印刷サービス
│ │ └── PrintException.java
│ ├── usb/
│ │ ├── UsbDeviceHandle.java # libusb JNA バインディング
│ │ ├── LibUsb.java
│ │ ├── TransferResult.java
│ │ └── UsbException.java
│ ├── bluetooth/
│ │ ├── BluetoothDeviceHandle.java # PyObjC RFCOMM ヘルパー起動
│ │ └── BluetoothException.java
│ └── ui/
│ └── PrinterWindow.java # Swing GUI(USB/BT 切替ラジオ付き)
├── scripts/
│ └── bt_rfcomm_send.py # IOBluetooth ACL + RFCOMM 送信
└── src/main/resources/
└── application.properties # USB/BT 設定
Quarkus を使うことで PrinterService は UsbDeviceHandle と BluetoothDeviceHandle を @Inject で受け取り、UI のラジオボタンで切り替えるだけで USB/BT の両方に対応できました。「Web フレームワーク」として知られる Quarkus ですが、CDI コンテナとしての力をデスクトップアプリでも十分に発揮できます。
IBM Bob と一緒に、ハードウェアの泥臭い問題に向き合った記録でした。相棒は画面の中だけにいるのではなく、現実の機械と格闘するときも隣にいてくれます。ただし、最後に紙を見るのは人間です。