Skip to content

IDynChannel — SIL / HIL 信道抽象

> Status: NEW · 已对齐 PCR Master Blueprint v1.0 §1.3 / §2.10 > 范畴: runtime/{IDynChannel, InMemoryDynChannel, UdpDynChannel}.{h,cpp} > 依赖: contracts(DynInFrame/DynOutFrame) > 被依赖: runtime/Runner


1. 问题陈述

仿真有四种部署模式(Blueprint §1.3):

模式AvionicsSystem 位置PlantPhysics 位置DynChannel 类型
avionics_dry本进程冻结InMemory(plant 不演化,FCC 走时序)
sil_monolithic本进程本进程InMemory
hil_dyn本进程远端(UDP server)Udp Client
hil_fcc远端 RTOS本进程(UDP server)Udp Server

关键洞察(Blueprint §1.3):HIL 的本质不是重新设计架构,而是在两个数据包边界上换信道

  • DynChannel 跨 AvionicsSystem ↔ PlantPhysics(payload = DynInFrame / DynOutFrame
  • Bus 跨 FCC ↔ Devices(payload = BusMessage

反例:HIL 需要重写 world_tick、新写一套通信状态机、双份控制律代码。 正解:BodyTick / WorldTick 骨架不变,仅替换 IDynChannel::send/recvIBus 的具体实现。


2. 接口

cpp
// runtime/IDynChannel.h
namespace runtime {

class IDynChannel {
public:
    virtual ~IDynChannel() = default;

    // Plant → Avionics(plant 把 DynOut 发出)
    virtual void send(const std::vector<contracts::DynOutFrame>& frames) = 0;

    // Avionics → Plant(plant 收回 DynIn / 命令)
    virtual std::vector<contracts::DynInFrame> recv() = 0;

    // 反方向(对称:FCC 节点视角)
    virtual void send(const std::vector<contracts::DynInFrame>&  frames) = 0;
    virtual std::vector<contracts::DynOutFrame> recv_out() = 0;

    // 状态
    virtual bool is_connected() const = 0;
    virtual void close() = 0;
};

} // namespace runtime

> 签名设计:单一类同时支持两种方向;具体实现根据 PlantScope 选择性 use 一半。 > 例如 SIL 同进程时,InMemoryDynChannel 维护两个队列(DynIn / DynOut),plant 调用 send(DynOut),FCC 调用 recv() → DynOut。 > HIL UDP 时只用各自方向的 UDP socket。


3. InMemoryDynChannel(SIL / avionics_dry)

cpp
// runtime/InMemoryDynChannel.h
namespace runtime {

class InMemoryDynChannel : public IDynChannel {
public:
    void send(const std::vector<contracts::DynOutFrame>& fs) override {
        std::scoped_lock g(mu_);
        for (auto& f : fs) out_queue_.push_back(f);
    }

    std::vector<contracts::DynInFrame> recv() override {
        std::scoped_lock g(mu_);
        auto v = std::move(in_queue_);
        in_queue_.clear();
        return v;
    }

    void send(const std::vector<contracts::DynInFrame>& fs) override {
        std::scoped_lock g(mu_);
        for (auto& f : fs) in_queue_.push_back(f);
    }
    std::vector<contracts::DynOutFrame> recv_out() override {
        std::scoped_lock g(mu_);
        auto v = std::move(out_queue_);
        out_queue_.clear();
        return v;
    }

    bool is_connected() const override { return true; }
    void close() override {}

private:
    mutable std::mutex                         mu_;
    std::deque<contracts::DynOutFrame>         out_queue_;
    std::deque<contracts::DynInFrame>          in_queue_;
};

} // namespace runtime

性质

  • 零拷贝(move into queue),零序列化
  • 与 SIL 部署等价于"FCC ↔ Plant 完全可见对方内存"
  • avionics_dry 模式下 plant 不演化,recv_out 总返回空 → FCC 看到全零 DynOutFrame(或保留上一帧)

4. UdpDynChannel(HIL)

cpp
// runtime/UdpDynChannel.h
namespace runtime {

struct UdpConfig {
    std::string  remote_host;     // "192.168.1.100"
    uint16_t     local_port;      // 31000
    uint16_t     remote_port;     // 31001
    Time         timeout;         // 接收超时
};

class UdpDynChannel : public IDynChannel {
public:
    explicit UdpDynChannel(const UdpConfig& cfg);
    ~UdpDynChannel() override;   // 关闭 socket

    void send(const std::vector<contracts::DynOutFrame>& fs) override;
    std::vector<contracts::DynInFrame> recv() override;
    // ... 对称方向 ...

private:
    int sock_fd_;
    UdpConfig cfg_;
    // 协议:[u32 magic][u32 frame_count][frames...]
    std::vector<uint8_t> serialize_(const std::vector<contracts::DynOutFrame>&);
    std::vector<contracts::DynInFrame> deserialize_(const std::vector<uint8_t>&);
};

} // namespace runtime

性质

  • 单 UDP socket(可换成 TCP,签名不变)
  • 序列化用紧凑二进制(禁止 JSON / Protobuf,HIL 节点要求低延迟)
  • recv() 阻塞至超时;超时返回空 vector(Runner 视为"上一帧无更新")

> HIL 时序契约: > - phys 节点(plant)每 phys_dt(1ms)调用 dyn_channel_->send(out_frames) > - fcc 节点每 fcc_dt(20ms)调用 dyn_channel_->send(in_frames) > - 不同步包丢弃 vs 重传策略由 PlantScope 决定


5. Bus 信道对称

对称地,bus::IBus(详见 03_Avionics_and_Bus/Semantic_Bus_Pattern.md)也支持多种部署:

部署bus 实现
sil_monolithicInMemoryBus(同进程,零拷贝 std::variant 投递)
hil_fcc(FCC RTOS)Bus1553B / BusTTE(真实总线驱动)
hil_dyn(plant 本机,FCC 远端)BusBridge(UDP 桥接到远端 InMemoryBus

bus::IBus 的接口与 IDynChannel 互不重叠:

  • IDynChannel飞控仿真协议帧(DynIn/DynOut,物理真值+总输出指令)
  • IBus设备级语义消息(BusMessage = ImuPayload | GpsPayload | ScuPayload | IcuPayload)

> 不要混用:DynIn ≠ Bus payload。前者是 FCC↔仿真器协议,后者是 FCC↔设备协议。


6. 工厂分发

cpp
// runtime/ChannelFactory.cpp
namespace runtime {

std::unique_ptr<IDynChannel> make_dyn_channel(const PlantScope& scope) {
    using Kind = PlantScope::TransportKind;
    switch (scope.dyn_transport) {
        case Kind::InMemory:
            return std::make_unique<InMemoryDynChannel>();
        case Kind::UdpServer:
        case Kind::UdpClient:
            return std::make_unique<UdpDynChannel>(scope.udp_config);
    }
    throw std::runtime_error("unknown dyn_transport");
}

std::unique_ptr<bus::IBus> make_bus(const PlantScope& scope) {
    using Kind = PlantScope::BusKind;
    switch (scope.bus_kind) {
        case Kind::InMemory:    return std::make_unique<bus::InMemoryBus>();
        case Kind::Bridge1553B: return std::make_unique<bus::Bus1553Bridge>(scope.bus_bridge_config);
        case Kind::RealtimeBus: return std::make_unique<bus::BusRealtime>(scope.bus_rt_config);
    }
    throw std::runtime_error("unknown bus_kind");
}

} // namespace runtime

关键:工厂函数只在 Runner 构造时调用一次。整个仿真生命周期内 dyn_channel / bus 对象稳定。


7. Runner 的部署适配

Runner 不需要为不同部署写多个 main loop;它只依赖 scope 决定信道:

cpp
// runtime/Runner.cpp 的简化版(部署无关)
Runner::Runner(SimulationInstance inst) : instance_(std::move(inst)) {
    bus_         = make_bus(instance_.scope);
    dyn_channel_ = make_dyn_channel(instance_.scope);
    // ... rest is identical across all deployments
}

int Runner::run() {
    while (state.current_time < end_time) {
        auto [wout, new_state, wlog] = sim::world_tick(dt).run(env, std::move(state));
        state = std::move(new_state);
        log_writer_.write(wlog);

        // 部署透明:dyn_channel 是 InMemory / Udp 都不影响
        if (dyn_channel_) {
            dyn_channel_->send(make_dyn_out_frames(wout));
            // hil_fcc 模式:fcc 在远端,plant 把控制命令收回
            // sil_monolithic 模式:DynInFrame 通过 InMemoryDynChannel 即时回环
            auto cmds = dyn_channel_->recv();
            apply_dyn_in_frames(state, cmds);
        }
    }
}

胜利条件:换部署模式只需换 YAML,不改一行 main loop 代码


8. 序列化契约

HIL 模式下 DynInFrame / DynOutFrame 必须可序列化:

cpp
// contracts/DynFrames.h
namespace contracts {

struct DynOutFrame {
    BodyId         body_id;
    Time           timestamp;
    Vec3_T<frames::ECF>   pos_ecf;
    Vec3_T<frames::ECF>   vel_ecf;
    Quat                  att_q;
    Vec3_T<frames::BODY>  omega_b;
    Vec3_T<frames::BODY>  spec_force_b;
    double                total_mass;
    Vec3                  centroid_b;
};

struct DynInFrame {
    BodyId         body_id;
    Time           timestamp;
    // 控制命令(从 FCC 来)
    std::vector<EngineCommand>  engine_commands;
    std::vector<ServoCommand>   servo_commands;
    std::vector<FinCommand>     fin_commands;
    // 事件
    std::vector<DiscreteEvent>  events;
};

} // namespace contracts

字段约束

  • 全部 POD-ish(无 std::function、无指针、无 std::any)
  • 浮点数布局:所有 double 8 字节小端(HIL 节点统一约定)
  • 字符串(debug_id)禁止——用 enum class ID 或固定长度 char 数组

> 理由:HIL 节点可能是 RTOS(无 std::string heap),必须 POD 友好。


9. 反模式

反模式后果正确做法
world_tick 内部直接判断 if (scope.dyn_transport == Udp) { ... }部署逻辑泄漏进 simulation/部署是 runtime 的事;simulation 只看 IBus / IDynChannel 接口
DynInFrame 持有 std::function<void()> 回调HIL 不可序列化DynFrame 是纯数据;回调走 Bus / 信道
BusMessage 通过 IDynChannel 转发信道职责混淆BusMessage 走 IBus;DynFrame 走 IDynChannel
每个 tick 内 make_unique<UdpDynChannel>()端口反复绑定;崩溃构造一次,整生命周期复用
HIL 用 JSON / Protobuf 序列化 DynFrame延迟 + 内存开销不可控紧凑二进制(手写或 flatbuffers)
UDP socket 阻塞主 loop整个仿真挂起用 timeout + 非阻塞 recv
LogConfig 通过 IDynChannel 发出业务策略漂出仿真LogConfig 是本地的,每个节点自己持有

10. C-Distillation 路径

C++ 抽象C 蜕化
IDynChannel 虚类函数指针表:struct DynChannelOps { send_fn, recv_fn, ... }
std::unique_ptr<IDynChannel>DynChannelOps* 全局静态
InMemoryDynChannel静态环形缓冲区 + 内存屏障
UdpDynChannelLwIP / RTOS socket API
std::vector<DynOutFrame> 序列化固定大小 struct 数组 + memcpy
serialize_ / deserialize_编译期 layout struct(不需要 codegen)
make_dyn_channel(scope)编译期 #ifdef DEPLOYMENT_SIL_MONOLITHIC 选择

参见 Blueprint §7.12。


11. 测试策略

11.1 单元层

  • InMemoryDynChannel::send + recv 回环:发 N 帧,收 N 帧,bit-identical
  • UdpDynChannel 在 localhost 上回环:send/recv 数据完整性

11.2 组件层

  • ChannelFactory × 三种 scope:返回正确具体类型
  • 序列化往返:serialize(frame) → deserialize → frame',所有字段 bit-equal

11.3 集成层

  • 1s SIL 跑通(InMemoryDynChannel)
  • 1s HIL 模拟(两个进程 UdpDynChannel 互连,验证 tick 数对齐)

12. 引用

  • Blueprint §1.3(三种部署模式 + 信道本质)、§2.10(信道抽象与 Bus)
  • 07_Runtime/PlantScope.md(scope 决定信道选择)
  • 07_Runtime/Assembler_and_Runner.md(Runner 构造时调 make_dyn_channel/make_bus)
  • 03_Avionics_and_Bus/Semantic_Bus_Pattern.md(bus::IBus 对称设计)
  • 01_Foundation/Contracts_PerBodyFrames.md(DynInFrame / DynOutFrame 类型)