内容简介

智能产品、穿戴设备、3D打印的出现，整个IT行业又开始了新一轮的冲击，软、硬结合符合现在行业需求。本书通过开源系统OpenWrt和Arduino，以及WiFi、NRF24I01（Zigbee/350/433/蓝牙等）无线模块，实现家居网关、电灯开关、无线插座、远程开门、窗帘开闭、红外摇控、温度调控、空气质量、烟雾火警、水滴检测、安防报警、WiFi摄像头、语音控制、Android客户端、3D打印产品的实战演示，任何个人、公司、机构都可以进行二次开发。

本书适合智能家居、智能硬件开发人员及爱好者阅读。

未经许可，不得以任何方式复制或抄袭本书之部分或全部内容。

版权所有，侵权必究。

图书在版编目（CIP）数据

智能家居DIY：OpenWrt＋Arduino＋Zigbee＋3D打印＋手机客户端／莫宏貌，梁依立，周国才编著．—北京：电子工业出版社，2015.8

ISBN 978-7-121-22481-2

Ⅰ．①智…　Ⅱ．①莫…②梁…③周…　Ⅲ．①住宅－智能建筑－研究　Ⅳ．①TU241

中国版本图书馆CIP数据核字（2015）第147442号

责任编辑：富　军

印　　刷：

装　　订：

出版发行：电子工业出版社

北京市海淀区万寿路173信箱　邮编　100036

开　　本：787×1092　1/16　印张：17.5　字数：448千字

版　　次：2015年8月第1版

印　　次：2015年8月第1次印刷

定　　价：68.00元

凡所购买电子工业出版社图书有缺损问题，请向购买书店调换。若书店售缺，请与本社发行部联系，联系及邮购电话：（010）88254888。

质量投诉请发邮件至zlts@phei.com.cn，盗版侵权举报请发邮件至dbqq@phei.com.cn。

服务热线：（010）88258888。

前言

随着越来越火热的智能产品、穿戴设备、3D打印的出现，整个科技行业又开始了新一轮的冲击，软、硬结合成了新潮流。

各大IT老大们开始进入智能家居的准备，小米、360、百度等研究团队都在争分夺秒地占据市场，时机成熟后，路由器、机顶盒都有可能成为家居控制中心（本书简称为家居网关）。

身为学生、技术、创客、极客的我们能在这场革命中获得什么呢？我们智能创客团队编写出了这本《智能家居DIY》，个人就可以DIY自己的智能家居。家居网关主要采用开源系统OpenWrt和Arduino，涉及WiFi、NRF24L01（Zigbee/350/433）等无线模块。写作本书的目的是为了学习、分享、创业等，为中国的创客精神奉献一些微薄之力，任何个人、公司、机构都可以进行二次开发。

本书提供了配件清单、原理图、源代码、软件下载、制作流程，如果不具备做板能力，也可以登录智能创客网站www.znck007.com直接购买。

同时，我们还扩展使用Android功能制作语音控制APP，3D打印模型外壳、源代码也全部奉献给大家。阅读本书可以实现家居网关、电灯开关、无线插座、远程开门、窗帘开闭、红外摇控、温度监控、空气质量、烟雾火警、水滴检测、人体红外探测器、WiFi摄像头、语音控制、Android客户端、3D打印模型等的DIY。

由于本书是以实战DIY教程为主，因此大多采用图文并茂，没有太多的理论知识，读者只需按本书的步骤实践，就能DIY出智能家居。

采用市场上使用最多的芯片、传感器、控制器等DIY下来的成本只有1000多元，也就是说，用这个对于家居来说是个极小的数目就能打造自己智能的家。

编著者

目录

前言

第1章　智能家居系统

1.1　OpenWrt系统

1.2　Arduino系统

1.3　NRF24L01无线模块

1.4　Zigbee模块

1.5　控制器、传感器

1.6　智能家居系统流程图

1.7　相关软件介绍

1.8　Web手机控制端

第2章　家居网关

2.1　WiFi核心板DIY

2.2　USB扩展板DIY

2.3　Arduino扩展板DIY

2.4　无线扩展板DIY

2.5　用OpenWrt刷固件

2.6　设置中文、时区、修改软件包源

2.7　设置sta+ap模式（上网+手机连接）

2.8　使用putty的SSH登录OpenWrt

2.9　TF卡挂载

2.10　使用winscp上传文件（上传下载源文件到OpenWrt）

2.11　安装配置php+mysql+uhttpd环境

2.12　安装mysql

2.13　使用NavicatForMysql软件新建mysql表

2.14　安装Python

2.15　Arduino网关代码编译（NRF24L01/ZIGBEE）

2.16　家居网关演示视频

第3章　电灯开关

3.1　用途

3.2　材料列表、原理图

3.3　继电器介绍

3.4　代码实例

3.5　实战演示

3.6　常见的问题及注意事项

第4章　无线插座

4.1　用途

4.2　智能开关

4.3　实战演示

4.4　常见问题及注意事项

第5章　远程开门

5.1　用途

5.2　材料列表、原理图

5.3　电子门锁

5.4　代码实例

5.5　实战演示

5.6　常见问题及注意事项

第6章　窗帘开关

6.1　用途

6.2　材料列表、原理图

6.3　无线遥控窗帘

6.4　代码实例

6.5　实战演示

6.6　常见问题及注意事项

第7章　红外遥控

7.1　用途

7.2　材料列表、原理图

7.3　红外原理

7.4　代码实例

7.5　实战演示

7.6　市面上的空调、电视解码

7.7　常见问题及注意事项

第8章　温湿监控

8.1　用途

8.2　材料列表、原理图

8.3　温度/湿度传感器说明

8.4　代码实例

8.5　实战演示

8.6　常见问题及注意事项

第9章　空气质量

9.1　用途

9.2　材料列表、原理图

9.3　PM2.5介绍

9.4　代码实例

9.5　实战演示

9.6　常见问题及注意事项

第10章　烟雾火警

10.1　用途

10.2　材料列表、原理图

10.3　MQ-2说明

10.4　代码实例

10.5　实战演示

10.6　常见问题及注意事项

第11章　水滴检测

11.1　用途

11.2　材料列表、原理图

11.3　水滴传感器说明

11.4　代码实例

11.5　实战演示

11.6　常见问题及注意事项

第12章　人体红外探测器

12.1　用途

12.2　材料列表、原理图

12.3　HC-SR501传感器

12.4　代码实例

12.5　实战演示

12.6　常见问题及注意事项

第13章　WiFi摄像头

13.1　用途

13.2　材料列表

13.3　配置摄像头

13.4　开机自动启动

13.5　端口映射

13.6　网页和手机访问

13.7　常见问题及注意事项

第14章　语音控制

14.1　用途

14.2　材料列表、原理图

14.3　语音识别技术

14.4　Android代码讲解

14.5　实战演示视频

14.6　常见问题及注意事项

第15章　Android客户端

15.1　用途

15.2　代码讲解

15.3　实战演示视频

15.4　常见问题及注意事项

第16章　3D打印模型

16.1　3D打印的作用

16.2　如何3D打印

16.3　3D模型源文件下载

16.4　常见问题及注意事项

第1章　智能家居系统

智能家居系统是以家居网关为中心，DIY各种智能模块，再利用3D打印技术打印漂亮的外壳，如图1-1所示。

图1-1　智能家居系统

在智能家居DIY之前，我们先介绍一些将要使用到的系统和模块。

1.1　OpenWrt系统

OpenWrt可以认为是一个嵌入式的Linux发行版，主要用于路由器系统。目前，市面上的智能路由器有些采用的就是该系统。由于OpenWrt是个开源系统，完全可能定制为我们需要的功能，所以我们的家居网关采用了这个系统。

OpenWrt从2004年开始至今，采用安装包模式，其文件系统是可写的，开发者无须在每一次修改后重新编译。它更像一个小型的Linux电脑系统，有3000多个软件包，新手也能直接使用，像在安卓手机中直接安装App应用一样简单。

如果对Linux系统有一定的认识，并想学习或接触嵌入式Linux的话，那么OpenWrt很适合。OpenWrt支持各种处理器架构，无论是对ARM、X86、PowerPC或者MIPS都有很好的支持。3000多种软件包囊括从工具链（toolchain），到内核（linux kernel），到软件包（packages），再到根文件系统（rootfs）整个体系，使得用户只需简单的一个make命令，即可方便快速地定制一个具有特定功能的嵌入式系统来制作固件。

OpenWrt采用Web、SSH、SCP等进行设置，如图1-2、图1-3、图1-4所示。

图1-2　OpenWrt系统的Web介绍

图1-3　OpenWrt系统的SSH登录界面

图1-4　OpenWrt系统的SCP登录界面

1.2　Arduino系统

Arduino系统包含两个部分，即硬件（各种型号的Arduino板）和软件（Arduino IDE），是一款便捷灵活、方便上手的开源电子原型平台，有庞大的用户群体和开源的类库，适合学生、创客、极客、电子爱号者等，如图1-5所示。因此，我们采用Arduino作为主控单片机，如图1-6、图1-7所示。

图1-5　Arduino的LOGO

图1-6　Arduino硬件图

图1-7　Arduino IDE软件图

Arduino IDE基于processing IDE开发，对于初学者来说极易掌握，同时有着足够的灵活性。Arduino语言基于wiring语言开发，是对AVRGCC库的二次封装，不需要太多的单片机基础、编程基础，简单学习后，就可以快速地进行开发。

Arduino IDE可以在Windows、Macintosh OSX、Linux三大主流操作系统上运行，而其他的大多数控制器只能在Windows上开发。

Arduino有着众多的开发者和用户，你可以找到它提供的众多开源示例代码、硬件设计。

Arduino不仅仅是全球最流行的开源硬件，也是一个优秀的硬件开发平台，更是硬件开发的趋势。因为Arduino的种种优势，所以越来越多的专业硬件开发者已经或开始使用Arduino来开发他们的项目、产品，越来越多的软件开发者使用Arduino进入硬件、物联网等开发领域，大学中的自动化、软件甚至艺术专业也纷纷开展了Arduino的相关课程。

1.3　NRF24L01无线模块

NRF24L01是由NORDIC生产的工作在2.4～2.5GHz的ISM频段的单片无线收发器芯片。由于简单易用，因此我们使用它来作为无线传输模块，如图1-8所示。

图1-8　NRF24L01无线模块

NRF24L01输出功率频道的选择和协议的设置可以通过SPI接口进行，126个通信通道，6个数据通道，满足多点通信和调频需要，如图1-9所示。由于价格已降为10元左右，因此性价比非常高。不足之处是穿墙能力不足，传输距离受到环境干扰较大，容易丢包。

图1-9　NRF24L01针脚图

1.4　Zigbee模块

Zigbee模块相对于NRF24L01来说传输距离更远，同时拥有中继功能，因此可以无限延伸，并拥有加密功能，使传输数据更安全，但成本比NRF24l01要高，所以我们将Zigbee作为无线传输的第二方案，如图1-10所示，针脚图如图1-11所示。当NRF24l01无法满足需求时，可使用Zigbee传输模块。

图1-10　Zigbee模块

图1-11　Zigbee针脚图

Zigbee是一种高可靠的无线数传网络，类似于CDMA和GSM网络。Zigbee数传模块类似于移动网络基站。通信距离从标准的75米到几百米、几千米，并且支持无限扩展。

Zigbee是一个由可多到65000个无线数传模块组成的一个无线数传网络平台，在整个网络范围内，每一个Zigbee网络数传模块之间可以相互通信，每个网络节点间的距离可以从标准的75米无限扩展。

Zigbee拥有自组织网功能，举一个简单的例子就可以说明这个问题。当一队伞兵空降后，每人持有一个Zigbee网络模块终端，降落到地面后，只要他们彼此间在网络模块的通信范围内，通过彼此自动寻找，很快就可以形成一个互联互通的Zigbee网络。而且，由于人员的移动，彼此间的联络还会发生变化。因而，模块还可以通过重新寻找通信对象，确定彼此间的联络，对原有网络进行刷新，这就是自组织网，如图1-12所示。

图1-12　Zigbee自组网网状结构

如果自己购买了Zigbee模块，则需要给模块刷写程序才能实现自组网功能，本章不再扩展说明，具体可以扫描图1-13或通过网址下载到PC了解。

图1-13　Zigbee自组网源代码下载二维码

网址http://book.znck007.com/?p=zizhuwangdaima

1.5　控制器、传感器

在我们DIY的各种功能模块中，大量用到控制器、传感器等，下面为大家介绍一些本书要用到的元件。

继电器是可以使用5V电压控制的220V电路元件，利用电磁闭合220V线路达到开关等效果，如图1-14所示。

图1-14　继电器

DHT11、DS18B20是可以获取温度和湿度的传感器，如图1-15所示。

图1-15　DHT11、DS18B20传感器

GP2Y1050AU灰尘传感器可以获取PM2.5的浓度值，如图1-16所示。

图1-16　GP2Y1050AU灰尘传感器

MQ-2气体传感器能获取到有害气体值，可以用于判断是否发生火灾、煤气泄漏等，如图1-17所示。

图1-17　MQ-2气体传感器

当下雨或漏水时，水滴传感器就会发出报警电位，如图1-18所示。

图1-18　水滴传感器

HC-SR501人体红外探测器如图1-19所示，常常被用于电灯自动开关，当发现有人接近时，灯就会打开。我们使用HC-SR501的这一功能来探测是否有人进入家中。

图1-19　HC-SR501人体红外探测器

1.6　智能家居系统流程图（见图1-20）

图1-20　智能家居系统流程图

1.7　相关软件介绍

1．Arduino IDE软件

可以扫描图1-21或通过网址下载到PC。

图1-21　Arduino IDE软件下载二维码

网址http://book.znck007.com/?p=arduinosoft

2．putty软件（登录OpenWrt系统）

可以扫描图1-22或通过网址下载到PC。

图1-22　putty软件下载二维码

网址http://book.znck007.com/?p=puttysoft

3．winscp软件（上传下载源文件到OpenWrt）

可以扫描图1-23或通过网址下载到PC。

图1-23　winscp软件下载二维码

网址http://book.znck007.com/?p=winscpsoft

4．NavicatForMysql软件

可以扫描图1-24或通过网址下载到PC。

图1-24　winscp软件下载二维码

网址http://book.znck007.com/?p=navicatformysqlsoft

1.8　Web手机控制端

使用云端服务器需搭建PHP+MYSQL+Web服务器，如图1-25所示。

图1-25　云端服务器PHP首页

整个源代码包含了MYSQL创建、api.znck007.com接口、api.znck007.com/mobile.php安卓客户端接口，会员管理等，共50多个源文件，如图1-26所示，在《智能创客》论坛上线运行一年，由于此篇涉及到PHP、Web SERVER等其他编程语言，篇幅过大，因此只提供源代码下载参考。

图1-26　云端源文件

可以扫描图1-27或通过网址下载到PC。

图1-27　Web手机控制端源文件下载二维码

网址http://book.znck007.com/?p=phpwebserverfile

第2章　家居网关

家居网关采用4个开发板，即WiFi核心板、USB扩展板、Arduino扩展板、无线扩展板，可实现连入互联网、控制无线收发功能，是一个控制中心，负责接收用户命令，并发送命令到指定的控制端。好比家居网关就是人脑，接收外界信息后控制身体动作。家居网关左面介绍如图2-1所示。家居网关右面介绍如图2-2所示。

图2-1　家居网关左面介绍

图2-2　家居网关右面介绍

我们DIY所用的WiFi核心板是市面上的703n（Ar9331芯片）路由器，如图2-3所示。通过DIY更换Flash和内存，再刷入OpenWrt系统，利用OpenWrt系统里的Python代码和Arduino扩展板串口通信，Arduino再控制nrf24L01、Zigbee等的无线收发，从而形成了我们DIY的家居网关。

图2-3　市面上的703n路由器

利用路由器做（或准备做）家居网关有很多的公司，小米路由器、极路由、百度路由器、360安全路由器，通过家用路由器功能抢占市场，时机成熟时将升级为具有家居网关的功能，控制家里的灯具、家电、门窗等，从而为每个家庭定制智能的家。这应该是未来智能家居的方向。

我们DIY家居网关的目的是DIY自己的智能家居网关，不仅能学习到各种知识，如Linux、OpenWrt、Arduino、nrf24L01、Zigbee等，还能提供智能家居的真实可行方案。

2.1　WiFi核心板DIY

我们从市场上购买的703n（AR9331芯片）路由器，拆开后，里面是一块WiFi核心板，如图2-4、图2-5所示。

图2-4　WiFi核心板正面

图2-5　WiFi核心板背面

下面介绍如何DIY改造成我们的WiFi核心板。

提醒

由于更换芯片需要非常细腻的焊功，亲自更换时需要多加小心，否则容易变砖，建议让专业人士更换。

1．材料列表（见表2-1）

表2-1　WiFi核心板改造材料列表

名称

型号

数量

备注

Ar9331芯片路由器

703N

1

家用迷你路由器

Flash芯片

W25Q128FVSG SOP8

1

16M

内存颗粒

HY5DU121622DTP-D43 TSOP66

1

64M

单排针

2.54

n

圆排针

2.54

n

2．刷写固件到Flash芯片

使用CH341A编程器（万能的某宝有卖，并会提供相应刷写软件教程等）将OpenWrt固件刷写到Flash里，如图2-6所示。

图2-6　CH341A编程器实物图

将16M的Flash芯片放入编程器中，如图2-7所示，安装驱动运行编程器软件，如图2-8所示，然后下载固件http://book.znck007.com/?p=openwrtflashbin进行刷写。

图2-7　CH341A编程器插入电脑USB口

图2-8　使用P1119编程

3．取下原Flash和内存

要使用专业工具电烙铁等取下内存，如图2-9所示，取下Flash芯片如图2-10所示。

图2-9　更换内存

图2-10　更换Flash芯片

4．焊上刷好的Flash芯片、新内存、脚针（见图2-11）

图2-11　焊上刷好的Flash芯片、新内存、针脚

完成后，上电，WiFi核心板就可以开机进入OpenWrt了。

2.2　USB扩展板DIY

USB扩展板在家居网关中起到很重要的作用，如图2-12所示，不仅提供多个USB口，而且还添加了TF卡读取，可以保存大量的程序文件和数据。

图2-12　USB扩展板介绍图

USB扩展板原理图如图2-13所示。

图2-13　USB扩展板原理图

1．材料列表（见表2-2）

表2-2　USB扩展板材料列表

名称

型号

数量

备注

USB HUB芯片

FE1.1S

1

主芯片

TF读卡芯片

GL823F

1

晶振

12M

1

10μF电容

0603

4

10μF电容

1206

4

0.1μF电容

0603

8

贴片led（红）

0805

1

贴片电阻470Ω

0603

1

贴片电阻100kΩ

0603

3

贴片电阻47kΩ

0603

1

贴片电阻2.7kΩ

0603

1

贴片电阻10kΩ

0603

1

TF自弹式卡座

内焊式

1

USB-A型接口

母座

2

90°弯脚

迷你USB贴片

母座

1

mini-USB

圆排母

2.54

n

方排母

2.54

n

圆排针

2.54

n

2．根据原理图焊接

通过原理图2-13画好线路板，如今广东PCB样板的厂家很多，最低达到50元就可以打样板，然后按表2-2的材料焊接，就能做成如图2-14所示的样板了。

图2-14　焊接好的USB扩展板

提示

USB扩展板也可以单独使用，使用USB线插入电脑，就可以识别TF卡和当USB HUB使用了。

2.3　Arduino扩展板DIY

Arduino扩展板采用的是ATMEG644PA芯片，拥用64K Flash，并有两个串口，Serial用于与OpenWrt串口通信，Serial1用于与Zigbee串口通信，与nrf24L01则使用SPI通信，如图2-15、图2-16所示。

图2-15　Arduino扩展板介绍图

图2-16　Arduino扩展板原理图

1．材料列表（见表2-3）

表2-3　Arduino扩展板材料列表

名称

型号

数量

备注

ATMEGA芯片

644PA-20AU

1

Arduino芯片，64K双串口

CH340T

T

1

TTL转串口芯片

晶振

16M

1

晶振

12M

1

SS34贴片肖特基二极管

1N5822

1

贴片XC6206P332MR (662K)

SOT-23

1

3.3V/0.5A稳压芯片

10μF电容

1206

3

1μF电容

0603

1

0.1μF电容

0603

8

0.01μF电容

0603

1

22pF电容

0603

2

20pF电容

0603

2

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1.5kΩ

0603

1

贴片电阻470Ω

0603

4

贴片led（红）

0805

1

贴片led（蓝）

0805

1

贴片led（黄）

0805

2

迷你USB贴片

母座

1

mini-USB

按键贴片

3*6*2.5mm

1

微动开关

圆排母

2.54

n

2．根据原理图焊接

通过原理图2-16画好线路板，焊上表2-3中的元件，焊接好的Arduino扩展板如图2-17所示。

图2-17　焊接好的Arduino扩展板

提示

（1）Arduino在使用前请先用usbasp烧写bootloader。

（2）Arduino扩展板也可以单独使用，使用Arduino IDE下载程序，与普通Arduino板使用一样，支持大量Arduino类库，有两个串口Serial/Serial1。

2.4　无线扩展板DIY

无线扩展板的作用就是将nrf24L01、Zigbee等无线模块与Arduino扩展板连接，由Arduino控制无线的发射，接收和控制其他无线终端设备，从而形成家居网关的概念，如图2-18所示。

图2-18　无线扩展板介绍图

1．材料列表（见表2-4）

表2-4　无线扩展板材料列表

名称

型号

数量

备注

贴片电阻1.5kΩ

0603

1

0.1μF电容

0603

1

长脚排针

2.54

n

双排排针

2.54

n

单排排针

2.54

n

双排排母

2.54

n

单排排母

1.27

n

2．无线扩展板叠加

无线扩展板与Arduino扩展板在图2-16中已有接线原理图。

提示

按如图2-19所示叠加后也可以单独使用，插上nrf24L01和Zigbee就是一个没连入互联网的家居网关，如果不使用OpenWrt的话，则在PC上编写一个串口通信程序，也可以达到连入互联网，因此怎么玩儿由自己的想法决定。

图2-19　无线扩展板与Arduino扩展板叠加

2.5　用OpenWrt刷固件

1．网线将电脑和WiFi核心板直接连接

提醒

如果是笔记本电脑，则没有网线口，请购买一个USB转RJ45网线，如图2-20所示。

图2-20　四板叠加的家居网关连线

将自己的电脑IP改成192.168.1.164（150～255之间），电脑网关改成192.168.1.1，如图2-21所示。

图2-21　设置电脑的IP和网关

2．使用浏览器打开192.168.1.1

按如图2-22所示，输入用户名root，密码znck007（或者为空）。

图2-22　OpenWrt首页

3．下载固件文件

可以扫描图2-23或通过网址下载到PC。

图2-23　固件下载二维码

网址http://book.znck007.com/?p=gujian3_10_49

4．进入刷固件页面，如图2-24所示

图2-24　刷写新固件页面

5．确定刷固件（见图2-25）

图2-25　确定刷固件

如图2-26所示，需等1分钟左右，浏览器会自动跳转到192.168.1.1，说明固件刷成功了！

图2-26　确定刷固件

提示

在刷固件过程中，切记不要断电，否则会变砖。

2.6　设置中文、时区、修改软件包源

刷完固件后，跳转到首页，如图2-27所示，无须填写密码，直接单击登录。

图2-27　OpenWrt英文首页

1．设置中文，如图2-28、图2-29所示

图2-28　设置中文显示

图2-29　单击保存&应用

2．设置时区，如图2-30所示

图2-30　设置时区

3．修改编译源，如图2-31所示（用于安装各种驱动和软件）

图2-31　修改编译源

在软件包里，将src/gz开头的删除，然后添加src/gz attitude_adjustment_12.09_rc2 http://downloads.openwrt.org/attitude_adjustment/12.09/ar71xx/nand/packages再保存/应用。

2.7　设置sta+ap模式（上网+手机连接）

1．打开无线，搜索家里的WiFi，如图2-32所示

图2-32　搜索家里的WiFi

2．加入到家里的WiFi，如图2-33、图2-34、图2-35所示

图2-33　加入到家里的WiFi

图2-34　输入WiFi密码

图2-35　单击保存&应用

提交后，一定记得保存&应用，才会生效哦！

3．设置ap模式，提供给手机直接连接

单击添加，如图2-36所示。

图2-36　单击添加

设置WiFi名称，这里就叫znck007吧，如图2-37所示，记得选择lan打勾，很重要！

图2-37　设置WiFi名称

设置WiFi的密码后，手机连接就要输入密码了，如图2-38所示。

图2-38　设置WiFi密码

保存&应用成功，就出现这两个列表了，如图2-39所示。

图2-39　设置成功列表

提示

除了可以sta连接其他的WiFi，也可以使用PPPoE拨号上网/固定IP上网等，可代替家用路由器，同时充当家居网关的功能。

如果发现连上但无法上网，请尝试设置本地运营商指定的DNS。

4．设置OpenWrt的DHCP（自动分配IP），如图2-40、图2-41所示。

图2-40　单击修改LAN

图2-41　设置DHCP选项

2.8　使用putty的SSH登录OpenWrt

1．设置OpenWrt密码（有密码才能用ssh远程连接）

设置密码znck007，如图2-42、图2-43所示。

图2-42　单击系统管理权

图2-43　设置登录密码

2．使用putty登录OpenWrt

连接192.168.1.1，选择SSH登录OpenWrt，如图2-44所示。

图2-44　使用putty登录OpenWrt

输入登录名root和密码，如图2-45所示。

图2-45　输入登录名和密码

登录成功界面如图2-46所示。

图2-46　登录OpenWrt成功

2.9　TF卡挂载

TF卡挂载之后，在TF卡内存中保存php文件、脚本文件、数据文件等，首次需要opkg update更新软件包，如图2-47所示。

图2-47　opkg update更新软件包

1．安装挂载软件库（如图2-48所示）和挂载

图2-48　安装挂载软件库

opkg update

opkg install kmod-usb-storage block-mount block-hotplug kmod-fs-ext4

mkfs.ext4 /dev/sda1    #格式化TF卡，第一次使用就格式吧

mkdir -p /mnt/sda1    #生成一个TF卡目录

mount /dev/sda1 /mnt/sda1    #将TF卡挂载到/mnt/sda1目录

2．自动挂载

putty输入vi/etc/config/fstab，将打开fstab文件。

光标移到最下面，按i键（编辑模式），添加如下配置后，按Esc键→：键→X键→回车，保存退出，如图2-49所示。

config 'mount'

option 'device' '/dev/sda1'

option 'options' 'rw,sync'

option 'enabled_fsck' '0'

option 'enabled' '1'

option 'target' '/mnt/sda1'

图2-49　添加自动挂载配置

2.10　使用winscp上传文件（上传下载源文件到OpenWrt）

可以扫描图2-50或通过网址下载到PC。

图2-50　PHP文件、python脚本、自启动脚本等下载二维码

网址http://book.znck007.com/?p=serverfile

使用winscp登录OpenWrt，如图2-51所示。

图2-51　使用SCP协议登录OpenWrt

登录成功界面如图2-52所示。

图2-52　winscp登录成功

在/mnt目录新建sda1/www目录，将程序文件上传文件到OpenWrt，如图2-53所示。

图2-53　上传文件到OpenWrt

右击www目录，设置777权限，否则无法访问脚本文件，如图2-54所示。

图2-54　给www设置777权限

2.11　安装配置php+mysql+uhttpd环境

1．安装PHP到TF卡，如图2-55所示

图2-55　安装PHP到TF卡

mkdir -p /mnt/sda1/php/ #新建软件包目录

echo dest phpdisk /mnt/sda1/php/ >> /etc/opkg.conf  #添加到opgk配置文件

opkg --dest phpdisk install php5-fastcgi php5-mod-gd php5-mod-xml php5-mod-ctype

php5-mod-session php5-mod-sockets php5-mod-tokenizer php5-mod-mcrypt php5-mod-mbstring

php5-mod-pdo php5-mod-curl php5-mod-mysql

opkg install zoneinfo-asia #安装一个时区信息软件包，否则系统无法识别时区。

2．映射TF卡的库文件，如图2-56所示

图2-56　映射TF卡的库文件

建立软连接，否则会找不到对应的库和配置文件：

ln -s /mnt/sda1/php/etc/php.ini /etc/php.ini

ln -s /mnt/sda1/php/etc/php5 /etc/php5

ln -s /mnt/sda1/php/usr/lib/libpcre.so.0.0.1 /usr/lib/libpcre.so.0

ln -s /mnt/sda1/php/usr/lib/libpcreposix.so.0.0.0 /usr/lib/libpcreposix.so.0

ln -s /mnt/sda1/php/usr/lib/libsqlite.so.0.8.6 /usr/lib/libsqlite.so.0

ln -s /mnt/sda1/php/usr/lib/libsqlite3.so.0.8.6 /usr/lib/libsqlite3.so.0

ln -s /mnt/sda1/php/usr/lib/libxml2.so.2.7.8 /usr/lib/libxml2.so.2

ln -s /mnt/sda1/php/usr/lib/libz.so.1.2.3 /usr/lib/libz.so

ln -s /mnt/sda1/php/usr/lib/libuClibc++-0.2.4.so /usr/lib/libuClibc++.so.0

ln -s /mnt/sda1/php/usr/lib/libmysqlclient.so.16 /usr/lib/libmysqlclient.so.16

ln -s /mnt/sda1/php/usr/lib/php /usr/lib/php

ln -s /mnt/sda1/php/usr/bin/php-cgi /usr/bin/php-cgi

3．修改php.ini文件

使用winscp打开目录/mnt/sda1/php/etc/，右击编辑php.ini，如图2-57所示。

图2-57　右击编辑php.ini文件

找到下面的字段修改为如下，如果有用";"注释的就删掉：

short_open_tag = On

error_log = /var/log/php_errors.log

doc_root = "/mnt/sda1/www"  #修改成TF卡的路径。

extension=ctype.so

extension=curl.so

extension=dom.so

extension=exif.so

extension=ftp.so

extension=gd.so

extension=gmp.so

extension=hash.so

extension=iconv.so

extension=json.so

extension=ldap.so

extension=mbstring.so

extension=mcrypt.so

extension=mysql.so

extension=openssl.so

extension=pcre.so

extension=pdo.so

extension=pdo-mysql.so

extension=pdo-pgsql.so

extension=pdo_sqlite.so

extension=pgsql.so

extension=session.so

extension=soap.so

extension=sockets.so

extension=sqlite.so

extension=sqlite3.so

extension=tokenizer.so

extension=xml.so

extension=xmlreader.so

extension=xmlwriter.so

date.timezone = Asia/Shanghai

mysql.default_socket = /var/run/mysqld.sock

session.auto_start = 1

4．配置uhttpd

一样使用winscp打开目录/etc/config，右击打开uhttpd，按如图2-58所示修改、添加。

图2-58　右击编辑uhttpd文件

option home '/mnt/sda1/www' #把/www修改成/mnt/sda1/www为放置网页的目录。

option index_page index.php                  #添加

list interpreter ".php=/usr/bin/php-cgi"     #添加

然后重启uhttpd（在putty里输入命令回车）

/etc/init.d/uhttpd restart

5．测试php是否安装成功

打开http://192.168.1.1/znck007/phpinfo.php，如果看到php的配置信息，就说明php安装成功了，如图2-59所示。

图2-59　php的配置信息

2.12　安装mysql

1．使用putty安装mysql

opkg install mysql-server

2．使用winscp编辑/etc/my.cnf，如图2-60所示

图2-60　右击编辑my.cnf文件

查找到以下内容并修改为

datadir = /mnt/sda1/mysql/data/mysql/

tmpdir  = /mnt/sda1/mysql/data/tmp/

bind-address = 192.168.1.1

3．使用putty安装创建数据库目录，如图2-61所示

图2-61　新建mysql文件目录

mkdir -p /mnt/sda1/mysql/data/mysql

mkdir -p /mnt/sda1/mysql/data/tmp

4．使用putty初始化数据库，如图2-62所示

图2-62　初始化数据库、设置密码等

#初始化建库

mysql_install_db --force

#以关闭授权的方式启动mysql

mysqld --skip-grant &

#进入mysql，修改账号连接权限

mysql -u root mysql

#进入mysql以后出现mysql>提示符，再修改账号连接权限：

update user set host='%' where user='root' and host='localhost';

#修改数据库密码为znck007

update user set password=PASSWORD('znck007') where user='root';

#刷新数据库

flush privileges;

#退出mysql

exit

5．使用putty重启mysql

killall mysqld  #停mysqld

/etc/init.d/mysqld enable  #开机运行

/etc/init.d/mysqld start  #再启动MySQL服务

2.13　使用NavicatForMysql软件新建mysql表

输入如图2-63所示的信息确定，密码为znck007（之前安装mysql设置的）。

图2-63　使用NavicatForMysql连接mysql

新建一个数据库，如图2-64所示。

图2-64　新建一个数据库

输入数据库名称、编码，确定后，就新建了znckapi数据库，如图2-65所示。

图2-65　输入数据库名称

再新建表来存储数据，右击“新建查询”执行sql语句，如图2-66所示。

图2-66　右击“新建查询”

输入下面的代码，然后单击运行，如图2-67所示。

CREATE TABLE IF NOT EXISTS `api_worklist` (

`id` int(16) unsigned NOT NULL AUTO_INCREMENT,

`type` int(2) NOT NULL COMMENT '1网设2上传3定时',

`uid` int(8) NOT NULL,

`sid` varchar(3) NOT NULL,

`nid` varchar(3) NOT NULL,

`data` varchar(32) NOT NULL,

`note` varchar(64) NOT NULL,

`status` int(2) NOT NULL COMMENT '1成功2失败3超次数4超15分',

`time` datetime NOT NULL,

`ip` varchar(16) NOT NULL,

`num` int(2) NOT NULL,

PRIMARY KEY (`id`),

KEY `uid` (`uid`),

KEY `time` (`time`)

) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

图2-67　执行sql建表代码

刷新一下，表就出现了，双击就可以查看表数据了，如图2-68所示。

图2-68　刷新查看生成的表

api_worklist表字段定义：

sid模块类型：如001代表网关，可以由你自己定义代表什么。

nid模块编号：如002代表同类型的第二个模块等，可以由你自己定义。

data操作数据：一般0代码关闭，1代表开灯，1～255代表从1升到255等，也由你定义。

status当前状态：如我们定义0未处理、1成功、2失败、3次数、4超15分。

这些字段数据对应我们无线传输的{ck00x00x000000}等是一样的道理。

{ck}是代表我们的标示，00x是模块类型，00x是模块编号，后面就是操作数据。

2.14　安装Python

1．安装Python相应类库，如图2-69所示。

图2-69　将Python安装到TF卡

mkdir -p /mnt/sda1/python/  #新建软件包目录

echo dest pythondisk /mnt/sda1/python/ >> /etc/opkg.conf  #添加到opgk配置文件

opkg --dest pythondisk install python  #将python安装到TF卡

ln -s /mnt/sda1/python/usr/bin/python /usr/bin/python  #为python创建快捷方式，如省略此步，

需键入 /mnt/sda1/python/usr/bin/python 才能启动python

opkg --dest pythondisk install pyserial   #安装串口通信类

opkg --dest pythondisk install python-mysql  #安装mysql操作类

2．编写Python脚本（串口通信、读写、更新、删除MYSQL数据库）

在/mnt/sda1/www/cgi-bin里的znckgbrun就是python脚本，记得给它设置权限：属性-》都打勾，如图2-70所示。

图2-70　右击编辑Python脚本znckgbrun

#!/usr/bin/python

# -*- coding: utf-8 -*-

import cgi, os

import commands

import serial

import urllib,urllib2

import time

import MySQLdb

apikey='你的Key' #云服务器key，改成自己的Key（可以到http://m.znck007.com/免费申请，本书结尾提供源代码下载）

apiurl='http://api.znck007.com' #云服务器网址，也可以建自己的云服务器

isyunserver=3 #1只使用本地服务器 2只使用云服务器 3本地和云服务器一起使用

logpath='/mnt/sda1/www/cgi-bin/znckbgrun' #日志保存路径

dogpath='/mnt/sda1/www/cgi-bin/znckdogtime' #看门狗的保存路径

#GET数据

def BaseGet(url,data):

try:

url_values=urllib.urlencode(data)

full_url=url+'?'+url_values

response=urllib.urlopen(full_url)

result=response.read()

#print repr(result)

return result

except:

print "BaseGet except:"

WriteLog(logpath,"BaseGet except:") #写日志

return ""

#POST数据

def BasePost(url,data):

try:

postData = urllib.urlencode(data);

req = urllib2.Request(url, postData);

req.add_header('Content-Type', "application/x-www-form-urlencoded");

resp = urllib2.urlopen(req);

code = resp.getcode()

result=resp.read()

#print repr(result)

return result

except:

print "BasePost except:"

WriteLog(logpath,"BasePost except:") #写日志

return ""

#获取云服务器数据

def ZnckApiGetData():

try:

#获取PHP交互数据

postdata = {}

postdata['data']='{ck001000000000}'

postdata['apikey']=apikey

url=apiurl

repost=BasePost(url,postdata)

print repost

return repost

#结束获取PHP交互数据

except:

print "ZnckApiGetData except:"

WriteLog(logpath,"ZnckApiGetData except:") #写日志

return ""

def ZnckApiPostData(value):

try:

value0="%03d" % int(value[0].strip()) #转成00x格式

value1="%03d" % int(value[1].strip()) #转成00x格式

valueall='{ck'+value0+value1+value[2]+'}'

#获取PHP交互数据

postdata = {}

postdata['data']=valueall

postdata['apikey']=apikey

url=apiurl

repost=BasePost(url,postdata)

print repost

return repost

#结束获取PHP交互数据

except:

print "ZnckApiPostData except:"

WriteLog(logpath,"ZnckApiPostData except:") #写日志

return ""

#获取mysql数据

def SelectWorklist():

try:

#连接数据库

conn=MySQLdb.connect(host='192.168.1.1',user='root',passwd='znck007',port=3306)

cur=conn.cursor()

conn.select_db('znckapi')#选择数据库名

count=cur.execute('SELECT * FROM api_worklist WHERE status=0 ORDER BY id

DESC LIMIT 0,1') #最后1条

#print "count="+str(count)

repost=""

results=cur.fetchall()

if results :

for r in results:

#print str(r[0])+str(r[1])+str(r[2])+str(r[3])+str(r[4])+str(r[5])+str(r[6])

value0="%03d" % int(r[3].strip()) #转成00x格式

value1="%03d" % int(r[4].strip()) #转成00x格式

repost='{ck'+value0+value1+str(r[5])+'}'

snvalue=[value0,value1]

if(r[10]>5):#如果num字段大于5则设为超时

cur.execute('UPDATE api_worklist SET status=3 where sid=%s and

nid=%s and status=0',snvalue)

#更新调用次数，用来判断设备是否

cur.execute('UPDATE api_worklist SET num=num+1 where sid=%s and

nid=%s and status=0',snvalue)

#如果是红外摇控只获取一次，否则可能有重复发射现象。

conn.commit()#提交

cur.close()#关闭

conn.close()#关闭

return repost

except MySQLdb.Error,e:

print "Mysql Error %d: %s" % (e.args[0], e.args[1])

WriteLog(logpath,"Mysql Error %d: %s" % (e.args[0], e.args[1])) #写日志

except:

print "SelectWorklist except..."

WriteLog(logpath,"SelectWorklist except...") #写日志

return ""

#添加数据到mysql

def InsertWorklist(value):

try:

#连接数据库

conn=MySQLdb.connect(host='192.168.1.1',user='root',passwd='znck007',port=3306)

cur=conn.cursor()

conn.select_db('znckapi')#选择数据库名

now=time.strftime('%Y-%m-%d %H:%M:%S')#当前时间

value0="%03d" % int(value[0].strip()) #转成00x格式

value1="%03d" % int(value[1].strip()) #转成00x格式

snvalue=[value0,value1]

cur.execute('DELETE FROM api_worklist where type=2 and sid=%s and nid=%s and

status>0',snvalue) #删除相同数据

invalue=[2,0,value0,value1,value[2],"upload data",1,now,"192.168.1.1",0]

cur.execute('INSERT INTO api_worklist(type,uid,sid,nid,data,note,status,time,ip,num)

ALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)',invalue) #插入数据

#更新执行成功{ck00x00xupdate}格式

if( value[2]= ='update' ):

cur.execute('UPDATE api_worklist SET status=1 where sid=%s and

nid=%s',snvalue)

conn.commit()#提交

cur.close()#关闭

conn.close()#关闭

except MySQLdb.Error,e:

print "Mysql Error %d: %s" % (e.args[0], e.args[1])

WriteLog(logpath,"Mysql Error %d: %s" % (e.args[0], e.args[1])) #写日志

except:

print "InsertWorklist except..."

WriteLog(logpath,"InsertWorklist except...") #写日志

return ""

#写日志

def WriteLog(file,value):

try:

file=file+time.strftime('%Y-%m-%d')+".log" #生成log文件名称

now=time.strftime('%Y-%m-%d %H:%M:%S')#当前时间

f = open(file,'ab')

f.write(now+":"+value+"\r\n")

f.close()

except:

print "except WriteLog..."

def Writedogtime(file,value):

try:

f = open(file,'w')

f.write(value)

f.close()

except:

print "except Writedogtime..."

#解析{ck00x00x000000}格式

def ResolveZnck(data):

if( data.find('{ck')>=0 & data.find('}')>=0 ): #如果是'{ck00x00x000000}'格式

start=data.find('{ck'); #定位字符开始位置

end=len(data); #定位字符长度

data_t=data[start:end]; #截取正确的格式

data_t= data_t.replace('\r','') #替换回车换行

data_t= data_t.replace('\n','') #替换回车换行

sid=data_t[3:6];

nid=data_t[6:9];

data_=data_t[9:data_t.find('}')];

value=[sid,nid,data_]

return value;

#以下代码在后台一直运行

cmdstring = "ls /dev/ttyUSB0" #判断是否是ttyUSB0

(status, output) = commands.getstatusoutput(cmdstring)

if( status = = 0 ):#0代表成功 大于0是各种错误

print "ttyUSB0 ready..."

ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1) #打开串口通信

#ser.write("lABC") #向串口发送字符

else:

cmdstring = "ls /dev/ttyUSB1" #判断是否是ttyUSB1

(status, output) = commands.getstatusoutput(cmdstring)

if( status = = 0 ):#0代表成功 大于0是各种错误

print "ttyUSB1 ready..."

ser = serial.Serial('/dev/ttyUSB1', 115200, timeout=1) #打开串口通信

#ser.write("lABC") #向串口发送字符

else:

print "ttyUSB check error..."

WriteLog(logpath,"ttyUSB check error...") #写日志

#exit()

cmdstring = "rm -rf "+dogpath #删除旧的看门狗文件

(status, output) = commands.getstatusoutput(cmdstring)

sendcount=0 #记录循环次数

errorcount=0 #记录错误次数

while True:

try:

data=ser.readline() #读取一行 读取1个字符read(1)

if( data.find('{ck')>=0 and data.find('}')>=0 ): #如果是'{ck00x00x000000}'格式

data= data.replace('\r','') #替换回车换行

data= data.replace('\n','') #替换回车换行

datavalue=ResolveZnck(data)

#使用本地服务器

if(isyunserver= =1 or isyunserver= =3):

InsertWorklist(datavalue)

#使用云服务器

if(isyunserver= =2 or isyunserver= =3):

ZnckApiPostData(datavalue)

if data!="" :

#print "serial data <<<"+data

WriteLog(logpath,data) #写日志

continue

sendcount=sendcount+1 #计数器加1

if( sendcount%1 = =0 ): #间隔1次执行

#使用本地服务器

if(isyunserver= =1 or isyunserver= =3):

repost=SelectWorklist()

if( repost.find('{ck')>=0 and repost.find('}')>=0 ):

#如果是'{ck00x00x000000}'格式

ser.write(repost)

#print "serial write >>>"+repost

time.sleep(2)

#使用云服务器

if(isyunserver= =2 or isyunserver= =3):

repost=ZnckApiGetData()

if( repost.find('{ck')>=0 and repost.find('}')>=0 ): #如果是'{ck00x00x000000}'格式

ser.write(repost)

#print "serial write >>>"+repost

time.sleep(2)

#print 'sendcount='+str(sendcount)

if( sendcount>10000 ):

sendcount=0 #初始化为0

Writedogtime(dogpath,time.strftime('%Y-%m-%d %H:%M:%S'))

except Exception,e:

errorcount=errorcount+1 #计数器加1

#print 'errorcount='+str(errorcount)

#WriteLog(logpath+"err","Exception,errorcount="+str(errorcount))

print "except Error %s" % (e)

WriteLog(logpath+"err","except Error %s" % (e)+str(errorcount)) #写日志

time.sleep(1)

ser.close()

提示

znckgbrun脚本的原理：

（1）连接Arduino串口，并时时获取数据。

（2）当发现有数据时，则写入mysql（一般用来保存传感器数据）。

（3）每一秒可获取mysql或云端服务器未处理的数据（一般为网页/App等插入的命令数据）。

（4）如果有未处理的数据，则串口发送到Arduino。

（5）Arduino处理命令后，通过Zigbee、nrf 24l01发送到控制端。

关于云端服务器apikey说明：

（1）手机或Chrome浏览器打开云端服务器m.znck007.com。

（2）免费注册一个账号，登录获取apikey。

（3）在脚本znckgbrun修改成自己的apikey时，使用UTF-8编码保存，见图2-71。

图2-71　使用UTF-8编码查看脚本代码

本书第1章1.8节提供云端服务器全部源代码下载，可以塔建自己的云端服务器。

3．测试脚本是否运行成功，如图2-72所示

图2-72　运行python脚本

python /mnt/sda1/www/cgi-bin/znckbgrun #直接运行

如图2-73所示，说明脚本正常运行了！

图2-73　查看串口获取到的数据

提示

apikey改成自己注册的apikey（前往http://m.znck007.com注册）

关闭请先按Ctrl+Z，然后输入

killall -9 python

killall -9 python

二次killall确保完全关闭python

4．实现python自动启动脚本

如图2-74所示，使用winscp上传znckserver、znckdog文件到/etc/init.d。

图2-74　上传自启动文件到/etc/init.d目录

设置znckserver、znckdog成777权限如图2-75所示。

图2-75　设置znckserver、znckdog成777权限

znckserver自启动脚本代码如下：

#!/bin/sh /etc/rc.common

# Copyright (C) 2013 OpenWrt.org

START=99

start() {

python /mnt/sda1/www/cgi-bin/znckbgrun &

}

stop() {

killall -9 python

}

输入以下命令，开机启动和马上运行，如图2-76所示。

/etc/init.d/znckserver enable

/etc/init.d/znckserver start

图2-76　开机启动、马上运行

#如果要停止，请参考以下代码。

/etc/init.d/znckserver disable  #禁止启动时自动运行

/etc/init.d/znckserver stop  #停止znckserver

提示

znckdog是防脚本假死服务，启动方式和znckserver是一样的：启动/etc/init.d/znckdog enable和运行/etc/init.d/znckserver start。

切记：一定要调试好所有的程序并能正常运行后才能启动znckdog（否则，会让网关不断重启），防死机只是为了能365天长期运行才要启动。

2.15　Arduino网关代码编译（NRF24L01/ZIGBEE）

可以扫描图2-77或通过网址下载到PC。

图2-77　Arduino网关的代码下载二维码

网址http://book.znck007.com/?p=wangguanfile

1．安装CH340驱动

在Arduino软件里的drivers目录里找到相应的CH340驱动，如图2-78所示。

图2-78　查找CH340驱动程序

单击Next按钮安装驱动，如图2-79所示，直到完成。

图2-79　安装CH340驱动

2．拆下Arduino扩展板和无线扩展板直接USB连接电脑

由于我们先要调试好NRF2401/Zigbee通信，所以单独摘出来直接USB连接电脑调试，如图2-80所示。

图2-80　拆下Arduino扩展板和无线扩展板

3．网关源代码的编译

使用Arduino ide软件打开下载的网关源代码，如图2-81所示。

图2-81　Arduino ide软件打开网关代码

选择板znck007 arduino Core+ (Atmega644PA@16M,5V)如图2-82所示。

图2-82　选择Atmega644PA板

提示

官方最新版本的Arduino ide是没有这个板的，如果要添加，则复制Arduino软件的hardware目录里的znckarduino目录，粘贴到官方软件的hardware目录里。

选择正确的COM口，如图2-83所示，COM口可以在设备管理器中查看。

图2-83　选择Arduino的串口

编译代码下载到Arduino板，显示Done uploading表示成功，如图2-84所示。

图2-84　单击编译下载到Arduino板

按Ctrl+Shift+M启动串口监视窗口，会看出相关数据输出，如图2-85所示，那么源代码已经正确刷进Arduino板了，可以与NRF24l01通信了（也与Zigbee串口通信）。

图2-85　串口监视

4．源代码的说明和讲解

如图2-86所示，网关我们将sid默认设置为1，nid为1，用于区别不同的设备。

图2-86　网关源代码说明

代码最关键的三个部分：

（1）OpenWrt串口通信处理；

（2）无线串口通信处理（Zigbee/bluetooth等）；

（3）NRF24l01通信处理。

如图2-87所示，在setup里初始化NRF24L01通信。

图2-87　初始化NRF24L01

如图2-88所示，监视NRF24L01是否有数据，如果发现有，则开始读取。

图2-88　监视NRF24L01数据

如图2-89所示，如果为{ck00x00x000000}格式的数据，则发送到OpenWrt，由OpenWrt保存到数据库或POST到云端服务器。

图2-89　发送数据到OpenWrt串口

如果OpenWrt串口发来数据，格式为{ck00x00x000000}，则调用如图2-90所示的方法，发送数据到指定终端，从而实现了家居网关的功能。

图2-90　发送函数

5．家居网关叠加上电运行，如图2-91所示

图2-91　家居网关叠加效果图

家居网关运行日志如图2-92所示。

图2-92　家居网关运行日志

2.16　家居网关演示视频

可以扫描图2-93或通过网址查看家居网关演示视频。

图2-93　家居网关演示视频二维码

网址http://book.znck007.com/?p=wangguanshipin

第3章　电灯开关

电灯开关是最常用的开关，一般在家居装修时就确定了它的位置，而我们DIY则是在原来普通的家用电灯开关（见图3-1、图3-2）上添加一块DIY的无线开关模块（见图3-3），网关发送开、关命令到无线开关模块控制电灯，达到普通家庭智能电灯开关的效果。

图3-1　电灯开关的正面图

图3-2　电灯开关的背面图

图3-3　无线开关模块介绍图

3.1　用途

每个人应该都有出门忘记关灯，晚上睡觉时还要一个一个地把所有的灯关掉，最痛苦的是冷天已经躺在床上，睡前还要下床关灯这类的事，如果我们DIY电灯开关，那么通过手机、语音就可以随意控制整个房间的电灯了。

其实原理很简单，手机通过WiFi连接网关或互联网，再将命令字符写入网关数据库，网关获取到命令后，调用Arduino的无线模块NRF24L01/Zigbee等，再发送到指定控制端模块（根据编号），控制端模块收到命令字符后，执行操作，如图3-4所示。

图3-4　电灯开关模块功能流程图

3.2　材料列表（见表3-1）、原理图（见图3-5）

表3-1　电灯开关无线模块材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32K

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

三极管

1815

3

二极管

1N4148

3

AMS1117

3.3V

1

Micro USB母座

1

10μF电容

0603

2

0.1μF电容

0603

4

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻270Ω

0603

3

KF301-2P脚距5.08mm接线端子300V/16A

1

接线端子KF301-3P脚距5.08mm 300V/16A

1

继电器5V T73 SRD-5VDC-SL-C 5脚

3

220V交流转5V直流模块

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

n

图3-5　无线开关模块原理图

我们根据材料列表和原理图即可做成如图3-6所示的无线开关模块，这样我们就有3路可控开关了。

图3-6　无线开关模块DIY

3.3　继电器介绍

继电器是具有隔离功能的自动开关元件，广泛应用于遥控、遥测、通信、自动控制、机电一体化及电力电子设备中，是最重要的控制元件之一，如图3-7所示。

图3-7　继电器实物图

继电器的种类和作用比较多。我们采用的是舌簧继电器，利用密封在管内具有触电簧片和衔铁磁路双重作用的舌簧动作来开、闭或转换线路。当继电器收到高电频时就会接通常开端口，低电频时就会断开常开端口，实现控制220V电压的连通和断开，如图3-8所示。

图3-8　继电器介绍图

3.4　代码实例

可以扫描图3-9或通过网址下载到PC。

图3-9　开关源代码下载二维码

网址http://book.znck007.com/?p=kaiguandaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*ZIGBEE针脚连接线

TX -> RXD

RX -> TXD

*/

#include <SPI.h>                         //引用SPI库文件

#include <Mirf.h>                        //引用Mirf库文件

#include <nRF24L01.h>                    //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h>       //引用Mirf驱动库文件

#include <EEPROM.h>                      //引用EEPROM库文件

int sid=6;                                //模块类型

int nid=0;                                //模块编号

//无线串口通信处理(zigbee/bluetooth等)

unsigned long serial1nowlast;             //串口检测间隔时间

char serial1buff[129]={0};                //串口缓存字符

char serial1Data;                         //串口单个字符

int serial1i=0;                           //串口字符数

//NRF24L01

unsigned long nrf24l01nowlast;            //NRF24L01检测间隔时间

char nrf24l01buff[33]={0};                //NRF24L01缓存字符

char nrf24l01Data;                        //NRF24L01单个字符

int nrf24l01i=0;                          //NRF24L01字符数

//声名变量

int pinIn0=3;                             //手动开关针脚1

int pinIn1=4;                             //手动开关针脚2

int pinIn2=5;                             //手动开关针脚3

int val0;                                 //当前开关值1

int val1;                                 //当前开关值2

int val2;                                 //当前开关值3

int pinOut0=A0;                           //开关针脚1

int pinOut1=A1;                           //开关针脚2

int pinOut2=A2;                           //开关针脚3

int state0=0;                             //开关状态1

int state1=0;                             //开关状态2

int state2=0;                             //开关状态3

void setup()

{

Serial.begin(115200);                //设置串口波特率115200

/初始化Mirf，用于NRF24L01收发

har client[10]={0};                  //客户端名称

printf(client,"clie%d",sid);         //设置客户端名称

irf_Init(0,client,sid);              //初始化NRF24L01收发

pinMode(pinIn0,INPUT);               //设置输入模式

pinMode(pinIn1,INPUT);               //设置输入模式

pinMode(pinIn2,INPUT);               //设置输入模式

pinMode(pinOut0,OUTPUT);             //设置输出输式

pinMode(pinOut1,OUTPUT);             //设置输出输式

pinMode(pinOut2,OUTPUT);             //设置输出输式

pinMode(pinIn0,INPUT_PULLUP);        //将管脚设置为输入并且内部上拉模式

pinMode(pinIn1,INPUT_PULLUP);        //将管脚设置为输入并且内部上拉模式

pinMode(pinIn2,INPUT_PULLUP);        //将管脚设置为输入并且内部上拉模式

digitalWrite(pinIn0, HIGH);          //设置默认高电位

digitalWrite(pinIn1, HIGH);          //设置默认高电位

digitalWrite(pinIn2, HIGH);          //设置默认高电位

state0 = EEPROM.read(0);             //读取rom开关电位

state1 = EEPROM.read(1);             //读取rom开关电位

state2 = EEPROM.read(2);             //读取rom开关电位

Serial.println("zwifi_kaiguan");     //打印

Serial.print("state0=");

Serial.print(state0);                //打印当前电位状态1

Serial.print("/state1=");

Serial.print(state1);                //打印当前电位状态2

Serial.print("/state2=");            //打印当前电位状态3

if(state0= =255) state0=0;           //默认rom则设为关状态

if(state1= =255) state1=0;           //默认rom则设为关状态

if(state2= =255) state2=0;           //默认rom则设为关状态

}

void loop()

{

//检测无线串口数据处理 (zigbee/bluetooth等)

{

unsigned long serial1now = millis(); //获取现在的时间

if(serial1now - serial1nowlast >= 5000)

//如果数据间隔超过5秒而清空字符（为了防止数据错乱）

{

serial1nowlast = millis();         //记录当前时间

memset(serial1buff, 0, 129);       //清空缓存字符

serial1i=0;                        //初始数组数

}

while( Serial.available() )          //如果无线串口有数据

{

if(serial1i= =0)

{

Serial.println("serial->");      //打印出来方便调试

}

serial1Data=(char)Serial.read();   //读取串口数据

//Serial.print(serial1Data);       //这里不打印，否则检测到{ckxxxx}就认为是命令

serial1buff[serial1i]=serial1Data; //保存到数组

serial1i++;//数组长度+1

if(serial1Data= ='}' || serial1i>=129)

//如果发现}而说明命令结束（并少于129个字符，太长会出错）

{

serial1nowlast = millis();       //记录当前时间，不然5秒就超时了

char body[129]={0};              //声明新字符数组

get_znck_body(serial1buff,body);

//获取只是{ckxxxxxx}的字符,因为这是我们的命令格式

//serial.println(body);          //这里不打印，否则检测到{ckxxxx}就认为是命令

//如果命令格式正确则做处理

if(strstr(body,"{ck") && strstr(body,"}") )

{

//Serial.println(body);        //这里不打印，否则检测到{ckxxxx}就认为是命令

if(strlen(body)>10)              //字符数需大于10

{

set_onoff(body);               //设置开或关

}

}

serial1i=0;                        //字符数组长度初始为0

Serial.println("-------------------"); //打印结束标示

delay(100);                        //延时

}

}

}

unsigned long nrf24l01now = millis();     //获取现在的时间

if(nrf24l01now - nrf24l01nowlast >= 5000)

//如果数据间隔超过5秒而清空字符（为了防止数据错乱）

{

nrf24l01nowlast = millis();             //记录当前时间

memset(nrf24l01buff, 0, 33);            //清空缓存字符

nrf24l01i=0;                            //初始数组数

}

byte data[Mirf.payload];                //声明字节

if(Mirf.dataReady()){                   //如果有数据

Mirf.getData(data);                     //获取数据

Mirf.rxFifoEmpty();                     //清理NRF24L01援存

//Serial.println((char)*data);          //这里不打印，否则检测到{ckxxxx}就认为是命令

for (int i = 0; i < Mirf.payload; i++) //把收到的信息拼起来

{

if(nrf24l01i= =0)

{

Serial.println("nrf24l01->");       //打印出来方便调试

}

nrf24l01Data=(char)data[i];           //读取NRF24L01数据

if( nrf24l01Data= ='{') nrf24l01i=0;  //如果发现{则设长度为0开始

nrf24l01buff[nrf24l01i]=nrf24l01Data; //保存到数组

nrf24l01i++;                          //数组长度+1

if(nrf24l01Data= ='}' || nrf24l01i>=33)

//如果发现}而说明命令结束（并少于33个字符，太长会出错）

{

nrf24l01nowlast = millis();       //更新当前时间，不然5秒就超时了

char body[33]={0};                //声明新数组

get_znck_body(nrf24l01buff,body);

//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

//Serial.println(body);           //这里不打印，否则检测到{ckxxxx}就认为是命令

//如果命令格式正确则做处理

if(strstr(body,"{ck") && strstr(body,"}") )

{

//Serial.println(body);         //这里不打印，否则检测到{ckxxxx}就认为是命令

if(strlen(body)>10)             //字符数需大于10

{

set_onoff(body);            //设置开或关

}

}

memset(nrf24l01buff, 0, 33);      //字符数组长度初始为0

nrf24l01i=0;                      //字符数组长度初始为0

Serial.println("-------------------");//打印结束标示

delay(100);                       //延时

}

}

}

val0=digitalRead(pinIn0);                 //读取输入数据

if(val0= =LOW)                            //检测按键是否按下

{

delay(200);                             //延时，防抖处理

val0=digitalRead(pinIn0);               //读取数据

if(val0= =LOW)                          //检测按键是否按下

{

Serial.print("val0 state0=");

Serial.println(state0);               //打印

if(state0= =0)                        //如果原来是关状态

{

state0=1;                         //则状态值为开

digitalWrite(pinOut0,HIGH);       //则设为高电位

char handonoff[33]={0};           //声明手动开关字符

sprintf(handonoff,"{ck%03d000%d}",sid,state0);//生成手动开关字符

set_onoff(handonoff);             //设置开或关

delay(2000);                      //延时2000毫秒

}

else                                  //如果原来是开状态

{

state0=0;                         //则状态值为关

digitalWrite(pinOut0,LOW);        //则设为高电位

char handonoff[33]={0};           //声明手动开关字符

sprintf(handonoff,"{ck%03d000%d}",sid,state0);//生成手动开关字符

set_onoff(handonoff);             //设置开或关

delay(2000);                      //延时2000毫秒

}

}

}

val1=digitalRead(pinIn1);                 //读取数字

if(val1= =LOW)                            //检测按键是否按下

{

delay(200);                             //延时，防抖处理

val1=digitalRead(pinIn1);               //读取数字

if(val1= =LOW)                          //检测按键是否按下

{

Serial.print("val1 state1=");

Serial.println(state1);               //打印

if(state1= =0)                        //如果原来是关状态

{

state1=1;                         //则状态值为开

digitalWrite(pinOut1,HIGH);       //则设为高电位

char handonoff[33]={0};           //声明手动开关字符

sprintf(handonoff,"{ck%03d001%d}",sid,state1);//生成手动开关字符

set_onoff(handonoff);             //设置开或关

delay(2000);                      //延时2000毫秒

}

else                                  //如果原来是开状态

{

state1=0;                         //则状态值为关

digitalWrite(pinOut1,LOW);        //则设为高电位

char handonoff[33]={0};           //声明手动开关字符

sprintf(handonoff,"{ck%03d001%d}",sid,state1);//生成手动开关字符

set_onoff(handonoff);             //设置开或关

delay(2000);                      //延时2000毫秒

}

}

}

val2=digitalRead(pinIn2);                 //读取数字

if(val2= =LOW)                            //检测按键是否按下

{

delay(200);                             //延时，防抖处理

val2=digitalRead(pinIn2);               //读取数字

if(val2= =LOW)                          //检测按键是否按下

{

Serial.print("val2 state2=");

Serial.println(state2);               //打印

if(state2= =0)                        //如果原来是关状态

{

state2=1;                         //则状态值为开

digitalWrite(pinOut2,HIGH);       //则设为高电位

char handonoff[33]={0};           //声明手动开关字符

sprintf(handonoff,"{ck%03d002%d}",sid,state2);//生成手动开关字符

set_onoff(handonoff);             //设置开或关

delay(2000);                      //延时2000毫秒

}

else                                  //如果原来是开状态

{

state2=0;                         //则状态值为关

digitalWrite(pinOut2,LOW);        //则设为高电位

char handonoff[33]={0};           //声明手动开关字符

sprintf(handonoff,"{ck%03d002%d}",sid,state2);//生成手动开关字符

set_onoff(handonoff);             //设置开或关

delay(2000);                      //延时2000毫秒

}

}

}

//根据状态设置开或关（防漏）

if(state0= =1)

{

digitalWrite(pinOut0,HIGH);            //则设为高电位

}

else

{

digitalWrite(pinOut0,LOW);             //则设为低电位

}

//ON OFF SET

if(state1= =1)

{

digitalWrite(pinOut1,HIGH);            //则设为高电位

}

else

{

digitalWrite(pinOut1,LOW);             //则设为低电位

}

//ON OFF SET

if(state2= =1)

{

digitalWrite(pinOut2,HIGH);

//则设为高电位

}

else

{

digitalWrite(pinOut2,LOW);             //则设为低电位

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;         //设置spi

Mirf.init();                         //初始Mirf

Mirf.setRADDR((byte *)server);       //设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);       //设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);       //设置发送地址

}

Mirf.payload = sizeof(char);           //收发字节

Mirf.channel = channel;                //设置频道

Mirf.config();                         //生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);           //设置发送模式

int bufi=0;                            //声明变量

for(bufi=0;bufi<strlen(str);bufi++){   //循环发送

char words=str[bufi];                //发送的字符

Mirf.send((byte *)&words);           //发送命令

while(Mirf.isSending()){             //等待发送完闭

}

delay(50);                           //延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

void get_znck_body(char *p,char *s){

char rechar[33]={0};                   //声明数组

int bufi=0;                            //当前长度

bool isend=false;                      //开始和结束

int charnum=0;                         //字符长度

for(bufi=0;bufi<strlen(p);bufi++){    //循环截取指定字符

//Serial.print(p[bufi]);

if(p[bufi] = ='{'){                  //如果是{则开始截取

isend=true;                        //设为ture

}

if(p[bufi] = ='}' && isend= =true){  //如果是}则结束截取

isend=false;                       //设为false

rechar[charnum]=p[bufi];           //赋值到新数组

break;

}

if(isend){                           //如果为true则赋值

if(charnum<33)                     //小于指定长度

{

rechar[charnum]=p[bufi];         //赋值到新数组

charnum++;                       //字符长度加1

}

}

}

//Serial.println("");

//memcpy(s,rechar,33);

sprintf(s,"%s",rechar);              //赋值返回

}

//设置开或关函数

void set_onoff(char *body){

int s=get_sid(body);                 //获取sid

int n=get_nid(body);                 //获取nid

int d=get_data(body);                //获取data

if(  s= =sid && n= =0 )              //如果是1路开关

{

if( d= =1 )                        //如果数据是开

{

state0=1;                        //设置状态为开

EEPROM.write(0, state0);         //写入rom记录状态

}

if( d= =0 )                        //如果数据是关

{

state0=0;                        //设置状态为关

EEPROM.write(0, state0);         //写入rom记录状态

}

}

if(  s= =sid && n= =1 )              //如果是2路开关

{

if( d= =1 )                        //如果数据是开

{

state1=1;                        //设置状态为开

EEPROM.write(1, state1);         //写入rom记录状态

}

if( d= =0 )                        //如果数据是关

{

state1=0;                        //设置状态为关

EEPROM.write(1, state1);         //写入rom记录状态

}

}

if(  s= =sid && n= =2 )              //如果是3路开关

{

if( d= =1 )                        //如果数据是开

{

state2=1;                        //设置状态为开

EEPROM.write(2, state2);         //写入rom记录状态

}

if( d= =0 )                        //如果数据是关

{

state2=0;                        //设置状态为开

EEPROM.write(2, state2);         //写入rom记录状态

}

}

//更新反馈数据到网关

if(  s= =sid ){                      //如果是本设备

char server[10]={0};               //声明网关接收名称

sprintf(server,"serv%d",1);        //赋值网关接收名称

//Serial.println(server);          //打印

char updateData[33]={0};           //声明更新数组

char front[10]={0};                //声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",s,n); //赋值前缀

sprintf(updateData,"%supdate}",front); //生成反馈数组

Serial.println(updateData);        //发送到串口（zigbee会发给网关）

Mirf_Send(1,server,updateData);

//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};               //声明客户端

sprintf(client,"clie%d",sid);      //赋值客户端名称

Mirf_Init(1,client,sid);           //设置为接收模式

}

}

//获取sid函数

int get_sid(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charSid[4]={0};                //声明数组

memcpy(charSid,buff+3,3);           //赋值截取第3位后面的3个字符

Serial.println(charSid);            //打印

int intSid=atoi(charSid);           //转成int

Serial.println(intSid);             //打印

return intSid;                      //返回sid

}

else

{

return 0;                           //格式不对返回0

}

}

//获取nid函数

int get_nid(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)

//如果含{ck和}，并长度大于10

{

char charNid[4]={0};                //声明数组

memcpy(charNid,buff+6,3);           //赋值截取第6位后面的3个字符

Serial.println(charNid);            //打印

int intNid=atoi(charNid);           //转成int

Serial.println(intNid);             //打印

return intNid;                      //返回nid

}

else

{

return 0;                           //格式不对返回0

}

}

//获取data函数

int get_data(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charData[4]={0};              //声明数组

memcpy(charData,buff+9,1);         //赋值截取第9位后面的1个字符

Serial.println(charData);          //打印

int intData=atoi(charData);        //转成int

Serial.println(intData);           //打印

return intData;                    //返回data

}

else

{

return 0;                          //格式不对返回0

}

}

3.5　实战演示

1．将开关板连接电脑

我们将无线开关模块板、Zigbee、NRF24L01、杜邦线公母头、USB线准备好，如图3-10所示。

图3-10　连接电脑前的准备

插入Zigbee模块，如图3-11所示。

图3-11　插入Zigbee模块

插入NRF24L01模块，如图3-12所示。

图3-12　插入NRF24L01模块

引出D3、D4、D5和GND引脚，就可以手动开关了，如图3-13所示。

图3-13　手动开关引线介绍

当D3与GND接触就会开或关线路1，D4与GND接触就会开或关线路2，D5与GND接触就会开或关线路3，如图3-14所示。使用时，接上家用零线与火线（注意：接家用电源必须懂得电工知识，必须先关电再接，否则可能触电，危及到生命）。

图3-14　引线与线路

插入电脑USB插口，如图3-15所示。

图3-15　插入电脑USB插口

电脑会自动认出串口芯片和COM口，如图3-16所示，因为驱动CH340在第2章已经安装了。

图3-16　设备管理器识别USB串口和COM口

2．编译刷写开关代码

使用Arduino ide打开源代码，如图3-17所示。

图3-17　使用Arduino ide打开源代码

提示

int sid=3;//模块类型

int nid=0;//模块编号

代码中的sid，nid代表唯一标示，不可重复，家居网关才能准确的发送命令。

然后选择Tools->Board->Arduino Uno板，如图3-18所示。后面各章中的各种控制板和传感板都选择Arduino Uno板，因为我们都采用ATMEGA328P芯片。

图3-18　选择板Arduino Uno

选择对应的COM口，如图3-19所示，在设备管理器中会显示COM口（见图3-16）。

图3-19　选择COM口

编译代码到开关板，如图3-20所示。

图3-20　编译代码到开关板

打开ToolS→Serial Monitor，会弹出如图3-21所示右边的窗口，说明刷写成功了。

图3-21　打开串口监视器

3．测试无线开关模块

把D3和GND接触，如图3-22所示，如果串口监视器有反应，那么测试手动开关成功，如图3-23所示。

图3-22　测试手动开关针脚

图3-23　手动开关触发的串口数据

测试手机→云端服务器→网关→NRF24L01、Zigbee→无线开关模块控制端如图3-24所示，来确定WiFi、NRF24L01、Zigbee是否正常通信。

图3-24　手机→云端服务器→网关→NRF24L01、Zigbee→无线开关模块控制端

提示：

（1）手机或Chrome浏览器打开云端服务器m.znck007.com，如图3-25所示。

图3-25　云端服务器首页

（2）进入灯光照明，单击“灯光照明”，再单击“添加新电灯开关”，如图3-26所示，添加一个叫电灯3-0的开关。

图3-26　添加电灯开关设备

（3）在电灯开关的控制页面上会生成sid，nid，如图3-27所示，在编译开关源代码（见图3-20）时把里面的sid和nid改成此页面的值，否则无法控制。

图3-27　电灯开关控制页面

说明

① 如果家居网关无法连接无线开关模块，那么状态将会显示红色的警告字。

② 控制开关成功，最近开关时间将会更新，可根据时间判断异常。

③ 如果发现异常，则可参考家居网关运行日志和控制端监视串口打印来做判断依据。

（4）单击开和关，如果家居网关和控制板都正常，则无线开关模块监视串口打印如图3-28所示类似的数据。

图3-28　无线开关模块监视串口打印

（5）除了可以使用云端服务器控制开关处，OpenWrt也是一台服务器，也可以控制开关，用NavicatforMySQL直接操作MySQL数据库，如图3-29所示。

图3-29　使用NavicatForMysql控制开关

提醒

① 添加一条sid=003、nid=000、data=1为开灯、status=0未处理、num=0无响应6次则status=3说明这本命令超时了。

② 成功控制开关会生成一条data=update的记录，证明成功了。

（6）也可以使用PHP网页控制MySQL实现控制开关的目的，我们已经写好并放到OpenWrt里，直接打开http://192.168.1.1/znck007/mysqltest.php就可以访问，如图3-30所示。

图3-30　使用PHP网页控制MySQL

4．组装到家里的电灯开关上

拆掉家里的电灯开关面板后，如图3-31所示，一般蓝色为零线，红色为火线和电灯线。火线和电灯线相接，灯就亮了，我们要在这里添加我们的无线开关模块。

图3-31　家里电灯开关内部

无线开关模块的接线如图3-32所示。

图3-32　无线开关模块的接线

关掉电灯总开关后，用螺丝刀接电线到无线开关模块，如图3-33所示，并放入电灯开关槽，如图3-34所示。

图3-33　关掉总开关，接电线到无线开关模块

图3-34　将无线开关模块放入电灯开关槽

由于本节只控制1路电灯，因此将D3和GND线接到普通的电灯开关后，如图3-35所示，就可以手动开关电灯了。

图3-35　手动开关线接到旧电灯开关

安装上原电灯开关如图3-36所示。

图3-36　安装上原电灯开关

合上原电灯面板如图3-37所示。

图3-37　合上原电灯面板

现在就可以手动控制开关了，如图3-38所示，不在家也可以拿起手机开关家里的电灯了，但必须保证家里能上网。

图3-38　手动和手机都可以控制电灯

提示

由于原来的电灯开关是普通开关，因此手动开关灯时，应及时按回原来的位置，只需达到D3和GND接触一下就行。

5．电灯开关演示视频

可以扫描图3-39或通过网址查看电灯开关演示视频。

图3-39　电灯开关演示视频二维码

网址http://book.znck007.com/?p=diandenghipin

3.6　常见的问题及注意事项

【A】先关电灯总开关再接线。

记得在每一次安装时，都要先把总开关关闭，安装好后再上电，否则会有触电的危险，220V电压可能是致命的，因此在接触强电时千万小心，必须要懂得电工知识、有一定经验才去安装，否则后果自负。

【B】目前此方案只支持零线+火线，不支持单火线。

通常我们在装修的时候就已经决定了屋子里的电灯开关位置和布线，有些电灯开关为了节省成本可能是没有零线的，因此需要额外给模块供电。

【C】NRF24L01的穿墙问题。

NRF24L01的穿墙能力是有限的，如果发现测试正常，但隔墙又比较远就会收不到数据，那么可以用ZIGBEE解决。ZIGBEE的自组网能力可以达到56000个无线扩展距离。

【D】ZIGBEE无反应跳帽说明。

由于编译刷写代码时和ZIGBEE同用同一个串口，因此要使用ZIGBEE时，必须拔掉无线开关模块的跳帽，编译刷写代码时就插上跳帽。

第4章　无线插座

插座也是家中最常用的设备，不管是电视、空调、冰箱、风扇、电脑等都需要插上电源才能使用，那么我们将一转三插座DIY成无线插座，就可以在公司控制家里的电器开关了，如图4-1所示。

图4-1　一转三插座

同样，我们也可以采用电灯开关DIY的无线开关模块，如图4-2所示，编译代码时，区别sid和nid就能DIY成无线插座，因为材料列表、原理图、源代码编译都是一样的，因此在本章就不再讲解无线开关模块了，直接进行实战演示。

图4-2　无线开关模块

4.1　用途

无线插座是指利用控制板和电子元器件的组合及编程实现电路智能开关的控制。

无线插座被广泛应用于家居智能化改造、办公室智能化改造、工业智能化改造、农林渔牧智能化改造等多个领域，提高生成效率，降低运营成本，真正做到远程控制。

由于智能手机的普及，因此智能手机与无线插座有了完美的结合，通过手机控制家里的插座，就可以实现我们日常最常用的远程智能控制开关。

4.2　智能开关

2014年，智能开关是最火的智能产品之一，大部份采用WiFi+云服务器（也有采用路由映射功能的），实现了使用手机APP就能简单方便地设置和使用。其中的原理很简单：手机APP的指令通过云服务器传输到智能开关WiFi模块，从而实现远程控制，如图4-3所示。

图4-3　智能开关产品，图片来自网络

智能开关与无线插座的区别：智能开关包含WiFi芯片，直接连接家中的WiFi；无线插座连接家居网关，家居网关连接家中的WiFi或本身就是WiFi。无线插座是一整套智能家居中的一个，需要和家居网关配合使用，而智能开关可以单独使用。

4.3　实战演示

1．DIY准备

我们在DIY无线插座时需准备的配件如图4-4所示：一转三插座、无线开关模块、 Zigbee、NRF24L01、杜邦公母线。

图4-4　DIY无线插座时需准备配件

无线开关模块按第3章讲述的连接电脑刷写源代码，如图4-5所示。特别注意，sid和nid必须与云端服务器一样。

图4-5　连接电脑编译刷写源代码

2．插座拆改

使用螺丝刀拆开插座的后盖后如图4-6所示。

图4-6　拆开后盖后

拆开后就可看到插座的内部结构，如图4-7所示。

图4-7　插座内部结构

因为需要拆出两个插孔，所以空出的位置放无线开关模块，如图4-8、图4-9所示。

图4-8　拆出两个插孔

图4-9　拆出后的效果

按如图4-10、图4-11所示，使用电烙铁和剪刀拆除其他两个插孔。

图4-10　使用电烙铁拆除插孔

图4-11　使用剪刀剪开线

按如图4-12所示焊接220V电线，切记不可以焊错，否则可能会造成短路，引发火灾等。

图4-12　插座和无线开关模块接线图

手动开关焊接如图4-13所示，虚线为未焊上的220V线路。

图4-13　手动开关焊接

焊接好以后的内部图如图4-14所示。

图4-14　焊接好以后的内部图

合上插座盖子后如图4-15所示。

图4-15　合上插座盖子后

最后完成后的正面图和拆下的零件，如图4-16所示。

图4-16　DIY正面图和拆下的零件

贴上面板后，用贴布贴紧，如图4-17所示，一个无线插座就DIY完成了。

图4-17　贴上面板

3．使用无线插座

仔细检查确保线路正常后，就可以接上220V电压，为了测试，再插上一个LED灯，如图4-18所示。

图4-18　无线插座插上电源

打开手机云端，操作灯就亮了，同时手动拨动开关也可以操作，如图4-19所示。

图4-19　打开手机云端操作无线插座

4．无线插座演示视频

可以扫描图4-20或通过网址查看无线插座演示视频。

图4-20　无线插座演示视频二维码

网址http://book.znck007.com/?p=chazuoshipin

4.4　常见问题及注意事项

【A】防止插座内部的短路。

【B】防火、防水、防漏电，安全第一。

【C】使用时记得去掉跳帽，否则Zigbee无法使用。

第5章　远程开门

本章采用市场上的遥控门锁（市场上100元左右），如图5-1所示，和本章DIY的无线控制板，如图5-2所示，使用D3针脚的PMW来实现远程开门。

图5-1　摇控门锁

图5-2　无线控制板

5.1　用途

远程开门的作用很广，如当家人忘记带钥匙，就可以通过远程帮他开门；当客人来到一楼，不用下楼就可以开门；家中有小狗，可以远程控制小狗专设的门室。

远程开门没有火起来的原因是安全问题，如果小偷破解了开门系统，那么后果不堪设想。本章DIY的远程开门并非成熟产品，因此不可直接用在主门，可用在小区门、次门、卧室门等，防止小偷入室偷盗。

5.2　材料列表（见表5-1）、原理图（见图5-3）

表5-1　无线控制板材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

图5-3　无线控制模块原理图

根据原理图和材料列表DIY的无线控制板如图5-4所示。

图5-4　无线控制板DIY图

5.3　电子门锁

市面上的遥控门锁拆开后盖后如图5-5所示。它的原理是关门时先压紧弹簧，然后给电磁上电，产生的吸力打开弹簧开关，从而打开门锁。

图5-5　遥控门锁拆开后盖后

5.4　代码实例

可以扫描图5-6或通过网址下载到PC。

图5-6　远程开门源代码下载二维码

网址http://book.znck007.com/?p=kaimengdaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*ZIGBEE针脚连接线

TX -> RXD

RX -> TXD

*遥控锁连接线

D3 -> DATA

GND -> GND

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

#include <EEPROM.h> //引用EEPROM库文件

int sid=5;//模块类型

int nid=1;//模块编号

//无线串口通信处理（zigbee/bluetooth等）

unsigned long serial1nowlast; //串口检测间隔时间

char serial1buff[129]={0}; //串口缓存字符

char serial1Data; //串口单个字符

int serial1i=0; //串口字符数

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

//声名变量

int pinIn0=A0; //手动开关针脚1

int val0; //当前开关值1

int pinOut0=3; //开关针脚1

int state0=0; //开关状态1

void setup()

{

Serial.begin(115200); //设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

pinMode(pinIn0,INPUT); //设置输入模式

pinMode(pinOut0,OUTPUT); //设置输出输式

pinMode(pinIn0,INPUT_PULLUP); //将管脚设置为输入并且内部上拉模式

digitalWrite(pinIn0, HIGH); //设置默认高电位

state0 = EEPROM.read(0); //读取rom开关电位

Serial.println("zwifi_kaimen"); //打印

Serial.print("state0=");

Serial.print(state0); //打印当前电位状态1

if(state0= =255) state0=0; //默认rom则设为关状态

}

void loop()

{

//检测无线串口数据处理（zigbee/bluetooth等）

{

unsigned long serial1now = millis();//获取现在的时间

if(serial1now - serial1nowlast >= 5000)//如果数据间隔超过5秒而清空字符（为了防止数据错乱）

{

serial1nowlast = millis();//记录当前时间

memset(serial1buff, 0, 129);//清空缓存字符

serial1i=0;//初始数组数

}

while( Serial.available() )//如果无线串口有数据

{

if(serial1i= =0)

{

Serial.println("serial->");//打印出来方便调试

}

serial1Data=(char)Serial.read();//读取串口数据

//Serial.print(serial1Data);//这里不打印，否则检测到{ckxxxx}就认为是命令

serial1buff[serial1i]=serial1Data;////保存到数组

serial1i++;//数组长度+1

if(serial1Data= ='}' || serial1i>=129)//如果发现}而说明命令结束（并少于129个字符，太长会出错）

{

serial1nowlast = millis(); //记录当前时间，不然5秒就超时了

char body[129]={0};//声明新字符数组

get_znck_body(serial1buff,body);//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

//serial.println(body); //这里不打印，否则检测到{ckxxxx}就认为是命令

//如果命令格式正确则做处理

if(strstr(body,"{ck") && strstr(body,"}") )

{

//Serial.println(body);//这里不打印，否则检测到{ckxxxx}就认为是命令

if(strlen(body)>10) //字符数需大于10

{

set_onoff(body);//设置开或关

}

}

serial1i=0;//字符数组长度初始为0

Serial.println("-------------------");//打印结束标示

delay(100);//延时

}

}

}

unsigned long nrf24l01now = millis();//获取现在的时间

if(nrf24l01now - nrf24l01nowlast >= 5000)//如果数据间隔超过5秒而清空字符（为了防止数据错乱）

{

nrf24l01nowlast = millis();//记录当前时间

memset(nrf24l01buff, 0, 33);//清空缓存字符

nrf24l01i=0;//初始数组数

}

byte data[Mirf.payload];//声明字节

if(Mirf.dataReady()){//如果有数据

Mirf.getData(data);//获取数据

Mirf.rxFifoEmpty();//清理NRF24L01援存

//Serial.println((char)*data);//这里不打印，否则检测到{ckxxxx}就认为是命令

for (int i = 0; i < Mirf.payload; i++) //把收到的信息拼起来

{

if(nrf24l01i= =0)

{

Serial.println("nrf24l01->");//打印出来方便调试

}

nrf24l01Data=(char)data[i];//读取NRF24L01数据

if( nrf24l01Data= ='{') nrf24l01i=0;//如果发现{则设长度为0开始

nrf24l01buff[nrf24l01i]=nrf24l01Data;//保存到数组

nrf24l01i++;//数组长度+1

if(nrf24l01Data= ='}' || nrf24l01i>=33)//如果发现}而说明命令结束（并少于33个字符，太长会出错）

{

nrf24l01nowlast = millis(); //更新当前时间，不然5秒就超时了

char body[33]={0};//声明新数组

get_znck_body(nrf24l01buff,body);//获取只是{ckxxxxxx}的字符,因为这是我们的命令格式

//Serial.println(body);//这里不打印，否则检测到{ckxxxx}就认为是命令

//如果命令格式正确则做处理

if(strstr(body,"{ck") && strstr(body,"}") )

{

//Serial.println(body);//这里不打印，否则检测到{ckxxxx}就认为是命令

if(strlen(body)>10) //字符数需大于10

{

set_onoff(body);//设置开或关

}

}

memset(nrf24l01buff, 0, 33);//字符数组长度初始为0

nrf24l01i=0;//字符数组长度初始为0

Serial.println("-------------------");//打印结束标示

delay(100);//延时

}

}

}

val0=digitalRead(pinIn0);//读取输入数据

if(val0= =LOW)//检测按键是否按下

{

delay(200);//延时，防抖处理

val0=digitalRead(pinIn0);//读取数据

if(val0= =LOW)//检测按键是否按下

{

Serial.print("val0 state0=");

Serial.println(state0);//打印

if(state0= =0)//如果原来是关状态

{

state0=1;//则状态值为开

digitalWrite(pinOut0,HIGH);//则设为高电位

char handonoff[33]={0};//声明手动开关字符

sprintf(handonoff,"{ck%03d001%d}",sid,state0);//生成手动开关字符

set_onoff(handonoff);//设置开或关

delay(2000);//延时2000毫秒

}

else//如果原来是开状态

{

state0=0;//则状态值为关

digitalWrite(pinOut0,LOW);//则设为高电位

char handonoff[33]={0};//声明手动开关字符

sprintf(handonoff,"{ck%03d001%d}",sid,state0);//生成手动开关字符

set_onoff(handonoff);//设置开或关

delay(2000);//延时2000毫秒

}

}

}

//根据状态设置开或关（防漏）

if(state0= =1)

{

digitalWrite(pinOut0,HIGH);//则设为高电位

}

else

{

digitalWrite(pinOut0,LOW);//则设为低电位

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

void get_znck_body(char *p,char *s){

char rechar[33]={0};//声明数组

int bufi=0;//当前长度

bool isend=false;//开始和结束

int charnum=0;//字符长度

for(bufi=0;bufi<strlen(p);bufi++){//循环截取指定字符

//Serial.print(p[bufi]);

if(p[bufi] = ='{'){//如果是{则开始截取

isend=true;//设为ture

}

if(p[bufi] = ='}' && isend= =true){//如果是}则结束截取

isend=false;//设为false

rechar[charnum]=p[bufi];//赋值到新数组

break;

}

if(isend){//如果为true则赋值

if(charnum<33)//小于指定长度

{

rechar[charnum]=p[bufi];//赋值到新数组

charnum++;//字符长度加1

}

}

}

//Serial.println("");

//memcpy(s,rechar,33);

sprintf(s,"%s",rechar);//赋值返回

}

//设置开或关函数

void set_onoff(char *body){

int s=get_sid(body);//获取sid

int n=get_nid(body);//获取nid

int d=get_data(body);//获取data

if(  s= =sid && n= =0 )//如果是开关

{

if( d= =1 )//如果数据是开

{

state0=1;//设置状态为开

EEPROM.write(0, state0);//写入rom记录状态

}

if( d= =0 )//如果数据是关

{

state0=0;//设置状态为关

EEPROM.write(0, state0);//写入rom记录状态

}

}

//更新反馈数据到网关

if(  s= =sid ){//如果是本设备

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",s,n);//赋值前缀

sprintf(updateData,"%supdate}",front);//生成反馈数组

Serial.println(updateData);//发送到串口（zigbee会发给网关）

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

}

}

//获取sid函数

int get_sid(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charSid[4]={0};//声明数组

memcpy(charSid,buff+3,3);//赋值截取第3位后面的3个字符

Serial.println(charSid);//打印

int intSid=atoi(charSid);//转成int

Serial.println(intSid);//打印

return intSid;//返回sid

}

else

{

return 0;//格式不对返回0

}

}

//获取nid函数

int get_nid(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charNid[4]={0};//声明数组

memcpy(charNid,buff+6,3);//赋值截取第6位后面的3个字符

Serial.println(charNid);//打印

int intNid=atoi(charNid);//转成int

Serial.println(intNid);//打印

return intNid;//返回nid

}

else

{

return 0;//格式不对返回0

}

}

//获取data函数

int get_data(char *buff){

if(strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charData[4]={0};//声明数组

memcpy(charData,buff+9,1);//赋值截取第9位后面的1个字符

Serial.println(charData);//打印

int intData=atoi(charData);//转成int

Serial.println(intData);//打印

return intData;//返回data

}

else

{

return 0;//格式不对返回0

}

}

5.5　实战演示

1．DIY准备工作

远程开门DIY需要无线开关门锁、NRF24L01、Zigbee自组网、无线控制板，如图5-7所示。

图5-7　远程开门DIY准备

2．编译刷写源代码

将无线控制板连接电脑USB，如图5-8所示。

图5-8　无线控制板连接电脑USB

使用Arduino软件打开远程开门的源代码，编译时，选择好COM口和Arduino Uno板，将源代码里的sid、nid改成云端产生的sid、nid后进行编译，因为在第3章电灯开关DIY时已经详细地进行了说明，这里就不再讲解了。

3．安装门锁

安装门锁时要注意门栓和门锁的位置，过松、过紧都可能导致开门不便，如图5-9、图5-10所示。

图5-9　安装门栓

图5-10　安装门锁

合上后盖，安装好后的效果如图5-11、图5-12所示。

图5-11　盒上后盖

图5-12　安装好后的效果

连上刷写过的无线控制板，如图5-13、图5-14所示。

图5-13　门锁和无线控制板连接图

图5-14　连接好后的效果

无线控制板悬空在外面不好看，因此使用3D打印技术打印无线控制板的盒子，如图5-15、图5-16所示。3D盒子源文件将在第16章中的3D打印有下载。

图5-15　无线控制板的3D盒子

图5-16　3D盒子内部图

将无线控制板连接安装到3D盒子里，如图5-17所示。

图5-17　3D盒子和无线控制板安装

使用手机充电器和USB线为模块供电，并将3D盒子固定到门后，如图5-18所示。

图5-18　3D盒子固定和供电模块

4．远程开门演示视频

可以扫描图5-19或通过网址查看远程开门演示视频。

图5-19　远程开门演示视频二维码

网址http://book.znck007.com/?p=kaimengshipin

5.6　常见问题及注意事项

【A】不可直接用在主门，防止小偷破解后入室偷盗。

【B】由于无线传输并非加密，因此可用于小区门、次门、卧室门等。

第6章　窗帘开关

窗帘开关使用市场上的无线遥控窗帘（轨道/电动机/遥控器），300元左右，如图6-1所示，再加上433无线控制板，如图6-2所示，即可实现窗帘的自动开、闭功能。

图6-1　市场上的无线遥控窗帘

图6-2　443无线控制板

6.1　用途

早上起床，窗帘自动慢慢打开，阳光照进房间，美好的一天开始了。

晚上悄悄的关闭窗帘，保护隐私和安心睡眠。

6.2　材料列表（见表6-1）、原理图（见图6-3）

表6-1　433无线控制板材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

433无线

1

图6-3　433无线控制板原理图

根据原理图和材料清单做成如图6-4所示板子。

图6-4　DIY成的板子

6.3　无线遥控窗帘

无线遥控窗帘最重要的部分是电动机，如图6-5所示。

图6-5　无线遥控窗帘电动机

拆开电动机的尾部，如图6-6所示。

图6-6　拆开电动机的尾部

拆开后，发现电动机采用433模块控制，我们也采用433就可以控制它了，如图6-7所示。

图6-7　电动机内部机构

提示

使用433接收器接收电动机摇控器的信号，记录下来后，发送编码控制电动机。

6.4　代码实例

可以扫描图6-8或通过网址下载到PC。

图6-8　443无线控制板下载二维码

网址http://book.znck007.com/?p=433wuxiandaima

6.5　实战演示

1．DIY准备

准备的433无线控制板、USB线、NRF24L01无线、Zigbee自组网如图6-9所示。

图6-9　无线控制板DIY准备

2．编译刷写源代码

将433无线控制板连接电脑USB插口，如图6-10所示。

图6-10　433无线控制板连接电脑USB插口

使用Arduino软件打开433无线控制板的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图6-11所示。

图6-11　获取窗帘开关的sid、nid

3．使用3D打印盒子（见图6-12）

图6-12　将433无线控制板放入3D盒子

4．窗帘开关演示视频

可以扫描图6-13或通过网址查看窗帘开关演示视频。

图6-13　窗帘开关演示视频二维码

网址http://book.znck007.com/?p=kaichuangshipin

6.6　常见问题及注意事项

【A】市场上的无线摇控窗帘是需要定制的，如轨道长度、窗帘高度等，越大，价格会越贵些。

【B】在安装无线摇控窗帘时，建议请专业的安装师傅，保证家装的安全。

第7章　红外遥控

家里的电器很多采用红外线遥控，如电视、空调、机顶盒、风扇等。本章DIY的红外遥控是为了代替传统的遥控器，让其变身万能的红外遥控。当然，其中需要解码各种遥控器的编码。本章后面讲解如何破解市面上遥控器的详细方法。

我们采用3D打印盒子（见图7-1）和DIY的红外遥控板（见图7-2）实现红外摇控功能。

图7-1　3D打印盒子的外红遥控

图7-2　红外遥控板

7.1　用途

红外遥控是一种无线、非接触控制技术，具有抗干扰能力强、信息传输可靠、功耗低、成本低、易实现等显著优点，被诸多电子设备，特别是家用电器广泛采用。

如果能够DIY出模拟红外遥控信号的模块，那么就能轻松控制家用电器，如电视、空调、机顶盒、电风扇、VCD、录像机等。

市场上的遥控器品类非常繁多，要编码出所有遥控器的信号比较困难，因些我们只讲述编码的参考方法。在一般情况下，同品牌或同系列的遥控器及其编码都是一样的。

7.2　材料列表（见表7-1）、原理图（见图7-3）

表7-1　红外遥控材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32K

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

HX1838红外接收头

1

940nm红外发射管

1

22μF电容

1206

1

贴片电阻10kΩ

0603

1

贴片电阻100Ω

0603

1

贴片电阻20Ω

0603

1

图7-3　红外遥控板原理图

根据原理图和材料列表DIY焊接成的板子如图7-4所示。

图7-4　DIY焊接成的板子

7.3　红外原理

红外线遥控是利用近红外光传送遥控指令的，波长为0.76～1.5μm。

红外发射电路是采用红外发光二极管发出经过调制的红外光波。

红外接收电路由红外接收二极管、三极管或硅光电池组成，接收电路将发射器发出的已调制的编码指令信号接收下来，放大后，送解调电路，解调电路将已调制的指令编码信号解调出来，即还原为编码信号，指令译码器将编码指令信号进行译码。

本章中发射Arduino红外线库的编码或自定义的编码，接收端收到编码再译码后即可实现指定的操作。

7.4　代码实例

可以扫描图7-5或通过网址下载到PC。

图7-5　红外遥控源代码下载二维码

网址http://book.znck007.com/?p=hongwaiyaokongdaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*ZIGBEE针脚连接线

TX -> RXD

RX -> TXD

*HX1838红外接收头针脚连接线

A5 -> DATA

GND -> GND

VCC -> VCC

*940nm红外发射针脚连接线

D3 -> DATA

GND -> GND

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

#include <EEPROM.h> //引用EEPROM库文件

#include <IRremote.h> //引用红外线库文件

int sid=6;//模块类型

int nid=1;//模块编号

//无线串口通信处理（zigbee/bluetooth等）

unsigned long serial1nowlast; //串口检测间隔时间

char serial1buff[129]={0}; //串口缓存字符

char serial1Data; //串口单个字符

int serial1i=0; //串口字符数

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

//声名变量

int pinIn0=A0; //手动触发针脚

int val0; //当前触发值

int pinOut0=A1; //输出针脚

IRsend irsend; //声明红外变量

int isdump=0; //0为自学 1为发射

int RECV_PIN = A5; //红外接收针脚

IRrecv irrecv(RECV_PIN); //声明接收类

decode_results results; //红外解码结果类

void setup()

{

Serial.begin(115200);//设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

pinMode(pinIn0,INPUT); //设置输入模式

pinMode(pinOut0,OUTPUT); //设置输出输式

pinMode(pinIn0,INPUT_PULLUP); //将管脚设置为输入并且内部上拉模式

digitalWrite(pinIn0, HIGH); //设置默认高电位

irrecv.enableIRIn(); //启动红外接收

Serial.println("zwifi_hongweixian"); //打印

}

void loop()

{

//检测无线串口数据处理（zigbee/bluetooth等）

{

unsigned long serial1now = millis();//获取现在的时间

if(serial1now - serial1nowlast >= 5000)//如果数据间隔超过5秒而清空字符（为了防止数据错乱）

{

serial1nowlast = millis();//记录当前时间

memset(serial1buff, 0, 129);//清空缓存字符

serial1i=0;//初始数组数

}

while( Serial.available() )//如果无线串口有数据

{

if(serial1i= =0)

{

Serial.println("serial->");//打印出来方便调试

}

serial1Data=(char)Serial.read();//读取串口数据

//Serial.print(serial1Data);//这里不打印，否则检测到{ckxxxx}就认为是命令

serial1buff[serial1i]=serial1Data;////保存到数组

serial1i++;//数组长度+1

if(serial1Data= ='}' || serial1i>=129)//如果发现}而说明命令结束（并少于129个字符，太长会出错）

{

serial1nowlast = millis(); //记录当前时间，不然5秒就超时了

char body[129]={0};//声明新字符数组

get_znck_body(serial1buff,body);//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

//serial.println(body); //这里不打印，否则检测到{ckxxxx}就认为是命令

//如果命令格式正确则做处理

if(strstr(body,"{ck") && strstr(body,"}") )

{

//Serial.println(body);//这里不打印，否则检测到{ckxxxx}就认为是命令

if(strlen(body)>10) //字符数需大于10

{

set_infrared(body); //调用红外函数

}

}

serial1i=0;//字符数组长度初始为0

Serial.println("-------------------");//打印结束标示

delay(100);//延时

}

}

}

unsigned long nrf24l01now = millis();//获取现在的时间

if(nrf24l01now - nrf24l01nowlast >= 5000)//如果数据间隔超过5秒而清空字符（为了防止数据错乱）

{

nrf24l01nowlast = millis();//记录当前时间

memset(nrf24l01buff, 0, 33);//清空缓存字符

nrf24l01i=0;//初始数组数

}

byte data[Mirf.payload];//声明字节

if(Mirf.dataReady()){//如果有数据

Mirf.getData(data);//获取数据

Mirf.rxFifoEmpty();//清理NRF24L01援存

//Serial.println((char)*data);//这里不打印，否则检测到{ckxxxx}就认为是命令

for (int i = 0; i < Mirf.payload; i++) //把收到的信息拼起来

{

if(nrf24l01i= =0)

{

Serial.println("nrf24l01->");//打印出来方便调试

}

nrf24l01Data=(char)data[i];//读取NRF24L01数据

if( nrf24l01Data= ='{') nrf24l01i=0;//如果发现{则设长度为0开始

nrf24l01buff[nrf24l01i]=nrf24l01Data;//保存到数组

nrf24l01i++;//数组长度+1

if(nrf24l01Data= ='}' || nrf24l01i>=33)//如果发现}而说明命令结束（并少于33个字符，太长会出错）

{

nrf24l01nowlast = millis(); //更新当前时间，不然5秒就超时了

char body[33]={0};//声明新数组

get_znck_body(nrf24l01buff,body);//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

//Serial.println(body);//这里不打印，否则检测到{ckxxxx}就认为是命令

//如果命令格式正确则做处理

if(strstr(body,"{ck") && strstr(body,"}") )

{

//Serial.println(body);//这里不打印，否则检测到{ckxxxx}就认为是命令

if(strlen(body)>10) //字符数需大于10

{

set_infrared(body); //调用红外函数

}

}

memset(nrf24l01buff, 0, 33);//字符数组长度初始为0

nrf24l01i=0;//字符数组长度初始为0

Serial.println("-------------------");//打印结束标示

delay(100);//延时

}

}

}

val0=digitalRead(pinIn0);//读取输入数据

if(val0= =LOW || isdump= =1)//检测按键是否按下

{

digitalWrite(pinOut0,HIGH);//则设为高电位

if (irrecv.decode(&results)) { //如果红外编码正确

Serial.println(results.value, HEX); //打印编码值

dump(&results); //dump编码

irrecv.resume(); //接收下一个红外编码

}

}

digitalWrite(pinOut0,LOW); //则设为低电位

delay(10); //延时

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取只是{ckxxxxxx}的字符，因为这是我们的命令格式

void get_znck_body(char *p,char *s){

char rechar[33]={0};//声明数组

int bufi=0;//当前长度

bool isend=false;//开始和结束

int charnum=0;//字符长度

for(bufi=0;bufi<strlen(p);bufi++){//循环截取指定字符

//Serial.print(p[bufi]);

if(p[bufi] = ='{'){//如果是{则开始截取

isend=true;//设为ture

}

if(p[bufi]= ='}' && isend= =true){//如果是}则结束截取

isend=false;//设为false

rechar[charnum]=p[bufi];//赋值到新数组

break;

}

if(isend){//如果为true则赋值

if(charnum<33)//小于指定长度

{

rechar[charnum]=p[bufi];//赋值到新数组

charnum++;//字符长度加1

}

}

}

//Serial.println("");

//memcpy(s,rechar,33);

sprintf(s,"%s",rechar);//赋值返回

}

//设置红外函数

void set_infrared(char *body){

int s=get_sid(body);//获取sid

int n=get_nid(body);//获取nid

char d[32]={0};//声明字符组

get_data(body,d);//获取data

Serial.println(d);//打印字符

if(  s= =sid && n= =nid )//如果是本设备

{

if( strlen(d)= =1 && d[0]= ='0' )//如果数据是0

{

isdump=1;//接收红外自学模式

}

if( strlen(d)= =1 && d[0]= ='1' )//如果数据是1

{

char sdata[20]={0};//声明字符组

sendInfrared(sdata);//发送rom红外编码

}

if(  strlen(d)>5 )//如果字符大于5个

{

sendInfrared(d);//发送字符编码

}

}

//更新反馈数据到网关

if(  s= =sid ){//如果是本设备

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",s,n);//赋值前缀

sprintf(updateData,"%supdate}",front);//生成反馈数组

Serial.println(updateData);//发送到串口（zigbee会发给网关）

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

delay(2000);

}

}

//获取sid函数

int get_sid(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charSid[4]={0};//声明数组

memcpy(charSid,buff+3,3);//赋值截取第3位后面的3个字符

Serial.println(charSid);//打印

int intSid=atoi(charSid);//转成int

Serial.println(intSid);//打印

return intSid;//返回sid

}

else

{

return 0;//格式不对返回0

}

}

//获取nid函数

int get_nid(char *buff){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

char charNid[4]={0};//声明数组

memcpy(charNid,buff+6,3);//赋值截取第6位后面的3个字符

Serial.println(charNid);//打印

int intNid=atoi(charNid);//转成int

Serial.println(intNid);//打印

return intNid;//返回nid

}

else

{

return 0;//格式不对返回0

}

}

//获取data函数

int get_data(char *buff,char *sdata){

if( strstr(buff,"{ck") && strstr(buff,"}") && strlen(buff)>10)//如果含{ck和}，并长度大于10

{

for (int i = 0; i < strlen(buff); i++) {//循环原字符

if( buff[9+i]= ='}' ) break;//遇到}而退出循环

sdata[i]=buff[9+i];//赋值从第9位开始

}

return 1;//返回1

}

else

{

return 0;//格式不对返回0

}

}

//红外解码结构函数

void dump(decode_results *results) {

int count = results->rawlen;

if (results->decode_type = = UNKNOWN) {

return;

Serial.print("Unknown encoding: ");

}

else if (results->decode_type = = NEC) {

Serial.print("Decoded NEC: ");

}

else if (results->decode_type = = SONY) {

Serial.print("Decoded SONY: ");

}

else if (results->decode_type = = RC5) {

Serial.print("Decoded RC5: ");

}

else if (results->decode_type = = RC6) {

Serial.print("Decoded RC6: ");

}

else if (results->decode_type = = PANASONIC) {

Serial.print("Decoded PANASONIC - Address: ");

Serial.print(results->panasonicAddress,HEX);

Serial.print(" Value: ");

}

else if (results->decode_type = = JVC) {

Serial.print("Decoded JVC: ");

}

else {

return;

}

Serial.print(results->value, DEC);

Serial.print(" (");

Serial.print(results->bits, DEC);

Serial.println(" bits)");

Serial.print("Raw (");

Serial.print(count, DEC);

Serial.print("): ");

//打印红外编码

for (int i = 0; i < count; i++) {

if ((i % 2) = = 1) {

Serial.print(results->rawbuf[i]*USECPERTICK, DEC);

}

else {

Serial.print(-(int)results->rawbuf[i]*USECPERTICK, DEC);

}

}

Serial.println("");

//生成新格式字符

char rawbuf[255]={0};

char decode_type[10]={0};

sprintf(decode_type,"%d,",results->decode_type);

strcat(rawbuf,decode_type);

char decode_value[16]={0};

sprintf(decode_value,"%ld,",results->value);

strcat(rawbuf,decode_value);

char decode_bits[10]={0};

sprintf(decode_bits,"%d,",results->bits);

strcat(rawbuf,decode_bits);

//写入rom保存

for (int i = 0; i < 256; i++) {

EEPROM.write(i, 0);

}

for (int i = 0; i < strlen(rawbuf); i++) {

EEPROM.write(i, rawbuf[i]);

}

Serial.println(rawbuf);

Serial.print("EEPROM.write count=");

Serial.print(strlen(rawbuf));

Serial.println("");

isdump=0;//设置为发送模式

}

//发送红外函数

void sendInfrared(char* data){

Serial.print("sendEEPROM=");

Serial.println(data);

char eepchar[32]={0};//声明字符数组

if( strlen(data)>5 ){//如果数据字符大于5

memcpy(eepchar,data,strlen(data));//赋值到新数组

}else{//否则读取rom数组

for (int i = 0; i < 256; i++) {

long eep = EEPROM.read(i);

if(eep= =0){

break;

}

eepchar[i]=eep;

Serial.print(eep,DEC);

Serial.print(" ");

}

}

Serial.println("");

Serial.println(eepchar);

//获取编码类型、数值、字节

int ir_type=get_ir_type(eepchar);

long ir_value=get_ir_value(eepchar);

int ir_bits=get_ir_bits(eepchar);

Serial.println(ir_type); Serial.println(ir_value); Serial.println(ir_bits);

//发送字符

if( ir_type>0 && ir_value>0 && ir_bits>0 ){

Serial.println("irsend.sendRaw");

irsenddata(ir_type,ir_value,ir_bits);//根据红外编码类型发送

}

//irsend.sendRaw(rawbuf, strlen(rawbuf), 38 /* kHz */);

//irsend.sendNEC(0x82e0040a, 32);

}

//根据红外编码类型发送

void irsenddata(int type, unsigned long value, int bits) {

if (type = = NEC) {

irsend.sendNEC(value, bits);

Serial.print("sendNEC=");

Serial.print(value,DEC);

Serial.print(" bits=");

Serial.print(bits,DEC);

Serial.println("");

}

else if (type = = SONY) {

irsend.sendSony(value, bits);

Serial.print("sendSony=");

Serial.print(value,DEC);

Serial.print(" bits=");

Serial.print(bits,DEC);

Serial.println("");

}

else if (type = = RC5) {

irsend.sendRC5(value, bits);

Serial.print("sendRC5=");

Serial.print(value,DEC);

Serial.print(" bits=");

Serial.print(bits,DEC);

Serial.println("");

}

else if (type = = RC6) {

irsend.sendRC6(value, bits);

Serial.print("sendRC6=");

Serial.print(value,DEC);

Serial.print(" bits=");

Serial.print(bits,DEC);

Serial.println("");

}

else {

Serial.print(type);

Serial.println("Bad type!");

}

delay(200);

}

//获取红外编码的类型

int get_ir_type(char *buff){

if( strstr(buff,",") && strlen(buff)>5)

{

int n=0;

int m=0;

char chartype[4]={0};

for(int i=0;i< strlen(buff);i++){

if((char)buff[i]= =','){

n++;

if(n= =1) break;

}

chartype[m]=buff[i];

m++;

}

//memcpy(chartype,buff+6,3);

Serial.print("get_ir_type");

Serial.print(" char=");

Serial.print(chartype);

int inttype=atoi(chartype);

Serial.print(" int=");

Serial.print(inttype);

Serial.println("");

return inttype;

}

else

{

return 0;

}

}

//获取红外编码的数值

long get_ir_value(char *buff){

if( strstr(buff,",") && strlen(buff)>5)

{

int n=0;

int m=0;

char charvalue[16]={0};

for(int i=0;i< strlen(buff);i++){

if(buff[i]= =','){

n++;

if(n= =1) { m=0; memset(charvalue,0,16); continue;};

if(n= =2) break;

}

charvalue[m]=buff[i];

m++;

}

//memcpy(charvalue,buff+6,3);

Serial.print("get_ir_value");

Serial.print(" char=");

Serial.print(charvalue);

long longvalue=atol(charvalue);

Serial.print(" long=");

Serial.print(longvalue);

Serial.println("");

return longvalue;

}

else

{

return 0;

}

}

//获取红外编码的字节

int get_ir_bits(char *buff){

if( strstr(buff,",") && strlen(buff)>5)

{

int n=0;

int m=0;

char charbits[16]={0};

for(int i=0;i< strlen(buff);i++){

if((char)buff[i]= =','){

n++;

if(n= =1) { m=0; memset(charbits,0,16); continue;};

if(n= =2) { m=0; memset(charbits,0,16); continue;};

if(n= =3) break;

}

charbits[m]=buff[i];

m++;

}

//memcpy(charvalue,buff+6,3);

Serial.print("get_ir_bits");

Serial.print(" char=");

Serial.print(charbits);

int intbits=atoi(charbits);

Serial.print(" int=");

Serial.print(intbits);

Serial.println("");

return intbits;

}

else

{

return 0;

}

}

7.5　实战演示

1．DIY准备

我们要准备红外遥控板、USB线、NRF24L01无线、Zigbee自组网、3D打印模型，如图7-6所示。

图7-6　DIY准备

2．源代码编译

将红外遥控板连接电脑USB插口，如图7-7所示。

图7-7　红外遥控板连接电脑

使用Arduino软件打开红外遥控的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图7-8所示。

图7-8　云端红外控制页面

3．红外遥控测试

进入云端红外控制页面，修改发送数据为0，单击发射图标，如图7-9所示。

图7-9　手机进入云端测试

提示发送数据定义：

0为自学解码；1为发射自学的编码；1,16754775,32,发送已知红外编码。

此时，无线通信成功，Arduino串口监视器显示如图7-10所示。

图7-10　Arduino串口监视器显示

使用遥控器（不是所有的遥控器都可以）对着红外遥控板的接收按下某个按键，如图7-11所示。

图7-11　按下遥控器的某个按键

出现如图7-12所示的显示，那么解码就成功了，复制“转成字符”保存好，以后可以直接发送该字符，就是该按键的编码了。

图7-12　解码成功显示

在云端页面发送1或1,16712445,32，如图7-13所示。

图7-13　发送已知编码格式

编码成功、发射红外成功的显示如图7-14所示。

图7-14　编码成功、发射红外成功的显示

4．红外遥控演示视频

可以扫描图7-15或通过网址查看红外遥控演示视频。

图7-15　红外遥控演示视频二维码

网址http://book.znck007.com/?p=hongwaiyaokongshipin

7.6　市面上的空调、电视解码

如果无法通过程序解到正确的编码，则需要通过一个叫“逻辑分析仪”的设备分析出来空调、电视等的正确编码，如图7-16所示，市场上有，但可能长得不太一样。

图7-16　逻辑分析仪

可以扫描图7-17或通过网址查看具体的使用方法。

图7-17　逻辑分析仪使用方法二维码

网址http://book.znck007.com/?p=luojifengxiqi

7.7　常见问题及注意事项

【A】不是所有的遥控器都可以直接解码，程序只支持NEC、SONY、RC5、RC6、PANASONIC、JVC编码，其他的需要使用逻辑分析仪解码。

【B】如果NRF24L01出现传输丢包的现象，则可能会重复发送。

读累了记得休息一会哦~

公众号：古德猫宁李

- 电子书搜索下载

- 书单分享

- 书友学习交流

网站：沉金书屋 https://www.chenjin5.com

- 电子书搜索下载

- 电子书打包资源分享

- 学习资源分享

第8章　温湿监控

2014年，NEST公司凭着恒温器、烟雾探测器被谷歌宣布以32亿美元收购的计划。恒温器的主要功能是智能检测温度，并自动控制暖气等设备来调节家中的温度。

我们DIY的温湿监控由3D打印模型（见图8-1）和温湿传感板（见图8-2）构成，可以检测到当前的温度、湿度，方便查看、分析、操作等。

图8-1　温湿监控3D外壳

图8-2　温湿传感板

8.1　用途

通过温湿监控可以时时了解家里的温度和湿度，方便及时调节。如果用在工业领域，如烟房、蚕房、养殖厂、农业等，则更有商业价值。

8.2　材料列表（见表8-1）、原理图（见图8-3）

表8-1　温湿传感板材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

DHT11数字式温湿度传感器

1

DS18B20传感器

1

贴片电阻4.7kΩ

0603

2

图8-3　温湿传感板原理图

根据原理图和材料清单DIY的温湿传感板如图8-4所示。

图8-4　DIY的温湿传感板

8.3　温度/湿度传感器说明

DS18B20是常用的温度传感器，具有体积小、硬件开销低、抗干扰能力强、精度高等特点，如图8-5所示。

图8-5　DS18B20针脚介绍图

DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器，应用专用的数字模块采集技术和温湿度传感技术，确保产品具有极高的可靠性和卓越的长期稳定性，如图8-6所示。

图8-6　DHT11介绍图

8.4　代码实例

可以扫描图8-7或通过网址下载到PC。

图8-7　温湿监控源代码下载二维码

网址http://book.znck007.com/?p=wengshidaima

打开源代码如下：

/*

*NRF24101针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*ZIGBEE针脚连接线

TX -> RXD

RX -> TXD

*DS18B20温度传感器针脚连接线

GND -> GND

DQ -> A5

VDD -> VCC

*DHT11数字温湿度传感器针脚连接线

VDD -> VCC

DATA -> A4

NC -> 空脚

GND -> GND

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

#include <OneWire.h> //引用DS18B20库文件

int sid=1;//模块类型

int nid=2;//模块编号

//声名变量

const unsigned long interval = 60000; //时间间隔

unsigned long last_sent;//最后发送时间

One Wire  ds(A5); //声明ds变量

float celsius=0.0; //摄氏温度

float fahrenheit=0.0; //华氏温度

int DHT11_PIN=4; //DHT11针脚

int humdity=0; //湿度

int temperature=0; //温度

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

void setup()

{

Serial.begin(115200); //设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

ini_dht11(); //初始化dht11

Serial.println("zwifi_wengshitu"); //打印

}

void loop()

{

unsigned long now = millis();//获取现在的时间

if ( now - last_sent >= interval  ) //如果超过时间间隔

{

last_sent = now;//保存到最后时间

celsius=0;//初始化0

humdity=0;//初始化0

run_ds18();//调用函数DS18B20获取温度

if(celsius>0){//如果温度大于0

run_dht11();//调用函数dht11获取湿度

}

if(humdity>0){//如果湿度大于0

char celstr[10]={0};//声明字符

dtostrf(celsius,4,2,celstr);//格式化字符

char data[10]={0};//声明数据字符

sprintf(data,"%d.%s",humdity,celstr);//格式化字符 湿度.温度

send_data(data);//调用函数发送数据到网关

}

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取DS18S20传感器的温度函数

void run_ds18()

{

byte i;

byte present = 0;

byte type_s;

byte data[12];

byte addr[8];

//float celsius, fahrenheit;

if ( !ds.search(addr)) {

//Serial.println("No more addresses.");

//Serial.println();

ds.reset_search();

delay(250);

return;

}

//Serial.print("ROM =");

for( i = 0; i < 8; i++) {

//Serial.write(' ');

//Serial.print(addr[i], HEX);

}

if (OneWire::crc8(addr, 7) != addr[7]) {

Serial.println("CRC is not valid!");

return;

}

//Serial.println();

// the first ROM byte indicates which chip

switch (addr[0]) {

case 0x10:

Serial.print("Chip = DS18S20");  // or old DS1820

type_s = 1;

break;

case 0x28:

Serial.print("Chip = DS18B20");

type_s = 0;

break;

case 0x22:

Serial.print("Chip = DS1822");

type_s = 0;

break;

default:

Serial.print("Device is not a DS18x20 family device.");

return;

}

ds.reset();

ds.select(addr);

ds.write(0x44,1);         // start conversion, with parasite power on at the end

delay(1000);     // maybe 750ms is enough, maybe not

// we might do a ds.depower() here, but the reset will take care of it.

present = ds.reset();

ds.select(addr);

ds.write(0xBE);         // Read Scratchpad

//Serial.print("  Data = ");

//Serial.print(present,HEX);

//Serial.print(" ");

for ( i = 0; i < 9; i++) {           // we need 9 bytes

data[i] = ds.read();

//Serial.print(data[i], HEX);

//Serial.print(" ");

}

//Serial.print(" CRC=");

//Serial.print(OneWire::crc8(data, 8), HEX);

//Serial.println();

// convert the data to actual temperature

unsigned int raw = (data[1] << 8) | data[0];

if (type_s) {

raw = raw << 3; // 9 bit resolution default

if (data[7] = = 0x10) {

// count remain gives full 12 bit resolution

raw = (raw & 0xFFF0) + 12 - data[6];

}

} else {

byte cfg = (data[4] & 0x60);

if (cfg = = 0x00) raw = raw << 3;  // 9 bit resolution, 93.75 ms

else if (cfg = = 0x20) raw = raw << 2; // 10 bit res, 187.5 ms

else if (cfg = = 0x40) raw = raw << 1; // 11 bit res, 375 ms

// default is 12 bit resolution, 750 ms conversion time

}

celsius = (float)raw / 16.0;

fahrenheit = celsius * 1.8 + 32.0;

Serial.print("  Temperature = ");

Serial.print(celsius);

Serial.print(" Celsius, ");

Serial.print(fahrenheit);

Serial.println(" Fahrenheit");

}

//初始化dht11函数

void ini_dht11()

{

DDRC|=_BV(DHT11_PIN);

PORTC|=_BV(DHT11_PIN);

}

//读获dht11数据函数

byte read_dht11_dat()

{

byte i = 0;

byte result = 0;

for(i=0;i<8;i++)

{

while(!(PINC&_BV(DHT11_PIN)));

delayMicroseconds(30);

if(PINC&_BV(DHT11_PIN))

result|=(1<<(7-i));

while((PINC&_BV(DHT11_PIN)));

}

return result;

}

//获取dht11传感器湿度的函数

void run_dht11()

{

byte dht11_dat[5];

byte dht11_in;

byte i;

PORTC &= ~_BV(DHT11_PIN);

delay(18);

PORTC|=_BV(DHT11_PIN);

delayMicroseconds(40);

DDRC &= ~_BV(DHT11_PIN);

delayMicroseconds(40);

dht11_in = PINC & _BV(DHT11_PIN);

if(dht11_in)

{

Serial.println("dht11 start condition 1 not met");

return;

}

delayMicroseconds(80);

dht11_in=PINC & _BV(DHT11_PIN);

if(!dht11_in)

{

Serial.println("dht11 start condition 2 not met");

return;

}

delayMicroseconds(80);

for(i=0;i<5;i++)

dht11_dat[i]=read_dht11_dat();

DDRC|=_BV(DHT11_PIN);

PORTC|=_BV(DHT11_PIN);

byte dht11_check_sum = dht11_dat[0]+dht11_dat[1]+dht11_dat[2]+dht11_dat[3];

if(dht11_dat[4]!=dht11_check_sum)

{

Serial.println("DHT11 checksum error");

}

Serial.print("Chip = DHT11  Current humdity= ");

Serial.print(dht11_dat[0],DEC);

Serial.print(".");

Serial.print(dht11_dat[1],DEC);

Serial.print("%");

Serial.print("temperature = ");

Serial.print(dht11_dat[2],DEC);

Serial.print(".");

Serial.print(dht11_dat[3],DEC);

Serial.println("C");

humdity=(int)dht11_dat[0];

temperature=(int)dht11_dat[2];

}

//发送数据到网关的函数

void send_data(char *data){

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",sid,nid);//赋值前缀

sprintf(updateData,"%s%s}",front,data);//生成反馈数组

Serial.println(updateData);//发送到串口（zigbee会发给网关）

Serial.println();//换行

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

}

8.5　实战演示

1．DIY准备

准备的温湿传感板、USB线、NRF24L01无线、Zigbee自组网、3D打印模型如图8-8所示。

图8-8　准备的材料

2．编译刷写源代码

将温湿传感板连接电脑USB插口，如图8-9所示。

图8-9　连接电脑USB插口

使用Arduino软件打开温湿监控的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图8-10所示。

图8-10　获取温湿的sid、nid

3．温湿监控测试

打开串口监视器，1分钟后，如果打印出如图8-11所示，那么获取温度和湿度就成功了。

图8-11　串口监视器打印

此时，如果云端页面数据也更新了，那么无线通信也成功了，如图8-12所示。

图8-12　云端页面更新

此时，不论在哪里，打开手机就可以看到时时的温湿度了。

4．温湿监控演示视频

可以扫描图8-13或通过网址查看温湿监控演示视频。

图8-13　温湿监控演示视频二维码

网址http://book.znck007.com/?p=wengshishipin

8.6　常见问题及注意事项

【A】DHT11本身也可以获取温度，但没有DS18B20精确。

【B】获取间隔建议为60秒以上，可以减轻网关无线模块的收、发压力。

第9章　空气质量

北京的雾霾让人们心惊，空气的质量影响千家万户。我们DIY一个可以检测PM2.5的模块，如图9-1所示，包含3D打印模型、PM2.5传感板，如图9-2所示。

图9-1　空气质量3D打印模型

图9-2　PM2.5传感板

9.1　用途

时时了解PM2.5，帮助我们改善居家空气环境。

9.2　材料列表（见表9-1）、原理图（见图9-3）

表9-1　PM2.5传感板材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

GP2Y1050AU

第二代

1

空气灰尘传感器

图9-3　PM2.5传感板原理图

根据原理图和材料清单DIY的PM2.5传感板如图9-4所示。

图9-4　DIY的PM2.5传感板

9.3　PM2.5介绍

细颗粒物又称细粒、细颗粒、PM2.5。细颗粒物指环境空气中，空气动力学当量直径小于等于2.5微米的颗粒物。它能较长的时间悬浮于空气中，在空气中的含量浓度越高，就代表空气污染越严重。虽然PM2.5只是地球大气成分中含量很少的组分，但它对空气质量和能见度等有重要的影响。与较粗的大气颗粒物相比，PM2.5粒径小，面积大，活性强，易附带有毒、有害物质（如重金属、微生物等），且在大气中的停留时间长、输送距离远，因而对人体健康和大气环境质量的影响更大。

我们采用GP2Y1050AU灰尘传感器检测PM2.5，如图9-5所示。它是第二代灰尘传感器，拥用独立的MCU，并使用串口输出灰尘数据。

图9-5　GP2Y1050AU灰尘传感器

PM2.5检测网空气质量新标准：

优

0～35

良

35～75

轻度污染

75～115

中度污染

115～150

重度污染

150～250

严重污染

大于250及以上

9.4　代码实例

可以扫描图9-6或通过网址下载到PC。

图9-6　PM2.5传感板源代码下载二维码

网址http://book.znck007.com/?p=kongqidaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*Zigbee针脚连接线

TX -> RXD

RX -> TXD

*GP2Y1050AU灰尘传感器针脚连接线

GND -> GND

VCC -> VCC

NC

NC

RXD  -> A1

TXD  -> A0

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

#include <SoftwareSerial.h> //引用软串口库文件

int sid=1;//模块类型

int nid=3;//模块编号

//声名变量

const unsigned long interval = 60000;//时间间隔

unsigned long last_sent;//最后发送时间

SoftwareSerial mySerial(A0, A1); //定义软串口，对应RX,TX

float pm25=0.00;//声明pm2.5变量

int incomeByte[6];//声明pm2.5字节

int data;//当前数据

int z=0;//当前字符数

int sum;//数据总和

unsigned long error;//错误

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

void setup()

{

Serial.begin(115200); //设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

mySerial.begin(2400);//设置软串口的波特率

Serial.println("zwifi_pm25");//打印

}

void loop()

{

unsigned long now = millis();//获取现在的时间

if ( now - last_sent >= interval  ) //如果超过时间间隔

{

last_sent = now;//保存到最后时间

pm25=0.00;//初始化0

run_pm25();//调用函数PM2.5获取数值

if(pm25>0){//如果PM2.5大于0

char pmstr[10]={0};//声明字符

dtostrf(pm25,4,2,pmstr);//格式化字符

char data[10]={0};//声明数据字符

sprintf(data,"%s",pmstr);//格式化字符

send_data(data);//调用函数发送数据到网关

}

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取pm2.5传感器的数据函数

void run_pm25()

{

while (mySerial.available()){//如果软串口存在数据

data=mySerial.read();//读取软串口数据

if(data = = 170){//如果数据是170则是开始

z=0;//当前字符数

incomeByte[z]=data;//保存到当前字节

}else{//如果不是开始

z++;//当前字符数+1

incomeByte[z]=data;//保存到当前字节

}

if(z= =6)//如果满6个字符

{

sum=incomeByte[1]+ incomeByte[2]+ incomeByte[3] + incomeByte[4];//字节数相加，为了效验数据

if(incomeByte[5]= =sum && incomeByte[6]= =255 )//效验数据格式

{

Serial.print("Data OK! ");

Serial.print(" | Vo=");

float vo=(incomeByte[1]*256.0+incomeByte[2])/1024.0*5.00;//计算pm2.5算法

Serial.print(vo,2);

Serial.print("V  ");

float v1=analogRead(A0)/1024.0*5.00;

Serial.print("/ Ao=");

Serial.print(v1,2);

Serial.print("V  | ");

float c=vo*200;

pm25=c;//保存PM2.5变量

Serial.print(c,2);

Serial.print("ug/m3 | ");

for(int k=0;k<7;k++)

{

Serial.print(incomeByte[k]);//打印原始数据

Serial.print("|");

}

Serial.println();

break;

}else{

z=0;

Serial.flush();

data='/0';

for(int m=0;m<7;m++){incomeByte[m]=0;}

error++;

Serial.print(" ### This is ");

Serial.print(error);

Serial.println(" Error ###");

}

z=0;

}

}

}

//发送数据到网关的函数

void send_data(char *data){

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",sid,nid);//赋值前缀

sprintf(updateData,"%s%s}",front,data);//生成反馈数组

Serial.println(updateData);//发送到串口（Zigbee会发给网关）

Serial.println();//换行

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

}

9.5　实战演示

1．DIY准备

准备PM2.5传感板、GP2Y1050AU、USB线、NRF24L01无线、Zigbee自组网、3D打印模型如图9-7所示。

图9-7　DIY前的准备

2．编译刷写源代码

将PM2.5传感板连接电脑USB插口，如图9-8所示。

图9-8　PM2.5传感板连接电脑USB插口

使用Arduino软件打开PM2.5传感板的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图9-9所示。

图9-9　获取空气质量的sid、nid

3．空气检测测试

打开串口监视器，1分钟后，如果打印出如图9-10所示，那么获取空气检测测试成功了。

图9-10　串口监视器打印

此时，如果云端页面数据也更新了，无线通信也成功了，如图9-11所示。 点支烟熏一下，串口监视器就会有变化了，如图9-12所示。

图9-11　云端页面更新

图9-12　点支烟熏一下

我们DIY的空气质量检测功能就完成了。

4．空气质量检测演示视频

可以扫描图9-13或通过网址查看空气质量演示视频。

图9-13　空气质量演示视频二维码

网址http://book.znck007.com/?p=kongqishipin

9.6　常见问题及注意事项

【A】PM2.5传感器尽量能与空气接触才能更好地检测到正确的数值。

【B】获取间隔建议为60秒以上，可以减轻网关无线模块的收、发压力。

第10章　烟雾火警

对于一个家庭而言，起火的原因有很多种，如用火不慎、吸烟、小孩玩火、电气设备老化、电气设备安装不合规范等。火灾给家庭带来严重的灾难，轻者财务损失，重者人身伤害。

本章我们将DIY烟雾火警，如图10-1所示，包含3D打印模型和烟雾传感板，如图10-2所示，当发现异常时，就向家居网关发送警报，家居网关再发送到云端服务器通知手机、邮件等。

图10-1　烟雾火警3D打印模型

图10-2　烟雾传感板

10.1　用途

烟雾火警探测器可预防火灾、煤气泄漏，并在第一时间检测出室内烟雾、煤气浓度变化，提前做出反应，在火势还没有蔓延开的时候就发出警报或者开启消防水阀。

10.2　材料列表（见表10-1）、原理图（见图10-3）

表10-1　烟雾传感板材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

MQ-2

1

烟雾传感器

图10-3　烟雾传感板原理图

根据原理图和材料清单DIY焊接好的板子如图10-4所示。

图10-4　DIY焊接好的板子

10.3　MQ-2说明

MQ-2气体传感器所使用的气敏材料是在清洁空气中电导率较低的二氧化锡(SnO2 )。当传感器所处的环境中存在可燃气体时，传感器的电导率随空气中可燃气体浓度的增加而增大。使用简单的电路即可将电导率的变化转换为与该气体浓度相对应的输出信号。

MQ-2气体传感器对液化气、丙烷、氢气的灵敏度高，对天然气和其他可燃蒸气的检测也很理想。这种传感器可检测多种可燃性气体，是一款适合多种应用的低成本传感器，如图10-5所示。

图10-5　MQ-2

10.4　代码实例

可以扫描图10-6或通过网址下载到PC。

图10-6　烟雾火警源代码下载二维码

网址http://book.znck007.com/?p=yanwuhuojingdaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*ZIGBEE针脚连接线

TX -> RXD

RX -> TXD

*MQ-2传感器针脚连接线

VCC -> VCC

GND -> GND

DO

AO  -> A5

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

int sid=1;//模块类型

int nid=5;//模块编号

//声名变量

int mq_2alarm=500;//报警值

const unsigned long interval = 1000;//间隔时间

unsigned long last_sent;//最后发送时间

int mq_2val=0;//MQ-2数值

int mq_2_PIN=A5;//MQ-2针脚

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

void setup()

{

Serial.begin(115200);//设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

Serial.println("zwifi_mq_2_sensor");//打印

}

void loop()

{

unsigned long now = millis();//获取现在的时间

if ( now - last_sent >= interval  ) //如果超过时间间隔

{

last_sent = now;//保存到最后时间

mq_2val=0;//初始化0

run_mq_2();//调用函数mq-2数据

if(mq_2val>mq_2alarm){//如果值大于警报值则发送警报

char data[10]={0};//声明字符

sprintf(data,"%d",mq_2val);//格式化字符

send_data(data);//调用函数发送数据到网关

}

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取MQ-2传感器的数据函数

void run_mq_2()

{

mq_2val=analogRead(mq_2_PIN);//读取

Serial.println(mq_2val,DEC);//打印

delay(100);//延时

}

//发送数据到网关的函数

void send_data(char *data){

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",sid,nid);//赋值前缀

sprintf(updateData,"%s%s}",front,data);//生成反馈数组

Serial.println(updateData);//发送到串口（Zigbee会发给网关）

Serial.println();//换行

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

}

10.5　实战演示

1．DIY准备

准备Arduino板、MQ-2气体传感器、USB线、NRF24L01无线、Zigbee自组网、3D打印模型，如图10-7所示。

图10-7　DIY前的准备

2．编译刷写源代码

将烟雾传感板连接电脑USB插口，如图10-8所示。

图10-8　烟雾传感板连接电脑USB

使用Arduino软件打开烟雾传感板的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图10-9所示。

图10-9　获取烟雾火警的sid、nid

3．烟雾火警测试

打开串口监视器，如果打印出如图10-10所示，那么获取气体值成功了。

图10-10　串口监视器打印

点支烟熏一下，串口监视器就会有变化了，如图10-11所示。

图10-11　点支烟熏一下

如果MQ-2的值（如int mq_2alarm=500）大于警报值500，就会向网关发送报警字符，如图10-12所示。

图10-12　烟雾警报

4．烟雾火警演示视频

可以扫描图10-13或通过网址查看烟雾火警演示视频。

图10-13　烟雾火警演示视频二维码

网址http://book.znck007.com/?p=yanwuhuojingshipin

10.6　常见问题及注意事项

【A】MQ-2还可以检测甲烷、一氧化碳等气体。

【B】MQ-2一般通电几个小时后，检测就会更准确一些。

【C】MQ传感系列还有很多其他型号，如MQ-6、MQ-135等。

第11章　水滴检测

每当下雨时或漏水时，阳台上的衣服、物品不及时处理就会被淋湿，可能会造成不小的麻烦，如果有了水滴传感器，就可以提前知道下雨、房间漏水等。

本章带大家DIY水滴检测报警，如图11-1所示，包含3D打印模型和水滴传感模块，如图11-2所示。

图11-1　水滴检测3D打印模型

图11-2　水滴传感模块

11.1　用途

水滴传感模块主要用于检测室外的天气（是否下雨）、农作物叶面水分是否足够、水池或湖泊等是否涨水、室内是否漏水等。

11.2　材料列表（见表11-1）、原理图（见图11-3）

表11-1　水滴传感模块材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

雨滴检测板

1

雨滴主控小板

1

图11-3　水滴传感原理图

根据原理图和材料清单DIY的水滴传感板如图11-4所示。

图11-4　DIY的水滴传感板

11.3　水滴传感器说明

水滴传感器由一块雨滴检测板和主控小板组成。雨滴检测板通过两根线与控制小板相连后，放在需要检测的地方即可。控制小板由于涉及电路和元件，因此需要注意防水，否则会损坏整个板子，如图11-5所示。

图11-5　水滴传感模块

当雨滴检测板遇水时，DO会低电位，反之则是高电位。

11.4　代码实例

可以扫描图11-6或通过网址下载到PC。

图11-6　水滴传感模块源代码下载二维码

网址http://book.znck007.com/?p=shuididaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*Zigbee针脚连接线

TX -> RXD

RX -> TXD

*水滴传感器针脚连接线

VCC -> VCC

GND -> GND

DO  -> A5

AO

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

int sid=1;//模块类型

int nid=6;//模块编号

const unsigned long interval = 1000;//间隔时间

unsigned long last_sent;//最后发送时间

int shuidival=0;//水滴传感数值

int shuidi_PIN=A5;//水滴传感针脚

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

void setup()

{

Serial.begin(115200);//设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

pinMode(shuidi_PIN, INPUT);//设置输入模式

Serial.println("zwifi_shuidi");//打印

}

void loop()

{

unsigned long now = millis();//获取现在的时间

if ( now - last_sent >= interval  )//如果超过时间间隔

{

last_sent = now;//保存到最后时间

shuidival=0;//初始化0

run_shuidi();//调用函数获取水滴传感数据

if(shuidival= =LOW)///如果是警报值则发送警报，这里低电平是警报

{

char data[10]={0};//声明字符

sprintf(data,"%d",shuidival);//格式化字符

send_data(data);//调用函数发送数据到网关

delay(10000);//延时

}

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取水滴感器的数据函数

void run_shuidi()

{

shuidival=digitalRead(shuidi_PIN);//读取

Serial.println(shuidival,DEC);//打印

delay(100);//延时

}

//发送数据到网关的函数

void send_data(char *data){

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",sid,nid);//赋值前缀

sprintf(updateData,"%s%s}",front,data);//生成反馈数组

Serial.println(updateData);//发送到串口（zigbee会发给网关）

Serial.println();//换行

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

}

11.5　实战演示

1．DIY准备

准备雨滴检测板、雨滴控制小板、Arduino板、Zigbee自组网、NRF24L01无线、USB线、3D打印模型如图11-7所示。

图11-7　水滴检测DIY的准备

2．编译刷写源代码

将水滴传感模块连接电脑USB插口，如图11-8所示。

图11-8　水滴传感模块连接电脑USB插口

使用Arduino软件打开水滴传感模块的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图11-9所示。

图11-9　获取水滴检测的sid、nid

3．水滴检测测试

打开串口监视器，如果打印出如图11-10所示，那么获取水滴模块的值就成功了。

图11-10　串口监视器打印

倒点水给雨滴传感板，如图11-11所示，串口监视器就会有变化了，如图11-12所示。

图11-11　倒点水给雨滴传感板

图11-12　串口监视器打印

到这里，如果云端报警时间也更新了，那么DIY也就完成了！

4．水滴检测演示视频

可以扫描图11-13或通过网址查看水滴检测演示视频。

图11-13　水滴检测演示视频二维码

网址http://book.znck007.com/?p=shuidishipin

11.6　常见问题及注意事项

【A】水滴传感器顺时针调可提高灵敏度，逆时针调则相反。

【B】主控小板不可以接触水，只有雨滴检测板可以接触水。

第12章　人体红外探测器

本章我们采用被动红外探测器，靠探测人体发射的10μm左右的红外线，通过菲涅尔滤光片增强后，聚集到红外感应源上。红外感应源通常采用热释电元件，这种元件在接收到人体红外辐射温度发生变化时就会失去电荷平衡，向外释放电荷，后续电路经检测处理后，就能产生报警信号。

人体红外探测器由3D打印模型（如图12-1所示）和人体红外探测模块（如图12-2所示）构成。

图12-1　人体红外探测器3D打印模型

图12-2　人体红外探测模块

12.1　用途

外出后，当有人、动物等经过红外探测器时就会报警，防止家中被偷盗。

12.2　材料列表（见表12-1）、原理图（见图12-3）

表12-1　人体红外探测模块材料列表

名称

型号

数量

备注

ATMEGA328P-AU芯片

32k

1

Arduino主芯片

CH340G芯片

1

USB串口芯片

晶振

12M

1

晶振

16M

1

贴片SOT-23 XC6206P332MR (662K) 3.3V/0.5A稳压芯片

1

micro USB座

1

10μF电容

0603

2

0.1μF电容

0603

5

0.01μF电容

0603

1

贴片电阻1mΩ

0603

1

贴片电阻10kΩ

0603

1

贴片电阻1kΩ

0603

2

贴片电阻470Ω

0603

1

贴片led（红）

0805

1

1.27排母

n

2.54排母

n

2.54单排针

n

2.54跳线帽

1

HC-SR501

1

人体红外检测

图12-3　人体红外探测模块原理图

根据原理图和材料清单DIY的人体红外探测板如图12-4所示。

图12-4　DIY的人体红外探测板

12.3　HC-SR501传感器

HC-SR501是基于红外线技术的自动控制模块，采用德国原装进口LHI778探头设计，灵敏度高，可靠性强，超低电压工作模式，广泛应用于各类自动感应电器设备，尤其是干电池供电的自动控制产品，如图12-5所示。

图12-5　HC-SR501介绍图

12.4　代码实例

可以扫描图12-6或通过网址下载到PC。

图12-6　人体红外源代码下载二维码

网址http://book.znck007.com/?p=rentihongwaidaima

打开源代码如下：

/*

*NRF24l01针脚连接线

* MISO -> 12

* MOSI -> 11

* SCK -> 13

* Configurable:

* CE -> 8

* CSN -> 7

*Zigbee针脚连接线

TX -> RXD

RX -> TXD

*HC-SR501人体检测针脚连接线

VCC -> VCC

DO  -> A5

GND -> GND

*/

#include <SPI.h> //引用SPI库文件

#include <Mirf.h> //引用Mirf库文件

#include <nRF24L01.h> //引用NRF24L01库文件

#include <MirfHardwareSpiDriver.h> //引用Mirf驱动库文件

int sid=1;//模块类型

int nid=4;//模块编号

//声名变量

const unsigned long interval = 1000;//间隔时间

unsigned long last_sent;//最后发送时间

int rentival=0;//HC-SR501传感数值

int renti_PIN=A5;//HC-SR501传感针脚

//NRF24L01

unsigned long nrf24l01nowlast; //NRF24L01检测间隔时间

char nrf24l01buff[33]={0}; //NRF24L01缓存字符

char nrf24l01Data; //NRF24L01单个字符

int nrf24l01i=0; //NRF24L01字符数

void setup()

{

Serial.begin(115200);//设置串口波特率115200

//初始化Mirf，用于NRF24L01收发

char client[10]={0}; //客户端名称

sprintf(client,"clie%d",sid); //设置客户端名称

Mirf_Init(0,client,sid); //初始化NRF24L01收发

pinMode(renti_PIN, INPUT);//设置输入模式

Serial.println("zwifi_renti_sensor");//打印

}

void loop()

{

unsigned long now = millis();//获取现在的时间

if ( now - last_sent >= interval  )//如果超过时间间隔

{

last_sent = now;//保存到最后时间

rentival=0;//初始化0

run_renti();//调用函数获取HC-SR501红外传感数据

if(rentival= =HIGH){//如果是警报值则发送警报，这里高电平是警报

char data[10]={0};//声明字符

sprintf(data,"%d",rentival);//声明字符

send_data(data);//调用函数发送数据到网关

delay(10000);//延时

}

}

}

//初始化Mirf 0初始化1为接收2为发送

void Mirf_Init(int txrx,char *server,int channel){

//初始化Mirf

if(txrx= =0)  {

Mirf.spi = &MirfHardwareSpi;//设置spi

Mirf.init();//初始Mirf

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置接收模式

if(txrx= =1)  {

Mirf.setRADDR((byte *)server);//设置接收地址

}

//设置发送模式

if(txrx= =2)  {

Mirf.setTADDR((byte *)server);//设置发送地址

}

Mirf.payload = sizeof(char);//收发字节

Mirf.channel = channel;//设置频道

Mirf.config();//生效配置

}

//NRF24l01发送函数

void Mirf_Send(int channel,char *server,char *str){

Mirf_Init(2,server,channel);//设置发送模式

int bufi=0;//声明变量

for(bufi=0;bufi<strlen(str);bufi++){//循环发送

char words=str[bufi];//发送的字符

Mirf.send((byte *)&words);//发送命令

while(Mirf.isSending()){//等待发送完闭

}

delay(50);//延时，否则可能出现发送丢失现象

//Serial.print(words);

}

//Serial.println("");

}

//获取HC-SR501红外感器的数据函数

void run_renti()

{

rentival=digitalRead(renti_PIN);//读取

Serial.println(rentival,DEC);//打印

delay(100);//延时

}

//发送数据到网关的函数

void send_data(char *data){

char server[10]={0};//声明网关接收名称

sprintf(server,"serv%d",1);//赋值网关接收名称

//Serial.println(server);//打印

char updateData[33]={0};//声明更新数组

char front[10]={0};//声明更新前缀

//memcpy(front,body,9);

sprintf(front," {ck%03d%03d",sid,nid);//赋值前缀

sprintf(updateData,"%s%s}",front,data);//生成反馈数组

Serial.println(updateData);//发送到串口（Zigbee会发给网关）

Serial.println();//换行

Mirf_Send(1,server,updateData);//发送给NRF24L01（NRF24L01会发给网关）

char client[10]={0};//声明客户端

sprintf(client,"clie%d",sid);//赋值客户端名称

Mirf_Init(1,client,sid);//设置为接收模式

}

12.5　实战演示

1．DIY准备

准备人体红外探测、Arduino板、USB线、NRF24L01无线、Zigbee自组网如图12-7所示。

图12-7　人体红外探测DIY的准备

2．编译刷写源代码

将人体红外探测板连接电脑USB插口，如图12-8所示。

图12-8　人体红外探测板连接电脑USB插口

使用Arduino软件打开人体红外探测板的源代码，编译时，选择好COM口和Arduino Uno板，并修改源代码里的sid、nid，改成云端产生的sid、nid后进行编译，如图12-9所示。

图12-9　获取人体红外的sid、nid

3．人体红外检测测试

打开串口监视器，如果打印出如图12-10所示，那么获取人体红外传感值成功了。

图12-10　串口监视器打印

将手放在人体红外传感器前面，串口监视器打印如图12-11所示，向网关发送警报。 人体红外探测DIY完成。

图12-11　串口监视器打印报警字符

4．人体红外探测演示视频

可以扫描图12-12或通过网址查看人体红外探测演示视频。

图12-12　人体红外探测演示视频二维码

网址http://book.znck007.com/?p=rentihongwaishipin

12.6　常见问题及注意事项

【A】为了防止人体红外探测器误报，请调整好灵敏度。

【B】人体红外探测器也可以探测猫、狗等动物，报警只能证明有生物经过。

第13章　WiFi摄像头

WiFi摄像头，如图13-1所示。摄像头模块如图13-2所示。

图13-1　WiFi摄像头

图13-2　摄像头模块

13.1　用途

家居WiFi摄像头可以通过远程控制，在外地就可观看家里的情况，通过室内WiFi无线安装在家里的任何地方，可以通过手机、电脑在室外或外地看家里的各种情况。

13.2　材料列表（见表13-1）

表13-1　WiFi摄像头材料列表

名称

型号

数量

备注

Ar9331芯片路由器

703N

1

家用迷你路由器

USB扩展板

1

笔记本摄像头

1

需支持mjpg格式的摄像头

13.3　配置摄像头

DIY前的准备如图13-3所示。

图13-3　DIY前的准备

用网线将电脑和WiFi核心板连接，如图13-4，将自己的电脑IP改为192.168.1.173（150～255之间），电脑网关改为192.168.1.1。

图13-4　连接电脑网线

使用putty更新软件列表，如图13-5所示。

图13-5　使用putty更新软件列表

安装摄像头驱动软件如图13-6所示。

图13-6　安装摄像头驱动

opkg install mjpg-streamer

启动摄像头，如图13-7所示，表示成功！

图13-7　启动摄像头

mjpg_streamer -i "input_uvc.so /dev/video0 -r 640x480 " -o "output_http.so -p 8080 -w /www/webcam"

13.4　开机自动启动

在第2章“家居网关”的“winscp上传文件”中，init.d目录下有个znckmjpg，将其拖到/etc/init.d，如图13-8所示。

图13-8　将znckmjpg拖到/etc/init.d

znckmjpg的内容如下（实现在开机启动代码）：

#!/bin/sh /etc/rc.common

# Copyright (C) 2013 OpenWrt.org

START=99

start() {

mjpg_streamer -i "input_uvc.so /dev/video0 -r 640x480 " -o "output_http.so -p 8080 -w /www/webcam" &

}

stop() {

killall -9 mjpg_streamer

}

上传后，必须给文件权限，一般我们全打上勾，如图13-9所示。

图13-9　设置777权限

在putty输入以下命令，开机自启动，运行，如图13-10所示。

图13-10　自启动运行

/etc/init.d/znckmjpg enable

/etc/init.d/znckmjpg start

提示

/etc/init.d/znckmjpg disable禁止启动时自动运行

/etc/init.d/znckmjpg stop停止znckmjpg：

13.5　端口映射

（1）家用路由器会有一个端口映射功能，但不同名牌的名称有的叫法可能有不同，有如“转发规则”、“NAT”等，设置方法类似，如图13-11所示。

图13-11　路由器端口映射

原理：家里路由器→映射到OpenWrt的某一端口→OpenWrt→再映射到192.168.1.1的8080端口上。

（2）单击网络→无线→搜索，连接家里的WiFi，设置OpenWrt静态IP，如图13-12所示。

图13-12　连接家里的WiFi

单击网络→接口→修改，如图13-13所示。

图13-13　网络→接口→修改

（3）选择静态地址后，切换协议，如图13-14所示。

图13-14　切换静态地址

这一步设置固定IP地址很重要，如家里的路由器网关是192.168.14.1，所以我设置的IP就是192.168.14.100～192.168.14.250都可以，如图13-15所示。

图13-15　设置固定IP地址

提示

在电脑上查看自己的网关是多少，只需要程序→运行→打开cmd→ipconfig，就可以看到网关了！

（4）最后一步需要设置OpenWrt的端口转发，如图13-16所示。

图13-16　设备端口转发

提示

外部端口要与家里路由器对应上才可以。

13.6　网页和手机访问

使用Chrome谷歌浏览器，打开内网http://192.168.1.1:8080和http://192.168．14.141: 8080，如图13-17所示。

图13-17　电脑网页浏览视频

1．手机连接

可以扫描图13-18或通过网址下载到PC，进行手机安装或修改源代码。

图13-18　安卓手机APK源代码下载二维码

网址http://book.znck007.com/?p=apksourcefile

安卓手机连接家里的WiFi后，安装下载的APK，输入192.168.14.141，端口8080，如图13-19所示。

图13-19　安卓手机连接摄像头

成功连接摄像头显示图像，如图13-20所示。

图13-20　成功连接摄像头

提示

苹果手机直接打开http://192.168.14.141:8080就可以访问摄像头。

2．外网访问摄像头

进入ip138，获取到本机的外网IP，如图13-21所示，然后将内网的IP改成外网，如http://192.168.14.141:8080改成http://182.88.42.99:8080，就可以访问外网了。

图13-21　获取本机的外网IP

提示

如果外部8080端口无法连接，则将外部端口修改为6000-7000端口试试，因为三大运营商的限制关系，所以可能直接映射部分区域也无法正常连接。

13.7　常见问题及注意事项

【A】部分区域的宽带对外网端口是有限制的，如果映射内网成功但外网不成功，则可以换一个端口映射。

【B】如果摄像头启动不成功，则可能是USB供电不足！

第14章　语音控制

本章介绍如何使用国内第一大语音识别公司“科大讯飞”实现语音控制功能，在家中只需说出“打开大厅的灯”、“开窗”、“热水”、“睡眠模式”等，就能实现语音控制功能，如图14-1所示。

图14-1　讯飞语音官方首页

14.1　用途

利用语音识别控制家中的电器，极大地方便了我们的生活，这一场景常常出现在科幻电影中。当我们回到家中时，只需说话就可以控制电灯开关、风扇开关、电热水器、开窗等。

14.2　材料列表（见表14-1）、原理图（见图14-2）

表14-1　语音识别材料列表

名称

型号

数量

备注

Android手机

OS4.0以上

1

安卓手机

智能家居DIY

套件

1

第2章～第12章的智能家居DIY

图14-2　语音控制开发流程图

14.3　语音识别技术

由于科大讯飞语音识别技术不仅很好地支持标准国语，对各种方言也支持，因此如果你有些地方口音，也可以正确识别。

科大讯飞的核心技术主要包括语音识别技术、语音合成技术、自然语言理解技术、语音评测技术、声纹语种技术、手写识别技术等。科大讯飞提供国际领先的语音及语言整体解决方案。

自2000年以来，科大讯飞语音识别和声纹语种领域已成为NIST（美国国家标准与技术研究院）举办的语音技术相关的常规评测项目之一，参赛单位有美国麻省理工大学（MIT）、法国国家科研中心计算机科学实验室（LIMSI）、捷克布尔诺科技大学（BUT）、清华大学等17家国内外顶级语音研究机构。科大讯飞自2008年开始分别参加隔年举办的说话人识别和语种识别评测比赛。在说话人识别比赛上，科大讯飞于2008年荣获说话人识别评测大赛全球第一名，2010年荣获核心测试综合评价第二名；在语种识别比赛上，科大讯飞2009年荣获高混淆方言对识别指标综合排名冠军、通用测试指标综合排名亚军，2011年获9个高混淆度方言对评测中7个方言对第一名。

14.4　Android代码讲解

1．下载源代码

可以扫描图14-3或通过网址下载到PC。

图14-3　Android语音控制源代码下载二维码

网址http://book.znck007.com/?p=yuyingshibiedaima

2．打开Android语音控制源代码

使用Elipse软件打开Android语音控制源代码，如图14-4所示。由于Android开发属于另一门语言，因此本书不扩展此门语言的学习。

图14-4　用Elipse软件打开Android语音控制源代码

3．配置apiurl、apikey、app_id等

编译源代码前，首选要设置strings.xml文件，如图14-5所示。

图14-5　设置strings.xml文件

apiurl采用默认云端，apikey可到http://m.znck007.com申请（如果使用自己搭建云端服务器，请参考本书的1.8节）。

app_id为讯飞开发平台注册的id，并替换libs目录文件为你在讯飞开发平台下载的libs目录文件，才能正常进行语音识别功能。

4．IatDemo.java源代码

双击打开后如图14-16所示，大多数代码为讯飞demo原来的代码。

图14-6　IatDemo.java源代码

5．语音转文字的回调函数

在源代码中找到如下函数，这是讯飞SDK识别语音后转成文字的函数，我们只需将识别到的文字进行解析，如图14-7、图14-8所示。

private RecognizerListener recognizerListener=new RecognizerListener()

Private RecognizerDialogListener recognizerDialogListener=new RecognizerDialogListener()

图14-7　语音转文字的回调函数

图14-8　UI语音转文字的回调函数

6．语音文字上传到云端api函数

在语音转文字的回调函数中调用了yuyingtoapi函数，本函数是我们实现语音控制的关键部分，这里实现了POST数据到云端，从而实现与手机相同的单击命令，如图14-9所示。

图14-9　语音文字上传到云端api函数

根据这个函数，我们只需对手机说“打开卧室的灯”，在卧室开灯这个判断里，只要包含“开”、“灯”、“卧室”就将sid=003，nid=001，data=1上传到云端，然后家居网关获取到云端的命令就执行开灯操作了。

根据上面的原理，我们如果添加打开书房的命令，则只需模仿添下如下代码：

cmdstr="打开书房";//当前命令类型

if ( yuyin.contains("开") && yuyin.contains("门") && yuyin.contains("书房")) {

WebApi znckWebapi = new WebApi(IatDemo.this);//声明云端操作api类

znckWebapi.sid="005";//修改成云端对应的sid

znckWebapi.nid="001";//修改成云端对应的nid

String mode="yysetdata";//语音数据模式

String data="1";//上传的数据，1代表开门

znckWebapi.SetDate(mode,data);//调用SetDate数据函数发送

return;

}

对着手机说 “打开书房的门”，就开门了。

7．云端文件api/mobile.php

在本书1.8节的文件api/mobile.php中，当接受到语音POST来的数据后，插入到工作命令表，从而实现语音控制，如图14-10所示。

图14-10　文件api/mobile.php源代码

14.5　实战演示视频

可以扫描图14-11或通过网址查看客户端演示视频。

图14-11　客户端演示视频二维码

网址http://book.znck007.com/?p=yuyingshibieshipin

14.6　常见问题及注意事项

【A】本章采用的app_id为默认，请替换成你在讯飞开发平台申请的app_id和对应的库，否则默认的每天只能测试100次语音识别，超出将会语音识别错误。

【B】在吵闹的环境（如开音乐），可能会出现识别不准确，并使用标准普通话。

第15章　Android客户端

智能手机已经占据普通成人的生活，是不可缺少的一部分。智能手机的普及也使智能硬件有了飞速的发展。本章我们介绍如何开发一款android客户端APP并提供源代码下载，Android开发人员只需修改界面和部分源代码就可定制自己的APP。客户端APP界面功能如图15-1所示。

图15-1　客户端APP界面功能

15.1　用途

在有网络的地方打开手机APP，查看控制家中温湿度、空气质量，远程控制家中的电灯、窗帘、插座开关、空调等。

15.2　代码讲解

1．下载源代码

可以扫描图15-2或通过网址下载到PC。

图15-2　android客户端源代码下载二维码

网址http://book.znck007.com/?p=androidclientdaima

2．打开Android客户端源代码

使用Elipse软件打开Android客户端源代码，如图15-3所示，文件MainActivity.java为客户端APP首页，包名com.znck007.znjjdiy为智能家居DIY的业务操作界面。

图15-3　用Elipse软件打开Android客户端源代码

3．配置apiurl、apikey、app_id等

编译源代码前，首选要设置strings.xml文件，如图15-4所示。

图15-4　设置strings.xml文件

apiurl采用默认云端，apikey可到http://m.znck007.com申请（如果使用自己搭建云端服务器，请参考本书1.8节）。

app_id为讯飞开发平台注册的id，并替换libs目录文件为你在讯飞开发平台下载的libs目录文件，才能正常进行语音识别功能。

4．编译进入首页

打开编译成功手机界面的MainActivity.java后，其中最重要的是能够跳转到各项功能页面，如图15-5所示。

图15-5　首页MainActivity.java源代码

5．家居环境网关、温湿、PM2.5

进入到家居环境页面，包含网关、温度、湿度、空气质量PM2.5，如图15-6所示。

图15-6　家居环境页面

6．网关页面源代码

网关页面如图15-7所示，显示了当前状态、最后时间、apikey等信息。

图15-7　网关页面

打开HomeWanguan.java网关页面源代码，如图15-8所示。网关页面源代码主要由变量声明、onCreate函数、GetDate函数构成。

图15-8　HomeWanguan.java网关页面源代码

最关键的GetDate函数使用http协议从云端API（本书1.8节的文件api/mobile.php）获取sid=001和nid=001的记录数据，并显示到界面，如图15-9所示。

图15-9　文件api/mobile.php源代码

温度、湿度、空气PM2.5的源代码基本类似，因此不在重复讲解了。

7．电灯开关源代码

电灯开关界面如图15-10所示，显示云端添加的设备。

图15-10　电灯开关界面

在电灯开关Swtchi_.java文件中，主要通过GetDate获取type（3插座、4电灯、5红外、6开门开窗）类型的列表数据，如图15-11所示。

图15-11　电灯开关Swtchi_.java源代码

GetDate函数从云端API（文件api/mobile.php）获取相关type的列表数据并返回，如图15-12所示。

图15-12　文件api/mobile.php源代码

8．电灯开关控制页面源代码

在电灯开关控制页面，单击打开或关闭，就能控制云端开、关灯了，如图15-13所示。

图15-13　电灯开关控制页面

在电灯开关控制页面SwitchDetail.java文件里，GetDate函数获取当前开关状态数据，单击开关后，会向云端发送开关数据，如图15-14所示。

图15-14　电灯开关控制页面SwitchDetail.java源代码

云端API（文件api/mobile.php）收到数据添加到工作命令表，如图15-15所示。

图15-15　文件api/mobile.php源代码

其他插座开关、红外遥控、开门开窗都与电灯开关原理相同，只存在type、sid、nid的区别，安防报警则与温度、湿度、空气PM2.5相似。

9．WiFi摄像头

单击WiFi摄像则直接打开或安装第13章Android的摄像头apk，输入摄像头IP后，即可直接监视摄像，如图15-16所示。

图15-16　Android的摄像头apk

15.3　实战演示视频

可以扫描图15-17或通过网址查看客户端演示视频。

图15-17　客户端演示视频二维码

网址http://book.znck007.com/?p=androidclientshipin

15.4　常见问题及注意事项

【A】客户端包含了第14章的语音控制，因为也需要更改成你在讯飞开发平台申请的app_id和对应的库。

【B】自己根据PHP源代码搭建云服务器时，需对服务器的搭建有一定的基础（PHP+MYSQL环境），否则请使用默认的http://m.znck007.com云平台。

第16章　3D打印模型

这几年，3D打印飞速发展，各个城市的电子科技城都有3D打印店，如图16-1所示，只需在网上下载要打印的模型就能直接打印，同时在网上也有很多开源的3D打印机，通过购买配件即可组装成一台家用3D打印机。

图16-1　3D打印店

16.1　3D打印的作用

3D打印能快速成型，是一种以数字模型文件为基础，运用粉末状金属或塑料等可粘合材料，通过逐层打印的方式来构造物体的技术。

我们利用这一技术，可以打印出各种模块的外壳，实现美观和包装的目的。

16.2　如何3D打印

首先，我们要通过3D软件设计出模型，如图16-2所示，然后导出stl或obj格式，最后通过Cura等软件进行分层并转化为打印指令，调入3D打印机控制软件就可以打印了。

图16-2　3D软件设计的模型

3D打印技术包括三维模型的建模、机械及其自动控制（机电一体化）、模型分层并转化为打印指令代码软件等技术。

原理：3D打印技术就是将计算机中设计的三维模型分层，得到许多二维平面图形，再利用各种材质的粉末或熔融的塑料逐层打印这些二维图形堆叠成为三维实体。

16.3　3D模型源文件下载

本章提供3D源文件和导出obj文件下载。

1．网关

网关盒子是一款以简洁、现代风格为主的设计想法，想给人一种很强的不规则线条感和科技感，如图16-3、图16-4、图16-5所示。

图16-3　网关模型正面效果图

图16-4　网关模型背面效果图

图16-5　网关模型顶部效果图

2．开门窗帘

开门窗帘想强调一种不一样的感觉，残缺的美，追求突破和重生的感觉，如图16-6、图16-7、图16-8所示。

图16-6　开门窗帘模型正面效果图

图16-7　开门窗帘模型背面效果图

图16-8　开门窗帘模型顶部效果图

3．红外遥控

红外遥控功能需要360°，每一个角度都需要开口，所以恰好可以设计成飞碟状，满足功能，又能达到创意设计造型效果，如图16-9、图16-10、图16-11所示。

图16-9　红外遥控模型正面效果图

图16-10　红外遥控模型侧面效果图

图16-11　红外遥控模型顶部效果图

4．温湿度传感

温湿度传感需要全面接触到空气来检测室内的温湿度，所以每个面都需要开孔，设计成骰子样子，是为了增强它的趣味性，如图16-12、图16-13所示。

图16-12　温湿度传感模型正面效果图

图16-13　温湿度传感模型背面效果图

5．空气质量

空气质量也需要多孔来确定室内空气的情况，可以设计成小机器人可爱的样子，去掉呆板的严肃工业造型，如图16-14、图16-15、图16-16所示。

图16-14　空气质量正面效果图

图16-15　空气质量背面效果图

图16-16　空气质量顶部效果图

6．烟雾火警

烟雾火警的纹理不断往外扩展，像花瓣一样的枝干给人一种延伸感，强调一种自然有韵律的美，如图16-17、图16-18所示。

图16-17　烟雾火警侧面效果图

图16-18　烟雾火警顶部效果图

7．水滴检测

水滴检测设计了规则的菱角，生硬的切割，强调一种硬朗的感觉，如图16-19、图16-20、图16-21所示。

图16-19　水滴检测正面效果图

图16-20　水滴检测侧面效果图

图16-21　水滴检测顶部效果图

8．人体红外检测

人体红外检测需要立起来才能达到功能需要，所以设计的时候加重了盒子的厚度，造型设计看起来像火箭起飞的状态，增加了立体感和动感，如图16-22、图16-23所示。

图16-22　人体红外检测倒放效果图

图16-23　人体红外检测站立效果图

9．WiFi摄像头

WiFi摄像头有着囧字造型，嘴巴那里刚好放摄像头，这样对着摄像头都可以增加很多的趣味，如图16-24、图16-25、图16-26所示。

图16-24　WiFi摄像头站立效果图

图16-25　WiFi摄像头侧面效果图

图16-26　WiFi摄像头倒放效果图

可以扫描图16-27或通过网址下载到PC。

图16-27　3D打印源文件下载二维码

网址http://book.znck007.com/?p=3dprintdaimafile

16.4　常见问题及注意事项

【A】在制作3D软件建模时，直接从二维线转成三维，这样虽然很容易形成一个面，但这样的模型打印出来会很容易出现一个坏面，主要是因为切口没有布线导致的。

【B】模型要有支撑点，因为打印机成型是一个从低到顶的叠加过程，所以在制作模型的过程中，一定要给模型叠加起来的重力支撑，要保证从下到上都有连接的支撑点，这样才能完好无损的打印好一个模型出来。

读累了记得休息一会哦~

公众号：古德猫宁李

- 电子书搜索下载

- 书单分享

- 书友学习交流

网站：沉金书屋 https://www.chenjin5.com

- 电子书搜索下载

- 电子书打包资源分享

- 学习资源分享

反侵权盗版声明

电子工业出版社依法对本作品享有专有出版权。任何未经权利人书面许可，复制、销售或通过信息网络传播本作品的行为；歪曲、篡改、剽窃本作品的行为，均违反《中华人民共和国著作权法》，其行为人应承担相应的民事责任和行政责任，构成犯罪的，将被依法追究刑事责任。

为了维护市场秩序，保护权利人的合法权益，本社将依法查处和打击侵权盗版的单位和个人。欢迎社会各界人士积极举报侵权盗版行为，本社将奖励举报有功人员，并保证举报人的信息不被泄露。

举报电话：（010）88254396；（010）88258888

传　　真：（010）88254397

E-mail：dbqq@phei.com.cn

通信地址：北京市海淀区万寿路173信箱

电子工业出版社总编办公室

邮　　编：100036