SICK Ranger3源码分析——断线重连

paw5zx Lv4

前言

简单分析一下SICK Ranger3源码中断线重连的实现,这一块算是比较容易的,先择出来分析一下。

代码示例仅贴出关键部分以便分析

使用SDK版本为3.4.2.6

断线重连官方例程:Demo_R3_callback_with_heartbeat.cpp

断线检测

断线重连可以划分为两步,首先检测相机断线并通知,然后用户在收到通知后进行重连操作。我们先看SICK如何实现断线检测。

断线检测机制内置于SICK SDK中,由SICK SDK管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// file: Ranger.cpp
EXPORT_TO_DLL CAM_STATUS
Ranger3::connectCamera(CallbackEvent_HeartBeats pCallback, const uint32_t& microSecond, void * any)
{
try{
auto e = connectCamera();
if(e == CAM_STATUS::All_OK)
{
m_heartbeat_is_on = 1;
...
// 开启心跳检测线程
auto _thread = std::make_shared<std::thread>(&Ranger3::_check_HeartBeats_run, this);
_thread->detach();
}
return e;
}
...
}

void
Ranger3::_check_HeartBeats_run()
{
while (m_heartbeat_is_on==1)
{
__sleep1MS(m_heartbeat_interval);
...
try {
Str value("");
// 设备在线,不抛异常,反之,抛出异常
m_Param.getParameter(m_deviceNodeMap, "DeviceTemperature", value);
...
}
catch (...) {
...
}
}
}

可以看出,断线检测机制很简单,就是分离一个线程,循环访问相机寄存器(SICK的实现是通过定时获取设备温度访问相机寄存器),若访问不到(失败),就意味着相机已离线。

Paw5zx注:

断线通知

断线通知机制同样内置于SICK SDK中:在检测到设备离线后,调用注册好的回调函数(注册过程将在下文介绍)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// file: Ranger.cpp
void
Ranger3::_check_HeartBeats_run()
{
while (m_heartbeat_is_on==1)
{
__sleep1MS(m_heartbeat_interval);
...
try {
Str value("");
// 设备在线,不抛异常,反之,抛出异常
m_Param.getParameter(m_deviceNodeMap, "DeviceTemperature", value);
...
}
catch (...) {
// 一些资源释放操作
...
// m_on_lost_function为注册好的回调函数对象
// 设备离线,访问寄存器失败,捕获异常,调用m_on_lost_function
auto _thread = std::make_shared<std::thread>(m_on_lost_function, &m_DeviceName, &m_DeviceIP, &m_on_lost_mac, &msg, m_on_lost_inputs);
_thread->join();
return;
}
}
}

重连实现

重连机制的具体实现由用户进行。在例程Demo_R3_callback_with_heartbeat.cpp中,由用户自定义一个回调函数(在相机离线时会被调用),回调内循环对相机进行重连操作。用户在连接相机时注册这个回调

用户层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// file: Demo_R3_callback_with_heartbeat.cpp

// 用户定义的回调函数,在相机断开连接时被调用
void SICK_CALLBACK
on_lost_device_Demo_R3_callback_with_heartbeat(std::string* name, std::string* ip, std::string* mac, std::string* msg, void * pR3)
{
auto pCam = (SickCam::Ranger3*)pR3;
while (true)
{
// 根据相机对象存储的设备信息对物理相机进行重连操作,不展开说明了
auto ec = pCam->reconnectCamera();
...
__sleep1MS(1000);
}
}
// 连接相机时注册回调
auto err = pCam1->connectCamera(on_lost_device_Demo_R3_callback_with_heartbeat, 1000, pCam1.get());

在SICK SDK中,注册过程会:

  • 将用户注册的on_lost_device_Demo_R3_callback_with_heartbeat赋值给m_on_lost_function
  • 将用户传递上下文信息any赋值给m_on_lost_inputs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// file: Ranger3.h
typedef std::function<void(std::string* name, std::string* ip, std::string* mac, std::string* msg, void* any)> CallbackEvent_HeartBeats;

// file: Ranger.cpp
/*
[in] – pCallback 当失去心跳时,将调用由用户定义的回调函数。相应的处理可以在此函数中执行。
[in] – microSecond 读取心跳的时间间隔,单位毫秒,推荐值为 10 000;
[in] – any 在失去心跳的响应函数(CallbackEvent_HeartBeats)中,该指针将作为输入参数,由用户定义。
*/
EXPORT_TO_DLL CAM_STATUS
Ranger3::connectCamera(CallbackEvent_HeartBeats pCallback, const uint32_t& microSecond, void * any)
{
try{
auto e = connectCamera();
if(e == CAM_STATUS::All_OK)
{
...
m_on_lost_function = pCallback;
m_on_lost_inputs = any;
auto _thread = std::make_shared<std::thread>(&Ranger3::_check_HeartBeats_run, this);
_thread->detach();
}
return e;
}
...
}

注册完毕后,当相机出现离线情况,就如上文所述,SDK会调用注册的回调函数进行重连。

遇到的问题

在此仅记录一下我自己使用时发现的小问题。套个盾,我使用的SDK版本到目前为止肯定不是最新版本,以下提到的问题很有可能已在新版本被修复。

重连时失败

此问题由多个因素综合导致,先汇总现象,后逐个分析

  • ①设备掉线后进程崩溃。点此快速跳转
  • ②修复①后,在设备断线后重连时无法正常连接,且有报错GenTL call failed: -1006, Message: Requested handle not found in the pool点此快速跳转

在逐步分析之前,我们先总结一下相关的成员变量与函数。

成员变量

m_connectedDevices

声明及初始化
1
2
3
typedef std::map<Str, SPtr<DeviceConnection>> deviceList;
static deviceList m_connectedDevices;
deviceList CameraShared::m_connectedDevices= deviceList();
描述

已连接设备表,用于管理设备名称和设备连接对象的映射关系。表中管理所有连接至主机的GenICam设备(包含SICK设备和非SICK设备)

元素增加

_add_device()

  • 对于非SICK的GenICam设备,如果没有在m_connectedDevices中被管理,则将其加入;
  • 对于SICK的GenICam设备,如果其已经被管理,则什么都不做;如果没有在m_connectedDevices中被管理,则将其加入。
代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void
CameraShared::_add_device( cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
const SiDeviceList& devices,
const int64_t& id,
int & nDevices)
{
...
// 非SICK设备
if (device_name == "")
{
if(m_connectedDevices.count(it->first) == 0)
m_connectedDevices.insert({ it->first, std::make_shared<DeviceConnection>(it->first)});
return;
}
// 已管理的SICK设备
if (m_connectedDevices.count(device_name) == 1)
{
...
return;
}

// 未管理的SICK设备
auto pDev = std::make_shared<DeviceConnection>( m_pconsumer,
interfaceHandle,
devices,
id,
interfaceName);

...

m_connectedDevices.insert({pDev->mDeviceName, pDev});
}
元素减少

在这里我们仅总结与断线重连问题相关的地方。

_clear_invalid_device()中,直接删除名称为函数参数name的条目

代码折叠,点击展开
1
2
3
4
5
6
void
CameraShared::_clear_invalid_device(cStr& name)
{
m_connectedDevices.erase(name);
...
}

m_Interfaces_Map

声明及初始化
1
2
3
// 类型我自己更改了一下
static std::map<std::string, GenTL::IF_HANDLE> m_Interfaces_Map;
std::map<std::string, GenTL::IF_HANDLE> CameraShared::m_Interfaces_Map;
描述

可用接口表,用于管理接口名称与接口句柄的映射关系,例如对于以太网口,名称可能是enp3s0。表中管理所有挂载了GenICam设备(包含SICK设备和非SICK设备)的接口

元素增加

只有一处涉及到其元素的增加,即在_scan_interface()中,当检测到当前物理接口下挂载了GenICam设备(包含SICK设备和非SICK设备)且当前物理接口还没有在此表中管理,则向此表中插入一条{<接口名称>, <接口句柄>}

代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
CameraShared::_scan_interface(cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
int & nDevices)
{
...
auto devices = m_pconsumer->getDevices_is_changed(interfaceHandle);
if (!devices.empty())
{
if(m_Interfaces_Map.count(interfaceName) == 0)
m_Interfaces_Map.insert({interfaceName, interfaceHandle});
...
}
...
}
元素减少

在这里我们仅总结与断线重连问题相关的地方。

_clear_invalid_device()中,遍历接口-设备表中的每个元素:

  • 若接口下的设备名称集合为空(这里先不讨论什么情况下会为空),则说明接口不需要了,先通过IFClose关闭接口句柄(SDK源码中并没有这一步),然后移除此映射表中与当前接口对应的条目
  • 若接口下的设备名称集合不为空,且存在着要删除的SICK设备(其名称通过_clear_invalid_device()的参数name传入),则将其名称移出设备名称集合,并再次检查当前接口下的设备名称集合是否为空,若此时为空,则移除此映射表中与当前接口对应的条目

Paw5zx注:

我认为_clear_invalid_device中的代码逻辑和实现是有问题的,这里先展示SDK提供的源码,在《问题分析》中会继续分析

代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void
CameraShared::_clear_invalid_device(cStr& name)
{
...
for(auto devs = m_Device_inInterface_Map.begin(); devs!= m_Device_inInterface_Map.end();)
{
if (devs->second.empty())
{

m_Interfaces_Map.erase(devs->first);
...

continue;
}


if (devs->second.count(name) == 1)
{
devs->second.erase(name);
...
}


if(devs->second.empty())
{
...
m_pconsumer->closeInterface(m_Interfaces_Map[devs->first]);

m_Interfaces_Map.erase(devs->first);
...

}
else
++devs;
}
}

m_Device_inInterface_Map

声明及初始化
1
2
static std::map<std::string, std::set<std::string>>     m_Device_inInterface_Map;
std::map<std::string, std::set<std::string>> CameraShared::m_Device_inInterface_Map;
描述

接口-设备表,用于管理接口名称与其下挂载的GenICam设备(仅SICK设备)集合的映射关系。表中管理所有挂载了GenICam设备(包含SICK设备和非SICK设备)的接口,但是其对应的设备名称集合下,仅记录SICK设备。

元素增加

映射表元素的增加是在_scan_interface()中,当检测到当前物理接口下挂载了GenICam设备(包含SICK设备和非SICK设备),且当前物理接口还没有在此表中管理,则向表中插入一条{<接口名称>, {}},其中{}代表空设备集

代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
CameraShared::_scan_interface(cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
int & nDevices)
{
...
auto devices = m_pconsumer->getDevices_is_changed(interfaceHandle);
if (!devices.empty())
{
...
if(m_Device_inInterface_Map.count(interfaceName) == 0)
m_Device_inInterface_Map.insert({interfaceName, std::set<std::string>()});
...
}
...
}

而设备集内元素的增加则是在_add_device()中,当成功创建了SICK设备的连接后,会将此SICK设备的名称加入到映射表中对应物理接口条目下的设备名称集合中。

代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
CameraShared::_add_device( cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
const SiDeviceList& devices,
const int64_t& id,
int & nDevices)
{
...

// 成功创建了新SICK设备的连接后
auto pDev = std::make_shared<DeviceConnection>( m_pconsumer,
interfaceHandle,
devices,
id,
interfaceName);

...

m_Device_inInterface_Map[interfaceName].insert(pDev->mDeviceName);
}
元素减少

_clear_invalid_device()中,遍历此映射表中的每个元素:

  • 若接口下的设备名称集合为空(这里先不讨论什么情况下会为空),则说明接口不需要了,移除此映射表中与当前接口对应的条目
  • 若接口下的设备名称集合不为空,且存在着要删除的SICK设备(其名称通过_clear_invalid_device()的参数name传入),则将其名称移出接口对应的设备名称集合,并再次检查当前接口下的设备名称集合是否为空,若此时为空,则移除此映射表中与当前接口对应的条目
代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void
CameraShared::_clear_invalid_device(cStr& name)
{
...
for(auto devs = m_Device_inInterface_Map.begin(); devs!= m_Device_inInterface_Map.end();)
{
if (devs->second.empty())
{

m_Device_inInterface_Map.erase(devs++);
...

continue;
}


if (devs->second.count(name) == 1)
{
devs->second.erase(name);
...
}


if(devs->second.empty())
{
...
m_pconsumer->closeInterface(m_Interfaces_Map[devs->first]);

m_Device_inInterface_Map.erase(devs++);
...

}
else
++devs;
}
}

函数

主要的函数都位于CameraShared这个类中,这个类感觉可以理解为一个SDK中的最顶层的管理类,管理了要使用的接口和设备等

_scan_Device

声明
1
CAM_STATUS			_scan_Device(int& numberDeviceNewFound);
描述

扫描当前系统中所有可用接口下的设备,并更新m_Device_inInterface_Mapm_Interfaces_Mapm_connectedDevices

内部逻辑
  • 调用TLUpdateInterfaceList()更新Producer内部管理的可用接口列表,并在当前函数中记录一个临时的可用接口列表interfaces,列表内各元素为{<接口模块ID>, <接口的用户可读名称>}
  • interfaces中的每个可用接口创建类型为GenTL::IF_HANDLE的接口句柄interfaceHandle
    • 如果接口已经在m_Interfaces_Map中被管理,意味着其对应的接口句柄还存在,所以直接令interfaceHandle等于m_Interfaces_Map中管理的句柄即可
    • 若接口不在m_Interfaces_Map中,意味着这可能是一个新的可用接口,会调用TLOpenInterface()打开接口,然后将句柄赋值给interfaceHandle
  • interfaces中的每个成功打开的可用接口调用_scan_interface()
代码
代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
CAM_STATUS
CameraShared::_scan_Device(int& numberDeviceNewFound)
{
#ifdef CALLBACK_NEW
std::unique_lock<std::mutex> lock(m_mutex_scan);
#endif
*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: scan_Device() start \n";
if (m_tlHandle == GENTL_INVALID_HANDLE) return CAM_STATUS::ERROR_OPEN_TL_HANDLE;


// new found interface
#ifdef __linux__
auto interfaces = m_pconsumer->getInterfaces(m_tlHandle);
#endif
#ifdef _WIN32
auto interfaces = _getInterfaces(m_tlHandle, m_sTl);
#endif


*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: interfaces.size() = " << interfaces.size() << "\n";
for (size_t i = 0; i < interfaces.size(); ++i)
{
auto interfaceName = interfaces[i].second;
GenTL::TL_HANDLE interfaceHandle = GENTL_INVALID_HANDLE;
if (m_Interfaces_Map.count(interfaceName) == 1)
{
interfaceHandle = m_Interfaces_Map[interfaceName]; // has been saved in pervious scan
}
else
{
#ifdef __linux__
interfaceHandle = m_pconsumer->openInterfaceById(_findInterfaceByIndex(interfaces, i));
#endif
#ifdef _WIN32
interfaceHandle = _openInterfaceById(_findInterfaceByIndex(interfaces, i), m_tlHandle, m_sTl); // interface new found
#endif
}
if (interfaceHandle == GENTL_INVALID_HANDLE)
continue;

*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: Open Interface : " << interfaceName << "\n";
_scan_interface(interfaceName, interfaceHandle, numberDeviceNewFound);
}


*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: The number of available interfaces: " << m_Device_inInterface_Map.size() << "\n";
for (auto sub : m_Device_inInterface_Map)
{
*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: Interface: " << sub.first << ": \n";
for (auto sub_second : sub.second)
{
*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: Dev - " << sub_second << "\n";
}
}

m_isDevFound = false;
for(auto sub:m_connectedDevices)
{
if(sub.second->mDeviceName != "")
{
m_isDevFound = true;
break;
}
}

//m_isDevFound = !m_connectedDevices.empty();


// final check
if (numberDeviceNewFound > 0)
{
*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: After scanning all interfaces, " << numberDeviceNewFound << " new device(s) found!\n\n\n";
return CAM_STATUS::All_OK;
}

*m_log << CustomerLog::time() << "[CameraShared::scan_Device]: After scanning all interfaces, no new device found!\n\n\n";
return CAM_STATUS::All_OK;
}

_scan_interface

声明
1
2
3
4
5
6
7
8
/*
[in]interfaceName: 接口名称
[in]interfaceHandle: 接口句柄
[out]nDevices: 设备数量
*/
void _scan_interface(cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
int & nDevices);
描述

此函数对单个接口调用,用于扫描指定接口下的设备,并更新m_Device_inInterface_Mapm_Interfaces_Mapm_connectedDevices

内部逻辑
  • 调用IFUpdateDeviceList()更新Producer内部管理的可用设备列表,并在当前函数中记录一个临时的可用设备列表devices,列表中管理这当前接口下挂载的所有GenICam设备(包含SICK设备和非SICK设备)。
    • 对于非SICK设备,其在devices中对应的元素格式为{<modelName>(<sn>), ""}
    • 对于SICK设备,其在devices中对应的元素格式为{<deviceId>, <deviceId>_<deviceUserId>}
  • 如果devices非空(即当前接口下挂载了GenICam设备),则
    • m_Interfaces_Map中没有管理当前接口,则将接口加入
    • m_Device_inInterface_Map中没有管理当前接口,则将接口加入,并且设置接口下设备名称集合为空
    • 对于devices中的每个设备调用_add_device()
  • 如果devices为空(即当前接口下未挂载任何GenICam设备),则
    • m_Interfaces_Map中没有管理当前接口,则调用IFClose()关闭当前接口句柄
代码
代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void
CameraShared::_scan_interface(cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
int & nDevices)
{
#ifdef __linux__
auto devices = m_pconsumer->getDevices_is_changed(interfaceHandle);
#else
auto devices = _getDevices_is_changed(interfaceHandle, m_sTl);
#endif
if (!devices.empty())
{
if(m_Interfaces_Map.count(interfaceName) == 0)
m_Interfaces_Map.insert({interfaceName, interfaceHandle});

if(m_Device_inInterface_Map.count(interfaceName) == 0)
m_Device_inInterface_Map.insert({interfaceName, std::set<std::string>()});

*m_log << CustomerLog::time() << " [CameraShared::_scan_interface]: # Interface: " << interfaceName << " has GenICam device(s)! \n";
*m_log << CustomerLog::time() << " [CameraShared::_scan_interface]: # Find total " << devices.size() << " GenICam device(s) in Interface. \n";

nDevices = 0;
for (size_t j = 0; j < devices.size(); ++j)
_add_device(interfaceName, interfaceHandle, devices, j, nDevices);
}
else
{
if (m_Interfaces_Map.count(interfaceName) == 0)
{
#ifdef __linux__
m_pconsumer->closeInterface(interfaceHandle);
#else
_closeInterface(interfaceHandle, m_sTl);
#endif
*m_log << CustomerLog::time() << "[CameraShared::_scan_interface]: Warning: No device in Interface: " << interfaceName << ").\n";
}
}
}

_add_device

声明
1
2
3
4
5
6
7
8
9
10
// [in]interfaceName: 接口名称
// [in]interfaceHandle: 接口句柄
// [in]devices: 设备列表
// [in]id: 设备索引
// [out]nDevices: 设备数量
void _add_device ( cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
const SiDeviceList& devices,
const int64_t& id,
int & nDevices);
描述

此函数在单个接口下对单个设备调用,用于为新GenICam设备创建设备对象(封装了设备句柄的DeviceConnection)。注意:

  • 对于SICK和非SICK设备,创建设备对象的逻辑是不同的,但二者都会被加入m_connectedDevices(若之前没加入)。
  • SICK设备会被加入到m_Device_inInterface_Map中当前接口下的设备名称集合,而非SICK设备则不会
内部逻辑
  • 接口有效性检查,检查当前接口是否在m_Device_inInterface_Map中被管理
  • 在GenIcam设备列表中寻找第id个设备
    • 若此设备为非SICK设备且在m_connectedDevices中未被管理,则将其加入管理,然后返回。注意此设备对应的DeviceConnection为为非SICK设备准备的空构造
    • 若此设备为SICK设备且已在m_connectedDevices中被管理,直接返回。
    • 若此设备为SICK设备且在m_connectedDevices中未被管理(意味着是新发现的SICK设备),则为其创建DeviceConnection。若成功创建,则更新m_connectedDevices,并更新m_Device_inInterface_Map中当前接口下的设备名称集合
代码
代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void
CameraShared::_add_device( cStr& interfaceName,
GenTL::IF_HANDLE interfaceHandle,
const SiDeviceList& devices,
const int64_t& id,
int & nDevices)
{
if (m_Device_inInterface_Map.count(interfaceName) != 1)
{
*m_log << CustomerLog::time() << " [CameraShared::_add_device]: Error: interfaceName: " << interfaceName << " not found!\n";
return;
}

// [done]: add check to ignore the devices connected ---------------
auto it = devices.begin();
std::advance(it, id);
auto device_name = it->second;

// device: unsupported
if (device_name == "")
{
*m_log << CustomerLog::time() << " [CameraShared::_add_device]: [x ] Unsupported device: " << it->first << " \n";
if(m_connectedDevices.count(it->first) == 0)
m_connectedDevices.insert({ it->first, std::make_shared<DeviceConnection>(it->first)});
return;
}

// device: already found
if (m_connectedDevices.count(device_name) == 1)
{
if(m_connectedDevices[device_name]->mDeviceName != "")
*m_log << CustomerLog::time() << " [CameraShared::_add_device]: [! ] Ignore [SICK] device: " << device_name << ", already found! \n";
return;
}

auto pDev = std::make_shared<DeviceConnection>( m_pconsumer,
interfaceHandle,
devices,
id,
interfaceName);

// device: occupied or unreachable
if (pDev->isOccupied())
{
*m_log << CustomerLog::time() << " [CameraShared::_add_device]: [! ] Error [SICK] device: " << device_name << ", occupied, device is busy! \n";
return;
}
if (!pDev->isReachable())
{
*m_log << CustomerLog::time() << " [CameraShared::_add_device]: [! ] Error [SICK] device: " << device_name << ", unreachable, device ip = "<<pDev->getIp() << ", please check computer ip! \n";
return;
}

// device is available
++nDevices;
m_connectedDevices.insert({pDev->mDeviceName, pDev});
m_Device_inInterface_Map[interfaceName].insert(pDev->mDeviceName);
*m_log << CustomerLog::time() << " [CameraShared::_add_device]: [ok] Add [SICK] device: " << pDev->mDeviceName << ", successfully! \n";
}

_clear_invalid_device

声明
1
void        _clear_invalid_device       (cStr& name);
描述

此函数对单个设备调用,用于移除name对应的设备,包括其在m_connectedDevicesm_Device_inInterface_Map中的所有记录。如果某个接口下的设备都被移除完毕,还会顺便关闭这个接口

(这个描述是我根据SICK SDK的实现总结的,因为我认为这个SDK中这个函数实现的不太好,所以说不好是我总结的描述不符合SDK设计的意图,还是函数实现的有问题)

内部逻辑
  • m_connectedDevices中移除目标设备
  • 对于m_Device_inInterface_Map中的每个条目(接口),检查当前接口
    • ①若未挂载任何SICK设备,则从m_Interfaces_Mapm_Device_inInterface_Map中移除当前接口项(我认为SDK在这步之前少了一步关闭接口,并且对于迭代器重复++了)
    • ②若挂载了SICK设备,则在m_Interfaces_Map中接口项的设备名称集合中查找是否有目标设备,若有则将其从设备名称集合中移除。移除后再次进行①中逻辑
代码
代码折叠,点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 感觉此函数实现有大问题

void
CameraShared::_clear_invalid_device(cStr& name)
{
m_connectedDevices.erase(name);

// clear device
for(auto devs = m_Device_inInterface_Map.begin(); devs!= m_Device_inInterface_Map.end();)
{
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Try to close device: " << name << " in NetCad: " << devs->first << "\n";

if (devs->second.empty())
{
m_Interfaces_Map.erase(devs->first);
m_Device_inInterface_Map.erase(devs++);
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Not found device: " << name << " in NetCad: " << devs->first << "\n";
++devs;
continue;
}

if (devs->second.count(name) == 1)
{
devs->second.erase(name);
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Close device: " << name << " in NetCad: " << devs->first << "\n";
}

if(devs->second.empty())
{
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Interface is empty in NetCad: " << devs->first << "\n";
#ifdef __linux__
m_pconsumer->closeInterface(m_Interfaces_Map[devs->first]);
#endif
#ifdef _WIN32
CC(m_sTl, m_sTl->IFClose(m_Interfaces_Map[devs->first]));
#endif
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Close Interface: " << devs->first << "\n";
m_Interfaces_Map.erase(devs->first);
m_Device_inInterface_Map.erase(devs++);
}
else
++devs;
}
}

问题分析

上面我们总结完了相关的成员变量和函数,现在我们重新看下问题:

  • ①设备掉线后进程崩溃
  • ②修复①后,在设备断线后重连时无法正常连接,且有报错GenTL call failed: -1006, Message: Requested handle not found in the pool

设备掉线后进程崩溃

这个问题比较明显也比较好解决,原因就是SDK中访问了已释放的内存:

在分离运行的_check_HeartBeats_run()中有一个函数_freeDevice(),其中有:

1
m_pR3S->_clear_invalid_device(m_DeviceName);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// file: CameraShared.cpp
void CameraShared::_clear_invalid_device(cStr& name)
{

m_connectedDevices.erase(name);

for(auto devs = m_Device_inInterface_Map.begin(); devs!= m_Device_inInterface_Map.end();)
{
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Try to close device: " << name << " in NetCad: " << devs->first << "\n";

if (devs->second.empty())
{
m_Interfaces_Map.erase(devs->first);
m_Device_inInterface_Map.erase(devs++);

// 首先这个打印可能访问到已释放或者未定义的内存
// 其次这devs两次++,迷惑
*m_log << CustomerLog::time() << "[CameraShared::_clear_invalid_device]: Not found device: " << name << " in NetCad: " << devs->first << "\n";
++devs;
continue;
}

...
}
}

解决方法:

1
2
3
4
5
6
7
8
9
// file: CameraShared.cpp
if (devs->second.empty())
{
auto to_erase = devs++;
m_Interfaces_Map.erase(to_erase->first);
*m_log << CustomerLog::time() << "<" << std::this_thread::get_id() << ">" << "[CameraShared::_clear_invalid_device]: No device: " << name << " in NetCad: " << to_erase->first << "close now" << "\n";
m_Device_inInterface_Map.erase(to_erase);
continue;
}

断线后无法正常重连

先上结论,断线时未调用IFClose()关闭接口,然后重连时调用TLOpenInterface()尝试打开未关闭的接口,导致错误。

分析这个问题,首先要从调用链的最开始来看,例程中的调用链大概是这样子:

1
2
3
4
5
6
7
BEGIN   → CameraShared::CameraShared() → CameraShared::_scan_Device() → CameraShared::_scan_interface() → CameraShared::_add_device()
→ Ranger3::Ranger3() → 忽略
→ Ranger3::connectCamera(cb_on_lost) → Ranger3::_connectCamera() → 忽略
→ Ranger3::_check_HeartBeats_run()(if lost) → Ranger3::_freeDevice() → CameraShared::_clear_invalid_device()
→ cb_on_lost() → Ranger3::reconnectCamera() → CameraShared::_scan_Device() → 省略
→ Ranger3::connectCamera(cb_on_lost) → 省略
→ 忽略

其中cb_on_lost是用户注册的回调,在相机断线时SDK会在CameraShared::_freeDevice()后调用此函数。本文中我参考SICK例程,在回调中循环定时调用Ranger3::reconnectCamera()

然后需要关注的另一个点是,主机上都挂载了哪些GenICam设备,那么简单分为以下几类:

  • ①主机上仅挂载单个SICK设备;
  • ②主机上挂载两个及以上SICK设备(不同网口)
  • ③主机上挂载两个及以上SICK设备(同一网口下)
  • ④主机上挂载一个SICK设备和一个非SICK设备(不同网口)
  • ⑤主机上挂载一个SICK设备和一个非SICK设备(同一个网口)

对于①,目测SDK内的实现没有什么大问题
对于②③,暂时没条件测试,暂时过,后面可能会验证。
现在来谈谈④:

主机上挂载一个SICK设备和一个非SICK设备(不同网口)

在分析之前,我先例举一下主机上连接的设备,以便后续的流程梳理。

主机的两个网口enp3s0enp4s0(忽略其他网口)各挂载着一个GenICam设备:

  • enp3s0上挂载的是非SICK设备A
  • enp4s0上挂载的是SICK设备B

然后我们按照函数调用链开始梳理


在构造CameraShared时,会调用_scan_Device(),其中会对每个物理接口做一些操作,先看enp3s0

  • 通过openInterfaceById()间接调用TLOpenInterface()打开enp3s0,并返回其句柄。
  • 对其调用_scan_interface()。在_scan_interface()中,enp3s0被添加进m_Interfaces_Mapm_Device_inInterface_Map
  • enp3s0下每个设备调用_add_device()。在_add_device()中,由于A为非SICK设备,所以将其加入m_connectedDevices但不会更新m_Device_inInterface_Map中的设备集。
  • enp3s0的扫描过程结束,最终结果是:
    • m_Interfaces_Map{{"enp3s0", < enp3s0的接口句柄 >}}
    • m_Device_inInterface_Map{{"enp3s0", {}}}
    • m_connectedDevices{{< A的名称 >, < DeviceConnection for A >}}

对于enp4s0,由于其下挂载的B是SICK设备,所以与enp3s0不同的是,在_add_device中,会将其名称加入到m_Device_inInterface_Map中接口对应的设备集下,因此enp4s0扫描过程结束后的结果是:

  • m_Interfaces_Map{{"enp3s0", < enp3s0的接口句柄 >}, {"enp4s0", < enp4s0的接口句柄 >}}
  • m_Device_inInterface_Map{{"enp3s0", {}}, {"enp4s0", {< B的名称 >}}}
  • m_connectedDevices{{< A的名称 >, < DeviceConnection for A >}, {< B的名称 >, < DeviceConnection for B >}}

_scan_Device()还有一个需要注意的Postcondition是,所有挂载了GenICam设备的接口,都被打开了并在Producer中被管理。


现在我们关注,当设备断线时,会被调用的_clear_invalid_device(),因为问题就是出现在这里。注意!!!只有SICK设备才会调用到这个函数。所以对于B:

  • m_connectedDevices中移除B对应的元素
  • 对于m_Device_inInterface_Map中的每个接口:
    • 首先是enp3s0,由于其下的设备集合为空,因此在m_Interfaces_Mapm_Device_inInterface_Map中删除相关项。注意!!!问题出现在这里,这里并没有调用IFClose关闭接口,导致接口在Producer中没释放。
    • 然后是enp4s0,其下挂载了B,首先删除m_Device_inInterface_Mapenp4s0对应的设备集合中B的相关元素,然后调用IFClose关闭接口,最后在m_Interfaces_Mapm_Device_inInterface_Map中删除相关项
  • 至此B设备的清理完成,最终的结果:
    • m_Interfaces_Map{}
    • m_Device_inInterface_Map{}
    • m_connectedDevices{{< A的名称 >, < DeviceConnection for A >}}

这里的问题是没有调用IFClose关闭接口enp3s0,但是清除了其在Consumer中的相关记录,这就会导致Consumer这边认为enp3s0已关闭,而实际上Producer中可能仍管理着这个接口(具体取决于Producer实现,俺不知道)。然后在后续的reconnectCamera()调用中会再次调用到_scan_interface(),会有报错:

1
GenTL call failed: -1006, Message: Requested handle not found in the pool

排查一下,上述报错是_scan_Device()中第一个用宏CR修饰的涉及enp3s0接口的GenTL调用报出的,即

1
CR(mTl->IFUpdateDeviceList(ifHandle, &changed, 500));

但仔细分析,其实这不是源头,因为_scan_Device()中的第一个涉及enp3s0接口的GenTL调用并不是IFUpdateDeviceList(),而是TLOpenInterface(),只不过其没有使用CR修饰,因此不会抛出异常和打印报错信息。现在为了测试,我们为其添加CR

1
2
3
4
5
6
7
8
9
// file: Consumer.cpp
GenTL::IF_HANDLE Consumer::openInterfaceById(const InterfaceId& interfaceId)
{
...

CR(mTl->TLOpenInterface(mTlHandle, interfaceId.c_str(), &interfaceHandle));

...
}

然后报错会变为:

1
GenTL call failed: -1005, Message: ItfOpen: the requested interface exists in the list, but is marked as inaccessible

可以看出这里打开接口是失败了的,错误码-1005 GC_ERR_ACCESS_DENIED,因此接口句柄无效,所以在IFUpdateDeviceList才会报出-1006 GC_ERR_INVALID_HANDLE错误码。

初步的解决措施是在_clear_invalid_device()中对未挂载设备的接口增加一个调用closeInterface(),手动关闭接口即可。(其实_clear_invalid_device()大逻辑还是可以优化的,但还是等要到SDK最新版本看一下官方的解决方案吧)

主机上挂载一个SICK设备和一个非SICK设备(相同网口)

暂略,未做实验验证

声明错误

m_Interfaces_Map

1
2
3
4
// file: CameraShared.h

// 接口句柄映射
static std::map<std::string, GenTL::TL_HANDLE> m_Interfaces_Map;

这个结构体应该是接口名称到接口句柄的映射表,用于记录和管理当前已打开的接口(对于GigE就是网口)。在GenTL中对应的句柄应该是GenTL::IF_HANDLE,所以此变量的声明理应改为:

1
2
3
4
// file: CameraShared.h

// 接口句柄映射
static std::map<std::string, GenTL::IF_HANDLE> m_Interfaces_Map;

只不过由于:

1
2
3
// file: GenTL.h
typedef void * TL_HANDLE; /* Transport Layer handle, obtained through the TLOpen */
typedef void * IF_HANDLE; /* Interface handle, obtained through ::TLOpenInterface */

并且实际使用中向m_Interfaces_Map传入的都是GenTL::IF_HANDLE,因此代码实际上并不会出现什么错误。

上述结论也可以通过SDK中其他与m_Interfaces_Map相关的调用来验证。

interfaceHandle

错误原因与m_Interfaces_Map相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
// file: CameraShared.cpp

CAM_STATUS
CameraShared::_scan_Device(int& numberDeviceNewFound)
{
...
for (size_t i = 0; i < interfaces.size(); ++i)
{
...
GenTL::TL_HANDLE interfaceHandle = GENTL_INVALID_HANDLE;
}

}

应改为

1
2
3
4
5
6
7
8
9
10
11
12
13
// file: CameraShared.cpp

CAM_STATUS
CameraShared::_scan_Device(int& numberDeviceNewFound)
{
...
for (size_t i = 0; i < interfaces.size(); ++i)
{
...
GenTL::IF_HANDLE interfaceHandle = GENTL_INVALID_HANDLE;
}

}
  • 标题: SICK Ranger3源码分析——断线重连
  • 作者: paw5zx
  • 创建于 : 2025-03-09 00:19:32
  • 更新于 : 2025-05-26 11:16:38
  • 链接: https://paw5zx.github.io/SICK-Ranger3-source-code-analysis-01/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论