Files
DPM/docs/esp32-ir-control.md
T

9.1 KiB
Raw Blame History

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. 硬件清单与接线

硬件 型号/规格 数量
开发板 ESP32DevKitC / NodeMCU-32S 1
红外接收头 VS1838B / TL183838kHz3 脚) 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 库管理器安装):

  • IRremoteby Armin Joachimsmeyerv4.x
  • PubSubClientby Nick O'Leary
// esp32_ir_mqtt.ino —— 红外遥控 → MQTT 控制大屏
#include <WiFi.h>
#include <PubSubClient.h>
#include <IRremote.h>

// ── 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_inputAI 页提问)、ai_company(切企业)、ai_zone(切分区)、voice_stopnext/prev(媒体)、alert(通知)——参数见 docs/mqtt-commands.md

6. 学习模式(首次必须)

键码采集固件(只需串口打印,不用连 MQTT):

#include <IRremote.h>
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 过滤,指定某台大屏执行