😃 😃 😃由于最近做一些项目,因为不想频繁的连线,所以想到了无线调参的功能。
然后想起了以前看过的自平衡莱洛三角形的项目中有用到调参的功能,所以想把这部分功能单独拿出来做一个简单的整理,方便日后使用。莱洛三角形项目我的主页中也有专栏介绍。
一是方便自己日后使用调参功能,二是写出来也给大家做一下参考。
先祝愿我的代码没有BUG!!!
不知道我的拖延症会多久才能把这个写完,尤其是现在还要教弟弟中考。
本方法无线调参用到了EEPROM存储需要调整的参数,每次循环都要重新读取EEPROM中所存储的参数,然后进行运行。调参实际上是修改了对应参数在EEPROM的存储空间中存储的值。
原理是不是非常简单??
接下来,就是程序的问题了!!(很晚了,先睡觉,明天说!)
其中,由于需要用到EEPROM,故需引用 EEPROM.h 。
写EEPROM功能
写EEPROM
先看写入 EEPROM 库的内容
需要注意的是:
EEPROM.write(address,value) 往内存控件写入数据,只是写道申请的内存空间。
address:写入地址位置、value:写入的数据
EEPROM.cpmmit() 该功能用于把内存控件的数据覆盖到flash eeprom块去,真正写回到flash空间
先用这三个参数为例子写EEPROM的值
//其中的target_angle、swing_up_voltage、swing_up_angle是需要调的参数,都定义了初始值!
void do_TA(char* cmd) //是读到的值
{
comm.scalar(&target_angle, cmd); //给读到的值进行浮点数处理
EEPROM.writeFloat(0,target_angle); //写入到EEPROM中
}
void do_SV(char* cmd)
{
comm.scalar(&swing_up_voltage, cmd);
EEPROM.writeFloat(4, swing_up_voltage);
}
void do_SA(char* cmd)
{
comm.scalar(&swing_up_angle, cmd);
EEPROM.writeFloat(8, swing_up_angle);
}
其中的给读到的字符量转换为浮点数的定义为:
void Command::scalar(float* value, char* user_cmd)
{
*value = atof(user_cmd); //atof()把字符串转换成浮点数
}
//主函数文件中创建了实例:
Command comm;
读EEPROM
▶️ 咱就说,光写有个屁用!还得把EEPROM中的数据读出来供给程序使用啊!
//读对应的参数
target_angle = EEPROM.readFloat(0);
swing_up_voltage = EEPROM.readFloat(4);
swing_up_angle = EEPROM.readFloat(8);
对EEPROM函数的调用
简单流程:①注册函数到函数序列 ②需修改参数时判断需调用函数 ③调用写eeprom函数
这一部分的方法非常好,不只是可以适用于调节参数,还能修改以调用函数进行调试!
首先是一些基础的定义
//回调函数指针定义,定义的是指向函数的指针!
typedef void (* CommandCallback)(char*);
CommandCallback call_list[20]; //这一部分通过数组定义了存储函数指针的序列
char* call_ids[20]; //存储函数名
int call_count; //用于计算有多少函数写入序列
添加函数到函数序列中!
这一点还是很好理解的。方便调用!
void Command::add(char* id, CommandCallback onCommand)
{
call_list[call_count] = onCommand; //函数名
call_ids[call_count] = id; //参数
call_count++;
}
添加完了调用的时候怎么调用呢???
✅ 这就是怎么调用
假设传入的值为 TA28 ,str+strlen(call_ids[i]) 已经把该指针从 T 移动到了 2 !!这一点很重要,需要理解。
call_list[i](str+strlen(call_ids[i]));
就变成了do_TA(str+strlen(call_ids[i])); 即是最初写EEPROM的格式。
void Command::run(char* str) //读到的char型变量
{
for(int i=0; i < call_count; i++) //遍历存储函数名称的序列
{
if(isSentinel(call_ids[i],str)) //判断输入与函数名称序列是否对应
{
call_list[i](str+strlen(call_ids[i])); //调用对应函数指针序列的函数
break;
}
}
}
判断输入与函数名称序列是否对应函数:
此时假设esp32udp读到的为 TA28,此部分只判断 TA 有没有在函数名称序列里有没有与之相对应的函数名。28为传入的要修改的参数变量。
//第一个参数为函数名序列中某个函数的名字,第二个参数为读到的值
bool Command::isSentinel(char* ch,char* str)
{
char s[strlen(ch)+1]; //新建一个数组
strncpy(s,str,strlen(ch)); //复制输入的char变量,只复制名字部分
s[strlen(ch)] = '\0'; //数组末尾添加\0表示字符结束。
if(strcmp(ch, s) == 0) //比较字符,为字符ASCII码相减值
return true; //对应返回 1
else
return false; //不对应返回 0
}
注册函数
comm.add("TA",do_TA);
comm.add("SV",do_SV);
comm.add("SA",do_SA);
调参实现
上面所说都是调用的函数相关的信息,但是怎么调用呢?
▶️ 打算采用UDP协议来传输数据进行调用!
首先介绍收到数据包的事件
void onPacketCallBack(AsyncUDPPacket packet)
{
char* da;
da= (char*)(packet.data()); //强制类型转换
Serial.println(da); //串口输出数据,跟一个回车
comm.run(da); //执行修改参数入口函数
EEPROM.commit(); //提交操作,写入EEPROM
}
UDP的准备工作!
WiFi.mode(WIFI_AP); //设置为接入点模式AP
while(!WiFi.softAP(ssid, password)){}; //启动AP,函数获取并打印软AP的IP
Serial.println("AP启动成功");
while (!udp.listen(localUdpPort)){}; //等待udp监听设置成功
udp.onPacket(onPacketCallBack); //注册收到数据包事件
项目总览
✔️✔️✔️ 就算介绍了相关函数,但还是有很多同学不会!简单说明一下结构方面吧。
(以下代码未经过验证,仅仅是提供思路)
可把command放置在另一文件下。
上面已经详细介绍!现在不做注释
Command.cpp
//Command.cpp
#include "Command.h"
void Command::run(char* str)
{
for(int i=0; i < call_count; i++)
{
if(isSentinel(call_ids[i],str))
{
call_list[i](str+strlen(call_ids[i]));
break;
}
}
}
void Command::add(char* id, CommandCallback onCommand)
{
call_list[call_count] = onCommand;
call_ids[call_count] = id;
call_count++;
}
void Command::scalar(float* value, char* user_cmd)
{
*value = atof(user_cmd);
}
bool Command::isSentinel(char* ch,char* str)
{
char s[strlen(ch)+1];
strncpy(s,str,strlen(ch));
s[strlen(ch)] = '\0';
if(strcmp(ch, s) == 0)
return true;
else
return false;
}
Command.h
#include <Arduino.h>
typedef void (* CommandCallback)(char*);
class Command
{
public:
void add(char* id , CommandCallback onCommand);
void run(char* str);
void scalar(float* value, char* user_cmd);
bool isSentinel(char* ch,char* str);
private:
CommandCallback call_list[20];
char* call_ids[20];
int call_count;
};
main
因为创建了类,所以使用类要先创建实例
#include "Command.h"
#include <WiFi.h>
#include <AsyncUDP.h> //引用以使用异步UDP
#include "EEPROM.h"
AsyncUDP udp; //创建UDP实例
unsigned int localUdpPort = 2333; //本地端口号
Command comm; //创建Command实例
float target_angle = 89.3;
float swing_up_voltage = 1.8;
float swing_up_angle = 20;
const char *ssid = "esp32"; //账号
const char *password = "12345678"; //密码
void do_TA(char* cmd) { comm.scalar(&target_angle, cmd);EEPROM.writeFloat(0, target_angle); }
void do_SV(char* cmd) { comm.scalar(&swing_up_voltage, cmd); EEPROM.writeFloat(4, swing_up_voltage); }
void do_SA(char* cmd) { comm.scalar(&swing_up_angle, cmd);EEPROM.writeFloat(8, swing_up_angle); }
void onPacketCallBack(AsyncUDPPacket packet)
{
char* da;
da= (char*)(packet.data());
Serial.println(da);
comm.run(da);
EEPROM.commit();
}
void setup()
{
Serial.begin(115200); //设置串行数据通讯波特率
if (!EEPROM.begin(1000)) //申请ram内存空间并从flash中读取相应数据到内存 (4~4096)
{
Serial.println("Failed to initialise EEPROM"); //申请不到内存还玩个屁
Serial.println("Restarting...");
delay(1000);
ESP.restart(); //esp重启函数
}
// eeprom 读取,判断是否有数据,没有就填入初始数据
int k,j;
j = 0;
for(k=0;k<=8;k=k+4)
{
float nan = EEPROM.readFloat(k); //读取数据操作
if(isnan(nan)) //用于检查其参数是否为数值,是数值返回false,不是返回true
{
j = 1;
Serial.println("frist write");
//EEPROM.write(address,value)
//往内存控件写入数据,address:写入地址位置、value:写入的数据
//只是写道申请的内存空间
//EEPROM.cpmmit() 该功能用于把内存控件的数据覆盖到flash eeprom块去
//真正写回到flash空间
EEPROM.writeFloat(0, target_angle); delay(10);EEPROM.commit();
EEPROM.writeFloat(4, swing_up_voltage); delay(10);EEPROM.commit();
EEPROM.writeFloat(8, swing_up_angle); delay(10);EEPROM.commit();
}
}
if(j == 0) //有参数了
{
//读取数据操作
target_angle = EEPROM.readFloat(0);
swing_up_voltage = EEPROM.readFloat(4);
swing_up_angle = EEPROM.readFloat(8);
}
//注册函数
comm.add("TA",do_TA);
comm.add("SV",do_SV);
comm.add("SA",do_SA);
WiFi.mode(WIFI_AP); //设置为接入点模式AP
while(!WiFi.softAP(ssid, password)){}; //启动AP,函数获取并打印软AP的IP
Serial.println("AP启动成功");
while (!udp.listen(localUdpPort)){}; //等待udp监听设置成
udp.onPacket(onPacketCallBack); //注册收到数据包事件
}
void loop()
{
//其实这一段不写也行,就是每次调参需要人为重启一下才能更新。
//具体看自己的需求吧。
target_angle = EEPROM.readFloat(0);
swing_up_voltage = EEPROM.readFloat(4);
swing_up_angle = EEPROM.readFloat(8);
/***=======***/
//其他函数
/***=======***/
}
最后还有个非常重要的问题!!UDP!!,可搜索别人开发的UDP调试助手或者网络调试助手进行调参!
还是非常感谢开源环境的,能学到不少有用的知识,最后还是要感谢大佬 455555菌 提供的开源项目。
🎉🎉🎉最后说:本篇文章介绍的是无线调参,但是可通过修改部分代码实现无线调用函数功能,便于调试。也可和web配网那篇文章结合看,修改部分代码实现网页调参功能。都是可以的。
今日份又学会了新技能!😀😀😀😀😀
(由于是业余爱好者,并非行业从业人员,如有纰漏请您指出,谢谢!)