发现海康机器⼈⼯业相机SDK的两个BUG,顺便发布我的Java封装
背景
我司最近有款轮式巡检机器⼈⽤到了的⼯业相机MV-CA060-10GC,我们的开发平台是树莓派(运⾏Ubuntu Server 1804),开发语⾔是Java,但该相机没有Java SDK,于是我决定⾃⼰开发⼀个。
好消息是海康机器⼈提供了C语⾔的SDK,这样我就能通过直接调⽤,⽽不必写⼀⾏C代码。
问题的提出
开发过程中发现,有2个API在Windows下正常运⾏,在树莓派下却总是报错误的参数,错误码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-armhf版jnidispatch所致?
将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. 因为⼀直抓取,所以机器⼈从⼀个位置移动到另⼀个位置后,等待曝光重新稳定的时间很短,从⽽提⾼巡检效率
sdk3. 回调函数在⼦线程⾥运⾏,⽽接收⽤户拍照请求的函数在主线程中执⾏,要想收到拍照指令⽴即返回照⽚,需要做线程同步
考虑重新⽤JNA做,因为JNI依赖的C做线程同步是很⿇烦的,最好能⽤Java来做(synchronized关键字不要太简单)。
重构拍照流程
参照回调样例代码翻译成Java版,期间熟悉了JNA、,总算编译通过了,但⼀运⾏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内存内容,⽐对发现问题!
注意红线标注的数字,是enPixelType字段(C下是枚举类型,JNA推断其存储类型为int)的⼀种取值PixelType_Gvsp_RGB8_Packed,x64下正常,arm下却被赋值给nFrameNum字段,很诡异。
因为pFrameInfo的nFrameLen和enPixelType都会分别赋值给pSaveParam的nDataLen和enPixelType字段,所以拦截掉这2处赋值,替换为x64下的正常值,错误依旧。
最后没办法,想着只能是pSaveParam本⾝哪⾥有问题,于是⽐对该结构体在x64下和arm下的内存内容,结果发现⼀处异常!
发现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当作int、long之类的已知对应native类型的类来看待了。
完整的解决⽅案,见我的这个
尾声
解决此类问题的通⽤办法
如果相同的JNA代码,在⼀个平台OK,另⼀个不OK,那么⼀定是native部分出了问题。
native部分⼜分两种情况,⼀种是我们的JNA wrapper类没有为特定平台做适配,另⼀种是特定平台本⾝C代码有问题。怎么判定到底是那⼀种?
⽅法就是:⽐对JNA分配的内存跟C分配的内存,看哪⼀个跟头⽂件中结构体的定义有出⼊,问题就在哪边。
enum变8字节过程的⼀种猜测
为何枚举值定义成-1会触发这种异常⾏为?猜测是编译器实现enum时,会先检查所有枚举值中的最⼤值,如果是32位的,就⾄少⽤32位来存
3131
再检查是否有负数,有的话说明是有符号的,那枚举值的字⾯量就只能在-2到2-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 unsign
ed 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是不合适的,有空改下代码
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论