Skip to content

Extending Asuka

扩展模块(ExtensionModule)是运行时 dlopen 的共享库,用于消费前端状态或注入自己的 处理线程:后端优化、IMU 高频预测、可视化都是扩展。

记号与数据类型

位姿记号 T_world_imu 表示 IMU 系到世界系的变换(p_world = T_world_imu * p_imu)。 扩展模块收到的核心数据结构:

struct KeyFrame {
  long id;                                  // 前端关键帧序号(预测/优化帧为 -1)
  double stamp;                             // 产生该关键帧的传感器扫描起始时刻(契约见 callbacks.hpp)
  FrameId frame_id;                         // FrameId::IMU
  Eigen::Isometry3d T_world_imu;            // 位姿
  Eigen::Vector3d v_world_imu;              // 世界系速度
  Eigen::Matrix<double, 6, 1> imu_bias;     // [gyro_bias, acc_bias]
  PointCloudT::ConstPtr cloud_imu;          // 去畸变点云(IMU 系,逐帧新分配;发射后不可变)
};

on_odometry_imu / on_odometry_opt 发出的帧仅填充 stamp / T_world_imu / v_world_imu(id = -1、无点云),用于轨迹输出。

全局回调插槽

asuka::Callbacks(定义于 asuka_core 的 .cpp,保证跨 .so 单实例)提供六个核心插槽; 扩展可自持私有槽(见本节末说明)。 每个槽的发射者、线程、顺序与丢弃语义以 core/callbacks.hpp 的注释为准:

插槽 签名 发射者与时机
on_insert_imu (const ImuData::ConstPtr&) AsyncOdometryEstimation 在 worker 线程上、把样本喂给算法之前(每条送达事件恰好一次)
on_insert_frame (double, const PointCloudT::ConstPtr&) 同上,针对点云帧
on_insert_image (const ImageData::ConstPtr&) 同上,针对图像
on_new_frame (const KeyFrame::ConstPtr&) 前端算法在 worker 线程产出关键帧时
on_odometry_imu (const KeyFrame::ConstPtr&) imu_prediction 在自己的处理线程上按 IMU 频率发射
on_odometry_opt (const KeyFrame::ConstPtr&) 后端优化扩展在发布线程上按配置频率发射

扩展私有的点对点流不需要动 core:在自己的头文件里声明一个槽结构体,把静态槽对象 定义在扩展自己的 .so 的一个 .cpp 里(不可 inline 定义在头文件,否则每个 .so 各得 一份实例、回调静默失联),消费者直接链接该 .so——跨 .so 单实例靠 DT_NEEDED soname 去重,动态加载器会把重复加载折叠到首份映射。

四个"链路槽"(三个输入槽 + on_new_frame)共用前端 worker 线程,相对顺序即算法的 程序序;后两个是纯位姿帧(id = -1、无点云、无 bias),用于轨迹输出。 on_new_frame 上的帧 id >= 0。

CallbackSlot 采用 copy-on-write 快照:发射路径无锁无分配,add() 返回的整数 id 用于 remove();观察者异常被逐个捕获并记录,不会中断发射线程。回调在发射者线程同步执行 (通常是前端 worker),重活请入队到自己的线程——内置扩展均遵循"回调只入队"约定。

编写扩展模块

// my_extension.cpp
#include <asuka/core/callbacks.hpp>
#include <asuka/utility/extension_module.hpp>

namespace asuka {

class MyExtension : public ExtensionModule {
public:
  MyExtension() {
    logger = create_module_logger("myext");
    const Config config = load_config("config_extensions");
    param_ = config.param<double>("my_extension", "param", 1.0);

    on_new_frame_sub = Callbacks::on_new_frame.subscribe(
        [this](const KeyFrame::ConstPtr& frame) { on_new_frame(frame); });
    // 自己开线程的模块:在这里启动
  }

  ~MyExtension() override = default;       // RAII 订阅析构自动退订

  void stop() override {                   // 必须:join 自有线程,幂等
    // if (thread_.joinable()) thread_.join();
  }

private:
  void on_new_frame(const KeyFrame::ConstPtr& frame) {
    // 只做轻活 / 入队
  }

  CallbackSubscription<void(const KeyFrame::ConstPtr&)> on_new_frame_sub;
  double param_{1.0};
  std::shared_ptr<spdlog::logger> logger;
};

}  // namespace asuka

extern "C" asuka::ExtensionModule* create_extension_module() {
  return new asuka::MyExtension();
}

CMake:

add_library(asuka_my_extension SHARED my_extension.cpp)
target_link_libraries(asuka_my_extension asuka_core)
# 并加入 install(TARGETS ...)

在 config/config_extensions.json 中启用:

{
  "extensions": {
    "loading_extension_modules": [
      "libasuka_my_extension.so"
    ]
  },
  "my_extension": { "param": 1.0 }
}

生命周期与线程安全

框架析构顺序为:停止前端 worker -> 逐个调用扩展的 stop() -> at_exit() -> 析构(RAII 订阅析构退订)。扩展必须在自己的 stop() 中 join 所有会发射回调或 访问共享状态的线程(默认实现为空,注意覆写);stop() 之后不得再有槽发射。 带处理线程的模块请把订阅成员声明在线程成员之前(成员逆序析构 = 先 join 线程后退订,保证退订时无在途发射)。

不用动态加载时

也可以像 apps/ 一样直接在入口代码中持有模块实例,绕过 dlopen; 但推荐保持扩展形态以维持"核心零改动"。