一款好玩的esp32极客手表(WatchIO)之BLE心率表-Arduino中文社区 - Powered by Discuz!

Arduino中文社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 8676|回复: 10

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

[复制链接]
发表于 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
【代码及注释】
[mw_shl_code=arduino,true]/*
  ===========================================
       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);

  }
}[/mw_shl_code]

【关于渐变色的心率图形展示】
   首先我们看看成品的心率图形显示,利用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, 下载次数: 44)



发表于 2019-10-7 11:41 | 显示全部楼层
能不能发一下pcb原理图,谢谢
发表于 2019-10-7 15:54 | 显示全部楼层
不错,多少钱?
发表于 2019-10-7 21:02 | 显示全部楼层
电能用多久呢
 楼主| 发表于 2019-10-8 12:03 | 显示全部楼层
作业小斗士 发表于 2019-10-7 11:41
能不能发一下pcb原理图,谢谢

这块表可以去github搜一下。
 楼主| 发表于 2019-10-8 12:03 | 显示全部楼层
ynkmzyl 发表于 2019-10-7 15:54
不错,多少钱?

可以去某宝某鱼搜一下。好像比较亲民。
 楼主| 发表于 2019-10-8 12:07 | 显示全部楼层

ips彩屏和esp32都不是省油的灯,这么袖珍,电池也必然有限。所以穿戴还是看算法,我没有实测过使用时间。如果一个完整的穿戴作品肯定要做省电处理。这块表有姿态传感器以及一个多功能开关。可以有两个思路,一是姿态传感器计算抬手姿态后亮屏。二是多功能开关触碰亮屏。我觉得可以用mpy的多线程试试。
发表于 2019-10-9 16:44 | 显示全部楼层
本帖最后由 ynkmzyl 于 2019-10-9 16:45 编辑

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

除了m5stickv,您列举的都是电子消费品。极客平台和消费品没有可比性(一个是成熟电子产品,一个是自己diy的平台)。而m5stickv是基于k210的,重点在视觉识别。其实是一个视觉识别传感器。除了体积以外与这块极客手表也无任何可比性。见仁见智吧。关于待机,实际上主要在自己的算法设计,如果采用低功耗算法,肯定远远大于两天。这也是折腾的乐趣吧。
发表于 2019-10-10 20:01 | 显示全部楼层
沧浪老师的教程学习一下。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-11-28 04:42 , Processed in 0.242989 second(s), 18 queries .

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

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