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行) - 薄いオーケストレーション層

設計原則#

  1. 単責原則(SRP): 各モジュールは1つの関心事を持つ

    • model.py: データ構造のみ(依存関係なし)

    • lister.py: ポート列挙のみ

    • tester.py: 接続テストのみ

    • diagnoser.py: ESP32診断のみ

  2. 関心の分離: UI層(CLI)とビジネスロジックを明確に分離

    • ロジック: src/haniwers/v1/port/

    • UI: src/haniwers/v1/cli/port.py

  3. 再利用性: ポート関数を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ファイル

src/haniwers/v1/port/__init__.py

src/haniwers/v1/port/model.py

src/haniwers/v1/port/lister.py

src/haniwers/v1/port/tester.py

src/haniwers/v1/port/diagnoser.py

src/haniwers/v1/port/exceptions.py

tests/v1/unit/port/test_model.py

修正ファイル

4ファイル

src/haniwers/v1/cli/port.py (530行削除)

tests/v1/unit/cli/port/test_flash_info.py (インポート更新)

tests/v1/unit/port/__init__.py (新規)

uv.lock (依存関係更新)

コード統計#

削除行数:    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ユーザー向け#

変更なし。すべてのコマンドが同じまま動作します。

🎓 学習ポイント#

優れていた点#

  1. 段階的抽出: モノリシックコードから焦点を絞ったモジュールへの移行がスムーズ

  2. テスト駆動: 既存CLIテストが後方互換性検証の強力なツール

  3. 明確な境界: 単責原則により各モジュールの責務が自明

チャレンジと対応#

  1. データモデルの粒度: DetectorData.from_line() の例外処理を詳細に記述

  2. テストカバレッジ: CLI層削減により新規ユニットテストで補完

  3. ドキュメント: 各モジュールに充実した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 リリース)