デバイスレス開発ガイド: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 を読み込み、すべてのコンポーネント設定を一元管理します。

基本的な使い方#

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

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

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

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 オブジェクトには以下のプロパティがあります:

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:

[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 コード#

# 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 実行方法#

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

5. Workflow 2:過去データを使ったデモ実行(Mocker)#

シナリオ#

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

5.1 設定ファイルの作成#

config.demo.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 コード#

# 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 実行方法#

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

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

6. Workflow 3:本番運用(Device)#

シナリオ#

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

6.1 ポート確認#

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

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

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

6.2 設定ファイルの作成#

config.prod.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 コード#

# 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 実行方法#

# 本番データ取得
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 を切り替えるには?#

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

# RandomMocker を使う(ハードウェアなし)
[device]
port = "/dev/null"

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

Python コードは同じ:

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

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

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

  1. 指定したファイル(例:config.prod.toml

  2. ./hnw.toml

  3. ./config.toml

  4. ./config/*.toml

  5. ホームディレクトリの haniwers 設定

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

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

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

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

関連資料#