Funpack第八期-基于Arduino Nano 33 BLE的投篮运动手柄
介绍用本板卡最终实现了什么功能
利用NANO-33 BLE的加速度及角速度感应器,设计一款用于虚拟练习投篮的手柄。 实现根据篮球抛物线轨迹是否穿过篮筐附近的矩形框计算是否投中,根据自定义的命中率函数计算命中率,将投篮结果通过串口和oled屏幕输出。
各功能对应的主要代码片段及解释
· 篮筐高度:3.05m
· 罚球线投篮距离:4.5m
笔者平时并不怎么玩篮球,所以有些地方可能不太合理敬请谅解哈。
所采用的arduino库
#include <ArduinoBLE.h>
#include <Arduino_LSM9DS1.h>
#include <math.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
loop函数调用了basketball_player();
void loop()
{
basketball_player();
}
主要的代码功能放在basketball_player
中
void basketball_player()
{
static float record_ax[times] = {0};
static float record_ay[times] = {0};
static float record_az[times] = {0};
static float record_time[times];
static int record_count = 0;
static unsigned long t;
static unsigned long previousMillis, currentMillis = 0;
static int smooth_count = 0;
static int num_count = 0;
static bool flag = 0; //防止误触发
// Serial.println("entering basketball_player!");
if (IMU.accelerationAvailable())
{
IMU.readAcceleration(ax, ay, az);
}
asum = abs(ax) + abs(ay) + abs(az);
if (asum >= accelerationThreshold_HIGH && record_count == 0)
{
Serial.println("检测到动作");
display.clearDisplay(); //清空屏幕
display.setCursor(0, 0); //设置起点
display.setTextSize(1); //设置字体
display.setTextColor(SSD1306_WHITE); //设置字体颜色
display.println(F("capture!"));
smooth_count = 0;
flag = 1;
}
if (smooth_count <= 10 && 1 == flag) //数据记录
{
if (asum > accelerationThreshold_LOW)
{
smooth_count = 0;
}
if (record_count > times - 1)
record_count = 0;
record_ax[record_count] = ax;
record_ay[record_count] = ay;
record_az[record_count] = az;
currentMillis = millis();
t = currentMillis - previousMillis;
record_time[record_count] = t;
previousMillis = currentMillis;
record_count++;
}
if (asum <= accelerationThreshold_LOW)
{ //疑似动作停止
if (smooth_count >= 10 && record_count > 0 && 1 == flag) //动作停止
{
Serial.println("检测完成");
Serial.print("收集到的三轴元组数为:");
Serial.println(record_count);
float v0 = 0;
float theta = acos(abs(record_ax[(int)record_count / 5 * 4]));
for (int i = 0; i < record_count; i++)
{
/* code */
Serial.print(record_ax[i]);
Serial.print('\t');
Serial.print(record_ay[i]);
Serial.print('\t');
Serial.println(record_az[i]);
v0 += record_az[i];
}
Serial.print("第");
Serial.print(++num_count);
Serial.println("次投篮结果");
display.print(F("No. "));
display.print(num_count);
display.println(F(" time"));
display.print(F("speed: "));
display.print(v0);
display.println(F(" m/s"));
display.print(F("theta: "));
display.println(theta * M_PI / 180);
Judgement(distance_l, theta, 9.81, v0);
flag = 0;
/*
Serial.print("收集到的时间元组数为:");//时间基本都为一毫秒,为了计算方便,也就直接把收集到的加速度加起来,得到一个速度.
Serial.println(record_count);
for (int i = 0; i < record_count; i++)
{
Serial.print(i);
Serial.print('\t');
Serial.println(record_time[i]);
}
*/
Serial.println("一次传输结束.");
record_count = 0;
display.display();
}
smooth_count++;
}
// Serial.print("smooth_count = ");
// Serial.println(smooth_count);
//
// Serial.print("record_count = ");
// Serial.println(record_count);
}
这个函数主要是根据设定的采集上下阈值,进行数据的采集,然后调用后续的计算函数进行计算。
上下阈值的确定是通过分析前期所采集的投篮动作时的加速度数据得到。
根据传感器的参数,更新频率104Hz,和前期的试验,我发现每次采样大致为1ms,所以就采用偷懒的方法,将符合采样要求的加速度求和,得到初始的速度。
而夹角的计算,由于我采用的板子投篮姿势固定,通过x轴加速度与重力加速度的反余弦求夹角。根据我的动作,取采样的后4/5数据作为稳定的数据。
float theta = acos(abs(record_ax[(int)record_count / 5 * 4]));
根据投篮速度,角度,距离计算投篮高度的函数height
,参考的公式为:
float height(float x, float theta, float g, float v0)
{
float y;
float theta1;
float theta2;
float temp;
theta1 = (float)tan(theta * M_PI / 180);
theta2 = (float)cos(theta * M_PI / 180);
temp = v0 * theta2;
y = x * theta1 - g * x * x / (2 * temp * temp);
return y;
}
笔者在篮筐位置处划定一个矩形,但凡投球通过这个区域则判定投中,反之投不中。如果判定未投中,就会调用概率计算函数计算投中的概率。
int Judgement(float x, float theta, float g, float v0)
{
if (fabs(x - 4) < 0.00001)
{
/* code */
Serial.println("你太用力了!!!");
display.println(F("too heavy!"));
return 0;
}
float value[3];
value[0] = height(distance_l - region_x, theta, g, v0);
value[1] = height(distance_l, theta, g, v0);
value[2] = height(distance_l + region_x, theta, g, v0);
if (value[0] > distance_h - region_y & value[2] < distance_h + region_y)
{
/* code */
Serial.println("你击中了!!!命中率100%");
display.println(F("you win! 100%"));
return 1;
}
else if (value[0] < distance_h - region_y)
{
Serial.println("很遗憾未击中,多用点力!!!");
display.println(F("more force!"));
hit_possibility(region_x, region_y, distance_h, value[0]);
}
else if (value[2] > distance_h + region_y)
{
Serial.println("很遗憾未击中,少用点力!!!");
display.println(F("less force!"));
hit_possibility(region_x, region_y, distance_h, value[2]);
}
return 0;
}
投中的概率计算函数hit_possibility
float hit_possibility(float region_x, float region_y, float distance_h, float value)
{
float nu = sqrt(region_y * region_y + region_x * region_x);
float de = sqrt(region_x * region_x + abs(distance_h - value));
float possibility = nu / de;
Serial.print("命中率");
Serial.println(possibility);
display.print(F("possibility: "));
display.println(possibility);
return possibility;
}
完整代码如下:
#include <ArduinoBLE.h>
#include <Arduino_LSM9DS1.h>
#include <math.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int times = 70;
const float accelerationThreshold_HIGH = 2.4; //阈值为重力的2.4倍
const float accelerationThreshold_LOW = 1.8;
const float distance_l = 4.35;
const float distance_h = 1.6; //提0.1
const float region_x = 0.2; //只有穿过矩形才算投中
const float region_y = 0.2;
float asum;
float ax, ay, az;
int flash;
float height(float, float, float, float);
int Judgement(float, float, float, float);
float hit_possibility(float, float, float, float);
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library.
// On an arduino UNO: A4(SDA), A5(SCL)
// On an arduino MEGA 2560: 20(SDA), 21(SCL)
// On an arduino LEONARDO: 2(SDA), 3(SCL), ...
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void display_init();
void setup()
{
display_init();
flash = 0;
// put your setup code here, to run once:
IMU.begin();
Serial.begin(9600);
// while (!Serial)
// ;
delay(1000);
}
void loop()
{
basketball_player();
}
void basketball_player()
{
static float record_ax[times] = {0};
static float record_ay[times] = {0};
static float record_az[times] = {0};
static float record_time[times];
static int record_count = 0;
static unsigned long t;
static unsigned long previousMillis, currentMillis = 0;
static int smooth_count = 0;
static int num_count = 0;
static bool flag = 0; //防止误触发
// Serial.println("entering basketball_player!");
if (IMU.accelerationAvailable())
{
IMU.readAcceleration(ax, ay, az);
}
asum = abs(ax) + abs(ay) + abs(az);
if (asum >= accelerationThreshold_HIGH && record_count == 0)
{
Serial.println("检测到动作");
display.clearDisplay(); //清空屏幕
display.setCursor(0, 0); //设置起点
display.setTextSize(1); //设置字体
display.setTextColor(SSD1306_WHITE); //设置字体颜色
display.println(F("capture!"));
smooth_count = 0;
flag = 1;
}
if (smooth_count <= 10 && 1 == flag) //数据记录
{
if (asum > accelerationThreshold_LOW)
{
smooth_count = 0;
}
if (record_count > times - 1)
record_count = 0;
record_ax[record_count] = ax;
record_ay[record_count] = ay;
record_az[record_count] = az;
currentMillis = millis();
t = currentMillis - previousMillis;
record_time[record_count] = t;
previousMillis = currentMillis;
record_count++;
}
if (asum <= accelerationThreshold_LOW)
{ //疑似动作停止
if (smooth_count >= 10 && record_count > 0 && 1 == flag) //动作停止
{
Serial.println("检测完成");
Serial.print("收集到的三轴元组数为:");
Serial.println(record_count);
float v0 = 0;
float theta = acos(abs(record_ax[(int)record_count / 5 * 4]));
for (int i = 0; i < record_count; i++)
{
/* code */
Serial.print(record_ax[i]);
Serial.print('\t');
Serial.print(record_ay[i]);
Serial.print('\t');
Serial.println(record_az[i]);
v0 += record_az[i];
}
Serial.print("第");
Serial.print(++num_count);
Serial.println("次投篮结果");
display.print(F("No. "));
display.print(num_count);
display.println(F(" time"));
display.print(F("speed: "));
display.print(v0);
display.println(F(" m/s"));
display.print(F("theta: "));
display.println(theta * 180 /M_PI);
Judgement(distance_l, theta, 9.81, v0);
flag = 0;
/*
Serial.print("收集到的时间元组数为:");//时间基本都为一毫秒,为了计算方便,也就直接把收集到的加速度加起来,得到一个速度.
Serial.println(record_count);
for (int i = 0; i < record_count; i++)
{
Serial.print(i);
Serial.print('\t');
Serial.println(record_time[i]);
}
*/
Serial.println("一次传输结束.");
record_count = 0;
display.display();
}
smooth_count++;
}
// Serial.print("smooth_count = ");
// Serial.println(smooth_count);
//
// Serial.print("record_count = ");
// Serial.println(record_count);
}
float height(float x, float theta, float g, float v0)
{
float y;
float theta1;
float theta2;
float temp;
theta1 = (float)tan(theta * 180 / M_PI);
theta2 = (float)cos(theta * 180 / M_PI);
temp = v0 * theta2;
y = x * theta1 - g * x * x / (2 * temp * temp);
return y;
}
int Judgement(float x, float theta, float g, float v0)
{
if (fabs(x - 4) < 0.00001)
{
/* code */
Serial.println("你太用力了!!!");
display.println(F("too heavy!"));
return 0;
}
float value[3];
value[0] = height(distance_l - region_x, theta, g, v0);
value[1] = height(distance_l, theta, g, v0);
value[2] = height(distance_l + region_x, theta, g, v0);
if (value[0] < distance_h - region_y)
{
Serial.println("很遗憾未击中,多用点力!!!");
display.println(F("more force!"));
hit_possibility(region_x, region_y, distance_h, value[0]);
}
else if (value[2] > distance_h + region_y)
{
Serial.println("很遗憾未击中,少用点力!!!");
display.println(F("less force!"));
hit_possibility(region_x, region_y, distance_h, value[2]);
}
else if (value[0] > distance_h - region_y & value[2] < distance_h + region_y)
{
/* code */
Serial.println("你击中了!!!命中率100%");
display.println(F("you win! 100%"));
return 1;
}
return 0;
}
float hit_possibility(float region_x, float region_y, float distance_h, float value)
{
float nu = sqrt(region_y * region_y + region_x * region_x);
float de = sqrt(region_x * region_x + abs(distance_h - value));
float possibility = 1 - nu / de;
Serial.print("命中率");
Serial.println(possibility);
display.print(F("possibility: "));
display.println(possibility);
return possibility;
}
void display_init()
{
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.setCursor(0, 0); //设置起点
display.setTextSize(2); //设置字体
display.setTextColor(SSD1306_WHITE); //设置字体颜色
display.println(F("Basketball"));
display.println(F("Player"));
display.display();
}
输出情况
串口接收到的数据
检测到动作
检测完成
收集到的三轴元组数为:69
-0.42 -0.87 -1.64
-0.42 -0.87 -1.64
-0.42 -0.87 -1.64
-0.42 -0.87 -1.64
-0.42 -0.87 -1.64
-0.42 -0.87 -1.64
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.56 -0.75 -1.32
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.71 -0.64 -1.04
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-0.93 -0.62 -0.73
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.17 -0.61 -0.43
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
-1.23 -0.50 -0.04
第8次投篮结果
一次传输结束.
运行照片
对本活动的心得体会(包括意见或建议)
这是我第一次参加活动,本活动很有意思,不仅能够结交一些大佬,也能给枯燥的嗑盐生活找些乐子。会继续参加下去。
由于不咋玩篮球,所以有些地方想当然了,肯定各位大佬批评指正!
其实完全可以通过BLE传输结果,并进行控制,只是这个需要将传输的数据进行解码,或者说是还原,需要配套开发对应的BLE应用,比较麻烦,所以就没加进去。可视化的效果比较差。貌似有一款blinker的sdk能够很方便的创建应用,下次可以试试!
ssd1306编译失败的时候需要将头文件中#define SSD1306_128_64
取消注释
参考链接
https://ladvien.com/arduino-nano-33-bluetooth-low-energy-setup/
https://www.hackster.io/gov/imu-to-you-ae53e1
https://rootsaid.com/arduino-ble-accelerometer-tutorial/
https://www.jianshu.com/p/c327e495f89c
https://rootsaid.com//arduino-ble-example/
http://www.varesano.net/blog/fabio/simple-gravity-compensation-9-dom-imus
https://www.bilibili.com/video/BV11y4y1Y7py
https://www.jianshu.com/p/d7b56c11bb9b
https://www.zhihu.com/question/24099021
https://www.yiboard.com/thread-1256-1-1.html