From cdd0ba96eeb854778de75243bec1b146c189e59c Mon Sep 17 00:00:00 2001 From: PineHomePC Date: Sun, 23 Aug 2026 22:35:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=83=A8=E7=BD=B2=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E4=B8=8E=E8=BF=90=E7=BB=B4=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 47 +++++++ .gitignore | 45 +++++++ AGENTS.md | 6 + README.md | 199 ++++++++++++++++++++++++++++ docker-compose.yml | 100 ++++++++++++++ docs/esp32-ir-control.md | 234 +++++++++++++++++++++++++++++++++ docs/mqtt-commands.md | 175 ++++++++++++++++++++++++ docs/voice-integration-plan.md | 168 +++++++++++++++++++++++ docs/win-deploy.md | 78 +++++++++++ scripts/build-win.cmd | 40 ++++++ scripts/parse_layout.py | 119 +++++++++++++++++ 11 files changed, 1211 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 docs/esp32-ir-control.md create mode 100644 docs/mqtt-commands.md create mode 100644 docs/voice-integration-plan.md create mode 100644 docs/win-deploy.md create mode 100644 scripts/build-win.cmd create mode 100644 scripts/parse_layout.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7a2672a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# ============================================================ +# Docker 构建上下文忽略(构建根目录 = 项目根目录) +# 说明:仅 backend/ 会 COPY 进镜像;media 内的媒体文件必须保留, +# 因此图片/视频后缀不做全局排除,只锚定根目录的开发素材。 +# ============================================================ + +# ---- 版本控制 / 元数据 ---- +.git +.gitignore +.DS_Store +**/.DS_Store + +# ---- 前端 / 桌面端(不属于后端镜像) ---- +src +src-tauri +dist +public +node_modules +package.json +yarn.lock +package-lock.json +vite.config.js + +# ---- 根目录开发素材 / 标准资料(不进入镜像) ---- +/IMG_7801.JPG +/空间布局.png +/详细的布局.png +/数据资料 +*.zip +*.wps +*.xlsx +*.xls +*.pdf +*.ai +/昆明市创业园宣传册.pdf + +# ---- 后端内部:排除开发产物与密钥 ---- +backend/.venv +backend/.env +backend/**/__pycache__ +backend/**/*.pyc +backend/.pytest_cache +backend/.mypy_cache +backend/.ruff_cache +backend/.ultralytics +backend/kb_index.json +backend/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3af45e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr + +# 后端密钥 / 前端本地配置(含阿里云密钥、MQTT 凭据,不入库) +backend/.env +.env.local +*.local +*.env.* + +# 后端运行时数据(媒体上传、持久化配置) +backend/media/ +backend/data.json + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# 本地数据资料(不入库) +数据资料/ + +# Python +.venv/ +__pycache__/ +*.pyc + +# 本地语音模型缓存(silero VAD / nltk 数据;部署时随 backend/ 目录整体拷贝,不入库) +backend/.torch-cache/ +backend/nltk_data/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7240b70 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,6 @@ +# 工作规则(永远遵守) + +## 进程管理铁律 +- **绝对禁止 AI 启动、重启、停止任何前后端服务进程**(后端 10085 / s2s 8765 / vite 1420 / Tauri 等)。 +- 需要启动或重启时,**必须先通知用户**,由用户亲自操作。 +- AI 只做:改代码、编译检查、测试接口(接口测试仅在服务已在运行且不重启的前提下)、给出启动命令与验证清单。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..29016bd --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# 大屏媒体轮播系统 (DPM) + +**Digital Playback Media** — 基于 Tauri + React 的全屏媒体轮播控制系统,专为昆明市大学生创业园展播大屏设计。 + +## 概述 + +DPM 是一套完整的数字标牌(Digital Signage)解决方案,包含两大界面: + +- **大屏展示页** (`/screen`) — 全屏自动轮播,支持视频与图片,具备沉浸式空间视觉背景 +- **管理后台** (`/admin`) — 登录保护的管理面板,用于媒体上传、播放列表编排、播放控制与系统设置 + +系统通过 **SSE(Server-Sent Events)** 实现管理端与展示端的实时状态同步,支持局域网内远程管理。 + +## 技术栈 + +| 层级 | 技术 | +|------|------| +| 前端框架 | React 19 + React Router 7 | +| 构建工具 | Vite 7 | +| 桌面框架 | Tauri 2 (Rust) | +| HTTP 服务 | Axum 0.8 (嵌入式 HTTP 服务器) | +| 数据持久化 | JSON 文件存储 | +| 实时通信 | Server-Sent Events (SSE) | +| 内网穿透 | nps 客户端嵌入式集成 | + +## 功能特性 + +### 媒体管理 +- **本地上传** — 支持 .mp4、.mkv、.avi、.jpg、.jpeg、.png 格式 +- **远程 URL** — 添加网络媒体资源链接 +- **媒体预览** — 图片与视频的模态框预览 +- **文件删除** — 从媒体库移除文件 + +### 播放控制 +- **播放/暂停** — 实时控制展示端的播放状态 +- **上一曲/下一曲** — 切换播放内容 +- **顺序/随机播放** — 两种播放模式 +- **图片时长可调** — 1~300 秒 +- **音量控制** — 实时同步调节 + +### 展示端体验 +- **沉浸式空间背景** — 深邃星空背景 + 透视网格 + 动态环境光晕 +- **自动播放** — 启动后自动加载播放列表 +- **视频/图片自适应** — 全屏覆盖显示 +- **声音管理** — 浏览器自动播放策略处理 +- **点击暂停/恢复** — 现场快速控制 + +### 系统功能 +- **全屏/窗口模式切换** — 自由切换展示形态 +- **最小化窗口** — 后台运行 +- **开机自启动** — 系统级自启(Tauri autostart 插件) +- **关闭 → 最小化** — 防止误关闭 +- **内网穿透** — 内置 nps 客户端,支持外网远程访问 + +### 管理后台 +- **登录保护** — 账号密码验证 +- **实时状态显示** — 当前播放内容、状态 +- **上传进度条** — 文件上传实时反馈 +- **响应式设计** — 桌面/移动端自适应 +- **Toast 通知** — 操作即时反馈 + +## 快速开始 + +### 环境要求 + +- **Rust** (edition 2021) +- **Node.js** >= 18 +- **Yarn** 或 npm +- **Tauri CLI** (`cargo install tauri-cli`) + +### 安装 + +```bash +# 安装前端依赖 +yarn install + +# 开发模式运行(同时启动 Vite 开发服务器和 Tauri 应用) +yarn tauri dev + +# 仅启动 Web 开发服务器(浏览器中预览) +yarn dev +``` + +### 构建生产版本 + +```bash +yarn tauri build +``` + +构建产物位于 `src-tauri/target/release/` 目录。 + +## 项目结构 + +``` +dpm/ +├── index.html # HTML 入口 +├── package.json # 前端依赖配置 +├── vite.config.js # Vite 构建配置(API 代理到 :10801) +├── public/ +│ └── npc.zip # 内网穿透客户端(嵌入二进制) +├── src/ # 前端源代码 +│ ├── main.jsx # React 入口 +│ ├── App.jsx # 路由配置(/admin, /screen) +│ ├── pages/ +│ │ ├── Admin.jsx # 管理后台页面 +│ │ └── Screen.jsx # 大屏展示页面 +│ ├── utils/ +│ │ └── api.js # API 封装(REST + SSE) +│ └── styles/ +│ ├── tokens.css # CSS 基础重置 +│ ├── admin.css # 管理后台样式 +│ └── screen.css # 大屏展示样式 +└── src-tauri/ # Rust 后端源代码 + ├── Cargo.toml # Rust 依赖配置 + ├── tauri.conf.json # Tauri 应用配置 + ├── capabilities/ + │ └── default.json # Tauri 权限配置 + └── src/ + ├── main.rs # 程序入口 + ├── lib.rs # 模块导出 + 启动逻辑 + ├── server.rs # Axum HTTP 服务器(路由 + 处理器) + ├── models.rs # 数据模型(Settings, PlaybackState, etc.) + ├── storage.rs # JSON 文件持久化存储 + └── events.rs # SSE 事件广播管理器 +``` + +## API 参考 + +系统内置 HTTP 服务器监听 `0.0.0.0:10801`,提供以下 REST API: + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/login` | 管理员登录 | +| GET | `/api/settings` | 获取系统设置 | +| POST | `/api/settings` | 更新系统设置 | +| GET | `/api/playlist` | 获取播放列表(含音量/模式/时长) | +| POST | `/api/playlist/add` | 添加到播放列表 | +| POST | `/api/playlist/remove` | 从播放列表移除 | +| GET | `/media` | 获取所有媒体文件列表 | +| POST | `/upload` | 上传媒体文件(multipart) | +| POST | `/api/media/add-url` | 添加 URL 媒体 | +| POST | `/api/delete` | 删除媒体文件 | +| POST | `/api/control` | 播放控制(play/pause/next/prev) | +| GET | `/api/state` | 获取当前播放状态 | +| POST | `/api/state` | 更新播放状态 | +| GET | `/api/events` | SSE 实时事件流 | +| POST | `/api/display-command` | 显示命令(minimize 等) | +| GET | `/file/*` | 静态媒体文件服务 | + +### 默认登录凭据 + +- **账号**: `admin` +- **密码**: `**********` + +> 可通过编辑系统数据目录下的 `dpm/data.json` 修改。 + +### 数据存储位置 + +系统数据存储在操作系统标准数据目录下的 `dpm/` 文件夹中: + +- **macOS**: `~/Library/Application Support/dpm/data.json` +- **Linux**: `~/.local/share/dpm/data.json` +- **Windows**: `C:\Users\<用户>\AppData\Roaming\dpm/data.json` + +媒体文件存储在用户 `~/Downloads/Media/` 目录。 + +## 内网穿透 + +系统内置 [nps](https://github.com/ehang-io/nps) 客户端,自动从嵌入的 `public/npc.zip` 解压并启动,实现外网远程访问管理后台。 + +- 启动参数在 `server.rs` 中配置 +- *仅支持 Windows* +- 自动设置 Unix 可执行权限 +- 解压到系统数据目录 `dpm/npc/` + +## 开发说明 + +### 端口 + +| 端口 | 用途 | +|------|------| +| 10801 | 后端 HTTP API 服务器 | +| 1420 | Vite 前端开发服务器 | + +### Vite 开发代理 + +开发模式下,Vite 自动将 `/api`、`/file`、`/media`、`/upload` 路径代理到 `localhost:10801`,前端无需关心跨域问题。 + +### 构建 + +前端构建产物(`dist/`)通过 `RustEmbed` 编译到 Rust 二进制中,发布时只需分发单个可执行文件。 + +## 许可 + +本项目为昆明市大学生创业园公益定制开发。 + +--- + +*云南派音人工智能科技提供技术支持* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d117554 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +# ============================================================ +# OPC 智能园区后端 · Docker Compose 一键部署 +# +# 启动: docker compose up -d --build +# 停止: docker compose down +# 查看: docker compose logs -f backend +# 重建: docker compose up -d --build backend +# +# 部署前配置(可选,均可用环境变量覆盖): +# export MQTT_PUBLIC_HOST=192.168.1.50 # 展播端能访问到的 Broker 地址(服务器局域网 IP) +# export EMQX_USER=dpmserver EMQX_PASS=xxxxx +# export DASHSCOPE_API_KEY=sk-xxxx # AI/语音(未配置时 AI 走本地规则引擎) +# export DPM_S2S_ENABLED=0 # 不需要实时语音对话可关闭 +# +# 端口: +# · 10085 HTTP(API + 管理后台 + 媒体) +# · 8765 s2s 实时语音 WebSocket(DPM_S2S_ENABLED=1 时) +# · 1883/8083/18083 EMQX(MQTT / MQTT-WS / 控制台) +# ============================================================ + +services: + # ---------- MQTT Broker(EMQX 5.x) ---------- + emqx: + image: emqx/emqx:5.8 + container_name: dpm-emqx + restart: unless-stopped + ports: + - "1883:1883" # MQTT + - "8083:8083" # MQTT over WebSocket(展播端 /api/config 下发) + - "18083:18083" # EMQX Dashboard(可选,账号 admin / public) + environment: + EMQX_NAME: dpm-emqx + EMQX_HOST: node.emqx.dpm.local + EMQX_NODE__COOKIE: dpm-emqx-cookie + EMQX_DASHBOARD__DEFAULT_PASSWORD: "${EMQX_DASHBOARD_PASS:-public}" + # 鉴权:禁止匿名,单账号 dpmserver(与 backend/.env 一致) + EMQX_ALLOW_ANONYMOUS: "false" + EMQX_AUTH__USER__1__LOGIN: "${EMQX_USER:-dpmserver}" + EMQX_AUTH__USER__1__PASSWORD: "${EMQX_PASS:-123456}" + volumes: + - emqx_data:/opt/emqx/data + - emqx_log:/opt/emqx/log + healthcheck: + test: ["CMD", "emqx", "ctl", "status"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 15s + networks: + - dpm_net + + # ---------- 园区后端(FastAPI :10085 + s2s :8765) ---------- + backend: + build: + context: . + dockerfile: backend/Dockerfile + image: dpm-backend:latest + container_name: dpm-backend + restart: unless-stopped + depends_on: + emqx: + condition: service_healthy + ports: + - "10085:10085" # API / 管理后台 / 媒体 + - "8765:8765" # s2s 实时语音(DPM_S2S_ENABLED=1 时) + environment: + DPM_PORT: "10085" + DPM_MQTT_ENABLED: "1" + MQTT_BROKER_HOST: emqx + MQTT_BROKER_PORT: "1883" + MQTT_USERNAME: "${EMQX_USER:-dpmserver}" + MQTT_PASSWORD: "${EMQX_PASS:-123456}" + # 展播端(Tauri 大屏)经后端 /api/config 获取的 WebSocket 地址: + # 必须填"展播机能访问到的服务器地址",默认 192.168.1.9(同机部署) + DPM_MQTT_WS: "ws://${MQTT_PUBLIC_HOST:-192.168.1.9}:8083/mqtt" + DPM_S2S_ENABLED: "${DPM_S2S_ENABLED:-1}" + DASHSCOPE_API_KEY: "${DASHSCOPE_API_KEY:-}" + volumes: + - dpm_media:/app/backend/media # 媒体资源(上传持久化) + - dpm_knowledge:/app/backend/knowledge # 知识库 park.md(可热更新,删除 kb_index.json 触发重建) + - dpm_data:/app/backend/data.json # 播放列表 / 设置 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://192.168.1.9:10085/api/health', timeout=4).status==200 else 1)"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - dpm_net + +networks: + dpm_net: + driver: bridge + +volumes: + emqx_data: + emqx_log: + dpm_media: + dpm_knowledge: + dpm_data: diff --git a/docs/esp32-ir-control.md b/docs/esp32-ir-control.md new file mode 100644 index 0000000..9e445e4 --- /dev/null +++ b/docs/esp32-ir-control.md @@ -0,0 +1,234 @@ +# 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` 过滤,指定某台大屏执行 diff --git a/docs/mqtt-commands.md b/docs/mqtt-commands.md new file mode 100644 index 0000000..5a57f1e --- /dev/null +++ b/docs/mqtt-commands.md @@ -0,0 +1,175 @@ +# MQTT 标准操作手册(OPC 智能园区大屏系统) + +> 通过 MQTT 对前端大屏进行集中控制:切页、页面操作、媒体控制、视觉开关、状态上报。 + +## 1. Broker 连接 + +| 项 | 值 | 说明 | +|---|---|---| +| 地址 | `192.168.1.3` | 局域网 EMQX Broker | +| TCP 端口 | `1883` | MQTT 标准端口(后端/命令行用) | +| WebSocket 端口 | `8083` | 前端浏览器用(`ws://192.168.1.3:8083/mqtt`) | +| 用户名/密码 | `backend/.env` 的 `MQTT_USERNAME` / `MQTT_PASSWORD` | 连接鉴权 | + +## 2. 频道(Topic) + +| Topic | 方向 | 载荷 | 用途 | +|---|---|---|---| +| `opc/display/command` | 服务端 → 前端 | 指令信封 | **控制指令**(切页/页面操作/媒体/视觉) | +| `opc/dashboard/tick` | 服务端 → 前端 | 园区数据快照 | 数据大屏实时刷新(2.2s) | +| `opc/display/heartbeat` | 前端 → 服务端 | `{client_id, page, ts}` | 大屏在线心跳(10s) | +| `opc/frontend/state` | 前端 → 服务端 | `{client_id, page, ts}` | **页面状态即时上报**(路由变化即发) | +| `opc/display/ack` | 前端 → 服务端 | 指令回执 | 指令确认(可选) | + +## 3. 统一指令信封 + +所有控制指令发到 `opc/display/command`,JSON 格式: + +```json +{ + "cmd_id": "uuid", // 指令 ID(可选) + "ts": 1755490000000, // 时间戳 ms(可选) + "action": "navigate", // 命令名(必填) + "params": { "page": "/ai" } // 参数(按命令) +} +``` + +后端 `hub.publish_command(action, params)` 自动生成信封;命令行手动发送时只需 `action` + `params`。 + +## 4. 命令全集 + +### 4.1 页面切换 + +| action | params | 效果 | +|---|---|---| +| `navigate` | `page: "/" \| "/twin" \| "/ai" \| "/voice" \| "/wall" \| "/screen"`(或别名 home/twin/ai/voice/wall/screen/数据大屏/企业展示墙…) | 切换到指定页面 | +| `navigate_rel` | `delta: 1 \| -1` | 向右 / 向左循环切换页面 | + +### 4.2 全局视觉识别 + +| action | params | 效果 | +|---|---|---| +| `vision_set` | `enabled: true \| false` | 开启 / 关闭全局人物识别(YOLO 人脸/姿态/手势) | + +### 4.3 AI 助手页(/ai) + +| action | params | 效果 | +|---|---|---| +| `ai_input` | `text: "介绍一下入驻政策"` | 输入问题并自动发送 | +| `ai_preset` | `index: 0 \| 1 \| 2` | 选择预设问题并发送 | +| `ai_company` | `action: "next" \| "prev"` | 切换展示企业 | +| `ai_zone` | `zone: "加速区" \| "国际区" \| "成长区"` | 切换企业分区 | + +### 4.4 语音对话页(/voice) + +| action | params | 效果 | +|---|---|---| +| `voice_start` | — | 启动语音对话 | +| `voice_stop` | — | 关闭语音对话 | +| `voice_refresh` | — | 刷新页面 | + +### 4.5 媒体轮播页(/screen) + +| action | params | 效果 | +|---|---|---| +| `play` / `pause` | — | 播放 / 暂停 | +| `next` / `prev` | — | 下一项 / 上一项 | +| `set_mode` | `mode: sequential \| shuffle \| loop` | 播放模式 | +| `play_target` | `path: "媒体路径"` | 播放指定媒体 | +| `settings_changed` | 设置对象 | 应用设置变更 | +| `playlist_changed` | — | 重新加载播放列表 | + +### 4.6 企业展示墙页(/wall) + +> 39 家入驻企业信息缓慢滚动展示墙(数据来源:《入驻企业信息表》) + +| action | params | 效果 | +|---|---|---| +| `wall_pause` | — | 暂停滚动 | +| `wall_resume` | — | 继续滚动 | +| `wall_speed` | `speed: slow \| normal \| fast` | 滚动速度:慢速 150s / 标准 80s / 快速 40s 一圈 | + +### 4.7 全局 + +| action | params | 效果 | +|---|---|---| +| `alert` | `text: "通知内容"` | 全局通知弹窗 | +| `show_card` | 卡片数据 | 展示信息卡片 | +| `minimize` | — | 最小化窗口(Tauri) | + +## 5. 命令行发送 + +### mosquitto_pub(推荐,需安装 `mosquitto-clients`) + +```bash +# 切页到 AI 助手 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command \ + -m '{"action":"navigate","params":{"page":"/ai"}}' + +# 向左切页 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"navigate_rel","params":{"delta":-1}}' + +# 关闭人物识别 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"vision_set","params":{"enabled":false}}' + +# AI 页提问 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"ai_input","params":{"text":"介绍一下入驻政策"}}' + +# 切换企业分区 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"ai_zone","params":{"zone":"国际区"}}' + +# 语音启动 / 媒体下一项 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"voice_start","params":{}}' +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"next","params":{}}' + +# 切到企业展示墙 / 暂停滚动 +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"navigate","params":{"page":"/wall"}}' +mosquitto_pub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> \ + -t opc/display/command -m '{"action":"wall_pause","params":{}}' +``` + +### Python(paho-mqtt,后端 venv 自带) + +```bash +cd /Volumes/Pine/mycode/DPM/dpm/backend +.venv/bin/python - <<'EOF' +import paho.mqtt.client as mqtt + +def send(action, params=None): + c = mqtt.Client() + c.username_pw_set("用户名", "密码") + c.connect("192.168.1.3", 1883, 30) + c.publish("opc/display/command", str({"action": action, "params": params or {}})) + c.disconnect() + +send("navigate", {"page": "/voice"}) +send("ai_company", {"action": "next"}) +send("voice_stop") +EOF +``` + +### MQTTX(图形界面) + +1. 新建连接:`mqtt://192.168.1.3:1883`,填用户名/密码 +2. 发布到 `opc/display/command`,消息体如上 JSON + +## 6. 订阅调试 + +```bash +# 监听前端页面上报(验证"当前页面") +mosquitto_sub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> -t 'opc/frontend/state' + +# 监听心跳 +mosquitto_sub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> -t 'opc/display/heartbeat' + +# 监听全部指令 +mosquitto_sub -h 192.168.1.3 -p 1883 -u <用户名> -P <密码> -t 'opc/display/command' -v +``` diff --git a/docs/voice-integration-plan.md b/docs/voice-integration-plan.md new file mode 100644 index 0000000..7527e4c --- /dev/null +++ b/docs/voice-integration-plan.md @@ -0,0 +1,168 @@ +# DPM 实时语音对话整合方案(复用 live-avatar 云化栈) + +> 目标:砍掉数字人/口型/wav2lip 等全部视觉部分,只保留「语音识别 + 对话 + 实时交互」链路; +> 前端在 DPM React 应用中新增对话页(复用 live-avatar demo 前端的样式与逻辑); +> 后端并入 DPM `backend/pyproject.toml`。 +> 基线:live-avatar 云化栈已在 `/Volumes/Pine/mycode/opc/live` 跑通(s2s v0.2.11 + cloud-migration 补丁 + demo 前端)。 + +--- + +## 0. 现状对比 + +| | live-avatar(已跑通) | DPM(现状) | +|---|---|---| +| 前端 | 原生 JS demo(index.html / main.js / s2s-ws-client.js / 两个 worklet) | React 19 + Vite + Tailwind;`/ai` 页已有 `AiChatPanel`(文字 + **录音上传式**语音) | +| 后端 | s2s 独立进程 :8765(VAD 本地 + 云 ASR/LLM/TTS) | FastAPI :8000;已有 `app/asr.py`(dashscope paraformer)、`app/llm.py`(qwen + 工具)、`SSL_CERT_FILE=certifi` 同款处理 | +| 语音体验 | **实时全双工**:本地 VAD → 流式云 ASR → 流式 LLM → 流式 TTS → 音频回放 | 录音 → 上传 → 转写 → 文字回复(无流式、无 TTS 回音) | + +**DPM 缺的正是 live-avatar 栈的实时交互能力**;两边的云凭据/CA 处理已一致(dashscope + certifi),合并成本低。 + +--- + +## 1. 两个关键决策点 + +### 1.1 后端:内嵌进 DPM FastAPI(推荐) vs 独立 s2s 进程 + +- **推荐:内嵌**。s2s v0.2.11 的 `api/openai_realtime/websocket_router.create_app(pool, stop_event)` 本身就返回一个 FastAPI 应用, + 可在 DPM `app.main` 里构建管线池后 `app.mount("/v1", s2s_app)`,与现有 `/api/*` 共存、单进程单端口。 + Tauri 桌面打包只需一个后端进程,无子进程管理。 +- 备选:s2s 独立跑 :8765,DPM 负责拉起/守护。改动最小,但两进程两端口,打包与部署更重。 + +### 1.2 前端:iframe 内嵌(推荐 v1) vs React 组件移植(v2) + +- **推荐 v1:iframe 内嵌**。把 live-avatar `demo/` 目录拷入 DPM `public/voice-demo/`(去掉数字人 iframe/视觉元素), + React 新增 `VoiceAssistant.jsx`(路由 `/voice`)里 `