Files
DPM/docs/esp32-ir-control.md
T
Pine 0af159a175 feat: 数据真实化、企业展示墙、全局手势识别与 3D 模型完善
- 数据大屏改用真实数据(xls 统计表 + 宣传册口径);TOKEN/工具调用基于真实锚点测算
- 新增企业展示墙页(/wall,39 家全量档案 + MQTT/admin/ESP32 控制文档)
- 新增全局手势识别(GlobalVision 全局挂载 + dpm:gesture 事件分发)
- 3D 模型墙面逐间修复、去除门/过道顶棚/调试标签、房间资产不穿墙、会议室多小桌
- 侧栏企业分布扩展至 39 家完整信息,分区按官方四大孵化区域
- 语音页多轮对话展示优化、去除页脚与检测横条、背景对齐
2026-08-18 05:48:56 +08:00

235 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 库管理器安装):
- `IRremote`by Armin Joachimsmeyerv4.x
- `PubSubClient`by Nick O'Leary
```cpp
// 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[]` 键码映射表中追加):
```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 <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` 过滤,指定某台大屏执行