历史快照
本专题页写于包拆分与 FAST-LIVO2 / CustomMsg / 图像输入落地之前,反映当时的单包结构与
五前端状态。已知过时点:目录布局(apps/launch 现属 asuka_ros 包)、共享的
config_sensor.json(外参已改为各前端 odometry 配置的 sensor 块)、ROS 层 acc_scale
等描述、前端数量(现六种,新增 fastlivo)、CustomMsg 与图像输入链路。2026-09 下旬以来:lightning / superodom 前端与 optimization_miao 后端已移除
(现五前端:fastlio / batchlio / superlio / fastlivo / srlivo;smallpointlio 已于 2026-10-23 移除,odometry/10-11 为其历史快照);
freedom 扩展已整体迁出 asuka(移入 nagisa 包);KeyFrame::stamp 统一为扫描
起始时刻;执行器新增连续失败熔断与 wait_for() 排空检测、扩展与执行器有
ok() 健康查询。
现行事实以主文档 (index / architecture / parameters / odometry 等)与源码为准。
title: "AsukaROS 构造装配与模块装载" description: >- 构造函数定位 config 目录并初始化 GlobalConfig,读取 config_odometry 的 so_name,通过 OdometryEstimation::load_module 装载算法插件并取出共享 cloud_preprocess,创建 AsyncOdometryEstimation 异步执行器,再读取 lidar_type、acc_scale、时间偏移等 ROS 参数并加载扩展模块。
AsukaROS 构造装配与模块装载
AsukaROS 的构造函数(asuka_ros.cpp L19-L43)是整个进程的装配中枢:它把「配置系统 → 里程计插件 → 异步执行器 → ROS 边界参数 → 扩展模块」五部分按固定顺序组装成一条可运行的数据链路。本页只讲构造阶段的装配逻辑与模块装载机制;数据流入后的处理见后续页面。
装配顺序总览
flowchart TD
A["AsukaROS::AsukaROS(nh)"] --> B["ros::package::getPath('asuka') + '/config'"]
B --> C["GlobalConfig::instance(config_dir, true)"]
C --> L["create_module_logger('ros')"]
L --> D["Config = get_config_path('config_odometry')"]
D --> E["so_name = param('odometry', 'so_name', '')"]
E --> F["OdometryEstimation::load_module(so_name)"]
F --> G{"odometry 非空?"}
G -- "否" --> X["throw runtime_error(构造失败)"]
G -- "是" --> H["cloud_preprocess = odometry->cloud_preprocess()"]
H --> I["async = new AsyncOdometryEstimation(odometry)"]
I --> J["Config = get_config_path('config_ros')"]
J --> K["lidar_type / acc_scale / imu_time_offset / time_offset_lidar_to_imu / map_saving_path"]
K --> M["load_extensions()"]
M --> N["Config = get_config_path('config_extensions')"]
N --> O["遍历 loading_extension_modules"]
O --> P["ExtensionModule::load_module(so_name)"]
P --> Q{"ext 非空?"}
Q -- "否" --> R["静默跳过"]
Q -- "是" --> S["push_back 进 extension_modules"]
关键节点解释:
- GlobalConfig 初始化先于一切:config 目录用
ros::package::getPath("asuka") + "/config"定位,随后GlobalConfig::instance(config_dir, true)以单例方式建立(第二个布尔参数传true,其确切语义见「配置系统」页)。此后所有get_config_path调用都依赖这一步。 - logger 紧随其后:
create_module_logger("ros")建立本类的日志通道,保证后续步骤的运行期日志有归属。 - 里程计装载是唯一硬失败点:
odometry为空即抛出runtime_error("failed to load odometry module: " + so_name),构造直接失败。 cloud_preprocess的外提是关键装配动作:点云预处理实例由算法插件内部创建,再通过基类虚函数cloud_preprocess()交给 ROS 层持有。后续insert_frame用它把 Livox/Robosense 原始点云转成算法私有的PointCloudT——这是 ROS 层与算法私有预处理解耦的接合点。- 异步执行器必须在 odometry 之后创建:
AsyncOdometryEstimation构造时直接持有 odometry,从此 worker 线程开始按队列消费数据。 - 扩展模块最后装载:核心链路(里程计 + 执行器)已可用后才加载扩展,扩展失败不影响主链路。
dlopen 装载链路与句柄注册表
OdometryEstimation::load_module 是一个薄工厂:转发到模板 load_module_from_so<OdometryEstimation>(so_name, "create_odometry_estimation"),真正的装载工作在 utility/load_module.cpp 完成。
sequenceDiagram
participant C as AsukaROS 构造函数
participant F as OdometryEstimation::load_module
participant T as load_module_from_so
participant S as load_symbol
participant R as load_so / 句柄注册表
participant P as 插件 so
C->>F: load_module(so_name)
F->>T: load_module_from_so(so_name, "create_odometry_estimation")
T->>S: load_symbol(so_name, func_name)
S->>R: load_so(so_name)
alt 注册表已缓存该 so
R-->>S: 返回缓存句柄
else 首次加载
R->>P: dlopen(so_name, RTLD_LAZY)
R->>R: open_handles[so_name] = handle
end
S->>P: dlsym(handle, "create_odometry_estimation")
P-->>S: 工厂函数指针
T->>P: 调用工厂函数生成实例
T-->>C: shared_ptr<OdometryEstimation>
关键节点解释:
- 进程级句柄注册表:
open_handles是以 so 名为键的std::map,由handle_mutex保护。同一 so 重复装载(如多次构造、或里程计与扩展模块引用同一库)会命中缓存直接返回,不会重复dlopen。 RTLD_LAZY:延迟符号绑定,库打开更快;装载失败时用全局spdlog::warn输出 so 名与dlerror()内容并返回空指针。- 句柄永不关闭:源码注释明确说明插件代码必须常驻进程生命周期,注册表是未来 unload/reload 实现的「接缝」。
load_module.hpp头注释甚至写好了尚未实现的三步卸载顺序:先停所有可能发射回调的线程,再销毁模块让其退订回调槽,最后从注册表取句柄dlclose并删除条目。 - extern "C" 工厂契约:
odometry_estimation.hpp中直接声明extern "C" OdometryEstimation* create_odometry_estimation(),五种算法实现各自在create.cpp导出该符号。模板中将dlsym返回的void*强转为Module* (*)()并调用,结果包进shared_ptr接管所有权。
关键状态:构造后可用的成员清单
| 成员 | 默认值 | 装配来源 | 后续用途 |
|---|---|---|---|
odometry |
nullptr | load_module(so_name) |
算法实例,save() 时调 save_map() |
async |
nullptr | make_unique(odometry) |
worker 线程与环形缓冲队列所有者 |
cloud_preprocess |
nullptr | odometry->cloud_preprocess() |
insert_frame 的预处理(允许为空) |
extension_modules |
空 vector | load_extensions() |
析构时统一 stop() + at_exit |
lidar_type |
LIVOX |
parse_lidar_type(config_ros.lidar_type) |
点云分派分支 |
acc_scale / imu_time_offset |
1.0 / 0.0 | config_ros | insert_imu 中的标定叠加 |
time_offset_lidar_to_imu |
0.0 | config_ros | insert_frame 的时间对齐 |
map_saving_path |
"" | config_ros | save() 与析构 at_exit 的目标 |
last_timestamp_lidar/imu |
-1.0 | 类内默认初始化 | 回环检测基线(首帧必然通过) |
logger |
nullptr → "ros" | create_module_logger |
全部运行期日志 |
注意所有 config 参数读取都带默认值回落(param 的第三参),缺配置不会让构造失败——真正的硬失败点只有里程计装载一处。
边界条件
so_name缺省为空串:load_module找不到符号返回空指针,构造抛异常,进程无法启动——这是「配置了不存在的算法库」时的显式失败路径。- 严格性不对称:里程计装载失败是致命的(throw);扩展模块装载失败只是
if (ext)守卫下静默跳过,主链路照常运行。 cloud_preprocess允许为空:基类默认实现返回 nullptr,insert_frame中以三目运算守卫(cloud_preprocess ? ... : nullptr);预处理返回空同样直接丢弃该帧、不入异步队列。- 注册表的检查与 dlopen 不在同一临界区:先查缓存、解锁后
dlopen、再加锁插入。构造阶段是单线程执行,此窗口无实际影响;若未来在运行期并发装载模块则需留意。 - 卸载未实现:句柄永不关闭 +
RTLD_LAZY意味着插件代码一旦载入就常驻进程,unload/reload 只是注释中的设计预留。
扩展点
- 切换算法:只改
config_odometry的so_name即可在五种实现间切换,构造代码零改动。 - 新增扩展模块:在
config_extensions的loading_extension_modules列表追加 so 名,构造循环自动装载,与OdometryEstimation共用同一套 dlopen 工厂模式。 - unload/reload:句柄注册表 + 头注释中的三步卸载顺序是预留接缝。
- 新增 ROS 边界参数:构造函数的 config_ros 读取段(L34-L40)是传感器标定类修正的唯一收敛点。
生命周期对照
构造顺序(odometry → async → extensions)在析构中被精确逆序回收:先 async->stop() join worker,再逐个 ext->stop(),最后逐个 ext->at_exit(map_saving_path);cpp 注释解释了这是为了确保没有任何在途的回调槽发射后再销毁模块(详见「地图保存与退出时序」页)。
Sources:
- src/asuka/src/asuka/asuka_ros.cpp#L19-L43
- src/asuka/src/asuka/asuka_ros.cpp#L157-L169
- src/asuka/include/asuka/asuka_ros.hpp#L44-L60
- src/asuka/include/asuka/core/odometry_estimation.hpp#L11-L37
- src/asuka/src/asuka/core/odometry_estimation.cpp#L6-L8
- src/asuka/include/asuka/utility/load_module.hpp#L8-L28
- src/asuka/src/asuka/utility/load_module.cpp#L13-L58
关联源码
src/asuka/src/asuka/asuka_ros.cppsrc/asuka/include/asuka/asuka_ros.hpp