# 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なしで外部から使用可能
   ```python
   # 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 と使用例ドキュメント

**エクスポート**:
```python
from haniwers.v1.port import (
    # 関数
    list_available_ports,
    test_port_connectivity,
    diagnose_esp32,
    # データモデル
    DetectorData,
    FlashInfo,
    TestResult,
)
```

**使用例**:
```python
# ポート列挙
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行):
```python
# 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):
```python
# 成功結果の生成
result = TestResult.success_result(response_time=0.5, data_sample=line)

# 失敗結果の生成
result = TestResult.failure_result(error_type="timeout", message="...")
```

**2. クラスメソッドパターン** (DetectorData):
```python
# 文字列からオブジェクト生成
data = DetectorData.from_line(line)
```

**3. 検証メソッドパターン** (DetectorData, FlashInfo):
```python
# バリデーション
if data.is_valid():
    # 処理続行

# ヘルスチェック
if flash.is_healthy():
    # 正常処理
else:
    print(flash.get_diagnosis())  # 詳細な診断メッセージ
```

**4. 公開APIパターン** (__init__.py):
```python
# 外部からのインポート
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コマンド（変更なし）

```bash
# ポート一覧表示
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
```

### プログラマティック利用（新規）

```python
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` のみインポート更新が必要：

```python
# 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を付与

## 📦 インストール

```bash
pip install haniwers==1.7.0
```

または最新版：

```bash
pip install --upgrade haniwers
```

## 🔗 関連リンク

- [完全な変更ログ](../../CHANGELOG.md#170-2025-11-03)
- [実装の詳細](../../src/haniwers/v1/port/)
- [ユニットテスト](../../tests/v1/unit/port/)
- [進捗ログ](../progress/entries/2025-11-02-port-refactoring-complete.md)
- [ドキュメント](https://qumasan.gitlab.io/haniwers/docs/)
- [Issues](https://gitlab.com/qumasan/haniwers/-/issues)
- [リリースタグ](https://gitlab.com/qumasan/haniwers/-/tags/1.7.0)

---

**リリース担当**: shotakaha
**リリース日**: 2025-11-03
**バージョン**: 1.7.0 (MINOR update)
**実装期間**: 1日 (2025-11-02 実装, 2025-11-03 リリース)
