发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装

背景

我司最近有款轮式巡检机器人用到了海康机器人的工业相机MV-CA060-10GC,我们的开发平台是树莓派(运行Ubuntu Server 1804),开发语言是Java,但该相机没有Java SDK,于是我决定自己开发一个。
好消息是海康机器人提供了C语言的SDK,这样我就能通过JNA直接调用,而不必写一行C代码。

问题的提出

开发过程中发现,有2个API在Windows下正常运行,在树莓派下却总是报错误的参数,错误码80000004
错误的参数80000004
第一个API负责获取原始图像,第二个负责将原始图像压缩编码成jpg或bmp等文件格式,都是必须要用的API

int MV_CC_GetOneFrameTimeout(  void                    *handle,
  unsigned char           *pData,  unsigned int            nDataSize,
  MV_FRAME_OUT_INFO_EX    *pFrameInfo,  int                     nMsec);
  
  
  int MV_CC_SaveImageEx2(  IN void* handle, 
  MV_SAVE_IMAGE_PARAM_EX    *pSaveParam);

向厂商的技术支持求助,技术支持问样例代码在树莓派上运行正常吗?我试了试,发现正常,技术支持说那就是你们代码写得有问题,仔细查查吧。

走过的弯路

我的Java代码应该不会有问题,否则Windows上就报错了,平台差异JNA已经帮我屏蔽了呀,除非JNA屏蔽得有问题。

换64位Ubuntu

注意到JNA在不通平台有相应的jnidispatch动态库,而树莓派是armhf
架构,会不会是因为JNA没有linux-armhfjnidispatch所致?
发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装_第1张图片
windows
将Ubuntu server换成64位版,对应架构是linux-aarch64,所以是有专属的jnidispatch的,但错误依旧。

将Ubuntu换成Raspbian

怀疑是Ubuntu上的JDK有问题,那换树莓派官方发行版Raspbian应该没问题了吧?可惜,错误依旧。
另外Raspbian没有64位版,所以也没法进一步试

退而求其次,用JNI

项目进度逼迫之下,我用JNI方式实现了轮询式拍照,这是最简单的拍照方式,顺利实现功能。

JNI也有问题

但是测试中发现在Linux-arm下有一个BUG,如果开启抓取后几秒钟内没有调用GetOneFrameTimeout,则后续再调用就永远返回无数据(错误码80000007)且无法恢复,现在看来应该是缓冲区没管理好。
无数据
这个问技术支持,也没下文。

规避方法及其缺陷

最后自己想出规避办法,每次在拍照前开启抓取,开启后立即读取帧缓存,读完立即停止抓取。
但是在测试中又发现新问题,因为巡检机器人面对的环境光照强度差别较大,所以将相机配置成自动曝光后,每次开启抓取后等待曝光稳定就要好几秒,效率很低下。
总之,就用这么慢的相机,我们的机器人通过了中期验收

拍照改用callback方式

验收通过后,总结主要问题,拍照慢明显排第一。琢磨解决方法,既然第一个BUG无法解决,那么试试回调式拍照。
先在回调拍照的样例代码上做了几个实验,发现该方式有以下特点:

  1. 可以一直抓取,不用担心一段时间不读取引起无数据问题,因为SDK会启动一个线程不停地从相机帧缓存读取帧数据到用户缓存,用户回调函数对帧缓存处理完毕就会回收
  2. 因为一直抓取,所以机器人从一个位置移动到另一个位置后,等待曝光重新稳定的时间很短,从而提高巡检效率
  3. 回调函数在子线程里运行,而接收用户拍照请求的函数在主线程中执行,要想收到拍照指令立即返回照片,需要做线程同步

考虑重新用JNA做,因为JNI依赖的C做线程同步是很麻烦的,最好能用Java来做(synchronized关键字不要太简单)。

重构拍照流程

参照回调样例代码翻译成Java版,期间熟悉了JNA访问Union、注册回调,总算编译通过了,但一运行MV_CC_SaveImageEx2函数就报错误的参数这个老问题

死磕JNA

既然报错误的参数,那就说明参数格式有误,而不是内存不足或功能不支持等其他错误,所以检查MV_CC_SaveImageEx2的2个参数,第一个handle嫌疑很小,如果有问题前面的开启抓取就会失败,另一个参数pSaveParam是我自己创建的,按理说不会有问题,难道是回调函数的入参有问题?

void(__stdcall* cbOutput)(  unsigned char           *pData,
  MV_FRAME_OUT_INFO_EX    *pFrameInfo,  void                    *pUser);

在IDEA下检查3个入参,pData确实是图像数据,能看到相邻2个像素的值很接近;pUser也确实是我传的handle;就剩下pFrameInfo嫌疑最大。
导出x64下和arm下pFrameInfo内存内容,比对发现问题!
发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装_第2张图片
注意红线标注的数字,是enPixelType字段(C下是枚举类型,JNA推断其存储类型int)的一种取值PixelType_Gvsp_RGB8_Packed,x64下正常,arm下却被赋值给nFrameNum字段,很诡异。
因为pFrameInfonFrameLenenPixelType都会分别赋值给pSaveParamnDataLenenPixelType字段,所以拦截掉这2处赋值,替换为x64下的正常值,错误依旧。
最后没办法,想着只能是pSaveParam本身哪里有问题,于是比对该结构体在x64下和arm下的内存内容,结果发现一处异常!
发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装_第3张图片
发现x64比arm多了12字节,而不是我以为的8字节(2个pointer,x64下每个pointer 8字节),原来额外多出来的4字节是因为pImageBuffer前面为保证8字节对齐而填充了4字节,白高兴一场。

灵光一现

想看一下样例c代码中pSaveParam的内存内容,跟JNA申请的native内存里,是否一样。用gdb调试样例C代码,在回调执行编码函数处打断点,断住后

(gdb) p sizeof(MV_FRAME_OUT_INFO_EX)
$2 = 56

pSaveParam的size竟然是56,而同样跑在arm下,JNA版却是52,为何会不一样?逐个检查结构体的每个字段,最终找到异常字段enPixelType,其size是8!

(gdb) p sizeof(MvGvspPixelType)
$16 = 8

至于为什么是8?我做了一系列实验寻找答案,这应该是一种未定义的行为,要尽量避免

BUG的根源

就是厂商开发人员将PixelType_Gvsp_Undefined的值定义成-1导致的

enum MvGvspPixelType
{
    // Undefined pixel type
#ifdef WIN32
    PixelType_Gvsp_Undefined                =   0xFFFFFFFF, 
#else
    PixelType_Gvsp_Undefined                =   -1, 
#endif
}

猜测厂商开发人员一开始所有平台都是-1,后来发现win32平台编译不过,必须改成0xffffffff才能过,为了变动最小,于是加个条件编译宏,殊不知微软的编译报错才是用心良苦。

解决办法

知道是因为enum size变8字节导致JNA解析方法跟内存内容对不上,进而出现错误,所以解决办法就是更改JNA的解析方法,同时不影响x64
一开始尝试自定义type mapper,但随着了解的深入,发现不用大动干戈,只要为这个特殊的enum单独定义converter就行,但是这还不够简单,更简单的方法是让该enum实现NativeMapped接口,这样JNA就能将该enum当作intlong之类的已知对应native类型的类来看待了。
完整的解决方案,见我的这个repo

尾声

解决此类问题的通用办法

如果相同的JNA代码,在一个平台OK,另一个不OK,那么一定是native部分出了问题。
native部分又分两种情况,一种是我们的JNA wrapper类没有为特定平台做适配,另一种是特定平台本身C代码有问题。怎么判定到底是那一种?
方法就是:比对JNA分配的内存C分配的内存,看哪一个跟头文件中结构体的定义有出入,问题就在哪边。

enum变8字节过程的一种猜测

为何枚举值定义成-1会触发这种异常行为?猜测是编译器实现enum时,会先检查所有枚举值中的最大值,如果是32位的,就至少用32位来存
再检查是否有负数,有的话说明是有符号的,那枚举值的字面量就只能在-231到231-1之间了
再检查有没有字面量最高位为1的,如果有,编译器就认为用户想要这个字面量为正值,这样就超出了int32的表示范围,那就只能扩成int64

解决方案的一处小问题

在Linux-amd64平台上测试,MvGvspPixelType的size也是8字节

test.c: In function ‘main’:
test.c:8:38: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long unsigned int’ [-Wformat=]
     printf("sizeof enum PixelType = %d\n", sizeof(enum PixelType));
                                     ~^
                                     %ld
test.c:10:24: warning: format ‘%llx’ expects argument of type ‘long long unsigned int’, but argument 2 has type ‘long int’ [-Wformat=]
     printf("RGB = 0x%llx\n", RGB);
                     ~~~^
                     %lx
whp@whp-STCK1A32WFC:~$ ./a.out
sizeof enum PixelType = 8
MONO = 0xffffffff
RGB = 0x80000000

所以用芯片类型来区分enum szie是不合适的,有空改下代码

你可能感兴趣的:(Java)