Skip to content

Contracts: Per-Body Frames

> Aligned with PCR Master Blueprint v1.0 — see Blueprint §2.2. > 职责:定义所有跨子系统对话用的 POD 数据包。这些类型是 header-only,零 .cpp 文件,纯 struct,可被任何子域(plant / avionics / fcc / bus / dynamics_core / simulation)#include。 > C-Distillation note: 所有类型都是平铺 struct,无虚函数、无继承、无 STL 容器(除 std::vector / std::variant / std::optional,蒸馏时会被替换为定长 C 数组 + tag union + nullable 指针)。


1. 为什么需要 contracts/

13 库依赖图中,业务子域之间互不依赖(除了 avionics→plant 的特例)。但跨子域通信必然需要共享的数据语义。如果让 fcc 直接 include avionics 的内部 header 来读 IMU 数据,整个无知性承诺就崩塌了。

contracts/ 就是中立词汇表

  • fcc 只 include contracts/ 来理解 FccInFrame.imu.delta_v
  • avionics 只 include contracts/ 来产出 BusMessageDynInFrame
  • dynamics_core 不 include contracts/(它是 universal,连具体数据包都不知道)— 唯一例外是 Forces 在 dynamics_core 内部定义
  • simulation 是唯一一层会全部 include,因为它装配所有边界
mermaid
graph TB
    contracts[contracts/PerBodyFrames.h<br/>header-only POD]
    plant -.read.-> contracts
    avionics -.read+write.-> contracts
    fcc -.read+write.-> contracts
    bus -.read+write.-> contracts
    sim -.read+write.-> contracts
    contracts --> types[types/Vec3, Quat, Time]
    contracts --> frames[frames/]

2. 强类型 IDs

避免 uint32_t 互相误传:

cpp
// contracts/Ids.h
enum class BodyId   : uint32_t {};
enum class EngineId : uint32_t {};
enum class ServoId  : uint32_t {};
enum class FinId    : uint32_t {};
enum class ImuId    : uint32_t {};
enum class GpsId    : uint32_t {};
enum class IcuId    : uint32_t {};
enum class BusAddr  : uint32_t {};

enum class 强类型 + uint32_t 底层 → 无虚表、无封装、与 C 完全互兼。

BodyId 直接是 WorldState.bodies 的下标(uint32_t cast),无 hash map 开销。EngineIdRocketBody.engines 内 unique,跨 body 不复用。


3. Dynamics 侧契约(plant ⇄ dynamics_core)

3.1 DynInFrame:Avionics → Plant Physics

per-body,每 tick 由 avionics device step functions 累积生成,喂给 plant::physics::compute_*dynamics_core pipeline。

cpp
struct DynInFrame {
    BodyId                      body_id;
    std::vector<EngineEffect>   engine_effects;
    std::vector<FinDeflection>  fin_deflections;
    std::vector<DiscreteEvent>  events;
};

struct EngineEffect {
    EngineId  engine_id;
    double    vacuum_thrust;       // 真空推力(N)
    double    mass_flow_fuel;      // 燃料质量流(kg/s)
    double    mass_flow_ox;        // 氧化剂质量流(kg/s)
    double    current_throttle;    // 0.0 ~ 1.0
    Angle     nozzle_pitch;        // 俯仰摆角
    Angle     nozzle_yaw;          // 偏航摆角
};

struct FinDeflection {
    FinId    fin_id;
    Angle    actual_angle;         // 来自 plant::hardware::FinMech 演化
};

关键设计DynInFrame 不携带 Vec3_T<BODY> force_b 之类的"已经算好的合力"。力的计算是 plant/physics/compute_* 的职责,本帧只携带"激励量"(engine state、fin angle)。这保持了 plant/physics 的"配方与流水线分离"。

3.2 DynOutFrame:Plant Physics → Avionics

每 body 一份,作为 Sensor 真值输入。所有空间量用 ECF

cpp
struct DynOutFrame {
    BodyId                          body_id;
    Vec3_T<frame::ECF>              pos_ecf;
    Vec3_T<frame::ECF>              vel_ecf;
    Quat_T<frame::LIC, frame::BODY> att_q;          // 体相对 LIC 的姿态
    Vec3_T<frame::BODY>             omega_b;        // 体角速度
    Vec3_T<frame::BODY>             spec_force_b;   // 比力(IMU 测量)
    double                          total_mass;
    Vec3_T<frame::BODY>             centroid_b;
};

用法

  • ImuUnit::stepspec_force_b + omega_b,加噪声/量化,产出 ImuPayload
  • GpsUnit::steppos_ecf + vel_ecf,加噪声/延迟,产出 GpsPayload
  • Telemetry 读所有字段下传地面

4. FCC 侧契约(bus ⇄ fcc)

4.1 FccInFrame:Bus → FCC

cpp
struct FccInFrame {
    ImuPayload  imu;
    GpsPayload  gps;
    Time        timestamp;
};

精简结构:FCC 只关心 IMU 与 GPS 两类传感器输入。其他航电态信息(如 ECU 反馈)通过 BusBuffer 独立访问,不打包进 FccInFrame

4.2 FccOutFrame:FCC → Bus → Avionics

cpp
struct FccOutFrame {
    ScuPayload        scu;   // 伺服控制指令
    IcuPayload        icu;   // 时序/事件指令
    EcuPayload        ecu;   // 引擎控制指令
    TelemetryPayload  tlm;   // 下行遥测
};

每个 *Payload 直通对应的设备 step function 解码。FccOutFrame 是 FCC 周期性产出的"控制周期结论"。


5. Bus 侧契约(所有 device ⇄ bus)

5.1 BusMessage:总线上的统一信封

cpp
struct BusMessage {
    BusAddr  src;
    BusAddr  dst;
    Time     emitted_at;
    std::variant<
        ImuPayload, GpsPayload,
        ScuPayload, EcuPayload, IcuPayload, FinCtrlPayload,
        TelemetryPayload
    > payload;
};

std::variant 是 tagged union — 在 C-Distillation 阶段会被替换为:

c
struct BusMessage {
    uint32_t  src;
    uint32_t  dst;
    int64_t   emitted_at;  // ticks
    uint8_t   payload_kind;
    union {
        ImuPayload  imu;
        GpsPayload  gps;
        /* ... */
    } payload;
};

5.2 各类 Payload

cpp
// 传感器输出 payload(来自 avionics::device::*)
struct ImuPayload {
    Vec3_T<frame::BODY>  delta_v;     // 周期内累积比力积分
    Vec3_T<frame::BODY>  delta_theta; // 周期内累积角速度积分
    Time                 dt;
};

struct GpsPayload {
    Vec3_T<frame::ECF>   pos_ecf;
    Vec3_T<frame::ECF>   vel_ecf;
    Time                 timestamp;
    uint8_t              fix_quality;
};

// 控制 payload(FCC → 设备)
struct ScuPayload {
    ServoId  servo_id;
    Angle    target_angle;
};

struct EcuPayload {
    EngineId  engine_id;
    enum class Cmd : uint8_t { Ignite, Throttle, Shutdown };
    Cmd       cmd;
    double    throttle_target;
};

struct IcuPayload {
    enum class Event : uint8_t { StageSeparation, FinDeploy, BoltFire };
    Event     event;
    Time      execute_at;
};

struct FinCtrlPayload {
    FinId    fin_id;
    Angle    target_angle;
};

struct TelemetryPayload {
    // 下行遥测打包格式(视具体地面站协议而定)
    std::array<uint8_t, 128>  frame;
    uint16_t                   length;
};

6. 离散事件(DiscreteEvent)

DiscreteEvent跨 tick 的状态拓扑改变信号。它被 avionics device step function 产出,由 dynamics::algebra::evolve_topology 在下个 tick 之前消费。

cpp
struct DiscreteEvent {
    enum class Kind : uint8_t {
        EngineShutdown,
        BoltFired,
        StageSeparation,
        FinDeploy
    };
    Kind         kind;
    BodyId       source_body;
    uint32_t     payload_id;       // 配合 kind 解释(如 EngineId 数值)
    Time         emitted_at;
};

使用流程

  1. ECU step 检测到推力耗尽 → 产出 DiscreteEvent{EngineShutdown, body_id, engine_id, now}
  2. 写入当前 body 的 DynInFrame.events
  3. sim::world_tick 末尾,dynamics::algebra::evolve_topology 扫描所有 body 的 events,决定 WorldStage 转移与 body 拓扑变更
  4. 下一 tick 开始时,新拓扑生效

> 详见 05_Dynamics_Core/Topology_Algebra.md 与 Blueprint §2.9 / §7.3。


7. 命名与设计约定

约定说明
后缀 _ecf / _lic / _b显式标注空间向量所在 frame,避免靠类型推断
Payload 后缀仅用于 BusMessage::payload 内部 variant 候选
Frame 后缀跨子域大颗粒数据包(DynInFrame, FccOutFrame
Effect 后缀设备 step 函数产出的"激励量"(EngineEffect
Event 后缀离散拓扑改变信号(DiscreteEvent
强类型 ID全部 enum class : uint32_t {},无空 enumerator
零虚函数所有类型为 POD-like,可 trivially copy
零 .cppcontracts/ 全部 header-only

8. 反模式(必须避免)

反模式为什么不行
DynInFrame 里塞 Vec3_T<BODY> total_force_b越权:合力计算属 plant/physics,本帧只能携带"激励"
FccInFrame 里塞 std::vector<BusMessage> uplink 队列uplink 通过 BusBuffer 独立通道,不入 FccInFrame
BusMessage 加新 payload 类型时不更新 variantvariant 闭合,新 payload 必须显式列入
EngineId 直接传 uint32_t 字面量到接口编译期防误传依赖 enum class
在 contracts 里 include plant/model/EngineSpec.hcontracts 是中立层,不依赖任何业务子域
在 contracts 里定义带虚函数的 base class破坏 C-Distillation 路径

9. Cross References

  • Frame 命名与转换 → 01_Foundation/Coordinate_Frames.md
  • 推力流如何从 EcuPayload 走到 EngineEffect → Blueprint §4(推力跨域数据流五阶段)
  • Forces(Monoid)与 DynInFrame 的区别 → 05_Dynamics_Core/Forces_Monoid.md
  • BusMessage 编解码器 → 03_Avionics_and_Bus/Semantic_Bus_Pattern.md
  • 离散事件如何驱动拓扑演化 → 05_Dynamics_Core/Topology_Algebra.md