【教程】M5Stack ATOM与ai2制作图形化蓝牙心率监测-Arduino中文社区 - Powered by Discuz!

Arduino中文社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 4766|回复: 4

【教程】M5Stack ATOM与ai2制作图形化蓝牙心率监测

[复制链接]
发表于 2021-8-15 20:49 | 显示全部楼层 |阅读模式

        M5StackATOM与ai2制作图形化蓝牙心率监测

手机运行.jpg
【项目故事】
曾几何时,我感叹过就极客创作而言,PC和单片机的界限在不断逼近和模糊,小电脑(比如树莓派)可以直接控制IO,而强悍的单片机(如esp32)也已经可以运行micropython这样的解释型语言。两者在极客创作中不断上演精彩的剧目。而作为人人标配的手机,其实也是一个不错的极客创作平台。只不过它需要等待一个触发的条件,那就是App Inventor的到来。
传统的安卓APP开发工具要求玩家具有比较高的软件基础,进入门槛较高。而谷歌推出的App Inventor2.0(简称ai2,为了区别于人工智能的缩写)就是一款非常友好的极客APP开发工具,这个项目原本是Google实验室(Google Lab)的一个子计划,是完全在线开发的Android编程环境,也是一款安卓APP的积木式开发工具。大幅降低了开发门槛,使得没有受过严格软件训练的业余爱好者也可以尝试编写属于自己的安卓APP应用程序。对于我来说,更加看重ai2可以通过蓝牙、串口等方式与单片机交互。从而实现“软硬结合”的丰富应用。
比如:ai2负责手机端的人机交互、数据处理以及展示,单片机侧实现数据采集和上送,并且执行ai2下发的控制指令。各自发挥优势,相得益彰。
我曾经写过一个将手机的位置传感器信息通过蓝牙下发给esp32去展示,从而实现了一块运动手表的文章。https://www.arduino.cn/thread-102926-1-1.html
足见ai2+手机与单片机是可以双向交互的,只要你想得到,可以开发出大量有趣好玩的应用玩具。
App Inventor 2012年1月1日移交给麻省理工学院,目前在国内也有比较可靠的官方服务器(完全免费)可供开发者使用。
故事的另一个主角是M5Stack出品的ATOM Lite(以下简称ATOM),这是一个基于esp32的轻量级开发平台,集成了一个全彩led以及一只用户自定义按钮,没有配备电池,需要外部供电。
在本文中,一部安卓手机+ai2与一片esp32(M5Stack出品的ATOM)就可以非常方便地搭建一个图形化的心率监测系统。
【B站视频链接】
我们先看看视频演示,然后就一起开始项目制作的过程吧。
项目有趣度:★★★
项目难度:★★
仍然是一款容易上手、颇有趣味的小玩具。
【软硬件准备】
软件部分:
  
  
内容
版本
备注
1
App Inventor
2.0
手机端APP开发
2
Arduino IDE
1.8.5
ATOM开发
硬件部分
  
  
内容
厂商
备注
1
Google Pixel2 手机
Google
2
ATOM
M5Stack
3
BLE心率胸带
KYTO
来自网购
4
OTG转接头
绿联
,用于连接手机与ATOM,包括一根Type-C线
【软件基本原理】

ai2部分
ai2流程图.jpg

ATOM部分

atom流程图.jpg
【硬件连接】

将手机与ATOM通过OTG转接头进行连接,同时也实现了给ATOM供电,可谓一举两得。
IMG_20210814_221055_a.jpg IMG_20210814_221618a.jpg

【主要知识点】
1、 esp32读取ble心率带数据:这是一个老知识点,已经很成熟解决。
2、 esp32通过10组串口将数据发送至手机:由于需要在手机上用折线图展示,所以就生产了一组10个元素的数组,每采集一个新数据,就把这个队列向后顺延一位。从而在手机端呈现出数据列整体顺移的效果。
3、 图形展示:手机端,ai2使用了google家的chartmaker插件,可以实现折线、饼图以及柱状图等三种功能,我们这次使用折线图,折线图可以从csv格式的数据生成列表,从而展示。这个环节也是最关键的点,就是把ATOM采集到的十个数据用分隔符隔离,装配好,然后导出列表,供chart maker插件展示。

【ai2的程序设计】
ai2UI设计_副本.jpg 逻辑1_副本.jpg 逻辑2_副本.jpg 逻辑3_副本.jpg
注:1、webview控件,显示折线图的位置;2、折线图绘制按钮;3、5 串口开关按钮;4、显示收到ATOM的数据。
【开源代码】

ATOM部分

  1. /*这是AI2编写的APP,用以展示OTG-serial接收的Arduino数据
  2. *
  3. * 07-24 解决了AI2接收串口数据、展示,解决了115200波特率更改
  4. * 07-25 测试一组随机心率数据,传送至SoftwareSerial,由AI2展示
  5. * 07-25 将BLE心率传感器结合进来,心率数据发送至手机端展示
  6. * *作者:沧海
  7. */


  8. //===== Run-Time variables =====//
  9. unsigned long prevTime   = 0;
  10. unsigned long curTime    = 0;
  11. unsigned long hrms       = 0;//全局变量-心率值
  12. unsigned int val[11];  //定义一个10元素的数组,1~10
  13. String SendText="" ;

  14. //BLE部分设置
  15. #include "BLEDevice.h"
  16. // BLE
  17. // The remote HRM service we wish to connect to.
  18. static  BLEUUID serviceUUID(BLEUUID((uint16_t)0x180D));
  19. // The HRM characteristic of the remote service we are interested in.
  20. static  BLEUUID    charUUID(BLEUUID((uint16_t)0x2A37));
  21. static BLEAddress *pServerAddress;
  22. static boolean doConnect = false;
  23. static boolean connected = false;
  24. static boolean notification = false;
  25. static BLERemoteCharacteristic* pRemoteCharacteristic;

  26. // TypeDef
  27. typedef struct {
  28.   char ID[20];
  29.   uint16_t HRM;
  30. }HRM;
  31. HRM hrm;



  32. //--------------------------------------------------------------------------------------------
  33. // BLE notifyCallback 在此Callback函数中,读取心率带数据,然后给全局变量unsigned long hrms 赋值
  34. //--------------------------------------------------------------------------------------------
  35. static void notifyCallback( BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
  36.     hrm.HRM = pData[1];
  37.     String hrm_str =  String(pData[1], DEC);   
  38.     hrms=hrm_str.toInt();
  39. }

  40. //--------------------------------------------------------------------------------------------
  41. //  Connect to BLE HRM
  42. //--------------------------------------------------------------------------------------------
  43. bool connectToServer(BLEAddress pAddress) {
  44.     Serial.print(F("Forming a connection to "));
  45.     Serial.println(pAddress.toString().c_str());

  46.     BLEClient*  pClient  = BLEDevice::createClient();
  47.     Serial.println(F(" - Created client"));
  48.    
  49.     // Connect to the HRM BLE Server.
  50.     pClient->connect(pAddress);
  51.     Serial.println(F(" - Connected to server"));

  52.     // Obtain a reference to the service we are after in the remote BLE server.
  53.     BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  54.     if (pRemoteService == nullptr) {
  55.       Serial.print(F("Failed to find our service UUID: "));
  56.       Serial.println(serviceUUID.toString().c_str());
  57.       return false;
  58.     }
  59.     Serial.println(F(" - Found our service"));


  60.     // Obtain a reference to the characteristic in the service of the remote BLE server.
  61.     pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
  62.     if (pRemoteCharacteristic == nullptr) {
  63.       Serial.print(F("Failed to find our characteristic UUID: "));
  64.       Serial.println(charUUID.toString().c_str());
  65.       return false;
  66.     }
  67.     Serial.println(F(" - Found our characteristic"));

  68.     // Register for Notify
  69.     pRemoteCharacteristic->registerForNotify(notifyCallback);
  70. }

  71. //--------------------------------------------------------------------------------------------
  72. // Scan for BLE servers and find the first one that advertises the service we are looking for.
  73. //--------------------------------------------------------------------------------------------
  74. class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  75. /**
  76.    * Called for each advertising BLE server.
  77.    */
  78.   void onResult(BLEAdvertisedDevice advertisedDevice) {
  79.     Serial.print(F("BLE Advertised Device found: "));
  80.     Serial.println(advertisedDevice.toString().c_str());

  81.     // We have found a device, let us now see if it contains the service we are looking for.
  82.     if (advertisedDevice.haveServiceUUID() && advertisedDevice.getServiceUUID().equals(serviceUUID)) {

  83.       //
  84.       Serial.print(F("Found our device!  address: "));
  85.       advertisedDevice.getScan()->stop();

  86.       pServerAddress = new BLEAddress(advertisedDevice.getAddress());
  87.       doConnect = true;

  88.     } // Found our server
  89.   } // onResult
  90. }; // MyAdvertisedDeviceCallbacks


  91. void setup() {
  92.   Serial.begin(115200);     // 打开串口,设置数据传输速率115200
  93. //  mySerial.begin(115200);
  94.   Serial.println("Begin...");
  95.     for (int i = 1; i < 11; i++) {
  96.       val[i] = 0;
  97.     }
  98.      //Start BLE--BLE部分初始化
  99.   // Retrieve a Scanner and set the callback we want to use to be informed when we
  100.   // have detected a new device.  Specify that we want active scanning and start the
  101.   // scan to run for 30 seconds.
  102.   BLEDevice::init("");
  103.   BLEScan* pBLEScan = BLEDevice::getScan();
  104.   pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  105.   pBLEScan->setActiveScan(true);
  106.   pBLEScan->start(30);
  107.   
  108. }

  109. void loop() {
  110.    //---------BLE部分
  111.   curTime = millis();  //当前时间计时
  112.   // If the flag "doConnect" is true then we have scanned for and found the desired
  113.   // BLE Server with which we wish to connect.  Now we connect to it.  Once we are
  114.   // connected we set the connected flag to be true.
  115.   if (doConnect == true) {
  116.     if (connectToServer(*pServerAddress)) {
  117.       Serial.println(F("We are now connected to the BLE HRM"));
  118.       connected = true;
  119.     } else {
  120.       Serial.println(F("We have failed to connect to the HRM; there is nothin more we will do."));
  121.     }
  122.     doConnect = false;
  123.   }
  124.   //every 3 second
  125.   if (curTime - prevTime >= 3000) {
  126.     // Turn notification on
  127.   if (connected) {
  128.     if (notification == false) {
  129.       Serial.println(F("Turning Notifocation On"));
  130.       const uint8_t onPacket[] = {0x1, 0x0};
  131.       pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)onPacket, 2, true);
  132.       notification = true;
  133.     }
  134.   }
  135.     prevTime = curTime;

  136.    
  137.   //randNumber = random(60, 90);// print a random number from 60 to 90
  138.   //Serial.println(randNumber);
  139.   //Serial.println(hrms);
  140.   //第一步:整体左移所有的心率值
  141.     for (int i = 1; i < 10; i++) {
  142.       val[i] = val[i + 1];
  143.       //Serial.println(val[i]);
  144.     }
  145.     val[10] = hrms;//最新心率数据放入val[10]
  146.   //第二步:组装一个SendText,并且循环至10
  147.     for (int i = 1; i < 10; i++) {
  148.       SendText=SendText+String(i)+","+String(val[i])+"*";
  149.     }
  150.       SendText=SendText+String(10)+","+String(val[10]);
  151.    
  152. // 第三步:打印至mySerial,供AI2 chart展示
  153.     Serial.println(SendText);
  154.     //mySerial.println(SendText);
  155.     SendText="";
  156.     hrms    = 0;

  157.    delay(5000);
  158.   }
  159. }
复制代码



【小结】
这个玩具和上次手机+ai2与M5StackC构成的GPS运动手表是一个风格,那就是硬件基本不需要自己搭建,通过与手机(或者单片机)传感器的交互,实现你需要的功能。
上次信息流是从手机到单片机,这次是蓝牙心率带通过单片机传送到手机,并且加以展示,正好从两个角度来展示,手机+ai2与单片机之间的种种可能。
我喜欢“软硬结合”的玩法。也期待和大家交流,碰撞出更多的创意火花。
立秋已至,秋风渐凉。硕果累累。
沧海抱拳。

我将ai2的源程序、ATOM源代码都打包作为附件,附在文章最后,供玩家参考。
apk以及chart maker插件超出了论坛限制,大家可以自行Bulid和搜索。chart maker插件是谷歌制作的图表插件。
serial_0725_hrms.zip (198.67 KB, 下载次数: 7)


发表于 2021-8-15 23:42 | 显示全部楼层

沧海笑1122老师写的文章条理清晰,逻辑严密值得推荐,一起学习一起进步。
发表于 2021-8-16 08:25 | 显示全部楼层
                                            
学习了,我只有这种,还没做过实验



0-.jpg


 楼主| 发表于 2021-8-16 10:54 | 显示全部楼层
本帖最后由 沧海笑1122 于 2021-8-16 10:55 编辑
eagler8 发表于 2021-8-16 08:25
学习了,我只有这种,还没做过实验

感谢支持
发表于 2021-8-16 11:15 | 显示全部楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|Archiver|手机版|Arduino中文社区

GMT+8, 2024-11-28 06:34 , Processed in 0.181914 second(s), 18 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表