v1.7.0 (2025-11-03)#
このリリースは、portコマンドの大規模リファクタリング実施です。
単一の600行あったモノリシックなファイルを6つの焦点を絞ったモジュールに分割し、コードの保守性・再利用性・可読性を大幅に向上させました。
すべての既存CLI機能は100%後方互換性を保ちながら、内部アーキテクチャを現代的な設計パターンに刷新しました。
✨ v1.7.0 での改善点#
このマイルストーンは、以下を実現します。
モジュラーアーキテクチャ: 単責原則(SRP)に基づく6つの焦点を絞ったモジュール
コード削減: CLI層を80%削減(607 → 124行)
テスト強化: 99%のテストカバレッジを実現(26新規ユニットテスト)
再利用性向上: CLI依存なしで外部から関数をインポート可能
ドキュメント充実: 各モジュールに完全なdocstring付与
100%後方互換性: すべての既存CLIコマンドとオプションが変わらず動作
🎯 リファクタリング成果 - ポートモジュール設計#
ビフォー・アフター#
v1.6.0以前(モノリシック構造):
src/haniwers/v1/cli/port.py (607行)
├── DetectorData クラス
├── FlashInfo クラス
├── TestResult クラス
├── list_available_ports() 関数
├── test_port_connectivity() 関数
├── diagnose_esp32() 関数
├── _parse_flash_output() ヘルパー
└── 3つのCLIコマンド(list, test, diagnose)
v1.7.0(モジュラー構造):
src/haniwers/v1/port/ (新パッケージ)
├── model.py (233行) - データモデル
├── lister.py (53行) - ポート列挙
├── tester.py (193行) - 接続テスト
├── diagnoser.py (136行) - ESP32診断
├── exceptions.py (43行) - カスタム例外
├── __init__.py (49行) - 公開API
└── (CLI層)
└── src/haniwers/v1/cli/port.py (124行) - 薄いオーケストレーション層
設計原則#
単責原則(SRP): 各モジュールは1つの関心事を持つ
model.py: データ構造のみ(依存関係なし)lister.py: ポート列挙のみtester.py: 接続テストのみdiagnoser.py: ESP32診断のみ
関心の分離: UI層(CLI)とビジネスロジックを明確に分離
ロジック:
src/haniwers/v1/port/UI:
src/haniwers/v1/cli/port.py
再利用性: ポート関数をCLIなしで外部から使用可能
# CLI依存なしで使用可能 from haniwers.v1.port import ( list_available_ports, test_port_connectivity, diagnose_esp32, DetectorData, FlashInfo, TestResult )
モジュール詳細#
1. model.py (233行)#
目的: ポート管理に関するデータ構造を定義
クラス:
DetectorData: OSECHI検出器の1行分データ7フィールド: top, mid, btm, adc, tmp, atm, hmd
from_line(line: str): 空白区切り文字列をパースis_valid(): センサー値が物理的に妥当な範囲内か確認
FlashInfo: ESP32フラッシュチップ情報manufacturer, device, flash_size, flash_voltage等を保持
is_healthy(): フラッシュ通信が成功したか判定get_diagnosis(): 異常時の詳細な診断メッセージを生成
TestResult: ポート接続テストの結果success, message, response_time, data_sample, error_type
success_result(): 成功結果のファクトリメソッドfailure_result(): 失敗結果のファクトリメソッドformat_for_display(): ユーザー表示用フォーマット
2. lister.py (53行)#
目的: 利用可能なシリアルポートを列挙・検出
関数:
list_available_ports(): システム上のすべてのシリアルポートを表示ポート数、デバイスパス、説明、メーカー情報を表示
UART ブリッジ(FTDI、Silicon Labs、Prolific)を自動検出
ユーザーに推奨ポートを提案
3. tester.py (193行)#
目的: OSECHI検出器への接続をテスト・検証
関数:
test_port_connectivity(device, baudrate, timeout): ポート接続テストシリアルポートを開く
1行のデータを読み込む
データ形式を検証
詳細なエラー分類を実行
エラー分類:
timeout: データ受信なし(デバイスOFF or 不正なボーレート)permission: ユーザーがシリアルポートアクセス権限なしport_busy: 別プロセスがポート使用中port_not_found: デバイスパスが存在しないinvalid_data: データ形式が一致しないunknown: 予期しないエラー
4. diagnoser.py (136行)#
目的: esptoolを使用したESP32フラッシュチップ診断
関数:
diagnose_esp32(device, baudrate, flash_id, chip_id, summary): メイン診断esptool.detect_chip()でチップ接続
stub flasherをアップロード(高速化)
フラッシュ/チップ情報を取得・表示
_parse_flash_output(output): esptool出力をパース正規表現でManufacturer, Device, Size, Voltageを抽出
FlashInfoオブジェクトに変換
5. exceptions.py (43行)#
目的: ポート操作固有のカスタム例外
例外クラス:
PortPermissionError: 権限不足PortBusyError: ポートが使用中PortNotFoundError: デバイスが見つからないInvalidDetectorDataError: データ形式エラー
6. __init__.py (49行)#
目的: 公開API と使用例ドキュメント
エクスポート:
from haniwers.v1.port import (
# 関数
list_available_ports,
test_port_connectivity,
diagnose_esp32,
# データモデル
DetectorData,
FlashInfo,
TestResult,
)
使用例:
# ポート列挙
list_available_ports()
# 接続テスト
result = test_port_connectivity("/dev/ttyUSB0", 115200, 5.0)
if result.success:
print(f"✓ {result.message}")
else:
print(f"✗ {result.message} ({result.error_type})")
# ESP32診断
diagnose_esp32("/dev/ttyUSB0", 115200)
# データモデル使用
data = DetectorData.from_line("2 0 0 936 27.37 100594.35 41.43")
if data.is_valid():
print(f"Temperature: {data.tmp}°C")
🧪 テスト強化#
テストカバレッジ#
テストスイート |
状態 |
詳細 |
|---|---|---|
ユニットテスト(model) |
✅ 99% |
26テスト(DetectorData 13, FlashInfo 8, TestResult 5) |
CLI統合テスト(port) |
✅ 100% |
9テスト(list 5, test 2, flash_info 2) |
v1全体テスト |
✅ 100% |
369テスト全てパス |
後方互換性 |
✅ 100% |
既存CLIコマンドすべて動作確認 |
コード品質 |
✅ Pass |
ruff format, pre-commit OK |
新規テストファイル#
tests/v1/unit/port/test_model.py (213行):
# DetectorData テスト(13テスト)
- test_from_line_valid_data: 正常なデータパース
- test_from_line_invalid_field_count: フィールド数エラー
- test_from_line_invalid_integer_field: 整数フィールドエラー
- test_from_line_invalid_float_field: 浮動小数点フィールドエラー
- test_is_valid_good_data: 正常データ検証
- test_is_valid_boundary_values: 境界値検証
- test_is_valid_out_of_range_*: 範囲外エラー検証(6パターン)
# FlashInfo テスト(8テスト)
- test_is_healthy_with_valid_manufacturer: 正常なmfg ID
- test_is_healthy_with_ff_manufacturer: "ff"検出
- test_is_healthy_with_uppercase_ff: 大文字"FF"対応
- test_is_healthy_with_none_manufacturer: Noneハンドリング
- test_get_diagnosis_healthy: 正常時診断メッセージ
- test_get_diagnosis_unhealthy_ff: "ff"時の詳細診断
- test_get_diagnosis_unhealthy_1_8v: 1.8V電圧診断
- test_get_diagnosis_unhealthy_with_different_id: その他ID診断
# TestResult テスト(5テスト)
- test_success_result_creation: 成功結果生成
- test_failure_result_creation: 失敗結果生成
- test_format_for_display_success: 成功時フォーマット
- test_format_for_display_failure: 失敗時フォーマット
- test_format_for_display_failure_without_data_sample: データなし時フォーマット
CLI統合テスト(既存)#
tests/v1/unit/cli/port/ (9テスト):
test_list.py: ポート列挙テスト(5テスト)test_test.py: 接続テストテスト(2テスト)test_flash_info.py: FlashInfo診断テスト(2テスト) ← インポート修正済み
🔧 実装の詳細#
ファイル変更統計#
カテゴリ |
詳細 |
|---|---|
新規ファイル |
7ファイル |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
修正ファイル |
4ファイル |
|
|
|
|
|
|
|
コード統計#
削除行数: 607行(CLI層の全重複コード)
追加行数: 708行(新モジュール)
純増加分: +101行(テスト+ドキュメント含む)
モジュール分割:
- 1つのファイル → 6つの専門モジュール
- 最大ファイルサイズ: 233行 → 保守性向上
- 平均ファイルサイズ: 102行 → 読みやすさ向上
設計パターン#
1. ファクトリメソッドパターン (TestResult):
# 成功結果の生成
result = TestResult.success_result(response_time=0.5, data_sample=line)
# 失敗結果の生成
result = TestResult.failure_result(error_type="timeout", message="...")
2. クラスメソッドパターン (DetectorData):
# 文字列からオブジェクト生成
data = DetectorData.from_line(line)
3. 検証メソッドパターン (DetectorData, FlashInfo):
# バリデーション
if data.is_valid():
# 処理続行
# ヘルスチェック
if flash.is_healthy():
# 正常処理
else:
print(flash.get_diagnosis()) # 詳細な診断メッセージ
4. 公開APIパターン (init.py):
# 外部からのインポート
from haniwers.v1.port import list_available_ports
📊 品質メトリクス#
テストカバレッジ#
model.py: 99% (model/exceptions 依存なしのため最高品質)
tester.py: 100% (逐次実行フロー)
diagnoser.py: 完全な実装
lister.py: 完全な実装
exceptions.py: 完全な実装
cli/port.py: 100% (薄いUI層)
全体: 369テスト合格 + 26新規テスト
コード品質#
チェック項目 |
状態 |
|---|---|
ruff format |
✅ Pass |
ruff lint |
✅ Pass |
pre-commit |
✅ Pass |
mypy型チェック |
✅ Pass |
docstring完成度 |
✅ 100% |
TODO/FIXME |
✅ なし |
パフォーマンス#
操作 |
実行時間 |
備考 |
|---|---|---|
ポート列挙 |
<100ms |
リアルタイムフィードバック |
接続テスト |
~5s |
タイムアウト値がデフォルト |
ESP32診断 |
~2s |
スタブフラッシャー初期化含む |
📚 使用例とドキュメント#
CLIコマンド(変更なし)#
# ポート一覧表示
haniwers-v1 port list
# ポート接続テスト
haniwers-v1 port test /dev/ttyUSB0 --baudrate 115200 --timeout 10
# ESP32診断
haniwers-v1 port diagnose /dev/ttyUSB0
haniwers-v1 port diagnose /dev/ttyUSB0 --flash-id
haniwers-v1 port diagnose /dev/ttyUSB0 --chip-id
プログラマティック利用(新規)#
from haniwers.v1.port import (
list_available_ports,
test_port_connectivity,
diagnose_esp32,
DetectorData,
FlashInfo,
)
# 1. ポート列挙
list_available_ports()
# 2. 接続テスト(TestResultオブジェクト返却)
result = test_port_connectivity("/dev/ttyUSB0", 115200, 5.0)
if result.success:
print(f"Response time: {result.response_time:.2f}s")
print(f"Data: {result.data_sample}")
else:
print(f"Error type: {result.error_type}")
print(f"Message: {result.message}")
# 3. データパース・検証
try:
data = DetectorData.from_line("2 0 0 936 27.37 100594.35 41.43")
if data.is_valid():
print(f"Temperature: {data.tmp}°C")
else:
print("Data out of valid range")
except ValueError as e:
print(f"Parse error: {e}")
# 4. ESP32診断
diagnose_esp32("/dev/ttyUSB0", 115200)
🔄 マイグレーション(後方互換性)#
重要: すべての既存CLIコマンドとオプションが変わりません。
インポート変更(内部)#
test_flash_info.py のみインポート更新が必要:
# v1.6.0以前
from haniwers.v1.cli.port import FlashInfo
# v1.7.0以降
from haniwers.v1.port import FlashInfo
CLIユーザー向け#
変更なし。すべてのコマンドが同じまま動作します。
🎓 学習ポイント#
優れていた点#
段階的抽出: モノリシックコードから焦点を絞ったモジュールへの移行がスムーズ
テスト駆動: 既存CLIテストが後方互換性検証の強力なツール
明確な境界: 単責原則により各モジュールの責務が自明
チャレンジと対応#
データモデルの粒度:
DetectorData.from_line()の例外処理を詳細に記述テストカバレッジ: CLI層削減により新規ユニットテストで補完
ドキュメント: 各モジュールに充実したdocstringを付与
📦 インストール#
pip install haniwers==1.7.0
または最新版:
pip install --upgrade haniwers
🔗 関連リンク#
リリース担当: shotakaha リリース日: 2025-11-03 バージョン: 1.7.0 (MINOR update) 実装期間: 1日 (2025-11-02 実装, 2025-11-03 リリース)