前言
在前两篇文章中,我们已经完成了控制与距离感应,建议先看完前两篇文章再来看这篇文章,不然你会看的一头雾水的。
在今天这篇文章中,我们需要解决的是将某些参数设置持久化储存在 ESP32 的储存器中,并且在重新上电运行时实时读取保存的参数。
而这些参数应该由手机通过蓝牙与 ESP32 通信来设置。
正如前面说过的,为了安全性,所以这里的与手机通讯会用回经典蓝牙。
实现过程
持久化存储
EEPROM
Arduino 自带一个持久化储存的方案: EEPROM
EEPROM (Electrically Erasable Programmable Read-Only Memory),电可擦可编程只读存储器–一种掉电后数据不丢失的存储芯片。
EEPROM 使用也比较简单,首先导入头文件 #include <EEPROM.h>
。
写入数据:
EEPROM.write(addr, val);
读取数据:
EEPROM.read(address);
可以看到,EEPROM 读取和写入都是直接按地址写/读字节,对于我们的需求:储存参数设置。十分的不方便,还需要我们自行处理保存地址和参数的关系。非常麻烦,因此我们放弃这个方案。
好在 ESP32 支持一种键值对的持久化储存方法:Preferences
Preferences
Preferences 的键值对储存方法简直就是为了储存参数量身定制的。
先来简单的看一下怎么使用:
首先依然是导入头文件:
#include <Preferences.h>
然后定义一个 Preferences 对象:
Preferences prefs;
创建命名空间:
prefs.begin("setting");
写入:
prefs.putXXX("key", value);
读取:
prefs.getInt("key", defalutValue)
注意:上面是伪代码,运行不了的
因为 Preferences 储存的是键值对,所以不管是写入还是读取,都需要一个 key,且 key 只能是 string。
而写入的数据可以是所有 c++ 支持的数据类型,例如想要写入 int 则将XXX 替换为 int:
prefs.putInt("key", int_value);
需要注意的是,如果写入同一个命名空间的键(key)重复的话会覆盖(更新)已有键的值为新值。
读取数据时同理,如果需要读取 int 数据则为:
prefs.getInt("key", defalutIntValue)
第二个参数为默认值,如果没有查找到该键对应的值则返回这个默认值。
另外,prefs.end();
用于关闭命名空间。
更改参数
确定了方案后,我们来更改之前写的代码。
这里以 BLE 扫描持续为例,之前我们是直接硬编码了扫描持续时间为 5 s。
现在我们改为通过读取 Preferences 中的数据确定扫描持续时间:
....
#define KEY_SCAN_TIME "SCAN_TIME"
....
void start_scan() {
isScanedDevice = 0;
BLEDevice::init("");
BLEScan* pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true);
BLEScanResults foundDevices = pBLEScan->start(prefs.getInt(KEY_SCAN_TIME, 5)); // 扫描持续时间实时从 Preferences 读取
if (isScanedDevice == 0) {
Serial.println("Power Off by ble not found!");
lock_car();
}
}
...
而更新参数只需要在接受到新值时 prefs.putInt(KEY_SCAN_TIME, newValue);
即可。
与手机通信更新设置参数
上一节中我们已经说了如何持久化储存参数设置,但是光是能储存显然是不够的,我们还需要支持通过手机与 ESP32 通信更新参数。
在之前的代码中,我们已经实现了通过经典蓝牙让手机和 ESP32 交互数据。
但是有一个问题,ESP32 接收数据时是使用流的方式一个字节一个字节的接收的。
这意味着我们可能需要定义一个通信协议用来传输更新参数。
这里我们就简单定义如下:
第一个字节固定为 FF 表示起始标记;
第二个字节为需要设置的参数码,例如这里我把 01 定义为设置扫描持续时间;
第三个字节表示将该参数值设置为多少,例如这里表示更新扫描持续时间为 5;
第四个字节固定为 FF 表示结束标记。
其实这个协议非常粗糙,目前有几个问题:
- 数据结束符可能会和设置的参数值冲突,如果参数值设置为 FF(255)就会被错误的处理为结束。这个也很好解决,只要在通信头紧跟一个字节用于表示所有通信数据长度即可,这样就可以不用设置结束符,也避免了冲突。但是这里就先不改了(因为我懒)。
- 没有对通信数据做校验,因为是无线通信,所以肯定会有数据丢失的情况发生,正常来说都应该加一个校验,例如 CRC 校验,但是这里先不加了(还是因为我懒)
- 设置参数最大值只能支持设置到 255 ,因为参数值只能使用一个字节,所以最大值只能是 255,解决方法同 1(但是我还是懒得改)
代码如下:
void loop()
{
if (!isConnectDevice) {
start_scan(); // 开始搜索 BLE 设备
}
if (confirmRequestPending)
{
if (Serial.available())
{
int dat = Serial.read();
if (dat == '1')
{
SerialBT.confirmReply(true);
}
else
{
SerialBT.confirmReply(false);
}
}
}
else
{
if (Serial.available())
{
SerialBT.write(Serial.read());
}
if (SerialBT.available())
{
int msg = SerialBT.read();
Serial.write(msg);
if (msg == 1) {
get_start();
Serial.println("rev 1");
}
else if (msg == 2) {
cut_power();
Serial.println("rev 2");
}
else if (msg == 3) {
lock_car();
Serial.println("rev 3");
}
else if (msg == 4) {
luncher_car();
Serial.println("rev 4");
}
else if (msg == 5) {
shut_down_car();
Serial.println("rev 5");
}
else if (msg == 6) {
find_my_car();
Serial.println("rev 6");
}
else if (msg == 7) {
click_loop();
Serial.println("rev 7");
}
else if (msg == 8) {
click_lock();
Serial.println("rev 8");
}
else if (msg == 9) {
click_unlock();
Serial.println("rev 9");
} // 上面这几个(1-10)都是之前写的手动控制
else if (msg == 255) { // 接受到设置参数的头字符
Serial.println("rev 255");
while (SerialBT.available()) {
msg = SerialBT.read();
if (setName == "null") { // 设置参数类型为空,在这里读取设置参数类型
if (msg == 1) {
Serial.println("rev 1 in setting");
setName = KEY_SCAN_TIME; // 间隔时间
}
else if (msg == 2) {
Serial.println("rev 2 in setting");
setName = KEY_RSSI; // rssi 阈值
}
else if (msg == 3) {
Serial.println("rev 3 in setting");
setName = KEY_IS_UNLOCK; // 是否触发解锁
}
else { // 查找到未指定的参数类型,退出
Serial.println("rev else in setting");
Serial.printf("value=%d\n", msg);
break;
}
}
else if (msg == 255) { // 接收到请求设置参数结束符号
Serial.println("rev 0 in setting");
break;
}
else { // 这里是参数的具体数值,开始保存(参数值不能设置成 255 否则会和结束符号冲突)
Serial.println("rev else in setting");
Serial.printf("new value=%d\n", msg);
prefs.putInt(setName, msg);
setName = "null"; // 重置参数名
}
}
}
else if (msg == 10) {
Serial.println("rev 10");
read_status();
}
}
}
}
运行效果如下:
上面图 2 是 手机端的截图。
图 3 是 ESP32 的串口日志。
手机端的截图它自动把十六进制数据转换成了 ACII 字符,所以,^J
表示 A;^A
表示 1;“乱码”表示的是 FF。
可以看到,我在手机端先发送了一个读取状态指令 A(10),其中扫描持续时间 scale_time 现在是 10 。
然后我发送了一个更新 01 (持续时间)参数为 01 的指令。
最后又发送了一个读取状态指令,可以看到持续时间已经成功被改为 1 了。
对了,读取状态的代码为:
void read_status() {
int power_value = digitalRead(PIN_POWER);
int loop_value = digitalRead(PIN_LOOP);
int lock_value = digitalRead(PIN_LOCK);
int unlock_value = digitalRead(PIN_UNLOCK);
int scale_time = prefs.getInt(KEY_SCAN_TIME, 5);
int rssi = prefs.getInt(KEY_RSSI, 100);
int is_unlock = prefs.getInt(KEY_IS_UNLOCK, 0);
char s[200];
sprintf(s, "Pin Status: \n power %d, loop %d, lock %d, unlock %d\n Setting Value: \n scale_time %d, rssi %d, is_unlock %d", power_value, loop_value, lock_value, unlock_value, scale_time, rssi, is_unlock);
SerialBT.print(s);
}
总结
自此,ESP32 程序基本已经为完全完成了,下一步是编写一个专属的控制 APP,毕竟不可能每次更新参数都要手撸代码吧,多麻烦啊,写个傻瓜式 APP 一键设置多方便啊。
哈哈,终于可以干回我的老本行:安卓 了。