【教程】No-gps跑步手表之ai2遇见小C-Arduino中文社区 - Powered by Discuz! Archiver

沧海笑1122 发表于 2021-2-20 23:38

【教程】No-gps跑步手表之ai2遇见小C

本帖最后由 沧海笑1122 于 2021-2-20 23:39 编辑


【项目故事】又双叒叕是跑步手表。为什么说又?是因为我已经做过四个版本的跑步手表,主控从644p到esp32,它们都有一个共同点,那就是都需要外接gps传感器。无论是直接装配在手表上,还是分离式蓝牙gps模块。都离不开gps模块与主控的通信,有线或蓝牙。而我们在跑步时,通常会带着手机,或听音乐,或用来保持通信。那么实际上我们就随身携带了一部高精度的定位终端。本次尝试的手表,不再与单一的gps模块相连接,而是使用了安卓手机的位置传感器。将定位信息用ble传送至esp32主控,就构成了一个没有gps模块的跑步手表。什么是ai2?MIT著名的入门级安卓手机APP图形式编写软件。为了和大热的AI(人工智能)相区别,因此采用小写,目前是第二版,因此简称ai2。小C又是谁?小C是m5stack出品的基于esp32主控的,小巧的集成模块。拥有彩屏、按钮、电源管理以及外壳,是为物联网以及穿戴而生的开发平台。设计了卡槽以及专门的手表背夹和表带。很容易变身一块esp32手表。主要知识点:一是app inventor 2的编写,重点是ble的连接以及位置传感器数据的获取(经纬度以及速度),现在的手机位置传感器并非gps一种,而是融合了网络定位之后的一个综合输出,咱们在高德地图等形形色色的地图软件中使用的就是这个融合后的位置信息。比起单一的gps而言,不再单一依赖天空中的卫星,而是在有网络的车内(室内)也可以完成定位,而且速度很快,用户体验非常好。二是手机与esp32通过ble的通信;三是手机传送下来的数据有三个参数:经纬度以及速度,我们在手机端装配成三段数据传送下来,在esp32侧组装成一个json格式的字符串并且进行解析,这是本项目的第三个知识点。四是对手机传送来的经纬度坐标进行距离计算以及速度计算。我对本项目难度的定位是,创意四星★★★★☆,难度系数两星★★☆☆☆。依然是一个非常好的学习ai2以及ble通信入门的小项目。一如既往,本项目制作过程中,我借鉴了来自因特网的玩家资源(已经在代码前端注明),实践再一次证明了,我们能想到的创意,利用现有资源进行一个乐高式的搭建,基本上都可以实现(前提是技术本身能实现的话)。

一、硬件准备

名称数量及型号备注
1安卓手机1安卓4.4以上
2M5stickc1主控esp32

二、软件准备

名称版本备注
1App inventor 22.0Apk编写ide
2arduinoJson6.0优秀的json解析库
3Arduino ble for esp32
Esp32的ble库

三、制作过程1、 接线:这次省事了,用的两个都是成品,手机+m5stickc,所以,我们这次没有接线。2、 软件编写:(1)    手机侧:手机侧采用ai2编程,我使用的是广州教育中心服务器(http://app.gzjkw.net/),这是ap2官方认可的在国内的服务器之一,非常稳定。玩家也可以尝试从github上下载最新的代码自己搭建离线版服务器。我认为稳定更重要,所以没有在搭建离线传感器上使用更多时间。本项目apk主要的功能有:1、搜索并且连接esp32侧的ble,2、打开并且定位,3、建立一个定时器,每个3秒向已经连接的esp32传送json格式的定位数据。定位数据格式如下:{'lat':0,'lon':0,'speed':0} (1)    小c侧:小c侧主要的功能:一是建立一个myesp32的Ble设备,等待与手机端建立连接,二是设计一个回调函数,在接收到来自手机的数据之后,将经纬度以及速度等三段数据进行组装,需要注意的是,在每一段数据传送时,默认都会带一个结束符'/0',这个结束符是不可见的,但是会告诉解析程序,本字符串已经结束。所以在组装三个参数的过程中,需要把结束符剔除出去。三是使用伟大的arduinojson库对收到的参数进行解析,解析成三个double数据:经纬度及速度。然后在小C上进行显示。计算两组坐标之间的距离函数,来自一个经典的gps解析库tinygps++,由于我们只用到这一个函数,所以没有引用整个库,我把这个函数直接取过来放在程序中。顺手做了一个开机欢迎语,跑者都非常喜欢的口号“Just do it”。小c开机后,会定义一个ble设备,然后等待手机侧的连接。一旦连接成功,会每隔三秒从手机下发定位参数。/*
    Based on Neil Kolban example for IDF:
https://github.com/nkolban/esp32-snippets/blob/master/cpp_utils/tests/BLE%20Tests/SampleWrite.cpp
    Ported to Arduino ESP32 by Evandro Copercini
*/
// Modificado por Juan Antonio Villalpando.
// From: http://kio4.com/arduino/160_Wemos_ESP32_BLE.htm
//======2021-02-17 解决了从手机下送数据的json解析问题
//======2021-02-19 调试完字体,使用手机下送的数据,不再通过距离折算计算速度,因通过比对,两者相差在3~5%以内。
//======沧海

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <M5StickC.h>
#include <ArduinoJson.h>

//解析从手机下送数据的json准备
String valor=""; //每次接收到的字符串
String all_valor=""; //全部组装好的,即将用于解析的字符串
int string_num=0; //收取序号
char s_str; //工作数据组
StaticJsonDocument<200> doc;


//上一次经纬度、本次经纬度、速度、可用卫星数、时间
double lst_lat, lst_lng, f_lat, f_lng, f_speed; //本次经纬度、上次经纬度、速度
double f_dist=0; //本次测距的距离


//BLE 定义
#define SERVICE_UUID      "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// DistanceBetween funtion from TinyGps++ 测距函数来自TinyGps++
//
double distanceBtw(double lat1, double long1, double lat2, double long2)
{
// returns distance in meters between two positions, both specified
// as signed decimal-degrees latitude and longitude. Uses great-circle
// distance computation for hypothetical sphere of radius 6372795 meters.
// Because Earth is no exact sphere, rounding errors may be up to 0.5%.
// Courtesy of Maarten Lamers
double delta = radians(long1-long2);
double sdlong = sin(delta);
double cdlong = cos(delta);
lat1 = radians(lat1);
lat2 = radians(lat2);
double slat1 = sin(lat1);
double clat1 = cos(lat1);
double slat2 = sin(lat2);
double clat2 = cos(lat2);
delta = (clat1 * slat2) - (slat1 * clat2 * cdlong);
delta = sq(delta);
delta += sq(clat2 * sdlong);
delta = sqrt(delta);
double denom = (slat1 * slat2) + (clat1 * clat2 * cdlong);
delta = atan2(delta, denom);
return delta*6372795;
}



void Cal(double lat_0,double long_0, double speed_num_0)   //计算距离、运动时间的函数
{
//===========显示累计里程
    //=====计算里程
    f_lat=lat_0;
    f_lng=long_0;
    if (lst_lat == 0) { //还没开始测距,所以lst_lat==0
      f_dist = f_dist;
    }
    else //前后两组经纬度都有了,就可以进行测距
      {
          f_dist = f_dist + distanceBtw(f_lat, f_lng, lst_lat, lst_lng)/ 1000;
          f_speed= distanceBtw(f_lat, f_lng, lst_lat, lst_lng)*1.2; //两点间隔为3秒数据采样,所以除以3/3600小时,此数据作为方案二,可以替代手机下送的speed
      }
   lst_lat = f_lat;//计算后,将现坐标赋值为前坐标,下同
   lst_lng = f_lng;
   //在手表上进行显示。里程、计算速度、手机传送下来的速度
   M5.Lcd.setTextColor(TFT_YELLOW);
   M5.Lcd.setCursor(3, 40, 2);
   M5.Lcd.setTextFont(4);
   M5.Lcd.print("V= ");
   M5.Lcd.print(speed_num_0*3.6); //显示手机测速。其中:3.6是一个系数,将手机传送下来的speed(米/秒)折算为(公里/小时)=x*3600/1000=3.6
   M5.Lcd.setTextFont(2);
   M5.Lcd.print("km/h");
   //M5.Lcd.setCursor(3, 33, 2);
   //M5.Lcd.print(f_speed); //显示实时速度,作为备选速度计算方式
   M5.Lcd.setTextColor(TFT_WHITE);
   M5.Lcd.setCursor(3, 13, 2);
   M5.Lcd.setTextFont(2);
   M5.Lcd.print("Dist.= ");
   M5.Lcd.print(f_dist);//显示累计运动里程km
   M5.Lcd.print(" km");
   //M5.Lcd.setCursor(60, 33, 2);
   //M5.Lcd.print(f_speed/(speed_num_0*3.6)); //显示实时速度与手机测速之比。
   

}

class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
      std::string value = pCharacteristic->getValue();
      
      if (value.length() > 0) {
      valor = "";
      for (int i = 0; i < value.length(); i++){
          // Serial.print(value); // Presenta value.
          valor = valor + value;
      }
      }
       if (string_num==3) {      
         //字段组装完整时,LCD display
      M5.Lcd.fillScreen(BLACK);
      //Serial.print(all_valor.length());
      //Serial.println(all_valor); // Presenta valor.

      for(int i=0;i< all_valor.length();i++) //将接收到并且组装好的字符串all_valor,一对一转换成char s_str数组
          s_str=all_valor.charAt(i);
            
      //Serial.print("s_str:");
      Serial.println(s_str);
      
      //===========解析json
       // Deserialize the JSON document
      DeserializationError error = deserializeJson(doc, s_str);

       // Test if parsing succeeds.
      if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.c_str());
      return;
      }

      // Fetch values.
      //
      // Most of the time, you can rely on the implicit casts.
      double latitude = doc["lat"];
      double longitude = doc["lon"];
      double speed_num = doc["speed"];

      Cal(latitude,longitude,speed_num);//计算距离、速度,并且显示手机传送下来的速度


         //============解析完毕
         string_num=0;
         all_valor="";
      for(int i=0;i< 60;i++)   //工作char[]清空
         s_str=all_valor.charAt(i);
      }
      if (string_num<3) { //如果还没有收到三个参数(经纬度及速度),则进行收到的字符串叠加,并且去除'/0'结束符
          all_valor= all_valor+valor.substring(0,valor.length());//此语句可以去除每次从手机发来的数据末尾的'/0'结束符,
                                                            //这个结束符不可见,但是在转换char[]时,会使得编译程序以为字符串已经结束。
          string_num=string_num+1;
      }
      else // 三个参数一组已经收取完毕,则准备进行下一组数据的收取,将工作char[]清空
      {

       string_num=0;
       all_valor="";
       //for(int i=0;i< 60;i++)   //工作char[]清空
       //    s_str=all_valor.charAt(i);
      }
               

    }
};

void setup() {

Serial.begin(115200);
M5.begin();
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(TFT_RED);

M5.Lcd.setCursor(30, 35, 2);
M5.Lcd.setTextFont(4);
M5.Lcd.print("Just Do It");
BLEDevice::init("MyESP32");

BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                       CHARACTERISTIC_UUID,
                                       BLECharacteristic::PROPERTY_READ |
                                       BLECharacteristic::PROPERTY_WRITE
                                       );

pCharacteristic->setCallbacks(new MyCallbacks());
pCharacteristic->setValue("Hello World");
pService->start();

BLEAdvertising *pAdvertising = pServer->getAdvertising();
pAdvertising->start();


}

void loop() {
delay(2000);
}
一、路测及小结
路测中一个非常重要的内容是对速度的比对,这次我们在实现测速时,采用了两个方法。方法一:直接使用位置传感器提供的speed,当时我对这个数据曾经非常迷惑,因为在汽车行进中,我发现这个数据是线性变化的,但是总是和汽车车速表显示大约相差3.85倍左右。3.85是什么?我请教了从事多年ai2推广的老师,最后在小华师兄那里找到了答案,小华师兄是java资深工程师,他猜测有可能手机的速度是米/秒,在解析了位置传感器的java代码后,确认了这个猜想,因此这个折算参数应当是3.6,即:x*3600/1000=3.6*x。为了在路测中,印证这个测速的准确性,我将收取到的坐标(经纬度)用三角函数法计算出了距离,我在apk中的定时器是按照3秒间隔发送的,所以将间隔3秒的两组坐标之间的距离求出后,那么实时速度也就求出了。用这个速度与手机位置传感器提供的速度进行比对,结果非常令人满意,两种结果之间的误差小于3~5%。汽车在高速行驶时,两种方法几乎没有误差。
所以我只是在代码的注释中,保留了两组坐标间隔3秒的计算方法,有兴趣的玩家也可以试试。在手表显示时,直接使用了手机位置传感器的速度。经过多次跑步、车辆测试,这块小c与手机构成的跑步手表,表现非常稳定,可以为跑者提供速度以及累计运动距离等两组数据。后续提升:后续的玩法其实非常多,我能想到的,一是增加参数,如累计运动时间;二是记录轨迹,在手机端将运动轨迹记录下来存档在数据库中进行展示。三是不再使用屏幕进行展示,而是用一颗m5stack的atom(更加精巧的、不带屏幕的esp32主控)带2颗全彩led,用颜色显示此刻跑者的跑步速度(如绿色代表7~9公里,红色代表10公里以上,7公里以下为蓝色);而随着你跑步距离的延伸,另一颗led也会以颜色变化来表征你的累计运动距离。这两颗led甚至可以镶嵌在你的帽檐上,运动参数抬眼可知,是不是也很酷啊。用Ble建立手机和单片机(或者树莓派)的联系,实际上扩展了大量的玩法,后续我还会尝试各种玩法。“单片机就是一个传感器与执行元件的hub”-这是多年前我接触arduino时一位师兄的话。手机的运算能力超强、搭载了丰富的传感器,两者之间通过ai2这样的图形化软件作为桥梁,一定可以做出更多的有趣玩具。
感谢小华师兄的支持,感谢arduino.cn、《无线电》杂志师兄们的支持。雨水已过,惊蛰在望,没有一个春天不会到来。
沧海抱拳。
小C以及aia代码分享。

topdog 发表于 2021-2-21 12:41

沧海笑1122老师出品必是精品,谢谢分享,学习了。

沧海笑1122 发表于 2021-2-21 21:57

本帖最后由 沧海笑1122 于 2021-2-21 21:58 编辑

topdog 发表于 2021-2-21 12:41
沧海笑1122老师出品必是精品,谢谢分享,学习了。
感谢师兄支持:handshake
本文当中的位置传感器例程、ai2的软件资源,多亏topdog师兄提供,在此一并致谢。
页: [1]
查看完整版本: 【教程】No-gps跑步手表之ai2遇见小C