# デバイスレス開発ガイド：ConfigLoader でハードウェアなしで開発する

## 目次

1. 概要
2. 3つのデバイスタイプ
3. ConfigLoader の基本
4. Workflow 1：ローカル開発（RandomMocker）
5. Workflow 2：CI/CD テスト（Mocker）
6. Workflow 3：本番運用（Device）
7. よくある質問

---

## 1. 概要

このガイドでは、`ConfigLoader` を使ってハードウェアなしで haniwers を開発・テストする方法を説明します。

**キーコンセプト**：

- ConfigLoader が TOML ファイルから設定を読み込む
- 設定に基づいて Device、Mocker、RandomMocker を選択
- コードを変更せず、設定ファイルだけでデバイスを切り替え可能

---

## 2. 3つのデバイスタイプ

### Device：本番運用（実ハードウェア）

- 用途：OSECHI 検出器から実際のデータを取得
- 実行環境：実ハードウェアを接続したマシン
- 特徴：本物のセンサー値を取得

### Mocker：再現テスト（記録済みデータ再生）

- 用途：記録済み CSV ファイルからイベントを再生
- 実行環境：開発環境、CI/CD パイプライン
- 特徴：完全に再現可能、決定論的

### RandomMocker：開発・ストレステスト（ランダム生成）

- 用途：テストデータが不要な開発やストレステスト
- 実行環境：ローカル開発
- 特徴：ハードウェア不要、高速

---

## 3. ConfigLoader の基本

### ConfigLoader とは

ConfigLoader は TOML 設定ファイルから HaniwersConfig を読み込み、すべてのコンポーネント設定を一元管理します。

### 基本的な使い方

**設定ファイルを明示的に指定する場合**:

```python
from pathlib import Path
from haniwers.v1.config.loader import ConfigLoader

# ConfigLoader インスタンスを作成
loader = ConfigLoader(Path("custom.toml"))

# config プロパティで HaniwersConfig を取得
config = loader.config

# 各設定にアクセス
print(config.device.port)       # "/dev/cu.usbserial-140"
print(config.device.baudrate)   # 115200
print(config.daq.workspace)     # "./data"
print(config.mocker.speed)      # 5.0
```

**デフォルト設定を自動検索する場合**:

```python
from haniwers.v1.config.loader import ConfigLoader

# 設定ファイルを指定しない場合、自動検索
loader = ConfigLoader()
config = loader.config
```

### 設定の検索順序

ConfigLoader は以下の順序で設定ファイルを探します：

1. `./hnw.toml`
2. `./config.toml`
3. `./config/*.toml` （アルファベット順）
4. `~/.config/haniwers/hnw.toml` （macOS/Linux）
5. `~/.config/haniwers/config.toml` （macOS/Linux）
6. `~/.config/haniwers/*.toml` （アルファベット順）

**見つからない場合**: `FileNotFoundError` が発生

### HaniwersConfig の構造

ConfigLoader が返す `config` オブジェクトには以下のプロパティがあります：

```python
config.device    # DeviceConfig：デバイス接続設定
config.daq       # DaqConfig：データ取得出力設定
config.scan      # ScanConfig：スキャン設定
config.sensors   # Dict[str, SensorConfig]：センサー設定
config.mocker    # MockerConfig：モッカー設定
```

---

## 4. Workflow 1：ローカル開発（RandomMocker）

### シナリオ

ハードウェアなしでデータ取得パイプラインを開発したい。

### 4.1 設定ファイルの作成

**config.local.toml**:

```toml
[device]
label = "local-dev"
port = "/dev/ttyUSB0"
baudrate = 115200
timeout = 1.0

[daq]
label = "dev-daq"
workspace = "./sandbox"
filename_prefix = "random_mock"
filename_suffix = ".csv"
events_per_file = 1000
number_of_files = 5
stream_mode = true

[mocker]
csv_path = null
shuffle = false
speed = 5.0
jitter = 0.01
loop = false

[sensors.ch1]
label = "top"
step_size = 1
threshold = 500
center = 512
nsteps = 10

[sensors.ch2]
label = "mid"
step_size = 1
threshold = 300
center = 311
nsteps = 10

[sensors.ch3]
label = "btm"
step_size = 1
threshold = 400
center = 420
nsteps = 10

[scan]
label = "scan"
workspace = "./sandbox"
filename_prefix = "scan_data"
filename_suffix = ".csv"
events_per_file = 1000
number_of_files = 1
stream_mode = true
duration = 10.0
suppress_threshold = 1000
max_retry = 3
```

### 4.2 Python コード

```python
# custom_script.py
from pathlib import Path
from haniwers.v1.config.loader import ConfigLoader
from haniwers.v1.daq.mocker import RandomMocker
from haniwers.v1.daq.sampler import Sampler

def main():
    # 設定を読み込み
    loader = ConfigLoader(Path("config.local.toml"))
    config = loader.config

    # RandomMocker を作成
    mocker = RandomMocker(config.mocker, seed=42)

    # Sampler を作成
    output_dir = Path(config.daq.workspace)
    output_dir.mkdir(parents=True, exist_ok=True)
    sampler = Sampler(mocker, config.daq, output_dir)

    # データ取得実行
    try:
        sampler.acquire_by_count(
            file_path=output_dir / f"{config.daq.filename_prefix}_001{config.daq.filename_suffix}",
            event_count=config.daq.events_per_file
        )
        print("✅ データ取得完了")
    finally:
        mocker.close()

if __name__ == "__main__":
    main()
```

### 4.3 実行方法

```bash
# ローカル開発環境で実行
uv run python custom_script.py
```

---

## 5. Workflow 2：過去データを使ったデモ実行（Mocker）

### シナリオ

記録済み CSV ファイルからイベントを再生してデータを取得し、結果をリアルタイムで表示したい。

### 5.1 設定ファイルの作成

**config.demo.toml**:

```toml
[device]
label = "demo-replay"
port = "/dev/null"
baudrate = 115200
timeout = 1.0

[daq]
label = "demo-daq"
workspace = "./demo-output"
filename_prefix = "replay"
filename_suffix = ".csv"
events_per_file = 100
number_of_files = 10
stream_mode = true

[mocker]
csv_path = "./examples/data/cosmic_rays.csv"
shuffle = false
speed = 10.0
jitter = 0.0
loop = false

[sensors.ch1]
label = "top"
step_size = 1
threshold = 500
center = 512
nsteps = 10

[sensors.ch2]
label = "mid"
step_size = 1
threshold = 300
center = 311
nsteps = 10

[sensors.ch3]
label = "btm"
step_size = 1
threshold = 400
center = 420
nsteps = 10

[scan]
label = "scan"
workspace = "./demo-output"
filename_prefix = "scan_data"
filename_suffix = ".csv"
events_per_file = 1000
number_of_files = 1
stream_mode = true
duration = 10.0
suppress_threshold = 1000
max_retry = 3
```

### 5.2 Python コード

```python
# demo_replay.py
from pathlib import Path
from haniwers.v1.config.loader import ConfigLoader
from haniwers.v1.daq.mocker import Mocker
from haniwers.v1.daq.sampler import Sampler
import subprocess
import threading
import time

def main():
    # 設定を読み込み
    loader = ConfigLoader(Path("config.demo.toml"))
    config = loader.config

    # 出力ディレクトリを作成
    output_dir = Path(config.daq.workspace)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Mocker を作成（CSV ファイルから再生）
    mocker = Mocker(config.mocker, seed=None)

    # Sampler を作成
    sampler = Sampler(mocker, config.daq, output_dir)

    # データ取得実行
    try:
        output_file = output_dir / f"{config.daq.filename_prefix}_001{config.daq.filename_suffix}"
        sampler.acquire_by_count(
            file_path=output_file,
            event_count=config.daq.events_per_file * config.daq.number_of_files
        )
        print("✅ デモ実行完了")
    finally:
        mocker.close()

if __name__ == "__main__":
    main()
```

### 5.3 実行方法

```bash
# デモを実行
uv run python demo_replay.py

# 別のセッションからtail -f でリアルタイム表示
tail -f demo-output/replay_001.csv
```

---

## 6. Workflow 3：本番運用（Device）

### シナリオ

実OSECHI検出器を接続して、本物のセンサーデータを取得する。

### 6.1 ポート確認

まず、OSECHI検出器が接続されているシリアルポートを確認します：

```bash
# 利用可能なシリアルポートを一覧表示
uv run haniwers-v1 port list

# デバイスの接続確認
uv run haniwers-v1 port test /dev/cu.usbserial-14230
```

### 6.2 設定ファイルの作成

**config.prod.toml**:

```toml
[device]
label = "osechi-main"
port = "/dev/cu.usbserial-14230"
baudrate = 115200
timeout = 5.0

[daq]
label = "production"
workspace = "./data/production"
filename_prefix = "osechi_data"
filename_suffix = ".csv"
events_per_file = 5000
number_of_files = 10
stream_mode = true

[sensors.ch1]
label = "top"
step_size = 1
threshold = 500
center = 512
nsteps = 10

[sensors.ch2]
label = "mid"
step_size = 1
threshold = 300
center = 311
nsteps = 10

[sensors.ch3]
label = "btm"
step_size = 1
threshold = 400
center = 420
nsteps = 10

[scan]
label = "scan"
workspace = "./data/production"
filename_prefix = "scan_data"
filename_suffix = ".csv"
events_per_file = 1000
number_of_files = 1
stream_mode = true
duration = 10.0
suppress_threshold = 1000
max_retry = 3
```

### 6.3 Python コード

```python
# production_daq.py
from pathlib import Path
from haniwers.v1.config.loader import ConfigLoader
from haniwers.v1.daq.device import Device
from haniwers.v1.daq.sampler import Sampler

def main():
    # 設定を読み込み
    loader = ConfigLoader(Path("config.prod.toml"))
    config = loader.config

    # 実デバイスに接続
    device = Device(config.device)

    # 出力ディレクトリを作成
    output_dir = Path(config.daq.workspace)
    output_dir.mkdir(parents=True, exist_ok=True)

    # Sampler を作成
    sampler = Sampler(device, config.daq, output_dir)

    # データ取得実行
    device.connect()

    try:
        output_file = output_dir / f"{config.daq.filename_prefix}_001{config.daq.filename_suffix}"
        sampler.acquire_by_count(
            file_path=output_file,
            event_count=config.daq.events_per_file * config.daq.number_of_files
        )
        print("✅ 本番データ取得完了")
    finally:
        device.disconnect()

if __name__ == "__main__":
    main()
```

### 6.4 実行方法

```bash
# 本番データ取得
uv run python production_daq.py
```

---

## 7. よくある質問

### Q: RandomMocker と Mocker の違いは？

| 項目 | RandomMocker | Mocker |
|-----|-------------|--------|
| データ源 | ランダム生成 | CSV ファイル |
| 再現性 | 低い（seed で部分的に制御可能） | 高い（完全に再現可能） |
| セットアップ | CSV ファイル不要 | CSV ファイル必須 |
| 速度 | 高速（メモリ内で生成） | 中速（ファイル読み込み） |
| 用途 | ローカル開発、ストレステスト | テスト、デバッグ、デモ |
| 設定ファイル例 | `csv_path = null` | `csv_path = "./data.csv"` |

### Q: Device と RandomMocker を切り替えるには？

設定ファイルのポート設定を変更するだけです。コードは変わりません：

```toml
# RandomMocker を使う（ハードウェアなし）
[device]
port = "/dev/null"

# Device を使う（ハードウェアあり）
[device]
port = "/dev/cu.usbserial-14230"
```

Python コードは同じ：

```python
device = Device(config.device)  # ポート設定に応じて自動選択
```

### Q: 設定ファイルが見つからないエラーが出た

ConfigLoader は以下の場所を順に探します：

1. 指定したファイル（例：`config.prod.toml`）
2. `./hnw.toml`
3. `./config.toml`
4. `./config/*.toml`
5. ホームディレクトリの haniwers 設定

見つからない場合は、明示的に設定ファイルを指定してください：

```python
loader = ConfigLoader(Path("config.local.toml"))
```

または、以下のコマンドで確認：

```bash
ls -la hnw.toml config.toml config/*.toml 2>/dev/null
```

---

## 関連資料

- [`src/haniwers/v1/config/loader.py`](../../../src/haniwers/v1/config/loader.py) - ConfigLoader の実装
- [`src/haniwers/v1/config/model.py`](../../../src/haniwers/v1/config/model.py) - 設定モデル
- [`src/haniwers/v1/daq/device.py`](../../../src/haniwers/v1/daq/device.py) - Device クラス
- [`src/haniwers/v1/daq/mocker.py`](../../../src/haniwers/v1/daq/mocker.py) - Mocker/RandomMocker
- [`src/haniwers/v1/daq/sampler.py`](../../../src/haniwers/v1/daq/sampler.py) - Sampler クラス
- [`examples/`](../../../examples/) - 設定ファイルの例
