打印
[活动]

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

[复制链接]
787|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 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)




使用特权

评论回复

相关帖子

沙发
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 | 只看该作者
学习学习

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

101

主题

375

帖子

8

粉丝