# E2E テレコマ動作確認システム
実機フライトソフト / SILS の振る舞いを、**テレコマンドバス (MQTT) 経由で外部から E2E 検証**する仕組みです。
「`RESET` を送ると本当にソフトウェアリセットがかかるか」「`CONTROL,OFF` で制御が止まるか」「`CAPTURE` で画像が降りてくるか」といったシナリオを、コマンド送信と受信テレメトリの観測だけで自動判定します。
実機と SILS は同じ MQTT トピック (`sat/command` / `sat/telemetry` / `sat/image`) と共通テレメトリスキーマ (`shared/shared/telemetry_schema.json`) を喋るため、**同一のシナリオコードを `--target=sils|hardware` で両対象に当てられます**。
## 構成
```mermaid
flowchart LR
subgraph Test[pytest E2E ハーネス ground/e2e/]
TC[TelecommandClient
MQTT ラッパ]
SC[シナリオテスト群]
SC --> TC
end
TC -- publish sat/command --> B[(MQTT ブローカー
mosquitto)]
B -- sat/telemetry / sat/image --> TC
B <-- TCP80/81 --> BR[bridge.py]
BR <-- WiFi --> HW[実機 Pico W]
B <-- 直結 --> SILS[SILS]
style HW fill:#fdd
style SILS fill:#ddf
```
テストは MQTT ブローカーにだけ依存します。`--target=hardware` では別途 `bridge.py` + 実機が、`--target=sils` では SILS が、同じブローカーにぶら下がっている前提です。
実機 / SILS の差 (リセット時の挙動・所要時間・期待 `reset_reason`) は `e2e/targets.py` の `TargetProfile` に閉じ込め、シナリオ本体は共通化しています。
## ファイル構成
| ファイル | 役割 |
|---------|------|
| `e2e/telecommand.py` | `TelecommandClient` — コマンド送信、テレメトリ・画像の subscribe と待機ヘルパ |
| `e2e/targets.py` | `TargetProfile` — sils / hardware ごとのタイムアウト・期待値 |
| `conftest.py` | `--target` / `--broker-host` オプション、fixtures、マーカー skip |
| `tests/test_reset.py` | ソフトウェアリセット試験 (フラッグシップ) |
| `tests/test_control.py` | コマンド → テレメトリ反映 (parametrize でケース追加可) |
| `tests/test_target_capture.py` | 撮影・画像ダウンリンク |
| `tests/test_liveness.py` | テレメトリ疎通・レート・コマンドカウンタ |
## RESET 試験の判定基準
テレメトリの `uptime` / `command_count` / `reset_reason` の変化から、再起動の成立を判定します。
| 指標 | RESET 前 | RESET 後 | 意味 |
|------|---------|---------|------|
| `uptime` [ms] | 大きい値 | 大幅に低下 | 起動からの経過時間がリセット = 再起動した |
| `command_count` | > 0 | ほぼ 0 | コマンドカウンタがリセット |
| `reset_reason` | 直前要因 | SOFTWARE/WATCHDOG | リセット要因 (補助判定) |
- **主基準**は複合 (`uptime` 低下 + `command_count` リセット)。SILS は接続維持のまま値が即時リセットされ、実機は瞬断後に `bridge.py` が再接続して低 `uptime` で復帰します。どちらも同じ条件式で検出できます。
- `reset_reason` は補助判定です。実機の値は `vreg_and_chip_reset_hw->chip_reset` (`main.cpp`) の生値で、ブリングアップ時に実測して `targets.py` の `HARDWARE.expected_reset_reason` を校正します。未校正でも再起動自体の検証は独立した別テストで担保されます。
## 実行方法
### SILS (CI 相当・ローカル)
```bash
uv sync --all-packages --dev # 一度だけ全パッケージ同期
cd ground && docker compose up -d mosquitto # MQTT ブローカー
uv run --no-sync python -m sils # 別シェルで SILS 起動
uv run --no-sync pytest ground/e2e/tests --target=sils -v
```
`--no-sync` は `uv run` の自動同期がワークスペースの editable (`sils` / `simulator`) を
prune して SILS が起動できなくなるのを防ぐため (`uv sync --all-packages --dev` 実施済みが前提)。
### 実機 (HILS)
```bash
# mosquitto + 実機 Pico W 稼働 + bridge.py 起動 の状態で
uv run pytest ground/e2e/tests --target=hardware -v
```
## テストケースの追加と pytest 引数
素の pytest に乗っているため、標準の引数・機能がそのまま使えます。
- **単純なケース** (コマンド → テレメトリ反映) は `tests/test_control.py` の `CASES` 表に 1 行追加するだけで増やせます。
- **複雑なシナリオ**は専用 `test_*.py` に「1 関数 = 1 シナリオ」で追加します。
- **マーカー**で対象を絞れます: `hardware` (実機限定 / sils では自動 skip)、`slow` (RESET 等)、`image` (撮影系)。
- 主な引数: `-k <式>` (名前選択)、`-m <マーカー>` (種別選択)、`-x` / `--maxfail` (失敗で停止)、`-v` / `-s` (出力)、`--target` / `--broker-host` (独自)。
```bash
# 例: 制御系のみ、低速試験を除外して実行
uv run pytest ground/e2e/tests -m "not slow" -k control --target=sils -v
```
## 既知の課題 / TODO
### 姿勢制御が目標近傍で整定せず自動撮影が発火しない
`test_target_triggers_auto_capture` (TARGET → ミッション成功時の自動撮影) は現在 **xfail** です。
- **症状**: 目標角に対し太陽方向は許容誤差 (±10°) 内まで寄るものの、制御がバンバン制御 (固定量 `pulse_delta_us` のキック) のため整定せず、角速度が ±18°/s 規模で振動する**限界サイクル**に入る。`STABLE` 状態かつ `|error| < 許容` かつ `角速度 < 0.5°/s` が同時に成立して `is_target_in_angle` が立つ瞬間が、1Hz のテレメトリ/撮影判定境界と安定して一致しない。結果、`is_control_finished()` を契機とする自動撮影が発火しない。
- **重要**: これは **SILS で再現する不具合**であり、同一の制御ロジックを使う**実機でも発火しない**。したがって hardware 限定にして隠すのではなく、xfail として可視化する。
- **影響箇所 (移植関係・同期修正が必要)**:
- `satelite/firmware/control.cpp` の `STABLE` 状態 (実機 FSW)
- `satelite/sils/sils/controller.py` の `STABLE` 状態 (SILS 移植)
- **修正方針 (いずれか / 組み合わせ)**:
1. `error` に比例してパルス変化量を縮小する比例制御化 (目標近傍でキックを弱める)
2. デッドバンド導入 — 小誤差では `LOTATE` に遷移させない
3. 整定保持 — `|error| < 許容` を一定時間継続したら `is_target_in_angle` を確定する
- **完了条件**: 上記修正後、`is_control_finished()` が安定して立つことを確認し、`test_target_triggers_auto_capture` の `@pytest.mark.xfail` を外す。