原课程链接仅作为我学习物联网发展实战的学习笔记:极客时间物联网发展实战-郭朝斌
第一步:通信技术
郭老师建议选择 BLE 低功耗蓝牙技术作为光传感器设备的通信手段。由于传感器的部署位置灵活,无线通信技术不能直接使用电源线,而是因为 BLE 的功耗比 Wi-Fi 低,所以我们选择 BLE 通信技术。
BLE 设备可以在 4 工作模式:
- (Boradcaster),简单的广播模式。在这种模式下,设备不能连接,只能在一定的时间间隔内广播数据,用于其他设备,如手机扫描处理。
- (Peripheral),在这种模式下,设备仍然可以同时广播数据和连接。建立连接后,双方可以进行双向通信。
- (Central),在这种模式下,设备不能广播,但可以扫描周围的蓝牙广播包,找到其他设备,然后主动连接。
- (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__()
函数中设定的,
查看蓝牙的广播包,感觉有点问题。。。(难道是调试助手的问题?)