diff --git a/docs/esp32-ir-control.md b/docs/esp32-ir-control.md new file mode 100644 index 0000000..c50a43d --- /dev/null +++ b/docs/esp32-ir-control.md @@ -0,0 +1,210 @@ +# ESP32 红外遥控控制大屏 —— 开发文档 + +> 通过红外遥控器(NEC 协议)直接控制园区大屏页面切换与操作。 +> 链路:红外遥控 → ESP32(红外接收 + 解码)→ MQTT → 前端调度器 → 页面动作 + +## 1. 系统架构 + +``` +┌──────────┐ 38kHz 红外 ┌─────────┐ WiFi/MQTT ┌──────────────┐ opc/display/command ┌────────┐ +│ 红外遥控器 │ ───────────► │ ESP32 │ ───────────► │ EMQX Broker │ ─────────────────────► │ 大屏前端 │ +│ (NEC协议) │ │ 接收+解码 │ │ 192.168.1.3 │ │ 调度器 │ +└──────────┘ └─────────┘ └──────────────┘ └────────┘ +``` + +- **红外遥控器**:任意 NEC 协议遥控器(电视/机顶盒/学习型均可),按键映射可自定义 +- **ESP32**:接收红外信号 → 解码键码 → 映射成指令 → MQTT 发布 +- **MQTT**:复用现有 `opc/display/command` 频道(见 `docs/mqtt-commands.md`) +- **大屏前端**:`useMqttControl` 调度器接收指令执行(切页/媒体/语音/视觉) + +## 2. 硬件清单与接线 + +| 硬件 | 型号/规格 | 数量 | +|---|---|---| +| 开发板 | ESP32(DevKitC / NodeMCU-32S) | 1 | +| 红外接收头 | VS1838B / TL1838(38kHz,3 脚) | 1 | +| 电阻 | 100Ω(限流,可选) | 1 | +| 红外遥控器 | NEC 协议(电视/机顶盒遥控器) | 1 | + +**VS1838B 接线**(面对接收头平面,引脚从左到右): + +| 引脚 | 接 ESP32 | +|---|---| +| 1 (OUT/信号) | GPIO 15(可改,见代码 `IR_RX_PIN`) | +| 2 (GND) | GND | +| 3 (VCC) | 3.3V(或 5V,接收头宽压 2.7~5.5V) | + +## 3. 红外协议(NEC) + +- NEC 协议:38kHz 载波,地址码 + 命令码 + 反码 +- 本方案**只取命令码(command byte)**即可区分按键,地址码忽略 +- 任意 NEC 遥控器的按键都有固定键码;不同遥控器键码不同 → **首次需"学习模式"采集键码**(见 §6) + +## 4. Arduino 固件 + +依赖库(Arduino IDE 库管理器安装): +- `IRremote`(by Armin Joachimsmeyer,v4.x) +- `PubSubClient`(by Nick O'Leary) + +```cpp +// esp32_ir_mqtt.ino —— 红外遥控 → MQTT 控制大屏 +#include +#include +#include + +// ── WiFi / MQTT 配置 ───────────────────────────────────── +const char* WIFI_SSID = "你的WiFi"; +const char* WIFI_PASS = "你的WiFi密码"; +const char* MQTT_HOST = "192.168.1.3"; // EMQX Broker +const int MQTT_PORT = 1883; +const char* MQTT_USER = "mqtt用户名"; // backend/.env MQTT_USERNAME +const char* MQTT_PASS = "mqtt密码"; // backend/.env MQTT_PASSWORD +const char* MQTT_TOPIC = "opc/display/command"; + +// ── 红外引脚 ───────────────────────────────────────────── +const uint8_t IR_RX_PIN = 15; + +// ── 按键映射表:遥控器键码 → 大屏指令 ───────────────────── +// 键码需先用「学习模式」采集(串口打印 decodedIRData.command),再填入 +struct CmdMap { uint32_t key; const char* payload; }; +const CmdMap MAP[] = { + // 数字键 1~5:直接切页 + { 0x16, R"({"action":"navigate","params":{"page":"/"}})" }, // 1 → 数据大屏 + { 0x19, R"({"action":"navigate","params":{"page":"/twin"}})" }, // 2 → 数字孪生 + { 0x0D, R"({"action":"navigate","params":{"page":"/ai"}})" }, // 3 → AI 助手 + { 0x0C, R"({"action":"navigate","params":{"page":"/voice"}})" }, // 4 → 语音对话 + { 0x18, R"({"action":"navigate","params":{"page":"/screen"}})" }, // 5 → 媒体轮播 + // CH+/CH-(或左右方向):向左/右切页 + { 0x00, R"({"action":"navigate_rel","params":{"delta":-1}})" }, // CH- → 左切 + { 0x01, R"({"action":"navigate_rel","params":{"delta":1}})" }, // CH+ → 右切 + // 音量+/−:媒体播放/暂停(或 next/prev) + { 0x02, R"({"action":"play","params":{}})" }, // VOL+ → 播放 + { 0x03, R"({"action":"pause","params":{}})" }, // VOL- → 暂停 + // 电源键:语音对话开关 + { 0x0B, R"({"action":"voice_start","params":{}})" }, // 电源 → 语音启动 + // OK/静音键:视觉识别开关(toggle 示例:直接开/关,可换 toggle 逻辑) + { 0x0A, R"({"action":"vision_set","params":{"enabled":true}})" }, // 静音 → 开识别 +}; +const int MAP_LEN = sizeof(MAP) / sizeof(MAP[0]); + +// ── 全局 ───────────────────────────────────────────────── +WiFiClient espClient; +PubSubClient mqtt(espClient); +unsigned long lastReconnect = 0; + +void setup() { + Serial.begin(115200); + Serial.println("ESP32 IR → MQTT 启动"); + + IrReceiver.begin(IR_RX_PIN, ENABLE_LED_FEEDBACK); + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASS); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\nWiFi 已连接: " + String(WiFi.localIP())); + + mqtt.setServer(MQTT_HOST, MQTT_PORT); + mqtt.setKeepAlive(30); +} + +void loop() { + if (!mqtt.connected()) reconnect(); + mqtt.loop(); + + // ── 红外解码 ── + if (IrReceiver.decode()) { + uint32_t cmd = IrReceiver.decodedIRData.command; + Serial.printf("[IR] 地址=0x%02X 键码=0x%02X\n", + IrReceiver.decodedIRData.address, cmd); + IrReceiver.resume(); // 立即恢复接收 + + sendCommand(cmd); + } +} + +// ── 键码 → MQTT ── +void sendCommand(uint32_t key) { + for (int i = 0; i < MAP_LEN; i++) { + if (MAP[i].key == key) { + Serial.println("[MQTT] 发送: " + String(MAP[i].payload)); + mqtt.publish(MQTT_TOPIC, MAP[i].payload); + return; + } + } + Serial.println("[IR] 未映射键码(可在学习模式后加入映射表)"); +} + +// ── MQTT 重连 ── +void reconnect() { + if (millis() - lastReconnect < 3000) return; + lastReconnect = millis(); + String id = "esp32-ir-" + String((uint32_t)ESP.getEfuseMac(), HEX); + if (mqtt.connect(id.c_str(), MQTT_USER, MQTT_PASS)) { + Serial.println("MQTT 已连接"); + } else { + Serial.printf("MQTT 连接失败 rc=%d\n", mqtt.state()); + } +} +``` + +## 5. 键位映射建议 + +| 遥控器按键 | 键码(示例) | 指令 | 效果 | +|---|---|---|---| +| 1 ~ 5 | 0x16/0x19/0x0D/0x0C/0x18 | `navigate` | 直达各页面 | +| CH− / CH+ | 0x00 / 0x01 | `navigate_rel` | 向左/右循环切页 | +| VOL+ / VOL− | 0x02 / 0x03 | `play` / `pause` | 媒体播放/暂停 | +| 电源 | 0x0B | `voice_start` | 启动语音对话 | +| 静音 | 0x0A | `vision_set` | 开/关人物识别 | + +> 键码因遥控器而异——上面的 0x16/0x19… 是常见 NEC 遥控器的值,**首次使用务必先跑学习模式实测**。 + +其他可用指令(自定义映射):`ai_input`(AI 页提问)、`ai_company`(切企业)、`ai_zone`(切分区)、`voice_stop`、`next`/`prev`(媒体)、`alert`(通知)——参数见 `docs/mqtt-commands.md`。 + +## 6. 学习模式(首次必须) + +键码采集固件(只需串口打印,不用连 MQTT): + +```cpp +#include +const uint8_t IR_RX_PIN = 15; +void setup() { + Serial.begin(115200); + IrReceiver.begin(IR_RX_PIN, ENABLE_LED_FEEDBACK); + Serial.println("按遥控器按键,串口会打印键码(学习模式)"); +} +void loop() { + if (IrReceiver.decode()) { + Serial.printf("address=0x%02X command=0x%02X\n", + IrReceiver.decodedIRData.address, + IrReceiver.decodedIRData.command); + IrReceiver.resume(); + } +} +``` + +步骤: +1. 烧录学习固件,打开串口监视器(115200) +2. 逐个按遥控器按键,记录每个键的 `command` 十六进制值 +3. 把键码填入正式固件的 `MAP[]` 映射表 +4. 烧录正式固件 + +## 7. 调试与故障排查 + +| 现象 | 排查 | +|---|---| +| 串口无任何 IR 输出 | 接线检查(OUT→GPIO15、GND、VCC);接收头正面对遥控器;换遥控器电池 | +| 有 IR 输出但 MQTT 不发送 | 打印显示"未映射键码"→ 键码表没配对该遥控器;WiFi/MQTT 未连接 | +| MQTT 连接失败 rc=5 | 用户名/密码错误(对照 backend/.env) | +| 前端无反应但 MQTT 收到 | `mosquitto_sub -t opc/display/command` 验证消息到达 Broker;前端需在运行且 MQTT 已连(页面右上角在线状态) | +| 连按无效 | NEC 重复码(0xFFFFFFFF)被忽略属正常;长按只发一次指令 | + +## 8. 进阶扩展(可选) + +- **学习型配置**:ESP32 加按钮 + OLED,现场学习键码并存入 NVS,免改代码 +- **多键组合**:短按/长按区分(记录按下时长)→ 同一键多指令 +- **OTA 升级**:ArduinoOTA 远程更新固件 +- **多屏控制**:指令包带 `client_id` 过滤,指定某台大屏执行