mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
mod
This commit is contained in:
290
极客时间专栏/物联网开发实战/实战篇/16 | 实战准备:如何搭建硬件开发环境?.md
Normal file
290
极客时间专栏/物联网开发实战/实战篇/16 | 实战准备:如何搭建硬件开发环境?.md
Normal file
@@ -0,0 +1,290 @@
|
||||
<audio id="audio" title="16 | 实战准备:如何搭建硬件开发环境?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/17/4183d497dc2040d66283d60083773a17.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
从今天开始,我们就进入了课程的实战篇,我会手把手带你从0开始完成自己的智能家居项目。
|
||||
|
||||
这个项目具体包括哪些产品呢?在[第5讲](https://time.geekbang.org/column/article/309786)中,我们根据智能家居产品的设计原则,已经设计好了 4 个产品场景,分别是:
|
||||
|
||||
1. 可以手机控制的智能电灯
|
||||
1. 可以基于光线自动调节的智能电灯
|
||||
1. 可以语音控制的智能音箱
|
||||
1. 可以基于环境温湿度和土壤湿度自动浇水的浇花器
|
||||
|
||||
它们分别对应了实战篇的第17~21讲的内容(如有需要,你可以根据[这份文档](https://shimo.im/sheets/D3VVPdwcYRhhQRXh/MODOC)自行采购相关硬件)。
|
||||
|
||||
不过,在打造这些产品场景之前,我们还需要先搭建好**硬件开发环境**。在这一讲,我就以智能电灯为例,带你完成这个准备工作。
|
||||
|
||||
## 通信技术:Wi-Fi
|
||||
|
||||
为了能让手机控制电灯,我们首先要让电灯接入网络。在[第2讲](https://time.geekbang.org/column/article/306976)中,我们介绍了很多种通信技术,智能家居场景下常用的有 Wi-Fi、BLE 和 ZigBee 等。那么智能电灯应该选择哪种技术呢?
|
||||
|
||||
从**通信速率**来看,智能电灯传输的数据,包括控制命令和几种状态的数值,数据量都非常小,这几种通信技术都可以满足要求。
|
||||
|
||||
从**功耗**来看,智能电灯是直接连接电线的,不需要电池供电,所以低功耗的 BLE 和 ZigBee 技术不是必须的选择,功耗相对较高的 Wi-Fi 也可以考虑。
|
||||
|
||||
从**普及度**和**易用性**的角度分析,如果使用BLE,设备与手机的交互确实会非常方便。但是BLE和ZigBee 的设备都有一个缺点,就是需要**搭配专有的网关**才能连接互联网,这在部署和使用的时候都比较麻烦。所以,我们选择 **Wi-Fi** 作为智能电灯的通信方式。
|
||||
|
||||
## 开发板:NodeMCU
|
||||
|
||||
确定使用 Wi-Fi 之后,我们又该怎么选开发板呢?(你可能也关心选择哪一款芯片。不过,为了方便讲解和动手实践,这里我们还是围绕开发板来展开。关于芯片的选型,我们可以另外找机会交流。)
|
||||
|
||||
我推荐选择开源硬件的开发板,有两个原因。第一,硬件、软件的各种技术实现是公开的,方便分析问题,也方便后期转化为量产的产品;第二,有社区氛围,使用的人比较多,大家可以针对具体的问题进行交流。
|
||||
|
||||
比如说 [NodeMCU](https://nodemcu.readthedocs.io/en/release/) 就是一个不错的选择。基于 ESP8266 芯片的版本,Flash 空间有4MB,自带 Wi-Fi 功能,而且价格便宜,在国内外都非常流行。(这里顺带说一句,ESP8266 是国内企业的芯片产品,国内企业在 Wi-Fi 和 BLE 芯片上的优势真的是越来越明显。)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/c5/14d143cfbaa113yy185726a4a23002c5.jpg" alt="">
|
||||
|
||||
## 开发语言:Python
|
||||
|
||||
那么,开发语言用哪一种比较好呢?我计划使用**Python**。
|
||||
|
||||
你可能会觉得奇怪:嗯?为什么不用**C语言**?
|
||||
|
||||
主要原因是,我不希望开发语言成为实战项目的障碍。先不说C语言本身的难度,光是它需要交叉编译的特性和不够便捷的调试方式,就已经很影响效率了。
|
||||
|
||||
相比之下,使用比较简单的 Python 语言,开发和调试都会非常方便。当然,选择 Python 还有别的好处,你在后面的实战过程中可以逐渐感受到。
|
||||
|
||||
如果你是嵌入式开发的高手,对C语言了然于胸,可以信手拈来,那你也可以基于我介绍的步骤,用C语言,甚至其他的语言来实践项目的编程(期待你的分享)。语言是一个工具,我们完全可以拿来灵活应用,实现我们的工作任务,而不应该成为一种羁绊。
|
||||
|
||||
当然,我也建议你不要排斥这次使用 Python 的机会。一方面,这次尝试可以拓展你的技术视野;另一方面,掌握 Python 对你写后台、做数据分析和写脚本也非常有帮助,可以在很多方面提高你的效率。
|
||||
|
||||
不过,你可能还是不放心:嵌入式硬件的计算资源都非常有限,在开发板上面运行 Python 代码可行吗?
|
||||
|
||||
这确实是一个挑战,好在 [MicroPython](https://docs.micropython.org/) 项目已经提供了解决方案。
|
||||
|
||||
**MicroPython** 是专门为**嵌入式系统**打造的 Python 实现。它完整实现了 Python3.4 的语言特性,部分支持 Python3.5 的特性。在标准库方面,MicroPython 实现了 Python 语言的一个子集,另外还增加了与底层硬件交互的库模块。
|
||||
|
||||
## 搭建 MicroPython 开发环境
|
||||
|
||||
接下来,我们就来把 MicroPython 部署到 NodeMCU 开发板上,准备好开发环境。
|
||||
|
||||
### 第一步:准备固件文件
|
||||
|
||||
首先,我们需要为 NodeMCU 准备好 MicroPython **固件文件**。MicroPython 官方已经为 ESP8266 芯片准备了[现成的固件](https://micropython.org/download/esp8266/),省去了交叉编译的工作。否则,我们还需要在电脑上使用专门的编译软件,为 ESP8266 芯片编译 MicroPython 源代码。
|
||||
|
||||
MicroPython 的固件分为 2M、1M 和 512K 三个不同的版本,针对不同大小的 Flash 存储空间。我们下载最新的 **2M 稳定版本**(带 stable 的)就行,因为 NodeMCU 开发板的 Flash 空间是足够的。
|
||||
|
||||
### 第二步:安装**烧录工具**
|
||||
|
||||
然后,我们使用一根 USB 数据线,将 NodeMCU 开发板和电脑连接起来。
|
||||
|
||||
接着,我们在电脑终端运行下面的命令,安装用来烧录的工具 **esptool** :
|
||||
|
||||
```
|
||||
pip install esptool
|
||||
|
||||
```
|
||||
|
||||
esptool 安装完成后,你可以运行 esptool.py read_mac 命令,确认 NodeMCU 板子是否连接成功。连接成功后的屏幕显示是这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/52/1b/525ec8069d036df1dae19d4ba184a21b.png" alt="">
|
||||
|
||||
如果连接不成功,或者没有正确识别设备,屏幕上则会出现下面的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/82/410cb9a3d4d9c149abe1ff0dba606182.png" alt="">
|
||||
|
||||
这时候怎么办呢?
|
||||
|
||||
首先,检查一下你使用的 USB 线能否传输数据。不是说笑,我就犯过这个低级错误。现在很多电子产品会随带 USB 充电线,但是为了节约成本,有些 USB 线内部实际上并没有集成两根数据信号线。你如果使用了这种线,就只能充电,而电脑是识别不出设备的。
|
||||
|
||||
另外,注意我们使用的数据线,一头是 USB-A 接口,另一头是 Micro-USB 接口。USB 的接口规格繁多,我在这里放了[一张图](https://getprostorage.com/blog/usb-c-thunderbolt-3-rundown/),方便你区分。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/58/d0f2e713f2ae451bcdbc0fd794005358.png" alt="" title="不同的USB接口(图片来源:ProStorage)">
|
||||
|
||||
如果USB线没有问题,那可能是电脑没有正确识别开发板,我们需要检查一下驱动文件有没有安装好。
|
||||
|
||||
如果你跟我一样,用的是 macOS 系统,可以在电脑的终端上输入 `ls /dev/cu*` 命令,查看是否有类似 /dev/cu.wchusbserialxxxxx 名字的设备文件。
|
||||
|
||||
如果你使用 Windows 系统,那么需要查看一下“设备管理器”,看看“端口(COM 和 LPT)”下面,有没有 COM* 结尾的设备。
|
||||
|
||||
如果没有,你可以参考[这篇文章](https://learn.sparkfun.com/tutorials/how-to-install-ch340-drivers/all#mac-osx),下载相应的驱动文件安装。(注意,我的 NodeMCU 开发板使用的是 CH340 这款 USB 转串口芯片。如果是 CP2102 芯片,可以参考[这篇文章](https://learn.sparkfun.com/tutorials/cp2102-usb-to-serial-converter-hook-up-guide)。)
|
||||
|
||||
当你在终端看到类似下面的结果,或者在 Windows 的设备管理器中看到 COM* 设备时,就说明开发板已经成功识别。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/40/09a332dcdd12cc43ed7ce69babeeec40.png" alt="">
|
||||
|
||||
如果仍然无法正确识别,你可以到一些论坛去交流,比如[安信可的官方论坛](http://bbs.ai-thinker.com/forum.php)。
|
||||
|
||||
### 第三步:烧录固件
|
||||
|
||||
接下来我们烧录固件。在这之前,我们需要先输入下面命令,擦除 Flash 芯片:
|
||||
|
||||
```
|
||||
# 注意设备名称替换为你电脑上的名称
|
||||
esptool.py --port /dev/cu.wchusbserial14230 erase_flash
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/7a/d20d91566a5e3ebbd05bbae25472447a.png" alt="">
|
||||
|
||||
擦除成功后,我们进入存储前面下载固件的目录中,运行下面的命令,将固件文件烧录到开发板的 Flash 中:
|
||||
|
||||
```
|
||||
# 注意设备名称替换为你电脑上的名称,固件文件名称做类似修改
|
||||
esptool.py --port /dev/cu.wchusbserial14230 --baud 460800 write_flash --flash_size=detect 0 esp8266-20200911-v1.13.bin
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/de/78ab58569f82386852b942aabfe992de.png" alt="">
|
||||
|
||||
烧录成功后,MicroPython 已经在你的开发板上运行起来了。
|
||||
|
||||
### 第四步:确认运行状态
|
||||
|
||||
但是开发板跟电脑不一样,是没有显示屏的,我们要怎么确认它的运行状态呢?
|
||||
|
||||
有一种非常简便的方法,你可以用电脑或者手机搜索一下周围的 Wi-Fi 热点,如果看到类似 “MicroPython-xxxxxx” 名称的热点(xxxxxx是开发板 MAC 地址后半部分),说明你的 NodeMCU 工作正常。比如我的开发板MAC地址是“40:f5:20:07:3b:52”,现在我看到了“MicroPython-073b52”这个热点,就说明开发板在正常运行。
|
||||
|
||||
当然,对于 Python 来说,更方便的交互方式还是REPL (交互式解释器),这个 MicroPython 也提供了。我们可以通过 REPL 来检验开发板的运行。
|
||||
|
||||
我们还是使用烧录时用到的 USB 线连接开发板和电脑。在 MacOS 电脑上,重新连接开发板的时候,串口设备名称可能会改变,所以为保险起见,再次运行命令:
|
||||
|
||||
```
|
||||
ls /dev/cu*
|
||||
|
||||
```
|
||||
|
||||
获得串口设备名称之后,我们可以使用终端模拟器软件,比如 [SecureCRT](https://www.vandyke.com/cgi-bin/releases.php?product=securecrt),通过串口协议连接上开发板,进行交互。
|
||||
|
||||
需要注意的是,波特率(Baud rate)设置为 115200,这与前面烧录时选择的值不同。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/c7/cc80630cc1fe808e75ee01e6a85b19c7.png" alt="">
|
||||
|
||||
如果你使用 Windows 操作系统,那么 [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) 更加流行。当然,建立连接的参数设置都是类似的。
|
||||
|
||||
成功连接后,SecureCRT 的窗口会输出类似下面的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/bc/3304c75a8fb57068073c4d6aac4fc6bc.png" alt="">
|
||||
|
||||
看到熟悉的符号 “>>>”,我们就知道,可以真正进行交互了。
|
||||
|
||||
### 第五步:体验交互
|
||||
|
||||
先用“Hello World”来个经典的打招呼吧。
|
||||
|
||||
接着,我们体验一下 MicroPython 控制 LED 灯。因为开发板 NodeMCU 12F的 GPIO2 管脚接有一个 LED 灯,你可以输入下面的代码,控制它的点亮和熄灭。
|
||||
|
||||
```
|
||||
>>> print("Hello World from MicroPython!")
|
||||
Hello World from MicroPython!
|
||||
>>> import machine
|
||||
>>> pin = machine.Pin(2, machine.Pin.OUT)
|
||||
>>> pin.off()
|
||||
>>> pin.on()
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,不同的板子上,这个管脚的高低电平的设计可能不同,所以 `pin.on()` 可能是熄灭 LED 灯;`pin.off()` 反而是点亮 LED 灯。
|
||||
|
||||
## 部署代码到开发板
|
||||
|
||||
那么,能不能运行一个 Python 代码文件呢?比如,基于在 REPL 中尝试的点亮 LED 操作。
|
||||
|
||||
我们写一个代码段:
|
||||
|
||||
```
|
||||
import machine
|
||||
import time
|
||||
|
||||
# 指明 GPIO2 管脚
|
||||
pin = machine.Pin(2, machine.Pin.OUT)
|
||||
|
||||
# 循环执行
|
||||
while True:
|
||||
time.sleep(2) # 等待 2 秒
|
||||
pin.on() # 控制 LED 状态
|
||||
time.sleep(2) # 等待 2 秒
|
||||
pin.off() # 切换 LED 状
|
||||
|
||||
```
|
||||
|
||||
这段代码实现的功能是,控制 LED 灯以 2 秒的间隔,不断点亮、熄灭。
|
||||
|
||||
为了在电路板上运行这个 Python 代码,我们需要做两件事情:
|
||||
|
||||
1. 将代码段保存到一个文件中,这个文件的名字必须是 main.py。
|
||||
1. 将代码文件 main.py 放到开发板的文件系统中,而且是根目录。
|
||||
|
||||
这样,当开发板启动或者重启的时候,就会自动执行 main.py 文件中的代码。
|
||||
|
||||
第一点我们可以很容易做到。但是,怎么把代码文件上传到开发板上呢?
|
||||
|
||||
MicroPython 的官方提供了一个工具[pyboard.py](https://docs.micropython.org/en/latest/reference/pyboard.py.html),它也是基于串口连接与开发板通信的。你可以使用它操作开发板上的文件系统,比如常用的拷贝文件、创建文件夹、删除等功能,甚至可以将电脑上的代码文件加载到内存中,直接运行。这非常便于你在开发过程中,进行代码的调试。
|
||||
|
||||
下载 pyboard.py 的源文件到电脑后,你可以运行下面的命令,将 main.py 文件部署到你的开发板:
|
||||
|
||||
```
|
||||
# 设置环境变量,指明串口设备
|
||||
export PYBOARD_DEVICE=/dev/cu.wchusbserial14220
|
||||
|
||||
|
||||
#拷贝当前目录下的 main.py 到开发板
|
||||
./pyboard.py cp main.py :
|
||||
|
||||
```
|
||||
|
||||
不过,pyboard.py 在 MacOS 系统上运行有问题。比如,在电脑终端,尝试运行下面的命令,就会收到 “could not enter raw repl” 这个错误信息。
|
||||
|
||||
```
|
||||
./pyboard.py -f ls /
|
||||
|
||||
```
|
||||
|
||||
这可能是 MacOS 上的串口芯片 CH340 的驱动的问题,它会在建立串口连接时,重置 NodeMCU ,导致 enter_raw_repl 函数无法正常执行。如果你只能在 MacOS上开发,怎么办呢?
|
||||
|
||||
我试用过几种类似的工具,这里推荐你使用 [AdaFruit MicroPython tool —— ampy](https://learn.adafruit.com/micropython-basics-load-files-and-run-code/install-ampy)。安装过程可以打开链接了解,我就不展开了。一般情况下,你可以用下面的命令完成安装:
|
||||
|
||||
```
|
||||
pip install adafruit-ampy
|
||||
# ---或者---
|
||||
pip3 install adafruit-ampy
|
||||
|
||||
```
|
||||
|
||||
ampy 是通过增加延时的方法,来规避 MacOS 系统上的问题的。所以在使用的时候,我们需要先设置一个环境变量 —— AMPY_DELAY。延时的推荐值是 0.5,不过,具体实践时,你需要根据自己的开发板的试验情况,灵活调整这个数值。
|
||||
|
||||
```
|
||||
export AMPY_DELAY=0.5
|
||||
|
||||
```
|
||||
|
||||
我们可以在终端中输入上面的指令,也可以将它加入到 .bashrc 或 .zshrc 等配置文件中,避免每次都要重复输入。
|
||||
|
||||
使用 ampy 的过程中,常用的环境变量还有下面两个,可以根据具体情况设置:
|
||||
|
||||
```
|
||||
#设备名称请根据你的情况修改
|
||||
export AMPY_PORT=/dev/cu.wchusbserial14220
|
||||
|
||||
#串口通信的波特率
|
||||
export AMPY_BAUD=115200
|
||||
|
||||
```
|
||||
|
||||
然后,输入下面的命令,就把代码部署到开发板上了。
|
||||
|
||||
```
|
||||
ampy put main.py
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
在这一讲中,我带你搭建了智能电灯的硬件开发环境。
|
||||
|
||||
1. 在通信技术方面,我从通信速率、功耗、普及度和易用性等角度考虑,最终选择了Wi-Fi。在实际工作中,你也可以通过同样的思路来选择其他产品的通信技术。
|
||||
1. 在开发板上面,推荐选择开源硬件的开发板,因为使用和交流都更方便。实战项目选择的是NodeMCU ESP8266,它在Flash空间、通信技术和价格方面有一定优势。
|
||||
1. 为了降低动手实践的难度,我们选择了Python开发语言。而MicroPython为我们提供了在NodeMCU上使用Python语言的条件。
|
||||
1. 在搭建MicroPython开发环境的过程中,我们需要使用esptool工具。通过USB线连接NodeMCU,你可以将固件烧录到开发板的Flash中。
|
||||
|
||||
这个选择思路和搭建过程不仅适用于智能电灯,也适用于自动浇花器,它们的开发环境是一样的。至于智能音箱,使用的开发板是树莓派,我在第15讲已经介绍过使用方法了,所以你也不需要担心。
|
||||
|
||||
## 思考题
|
||||
|
||||
这一讲是实战篇的第一讲,所以最后我想给你留一个需要动手的问题。
|
||||
|
||||
MicroPython 是专门为嵌入式开发设计、实现的 Python 语言开发环境。在这一讲中,我们通过 GPIO 的**输出**,实现了 LED 灯的控制。你能否实现一个 GPIO 的**输入**功能,并且通过这个输入信号,控制 LED 灯的点亮和熄灭?
|
||||
|
||||
希望你能留言区和我交流,也欢迎你将本讲分享给你的朋友一起学习讨论。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/30/4c/30e17yy335dbf8f251cc181b0cd9414c.jpg" alt="">
|
||||
527
极客时间专栏/物联网开发实战/实战篇/17 | 远程控制:怎样打造联网的智能电灯?.md
Normal file
527
极客时间专栏/物联网开发实战/实战篇/17 | 远程控制:怎样打造联网的智能电灯?.md
Normal file
@@ -0,0 +1,527 @@
|
||||
<audio id="audio" title="17 | 远程控制:怎样打造联网的智能电灯?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/11/aac65e68cdcb9ae87fa619be99e13411.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
在上一讲,我们把智能电灯开发的实验环境搭好了。今天,我们就一起去完成智能电灯的开发工作,并且连接到腾讯云的[物联网平台](https://console.cloud.tencent.com/iotexplorer)吧(如有需要,你可以根据[这份文档](https://shimo.im/sheets/D3VVPdwcYRhhQRXh/MODOC)自行采购相关硬件)。
|
||||
|
||||
那为什么一定要连接到一个物联网平台呢?这是因为物联网平台提供了基本的设备管理功能,可以帮助我们更快速地实现设备的远程控制功能。比如说,我们可以通过“腾讯连连”小程序,来控制电灯的状态,而不用自己花费时间和精力去写相应的代码等工作。
|
||||
|
||||
## 物联网平台上需要做什么准备?
|
||||
|
||||
那为什么要选择腾讯云的物联网平台呢?
|
||||
|
||||
主要是两个原因。一方面是,它的平台是开放注册的,我们普通的用户也可以注册来使用,不像很多平台那样需要企业用户才能注册。另一方面是,腾讯云提供的交互方式非常方便,不需要编译,或者下载其他App,在微信上用小程序就可以进行。
|
||||
|
||||
### 注册与登录
|
||||
|
||||
确定了要接入腾讯云的物联网平台以后,我们就需要先在这个平台上做一些准备工作了。准备工作的第一步,当然就是你得先注册个账号并且登录。注册和登录的流程非常简单,你打开[这个链接](https://console.cloud.tencent.com/iotexplorer),一眼就能看明白。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/4f/a8a8c11d4be7c1e1c6fd69b2febed24f.png" alt="">
|
||||
|
||||
### 创建项目和产品
|
||||
|
||||
登录之后,我们直接进入准备工作的第二步,创建项目和产品。我们先在物联网开发平台创建一个新项目“智能家居”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/81/bc6d5d04e08bae0939c3a783775fd281.png" alt="">
|
||||
|
||||
然后,进入这个“智能家居”项目,创建一个新产品“智能电灯”。到这里,我们需要简单设置几个参数:
|
||||
|
||||
- 产品品类,直接选择“智能生活”-->“电工照明”-->“灯”。
|
||||
- 认证方式选择密钥认证,这个比较简单,而且适合我们的开发板NodeMCU。
|
||||
- 通信方式,选择Wi-Fi。
|
||||
- 数据协议选择“数据模板”,也就是基于物模型来解析数据。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cd/f0/cdac3c2b897e53917e9afd6aa28ca7f0.png" alt="">
|
||||
|
||||
设置完成后,我们就可以点击进入智能电灯这个产品了,然后开始定义物模型。
|
||||
|
||||
### 物模型在哪里使用?
|
||||
|
||||
点击进入产品,我们可以看到“数据模板”界面中列出了“电灯开关”“亮度”“颜色”和“色温”等属性和事件。这些都是平台根据上一步选择的产品品类,自动生成的。
|
||||
|
||||
当然了,我们也可以通过“导入 JSON”的方式,把我们在[第6讲](https://time.geekbang.org/column/article/310441)编写的 JSON 文本导入,完成产品功能的定义。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/22/153908e6522de593d07d46346e879722.png" alt="">
|
||||
|
||||
定义好物模型之后,我们就完成了“设备开发”的工作,需要继续完成“交互开发”配置了。
|
||||
|
||||
### 交互界面如何定义?
|
||||
|
||||
在“交互开发”界面中,我们需要关注两个配置项:“使用官方小程序控制产品”的选项要保持打开,因为我们后面要通过小程序来控制智能电灯;在“扫一扫产品介绍”配置项中,设置产品图片和备注信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/27/62a990cf4a6ea288f9770445134a1f27.png" alt="">
|
||||
|
||||
其他项目,比如“配置引导”“面板配置”和“快捷入口配置”,我们保持默认配置就行,当然你也可以根据自己的喜好进行调整。
|
||||
|
||||
这些都配置好之后,我们就可以开始准备“调试设备”的配置了。
|
||||
|
||||
### 为调试设备做准备
|
||||
|
||||
在“设备调试”界面中,我们创建一个测试设备。点击“新建设备”,输入设备名称“Led_1”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/54/759caa00ed7f5ebd1c001b18f4808054.png" alt="">
|
||||
|
||||
创建成功后,在测试设备列表中,点击“Led_1”,进入设备的详情页面:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9d/e9/9db3727e33082f821c8f21c6b642c1e9.png" alt="">
|
||||
|
||||
在这个“设备信息”标签页,我们可以看到“设备名称”“设备秘钥”和“产品ID”的信息。我们需要把这些信息记录下来,因为在后面设备的开发中需要用到。
|
||||
|
||||
这里有一点我们要注意下。**设备名称(DeviceName)、设备秘钥(SecretKey)和产品ID(ProductID)也经常被称为设备三元组**。它完整地标识了一个设备。在调试阶段,设备名称可以手动命名,不过在正式应用中,为了保证设备名称的唯一性,平台会帮你自动生成设备名称。
|
||||
|
||||
另外,在“设备调试”标签页,你需要点击下图中“二维码”,获取这个设备的二维码,并保存好。因为在后面的步骤中,你需要使用“腾讯连连”小程序扫描这个二维码,将设备添加到小程序中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/97/a6/9721c6f1f394c14ca03e72dfe0c55ea6.png" alt="">
|
||||
|
||||
这是我在这个配置界面中定义的产品的二维码。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/ff/b515137b43f769c249df48b7f6f550ff.png" alt="">
|
||||
|
||||
这个二维码的信息内容,如下所示:
|
||||
|
||||
```
|
||||
{"DeviceName":"Led_1","ProductId":"XNXP231VQA","Signature":"2aa86e4e826b49b2a93949955f50761"}
|
||||
|
||||
```
|
||||
|
||||
可以看到,这个链接中主要包含了产品ID的信息。每个产品 ID 是唯一的,所以你的产品 ID 与这个不同。
|
||||
|
||||
到这里,我们就完成了腾讯云的物联网开发平台的准备工作。接下来,我们就要实打实地在开发板上,实现一个功能更加完善的智能电灯产品了。
|
||||
|
||||
## 如何打造智能电灯设备?
|
||||
|
||||
在上一讲,我们用代码实现了开发板上LED的控制。不过那个功能非常简单,为了让我们的智能电灯功能更完善,效果更酷炫,我们可以开发更多的功能,主要包括控制LED灯的颜色、开关,并能够实现远程控制。
|
||||
|
||||
我们先看看如何控制灯的颜色。
|
||||
|
||||
### 如何控制 LED 灯的颜色?
|
||||
|
||||
我们使用的 RGB LED 灯模块,是使用 [PWM](https://en.wikipedia.org/wiki/Pulse-width_modulation) (Pulse Width Modulation,脉冲宽度调制)来实现控制 LED 的颜色和亮度的。PWM的原理是,通过芯片的数字管脚(GPIO)来获得模拟电路信号的输出,它会控制芯片管脚在高电平和低电平之间进行快速切换。
|
||||
|
||||
那如何产生不同的 PWM 信号呢?这涉及到 2 个 PWM 信号的参数:**频率和占空比**。PWM 信号是一个方波信号,如下图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/63/y4/633d089ea84047198dd53f560488fyy4.jpg" alt="">
|
||||
|
||||
频率,是指1秒内方波的周期个数,一个周期包含一个完整的高、低电平变化。比如一个周期是 20 ms(毫秒),那么通过计算:
|
||||
|
||||
1000毫秒/20毫秒 = 50Hz
|
||||
|
||||
我们可以知道方波的频率是50 Hz(赫兹)。
|
||||
|
||||
那PWM输出的方波信号的频率不同,会对我们有什么影响呢?
|
||||
|
||||
如果频率小于 100 Hz的话,我们的肉眼就会感受到灯的闪烁,甚至产生生理上的不适,比如视觉疲劳、偏头痛等。因此,对于 LED 灯,PWM 的频率必须大于 100Hz,最好在 200Hz以上。对于我们选择的NodeMCU 开发板来说,可以选择它支持的最大值,也就是1000Hz。
|
||||
|
||||
在PWM的信号图中,我们还可以看到一个叫做“脉宽时间”的标识,它代表的是一个周期里高电平的占用时间。而所谓的占空比,就是脉宽时间占整个周期时间的比例。比如,脉宽时间是10ms,那占空比的计算公式就是:
|
||||
|
||||
10/20 = 50%
|
||||
|
||||
占空比等于 50%。关于占空比参数,我需要提前说明一下:在 [MicroPython 代码](https://docs.micropython.org/en/latest/esp8266/tutorial/pwm.html)中,占空比 Duty 不是使用百分比数值来表示的,而是 0 到 1023 之间的数值。0 代表占空比为 0%,1023 代表占空比为 100%。
|
||||
|
||||
在代码中,当你设置了不同的占空比参数时,对应管脚输出的方波信号也会不同。下图展示了占空比分别为 0%、25%、50%、75% 和 100% 的方波信号,它们的平均电压(下图右侧)逐渐增大。我们正是通过平均电压的变化,达到了控制 LED 颜色和亮度等效果的目的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/66/31/66f9943cc758b8d42e165caa47ed7c31.jpg" alt="">
|
||||
|
||||
关于PWM信号,我们了解这些就够了。接下来,我们解决的问题,就是通过上一讲选定的开发板NodeMCU上的GPIO管脚来控制LED等的颜色了。NodeMCU的[管脚图](https://github.com/nodemcu/nodemcu-devkit-v1.0)如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/ae/d53af13yy9320eaa90b8ff6cf3c565ae.png" alt="" title="图片来自GitHub">
|
||||
|
||||
一下子看到这么多管脚,你不要担心,因为只要关注带浅黄色背景的“GPIO”的几个管脚就够了。
|
||||
|
||||
我们先考虑一个问题,NodeMCU 开发板中的所有 GPIO 管脚都可以连接 RGB LED 模块吗?
|
||||
|
||||
答案是不可以。因为不是所有的 GPIO 管脚都可以输出 PWM 信号。NodeMCU 开发板是基于 ESP8266 芯片的,管脚GPIO0、GPIO2、GPIO4、GPIO5、GPIO12、GPIO13、GPIO14 和 GPIO15 具备 PWM 的输出能力,它们分别对应 NodeMCU 开发板的 D3、D4、D2、D1、D6、D7、D5 和 D8 接口。
|
||||
|
||||
因此,我们选择 D1、D2和D3这三个接口,分别连接 RGB LED 模组的红色、绿色和蓝色通道。
|
||||
|
||||
这里,我提供一下我创建的LED类文件,供你参考:
|
||||
|
||||
```
|
||||
from machine import PWM
|
||||
from machine import Pin
|
||||
|
||||
class Led():
|
||||
"""
|
||||
创建LED类
|
||||
"""
|
||||
def __init__(self, rpin, gpin, bpin, freq=1000):
|
||||
"""
|
||||
构造函数
|
||||
:param pin: 接LED的管脚,必须支持PWM
|
||||
:param freq: PWM的默认频率是1000
|
||||
"""
|
||||
self.pin_red = Pin(rpin)
|
||||
self.pin_green = Pin(gpin)
|
||||
self.pin_blue = Pin(bpin)
|
||||
|
||||
self.led_red = PWM(self.pin_red, freq = freq)
|
||||
self.led_green = PWM(self.pin_green, freq = freq)
|
||||
self.led_blue = PWM(self.pin_blue, freq = freq)
|
||||
|
||||
def rgb_light(self, red, green, blue, brightness):
|
||||
if red in range(256) and \
|
||||
green in range(256) and \
|
||||
blue in range(256) and \
|
||||
0.0 <= brightness and \
|
||||
brightness <=1.0:
|
||||
self.led_red.duty(int(red/255*brightness*1023))
|
||||
self.led_green.duty(int(green/255*brightness*1023))
|
||||
self.led_blue.duty(int(blue/255*brightness*1023))
|
||||
else:
|
||||
print("red green blue must between 0 and 255, and brightness from 0.0 to 1.0")
|
||||
|
||||
def deinit(self):
|
||||
"""
|
||||
析构函数
|
||||
"""
|
||||
self.led_red.deinit()
|
||||
self.led_green.deinit()
|
||||
self.led_blue.deinit()
|
||||
|
||||
```
|
||||
|
||||
### 如何控制电灯的开关?
|
||||
|
||||
智能电灯的“开”和“关”控制,我们使用继电器来实现。
|
||||
|
||||
继电器分为弱电(小电流、低电压)和强电(大电流、高电压)两个部分。其中,弱电的部分可以接微处理芯片;强电部分可以连接交流电设备,比如电风扇、冰箱和灯泡等。继电器其实就像是我们现实生活中“中间人”的角色,它通过电磁器件、或者光耦单元将弱电和强电联系起来,以完成微处理芯片对强电设备的控制。
|
||||
|
||||
在这次的实验中,我使用的一款基于 SRD-05VDC-SL-C 型号的电磁继电器。使用中,模块的控制接口,需要连接 NodeMCU 开发板的 GPIO 管脚。我们通过设置这个 GPIO 的输出电平高、低状态,实现控制继电器强电部分电路的“通”和“断”。
|
||||
|
||||
需要注意的是:在我们这一讲的实战中,继电器强电部分连接的 LED 灯,属于低电压设备,电压不超过 5V,这是对人体没有危害的电压;我们实战的目的是学习知识,在这个基础上,我们基于安全考虑,不建议把继电器的强电部分,连接220V交流电供电的电灯。
|
||||
|
||||
如果你有一定的交流电实践经验,那么在实践时也要注意两点:
|
||||
|
||||
1. 注意自身和周围人的安全,比如强电部分不要有裸露的电线,一定用绝缘胶带包扎好;
|
||||
1. 弱电部分的供电,不要使用电脑的 USB 接口;为了电脑设备安全,建议使用独立的电源为开发板供电。
|
||||
|
||||
我同样把我创建的Relay类文件放在这里,供你参考:
|
||||
|
||||
```
|
||||
from machine import ADC
|
||||
from machine import Pin
|
||||
|
||||
class Relay():
|
||||
|
||||
def __init__(self, pin):
|
||||
self.relaypin = Pin(pin, Pin.OUT)
|
||||
self.last_status = 1
|
||||
|
||||
def set_state(self, state):
|
||||
self.relaypin.value(state)
|
||||
self.last_status = state
|
||||
|
||||
```
|
||||
|
||||
### 智能电灯的整体电路如何搭建?
|
||||
|
||||
确定了LED的技术方案和继电器后,我们就可以搭建出智能电灯的电路。我简单画了一下电路中各模块的连线情况,你在连接电路的时候按照这个连线来就行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dd/61/dde075941211c591c44ea46cf4673861.png" alt="">
|
||||
|
||||
电路搭建完成后,你可以运行下面的代码测试一下:
|
||||
|
||||
```
|
||||
from machine import PWM, Pin
|
||||
import time
|
||||
|
||||
#设置对应红、绿、蓝的三个GPIO管脚
|
||||
led_red = PWM(Pin(5), freq = 1000)
|
||||
led_green = PWM(Pin(4), freq = 1000)
|
||||
led_blue = PWM(Pin(0), freq = 1000)
|
||||
|
||||
#继电器的GPIO管脚
|
||||
relaypin = Pin(16, Pin.OUT)#
|
||||
|
||||
#通过PWM的占空比设置颜色
|
||||
def rgb_light(red, green, blue, brightness):
|
||||
pwm_red = led_red.duty(int(red/255*brightness*1023))
|
||||
pwm_green = led_green.duty(int(green/255*brightness*1023))
|
||||
pwm_blue = led_blue.duty(int(blue/255*brightness*1023))
|
||||
|
||||
rgb_light(255, 255, 0, 1.0)
|
||||
|
||||
#周期点亮、熄灭
|
||||
while True:
|
||||
relaypin.on()
|
||||
time.sleep(2)
|
||||
relaypin.off()
|
||||
time.sleep(2)
|
||||
|
||||
```
|
||||
|
||||
### 远程控制如何实现?
|
||||
|
||||
准备好了智能电灯设备后,要实现远程控制,我们还需要让智能电灯连接到物联网平台。那智能电灯如何与物联网平台通信交互呢?这里就要用到MQTT通信协议了。
|
||||
|
||||
首先,你需要在NodeMCU开发板上安装一个 MQTT 客户端代码库 [umqtt.simple 库](https://github.com/micropython/micropython-lib/tree/master/umqtt.simple)。它来自MicroPython官方维护的非内核标准库 [micropython-lib](https://github.com/micropython/micropython-lib),你可以使用upip包管理器来安装。在串口 REPL 中运行下面的命令,就可以完成安装:
|
||||
|
||||
```
|
||||
>>> import upip
|
||||
>>> upip.install('micropython-umqtt.simple')
|
||||
|
||||
```
|
||||
|
||||
安装命令是不是很简单?但是这里有一个前提要求,就是NodeMCU需要连接到Wi-Fi路由器上,也就是能够访问网络,因为这个安装过程是从网络下载安装文件。
|
||||
|
||||
怎么让NodeMCU连接到Wi-Fi路由器呢?你仍然可以通过串口 REPL 来完成。你可以在REPL中依次输入下面的命令来接入网络:
|
||||
|
||||
```
|
||||
>>> import network
|
||||
>>> wifi = network.WLAN(network.STA_IF)
|
||||
>>> wifi.active(True)
|
||||
>>> wifi.scan()
|
||||
>>> wifi.isconnected()
|
||||
>>> wifi.connect('你家中Wi-Fi的SSID', '你家中Wi-Fi密码')
|
||||
>>> wifi.isconnected()
|
||||
|
||||
```
|
||||
|
||||
**安装好 umqtt.simple 库之后,我们需要再设置一下物联网平台的MQTT协议交互的 Topic和具体的连接参数。**
|
||||
|
||||
我们用到的MQTT Topic主要有两个:一个用于发布消息,即消息流向是从设备到物联网平台;另一个用于接收订阅消息,即消息流向是从物联网平台到设备。
|
||||
|
||||
```
|
||||
#发布消息
|
||||
$thing/up/property/ProductID/DeviceName
|
||||
|
||||
#接收订阅消息
|
||||
$thing/down/property/ProductID/DeviceName
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,ProductID和DeviceName需要替换为我们在上面创建设备的具体值。
|
||||
|
||||
设备与物联网平台建立MQTT连接,涉及Broker 服务器地址、端口号、设备ID(ClientID)、用户名(UserName)和密码(Password)。我把这些参数整理到了一张表里,供你参考。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/46/12623e989f7c3d29a7efc1a3e1d86246.jpg" alt="">
|
||||
|
||||
用户名和密码不太好手动生成,我们可以借助一个[网页工具](https://iot-exp-individual-1258344699.cos.ap-guangzhou.myqcloud.com/password%E7%94%9F%E6%88%90%E5%B7%A5%E5%85%B7.zip)来生成。下载完成后,你可以解压缩,得到一些网页原文件,双击打开sign.html,然后在页面输入设备三元组,点击“Generate”即可生成用户名和密码。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/96/72db80e852a9d87a073c78f8d5272e96.png" alt="">
|
||||
|
||||
有了这些信息,我们就可以开始为智能电灯设备编写MQTT代码了:
|
||||
|
||||
```
|
||||
from LED import Led
|
||||
from Button import Button
|
||||
from Relay import Relay
|
||||
|
||||
import time
|
||||
import uasyncio
|
||||
import network
|
||||
import ujson
|
||||
from umqtt.simple import MQTTClient
|
||||
|
||||
"""
|
||||
Wi-Fi Gateway : SSID and Password
|
||||
"""
|
||||
WIFI_AP_SSID = "你家的Wi-Fi SSID"
|
||||
WIFI_AP_PSW = "你家的Wi-Fi密码"
|
||||
|
||||
"""
|
||||
QCloud Device Info
|
||||
"""
|
||||
DEVICE_NAME = "你的设备名称"
|
||||
PRODUCT_ID = "你的产品ID"
|
||||
DEVICE_KEY = "你的设备密钥"
|
||||
|
||||
"""
|
||||
MQTT topic
|
||||
"""
|
||||
MQTT_CONTROL_TOPIC = "$thing/down/property/"+PRODUCT_ID+"/"+DEVICE_NAME
|
||||
MQTT_CONTROL_REPLY_TOPIC = "$thing/up/property/"+PRODUCT_ID+"/"+DEVICE_NAME
|
||||
|
||||
led = Led(5, 4, 0)
|
||||
relay = Relay(16)
|
||||
button = Button(14)
|
||||
|
||||
mqtt_client = None
|
||||
color = 0 #enum 0=red, 1=green, 2=blue
|
||||
name= "" #light name. it is optional
|
||||
brightness = 100 # 0%~100%
|
||||
light_changed = False
|
||||
|
||||
async def wifi_connect(ssid, pwd):
|
||||
sta = network.WLAN(network.STA_IF)
|
||||
sta.active(True)
|
||||
sta.connect(ssid, pwd)
|
||||
|
||||
while not sta.isconnected():
|
||||
print("Wi-Fi Connecting...")
|
||||
time.sleep_ms(500)
|
||||
|
||||
def mqtt_callback(topic, msg):
|
||||
global led, relay, button
|
||||
global color, name, brightness, light_changed
|
||||
|
||||
print((topic, msg))
|
||||
msg_json = ujson.loads(msg)
|
||||
if msg_json['method'] == 'control':
|
||||
params = msg_json['params']
|
||||
|
||||
power_switch_tmp = params.get('power_switch')
|
||||
if power_switch_tmp is not None:
|
||||
power_switch = power_switch_tmp
|
||||
relay.set_state(power_switch)
|
||||
|
||||
brightness_tmp = params.get('brightness')
|
||||
if brightness_tmp is not None:
|
||||
brightness = brightness_tmp
|
||||
|
||||
color_tmp = params.get('color')
|
||||
if color_tmp is not None:
|
||||
color = color_tmp
|
||||
|
||||
name_tmp = params.get('name')
|
||||
if name_tmp is not None:
|
||||
name = name_tmp
|
||||
|
||||
if brightness_tmp is not None or color_tmp is not None:
|
||||
light_changed = True
|
||||
|
||||
async def mqtt_connect():
|
||||
global mqtt_client
|
||||
|
||||
MQTT_SERVER = PRODUCT_ID + ".iotcloud.tencentdevices.com"
|
||||
MQTT_PORT = 1883
|
||||
MQTT_CLIENT_ID = PRODUCT_ID+DEVICE_NAME
|
||||
MQTT_USER_NAME = "你的用户名"
|
||||
MQTTT_PASSWORD = "你的密码"
|
||||
|
||||
mqtt_client = MQTTClient(MQTT_CLIENT_ID, MQTT_SERVER, MQTT_PORT,MQTT_USER_NAME, MQTTT_PASSWORD, 60)
|
||||
mqtt_client.set_callback(mqtt_callback)
|
||||
mqtt_client.connect()
|
||||
|
||||
def mqtt_report(client, color, name, switch, brightness):
|
||||
|
||||
msg = {
|
||||
"method": "report",
|
||||
"clientToken": "clientToken-2444532211",
|
||||
"params": {
|
||||
"color": color,
|
||||
"color_temp": 0,
|
||||
"name": name,
|
||||
"power_switch": switch,
|
||||
"brightness": brightness
|
||||
}
|
||||
}
|
||||
|
||||
client.publish(MQTT_CONTROL_REPLY_TOPIC.encode(), ujson.dumps(msg).encode())
|
||||
|
||||
async def light_loop():
|
||||
global led, relay, button
|
||||
global color, name, brightness, light_changed
|
||||
|
||||
switch_status_last = 1
|
||||
LED_status = 1
|
||||
|
||||
color = 2 #blue
|
||||
brightness = 100 #here 100% == 1
|
||||
led.rgb_light(0, 0, 255, brightness/100.0)
|
||||
|
||||
time_cnt = 0
|
||||
|
||||
mqtt_client.subscribe(MQTT_CONTROL_TOPIC.encode())
|
||||
|
||||
while True:
|
||||
mqtt_client.check_msg()
|
||||
|
||||
switch_status = button.state()
|
||||
LED_status = relay.state()
|
||||
if switch_status != switch_status_last:
|
||||
if switch_status == 0 and switch_status_last == 1:
|
||||
LED_status = 0 if LED_status else 1
|
||||
relay.set_state(LED_status)
|
||||
switch_status_last = switch_status
|
||||
|
||||
if light_changed:
|
||||
light_changed = False
|
||||
led.rgb_light(255 if color==0 else 0, 255 if color==1 else 0, 255 if color==2 else 0, brightness/100.0)
|
||||
|
||||
if time_cnt >= 20:
|
||||
mqtt_report(mqtt_client, color, name, LED_status, brightness)
|
||||
time_cnt = 0
|
||||
time_cnt = time_cnt+1
|
||||
uasyncio.sleep_ms(50)
|
||||
|
||||
async def main():
|
||||
global mqtt_client
|
||||
|
||||
# Wi-Fi connection
|
||||
try:
|
||||
await uasyncio.wait_for(wifi_connect(WIFI_AP_SSID, WIFI_AP_PSW), 20)
|
||||
except uasyncio.TimeoutError:
|
||||
print("wifi connected timeout!")
|
||||
|
||||
# MQTT connection
|
||||
try:
|
||||
await uasyncio.wait_for(mqtt_connect(), 20)
|
||||
except uasyncio.TimeoutError:
|
||||
print("mqtt connected timeout!")
|
||||
|
||||
await uasyncio.gather(light_loop())
|
||||
|
||||
uasyncio.run(main())
|
||||
|
||||
```
|
||||
|
||||
## 如何通过手机远程控制?
|
||||
|
||||
在完成代码后,我们通过ampy工具或者pyboard.py工具,将这些源代码上传到NodeMCU开发板中。程序开始自动执行,智能电灯自动接入物联网平台。打开物联网平台的设备调试页面,我们就可以看到设备显示“在线”。
|
||||
|
||||
点击“调试”,通过调试界面发送MQTT消息来控制智能电灯。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/28/5d03e52c440c12f7d0e00c972223dd28.png" alt="">
|
||||
|
||||
点击“发送”,物联网平台会向设备发送下面这样的消息内容:
|
||||
|
||||
```
|
||||
{
|
||||
"method": "control",
|
||||
"clientToken": "clientToken-e9d920ea-a1f4-4a53-aada-a1d36fbbdd20",
|
||||
"params": {
|
||||
"power_switch": 1,
|
||||
"brightness": 50,
|
||||
"color": 0,
|
||||
"color_temp": 0,
|
||||
"name": ""
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
那怎么通过手机小程序控制电灯呢?这也很好实现,我们只需要在小程序上添加设备,就可以使用小程序界面控制了。
|
||||
|
||||
打开“腾讯连连”小程序,点击“+”按钮,扫描我们在“设备调试”界面保存的二维码,就完成添加动作了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/7f/1d7b56914e783d7f5bfe18a633af2e7f.png" alt="">
|
||||
|
||||
然后,点击设备卡片,进入设备交互界面,就可以进行远程控制了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/40/5c/40969abc41cafa86910c231046099d5c.png" alt="">
|
||||
|
||||
到这里,我们就可以用手机来远程控制智能电灯的开关状态和颜色了。
|
||||
|
||||
## 小结
|
||||
|
||||
在这一讲中,我们边实战边学习了智能电灯的电路组成、程序代码,以及与腾讯云物联网平台进行MQTT通信的相关知识。我再来帮你总结下需要记住的几个核心知识点。
|
||||
|
||||
1. 基于物联网平台开发产品,我们一般需要完成三件事,分别是物理网平台的创建和设置、智能设备的功能开发(每个产品最重要的部分),以及用户交互界面的开发。
|
||||
1. 物模型是在物联网平台上定义设备的重要概念,你可以将[第6讲](https://time.geekbang.org/column/article/310441)的物模型直接导入来创建设备。
|
||||
1. PWM 是照明控制中非常重要的一种技术手段,了解了频率和占空比的概念,也就掌握了它的工作原理。
|
||||
1. 智能电灯通过MQTT通信协议与物联网平台交互。在NodeMCU开发板上可以安装一个 MQTT 客户端代码库 [umqtt.simple 库](https://github.com/micropython/micropython-lib/tree/master/umqtt.simple),来开发MQTT客户端代码。
|
||||
|
||||
其实今天我们控制的 RGB LED 灯只是一颗灯珠,但是在实际产品中,我们很可能要控制多颗 LED。这时候,我们面临的首要问题,就是微控制芯片(MCU)的管脚不够用了。那怎么办呢?
|
||||
|
||||
关于这个问题,行业里已经有很多解决方案了。其中,[NeoPixel](https://learn.adafruit.com/adafruit-neopixel-uberguide) 是开源硬件方案,你可以在网上搜索、了解一下,当然也可以和我多多交流。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我给你留一道思考题吧。
|
||||
|
||||
在物联网平台的设备调试界面,我们可以发送MQTT 消息来控制设备,而且我还给出了具体的消息内容。你可以和我说说,这个消息的主题(Topic)是什么吗?
|
||||
|
||||
另外,在智能电灯的功能实现中,我们通过继电器实现了电路通断的控制,通过LED灯实现了颜色的调节。你可以实现一下按钮的功能吗?这样就可以通过设备本身来控制电灯的打开和熄灭了。这也更符合现实中灯的样式。
|
||||
|
||||
欢迎你在留言区和我分享你的思考,如果在实战今天的智能电灯控制时遇到了什么问题,也可以和我进一步交流。同时,也欢迎你把今天的内容分享给你的朋友,一起动手实现一个酷炫的智能电灯控制系统吧。
|
||||
311
极客时间专栏/物联网开发实战/实战篇/18 | 场景联动:智能电灯如何感知光线?(上).md
Normal file
311
极客时间专栏/物联网开发实战/实战篇/18 | 场景联动:智能电灯如何感知光线?(上).md
Normal file
@@ -0,0 +1,311 @@
|
||||
<audio id="audio" title="18 | 场景联动:智能电灯如何感知光线?(上)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/ff/1924745036eccd50c3a5ca0ec7dcccff.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
在上一讲,我们打造了自己的联网智能电灯,你可以通过手机小程序来控制它的打开和关闭,也就是实现远程控制。
|
||||
|
||||
其实,我们还可以进一步提高体验,让智能电灯可以基于环境的明暗来自动地打开和关闭。要做到这一点并不难,可以分为两个阶段,第一阶段是打造传感器设备来感知光照的强弱,判断出环境的明暗状态,第二阶段是创建一个场景联动,根据传感器的数值来控制智能电灯的状态。
|
||||
|
||||
这一讲,我先带你一步一步地实现第一阶段的工作(如有需要,你可以根据[这份文档](https://shimo.im/sheets/D3VVPdwcYRhhQRXh/MODOC)自行采购相关硬件)。
|
||||
|
||||
## 第一步:通信技术
|
||||
|
||||
首先,我们为光照传感器设备选择通信技术。
|
||||
|
||||
因为光照传感器设备的部署位置比较灵活,不太可能像智能电灯一样连接房间里的电源线,所以我们要用一种比Wi-Fi功耗更低的通信技术。这样的话,就算使用电池供电,也可以长时间(一年以上)持续工作。
|
||||
|
||||
经过对比,我建议选择 BLE 低功耗蓝牙技术(关于通信技术的选择策略,你可以参考[第2讲](https://time.geekbang.org/column/article/306976))。随着智能手机的发展,蓝牙早已成为手机标配的通信技术,蓝牙芯片和协议栈的成熟度非常高,而且在设备的供应链方面,蓝牙芯片可以选择的供应商也非常多。
|
||||
|
||||
不过在正式开发之前,我还得为你补充说明一些BLE的相关知识。
|
||||
|
||||
BLE设备可以在4种模式下工作:
|
||||
|
||||
1. **广播模式**(Broadcaster),这里特指单纯的广播模式。这种模式下设备不可以被连接,只能够以一定的时间间隔把数据广播出来,供其他设备使用,比如手机扫描处理。蓝牙Beacon设备就是工作在这种模式。
|
||||
1. **从机模式**(Peripheral),这种模式下设备仍然可以广播数据,同时也可以被连接。建立连接后,双方可以进行双向通信。比如你用手机连接一个具有蓝牙功能的体温计,这时体温计就是从机(Peripheral)。
|
||||
1. **主机模式**(Central),这种模式下设备不进行广播,但是可以扫描周围的蓝牙广播包,发现其他设备,然后主动对这些设备发起连接。还是刚才那个例子,主动连接蓝牙体温计的手机就是主机(Central)角色。
|
||||
1. **观察者模式**(Observer),这种模式下设备像主机模式一样,也不进行广播,而是扫描周围的蓝牙广播包,但是不同的地方是,它不会与从机设备建立连接。一般收集蓝牙设备广播包的网关就是在这种模式下工作的,它会将收集的广播数据通过网线、Wi-Fi或者4G等蜂窝网络上传到云平台。
|
||||
|
||||
在这一讲中,我们打造的光照传感器只需要提供光照强度数据就行了,并不需要进行双向通信,所以我们可以定义设备在广播模式下工作。
|
||||
|
||||
## 第二步:选择开发板
|
||||
|
||||
那么光照传感器设备要选择什么开发板呢?
|
||||
|
||||
我们在上一讲打造的联网智能电灯中使用的NodeMCU是基于**ESP8266芯片**的,相信你也注意到了,这款芯片并不支持低功耗蓝牙。
|
||||
|
||||
好在市场上还有一款基于**ESP32芯片**的NodeMCU开发板。[ESP32](https://www.espressif.com/zh-hans/products/socs/esp32/overview)是乐鑫科技出品的另一款性能优良且满足低功耗的物联网芯片,它同时支持Wi-Fi和低功率蓝牙通信技术,还有丰富的ADC接口。
|
||||
|
||||
更重要的是,MicroPython也支持ESP32芯片,这样我们就可以继续使用Python语言来开发了。
|
||||
|
||||
## 第三步:准备MicroPython环境
|
||||
|
||||
接下来,我们就在NodeMCU(ESP32)上安装MicroPython固件,准备Python程序的运行环境。
|
||||
|
||||
MicroPython官网已经为我们准备了编译好的固件文件,这省掉了我们在电脑上进行交叉编译的工作。你可以从[这个链接](http://micropython.org/download/esp32/) 中选择“Firmware with ESP-IDF v3.x”下面的“GENERIC”类别,直接下载最新版本的固件文件到电脑中。
|
||||
|
||||
然后,我们使用一根 USB 数据线,将 NodeMCU 开发板和电脑连接起来。USB数据线仍然选择一头是 USB-A 接口、另一头是 Micro-USB 接口,并且支持数据传输的完整线缆。具体细节,你可以再回顾[第16讲](https://time.geekbang.org/column/article/321652)中的相关内容。
|
||||
|
||||
我们使用esptool工具把这个固件烧录到NodeMCU开发板上。先在电脑终端上输入下面的命令,清空一下NodeMCU的Flash存储芯片。
|
||||
|
||||
```
|
||||
esptool.py --chip esp32 --port /dev/cu.usbserial-0001 erase_flash
|
||||
|
||||
```
|
||||
|
||||
你可以从命令里看到,和之前智能电灯用的命令相比,这里增加了芯片信息“esp32”。另外,“--port”后面的串口设备名称,需要你替换为自己电脑上对应的名称。
|
||||
|
||||
成功擦除Flash之后,就执行下面的命令,将固件写入Flash芯片。
|
||||
|
||||
```
|
||||
esptool.py --chip esp32 --port /dev/cu.usbserial-0001 --baud 460800 write_flash -z 0x1000 esp32-idf3-20200902-v1.13.bin
|
||||
|
||||
```
|
||||
|
||||
这时,我们使用电脑上的终端模拟器软件,比如 SecureCRT,通过串口协议连接上开发板,注意波特率(Baud rate)设置为 115200。
|
||||
|
||||
然后你应该就能看到下图所示的内容,并且可以进行交互。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e2/8b/e2a9a62bc472a7f41ddca0e5b6ca678b.png" alt="">
|
||||
|
||||
## 第四步:搭建光照传感器硬件电路
|
||||
|
||||
现在,我们开始基于NodeMCU搭建光照传感器的硬件电路。
|
||||
|
||||
首先,我们要准备好实验的材料:
|
||||
|
||||
1. NodeMCU(ESP32)开发板一个。注意区分芯片的具体型号。
|
||||
1. 光照传感器模块一个
|
||||
1. 杜邦线/跳线若干个
|
||||
1. 面包板一个
|
||||
|
||||
然后,你可以按照我画的连线图来搭建出自己的电路。跟联网智能电灯的电路比起来,这个还是非常简单的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ff/ab/ffcd6bb49b89e3d079fc1ca5c2dd18ab.png" alt="">
|
||||
|
||||
这里说明一下,在我的电路图中,光照传感器模块从左到右,管脚分别是光强度模拟信号输出管脚、电源地GND和电源正VCC管脚。你需要根据自己的传感器模块调整具体的连线。
|
||||
|
||||
我选择的是基于PT550环保型光敏二极管的光照传感器元器件,它的灵敏度更高,测量范围是0Lux~6000Lux。
|
||||
|
||||
Lux(勒克斯)是光照强度的单位,它和另一个概念Lumens(流明)是不同的。Lumens是指一个光源(比如电灯、投影仪)发出的光能力的总量,而Lux是指空间内一个位置接收到的光照的强度。
|
||||
|
||||
这个元器件通过信号管脚输出模拟量,我们读取NodeMCU ESP32的ADC模数转换器(ADC0,对应GPIO36)的数值,就可以得到光照强度。这个数值越大,表示光照强度越大。
|
||||
|
||||
因为ADC支持的最大位数是12bit,所以这个数值范围是0~4095之间。这里我们粗略地按照线性关系做一个转换。具体计算过程,你可以参考下面的代码:
|
||||
|
||||
```
|
||||
from machine import ADC
|
||||
from machine import Pin
|
||||
|
||||
class LightSensor():
|
||||
|
||||
def __init__(self, pin):
|
||||
self.light = ADC(Pin(pin))
|
||||
|
||||
def value(self):
|
||||
value = self.light.read()
|
||||
print("Light ADC value:",value)
|
||||
return int(value/4095*6000)
|
||||
|
||||
```
|
||||
|
||||
## 第五步:编写蓝牙程序
|
||||
|
||||
NodeMCU ESP32的固件已经集成了BLE的功能,我们可以直接在这个基础上进行软件的开发。这里我们需要给广播包数据定义一定的格式,让其他设备可以顺利地解析使用扫描到的数据。
|
||||
|
||||
那么怎么定义蓝牙广播包的格式呢?我们可以使用小米制定的[MiBeacon](https://iot.mi.com/new/doc/embedded-development/ble/ble-mibeacon.html)蓝牙协议。
|
||||
|
||||
**MiBeacon蓝牙协议**的广播包格式是基于BLE的GAP(Generic Access Profile)制定的。GAP控制了蓝牙的广播和连接,也就是控制了设备如何被发现,以及如何交互。
|
||||
|
||||
具体来说,GAP定义了两种方式来让设备广播数据:
|
||||
|
||||
一个是广播数据(Advertising Data payload),这个是必须的,数据长度是31个字节;
|
||||
|
||||
另一个是扫描回复数据(Scan Response payload),它基于蓝牙主机设备(比如手机)发出的扫描请求(Scan Request)来回复一些额外的信息。数据长度和广播数据一样。
|
||||
|
||||
(注意,蓝牙5.0中有扩展的广播数据,数据长度等特性与此不同,但这里不涉及,所以不再介绍。)
|
||||
|
||||
所以,只要含有以下指定信息的广播报文,就可以认为是符合MiBeacon蓝牙协议的。
|
||||
|
||||
1. Advertising Data中 Service Data (0x16) 含有Mi Service UUID的广播包,UUID是0xFE95。
|
||||
1. Scan Response中 Manufacturer Specific Data (0xFF)含有小米公司识别码的广播包,识别码ID是0x038F。
|
||||
|
||||
其中,无论是在Advertising Data中,还是Scan Response中,均采用统一格式定义。
|
||||
|
||||
具体的广播报文格式定义,你可以参考下面的表格。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/aa/23a2b5245717b21c4ce766bc13d69faa.jpg" alt="">
|
||||
|
||||
因为我们要为光照传感器增加广播光照强度数据的能力,所以主要关注[Object的定义](https://iot.mi.com/new/doc/embedded-development/ble/object-definition)。Object分为属性和事件两种,具体定义了设备数据的含义,比如体温计的温度、土壤的湿度等,数据格式如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/4e/a79e59c00b0ba726923910c8768f674e.jpg" alt="">
|
||||
|
||||
按照MiBeacon的定义,光照传感器的Object ID是0x1007,数据长度3个字节,数值范围是0~120000之间。
|
||||
|
||||
我将代码贴在下面,供你参考。
|
||||
|
||||
```
|
||||
#File:ble_lightsensor.py
|
||||
import bluetooth
|
||||
import struct
|
||||
import time
|
||||
from ble_advertising import advertising_payload
|
||||
|
||||
from micropython import const
|
||||
|
||||
_IRQ_CENTRAL_CONNECT = const(1)
|
||||
_IRQ_CENTRAL_DISCONNECT = const(2)
|
||||
_IRQ_GATTS_INDICATE_DONE = const(20)
|
||||
|
||||
_FLAG_READ = const(0x0002)
|
||||
_FLAG_NOTIFY = const(0x0010)
|
||||
|
||||
_ADV_SERVICE_DATA_UUID = 0xFE95
|
||||
_SERVICE_UUID_ENV_SENSE = 0x181A
|
||||
_CHAR_UUID_AMBIENT_LIGHT = 'FEC66B35-937E-4938-9F8D-6E44BBD533EE'
|
||||
|
||||
# Service environmental sensing
|
||||
_ENV_SENSE_UUID = bluetooth.UUID(_SERVICE_UUID_ENV_SENSE)
|
||||
# Characteristic ambient light density
|
||||
_AMBIENT_LIGHT_CHAR = (
|
||||
bluetooth.UUID(_CHAR_UUID_AMBIENT_LIGHT),
|
||||
_FLAG_READ | _FLAG_NOTIFY ,
|
||||
)
|
||||
_ENV_SENSE_SERVICE = (
|
||||
_ENV_SENSE_UUID,
|
||||
(_AMBIENT_LIGHT_CHAR,),
|
||||
)
|
||||
|
||||
# https://specificationrefs.bluetooth.com/assigned-values/Appearance%20Values.pdf
|
||||
_ADV_APPEARANCE_GENERIC_AMBIENT_LIGHT = const(1344)
|
||||
|
||||
class BLELightSensor:
|
||||
def __init__(self, ble, name='Nodemcu'):
|
||||
self._ble = ble
|
||||
self._ble.active(True)
|
||||
self._ble.irq(self._irq)
|
||||
((self._handle,),) = self._ble.gatts_register_services((_ENV_SENSE_SERVICE,))
|
||||
self._connections = set()
|
||||
time.sleep_ms(500)
|
||||
self._payload = advertising_payload(
|
||||
name=name, services=[_ENV_SENSE_UUID], appearance=_ADV_APPEARANCE_GENERIC_AMBIENT_LIGHT
|
||||
)
|
||||
self._sd_adv = None
|
||||
self._advertise()
|
||||
|
||||
def _irq(self, event, data):
|
||||
# Track connections so we can send notifications.
|
||||
if event == _IRQ_CENTRAL_CONNECT:
|
||||
conn_handle, _, _ = data
|
||||
self._connections.add(conn_handle)
|
||||
elif event == _IRQ_CENTRAL_DISCONNECT:
|
||||
conn_handle, _, _ = data
|
||||
self._connections.remove(conn_handle)
|
||||
# Start advertising again to allow a new connection.
|
||||
self._advertise()
|
||||
elif event == _IRQ_GATTS_INDICATE_DONE:
|
||||
conn_handle, value_handle, status = data
|
||||
|
||||
def set_light(self, light_den, notify=False):
|
||||
self._ble.gatts_write(self._handle, struct.pack("!h", int(light_den)))
|
||||
self._sd_adv = self.build_mi_sdadv(light_den)
|
||||
self._advertise()
|
||||
if notify:
|
||||
for conn_handle in self._connections:
|
||||
if notify:
|
||||
# Notify connected centrals.
|
||||
self._ble.gatts_notify(conn_handle, self._handle)
|
||||
|
||||
def build_mi_sdadv(self, density):
|
||||
|
||||
uuid = 0xFE95
|
||||
fc = 0x0010
|
||||
pid = 0x0002
|
||||
fcnt = 0x01
|
||||
mac = self._ble.config('mac')
|
||||
objid = 0x1007
|
||||
objlen = 0x03
|
||||
objval = density
|
||||
|
||||
service_data = struct.pack("<3HB",uuid,fc,pid,fcnt)+mac+struct.pack("<H2BH",objid,objlen,0,objval)
|
||||
print("Service Data:",service_data)
|
||||
|
||||
return advertising_payload(service_data=service_data)
|
||||
|
||||
def _advertise(self, interval_us=500000):
|
||||
self._ble.gap_advertise(interval_us, adv_data=self._payload)
|
||||
time.sleep_ms(100)
|
||||
|
||||
print("sd_adv",self._sd_adv)
|
||||
if self._sd_adv is not None:
|
||||
print("sdddd_adv",self._sd_adv)
|
||||
self._ble.gap_advertise(interval_us, adv_data=self._sd_adv)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
#File: main.py
|
||||
from ble_lightsensor import BLELightSensor
|
||||
from lightsensor import LightSensor
|
||||
import time
|
||||
import bluetooth
|
||||
|
||||
def main():
|
||||
ble = bluetooth.BLE()
|
||||
ble.active(True)
|
||||
ble_light = BLELightSensor(ble)
|
||||
|
||||
light = LightSensor(36)
|
||||
light_density = light.value()
|
||||
i = 0
|
||||
|
||||
while True:
|
||||
# Write every second, notify every 10 seconds.
|
||||
i = (i + 1) % 10
|
||||
ble_light.set_light(light_density, notify=i == 0)
|
||||
print("Light Lux:", light_density)
|
||||
|
||||
light_density = light.value()
|
||||
time.sleep_ms(1000)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
```
|
||||
|
||||
## 第六步:验证光照传感器
|
||||
|
||||
到这里,我们已经完成了光照传感器设备的开发工作。那么怎么验证设备有没有正常工作呢?
|
||||
|
||||
我们可以通过手机上的蓝牙调试软件来扫描周围蓝牙设备,查看设备有没有蓝牙广播包输出,能不能跟手机正常交互。常用的软件有LightBlue、nRFConnect 和 BLEScanner,选择其中一个就行了。
|
||||
|
||||
比如我选择的是nRF Connect,打开之后,它会自动扫描周围的蓝牙广播包,将发现的设备以列表的形式展示。
|
||||
|
||||
如果周围蓝牙设备很多的话,为了方便发现自己的开发板,你可以点击列表上方的“No Filter”,选择将“Max.RSSI”打开。拖动其中的滑竿到合适的值,比如-50dBm,就可以过滤掉蓝牙信号强度比较弱(一般也是比较远)的设备。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/54/fbe2863b3968e69e36c4f880714fba54.png" alt="">
|
||||
|
||||
下面是我的手机扫描到的基于NodeMCU开发板的蓝牙设备。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/fc/90569129dbf544c3a9da37f1e2fd84fc.png" alt="">
|
||||
|
||||
其中名称Nodemcu下面的就是广播包的具体数据。
|
||||
|
||||
到这里,我们就完成了光照传感器设备的开发工作。
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下,在这一讲中,我介绍了光照传感器的开发过程,并且补充了低功耗蓝牙技术的相关知识。下面,我们回顾以下重点:
|
||||
|
||||
1. 对于无法连接电源线、需要灵活放置甚至经常移动的设备,低功耗蓝牙技术是合适的通信技术选择。
|
||||
1. MiBeacon协议的广播包定义是基于BLE的GAP(Generic Access Profile)制定的,主要有广播数据(Advertising Data)和扫描回复数据(Scan Response)两种。其中广播数据中Service Data的UUID是0xFE95,扫描回复数据中Manufacturer Specific Data的厂家识别码是0x038F。
|
||||
1. 在日常的蓝牙设备开发工作中,我们经常需要调试、测试蓝牙功能,这时你可以使用手机上的蓝牙调试软件来验证,比如LightBlue、nRFConnect 和 BLEScanner等。
|
||||
|
||||
不过,准备好光照传感器设备只是第一步,为了实现光照传感器和智能电灯的联动,我们还需要将光照传感器接入网络。这就需要借助蓝牙网关设备了,在下一讲中,我将基于树莓派讲解网关设备的开发过程。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,给你留一个思考题吧。
|
||||
|
||||
在这一讲的开头,我提到蓝牙设备除了广播数据的能力,还可以连接进行交互。在我提供的代码中其实也包含了一个可供连接获取数据的Service和Characteristic,你发现了吗?你知道这些是基于低功耗蓝牙中的什么Profile协议吗?
|
||||
|
||||
欢迎你在留言区写下自己的答案和我交流一下,也欢迎你将这一讲分享给你的朋友,一起讨论学习。
|
||||
572
极客时间专栏/物联网开发实战/实战篇/19 | 场景联动:智能电灯如何感知光线?(下).md
Normal file
572
极客时间专栏/物联网开发实战/实战篇/19 | 场景联动:智能电灯如何感知光线?(下).md
Normal file
@@ -0,0 +1,572 @@
|
||||
<audio id="audio" title="19 | 场景联动:智能电灯如何感知光线?(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/eb/4a/ebc93760yyc365cf45252c464e09e54a.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
在上一讲,我们基于NodeMCU ESP32开发板,开发了一款光照传感器。考虑到低功耗的需求,它是基于低功耗蓝牙技术来实现的。但是蓝牙设备本身无法直接联网上报数据,那么我们要怎么根据光照强度数据来联动控制智能电灯呢?
|
||||
|
||||
不知道你还记不记得[第9讲](https://time.geekbang.org/column/article/313631)的内容?对于蓝牙设备,我们需要借助**网关**来实现联网的目的。所以在这一讲中,我会带你用树莓派打造蓝牙网关,最终实现光照传感器和智能电灯的场景联动(如有需要,你可以根据[这份文档](https://shimo.im/sheets/D3VVPdwcYRhhQRXh/MODOC)自行采购相关硬件)。
|
||||
|
||||
## 网关系统架构
|
||||
|
||||
首先,我们先看一下网关的系统架构。
|
||||
|
||||
网关的主要功能是**协议转换**,一方面它需要接收低功耗蓝牙技术的光照传感器的广播数据,另一方面,它需要把解析的数据上传到云平台。
|
||||
|
||||
具体的架构图如下所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/44/e44a8bfe765e535f320568f57a3cfa44.jpg" alt="19.01">
|
||||
|
||||
## 南向蓝牙通信
|
||||
|
||||
在树莓派上进行蓝牙开发,你可以使用[bluepy](https://github.com/IanHarvey/bluepy)软件包。它提供了一个Python语言版本的低功耗蓝牙API接口,而且对树莓派的适配非常好。
|
||||
|
||||
### 通过终端登录树莓派
|
||||
|
||||
在学习[第15讲](https://time.geekbang.org/column/article/320675)的时候,你应该已经在树莓派上部署好了包含Gladys Assistant系统的Raspbian操作系统,现在你可以直接使用这个系统。安装软件包之前,我们在电脑终端上输入下面的命令,通过SSH协议登录到树莓派系统中。
|
||||
|
||||
```
|
||||
$ ssh pi@gladys.local
|
||||
|
||||
```
|
||||
|
||||
其中,pi就是默认的登录用户名,gladys.local是树莓派开发板的本地域名。
|
||||
|
||||
当提示输入密码时,我们输入默认密码raspberry,然后回车,就登录到了树莓派系统中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/81/9171ef2f8a94c6ee8d1869d571677781.png" alt="19.02">
|
||||
|
||||
### 通过图形化窗口软件登录树莓派
|
||||
|
||||
当然,你也可以使用提供图形化窗口的软件来登录树莓派,比如**SecureCRT**,它除了支持串口协议,同时也支持SSH协议。你只需要新建一个连接会话,按照下图所示的内容填写就行了:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/57/f39028873bb6ce7d99f411881f4a3357.png" alt="19.03">
|
||||
|
||||
第一次登录时,SecureCRT会弹窗提示我们查看“Host Key”,这时点击“Accept Once”即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/59/786bd55b06ea8c11d3171fb57ddec459.png" alt="19.04">
|
||||
|
||||
然后我们输入密码“raspberry”,同时勾选“Save password”,省去以后重复输入密码的麻烦。点击“OK”后,就进入树莓派系统了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/ee/4af7047e2d3yy7a2dfa511628708a1ee.png" alt="19.05">
|
||||
|
||||
### 在树莓派开发蓝牙程序
|
||||
|
||||
我们在树莓派的终端上输入下面命令,就可以完成bluepy的安装:
|
||||
|
||||
```
|
||||
$ sudo apt-get install python3-pip libglib2.0-dev
|
||||
$ sudo pip3 install bluepy
|
||||
|
||||
```
|
||||
|
||||
另外,我们还需要安装interruptingcow软件包。它主要是便于编写定时任务。它的安装命令是:
|
||||
|
||||
```
|
||||
$ sudo pip3 install interruptingcow
|
||||
|
||||
```
|
||||
|
||||
具体代码如下,供参考:
|
||||
|
||||
```
|
||||
#File: blescan.py
|
||||
import time
|
||||
from threading import Thread
|
||||
from interruptingcow import timeout
|
||||
|
||||
from bluepy.btle import DefaultDelegate, Peripheral, Scanner, UUID, capitaliseName, BTLEInternalError
|
||||
from bluepy.btle import BTLEDisconnectError, BTLEManagementError, BTLEGattError
|
||||
|
||||
class LightScanner():
|
||||
SCAN_TIMEOUT = 5
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
def status_update(self):
|
||||
results = self._get_data()
|
||||
|
||||
# messages = [
|
||||
# MqttMessage(
|
||||
# topic=self.format_topic("property/light"),
|
||||
# payload=results.lightlevel,
|
||||
# )
|
||||
# ]
|
||||
|
||||
return results
|
||||
|
||||
def _get_data(self):
|
||||
|
||||
scan_processor = ScanProcessor(self._name)
|
||||
scanner = Scanner().withDelegate(scan_processor)
|
||||
scanner.scan(self.SCAN_TIMEOUT, passive=True)
|
||||
|
||||
with timeout(
|
||||
self.SCAN_TIMEOUT,
|
||||
exception=Exception(
|
||||
"Retrieving data from {} device {} timed out after {} seconds".format(
|
||||
repr(self), self._name, self.SCAN_TIMEOUT
|
||||
)
|
||||
),
|
||||
):
|
||||
while not scan_processor.ready:
|
||||
time.sleep(1)
|
||||
return scan_processor.results
|
||||
|
||||
return scan_processor.results
|
||||
|
||||
class ScanProcessor:
|
||||
|
||||
ADV_TYPE_SERVICE_DATA = 0x16
|
||||
def __init__(self, name):
|
||||
self._ready = False
|
||||
self._name = name
|
||||
self._results = MiBeaconData()
|
||||
|
||||
def handleDiscovery(self, dev, isNewDev, _):
|
||||
is_nodemcu = False
|
||||
if isNewDev:
|
||||
for (adtype, desc, value) in dev.getScanData():
|
||||
#Service Data UUID == 0xFE95 according to MiBeacon
|
||||
if adtype == self.ADV_TYPE_SERVICE_DATA and value.startswith("95fe"):
|
||||
print("FOUND service Data:",adtype, desc, value)
|
||||
#Object ID == 0x1007 according to MiBeacon
|
||||
if len(value) == 38 and value[26:30] == '0710':
|
||||
light_den = int((value[-2:] + value[-4:-2]), 16)
|
||||
mac = value[14:26]
|
||||
|
||||
self._results.lightlevel = light_den
|
||||
self._results.mac = mac
|
||||
|
||||
self.ready = True
|
||||
|
||||
@property
|
||||
def mac(self):
|
||||
return self._mac
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
return self._ready
|
||||
|
||||
@ready.setter
|
||||
def ready(self, var):
|
||||
self._ready = var
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
return self._results
|
||||
|
||||
class MiBeaconData:
|
||||
def __init__(self):
|
||||
self._lightlevel = None
|
||||
self._mac = None
|
||||
|
||||
@property
|
||||
def lightlevel(self):
|
||||
return self._lightlevel
|
||||
|
||||
@lightlevel.setter
|
||||
def lightlevel(self, var):
|
||||
self._lightlevel = var
|
||||
|
||||
@property
|
||||
def mac(self):
|
||||
return self._mac
|
||||
|
||||
@mac.setter
|
||||
def mac(self, var):
|
||||
self._mac = var
|
||||
|
||||
```
|
||||
|
||||
## 北向MQTT对接云平台
|
||||
|
||||
接下来,我们要实现网关和云平台的对接。
|
||||
|
||||
### MQTT开发环境准备
|
||||
|
||||
1. **安装软件包**
|
||||
|
||||
蓝牙网关与云平台交互的通信协议也是使用MQTT协议,所以我们需要安装MQTT的软件包。
|
||||
|
||||
使用哪个软件包呢?在[第8讲](https://time.geekbang.org/column/article/312691)中我介绍过几个常用的MQTT软件包,这里我们选择支持Python语言开发的[Eclipse Paho](http://www.eclipse.org/paho/)软件包。我们在树莓派的终端上输入下面的命令来安装。
|
||||
|
||||
```
|
||||
$ sudo pip3 install paho-mqtt
|
||||
|
||||
```
|
||||
|
||||
安装成功后,我们可以写一个demo程序测试一下。下面是我测试的代码,你可以参考。和第8讲一样,这段代码仍然会连接到 test.mosquitto.org,并且订阅“/geektime/iot”的主题消息。
|
||||
|
||||
```
|
||||
#File: mqttdemo.py
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
print("Connected with result code "+str(rc))
|
||||
|
||||
client.subscribe("/geektime/iot")
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
print(msg.topic+" "+str(msg.payload))
|
||||
|
||||
client = mqtt.Client()
|
||||
client.on_connect = on_connect
|
||||
client.on_message = on_message
|
||||
|
||||
#Still connect to mqtt.eclipse.org
|
||||
client.connect("test.mosquitto.org", 1883, 60)
|
||||
|
||||
client.loop_forever()
|
||||
|
||||
```
|
||||
|
||||
1. **部署文件到树莓派**
|
||||
|
||||
现在,我们把测试文件 mqttdemo.py 上传到树莓派上。
|
||||
|
||||
你可以在电脑终端上,运行下面的命令。(注意,你需要先在树莓派上创建 pi-gateway 这个目录。)
|
||||
|
||||
```
|
||||
$ scp mqttdemo.py pi@gladys.local:/home/pi/pi-gateway/
|
||||
|
||||
```
|
||||
|
||||
其中这个scp命令是基于SSH协议实现的安全文档传输功能。
|
||||
|
||||
当然,你也可能更习惯图形化的软件,所以我再介绍一个能实现scp功能的软件 [FileZilla](https://filezilla-project.org/download.php?type=client)。它支持MacOS、Windows和Linux操作系统,操作界面也非常直观。
|
||||
|
||||
打开“站点管理器”,创建“新站点”。你可以按照下图设置具体配置参数,然后点击“连接”,登录到树莓派系统。为了方便之后的使用,你可以勾选“保存密码”选项。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/90/29db1ea1b71c06b0845b82bbefc72190.png" alt="19.06">
|
||||
|
||||
在软件界面的左半部分是你的电脑上的文件目录,右半部分是树莓派上的目录。你只需要双击左边的某个文件,就可以将文件传输到树莓派上。当然你也可以双击右边树莓派上的文件,将它传输到你的电脑。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/69/aa1b5c0538b4f62f33ce6ed22c77a469.png" alt="19.07">
|
||||
|
||||
把文件传输到树莓派之后,我们就可以在树莓派的终端上输入下面的命令,运行上面的demo程序。
|
||||
|
||||
```
|
||||
$ sudo python3 mqttdemo.py
|
||||
|
||||
```
|
||||
|
||||
这时我们把[第8讲](https://time.geekbang.org/column/article/312691)中的发布消息命令再执行一次,如果一切顺利执行,那么就可以在树莓派的终端上看到这个消息。
|
||||
|
||||
```
|
||||
hbmqtt_pub --url mqtt://test.mosquitto.org:1883 -t /geektime/iot -m Hello,World!
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/b7/16e0b2312275956965643dc825ff17b7.png" alt="19.08">
|
||||
|
||||
### 云平台创建光照传感器设备
|
||||
|
||||
现在,我们已经做好了对接云平台的准备工作。在树莓派上开发与云平台的通信代码之前,我们还需要在腾讯云平台上创建对应的光照传感器设备。
|
||||
|
||||
创建的过程与第17讲智能电灯的过程类似。我快速介绍一下,你重点关注不同的地方就可以了。
|
||||
|
||||
在“新建产品”中,产品类别选择“智慧生活”-->“安防报警”-->“光照度传感器”。数据协议仍然选择“数据模板”,其他的保持默认值即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/37/a15fc960131a83818be3f979044b0037.png" alt="19.09">
|
||||
|
||||
创建成功后,我们点击进入数据模板的设置界面。为了尽量简单,我只定义了一个属性“光照度”,而且是只读类型。你可以直接导入下面的JSON文件完成数据模板的设置。
|
||||
|
||||
```
|
||||
{
|
||||
"version": "1.0",
|
||||
"profile": {
|
||||
"ProductId": "你的ProductID",
|
||||
"CategoryId": "112"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "Illuminance",
|
||||
"name": "光照度",
|
||||
"desc": "光照度检测",
|
||||
"mode": "r",
|
||||
"define": {
|
||||
"type": "float",
|
||||
"min": "0",
|
||||
"max": "6000",
|
||||
"start": "0",
|
||||
"step": "1",
|
||||
"unit": "Lux"
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [],
|
||||
"actions": []
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/77/da/779d9d3f3c5f35a1490bbe91f2f4b7da.png" alt="19.10">
|
||||
|
||||
在“交互开发”标签页中,和智能电灯一样,我们仍然保持“使用官方小程序控制产品”选项是打开状态。另外,还有一个配置项需要关注,那就是“智能联动配置”,因为后面我们要为光照传感器设置联动场景。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/7a/76c20e82ae392b96e52a0467381fbc7a.png" alt="19.11">
|
||||
|
||||
我们点击“配置”,在设置页面中,就可以看到“光照度”这个属性,因为它是只读属性,所以只能作为联动的触发条件。我们勾选“作为条件”的选项,完成配置。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/77/395790a876a9e616dafc107dcf872177.png" alt="19.12">
|
||||
|
||||
下一步,在“设备调试”界面中,我们创建一个测试设备。点击“新建设备”,输入设备名称“Lightsensor_1”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ff/2a/ff73e5da081e581cb8cb1229e08e302a.png" alt="19.13">
|
||||
|
||||
创建成功后,在测试设备列表中,点击“Lightsensor_1”,进入设备的详情页面,我们可以看到设备三元组的信息。你需要将这些信息记录下来,因为后面的开发中需要使用。
|
||||
|
||||
在测试设备列表中,我们点击“二维码”操作,获取测试设备的二维码,以便在小程序“腾讯连连”中添加这个设备。
|
||||
|
||||
到这里,腾讯云平台上的产品创建工作就完成了。
|
||||
|
||||
### 产品联网开发
|
||||
|
||||
在腾讯云平台准备好产品的配置工作之后,我们继续在树莓派上完成北向的通信交互的开发工作。
|
||||
|
||||
在[第17讲](https://time.geekbang.org/column/article/322528)中,我们已经了解了MQTT通信的主题 Topic ,以及 Broker 服务器地址、端口号、设备ID(ClientID)、用户名(UserName)和密码(Password)等连接参数的知识。
|
||||
|
||||
我们还是可以使用**sign.html**这个网页工具生产用户名和密码,然后就能得到所有的参数。这时,把这些参数替换到下面这段代码的对应位置就可以了。
|
||||
|
||||
```
|
||||
#File: gateway.py
|
||||
from blescan import LightScanner, MiBeaconData
|
||||
|
||||
import time
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
import paho.mqtt.client as MQTTClient
|
||||
|
||||
"""
|
||||
QCloud Device Info
|
||||
"""
|
||||
DEVICE_NAME = "Lightsensor_1"
|
||||
PRODUCT_ID = "MAO3SVUCFO"
|
||||
DEVICE_KEY = "TYjuKNc2GpDykXUv4MWBOA=="
|
||||
|
||||
"""
|
||||
MQTT topic
|
||||
"""
|
||||
MQTT_CONTROL_TOPIC = "$thing/down/property/"+PRODUCT_ID+"/"+DEVICE_NAME
|
||||
MQTT_CONTROL_REPLY_TOPIC = "$thing/up/property/"+PRODUCT_ID+"/"+DEVICE_NAME
|
||||
|
||||
def mqtt_callback(client, userdata, msg):
|
||||
# Callback
|
||||
print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic")
|
||||
|
||||
async def mqtt_connect():
|
||||
#connect callback
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
if rc == 0:
|
||||
print("Connected to MQTT Broker!")
|
||||
else:
|
||||
print("Failed to connect, return code %d\n", rc)
|
||||
|
||||
mqtt_client = None
|
||||
MQTT_SERVER = PRODUCT_ID + ".iotcloud.tencentdevices.com"
|
||||
MQTT_PORT = 1883
|
||||
MQTT_CLIENT_ID = PRODUCT_ID+DEVICE_NAME
|
||||
MQTT_USER_NAME = "MAO3SVUCFOLightsensor_1;12010126;2OYA5;1609057368"
|
||||
MQTTT_PASSWORD = "8f79b7f1b0bef9cde7fd9652383b6ff8bfeb8003cc994c64f3c8e069c11fd4c7;hmacsha256"
|
||||
|
||||
mqtt_client = MQTTClient.Client(MQTT_CLIENT_ID)
|
||||
mqtt_client.username_pw_set(MQTT_USER_NAME, MQTTT_PASSWORD)
|
||||
mqtt_client.on_connect = on_connect
|
||||
|
||||
mqtt_client.connect(MQTT_SERVER, MQTT_PORT, 60)
|
||||
|
||||
return mqtt_client
|
||||
|
||||
def mqtt_report(client, light_level):
|
||||
client_token = "clientToken-" + str(uuid.uuid4())
|
||||
|
||||
msg = {
|
||||
"method": "report",
|
||||
"clientToken": client_token,
|
||||
"params": {
|
||||
"Illuminance": light_level
|
||||
}
|
||||
}
|
||||
|
||||
client.publish(MQTT_CONTROL_REPLY_TOPIC, json.dumps(msg))
|
||||
|
||||
async def light_loop(mclient):
|
||||
|
||||
bles = LightScanner('Nodemcu')
|
||||
|
||||
mclient.subscribe(MQTT_CONTROL_TOPIC)
|
||||
mclient.on_message = mqtt_callback
|
||||
|
||||
mclient.loop_start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
data = bles.status_update()
|
||||
except Exception as e:
|
||||
print("BLE SCAN error:", e)
|
||||
continue
|
||||
|
||||
print("Light Level:", data.lightlevel)
|
||||
|
||||
mqtt_report(mclient, data.lightlevel)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
async def main():
|
||||
mqtt_client = None
|
||||
# MQTT connection
|
||||
try:
|
||||
mqtt_client = await asyncio.wait_for(mqtt_connect(), 20)
|
||||
except asyncio.TimeoutError:
|
||||
print("mqtt connected timeout!")
|
||||
|
||||
if mqtt_client is not None:
|
||||
await asyncio.gather(light_loop(mqtt_client))
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
```
|
||||
|
||||
### 在树莓派上部署软件
|
||||
|
||||
接下来,我们把代码文件gateway.py 和 blescan.py 两个文件也上传到树莓派的/home/pi/pi-gateway目录中。
|
||||
|
||||
同时,为了让程序作为后台服务运行,并且能够开机自启动,我们来做一个Pi Gateway Service。
|
||||
|
||||
首先,你需要新建一个service.sh脚本文件,内容如下:
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
set -e
|
||||
SCRIPT_DIR=$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
sudo python3 ./gateway.py "$@"
|
||||
|
||||
```
|
||||
|
||||
然后,创建我们service的配置文件,内容如下:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Pi Gateway
|
||||
Documentation=https://time.geekbang.org/column/intro/100063601
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/pi-gateway
|
||||
ExecStart=/home/pi/pi-gateway/service.sh
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
```
|
||||
|
||||
接着,把这两个文件上传到树莓派系统的/home/pi/pi-gateway目录中,并且运行下面命令,修改文件的属性。
|
||||
|
||||
```
|
||||
$ sudo chmod a+x service.sh
|
||||
$ sudo chmod a+x pi-gateway.service
|
||||
|
||||
```
|
||||
|
||||
最后,执行下面的几条命令,为树莓派系统增添上 Pi Gateway 这个服务。
|
||||
|
||||
```
|
||||
$ sudo cp /home/pi/pi-gateway/pi-gateway.service /etc/systemd/system/
|
||||
$ sudo systemctl daemon-reload
|
||||
$ sudo systemctl start pi-gateway
|
||||
$ sudo systemctl status pi-gateway
|
||||
$ sudo systemctl enable pi-gateway
|
||||
|
||||
```
|
||||
|
||||
到这里,网关程序已经在树莓派上运行起来。我们在腾讯云物联网平台上可以看到,光照传感器变为“在线”状态。
|
||||
|
||||
## 设置场景联动
|
||||
|
||||
在第17讲和第18讲的实战中,我们分别完成了智能电灯和光照传感器的开发,现在终于可以为它们设置场景联动了。
|
||||
|
||||
### 场景联动任务分解
|
||||
|
||||
我们希望实现的联动场景是,基于环境的光照强度自动控制电灯的开和关。具体来说,这个目标可以拆解为3个自动触发任务:
|
||||
|
||||
1. 当光照强度大于1024Lux时,关闭电灯。
|
||||
1. 当光照强度小于1024Lux时,打开电灯。
|
||||
1. 至于光照强度等于1024Lux时,也打开电灯。
|
||||
|
||||
注意,这里的1024Lux是我自己选择的一个值,你可以根据房屋情况自己调整。
|
||||
|
||||
### 联动设备准备
|
||||
|
||||
如果你还没有在小程序中添加光照传感器设备,这时可以打开微信中的腾讯连连小程序,扫描上面云平台“设备调试”中保存的那个二维码,添加光照传感器测试设备“Lightsensor_1”。
|
||||
|
||||
现在你的小程序里面已经有了两个设备,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/a2/6c9d5793b7b42e26151315bfc4865ea2.png" alt="19.14">
|
||||
|
||||
刚才我们已经在腾讯云物联网平台上,为光照传感器设置了“智能联动配置”。现在,我们来为智能电灯配置智能联动能力。
|
||||
|
||||
我们进入智能电灯的“交互开发”页面,打开下面的“智能联动配置”页面,然后,像下图显示的那样,把“电灯开关”的“作为任务”条件勾选上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/2e/0ef30835ec3fa62c4968117c3f81372e.png" alt="19.15">
|
||||
|
||||
### 联动任务创建
|
||||
|
||||
然后,我们进入腾讯连连小程序,点击下面的“+”,选择“添加智能”,开始配置工作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/96/89/965f2898374796898f59bd8ff4774b89.png" alt="19.16">
|
||||
|
||||
我们从弹框里选择“自动智能”,可以看到下图的配置界面:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/9f/4112709af28b0a9a20f340035f85a09f.png" alt="19.17">
|
||||
|
||||
首先,我们添加条件,选择光照传感器设备,然后就会看到光照度属性。我们先设置大于1024Lux的条件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/4e/f23b76ed4df0352295754d09845a384e.png" alt="19.18">
|
||||
|
||||
然后,我们添加任务,选择智能电灯设备后,可以看到电灯开关的属性,选择“关”,点击保存。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/eb/6bd677fe4e4d404ce547d47446c3f4eb.png" alt="19.19">
|
||||
|
||||
这时,我们可以看到这个智能联动的条件和任务已经配置完成。腾讯连连小程序还支持配置“生效时间段”,可以限定智能联动在选定的时间段内运行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/b5/9cdc8bb22865540b5148572a1698d0b5.png" alt="19.20">
|
||||
|
||||
接下来,我们还可以设置一个主题图片和名称,这个根据喜好来就行了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/9d/856b5cc93cf8c90c22be43e5986ca69d.png" alt="">
|
||||
|
||||
按照相同的方法,我们可以设置其他两个条件,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/97/fa1649f112046da7d44f0c5f5ede0c97.png" alt="">
|
||||
|
||||
最终的智能联动,包括了刚才提到的3个不同的触发条件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/9d/e35a3byybe3148926bfe228e200f759d.png" alt="">
|
||||
|
||||
现在,你可以通过控制光照传感器的光照明暗(比如用手遮挡光敏元器件然后再把手拿开),来观察智能电灯的打开和关闭,检验功能是否正常。
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下,在这一讲中,我介绍了利用树莓派打造网关,让光照传感器接入物联网平台的办法,并且带你实现了光照传感器和智能电灯的场景联动。你需要重点关注的内容有:
|
||||
|
||||
1. 为了实现协议转换,树莓派的南向接口,也就是蓝牙功能,你可以基于bluepy软件包开发。这里实现的功能是扫描光照传感器的广播包,并按照MiBeacon蓝牙协议解析出光照强度的数值。
|
||||
1. 北向接口要实现对接云平台的功能,这是基于MQTT协议实现的。你可以基于Eclipse paho的Python语言版本来开发MQTT Client的功能。
|
||||
1. 场景联动一般由条件和任务组成。其中,条件和任务是从我们的设备中定义的智能联动配置中选择的。
|
||||
|
||||
为了避免光照的短暂变化,导致智能电灯的忽明忽暗,我将光照传感器的数据上报间隔设置得比较长。如果你有特殊的需求,可以修改光照传感器和网关程序中的参数来实现。
|
||||
|
||||
下一讲,我将讲解智能音箱的实现,并通过智能音箱控制智能电灯的开关。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我给你留一个动手实践题。
|
||||
|
||||
在这一讲的场景联动中,我们实现了光照强度对电灯打开和关闭的自动控制。你可以通过光照强度的不同数值实现对智能电灯亮度,或者颜色的控制吗?
|
||||
|
||||
你可以动手设置一下,并且在留言区和我分享你的成果,同时,也欢迎你将这一讲分享给你的朋友,大家一起讨论学习。
|
||||
488
极客时间专栏/物联网开发实战/实战篇/20 | 智能语音:好玩的语音控制是怎么实现的?.md
Normal file
488
极客时间专栏/物联网开发实战/实战篇/20 | 智能语音:好玩的语音控制是怎么实现的?.md
Normal file
@@ -0,0 +1,488 @@
|
||||
<audio id="audio" title="20 | 智能语音:好玩的语音控制是怎么实现的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/e6/f58b2668e880a0a66285553da794bfe6.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
实战篇的前几讲,我们打造了联网智能电灯,并实现了跟光照传感器的场景联动。今天我们来玩一个更酷的,智能音箱。
|
||||
|
||||
智能音箱为我们提供了一种更加自然的交互方式,所以亚马逊的Echo产品一经问世,就迅速流行起来。与智能家居结合之后,它更是引起了行业巨头的注意,被认为是很有发展潜力的用户入口和平台级产品。
|
||||
|
||||
我们先不论智能音箱最终到底能不能发展成智能家居的平台级产品,至少这波热潮已经极大地推动了相关技术的发展,而且用户覆盖率也有了很大的提升。
|
||||
|
||||
这一讲我就为你介绍一下智能音箱的语音控制是怎么实现的,并且带你动手完成开发过程(如有需要,你可以根据[这份文档](https://shimo.im/sheets/D3VVPdwcYRhhQRXh/MODOC)自行采购相关硬件)。
|
||||
|
||||
## 智能音箱的技术架构
|
||||
|
||||
智能音箱主要涉及**拾音**、**前端信号处理**、**语音识别**、**自然语言处理**和**语音合成**等技术,现在一些产品甚至提供了声纹识别技术。
|
||||
|
||||
当然,智能音箱最重要的是提供各种功能,完成一些任务,比如控制电灯的开和关,这被称为**技能**。
|
||||
|
||||
整体的技术架构如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2a/f5/2a01c24619120c4c464d975bb0e8e4f5.jpg" alt="">
|
||||
|
||||
接下来,我会逐个讲解这些技术组成。
|
||||
|
||||
### 拾音
|
||||
|
||||
拾音,就是通过**麦克风**获取你的语音。
|
||||
|
||||
我们都用微信发送过语音消息,手机就是通过麦克风来获取你说的话的,这么说起来,拾音好像很简单。但是,智能音箱应对的环境要更复杂,因为用户可能在比较远的地方下达语音指令。
|
||||
|
||||
因此,智能音箱上一般采用**麦克风阵列**(Mic Array),也就是按照一定规则排列的多个麦克风,比如下图展示的就是[Amazon Echo](https://zh.ifixit.com/Teardown/Amazon+Echo+Teardown/33953)由7个麦克风组成的阵列(绿色圆圈部分)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/83/f0/831c5e67ddb3cbdeda86c496317b8df0.jpg" alt="">
|
||||
|
||||
### 前端语音信号处理
|
||||
|
||||
在收集到声音信号后,还需要进行前端语音信号处理。只有经过处理,智能音箱才能获取到相对干净的语音信号,也才能提高后面的语音识别的准确率。
|
||||
|
||||
这些处理技术包括回声消除(Acoustic Echo Cancellaction, AEC)、噪音抑制(Noise Suppression,NS)、语音检测(Voice Activity Detection,VAD)、声源定位(Direction of Arrival estimation,DOA)、波束成型(Beamforming)和混响消除(Speech Dereverberation)等。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/71/aaffc6862eab6a9af9cb27ec6dacd971.jpg" alt="">
|
||||
|
||||
### 语音唤醒
|
||||
|
||||
语音唤醒(Keyword Spotting,KWS),就是通过特定的**唤醒词**来激活智能音箱,以便进行后续的语音交互任务。这样做一方面可以保护用户的隐私,因为只有唤醒后,音箱才收集和识别用户的语音信息,另一方面也可以简化语音的识别和理解,比如小米智能音箱的“小爱同学”就是这样的唤醒词。
|
||||
|
||||
### 语音识别
|
||||
|
||||
语音识别(Automatic Speech Recognition,ASR),主要完成的任务是将语音转换成文本,所以也被称为STT(Speech to Text)。
|
||||
|
||||
### 自然语言理解
|
||||
|
||||
自然语言理解(Natural Language Understanding,NLU),是对语音识别生成的文本进行处理,识别用户的意图,并生产结构化的数据。
|
||||
|
||||
当然,以现在的人工智能发展水平来看,自然语言理解还有很长的路要走。这也是我们常发现智能音箱不够“智能”的原因。
|
||||
|
||||
### 技能
|
||||
|
||||
技能(Skills)一般要借助后端云平台的强大能力,云平台可以提供知识图谱、家居设备远程控制和音乐等音频资源等能力。
|
||||
|
||||
### 自然语言生成
|
||||
|
||||
自然语言生成(Natural Language Generation,NLG),就是将各种技能的响应结果组织成文本语言。比如当你询问天气时,根据获取的天气状况和温度等信息生成“北京今天晴,最高温度5°,最低温度-6°”这样的语句。自然语言生成和自然语言理解都属于**自然语言处理**(Natural Language Processing,NLP)的范畴。
|
||||
|
||||
### 语音合成
|
||||
|
||||
语音合成(Speech Synthesis),就是将自然语言生成的文本转换为语音的形式,提供给智能音箱播放出来,给人的感觉就像和音箱在对话。因此,这个过程也叫做TTS(Text to Speech)。
|
||||
|
||||
## 智能音箱的开发
|
||||
|
||||
了解完智能音箱的基本技术构成,下面我们就基于树莓派开发一个自己的简易智能音箱吧。
|
||||
|
||||
首先,我需要说明一下树莓派的系统。为什么呢?因为在[第15讲](https://time.geekbang.org/column/article/320675)中,我们安装了Gladys Assistant系统镜像,而这个系统Raspbian是基于Debian buster版本的,一些语音识别开源库对于buster的支持并不够好。
|
||||
|
||||
所以,如果你的树莓派是Raspberry Pi 3系列,强烈建议你把系统镜像切换成**Debian stretch**版本。通过[这个链接](https://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/)就可以下载基于Debian stretch版本的Raspbian镜像文件压缩包,安装还是使用Etcher工具,你可以回头看一下第15讲的介绍。
|
||||
|
||||
至于树莓派Raspberry Pi 4系列,因为官方系统Raspbian只有buster版本支持,所以我们还是继续基于第15讲的系统开发。
|
||||
|
||||
### 麦克风阵列
|
||||
|
||||
麦克风阵列我使用的是**ReSpeaker 2-Mics Pi HAT**,它的2个麦克风分布在模组的两边。我们现在来配置一下,让它可以在树莓派上正常工作。
|
||||
|
||||
你可以通过下面的命令安装它的驱动程序。首先,你最好切换一下树莓派的软件安装源,将它切换到国内的腾讯云安装源,这样下载安装的速度比较快。运行下面的命令修改配置文件:
|
||||
|
||||
```
|
||||
$ sudo vim /etc/apt/sources.list
|
||||
|
||||
```
|
||||
|
||||
将文件修改为下面的内容:
|
||||
|
||||
```
|
||||
deb https://mirrors.cloud.tencent.com/raspbian/raspbian/ buster main contrib non-free rpi
|
||||
# Uncomment line below then 'apt-get update' to enable 'apt-get source'
|
||||
deb-src https://mirrors.cloud.tencent.com/raspbian/raspbian/ buster main contrib non-free rpi
|
||||
|
||||
```
|
||||
|
||||
修改另一个软件安装源的配置文件,命令如下所示:
|
||||
|
||||
```
|
||||
$ sudo vim /etc/apt/sources.list.d/raspi.list
|
||||
|
||||
```
|
||||
|
||||
修改后的文件内容如下:
|
||||
|
||||
```
|
||||
deb https://mirrors.cloud.tencent.com/raspberrypi/ buster main
|
||||
# Uncomment line below then 'apt-get update' to enable 'apt-get source'
|
||||
deb-src https://mirrors.cloud.tencent.com/raspberrypi/ buster main
|
||||
|
||||
```
|
||||
|
||||
然后,你需要运行下面的命令更新安装源:
|
||||
|
||||
```
|
||||
$ sudo apt-get clean all
|
||||
$ sudo apt-get update
|
||||
|
||||
```
|
||||
|
||||
现在,你可以运行下面命令安装麦克风阵列的驱动程序。因为这个驱动依赖的wm8960 编解码器没有包含在树莓派系统的内核里面,需要重新加载内核,编译驱动,所以整个过程比较久。在等待的过程中,你可以先阅读这一讲的其他部分。
|
||||
|
||||
```
|
||||
$ sudo apt-get install git
|
||||
$ git clone --depth=1 https://github.com/respeaker/seeed-voicecard
|
||||
$ cd seeed-voicecard
|
||||
$ sudo ./install.sh
|
||||
$ sudo reboot
|
||||
|
||||
```
|
||||
|
||||
树莓派重启之后,你可以在树莓派终端输入下面的命令,查看音频的输入和输出设备是否正常工作。
|
||||
|
||||
```
|
||||
$ arecord -l
|
||||
$ aplay -l
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/54/ef1645e03ef2ba2e2c78edf6a9804e54.png" alt="">
|
||||
|
||||
如果一切正常,我们就可以测试录音和播放功能了。在ReSpeaker 2-Mics Pi HAT的耳机插口上插入耳机或者扬声器,运行下面的命令,并说几句话。
|
||||
|
||||
```
|
||||
$ arecord -d 5 test.wav
|
||||
$ aplay test.wav
|
||||
|
||||
```
|
||||
|
||||
另外,你也可以通过软件**AlsaMixer**(命令alsamixer)来配置声音设置和调整音量,左、右箭头键用于选择通道或设备,向上、向下箭头控制当前所选设备的音量。退出程序使用ALT + Q,或者按Esc键。
|
||||
|
||||
为了简化开发,也考虑到麦克风硬件的限制,我们这里就先不关注前端语音信号处理的相关开发了。接下来,我们直接来到实现语音唤醒的环节。
|
||||
|
||||
### 语音唤醒
|
||||
|
||||
为了实现语音唤醒,我们需要选择一个轻量级的、可以在树莓派上运行的唤醒词监测器软件。
|
||||
|
||||
你可能首先想到的是**Snowboy**,没错,它确实是一个非常流行的工具。不过,Snowboy团队在2020年初的时候宣布,2020年12月31日会停止提供服务,所以我们只能寻找替代方案。
|
||||
|
||||
我选择的是[Mycroft Precise](https://github.com/MycroftAI/mycroft-precise),它是一个基于RNN神经网络的语音唤醒工具。
|
||||
|
||||
接下来,我们在树莓派安装Mycroft Precise。因为需要训练唤醒词模型,我们需要基于源代码来编译、安装。
|
||||
|
||||
首先,我们通过git命令把Mycroft Precise的源代码下载到树莓派的/home/pi目录:
|
||||
|
||||
```
|
||||
$ cd ~
|
||||
$ git clone https://github.com/mycroftai/mycroft-precise
|
||||
$ cd mycroft-precise
|
||||
|
||||
```
|
||||
|
||||
在安装之前,把pypi的安装源修改到清华数据源,可以获得更快的下载速度。我们打开目录中的setup.sh文件:
|
||||
|
||||
```
|
||||
$ vim setup.sh
|
||||
|
||||
```
|
||||
|
||||
将文件中的这行内容:
|
||||
|
||||
```
|
||||
extra-index-url=https://www.piwheels.org/simple
|
||||
|
||||
```
|
||||
|
||||
替换成下面的内容:
|
||||
|
||||
```
|
||||
index-url=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
extra-index-url=https://www.piwheels.org/simple
|
||||
|
||||
```
|
||||
|
||||
然后,我们运行它自带的安装脚本,开始编译和安装。中间如果执行中断,可以重新执行这个命令,继续安装过程。(提示:有些ARM平台的库只有piwheels上有,所以这些库安装时速度还是很慢。这种情况下,可以电脑上使用下载工具获取这个模块的安装文件,然后上传到树莓派上,手动安装。)
|
||||
|
||||
```
|
||||
$ ./setup.sh
|
||||
|
||||
```
|
||||
|
||||
安装完成后,我们开始使用Mycroft Precise来训练一个唤醒词模型,唤醒词可以根据喜好来选择,比如“极客时间”。
|
||||
|
||||
我们需要先激活Python的虚拟环境,因为Mycroft Precise在安装过程中创建了这个虚拟环境。
|
||||
|
||||
```
|
||||
$ source .venv/bin/activate
|
||||
|
||||
```
|
||||
|
||||
接下来,我们通过工具precise-collect来收集语音模型训练的声音素材,运行后,根据提示录制12段声音。
|
||||
|
||||
```
|
||||
$ precise-collect
|
||||
Audio name (Ex. recording-##): geektime.##
|
||||
|
||||
Press space to record (esc to exit)...
|
||||
Recording...
|
||||
Saved as geektime-00.wav
|
||||
Press space to record (esc to exit)...
|
||||
|
||||
```
|
||||
|
||||
然后,我们需要将这些声音随机分为两份,一份是训练样本,包括8个声音文件,另一份是测试样本,包括4个声音文件,并且把这两份样本分别放到geektime/wake-word/和/geektime/test/wake-word/这两个目录下面。
|
||||
|
||||
接着,我们执行下面的命令,生成神经网络模型geektime.net:
|
||||
|
||||
```
|
||||
$ precise-train -e 60 geektime.net geektime/
|
||||
|
||||
```
|
||||
|
||||
最后,我们还需要将geektime.net的模型格式做一下转换,将它从Keras模型格式改为TensorFlow模型格式,因为TensorFlow模型更加通用。
|
||||
|
||||
```
|
||||
$ precise-convert geektime.net
|
||||
|
||||
```
|
||||
|
||||
执行完成之后,我们会得到两个文件:
|
||||
|
||||
1. geektime.pb,TensorFlow模型文件
|
||||
1. geektime.pb.params,包含Mycroft Precise在处理音频时需要的一些参数信息。
|
||||
|
||||
当然,为了提高模型的准确性,我们还可以使用precise-train-incremental工具来增加负样本,重新训练刚才的模型。如果环境复杂的话,你可以尝试一下。
|
||||
|
||||
然后,我们可以运行一段代码来测试这个唤醒词模型。不过,因为portaudio这个库在树莓派上运行有问题,我们需要先修复一下portaudio库。你可以运行下面的命令:
|
||||
|
||||
```
|
||||
$ sudo apt-get remove libportaudio2
|
||||
$ sudo apt-get install libasound2-dev
|
||||
$ git clone -b alsapatch https://github.com/gglockner/portaudio
|
||||
$ cd portaudio
|
||||
$ ./configure && make
|
||||
$ sudo make install
|
||||
$ sudo ldconfig
|
||||
|
||||
```
|
||||
|
||||
测试程序的代码如下:
|
||||
|
||||
```
|
||||
# File:kwsdemo.py
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from precise_runner import PreciseEngine, PreciseRunner
|
||||
|
||||
engine = PreciseEngine('precise-engine/precise-engine', 'geektime.pb')
|
||||
runner = PreciseRunner(engine, on_activation=lambda: print('hello'))
|
||||
runner.start()
|
||||
|
||||
# Sleep forever
|
||||
from time import sleep
|
||||
while True:
|
||||
sleep(10)
|
||||
|
||||
```
|
||||
|
||||
现在,我们把kwsdemo.py文件,还有两个geektime.pb模型相关的文件,都上传到树莓派的Mycroft Precise目录下,然后运行kwsdemo.py文件,说出“极客时间”几个字,就会看到终端显示出“hello”这个单词。
|
||||
|
||||
### 语音识别
|
||||
|
||||
对于语音识别,我们直接采用腾讯云提供的语音识别SDK来完成(你需要提前在腾讯云控制台开通这个服务)。它会将语音发送到云端,由云端服务器计算出文本信息。你可以通过下面命令来安装:
|
||||
|
||||
```
|
||||
$ pip3 install tencentcloud-sdk-python
|
||||
|
||||
```
|
||||
|
||||
在开始使用之前,你需要访问[这个链接](https://console.cloud.tencent.com/cam/capi)创建一个密钥,然后记录下SecretId和SecretKey的信息。
|
||||
|
||||
你可以参考下面的代码,来完成一个录音文件的识别。
|
||||
|
||||
```
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
from tencentcloud.asr.v20190614 import asr_client, models
|
||||
import base64
|
||||
import io
|
||||
import sys
|
||||
|
||||
SECRET_ID = "你的Secret ID"
|
||||
SECRET_KEY = "你的Secret Key"
|
||||
|
||||
try:
|
||||
cred = credential.Credential(SECRET_ID, SECRET_KEY)
|
||||
httpProfile = HttpProfile()
|
||||
httpProfile.endpoint = "asr.tencentcloudapi.com"
|
||||
clientProfile = ClientProfile()
|
||||
clientProfile.httpProfile = httpProfile
|
||||
clientProfile.signMethod = "TC3-HMAC-SHA256"
|
||||
client = asr_client.AsrClient(cred, "ap-beijing", clientProfile)
|
||||
#读取文件以及 base64
|
||||
with open('./geektime-00.wav', "rb") as f:
|
||||
if sys.version_info[0] == 2:
|
||||
content = base64.b64encode(f.read())
|
||||
else:
|
||||
content = base64.b64encode(f.read()).decode('utf-8')
|
||||
f.close()
|
||||
#发送请求
|
||||
req = models.SentenceRecognitionRequest()
|
||||
params = {"ProjectId":0,"SubServiceType":2,"SourceType":1,"UsrAudioKey":"sessionid-geektime"}
|
||||
req._deserialize(params)
|
||||
req.DataLen = len(content)
|
||||
req.Data = content
|
||||
req.EngSerViceType = "16k_zh"
|
||||
req.VoiceFormat = "wav"
|
||||
resp = client.SentenceRecognition(req)
|
||||
print(resp.to_json_string())
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
|
||||
```
|
||||
|
||||
### 语音合成
|
||||
|
||||
接下来,我来介绍一下语音合成。
|
||||
|
||||
你可能会问,刚才介绍技术架构的时候,不是还讲了自然语言理解、技能和自然语言生成吗?这里怎么跳过去了呢?
|
||||
|
||||
首先,因为我们的任务很简单,只需要查询语音识别的文本中是否有“开”、“灯”,和“关”、“灯”就可以完成判断,所以自然语言理解直接判断字符串是否匹配即可。
|
||||
|
||||
其次,我们要实现控制智能电灯,这个技能我在后面会介绍。
|
||||
|
||||
最后,智能音箱只需要反馈执行开关灯的结果就可以,比如“我已经把灯打开了”或者“我已经把灯关了”,自然语言生成的部分按照固定的文本就可以了,不需要考虑动态生成的问题。
|
||||
|
||||
语音合成,就是我们希望把类似“我已经把灯关了”这样的文本信息,转换为音频,便于智能音箱播放出来。你可以基于离线的TTS引擎来实现,比如[HanTTS](https://github.com/junzew/HanTTS)这个项目。
|
||||
|
||||
当然,我们也可以使用腾讯云的语音合成服务(你需要提前在腾讯云控制台开通这个服务)。你可以参考下面的代码:
|
||||
|
||||
```
|
||||
import json
|
||||
import base64
|
||||
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
from tencentcloud.tts.v20190823 import tts_client, models
|
||||
|
||||
SECRET_ID = "你的Secret ID"
|
||||
SECRET_KEY = "你的Secret Key"
|
||||
|
||||
try:
|
||||
cred = credential.Credential(SECRET_ID, SECRET_KEY)
|
||||
httpProfile = HttpProfile()
|
||||
httpProfile.endpoint = "tts.tencentcloudapi.com"
|
||||
|
||||
clientProfile = ClientProfile()
|
||||
clientProfile.httpProfile = httpProfile
|
||||
client = tts_client.TtsClient(cred, "ap-beijing", clientProfile)
|
||||
|
||||
req = models.TextToVoiceRequest()
|
||||
params = {
|
||||
"Text": "我已经把灯关了",
|
||||
"SessionId": "sessionid-geektime",
|
||||
"ModelType": 1,
|
||||
"ProjectId": 0,
|
||||
"VoiceType": 1002
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.TextToVoice(req)
|
||||
print(resp.to_json_string())
|
||||
|
||||
if resp.Audio is not None:
|
||||
audio = resp.Audio
|
||||
data = base64.b64decode(audio)
|
||||
wav_file = open("temp.wav", "wb")
|
||||
wav_file.write(data)
|
||||
wav_file.close()
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
|
||||
```
|
||||
|
||||
## 通过智能音箱控制电灯
|
||||
|
||||
为了实现控制智能电灯的目的,我们需要借助物联网平台提供的开发接口。
|
||||
|
||||
首先,我们进入物联网开发平台,选择“智能家居”项目。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/Uq30gnPmDDQlw0D0.png!thumbnail" alt="">
|
||||
|
||||
然后,点击左侧的“应用开发”,进入新建应用的界面,点击“新建应用”。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/JvPjDFjIhvaeLXz6.png!thumbnail" alt="">
|
||||
|
||||
完成后,点击应用列表里面的应用名称,进入应用的详情页面。你可以看到应用的SecretId和SecretKey信息。这里,你需要将下面“关联产品”中的智能电灯勾选上。只有建立关联,应用才可以控制这个设备。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/dUahCzDFYnZG9r9a.png!thumbnail" alt="">
|
||||
|
||||
具体代码可以参考腾讯提供的开源实现,包括[iOS](https://github.com/tencentyun/iot-link-ios/tree/master/Source/LinkApp)、[Android](https://github.com/tencentyun/iot-link-android/tree/master/app)和[小程序](https://github.com/tencentyun/qcloud-iotexplorer-appdev-miniprogram-sdk-demo)。
|
||||
|
||||
不过,这种方式需要用户账号的登录认证,在树莓派上不太方便。还有一个方式就是基于物联网开发平台提供的通用API接口。其中的“[设备远程控制](https://cloud.tencent.com/document/product/1081/34973)”接口可以满足我们的需求。
|
||||
|
||||
具体的控制方法,你可以参考下面的代码(注意,目前只支持ap-guangzhou区域)。
|
||||
|
||||
```
|
||||
import json
|
||||
from led2.main import PRODUCT_ID
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
from tencentcloud.iotexplorer.v20190423 import iotexplorer_client, models
|
||||
|
||||
SECRET_ID = "你的Secret ID"
|
||||
SECRET_KEY = "你的Secret Key"
|
||||
PRODUCT_ID = "你的ProductID"
|
||||
|
||||
def Light_control(state):
|
||||
try:
|
||||
cred = credential.Credential(SECRET_ID, SECRET_KEY)
|
||||
httpProfile = HttpProfile()
|
||||
httpProfile.endpoint = "iotexplorer.tencentcloudapi.com"
|
||||
|
||||
clientProfile = ClientProfile()
|
||||
clientProfile.httpProfile = httpProfile
|
||||
client = iotexplorer_client.IotexplorerClient(cred, "ap-guangzhou", clientProfile)
|
||||
|
||||
req = models.ControlDeviceDataRequest()
|
||||
data = {
|
||||
"power_switch": state
|
||||
}
|
||||
data_str = json.dumps(data)
|
||||
|
||||
params = {
|
||||
"DeviceName": "Led_1",
|
||||
"ProductId": PRODUCT_ID,
|
||||
"Data": data_str
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.ControlDeviceData(req)
|
||||
print(resp.to_json_string())
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
print(err)
|
||||
|
||||
Light_control(0)
|
||||
|
||||
```
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下,在这一讲中,我介绍了智能音箱的技术架构,以及在树莓派上用于实现智能音箱的一些可选的技术方案,并且带你实现了语音控制智能电灯的目的。你需要重点关注的知识有:
|
||||
|
||||
1. 智能音箱的实现,需要前端音箱本体和后端云平台上一系列技术的支持。这些技术有前端的拾音、语音信号处理、语音唤醒和播音,以及后端的语音识别、自然语言理解、技能、自然语言生成和语音合成。
|
||||
1. 在树莓派的实现上,拾音可以选择使用麦克风阵列,因为基于麦克风阵列可以更好地实现前端语音信号处理,比如声源定位和波束成型等。
|
||||
1. 语言唤醒需要在智能音箱本体上实现,所以需要一些轻量级的识别引擎和训练好的唤醒词模型。之前比较流行的Snowboy将要停止服务,这里我选择了Mycroft Precise这个开源方案。
|
||||
1. 语音识别、自然语言理解、技能、自然语言生成和语音合成等任务适合基于云平台的能力来实现,因为云平台的计算能力更强,有更好的性能和准确度。
|
||||
|
||||
智能音箱的技术也一直在发展,比如现在越来越多的智能音箱开始配备屏幕和摄像头,这为智能音箱引入了声音、UI和视觉等多模态的交互方式,相应地,这也给声纹识别、人脸识别和动作识别等技术带来了新的应用场景。我相信智能音箱未来的产品形态和功能还会不断地进化和发展。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我给你留一个思考题吧。
|
||||
|
||||
在这一讲中,我们是通过物联网平台提供的API接口来控制智能电灯的。除了这种方式,你还能想到其他的方法来远程控制智能电灯吗?你能实现一个虚拟的联网开关,基于场景联动来控制智能电灯的开和关吗?
|
||||
|
||||
欢迎你在留言区写下你思考的结果,也欢迎你将这一讲分享给你的朋友,大家一起交流学习。
|
||||
308
极客时间专栏/物联网开发实战/实战篇/21 | 多传感器集成:浇花怎么实现自动化?.md
Normal file
308
极客时间专栏/物联网开发实战/实战篇/21 | 多传感器集成:浇花怎么实现自动化?.md
Normal file
@@ -0,0 +1,308 @@
|
||||
<audio id="audio" title="21 | 多传感器集成:浇花怎么实现自动化?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dc/8c/dc98300cf54064eabd081dc86325368c.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
在前几讲的实战中,我们已经完整地实现了一个智能家居的照明场景。你不但可以用手机小程序控制智能电灯的开关、亮度和颜色,也能让智能电灯根据光照强度自动地打开和关闭,甚至你还可以语音来操控它。
|
||||
|
||||
在这个过程中,你应该已经掌握了基于硬件开发板快速地开发智能硬件的方法。这一讲中,我们就围绕**自动浇花**,做一个新的智能家居场景解决方案,同时也巩固一下你在前几讲学习到的硬件开发知识(如有需要,你可以根据[这份文档](https://shimo.im/sheets/D3VVPdwcYRhhQRXh/MODOC)自行采购相关硬件)。
|
||||
|
||||
## 研究场景需求
|
||||
|
||||
在设计物联网产品的时候,我们需要先研究场景需求,明确监控指标,再根据这些指标安排合适的传感器。
|
||||
|
||||
比如自动浇花这个场景,很明显是为了自动控制水泵,及时给植物补充**水分**。毕竟水不仅是植物体的主要成分,支撑着细胞、维持着植物形态,而且也是传输营养物质的重要载体。所以,我们要监控的最重要的指标就是**土壤湿度**。
|
||||
|
||||
不过,用户的目的并不只是浇水而已,他们真正想要的,是看到自己养的植物健康地生长,所以我们最好把其他相关的数据也提供给他们。现在很多智能体重秤都会同时检测你的体脂率等其他反映身体健康的指标,也是一样的道理。
|
||||
|
||||
那么,植物生长还需要什么条件呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/12/38/125935bdc5a8f747884871d6f7fd5c38.jpg" alt="">
|
||||
|
||||
首先是**环境温湿度条件**,热带的植物无法忍受低温和干燥的环境条件,而温带植物遇到高温,也可能出现热衰竭的现象,所以环境的**温度**和**湿度**最好也能监控。
|
||||
|
||||
其次是**光照条件**,毕竟要植物进行光合作用嘛,所以**光照强度**需要监控。
|
||||
|
||||
最后,**空气质量**对植物的生长来说也很重要,比如**二氧化碳**是否充足,**有害气体**是否超标等。
|
||||
|
||||
不过考虑到家居环境,空气质量在日常种植过程中一般没有问题,不需要特别关注。当然,你也可以作为扩展任务来尝试一下。
|
||||
|
||||
所以综合来看,我们需要监控的指标包括土壤湿度、环境温湿度和光照强度。相比于市面上其他只能控制浇水的产品,我们的产品考虑的场景就更加全面了。这也是自己动手(DIY)的好处。
|
||||
|
||||
## 硬件电路
|
||||
|
||||
明确了监控指标之后,相应的传感器也就可以确定了,分别是土壤湿度传感器、环境温湿度传感器和光照传感器。
|
||||
|
||||
下面,我总结一下自动浇花器需要用到的材料:
|
||||
|
||||
1. NodeMCU ESP32 开发板。不过,这里我们可以使用Wi-Fi来连接网络,而不是第18讲中用到的蓝牙技术。因为在现实中,考虑到自动浇花器的工作环境,连接电源还是比较常见的。
|
||||
1. 继电器,用于控制水泵的供电电路的通断。
|
||||
1. 水泵,用于从水箱中抽水,并送到花盆中。
|
||||
1. 电池盒,用于给水泵供电。
|
||||
1. 土壤湿度传感器,它可以测量花盆土壤的湿度,然后输出模拟信号。
|
||||
1. 环境温湿度传感器,它基于DHT11传感器,用于测量房屋中的温度和湿度,并且输出数字信号。它采用单总线(1-wire)接口与NodeMCU连接。
|
||||
1. 光照传感器,它可以测量花盆位置接收到的光照条件。每种植物有喜阳光、耐阴等不同的特性,光照度和一段时间的光照累积量可以给我们提供参考,以便更好地满足植物的光照需求。
|
||||
1. 面包板和杜邦线。
|
||||
|
||||
这里,我给出了一个电路图,方便你了解整个自动浇花器的设备组成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/2d/276ff8040178c4f6ef7bdc347c27102d.png" alt="">
|
||||
|
||||
## 软件实现
|
||||
|
||||
接下来,我们还是使用Python语言来完成软件的开发工作。
|
||||
|
||||
### 继电器
|
||||
|
||||
首先是继电器的控制。它和[第17讲](https://time.geekbang.org/column/article/322528)介绍的智能电灯的继电器控制方法没有本质区别,主要是改变了连接的GPIO管脚。根据硬件电路的连线,这里我们使用的是GPIO23管脚。
|
||||
|
||||
我把代码直接贴在文稿中,供你参考(它连接的管脚是**GPIO23**):
|
||||
|
||||
```
|
||||
## !!!本文件采用商城的继电器模块FL-3FF-S-Z
|
||||
## !!!on(), off()状态相反。
|
||||
## !!!初始化中需要调用on()先关闭水泵电路
|
||||
from machine import Pin
|
||||
|
||||
class Relay():
|
||||
ON = 0
|
||||
OFF = 1
|
||||
|
||||
def __init__(self, pin):
|
||||
self.relaypin = Pin(pin, Pin.OUT)
|
||||
self.relaypin.on()
|
||||
self.last_status = self.OFF
|
||||
|
||||
def set_state(self, state):
|
||||
tmp_state = self.ON if state==1 else self.OFF
|
||||
self.relaypin.value(tmp_state)
|
||||
self.last_status = tmp_state
|
||||
|
||||
def state(self):
|
||||
return self.last_status
|
||||
def on(self):
|
||||
self.relaypin.value(self.ON)
|
||||
self.last_status = self.ON
|
||||
|
||||
def off(self):
|
||||
self.relaypin.value(self.OFF)
|
||||
self.last_status = self.OFF
|
||||
|
||||
```
|
||||
|
||||
### 土壤湿度传感器
|
||||
|
||||
然后是土壤湿度传感器,我采用的是基于测量电阻值的变化来判断土壤水分含量的传感器模块。它会根据不同的电阻值,输出变化的模拟信息,数值越小,说明越干燥。使用的时候,你需要把它完全插入花盆的土壤中。
|
||||
|
||||
另外,还有一种基于土壤湿度变化,引起电容值变化的原理设计的[土壤湿度传感器](https://wiki.dfrobot.com/Capacitive_Soil_Moisture_Sensor_SKU_SEN0193),你也可以考虑使用。它的好处是长期使用过程中不易腐蚀,因为没有裸露金属。
|
||||
|
||||
我把代码直接贴在文稿中,供你参考(它连接的管脚是**GPIO34**):
|
||||
|
||||
```
|
||||
from machine import ADC
|
||||
from machine import Pin
|
||||
|
||||
class SoilSensor():
|
||||
|
||||
def __init__(self, pin):
|
||||
self.sensor = ADC(Pin(pin))
|
||||
|
||||
def value(self):
|
||||
value = self.sensor.read()
|
||||
print("Sensor ADC value:",value)
|
||||
return int(value*100/4095)
|
||||
|
||||
```
|
||||
|
||||
### 环境温湿度传感器
|
||||
|
||||
我刚才介绍过,环境温湿度传感器是基于DHT11实现的,并且使用的是单总线的连接方式。不过代码的开发过程并不复杂,因为MicroPython已经为ESP32(也包括ESP8266)实现了DHT代码组件。我们可以直接使用。
|
||||
|
||||
注意,DHT11的单总线协议是奥松电子的[自定义协议](https://cdn-shop.adafruit.com/datasheets/DHT11-chinese.pdf),它与Dallas半导体公司的OneWire协议是不同的。
|
||||
|
||||
代码我贴在下面,供你参考(它连接的管脚是**GPIO14**):
|
||||
|
||||
```
|
||||
import dht
|
||||
from machine import Pin
|
||||
|
||||
class EnvSensor():
|
||||
|
||||
def __init__(self, pin):
|
||||
self.sensor = dht.DHT11(Pin(pin))
|
||||
|
||||
def value(self):
|
||||
self.sensor.measure()
|
||||
return (self.sensor.temperature(), self.sensor.humidity())
|
||||
|
||||
```
|
||||
|
||||
### 光照传感器
|
||||
|
||||
至于光照传感器,我们还可以继续使用[第18讲](https://time.geekbang.org/column/article/323428)的模组,所以我就不过多介绍了。
|
||||
|
||||
我直接贴出代码,供你参考(它连接的管脚是**GPIO36**):
|
||||
|
||||
```
|
||||
from machine import ADC
|
||||
from machine import Pin
|
||||
|
||||
class IllumSensor():
|
||||
|
||||
def __init__(self, pin):
|
||||
self.sensor = ADC(Pin(pin))
|
||||
|
||||
def value(self):
|
||||
value = self.sensor.read()
|
||||
print("Sensor ADC value:",value)
|
||||
return int(value/4095*600
|
||||
|
||||
```
|
||||
|
||||
## 完成联网开发
|
||||
|
||||
为了实现自动浇花器的联网控制,我们仍然需要将它接入腾讯云物联网平台。不过,我希望你能尝试独立完成这个工作。完成之后,你一定会感受到自己的成长。如果实现过程中有困难,可以回头参考第17讲的内容。
|
||||
|
||||
这里,我提供一下自动浇花器的物模型JSON文件,供你参考。
|
||||
|
||||
```
|
||||
{
|
||||
"version": "1.0",
|
||||
"profile": {
|
||||
"ProductId": "你的ProductID",
|
||||
"CategoryId": "909"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "power_switch",
|
||||
"name": "水泵开关",
|
||||
"desc": "控制水泵启动关闭",
|
||||
"mode": "rw",
|
||||
"define": {
|
||||
"type": "bool",
|
||||
"mapping": {
|
||||
"0": "关",
|
||||
"1": "开"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "water_shortage",
|
||||
"name": "缺水状态",
|
||||
"desc": "水箱是否缺水",
|
||||
"mode": "r",
|
||||
"define": {
|
||||
"type": "bool",
|
||||
"mapping": {
|
||||
"0": "否",
|
||||
"1": "是"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "humidity",
|
||||
"name": "土壤湿度",
|
||||
"desc": "当前的土壤湿度",
|
||||
"mode": "r",
|
||||
"define": {
|
||||
"type": "int",
|
||||
"min": "0",
|
||||
"max": "100",
|
||||
"start": "0",
|
||||
"step": "1",
|
||||
"unit": "%"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "env_temp",
|
||||
"name": "环境温度",
|
||||
"desc": "空间环境的温度",
|
||||
"mode": "r",
|
||||
"define": {
|
||||
"type": "float",
|
||||
"min": "-40",
|
||||
"max": "100",
|
||||
"start": "0",
|
||||
"step": "0.1",
|
||||
"unit": "℃"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "env_hum",
|
||||
"name": "环境湿度",
|
||||
"desc": "周围环境的湿度",
|
||||
"mode": "r",
|
||||
"define": {
|
||||
"type": "int",
|
||||
"min": "0",
|
||||
"max": "100",
|
||||
"start": "0",
|
||||
"step": "1",
|
||||
"unit": "%"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"id": "env_illum",
|
||||
"name": "环境光照度",
|
||||
"desc": "周围环境的光照度",
|
||||
"mode": "r",
|
||||
"define": {
|
||||
"type": "int",
|
||||
"min": "0",
|
||||
"max": "6000",
|
||||
"start": "0",
|
||||
"step": "1",
|
||||
"unit": "lux"
|
||||
},
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"events": [],
|
||||
"actions": []
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
需要说明的一点是,其实DHT11的测温范围只有0~50℃,但这里设定的环境温度范围是-40℃~80℃。之所以这么设定,是因为另外一款温湿度传感器DHT22可以达到这个范围,这样当你想根据不同的环境条件灵活地调整温湿度传感器模块的时候,就不需要修改物模型了。
|
||||
|
||||
## 多传感器融合
|
||||
|
||||
在刚才的开发工作中,我们只使用了一个土壤湿度传感器来判断土壤的水分含量。先不考虑传感器本身的测量误差,单纯只测量土壤中一个位置的湿度数值,其实是不能准确反映整片土壤的水分情况的。
|
||||
|
||||
所以,我们可以使用多个土壤传感器进行测量,然后根据多个测量值来计算出更可靠的土壤水分含量。
|
||||
|
||||
这个计算过程就是**多传感器融合**(Multi-Sensor Fusion,MSF)。
|
||||
|
||||
它不仅在自动驾驶这样的前沿领域中成为了保证决策正确的关键技术,而且在日常生活中也已经有了广泛的应用,比如手机上的地理位置定位。
|
||||
|
||||
我们知道,GPS是通过卫星进行定位的,但是它的精度受到很多因素的影响。为了提高定位的精度,现在我们的手机上就普遍采用多传感器融合的方法,通过 GPS、蜂窝通信网基站和 Wi-Fi 热点的数据来综合计算,得到更准确的地理定位信息。
|
||||
|
||||
多传感器融合最关键的地方,不是多个传感器的**硬件连接**或**数据收集**,而是**融合算法**,也就是怎么进行多个不同维度的参数的处理,从而得到一个相对准确的、有意义的数据信息。
|
||||
|
||||
最基本的算法就是计算多个数值的加权平均值。适应性更广的算法有卡尔曼滤波和多贝叶斯估计等方法。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/c1/8170f7fa73182f618b47afaecbdb35c1.jpg" alt="">
|
||||
|
||||
在条件允许的情况下,你可以尝试一下这个方法。毕竟NodeMCU丰富的ADC接口为我们提供了不错的实验条件。比如,你可以基于两个土壤湿度传感器,使用加权平均值算法优化自己的自动浇花器。
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下,这一讲中,我介绍了自动浇花器的硬件电路参考设计和代码。你需要关注的主要内容有:
|
||||
|
||||
1. 自动浇花器主要是通过检测土壤湿度来判断水分含量,然后基于水分含量来控制水泵电源通断来实现自动浇花的目的。
|
||||
1. 环境温湿度传感器采用的是DHT11模组,它采用单总线的方式与控制芯片通信,这是奥松电子的[自定义协议](https://cdn-shop.adafruit.com/datasheets/DHT11-chinese.pdf)。不过,基于MicroPython开发的时候,我们不需要自己开发驱动,可以直接使用MicroPython提供的DHT库。
|
||||
1. 联网的开发,我们可以继续基于腾讯云物联网平台来实现。我提供了物模型,希望你可以基于智能电灯的开发经验独立完成这个任务。
|
||||
1. 多传感器融合是提高决策正确性的重要方法,其中融合算法是最关键的。
|
||||
|
||||
在实战篇,我们主要基于开源的开发板硬件来完成智能硬件的开发工作。这在工作中有很大的用处,因为当我们有新的产品想法时,可以快速地进行技术验证,甚至用户反馈。
|
||||
|
||||
这也就是**最小可行性产品**(Minimum Viable Product,MVP)方法论。在软件领域,这种方法比较常见,而基于硬件开发板,我们可以很好地验证智能硬件产品的可行性,为企业节约成本、降低风险。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我给你留一个思考题吧。
|
||||
|
||||
在我提供的物模型里面,你应该可以看到“缺水状态”的属性。它可以用于指示水泵连接的水箱是不是已经缺水了,如果水箱水位不足,就会提醒我们及时给水箱补水。请你想一想,要怎么实现这个水位检测功能呢?请用文字描述一下你的思路。
|
||||
|
||||
欢迎你在留言区和我交流,也欢迎你将这一讲分享给你的朋友,大家一起交流学习。
|
||||
928
极客时间专栏/物联网开发实战/实战篇/22 | 掌控数据:家里的数据可以怎么利用?.md
Normal file
928
极客时间专栏/物联网开发实战/实战篇/22 | 掌控数据:家里的数据可以怎么利用?.md
Normal file
@@ -0,0 +1,928 @@
|
||||
<audio id="audio" title="22 | 掌控数据:家里的数据可以怎么利用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/25/6c052c71c91c469360456c3cd9d68025.mp3"></audio>
|
||||
|
||||
你好,我是郭朝斌。
|
||||
|
||||
到目前为止,我们已经完成智能电灯、光照传感器、智能音箱和自动浇花器的实战训练,在这个过程中,我们主要关注的是设备功能和远程控制的实现。
|
||||
|
||||
其实,物联网设备会生成大量的数据。如果我们能把这些数据存储到物联网系统的数据库中,并且好好应用这些数据,比如提供查询和分析功能,就能够产出更大的价值。
|
||||
|
||||
这一讲,我就基于自动浇花器来讲一讲数据的应用方法,主要包括以下两种:
|
||||
|
||||
1. 基于腾讯云物联网平台提供的**数据流**功能,介绍一个**设备消息推送应用**的配置方法。
|
||||
1. 基于腾讯云的**HTTP方式的数据同步**功能,开发一个**Web数据应用系统**。因为需要购买云服务器,所以你可以酌情选择是否实际部署。
|
||||
|
||||
## 方法一:基于数据流的设备消息推送应用
|
||||
|
||||
腾讯云物联网平台已经为我们提供了一种简便的数据应用方法。我们可以使用它的可视化编辑界面来完成数据流的创建工作。
|
||||
|
||||
你可以登录腾讯云物联网平台的[控制台](https://console.cloud.tencent.com/iotexplorer),然后进入我们之前创建的“智能家居”项目,点击左边菜单栏中的“数据开发”。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/gQZPGbwpSJRbg0Xa.png!thumbnail" alt="">
|
||||
|
||||
然后,你需要新建一个数据流,名称可以是“自动浇花器”。点击数据流列表中的“自动浇花器”项目,你就可以进入可视化的编辑界面。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/YHiZxGoEO2MmIeAX.png!thumbnail" alt="">
|
||||
|
||||
在可视化编辑界面,我们可以看到,一个数据流包括“输入”“处理”和“输出”三个部分。
|
||||
|
||||
1. 输入,包括设备数据、设备事件和设备状态三种,其中设备数据和设备事件与物模型中的定义是一致的。设备状态是设备的上线、下线的状态变化。
|
||||
1. 处理,可以编写基本的判断逻辑来过滤输入数据。
|
||||
1. 输出,可以作为消息将数据推送到App或者小程序中。你可以对消息的内容模板进行定义。
|
||||
|
||||
自动浇花器设备会上报环境的温度、湿度信息,那么我们可以定义一个温湿度不适宜的消息提醒。
|
||||
|
||||
你可以拖拽“设备数据”到编辑区域,然后点击这个模块,在右边的选项中定义设备数据,产品选择“自动浇花器”,属性选择“环境温度”和“环境湿度”。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/cMStN80VDhRiCo8p.png!thumbnail" alt="">
|
||||
|
||||
接着,你可以添加“数据过滤”模块,并且将这个模块与“设备数据”模块相连,然后点击这个模块,在右边编辑过滤条件。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/ZWokFm4h3v20SEXW.png!thumbnail" alt="">
|
||||
|
||||
<img src="https://uploader.shimo.im/f/XINTSH99JV0v0WPX.png!thumbnail" alt="">
|
||||
|
||||
完成数据过滤的定义后,你需要继续添加“公众号推送”模块,并且编辑消息推送的模板。具体的消息模板定义,你可以参考下面内容:
|
||||
|
||||
```
|
||||
环境温度、湿度不适宜:
|
||||
当前温度是$env_temp
|
||||
当前湿度是$env_hum
|
||||
当前时间是$timeStamp
|
||||
|
||||
```
|
||||
|
||||
<img src="https://uploader.shimo.im/f/fdGE9qFP81xoVLtG.png!thumbnail" alt="">
|
||||
|
||||
最后,点击页面上方的“保存”和“启用”,你就完成了数据流的定义。
|
||||
|
||||
如果你的自动浇花器设备是在线状态,那么当环境的温度或者湿度过高、过低时,你就会在腾讯连连小程序的消息列表中收到“告警”消息。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/uKGvPeQ6L5TvqA0R.jpg!thumbnail" alt="">
|
||||
|
||||
## 方法二:基于HTTP数据同步的Web数据应用
|
||||
|
||||
物联网平台除了提供数据流的方式,还可以基于HTTP协议把数据推送到你指定的网址,比如你开发的Web服务器的网址。所以你也可以使用这种方式,更加灵活地利用物联网设备的数据。
|
||||
|
||||
### 数据同步体验
|
||||
|
||||
我们借助在线的Webhook服务,测试一下数据同步功能。
|
||||
|
||||
首先,打开[webhook.site](https://webhook.site/)网站,并且记录页面中显示的专属URL地址。
|
||||
|
||||
然后,你需要登录腾讯云物联网平台,进入我们之前创建的“智能家居”项目,再点击左边菜单栏中的“数据同步”,选择“HTTPS”标签。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/WJGxUfUpJLgXvUeO.png!thumbnail" alt="">
|
||||
|
||||
接着,点击“自动浇花器”对应的“设置”链接,在设置窗口,将webhook网站获取到的URL地址,粘贴在输入框。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/rybXFS7FwK2oVD85.png!thumbnail" alt="">
|
||||
|
||||
完成设置后,打开“生效状态”。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/SUKwtfVaGUWeaoz4.png!thumbnail" alt="">
|
||||
|
||||
在保证自动浇花器设备正常运行的情况下,你就可以在webhook网站的页面中看到设备上报的数据了。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/rtdqJo4NFSnghZq1.png!thumbnail" alt="">
|
||||
|
||||
下面我来介绍一下用Python语言来开发Web服务器的方法。
|
||||
|
||||
因为购买云服务器需要费用,所以这个实战任务是选学内容,你可以在自己的电脑上实践一下Web服务器的开发过程,然后酌情考虑要不要部署到云服务器上。
|
||||
|
||||
### 准备Django开发环境
|
||||
|
||||
Django是一个流行的基于Python语言的Web开发框架。它提供了强大的功能,同时也简单易用。接下来,我们就基于Django来实现一个Web应用程序。
|
||||
|
||||
首先是在电脑上配置Django的开发环境,在终端上运行下面的命令安装Django,准备好需要用到的工具。
|
||||
|
||||
```
|
||||
$ pip3 install django
|
||||
|
||||
```
|
||||
|
||||
安装完成后,你可以在终端上切换到一个代码开发目录,比如:
|
||||
|
||||
```
|
||||
$ cd ~/study/iot/geektime
|
||||
|
||||
```
|
||||
|
||||
然后,直接使用Django提供的脚手架工具django-admin来创建一个项目:
|
||||
|
||||
```
|
||||
$ django-admin startproject watering_web
|
||||
|
||||
```
|
||||
|
||||
这时,命令会创建一个名称为watering_web的目录。目录中包含manage.py、settings.py、urls.py、wsgi.py和asgi.py等几个项目“骨架”文件,其中:
|
||||
|
||||
- manage.py是项目管理脚本,比如创建子应用、运行开发服务器等都可以通过它实现。
|
||||
- settings.py是项目的整体配置文件。比如子应用的配置、数据库的配置等。
|
||||
- urls.py是项目的全局路由声明文件。
|
||||
- wsgi.py是WSGI(Python Web Server Gateway Interface的缩写)服务接口文件,是整个Django应用的调用入口。
|
||||
- asgi.py是ASGI(Asynchronous Server Gateway Interface的缩写)服务接口文件。ASGI是WSGI的替代者,它增加了异步应用的能力。除了支持WSGI协议,同时对Websocket和HTTP2.0这些长连接方式的协议提供了支持。
|
||||
|
||||
现在,我们进入watering_web目录,运行下面的命令,就可以在电脑上启动这个项目的开发服务器了。
|
||||
|
||||
```
|
||||
$ python manage.py runserver
|
||||
|
||||
```
|
||||
|
||||
<img src="https://uploader.shimo.im/f/unz2fDt0y6oOS9qB.png!thumbnail" alt="">
|
||||
|
||||
这非常有利于你的开发调试工作。比如,你可以在浏览器输入 [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 随时访问Web应用的开发效果。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/mb5wW8VRe8ddoe78.png!thumbnail" alt="">
|
||||
|
||||
当你在浏览器上看到这个界面时,就说明Django应用的开发环境已经准备好了。下面,我们就来开发Web应用。
|
||||
|
||||
### Django应用开发
|
||||
|
||||
#### 创建子应用watering
|
||||
|
||||
首先,我们通过运行下面的命令,创建watering_web项目的子应用watering:
|
||||
|
||||
```
|
||||
$ python manage.py startapp watering
|
||||
|
||||
```
|
||||
|
||||
这时,你应该可以看到项目目录下,新增加了watering的目录。目录中包含了watering子应用的基本代码模块。
|
||||
|
||||
- admin.py是后台管理应用的配置文件,我们可以在其中增加数据库模型对象,让Django应用管理员能通过后台管理页面进行编辑。你在浏览器输入[http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/)可以访问到后台管理应用。
|
||||
- apps.py是子应用的配置文件。
|
||||
- migrations是数据库迁移的文件目录,它下面会保存每次数据库迁移的中间文件。
|
||||
- models.py是定义子应用的数据库数据模型文件。
|
||||
- tests.py是子应用的单元测试代码文件。
|
||||
- views.py是子应用的视图代码文件。
|
||||
|
||||
#### 理解Django的视图调用
|
||||
|
||||
在Django里面,网页或者说HTTP的响应都是视图生成的,每个视图对应views.py中的一个函数。Django会根据HTTP请求的URL域名地址来选择相应的视图,而URL和视图函数的映射关系是在urls.py文件中定义的。
|
||||
|
||||
比如,你可以打开watering目录下的views.py文件,添加下面的函数:
|
||||
|
||||
```
|
||||
from django.http import HttpResponse
|
||||
|
||||
def demo(request):
|
||||
return HttpResponse('Hello World!')
|
||||
|
||||
```
|
||||
|
||||
然后,在watering目录下,新建一个urls.py文件,文件内容如下:
|
||||
|
||||
```
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('demo', views.demo, name='demo'),
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
最后,在watering_web目录下的项目全局路由文件中,将urlpatterns的内容替换为下面的内容:
|
||||
|
||||
```
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('watering.urls')),
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
现在,你在浏览器输入地址[http://127.0.0.1:8000/demo](http://127.0.0.1:8000/demo),就可以看看我们刚刚定义的demo视图的内容。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/KNyQrpXeaWABnhn2.png!thumbnail" alt="">
|
||||
|
||||
#### 应用代码开发
|
||||
|
||||
接下来,我们正式开始子应用的开发。
|
||||
|
||||
首先,我们需要定义数据库的数据模型。Django框架实现了ORM(Object-Relational Mapping,对象关系映射器)技术。你可以直接在Python代码中定义数据库的表结构。
|
||||
|
||||
基于这个定义,Django的migration工具能自动在数据库中创建相应的数据库表。在后面的部署阶段,我会讲解到具体的migration命令。
|
||||
|
||||
我把models.py的代码贴在文稿中,供你参考:
|
||||
|
||||
```
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class Watering(models.Model):
|
||||
seq_no = models.IntegerField(blank=False, null=False)
|
||||
device_name = models.CharField(max_length=64, blank=False)
|
||||
product_id = models.CharField(max_length=64, blank=False)
|
||||
power_switch = models.IntegerField(default=0)
|
||||
humidity = models.IntegerField(default=0)
|
||||
env_temp = models.FloatField(default=0.0)
|
||||
env_hum = models.IntegerField(default=0)
|
||||
env_illum = models.IntegerField(default=0)
|
||||
timestamp = models.DateTimeField(auto_now=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.device_name + '_' + str(self.seq_no)
|
||||
|
||||
```
|
||||
|
||||
有了数据模型之后,你就可以在views.py文件中开发视图,接收腾讯云物联网平台推送的HTTP请求,并且将数据存储到数据库中。
|
||||
|
||||
文稿中是我的示例代码,供你参考:
|
||||
|
||||
```
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.forms.models import model_to_dict
|
||||
import json
|
||||
from datetime import datetime
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
from tencentcloud.iotexplorer.v20190423 import iotexplorer_client, models
|
||||
|
||||
SECRET_ID = "你的Secret ID"
|
||||
SECRET_KEY = "你的Secret Key"
|
||||
PRODUCT_ID = "你的ProductID"
|
||||
|
||||
from .models import Watering
|
||||
|
||||
def build_response(resp, msg):
|
||||
dict = {
|
||||
"resp": resp,
|
||||
"msg": msg,
|
||||
}
|
||||
return HttpResponse(json.dumps(dict), content_type='application/json')
|
||||
|
||||
def demo(request):
|
||||
return HttpResponse('Hello World!')
|
||||
|
||||
@csrf_exempt
|
||||
def data_sync(request):
|
||||
if request.method == 'POST':
|
||||
json_data = json.loads(request.body.decode())
|
||||
else:
|
||||
return HttpResponseBadRequest('Bad Request')
|
||||
|
||||
if 'seq' in json_data:
|
||||
_seq = json_data['seq']
|
||||
if 'devicename' in json_data:
|
||||
_device_name = json_data['devicename']
|
||||
if 'productid' in json_data:
|
||||
_product_id = json_data['productid']
|
||||
if 'timestamp' in json_data:
|
||||
_timestamp = json_data['timestamp']
|
||||
if 'payload' in json_data:
|
||||
_payload = json_data['payload']
|
||||
if 'method' in _payload and 'report' == _payload['method']:
|
||||
_params = _payload['params']
|
||||
_env_temp = _params['env_temp']
|
||||
_env_hum = _params['env_hum']
|
||||
_env_illum = _params['env_illum']
|
||||
_humidity = _params['humidity']
|
||||
_power_status = _params['power_switch']
|
||||
|
||||
try:
|
||||
Watering.objects.create(
|
||||
seq_no=_seq, device_name=_device_name, product_id=_product_id,
|
||||
power_switch=_power_status, humidity=_humidity,
|
||||
env_temp = _env_temp, env_hum = _env_hum, env_illum = _env_illum,
|
||||
timestamp=datetime.fromtimestamp(_timestamp) )
|
||||
|
||||
except:
|
||||
return HttpResponse('Insert Failed!')
|
||||
|
||||
return HttpResponse('OK')
|
||||
|
||||
@csrf_exempt
|
||||
def latest_data(request):
|
||||
if request.method == 'POST':
|
||||
json_data = json.loads(request.body.decode())
|
||||
else:
|
||||
return HttpResponseBadRequest('Bad Request')
|
||||
|
||||
if 'devicename' in json_data:
|
||||
_device_name = json_data['devicename']
|
||||
else:
|
||||
_device_name = None
|
||||
|
||||
if _device_name is not None:
|
||||
try:
|
||||
data = Watering.objects.filter(device_name=_device_name).latest('timestamp')
|
||||
data_dict = model_to_dict(data, fields=['seq_no','device_name','product_id','power_switch','humidity','env_temp','env_hum','env_illum'])
|
||||
data_dict['timestamp'] = str(data.timestamp)
|
||||
dict = {
|
||||
"resp": 0,
|
||||
"msg": "OK",
|
||||
"data": data_dict
|
||||
}
|
||||
return HttpResponse(json.dumps(dict), content_type='application/json')
|
||||
except Exception as e:
|
||||
return build_response(1, str(e))
|
||||
else:
|
||||
return build_response(1, 'Parameter invalid.')
|
||||
|
||||
```
|
||||
|
||||
为了能够控制自动浇花器的水泵打开和关闭,你也可以增加发送控制命令的视图。同样,你可以参考下面的代码:
|
||||
|
||||
```
|
||||
@csrf_exempt
|
||||
def control_device(request):
|
||||
if request.method == 'POST':
|
||||
json_data = json.loads(request.body.decode())
|
||||
else:
|
||||
return HttpResponseBadRequest('Bad Request')
|
||||
|
||||
if 'devicename' in json_data:
|
||||
_device_name = json_data['devicename']
|
||||
else:
|
||||
_device_name = None
|
||||
if 'status' in json_data:
|
||||
_status = json_data['status']
|
||||
else:
|
||||
_status = None
|
||||
|
||||
if _device_name is not None and _status is not None:
|
||||
try:
|
||||
cred = credential.Credential(SECRET_ID, SECRET_KEY)
|
||||
httpProfile = HttpProfile()
|
||||
httpProfile.endpoint = "iotexplorer.tencentcloudapi.com"
|
||||
|
||||
clientProfile = ClientProfile()
|
||||
clientProfile.httpProfile = httpProfile
|
||||
client = iotexplorer_client.IotexplorerClient(cred, "ap-guangzhou", clientProfile)
|
||||
|
||||
req = models.ControlDeviceDataRequest()
|
||||
data = {
|
||||
"power_switch": _status
|
||||
}
|
||||
data_str = json.dumps(data)
|
||||
|
||||
params = {
|
||||
"DeviceName": _device_name,
|
||||
"ProductId": PRODUCT_ID,
|
||||
"Data": data_str
|
||||
}
|
||||
req.from_json_string(json.dumps(params))
|
||||
|
||||
resp = client.ControlDeviceData(req)
|
||||
|
||||
return build_response(0, resp.to_json_string())
|
||||
|
||||
except TencentCloudSDKException as err:
|
||||
return build_response(1, str(err))
|
||||
|
||||
else:
|
||||
return build_response(1, 'Parameter invalid.')
|
||||
|
||||
```
|
||||
|
||||
完成视图的开发后,你需要更新urls.py文件中的urlpatterns,为新增视图添加映射关系。
|
||||
|
||||
```
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('geektime/', include('watering.urls')),
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
同时,在watering子应用的目录增加一个urls.py文件,内容如下:
|
||||
|
||||
```
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('demo', views.demo, name='demo'),
|
||||
path('data', views.data_sync, name='data_sync'),
|
||||
path('control', views.control_device, name='control_device'),
|
||||
path('fetch', views.latest_data, name='latest_data'),
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
然后,我们来开发一个简单的网页,实现设备信息的显示和控制。为了便于理解,我这里只使用最基础的方法。当熟悉基本原理之后,你可以尝试使用VUE、React等前端应用框架来开发功能更丰富的Web前端应用。
|
||||
|
||||
我把代码贴在文稿中,供你参考:
|
||||
|
||||
```
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta content="webkit" name="renderer">
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
|
||||
<meta content="IE=Edge,chrome=1" http-equiv="X-UA-Compatible">
|
||||
<style type="text/css">
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
<title>Plant Watering</title>
|
||||
<script src="jquery-3.1.1.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Plant Watering</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Device Name: </th><th id='devicename'></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Date: </th><th id='datetime'></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Soil Moisture (&#37;): </th><th id='moisture'></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Environment Temperature (℃): </th><th id='envtemp'></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Environment Humidity (&#37;): </th><th id='envhum'></th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Environment Illumination (Lux): </th><th id='envillum'></th>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<button id='on'>Open</button>
|
||||
<button id='off'>Close</button>
|
||||
|
||||
<script type='text/javascript'>
|
||||
var timer = false
|
||||
var interval = 5*1000 //5 seconds
|
||||
var device_name = 'Watering_1'
|
||||
|
||||
$("#on").on('click',function(){
|
||||
control(1)
|
||||
})
|
||||
$("#off").on('click',function(){
|
||||
control(0)
|
||||
})
|
||||
|
||||
function control(state){
|
||||
|
||||
var payload = {
|
||||
"devicename":device_name,
|
||||
"status":state
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url : "http://159.75.214.14/geektime/control",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
method : 'POST',
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload),
|
||||
|
||||
success:function (obj) {
|
||||
|
||||
if(obj.resp === 0){
|
||||
console.log("pump on");
|
||||
}
|
||||
else{
|
||||
console.log(obj.msg);
|
||||
}
|
||||
},
|
||||
error:function(e){
|
||||
console.log("control device post failed.");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function update_data(){
|
||||
var payload = {
|
||||
"devicename":device_name,
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url : "http://159.75.214.14/geektime/fetch",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
method : 'POST',
|
||||
dataType: "json",
|
||||
data: JSON.stringify(payload),
|
||||
|
||||
success:function (obj) {
|
||||
|
||||
if(obj.resp === 0){
|
||||
console.log("fetch success");
|
||||
$("#devicename").text(device_name)
|
||||
$("#datetime").text(obj.data.timestamp)
|
||||
$("#moisture").text(obj.data.humidity)
|
||||
$("#envtemp").text(obj.data.env_temp)
|
||||
$("#envhum").text(obj.data.env_hum)
|
||||
$("#envillum").text(obj.data.env_illum)
|
||||
}
|
||||
else{
|
||||
console.log(obj.msg);
|
||||
}
|
||||
},
|
||||
error:function(e){
|
||||
console.log("fetch latest data failed.");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.onload = function(){
|
||||
update_data() //Fetch data to get devicename first.
|
||||
timer = setInterval(function(){
|
||||
update_data()
|
||||
}, interval)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
对于这些前端代码文件,你可以在watering_web项目的根目录下创建一个web目录,然后把它们放入这个目录下。
|
||||
|
||||
关于Django项目的settings.py文件,你需要在ALLOWED_HOSTS中增加自己服务器的IP地址。
|
||||
|
||||
```
|
||||
ALLOWED_HOSTS = [
|
||||
'159.75.214.14', #替换自己的云服务器IP
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
同时,在数据库的配置中,修改为MySQL的配置内容:
|
||||
|
||||
```
|
||||
DATABASES = {
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||
# 'NAME': BASE_DIR / 'db.sqlite3',
|
||||
# }
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'geektime',
|
||||
'USER': 'root',
|
||||
'PASSWORD': 'geektime',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '3306',
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Django应用部署
|
||||
|
||||
完成Django应用的开发,我们就可以把它部署到云服务器中。
|
||||
|
||||
首先,你需要先登录到[腾讯云控制台](https://console.cloud.tencent.com/),从“云产品”中点击选择“云服务器”,进入云服务的配置页面。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/Ljni1rDHy0cPE46B.png!thumbnail" alt="">
|
||||
|
||||
在云服务器页面,点击“新建”,购买一台服务器。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/bh1F82gO1yMlG9tW.png!thumbnail" alt="">
|
||||
|
||||
在新建的页面,你可以根据自己的需求选择服务器的硬件配置。如果只是用来练习,选择最低的配置就行了。
|
||||
|
||||
服务器的操作系统选择“Ubuntu Server 16.04.1”,其他选项保持默认值。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/nDMcg5fmle2sSMQQ.png!thumbnail" alt="">
|
||||
|
||||
<img src="https://uploader.shimo.im/f/n5WjGhdUjk3KQYYS.png!thumbnail" alt="">
|
||||
|
||||
点击“立即购买”,完成支付后,我们重新进入云服务器控制台,就可以看到我们的服务器新实例了。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/mmy44RHTSD6zNId2.png!thumbnail" alt="">
|
||||
|
||||
准备好云服务器后,我再介绍一下Django应用在Ubuntu服务器上的具体部署操作。
|
||||
|
||||
以下的介绍都是针对单台服务器部署展开的。如果多台机子部署,你需要做相应的调整,比如使用腾讯云的CDB(云数据库服务),你就需要对Django应用的设置文件(settings.py)中数据库部分作修改。
|
||||
|
||||
#### Ubuntu准备
|
||||
|
||||
首先,通过SSH登录到Ubuntu服务器,像连接树莓派一样,你仍然可以使用Putty或者SecureCRT这样的终端软件。
|
||||
|
||||
服务器的IP地址可以从腾讯云的云服务器控制台获取。关于用户名和密码,“腾讯云助手”公众号会在购买云服务器时发送消息通知。如果没有收到,你可以在控制台重置密码。
|
||||
|
||||
登录后,首先需要更新apt,你可以运行下面的命令:
|
||||
|
||||
```
|
||||
$ sudo apt-get update
|
||||
|
||||
```
|
||||
|
||||
#### 代码上传
|
||||
|
||||
接着,你需要将Django应用的代码上传到服务器某个目录下,比如 /home/ubuntu/iot/ 这个目录下。这涉及到下面介绍的Nginx和uWSGI的配置文件中的路径,因此,**如果代码的路径不是这个,你需要相应地修改这些配置中的路径。**
|
||||
|
||||
上传的工具,你还是可以使用第19讲中提到的FileZilla等软件。
|
||||
|
||||
#### 数据库
|
||||
|
||||
我们使用的MySQL数据库,可以通过以下命令来安装:
|
||||
|
||||
```
|
||||
$ sudo apt-get install mysql-server mysql-client libmysqlclient-dev
|
||||
|
||||
```
|
||||
|
||||
说明一下,如果没有安装libmysqlclient-dev的话,接下来安装mysql-python的步骤可能会报错。
|
||||
|
||||
安装过程中,终端也提示你输入MySQL数据库的密码,你可以像我一样,输入“geektime”。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/82e0vPFxvGbqlsp6.png!thumbnail" alt="">
|
||||
|
||||
安装完成后,你可以通过下面的命令,连接上数据库。
|
||||
|
||||
```
|
||||
$ mysql -p -u root
|
||||
|
||||
```
|
||||
|
||||
然后,在数据库的交互命令行中,你需要输入下面的命令,创建 geektime 数据库。
|
||||
|
||||
```
|
||||
create database geektime;
|
||||
exit;
|
||||
|
||||
```
|
||||
|
||||
#### Python环境
|
||||
|
||||
接下来,我们需要配置Python语言环境,因为应用程序和Django应用框架都是基于Python的。
|
||||
|
||||
首先,我们修改一下系统的默认python,将它修改为python3版本。运行下面的命令:
|
||||
|
||||
```
|
||||
$ vim ~/.bashrc
|
||||
|
||||
```
|
||||
|
||||
在文件中,增加下面的内容:
|
||||
|
||||
```
|
||||
alias python='/usr/bin/python3'
|
||||
|
||||
```
|
||||
|
||||
添加完成后,退出vim。在终端输入下面的命令,使其生效:
|
||||
|
||||
```
|
||||
$ source ~/.bashrc
|
||||
|
||||
```
|
||||
|
||||
接着,我们需要安装 pip ,Python包管理器(Python package manager)。你可以运行下面的命令:
|
||||
|
||||
```
|
||||
$ sudo apt-get install python3-pip
|
||||
$ sudo pip3 install --upgrade pip
|
||||
|
||||
```
|
||||
|
||||
接着,你还需要安装 python-dev ,一些软件包的安装需要依赖它。命令如下:
|
||||
|
||||
```
|
||||
$ sudo apt-get install python3-dev
|
||||
|
||||
```
|
||||
|
||||
后面,你需要进入Django应用目录(我们这里是 /home/ubuntu/iot/watering_web),然后运行下面的命令,安装所有依赖的软件包。:
|
||||
|
||||
```
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
```
|
||||
|
||||
如果出现 locale.Error: unsupported locale setting 的错误,请在命令行输入下面的命令:
|
||||
|
||||
```
|
||||
export LC_ALL="en_US.UTF-8"
|
||||
|
||||
```
|
||||
|
||||
requirements.txt文件的内容如下:
|
||||
|
||||
```
|
||||
tencentcloud-sdk-python==3.0.313
|
||||
mysqlclient
|
||||
Django
|
||||
|
||||
```
|
||||
|
||||
现在,环境已经准备就绪,下面我来讲解一下数据库和uWSGI、Nginx的配置。
|
||||
|
||||
#### 数据库Migrate
|
||||
|
||||
在Django中,数据库的创建已经处理得非常简单,框架本身做了很多的工作。
|
||||
|
||||
首先,在manage.py文件所在的目录,运行下面的命令:
|
||||
|
||||
```
|
||||
$ python manage.py makemigrations
|
||||
|
||||
```
|
||||
|
||||
接着,只需要运行下面的命令,就可以完成所有的数据库表创建工作。
|
||||
|
||||
```
|
||||
$ python manage.py migrate
|
||||
|
||||
```
|
||||
|
||||
#### 确认测试
|
||||
|
||||
现在,你可以执行下面的命令,启动Django应用。然后,你可以通过浏览器确认Django 应用是否可以正常运行。
|
||||
|
||||
```
|
||||
$ python manage.py runserver 0.0.0.0:8080
|
||||
|
||||
```
|
||||
|
||||
下面,我们就可以安装uWSGI和Nginx了。
|
||||
|
||||
#### uWSGI安装
|
||||
|
||||
uWSGI相当于是Django应用和Nginx之间的桥梁,它使用标准的WSGI接口与应用通信。你需要运行命令,安装uWSGI:
|
||||
|
||||
```
|
||||
$ sudo apt-get install -y uwsgi
|
||||
$ sudo apt-get install uwsgi-plugin-python3
|
||||
$ pip3 install uwsgi
|
||||
|
||||
```
|
||||
|
||||
然后,在 /home/ubuntu/iot/config 目录下为uWSGI增加配置文件,文件内容如下:
|
||||
|
||||
```
|
||||
[uwsgi]
|
||||
socket = 127.0.0.1:3031
|
||||
chdir = /home/ubuntu/iot/watering_web
|
||||
wsgi-file = watering_web/wsgi.py
|
||||
processes = 4
|
||||
plugins = python3
|
||||
threads = 2
|
||||
stats = 127.0.0.1:9191
|
||||
|
||||
```
|
||||
|
||||
接着,运行下面的命令,查看uWSGI是否可以正常运行。
|
||||
|
||||
```
|
||||
$ sudo uwsgi --ini ./config/uwsgi.ini
|
||||
|
||||
```
|
||||
|
||||
之后,我们还需要配置基于systemd的uWSGI的自启动流程。这需要创建systemd的unit文件。我们还是在 /home/ubuntu/config 目录下增加 uwsgi.service文件,内容如下:
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=uWSGI
|
||||
After=syslog.target
|
||||
|
||||
[Service]
|
||||
User=ubuntu
|
||||
ExecStart=/usr/bin/uwsgi --ini /home/ubuntu/iot/config/uwsgi.ini
|
||||
Restart=always
|
||||
KillSignal=SIGQUIT
|
||||
Type=notify
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
```
|
||||
|
||||
这时,你还不需要添加这个service,在后面我们会通过命令添加、执行这个配置文件。
|
||||
|
||||
#### 安装Nginx
|
||||
|
||||
首先,你需要执行下面的命令,安装Nginx软件。
|
||||
|
||||
```
|
||||
$ sudo apt-get install nginx
|
||||
|
||||
```
|
||||
|
||||
然后,我们需要配置它。我们通过新添加文件,来配置我们的服务,在 /etc/nginx/conf.d/ 目录下,增加 iot.conf 文件,文件的内容如下:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
#listen [::]:80 default_server;
|
||||
server_name 159.75.214.14; #替换为自己服务器IP地址
|
||||
|
||||
location / {
|
||||
root /home/ubuntu/iot/watering_web/web/;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
location ^~ /geektime/ {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass 127.0.0.1:3031;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
location = /40x.html {
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
并且,将/etc/nginx/nginx.conf文件中的“user www-data;” 修改为“user ubuntu;”。
|
||||
|
||||
现在,你需要执行下面的命令检查Nginx配置文件的语法:
|
||||
|
||||
```
|
||||
$ sudo nginx -t
|
||||
|
||||
```
|
||||
|
||||
如果没有错误,就可以重启Nginx,来加载新的配置文件:
|
||||
|
||||
```
|
||||
$ sudo service nginx restart
|
||||
|
||||
```
|
||||
|
||||
然后,把uWSGI的service配置文件拷贝到systemd配置目录下:
|
||||
|
||||
```
|
||||
$ sudo cp uwsgi.service /etc/systemd/system/
|
||||
|
||||
```
|
||||
|
||||
现在,你可以执行下面的命令,启动uWSGI服务:
|
||||
|
||||
```
|
||||
$ sudo systemctl start uwsgi
|
||||
|
||||
```
|
||||
|
||||
如果一切正常,我们就可以把uWSGI添加到开机自启动中:
|
||||
|
||||
```
|
||||
$ sudo systemctl enable uwsgi
|
||||
|
||||
```
|
||||
|
||||
到这里,服务就部署完毕了。
|
||||
|
||||
### 云平台数据同步URL更新
|
||||
|
||||
云服务器上的Web应用运行正常后,你可以对腾讯云物联网平台上的HTTP数据同步进行更新。修改配置中的URL地址为自己云服务器的视图地址。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/SDBx7kqGp5LVzkdt.png!thumbnail" alt="">
|
||||
|
||||
更新完成后,你就可以在浏览器上访问自己的服务器地址,查看自动浇花器的实时数据,并且控制它进行浇水。
|
||||
|
||||
<img src="https://uploader.shimo.im/f/lQQUwhToOtUKDL9r.png!thumbnail" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下,在这一讲中,我围绕自动浇花器讲解了智能家居系统中设备数据的应用系统开发方法。主要的内容有:
|
||||
|
||||
1. 腾讯云物联网平台为我们提供了简单易用的数据开发方法。基于可视化界面,我们编辑“输入”,“处理”和“输出”就可以完成一个数据流的创建,实现数据的过滤和消息推送。
|
||||
1. 我们可以基于物联网平台的数据同步能力,比如HTTP推送,实现物联网平台和应用服务器的对接。在应用服务器上,我们可以灵活地开发数据应用系统。
|
||||
1. Django是非常流行的,基于Python语言的Web应用开发框架。工作中,你可以使用Django比较快速地实现一个Web应用系统。
|
||||
|
||||
在这一讲中,我们的数据应用系统只包含了自动浇花器的数据,你在时间允许的情况下,也可以尝试在这个系统中增加一下智能电灯和光照传感器的数据。
|
||||
|
||||
另外,物联网平台的数据同步也提供了 CKafka(Cloud Kafka)的数据转发方式。通过订阅Topic主题消息,我们可以消费Kafka的消息,也可以直接将Kafka消息转储到MySQL云数据库中。
|
||||
|
||||
不过,Kafka的费用比云服务器要贵不少,这里就不介绍了。如果你的条件允许,也可以动手实践一下这种方式。
|
||||
|
||||
这里,我整理了一个思维导图,供你参考:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/21/76ce53374f7e4c35bf71c8836fff4521.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我给你留一个思考题吧。
|
||||
|
||||
在Django应用开发的介绍部分,我提到了admin.py是后台管理应用的配置文件,而且访问[http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/)地址,就会出现管理员登录页面。你知道在Django项目中如果创建管理员账号吗?另外,如何在admin.py文件中增加Watering数据库模型对象,实现对自动浇花器监测数据的查询呢?
|
||||
|
||||
欢迎你在留言区写一写自己的思考,同时,也欢迎你将这一讲分享给对Web应用开发感兴趣的朋友,大家一起交流学习。
|
||||
Reference in New Issue
Block a user