369 lines
8.7 KiB
Markdown
369 lines
8.7 KiB
Markdown
# AGV GLB 接入教程
|
||||
|
|
|
|||
|
|
> 面向 MES、数字孪生大屏、调度平台等外部系统的集成指南。
|
|||
|
|
> API 协议细节见 [AGV播放API.md](./AGV播放API.md)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 架构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────┐ HTTP POST/GET ┌──────────────┐ SSE 推送 ┌─────────────────┐
|
|||
|
|
│ 你的业务系统 │ ──────────────────────▶│ serve.py │ ───────────────▶│ play.html │
|
|||
|
|
│ (任意语言) │ /api/catalog │ :8765 │ /api/agv/events│ (3D 播放页) │
|
|||
|
|
│ │ /api/agv │ │ │ │
|
|||
|
|
└─────────────────┘ └──────────────┘ └─────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 组件 | 职责 |
|
|||
|
|
|------|------|
|
|||
|
|
| **业务系统** | 调用 HTTP 接口,无需了解 Three.js |
|
|||
|
|
| **serve.py** | 协议校验、命令缓存与转发 |
|
|||
|
|
| **play.html** | 3D 渲染;须保持打开才会执行动作 |
|
|||
|
|
|
|||
|
|
播放页未打开时,命令会**缓存**;打开 `play.html` 后自动同步执行。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 快速开始
|
|||
|
|
|
|||
|
|
### 1. 启动服务
|
|||
|
|
|
|||
|
|
**Windows(推荐):** 双击项目根目录 `start.bat`
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
# 或命令行
|
|||
|
|
.\start.ps1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**手动启动:**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python serve.py
|
|||
|
|
# 默认端口 8765;可通过环境变量 PORT 修改
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
首次部署且需使用 `analyze_agv.py` 扫描 GLB 时:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
.\start.ps1 -Setup
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 打开播放页
|
|||
|
|
|
|||
|
|
服务启动后,在浏览器中保持以下页面打开:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
http://127.0.0.1:8765/viewer/play.html
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
配置页(编辑动作,非播放必需):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
http://127.0.0.1:8765/viewer/index.html
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 验证连通
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl http://127.0.0.1:8765/api/catalog
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
返回 `ok: true` 且 `data.vehicles` 非空即表示服务正常。
|
|||
|
|
|
|||
|
|
### 4. 发送第一条播放命令
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -X POST http://127.0.0.1:8765/api/agv \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d "{\"version\":1,\"method\":\"playAction\",\"params\":{\"modelId\":\"英招\",\"actionId\":\"zuozhuan\"}}"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应中 `data.subscribers >= 1` 表示播放页在线;为 `0` 时命令已缓存,打开播放页后会自动执行。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 依赖说明
|
|||
|
|
|
|||
|
|
| 用途 | 要求 |
|
|||
|
|
|------|------|
|
|||
|
|
| 运行服务 + 播放 | Python 3.10+(标准库即可) |
|
|||
|
|
| 前端 Three.js | CDN 加载,**无需 npm install** |
|
|||
|
|
| GLB 扫描工具 | 可选,运行 `.\start.ps1 -Setup` 安装 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 接入方式
|
|||
|
|
|
|||
|
|
### 方式 A:HTTP API(推荐)
|
|||
|
|
|
|||
|
|
业务系统直接请求 `serve.py` 提供的 REST 接口,适合 Python、Java、C#、Node.js 等任意语言。
|
|||
|
|
|
|||
|
|
### 方式 B:iframe 嵌入(适合前端大屏)
|
|||
|
|
|
|||
|
|
将 `play.html` 嵌入 iframe,通过 `postMessage` 或 `AgvPlaybackApi` 控制,协议字段与 HTTP 一致。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 标准流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
① 启动 serve.py
|
|||
|
|
↓
|
|||
|
|
② 打开 play.html(可全屏或 iframe 嵌入大屏)
|
|||
|
|
↓
|
|||
|
|
③ GET /api/catalog → 获取 modelId、actionId
|
|||
|
|
↓
|
|||
|
|
④ POST /api/agv → playAction / stopAction / clearActions
|
|||
|
|
↓
|
|||
|
|
⑤ 检查 data.subscribers >= 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## HTTP API
|
|||
|
|
|
|||
|
|
**Base URL:** `http://127.0.0.1:8765`
|
|||
|
|
**协议版本:** `version: 1`
|
|||
|
|
**Content-Type:** `application/json`
|
|||
|
|
|
|||
|
|
### GET /api/catalog — 查询目录
|
|||
|
|
|
|||
|
|
接入前先调用,获取可用的 `modelId` 与 `actionId`。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
GET http://127.0.0.1:8765/api/catalog
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
响应示例:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"version": 1,
|
|||
|
|
"ok": true,
|
|||
|
|
"data": {
|
|||
|
|
"vehicles": [
|
|||
|
|
{
|
|||
|
|
"modelId": "英招",
|
|||
|
|
"name": "英招",
|
|||
|
|
"actions": [
|
|||
|
|
{ "id": "zuozhuan", "label": "左转" }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"error": null
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### POST /api/agv — 控制播放
|
|||
|
|
|
|||
|
|
| method | 说明 | params |
|
|||
|
|
|--------|------|--------|
|
|||
|
|
| `playAction` | 播放动作,自动切换车辆 | `modelId` + `actionId` 必填 |
|
|||
|
|
| `stopAction` | 停止指定动作 | `modelId` + `actionId` 必填 |
|
|||
|
|
| `clearActions` | 停止该车全部动作 | 仅 `modelId` 必填 |
|
|||
|
|
|
|||
|
|
**playAction 示例:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"version": 1,
|
|||
|
|
"method": "playAction",
|
|||
|
|
"params": {
|
|||
|
|
"modelId": "英招",
|
|||
|
|
"actionId": "zuozhuan"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**成功响应:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"version": 1,
|
|||
|
|
"ok": true,
|
|||
|
|
"data": {
|
|||
|
|
"dispatched": true,
|
|||
|
|
"subscribers": 1,
|
|||
|
|
"modelId": "英招",
|
|||
|
|
"activeActions": ["zuozhuan"]
|
|||
|
|
},
|
|||
|
|
"error": null
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**失败响应:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"version": 1,
|
|||
|
|
"ok": false,
|
|||
|
|
"data": null,
|
|||
|
|
"error": {
|
|||
|
|
"code": "INVALID_REQUEST",
|
|||
|
|
"message": "params.modelId 必填"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 响应字段 | 含义 |
|
|||
|
|
|----------|------|
|
|||
|
|
| `subscribers` | 当前连接的播放页数量;`0` = 无播放页,命令已缓存 |
|
|||
|
|
| `activeActions` | 当前正在播放的动作 ID 列表 |
|
|||
|
|
| `dispatched` | 命令是否已成功写入总线 |
|
|||
|
|
|
|||
|
|
更多请求/响应示例见 [AGV播放API.md](./AGV播放API.md)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 代码示例
|
|||
|
|
|
|||
|
|
### Python
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import requests
|
|||
|
|
|
|||
|
|
BASE = "http://127.0.0.1:8765"
|
|||
|
|
|
|||
|
|
def get_catalog():
|
|||
|
|
r = requests.get(f"{BASE}/api/catalog", timeout=5)
|
|||
|
|
r.raise_for_status()
|
|||
|
|
return r.json()["data"]["vehicles"]
|
|||
|
|
|
|||
|
|
def play_action(model_id: str, action_id: str):
|
|||
|
|
payload = {
|
|||
|
|
"version": 1,
|
|||
|
|
"method": "playAction",
|
|||
|
|
"params": {"modelId": model_id, "actionId": action_id},
|
|||
|
|
}
|
|||
|
|
r = requests.post(f"{BASE}/api/agv", json=payload, timeout=5)
|
|||
|
|
r.raise_for_status()
|
|||
|
|
data = r.json()
|
|||
|
|
if not data["ok"]:
|
|||
|
|
raise RuntimeError(data["error"]["message"])
|
|||
|
|
if data["data"]["subscribers"] == 0:
|
|||
|
|
print("警告: 播放页未打开,命令已缓存")
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
vehicles = get_catalog()
|
|||
|
|
vehicle = vehicles[0]
|
|||
|
|
action_id = vehicle["actions"][0]["id"]
|
|||
|
|
play_action(vehicle["modelId"], action_id)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### JavaScript
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const BASE = "http://127.0.0.1:8765";
|
|||
|
|
|
|||
|
|
async function getCatalog() {
|
|||
|
|
const res = await fetch(`${BASE}/api/catalog`);
|
|||
|
|
const json = await res.json();
|
|||
|
|
return json.data.vehicles;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function playAction(modelId, actionId) {
|
|||
|
|
const res = await fetch(`${BASE}/api/agv`, {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
version: 1,
|
|||
|
|
method: "playAction",
|
|||
|
|
params: { modelId, actionId },
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
const json = await res.json();
|
|||
|
|
if (!json.ok) throw new Error(json.error.message);
|
|||
|
|
return json;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### iframe 嵌入
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<iframe
|
|||
|
|
id="agv-player"
|
|||
|
|
src="http://127.0.0.1:8765/viewer/play.html"
|
|||
|
|
style="width:100%;height:100%;border:none"
|
|||
|
|
></iframe>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
const iframe = document.getElementById("agv-player");
|
|||
|
|
|
|||
|
|
// postMessage(协议与 HTTP 一致)
|
|||
|
|
iframe.contentWindow.postMessage(
|
|||
|
|
{
|
|||
|
|
version: 1,
|
|||
|
|
id: "req-1",
|
|||
|
|
method: "playAction",
|
|||
|
|
params: { modelId: "英招", actionId: "zuozhuan" },
|
|||
|
|
},
|
|||
|
|
"*"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
window.addEventListener("message", (e) => {
|
|||
|
|
if (e.data?.id === "req-1") console.log("播放结果:", e.data);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 同源时可直接调用 AgvPlaybackApi
|
|||
|
|
iframe.onload = async () => {
|
|||
|
|
const api = iframe.contentWindow.AgvPlaybackApi;
|
|||
|
|
await api.request({
|
|||
|
|
version: 1,
|
|||
|
|
method: "playAction",
|
|||
|
|
params: { modelId: "英招", actionId: "zuozhuan" },
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 业务映射建议
|
|||
|
|
|
|||
|
|
| 业务事件 | API 调用 |
|
|||
|
|
|----------|----------|
|
|||
|
|
| AGV 开始某动作 | `playAction` + 对应 `actionId` |
|
|||
|
|
| AGV 停止当前动作 | `stopAction` + 对应 `actionId` |
|
|||
|
|
| AGV 复位 / 全部停止 | `clearActions` |
|
|||
|
|
| 切换 AGV 型号 | `playAction` 带新的 `modelId`(自动切车) |
|
|||
|
|
|
|||
|
|
建议启动时拉一次 catalog,将业务编码(如 `"TURN_LEFT"`)映射到 `actionId`,避免硬编码。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 生产部署
|
|||
|
|
|
|||
|
|
1. **播放页常开**:大屏或工控机全屏打开 `play.html`,或用 iframe 嵌入业务页面
|
|||
|
|
2. **单实例**:同一端口只运行一个 `serve.py`
|
|||
|
|
3. **自定义端口**:`PORT=9000 python serve.py` 或 `.\start.ps1 -Port 9000`
|
|||
|
|
4. **健康检查**:定期 `GET /api/catalog` 确认服务存活
|
|||
|
|
5. **新增车辆**:将 GLB + JSON 放入 `agv/`,更新 `agv/manifest.json`,重启服务后 catalog 自动更新
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
| 现象 | 处理 |
|
|||
|
|
|------|------|
|
|||
|
|
| `subscribers: 0` | 先打开 `play.html` |
|
|||
|
|
| 页面打不开 | 只保留一个 `serve.py` 进程 |
|
|||
|
|
| Postman 卡住 | 重启 `serve.py` |
|
|||
|
|
| `params.modelId 必填` | 三个 method 都要带 `modelId` |
|
|||
|
|
| `UNKNOWN_METHOD` | POST 只能用 3 个 method;查目录用 GET |
|
|||
|
|
| `动作不存在` | 先调 catalog 核对 `actionId` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 相关文档
|
|||
|
|
|
|||
|
|
| 文档 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| [AGV播放API.md](./AGV播放API.md) | HTTP 协议与请求/响应格式 |
|
|||
|
|
| [AGV_GLB导出规范.md](./AGV_GLB导出规范.md) | GLB 模型导出与配置规范 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*数字孪生开发组*
|