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


    前言

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

    在今天这篇文章中,我们需要解决的是将某些参数设置持久化储存在 ESP32 的储存器中,并且在重新上电运行时实时读取保存的参数。

    而这些参数应该由手机通过蓝牙与 ESP32 通信来设置。

    正如前面说过的,为了安全性,所以这里的与手机通讯会用回经典蓝牙。

    实现过程

    持久化存储

    EEPROM

    Arduino 自带一个持久化储存的方案: EEPROM

    EEPROM (Electrically Erasable Programmable Read-Only Memory),电可擦可编程只读存储器–一种掉电后数据不丢失的存储芯片。

    EEPROM 使用也比较简单,首先导入头文件 #include

    写入数据:

    EEPROM.write(addr, val);

    读取数据:

    EEPROM.read(address);

    可以看到,EEPROM 读取和写入都是直接按地址写/读字节,对于我们的需求:储存参数设置。十分的不方便,还需要我们自行处理保存地址和参数的关系。非常麻烦,因此我们放弃这个方案。

    好在 ESP32 支持一种键值对的持久化储存方法:Preferences

    Preferences

    Preferences 的键值对储存方法简直就是为了储存参数量身定制的。

    先来简单的看一下怎么使用:

    首先依然是导入头文件:

    #include

    然后定义一个 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 中的数据确定扫描持续时间:

    1. ....
    2. #define KEY_SCAN_TIME "SCAN_TIME"
    3. ....
    4. void start_scan() {
    5. isScanedDevice = 0;
    6. BLEDevice::init("");
    7. BLEScan* pBLEScan = BLEDevice::getScan();
    8. pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
    9. pBLEScan->setActiveScan(true);
    10. BLEScanResults foundDevices = pBLEScan->start(prefs.getInt(KEY_SCAN_TIME, 5)); // 扫描持续时间实时从 Preferences 读取
    11. if (isScanedDevice == 0) {
    12. Serial.println("Power Off by ble not found!");
    13. lock_car();
    14. }
    15. }
    16. ...
    17. 复制代码

    而更新参数只需要在接受到新值时 prefs.putInt(KEY_SCAN_TIME, newValue); 即可。

    与手机通信更新设置参数

    上一节中我们已经说了如何持久化储存参数设置,但是光是能储存显然是不够的,我们还需要支持通过手机与 ESP32 通信更新参数。

    在之前的代码中,我们已经实现了通过经典蓝牙让手机和 ESP32 交互数据。

    但是有一个问题,ESP32 接收数据时是使用流的方式一个字节一个字节的接收的。

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

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

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

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

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

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

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

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

    代码如下:

    1. void loop()
    2. {
    3. if (!isConnectDevice) {
    4. start_scan(); // 开始搜索 BLE 设备
    5. }
    6. if (confirmRequestPending)
    7. {
    8. if (Serial.available())
    9. {
    10. int dat = Serial.read();
    11. if (dat == '1')
    12. {
    13. SerialBT.confirmReply(true);
    14. }
    15. else
    16. {
    17. SerialBT.confirmReply(false);
    18. }
    19. }
    20. }
    21. else
    22. {
    23. if (Serial.available())
    24. {
    25. SerialBT.write(Serial.read());
    26. }
    27. if (SerialBT.available())
    28. {
    29. int msg = SerialBT.read();
    30. Serial.write(msg);
    31. if (msg == 1) {
    32. get_start();
    33. Serial.println("rev 1");
    34. }
    35. else if (msg == 2) {
    36. cut_power();
    37. Serial.println("rev 2");
    38. }
    39. else if (msg == 3) {
    40. lock_car();
    41. Serial.println("rev 3");
    42. }
    43. else if (msg == 4) {
    44. luncher_car();
    45. Serial.println("rev 4");
    46. }
    47. else if (msg == 5) {
    48. shut_down_car();
    49. Serial.println("rev 5");
    50. }
    51. else if (msg == 6) {
    52. find_my_car();
    53. Serial.println("rev 6");
    54. }
    55. else if (msg == 7) {
    56. click_loop();
    57. Serial.println("rev 7");
    58. }
    59. else if (msg == 8) {
    60. click_lock();
    61. Serial.println("rev 8");
    62. }
    63. else if (msg == 9) {
    64. click_unlock();
    65. Serial.println("rev 9");
    66. } // 上面这几个(1-10)都是之前写的手动控制
    67. else if (msg == 255) { // 接受到设置参数的头字符
    68. Serial.println("rev 255");
    69. while (SerialBT.available()) {
    70. msg = SerialBT.read();
    71. if (setName == "null") { // 设置参数类型为空,在这里读取设置参数类型
    72. if (msg == 1) {
    73. Serial.println("rev 1 in setting");
    74. setName = KEY_SCAN_TIME; // 间隔时间
    75. }
    76. else if (msg == 2) {
    77. Serial.println("rev 2 in setting");
    78. setName = KEY_RSSI; // rssi 阈值
    79. }
    80. else if (msg == 3) {
    81. Serial.println("rev 3 in setting");
    82. setName = KEY_IS_UNLOCK; // 是否触发解锁
    83. }
    84. else { // 查找到未指定的参数类型,退出
    85. Serial.println("rev else in setting");
    86. Serial.printf("value=%d\n", msg);
    87. break;
    88. }
    89. }
    90. else if (msg == 255) { // 接收到请求设置参数结束符号
    91. Serial.println("rev 0 in setting");
    92. break;
    93. }
    94. else { // 这里是参数的具体数值,开始保存(参数值不能设置成 255 否则会和结束符号冲突)
    95. Serial.println("rev else in setting");
    96. Serial.printf("new value=%d\n", msg);
    97. prefs.putInt(setName, msg);
    98. setName = "null"; // 重置参数名
    99. }
    100. }
    101. }
    102. else if (msg == 10) {
    103. Serial.println("rev 10");
    104. read_status();
    105. }
    106. }
    107. }
    108. }
    109. 复制代码

    运行效果如下:

    上面图 2 是 手机端的截图。

    图 3 是 ESP32 的串口日志。

    手机端的截图它自动把十六进制数据转换成了 ACII 字符,所以,^J 表示 A;^A 表示 1;“乱码”表示的是 FF。

    可以看到,我在手机端先发送了一个读取状态指令 A(10),其中扫描持续时间 scale_time 现在是 10 。

    然后我发送了一个更新 01 (持续时间)参数为 01 的指令。

    最后又发送了一个读取状态指令,可以看到持续时间已经成功被改为 1 了。

    对了,读取状态的代码为:

    1. void read_status() {
    2. int power_value = digitalRead(PIN_POWER);
    3. int loop_value = digitalRead(PIN_LOOP);
    4. int lock_value = digitalRead(PIN_LOCK);
    5. int unlock_value = digitalRead(PIN_UNLOCK);
    6. int scale_time = prefs.getInt(KEY_SCAN_TIME, 5);
    7. int rssi = prefs.getInt(KEY_RSSI, 100);
    8. int is_unlock = prefs.getInt(KEY_IS_UNLOCK, 0);
    9. char s[200];
    10. 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);
    11. SerialBT.print(s);
    12. }
    13. 复制代码

    总结

    自此,ESP32 程序基本已经为完全完成了,下一步是编写一个专属的控制 APP,毕竟不可能每次更新参数都要手撸代码吧,多麻烦啊,写个傻瓜式 APP 一键设置多方便啊。

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

  • 相关阅读:
    【luogu P3295】萌萌哒(并查集)(倍增)
    第十五章 Spring Cloud Alibaba 入门介绍
    Pr:添加字幕轨道
    postman报错 “error“: “Unsupported Media Type“
    SAP 物料分类账配置详解Part 2( 基于SAP S/4HANA1909 版本)
    性能调优读书笔记(下篇)
    电脑锁屏设置的方法,直接在系统设置里完成
    insightface数据制作全过程记录更新
    服务器配置宝塔(Linux 服务器运维管理软件)
    VUE 入门及应用 ( API )
  • 原文地址:https://blog.csdn.net/sinat_17133389/article/details/126931943