11 KiB
11 KiB
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)
// 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_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[] 键码映射表中追加):
// 键码 → 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):
#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();
}
}
步骤:
- 烧录学习固件,打开串口监视器(115200)
- 逐个按遥控器按键,记录每个键的
command十六进制值 - 把键码填入正式固件的
MAP[]映射表 - 烧录正式固件
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过滤,指定某台大屏执行