今回のコード改修は、単なるファイルの分割ではなく、「関心の分離」と「状態管理の安全性向上」を目的としたモダンな設計への移行です。フレームワーク(Angularなど)の導入なしに、標準のES Modulesとクラス設計のみで保守性の高い実装を実現しています。
具体的な改善点は以下の5点です。
約800行にわたりHTML、CSS、JSが混在していた元のファイルを、役割ごとに分割しました(main.js, SerialManager.js, FileManager.js, utils.js)。
これにより、見通しが改善され、特定の機能を修正する際の影響範囲がひと目でわかるようになっています。
旧コードにおける最大の技術負債は、waitForSerialIn や transferFileObj といったグローバル変数がファイル全体に散らばり、状態管理が予測不能になっていた点です。
新コードでは、これらの状態を SerialManager や FileManager のクラスインスタンス内にプライベートな状態としてカプセル化しました。これにより、外部からの意図しない書き換えや、複数処理の競合バグを構造的に防いでいます。
シリアル通信のストリーム(断片的に送られてくるデータ)を待ち受ける処理において、旧コードはループ内で常にグローバル変数をチェックする力技の構造でした。
新コードではこれを writeAndWaitFor というPromiseベースのメソッドに隠蔽しています。呼び出し側(アプリケーション層)は「この文字列が返ってくるまで待つ」という直感的な await 処理を書くだけでよくなり、非同期処理のコールバック問題や同期ズレを解消しました。(なお、別ウィンドで開かれる連携先アプリの改修がまだのため、windowオブジェクトに互換性保証用のインターフェースをあえて露出させています)
旧コードでは、ファイルのダウンロード処理中に直接 document.getElementById を呼び出してプログレスバーを更新するなど、ロジックとUIが密結合していました。
新設計では、ファイル操作クラス(FileManager)はDOMの存在を知らず、進捗や結果をコールバック関数でUI層(main.js)へ返す設計にしています。これは、昨今のフロントエンド開発におけるベストプラクティスに沿った設計です。
コンポーネント指向やRxJSのような概念を持つAngular等のフレームワークを導入すると、開発チームの学習コストが跳ね上がる(バス係数の低下)リスクがありました。 今回の設計は、「標準的な最新のJavaScript(ES6+)の知識」さえあれば誰でも読み解き、保守・拡張ができるという点で、現在のプロジェクト規模/構成に適した技術選定としています。
今回のコード改修は、RxJS(Reactive Extensions)やAngularのDI(依存性注入)が提供する高度なアーキテクチャ設計の思想を、標準的なVanilla JS(ES ModulesとAsync/Await)の比較的コンパクトなコード(コアのSerialManager.js 約200ステップ)に落とし込んだものです。ストリームの分離やプロンプト同期、ハードウェア特有の泥臭いハックを安全かつ体系的に構築できています。
Angular版の設計思想(責務レイヤの分離)は、今回のVanilla JSアーキテクチャでも以下のように再現・担保されています。UI層が直接ストリームの生データを触ることはありません。
| 概念(RxJS版の設計思想) | 今回のVanilla JS(ESM)版での解決策 | メリット |
|---|---|---|
terminalText$ (UIライブ表示専用) |
SerialManager.onDataReceived コールバック |
受信データをxterm.jsに流す専用経路。プロンプト判定ロジックとは分離。 |
receive$ (内部バッファ・プロンプト同期用) |
SerialManager内の receiveBuffer と checkWaiter() |
生のチャンクをクラス内に隠蔽。UI層を汚染せず、内部で文字列を結合して待機状態を解決。 |
exec$ / readUntilPrompt$ |
SerialManager.writeAndWaitFor() |
非同期のPromiseとして実装。呼び出し元は await でストリームの同期待ちが可能になり、直感的。 |
| Facade / Pipeline / Transport層 | main.js / FileManager / SerialManager |
役割を3つのファイルに分割。循環参照を防ぎ、単一責任の原則(SRP)を遵守。 |
concatMap / exhaustMap(キューイング・直列化) |
SerialManager内の queue と processQueue() |
非同期のコマンド呼び出しを内部でFIFOキューに蓄積し1つずつ順次実行。競合(Race Condition)を解決。 |
RxJSの filter や takeUntil、buffer などを駆使して構築するプロンプト待機ロジックを、「バッファループ機構」と「Promiseの解決(resolveの保持)」の組み合わせで実装しています。
さらに、RxJSの concatMap が担う「ストリームの直列化」を、SerialManager 内部の配列(this.queue)を用いた再帰的なタスク消化ループ(processQueue)で再現しました。これにより、バックグラウンドで受信ストリームを監視しながら、衝突することなく複数の非同期送受信を実施する制御を実現しています。
ソフトウェアレイヤーでストリームを構築しても、相手はRaspberry Pi ZeroのUSB OTGシリアルのため、大量のデータを一気に流し込めば、バッファオーバーフローを起こしデータが欠損することがあります。これを防ぐために、旧コードにあった「泥臭いフロー制御」が必要となります。
今回の設計では、この「泥臭いハック」を、クラス設計の中に安全に封じ込めました。
- ダウンロード時のフロー制御(moreコマンドの活用)
RxJS側でチャンクを結合するだけでなく、Pi側に対して
more -50で50行ずつ出力させ、アプリ側が5ミリ秒のsleepを挟みながらスペースキー(" ")を送って次を要求する。このハードウェアの限界に対応した制御をFileManager.getFile()内のwhileループとPromiseで実装しています。 - アップロード時のフロー制御(512バイト分割)
Base64文字列を全送信するのではなく、
lineLength = 512バイトごとに区切り、改行を送り、1ミリ秒待機する。この一連のシーケンスもFileManager.saveFile()にカプセル化し、UI層には進捗(パーセンテージ)だけを通知する設計にしています。
RxJSは非常に強力なツールですが、本プロジェクトの規模とチームの持続可能性を考慮した場合、「学習コストの高さ」と「バス係数の低下(特定個人への依存)」という深刻な技術負債を生むリスクがあります。 本実装は、「RxJSが目指す美しいストリームの責務分離」と「Pi Zero特有の泥臭いハードウェアハック」を、標準的なJavaScriptの文法(ESM + Async/Await)で比較的コンパクトに両立させたものです。