资讯详情

《物联网开发实战》18 场景联动:智能电灯如何感知光线?(上)(学习笔记)

原课程链接仅作为我学习物联网发展实战的学习笔记:极客时间物联网发展实战-郭朝斌

文章目录

  • 第一步:通信技术
  • 第二步:选择开发板
  • 第三步:准备 MicroPython 环境
  • 第四步:构建光传感器硬件电路
  • 第五步:编写蓝牙程序:
  • 第六步:验证光传感器

第一步:通信技术

郭老师建议选择 BLE 低功耗蓝牙技术作为光传感器设备的通信手段。由于传感器的部署位置灵活,无线通信技术不能直接使用电源线,而是因为 BLE 的功耗比 Wi-Fi 低,所以我们选择 BLE 通信技术。

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

  1. (Boradcaster),简单的广播模式。在这种模式下,设备不能连接,只能在一定的时间间隔内广播数据,用于其他设备,如手机扫描处理。
  2. (Peripheral),在这种模式下,设备仍然可以同时广播数据和连接。建立连接后,双方可以进行双向通信。
  3. (Central),在这种模式下,设备不能广播,但可以扫描周围的蓝牙广播包,找到其他设备,然后主动连接。
  4. (Observer),在这种模式下,设备不会像主机模式一样广播,而是扫描周围的蓝牙广播包,但不同的是它不会与从机器建立连接。一般来说,收集蓝牙设备广播包的网关在这种模式下工作。

在这篇文章中,光传感器只需要提供光强数据,所以我们可以让它在广播模式下工作。

第二步:选择开发板

开发板开发板 NodeMCU,基础需要使用 ESP32 芯片的 NodeMCU 同时支持开发板 Wi-Fi 和低功耗蓝牙通信技术,还有很多 ADC 接口。

第三步:准备 MicroPython 环境

可参考环境建设 物联网发展实战16 实战准备:如何构建硬件开发环境?(学习笔记)

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

以下是郭老师的连线图:

在这里插入图片描述

郭老师选择了基础 PT550 环保光敏二极管光敏传感器元件灵敏度较高,测量范围较大 0Lux~6000Lux。

该组件通过信号管脚输出模拟量,我们读取 NodeMCU ESP32 的 ADC 模数转换器的值(ADC7,GPIO35),可以得到光的强度。这个值越大,光的强度就越大。

ADC 支持的最大精度是 12 bit,对应十进制 0~4095,我们需要将电压值与 ADC 线性转换值可参考以下代码(摘自原文)

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/4096*6000) 

第五步:编写蓝牙程序:

NodeMCU ESP32 固件已集成 BLE 但是,我们还需要定义一定格式的广播包数据,使其他设备能够顺利地分析扫描到的数据。

如何定义蓝牙广播包的格式?郭先生推荐小米定制 MiBeacon 蓝牙协议。

为了方便用户在米家使用APP 与蓝牙网关时,可以快速发现并与蓝牙网关合作BLE 小米连接,小米IoT 平台在BLE 设备广播(基于 BLE 协议4.0)添加小米服务数据(ServiceData UUID 0xFE95,即Mibeacon),使BLE 在广播数据中,设备可以识别设备本身的身份和类型,用户或蓝牙网关可以及时识别和连接;此外,为了更好地改进BLE 智能设备的能力,BLE MiBeacon 该协议还支持开发者根据实际使用需要选择添加Object 字段,通过网关到小米,IoT 平台上报BLE 实现设备状态远程报告和智能联动等功能。

https://iot.mi.com/

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 Servce UUID 的广播包,UUID 是 0xFE95。 2 . Scan Response 中 Manufacturer Specific Data(0xFF)含有小米公司识别码的广播包,识别码 ID 是 0x038F。 其中,无论是在 Advertising Data 中,还是 Scan Response 中,均采用统一格式定义。据图的广播报文格式定义,可以参考下面的表格。

——原文

名称 长度(byte) 是否必须 描述
Frame Control 2 必须 控制位
Product ID 2 必须 产品 ID,需要在小米 IoT 开发平台申请
Frame Counter 1 必须 序号,用于去重
MAC Address 6 基于 Frame Control 设备 Mac 地址
Capability 1 基于 Frame Control 设备能力
I/O capability 2 基于 Capacity I/O 能力,目前只有高安全级 BLE 接入才会用到此字段
Object n(根据实际需求) 基于 Frame Control 触发事件或广播属性
Random Number 3 基于 Frame Control 如果加密则为必选字段,与 Frame Counter 合并成为 4 字节 Counter,用于防重放
Message Integrity Check 4 基于 Frame Control 如果加密则为必选字段,MIC 四字节

由于我们要给光照传感器增加广播光照强度数据的能力,所以需要重点关注 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[1]+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(35)
    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()

除了上文提到的 3 个 Python 脚本,还需要一个 ble_advertising.py,可以到 MicroPython 官方的 Bluetooth 例子中获取,地址:https://github.com/micropython/micropython/tree/master/examples/bluetooth

广播 service data 这一功能我调了很久都没成功,最后发现。。。。:

下面是我使用 ble_advertising.py 脚本文件:

# Helpers for generating BLE advertising payloads.

from micropython import const
import struct
import bluetooth

# Advertising payloads are repeated packets of the following form:
# 1 byte data length (N + 1)
# 1 byte type (see constants below)
# N bytes type-specific data

_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_NAME = const(0x09)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_UUID16_MORE = const(0x2)
_ADV_TYPE_UUID32_MORE = const(0x4)
_ADV_TYPE_UUID128_MORE = const(0x6)
_ADV_TYPE_APPEARANCE = const(0x19)
_ADV_TYPE_SERVICE_DATA = const(0x16)


# Generate a payload to be passed to gap_advertise(adv_data=...).
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0, service_data = None):
    payload = bytearray()

    def _append(adv_type, value):
        nonlocal payload
        payload += struct.pack("BB", len(value) + 1, adv_type) + value

    _append(
        _ADV_TYPE_FLAGS,
        struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
    )

    if name:
        _append(_ADV_TYPE_NAME, name)

    if services:
        for uuid in services:
            b = bytes(uuid)
            if len(b) == 2:
                _append(_ADV_TYPE_UUID16_COMPLETE, b)
            elif len(b) == 4:
                _append(_ADV_TYPE_UUID32_COMPLETE, b)
            elif len(b) == 16:
                _append(_ADV_TYPE_UUID128_COMPLETE, b)

    # See org.bluetooth.characteristic.gap.appearance.xml
    if appearance:
        _append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))

    if service_data:
        _append(_ADV_TYPE_SERVICE_DATA, service_data)

    return payload


def decode_field(payload, adv_type):
    i = 0
    result = []
    while i + 1 < len(payload):
        if payload[i + 1] == adv_type:
            result.append(payload[i + 2 : i + payload[i] + 1])
        i += 1 + payload[i]
    return result


def decode_name(payload):
    n = decode_field(payload, _ADV_TYPE_NAME)
    return str(n[0], "utf-8") if n else ""


def decode_services(payload):
    services = []
    for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
        services.append(bluetooth.UUID(struct.unpack("<h", u)[0]))
    for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
        services.append(bluetooth.UUID(struct.unpack("<d", u)[0]))
    for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
        services.append(bluetooth.UUID(u))
    return services


def demo():
    payload = advertising_payload(
        name="micropython",
        services=[bluetooth.UUID(0x181A), bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")],
    )
    print(payload)
    print(decode_name(payload))
    print(decode_services(payload))


if __name__ == "__main__":
    demo()

第六步:验证光照传感器

接下来我们需要验证设备有没有正常工作,首先使用串口终端查看程序运行情况,终端上打印了设备发送的 Service Data,以及光照强度和光照传感器 ADC 值:

接下来用手机下载一个蓝牙调试 APP,原文推荐了 3 款(LightBlue、nRFConnect、BLEScanner),也可以随便下一个其他类似的软件。

设备的蓝牙名称为 “Nodemcu”,这是在 BLELightSensor 类的 __init__() 函数中设定的,

查看蓝牙的广播包,感觉有点问题。。。(难道是调试助手的问题?)

标签: 光敏传感器连线

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台