魔改车钥匙实现远程控车:(3)通过蓝牙与手机通信并持久化保存参数设置

前言

在前两篇文章中,我们已经完成了控制与距离感应,建议先看完前两篇文章再来看这篇文章,不然你会看的一头雾水的。

在今天这篇文章中,我们需要解决的是将某些参数设置持久化储存在 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 接收数据时是使用流的方式一个字节一个字节的接收的。

这意味着我们可能需要定义一个通信协议用来传输更新参数。

这里我们就简单定义如下:

1

第一个字节固定为 FF 表示起始标记;

第二个字节为需要设置的参数码,例如这里我把 01 定义为设置扫描持续时间;

第三个字节表示将该参数值设置为多少,例如这里表示更新扫描持续时间为 5;

第四个字节固定为 FF 表示结束标记。

其实这个协议非常粗糙,目前有几个问题:

  1. 数据结束符可能会和设置的参数值冲突,如果参数值设置为 FF(255)就会被错误的处理为结束。这个也很好解决,只要在通信头紧跟一个字节用于表示所有通信数据长度即可,这样就可以不用设置结束符,也避免了冲突。但是这里就先不改了(因为我懒)。
  2. 没有对通信数据做校验,因为是无线通信,所以肯定会有数据丢失的情况发生,正常来说都应该加一个校验,例如 CRC 校验,但是这里先不加了(还是因为我懒)
  3. 设置参数最大值只能支持设置到 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

上面图 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 一键设置多方便啊。

哈哈,终于可以干回我的老本行:安卓 了。