本帖最后由 dql2015 于 2023-2-5 20:33 编辑
一、概述
感谢21ic和贸泽电子的DIY设计活动,非常荣幸能够入选。本次DIY活动作品是基于NUCLEO-WB55RG的室内环境监测,本设计STM32WB55RG提供I2C接口读取传感器模块SGP30数据,通过运行蓝牙GATT服务器,作为蓝牙中央设备,微信小程序作为客户端连接GATT服务器,通过定义的服务特征值以通知方式完成传感器采集数据的发送,微信小程序订阅通知。
二、系统设计
2.1硬件
NUCLEO-WB55RG开发板:NUCLEO-WB55RG 是意法半导体ST家的无线MCU开发板,板载STM32WB55RG,1MB Flash、256KB SRAM,双核(Arm® Cortex®-M4 and dedicated M0+),支持低功耗蓝牙BLE5.2。
SGP30传感器模块:Grove-VOC和eCO2气体传感器(SGP30)是一种空气质量检测传感器。 该模块基于SGP30,提供TVOC(总挥发性有机化合物)当量输出和CO2当量输出,SGP30是一款数字多气体传感器,传感器的CMOSens®技术在单个芯片上提供完整的传感器系统,并带有I2C接口、温度控制的微型加热板和两个预处理的室内空气质量信号。
Parameter | Signal | Values | 工作电压 | 3.3V/5V | 输出范围 | TVOC | 0 ppb to 60000ppb | CO₂当量 | 400 ppm to 60000 ppm |
采样频率 | TVOC | 1HZ | CO₂当量 | 1HZ |
Resolution
|
TVOC | 0 - 2008 ppb / 1 ppb | 2008 - 11110 ppb / 6 ppb | 11110 - 60000 ppb / 32 ppb |
CO₂eq | 400 - 1479 ppm / 1 ppm | 1479 -5144 ppm / 3 ppm | 5144 - 17597 ppm / 9 ppm | 17597 - 60000 ppm / 31 ppm | Default I2C address | 0X58 |
2.2软件
本设计软件分为2部分,1是stm32wb55rg软件的开发,2是微信小程序的开发。
2.2.1MCU软件
MCU软件使用Mbed Studio集成开发环境开发,基于mbed os及其丰富的中间件,可以十分方便的开发应用程序,该平台使用C++开发,在程序较为复杂的情况下使用C++能够提高开发效率。
设计类MyService,实例化服务对象_my_service("51311102-030e-485f-b122-f8f381aa84ed",实例化特征值_my_char("8dd6a1b7-bc75-4741-8a26-264af75807de"),
蓝牙写特征值数据接收回调when_data_written
void when_data_written(const GattWriteCallbackParams *e)
{
printf("data written:\r\n");
printf("\tconnection handle: %u\r\n", e->connHandle);
printf("\tattribute handle: %u", e->handle);
if (e->handle == _my_char.getValueHandle())
{
printf(" (second characteristic)\r\n");
}
else
{
printf("\r\n");
}
printf("\twrite operation: %u\r\n", e->writeOp);
printf("\toffset: %u\r\n", e->offset);
printf("\tlength: %u\r\n", e->len);
printf("\t data: ");
for (size_t i = 0; i < e->len; ++i)
{
printf("%02X", e->data[i]);
if(e->data[0]=='1')
{
led2.write(1);
printf("\t led on");
}
if(e->data[0]=='2')
{
led2.write(0);
printf("\t led off");
}
}
printf("\r\n");
}
里面可以添加用户业务逻辑,本设计这里接收到字符1就点亮led2,收到字符2就熄灭led2。通过定时调用将数据以通知的形式发送到蓝牙客户端:
void update_data(void)
{
uint8_t second[11] = {0};
ble_error_t err;
sprintf((char*)second, "%02x%04x%04x", bat,CO2,TVOC);
err = _my_char.set(*_server, second,10);
if (err)
{
printf("write of the second value returned error %u\r\n", err);
return;
}
}
调用特征值_my_char对象set方法将数据发送出去。
传感器数据采集线程里面首先初始化传感器,然后周期性的读取传感器数据保存到全局变量里面。
void sgp30_work_thread()
{
int ret;
SGP30 *sgp30 = new SGP30(I2C_SCL, I2C_SDA);
DigitalOut led1(LED1);
uint8_t ID[6] = {0};
while (sgp30->sgp30_init() < 0)
{
printf(" sgp30 init fail\r\n");
wait_ms(1000);
}
if (sgp30->sgp30_get_serial_id(ID) < 0)
{
printf(" sgp30 read serial id failed\r\n");
}
else
{
printf("SGP30 Serial number: ");
for (int i = 0; i < 6; i++)
printf("%02X", ID[i]);
printf("\r\n");
}
printf("sgp30 wait air for init");
fflush(stdout);
do
{
ret = sgp30->sgp30_read(&CO2, &TVOC);
if (ret < 0)
{
printf("SGP30 read failed,ret=%d\r\n", ret);
}
else
{
printf("-");
fflush(stdout);
}
} while (TVOC == 0 && CO2 == 400);
printf("\r\n");
while (true)
{
ret = sgp30->sgp30_read(&CO2, &TVOC);
if (ret < 0)
{
printf(" sgp30 read fail,ret=%d\r\n", ret);
}
else
{
printf("CO2:%5dppm TVOC:%5dppb\r\n", CO2, TVOC);
}
led1 = !led1;
ThisThread::sleep_for(1000);
}
}
main.cpp完整代码如下:
#include <cstdint>
#include <cstring>
#include <stdio.h>
#include "events/EventQueue.h"
#include "platform/Callback.h"
#include "platform/NonCopyable.h"
#include "BLEProcess.h"
#include "ble/BLE.h"
#include "ble/Gap.h"
#include "ble/GapAdvertisingData.h"
#include "ble/GapAdvertisingParams.h"
#include "ble/GattClient.h"
#include "ble/GattServer.h"
#include "mbed.h"
#include "SGP30.h"
using mbed::callback;
uint8_t bat = 0;
uint16_t CO2 = 0;
uint16_t TVOC = 0;
DigitalOut led2(LED2);
class MyService {
typedef MyService Self;
public:
MyService()
: _my_service("51311102-030e-485f-b122-f8f381aa84ed",_my_characteristics,sizeof(_my_characteristics) / sizeof(_my_characteristics[0])),
_my_char("8dd6a1b7-bc75-4741-8a26-264af75807de"),
_server(NULL), _event_queue(NULL) {
// update internal pointers (value, descriptors and characteristics array)
_my_characteristics[0] = &_my_char;
// setup authorization handlers
//_my_char.setWriteAuthorizationCallback(this,&Self::authorize_client_write);
}
void start(BLE &ble_interface, events::EventQueue &event_queue)
{
if (_event_queue)
{
return;
}
_server = &ble_interface.gattServer();
_event_queue = &event_queue;
// register the service
printf("Adding demo service\r\n");
ble_error_t err = _server->addService(_my_service);
if (err)
{
printf("Error %u during demo service registration.\r\n", err);
return;
}
// read write handler
_server->onDataSent(as_cb(&Self::when_data_sent));
_server->onDataWritten(as_cb(&Self::when_data_written));
_server->onDataRead(as_cb(&Self::when_data_read));
// updates subscribtion handlers
_server->onUpdatesEnabled(as_cb(&Self::when_update_enabled));
_server->onUpdatesDisabled(as_cb(&Self::when_update_disabled));
_server->onConfirmationReceived(as_cb(&Self::when_confirmation_received));
// print the handles
printf("my service registered\r\n");
printf("service handle: %u\r\n", _my_service.getHandle());
printf("\tmy characteristic value handle %u\r\n",_my_char.getValueHandle());
_event_queue->call_every(3000,callback(this, &Self::update_data));
}
private:
/**
* Handler called when a notification or an indication has been sent.
*/
void when_data_sent(unsigned count)
{
//printf("sent %u updates\r\n", count);
}
/**
* Handler called after an attribute has been written.
*/
void when_data_written(const GattWriteCallbackParams *e)
{
printf("data written:\r\n");
printf("\tconnection handle: %u\r\n", e->connHandle);
printf("\tattribute handle: %u", e->handle);
if (e->handle == _my_char.getValueHandle())
{
printf(" (second characteristic)\r\n");
}
else
{
printf("\r\n");
}
printf("\twrite operation: %u\r\n", e->writeOp);
printf("\toffset: %u\r\n", e->offset);
printf("\tlength: %u\r\n", e->len);
printf("\t data: ");
for (size_t i = 0; i < e->len; ++i)
{
printf("%02X", e->data[i]);
if(e->data[0]=='1')
{
led2.write(1);
printf("\t led on");
}
if(e->data[0]=='2')
{
led2.write(0);
printf("\t led off");
}
}
printf("\r\n");
}
/**
* Handler called after an attribute has been read.
*/
void when_data_read(const GattReadCallbackParams *e)
{
printf("data read:\r\n");
printf("\tconnection handle: %u\r\n", e->connHandle);
printf("\tattribute handle: %u", e->handle);
if (e->handle == _my_char.getValueHandle())
{
printf(" (my characteristic)\r\n");
}
else
{
printf("\r\n");
}
}
/**
* Handler called after a client has subscribed to notification or indication.
*
* @param handle Handle of the characteristic value affected by the change.
*/
void when_update_enabled(GattAttribute::Handle_t handle)
{
printf("update enabled on handle %d\r\n", handle);
}
/**
* Handler called after a client has cancelled his subscription from notification or indication.
*
* @param handle Handle of the characteristic value affected by the change.
*/
void when_update_disabled(GattAttribute::Handle_t handle)
{
printf("update disabled on handle %d\r\n", handle);
}
/**
* Handler called when an indication confirmation has been received.
*
* @param handle Handle of the characteristic value that has emitted the indication.
*/
void when_confirmation_received(GattAttribute::Handle_t handle)
{
printf("confirmation received on handle %d\r\n", handle);
}
/**
* Handler called when a write request is received.
*
* This handler verify that the value submitted by the client is valid before authorizing the operation.
*/
void authorize_client_write(GattWriteAuthCallbackParams *e)
{
printf("characteristic %u write authorization\r\n", e->handle);
if (e->offset != 0)
{
printf("Error invalid offset\r\n");
e->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_OFFSET;
return;
}
if (e->len != 1)
{
printf("Error invalid len\r\n");
e->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
return;
}
/*if ((e->data[0] >= 60) ||
((e->data[0] >= 24) && (e->handle == _hour_char.getValueHandle()))) {
printf("Error invalid data\r\n");
e->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
return;
}*/
e->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
}
/**
* update the data to notify.
*/
void update_data(void)
{
uint8_t second[11] = {0};
ble_error_t err;
/*err = _my_char.get(*_server, second);
if (err)
{
printf("read of the second value returned error %u\r\n", err);
return;
}*/
sprintf((char*)second, "%02x%04x%04x", bat,CO2,TVOC);
err = _my_char.set(*_server, second,10);
if (err)
{
printf("write of the second value returned error %u\r\n", err);
return;
}
else
{
//printf("data:%s\r\n",second);
}
}
private:
/**
* Helper that construct an event handler from a member function of this instance.
*/
template <typename Arg>
FunctionPointerWithContext<Arg> as_cb(void (Self::*member)(Arg))
{
return makeFunctionPointer(this, member);
}
/**
* Read, Write, Notify, Indicate Characteristic declaration helper.
*
* @tparam T type of data held by the characteristic.
*/
template <typename T>
class ReadWriteNotifyIndicateCharacteristic : public GattCharacteristic {
public:
/**
* Construct a characteristic that can be read or written and emit notification or indication.
*
* @param[in] uuid The UUID of the characteristic.
* @param[in] initial_value Initial value contained by the characteristic.
*/
ReadWriteNotifyIndicateCharacteristic(const UUID &uuid)
: GattCharacteristic(
/* UUID */ uuid,
/* Initial value */ _value,
/* Value size */ sizeof(_value),
/* Value capacity */ sizeof(_value),
/* Properties */
GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ |
GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE_WITHOUT_RESPONSE |
GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY,
/* Descriptors */ NULL,
/* Num descriptors */ 0,
/* variable len */ true){}
/**
* Get the value of this characteristic.
*
* @param[in] server GattServer instance that contain the characteristic
* value.
* @param[in] dst Variable that will receive the characteristic value.
*
* [url=home.php?mod=space&uid=266161]@return[/url] BLE_ERROR_NONE in case of success or an appropriate error code.
*/
ble_error_t get(GattServer &server, T &dst) const
{
uint16_t value_length = sizeof(dst);
return server.read(getValueHandle(), &dst, &value_length);
}
/**
* Assign a new value to this characteristic.
*
* @param[in] server GattServer instance that will receive the new value.
* @param[in] value The new value to set.
* @param[in] local_only Flag that determine if the change should be kept
* locally or forwarded to subscribed clients.
*/
ble_error_t set(GattServer &server, const uint8_t value[],int len,bool local_only = false) const
{
return server.write(getValueHandle(), value, len, local_only);
}
private:
uint8_t _value[10];
};
ReadWriteNotifyIndicateCharacteristic<uint8_t> _my_char;
// list of the characteristics of the clock service
GattCharacteristic *_my_characteristics[1];
// demo service
GattService _my_service;
GattServer *_server;
events::EventQueue *_event_queue;
};
Thread thread1;
Thread thread2;
Thread thread3;
Thread thread4;
void sgp30_work_thread()
{
int ret;
SGP30 *sgp30 = new SGP30(I2C_SCL, I2C_SDA);
DigitalOut led1(LED1);
uint8_t ID[6] = {0};
while (sgp30->sgp30_init() < 0)
{
printf(" sgp30 init fail\r\n");
wait_ms(1000);
}
if (sgp30->sgp30_get_serial_id(ID) < 0)
{
printf(" sgp30 read serial id failed\r\n");
}
else
{
printf("SGP30 Serial number: ");
for (int i = 0; i < 6; i++)
printf("%02X", ID[i]);
printf("\r\n");
}
printf("sgp30 wait air for init");
fflush(stdout);
do
{
ret = sgp30->sgp30_read(&CO2, &TVOC);
if (ret < 0)
{
printf("SGP30 read failed,ret=%d\r\n", ret);
}
else
{
printf("-");
fflush(stdout);
}
} while (TVOC == 0 && CO2 == 400);
printf("\r\n");
while (true)
{
ret = sgp30->sgp30_read(&CO2, &TVOC);
if (ret < 0)
{
printf(" sgp30 read fail,ret=%d\r\n", ret);
}
else
{
printf("CO2:%5dppm TVOC:%5dppb\r\n", CO2, TVOC);
}
led1 = !led1;
ThisThread::sleep_for(1000);
}
}
void led_thread()
{
DigitalOut *led2 = new DigitalOut(LED2);
//DigitalOut led3(LED3);
while (true)
{
led2->write(led2->read() == 1 ? 0 : 1);
// ThisThread::sleep_for(200);
// led3 = !led3;
wait_ms(200);
}
}
void key_thread()
{
DigitalIn sw1(PC_4, PullUp);
DigitalIn sw2(PD_0, PullUp);
DigitalIn sw3(PD_1, PullUp);
while (true)
{
if (sw1.read() == 0)
{
printf("sw1\r\n");
}
if (sw2.read() == 0)
{
printf("sw2\r\n");
}
if (sw3.read() == 0)
{
printf("sw3\r\n");
}
ThisThread::sleep_for(20);
}
}
void get_bat_thread()
{
AnalogIn ain(A0);
float bat_f;
while (true)
{
bat_f = ain.read();
bat=(uint8_t)(bat_f*100);
//printf("ain: %f - %d\n", bat_f,bat);
ThisThread::sleep_for(2000);
//wait_ms(500);
}
}
int main()
{
thread1.start(sgp30_work_thread);
//thread2.start(led_thread);
thread3.start(get_bat_thread);
//thread4.start(key_thread);
BLE &ble_interface = BLE::Instance();
events::EventQueue event_queue;
MyService demo_service;
BLEProcess ble_process(event_queue, ble_interface);
ble_process.on_init(callback(&demo_service, &MyService::start));
// bind the event queue to the ble interface, initialize the interface and start advertising
ble_process.start();
// Process the event queue.
event_queue.dispatch_forever();
return 0;
}
2.2.2微信小程序
微信小程序用于搜索蓝牙设备、建立连接,订阅通知,显示接收数据。连接蓝牙设备成功后将查询服务和该服务下的特征值列表,根据MCU中定义的特征值,订阅该特征值的通知属性,这样就可以接收到蓝牙设备发送的数据了。
本设计定义了2个页面,设置页面和设备信息显示页面,不同页面间通过event库实现通信:
//接收别的页面传过来的数据
event.on('EnvMonitorSendData2Device', this, function(data) {
//另外一个页面传过来的data是16进制字符串形式
console.log("要发送给蓝牙设备的数据:"+data);
var buffer=that.stringToBytes(data);
var dataView = new Uint8Array(buffer)
dataView = data;
wx.writeBLECharacteristicValue({
deviceId: app.globalData._deviceId,//蓝牙设备 id
serviceId: app.globalData._serviceId,//蓝牙特征值对应服务的 uuid
characteristicId: app.globalData._writeCharacteristicId,//蓝牙特征值的 uuid
value: buffer,//ArrayBuffer 蓝牙设备特征值对应的二进制值
success: function (res) {//接口调用成功的回调函数
console.log('发送成功')
},
fail: function(res) {//接口调用失败的回调函数
//发送蓝牙数据失败
console.log('发送失败')
}
}
)
})
接收到通知数据后,进行转换后送到页面显示:
//接收别的页面传过来的数据
event.on('environmetDataChanged', this, function(data) {
//另外一个页面传过来的data是16进制字符串形式
console.log("接收到蓝牙设备发来的数据:"+data)
//bat 1byte
var aa=parseInt(data[1]+data[3],16);
//co2 4byte
var f=parseInt(data[5]+data[7],16);
var g=parseInt(data[9]+data[11],16);
var bb=f*256+g;
//tvoc 4byte
var i=parseInt(data[13]+data[15],16);
var j=parseInt(data[17]+data[19],16);
var cc=i*256+j;
//实时修改显示值
var up0 = "charts[" + 0 + "].data";
var up1 = "charts[" + 1 + "].data";
var up2 = "charts[" + 2 + "].data";
this.setData({
[up0]:aa,
[up1]:bb,
[up2]:cc,
});
})
},
三、调试与DIY展示
连接好线路,首先使用蓝牙调试助手进行设备连接、数据收发的简单测试,
点击接收通知按钮,就可以看到接收的通知数据了:
点击write按钮,发送字符1,可以看到收到了数据且led2点亮了:
四、总结
初次接触ST家的无线MCU产品,通过厂家的培训PPT资料能够快速上手STM32WB55的开发,丰富的开发生态给不懂蓝牙底层协议的开发者解决了很多难题。通过本次DIY活动,对ST家的无线MCU产品有了初步的认识,通过傻瓜化的开发生态能够快速实现创意,但是对于蓝牙等无线协议底层知识还是缺乏的,如果想把创意产品化还是需要多多学习蓝牙底层知识。
小程序工程源码:
voc_demo.rar
(364.36 KB)
stm32wb55rg工程源码:
mbed-os_stm32wb55rg_ble-GattServer_wechat.rar
(4.34 MB)
|
共1人点赞
|