基于手势识别完成ESP32C3控制8位继电器实现智能鱼缸整体方案设计
具体项目代码已上传到gitee. 链接见文章结尾
1. 设计背景
题主有两个鱼缸一个龟缸, 三个缸每个缸包括一个灯, 一个过滤相当于六个设备
遇到的问题:
- 每次喂鱼时, 希望三个缸的过滤可以同时关闭, 等喂完鱼后, 能后在关闭20分钟后自动打开(喂鱼20分钟都没吃完, 那只能说鱼食喂多了)
- 每次停电在启动后, 能够自动打开过滤
- 每次人靠近能够自动亮灯, 人走后30分钟自动关灯(毕竟人走后可能一会又回来赏鱼了, 为了防止来来回回开关, 增加30分钟延长)
- 晚上有一个灯常亮 (鱼缸在客厅, 这样如果有需要到客厅办事, 有光的话就不会摸黑走路了)
2. 设计方案
对于上面遇到的四个问题, 给出设计方案
- 开始喂鱼时, 通过手势识别能够关闭所有的鱼缸过滤, 比如向上摆手
- 停电后自启动过滤: 设置一部分开关上电默认打开状态
- 增加人体感应模块, 做到人来人走感知, 控制灯光开闭
- 联网获取日出日落时间, 根据该时间实现夜晚自动亮灯的功能
2.1. 硬件相关
- 主控: esp32C3 XIAO, 优点: 小, 功耗低, WiFi, 蓝牙; 缺点: 贵, 引脚少
- 手势识别: PAJ7620, IIC通信
- 显示整体状态: oled0.9寸, IIC通信
- 继电器模块: 8位普通继电器模块
- 串行转并行控制芯片: HC595, 三个esp32的引脚即可控制8位继电器的输出
- 小爱音响: 控制8位继电器开关
- 米家人体存在传感器: 感知人体存在
2.2. 平台相关
- 米家: 接入米家设备(小爱音响, 人体存在传感器)和其他平台的设备
- 巴法云: mqtt的平台, 可以将本地设备状态关联联网, 在将巴法云接入到米家, 米家间接控制整个系统
2.3. 总结
整体的解决方案如下所示
3. 软件设计与实现
3.1. 整体规划 UML图
3.2. 不同器件控制的细节
3.2.1. 按键 短按, 长按, 双击设计
因为有八个开关, 需要使用按键来选择手势控制开关开闭时, 选择哪几个开关控制
即按键选择对应开关, 手势控制开关开闭状态
本项目只设计了一个开关, 需要将所有模式选择的功能都放到这个开关上, 所以在软件实现上增加了短按, 长按, 双击三个功能, 以此增加不同的场景控制
button软件设计
- 增加定时器 根据超时判断单击 双击 还是长按
/* 读取事件(非阻塞,读取后自动清除) */ KeyEvent readKeyEvent();
整个流程是往g_event写按键结果, 然后在非阻塞状态下, 获取这个按键结果, 处理对应的业务流- 如果在主函数的loop中调用
update_keyMode
将结果取走, 无法在其他位置获取结果, 因此本项目中按键的控制部分放到update_keyMode
中
#include <Arduino.h>
#include <Ticker.h>
#include "feature_control_switch.h"#define KEY_PIN D8 // 按键引脚
#define DEBOUNCE_MS 20 // 消抖时间
#define LONG_TIME 800 // 长按判定
#define DOUBLE_GAP 300 // 双击间隔enum KeyEvent {KEY_NONE,KEY_SHORT,KEY_LONG,KEY_DOUBLE
};static volatile KeyEvent g_event = KEY_NONE;/*---------------- 内部状态机 ----------------*/
static volatile bool key_raw = true; // 原始引脚电平
static volatile bool key_stable = true;// 消抖后的电平
static volatile uint32_t t_last = 0; // 上次变化时刻
static volatile bool pressed = false; // 已确认按下
static volatile uint32_t down_ms = 0; // 按下时间戳
static volatile uint8_t click_cnt = 0; // 点击计数/* 软件定时器 */
Ticker scanTicker; // 1 ms 周期扫描
Ticker longTicker;
Ticker doubleTicker;/* 设置事件给 loop() 取走 */
static void setEvent(KeyEvent e) {g_event = e;
}/* 长按超时 */
static void longTimeout() {if (pressed) {setEvent(KEY_LONG);/* 新增:清除后续可能的短按/双击 */click_cnt = 0;longTicker.detach();doubleTicker.detach();}
}/* 双击超时 */
static void doubleTimeout() {if (click_cnt == 1) {setEvent(KEY_SHORT);}click_cnt = 0;
}/* 1 ms 周期任务:消抖 + 状态机 */
static void scanTask() {bool now = digitalRead(KEY_PIN); // 读原始电平if (now != key_raw) { // 电平变化key_raw = now;t_last = millis();}/* 消抖完成,检查稳定电平 */if ((millis() - t_last) >= DEBOUNCE_MS) {if (key_stable != key_raw) {key_stable = key_raw;/* 稳定为低:按下 */if (!key_stable && !pressed) {pressed = true;down_ms = millis();click_cnt++;longTicker.once_ms(LONG_TIME, longTimeout);}/* 稳定为高:松开 */if (key_stable && pressed) {pressed = false;longTicker.detach();/* 如果已经触发过长按,这里什么也不做 */if (click_cnt == 0) return;if (click_cnt == 1) {doubleTicker.once_ms(DOUBLE_GAP, doubleTimeout);} else if (click_cnt == 2) {doubleTicker.detach();setEvent(KEY_DOUBLE);click_cnt = 0;}}}}
}/* 读取事件(非阻塞,读取后自动清除) */
KeyEvent readKeyEvent() {noInterrupts();KeyEvent e = g_event;g_event = KEY_NONE;interrupts();return e;
}void update_keyMode() {KeyEvent e = readKeyEvent();switch (e) {case KEY_SHORT: Serial.println("short"); incFeatureCtrSwMode(); break;case KEY_LONG: Serial.println("long"); break;case KEY_DOUBLE: Serial.println("double");break;default: break;}
}/*----------------------------------*/
void button_init() {pinMode(KEY_PIN, INPUT_PULLUP);/* 启动 1 ms 周期性扫描 */scanTicker.attach_ms(1, scanTask);
}void button_loop() {KeyEvent e = readKeyEvent();switch (e) {case KEY_SHORT: Serial.println("short"); break;case KEY_LONG: Serial.println("long"); break;case KEY_DOUBLE: Serial.println("double");break;default: break;}
}
3.2.2. 手势识别paj7620 上下左右
直接使用淘宝买的模块对应的demo即可
3.2.3. OLED显示 图片, 文字
直接使用淘宝买的模块对应的demo即可
3.2.4. HC595 串行转并行
这里使用ESP32的普通引脚, 用于控制74HC595输出指定的并行结果 比如 shiftOutByte(0x0f)
即对应的八位继电器状态是 00001111
#include <Arduino.h>// 引脚定义
#define PIN_HC595_DS D1 // 数据
#define PIN_HC595_SHCP D3 // 移位时钟
#define PIN_HC595_STCP D2 // 锁存时钟// 发送 1 字节到 74HC595
void shiftOutByte(uint8_t data) {digitalWrite(PIN_HC595_STCP, LOW); // 准备锁存for (uint8_t i = 0; i < 8; i++) {digitalWrite(PIN_HC595_DS, (data & (0x80 >> i)) ? HIGH : LOW);digitalWrite(PIN_HC595_SHCP, LOW);digitalWrite(PIN_HC595_SHCP, HIGH); // 上升沿移位}digitalWrite(PIN_HC595_STCP, HIGH); // 上升沿锁存
}void hc595_init() {pinMode(PIN_HC595_DS, OUTPUT);pinMode(PIN_HC595_SHCP, OUTPUT);pinMode(PIN_HC595_STCP, OUTPUT);digitalWrite(PIN_HC595_STCP, LOW);
}
3.2.5. 消息队列mqtt
直接使用巴法云对应的demo即可
3.2.6. 巴法接入米家控制新增的多个插座设备
图片中左侧部分是巴法云页面, 具体怎么使用见巴法云的文档, 这里增加8个开关
关联米家里八个开关, 测试可通过米家的小爱音响控制, 看esp32收到的消息是否正确
具体代码在 include\mqtt.h
中
3.3. 整合上述器件, 完成预期设想
feature_control_switch.cpp
processon绘图链接
https://www.processon.com/view/link/68739a8e5f31900ebd96f314?cid=68738ef1d8962758cba631cd
4. 最终展示
通过按钮点按, 选择控制哪几个继电器, 如下所示, 比如模式FeatureCtrSwMode_S0S1 表示当前控制的是01两个开关
向左手势打开0开关, 向右打开1开关, 向上全打开, 向下全关比
以此类推, 比如 FeatureCtrSwMode_S01234567 表示当前控制01234567八个开关, 向左打开0123, 向右打开4567, 向上全开, 向下全关
enum FeatureCtrSwMode : uint8_t
{FeatureCtrSwMode_S0S1 = 0,FeatureCtrSwMode_S2S3 = 1,FeatureCtrSwMode_S4S5 = 2,FeatureCtrSwMode_S6S7 = 3,FeatureCtrSwMode_S0123,FeatureCtrSwMode_S4567,FeatureCtrSwMode_S01234567,FeatureCtrSwMode_BUTT
};const uint8_t GROUPS[7][8] = {{0, 1},{2, 3},{4, 5},{6, 7},{0, 1, 2, 3},{4, 5, 6, 7},{0, 1, 2, 3, 4, 5, 6, 7},
};String groupNames[] = {"Group0_01","Group1_23","Group2_45","Group3_67","Group4_0123","Group5_4567","Group6_01234567","Group_but"
};const uint8_t GROUP_LEN[7] = {2, 2, 2, 2, 4, 4, 8};
4.1. 实际功能描述
5. 后续计划
买一个米家的无线按钮开关
短按切换所有鱼缸过滤(当前是通过手势控制, 也准备长按设计中的按钮, 实现所有过滤的开闭, 这个很关键)
代码仓库:
https://gitee.com/nwu_zjq/iot-fish.git