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;
但推荐保持扩展形态以维持"核心零改动"。