某天,曾经的前同事找我,说有个USB项目。因为知道我当时离职在找工作,于是转给我,然后介绍客户给我。
了解需求后,我分析了一下,主要是与USB设备通信的上位机,MFC我熟悉,USB找了一个开源的库hidapi,可跨平台运行,下载编码,尝试读取鼠标信息,获取到信息。于是确定可以接,就答应客户。之后深入了解需求。认为难度在于MFC绘图以及HID协议交互。最后决定分阶段实施,当然,费用也是分阶段付。
前期做界面,完成2种版本发给客户,客户选中其中一款。同时寄硬件设备过来。
在板子未到之前,完成主体界面窗口以及主要布局。研究MFC程序开机自动启动,系统托盘等功能。
板子到之后,就开始研究HID通信。客户发来一个工具读取参数的,可与设备正常通信。不过自编写的代码读取不到参数。重新研究HID协议,安装bus hound抓USB包,对照协议分析报文,对HID有一点认识。深入跟踪hidapi库源码,发现打开USB设备时出错,具体来说,枚举阶段,以读的方式打开,其后使用读写方式,但失败,返回ERROR_ACCESS_DENIED(错误码为5L),于是再使用读方式打开,成功。于是怀疑是因为读写方式打开失败的原因。网上说windows10系统不让以读写方式打开HID,切换win7虚拟机,测试,效果一样。在Linux系统用root权限跑同样代码,却正常。一度陷入困境。
研究了几天,实在无法,跟客户反映困难,客户找了份C#的代码,看了里面标记的时间,虽然有些年头了,但也尝试跑,发现可以。即:同样使用windows10系统,是能正常与HID设备通信的(其实先前工具能读取参数亦证明了)。于是分析C#代码,发现在收发feature report的地方有问题。原来feature report对大小有规定,目前知道是32个字符(使用hidapi等代码,必须多加一个ID的字节,共33字节),如果不符合长度要求,会返回错误,错误码为87L(即ERROR_INVALID_PARAMETER)。修改了,一切正常,难题解决。
接着研究通过USB写数据到板子上的flash上,亦遇到问题。通过bus hound分析,发现写的数据有部分与烧写文件二进制对应不上。找了很久,发现代码的偏移量范围有问题。由于feature report只有32字节,一次要写512字节,所以要循环多次,偏移量变量使用uint8,范围只有255,所以512字节都是前面128字节的内容,内容出错了,当然失败。修正后,一切正常。
到此,第一阶段结束,界面布局、语言切换、操作flash,全部完成,从第一天接触至完成,耗时三周。为了给客户一个良好印象,赶了进度,晚上基本搞到1~2点,周末大部分时间也在搞。虽然赋闲在家,但因为要照料小孩,也要煮饭买菜,日常琐事也占用很多时间。还好,客户如期打款,暂时缓解了燃眉之急。
后续阶段基本没有真正的难度,要说耗时的,主要是需求的不确定性,由于对背景及行业知识了解不多,很多时候,客户所述的需求都很多简单,但对我而言并不简单,所以来回多次沟通。
客户提到要支持 windows xp 和 windows 7 系统,由于前期没有确认这点,所以选择 VS 2015 开发,经测试,还是不能在 xp 上运行,于是跟客户反馈,最终确认无须支持 xp 系统。
由于要显示校准的过程,所以需要画光标,并进行闪烁,最大支持9点,约十年前知道了 tslib 库,当在 GitHub 上看到 tslib 的十字形图标时,倍感亲切。于是在 MFC 中实现了一模一样的图案。不知这叫抄袭还是叫致敬。
另一块画图相关的是坐标及柱状图显示,但无论怎样,也找不到根据鼠标缩放的方案,也是由于这个原因,开始学习 Qt,想看看另一套图形开发框架的效果。当然,这是另一话题了。
由于个人崇尚简洁,因此原则上最终的程序只有一个exe,不依赖其它文件(当然,有些系统级别的dll,不在此列)。所以将 hidapi 源码文件直接添加到工程项目中,再将其封装为类。接着是中英文的切换,开始考虑使用po来实现语言的翻译,但实施起来过于复杂。既要编写语言文件,又要编译,所以舍弃。最终使用笨方式,在界面设计之时使用中文,然后再在代码中切换。语言切换和开机启动等标志写到注册表中。
此项目中,使用到如下技术:HID 数据读写、USB 设备拔插检测、父子窗口通信、开机启动、系统托盘、注册表读写、控件画图。
1、打开HID设备返回 ERROR_ACCESS_DENIED(错误码为5L) 问题。
失败的原因是,Windows认为鼠标、键盘,不应该用读写(实际影响的应该是“写”)方式打开。为安全起见,因此不提供写机制。hidapi 作者在 GitHub 的 issue 中亦提到这点。
打开设备函数为 open_device,如下:
static HANDLE open_device(const char *path, BOOL open_rw)
{
HANDLE handle;
DWORD desired_access = (open_rw)? (GENERIC_WRITE | GENERIC_READ): 0;
DWORD share_mode = FILE_SHARE_READ|FILE_SHARE_WRITE;
handle = CreateFileA(path,
desired_access,
share_mode,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,/*FILE_ATTRIBUTE_NORMAL,*/
0);
return handle;
}
实际使用CreateFileA函数,传入参数有二:路径以及权限(是否读写)。枚举时,不使用读写。即CreateFileA第二个参数dwDesiredAccess为0。在正式打开时,先尝试读写(一般会失败),失败后再用0,此时成功。误入死胡同,以为发送数据一定要读写方式打开,关键语句:
desired_access = (open_rw)? (GENERIC_WRITE | GENERIC_READ): 0;
研究很久,也设置过STANDARD_RIGHTS_READ、STANDARD_RIGHTS_WRITE,但失败。
后找到一篇类似问题的帖子。 里面提到的问题虽然类似,但本质不同。帖子作者是同一USB设备被识别出2个设备,因此可以通过path判断出HID class那一个。但是作者并不成功,鉴于问题表现不同,没细看帖子(英文资料都是跳着看的)。
2、feature report 发送问题。
发送report,出错,在hid_send_feature_report函数,即HidD_SetFeature函数用GetLastError获取错误码,返回87L(即ERROR_INVALID_PARAMETER)。改report报文大小为0x20+1,成功,bus hound可捕获到。(观察bus hound以及网上一些报文分析(长度为0x20),结合ERROR_INVALID_PARAMETER,猜测可能是长度问题,改之,亦成功。)
开始之时,先完成主体框架,再慢慢细化,先有大纲,再有细节,做到胸有成竹,不怕花时间修改,否则要等某个前置资源,如果资源不到位,只能等,一切都是空想。另外,整体架构好之后,不一定按需求前后实现,可以切换,即把多个需求错峰实现,这样,利用大脑的潜时间、暗时间帮我们思考问题。通俗地说,当遇到一个坎时,可以先跳过,过几天可能想到解决之法。做其它事也类似,比如写书。
对于结构体、移位或crc计算等,需要限制变量的位数。但是对于长度、返回值、偏移量等,直接用int即可,在PC领域开发,不考虑字节的节省。这个在开发时没有引起足够重视,导致花费一定时间。
关于需求,其实谁也不能保证一开始就十分准确和完整,都是慢慢补充的。有时候,客户也不知道要实现什么样的功能,做成什么样的东西,此时,我们可以引导客户,甚至先按自己想法完成一版,让客户评估。如果我们等客户,客户等我们,这样徒耗时间,于项目无补。如果谁也不提方案,则自行提出,如果谁也不提意见,则按自己的意见。这是我比较喜欢的做事方式。
做这个项目时,汲取了之前的经验,一切从实际问题出发,追求速度,不扩展研究技术。先完成,再慢慢回顾和总结。其实,编程这么多年,在开发时会有自己的一套准则,以目前来看,开发必须要有git版本管理,编码格式和注释,简短的开发手记,如果不做这些,会觉得不安心。
说实话,现在回顾,当初的评估是有点冒险的。我只是使用 hidapi 获取了鼠标的信息,但写数据未测试(注:尝试写数据给鼠标,失败)。因此,首次调试硬件时就遇到大问题,一度以为案子会失败。幸好客户给了 C# 代码,幸好有一点点 C# 基础,跟踪调试后最终解决问题。解决过程中到网卡查阅大量资料,还研究了 hidapi 源码。
无论怎样,从开发中的紧张担心到现在的释然,成就感还是有一点的,因为积累了一个方面的开发经验,除了熬夜的不良后果外,其它都是好的。