查看: 1239|回复: 10

一款好玩的esp32极客手表(WatchIO)之BLE心率表

[复制链接]
  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

    发表于 2019-10-6 23:22 | 显示全部楼层 |阅读模式
    本帖最后由 沧海笑1122 于 2019-10-6 23:30 编辑

    一款好玩的esp32极客手表(WatchIO)之BLE心率表

    【故事缘由】
        我的好友小华师兄的新作,一款好玩的极客手表(WatchIO),基于esp32的core,是目前我遇到的最袖珍的极客手表。说是极客手表,也就是可编程、可折腾,并且消费品意义的手表。
       一张图我们了解一下这款袖珍的极客手表。
    intro.jpg
    我们看看它的尺寸。
    pcb.png
    对,没错,是31mm*22.6mm,比一个U盘还袖珍。
    手表到手后,想到还是延续我的智能穿戴想法,就把BLE心率表移植到了这块WatchIO,先看看小视频,了解一下工作情况吧。
    这是锻炼后恢复中的一个心率图形。
    http://player.youku.com/embed/XNDM4ODM4MzEzNg==


    【硬件准备】
    序号
    名称
    备注
    1
    WatchIO
    1
    是的,只有WatchIO,不需要别的配件。
    【软件准备】
    名称
    备注
    1
    Adafruit_GFX_Library
    Adafruit著名的图形库
    2
    Adafruit_ST7735_and_ST7789_Library
    AdafruitST7735库,在此我们用到的是7735
    3
    ESP32 BLE arduino
    已经被吸收为官方的esp32 ble
    【代码及注释】

    kittenblock中小学创客名师推荐的图形化编程软件

    /*
      ===========================================
           Copyright (c) 2018 Stefan Kremser
                  github.com/spacehuhn
      ===========================================
      (1)图形部分根据ESP8266 + OLED = WiFi Packet Monitor 修改
      (2)BLE蓝牙带部分,根据# ESP32 BLE HRM/MQTT Gateway Code for an ESP32 with an integrated OLED Connecting to a BLE HRM Monitor
         Links:- Video https://youtu.be/iCKIIMrphtg
      Date:
      2019-08-27 
                    1、利用watchIO平台,读取ble心率带数据并且显示
                    2、后续将心率数据以彩色图形方式显示(160*80)
       2019-10-04
       1、优化显示页面以及图形部分
       2019-10-05
       1、增加了11段渐变色显示心率图形
    
    */
    
    char c_hrm[]="";  //char 类型的hrm值,用于在手表显示器中显示字符数据
    
    //WatchIO手表配置
    #include "config.h"
    #include "power.h"
    #include "lcd.h"
    const uint16_t COLORS_LIGHT[10] = {
      0xFA55, 0x0C3E, 0xC01E, 0xF255, 0xF820,
      0xF321, 0xFFA0, 0x17A0, 0x04BF, 0xC01F
    };
    
    const uint32_t COLORS_DARK[10] = {
      0x40E6, 0x0128, 0x3809, 0x38C5, 0x4001,
      0x40E0, 0x4A20, 0x0220, 0x0108, 0x3008
    };
    
    //BLE部分设置
    #include "BLEDevice.h"
    const String sketchName = "ESP32 HRM BLE Client";
    static  BLEUUID serviceUUID(BLEUUID((uint16_t)0x180D));
    // The HRM characteristic of the remote service we are interested in.
    static  BLEUUID    charUUID(BLEUUID((uint16_t)0x2A37));
    static BLEAddress *pServerAddress;
    static boolean doConnect = false;
    static boolean connected = false;
    static boolean notification = false;
    static BLERemoteCharacteristic* pRemoteCharacteristic;
    
    unsigned long screen_update, stats_update;
    
    // TypeDef
    typedef struct {
      char ID[20];
      uint16_t HRM;
    }HRM;
    HRM hrm;
    
    /*===== GRAPH SETTINGS =====*/
    /* Display settings */
    #define minRow       0              /* default =   0 */
    #define maxRow     87              /* 局部刷新区88列 */
    #define minLine      0              /* default =   0 */
    #define maxLine    63             /* 局部刷新区64行 */
    
    //* render settings */此部分备用
    #define Row1         0
    #define Row2        30
    
    //#define LineText     0
    //#define Line        60
    #define LineVal    60 //按60做比例系数,<最高行数63
    
    //===== Run-Time variables =====//
    unsigned long prevTime   = 0;
    unsigned long curTime    = 0;
    unsigned long hrms       = 0;//全局变量-心率值
    unsigned long maxVal     = 0;
    double multiplicator     = 0.0;
    unsigned int val[90];  //数组size
    
    //计算显示用的比例因子
    void getMultiplicator() {
      maxVal = 1;
      //选出最大值
      for (int i = 0; i < maxRow; i++) {
        if (val > maxVal) maxVal = val;
      }
      //如果maxVal大于LineVal,则比例因子=LineVal/maxVal,否则比例因子=1
      if (maxVal > LineVal) multiplicator = (double)LineVal / (double)maxVal;
      else multiplicator = 1;
    }
    
    
    //--------------------------------------------------------------------------------------------
    // BLE notifyCallback 在此Callback函数中,读取心率带数据,然后给全局变量unsigned long hrms 赋值
    //--------------------------------------------------------------------------------------------
    static void notifyCallback( BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) {
        hrm.HRM = pData[1];
        String hrm_str =  String(pData[1], DEC);   
              hrms=hrm_str.toInt();
    }
    
    //--------------------------------------------------------------------------------------------
    //  Connect to BLE HRM
    //--------------------------------------------------------------------------------------------
    bool connectToServer(BLEAddress pAddress) {
        Serial.print(F("Forming a connection to "));
        Serial.println(pAddress.toString().c_str());
    
        BLEClient*  pClient  = BLEDevice::createClient();
        Serial.println(F(" - Created client"));
        
        // Connect to the HRM BLE Server.
        pClient->connect(pAddress);
        Serial.println(F(" - Connected to server"));
    
        // Obtain a reference to the service we are after in the remote BLE server.
        BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
        if (pRemoteService == nullptr) {
          Serial.print(F("Failed to find our service UUID: "));
          Serial.println(serviceUUID.toString().c_str());
          return false;
        }
        Serial.println(F(" - Found our service"));
    
    
        // Obtain a reference to the characteristic in the service of the remote BLE server.
        pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
        if (pRemoteCharacteristic == nullptr) {
          Serial.print(F("Failed to find our characteristic UUID: "));
          Serial.println(charUUID.toString().c_str());
          return false;
        }
        Serial.println(F(" - Found our characteristic"));
    
        // Register for Notify
        pRemoteCharacteristic->registerForNotify(notifyCallback);
    }
    
    //--------------------------------------------------------------------------------------------
    // Scan for BLE servers and find the first one that advertises the service we are looking for.
    //--------------------------------------------------------------------------------------------
    class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
     /**
       * Called for each advertising BLE server.
       */
      void onResult(BLEAdvertisedDevice advertisedDevice) {
        Serial.print(F("BLE Advertised Device found: "));
        Serial.println(advertisedDevice.toString().c_str());
    
        // We have found a device, let us now see if it contains the service we are looking for.
        if (advertisedDevice.haveServiceUUID() && advertisedDevice.getServiceUUID().equals(serviceUUID)) {
    
          // 
          Serial.print(F("Found our device!  address: ")); 
          advertisedDevice.getScan()->stop();
    
          pServerAddress = new BLEAddress(advertisedDevice.getAddress());
          doConnect = true;
    
        } // Found our server
      } // onResult
    }; // MyAdvertisedDeviceCallbacks
    
    
    
    
    //===== SETUP =====
    void setup() {
      /* start Serial */
      Serial.begin(115200);
      Serial.println("starting!");
    
      //Start BLE--BLE部分初始化
      // Retrieve a Scanner and set the callback we want to use to be informed when we
      // have detected a new device.  Specify that we want active scanning and start the
      // scan to run for 30 seconds.
      BLEDevice::init("");
      BLEScan* pBLEScan = BLEDevice::getScan();
      pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
      pBLEScan->setActiveScan(true);
      pBLEScan->start(30);
      //-----watchIO 部分初始化
      init_power();
      lcd_init();
      
    }
    
    //===== LOOP =====//
    void loop() {
      curTime = millis();  //当前时间计时
      // If the flag "doConnect" is true then we have scanned for and found the desired
      // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
      // connected we set the connected flag to be true.
      if (doConnect == true) {
        if (connectToServer(*pServerAddress)) {
          Serial.println(F("We are now connected to the BLE HRM"));
          connected = true;
        } else {
          Serial.println(F("We have failed to connect to the HRM; there is nothin more we will do."));
        }
        doConnect = false;
      }
      //every 3 second
      if (curTime - prevTime >= 3000) {
              // Turn notification on
      if (connected) {
        if (notification == false) {
          Serial.println(F("Turning Notifocation On"));
          const uint8_t onPacket[] = {0x1, 0x0};
          pRemoteCharacteristic->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)onPacket, 2, true);
          notification = true;
        }
      }
        prevTime = curTime;
        char c_hrm[]="";
              dtostrf(hrms,3,0,c_hrm); //转换为char类型,用于显示
        //整体左移所有的心率值
        for (int i = 0; i < maxRow; i++) {
          val = val[i + 1];
        }
        val[87] = hrms;//最新数据放入val[87]
    
        //recalculate scaling factor 重新计算比例因子
        getMultiplicator();
        //调试信息,运行时可以去除
       Serial.print("hrm:");
       Serial.println(hrms);   //串口打印一个心率值
      //canvas.fillScreen(ST77XX_BLUE);
      canvas.fillScreen(0x435c);  //屏幕背景色
      canvas.setCursor(10, 10);
      canvas.setTextSize(2);
      canvas.setTextColor(ST77XX_WHITE);
       canvas.print("HRM:"); //显示HRM标签
       canvas.setTextSize(3);
      canvas.setTextColor(ST77XX_YELLOW);
       canvas.setCursor(10, 50);
       canvas.print(hrms);  //显示心率数据
            //画一个矩形填充框,图像在其中显示
          canvas.fillRect(60,4,94,71,0xFF79);
      //============在矩形填充框内绘制心率图形
        for (int i = 0; i <=maxRow; i++) { 
         // 用垂直线表达心率值,并且按照比例尺进行调整
         //调试信息,运行时请去除
         //Serial.println("i--------     val ---------        multiplicator");
         //Serial.println(i);
        //  Serial.println(val);
        // Serial.println(multiplicator);
          int line3;//最初是三段式渐变,因此命名为Line3,后面延用
          line3= val*multiplicator;
          //以下我们准备了三个方案显示心率图形
          /*方案一:三段显示
         // 第一段:0~33%  0xEFDE浅
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.66, line3*0.34, 0xEFDE);
         // 第二段:34~66%  0x76B8中
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.33, line3*0.33, 0x76B8);
          //第三段:67~100%  0x1531深
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9,  line3*0.33, 0x1531 );
          */
          //------------方案二,四段显示
          /*
         //第一段:0~33%  0xff79浅
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.55, line3*0.45, 0xff79);
         // 第二段:34~66%  0xfef9中
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.25, line3*0.3, 0xfef9);
          //第三段:67~100%  0xfe79较深
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.05,  line3*0.2, 0xfe79 );
          //第四段:61~100%  0xfdd9深
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9,  line3*0.05, 0x7840 );
          */
      //----------------方案三,11段,第一段和最后一段各5%,剩余10%每段
             //第一段:0~5% 0xef60 浅
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.94, line3*0.05+1, 0xef60);
         // 第二段:6~15%  0xf6e0
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.84, line3*0.1+1, 0xf6e0);
          //第三段:16~25% 0xf660
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.74, line3*0.1+1, 0xf660);
          //第四段:26~35%  0xf5c0
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.64, line3*0.1+1, 0xf5c0);
          //第五段:36~45% 0xf540
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.54, line3*0.1+1, 0xf540);
         // 第六段:46~55%  0xf4c0
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.44, line3*0.1+1,0xf4c0);
          //第七段:56~65%  0xfc40
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.34,  line3*0.1+1,0xfc40);
          //第八段:66~75%  0xfbc0
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.24,  line3*0.1+1,0xfbc0);
          //第九段:76~85% 0xfb20
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.14, line3*0.1+1,0xfb20);
         // 第十段:86~95%  0xfaa0
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9+line3*0.05, line3*0.1+1,0xfaa0);
         // 第十一段:96~100%  0xfa20 深
          canvas.drawFastVLine(i+63, int(maxLine - val*multiplicator)+9, line3*0.05,0xfa20); //红色
                
         }
        hrms       = 0;
        delay(500);
    
      }
    }

    【关于渐变色的心率图形展示】
       首先我们看看成品的心率图形显示,利用11段渐变色进行展示,本文的示例使用的是暖色调,也就是从橘红色到黄色的一个渐变处理。
    一、渐变色处理的原理以及公式

    Gradient = A + (B-A) / Step * N
    说明:
    Gradient 是总数为step的分段数中第N段渐变色数值
    A是目标值,B是起始值,step是总分段数,N是第n段数据

    二、分步实现渐变色的计算和变换
    Step1:设定起始颜色以及目标颜色
    本例中我们设定了橘红色(255-69-0)作为起始值,黄色2(238-238-0)作为渐变得目标值。
    Step2:计算分段渐变色的RGB数值,并且转换为24位颜色数据
    2019-10-06_223846_副本.png
    Step3:将RGB888转换为RGB565
    1、888和565的区别
    https://blog.csdn.net/ctthuangcheng/article/details/8551559
    【引用此贴的描述】正常的RGB24是由24位即3个字节来描述一个像素,R、G、B各8位。而实际使用中为了减少图像数据的尺寸,如视频领域,对R、G、B所使用的位数进行的缩减,如你所说的RGB565和RGB555。
    RGB565 就是R-5bit,G-6bit,B-5bit
    RGB888 就是R-8bit,G-8bit,B-8bit ;其实这就是RGB24
    具体来说:
    RGB565 是16位的,2个字节,5+6+5,第一字节的前5位是R,后三位+第二字节前三位是G,第二字节后5位是B。
    RGB888 是24位的,3个字节。
    2、转换小程序
    因为在WatchIO使用到的屏幕以及Adafruit_GFX_Library支持16位的RGB565数据。所以我们需要做一个888-565的转换。我们不需要造轮子了,在网上找到了这样的一个实用小工具,我将其附在帖子的附件里。
    2019-10-06_223109_副本.png

    完成上述步骤后,我们就得到了一组渐变色的RGB565数据,实际运行后,得到了一个类似火焰的暖色调渐变色心率数据。
    IMG_20191006_215126_副本.png

    【小结】
        这是一块很好玩的极客手表,我只是将esp32的BLE心率带做了一个简单的移植,希望起到一个抛砖引玉的作用,使得更多的玩家关注这块手表,关注小华师兄等极客在袖珍的智能穿戴之路上的探索。

    以下附件附上源代码、必要的库以及888-565小工具,还有我的渐变色计算表。
    upload1005.zip (390.58 KB, 下载次数: 12)
  • TA的每日心情
    擦汗
    2019-7-28 12:29
  • 签到天数: 25 天

    [LV.4]偶尔看看III

    发表于 2019-10-7 11:41 | 显示全部楼层
    能不能发一下pcb原理图,谢谢
  • TA的每日心情

    2019-3-19 13:20
  • 签到天数: 5 天

    [LV.2]偶尔看看I

    发表于 2019-10-7 15:54 | 显示全部楼层
    不错,多少钱?
  • TA的每日心情
    奋斗
    2019-7-12 18:42
  • 签到天数: 48 天

    [LV.5]常住居民I

    发表于 2019-10-7 21:02 | 显示全部楼层
    电能用多久呢
  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

     楼主| 发表于 2019-10-8 12:03 | 显示全部楼层
    作业小斗士 发表于 2019-10-7 11:41
    能不能发一下pcb原理图,谢谢

    这块表可以去github搜一下。
  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

     楼主| 发表于 2019-10-8 12:03 | 显示全部楼层
    ynkmzyl 发表于 2019-10-7 15:54
    不错,多少钱?

    可以去某宝某鱼搜一下。好像比较亲民。
  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

     楼主| 发表于 2019-10-8 12:07 | 显示全部楼层

    ips彩屏和esp32都不是省油的灯,这么袖珍,电池也必然有限。所以穿戴还是看算法,我没有实测过使用时间。如果一个完整的穿戴作品肯定要做省电处理。这块表有姿态传感器以及一个多功能开关。可以有两个思路,一是姿态传感器计算抬手姿态后亮屏。二是多功能开关触碰亮屏。我觉得可以用mpy的多线程试试。
  • TA的每日心情

    2019-3-19 13:20
  • 签到天数: 5 天

    [LV.2]偶尔看看I

    发表于 2019-10-9 16:44 | 显示全部楼层
    本帖最后由 ynkmzyl 于 2019-10-9 16:45 编辑

    问了一下卖家,待机才2天,讲实用性不如M5StickV,讲娱乐性不如手机甚至不如儿童手表讲外观,手环实用性远不如比它便宜一点的小米手环4
  • TA的每日心情
    开心
    2019-7-17 13:13
  • 签到天数: 264 天

    [LV.8]以坛为家I

     楼主| 发表于 2019-10-10 09:38 | 显示全部楼层
    ynkmzyl 发表于 2019-10-9 16:44
    问了一下卖家,待机才2天,讲实用性不如M5StickV,讲娱乐性不如手机甚至不如儿童手表讲外观,手环实用 ...

    除了m5stickv,您列举的都是电子消费品。极客平台和消费品没有可比性(一个是成熟电子产品,一个是自己diy的平台)。而m5stickv是基于k210的,重点在视觉识别。其实是一个视觉识别传感器。除了体积以外与这块极客手表也无任何可比性。见仁见智吧。关于待机,实际上主要在自己的算法设计,如果采用低功耗算法,肯定远远大于两天。这也是折腾的乐趣吧。
  • TA的每日心情
    开心
    2019-11-15 00:06
  • 签到天数: 132 天

    [LV.7]常住居民III

    发表于 2019-10-10 20:01 | 显示全部楼层
    沧浪老师的教程学习一下。
    您需要登录后才可以回帖 登录 | 立即注册  

    本版积分规则

    热门推荐

    Arduino MEGA 与UNO 通过nRF24L模块通讯
    Arduino MEGA 与UNO 通过n
    之前在深水宝很“实惠”的店铺买了一些原件,随手砍了esp8266以及nRF24L*3 因为缺
    【原创】 drawbot平面关节scara机械臂写字机 画画机器人直播...
    【原创】 drawbot平面关节
    这个项目上个月就在做了,结构和代码反反复复改了多次,加上自己又太忙,一直没来得及
    新手求教:用模拟口读取可调电阻的值
    新手求教:用模拟口读取可
    我想问的有以下两个问题: 1.如图,模拟口读取的是可调电阻至VCC一侧的模拟值,还是可
    【Arduino】108种传感器系列实验(37)---MQ-3酒精传感器模块
    【Arduino】108种传感器系
    37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是
    AI分拣系统
    AI分拣系统
    人工智能分拣系统 应用简介 在日常生活中,人们经常需要对物体进行分类,”材料分类
    Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   
    快速回复 返回顶部 返回列表