1 ag_driver 源代码解析
1 基本概念
1.1 MEMS雷达
ag_driver 目前只支持一种雷达:
MEMS 雷达。如 A0。MEMS 雷达是单轴、谐振式的MEMS扫描镜,其水平扫描角度可达 120°。
1.2 通道 Channel
MEMS 雷达的每个通道对应一块区域,比如一个矩形区域。
1.3 MSOP/DIFOP
Asensing 雷达与电脑主机的通信协议有三种。
MSOP(Main data Stream Ouput Protocol)。激光雷达将扫描出来的距离、角度、反射率等信息封装成 MSOP Packet,输出给电脑主机。
DIFOP(Device Information Output Protocol)。激光雷达将自身的配置信息,以及当前的状态封装成 DIFOP Packet,输出给电脑主机。
UCWP(User Configuration Write Protocol)。用户可以修改激光雷达的某些配置参数。
ag_driver 处理前两类协议的包,也就是 MSOP Packet 和 DIFOP Packet。
一般来说,激光雷达与电脑主机通过以太网连接,使用 UDP 协议。MSOP/DIFOP 的格式,不同的雷达可能有较大差异。
1.4 点云帧
对于 MEMS 雷达,点云在 MSOP Packet 序列中的开始和结束位置,由雷达自己确定。
一帧点云包含固定数目(比如N)的 MSOP Packet。雷达对 MSOP Packet 从 1 到 N 编号,并一直循环。
2 ag_driver的组件
ag_driver 主要由三部分组成: Input、Decoder、LidarDriverImpl。

Input 部分负责从 Socket 或 PCAP 文件等数据源,获取 MSOP/DIFOP Packet。Input 的类一般有自己的接收线程
recv_thread_。Decoder 部分负责解析 MSOP/DIFOP Packet,得到点云。Decoder 部分没有自己的线程,它运行在 LidarDriverImpl 的 Packet 处理线程
handle_thread_中。LidarDrvierImpl 部分将 Input 和 Decoder 组合到一起。它从 Input 得到 Packet,根据 Packet 的类型将它派发到 Decoder。得到点云后,通过用户的回调函数传递给用户。
LidarDriverImpl 提供 Packet 队列。Input 收到 MSOP/DIFOP Packet 后,调用 LidarDriverImpl 的回调函数。回调函数将它保存到 Packet 队列。
LidarDriverImpl 提供 Packet 处理线程
handle_thread_。在这个线程中,将 MSOP Packet 和 DIFOP Packet 分别派发给 Decoder 相应的处理函数。Decoder 解析完一帧点云时,通知 LidarDriverImpl。后者再将点云传递给用户。
3 Packet接收
Input 部分负责接收 MSOP/DIFOP Packet,包括:
Input,
Input 的派生类,如 InputSock、InputPcap、InputRaw
Input 的工厂类 InputFactory

3.1 Input
Input 定义接收 MSOP/DIFOP Packet 的接口。
成员
input_param_是用户配置参数 AGInputParam,其中包括从哪个 port 接收 Packet 等信息。Input 自己不分配接收 Packet 的缓存。
Input 的使用者调用 Input::regCallback(),提供两个回调函数 cb_get_pkt 和 cb_put_pkt, 它们分别保存在成员变量
cb_get_pkt_和cb_put_pkt_中。Input 的派生类调用
cb_get_pkt_可以得到空闲的缓存;在缓存中填充好 Packet 后,可以调用cb_put_pkt_将它返回。
Input 有自己的线程
recv_thread_。Input 的派生类启动这个线程读取 Packet。

3.2 InputSock
InputSock 类从 UDP Socket 接收 MSOP/DIFOP Packet。雷达将 MSOP/DIFOP Packet 发送到这个 Socket。

一般情况下,雷达将 MSOP/DIFOP Packet 发送到不同的目的 Port,所以 InputSock 创建两个 Socket 来分别接收它们。
成员变量
fds_[2]保存这两个 Socket 的描述符。fds_[0]是 MSOP socket,fds_[1]是 DIFOP socket。但也可以配置雷达将 MSOP/DIFOP Packet 发到同一个 Port,这时一个 Socket 就够了,fds_[1]就是为无效值-1。MSOP/DIFOP 对应的 Port 值可以在 AGInputParam 中设置,分别对应于
AGInputParam::msop_port和AGInputParam::difop_port。
一般情况下,MSOP/DIFOP Packet 直接构建在 UDP 协议上。但在某些客户的场景下(如车联网),MSOP/DIFOP Packet 可能构建在客户的协议上,客户协议再构建在 UDP 协议上。这时,InputSock 派发 MSOP/DIFOP Packet 之前,会先丢弃
USER_LAYER的部分。成员变量sock_offset_保存了USER_LAYER部分的字节数。USER_LAYER部分的字节数可以在 AGInputParam 中设置,对应于AGInputParam::user_layer_bytes。
有的场景下,客户的协议会在 MSOP/DIFOP Packet 尾部附加额外的字节。这时,InputSock 派发 MSOP/DIFOP Packet 之前,会先丢弃
TAIL_LAYER的部分。成员变量sock_tail_保存了TAIL_LAYER部分的字节数。TAIL_LAYER部分的字节数可以在 AGInputParam 中设置,对应于AGInputParam::tail_layer_bytes。

3.2.1 InputSock::createSocket()
createSocket() 用于创建 UDP Socket。
调用 setsockopt(), 设置选项
SO_REUSEADDR;调用 bind() 将 socket 绑定到指定的 (IP, PORT) 组上;
如果雷达是组播模式,则将指定 IP 加入该组播组;
调用 fcntl() 设置
O_NONBLOCK选项,以异步模式接收 MSOP/DIFOP Packet。
该 Socket 的配置参数可以在 AGInputParam 中设置。根据设置的不同,createSocket() 支持如下几种模式。
| msop_port/difop_port | host_address | group_address | |
|---|---|---|---|
| 51180/7788 | 0.0.0.0 | 0.0.0.0 | 雷达的目的地址可以为广播地址、或电脑主机地址 |
| 51180/7788 | 192.168.101.101 | 0.0.0.0 | 雷达的目的地址可以为电脑主机地址 |
| 51180/7788 | 192.168.101.101 | 224.1.1.1 | 雷达的目的地址可以为组播地址、或电脑主机地址 |
3.2.2 InputSock::init()
init() 调用 createSocket(),创建两个 Socket,分别接收 MSOP Packet 和 DIFOP Packet。
3.2.3 InputSock::start()
start() 开始接收 MSOP/DIFOP Packet。
启动接收线程,线程函数为 InputSock::recvPacket()。
3.2.4 InputSock::recvPacket()
recvPacket() 接收 MSOP/DIFOP Packet。
在 while() 循环中,调用 FD_ZERO() 初始化本地变量 rfds,调用 FD_SET() 将 fds_[2] 中的两个 fd 加入 rfds。当然,如果 MSOP/DIFOP Packet 共用一个 socket, 无效的 fds_[1] 就不必加入了。
调用 select() 在 rfds 上等待 Packet, 超时值设置为 1 秒。如果 select() 的返回值提示 rfds 上有信号,调用 FD_ISSET() 检查是 fds_[] 中的哪一个 fd 可读。对这个 fd,调用回调函数 cb_get_pkt_, 得到大小为 MAX_PKT_LEN 的缓存,MAX_PKT_LEN = 1500,对当前 Asensing 雷达来说,够大了;调用 recvfrom() 接收 Packet,保存到这个缓存中;调用回调函数 cb_put_pkt_,将 Packet 派发给 InputSock 的使用者。
注意在派发之前,调用 Buffer::setData() 设置了 MSOP Packet 在 Buffer 的中偏移量及长度,以便剥除 USER_LAYER 和 TAIL_LAYER(如果有的话)。
3.3 InputPcap
InputPcap 解析 PCAP 文件得到 MSOP/DIFOP Packet。使用第三方工具,如 WireShark,可以将雷达数据保存到 PCAP 文件中。

InputPcap 基于第三方的 libpcap 库,使用它可以遍历 PCAP 文件,依次得到所有 UDP Packet。
成员变量
pcap_变量保存 Pcap 文件指针,pcap_t定义来自 libpcap 库。
与 InputSock 一样,在有的客户场景下,InputPcap 也需要处理
USER_LAYER和TAIL_LAYER的情况。InputPcap 的成员pcap_offset_和pcap_tail_分别保存USER_LAYER和TAIL_LAYER的字节数。但也有不同的地方。InputSock 从 Socket 接收的 Packet 只有 UDP 数据部分,而 InputPcap 从 PCAP 文件得到的 Packet 不同,它包括所有 Packet 的所有层。
pcap_offset_除了USER_LAYER的长度之外,还要加上其他所有层。对于一般的以太网包,
pcap_offset_需要加上其他层的长度,也就是 14 (ETHERNET) + 20 (IP) + 8 (UDP) = 42 字节。如果还有 VLAN 层,
pcap_offset_还需要加上 4 字节。

PCAP 文件中可能不止包括 MSOP/DIFOP Packet,所以需要使用 libpcap 库的过滤功能。libpcap 过滤器
bpf_program,由库函数 pcap_compile() 生成。成员msop_filter_和difop_filter_分别是 MSOP Packet 和 DIFOP Packet 的过滤器。MSOP/DIFOP Packet 都是 UDP Packet,所以给 pcap_compile() 指定选项
udp。如果是基于 VLAN 的,则需要指定选项
vlan。如果在一个 PCAP 文件中包含多个雷达的 Packet,则还需要指定选项
udp dst port,以便只提取其中一个雷达的 Packet。
用户配置参数 AGInputParam 中指定选项 udp dst port。有如下几种情况。
| msop_port | difop_port | 说明 |
|---|---|---|
| 0 | 0 | 如果 PCAP 文件中只包含一个雷达的 Packet |
| 51180 | 7788 | 如果 PCAP 文件中包含多个雷达的 Packet,则可以只提取指定雷达的Packet(该雷达 MSOP/DIFOP 端口不同) |
| 51180 | 51180/0 | 如果 PCAP 文件中包含多个雷达的 Packet,则可以只提取指定雷达的 Packet(该雷达 DIFOP/DIFOP 端口相同) |
3.3.1 InputPcap::init()
init() 打开 PCAP 文件,构造 PCAP 过滤器。
调用 pcap_open_offline() 打开 PCAP 文件,保存在成员变量
pcap_中。调用 pcap_compile() 构造 MSOP/DIFOP Packet 的 PCAP 过滤器。
如果它们使用不同端口,则需要两个过滤器,分别保存在
mosp_filter_和difop_filter_中。如果使用同一端口,那么
difop_filter_就不需要了。
3.3.2 InputPcap::start()
start() 开始解析 PCAP 文件。
调用 std::thread(),创建并启动 PCAP 解析线程,线程的函数为 recvPacket()。
3.3.3 InputPcap::recvPacket()
recvPacket() 解析 PCAP 文件。
在循环中,
调用 pcap_next_ex() 得到文件中的下一个 Packet。
如果 pcap_next_ex() 还能读出 Packet,
本地变量
header指向 Packet 的头信息,变量pkt_data指向 Packet 的数据。调用 pcap_offline_filter(),使用 PCAP 过滤器校验 Packet(检查端口、协议等是否匹配)。
如果是 MSOP Packet,
调用
cb_get_pkt_得到大小为MAX_PKT_LEN的缓存。MAX_PKT_LEN = 1500,对当前的 Asensing 雷达来说,够大了。调用 memcpy()将 Packet 数据复制到缓存中,并调用 Buffer::setData() 设置 Packet 的长度。复制时剥除了不需要的层,包括
USER_LAYER和TAIL_LAYER(如果有的话)。调用回调函数
cb_put_pkt_,将 Packet 派发给 InputSock 的使用者。
如果是 DIFOP Packet,处理与 MSOP Packet 一样。
调用 this_thread::sleep_for() 让解析线程睡眠一小会。这是为了模拟雷达发送 MSOP Packet 的间隔。这个间隔时间来自每个雷达的
Decoder类,每个雷达有自己的值。在 Decoder 部分,会说明如何计算这个值。
如果 pcap_next_ex() 不能读出 Packet,一般意味着到了文件结尾,则:
调用 pcap_close() 关闭 pcap 文件指针
pcap_。
用户配置 AGInputParam 的设置决定是否重新进行下一轮的解析。这个选项是 AGInputParam::pcap_repeat。
如果这个选项为真,调用 pcap_open_offline() 重新打开 PCAP 文件。这时成员变量
pcap_回到文件的开始位置。下一次调用 pcap_next_ex(),又可以重新得到 PCAP 文件的第一个 Packet 了。
3.4 InputRaw
InputRaw 是为了重播 MSOP/DIFOP Packet 而设计的 Input 类型。将在后面的 Packet Record/Replay 章节中说明。
3.5 InputFactory
InputFactory 是创建 Input 实例的工厂。

Input 类型如下。
enum InputType
{
ONLINE_LIDAR = 1, // InputSock
PCAP_FILE, // InputPcap
RAW_PACKET // InputRaw
};
3.5.1 InputFactory::creatInput()
createInput() 根据指定的类型,创建 Input 实例。
创建 InputPcap 时,需指定
sec_to_delay。这是 InputPcap 回放 MSOP Packet 的间隔。创建 InputRaw 时,需指定
cb_feed_pkt。这个将在后面的 Packet Record/Replay 章节中说明。
4 Packet解析
4.1 MSOP格式
这里说明 MSOP 格式中这些字段。
距离
distance角度
azimuth,elevation发射率
intensity通道
ring时间戳
timestamp温度
temperature回波模式
echo_mode
其中前五个与点云直接相关。
MSOP 格式中的点是极坐标系的坐标,包括极径和极角。距离就是这里的极径。从距离和角度可计算直角坐标系的坐标,也就是点云使用的坐标。
4.1.1 Distance
Distance 用两个字节表示。它乘以一个解析度得到真实距离。
不同的雷达的解析度可能不同。
特定于雷达的配置参数
AGDecoderConstParam::DISTANCE_RES保存这个解析度。
uint16_t distance;
4.1.2 角度
对于 MEMS 雷达, 角度是
azimuth和elevation。uint16_t azimuth; uint16_t elevation;
从
distance、azimuth和elevation,可计算直角坐标系的坐标。雷达的角度分辨率是 0.01 度。这意味着一圈 360 度,可以划分成 36000 个单位。
MSOP 格式中,角度以 0.01 度为单位,范围是 (0, 36000),所以可以用
uint16_t来表示。
4.1.3 intensity
intensity 保存在 1 个字节中。
uint8_t intensity;
4.1.4 ring
ring 在后面的 ChanAngles 章节说明。
4.1.5 timestamp
Asensing 雷达使用了一种时间戳格式。
4.1.5.2 UTC 格式
UTC 格式定义如下:
成员
sec[6]保存的是秒数;成员
ss[4]保存微秒值或纳秒值。
typedef struct
{
uint8_t sec[6]; //year
//month
//day
//hour
//minute
//second
uint8_t ss[4]; //identify the timestamp for point cloud
} AGTimestampUTC;
如果
ss[4]保存微秒值,使用parseTimeUTCWithUs()解析。(A0 使用parseTimeWithA0())。
当前版本的 ag_driver 只支持微秒格式的解析。
4.1.6 temperature
Asensing 雷达使用了几种温度格式。
4.1.6.1 用解析度表示温度
一种是用 2 字节表示温度值,这个值乘以一个解析度得到真实温度。
特定于雷达的配置参数
AGDecoderConstParam::TEMPERATURE_RES保存这个解析度。
4.1.6.2 相对温度
另一类用 1 个字节表示温度。这个值加上一个初始值得到真实温度。遵循这种格式的有 A0。
特定于雷达的配置参数
AGDecoderConstParam::TEMPERATURE_RES保存这个初始值。
int8_t temperature;
4.1.7 echo_mode
雷达内部有多种回波模式:
最强回波,
最后回波,
双回波,
MSOP 格式中用一个字节表示:
int8_t return_mode;
但 ag_driver 并不在意是回波是“最强的”,还是“最后的”。因为影响 MSOP 解析的只是:有几个回波?
如下是才是 ag_driver 关心的回波模式。
enum AGEchoMode
{
ECHO_SINGLE = 0,
ECHO_DUAL
};
不同雷达有不同的回波模式 return_mode。每个 Decoder 实现自己的解析函数 getEchoMode(),得到 ag_driver 的回波模式。
回波模式会影响 MSOP Packet 中数据的布局,还可能影响点云的分帧。
4.2 ChanAngles
4.2.1 垂直角/水平角的修正
如前面 MSOP 格式的章节所说,理论上,从 distance、垂直角、水平角就可以计算点的直角坐标系的坐标。
但在生产实践中,装配雷达总是无可避免地有细微的误差,导致雷达的角度不精确,需要进行修正。雷达装配后的参数标定过程,会找出相关的修正值,写入雷达的寄存器。标定后,使用修正值调整点,就可以使其精度达到要求。
MEMS 雷达的角度修正,在雷达内部完成,所以 ag_driver 不需要做什么。
4.3 Trigon
4.3.1 查表计算三角函数值
如前面所说,MSOP Packet 中的点是极坐标系的坐标。ag_driver 将点坐标,从极坐标系转换为用户使用的直角坐标系。这时需要计算角度的 sin/cos 值。
调用三角函数又耗时又耗 CPU 资源,优化的方式是查表。
首先确定表的范围。
垂直角的范围在 (-90, 90) 内。加上修正值,也还是在 (-90, 90) 内。
水平角的范围在 (0, 360) 内。加上修正值,在 (-90, 450) 内。
MSOP 格式中,角度以 0.01 度为单位。ag_driver 也是这样。对于 (-90, 450) 的角度范围,需要对 (-9000, 45000) 内的整数角度值,计算 sin/cos 值。
4.3.2 Trigon定义
Trigon 用于计算指定范围内的 sin/cos 值,并用于查询。

成员变量
ANGLE_MIN和ANGLE_MAX保存角度范围。这里ANGLE_MIN = -9000,ANGLE_MAX = 45000。成员变量
o_sins_保存所有角度的sin值,o_coss_保存所有角度的 cos 值。o_sins_[]和o_coss_[]是两个大小为AMGLE_MAX - ANGLE_MIN的数组。引用
os_sins_[]和o_coss_[]计算三角函数值时,需要减去一个偏移。为了免去这个麻烦,重新定义了两个指针sins_和coss_,让它们分别指向os_sins_[9000]和os_cons_[9000]。这样就可以用角度值直接引用sins_和coss_了。

4.3.3 Trigon::Trigon()
Trigon 的构造函数 Trigon() 负责初始化 o_sins_[] 和 o_coss_[]。
根据角度范围,给
o_sins[]和o_coss_[]分配相应大小的空间,遍历范围内的角度值,调用 std::sin() 和 std::cos(),将三角函数值分别保存到
o_sins_[]和o_coss_[]中。让
sins_指向sins_[]中 0 度角的位置,这里是sins_[9000]。类似地设置coss_。
4.3.4 Trigon::sin()
sin() 查表返回角度的 sin 值。
4.3.5 Trigon::cos()
cos() 查表返回角度的 cos 值。
4.4 分帧
4.4.1 MEMS 雷达的分帧模式
MEMS 雷达的分帧是在雷达内部确定的。
一帧的 MSOP Packet 数是固定的。假设这个数为
N, 则雷达给 Packet 编号,从 1 开始,依次编号到 N。对于 A0,单回波模式下,Packet 数是 1344;在双回波模式下,输出的点数要翻倍,相应的,Packet 数也要翻倍,Packet 数是 2688。
4.4.2 SplitStrategyBySeq
SplitStrategyBySeq 按 Packet 编号分帧。
成员变量
prev_seq_是前一个 Packet 的编号。成员变量
safe_seq_min_和safe_seq_max,是基于prev_seq_的一个安全区间。

4.4.2.1 SplitStrategyBySeq::newPacket()
使用者用 MSOP Packet 的编号值,调用 newPacket()。如果分帧,返回 true。
MSOP 使用 UDP 协议,理论上 Packet 可能丢包、乱序。
先讨论没有安全区间时,如何处理丢包、乱序。
理想情况下,如果不丢包不乱序,Packet 编号从 1 到 1344, 只需要检查 Packet 编号是不是 1。如果是就分帧。
那假如只有丢包呢?举个例子,如果编号为 1 的 Packet 丢了,则可以加入检查条件,就是当前 Packet 编号小于
prev_seq_,就分帧。在乱序的情况下,这个检查条件会导致另一个困境。举个例子,如果编号为 300 和 301 的两个 Packet 乱序,那么这个位置分帧,会导致原本的一帧拆分成两帧。
为了在一定程度上包容可能的 Packet 丢包、乱序情况,引入安全区间的概念。
以
prev_seq_为参考点,划定一个范围值RANGE
safe_seq_min_ = prev_seq_ - RANGE
safe_seq_max_ = prev_seq_ + RANGE

如果 Packet 在范围 (
safe_seq_min_,safe_seq_max_) 内,都不算异常,丢包、乱序都不触发分帧。这样在大多数情况下,之前的问题可以避免。
4.5 点云的有效性校验
4.5.1 DistanceSection
DistanceSection 检查指定的 distance 是否在有效范围内。
成员
min_和max_分别是这个范围的最小值和最大值。不同雷达有不同的测距范围。雷达配置参数
AGDecoderConstParam::DISTANCE_MIN,和AGDecoderConstParam::DISTANCE_MAX指定这个范围。用户也可以通过用户配置参数
AGDecoderParam::min_distance, 和AGDecoderParam::max_distance进一步限缩这个范围。

4.5.1.1 DistanceSection::in()
in() 检查指定的 distance 是否在有效范围内。
4.6 Decoder
Decoder 解析雷达 MSOP/DIFOP Packet,得到点云。
它是针对所有雷达的接口类。
MEMS 雷达的类,如 DecoderA0,直接派生自 Decoder。
DecoderFactory 根据指定的雷达类型,生成 Decoder 实例。

4.6.1 Decoder定义
如下图是 Decoder 的详细定义。
成员
const_param_是雷达的参数配置。成员
param_是用户的参数配置。成员
trigon_是Trigon类的实例,提供快速的 sin/cos 计算。定义如下的宏,可以清晰、方便调用它。
#define SIN(angle) this->trigon_.sin(angle)
#define COS(angle) this->trigon_.cos(angle)
成员
packet_duration_是 MSOP Packet 理论上的持续时间,也就是相邻两个 Packet 之间的时间差。Decoder 的派生类计算这个值。InputPcap 回放 PCAP 文件时,需要这个值在播放 Packet 之间设置延时。
成员
echo_mode_是回波模式。Decoder 的派生类解析 DIFOP Packet 时,得到这个值。成员
temperature_是雷达温度。Decoder 的派生类解析 MSOP Packet 时,应该保存这个值。成员
angles_ready_是当前的配置信息是否已经就绪。不管这些信息是来自于 DIFOP Packet,还是来自外部文件。成员
point_cloud_是当前累积的点云。成员
prev_pkt_ts_是最后一个 MSOP Packet 的时间戳,成员prev_point_ts_则是最后一个点的时间戳。成员
cb_split_frame_是点云分帧时,要调用的回调函数。由使用者通过成员函数 setSplitCallback() 设置。

4.6.1.1 AGDecoderConstParam
AGDecoderConstParam 是雷达配置参数,这些参数都是特定于雷达的常量。
MSOP_LEN是 MSOP Packet 大小。DIFOP_LEN是 DIFOP Packet 大小。MSOP_ID[]是 MSOP Packet 的标志字节。各雷达的标志字节不同,用MSOP_ID_LEN指定其长度。DIFOP_ID[]是 DIFOP Packet 的标志字节。各雷达的标志字节不同,用DIFOP_ID_LEN指定其长度。LASER_NUM是雷达的通道数。如 A0 是 10。BLOCKS_PER_PKT、CHANNELS_PER_BLOCK分别指定每个 MSOP Packet 中有几个 Block,和每个 Block 中有几个 Channel。DISTANCE_MIN、DISTANCE_MAX指定雷达测距范围DISTANCE_RES指定 MSOP 格式中distance的解析度。TEMPERATURE_RES指定 MSOP 格式中,雷达温度值的解析度。
struct AGDecoderConstParam
{
// packet len
uint16_t MSOP_LEN;
uint16_t DIFOP_LEN;
// packet identity
uint8_t MSOP_ID_LEN;
uint8_t DIFOP_ID_LEN;
uint8_t MSOP_ID[8];
uint8_t DIFOP_ID[8];
// packet structure
uint16_t LASER_NUM;
uint16_t BLOCKS_PER_PKT;
uint16_t CHANNELS_PER_BLOCK;
// distance & temperature
float DISTANCE_MIN;
float DISTANCE_MAX;
float DISTANCE_RES;
float TEMPERATURE_RES;
};
4.6.1.2 Decoder::processDifopPkt()
processDifopPkt() 处理 DIFOP Packet。
校验 Packet 的长度是否匹配。
校验 Packet 的标志字节是否匹配。
如果校验无误,调用 decodeDifopPkt()。这是一个纯虚拟函数,由各雷达的派生类提供自己的实现。
4.6.1.3 Decoder::processMsopPkt()
processMsopPkt() 处理 MSOP Packet。
检查当前配置信息是否已经就绪(
angles_ready_)。通过用户配置参数
AGDecoderParam::wait_for_difop,可以设置是否等待配置信息就绪。
校验 DIFOP Packet 的长度是否匹配。
校验 DIFOP Packet 的标志字节是否匹配。
如果以上校验通过,调用 decodeMsopPkt()。这是一个纯虚拟函数,由各雷达的派生类提供自己的实现。
4.6.1.4 Decoder::transformPoint()
transformPoint() 对点做坐标变换。它基于第三方开源库 Eigen。
默认情况下,transformPoint() 功能不开启。要启用这个特性,编译时使用 -DENABLE_TRANSFORM 选项。
cmake -DENABLE_TRANSFORM .
4.6.2 DecoderA0
A0 是一款 MEMS 激光雷达,下面介绍 A0 的 Decoder。
DecoderA0 的常量配置由成员函数 getConstParam() 生成。这个配置定义为静态本地变量。
成员
split_strategy_是 SplitStrategyBy 类的实例,保存分帧策略。

4.6.2.1 AGDecoderConstParam 设置
| 常量参数 | 值 | 说明 |
|---|---|---|
| MSOP_LEN | 1172 | MSOP Packet字节数 |
| DIFOP_LEN | 108 | DIFOP Packet字节数 |
| MSOP_ID[] | {0xAA, 0x55, 0xA5, 0x5A} | MSOP Packet标志字节,长度为4 |
| MSOP_ID_LEN | 4 | MSOP_LEN[]中标志字节的长度 |
| DIFOP_ID[] | {0xA5, 0xFF, 0x00, 0x5A} | DIFOP Packet标志字节,长度为4 |
| DIFOP_ID_LEN | 4 | DIFOP_LEN[]中的字节长度 |
| LASER_NUM | 10 | 10通道(5个模组*2个通道) |
| BLOCKS_PER_PKT | 12 | 每Packet中12个Block |
| CHANNEL_PER_BLOCK | 10 | A0有10个通道 |
| DISTANCE_MIN | 0.2 | 测距最小值,单位米 |
| DISTANCE_MAX | 200.0 | 测距最大值,单位米 |
| DISTANCE_RES | 0.005 | Packet中distance的解析度,单位米 |
| TEMPERATURE_RES | 80 | 雷达温度的初始值 |
4.6.2.2 DecoderA0::decodeDifopPkt()
decodeDifopPkt() 解析 DIFOP Packet。
调用 getEchoMode(),解析
AGDifopPkt::return_mode,得到回波模式。根据回波模式,设置成员成员
max_seq_。
4.6.2.3 DecodeA0::decodeMsopPkt()
decodeMsopPkt() 解析 MSOP Packet。
解析 Packet 中的
temperature字段,得到雷达温度,保存到temperature_。调用 parseTimeWithA0() 得到 Packet 的时间戳,保存到本地变量
pkt_ts(因为 A0 直接在下位机处理,把 tm 结构体拆开发过来,因此不使用 parseTimeUTCWithUs获取)。调用 SplitStrategyBySeq::newPacket(),检查是否分帧。如果是,调用回调函数
cb_split_frame_,通知使用者。cb_split_frame_应该转移点云pont_cloud_,并重置它。遍历 Packet 内所有的 Block。
从 Block 相对于 Packet 的偏移,得到 Block 的时间戳。对于 A0, Block 内的所有 Channel 的时间戳都是这个时间戳。
遍历 Block 内所有的 Channel。
解析 Channel 的 distance。
调用 DistanceSection::in() 校验
distance。如果 distance 合法,
计算点云坐标 (
x,y,z)。调用 transfromPoint() 做坐标转换。
设置点云的
intensity、timestamp、ring、range。将点保存到点云
point_cloud_的尾部。
如果
distance不合法,将
NAN点保存到点云point_cloud_尾部。
当前点的时间戳保存到成员
prev_point_ts_。如果下一个 Block 分包,那么这个时间戳就是点云的时间戳。将当前 Packet 的时间戳保存到成员
prev_pkt_ts_。这样,Decoder 的使用者不需要重新解析 Packet 来得到它。
4.6.3 DecoderFactory
DecoderFactory 是创建 Decoder 实例的工厂。

Decoder 雷达的类型如下。
num LidarType
{
A0 = 1
};
4.6.3.1 DecoderFactory::creatDecoder()
createDecoder() 根据指定的雷达类型,创建 Decdoer 实例。
4.7 LidarDriverImpl - 组合 Input 与 Decoder
LidarDriverImpl 组合 Input 部分和 Decoder 部分。
成员
input_ptr_是 Input 实例。成员decoder_ptr_是 Decoder 实例。LidarDriverImpl 只有一个 Input 实例和一个 Decoder 实例,所以一个 LidarDriverImpl 实例只支持一个雷达。如果需要支持多个雷达,就需要分别创建多个 LidarDriverImpl 实例。
成员
handle_thread_是 MSOP/DIFOP Packet 的处理线程。成员
driver_param_是 AGDriverParam 的实例。AGDriverParam 打包了 AGInputParam 和 AGDecoderParam,它们分别是 Input 部分和 Decoder 部分的参数。
typedef struct AGDriverParam
{
LidarType lidar_type = LidarType::A0; ///< Lidar type
InputType input_type = InputType::ONLINE_LIDAR; ///< Input type
AGInputParam input_param;
AGDecoderParam decoder_param;
} AGDriverParam;

组合 Input,
成员
free_pkt_queue_、pkt_queue_分别保存空闲的 Packet, 待处理的 MSOP/DIFOP Packet。这2个队列是 SyncQueue 类的实例。SyncQueue 提供多线程访问的互斥保护。
函数 packetGet() 和 packetPut() 用来向
input_ptr_注册。input_ptr_调用前者得到空闲的 Buffer,调用后者派发填充好 Packet 的 Buffer。
组合 Decoder,
成员
cb_get_cloud_和cb_put_cloud_是回调函数,由驱动的使用者提供。它们的作用类似于 Input 类的cb_get_pkt_和cb_put_pkt_。驱动调用cb_get_cloud_得到空闲的点云,调用cb_put_cloud_派发填充好的点云。驱动的使用者调用成员函数 regPointCloudCallback(),设置
cb_get_cloud_和cb_put_cloud_。
成员函数 splitFrame() 用来向
decoder_ptr_注册。decoder_ptr_在需要分帧时,调用 split_Frame()。这样 LidarDriverImpl 可以调用cb_put_cloud_将点云传给使用者,同时调用cb_get_cloud_得到空闲的点云,用于下一帧的累积。
4.7.1 LidarDriverImpl::getPointCloud()
LidarDriverImpl 的成员 cb_get_cloud_ 是 ag_driver 的使用者提供的。getPointCloud() 对它加了一层包装,以便较验它是否合乎要求。
在循环中,调用 cb_get_cloud_,得到点云。如果点云有效,将点云大小设置为0。如果点云无效,调用 runExceptionCallback() 报告错误。
4.7.2 LidarDriverImpl::init()
init() 初始化 LidarDriverImpl 实例。
初始化 Decoder 部分,
调用 DecoderFactory::createDecoder(),创建 Decoder 实例。
调用 getPointCloud()得到空闲的点云,设置
decoder_ptr_的成员point_cloud_。调用 Decoder::regCallback(), 传递成员函数 splitFrame() 作为参数。这样 Decoder 分帧时,会调用 splitFrame() 通知。
调用 Decoder::getPacketDuration() 得到 Decoder 的 Packet 持续时间。
初始化 Input 部分,
调用 InputFactory::createInput(),创建 Input 实例。
调用 Input::regCallback(),传递成员函数 packetGet() 和 packetPut()。这样 Input 可以得到 Buffer, 和派发填充好 Packet 的 Buffer。
调用 Input::init(),初始化 Input 实例。
4.7.3 LidarDriverImpl::start()
start() 开始处理 MSOP/DIFOP Packet。
启动 Packet 处理线程
handle_thread_, 线程函数为 processPacket()。调用 Input::start(), 其中启动接收线程,接收 MSOP/DIFOP Packet。
4.7.4 LidarDriverImpl::packetGet()
packetGet() 分配空闲的 Buffer。
优先从
free_pkt_queue_队列得到可用的 Buffer。如果得不到,重新分配一个 Buffer。
4.7.5 LidarDriverImpl::packetPut()
packetPut() 将收到的 Packet,放入队列 pkt_queue_。
检查
msop_pkt_queue_/difop_pkt_queue中的 Packet 数。如果处理线程太忙,不能及时处理, 则释放队列中所有 Buffer。
4.7.6 LidarDriverImpl::processPacket()
processMsop() 是 MSOP Packet 处理线程的函数。在循环中,
调用 SyncQueue::popWait() 获得 Packet,
检查 Packet 的标志字节。
如果是 MSOP Packet,调用 Decoder::processMsopPkt(),处理 MSOP Packet。如果 Packet 触发了分帧,则 Decoder 会调用回调函数,也就是 DriverImpl::splitFrame()。
如果是 DIFOP Packet, 调用 Decoder::processDifopPkt(),处理 Difop Packet。
将 Packet 的 Buffer 回收到
free_pkt_queue_,等待下次使用。
4.7.7 LidarDriverImpl::splitFrame()
splitFrame() 在 Decoder 通知分帧时,派发点云。
得到点云,也就是成员
decoder_ptr的point_cloud_。校验
point_cloud_,
如果点云有效,
调用 setPointCloudHeader() 设置点云的头部信息,
调用
cb_put_pc_,将点云传给驱动的使用者。调用 getPointCloud() 得到空闲点云,重新设置成员
decoder_ptr的point_cloud_。
4.7.8 LidarDriverImpl::getTemperature()
getTemperature() 调用 Decoder::getTemperature(), 得到雷达温度。
5 Packet 的录制与回放
使用者调试自己的程序时,点云的录制与回放是有用的,只是点云占的空间比较大。MSOP/DIFOP Packet 占的空间较小,所以 Packet 的录制与回放是更好的选择。
与 MSOP/DIFP Packet 的录制与回放相关的逻辑,散布在 ag_driver 的各个模块中,所以这里单独分一个章节说明。

5.1 录制
5.1.1 LidarDriverImpl::regPacketCallback()
通过 regPacketCallback(),ag_driver 的使用者注册一个回调函数,得到原始的 MSOP/DIFOP Packet。
回调函数保存在成员
cb_put_pkt_。
5.1.2 LidarDriverImpl::processMsopPkt()
在 processMsopPkt() 中,
调用 Decoder::processMsopPkt(),
调用
cb_put_pkt_,将 MSOP Packet 传给调用者。设置 Packet 的时间戳。这个时间戳调用 Decoder::prevPktTs() 得到。
设置这个 Packet 是否触发分帧。这个标志是 Decoder::processMsopPkt() 的返回值。
5.1.3 LidarDriverImpl::processDifopPkt()
在 processDifopPkt() 中,
调用 Decoder::processDifopPkt(),
调用
cb_put_pkt_,将 MSOP Packet 传给调用者。DIFOP Packet 的时间戳不重要。
5.2 回放
5.2.1 InputRaw

InputRaw 回放 MOSP/DIFOP Packet。
使用者从某种数据源(比如 rosbag 文件)中解析 MSOP/DIFOP Packet,调用 InputRaw 的成员函数 feedPacket(),将 Packet 喂给它。
在 feedPacket() 中,InputRaw 简单地调用成员
cb_put_pkt_,将 Packet 推送给调用者。这样,它的后端处理就与 InputSock/InputPcap 一样了。
5.2.2 LidarDriverImpl
InputRaw::feedBack() 在 InputFactory::createInput() 中被打包,最后保存在 LidarDriverImpl 类的成员
cb_feed_pkt_中。使用者调用 LidarDriverImpl 的成员函数 decodePacket(),可以将 Packet 喂给它。decodePacket() 简单地调用成员
cb_feed_pkt_。
5.3 时间戳处理
点云的时间戳来自于 MSOP Packet 的时间戳。MSOP Packet 的时间戳可以有两种产生方式。
用户配置参数
AGDecoderParam::use_lidar_clock决定使用哪种方式。use_lidar_clock = true时, 使用雷达产生的时间戳,这个时间戳在 Packet 中保存。这种情况下,一般已经使用时间同步协议对雷达做时间同步。use_lidar_clock = false时, 忽略 Packet 中的时间戳,在电脑主机侧由 ag_driver 重新产生一个时间戳。此时,如果write_pkt_ts = true,那么就会将 packets 中的时间戳重写为电脑主机侧新产生的时间戳。
5.3.1 使用雷达时间戳
录制时,设置 use_lidar_clock = true
解析 MSOP Packet 的时间戳。这个时间戳是雷达产生的。
输出的点云使用这个时间戳。
如果输出 Packet,也是这个时间戳。
回放时,设置use_lidar_clock = true
MSOP Packet 内保存的仍然是雷达产生的时间戳。
输出点云仍然使用这个时间戳。
5.3.2 使用主机时间戳
录制时,设置 use_lidar_clock = false
ag_driver 在电脑主机侧重新产生时间戳。如果这时有点云输出,使用 ag_driver 产生的时间戳。
在 DecoderA0::internDecodeMsopPacket() 中,ag_driver 调用 getTimeHost() 产生时间戳,然后调用 createTime(),用这个新时间戳。(Packet中原有时间戳没变,只是不使用)
这时输出的 Packet 的时间戳是 ag_driver 产生的时间戳
回放时,设置 use_lidar_clock = true
解析 MSOP Packet 的时间戳。这个时间戳是录制时 ag_driver 在电脑主机侧产生的。
输出的点云使用这个时间戳。
回放时,设置 use_lidar_clock = false
电脑主机侧又会重新产生时间戳(即此时回放的时间戳)。
输出的点云使用这个时间戳。
5.3.3 使用录制 rosbag 时,用当时主机侧重写 packets 的时间戳(即当时主机侧的时间戳)
录制时,设置 use_lidar_clock = false 且 write_pkt_ts = true
ag_driver 在电脑主机侧重新产生时间戳。这个时间戳覆盖 Packet 文件中原有的时间戳;如果这时有点云输出,使用 ag_driver 产生的时间戳
在 DecoderA0::internDecodeMsopPacket() 中,ag_driver 调用 getTimeHost() 产生时间戳,然后调用 createTime(),用这个新时间戳替换 Packet 中原有的时间戳。(Packet 中时间戳被替换)
这时输出的 Packet 的时间戳是 ag_driver 产生的时间戳。
回放时,设置 use_lidar_clock = true
解析MSOP Packet的时间戳。这个时间戳是录制时,当时主机侧 重写packets 的时间戳(即当时主机侧的时间戳)
输出的点云使用这个时间戳。
回放时,设置 use_lidar_clock = false
电脑主机侧又会重新产生时间戳(即此时回放的时间戳)。
输出的点云使用这个时间戳。