0af159a175
- 数据大屏改用真实数据(xls 统计表 + 宣传册口径);TOKEN/工具调用基于真实锚点测算 - 新增企业展示墙页(/wall,39 家全量档案 + MQTT/admin/ESP32 控制文档) - 新增全局手势识别(GlobalVision 全局挂载 + dpm:gesture 事件分发) - 3D 模型墙面逐间修复、去除门/过道顶棚/调试标签、房间资产不穿墙、会议室多小桌 - 侧栏企业分布扩展至 39 家完整信息,分区按官方四大孵化区域 - 语音页多轮对话展示优化、去除页脚与检测横条、背景对齐
235 lines
11 KiB
Markdown
235 lines
11 KiB
Markdown
# 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 <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` 过滤,指定某台大屏执行
|