跳转至

18 场景联动:智能电灯如何感知光线?(上)

你好,我是郭朝斌。

在上一讲,我们打造了自己的联网智能电灯,你可以通过手机小程序来控制它的打开和关闭,也就是实现远程控制。

其实,我们还可以进一步提高体验,让智能电灯可以基于环境的明暗来自动地打开和关闭。要做到这一点并不难,可以分为两个阶段,第一阶段是打造传感器设备来感知光照的强弱,判断出环境的明暗状态,第二阶段是创建一个场景联动,根据传感器的数值来控制智能电灯的状态。

这一讲,我先带你一步一步地实现第一阶段的工作(如有需要,你可以根据这份文档自行采购相关硬件)。

第一步:通信技术

首先,我们为光照传感器设备选择通信技术。

因为光照传感器设备的部署位置比较灵活,不太可能像智能电灯一样连接房间里的电源线,所以我们要用一种比Wi-Fi功耗更低的通信技术。这样的话,就算使用电池供电,也可以长时间(一年以上)持续工作。

经过对比,我建议选择 BLE 低功耗蓝牙技术(关于通信技术的选择策略,你可以参考第2讲)。随着智能手机的发展,蓝牙早已成为手机标配的通信技术,蓝牙芯片和协议栈的成熟度非常高,而且在设备的供应链方面,蓝牙芯片可以选择的供应商也非常多。

不过在正式开发之前,我还得为你补充说明一些BLE的相关知识。

BLE设备可以在4种模式下工作:

  1. 广播模式(Broadcaster),这里特指单纯的广播模式。这种模式下设备不可以被连接,只能够以一定的时间间隔把数据广播出来,供其他设备使用,比如手机扫描处理。蓝牙Beacon设备就是工作在这种模式。
  2. 从机模式(Peripheral),这种模式下设备仍然可以广播数据,同时也可以被连接。建立连接后,双方可以进行双向通信。比如你用手机连接一个具有蓝牙功能的体温计,这时体温计就是从机(Peripheral)。
  3. 主机模式(Central),这种模式下设备不进行广播,但是可以扫描周围的蓝牙广播包,发现其他设备,然后主动对这些设备发起连接。还是刚才那个例子,主动连接蓝牙体温计的手机就是主机(Central)角色。
  4. 观察者模式(Observer),这种模式下设备像主机模式一样,也不进行广播,而是扫描周围的蓝牙广播包,但是不同的地方是,它不会与从机设备建立连接。一般收集蓝牙设备广播包的网关就是在这种模式下工作的,它会将收集的广播数据通过网线、Wi-Fi或者4G等蜂窝网络上传到云平台。

在这一讲中,我们打造的光照传感器只需要提供光照强度数据就行了,并不需要进行双向通信,所以我们可以定义设备在广播模式下工作。

第二步:选择开发板

那么光照传感器设备要选择什么开发板呢?

我们在上一讲打造的联网智能电灯中使用的NodeMCU是基于ESP8266芯片的,相信你也注意到了,这款芯片并不支持低功耗蓝牙。

好在市场上还有一款基于ESP32芯片的NodeMCU开发板。ESP32是乐鑫科技出品的另一款性能优良且满足低功耗的物联网芯片,它同时支持Wi-Fi和低功率蓝牙通信技术,还有丰富的ADC接口。

更重要的是,MicroPython也支持ESP32芯片,这样我们就可以继续使用Python语言来开发了。

第三步:准备MicroPython环境

接下来,我们就在NodeMCU(ESP32)上安装MicroPython固件,准备Python程序的运行环境。

MicroPython官网已经为我们准备了编译好的固件文件,这省掉了我们在电脑上进行交叉编译的工作。你可以从这个链接 中选择“Firmware with ESP-IDF v3.x”下面的“GENERIC”类别,直接下载最新版本的固件文件到电脑中。

然后,我们使用一根 USB 数据线,将 NodeMCU 开发板和电脑连接起来。USB数据线仍然选择一头是 USB-A 接口、另一头是 Micro-USB 接口,并且支持数据传输的完整线缆。具体细节,你可以再回顾第16讲中的相关内容。

我们使用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。

然后你应该就能看到下图所示的内容,并且可以进行交互。

第四步:搭建光照传感器硬件电路

现在,我们开始基于NodeMCU搭建光照传感器的硬件电路。

首先,我们要准备好实验的材料:

  1. NodeMCU(ESP32)开发板一个。注意区分芯片的具体型号。
  2. 光照传感器模块一个
  3. 杜邦线/跳线若干个
  4. 面包板一个

然后,你可以按照我画的连线图来搭建出自己的电路。跟联网智能电灯的电路比起来,这个还是非常简单的。

这里说明一下,在我的电路图中,光照传感器模块从左到右,管脚分别是光强度模拟信号输出管脚、电源地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蓝牙协议。

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。
  2. Scan Response中 Manufacturer Specific Data (0xFF)含有小米公司识别码的广播包,识别码ID是0x038F。

其中,无论是在Advertising Data中,还是Scan Response中,均采用统一格式定义。

具体的广播报文格式定义,你可以参考下面的表格。

因为我们要为光照传感器增加广播光照强度数据的能力,所以主要关注Object的定义。Object分为属性和事件两种,具体定义了设备数据的含义,比如体温计的温度、土壤的湿度等,数据格式如下表所示:

按照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,就可以过滤掉蓝牙信号强度比较弱(一般也是比较远)的设备。

下面是我的手机扫描到的基于NodeMCU开发板的蓝牙设备。

其中名称Nodemcu下面的就是广播包的具体数据。

到这里,我们就完成了光照传感器设备的开发工作。

小结

总结一下,在这一讲中,我介绍了光照传感器的开发过程,并且补充了低功耗蓝牙技术的相关知识。下面,我们回顾以下重点:

  1. 对于无法连接电源线、需要灵活放置甚至经常移动的设备,低功耗蓝牙技术是合适的通信技术选择。
  2. MiBeacon协议的广播包定义是基于BLE的GAP(Generic Access Profile)制定的,主要有广播数据(Advertising Data)和扫描回复数据(Scan Response)两种。其中广播数据中Service Data的UUID是0xFE95,扫描回复数据中Manufacturer Specific Data的厂家识别码是0x038F。
  3. 在日常的蓝牙设备开发工作中,我们经常需要调试、测试蓝牙功能,这时你可以使用手机上的蓝牙调试软件来验证,比如LightBlue、nRFConnect 和 BLEScanner等。

不过,准备好光照传感器设备只是第一步,为了实现光照传感器和智能电灯的联动,我们还需要将光照传感器接入网络。这就需要借助蓝牙网关设备了,在下一讲中,我将基于树莓派讲解网关设备的开发过程。

思考题

最后,给你留一个思考题吧。

在这一讲的开头,我提到蓝牙设备除了广播数据的能力,还可以连接进行交互。在我提供的代码中其实也包含了一个可供连接获取数据的Service和Characteristic,你发现了吗?你知道这些是基于低功耗蓝牙中的什么Profile协议吗?

欢迎你在留言区写下自己的答案和我交流一下,也欢迎你将这一讲分享给你的朋友,一起讨论学习。

精选留言(14)
  • 郭朝斌 👍(1) 💬(0)

    由于MicroPython官方的ble_advertising.py中payload函数,不支持广播service data,所以我进行了增补: 常量定义增加:_ADV_TYPE_SERVICE_DATA = const(0x16) advertising_payload函数中增加: if service_data: _append(_ADV_TYPE_SERVICE_DATA, service_data) 希望大家可以自己动手实践一下哈。

    2020-12-27

  • 加油加油 👍(3) 💬(2)

    老师您好,关于开发板这类的硬件知识如何学习比较好?我平时是做云平台的业务,对硬件部分有点陌生

    2020-12-21

  • LDxy 👍(2) 💬(2)

    在我提供的代码中其实也包含了一个可供连接获取数据的 Service 和 Characteristic,你发现了吗?你知道这些是基于低功耗蓝牙中的什么 Profile 协议吗? self._ble.gatts_write(self._handle, struct.pack("!h", int(light_den))) GATT (Generic Attribute Profile) 是一个在蓝牙连接之上的发送和接收很短的数据段的通用规范,这些很短的数据段被称为属性(Attribute)。

    2020-12-21

  • Josen 👍(1) 💬(1)

    老师,我是一个软件工程师,已经在软件行业工作两年了,最近想进军物联网,对硬件这块不是特别熟悉,买了块esp32 想让它作为gatt server,然后用蓝牙调试宝测试能否连接成功,但是我发现很吃力,我去micropython官网查看了esp32的相关文档,发现还是云里雾里的,没有esp32完整的例子可以参考的,请问老师我需要去哪里寻找资源呢?csdn,博客园这些我都找了,关于esp32 micropython调用蓝牙的案例少之又少

    2021-02-22

  • 王宁 👍(1) 💬(1)

    MiBeacon 需要安装吗?有没有安装步骤

    2021-01-02

  • Lee 👍(1) 💬(2)

    请问第四步的电路图是用的什么软件呀?看着3D模组做的很好看呀;

    2020-12-22

  • 加油加油 👍(0) 💬(2)

    老师,由于晚购买的实验套件,想问两个问题:1. 我运行程序之后,输出信息 Light Lux: 2268 Light ADC value: 1553 Service Data: b'\x95\xfe\x10\x00\x02\x00\x01\xe8\xdb\x84\x01\tj\x07\x10\x03\x00\xe3\x08' GAP procedure initiated: stop advertising. GAP procedure initiated: advertise; disc_mode=2 adv_channel_map=7 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=800 adv_itvl_max=800 sd_adv bytearray(b'\x02\x01\x06\x14\x16\x95\xfe\x10\x00\x02\x00\x01\xe8\xdb\x84\x01\tj\x07\x10\x03\x00\xe3\x08') sdddd_adv bytearray(b'\x02\x01\x06\x14\x16\x95\xfe\x10\x00\x02\x00\x01\xe8\xdb\x84\x01\tj\x07\x10\x03\x00\xe3\x08') GAP procedure initiated: stop advertising. GAP procedure initiated: advertise; disc_mode=2 adv_channel_map=7 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=800 adv_itvl_max=800 这个是否正常呢,我打开软件没有扫描到nodemcu的蓝牙 2. 是不是esp32 运行这个程序的时候 ampy连接不到串口 我一直连接不上

    2021-01-10

  • 小伟 👍(7) 💬(0)

    学习本讲课程后,这边做个补充和勘误: 1. 补充ble_advertising.py源文件来源(https://raw.githubusercontent.com/micropython/micropython/master/examples/bluetooth/ble_advertising.py) 2. ble_lightsensor.py中build_mi_sdadv方法下语句`service_data = struct.pack("<3HB",uuid,fc,pid,fcnt)+mac+struct.pack("<H2BH",objid,objlen,0,objval)`里的mac应修改为mac[1]; 原因: mac是一个tuple对象 ex: (0, b'4\\x86]\\xb6\\xeb\\x0e'), 应取第二个值 3. 本讲相关代码已上传github,供大家参考:https://github.com/Kevin181/geektime/tree/main/iot/led/light-sensor

    2022-03-01

  • A=X+Y+Z 👍(2) 💬(1)

    老师,这个报错,换了几个版本的固件都没有用 File "main.py", line 3, in <module> File "ble_lightsensor.py", line 6, in <module> ImportError: no module named 'ble_advertising' MicroPython v1.13 on 2020-09-02; ESP32 module with ESP32

    2021-07-04

  • RG 👍(0) 💬(0)

    购买ESP32如果选择其他开发板,需要注意是什么系列,ESP32?EPS32-C3?ESP32-S3?ESP32-S2?需要注意这个S2,不带蓝牙。同时买了几块,发现s2没有蓝牙,其他的都有,看了一眼 spec 的确是只有这个s2没有

    2022-07-05

  • Geek_jg3r26 👍(0) 💬(0)

    跳线和杜邦线是说的一个吗

    2022-06-28

  • Geek_1bbdee 👍(0) 💬(0)

    老师,我按照清单买的硬件设备,然后按照步骤一步步去做,最后把代码也放到了ESP32上,硬件电路应该没问题。但是没有现象显示,该如何去定位问题呢?

    2022-01-28

  • 牛哥哥 👍(0) 💬(1)

    真的搜不到蓝牙,也不会调试程序

    2021-07-07

  • Allen5g 👍(0) 💬(0)

    实践打卡

    2021-04-10