dql2015 发表于 2023-2-5 20:33

我的无线DIY设计-基于NUCLEO-WB55RG的室内环境监测

本帖最后由 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接口、温度控制的微型加热板和两个预处理的室内空气质量信号。


ParameterSignalValues
工作电压3.3V/5V
输出范围TVOC0 ppb to 60000ppb
CO₂当量400 ppm to 60000 ppm


采样频率TVOC1HZ
CO₂当量1HZ







Resolution


TVOC0 - 2008 ppb / 1 ppb
2008 - 11110 ppb / 6 ppb
11110 - 60000 ppb / 32 ppb



CO₂eq400 - 1479 ppm / 1 ppm
1479 -5144 ppm / 3 ppm
5144 - 17597 ppm / 9 ppm
17597 - 60000 ppm / 31 ppm
Default I2C address0X58

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);
      if(e->data=='1')
      {
          led2.write(1);
          printf("\t led on");
      }
   if(e->data=='2')
      {
         led2.write(0);
          printf("\t led off");
      }
    }
    printf("\r\n");
}里面可以添加用户业务逻辑,本设计这里接收到字符1就点亮led2,收到字符2就熄灭led2。通过定时调用将数据以通知的形式发送到蓝牙客户端:
void update_data(void)
{
    uint8_t second = {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 = {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);
    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)),
      _my_char("8dd6a1b7-bc75-4741-8a26-264af75807de"),
      _server(NULL), _event_queue(NULL) {
    // update internal pointers (value, descriptors and characteristics array)
    _my_characteristics = &_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);
      if(e->data=='1')
      {
          led2.write(1);
          printf("\t led on");
      }
   if(e->data=='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 >= 60) ||
      ((e->data >= 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 = {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, IndicateCharacteristic 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 uuid The UUID of the characteristic.
   * @param 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 server GattServer instance that contain the characteristic
   * value.
   * @param dst Variable that will receive the characteristic value.
   *
   * @return 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 server GattServer instance that will receive the new value.
   * @param value The new value to set.
   * @param 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;
};

ReadWriteNotifyIndicateCharacteristic<uint8_t> _my_char;

// list of the characteristics of the clock service
GattCharacteristic *_my_characteristics;

// 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 = {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);
    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+data,16);

      //co2 4byte
      var f=parseInt(data+data,16);
      var g=parseInt(data+data,16);
      var bb=f*256+g;

      //tvoc 4byte
      var i=parseInt(data+data,16);
      var j=parseInt(data+data,16);
      var cc=i*256+j;

      //实时修改显示值
      var up0 = "charts[" + 0 + "].data";
      var up1 = "charts[" + 1 + "].data";
      var up2 = "charts[" + 2 + "].data";
      this.setData({
      :aa,
      :bb,
      :cc,
      });
    })
},
三、调试与DIY展示
连接好线路,首先使用蓝牙调试助手进行设备连接、数据收发的简单测试,



点击接收通知按钮,就可以看到接收的通知数据了:

点击write按钮,发送字符1,可以看到收到了数据且led2点亮了:



四、总结
初次接触ST家的无线MCU产品,通过厂家的培训PPT资料能够快速上手STM32WB55的开发,丰富的开发生态给不懂蓝牙底层协议的开发者解决了很多难题。通过本次DIY活动,对ST家的无线MCU产品有了初步的认识,通过傻瓜化的开发生态能够快速实现创意,但是对于蓝牙等无线协议底层知识还是缺乏的,如果想把创意产品化还是需要多多学习蓝牙底层知识。

小程序工程源码:

stm32wb55rg工程源码:





dql2015 发表于 2023-2-5 20:34

本帖最后由 dql2015 于 2023-2-5 20:39 编辑

视频插入被吞了,放个链接https://www.bilibili.com/video/BV18T411X7HE/?vd_source=ca97f470e8475c0e94ca830168f6017b

WoodData 发表于 2023-2-6 08:32

学习学习
页: [1]
查看完整版本: 我的无线DIY设计-基于NUCLEO-WB55RG的室内环境监测