# 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`。 ## 5.1 企业展示墙控制(/wall,39 家入驻企业滚动墙) **MQTT 控制消息**(ESP32 解码红外键码后发布到 `opc/display/command`,JSON 载荷): | 遥控器按键 | 建议指令 action | params | 效果 | 示例消息 | |---|---|---|---|---| | 1 / 2 / 3 | `wall_speed` | `speed: "slow" \| "normal" \| "fast"` | 慢速 150s / 标准 80s / 快速 40s 一圈 | `{"action":"wall_speed","params":{"speed":"slow"}}` | | CH− | `wall_pause` | `{}` | 暂停滚动 | `{"action":"wall_pause","params":{}}` | | CH+ | `wall_resume` | `{}` | 继续滚动 | `{"action":"wall_resume","params":{}}` | | 电源 | `navigate` | `page: "/wall"` | 切换到企业展示墙 | `{"action":"navigate","params":{"page":"/wall"}}` | **ESP32 固件映射示例**(在 §4 固件的 `MAP[]` 键码映射表中追加): ```cpp // 键码 → MQTT 指令(payload 为 JSON 字符串,经 publishCommand 发布) { 0x02, R"({"action":"wall_pause","params":{}})" }, // VOL− 暂停墙滚动 { 0x03, R"({"action":"wall_resume","params":{}})" }, // VOL+ 继续墙滚动 { 0x00, R"({"action":"wall_speed","params":{"speed":"fast"}})" }, // CH− 快速 { 0x01, R"({"action":"wall_speed","params":{"speed":"slow"}})" }, // CH+ 慢速 { 0x0B, R"({"action":"navigate","params":{"page":"/wall"}})" }, // 电源 切到展示墙 ``` > 前端 `useMqttControl` 收到 `wall_*` 指令后派发 `dpm:wall-control` 事件,展示墙页据此暂停/继续/变速;未在展示墙页时 `wall_*` 指令不产生副作用(安全幂等)。 ## 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` 过滤,指定某台大屏执行