仅作为本人学习《物联网开发实战》的学习笔记,原课程链接:极客时间《物联网开发实战》——郭朝斌
文章目录
- 第一步:通信技术
- 第二步:选择开发板
- 第三步:准备 MicroPython 环境
- 第四步:搭建光照传感器硬件电路
- 第五步:编写蓝牙程序
- 第六步:验证光照传感器
第一步:通信技术
郭老师建议选择 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 Service 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(",uuid,fc,pid,fcnt)+mac[1]+struct.pack(",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(", 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(", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
services.append(bluetooth.UUID(struct.unpack(", 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__()
函数中设定的,
查看蓝牙的广播包,感觉有点问题。。。(难道是调试助手的问题?)
本文章来源于互联网,如有侵权,请联系删除!