65.9K
CodeProject 正在变化。 阅读更多。
Home

第五阶段-UROBI:Arduino 的物联网终极机器人框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (30投票s)

2014年12月21日

CPOL

62分钟阅读

viewsIcon

79452

downloadIcon

1428

本教程提供了一个分步指南,用于为通过物联网控制的 Arduino 机器人构建终极机器人控制和信息汇集系统。

查看演示视频

 

 

1. 背景
     1.1 概述
     1.2 系统架构

A部分:机电一体化

2. 机器人建造
    2.1 OWI机器人手臂套件
    2.2 底盘建造
    2.3 准备机器人连接以进行控制系统

3. 机器人电源
    3.1 电源电路建造
    3.2 用电源测试机器人
    3.3 通过电池连接

4.  继电器系统用于机器人控制
     4.1 控制单元的整体概念
     4.2 电压线选择
     4.3 机器人与继电器板连接
     4.4 机器人手动测试

B部分:物联网集成

5.  控制机器人 
     5.1 通过串行端口控制
           5.1.1 使用ArdOS
           5.1.2 非ArdOS方法                                                                                                                                      5.2 在.Net中构建控制界面                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      and 5.3.2 IoT 测试客户端

6.  机器人监控远程摄像头流
    6.1 目标和设计问题
    6.2 集成EmguCV和人脸检测
    6.3 将摄像头图像流式传输到局域网
    6.4 用于机器人控制的IoT .Net客户端
          6.4.1 MJPEG流查看器                                                                                                                                           6,4.2 为机器人控制构建IoT远程控制界面                                                                           6.5 使用BackgroundWorker进行性能调优r

C部分:控制模式

7. 为什么需要多模态以及有哪些不同的可用模式

8. 通过计算机视觉控制
    8.1 通过面部运动控制机器人
    8.2 通过激光手势控制

9. 集成语音识别和TTS 

10. 无线红外机器人控制

D部分:其他重要物联网服务

11. 通知服务
     11.1 理解通知系统
      11.2 IFTTT (如果这个,那么那个)
             11.2.1 介绍IFTTT
             11.2.2:为UROBI生成基于IFTTT的通知系统
       11.3 通过Gmail在警报时发送邮件
       11.4:通过IFTTT将Gmail数据广播到其他频道
       11.5 从串行数据处理程序调用GmailSend方法


 12.  安全服务
        12.1 物联网环境下的安全需求
        12.2 AES加密与解密
        12.3 在UROBI框架中集成加密解密
        12.4 关于其他服务的讨论


13. 结论

1. 背景

1.1 概述

在整个物联网教程竞赛期间,我撰写了关于Arduino的专题文章,详细阐述了物联网平台软硬件设计的各个方面。本文应作为我努力的“结论”。我一直在思考,如何才能找到一个好的主题来结束这一系列教程,使其能够作为教程的结束课程。然后我想到,为什么不做一个机器人呢?

机器人制造很有趣。每个DIY爱好者的首要任务是设计世界上最酷的机器人。世界各地都有机器人竞赛来鼓励机器人发展。机器人不仅限于爱好项目,还为学习电机、执行器控制以及在嵌入式系统中实现逻辑提供了极好的平台。最重要的是,精心设计的机器人控制系统可以扩展到更复杂的工业控制单元。

从制造爱好者套件到编程机器人,有大量的机器人资源可供选择。许多教程为初学者提供了硬件和软件层面的精彩平台。但我还没有遇到一个教程或机器人框架能够集成基本的机器人系统和物联网,同时还能集成多模态控制。

有些教程教如何用小型键盘控制机器人,有些解释了红外线控制机器人等过程。但作为一个DIY爱好者,我一直想构建一个可以从多种方式控制的系统:语音、身体运动、远程控制、手机以及所有其他可能的酷炫控制方式。

在编写这个教程的过程中,我将一边建造机器人,一边编写代码,同时集成服务,这将是一个非常有趣的过程。我邀请您与我一起加入这个充满乐趣的旅程。

1.2 系统架构

首先,机器人实际上是由电机组成的。不同的电机(步进电机、直流电机、带减速箱电机、伺服电机)被用作手臂和腿部结构的关节,以创造一个机器。当我们开始研究机器人技术时,有几种简单的机器人设计可供选择。还有一些非常好的机器人套件可以帮助您开始机器人建造。我更喜欢套件,因为它们节省时间而且更坚固。OWI机器人套件是其中一个物有所值的套件。我尤其喜欢这个套件,因为它提供了一个具有5个自由度的手臂,与同类功能的其他套件相比价格相对便宜,它使用直流电机,不需要编码器,而且最重要的是组装和操作这个机器人本身非常有趣。

因此,在本教程中,我们将构建一个系统来控制图1.1所示的机器人手臂。

图1.1 OWI机器人手臂边缘(图片来源:robotshop.com)

有趣的是,这个特定的套件自带了一个开关式控制盒,用于控制机器人手臂。我们将破解这个机器人,用我们复杂的控制机制绕过它自己的控制系统,该机制将通过我们扩展的物联网框架完美地通过互联网和局域网进行控制。我们还将通过将这个手臂套件安装在一个移动底盘上来改造机器人,使其成为一个迷人的移动机器人。我们还将安装一些传感器,以便机器人能够“执行”某些任务。

图1.2 更清晰地展示了我们将在DIY项目中做什么。

图1.2:拟议系统模型

因此,我们首先将开发一个基于继电器的控制单元来控制机器人电机,从而控制机器人本身。这个控制单元由一个Arduino板控制,该板处理从串行端口传入的命令。一个C#串行通信客户端将通过Arduino板获取机器人单元的传感器数据,并将其推送到ThingSpeak进行实时数据更新。客户端将升级以拥有一个摄像头接口,该接口将跟踪机器人,使视频流可以通过流媒体服务器远程访问,以便远程操作机器人的人员能够跟踪机器人的位置和其他细节。

C#客户端将通过我们自定义的Web服务连接到云端。它将持续轮询命令给机器人,这些命令可以由远程客户端远程生成。

C#客户端还将扩展机器人控制功能,并具有本地控制机制。语音识别和人脸检测将与之结合,以实现无缝的多模态控制。将不同模态集成到控制界面的目标是演示不同模态生成机器人命令的能力。

因此,到本教程结束时,您应该能够将硬件、连接的板卡、服务和输入模态集成到物联网中。

所以,让我们不要再浪费时间了,开始工作吧。

A部分:机电一体化

2. 机器人建造

2.1 OWI机器人套件

第一步显然是购买一个OWI机器人手臂边缘套件。在订购之前,请比较不同商店的价格。该套件附带一本非常好的手册,说明如何组装套件的零件。但是,如果您想在购买前查看该系统,可以查看此在线手册

这个Youtube视频资源是学习组装过程以建造机器人的一个好起点。机器人手臂具有5个自由度。每个自由度的运动由一个齿轮箱驱动,该齿轮箱由BLDC电机驱动。图2.1提供了零件列表的详细视图。

图2.1:OWI机器人手臂套件的独立零件

显然,一旦您获得了套件,您就需要完成组装过程。但一开始,您只需要知道,控制该特定手臂的系统基本上就是控制与之相关的电机的某种方式。每个电机都连接有一个跨接在两端的电容器,以帮助其平稳、无抖动地启动。由于所有自由度都使用适当的齿轮进行平衡,因此您无需担心使用PWM控制电机,因为电机将承受高负载,因此需要100%的占空比。因此,控制手臂实际上就是打开和关闭电机。

回到电机,可以看到每个电机都有两根线引出,末端连接到一个两针连接器。您可以从在线手册的第24页看到,所有这些线都通过其连接器连接到套件附带的开关控制器。我们需要破解套件,绕过其自身的控制器,然后将电线连接到我们自己的控制板。

 完成机器人组装。但不需要完成开关板或将电线从电机连接到开关板。这部分将被破解、更改和修改,以使其与我们的控制系统一起工作。一旦您成功完成机器人,它看起来应该像图1.1。

2.2 底盘建造

使用OWI套件的最大缺点是它主要是一个手臂套件,这意味着它没有移动能力。如果机器人不动,那肯定不是什么机器人。所以,我决定稍微调整一下设计,并将手臂安装在一个底盘的顶部。机器人商店里有很多底盘和轮胎可供选择。您可以购买四个轮胎和一个底盘,然后将机器人安装在上面。

这是来自ebay.in的一个链接

另一方面,您也可以设计自己的底盘。订购时要小心底盘的长度,因为您需要将手臂安装在上面。我的底盘底部有一个孔。我在手臂的底部钻了孔,然后将手臂螺钉固定在上面。您可以在图2.2中看到完成的组装。

图2.2:安装在漫游者上的机器人手臂的完成视图。

底盘必须承受大量的负载以及手臂。因此,底盘需要带减速箱的电机。您可以在2.2 a)中看到,我用一个带减速箱的电机替换了一个轮胎轴。如果您想让漫游者向左或向右移动,您可能需要两个电机。我选择了一个100 RPM的带减速箱电机,以保持较低的速度,这样在启动和停止时就不会有抖动。2.2 c)更清晰地展示了安装情况,您可以看到如何使用螺钉将轮胎连接到轴上。

 

2.3 准备机器人连接以进行控制系统

我们总共有六个电机。五个BLDC电机用于套件的五个齿轮箱,第六个电机是用于漫游者的带减速箱电机。我们需要完全绕过机器人套件自带的开关。这些电机可以产生两种运动:正向运动和反向运动,具体取决于施加在电机极子上的电压极性。由于我们的带减速箱电机额定值为12V-1A,我将使用一个12V-1A的电源,并通过它驱动所有电机。现在的问题是,如何使用5V的Arduino来控制这些电机的**开关**和**极性**,而驱动电压是12V?

如果我们需要电机的反向运动,那么电机可以用一个简单的晶体管来控制,正如我的Arduino连接设备教程的这一部分所解释的

但是,当您改变施加到电机上的电压极性时,您基本上是在提供-12V,所以NPN晶体管将始终处于反向电压状态,因为基极电压始终只有5V,而集电极电压将是-12V。因此,我们必须使用继电器。

为了理解如何使用继电器控制电机,请参考我的Arduino连接设备教程的继电器部分

在我们开始理解控制部分之前,我们需要对机器人单元的连接进行一些修改。一旦完成组装,您会发现机器人手臂有五对线引出。每对线有一根黑线和一根彩色线。黑线是接地线,彩色线是施加电压的线。正如继电器教程部分可能让您理解了使用继电器和微控制器控制任何设备的原理,微控制器、设备和继电器的接地必须是共用的。因此,一旦我们尝试通过我们的控制系统控制电机,所有电机的黑线都应该连接在一起。

为了控制,我们将不得不构建一个包含继电器的电路,正如我们稍后将看到的。机器人应该通过电缆连接到电路板。所以,我们现在将从每个电机电线对上剪断连接器,并将所有接地线连接在一起。

现在取一根长约1米的彩色扁平电缆,电缆的黑色线应连接到公共地线,而相应的彩色线必须连接到扁平彩色电缆的相同颜色的线上。

下面的图2.3将为您提供有关我们所说的破解的思路。

图2.3:带有机器人的彩色扁平电缆连接

由于我们需要适当的电源来驱动电机和继电器单元,因此我们将首先进行电源的建造,然后再回到控制单元。

3. 机器人电源

手臂边缘电机通常工作在9V至12V的范围内,最大电流为1A。带减速箱的电机是12V-1A规格。因此,使用12V电源为带减速箱的电机和其他电机供电是有意义的。市场上也有12V继电器。因此,12V电源是我们的机器人的自动化选择。

我长期以来一直在使用这个机器人套件来设计和测试各种基于直流电机的自动化系统。根据我的经验,我观察到,一旦您给手臂增加负载(例如,当手臂抬起重量大于150克的物体时),电机就必须消耗更多电流以维持恒定速度。因此,电压会下降,导致电机运动非常缓慢且不令人满意。因此,我建议使用12V-1A电源来驱动这个机器人套件的电机。但是,如果您的负载非常小,那么您甚至可以使用9V标准电池。

现在您有两个电源选择。a)使用12V电池,或b)自己构建电源。

我将在这里使用一个电源,因为我们的机器人是电线连接的。因此,持久的电源有助于为机器人维持恒定和所需的电流。其次,我们可以方便地从电源电路获得**+12V**和**-12V**。如果我们使用电池,那么我们需要使用两块电池来获得+12V和-12V。然而,作为旁注,我也会展示如何使用电池,以便您在操作机器人时可以选择您偏好的电源。

3.1 电源电路建造

图3.1:机器人的电源单元

我们使用12-0-12中心抽头降压变压器为电源提供交流输入。变压器将交流电压降低到24V峰峰值(正峰值高和负峰值高之间的电动势)。交流电通过由四个二极管组成的桥式整流器整流为直流电。与简单的双二极管全波整流器相比,使用四个二极管桥式整流器的优点是桥式整流器的电流输出更好。当电源预期驱动电机等负载时,桥式整流器总是比更简单的全波整流器更受欢迎。

整流后的输出始终是含有由于二极管开关延迟引起的交流分量的脉动直流电。使用电容器C1和C2可以去除这些交流分量。结果的直流电约为15V DC。然而,电压会根据负载的消耗而变化。为了保持恒定的电压输出,我们需要使用电压调节器。该电路的优点在于我们可以从同一电路调节并获得+12V和-12V。我们只需要7812用于+12V稳压,7912用于-12V稳压。它们各自的引脚图如图3.1 b所示。

C4和C4有助于进一步过滤输出电压。我们使用两个LED作为指示灯,以表明两种电压都可用。最后,地线、+12V和-12V连接到一个3针端口,从中可以为电路取电。

我将电路安装在一个通用电路板上,因为它易于连接且寿命长。如果您不熟悉焊接和安装,也可以将元件安装在面包板上。图3.2显示了PCB的背面。

图3.2:电源的通用PCB

之所以展示图3.2,是为了鼓励您开始使用PCB。这并不难。您只需将元件的引脚插入电路板。然后,剥去一些连接线的绝缘层,并使用裸露的细线将引脚焊接到相应的连接点上。如果您要用DIY做一些严肃的事情,您必须学会制作电路板。

一旦电源准备好,在操作电机之前,最好先用万用表测量电压。高电压可能会烧毁您的电机的绕组。

您有一个三槽端口。一个用于+12V,一个用于-12V,一个是地线点。所以,测量正电压与地之间的电压,以及负电压与地之间的电压。它们分别必须是+12V和-12V(虽然实际上您可能得不到精确的12V,但它会接近该值)。图3.3显示了我电源单元的电压测量。

图3.3:验证电源单元的电压

3.2 用电源测试机器人

现在您同时拥有正负电压来驱动电机正向和反向运动。在进行程序化操作之前,让我们先对手部电机的运动进行手动测试。您的机器人电机现在连接到一个多色扁平电缆,黑色连接到电机的公共接地黑线。您现在需要做的是将扁平电缆的黑色线连接到接地端。首先将相应的彩色线连接到+12V点,然后连接到-12V点。您会看到,手臂的一部分会以正向/反向运动,具体取决于所施加电压的极性。在此阶段,您应该能够手动测试您手臂的所有五个自由度以及漫游者的运动。您也可以通过将多条线连接在一起到+12V/-12V来测试。您会发现,当该板驱动的电机数量增加时,线路电流严重不足,电源板上的LED会开始闪烁。因此,如果您要测试复杂的项目,一个好的电源非常重要。

但是,在所有情况下,以及出于趣味性,您想用电池连接进行测试。下一个子部分仅向您展示如何为这个特定的机器人模型使用电池而不是电源。

3.3 通过电池连接

为了驱动您的机器人组装件以及您接下来将设计的继电器控制单元,您需要一个12V、3.4AH的电池。请务必不要使用过高的安培额定值,否则电机将被烧毁。问题是,如何从电源产生正负电压?好吧,拿两块电池。将一块电池的正极与另一块电池的负极连接。此连接完成了接地。因此,您现在剩下第一块电池的负极和第二块电池的正极,它们将分别提供-12V和+12V,如图3.4所示。

图3.4:使用电池对为我们的机器人准备电源

在连接机器人电机之前,请测量电路的电流。如果您找不到低电流电池,请分别使用**7809**和**7909** **IC**将正电压稳压到**+9V**,将负电压稳压到**-9V**。这些IC的引脚图与它们的12V稳压器7812和8912相同,如图3.1 b所示。

您也可以设计一个带有单个继电器的双电源,该继电器可在主电源和电池单元之间进行选择。

4.  继电器系统用于机器人控制

4.1 控制单元的整体概念

请参考此继电器部分,以便更清晰地理解使用继电器与Arduino的概念。

我们需要控制每个电机的三种状态:电机可以关闭,或者向前旋转(顺时针),或者反向旋转(逆时针)。因此,继电器必须控制三种状态。继电器最多可以有两个极:即有两个输入可供选择。但在这里,我们希望继电器在三种电压之间切换:+12V、0V、-12V。这怎么可能?

我们将在此处使用电压线选择的概念。首先,一个继电器将充当极性选择继电器。它将在NO和NC极上具有两个输入+12V和-12V。其输出将提供给每个电机的独立继电器的NO极。因此,当极性继电器关闭且任何一个电机控制继电器打开时,相应电机将顺时针移动。当电机控制继电器关闭时,电机不供电。所以电机是关闭的。当极性选择继电器和电机控制继电器都打开时,电机将逆时针移动,因为电机控制继电器将通过极性选择继电器获得-12V的NO供电。

4.2 电压线选择

图4.1:电压线选择继电器单元电路图

因此,从图4.1可以看出,Arduino的引脚13用于控制极性选择继电器,该继电器通过光耦MCT2E耦合到继电器。当引脚13高电平时,输入侧的光电晶体管激活,从而激活接收器侧的光电二极管,从而完成输出电路,并将引脚5给的12V提供给引脚4。引脚4连接到极性继电器的Vs引脚,触发继电器,使其输出-12V。当引脚13关闭时,光耦引脚4的输出电路未完成,输出为0V。这强制继电器处于关闭状态,使得输出端提供+12V。

4.3 电机控制继电器单元

图4.2:主继电器单元电路图

极性选择继电器电路与电机控制继电器电路的区别在于,这里通常连接的端子接地。因此,当继电器未触发时,电机不供电。当此继电器触发时,它将电压切换到NO端,即极性继电器的输出。因此,基于极性继电器的输出,此继电器将电机向前或向后驱动。

4.3 机器人与继电器板连接

图4.3 显示了机器人单元的整体电路图,从变压器到继电器单元(单击图像查看完整大小的图像)。

图4.3:机器人单元完整电路图

每个继电器的输出连接到电机的正极。所有电机的地线都接地,并连接到电源地线,电源地线进一步与Arduino地线共用。引脚13连接到极性选择继电器。六个其他继电器按顺序连接,从引脚12开始,但要排除PWM引脚。所有光耦的4号和5号引脚短接,并连接到电源的+12V端口。所有光耦和继电器的接地都共同接地,并连接到公共接地端。**注意:尽管电路图中未显示,但始终建议使用二极管将Arduino引脚连接到光耦,以避免任何反向电流流回微控制器板。**

我将继电器安装在一个单独的板上,将电源安装在另一个板上,这样如果出现任何问题,我可以分别调试它们。继电器单元和电源单元连接后的样子如下。

图4.4:带电源单元的继电器单元

 

4.4 机器人手动测试

在为Arduino板通电并继续对机器人进行编码之前,最好进行手动测试。物联网的每个独立组件都必须可以单独调试。所以我们应该检查我们的电路是否正常工作。打开电源,用螺丝刀或任何导体(如万用表表笔)触摸光耦的引脚4和引脚6。记住5和6是光耦输出端的光电晶体管的集电极。当您短接引脚4和引脚5时,您就绕过了光电晶体管,使引脚4(连接到继电器)可用12伏电压。一旦您连接了这两个引脚,光耦就会触发相应的继电器,从而移动机器人的相应部分。

当您短接极性选择继电器和另一个继电器的引脚4和5时,机器人的该特定部分将以相反的方向移动。请参阅4.5以了解如何在没有编码的情况下手动测试您的机器人。

图4.5:使用继电器板手动测试

 

B部分:物联网集成

5.  控制机器人 

5.1 通过串行端口控制

一旦机器人组装并连接到继电器电路,控制机器人就会变得更容易。这里唯一需要做的就是编写一个Arduino草图,该草图从串行端口获取输入,并激活连接到驱动继电器的一个光耦的相应数字引脚。

在测试机器人时,通过连接光耦的引脚4和5,您可能会注意到手臂的部分移动得非常快,因为直流电机的速度约为2200RPM。因此,如果您生成一个ON命令并保持它不动,由于运动受限,机器人部分可能会损坏。所以诀窍是打开机器人一段时间,然后将其关闭。通常50毫秒到100毫秒的延迟足以使部件良好移动。尽管有些部件可能需要不同的延迟进行测试。例如,“夹爪”可以在50毫秒内打开和关闭。但是,下臂和上臂需要大约70-100毫秒的延迟才能实现可接受的运动。

因此,我们的Arduino程序逻辑是:

a)将连接到光耦的引脚声明为OUTPUT

b)持续监控串行命令。

c)为极性继电器保留一对命令

d)为六个继电器分别保留一个命令。当板卡收到与这些继电器相关的串行命令时,将引脚设置为HIGH,等待延迟时间,然后将其设置为LOW。

5.1.1 使用ArdOS

我们将使用ArdOS进行编码。所以,如果您不熟悉ArdOS的工作原理,我建议您阅读这个关于ArdOS的教程

因此,我们将修改为该特定教程开发的**SerialCommArduinoSketch**代码,以便能够控制机器人。

#include <kernel.h>
#include <queue.h>
#include <sema.h>

#define NUM_TASKS  2
#define SIZE 6

int pins[SIZE]={12,9,8,7,6,5};
void taskSerialRead(void *p)
{
  int val=0;
  
  int i=0;
  int polarity=13;

  int DIR=1;
   while(1)
  {

    if(Serial.available())
    {
      
    val=Serial.read() ;
    val=val-48;
   
    ////////////// Switching Logic///////////
    if(val>=0)
    { 
      if(val<SIZE)
      {
        Serial.println(pins[val]);
       digitalWrite(pins[val],HIGH);
       OSSleep(100);
       digitalWrite(pins[val],LOW);
      }
      if(val==7)
      {

       digitalWrite(polarity,HIGH);

      }
      if(val==8)
      {

       digitalWrite(polarity,LOW);

      }
     
     
     
    }
    
    OSSleep(100);
    }
  }
   
  
}

void setup()
{
 

  
  int polarity1=13;

  Serial.begin(115200);
  pinMode(polarity1, OUTPUT);
  digitalWrite(polarity1,LOW);

///////////// Making All the Pins Low Initially/////////
 for(int i1=0;i1<SIZE;i1++)
  {
    pinMode(pins[i1],OUTPUT);
    digitalWrite(pins[i1],LOW);
  }
  ////////////////////////////////////////////////
   OSInit(NUM_TASKS);
   OSCreateTask(0, taskSerialRead, NULL);    

   OSRun();
}

void loop()
{
  // Empty
}

ArdOS仍处于Beta阶段,存在一些问题。其中一个问题是,当您在任务中声明数组时,草图根本无法找到数组元素。因此,pins数组被声明为全局的。然而,如果您声明普通变量为全局变量,任务将根本无法识别它。

其余逻辑很简单。数字7和8用于打开和关闭极性继电器。数字0-5用于激活驱动6个电机的6个继电器。您可以调试并上传代码,通过输入0-8来测试您的机器人运动。

5.1.2 非ArdOS方法

如果您在ArdOS方面遇到问题,请使用这个简单的Arduino草图来使您的机器人正常工作。

#define SIZE 6
int pins[SIZE]={12,9,8,7,6,5};
int i=0;
int polarity=13;
//15->13 or polarity on
//16->13 OFF
//17->ALL OFF
int DIR=1;
void setup()
{
  for(i=0;i<SIZE;i++)
  {
    pinMode(pins[i],OUTPUT);
    digitalWrite(pins[i],LOW);
  }
    pinMode(polarity,OUTPUT);
    digitalWrite(polarity,LOW);
  Serial.begin(115200);
}

void loop()
{
if(Serial.available()>0)
{
  int val=  Serial.read();
  val=val-48;
  Serial.println(val);
  if(val==7)
  {
    DIR=-1;
  digitalWrite(polarity,HIGH);
  }
  if(val==8)
  {
    DIR=1;
  digitalWrite(polarity,LOW);
  }
  if(val==17)
  {
  for(i=0;i<SIZE;i++)
  {
   
    digitalWrite(pins[i],HIGH);
  }
  }
  if(val<SIZE)
  {
    digitalWrite(pins[val],HIGH);
    delay(100);
   
    digitalWrite(pins[val],LOW);
  }
  
  
  
}
delay(20);  
}

需要注意的一点:一些开发人员在编译ArdOS时遇到问题,并出现“Naked Function”错误。这显然是IDE特定的问题。如果您想坚持使用ArdOS,可以尝试使用旧版本的Arduino软件。

5.2 在.Net中构建控制界面

C#客户端的设计问题在于它将连接的硬件与物联网连接起来,其任务是解释一系列命令,并通过串行接口将相应的代码发送到Arduino设备。然而,机器人有六个不同的部分,代表其不同的自由度,所有部分都需要通过命令进行编码。例如,夹爪打开、夹爪关闭、手腕向下、手腕向上、手臂向上、手臂向下、肩部向上、肩部向下、向前移动和向后移动。

由于我们希望将不同的控制界面集成到客户端系统,以便用户可以使用不同的模态来生成命令,因此我们将通过一系列方法来封装发送命令到串行端口的过程。

首先,我们将构建一个简单的UI,让用户了解机器人哪个部分被标记为哪个,并提供简单的按钮界面,以便我们可以独立测试每个部分。现在,因为我们已经有了控制Arduino中继电器的程序,我们只需要测试哪个数字对应哪个继电器,并相应地准备我们的方法。一个必须使电机顺时针移动的方法必须发送命令8,然后是该特定继电器的命令号。对于同一个电机反向运动,它应该发送7,然后是该继电器的编号。这是因为命令7和8将控制极性选择继电器。

在我们继续前进之前,我敦促您查看我们的将Arduino变成物联网节点教程,以获得关于串行通信和物联网集成的整体想法。

不要忘记评论,

val=val-48;

Arduino代码部分,并在集成C#客户端之前上传新的草图。

图5.1:用于串行通信和机器人控制的窗体面板

上面的截图包含一个非常简单的按钮GUI,使用户易于理解他们需要操作的控件。此外,我还添加了**I^**代表“向上”,**"V"**代表“向下”,**">>"**代表“顺时针”和**"<<"**代表“逆时针”运动。现在,在深入.Net编码之前,请测试您在第5节中的Arduino串行命令列表,并记下哪个数字与哪个命令相关联。

这个简单的UI有助于新用户立即理解他们想要控制哪个部分。

我们将首先为上面界面中显示的按钮相关的每个操作构建简单的方法,然后仅从按钮的Click事件处理程序中调用这些方法。

#region Control relay codes

static void AntiClockWise()
    {
        serialPort1.Write(new byte[] { (byte)8 }, 0, 1);
        System.Threading.Thread.Sleep(50);
        serialPort1.Write(new byte[] { (byte)5 }, 0, 1);

    }

    static void WristDown()
    {
        serialPort1.Write(new byte[] { (byte)7 }, 0, 1);
        System.Threading.Thread.Sleep(50);
        serialPort1.Write(new byte[] { (byte)3 }, 0, 1);
    }
    static void WristUp()
    {
        serialPort1.Write(new byte[] { (byte)8 }, 0, 1);
        System.Threading.Thread.Sleep(50);
        serialPort1.Write(new byte[] { (byte)3 }, 0, 1);
    }
    static void OpenJaw()

    {

        serialPort1.Write(new byte[] { (byte)8 }, 0, 1);
        System.Threading.Thread.Sleep(50);
        serialPort1.Write(new byte[] { (byte)1 }, 0, 1);

    }
    static void CloseJaw()
    {

        serialPort1.Write(new byte[] { (byte)7 }, 0, 1);
        System.Threading.Thread.Sleep(50);
        serialPort1.Write(new byte[] { (byte)1 }, 0, 1);
    }
  static  void Forward()
    {
        serialPort1.Write(new byte[] { (byte)8 }, 0, 1);
        System.Threading.Thread.Sleep(50);
        serialPort1.Write(new byte[] { (byte)4 }, 0, 1);

    }
  static void Reverse()
  {

      serialPort1.Write(new byte[] { (byte)7 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)4 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)8 }, 0, 1);

  }
  static void ElbowUP()
  {

      serialPort1.Write(new byte[] { (byte)7 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)2 }, 0, 1);
     
      

  }
  static void ElbowDown()
  {

      serialPort1.Write(new byte[] { (byte)8 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)2 }, 0, 1);     
  }
  static void ShoulderUp()
  {
      serialPort1.Write(new byte[] { (byte)8 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)0 }, 0, 1);

  }
  static void ShoulderDown()
  {
      serialPort1.Write(new byte[] { (byte)7 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)0 }, 0, 1);

  }
  static void ClockWise()
  {
      serialPort1.Write(new byte[] { (byte)7 }, 0, 1);
      System.Threading.Thread.Sleep(50);
      serialPort1.Write(new byte[] { (byte)5 }, 0, 1);

  }
    #endregion

您可能会注意到所有方法都被设为静态。这是因为我们将多个模态集成到客户端,如语音识别和人脸检测。因此,会有跨线程调用方法。为了避免对每个调用都使用beginInvoke,我选择了使用静态方法。

一旦这些方法成为静态的,serialPort1的声明也需要改为静态,并且初始化必须在FormLoad()方法而不是InitializeComponent()中完成。

下面是调用上述方法以执行操作的按钮事件处理程序。

private void btnJawOpen_Click(object sender, EventArgs e)
        {
            OpenJaw();
            
        }

        private void btnElbowUp_Click(object sender, EventArgs e)
        {
            ElbowUP();
        }

        private void btnRoverReverse_Click(object sender, EventArgs e)
        {
            Reverse();
        }

        private void btnRoverForward_Click(object sender, EventArgs e)
        {
            Forward();
        }

        private void btnBaseAnticlockwise_Click(object sender, EventArgs e)
        {
            AntiClockWise();
        }

        private void btnBaseClockwise_Click(object sender, EventArgs e)
        {
            ClockWise();
        }

        private void btnShoulderDown_Click(object sender, EventArgs e)
        {
            ShoulderDown();
        }

        private void btnShoulderUp_Click(object sender, EventArgs e)
        {
            ShoulderUp();
        }

        private void btnElbowDown_Click(object sender, EventArgs e)
        {
            ElbowDown();
        }

        private void btnWristUp_Click(object sender, EventArgs e)
        {
            WristUp();
        }

        private void btnWristDown_Click(object sender, EventArgs e)
        {
            WristDown();
        }

        private void btnJawClose_Click(object sender, EventArgs e)
        {
            CloseJaw();
        }

当您运行程序并进行测试时,您会发现您现在可以从UI本身控制机器人。

图5.2演示了从UI控制机器人(顺时针命令)

 5.3 物联网集成用于机器人控制

5.3.1 C#客户端的服务集成

请记住,我们已经在我们的Arduino物联网集成教程中开发了自己的物联网中间件,并托管为

http://grasshoppernetwork.com/IoTService.asmx

我们还重用了我们在Arduino物联网集成教程中开发的C#客户端,在该教程中我们讨论了如何将客户端绑定到此部分的WebService。我们现在需要做的就是更改项目ID并缩短轮询时间。由于机器人控制应该更加响应迅速,我们必须将轮询时间缩短到大约100毫秒,以给远程用户一种实时感觉。

让我们先更改projId变量的值。

string projId = "UROBI";

现在将timPollCommands计时器的Interval属性更改为100。

现在用户将发送特定于机器人控制的命令,而不是我们在上一个教程中使用的LED ON和LED OFF命令。因此,您需要更改客户端处理远程命令的方式。

这是timPollCommands的事件处理程序。请注意,由于预期的负载很重,我们将在命令被客户端处理后立即删除它。

 ArduinoSerial.ServiceReference1.IoTServiceSoapClient iotClient = new ArduinoSerial.ServiceReference1.IoTServiceSoapClient();
        private void timPollCommands_Tick(object sender, EventArgs e)
        {
            timPollCommands.Enabled = false;
            try
            {
                string[] result = iotClient.CommandToExecute(projId, ipAddress).Split(new char[] { '#' });
                if (result.Length < 3)
                {
                    timPollCommands.Enabled = true;
                    return;
                }
                string command = result[0].ToUpper();
                string status = result[1];
                if (command.Equals("FORWARD") && !status.Equals("OK"))
                {
                    try
                    {
                        Forward();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("REVERSE") && !status.Equals("OK"))
                {
                    try
                    {
                        Reverse();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("CLOCKWISE") && !status.Equals("OK"))
                {
                    try
                    {
                        Forward();
                        ClockWise();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("ANTI CLOCKWISE") && !status.Equals("OK"))
                {
                    try
                    {
                        AntiClockWise();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("OPEN JAW") && !status.Equals("OK"))
                {
                    try
                    {
                        OpenJaw();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("CLOSE JAW") && !status.Equals("OK"))
                {
                    try
                    {
                        CloseJaw();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("ELBOW UP") && !status.Equals("OK"))
                {
                    try
                    {
                        ElbowDown();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("ELBOW DOWN") && !status.Equals("OK"))
                {
                    try
                    {
                        ElbowDown();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("SHOULDER UP") && !status.Equals("OK"))
                {
                    try
                    {
                        ShoulderUp();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("SHOULDER DOWN") && !status.Equals("OK"))
                {
                    try
                    {
                        ShoulderDown();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }

            }
            catch
            {
            }
            timPollCommands.Enabled = true;

        }

5.3.2 物联网测试客户端

我们可以简单地使用我们在Arduino物联网集成教程中开发的ASP.Net TestClient.aspx客户端。但是,由于我们期望响应更快一些,我还添加了一个带有所有命令的下拉列表框,这样用户就不用手动输入命令了。通过激活DropDownListBox的AutoPostback属性,我们确保所选命令在选择后立即可用于文本框,然后可以通过我们的IoTService.asmx中间件发送到服务器。不要忘记在测试客户端中将项目ID更改为UROBI。图5.3显示了测试客户端的设计。

图5.3:TestClient.aspx设计

以下是客户端的代码后端。

void Button1_Click(object sender, EventArgs e)
{
iics.IoTService c=new iics.IoTService();
 c.InsertCommand("UROBI",txtIp.Text,txtCommand.Text,"WebClient",DateTime.Now,"EXECUTE");
 Label4.Text="Command Sent";
}

void Button2_Click(object sender, EventArgs e)
{
iics.IoTService c=new iics.IoTService();
string s=c.FetchNotification("UROBI",txtIp.Text);
Label4.Text=s;

}

void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
txtCommand.Text=DropDownList1.SelectedItem.ToString();
}


一旦您在浏览器中运行文件并在IP地址与您的C#客户端的IP地址相同的情况下生成命令,您就可以通过这个简单的Web界面远程控制机器人。图5.4显示了从物联网测试客户端控制我们的机器人。

图5.4:通过物联网服务远程控制机器人的结果

6. 机器人监控远程摄像头流

6.1 目标和设计问题

请看图5.4。现在考虑您正在通过TestClient.aspx中的浏览器从远程位置控制机器人。如果您看不到机器人的位置,您能控制什么吗?如果不看另一端发生的情况,您怎么知道要控制什么以及控制多少?因此,远程摄像头流是远程设备控制(尤其是自动化)不可或缺的一部分。

为了实现这一点,我们需要首先将实时摄像头帧集成到我们的窗体中。然后我们可以编写我们的流媒体服务器,使窗体可以远程访问。

由于我们打算将人脸检测集成到控制决策中,我们将集成EmguCV来检测人脸,它带有一组用于捕获摄像头帧的API。所以我们将从一开始就使用这个技术。我还恳请您阅读Sergio Andrés Gutiérrez Rojas提供的这篇出色的人脸检测教程,从中我们将使用人脸检测模块。EmguCV是OpenCV的封装,提供了大量出色的实时视频/图像处理API。

使用EmguCV的原因之一是它相对容易将EmguCV图像转换为.Net图像,反之亦然。因此,在本节中,我们将学习如何将摄像头集成到应用程序中,启用人脸检测,然后通过局域网流式传输摄像头帧。所以,如果用户坐在C#客户端机器上,他可以利用摄像头接口生成一些很酷的机器人命令。如果他不在本地,而是远程控制机器人,他可以从远程位置看到机器人,这使他更容易控制机器。

虽然我们在这里会启用人脸检测,但我们不会在本节中讨论如何将面部运动转换为控制。我们将把控制部分作为一个完全独立的部分来处理。

6.2 集成EmguCV和人脸检测

将Emgu添加到项目所需的所有必要dll文件都已包含在C# ArduinoClient项目文件夹的bin文件夹中。您将看到一套OpenCV的dll以及Emgu特定的dll文件。您需要首先通过从项目菜单选择“添加引用”,然后在bin目录中浏览文件来添加Emgu.CVEmgu.CV.GPUEmgu.CV.MLEmgu.CV.UIEmgu.CV.Util dll文件。

添加dll文件后,添加一个PictureBox对象用于显示捕获的帧。现在声明一个Emgu.CV.Capture类的对象,我们称之为grabber。

从按钮点击事件初始化该对象(或者,如果您愿意,也可以在窗体加载时初始化)。添加一个Application.Idle事件的处理器,捕获一帧并进行处理。

人脸检测使用 Haar Cascade。Cascade基本上是许多弱分类器的组合。Emgu还附带Haar_cascade文件,该文件基本上是一组通过训练多张人脸图像获得的特征。对象检测模块将当前帧的每个部分的特征与此级联特征进行比较,并定位与人脸最匹配的区域。

我们在“启动摄像头”按钮事件处理程序中初始化一个Haar Cascade对象,然后初始化grabber。在FrameGrabber()方法中,我们捕获一帧,尝试检测其中的人脸,如果存在,则在人脸周围绘制一个矩形,最后将帧显示在picturebox中。以下是上述概念的列表。

  #region camera Utility and Face Detection Part
        Image<Bgr, Byte> currentFrame;
        Capture grabber;
        HaarCascade face;
        MCvFont font = new MCvFont(FONT.CV_FONT_HERSHEY_TRIPLEX, 0.5d, 0.5d);
        Image<Gray, byte> result, TrainedFace = null;
        Image<Gray, byte> gray = null;
        void FrameGrabber(object sender, EventArgs e)
        {
         

            //Get the current frame form capture device
            currentFrame = grabber.QueryFrame().Resize(320, 240, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);

            //Convert it to Grayscale
            gray = currentFrame.Convert<Gray, Byte>();

            //Face Detector
            MCvAvgComp[][] facesDetected = gray.DetectHaarCascade(
          face,
          1.2,
          10,
          Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
          new Size(20, 20));

            //Action for each element detected
            foreach (MCvAvgComp f in facesDetected[0])
            {
            
                result = currentFrame.Copy(f.rect).Convert<Gray, byte>().Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);

                //draw the face detected in the 0th (gray) channel with blue color
                currentFrame.Draw(f.rect, new Bgr(Color.Red), 2);

                 
                

            }
            pictureBox1.Image = currentFrame.ToBitmap();

            
        }
        bool cameraStarted = false;
        private void button4_Click(object sender, EventArgs e)
        {

            if (!cameraStarted)
            {
                cameraStarted = true;
                button4.Text = "Stop Camera";
                face = new HaarCascade("haarcascade_frontalface_default.xml");
                grabber = new Capture();
                grabber.QueryFrame();
                //Initialize the FrameGraber event
                Application.Idle += new EventHandler(FrameGrabber);
                return;
            }
            else
            {
                Application.Idle -= new EventHandler(FrameGrabber);
                cameraStarted = false;
                button4.Text = "Start Camera";
            }

        }
        #endregion

运行项目后,您将开始在picture box中看到实时帧。您还会在您的脸上看到一个红色的矩形。图6.1显示了一个实时会话的结果。

图6.1:摄像头集成和人脸检测结果

6.3 将摄像头图像流式传输到局域网

Codeproject确实像伊甸园,当您需要查找代码片段时,尤其是如果您想用C#.Net做一些事情。当我寻找将流媒体服务集成到项目中时,我尝试了不同的技术和代码块,最后发现了这个关于图像流媒体的宝石教程

https://codeproject.org.cn/Articles/371955/Motion-JPEG-Streaming-Server

本教程展示了如何流式传输桌面或文件夹中存储的一组图像,无需安装任何特定软件,只需通过浏览器即可远程查看。代码非常直接,设计也非常出色。因此,我们将利用这个特定的教程并稍微调整代码,以便能够流式传输实时摄像头帧。

首先,我们将ImageStreamingServer.csMjpegWriter.cs文件包含到我们的项目中。现在现有的工作要么从目录流式传输图像,要么捕获当前屏幕并流式传输。我们希望服务器流式传输由frame grabber捕获的帧。由于grabber和流媒体服务器是两个完全不同的线程,我们需要一个共享变量,从FrameGrabber()写入,并在ImageStreamingServer线程中读取。我们声明一个Image对象,称为camImage

public static Image camImage;

让我们更新构造函数,以便使用Screen类中的一个方法来初始化服务器,该方法可以流式传输由FrameGrabber()创建的图像。

 public ImageStreamingServer()
            : this(Screen.WebCamStream())
        {
            
        }

最后,ServerImageStream类中的WebCamStream()方法在流式传输时锁定camImage,以避免任何访问冲突,并在一个IEnumerable<Image>对象中产生图像,该对象由服务器作为数据包发送给客户端。

 public static IEnumerable<Image> WebCamStream()
        {
            while (true)
            {

                lock (ImageStreamingServer.camImage)
                {

                    yield return ImageStreamingServer.camImage;
                }
                

            }

            

            yield break;
        }

流媒体服务器只需要这些修改。一旦您向服务器提供IEnumerable<Image>对象,它就会处理其余的事情。

我强烈建议您阅读本教程以获得更深入的流媒体过程理解。

现在,从我们的FrameGrabber()方法中,我们可以将currentFrame.ToBitmap()赋值给ServerImageStream类的camImage。

           try
            {
                ImageStreamingServer.camImage = currentFrame.ToBitmap(); 
            }
            catch
            {
            }

我将服务器设置为在端口号8090上运行。因此,您可以通过键入机器人的IP地址(如标签所示),然后指定端口号8090并加上冒号(:)来查看流。

您可以在图6.2中看到结果。

图6.2:远程查看连接到机器人的流

6.4 用于机器人控制的IoT .Net客户端

我们已经看到如何可以在我们开发的物联网服务之上构建机器人控制系统,并且机器人可以远程控制。我们还了解了在服务器上实现摄像头流媒体服务的必要性,以便远程查看机器人状态。现在我们真正需要的是一个能够充当我们UROBI系统的完美客户端的单一界面。用户应该能够从单一界面远程查看和控制机器人。

考虑到我们迄今为止取得的成就,这项工作并不那么困难!所以,让我们创建一个新的C#项目(Windows窗体应用程序)。我们将首先创建一个MJPEG客户端用于查看远程客户端,然后创建一个用于远程机器人控制的UI界面。

6.4.1 MJPEG流查看器

首先从Codeplex下载MJPEG解码器。解压文件夹。它包含几个dll文件。您所要做的就是将MJpegProcessor.dll引用到您的项目中。添加dll文件后,构建一个客户端非常简单。

声明一个MjpegDecoder对象。

 MjpegDecoder m_mjpeg;

创建一个简单的界面,带有一个用于打开和关闭流的按钮,以及一个用于指定远程地址的文本框。如果没有选择流,则初始化m_mjpeg对象。添加一个事件处理程序来处理新帧,并使用服务器Uri调用ParseStream()方法。处理用户忘记提供http://扩展名的情况,方法是将其与地址字符串连接。该对象打开远程流,并在可用新流时触发事件处理程序。在事件处理程序中,只需将e.Bitmap对象赋给您picturebox的Image属性。

        private void button1_Click(object sender, EventArgs e)
        {
            var b = (Button)sender;
            if (b.Text.Equals("Open Stream"))
            {
                m_mjpeg = new MjpegDecoder();

                m_mjpeg.FrameReady += new EventHandler<FrameReadyEventArgs>(m_mjpeg_FrameReady);
                if (!txtServerAddress.Text.StartsWith("http://"))
                    m_mjpeg.ParseStream(new Uri("http://" + txtServerAddress.Text));
                else
                    m_mjpeg.ParseStream(new Uri("http://" + txtServerAddress.Text));
                button1.Text = "Stop Stream";
                return;
            }
            else
            {
                button1.Text="Open Stream";
                m_mjpeg.FrameReady -= new EventHandler<FrameReadyEventArgs>(m_mjpeg_FrameReady);

            }

        }

运行C# Arduino客户端。启动摄像头并启动流媒体服务器。运行UROBI客户端并打开流。您可以在客户端中看到远程流,如图6.3所示。

图6.3:在UROBI客户端中查看远程流

6.4.2 为机器人控制构建IoT远程控制界面

我们希望在客户端提供相同的控制界面,以便客户端更容易知道单击哪个按钮。但与直接将命令发送到Arduino的串行客户端不同,UROBI客户端必须通过Web服务将命令推送到IoT命令堆栈。

我们将首先复制C#串行Arduino客户端的相同界面到UROBI客户端。添加对我们自定义IoT服务的服务引用。

http://grasshoppernetwork.com/IoTService.asmx

我们将稍微更改按钮文本,并保留客户端将处理的确切命令,该命令已连接到Arduino。所有按钮都必须有一个单一的点击事件处理程序。

从文本框中指定的服务器地址,我们将只分离IP地址部分,并在单击按钮时生成命令。

ServiceReference1.IoTServiceSoapClient iot = new ServiceReference1.IoTServiceSoapClient();
string projId = "UROBI";

 private void AllButtonClickHandler(object sender, EventArgs e)
        {
            try
            {
                var b = (Button)sender;
                string command = b.Text;

                string ip = "";
                if (txtServerAddress.Text.StartsWith("http://"))
                {
                    ip = txtServerAddress.Text.Split(new string[] { "http://" }, StringSplitOptions.None)[1];
                }
                if (ip.Contains(":"))
                {
                    ip = ip.Split(new char[] { ':' }, StringSplitOptions.None)[0];
                }
                iot.InsertCommand(projId, ip, command, "UROBO Client", DateTime.Now, "PENDING");
            }
            catch
            {
            }
        }

是的,就是这么简单!这是从UROBI客户端控制机器人的截图。

图6.4:使用UROBI客户端远程控制机器人

测试时,我发现由于轮询次数过多,C# Arduino串行客户端经常因超时问题而无法通过Web服务获取数据。因此,我更改了C#串行客户端的App.Config中的绑定为自定义绑定。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
      <bindings>
        <basicHttpBinding>
          <binding name="IoTServiceSoap" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="2147483647" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true">
            <readerQuotas maxDepth="32" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="4096" maxNameTableCharCount="16384"/>
          </binding>
        </basicHttpBinding>
      </bindings>

      <client>
            <endpoint address="http://grasshoppernetwork.com/IoTService.asmx"
                binding="basicHttpBinding" bindingConfiguration="IoTServiceSoap"
                contract="ServiceReference1.IoTServiceSoap" name="IoTServiceSoap" />
        </client>
    </system.serviceModel>
</configuration>

6.5 使用BackgroundWorker进行性能调优

尽管我们已经成功地让物联网框架工作并远程控制机器人,但您可能已经注意到,随着轮询速率的增加,GUI的响应性越来越差。这是因为我们过于频繁地阻塞主线程以进行远程调用。因此,将WebMethod的调用与主线程分离变得至关重要。我们希望为每次轮询实例创建一个后台线程,并在C# Arduino串行客户端和UROBI远程客户端中分别调用实例。我们可以通过在timPollCommands的tick事件处理程序中创建一个后台线程,并在工作线程的DoWork方法中执行WebMethod调用来实现这一点。类似地,远程调用可以在UROBI客户端中从AllButtonClickHandler派生的BackgroundWorkerDoWork方法中完成。

这是C# Arduino串行客户端的更新代码。

 private void timPollCommands_Tick(object sender, EventArgs e)
        {
            timPollCommands.Enabled = false;
            BackgroundWorker bw = new BackgroundWorker();
            bw.DoWork += new DoWorkEventHandler(bw_DoWork);
            bw.WorkerSupportsCancellation = true;
            bw.RunWorkerAsync();
            timPollCommands.Enabled = true;

        }

        void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            //throw new NotImplementedException();
            try
            {
                string[] result = iotClient.CommandToExecute(projId, ipAddress).Split(new char[] { '#' });
                if (result.Length < 3)
                {
                    timPollCommands.Enabled = true;
                    return;
                }
                string command = result[0].ToUpper();
                string status = result[1];
                if (command.Equals("FORWARD") && !status.Equals("OK"))
                {
                    try
                    {
                        Forward();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("REVERSE") && !status.Equals("OK"))
                {
                    try
                    {
                        Reverse();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("CLOCKWISE") && !status.Equals("OK"))
                {
                    try
                    {
                        Forward();
                        ClockWise();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("ANTI CLOCKWISE") && !status.Equals("OK"))
                {
                    try
                    {
                        AntiClockWise();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("OPEN JAW") && !status.Equals("OK"))
                {
                    try
                    {
                        OpenJaw();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("CLOSE JAW") && !status.Equals("OK"))
                {
                    try
                    {
                        CloseJaw();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("ELBOW UP") && !status.Equals("OK"))
                {
                    try
                    {
                        ElbowDown();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("ELBOW DOWN") && !status.Equals("OK"))
                {
                    try
                    {
                        ElbowDown();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("SHOULDER UP") && !status.Equals("OK"))
                {
                    try
                    {
                        ShoulderUp();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }
                if (command.Equals("SHOULDER DOWN") && !status.Equals("OK"))
                {
                    try
                    {
                        ShoulderDown();
                        iotClient.DeleteCommand(projId, ipAddress);

                    }
                    catch
                    {
                        iotClient.UpdateCommandStatus(projId, ipAddress, "FAILED");
                    }
                }

            }
            catch
            {
            }
        }


这显然提高了UI的整体响应能力。同样,在UROBI客户端中,我们从按钮点击事件处理程序中派生的BackgroundThreadDoWork方法调用WebMethod。

string projId = "UROBI";
        String command = "";
        string ip = "";
        private void AllButtonClickHandler(object sender, EventArgs e)
        {
            try
            {
                var b = (Button)sender;
                command = b.Text;

                
                if (txtServerAddress.Text.StartsWith("http://"))
                {
                    ip = txtServerAddress.Text.Split(new string[] { "http://" }, StringSplitOptions.None)[1];
                }
                else
                {
                    ip = txtServerAddress.Text;
                }
                if (ip.Contains(":"))
                {
                    ip = ip.Split(new char[] { ':' }, StringSplitOptions.None)[0];
                }
                BackgroundWorker bw = new BackgroundWorker();
                bw.DoWork += new DoWorkEventHandler(bw_DoWork);
                bw.RunWorkerAsync();
              
            }
            catch
            {
            }
        }

        void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                iot.InsertCommand(projId, ip, command, "UROBI Client", DateTime.Now, "EXECUTE");
            }
            catch
            {
            }
            //throw new NotImplementedException();
        }

 

C部分:控制模式

7. 为什么需要多模态以及有哪些不同的可用模式

理想情况下,如果我们将主题限制在第6节,那么关于UROBI(终极机器人控制框架)的教程就足够了,因为在第6节中,我们已经构建了机器人,通过服务将其连接到互联网,并且服务已扩展到摄像头共享,以便我们可以远程访问机器人及其视觉流并进行控制。

物联网本质上是一个同质的环境,它提供了许多服务(本地和Web)来以无缝的方式与底层机器进行交互。因此,如何将某些实用程序服务添加到现有框架也决定了该框架是否适合实际的复杂硬件工作流。

人类总是倾向于自然的交互方式,并通过语音、手势和自然动作来控制事物。因此,我们需要构建能够适应新的通信、交互和通知技术的系统。

本教程的C部分致力于将不同的模态集成到应用程序中,以表明即使将不同的输入方法集成到系统中,应用程序也能良好运行。这也证明了系统足够响应,可以处理多个输入。

有时,这些输入起着重要的作用,有时它们只是为了好玩。但尽管如此,在这一部分,我们将集成不同的模态,使应用程序更有趣。

提供给系统的一些常见输入模式是:

  • 面部运动
  • 语音识别
  • 激光手势
  • 手势
  • 眨眼、眼球运动
  • GSM
  • 射频遥控
  • 红外遥控

在这一部分,我们将看到并集成不同的模态。目标是使框架更加健壮并保持实时性。

8. 通过计算机视觉控制

8.1 通过面部运动控制机器人

在集成摄像头时,我们已经通过EmguCV检测到人脸。我已经在关于将面部运动转换为摩尔斯电码的教程中详细介绍。在该教程中,我详细介绍了如何检测简单的面部手势,如向上、向下、向左和向右。我们所需要做的就是将检测面部运动的逻辑集成到我们的人脸检测部分。一旦检测到手势,我们就可以采取适当的行动。这里我使用了顺时针和逆时针运动来代表左和右手势,以及上臂和下臂运动来分别代表上和下命令。

enum HeadState { UP,DOWN,LEFT,RIGHT,NONE};
        double x1 = -1, y1 = -1, x, y;
        HeadState head = HeadState.NONE;
        int nn = 0;
        void FrameGrabber(object sender, EventArgs e)
        {
         

            //Get the current frame form capture device
            currentFrame = grabber.QueryFrame().Resize(320, 240, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
            
            //Convert it to Grayscale
            gray = currentFrame.Convert<Gray, Byte>();

            //Face Detector
            MCvAvgComp[][] facesDetected = gray.DetectHaarCascade(
          face,
          1.2,
          10,
          Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
          new Size(20, 20));

            //Action for each element detected
            ///////////////////// Lasser recognizer////////////
            Bitmap lastFrame=currentFrame.ToBitmap();
            detector.ProcessFrame(ref lastFrame);
            currentFrame = new Emgu.CV.Image<Bgr, Byte>(lastFrame);
            ///////////////////////////////////////////////////////
            foreach (MCvAvgComp f in facesDetected[0])
            {
            
                result = currentFrame.Copy(f.rect).Convert<Gray, byte>().Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
                currentFrame.Draw(f.rect, new Bgr(Color.Red), 2);
                x = f.rect.Left;
                y = f.rect.Top;

                if (x1 == -1)
                {
                    x1 = x;
                    y1 = y;
                }
                else
                {
                    double variation = 0;

/////////////////////////////// This code block comes from Face to Morse code tutorial///////////////////////

#region face gesture detection logic from morse code tutorial
                    try
                    {
                        variation = (float)Math.Sqrt((double)((x - x1) * (x - x1) + (y - y1) * (y - y1)));
                    }
                    catch
                    {
                    }
                   if (!head.Equals(HeadState.NONE))
                   {
                       head = HeadState.NONE;
                       x1 = x;
                       y1 = y;
                      
                   }
                   if (nn != 0)
                   {
                       if (nn > 0)
                           nn--;

                   }
                   if ((variation > 29) && (nn <= 0))
                   {
                       double xvar = x - x1;
                       double yvar = y - y1;
                       nn = 18;
                       if (Math.Abs(yvar) > Math.Abs(xvar))
                       {
                           if (Math.Abs(yvar) > 9)
                           {
                               if (yvar < 0)
                               {
                                   head = HeadState.UP;
                               }
                               if (yvar > 0)
                               {
                                   head = HeadState.DOWN;
                               }
                           }
                       }
                       else
                       {
                           if (Math.Abs(xvar) > Math.Abs(yvar))
                           {
                               if (Math.Abs(xvar) > 9)
                               {
                                   if (xvar < 0)
                                   {
                                       head = HeadState.LEFT;
                                   }
                                   if (xvar > 0)
                                   {
                                       head = HeadState.RIGHT;
                                   }
                               }
                           }
                       }
                   }

#endregion

////////////////////////////////// End of Face gesture code from Morse code tutorial////////////////////
                }

                //draw the face detected in the 0th (gray) channel with blue color
                
                 
                

            }

/////////////////////////// Robotic Control Based On Detected Gesture///////////////////
            switch (head)
            {
                case HeadState.UP:
                    currentFrame.Draw("UP", ref font, new Point(20, 20), new Bgr(0, 255, 0));
                    ShoulderUp();
                    nn = 18;
                    break;
                case HeadState.DOWN:
                    currentFrame.Draw("DOWN", ref font, new Point(20, 20), new Bgr(0, 255, 0));
                    nn = 18;
                    ShoulderDown();
                    break;
                case HeadState.RIGHT:
                    currentFrame.Draw("RIGHT", ref font, new Point(20, 20), new Bgr(0, 255, 0));
                    ClockWise();
                    nn = 18;
                    break;
                case HeadState.LEFT:
                    currentFrame.Draw("LEFT", ref font, new Point(20, 20), new Bgr(0, 255, 0));
                    AntiClockWise();
                    nn = 18; ;
                    break;
            }
     //////////////////////////////////// Face Gesture based Controlling Ends/////////////////////////           
            pictureBox1.Image = currentFrame.ToBitmap();
         
            try
            {
                ImageStreamingServer.camImage = currentFrame.ToBitmap(); 
            }
            catch
            {
            }
            
        }

现在您可以通过面部运动来控制手臂的上下和左右移动。图8.1显示了机器人手臂的左移。

图8.1:通过头部运动控制机器人

 

8.2 通过激光手势控制

激光手势是另一种非常流行的无线设备控制模式。它广泛用于PowerPoint演示文稿中的幻灯片控制。同样,您不需要从头开始做太多事情。CodeProject已经有一个关于激光手势识别和Windows Media Player控制的出色教程。

该项目有自己的摄像头界面。但由于我们使用EmguCV来捕获和处理帧,所以在我们的工作中只需要MotionDetector1.csUnsafeBitmap.cs文件。只需初始化MotionDetector类的对象,并在人脸检测完成后、绘制人脸之前调用ProcessFrame()方法。

原始项目调用ControlMediaPlayer()方法,并将识别到的手势作为参数。这里我们更关注机器人控制。所以,我们将修改该方法为ControlRobot()方法。对于UP、DOWN、LEFT、RIGHT等手势,我们可以调用Form1类公开的静态方法,这些方法是为控制机器人不同部分而开发的,例如Forward()Reverse()ClockWise()等。

在MotionDetector1类中,我们更新了ProcessFrame()方法,以便在检测到手势后调用ControlRobot()方法。

     if (gesture != "?")
    ControlRobot(gesture);

ControlRobot()方法根据手势调用独立的机器人控制方法。

private void ControlRobot(string gesture)
        {
            try
            {

                switch (gesture)
                {
                    case "LEFT":
                        Form1.AntiClockWise();
                        break;

                    case "RIGHT":
                        Form1.ClockWise();
                        break;

                    case "UP":
                        Form1.ShoulderUp();
                        break;

                    case "DOWN":
                        Form1.ShoulderDown();

                        break;
                }
            }
            catch
            {
            }
        }

阈值通过Form1变量设置为250。

double threshold=250;

您可以在运行时轻松集成一个数值增减控件来选择其值。图8.2显示了检测到UP命令并通过它轻微移动夹爪。

图8.2:通过激光手势控制机器人

9. 集成语音识别和TTS 

语音识别也不算太难。首先,您需要添加对System.Speech的引用(从Project-.Add Reference-.Net)。

添加引用后,您可以使用SpeechRecognitionEngine对象

SpeechRecognitionEngine  _recognizer;

来加载语法集。语法是您希望识别器识别的单词或句子。在我们的例子中,我们将加载与GUI组件相同的语法,如“Clockwise”、“Anti ClockWise”、“Forward”、“Reverse”等。

加载语法后,添加一个应在检测到语音时触发的事件处理程序。

 _recognizer.SpeechRecognized += _recognizeSpeechAndWriteToConsole_SpeechRecognized; 

从事件处理程序中,我们可以调用相应的静态方法。

我将整个语音识别逻辑封装到一个名为RecognizeSpeechAndControlRobot()的单一方法中。一旦与Arduino建立串行通信,您就可以调用此方法。

#region Recognize speech and write to console
        static SpeechRecognitionEngine _recognizer = null;
        static void RecognizeSpeechAndControlRobot()
        {
            Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
            _recognizer = new SpeechRecognitionEngine();
           

            _recognizer.RequestRecognizerUpdate(); // request for recognizer update
            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("shoulder up"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("shoulder down"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("wrist up"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("wrist down"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("elbow up"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("elbow down"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("clockwise"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("anticlockwise"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("forward"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("reverse"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("open"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update

            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("close"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update
            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("right"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate();
            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("left"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate();
            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("enter"))); // load a "test" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update
            _recognizer.LoadGrammar(new Grammar(new GrammarBuilder("exit"))); // load a "exit" grammar
            _recognizer.RequestRecognizerUpdate(); // request for recognizer update
            _recognizer.SpeechRecognized += _recognizeSpeechAndWriteToConsole_SpeechRecognized; // if speech is recognized, call the specified method
            _recognizer.SpeechRecognitionRejected += _recognizeSpeechAndWriteToConsole_SpeechRecognitionRejected; // if recognized speech is rejected, call the specified method
            _recognizer.SetInputToDefaultAudioDevice(); // set the input to the default audio device
            _recognizer.RecognizeAsync(RecognizeMode.Multiple); // recognize speech asynchronous

        }
        static System.Speech.Synthesis.SpeechSynthesizer speaker = new System.Speech.Synthesis.SpeechSynthesizer();
        static void _recognizeSpeechAndWriteToConsole_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            //Console.WriteLine(e.Result.Text);
            //  MessageBox.Show(e.Result.Text);
            string s = e.Result.Text;
            //// Speaking/////////////////
            speaker.Rate = -4;

            ///// Forward///////////
            if (s.Equals("forward"))
            {
                Forward();
            }
            ///////Reverse//////////////////////////
            if (s.Equals("reverse"))
            {
                Reverse();
            }
            ///////////////////shoulder up/////////////////////////////////////
            if (s.Equals("shoulder up"))
            {
                ShoulderUp();
            }
            ////////////////// Shoulder down/////////////////////////////
            if (s.Equals("shoulder down"))
            {
                ShoulderDown();
            }
            /////////////////// elbow up////////////////////
            if (s.Equals("elbow up"))
            {
                ElbowUP();
            }
            ////////////////// elbow down//////////////////////////////
            if (s.Equals("elbow down"))
            {
                ElbowDown();
            }
            ///////////////////wrist up//////////////////////////////
            if (s.Equals("wrist up"))
            {
                WristUp();
            }
            ////////////// Wrist Down/////////////////////////////////
            if (s.Equals("wrist down"))
            {
                WristDown();
            }
            ////////////////// close//////////////////////////////////

            if (s.Equals("close"))
            {
                CloseJaw();
            }
            //////////////////////open ///////
            if (s.Equals("open"))
            {
                OpenJaw();
            }
            /////////////// anticlockwise//////////////////////
            if (s.Equals("anticlockwise"))
            {
                ClockWise();
            }
            //////////////////////clockwise//////////////
            if (s.Equals("clockwise"))
            {
                AntiClockWise();
            }

        }
        static void _recognizeSpeechAndWriteToConsole_SpeechRecognitionRejected(object sender, SpeechRecognitionRejectedEventArgs e)
        {

        }
        #endregion

我们将从button2的点击事件处理程序连接到串行端口后调用此方法。

private void button2_Click(object sender, EventArgs e)
        {
            try
            {
                serialPort1.PortName = comboBox1.SelectedItem.ToString();
                serialPort1.BaudRate = 115200;
                serialPort1.Open();

                RecognizeSpeechAndControlRobot();

                MessageBox.Show("Success");
                timPollCommands.Enabled = true;
                startTime = DateTime.Now;
            }
            catch (Exception ex)
            {
                MessageBox.Show("Failed: "+ex.Message);
            }
        }

测试部分留给您,因为我无法通过gif文件展示结果!

10. 无线红外机器人控制

**此IR部分**是我关于Arduino基本编程与硬件教程的一部分,解释了如何连接IR和进行编程。不幸的是,IR库有自己的计时器控件,这使得ArdOS无法与IR库正常工作。因此,如果您打算集成红外遥控,您需要放弃ArdOS。

IR 命令有两种解读方式:您可以直接在硬件中处理命令,也可以将检测到的命令通过串口发送到 C# 客户端,然后在客户端决定如何处理该命令。第一种技术的优点是速度更快,并能提供无延迟的高效硬件控制。然而,在这种技术中,您无法利用更高级的服务。在第二种方法中,代码需要由 c# 程序进行分析,然后(如果需要)再次生成串口命令来控制设备。这个过程会给关键应用带来严重的延迟。但是,在 C# 中实现逻辑的优点是,不需要更改连接设备的底层固件,只需要更新上层软件。

尽管如此,我将设计权留给您。我通过在 Arduino 板上部署逻辑来演示用法。我的 IR 接收器 PIN 连接到引脚 3。

这里是我为 ArdOS sketch 开发的 mySimpleRobo.ino 草图的更新版本。

#include <IRremote.h>
#define SIZE 6
int RECV_PIN = 3;

IRrecv irrecv(RECV_PIN);
decode_results results;

int pins[SIZE]={12,11,10,9,8,7};
int i=0;
int polarity=13;
//15->13 or polarity on
//16->13 OFF
//17->ALL OFF
int DIR=1;
void setup()
{
  for(i=0;i<SIZE;i++)
  {
    pinMode(pins[i],OUTPUT);
    digitalWrite(pins[i],LOW);
  }
    pinMode(polarity,OUTPUT);
    digitalWrite(polarity,LOW);

  Serial.begin(115200);
       irrecv.enableIRIn();
}

void loop()
{
   if (irrecv.decode(&results)) {
    Serial.println(results.value, HEX);
  
   switch(results.value)
    {
      case 0xC090060A:
      digitalWrite(polarity,HIGH);
      delay(10);
      digitalWrite(pins[3],HIGH);
      delay(150);
      digitalWrite(pins[3],LOW);
      digitalWrite(polarity,LOW);
      Serial.println("Arm Down");
      break;
   
    case 0xD090060A:

      digitalWrite(pins[3],HIGH);
      delay(150);
      digitalWrite(pins[3],LOW);
      Serial.println("Arm UP");
      break;
    }
    irrecv.resume(); // Receive the next value
  }
if(Serial.available()>0)
{
  int val=  Serial.read();
  val=val-48;
  Serial.println(val);
  if(val==7)
  {
    DIR=-1;
  digitalWrite(polarity,HIGH);
  }
  if(val==8)
  {
    DIR=1;
  digitalWrite(polarity,LOW);
  }
  if(val==17)
  {
  for(i=0;i<SIZE;i++)
  {
   
    digitalWrite(pins[i],HIGH);
  }
  }
  if(val<SIZE)
  {
    digitalWrite(pins[val],HIGH);
    delay(100);
   
    digitalWrite(pins[val],LOW);
  }
  
  
  
}
delay(20);  
}

我已经在 Arduino 上使用我的 AC 遥控器的 ON-OFF 命令对实现了简单的 ShoulderUP 和 ShoulderDown 命令逻辑。您可以分析代码并为不同的按钮开发不同的命令。

图 10.1 显示了结果。

图 10.1:使用 IR 遥控器控制机器人

D部分:其他重要物联网服务

11. 通知服务

11.1 理解通知系统

在我们的例子中,至少没有通知这样的东西。但是许多物联网应用需要良好的集成通知服务。那么什么是通知服务呢?假设您正在创建一个集中的安全应用程序,您希望在入侵者试图闯入您的场所(如 ATM、博物馆等)时,系统能够发出警报。监控是一个持续感知数据的过程,就像我们到目前为止所做的那样。在这种情况下,系统的职责是获取数据,并通过自定义服务或更相关的物联网服务(如 ThingsSpeak)使其可用。我在这里不讨论监控服务。如果您有兴趣了解监控服务,请参阅此使用 ThingsSpeak 进行监控的物联网服务教程。您可以看到 Arduino 设备获取的温度通过 ThingsSpeak 中的一个频道进行网络广播。但是,如果传感器旨在监测火灾可能性高的区域的温度呢?如果传感器部署在一个小熔炉附近,或者让我们的想象力更活跃一些,如果传感器是您的微波炉的一部分呢?在这些情况下,系统不仅应该将数据提供给最终用户,还应该确保在出现警报时通知相关方。您不能总是确定有人一直在监视网络广播的数据。因此,我们需要一个系统来获取这些数据,应用某些统计过程(如过滤、回归或阈值处理),然后决定数据是否指的是特定事件或警报。天然气泄漏、火灾、地震分别是 MQ-35、PT100 和加速度计传感器的事件。

因此,尽管我们当前的设置没有任何传感器,但我们将深入研究通知系统。为了实时检查它,我们将使用 LM35带 Arduino 的温度传感器的相同硬件设置,这是我们在上一篇文章中关于 ThingsSpeak 时看到的例子。

因此,我们的目标在这里稍有修改,我们希望在发生火灾时会发出警报。我们可以通过将蜡烛靠近传感器来模拟这种情况(请小心此类冒险,并注意不要在实验过程中烧毁传感器)。因此,每当检测到这种情况时,都应该生成警报。

现在,从上述教程中的实验中,您知道一旦传感器检测到高温值,即使您移开了传感器附近的火源,传感器也需要一段时间才能读取较低的值。这是因为传感器表面需要一些时间来散热。因此,报警系统不应有过多的警报。它必须配置为生成简洁的事件。

在硬件和嵌入式术语中,我们使用两个术语,它们有时可能看起来相同,但差别很大。事件可以通过警报来响应,警报可以是持续的,如蜂鸣器/报警系统/继电器驱动器等,也可以是通知系统,主要指远程传播警报。因此,在本节中,我们将研究物联网框架的通知系统。

11.2 IFTTT (如果这个,那么那个)

11.2.1 介绍 IFTTT

当我们的硬件生成警报时,我们希望通过电子邮件收到警报,我们也可能希望向特定号码发送 SMS,我们可能希望在 Twitter 上发布,可能希望在 Facebook 上更新等等。几年前,自动化这个过程可能是一项艰巨的任务,因为您需要独立集成各种服务,这带来了非常高的维护成本(请记住,云服务提供商会不断更改 API)。但是,随着IFTTT 服务的推出,这变得更加容易,而且非常高效。

为了提前了解这样的通知系统,请查看我们自己的@Codeproject 的 Twitter 个人资料。每天都会发布大量的文章和技巧。想象一下,如果Chris 不得不花时间为每篇文章发推文,那么谁还会致力于那个网站的 Bug 和建议部分呢?不,这需要将每篇文章发布时都自动化地推送到 Twitter。凭借这里的开发人员支持,codeproject 本可以轻松实现自己的通知服务。但取而代之的是您看到了什么?

作为一个上瘾的 Twitter 用户,我非常喜欢截图。所以这是我们@Codeproject 的 Timeline ( TL) 的截图。

图 11.1:Codeproject 的 Twitter Timeline 展示 IFTTT

您可以看到这里的人们很聪明,通过集成 IFTTT 进行通知,节省了编码和维护工作。每当有文章发布时,它就会将链接发布到 CP 的时间线。所有这些年来,我都以为 Chris 的生活很轻松,因为他所做的就是将文章链接发布到 Twitter 上!

 

话虽如此,回到主题,IFTTT 因此是提供配方的通知服务。它基本上是一种生成通知的方式。如果一条推文那么一个 Gmail 邮件。但它提供了与各种其他服务的集成。

所以,请创建一个 IFTTT 帐户并查看频道部分。正如您在图 11.2 中看到的,IFTTT 提供了它提供集成的各种服务。每个频道还有相应的事件,例如 Twitter 的“发布新推文”选项,Gmail 的“接收新电子邮件”选项等等。所以,只需前往配方部分并创建您的配方。

11.2.2:为UROBI生成基于IFTTT的通知系统

凭借 codeproject 这里杰出的才华库,我假设对 IFTTT 的任何进一步解释都将是对在职开发人员智能的严重不尊重。您肯定会比学习制作您的第一个厨房食谱更快地开始制作食谱。

但问题在这里更深。它不是如何使用 IFTTT,而是如何在当前上下文中以及如何使用 IFTTT?如何通过所有渠道减轻我们的硬件生成的警报?

答案很简单。通过实际将警报推送到其中一个频道,然后通过 IFTTT 根据我们通过代码集成的频道的输入事件来集成其他频道。

我们使用 C#.Net 作为 Arduiono 的串口客户端。所以我们可以从我们的应用程序发送 Gmail,我们可以集成 Twitter 服务等等。

我在这里更倾向于 Gmail。看,目标是减轻某些警报。但我可能对跟踪其他警报感兴趣。例如,我可能对跟踪极低和非常高的温度感兴趣。但我只想为高温值生成通知。

因此,我采用的方法是将第一个生成的警报集成到私有频道,然后通过配方将通知链接到其他频道。

让我们首先更新我们的 UROBI 的串口通信客户端以生成 Gmail 消息。

11.3 通过Gmail在警报时发送邮件

bool GmailSend(double tempValue, double threshold)
        {
            var fromAddress = new MailAddress("rupam.iics@gmail.com", "Rupam");// sender's mail address and name
            var toAddress = new MailAddress("rupam.iics@gmail.com", "Rupam");// To whom you want to send
            string fromPassword = "YOUR_FROM_EMAIL_ACCOUNT'S PASSWORD";
            string subject = "Arduino ALERT : Temperature Exceeds Threshold";
            string body = String.Format("TEMPERATURE = {0} 'C, Threshold={1}'C !\nALERT: Temperature Exceeds Value", tempValue, threshold);

            //Attachment att = new Attachment("form.jpg");
            // Use the above line if you want to send any image also.
            // You can save PictureBox1's image with pictureBox1.Save("hello.jpg",System.Drawing.Imaging.ImageFormat.JPEG);
            //and the attaching the hello.jog with att variable

            var smtp = new SmtpClient
            {
                Host = "smtp.gmail.com",
                Port = 587,
                EnableSsl = true,// Very important. You forget this and your mail is not going
                DeliveryMethod = SmtpDeliveryMethod.Network,
                Credentials = new NetworkCredential(fromAddress.Address, fromPassword),
                Timeout = 20000
            };
            using (var message = new MailMessage(fromAddress, toAddress)
            {
                Subject = subject,
                Body = body,

            })
            {
                //message.Attachments.Add(att);
                // Uncomment the above line if you are sending an Image
                try
                {
                    smtp.Send(message);
                 
                 
                }
                catch
                {
                    labCommandStat.Text = "Could Not Send Mail";
                    return false;

                }

            }
            return true;
        }

因此,上面的代码是通过 Gmail 服务器以编程方式发送电子邮件的经过时间考验的方法。不要忘记 EnableSSL 部分,否则代码将无法工作。

但是,不要期望此应用程序能立即工作。由于 Google 更改了其安全配置文件,您的第一次登录尝试将被阻止,并将向您发送一封关于此事的邮件。请点击安全配置文件,然后选择启用对我的帐户访问安全性较低的应用。(尽管我在开始测试之前并不知道这个变化。因此,在有更好的替代方案之前,我将坚持这个选项)。

图 11.3:发送数据到 Gmail 的安全设置

图 11.4:从我们的 UROBI 界面成功发送电子邮件的截图

11.4:通过IFTTT将Gmail数据广播到其他频道

一旦数据可用于 IFTTT 支持的频道之一,就可以通过完美设计的配方将其发送到其他频道。我希望一旦有 Gmail 通知就生成一条推文,以便所有关注我的人都能收到通知!

所以您需要首先选择“创建新配方”,然后在条款中选择 Gmail。选择

如果在收件箱中有来自搜索的新电子邮件

 在搜索词中输入“ALERT:”,因为我们正在发送带有“ALERT:”关键字的 Gmail 消息。现在在那个条款中选择发布推文。

这是我们完整配方的一瞥

11.5:基于 Gmail 警报配文的 Twitter 通知

这是截图,展示了我们的物联网模块现在如何通过 IFTTT 实现导航到不同的频道。

11.6 将通知从我们的 UROBI 系统通过 IFTTT 传播到世界

但请记住一件事,糟糕的设计就是糟糕的设计。即使在这种复杂的服务中,您也不会因此而幸免。为了让您了解在设计此类通知服务时应采取何种关怀,我创建了另一个配方,每当我发推文时就会生成一封电子邮件。图 11.7 显示了这两个配方如何创建一个循环并扰乱整个系统。

11.7:糟糕的 IFTTT 设计:Gmail 和 Twitter 服务陷入循环

11.5 在高温下调用 GmailSend 方法

我们通过异步 SerialReceived 事件处理程序接收串口数据,该处理程序由委托成员 my display 进一步处理。我们将以“T:52”格式从 Arduino 推送温度值。因此,在 display 方法中,我们必须提取温度部分,与我设定的阈值 50 进行比较,然后生成 Gmail 消息。为了不阻塞 UI 线程,我们必须使用后台工作程序。我们还需要确保不传输不必要的消息。因此,我们设置了一个 timerNotification,在数据传输后立即将其设置为 30 秒。在计时器停用之前,不会向 Gmail 发送任何数据。这样您就可以避免消息泛滥。

 if (s.Contains("T:"))
            {
                string s1 = s.Split(new string[] { "T:"}, StringSplitOptions.None )[1];
                try
                {
                    temp = double.Parse(s1);
                    if (temp > th)
                    {
                        if (!timerNotificationAlert.Enabled)
                        {
                            BackgroundWorker bw1 = new BackgroundWorker();
                            bw1.WorkerSupportsCancellation = true;
                            bw1.WorkerReportsProgress = true;
                            bw1.DoWork += new DoWorkEventHandler(bw1_DoWork);
                            bw1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw1_RunWorkerCompleted);

                            bw1.RunWorkerAsync();
                            timerNotificationAlert.Enabled = true;
                        }
                        
              
                    }
                }
                catch
                {
                }
            }
           

 

12. 安全服务

12.1 物联网环境下的安全需求

安全是任何企业或分布式服务的另一项基本、重要和关键的部分。如果您使用带有 SSL 的域名,那么所有进出您浏览器的 Ddata 都会被加密。因此,SSL 提供了更高的安全性。因此,像我们迄今为止讨论过的 Gmail、IFTTT、ThingSpeak 等服务都足够安全,无需考虑任何额外的安全性。

然而,自定义服务是关键。除了保护自定义服务之外,还必须建立一个安全框架,并提供基本的加密-解密服务。在处理这个项目时,我一直希望在整个框架中嵌入一个简单但安全层,因为它扩展了您可以共享的数据类型、您共享的服务以及服务处理这些受保护数据的能力。

互联网/分布式系统中通常有三种安全通信机制。1) 对称加密 2) 公钥加密和 3) 基于角色的加密。

公钥加密更常用且是自动化的。然而,在我们的例子中,我们希望在服务和客户端之间进行安全通信,我们将选择对称加密。这是一种机制,其中双方都知道用于加密和解密密钥,并且数据使用相同的密钥进行加密和解密。

因此,本节的目标将是使用 AES 来保护串行客户端和 UROBI 客户端之间交换的命令和响应。

12.2 AES加密与解密

AES 的详细细节超出了本教程的范围,我假设读者对该技术有基本了解。如果您想了解更多信息,可以随时参考这个AES 维基

我们在此部分的角色是演示如何在两端集成加密系统。

AES 是一种分组密码算法。它从用户那里获取一个密钥,并将其与一个随机的初始密钥组合以获得一个密码。然后将密码转换为二进制流,并逐块加密输入数据流。我使用了一个 16 字节的分组密码。棘手的地方在于,如果流的长度不能被块长度整除,则系统会在末尾填充额外的字符。这对于文本数据之类的东西来说是可以的,但对于其他类型的加密则会失败。因此,我调整了 AES 技术,使其没有额外的字符填充。最后一个块被加密到最后一个块的长度,而不是固定的 16 字节长度。这种调整使我能够对任何类型的加密使用 AES,并且我已经展示了文本和图像加密服务的机制。

这是我的 AES 加密代码实现,其中基本的字符串加密和解密是从互联网上提取并修改的。是的,您需要对现有代码进行一些工作,因为 AES 的结果将是 UTF 字符,而您希望小心地处理它们。

namespace EncryptStringSample
    {
        public static class StringCipher
        {
            // This constant string is used as a "salt" value for the PasswordDeriveBytes function calls.
            // This size of the IV (in bytes) must = (keysize / 8).  Default keysize is 256, so the IV must be
            // 32 bytes long.  Using a 16 character string here gives us 32 bytes when converted to a byte array.
            private const string initVector = "tu89geji340t89u2";

            // This constant is used to determine the keysize of the encryption algorithm.
            private const int keysize = 256;

            public static string Encrypt(string plainText, string passPhrase)
            {
                byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);
                byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
                PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
                byte[] keyBytes = password.GetBytes(keysize / 8);
                RijndaelManaged symmetricKey = new RijndaelManaged();
                symmetricKey.Mode = CipherMode.CBC;
                ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);
                MemoryStream memoryStream = new MemoryStream();
                CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                cryptoStream.FlushFinalBlock();
                byte[] cipherTextBytes = memoryStream.ToArray();
                memoryStream.Close();
                cryptoStream.Close();
                return Convert.ToBase64String(cipherTextBytes);
            }
            public static Bitmap  EncryptImage(Bitmap bmp, string passPhrase)
            {
                byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);

                MemoryStream ms = new MemoryStream();
                bmp.Save(ms, ImageFormat.Bmp);
                var header = ms.ToArray().Take(54).ToArray();
                //Take rest from stream
                var imageArray = ms.ToArray().Skip(54).ToArray();

                byte[] plainTextBytes = imageArray;
                PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
                byte[] keyBytes = password.GetBytes(keysize / 8);
                RijndaelManaged symmetricKey = new RijndaelManaged();
                symmetricKey.Mode = CipherMode.CFB;
                symmetricKey.Padding = PaddingMode.None;
                ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);
                MemoryStream memoryStream = new MemoryStream();
                CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                cryptoStream.FlushFinalBlock();
                byte[] cipherTextBytes = memoryStream.ToArray();
                memoryStream.Close();
                cryptoStream.Close();

               
                
                var image = Combine(header, cipherTextBytes);
                MemoryStream mstream = new MemoryStream(image);
                Bitmap b = (Bitmap)Bitmap.FromStream(mstream, true, false);
                //Bitmap b = new Bitmap(mstream);
                //return Convert.ToBase64String(cipherTextBytes);
                return b;
            }
            public static byte[] Combine(byte[] first, byte[] second)
            {
                byte[] ret = new byte[first.Length + second.Length];
                Buffer.BlockCopy(first, 0, ret, 0, first.Length);
                Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
                return ret;
            }

            public static Bitmap DecryptImage(Bitmap encBmp, string passPhrase)
            {
                byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);

                MemoryStream ms = new MemoryStream();
                encBmp.Save(ms, ImageFormat.Bmp);
                var header = ms.ToArray().Take(54).ToArray();
                //Take rest from stream
                var imageArray = ms.ToArray().Skip(54).ToArray();

                byte[] cipherTextBytes = imageArray;
                PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
                byte[] keyBytes = password.GetBytes(keysize / 8);
                RijndaelManaged symmetricKey = new RijndaelManaged();
                symmetricKey.Padding = PaddingMode.None;
                symmetricKey.Mode = CipherMode.CFB;
                ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, initVectorBytes);
                MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
                CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
                byte[] plainTextBytes = new byte[cipherTextBytes.Length];
                int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                memoryStream.Close();
                cryptoStream.Close();

                var image = Combine(header, plainTextBytes);
                MemoryStream mstream = new MemoryStream(image);
                Bitmap b = (Bitmap)Bitmap.FromStream(mstream, true, false);
                //return Convert.ToBase64String(cipherTextBytes);
                return b;

                //return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
            }
            public static string Decrypt(string cipherText, string passPhrase)
            {
                byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
                byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
                PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);
                byte[] keyBytes = password.GetBytes(keysize / 8);
                RijndaelManaged symmetricKey = new RijndaelManaged();
                symmetricKey.Mode = CipherMode.CBC;
                ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, initVectorBytes);
                MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
                CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
                byte[] plainTextBytes = new byte[cipherTextBytes.Length];
                int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                memoryStream.Close();
                cryptoStream.Close();
                return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
            }
        }
    }

因此,有四个重要方法:Encrypt、EncryptImage、Decrypt、DecryptImage。仔细观察会发现,EncryptImage 使用 Encrypt 方法的概念,而 DecryptImage 使用 Decrypt 方法的概念。

在图像加密中,首先将图像头部分离。然后加密数据,最后将头加回。因此,图像是可见的,并且可以被任何图像查看软件解码,只有内容被加密。Bmp 图像头有 54 字节。因此,它们在加密和解密之前被分离。

InitVector 被硬编码。您也可以将向量作为参数传递给方法。

这些方法非常直接且易于使用。只需传递数据和密码即可获得加密/解密的数据。

现在我们将这个类添加到 SerialClient 和 UROBI 客户端。我们希望在 URBOBI 客户端加密命令,并在 SerialClient 解密。

12.3 在UROBI框架中集成加密解密

在 UROBI 客户端方面,我们将在 Do_Work 中加密命令,然后再发送。因此,将使用 Encrypt。

 void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {
                
                //iot.InsertCommand(projId, ip, command, "UROBI Client", DateTime.Now, "EXECUTE");
                // Above is for Insecured Command Transmission
                iot.InsertCommand(projId, ip, EncryptStringSample.StringCipher.Encrypt(command, "integrated ideas"), "UROBI Client", DateTime.Now, "EXECUTE");
                // In Above Line we are sending Encrypted Command
            }
            catch
            {
            }
            //throw new NotImplementedException();
        }

在 Serial Client 中,我们将在命令接收和反序列化后对其进行解密。

    string[] result = iotClient.CommandToExecute(projId, ipAddress).Split(new char[] { '#' });
                if (result.Length < 3)
                {
                    timPollCommands.Enabled = true;
                    return;
                }
                string command = result[0];
                // For secured command transfer, the received command is encrypted. You need to decrypt
                command = EncryptStringSample.StringCipher.Decrypt(command,"integrated ideas").ToUpper();

请注意,密码被硬编码为“integrated ideas”,您可以随意更改!图 12.1 显示了如何在远程数据库中保存安全文本,使其难以解码。在 Serial Client 接收到命令后,该命令将被解密。

图 12.1:基于 AES 的物联网安全命令交换

趣味点:请注意,在当前上下文中无法使用此图像加密和解密服务进行安全图像通信,因为流服务器会将图像格式从 Bmp 更改为 MJPEG,因此无法在接收端解密图像。图像加密服务只能在 Bmp 图像的上下文中 M使用。但是,您可以在本地测试这些服务。您可以在一些创意中使用它们,同时 

12.4 关于其他服务的讨论

尽管物联网的上下文中存在无数重要的服务,但我必须承认,如果不具体讨论其中几个,本教程将是不完整的。首先是存储:我们已经看到了如何使用 ThingSpeak 存储设备中的数据。我们也使用了自定义框架来存储数据。但是如果您想存储文件呢?例如,您想将设备的数据附加到日志文件中?或者您想记录安全应用程序的关键帧?或者在 UROBI 客户端和串行模块之间交换文件呢?

如果您希望将此类工作流程集成到框架中,您可能需要将 DropBox 或 SkyDrive 服务集成到应用程序中。我很想在本次教程中涵盖其中一项,但教程的篇幅过大,使我无法将其扩展到一个存储服务,而这本身也将是一个相当长的子部分。但 SkyDrive 集成并非完全困难,您可以根据您的工作流程要求尝试一下。

其他重要的服务是认证。物联网支持各种智能对象。您可以提供传统的基于密码的认证,或者可以选择生物识别认证,甚至可以选择使用智能对象。在之前的教程中,我已经涵盖了一个这样的使用 RFID 对象进行认证。通过参考多面部检测和识别的教程,可以开发基于面部识别的认证。GSM 是另一个广泛用于通知和远程命令执行的服务。我个人多年来一直在毫无困难地使用这个GSM 调制解调器代码。该代码的优点是您可以将表单导入您的项目并运行它。GSM 调制解调器的价格范围在 20-30 美元之间,并且大多数都使用 RS232。您可以在 RFID 教程中了解如何将调制解调器与 USB 一起使用。

13. 结论

本教程旨在为 OWI 机器人套件开发一个很酷的机器人控制框架,更重要的是通过物联网。但人们常常认为物联网就是将设备连接到互联网并使用服务。然而,在实际工作中,存在各种约束和设计挑战。在这项工作中,我们构建了一个核心的物联网桥接器和客户端,用户可以通过该桥接器(Internet + 本地 LAN)控制 OWI 机器人手臂。

为了展示如何将其他本地服务无缝地添加到此框架中,我们还集成了计算机视觉技术用于机器人控制以及语音识别。由于许多控制系统也在硬件层面工作,我们使用了 IR 遥控器来控制机器人的一部分。

本教程完成了我关于物联网与 Arduino 的教程系列。将这项工作视为构建您超酷 DIY 机器人所需的所有可能服务的集合。物联网是一个拥有无限可能性的海洋。因此,与其声称完整,不如尝试触及框架中最重要的一些方面。在我之前的教程中,我已经涵盖了传感器集成,所以您可以将传感器连接到机器人板并将数据通过物联网扩散。

有时,编写一个接口来控制机器人(例如通过手势移动或通过互联网控制)是很容易的。但当如此多的服务和选项组合在一起时,系统就面临着性能挑战,以及它响应各种可能相互冲突的服务实时性的能力。

我希望本文能对那些正在从事物联网工作或想涉足物联网的人有所帮助。

 

© . All rights reserved.