fibril_can 開発者向け内部構造 (レイヤ責務と検証分担)
パッケージ間の責務境界と依存方向。ワイヤ+IR は core 一元、runtime は ROS/C++/malloc なし、bridge はスキーマ依存生成しない
要点: 各パッケージが分離されていることは「見栄え」ではなく機能要件。ワイヤ+IR を core に集めないと bridge が独自解釈してワイヤ不一致、runtime に malloc を撒くと決定性が壊れる、bridge がスキーマ依存生成すると slave のスキーマ変更で bridge 再ビルド。
依存方向 (ビルド時 + 実行時)
(build-time only) (run-time)
│ │
┌────────────────────┐ │ │
│ fibril_can_core │◀──┐ │ ┌──────────────────┴───┐
│ (proto + IR + 採番) │ │ │ │ fibril_can_bridge │
└────────────────────┘ │ │ │ (rclcpp) │
▲ │ │ └──────────────────────┘
│ │ │ │
┌─────────┴──────────┐ │ │ ▼
│ fibril_can_codegen │───┘ │ ┌──────────────────────┐
│ (YAML → C/C++) │ │ │ fibril_can_msgs │
└────────────────────┘ │ │ (NodeInfo, srv) │
│ │ └──────────────────────┘
▼ │ ┌──────────────────────┐
schema_gen.{h,c,hpp,cpp} │ │ ros_babel_fish │
schema_blob.c │ │ (動的 ROS 型解決) │
master_gen.{h,c} │ └──────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ fibril_can_runtime │ │ fibril_can_master_runtime│
│ (C99 slave) │ │ (C99 embedded master) │
└────────────────────┘ └──────────────────────────┘
ament 化なし、FetchContent / submodule で FW に取り込む
fibril_can_web (並列 TS 実装、C++ 側と独立)
@fibril/can-transport → @fibril/can-transport-{gsusb,mock}
→ @fibril/can-core → apps/{sniffer,master}矢印はビルド時 or 実行時依存。bridge と runtime は CAN バス経由でワイヤプロトコルで結ばれるが矢印は描けない (実体は SocketCAN + 物理 CAN FD、正本は SPEC.md)。
レイヤごとの責務
fibril_can_core — 共有知識の正本
ROS / CAN 非依存の C++ ライブラリ。ワイヤ + IR 知識の単一の正本。
- Protobuf schema (
.proto) + encode/decode - IR (
Field,Topic,Service,Param,BlockType,BlockArray,NodeSchema,SchemaDocument) - スカラ型表 (バイト数、C 型名、変換規則)
- 採番ロジック (
compute_numbering= blob と instance_counts から topic_index / service_index / param_index を導出する純関数) - packing (DLC 量子化、wire_size、frame 合計)
- ROS 型変換対応表
- SPEC 由来の定数 (protocol version、frame 最大長、予約 service_index)
fibril_can_codegen — YAML → C/C++ ソース
4 責務:
- 読み込み (
yaml_loader): YAML → IR - 静的検証 (
static_validator): SPEC §9.4 の検査を IR に適用 - IR 構築: 採番、ハッシュ計算、レポート用メタ情報 (core の純関数呼ぶ)
- emit: IR → 出力テキスト・バイト列 (C emitter / C++ emitter は別ファイルだが入出力は同型)
fibril_can_runtime — スレーブ C99 ランタイム
ROS / C++ / 動的確保 いずれも非依存。ament 化なし (submodule / FetchContent)。責務:
- SPEC §5〜§8 のスレーブ側全機能
- 状態機械 (UNPROVISIONED → PROVISIONED → RUNNING、FAULT、T_listen、ID 衝突)
- フレームテーブル (FRAME_DEFINE で構築、DISCOVER でクリア)
- データプレーンホットパス (送信スケジューラ、受信展開、Topic seqlock)
- Service (SINGLE / SEGMENT 転送)
- Parameter 保持 + PARAM_SET 適用
- 設定プレーンコマンドディスパッチ + ACK/NAK 生成
- master-lost watchdog
- HAL 境界 (CAN driver 抽象化
fcan_hal_t)
スキーマ非依存: スキーマで変わるのは「生成 const テーブル」と「生成ディスパッチャ」だけ、ランタイム本体は共通。
fibril_can_master_runtime — 組み込みマスター C99
fibril_can_bridge のサブセット、ROS / ros_babel_fish / SocketCAN 依存なし。fcan_codegen --role=master の const テーブルを消費。PC ではない MCU 上の master 用。
fibril_can_bridge — ROS ブリッジ
rclcpp コンポーネント。責務:
- DISCOVER / ANNOUNCE / SCHEMA_READ でのスレーブ発見と blob 取得
- スキーマから ROS entity 動的構築 (GenericPublisher / GenericSubscription / generic service / parameter declare)
- ROS ↔ CAN 変換 (field 単位)
- フレーム配置決定 (SPEC §6.3 のビンパッキング)
- FRAME_DEFINE + START の発行、ACK 待ち、ロールバック
- Service 呼び出し追跡、分割転送組立、timeout
- 再起動検知 (boot_id 不一致) と自動再プロビジョン
- ROS param 永続化 + PARAM_SET 注入
- 名前衝突検出 (NameOwnership)
- Field 再構成 (field_overrides + full reprovision)
スキーマ依存の生成コードを持たない、全 ROS entity は blob と実数ベクトルから runtime 組立。slave 側スキーマ変更で bridge 再ビルド不要。
fibril_can_msgs — 最小 ROS interface
NodeInfo.msg + ReprovisionNode.srv のみ。builtin_interfaces だけ依存 (重い ROS パッケージ引き込み防止)。
fibril_can_web — TypeScript 並列実装
C++ bridge と同じワイヤプロトコルを TS で並列実装。責務分けは C++ 側と対称:
@fibril/can-transport: raw CAN frame 抽象、fibril_can セマンティクス無し@fibril/can-transport-{gsusb,mock}: 実装@fibril/can-core: TS 側のワイヤ+IR 単一正本 (C++ 側fibril_can_coreと対、二重実装)apps/{sniffer,master}: React app
typed bindings (schema.blob → TS interface + path リテラル型) 生成器は未着手。詳細は fibril_can Web マスター。
境界をまたぐ鉄則
① ワイヤ + IR 知識は core に集める
ハッシュの取り方、wire_size の数え方、採番表、ROS 型対応は fibril_can_core (or @fibril/can-core) 以外に書かない。bridge が独自に「実は MotorState は X バイト」と書いた瞬間、スキーマ変更で片側だけ追従してワイヤ不一致になる。
② codegen と bridge は同じ採番関数を呼ぶ
SPEC §4.2 は「master (ANNOUNCE 受信時) と slave (fcan_init 時) が同一の表を導出」を要求。同じロジックでは足りず、同じコードを両者が呼ぶ。compute_numbering を bridge 側でコピペしない。
③ ランタイムに ROS / C++ / 動的確保を持ち込まない
MCU 上で動くため。fcan_init 後にアロケータを呼ばない制約は性能ではなく決定性のため、断片化と非決定性を構造的に排除。
④ ブリッジはスキーマ依存コードを生成しない
find_package(some_schema_pkg) を要求しない (slave 側スキーマ変更で bridge を再ビルドさせる罠)。ROS 型解決は ros_babel_fish が実行時に行う。
⑤ セーフティ (ウォッチドッグ等) はスキーマに持たせない
指令途絶時のサーボオフはアプリ層の責務、フレームワークが判断しない (SPEC §1.3-6)。
検証の分担
| 検査 | 実行場所 | タイミング |
|---|---|---|
| 構文 / サイズ / 採番 / 型カテゴリ整合 | fibril_can_codegen の static_validator | コード生成時 (ROS 不要) |
field_map の path 実在 | fibril_can_bridge | ノード接続時 |
ファームウェア側 CI は ROS 抜きで完結する。ROS 側改修も bridge 単独でテスト可。
開発者向けドキュメントの入口
docs/developer-guide/ 配下:
| ファイル | 内容 |
|---|---|
architecture-layers.md | 責務境界と依存方向 (本ノートの元) |
core-internals.md | IR、Protobuf blob、採番、packing、ROS 型変換表の内部 |
codegen-internals.md | YAML → IR → emitters のフェーズ分解、C/C++ emitter のサブモジュール |
runtime-internals.md | 状態機械、フレームテーブル、seqlock、サービス、設定プレーン |
bridge-internals.md | BridgeNode / NodeAgent / TopicIoEngine / FramePlanner / SocketCAN |
router-internals.md | ルータの状態・学習テーブル・転送規則・self HAL ラッパ |
web-internals.md | @fibril/can-core の TS 実装、既知の落とし穴 |
spec-supplements.md | SPEC.md の節と実装対応、設計判断の理由 |
file-overview.md | ソースツリー全体の 1 行サマリ索引 |
ドキュメント更新責務
コードを変えたら同じ PR で対応 doc も更新する (ズレは不変条件違反扱い)。
| 変更種別 | 必須更新先 |
|---|---|
| ワイヤ / プロトコル | SPEC.md (先に) → docs/06-protocol.md |
| 公開 API | 03-schema / 04-runtime / 05-bridge / 08-cpp-wrapper の該当章 |
| 責務境界 / 依存方向 | developer-guide/architecture-layers.md |
| ファイル / パッケージ追加削除 | developer-guide/file-overview.md, 07-packages.md |
| 内部実装の構造変更 | developer-guide/*-internals.md |
| 非自明な設計判断 | docs/adr/ に新規 ADR |
ADR を書くのは: 不変条件の増減 / レイヤ越境例外 / 外部依存追加 / 互換性破壊 / 性能・決定性のための非自明な選択。選択の理由と却下した選択肢を残す。