CSK.Blog--个人原创Weblog

低成本激光投射虚拟键盘的设计制作-上(原理和硬件)

姗姗来迟的文章...先前在《无线电》杂志2012年9-10月刊登过,近期转到我blog上
转载注意:
来源连接:
目前DFRobot正协助制作本设计的开源套件,感兴趣的朋友可以前去购买。详情见:
在制作3D激光扫描仪的过程中,我偶然在网络上发现有人销售可以投影出键盘画面的激光器组件,并且价格低廉。仔细想来其实激光投影键盘的原理和3D扫描仪一致,于是便产生了DIY一个低成本激光键盘的想法。最终,本作品在我完成3D扫描仪的3天内完成了,可见硬件部分的制作不复杂。虽然完成的时间短,但其中也涉及不少的设计细节需要考虑,于是便写了此文向大家分享我的制作过程。

简介

激光投射键盘相信大家之前也有所听说,他通过光学手段,将计算机键盘的画面通过激光投影到任意的平面上(如桌面)上,并且允许操作者像使用真实键盘那样进行输入操作。

图:Celluon公司出品的Magic Cube激光投影键盘

目前市面上能买到的激光键盘产品是由韩国公司Celluon生产销售的。这类产品目前也可以在国内买到,不过因为价格高昂,目前仍旧在千元水平。我之前并没有购买过,具体使用效果也不得而知。有人评价说因为缺少物理反馈,这样的键盘使用上手感可能不好。

但无论如何,激光虚拟键盘还是非常能吸引眼球的,在投射出的键盘影像上输入非常有科技感并且足够科幻。如果能自己以较低成本DIY一台出来,那岂不是更好?

本文提出的DIY激光投影键盘方案可以保证使用较低的成本(百元左右),并提供较强的性能:30hz的输入频率、支持多键输入,并且还有额外的功能:把他当作多点输入设备来使用,比如可以作为绘图板,并能感应手指的压力大小。

在完成本制作后我拍摄了一段演示视频展现它的性能,可以访问如下地址观看:

原理分析


在具体介绍实现过程前,我们首先需要分析这类激光投影键盘的工作原理以及给出解决问题的思路,这样也可方便大家举一反三。首先需要解决的核心问题有这么两个:

  1. 如何产生键盘的画面?
  2. 如何检测键盘输入事件?

产生键盘画面


对于产生键盘画面,可能很多人认为这种画面是通过激光+高速光学振镜来得到的。这种方式虽然在技术上是完全可行的,但由于需要采用精密的机械部件,成本非常高,并且也难以做成轻便的产品。
 
图:通过光学振镜扫描产生的激光投影画面。图片来自[1]

实际上在激光投影键盘产品中,这类画面往往是通过全息投影技术得到的。激光器通过照射先前保存有键盘画面的全息镜片的方式在目标平面上产生相应的画面。这种方式的成本非常低廉,市面销售的激光笔常配备的投影图案的镜头也是用这种原理产生的。

 
图:可以投射出图案的激光笔镜头

不过这类全息投影方式对于DIY来说仍旧不现实,幸好得益于目前网络的便利——通过网购可以直接买到用于产生激光键盘画面的全息投影设备了,且成本在¥50以内。


 
图:可以购买到的投影键盘画面的激光器模组

识别键盘输入事件


在解决了产生键盘画面的问题后,这里分析另一个看似简单的问题:如何判断键盘输入事件?

由于键盘画面可以投射在任意的表面上,因此传统的靠物理按钮的手段自然是不可能的(否则也称不上虚拟键盘)。需要非接触的手段来检测。这里给出了几种途径,他们在技术上都是可行的:

1) 通过计算机视觉的方式,通过图像来识别
通过摄像头捕捉键盘区域的画面并进行分析,判断出键盘输入事件。
2) 通过检测按键发出的声音来判断
这里假设使用者在按键时会碰触桌面,产生一定的敲击声。通过检测该声音传播时间,可以进行定位。该方案在国外的一些研究机构已经实现。
3) 通过超声波雷达手段来判断
通过发射超声波并检测反射波的传播时间查来检测目标物体(手指)的位置。

这3种方案国内外均有文献表明可以实现,不过相对来说计算机视觉的硬件较为简单,仅仅需要一个摄像头,因此这里我们采用这种方式。

下图给出了本制作早期阶段,摄像头所拍摄的使用过程的画面,基于这类画面进行计算机视觉的运算,可以得到我们需要的键盘事件:

图:通过计算机视觉的方式识别并判断键盘输入事件

其实这里涉及到了2个子问题:
  1. 判断手指按下的是哪个键?
  2. 如何判断手指已经“按到”了对应的“按钮”?

由于我们人类就是通过视觉来理解外部世界的,因此很直观的可以想到,只要能够识别并定位画面中手指的位置,第一个问题就可以解决了。这里先不讨论定位本身该如何实现,假设我们的算法已经可以和人脑一样轻松在一副画面中找到手指的位置并用相对于图像的坐标来表示(x,y)。

接下来就要考虑第二个问题,如何判断手指已经“按下按钮”?这里一个办法是通过捕捉声音:即像前文提到的通过捕捉手指碰触桌面产生的敲击声来判断。但这样会带来额外的问题:

  1. 需要额外的硬件和电路,增加了复杂性
  2. 如何将敲击声与画面中真正敲击的手指对应?就像上图中的5个手指都可能是在敲击状态,此时难以进行匹配
  3. 其他的噪音也会被当作键盘敲击

因此这里还是依靠视觉的手段来进行判断。在分析可行方案前,需要明确“按下按钮”的具体指标。我们可以定义当手指碰触桌面,或者距离桌面足够接近为“按下”。那么其实问题的实质就是我们需要检测出手指距离桌面的距离z。在求出该数值后,我们只需简单的判断它小于某一个值,就认为手指已经“按下按键”。

综合起来看,我们需要设计一种视觉处理算法:它可以在一副画面中找出每个手指相对画面的位置(x,y),并且手指距离桌面的高度z。事实上我们就是在检测手指的三维空间坐标了(x,y,z)了。
 

图:需要设计的视觉检测算法和期望的检测结果

如果之前有阅读我的3D激光扫描仪制作的话,相信大家就能猜到具体的实现方式了,其实算法本身就是做三维的激光测距。不过这里我们先按照前文的思路继续分析下去:

做三维测量有很多种方式,比如现在可以购买微软的KINECT深度传感器。如果不考虑成本,这的确是一种非常有效的方法,可以非常好的解决本文提出的这些问题。另一种类似的办法是使用双摄像头来做双目视觉处理,提取目标画面物体的深度信息。不过目前这类处理比较消耗处理资源,且校正过程比较复杂,并不适合这里的应用。

 
图:RoboPeak团队曾进行的双目视觉进行深度提取的实验

这里我们采用基于三角测距原理的激光测距仪[2]一致的办法,通过主动投射激光来做目标物体的三维坐标检测。在[2]中我使用一束线型激光照射目标物体,在目标物体的反射光被摄像头捕捉到,利用三角测距原理,可以求出目标物体中被线激光照亮部分的坐标信息。
 
在[2]中提到的利用线激光和单摄像头的3D测距原理

这里我们将线激光所产生的光线平面与桌面平行并紧贴在桌面之上,将摄像头放置于激光发射器上方并俯视桌面,如下图所示:

 
图:利用激光三角测距原理用于手指空间坐标检测

此时若手指接近桌面,则会阻挡住激光的通路,产生反射,反射的光点画面会被图中摄像头拍摄到。这是一个标准的三角测距的结构设置。细心的读者可能已经发现在前文给出的本制作摄像头拍摄到的画面中手指尖部的白色光斑,这正是安装了线激光器后被手指遮挡产生的反射效果。

 
图:本制作采用的线激光测距方案

对于这种基于线激光的测距的方式[2],我们可以通过三角关系求出被激光照亮部分相对于激光发射口为原点、位于线激光组成平面内的坐标P(x,y)。而又因为线激光组成的平面与桌面平行,且在设计上这两个平面是紧贴着的,所以可以近似的认为坐标点P(x,y)是位于桌面平面的。

这样的设置具有几个优点:

方便检测指尖
当手指遮挡激光平面后产生了反射,因此会在摄像头画面中出现较为明亮的光斑,可以通过简单的视觉算法来提取指尖部分

方便检测按键事件
由于只有当手指靠近或者碰触到桌面才会遮挡住激光产生反光,而在距离桌面较高的位置则不会被检测算法察觉。因此这里检测按键事件可以做出简化,不需要通过检测手指距离桌面高度来判断。而只要当检测算法检测到手指反光,则可认为出现了按键事件,且可直接用当前检测到的坐标来进行后续的处理。

值得一提的是,事实上这样的方式对从事多点触摸领域朋友来说应该不陌生,有一种称为LLP(Laser Light Plane)的技术[4]和我们的方式很类似。

图:LLP方式的多点触摸技术,图片来源自[5]

如上图所示,LLP技术也通过发射一束线型激光构造出一个光平面,并捕捉手指反光来做多点触摸定位。而摄像头的安装位置也并非一定位于上边示意图的底下,同样也可以安装在本制作一样的位置。事实上这里采用的激光测距方案的前期处理是和LLP一致的:提取手指指尖的兴趣点,并转换成相对于摄像头画面的坐标。不过对于多点触摸而言,不需要得到手指在桌面上的精确距离,就好比我们用的鼠标只需要知道一个相对位移的程度,而不需得到具体鼠标移动了多少毫米。但由于我们需要将手指的坐标转换成具体按键的信息,因此还有必要进行更进一步的处理。

不过相信有人会说这样的方法岂不是和之前通过求出指尖的三维坐标的方式的思路不同吗?的确目前的方式无法直接求得手指距离桌面的高度z。并且接触主动发射线激光的特性,通过求取z来判断是否存在按键事件也显得不必要了。但这并不表示我们无法求解手指距离桌面的高度,并且在后文我将指出,求出手指距离桌面的高度仍旧是有意义的。

判断并产生对应的按键事件


前面我们已经解决了如何检测手指当碰触到桌面时指尖在桌面平面上的坐标P(x,y)。但这与我们最终要产生对应的按键事件还有一定距离。我们需要建立一个映射机制,通过桌面坐标P(x,y)找到对应按键键值,并最终通知操作系统触发一个对应键值的按键事件。

这里的做法与GUI系统进行UI元素碰撞判断的过程类似。在图形系统中,所有UI元素均保存有他们相对于屏幕的坐标值。GUI系统不停地判断当前鼠标指针位置是否落入了某一个按钮或者选择框的坐标范围内。

图:GUI系统通过对比鼠标坐标和按钮UI元素的坐标来判断鼠标是否存在于按钮元素上方

与GUI系统类似,我们首先需要建立投射键盘图案中每个按键的坐标信息。然后将指尖相对于桌面平面上的坐标P(x,y)映射到键盘图案的坐标系内进行按键的判断。
 
图:将基于桌面平面以激光器为原点的指尖坐标P(x,y)映射到键盘坐标平面并进行按键判断

上述过程涉及到两个步骤:

1) 将以激光发射器为原点的指尖坐标P(x,y)映射到以键盘图案左上角为原点坐标的平面内,得到映射点P’(x,y)
该过程用数学表达记作:
P' (x,y)=fprojection (P(x,y))

2) 将映射点P’(x,y)与事先记录的每个按键的坐标值比对,求得当前对应按下的按键值。
该过程记作:
Key[n]=fmapping (P' (x,y))

可能有人会问为何不直接按照以激光发射器原点的坐标来表示每个按键的坐标?这样第一步的映射f_projection就可以省略了。这的确也可以达到同样的效果。但是由于在组装上会存在误差,不能保证每次制作出来的成品激光发射器与键盘图案投射的位置都完全一致,并且单个成品在使用过程中也会因为热胀冷缩等原因,键盘图案会发生偏移。一旦键盘图案发生了移动,则先前以这种方式记录的所有按键坐标都需要重新测量。但如果一开始就以键盘左上角为原点的方式记录坐标,则每次组装完成品或者发生图案偏移后,则只需进行简单的矫正,求出一个新的f_projection函数即可。

对于步骤2)中的函数f_mapping,一种简单的实现是依次按照前文例图中的判断代码对每个按键轮流计算,判断是否被“按下”。这种方式实现简单直观,但是性能较差。对于这类问题,可以使用Kd-Tree的数据结构[3]进行快速的查找。

系统框图


前面我们解决了制作激光投影键盘的两个关键问题,这里将我们最终采用方案的大致原理做出总结,方便大家理解。在实现过程部分,将具体就其中的实现细节做出介绍。

 

框图中还包含了本制作的另一个功能:多点触摸绘图板。该部分也将在具体制作过程中介绍。

实现过程

器件选择


有前面的原理分析可知,本激光键盘至少需要摄像头、投射键盘画面的激光器以及一字线激光。如下是我选用的器件和参数

元件

核心参数

摄像头

广角镜头,视角>120

投射键盘画面的激光器

无特殊要求

一字线激光

红外激光器,>50mW

红外带通滤光片

800nm左右光谱带通


对于摄像头,这里一个比较关键的特性是需要采用广角镜头。目前的USB摄像头一般视角在90度左右,这样的摄像头需要在很高处俯视才能拍摄到完整的激光键盘图案,如果想把本投影键盘制作的小巧,则需要采用广角镜头,视角最好在120度以上。

 
图:窄视角的摄像头需要安装在较高的位置

除了镜头外,摄像头本身没有特殊要求,一般市面VGA画质的普通USB摄像头即可满足要求。下图为我采用的镜头和摄像头照片:
 
图:两种不同视角的摄像头镜头,左边为窄视角镜头,右边为广角镜头

  
图:USB摄像头模组

对投射键盘图案的激光发射器模组前文已经提到,且没有特殊要求。

 
图:选用的激光键盘图案投射模组

对用于测距的激光发射器,只需要使用红外波段的一字线激光器,功率在50mW左右即可。为了安全考虑,不要使用过大功率的激光器。采用红外波段主要为了过滤可见光干扰,简化视觉处理中的兴趣点提取,同时也不会因为手指反射对投射的键盘图案产生干扰。
 
图:选用的红外线型激光器

摄像头改装


在我之前的3D测距仪文章[2]中提到过使用红外激光器配合红外滤光片进行测距可有效避免可见光干扰的技巧,在这里也仍旧适用。对此我们需要对摄像头镜头做一些改造,首先拆除镜头上的红外光截至滤光片,并在镜头头部安装红外带通滤光片:

 
图:拆除镜头内的红外截止滤光片

 
图:在镜头表面安装红外带通滤光片

电子系统


本制作的电子系统比较简单,仅仅涉及给两个激光发射器供电以及通过USB电缆连接摄像头至主机。并没有采用任何单片机,这样也意味着之前原理部分介绍的算法将完全在PC上实现。这样的优点是大幅降低了制作成本以及制作的难度。如果使用的是免驱动USB摄像头,在PC上使用本激光投影键盘只需要额外运行处理程序即可。不过缺点就是这样的设计无法给手机等不支持USB的设备使用。

由于摄像头已经使用了一条USB电缆,因此这里我们直接通过usb的5V供电来驱动另外2个激光器。电路很简单,如下图所示:

 

可以看到电路的核心就是一个3.3V的LDO芯片而已,可以裁剪一个洞洞板制作:

 
图:使用小块洞洞板制作简易的驱动电路

总体安装


由前面的分析可以得到如下的安装结构:
 
其中线型激光器需要安装在设备的最底下并紧靠底部,这样可以保证产生的激光平面能够尽可能的靠近桌面平面。在激光器上方安装投射键盘图案的激光模组,他的高度以能够投射出与常规键盘尺寸大小类似的图案来决定。在最顶端安装摄像头,其高度取决于摄像头能够完整拍摄整个键盘图案画面为宜。

我使用一块轻质木材作为安装这3个部件的整体支架:

 
图:将激光器安装在木板最底部,使用热熔胶固定
 
图:安装图案投射模块和摄像头并点亮
 
图:在木板背面安装稳压电路板和所有供电连线
 
图:将木板装入一个牢固的纸盒

 
图:完成并通电
 
图:顶部加装一个“保护罩”

最终本制作的实体部分组装完毕,接下来将介绍本制作的重点:视觉算法部分。

低成本激光投射虚拟键盘的设计制作-下(算法与实现)

姗姗来迟的文章...先前在《无线电》杂志2012年9-10月刊登过,近期转到我blog上
转载注意:
请保留作者和出处。
目前DFRobot正协助制作本设计的开源套件,感兴趣的朋友可以前去购买。详情见:

3.5. 前期视觉处理


由于本制作的视觉处理部分都在PC上进行,因此我使用了OpenCV[6]库来加速视觉运算代码的开发。在进行原理部分提到的手指尖提取和坐标求解前,需要对摄像头捕获的原始画面做一些前期的处理,方便后续的计算。

3.5.1. 镜头扭曲矫正

由于我们使用了广角镜头,因此摄像头拍摄到的画面会存在比较明显的扭曲,就如下图所示:

这样扭曲的画面难以直接进行处理计算出后续需要的坐标信息,因为这种扭曲变换是非线性的。因此这里首先需要对摄像头镜头进行校正。在之前的文章[2]中有对摄像头矫正技术的介绍,这里我仍旧使用matlab的摄像头校正工具[7]进行校正。在OpenCV中也提供了相关函数和工具可以对摄像头校正,不过很多人反映相比matlab的算法,OpenCV函数校正得到的结果略差。

在使用Chessboard图案完整校正后,可使用OpenCV的cvInitUndistortRectifyMap/ cvRemap函数通过校正数据将扭曲的画面重新修正。

图:将左侧广角镜头的原始画面进行扭曲修正,得到右侧画面

这里有一点需要注意,在先前的安装阶段我们提到摄像头需要安装红外带通滤光片。但在安装滤光片后,由于可见光都被过滤,因此之后无法再进行上述的镜头校正。因此这部分的校正工作需要先于红外滤光片的安装进行。
3.5.2. 滤波

由于摄像头镜头加装了红外带通滤光片,可见光可以有效地被阻挡,因此在摄像头捕获的画面上基本只含有手指对红外激光的反射。

图:红外带通滤光片有效地过滤了可见光干扰

对于这样的画面基本上是可以直接进行后续的视觉处理的,不过一般我们还需要额外进行几个步骤:灰度化、高斯滤波、阈值化、形态学滤波

灰度化即将原先RGB色彩的彩色图像转化成灰度图,因为后续的视觉算法并不关心色彩信息,但需要反射光亮度,使用灰度表示后可以大幅加速处理速度。高斯滤波(Gauss Filter)、阈值化(Threshold)、形态学滤波(Morphology Filter)用于过滤画面中的噪点并且使得反射光斑变得平滑和连贯。如果不熟悉这部分概念,可以参考Digital Image Processing一书[8]。这几步操作在OpenCV中均有对应函数可以实现。

图:对图像进行各类滤波处理后的效果

上图展现了经过上述滤波算法后,手指尖激光反光光斑处理后的效果。可以看到,原先光斑外围的反射光干扰以及两个比较靠近的指尖之间“黏连”的光斑已经有效地过滤掉了。通过一系列的滤波过程,使得我们可以很精确的求出指尖的坐标。

3.6. 兴趣点提取


可以使用OpenCV提供的cvFindContours对先前与处理得到的光斑画面进行轮廓提取,进而求解出每个光斑区域在图像中的位置。进一步的,我们通过质心法并以光斑亮度作为权重,可以大致求解出每个指尖中心的大致坐标。虽然这个中心坐标未必真的在指尖中心,但相比简单的以光斑区域中心作为指尖中心的方法要精确很多。

图:使用轮廓提取手段提取出来的手指区域坐标

3.7. 手指坐标计算和校正


在得到了手指光斑中心点相对于画面坐标之后,可以通过三角测距的方法,把手指在桌面平面内的坐标P(x,y)求出来。
该过程可以类比在激光3D测距中的算法,如下图所示:

图:将指尖坐标求解问题转化成3D激光测距问题

我们可以把兴趣点画面顺时针旋转90度,即将图像的X/Y轴相互交换,并在兴趣点之间连接起直线,如上图(b)所示。就会发现这样的画面就和[2]中进行3D激光测距出现的激光扫描线类似,如上图(c)。因此,我们可以按照[2]中推导的公式,求出画面中每个兴趣点的真实坐标(即以激光器为原点,桌面所在平面的坐标系坐标)

这部分的具体原理和公式推导请参考文章[2]。为了能够进行精确的激光测距,我们也需要和[2]中描述的那样对本制作进行校正。校正方法与[2]一样,可以在投影键盘前方用直尺做测量并进行曲线拟合。

图:对激光测距部分进行校正

经过本轮的运算,我们可以得到先前监测到的兴趣点在桌面平面坐标下的表示,测试坐标值具有了物理意义,均以毫米为单位表示。

3.8. 按键映射和校正


在得到了以桌面坐标平面表示的指尖坐标P(x,y)后,我们需要再进行校正得到前文提到的映射函数
P' (x,y)=fprojection (P(x,y))
该部分的校正比较容易,可以将手指放置在键盘图案的某几个“按键”上,然后将当前的坐标P(x,y)与所“按”“按键”在键盘图案的坐标P_key (x,y)做一下对比。

理想状态下,每个P(x,y)与对应的P_key (x,y)都偏差了一个固定的位移量D。不过由于校正中会带来误差,每次测量时的偏差量D’与理想的位移量都存在一个误差偏移Derror。即
Pkey (x,y)=P(x,y)+D+ Derror
这里我们就可以采用多次测量,并进行最小二乘拟合的方式,把Derror给消除掉,得到近似于D的一个校正值。因此,这里的映射函数就是:
P' (x,y)=P(x,y)+D
另外这里也需要把键盘图案的坐标事先保存下来。我直接对投影在桌面上的键盘图案进行了测量,并按照如下格式保存在程序当中:

图:使用一维数组,保存每个按键中心点的坐标(x,y),按键的尺寸,以及按键的键值

在有了上述数据后,一方面我们可以使用Kd-tree来快速查找到所按下的按键ID。及前文所说的函数
Key[n]=fmapping (P' (x,y))
这里的f_mapping在我的实现中,就是一个Kd-tree。相比循环查找的O(n)复杂度,Kd-tree可以在O(log(n))的时间复杂度下,找到距离点P’(x,y)最接近的元素,因此非常高效率。

目前OpenCV中提供了Kd-tree的实现,不过我在使用过程中遇到了诸多问题,尤其它可能会导致程序crash,因此我使用了由Martin F. Krafft开发的基于C++模板的Kd-tree实现libkdtree++[9]。

得到键盘图案坐标后的另一个好处是可以在PC上重新绘制出一个软键盘,用于提示当前“按下”的按键,提高使用体验:

图:将坐标映射到对应“按下”的按键,并提供视觉反馈

3.9. 键盘事件摸拟注入


在得到用户按下键盘按键的信息后,就要考虑将这些按键注入到当前的系统中,使得我们的投影键盘能够作为一个真正的键盘来使用。这里我没有采用最直接的编写系统驱动程序的办法,虽然那样性能最好最直接,但会额外增加制作时间。我采用Windows系统提供的SendInput API[10]向系统注入键盘事件。该API允许应用程序向整个系统注入任意的键盘事件,在实际效果上,已经和编写驱动程序的效果没有区别。像系统输入法等特性也可以很好的支持。另外,这样做也可以保证我们所有的处理程序都在用户态模式运行,可以很容易的把我们的代码移植到MacOS或者Linux下运行。

图:本制作可以很好的使用中文输入法
不过前面我们得到的按下按键信息还不能直接注入到系统,还需要额外处理如下两件逻辑:
1) 模拟组合键
2) 模拟连发事件

对于组合键的模拟其实比较容易,一方面我们的设计可以保证多按键同时输入,对于Ctrl+C这样的组合键,其实不需要做额外处理,只要将当前的实际按键传输给OS即可。而对于键盘的功能键,比如Fn+F1这种特殊用途的按键,就需要编写程序特殊实现。
对于连发事件OS就没有提供什么特殊帮助了,需要我们自己来模拟。

3.10. 检测手指对桌面的压力与多点触摸版应用


在本制作的介绍视频中,我演示了本制作可以检测出手指对桌面的“压力”,从而在绘图板应用中能够产生不同粗细画笔的应用。

图:本制作的另一应用,多点绘图板

上图中可以发现绘制的线条的粗细不是固定的,线条的粗细是正是通过估算手指对桌面的“压力”决定的。

在前文原理介绍中已经知道,我们是无法真正得到手指对桌面的压力的。一切的信息都是通过摄像头的画面捕捉得来。这里估算的压力,是通过先前兴趣点提取阶段计算出的光斑面积得出的:

图:通过兴趣点区域面积估算“压力”

如上图(b)所示,程序使用了不同直径的圆圈表示两个投影点对应手指的不同“压力”。而参考他们对应的兴趣点尺寸(a)可以发现,面积比较大的兴趣点区域往往也认为“压力”比较大(注意两个画面是上下颠倒关系)。

这样的估算其实也有一定的现实依据,因为当手指紧压桌面时候,指尖反射激光的面积会变大。不过直接使用面积大小作为压力大小的话就会出现问题。就如上图(c)所示,从兴趣点面积看,画面下侧的兴趣点面积显然比上边的大很多。不过这未必是因为对应的一个手指用力压着桌面而另一个没有用力造成的。大家都知道按照投影变换,距离镜头比较远的物体就算尺寸相同,与会显得比较小。在这里两个兴趣点也是同样的情况,所设计的算法需要考虑到这个问题,得到尽可能真实的“压力”估算,如上图(d)所示,本算法最终估算的两者“压力”几乎类似。

这里采用的手段很简单,因为我们可以通过三角测距原则计算出画面中任意点的真实物理坐标,因此可以摆脱投影变换的干扰。具体做法是除了每个兴趣点中心区域求解对应的物理坐标P(x,y)之外,我们对兴趣区域边框上的某一个点也求出他的真实坐标P2(x,y)。由于P(x,y)和P2(x,y)反应了真实世界的两点坐标,用他们之间的距离就可以比较真实的估算出手指的“压力”

图:将兴趣点和兴趣区域边缘一点分别进行测距求解坐标,并以物理坐标点之间的距离估算“压力”

3.11. 程序总体界面和完成图


完成了上述几个核心功能后,我在用OpenCV的绘图功能做了一个简易的程序界面,前面提到的每个处理状态都会实时的在界面中展现。就此大功告成!

图:程序界面

图:使用照片

4. 讨论和下一步工作


虽然制作成本低廉,但得益于在算法上的优势,本激光投影键盘的性能并不差。我并没有用过Celluon公司销售的激光投影键盘产品,因此无法与它们的产品做出比较,这里就单独按照我自己的使用感受以及性能做出评价。

得益于采用了[2]中描述的测距算法,该激光键盘可以识别出0.1mm的细微手指移动,并且通过校正可以达到2mm的最大误差水平。另外理论上通过视觉定位算法,本制作可以支持任意多个按键同时输入。而在输入响应速度上,由于采用的usb摄像头是30fps的,所以本键盘的最快响应频率就是30Hz,这虽然不敌传统键盘100hz以上的速度,但作为日常使用来说是足够了。

性能指标

描述

反应速度

30hz

最多同时输入的按键(多点触摸点数)

无穷多(软件目前限制在10点)

分辨率

0.1mm

最大测距误差

2mm

传输协议

USB

不过由于不是实体键盘,无法提供物理反馈,因此无法进行盲打,输入速度也不够快。这点我相信Celluon公司的产品也有同样的问题。就我们这个DIY作品来说,相信它应该是一个成功的作品。

下一步工作


虽然本制作已经完满达到当初的设计要求,不过还有不少可以改善的地方。这里我就当是抛砖引玉给大家一个借鉴,欢迎大家在此基础上做出更多的扩展。

就装置尺寸上就有很多可以改进的余地,比如大家会发现本制作的体积要比Celluon公司产品大很多,这主要是因为我们采用的摄像头镜头的视角仍旧不够大,需要安装在较高的高度上。

另外就定位算法而言,其实也有不少可以优化的地方,比如其实我们未必需要那么高精度的激光定位数据,如果只是简单的作为键盘输入的话,可以降低测距精度,或者完全使用其他算法来实现按键识别的过程。

另外目前的设计需要在PC上进行所有的视觉运算,这样的好处是大幅降低了制作成本和难度,但缺点也很明显:无法在手机等设备上使用。当然现在的智能手机一般带有USB OTG功能,可以外接usb设备,同时OpenCV也可以在Android、iOS等移动设备上运行,因此这里的实现仍旧可以通过移植运行在这些移动设备上。不过如果将这些算法移植到嵌入式芯片,比如DSP或者ARM内,则可以完全摆脱对PC的依赖。键盘自身就可处理所有视觉计算并直接输出按键事件,也可以加装WIFI或者蓝牙,做成无线传输。这其实也并不是个遥不可及的改进,不过那样的话,我们的制作就足以与Celluon的产品竞争了

5. 参考文献


[1] RGB laser projector – 0,5W
http://www.edaboard.com/thread230353.html

[2] 自制低成本3D激光扫描测距仪(3D激光雷达),第一部分
http://www.csksoft.net/blog/post/lowcost_3d_laser_ranger_1.html

[3] short for k-dimensional tree
http://en.wikipedia.org/wiki/K-d_tree

[4] Multi-Touch Technologies, NUI Group Authors
http://nuicode.com/attachments/download/115/Multi-Touch_Technologies_v1.01.pdf

[5] Tacchi is SigMusic's 46" fully multitouch table.
http://www.acm.uiuc.edu/sigmusic/tacchi.html

[6] OpenCV (Open Source Computer Vision) is a library of programming functions for real time computer vision.
http://opencv.org/

[7] Camera Calibration Toolbox for Matlab
http://www.vision.caltech.edu/bouguetj/calib_doc/

[8] Digital Image Processing, Second Edition, ISBN: 0-201-18075-8

[9] libkdtree++ is an STL-like C++ template container implementation of k-dimensional space sorting, using a kd-tree
http://libkdtree.alioth.debian.org/

[10] SendInput Win32 API
http://msdn.microsoft.com/en-us/library/windows/desktop/ms646310(v=vs.85).aspx

Arduino-Lite, 轻量级且高性能的AVR固件库

一篇拖了2年多的文章...目前首发于我们RoboPeak团队网站,可以过去查看中英文版本以及最好的阅读体验。这里只是缩略版本。

RP Blog文章:

Arduino-Lite, RoboPeak使用的高效轻量级AVR库(1) -- 介绍篇

http://www.robopeak.net/blog/?p=42

Arduino-Lite, RoboPeak使用的高效轻量级AVR库(2) -- 使用篇

http://www.robopeak.net/blog/?p=70

Arduino-Lite开发参考文档

http://www.robopeak.net/blog/?p=107

我blog早先时候写的一篇相关文章:

说说我从去年9月份开始的AVR单片机学习和使用

http://www.csksoft.net/blog/post/255.html

Arduino-Lite是我们RoboPeak机器人团队开发使用的轻量级高性能AVR固件库,我们机器人小车的AVR系统中全部采用Arduino-Lite所开发。目前已经将他全部开源了,希望对各位有所帮助。

Google Code项目主页: http://code.google.com/p/arduino-lite/

简介

Arduino固件库提供了非常易于初学者使用的函数库,可以很快对AVR进行开发,即使是专业人员用这个库也会觉得便利。同时他对引脚的抽象编号也有利于固件代码的移植。不过这个库相对体积过大,性能也不理想,所以难以在产品领域使用。这就是我们开发Arduino-Lite的原因所在。

Arduino-Lite包括了2大部分

1) 改良和扩充的Arduino固件库,

提供大多数情况下大于50%的代码尺寸缩小和10倍以上的运行速度加速。同时支持更多的AVR芯片和时钟频率。使得这个库能可被用于对成本和性能敏感的产品应用中

2) 自我包含的编译开发环境

不需要安装任何第三方程序即可完成AVR的开发、下载工作。Arduino-Lite自带了avr-gcc(WINAVR),不过创建新工程非常简单,基本的文件夹复制操作即可自动产生新的工程,也不用编写和修改Makefile脚本,都是智能进行的

 

这里先给出2个直观的例子:

实现PWM输出功能:

代码类型 代码 执行所需的AVR时钟周期
Arduino analogWrite(9, pwm_val); ~80
Arduino-Lite ANALOG_WRITE(9, pwm_val); 2
Arduino-Lite PWM_SET(9, pwm_val); 1
avr-gcc直接寄存器操作 OCR01 = pwm_val; 1

可以看出,Arduino-Lite提供的函数和使用方式与Arduino非常接近,但效率上他有和直接去操作AVR寄存器性能一致。

编写一个PWM驱动LED闪烁程序的代码尺寸对比: 

  • Arduino库产生的最终代码: 2048 byte
  • Arduino-Lite产生的最终代码: 100byte

对于AVR ISP编程器的支持,Arduino-Lite支持Arduino方式的串口下载,也支持HIDboot的USB-HID下载,同时原生支持我们RoboPeak做的开源USB免驱动编程器RoboPeak USB Connector进行AVR芯片编程(见http://www.robopeak.net/blog/?p=133)。

特点和适合使用的场合

同样使用C++/C编写且基于avr-gcc编译器。但与Arduino固件库相比,Arduino-Lite有如下优势。

非常轻量级

使用Arduino-Lite的固件往往比使用Arduino固件库小了50%以上.

高效率

许多Arduino-Lite提供的与Arduino固件相同功能的函数,例如digitalWrite之Arduino-Lite版本:DIGITAL_WRITE仅使用一条AVR指令实现.

支持更多的AVR芯片和时钟频率

除了 Atmega8(A), Atmega168(PA), Atmega328(PA), Atmega1280 芯片外, Arduino-Lite 也支持以下芯片: Attiny2313, Attiny26, Atmega48(PA), Atmega88(PA)

对于时钟频率, Arduino-Lite 支持从1Mhz 至 20Mhz 的频率范围.

 

除此之外,Arduino-Lite还有如下特点:

 

自包含,无需依赖任何第三方工具/编译器/库

只要系统中带有文本编辑器,即可直接用Arduino-Lite进行AVR固件开发、编译、烧录等动作。Arduino-Lite自带了avr-gcc(WINAVR)以及相关的函数库。

灵活易与整合的编译环境,基于Make,但无需用户编写或是生成任何Makefile

创建一个新的Arduino-Lite工程,最简单的办法是将模板工程文件夹解压缩并重命名为希望的工程文件。并将相关的源代码以任何目录结构放置于工程目录下,Arduino-Lite就能编译项目,无需用户修改/编写/生成Makefile.

 

我们认为Arduino-Lite适用于以下领域

  1. 对固件代码尺寸/器件成本敏感的场合,比如需要使用Attiny或者Atmega48等小Rom尺寸的芯片的场合
  2. 对固件执行速度有较高要求的场合,比如对实时性要求较高的工控领域和机器人控制器领域
  3. 喜欢使用Make脚本、自定义IDE等的开发环境
  4. Arduino/AVR爱好者,且有一定的编程经验,不满足于Arduino IDE环境,期望更高效的固件库
  5. 希望将Arduino的简易开发特性运用于Attiny, Atmega48以及不同时钟条件下的硬件环境
相比Ardunio的固件库,Arduino-Lite或许不适合
  1. 不喜欢命令行界面、Make编译脚本的人群 (我们也有计划将Arduino-Lite支持Arduino-IDE)
  2. 希望直接使用Arduino各类第三方库,急需应用的情况

实现细节

省略,请参考RP Blog原文:http://www.robopeak.net/blog/?p=42

 

如何使用

 可以在Google code上下载已经打包好的zip包,或者直接checkout 源代码使用,具体见:http://www.robopeak.net/blog/?p=70

 

开发和配置过程都异常的简单,基本都是鼠标操作即可搞定,也无需以来别的软件、库。

 

下面有段使用RP USB Connector进行AVR烧录的视频:

(http://www.tudou.com/programs/view/Den9uh3HTHE/)

 

函数手册

 略文,详见:http://www.robopeak.net/blog/?p=107

Arduino-Lite新增函数/宏

基本IO引脚控制

PWM输出控制

模拟量采集(ADC)

睡眠和延迟

中断处理和管理

串口通讯

调试功能

文本格式化

太阳能供电无线气象站及信件检测器和AVR以太网终端的设计制作

自己的概括能力不强,只好用很长的标题来描述了。在写这篇文章前我已经有预感这篇不是一两句话能够说完的,所以现在深吸一口气,开始写...

需要提前交待的:

  1. 欢迎转载,但请保留原始的文章链接、作者信息等。
  2. 文章中提到的设计均会提供源代码、电路原理图。但是这个制作跨度比较大,也涉及到我的其它一些项目,所以对于一些依赖的代码、设计,这里将不给出。或许他们其中一些的资料我会在以后给出。
  3. 文章中的照片是在设计过程中无意或者有意拍摄的,其中一些部分已经出现改动。如果发现图片与文字描述不符的情况,以本文描述为主
  4. 本人水平有限,也不是电子专业的,错误难免。请大家多多包涵和指正

1. 介绍

简单的说,这里将要介绍我制作的一个系统,用它来做下面几件事情:

  1. 定期检测位于室外的邮政信箱,如果有信件,会用中文语音通知我和家人。
  2. 一个简单的室外气象站,以半小时的间隔不间断的采集室外的温度和湿度信息,供后期分析。

同时,这个系统也是:

  1. 对太阳能供电设备和低功耗系统制作的尝试
  2. 对2.4G无线网络通讯电路的设计
  3. 基于AVR单片机的低成本以太网终端的设计

这里先给出这个系统的实际效果视频,给大家一个直观的印象:

 

在视频中我将一份报纸投入家中的信箱,随后在家里的IHES(家庭智能化环境系统)终端便会用中文语音报告“有信件,请查收!”。同时,在必要的时候它也会播报出当前由这个系统所采集到的温度和湿度情况。整个过程并无人工干预。

接下来我将开始介绍这个系统的制作过程。下文首先为介绍系统整体构架和技术要求,然后会按照硬件->软件,每个模块的顺序介绍原理和实现过程。然后会给出一些数据作性能分析,最后将交待整个系统的成本。对文章涉及的相关背景资料,会在文末的参考资料给出。相关代码、原理图也会在文章末尾给出。

“这个”系统包括的是:放置在室外的传感器和控制电路,在室内的以太网终端系统,它负责通过无线信号与室外传感器通讯,并将结果转发到以太网上。至于视频中看到(听到)的语音以及IHES核心系统的介绍,本文将不作涉及。我或许会在今后写这方面的介绍文章,也可以参考在参考资料章节给出的我已经写过的文章[5]。

2. 系统的构架和性能要求

或许你会想,要检测信箱的信件,方法有非常多。最简单的办法就是在信箱入口装一个开关,然后拖一根线到室内和电脑连接。这样应该也能实现视频中的功能。至于温湿度检测,那也直接连线出去不就得了?

但实际上这样做也会存在些问题。首先要布线,考虑到家里已经装修过,走明线会影响美观。同时如果直接将室外的信号与室内计算机系统连接,危险很大。在打雷是很容易被雷击到,同时室内外电势差也不得不考虑。另外,要能够有效地检测出信箱中是否存在信件,也不是简单的装一个开关那么简单。首先信箱是现成的,不方便做很大的改动,同时这个开关的设计(机械开关或者是光电)也需要一定的考虑。

总之,我还是用了一种比较折腾,但是仍旧适合个人DIY的办法:

首先这个系统会通过无线信号把位于室外的传感器和室内的计算机系统隔离开。当然,这样一来,事情又复杂了一些:室外的传感器需要自我供电,并且需要设计专门的控制器来负责与室内的系统进行通讯。另外,因为这里采用的无线信号并非采用标准的蓝牙或者WIFI等可以被PC所识别的协议,因此需要另外设计硬件将我们定义的无线协议转换成PC可以识别的协议,这里需要有电路将无线信号转换成以太网的TCP数据包中转给PC。

当然,要实现这些还会有更多的细节需要考虑,这也是为何我写这篇文章的原因了。我们暂时先不考虑这些细节,下面给出分析道这里我这个系统的框图:

 

IHES室外传感器框图

这里按照各模块所在地理位置(室外/室内)进行划分。图中上半部分是位于室外的传感器和控制器。其中各传感器会将它采集到的原始模拟信号转化成数字信号发送给传感器控制器。而传感器控制器会将各传感器报告的数据汇总并作简单处理后,通过无线模块发送给室内的接受终端。因为采用无线通讯,所以再考虑用一个电缆来给室外传感器供电就多此一举了。那么自然这里的电能供给就是指用电池供电了。但电池用尽后需要更换,这又显得比较麻烦。所以我的设计中传感器采用了太阳能供电+锂电池蓄电的方式来给室外传感器供电。考虑到成本,这里的太阳能电池不能用很大功率的,自然,整个室外传感器电路就需要运行在比较低的功耗条件下了。并且,南方经常有连续几周的阴雨天气,在阴雨天气中太阳能电池几乎不输出功率,此时就需要在设计中将系统的功耗压缩到最小。这里对供电和功耗指标是,室外传感器需要能够在一次完全电池充电后,仅靠电池电力维持持续工作15天以上。另一个指标是,在晴天太阳能电池的输出功率可以将电池完全充满。

图中的无线模块采用工作在2.4GHz波段具有收发功能的芯片/模块。这类模块仅实现了物理层的通讯协议,需要为他们设计编写对应的链路通讯协议。当然这样也适合用单片机来操作。因为功耗的限制,这里的无线模块的通讯距离比较小。

上图的下半部分是位于室内的以太网/无线转发器。他的主要工作就是将由以太网发来的IHES中央服务器请求转发成无线信号与室外的传感器通讯,并将收到的无线数据包转发到以太网络反馈给中央服务器。另外,这个模块上也开设了一个Web服务器,允许我们直接用浏览器登陆该模块来与室外传感器通讯。

其实这个模块做的大部分工作也可以直接交给中央服务器(PC)来实现,这样仅需要实现一个诸如usb口->无线信号的适配器即可,但是考虑到这里选用的无线模块功率较低,在中央服务器处以经无法介绍到室外发来的信号。所以我在设计中将这部分单独设计为一个模块,将他放置于离室外传感器比较近的阳台上。这样的一个限制就是:这个模块不适合用一台单独的PC来实现(考虑到阳台的环境恶劣、占地以及成本因素)。这里我选择用成本低廉的单片机来实现这个模块的转发和webserver功能。这样的另外一个好处就是功耗很低,可以通过网线供电的方式来给模块提供电能,而不需要另外接电源适配器。这后文我也会介绍如何改造交换机网口来实现用五类双绞线(以太网用线)给设备供电。

室外传感器的数据最终将以TCP数据包的形式发送到中央服务器上作处理和分析,并通过语音合成以中文语音将检测结果反馈给我们。最后就是上面视频中的效果了。

在开始具体介绍原理前,给出2张图片让大家有个感性认识:-)

 

位于室外传感器的摆放。其中传感其控制单元隐藏在了灌木后,图上不可见

 

 室内(阳台上)的以太网/无线转发控制器。

3. 各模块的原理、设计和制作

(终于到核心部分了...)这里将按照各模块:信件检测->温湿度检测->太阳能电池组->室外传感器控制器->以太网/无线控制器->以太网供电的顺序进行介绍。

信件检测

要检测信箱是否有信件,可以从2个方面入手。一方面是检测当有新信件的情况:在邮递员投递信件的时候,信件会从信箱的入口经过。要检测这个过程可以在信箱入口安装一个光电传感器。信件经过入口时候,光电传感器光路会被阻隔,因此产生一个脉冲信号。通过检测这个脉冲信号就可以知道此时有邮递员送信了。

但是这并不符合现在的要求。对于邮件检测,这里的一个需求是:

因为家中不可能时刻都有人在,所以当有新信件的时候,除了能够在当时尽可能早的通知这个事件外,还需要在每天的特定时间,比如晚上吃饭和早晨起床的时候告诉家里目前信箱里是否还有信件,显然检测新投递了信件的办法是不行的(因为很难检测信件取走的事件)。

要检测出信箱当前是否有信件,办法也有不少,复杂的可以安装摄像头通过图像识别来处理,但这显然没有必要。简单的有在信箱底部安放一个重力传感器,但是有时候信件就是一张纸(广告传单?),用重力传感器似乎不是很好的办法。

这里我采取的是在信箱底部安装一系列红外传感器,组成一个整列。当有信件投入信箱,它就会覆盖在这个传感器阵列上方,此时着要有1个传感器被遮盖,就可以检测出有信件存在。为了方便理解,这里给出一张原理图:

 

基于红外传感器进行信件检测

上图中蓝色框就是装有红外传感器阵列的平板,当有信件投入信箱后,它会覆盖在这个平板上方。当然,也会像图中那样因为面积较大只有一端与传感器平板接触。

这里的红外传感器采用的是反射式对管,既一个红外发射LED和一个红外接收管(光敏二极管)。当有信件覆盖在一个传感器上方时,红外发射LED产生的红外光会通过信件反射并被接收管感应。而如果传感器上方没有信件(或者在很远的距离),则接收管几乎不能收到由发射管产生的红外光。当然这里的一个前提是信箱内壁的反光率很低。

另外,因为信件的大小不一,从很大的报纸到小尺寸的信封都有。如果传感器数目过少或者摆放过于集中,很有可能出现信件正好落在没有放置传感器的区域。所以这里我用了8个传感器,放置成前排5个,后排3个的形式。至于如何摆放有效,主要是看实际测试的效果,可以参考的是2个横向的传感器的之间的间距比一个较小的信封宽度略小。

这里描述的传感器阵列目前市面没有现成的(至少我不知道),我找到了一块装修多余的木板自行加工:

首先是将木板切割成合适的大小,然后打孔,并按装红外传感器(一个红外发射LED和一个红外接收管为一对),效果如下图所示。用热胶将他们固定。

 

下图是安装好后的效果。

 

传感器阵列做好了,但是这样还不能被使用。需要有驱动电路来控制它并给出方便控制器理解的信号。这里需要考虑的问题是现在我有8个红外发射管和8个红外接收管,如果分别驱动,就需要至少16组信号线以及2根供电线。因为信箱是个密封性很差的容器,不适合将控制器也放置在信箱当中。这样就要求控制器与这个红外传感器阵列的信号连接用尽可能少的线路来实现。

这里采用的策略是将所有的红外接收管并联,所有的接收管只用一路信号来采集。这样一个好处是可以提高接受灵敏度。而对于发射管,采用轮流点亮的方式驱动。如果在一个发射管被点亮的同时,接受管检测到信号,就认为这个发射管上方有信件遮挡。

红外线不能被人眼所见,但是却可以用各种感光器件如摄像机感受到,下面是制作过程中将发射LED轮流点亮过程降低1000倍拍摄的视频。方便大家理解:

 

红外传感器阵列控制电路

要实现轮流点亮8组LED,自然就想到用74595移位寄存器来实现。这样可以仅采用5组信号线(电源2路,1路串行信号,1路使能信号,1路时钟信号)。而对于红外接收管,再需要一路信号传输它感受到的光电流即可。这样一共消耗了6组信号线。另外,邮政信箱虽然密封性不强,但是防雨,这很适合放置湿度传感器。所以在我的设计中湿度传感器也被安装在邮件传感器一起。湿度传感器我采用了国产的DHT11[7]单总线数字式传感器。虽然精度不是很高,但是对我来说已经够用了。关键是它成本比较低廉,数字量输出也适合单片机使用。

将上面提到的74595和DHT11整合在一起,便成了这里的传感器控制电路,它的原理图如下所示:

 

可以点击这里下载电路图的pdf文档:http://www.csksoft.net/data/ihes_osensor/circuit/IR_matrix.pdf

原理图中可以看出这样一来正好使用了8路的信号线,我最终使用了五类双绞线来进行信号的传输。

 

上图为这个控制电路的成品照片。将控制电路与传感器阵列焊接在一起后,稍微对这个传感器做一下包装

 

 

 

 

 

下面给出一段在测试这个信件传感器时拍摄的视频,给大家一个直观印象:

视频中传感器已经连接到了控制单元上,我制作了一块临时电路用于与控制单元建立无线链接,将邮件传感器的信息发送回PC供调试。可以看出这个传感器可以很有效的检测出位于其上方一定距离内的信件。

温湿度检测

我采用DS18B20单总线温度传感器[9]以及DHT11温湿度传感器[7]来采集室外的温湿度情况。其实仅仅使用DHT11便可以采集这2个数据,但是相比较DS18B20的误差和分辨率都更高,同时在我的制作过程中DHT11是在后期新增加的,所以我仅适用了DHT11的湿度采集功能。

在前文中已经有所介绍DHT11是和信件传感器组装在一起的,目的是为了将它放置在一个相对空气流通的环境。而DS18b20处于方便,我将它焊接在室外传感器控制器的PCB上,并按装在一个密封的壳体当中,这将在后文介绍。

太阳能面板和锂电池

处于成本和其他方面考虑,这里我选用了功率较小的面板。虽然输出功率不大,但是因为其表面积小,适合安装。

太阳能面板是我在淘宝网购得的单片价格为¥10的多晶面板,其性能参数如下:

最大输出电压:6V

短路电流:75mA

工作电流:~50mA

我使用4片这样的面板加以拼装,并且利用废旧机器的部件作了一个支架。效果如下:

 

 

这四块面板采用并联的方式连接(需要在之间串联保护二极管如5819来防止因为部分电池板电压差异造成的倒流)。在上海近期(冬天)晴天阳光照射的条件下,测得这块面板的短路电流可到170mA以上。

而对于电池的选择,按照前文提到的功耗和待机要求,我使用了900mAh的聚合物锂电池。这样如果按照无日照情况系统可以工作15天来计算,那么整个室外传感器系统允许的平均功耗大概为2.5mA@3.8V。并且,如果算一天日照为8小时计算,那么对这块电池完全充电所需的平均充电电流在112mA。差不多可以满足我的要求。

严格的说,要对锂电池充电需要有专门的充电电路来提供先恒流后恒压的充电方式。但是这里为了考虑成本以及考虑太阳能电池输出功率比较低,我就偷了下懒。一般锂电池自带的保护电路都会提供过充过压保护。所以在后文可以看到太阳能面板的输出在经过几个二极管降压后直接对锂电池进行充电。而在后文的验证数据中也可以看到锂电池保护电路对过充的保护。当然这是不标准的办法,不推荐使用。

无线模块

我选用了基于em198810芯片[6]的2.4G无线收发模块,市面上可以买到的模块型号如Tr24B/Tr24A以及其他类似模块。做出这样的选择没有什么特殊原因,主要是手头有现成的模块在。该模块内建64byte FIFO收发缓冲区,并且带有CRC校验。我在实际测试中该芯片在最大输出功率下室内可以穿透2-3层墙壁,并维持10-20米的通讯距离。按照手册的介绍,其工作在收发模式下的功耗为25mA@3.6V。相比而言比他优秀的其他模块也不少,同时如果情况允许可以选用314Mhz等较低频率的收发模块,可以有效提高通讯距离。

传感器控制单元

 该控制单元采用Atmega48PA单片机进行中央控制,负责如下功能

1. 监听来自室内无线终端的通讯请求,并读取信件传感器、温湿度传感器得到数据并通过无线发送回室内终端

2. 锂电池电压监控

3. 无线模块的驱动

4. 日光检测

 

其原理图如下:

 

可以点击这里下载电路图的pdf文档:http://www.csksoft.net/data/ihes_osensor/circuit/checker.pdf

PA后缀的AVR芯片相对它的之前版本(Atmega48V等)功耗更加低[10],适合这里的应用要求。原理图比较简单,基本就是对几个传感器的驱动,以及采用电阻分压后的锂电池电压检测。

对锂电池电压检测主要是为了我在调试过程中观察系统实际的工作性能。并且今后可以针对当前电池剩余电量适当调整无线通讯的频繁程度从而提高系统待机时间。锂电池的工作范围一般在2.7V-4.2V之间,这里我采用3:1电阻分压,将缩小4倍的电压信号(落入0-1.1V范围)接入AVR芯片的一个ADC引脚。通过用AVR内部的1.1V参考电压比较就可求得实际的电池电压。我最后固件中可以提供0.01V的电压分辨率。

图上的Solar Panel Probe是用于检测当前是否有光照的,不过目前我固件当中没有使用到。可以用它来做一些行为的调节,比如在白天有日照的时候允许开启更多的传感器。

这里的VCC实际就是锂电池的当前电压了,目前设计上只要锂电池维持在3-4.2V就可以保证系统的正常工作。由于无线模块需要工作在3.6V以下,所以这里串联了一个1N4148提供0.6V的压降,使得即使工作在4.2V时,无线模块也可以安全的工作。

该控制单元工作在1Mhz频率,使用内部RC振荡器。经过后期的固件优化,整个室外传感器电路工作功耗在1.1mA@3.8V。这样即使没有日照,室外传感器也可以工作近一个月时间!

下面是按照原理图实现的PCB:

 

 

 上面2幅图是分别在不同制作阶段拍摄的,所以可以看到他们的接口有区别。第二幅图上已经安装了无线模块。

室外传感器的组装

这里为了理解方便先介绍我将上述传感器各模块组装并安放在视频中方位的过程。至于固件的编写会在后文介绍。

前文已经提到,传感器的控制单元不适合与信件传感器一同放置在信箱内,除了前面提到的密封问题外,还有个因素是我家的信箱是铁质的,会对无线信号造成屏蔽。所以上图中的控制单元需要另外防止。为了保护电路不受风吹雨淋破坏,我使用了如下的密封机壳:

 

这类机壳可以从淘宝网购得,价格一般在¥15元左右,其中含有密封橡胶,防护性应该不错。

 

为了将PCB固定在壳体内,我用热胶固定出PCB的铜座。

 

因为传感器控制单元、太阳能电池组以及信件传感器分散的比较远,这里我用以太网双绞线作为信号线,一根用于信件传感器和湿度传感器信号,另一根用于太阳能供电传输。用热胶保护好接口引线。

 

随后在壳体上打孔,将这2根引线穿进壳内,并用热胶以及AB胶密封住缝隙。

 

最后,便是把控制单元PCB、锂电池等装入壳内并固定。

 

盖上盖子的效果

 

接下来把信件传感器放进信箱底部。

 

将太阳能电池组放在一个比较高的平台上,并用尼龙绳固定住。

 

把机壳放置在灌木丛中的隐蔽位置。目的之一是为了防止阳光直射。

 

 最后,如上图所示,把这些部件用双绞线连接。上图是还没有把连线隐藏的画面方便大家理解。到此室外传感器部分就制作完成了。

 

室内以太网/无线转发器

之前已经提过因为无线模块的通讯距离有限,且中央服务器不能直接与传感器无线信号建立通讯,所以需要设计一个模块负责将来自中央服务器发来的TCP数据请求打包成无线数据包发送至室外传感器,并且将接受的结果用TCP包送回中央服务器。

这里我使用Atmega8L作为该转发器的中央处理器,并使用ENC28J60 SPI接口的以太网适配芯片[8]实现AVR芯片接入以太网的工作。其原理图如下:

 

可以点击这里下载电路图的pdf文档:http://www.csksoft.net/data/ihes_osensor/circuit/etherbridge.pdf

该模块的AVR单片机工作在8Mhz的频率,电路也很简单,单片机负责将来自以太网的请求转发到无线网络。在后文的固件介绍部分会涉及它的工作策略。目前该模块实现了TCP数据请求的转发任务同时也内建了web服务器供人工登陆至该模块进行室外传感器操作和状态查询。

对于AVR与ENC28J60的配合使用,可以参考tuxgraphics.org的名为:An AVR microcontroller based Ethernet device地文章[2]。他的地址将在后文参考资料处给出。

整个模块工作在3.3V电压下,因为整个模块功耗很低,所以这里采用从以太网取电来给模块供电。在后文将介绍,以太网将提供13V左右的直流电能,为了有效利用,这里采用了DC-DC开关电源模块将12V电压转化成5V左右的电压再通过三端稳压器稳定在3.3V。该模块PCB如下:

 

这里我使用了现成的DC-DC降压开关电源模块以及ENC28J60以太网模块,他们都可以在网上购买到。

 

图上方为12->5V DC-DC开关电源模块

 

将以太网模块放置于PCB的下方

 

PCB的背面

 

实现以太网供电

以太网供电其实有专门的设备,也有专门的名词,叫做(POE)Power Over Ethernet。但是这类设备一般很贵,这里没有必要为此去购买他们。我用了一种不规范的办法来实现供电。因为对于10M/100M的以太网,8根双绞线只用到了4根传输信号,其余4根是空余的。可以用来传输电能。

当然这样做也有一定危险,比如误将其他设备介入这根以太网线,那么就会造成设备烧毁。所以如果要用这个办法需要平时多加小心。

那么现在的问题是用来传送的电能又谁来供给?我的办法是将这根以太网线连接的交换机引出自身的供电:

 

上图是我家用的某品牌交换机的PCB。我将它的电源部分引出(图上红蓝线)。测得他的电源电压在13.5V左右。

 

 然后观察用于连接室内收发传感器以太网口的对应网口,可以发现另外4根双绞信号线焊盘没有被使用(实际上都会把这些不占用的信号线用电阻拉低接地,所以自制时要注意将他们断开,防止短路)。将刚才引出的电源线焊接在上面。这样凡是连接在这个网口的以太网线便有了电源供应。

将带有电源供应的以太网线连接到转发器网口上,这里还需要把电源线单独引出,并接到DC-DC模块上,如下图所示。

 

下图展示了通过以太网给模块供电的情况:

 

模块的安装

这里同样先介绍最后的组装部分,固件的编写将在后文介绍。这个模块同样也用密封塑料机壳加以保护。下面首先固定住PCB的铜座。

 

然后将所有部件装入壳体。

 

完成后把它放置在阳台的一个角落。

 

 

好了,现在所有电路制作的介绍已经完成,下面我来介绍他们的固件开发和运行策略。

3.软件/固件设计

这里仅交待相关模块的固件的设计思路以及高层次的逻辑分析。至于具体的细节,诸如依赖的代码库,这里不作过多介绍。请参考文后的相关资料。

系统协作模式

先从比较高的层次来介绍整个系统的运作。整个系统中,室外传感器和室内无线/以太网转发器永远都是被动工作的。也就是说,如果没有外界的请求,它们永远只会工作在监听状态,不会主动去与外界联系。这样设计的好处是简化流程,同时也可以降低功耗。

整个系统中IHES中央服务器将占有主导权,在服务器中运行着一个守护进程,它负责每隔一定的时间通过以太网向室内转发器发出请求,室内转发器在接受到了请求后,进而向室外传感器发送无线请求。最后把收到的数据发送回IHES主服务器。

但是,因为无线通讯的不确定性,在设计当中中央服务器不适合在向室内转发器发出请求后等待着对方回应。这里中央服务器在发出请求并确认转发器收到请求后就切换到其他任务继续。在一定时间后,中央服务器会再次发出请求,要求转发器把上次请求到的数据发送回来。

同样的通讯模式也应用在转发器中。转发器会在收到IHES服务器数据请求后开始与室外传感器联络,但它不会一直等待对方将传感器数据传回,同样它也会切换到其他任务(处理Web请求?)并在一定时间后再次联系室外传感器,将采集到的数据收回并存储在自身内存中,等待IHES服务器来取回。

这样的通讯模式含有下面2个原语:

  1. 预取命令 (Prefetch Command)
  2. 取回命令 (Retrieve Command)

下图展示出了整个系统的协作模式:

 

如图所示,Sensor Checking Daemon是运行在IHES中央服务器上的一个“意识进程”,他会每半小时向以太网上的EtherBridge Agent(也就是室内转发器)发送一个IHES TCP请求包,但此时转发器不会开始无线通讯,而只是向服务器回应,告诉该请求已经收到,并告诉服务器数据将在多少时间后可以获取。中央服务器在收到回应后就转而调度其他IHES“意识进程”。

EtherBridge Agent在收到预取指令之后,就会将自身状态机转换到通讯模式,开始联络室外传感器(Outside Sensor)。但同样它也只是先发送预取指令,因为室外传感器可能会用比较长的时间(>5ms)来收集传感器数据。这里涉及到一个无线通讯的策略问题,所以适合用预取指令来实现。同样的,室外传感器在收到预取指令后,也仅仅是回应自身已经收到了请求。在室内转发其收到传感器回应之后,它会切换工作模式到其他任务,比如监听并处理新的TCP或者HTTP请求。室外传感器在回应预取命令后,开始真正的传感器数据收集,并且将数据缓存在自身内存中。在一定时间后,转发器又开始联系室外传感器,此时室外传感器就会将真正的数据传回。受到传回数据后同样转发器也将这份数据缓存在自身内存。等待IHES主服务器回收。

采用预取模式的原因:

对于无线部分的通讯,丢包现象比较普遍。目前的处理是在发生丢包时,命令发送者会再次发送重试,直到受到了对方的正确回应(通过CRC交验)。基于这样的设计,如果室外传感器在收到请求后就进行传感器数据读取并将结果送回,很有可能会因为这个过程中的丢包使得转发器再次不断的请求。这样一次数据请求就会造成室外传感器多次去收集数据,而这样会消耗较大电能(各传感器仅在收集数据时开启)同时数据收集比较耗时,需要大约5ms的时间,也就是说这样的模式,转发器也需要等待5ms重新发送数据包,可能会导致总的通讯时间延长。

对于以太网部分通讯,虽然丢包现象相对很低,但是会出现同时多个请求者的情况,比如IHES服务器和从web服务器登陆的用户都希望访问室外传感器。这里要处理的问题就是降低一次数据通讯的延迟。同时因为AVR单片机相对简单,不能很有效的处理多任务协调,如果在一次新的以太网数据请求过程中又发生新的请求,就难以进行数据的一致性维护。

好了,写的我自己都晕了,来看段视频来描述这个过程吧:

这段视频是IHES服务器进行开机自检中的片段。它会将与室外传感器的数据请求全过程用语音朗读出来。从中可以看(听)出这个系统的协作过程。

好了,有了总体的框架后,下面介绍室外传感器和室内转发器的固件实现。

室外传感器固件设计

前面已经介绍了该部分的电路以及协作关系,这部分的固件主要实现如下功能:

  1. 驱动各类传感器,读取传感器发来的数字信号
  2. 读取当前电池电压
  3. 无线模块驱动程序
  4. 监听无线网络请求
  5. 自身运行状态控制和节能控制

前2部分不需要做很多解释,当前电池电压会采用如下格式保存:

[15:8] Voltage integer : 2^7+2^6+...+2^0
[7:0]  Voltage decimal : 2^-1+2^-2+...+2^-7

他可以提供0.01V的电压分辨率。

无线模块驱动我参考了Alexander Yerezeyev编写的驱动代码[1],可以在后文参考资料出找到他的链接。这里主要对其驱动逻辑进行时序上的优化,同时增加了自动丢包重发功能。

信件传感器的信号采集:

之前已经介绍了信件传感器的工作原理,这里介绍一些细节问题。如果有信件遮挡一个传感器,则它会受到来自红外发射LED的反射光,在固件看来,这个反射光就是一个电压信号,且电压约低,反射光越强。

固件会驱动74595轮流点亮红外发射管,随后读取此时接收管上的电平信号:

ir_sensor_voltage = analogRead(IR_SENSOR);

这是一个0-1023范围的数值,如果某一次接收管的电平比较低时,则可以怀疑他对应的发射管上方有信件遮挡。所以还需要在之前读取一次参考电平,即在电亮任何发射管之前接收管的电平数值,因为此时没有任何红外光产生,所以这个电平信号可以认为是没有信件时接收管的输出信号(实际上并非如此,后文会有介绍)。

在我编写固件时,进行了一些测试,最后判断当前接收管电平和参考电平之差如果小于阈值:IR_DETECTION_THRESHOLD (0x65),则可以认为有信件存在。

但是实际情况比预想的存在偏差,因为阳光与随着信箱的入口照入箱内,日光会对接收管造成干扰。表现在接收管会将日光认为是反射信号。这样会使得参考电平也非常小,从而无法检测当前是否有信件。解决这个的办法是比较参考电平,如果它自身也小于一个阈值时,就认为本次的检测结果无效。下面给出实际的测试数据来说明这个问题:

2010-02-20      07:08   00.7    94       4.09   0666
2010-02-20      07:38   00.9    95       4.12   0071

上面2组数据是目前传感器在这些时间点采集到的信息。注意最后一列数值,他表示的就是红外接收管的参考电平,可以很明显的看到,7:08时阳光还比较柔和,参考电平比较高,但到了太阳升起后,参考电平迅速降低为71。这样的条件是无法测出信件是否存在的。

解决办法其实也很简单,就是在信箱入口安装遮阳布,不过我没有这么做。另外,如果信箱内有信件,它同样也会遮挡阳光入射,从而提高参考电平,这样又可以有效地检测出信件的存在了。

因为参考电平的波动性,我打算将参考电平有效性的比较交给IHES中央服务器来处理,这样可以避免频繁的修改室外传感器固件。所以,对信件的检测,将返回如下结果:

触发的红外传感器编号,如果没有传感器被触发,为0

参考电平

相信大家已经听到了之前的自检语音中播报参考电平的情况了。

运行状态和节能处理

降低功耗对室外传感器非常关键,固件可以做的其实很简单,就是尽可能的降低每个设备的功耗。对于各类传感器,仅在需要采集数据时将他们唤醒或者接通电源。对于无线模块,25mA的电流消耗绝对是这个模块中最耗电的。所幸可以将RF芯片设置为睡眠模式,这样可以将功耗设置为微安电流级别。对于AVR芯片自身,则尽可能在空闲的时候将其设置为睡眠或者待机模式。

这里面临的问题是无线模块在睡眠模式下无法接收信号。所以需要定期将它唤醒,监听一段时间后再次进入睡眠。但是,也需要在接收到数据预取指令后,保证无线模块工作一定的时间,方便室内终端发送取回指令后能够立刻做出响应。

所以,最终室外传感器会使用如下状态机进行工作:

 

分类两大状态,空闲和激活状态。在大部分情况下,室外传感器都将工作在空闲情况下,此时所有传感器都被关闭或者设置在睡眠模式,无线模块会定期唤醒进行监听,如果在监听过程中受到了来自室内终端的情况,则会切换到激活模式。否则无线模块会再次进入睡眠模式。这个模式下AVR芯片只有在维护状态变换的情况下才工作,平时都工作在睡眠模式(关闭时钟信号)。

空闲模式下室外传感器功耗很低,仅为1.1mA@3.8V的水平。

这里无线模块每2.9秒将被唤醒并设为监听模式,在工作约20毫秒后又进入睡眠模式。我们可以利用这个数据计算无线模块的平均功耗= 20/2900*25mA =~ 172uA。这下由无线模块产生的功耗负担就没有了。

如果按照1.1mA的平均功耗计算,那么900mAh的锂电池可以供整个传感器模块在没有日照情况下工作:34天。

当然如此短暂的监听时间会延长通讯的时常,所以在空闲模式下只要收到了预取指令,固件就会切换到激活模式下,已进入这个模式固件便开始收集传感器数据。而同时无线模块将持续监听请求。当然,激活模式有一个有效时常,当过了一定时间,固件又会回到空闲模式下。这个时长目前为1秒。

对于处理无线请求,在引入了状态管理后就非常简单。对于预取指令,固件要做的就是将Idle State切换到Active State即可。而对于取回指令,只需要简单的发送回目前内存中缓存的上一次去读结果即可。

室内无线收发器固件设计

收发器的以太网操作部分由tuxgraphics.org中提供的参考例程修改而来。该模块会同时监听80端口(Http服务)和8003(IHES agent service)。前者用于人工登陆至该模块操作,后者用于IHES中央服务器通讯。

该固件采用如下状态机工作:

 

 其中黑色箭头是实际的状态转换关系,而蓝色的是逻辑上的状态转换关系。前面已经指出,无线通讯可能需要最多3秒时间与室外传感器取得联系,为了保证这个等待时间内收发器可以及时相应来自网络的请求。所以这里将无线通讯拆分成几个子状态,同时必要时可以将CPU时间让出来处理以太网请求。

下面给出分别通过Web服务和IHES Agent协议于该模块驱动联系的效果图:

下图为用浏览器登陆至收发器后的画面。

 

下图是Linux下我编写的用于调试IHES协议的客户端得到数据画面。

 

 

 软件部分就介绍到此,相关的源代码在文章后续部分提供。

实测数据分析与讨论

实际功耗分析

写到这里,该系统已经持续工作了2天半时间,虽然这2天上海都是晴好天气,不过也不妨来看看实际的功耗情况。当然,依据就是采集到的数据了:

 

 上面的曲线记录了前天(2/19)到昨天晚上8点锂电池的工作情况,可以得到的结论有

  1. 夜间电池电压会因消耗降低0.03-0.06V左右
  2. 锂电池保护电路的过充模块发挥作用,电压在4.27V后就不再继续上升。

当然因为电池原先就处于充满状态,所以目前还不能判断出白天的阳光是否能够将电池完全充满。不过看着图中上升的曲线还是很有把握的。

 

温湿度曲线图

这2张曲线图就作分析了。

 

 

制作成本和用时

制作整个系统的成本还是很低廉的,下面列出一些器件的价格,大家可以自行估算

 

无线模块 ¥9-15 x2

ENC18J60 ¥50 x1

Atmega48Pa ¥8

Atmega8L ¥8.5

太阳能电池板 ¥10 x 4

锂电池和保护板 ¥20

壳体,小  ¥15

壳体,大 ¥20

DS18B20 ¥4.5

DHT11  ¥9

其他的一些小器件就不列出了,可以看出成本是不会超过150的。

 

对于花费的时间,这个项目在1-2年前就开始酝酿中。就单单算制作这篇文章介绍的部分,大致花费我4个周六和春节2天时间。也就差不多一周左右。当然,其中涉及的IHES中央服务器和构架、固件依赖的ArduinoLite库,RP infrastructure等这些的编写,如果把它们花费的时间算入,可能用来3-4个月左右。应该说还算是个短期的项目。

 

下一步工作和改进

目前这个系统虽然已经开始工作,但是还需要进一步验证,除了功耗以外,是否防水也是一个需要关注的问题。

对于改进,从前面分析上已经可以看出锂电池的过充是一个问题,解决办法可以通过单片机监控锂电池电压,当达到过充标准后就进入放电模式来抵消电压上升。

另外,无线模块也是可以改进的地方。可以选用314Mhz的模块,这样可以提高传输距离。

对于功能上来说,下一步可以考虑安装气压、风向传感器。那样差不多真的能叫做气象站了(山寨的)

相关源代码和仿制信息

 这里将提供如下代码:

  1. 室外传感控制器固件
  2. 室内无线/以太网收发器固件

不过需要说明的是因为这些固件依赖了许多额外的库和框架,那些框架属于我正在进行的其他项目,且还没有到发布阶段,所以暂时不提供,请谅解

固件采用avr-gcc4.2(WinAVR-20090313)编写。其中采用了Arduino-Lite AVR运行库。Arduino-Lite是我针对arduino库进行优化改良后的增强库,他提供类似的接口,但代码比原先可以缩减近一倍,执行效率提高10倍左右。Arduino-Lite我会在不久发布。也可以参考下面的参考资料中我之前写过的Arduino-Lite预告文章。

对于依赖的Tr24无线模块的驱动,可以去采用Alexander Yerezeyev编写的代码(见参考资料),也可以选用你喜欢的无线模块。

另外,这里也给出万用PCB的layout信息:

 

 

 

参考资料

[1] 基于AVR的TR24a驱动例程:spiriton-tr24a-demo, by Alexander Yerezeyev

http://code.google.com/p/spiriton-tr24a-demo/

[2] An AVR microcontroller based Ethernet device, by Guido Socher

http://www.tuxgraphics.org/electronics/200606/article06061.shtml

[3] Arduino-Lite Google Code repos, By 我(Shikai Chen)

http://code.google.com/p/arduino-lite/

[4] Arduino-Lite简介, By 我

http://www.csksoft.net/blog/post/255.html

[5] IHES中的语音合成介绍, By 我

http://www.csksoft.net/blog/post/linux_weatherforecast.html

[8] ENC28J60 Stand-alone Ethernet Controller with SPI

http://ww1.microchip.com/downloads/en/DeviceDoc/39662a.pdf

[9] DS18B20 Programmable Resolution 1-Wire Digital Thermometerler

http://datasheets.maxim-ic.com/en/ds/DS18B20.pdf

 

 终于写完了,没想到花了2天时间来写这个文章。也挺折腾得。我并不是闲得没事做,还是那句话,有时候就是要逼着自己做点事情,否则可能什么都做不了。好了,很感谢各位看到这里,尤其感谢坚持全部看完的:-)

 

截获网页客户端的IPCAM视频用于OpenCV等程序

请点击标题进入正文下载相关附件

好久没有更新自己Blog了。长时间不更新这次又是一篇讲技术的,下次要平衡下了。

这个标题应该没有表达清楚,其实也蛮难靠标题说清的。

情况是这样的,最近从网上订购了如图所示的无线网络摄像机,为了避开广告嫌疑,我不给出任何具体介绍链接了。

 

他的功能还不错,可以将拍摄的画面通过Wifi无线或者100Mbps以太网发送出来供远程观看,同时自带了近360度自由度的X-Y轴云台供远程操作移动画面。不过在使用的时候发现了个麻烦:他只提供Web客户端进行视频浏览和控制。无法像传统摄像头那样通过VFW或者DirectShow捕捉它的视频信号,也没有提供专业网络摄像机提供的mms/rtsp视频协议供我自己写的程序或者一些视觉计算库如OpenCV使用。同时对云台的操作也只能在浏览器中进行,没有直接提供供使用者调用的编程接口。

 

因为这类摄像机基本都是小作坊工厂出产的,我在包装上连他的厂家都没找到是谁,所以问厂家索要开发包无望,且网上也没有解决办法,所以就花了一天时间实现了我要的需求。特此将思路和代码给出,希望对有同样需求的人有所帮助。要注意的是似乎生产这类Ipcam的厂家有很多,他们虽然生产出外形和功能都类似的产品,但内部的机制多少有些不同。所以这里给的方法可能不能完全适用于你的机器,但思路应该都一样。

 

为了给个直观印象方便我说明问题,这里给个这类IpCam的Web客户端的样子:

如图所示,对这类摄像机的一切操作/访问都是通过浏览器访问位于摄像机上的web服务器实现的。在具体介绍实现我的需求前,我先介绍这类摄像机的技术细节。

 

该类Ipcam的硬件和客户端

当收到这个摄像机不久我就把它拆了一遍,因此对他的硬件有了个大致的了解。这类摄像机一般采用ARM的解决方案,里边烧录了Linux OS作为固件。而Wifi很出我意料的直接使用了块usb接口的802.11b/g网卡,当然usb接口是直接用电线连接的。看来将来即使自己修改个Linux内核烧录进去也很容易搞定,同时我也怀疑他的摄像头也是usb接口连到Arm主板上的... 至于云台电机,他采用了国内某厂的一套小功率步进电机驱动。

硬件就是如此了,再说说他的通讯协议。通过sniffer他的数据包分析,我发现他所有的通讯都是基于HTTP协议的,包括视频信号和云台控制信号的传输。原先以为视频信号可能会用某些传统的协议,看来这条捷径走不通了。虽然基于HTTP传输视频信号,但也只是利用HTTP做了层wrapper,我没有时间去分析它本身的传输协议。所以也不打算通过逆向他的传输格式来直接编写客户端。

然后再分析它的Web客户端,也就上上图的那个画面中的网页。从前面协议分析已经看出,上图中的视频画面不太可能是用Mediaplayer或者FlashPlayer这类插件实现的,而是厂家自己编写的一个ActiveX控件(也就是说只能用于windows了)。同时又发现了他网页客户端中所有的操作,如云台的控制,也是直接调用这个ActiveX控件所提供的方法来实现的。

换句话说,对这个摄像机的所有访问和操作,厂家都已经完全封装在所提供的ActiveX控件当中了。那样其实也好办,我们可以尝试在自己的程序中调用该控件,这样云台的控制就解决了,或许该控件也提供了某个接口供我们直接获得视频数据,如getCurrentFrame()?

 

利用厂家自带的ActiveX控件实现摄像机云台控制和画面获取

通过分析该客户端网页代码,得知他使用的控件文件名为:DVM_IPCam2.ocx

接下来就是获得这个控件所提供的接口和调用方法了,下面是用Visual Studio的对象浏览器列出他提供的接口:

图上可以看出,所有对该摄像机的操作都已经集成在该控件当中了,当遗憾的是我除了PlayVideo(void)外并没有找到任何其他和获取视频数据有关的接口。看来要达到我们获得每一帧视频信息的目标还有点距离。但无论如何,先尝试将这个控件加入我们的程序调用再说。

从之前网页代码的分析看出,这个控件的使用并非容易,首先需要调用接口完成登陆操作。但好在Javescript都是被迫开源的...对于这个控件接口的使用完全都有代码可以参考。我第一步先将他的网页客户端进行裁减,做了个仅仅包含登陆至该摄像机并显示画面的简易版本,如下图所示,代码在文末给出,供大家参考。

接下来就是将同样的代码用C++来实现了,这个没有什么好说的,我用WTL调用这个控件实现了将画面放置于自己程序当中,同时可以调用控件的接口来控制云台,程序的代码在文末提供,下面是效果图:

简单地写了下控制界面,图上那个白色方块里边9个橙色方块可以控制云台的移动。

到此控件算是已经可以被调用了,但最重要是如何获得我们需要的视频的每帧图像数据。下面将具体介绍:

 

首先厂商提供的控件并没有提供能够获得视频数据的接口,因此我们无法简单的通过调用函数来解决这个问题。不过分析了该控件(DVM_IPCam2.ocx)的导入表后,发现他调用了VFW的DibDrawDib函数,因此猜测该控件是通过VFW将画面绘制到屏幕上的。DibDrawDib的定义如下:

The DrawDibDraw function draws a DIB to the screen.

BOOL DrawDibDraw(
  HDRAWDIB hdd,             
  HDC hdc,                  
  int xDst,                 
  int yDst,                 
  int dxDst,                
  int dyDst,                
  LPBITMAPINFOHEADER lpbi,  
  LPVOID lpBits,            
  int xSrc,                 
  int ySrc,                 
  int dxSrc,                
  int dySrc,                
  UINT wFlags               
);

对于视频的每一帧画面,该控件应该都会调用DrawDibDraw函数将图像传送至屏幕,而该函数的参数则直接包含了每一帧图像的尺寸、RGB数据。那么,如果我们能截获对此函数的调用,并从中获取数据就完全可以达到我们的需求了。

要实现导入函数的截获很方便,直接修改对应模块的IAT表即可,具体原理在Windows核心编程中介绍。再上面介绍的自制客户端程序中,每一帧的视频画面都会出发我们自己写的myDrawDibDraw_func函数,该函数拥有和DrawDibDraw一样的参数,可以通过参数来获得当前收到的这一桢画面,我们可以在此处添加视觉处理的代码。下面是一个用这个办法实现的将ipcam视频画面进行边缘查找处理的效果:

 

 

好了。在文末给出这里所涉及到的代码:

飞信(Fetion)的Perl命令行版本

请点击标题进入正文下载附加脚本

飞信我就不多介绍了,随着网上出现了libFetion后,各类第三方飞信客户端程序就纷纷涌现,对于飞信的协议分析资料也不少。不过一直比较遗憾的是用于命令行的客户端版本不多,目前只有基于PHP的实现,同时libFetion似乎不开源,无法基于它作开发。

其实纯命令行的实现有很多优势,比如可以写一个自动天气预报脚本,每天定时运行,采集到天气信息后就可以用命令行的飞信程序发送到手机了。

我原本打算用bash script实现一个飞信客户端,不过考虑到perl几乎每个linux发行版本都自带,且处理文本更加方便,所以这个周末就参照着网上一些PHP版的飞信实现,写了一个perl版本的飞信客户端脚本。同时因为之前从没有写过perl脚本也完全不会,所以也称趁这个机会速成一把。所谓实践出真知,写了这个脚本觉得自己的perl水平应该能算中等水平:-P

因为我写这个脚本主要就是为了发送短信用,所以功能很单纯,就是用飞信给一个手机号码发送指定的文本短信息。至于例如查询好友、加好友等复杂功能我没有考虑,各位可以发挥各自想象力,在以后的脚本基础上添加即可。本程序采用GPL作为许可证。

不明真相的截图

脚本的使用和要求:

按照飞信的协议要求,脚本中需要通过HTTP协议也要通过TCP socket发送SIPC文报。后者perl提供了POSIX一致的socket规范,前者我采用了curl命令,所以请确保自身系统中安装了合适的curl。对于win32,我没有做过测试,不过相信只要有相应的win32版程序,还是可以工作的。

如果要发送中文短信,请确保自己的shell环境是采用UTF8编码的。否则请用iconv自己转换。

要发送短信,请使用下面的命令:

perlfetion.pl <绑定飞信的手机号码> <飞信密码> <发送目标手机号码> <发送的文本>

这里要注意的是参数中所有手机号码都是应当绑定了飞信帐号的,且已经和发送手机建立了飞信好友关系。否则将发送失败。

如果发送成功,将有如下回应:

$ ./perlfetion.pl 138***** ***** 138***** "Hello World!"
Retrieving the config xml...
Retrieving the SSIAPP URL...
Retrieving the SIPC Address...
Trying to get the fetion number of the current account...
Connecting to the SIPC via TCP socket...
Login OK
Send SMS succeed

为了各位调试或者了解协议详情,本脚本支持dump模式,请修改脚本,在执行前调用函数:fetion_verbose();脚本会将所有通讯文报打印至stdout。

 

Linux下的语音天气预报脚本

请点击文章标题进入正文下载代码

这是最近我刚在家里部署的一个程序,主要功能就是在每天的特定时间(7:30 以及 18:30 )计算机将自动用中文人声朗读出当天以及后一天的天气情况/预报。天气预报的信息由程序采集自中国气象网(weather.com.cn)。如果你对此感兴趣,不妨了解下这篇文章。

其实类似的应用网上应该也不少,我完成这个应用其实大部分精力也并不是花在天气预报的采集上。而是解决如何将Windows的TTS引擎(MS SAPI)“移植”到Linux上,以及编写相关的服务框架程序。同时,这个应用实际上只是我之前提过的智能化家庭系统(IHES)的一个小应用,所以这次也会连带着发布ihes framework的一些代码(比较简陋,看过算过,哈哈)。在给出具体的程序代码之前,我将一步步介绍其中的一些细节问题,以便有兴趣的人自己实现类似的功能。

1.天气预报的抓取

前文已经提到,我使用的是weather.com.cn的数据。实际上这部分工作是通过一个bash script来完成的。从该网站抓取并提取我们需要的天气信息的一个难点是如何从内容繁多的html代码中有效的筛选我们要的内容。这里我偷了下懒,通过观察他们网站,我发现其wap版本网站内容简单,特别适合抓取:http://wap.weather.com.cn/wap/

基本除了我们需要的天气情况外,只有写简单的关于信息,而且主要都使用了文字。这里以我家所在地:上海松江为例,分析信息提取过程:

该网站wap版不同城市以及预报范围的组织很简单,仅仅通过url地址来标示,比如上海松江24小时(当天)的预报信息页面地址为:http://wap.weather.com.cn/wap/58462/h24/ 而对应的48小时天气预报(第二天)地址为:http://wap.weather.com.cn/wap/58462/h48/ 。很明显,前面的58462应当为地区的区域代码,而预报范围就是后面的h{24|48|...}。因此,实现当天以及第二天的预报提取,只要抓取这2个地址即可。

采用curl工具抓取网页

对于还不熟悉linux环境的朋友,可以在shell里面输入man curl来参看该命令相关信息。直接通过curl即可将对应html输出到stdout。

$curl http://wap.weather.com.cn/wap/58462/h24/
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="
http://www.w3.org/1999/xhtml" xml:lang="utf-8">
<head>
<title>24小时[松江]城市天气详情</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
...

接下来要做的就是仅提取出我们需要的预报信息,比如:

2009年3月15日 星期天:
[农历二月十九]:
松江  :
天气:晴 :
气温:17度到4度:
风向风力:西南风4-5级转3-4级:
紫外线指数:中等:

我做的处理首先是过滤掉所有html标签,观察curl抓取的html代码可以发现我们需要的信息没有被html tag包含,经过这一步处理,输出的文本中含有可读文字了。接下来需要将空白行去除。最后仅提取我们需要的文本所在的其实行以及终止行即可。这里采用sed过滤。这样一来,最终的处理命令为:

curl -s http://wap.weather.com.cn/wap/58462/h24/ | sed  's/<[^<]*>//g' |

sed 's/^M//g' | sed '/^[ ]*$/d' | sed 's/^.*$/&:g' | sed -n '3,9p'

这样便可得到上面要求的文本输出了。

2.Linux下部署SAPI以及TTS引擎

一定要采用SAPI主要还是因为linux下面我没找到原生的中文TTS库。因此只好想办法利用Windows下面的资源了。办法其实也很简单,用wine... 这里所说的部署,是指同样通过wine运行自己编写的windows程序调用部署在linux下的tts engine来进行朗读。实际上就是进行SAPI com组件以及需要的tts engine com组件的注册工作。这里有一个偷懒的办法:http://www.linuxdiyf.com/viewarticle.php?id=121686

接下来要做的其实就和windows下一样了,打开VC写一段SAPI的调用程序吧...

不过其实问题要复杂些,存在两个困难:

  1. 该windows程序如何与原生的linux程序/脚本交互(基于wine)
  2. SAPI依赖窗口消息,需要X-windows环境

可能对一部分人第二个并不是什么问题,不过我目前家里用作服务器的机器配置比较老,为节省资源平时完全禁用了XServer。我又没心思去修改wine来解决这个问题,所以XServer是必须启动的了,那么需要将他的资源消耗尽可能减少。而对于第一个问题,我的解决办法是将TTS发音功能做成一个C/S构架的体系。提供一个./readit的命令,该命令为linux的native程序,readit将需要朗读的文本用tcp报文发送给基于windows(wine)的server程序完成真正的合成朗读。同时我发现每次加载/结束使用sapi的程序,所在的XServer的虚拟内存都会无故增加。因此C/S构架是的server常驻系统也避免了潜在的内存泄露。还有一个好处就是在局域网内任何机器都可以调用该TTS功能。具体的实现这里就不给出了,有兴趣的可以去看源代码。文末给出了readit的linux/win32版本。我的实现中,readit支持如下2种方式调用:

./readit <Text to read>

echo "Text to read by pipe" | ./readit

后者的好处在于能够朗读超过命令行参数长度限制的文本,同时能很好的结合前面介绍的脚本。Linux需要终端设置为UTF-8编码。

对于TTS engine的选择这里就不介绍了,大家选择比较合适的版本即可(Windows自带的那个中文男声其实不怎么样),我采用了上次eArts中运用的TTS引擎。

3.部署

在上面几个问题解决后,就是部署问题了,我把天气预报获取的脚本抽象成命令GetWeather( 24 | 48 )。要朗读明天的天气,采用命令

./GetWeather 48 | readit

即可。在运行该命令前要确保我们的TTS server已经启动了。因为Server部分依赖于Wine和XServer。这里用下面命令来完成Server的加载:

X :3 -ac &

sleep 60
export DISPLAY=":3" && wine win_voice.exe  1>/dev/null 2>&1 &

其中win_voice.exe就是TTS server。可以在开机时配置该脚本。不过要注意脚本运行的当前用户要和部署TTS时,wine所在的用户要一致。

公布个我自己用的备忘录/日记系统

早在去年3月就打算写一个作为我记录日记以及当作备忘录的东西。同时又希望能通过手机访问,不希望拥有过复杂的功能。于是去年9月份左右用asp写了下面这个日志管理系统:

 

 

虽然功能很简陋,不过自己用着还行。主要有下面几个功能:

1. 支持TODO:关键字的高亮显示

2. 支持*strong*, #URL#的解析

3. 按照工作日跟踪遗漏的日志,会给出提示要求记录

4. 自动按季度进行日志存档

5. 允许创建多个日志并分别存储

6. 数据存储为xml形式

 

同时,如果希望一次性记录不同日期的日志,可以在相应日志前加上日期,系统将自动识别:

[2009-02-01]

Log of this Date

[2009-02-03]

Log of that Date

功能介绍就到此为止,如果感兴趣并打算自己部署一个的话,直接将代码放到你希望的目录即可。不过如果打算记录私人信息的话最好加上权限访问。公布的版本中是没有密码保护的。还有要澄清的是上图画面不是我正在记录的日志,是为了示意做的。

至于源代码,风格很糟糕,而且还是JS和VBS混合编程。所以关于代码就别抱怨了。呵呵。不过能帮我做些改进的话还是很感谢。

我自己基本上每天都会用这个系统写当天的日志以及做一些摘录、点子等。感觉还是很好用的。同时通过手机访问效果也很好。

说实话有了这个工具自己生活状态改变了不少,以前一直觉得活得漫无目的。后来想想大概是自己定的计划没有坚持一直时刻对照和反省。同时每天没有很好的进行当日的总结和第二天的计划。其实这些也不是因为一个工具就改变的。但毕竟有了这样一个工具,同时也坚持保持这样一种习惯,感觉还是很不错的。

人生就是要时刻提醒自己,也要时常做些记录和总结。好了,扯淡扯远了。

我的blog似乎冷清了不少,偶尔发写文章貌似回复的也不多。看来自己太堕落了,以后要重新培养时常写blog的习惯。争取本周再发布两篇文章吧,应该都会比这篇有实际内容...

差点忘记了,本文提到的系统的代码下载地址:http://www.csksoft.net/data/Log_Tracer.rar

Linux下监控的Santak MT-500pro系列UPS信息

程序源代码请点击文章标题进入完整阅读模式获取

这段时间一直在"筹建"以前提到过的智能化家庭系统(IHES)。前不久购买并配置了500G的RAID 1存储阵列服务器,为了保证数据安全,同时又购买了Santak MT-500pro智能型UPS。

这里说智能型可能不是很专业,其实就是说该UPS提供了相关的接口允许从服务器上读取出UPS的相关状态信息。比如:

外部交流供电电压、负载端电压、负载功率、蓄电池电压等信息

不过,其实最需要关心的就是希望能够在发生断电情况下ups能即时告知。以便服务器机组能在UPS逆变供电实效前自动关机。毕竟不可能让我24小时监控家中服务器状况。

以上可以算是背景介绍了,现在面临的问题是,这个品牌的UPS虽然在官方提供了相关的监控和管理软件Winpower。不过使用后,存在许多不足:

  1. 程序采用java (1.14) 开发,甚至连与ups通讯的部分也完全采用java
  2. 该程序实现上十分低效,监控目模块间通讯需要采用java rmi
  3. 没有命令行接口,可制订性差

当然这些问题不是所有人都会抱怨的,不过目前考虑到家里的中央服务器还采用P3 CPU, 512mb内存,以及没有安装X Server环境。纯命令行下WinPower完全无法工作。同时采用java的实现虽然从厂家角度降低了porting成本,不过绝对是一个低效的实现。我测试过仅仅开启了Winpower中的监控模块,java进程就消耗了近20Mb的资源。同时还不考虑低效的实现(我逆向工程过其class文件,实现很糟糕)。仅仅为了监控ups信息而做出如此的资源消耗实在不值得。

另一方面,如果选用APC品牌的UPS,linux下则有现成的Apcupsd。当然东西都买下了,后悔也没用。于是,我实现一个满足我需求的ups监控程序。这里提供一个原形程序的代码以及整个整个制作过程的叙述。支持的型号是Santak MT-500pro。不过我相信通过源代码以及按照我的制作思路,其他型号也很容易做出对应的程序。原形程序是纯命令行的,很容易用bash shell写一个断电后一定时间内控制服务器自动关机的daemon脚本。

下面是制作介绍部分,没兴趣看得话代码在文章末尾给出了,请自行编译使用。Windows版本的也可以很容易修改而成。

MT-500pro的相关信息:

毕竟不是给人家做广告,这里什么技术参数就不给了。

该UPS采用串口(COM)提供自身的状态信息。串口的通讯采用2400bps速率,目前已知的状态信息有如下2种形式:

  • "#220.0 004 12.00 50.0\r"
  • "(231.4 231.0 231.4 011 50.1 13.9 25.0 00001001\r"

这里先暂时不介绍相关的通讯协议。在上面的状态信息中,第一条应当为该UPS的标准参数(即正常情况下理论值)。这几个数据每次都是固定不变的。其含义依次为:

<输出(或输出电压)> <未知> <蓄电池电压> <输出(或输入)频率>

而第二项则为当前UPS的实际数据,这也是我需要关心的内容。其可能含义依次为:

<外部电压> <内部电压(具体未知)> <输出电压> <负载率%> <电源工作频率(Hz)> <蓄电池电压> <UPS机内温度(摄氏度)> <状态标志>

这里需要对状态标志做下介绍,因为我在逆向分析时无法重现所有可能情况,这里只给出我知道的几个标志位:

00001001
|     +--- UPS输出关闭位
+--------- 外部供电失效位

虽然只知道2个标志位含义,不过我想应该足够了,最高位表示当前交流供电是否异常,如果为1就表示断电了。UPS输出关闭位有效则表示当前UPS没有工作。

知道这些格式信息后,下面的问题就是如何知道相关的请求代码,即通讯协议。

监听串口通讯:

我曾想过通过把Winpower逆向工程来研究通讯协议,毕竟java 1.5以前的class文件可以很容易被反汇编(应该说是反代码)。不过后来放弃了,这里就不多说了...

目前采用的办法是监听串口的通讯。这里介绍一个相对使用什么本机COM1与COM2对接等方法简单多得手段。在Windows平台下可以采用一款叫做HHD Free Serial Port Monitor的免费软件。或者Sysinternal上也提供了相关工具。他们都通过hook IOCTRL函数来截获COM的通讯内容。

使用这类工具,开启Winpower后,相关的通讯协议也一目了然。其实根本不能算协议,十分的简单:

 Request: 2009-2-7 20:00:19.95264 (+0.9013 seconds)

  46 0D                                             F.

 Answer: 2009-2-7 20:00:19.01264 (+0.0601 seconds)

  23 32 32 30 2E 30 20 30 30 34 20 31 32 2E 30 30   #220.0 004 12.00
  20 35 30 2E 30 0D                                  50.0.

 Request: 2009-2-7 20:00:20.97364 (+0.8913 seconds)

  51 31 0D                                          Q1.

 Answer: 2009-2-7 20:00:20.03364 (+0.0601 seconds)

  28 32 32 35 2E 39 20 32 32 35 2E 39 20 30 30 30   (225.9 225.9 000
  2E 30 20 30 30 30 20 30 30 2E 30 20 31 33 2E 38   .0 000 00.0 13.8
  20 32 35 2E 30 20 30 30 30 30 31 30 31 31 0D       25.0 00001011.

 

因此,上面提到的2组信息分别对应的请求数据为:

"F\r"

"Q1\r"

Linux下的轻量级监控程序:

有了上面的分析结果后,很容易的就能自己编写一个监控程序。为了各位方便,我提供一个简单的原形程序。用法如下:

# ./upsread [port name]

如果忽略了port name,则默认采用/dev/ttyS0。 程序会先输出第一项UPS信息,然后以1秒为间隔打印出第二项状态信息,如:

./upsread
#220.0 004 12.00 50.0
(233.9 234.3 233.9 011 50.1 13.9 25.0 00001001
(234.3 233.9 234.3 011 50.1 13.8 25.0 00001001
(234.3 234.3 233.9 011 50.1 13.9 25.0 00001001
(234.3 234.3 234.3 011 50.1 13.9 25.0 00001001

 

比较"优美"的进行html代码的截断

最近想到该更新下自己网站了,好吧,就从那个我已经看不惯很久的msn space同步转发器开刀...

如果你是先通过space看到这篇文章的话,应该就能看到效果了。撇去显示效果不说,这里提一个有趣的问题:

如何比较优雅的截取一段html代码?

换句话说,假设有下面这段html代码:

<p>this is a truncated html source<a href=\"some _fcksavedurl="\"some" URL\">some hyperlink</a>.<BR>

more text</p><p>one more</p>

出于某种需要,希望只显示(保留)这段代码的前n个字节。这样的需求其实不少见,最明显的就是在msn space文章同步的时候,我只希望对文章的大致内容转发过去,目前的做法就是截取文章的前1000字节。

如果仅仅是对一段普通文本进行这样的处理,那实在简单不过。直接截取即可。但是html却不同。

例如,对于上面的文字,如果截取的部分是:

<p>this is a truncated html source〈a href=\"some

今后将这段代码用于其它页面将出现不可能预见的问题。

考虑下面的代码:

<p>Some html code</p>

The following is a truncated article:<div><%=truncated_text%></div>

将上面被强制截断的代码片断填充到这段代码可能会让好好的html页面面目全非。

因此,我们需要"优雅"的去截断这段html。

何谓"优雅"?

造成上述问题的根本原因在于对html的强制截断忽略了html的结构信息,使得后面浏览器的分析操作出现紊乱。

因此,优雅的去进行html截断必须在截断后仍旧维持当前的html结构信息。

可能你会说,这还不简单,直接实现一个html parser或者用xml dom分析器来完成此事。的确可以,不过这样的效率和代价实在有点可怕了。因此,要完成这个优雅的截断操作,同时需要在执行效率以及资源消耗间做取舍。同时,尽量避免动辄用dom parser的恶俗。

寻找一种刚好能解决此问题,且保证效率较高并尽量避免滥用资源的途径是我们需要的。

我的方法

相信在这个所谓的什么"web2.0" "web3.0?"的扯淡名词时代恐怕已经有人实现过类似的东西,所以这里仅说说我目前的实现方案。如果你知道有更好的办法,希望能和大家分享。

撇开具体实现,这个问题其实很简单,就是一个配对问题。用一个stack来跟踪html标签的使用情况,每个<tag>应该有一个对应的</tag>配对。因此我们只要扫描一遍html,堆栈上残留的元素就是没有配对的tag。这些tag就是因为我们的暴力截取而成为孤对的。因此我们只要在html代码末尾FILO的补上这些这些tag的对应/tag即可。

但是还有些细节问题。

  1. 因为html没有xml规范,所以一些情况下未配对的tag也是允许的,例如<BR>。
  2. 存在单一自配对的<tag/>例如<img xxx />。

 同时,对于暴力截断的html代码,我们不能奢望它们正好截断在一对tag之间,就像上面给出的例子那样,对于末尾是

〈a href=\"some

 的情况,我们还首先需要将这些“残缺”的tag给删去,否则即使配对仍旧存在问题。

大致的思路就是这样,关键就是如何实现的问题了。这里我仅采用javascript的String.replace方法进行上述的html扫描。

具体实现

实际上实现的困难在于如何去识别html的tag。这里的底线是不能用现有的dom parser。Javascript的正则式功能在这里就能派上大用处了。根据上面的分析,为了进行tag匹配的分析,我们需要识别出html中的所有tag。

他们存在下面几种形式

  1. < tagname some_attr>
  2. < / tagname>
  3. < tagname />

我们的任务是编写正则式去查找这些tag。并且捕获到tagname。这里用到了一个偏方,利用String.replace方法。

一般,很多人都这样用它some_str.replace(/some regexp/, "new text");

实际上,他有另一个形式:

some.replace(/some regexp/, function( match_text)

     {

          return "new text";

     });

利用一个匿名函数进行替换...

实际上,这个replace是一个再好不过的基于正则式的查找操作手段。因为相比手写一个查找循环,他的迭代操作是在解析器内部实现的,效率肯定要高很多。同时据我所知,要写个循环依次查找每个匹配正则式的循环很不容易,貌似没法指定搜索的开始字符。

好了,到此关键问题解决了。试验证明用这个办法效率完全可以接受,同时完全没有使用任何第三方的东西。

是具体实现时候要注意考虑前面提到的所有特殊情况。

下面来看下效果,为了展示各种情况,另外给个例子:

<p>this is a truncated html sourcesome hyperlink</a>.<BR>more text</p><div><dt>some url〈a href=\"some UR

经过处理后,变成了:

<p>this is a truncated html sourcesome hyperlink</a>.<BR>more text</p><div><dt>some url</dt></div>

 

有了这个功能,相信在作诸如自动缩略文处理时候就会轻松许多。(此刻让我想起了某些blog强制把html转成纯文本作略文的办法...)

 

最后,给出一个测试代码。喜欢的可以直接拿去用。当然,留下原始作者信息

http://www.csksoft.net/data/legacyftp/Products/code_and_lib/wise_cut_csk.zip

分页:[«]1[2][3][4][5][»]

日历

<< 2015-6 >>

Sun

Mon

Tue

Wed

Thu

Fri

Sat

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

Copyright Shikai Chen 2000-2012. Powered By Z-Blog(CSK Modified)