课程设计项目——基于ESP32的智能跳绳监测系统
💁♂️前言介绍: 又到期末周了,期末的要求是交一份基于ESP32的智能化项目。所谓的智能化,就是在ESP32上跑模型,也很有意思,在这么小的一块板子上能跑些什么模型呢。所以我搞了一个简易的跳绳监测系统,和以往一样,相关的代码都附在文末,大家自取!
👨🏫内容1:前言
👨🏫 前言
👉先简单介绍一下这个项目大致的内容
👨💻我们将通过ESP32和陀螺仪,采集7000条跳绳的运动数据。这些数据将保存在csv文件中,再经过模型训练后,烧录到ESP32开发板中。之后,当我们再进行跳绳时,所获取的数据将在ESP32上进行运行,来检测你是否有效跳绳。最终的结果将在OLED和前端大屏上实时显示。
👋那我们依次来看看这个项目是如何实现的吧!
👨⚖️内容2:组件介绍
1️⃣ESP32开发板:
👉ESP32是一款由乐鑫(Espressif Systems)生产的低功耗微控制器,具有集成的Wi-Fi和蓝牙功能。它被广泛用于物联网(IoT)项目,因为它提供了一种成本效益高、易于编程的方式来连接设备到互联网。ESP32具有以下特点:
- 1.双核心处理器:ESP32具有两个Xtensa® 32位LX6 CPU核心,可以运行高达240MHz。
- 2.Wi-Fi和蓝牙:它支持802.11 b/g/n Wi-Fi和双模蓝牙(BR/EDR和BLE)。
- 3.多种外设接口:包括SPI、I2C、UART、ADC、DAC等。
- 4.丰富的GPIO:提供多个通用输入/输出(GPIO)引脚,支持各种传感器和外设。
2️⃣OLED显示屏:
👉位于下方的是一个小型OLED显示屏,能够显示128x64像素的图形,支持多种颜色模式,支持I2C和SPI两种通信接口,便于与不同的微控制器连接。在本项目中用于实时显示信息,如跳绳次数(Jump: 63)和心率(BPM: 76)
3️⃣陀螺仪模块:
👉GY-25Z是一种姿态传感模块,它通过UART接口与上位机或微控制器进行通信。以下是GY-25Z模块的一些关键特性和功能:
- 1.功耗小:功耗较低,适合长时间运行的便携式设备。
- 2.体积小:模块体积小巧,便于集成到各种设备中。
- 3.工作原理:GY-25Z通过内置的陀螺仪和加速度传感器,并结合数据融合算法来获取角度数据。
4️⃣心率传感器:
👉MAX30102是一款集成的脉搏血氧仪和心率监测模块,它集成了多个LED、光电检测器、光学元件和具有环境光抑制功能的低噪声电子电路,能够提供心率监测和血氧饱和度测量的功能。MAX30102通过标准的I2C兼容接口与主机进行通信,它使用光电容积法(PPG)来测量心率,通过LED灯照射人体组织,然后由光电传感器检测反射光并转换为电信号,再经过处理得到心率和血氧数据。
👨🎓内容3:数据采集
首先我们先采集一些跳绳的数据,用于模型的训练
接下来我将介绍数据采集部分的代码【代码解析看我的注释】🙈
库文件:
/*
功能: 数据采集
描述: 采集100次动作,每个动作70个元组;
当三个轴向的加速度之和大于3.5倍的重力时,认为动作发生;
当三轴加速度之和小于1.9个重力加速度时,认为是静止状态;
当三轴加速度之和连续10次低于1.9,认为动作结束;
输出: 串口输出7000条记录,每条记录为6个维度(aX, aY, aZ, gX, gY, gZ)
*/
#include <Hardwareserial.h>
#define TEST_PRINT 1
#define ACTION_COUNT_MAX 10
#define ONCE_ACTION_RECORD_NUM_MAX 70
#define ONCE_ACTION_RECORD_NUM_MIN 0
#define ONCE_ACTION_STOP_NUM_MAX 10
#define ONCE_ACTION_NOT_STOP 0
#define ONCE_ACTION_COLLECT_FINISH 1
#define ONCE_ACTION_COLLECT_NOT_FINISH 0
变量定义:
HardwareSerial mySerial(1); //声明串口1
const float accelerationThreshold_HIGH = 3.5; // 触发阈值(动作发生) 为3.5倍重力
const float accelerationThreshold_LOW = 1.9; // 静止状态(无动作) 为1.9倍重力
int stop_record_count = 0; //记录在一次动作采集种,静止节点的连续次数,连续记录低于1.9g的次数,10次认定动作结束(0-10)
int once_action_record_count = 0; //记录在一次动作种,采集节点的数量(0-70)
int action_count = 0;//记录动作的个数(0-10)
float record_aX[ONCE_ACTION_RECORD_NUM_MAX];
float record_aY[ONCE_ACTION_RECORD_NUM_MAX];
float record_aZ[ONCE_ACTION_RECORD_NUM_MAX];
float record_gX[ONCE_ACTION_RECORD_NUM_MAX];
float record_gY[ONCE_ACTION_RECORD_NUM_MAX];
float record_gZ[ONCE_ACTION_RECORD_NUM_MAX];
//捕获数据参数
int conut = 0, dirty = 0;
unsigned char sign = 0;
unsigned char Re_buf[25], counter = 0;
int16_t acc[3] = { 0 }; //加速器计
int16_t gyro[3] = { 0 }; //陀螺仪
int YPR[3]; //欧拉角
float aX, aY, aZ, gX, gY, gZ, yX, yY, yZ;
unsigned char once_action_over_sign = 0;//采集完成一次动作的信号
初始化设置:
void setup(){
//ESP32与电脑通信波特率(串口0)
Serial.begin(115200);
//ESP32与GY-25Z通信波特率(串口1)
mySerial.begin(115200, SERIAL_8N1, 32, 33); //rxPin = 32,txPin = 33
//init GY-25Z
delay(500);
//输出数据设置指令------- 0xA5+0x55+0xXX+sum
mySerial.write(0xA5);
mySerial.write(0x55);
mySerial.write(0x53);
mySerial.write(0x4D);
delay(100);
//自动输出数据指令-------0xA5+0x56+0x02+0xFD
mySerial.write(0xA5);
mySerial.write(0x56);
mySerial.write(0x02);
mySerial.write(0xFD);
delay(100);
}
相关函数:
//打印一组动作的数据,即70个节点
void Print_once_action_data(){
for(int k = ONCE_ACTION_RECORD_NUM_MIN ;k < ONCE_ACTION_RECORD_NUM_MAX ;k++){
Serial.print(record_aX[k]);
Serial.print(",");
Serial.print(record_aY[k]);
Serial.print(",");
Serial.print(record_aZ[k]);
Serial.print(",");
Serial.print(record_gX[k]);
Serial.print(",");
Serial.print(record_gY[k]);
Serial.print(",");
Serial.print(record_gZ[k]);
Serial.println("");
}
}
//记录一组动作数据
void Record_data(){
float aSum = fabs(aX) + fabs(aY) + fabs(aZ);
if (aSum >= accelerationThreshold_HIGH && once_action_record_count == 0){//动作开始
#if TEST_PRINT == 1
Serial.println("Action detected.");//检测到动作。
#endif
//记录开始动作
record_aX[once_action_record_count] = aX;
record_aY[once_action_record_count] = aY;
record_aZ[once_action_record_count] = aZ;
record_gX[once_action_record_count] = gX;
record_gY[once_action_record_count] = gY;
record_gZ[once_action_record_count] = gZ;
once_action_record_count = 1;
}
else if(once_action_record_count > ONCE_ACTION_RECORD_NUM_MIN && once_action_record_count < ONCE_ACTION_RECORD_NUM_MAX){//一个动作记录中
record_aX[once_action_record_count] = aX;
record_aY[once_action_record_count] = aY;
record_aZ[once_action_record_count] = aZ;
record_gX[once_action_record_count] = gX;
record_gY[once_action_record_count] = gY;
record_gZ[once_action_record_count] = gZ;
once_action_record_count++;
if(aSum >= accelerationThreshold_LOW)//非静止
stop_record_count = ONCE_ACTION_NOT_STOP;//清空标记
else//静止
stop_record_count++;
if(stop_record_count == ONCE_ACTION_STOP_NUM_MAX || once_action_record_count == ONCE_ACTION_RECORD_NUM_MAX){//动作完成
once_action_over_sign = ONCE_ACTION_COLLECT_FINISH;
once_action_record_count = 0;
#if TEST_PRINT == 1
Serial.println("One action collection is completed.");//一次动作采集完成。
#endif
}
}
else{//未检测到动作
#if TEST_PRINT == 1
Serial.println("No action detected !!!");
#endif
}
}
//陀螺仪采集一次数据
void Collect_data(){
dirty = 0;
while (dirty != 1) {
measure();
analysis();
}
//Print_zbw_lzh_yjy();
delay(100);
}
//分析数据
void analysis() {
if (sign) {
sign = 0;
if (Re_buf[0] == 0X5A && Re_buf[1] == 0X5A) {
dirty = 1;
acc[0] = (Re_buf[4] << 8 | Re_buf[5]);
acc[1] = (Re_buf[6] << 8 | Re_buf[7]);
acc[2] = (Re_buf[8] << 8 | Re_buf[9]);
gyro[0] = (Re_buf[10] << 8 | Re_buf[11]);
gyro[1] = (Re_buf[12] << 8 | Re_buf[13]);
gyro[2] = (Re_buf[14] << 8 | Re_buf[15]);
YPR[0] = (Re_buf[16] << 8 | Re_buf[17]);
YPR[1] = (Re_buf[18] << 8 | Re_buf[19]);
YPR[2] = (Re_buf[20] << 8 | Re_buf[21]);
for (int j = 0; j < 3; j++) {
if (YPR[j] > 46000) {
YPR[j] = YPR[j] - 29535;
}
else {
YPR[j] = YPR[j];
}
}
aX = acc[0] / 16383.5;
aY = acc[1] / 16383.5;
aZ = acc[2] / 16383.5;
gX = gyro[0] / 16.3835;
gY = gyro[1] / 16.3835;
gZ = gyro[2] / 16.3835;
yX = YPR[0];
yY = YPR[1];
yZ = YPR[2];
}
}
}
//从GY-25Z读取数据
void measure() {
while (mySerial.available()) {
Re_buf[counter] = (unsigned char)mySerial.read();
if (counter == 0 && Re_buf[0] != 0x5A) {
return;
}
counter++;
if (counter == 25) {
counter = 0;
sign = 1;
}
}
}
//串口打印
void Print_zbw_lzh_yjy() {
Serial.println("********************************************************");
Serial.print("加速度计:");
Serial.print("\taX:");
Serial.print(aX);
Serial.print("\taY:");
Serial.print(aY);
Serial.print(" \taZ:");
Serial.println(aZ);
Serial.print("陀螺仪:");
Serial.print("\tgX:");
Serial.print(gX);
Serial.print(" \tgY:");
Serial.print(gY);
Serial.print(" \tgZ:");
Serial.println(gZ);
Serial.print("欧拉角:");
Serial.print("\tyX:");
Serial.print(yX);
Serial.print("\tyY:");
Serial.print(yY);
Serial.print("\tyZ:");
Serial.println(yZ);
Serial.println("********************************************************");
}
循环语句:
void loop(){
//Collect_data();//收集数据
if(action_count == ACTION_COUNT_MAX){//收集完毕,采集完10个动作
Serial.println("All actions are collected.");//所有动作采集完毕
while(1);//loop()停止
}
else{//继续采集
while(once_action_over_sign == ONCE_ACTION_COLLECT_NOT_FINISH){//读完一组动作时,70个节点采集完(0-69)
Collect_data();//收集完一个节点的数据
Record_data();//记录完一个节点的数据
}
once_action_over_sign = ONCE_ACTION_COLLECT_NOT_FINISH;
Print_once_action_data();//打印数据
action_count++;
#if TEST_PRINT == 1
Serial.print("action_count == ");
Serial.println(action_count);
#endif
}
}
每次采集后的数据,我们都保存在csv文件中【共有7000条数据】
👨🎨内容4:模型训练
数据采集完成之后,我们将对其进行模型训练
这里我们就用最简单的序列模型来实现本次项目
1️⃣数据预处理:
我们先把数据文件导入
这里为了便利,我仅导入了1800条数据
import tensorflow as tf
import tensorflow.keras as keras
import tensorflow.keras.layers as layers
import numpy as np
import pandas as pd
from tqdm import tqdm
flex = pd.read_csv('D:\新型桌面\嵌入式系统\期末大作业\\final.csv',header=None)
flex
接着我们将数据存储为一个二维矩阵
然后进行归一化处理
并给数据进行标签处理
SAMPLES_PER_GESTURE = 30 #每个手势样本包含的数据点数
def processData(d,v): #d:数据 v:标签
dataX = np.empty([0,SAMPLES_PER_GESTURE*6]) #存储特征数据
dataY = np.empty([0]) #存储标签数据
data = d.values
dataNum = 60
for i in tqdm(range(dataNum)): #提供一个循环进度条
tmp = [] #存储当前样本的所有处理后的数据点
for j in range(SAMPLES_PER_GESTURE): #遍历每个样本的70个数据点,进行归一化处理
tmp += [(data[i * SAMPLES_PER_GESTURE + j][0] + 4.0) / 8.0]
tmp += [(data[i * SAMPLES_PER_GESTURE + j][1] + 4.0) / 8.0]
tmp += [(data[i * SAMPLES_PER_GESTURE + j][2] + 4.0) / 8.0]
tmp += [(data[i * SAMPLES_PER_GESTURE + j][3] + 2000.0) / 4000.0]
tmp += [(data[i * SAMPLES_PER_GESTURE + j][4] + 2000.0) / 4000.0]
tmp += [(data[i * SAMPLES_PER_GESTURE + j][5] + 2000.0) / 4000.0]
tmp = np.array(tmp)
tmp = np.expand_dims(tmp, axis=0) #提高维度 [420] => [1,420]
dataX = np.concatenate((dataX, tmp), axis=0) #将tmp添加到dataX的末尾,这样dataX就包含了所有样本的特征
dataY = np.append(dataY, v) #将当前样本的标签v[i]添加到dataY数组的末尾
return dataX, dataY
flexX, flexY = processData(flex,0)
dataX = flexX
dataY = flexY
dataY
2️⃣准备数据集:
我们将80%的数据作为训练集,20%的数据作为测试集
premutationTrain = np.random.permutation(dataX.shape[0])
dataX = dataX[premutationTrain]
dataY = dataY[premutationTrain]
idx = int(dataX.shape[0] *0.8) #80%训练集 20%测试集
x_train = dataX[0:idx]
y_train = dataY[0:idx]
x_test = dataX[idx:dataX.shape[0]]
y_test = dataY[idx:dataY.shape[0]]
3️⃣训练模型:
所有准备工作完成后
我们开始进行模型训练
我们创建一个序列模型
并将层按顺序堆叠起来
model = keras.Sequential() #使用Sequential顺序模型
model.add(keras.layers.Dense(32, input_shape=(6 * SAMPLES_PER_GESTURE,), activation='relu'))
model.add(keras.layers.Dense(16, activation='relu'))
model.add(keras.layers.Dense(2, activation='softmax'))
接着我们初始化了一个Adam优化器,并进行模型编译
Adam是一种自适应学习率优化算法,常用于训练深度学习模型
adam = keras.optimizers.Adam()
model.compile(loss='sparse_categorical_crossentropy',
optimizer=adam,
metrics=['sparse_categorical_crossentropy'])
model.summary() #查看模型各层的参数状况
最后我们进行模型训练
history = model.fit(x_train,y_train,batch_size=1, validation_data=(x_test,y_test), epochs=200, verbose=1) #模型训练
4️⃣模型转换保存:
由于我们要把模型烧录到ESP32开发板中
因此我们这里将训练好的模型进行一个转换保存
我们使用linux命令xxd –i 将model二进制文件内容存储在C代码静态数组
converter = tf.lite.TFLiteConverter.from_keras_model(model) #模型转换保存
tflite_model = converter.convert()
open("Final_model","wb").write(tflite_model)
🙇内容5:效果展示
为了使我们的项目更加模块化
我们绘制了PCB板来做成一个简易的跳绳装置
接着,我们在Arduino中,将上述的模型导入
我们将实时采取运动数据
并将数据传入到模型中进行计算
最终得出是否实现跳绳动作
为了更好的展示跳绳的效果
我们将最终的数据同步到前端页面中
具体的前端代码和ESP32的代码
这边就不做介绍
大家可以直接在文末获取整个项目的代码!
所有的代码都做了相应的注释
那我们来看一下最终的实现效果吧!
文档成果物:
内容包括【完整的项目报告+效果演示视频】(满分项目报告!!!)
点击下载文档成果物
系统工程文件
资源包括【ESP32采集代码、ESP32跳绳识别代码、模型训练代码、前端展示代码、PCB板工程文件】
点击下载系统工程文件