From 5e177f4514d8606bc1874f6d129e3c82a3eff74b Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 25 Jul 2026 15:38:24 +0800 Subject: [PATCH] docs: add dedicated DASHBOARD_DEV.md and PLUGIN_DEV.md, simplify README & CONTRIBUTING --- CONTRIBUTING.md | 69 +------------ README.md | 116 +--------------------- docs/DASHBOARD_DEV.md | 212 ++++++++++++++++++++++++++++++++++++++++ docs/PLUGIN_DEV.md | 222 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 438 insertions(+), 181 deletions(-) create mode 100644 docs/DASHBOARD_DEV.md create mode 100644 docs/PLUGIN_DEV.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 21f7967..ce7eb70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,73 +2,8 @@ ## 贡献方式 -### 提交游戏插件 (`.tsp`) - -创建一个包含 `manifest.json` 和 `parser.py` 的文件夹,压缩为 zip 改后缀: - -``` -my_game/ -├── manifest.json -└── parser.py -``` - -#### manifest.json - -```json -{ - "id": "unique_game_id", - "name": "游戏名称", - "parser_type": "forza", - "description": "简短描述", - "author": "你的名字", - "version": "1.0.0", - "default_port": 20777 -} -``` - -#### parser.py 规范 - -必须实现 `get_parser()` 函数,返回对象包含: - -```python -import time -from server.telemetry.data import TelemetryData - - -def get_parser(): - return MyParser() - - -class MyParser: - def game_id(self) -> str: - return "unique_game_id" - - def parse(self, data: bytes, addr: tuple) -> TelemetryData: - td = TelemetryData(game_id="unique_game_id", timestamp=time.time()) - td.raw = {"raw_hex": data.hex(), "length": len(data)} - # 填充遥测字段... - return td -``` - -安装方式:在 TurboSu 设置页面 → 游戏插件管理 → 导入 `.tsp` - -### 提交仪表盘主题 (`.tsd`) - -创建独立文件夹: - -``` -my_dashboard/ -├── manifest.json # 元信息 (参考下方格式) -└── index.html # 完整独立页面 -``` - -`index.html` 需包含: -- WebSocket 连接:`ws:///ws` -- 接收 `telemetry` 消息类型的数据 -- 用 `data-bind="字段名"` 属性绑定数据 -- 比例约束渲染逻辑 - -安装方式:在 TurboSu 仪表盘页面 → 导入 `.tsd`,或右键卡片导出。 +- **提交游戏插件** (.tsp) — 见 [游戏插件兼容开发指南](docs/PLUGIN_DEV.md) +- **提交仪表盘主题** (.tsd) — 见 [仪表盘开发指南](docs/DASHBOARD_DEV.md) ### 报告 Bug diff --git a/README.md b/README.md index 7287493..363409d 100644 --- a/README.md +++ b/README.md @@ -80,120 +80,8 @@ python app.py ## 开发指南 -### 自定义仪表盘格式 - -每个仪表盘是一个独立文件夹,放在 `dashboards/` 或 `data/dashboards/`: - -``` -dashboards/my_dashboard/ -├── manifest.json # 元信息 -└── index.html # 完整独立页面 (含 WS 连接 + 数据绑定) -``` - -#### manifest.json - -```json -{ - "id": "my_dashboard", - "name": "我的仪表盘", - "category": "custom", - "description": "简短描述", - "author": "你的名字", - "version": "1.0.0", - "config": { - "icon": "🏎️", - "aspect_ratio": "16:9", - "render_mode": "contain" - } -} -``` - -#### index.html - -完全独立的 HTML 页面。需包含: -- WebSocket 连接到 `ws:///ws` 接收遥测数据 -- `data-bind` 属性绑定数据字段(如 `data-bind="speed_kmh"`) -- 比例约束渲染逻辑(参考内置仪表盘) - -打包分发:将整个文件夹压缩为 zip,改后缀为 `.tsd`。 - -### 自定义游戏插件 - -插件文件夹放在 `games/user/`: - -``` -games/user/my_game/ -├── manifest.json # 元信息 -└── parser.py # 解析逻辑 (get_parser() + parse()) -``` - -#### manifest.json - -```json -{ - "id": "my_game", - "name": "我的游戏", - "parser_type": "forza", - "description": "自定义游戏遥测", - "author": "你的名字", - "version": "1.0.0", - "default_port": 20777 -} -``` - -#### parser.py - -```python -import time -from server.telemetry.data import TelemetryData - - -def get_parser(): - return MyGameParser() - - -class MyGameParser: - def game_id(self) -> str: - return "my_game" - - def parse(self, data: bytes, addr: tuple) -> TelemetryData: - td = TelemetryData(game_id="my_game", timestamp=time.time()) - td.raw = {"raw_hex": data.hex(), "length": len(data)} - # 解析你的游戏数据... - # td.speed_kmh = ... - # td.rpm = ... - # td.gear = ... - return td -``` - -打包分发:将文件夹压缩为 zip,改后缀为 `.tsp`,在设置页面导入。 - -### TelemetryData 可用字段 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `speed_kmh` | float | 速度 (km/h) | -| `speed_mph` | float | 速度 (mph) | -| `rpm` | float | 发动机转速 | -| `max_rpm` | float | 最大转速 | -| `gear` | int | 档位 (0=N, -1=R) | -| `throttle` | float | 油门 (0~1) | -| `brake` | float | 刹车 (0~1) | -| `clutch` | float | 离合 (0~1) | -| `handbrake` | float | 手刹 (0~1) | -| `steering` | float | 转向 (-1~1) | -| `lap_time` | float | 当前圈速 (秒) | -| `best_lap` | float | 最佳圈速 (秒) | -| `last_lap` | float | 上圈时间 (秒) | -| `lap_number` | int | 圈数 | -| `fuel` | float | 燃油 | -| `boost` | float | 涡轮增压 | -| `horsepower` | float | 马力 | -| `torque` | float | 扭矩 | -| `position_x/y/z` | float | 坐标 | -| `engine_temp` | float | 发动机温度 | -| `oil_temp` | float | 油温 | -| `raw` | dict | 原始数据 | +- [仪表盘开发指南](docs/DASHBOARD_DEV.md) — 创建自定义仪表盘主题 +- [游戏插件兼容开发指南](docs/PLUGIN_DEV.md) — 为任意赛车游戏编写遥测解析器 ## 项目结构 diff --git a/docs/DASHBOARD_DEV.md b/docs/DASHBOARD_DEV.md new file mode 100644 index 0000000..7e936ab --- /dev/null +++ b/docs/DASHBOARD_DEV.md @@ -0,0 +1,212 @@ +# 仪表盘开发指南 + +每个仪表盘是 `dashboards/` (内置) 或 `data/dashboards/` (用户) 下的独立文件夹,启动时自动扫描。 + +## 文件夹结构 + +``` +dashboards/my_dashboard/ +├── manifest.json # 元信息 +├── index.html # 完整独立页面 +└── preview.png # 预览图 (可选) +``` + +## manifest.json 格式 + +```json +{ + "id": "my_dashboard", + "name": "我的仪表盘", + "category": "custom", + "description": "简短描述", + "author": "你的名字", + "version": "1.0.0", + "config": { + "icon": "🏎️", + "aspect_ratio": "16:9", + "render_mode": "contain" + } +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | 唯一标识,= 文件夹名 | +| `name` | string | 是 | 显示名称 | +| `category` | string | 否 | 分类 (speed/rpm/basic/timing/custom) | +| `description` | string | 否 | 描述文字 | +| `author` | string | 否 | 作者 | +| `version` | string | 否 | 版本号 | +| `config.icon` | string | 否 | 预览图标 emoji | +| `config.aspect_ratio` | string | 否 | 渲染比例: `auto` / `16:9` / `4:3` / `1:1` / `21:9` | +| `config.render_mode` | string | 否 | 渲染模式: `contain` / `cover` / `fill` / `center` | + +### 渲染模式说明 + +| 模式 | 行为 | +|------|------| +| `contain` | 保持比例完整显示,不足处留黑边 (推荐) | +| `cover` | 保持比例铺满屏幕,超出部分裁切 | +| `fill` | 拉伸填满视口,忽略比例 | +| `center` | 居中显示,不超过 `max_width` × `max_height` | + +## index.html 规范 + +`index.html` 是完整的独立 HTML 页面,直接通过 `/dashboard/` 访问。 + +### 必须包含 + +1. **WebSocket 连接** — 连接到 `ws:///ws` 接收遥测数据 + +```javascript +const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; +const wsUrl = `${proto}//${location.host}/ws`; +const ws = new WebSocket(wsUrl); + +ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + if (msg.type === 'telemetry') { + // msg.data 包含所有遥测字段 + updateUI(msg.data); + } +}; + +ws.onclose = () => { + // 断线重连 + setTimeout(connect, 2000); +}; +``` + +2. **数据绑定** — 使用 `data-bind` 属性自动绑定 + +```html +
0
+0 +``` + +3. **比例约束渲染** — 处理不同设备屏幕比例 + +```javascript +const CONFIG = { aspect_ratio: "16:9", render_mode: "contain" }; + +function applyAspectRatio() { + const ratio = parseRatio(CONFIG.aspect_ratio); + const vw = window.innerWidth, vh = window.innerHeight; + + if (CONFIG.render_mode === 'contain') { + // 缩放以完整显示,不足处留黑边 + const scale = Math.min(vw / refW, vh / refH); + canvas.style.width = (refW * scale) + 'px'; + canvas.style.height = (refH * scale) + 'px'; + } + // ... 其他模式 +} +``` + +## 可绑定的遥测字段 + +| data-bind | 字段 | 类型 | 说明 | +|-----------|------|------|------| +| `speed_kmh` | speed_kmh | float | 速度 (km/h) | +| `speed_mph` | speed_mph | float | 速度 (mph) | +| `rpm` | rpm | float | 发动机转速 | +| `max_rpm` | max_rpm | float | 最大转速 | +| `gear` | gear | int | 档位 (0=N, -1=R, 1~8) | +| `throttle` | throttle | float | 油门 (0~1) | +| `brake` | brake | float | 刹车 (0~1) | +| `clutch` | clutch | float | 离合 (0~1) | +| `steering` | steering | float | 转向 (-1~1) | +| `lap_time` | lap_time | float | 当前圈速 (秒) | +| `best_lap` | best_lap | float | 最佳圈速 (秒) | +| `last_lap` | last_lap | float | 上圈时间 (秒) | +| `lap_number` | lap_number | int | 圈数 | +| `fuel` | fuel | float | 燃油量 | +| `boost` | boost | float | 涡轮增压 | +| `horsepower` | horsepower | float | 马力 | +| `torque` | torque | float | 扭矩 | +| `engine_temp` | engine_temp | float | 发动机温度 | +| `oil_temp` | oil_temp | float | 油温 | + +特殊自定义属性: + +| 属性 | 说明 | +|------|------| +| `data-bind-rpm` | CSS 变量 `--rpm` / `--rpm-pct` / `--rpm-max` | +| `data-bind-speed` | CSS 变量 `--speed` | +| `data-bind-gear` | CSS 变量 `--gear` | + +## 快速模板 + +```html + + + + + +TurboSu - My Dashboard + + + +
+
0
+
KM/H
+
+ + + +``` + +## 打包分发 + +将整个文件夹压缩为 zip,改后缀为 `.tsd`,即可在 TurboSu 仪表盘页面导入或在社区分享。 + +```bash +zip -r my_dashboard.tsd my_dashboard/ +``` diff --git a/docs/PLUGIN_DEV.md b/docs/PLUGIN_DEV.md new file mode 100644 index 0000000..7248336 --- /dev/null +++ b/docs/PLUGIN_DEV.md @@ -0,0 +1,222 @@ +# 游戏插件兼容开发指南 + +TurboSu 通过插件系统支持任意赛车游戏的遥测数据接入。每个插件是 `games/builtin/` 或 `games/user/` 下的独立文件夹。 + +## 文件夹结构 + +``` +games/user/my_game/ +├── manifest.json # 元信息 +└── parser.py # 解析逻辑 +``` + +## manifest.json 格式 + +```json +{ + "id": "my_game", + "name": "我的游戏", + "parser_type": "forza", + "description": "自定义游戏遥测", + "author": "你的名字", + "version": "1.0.0", + "default_port": 20777 +} +``` + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | 唯一标识,= 文件夹名 | +| `name` | string | 是 | 显示名称 | +| `parser_type` | string | 是 | 解析器类型标签 | +| `description` | string | 否 | 描述文字 | +| `author` | string | 否 | 作者 | +| `version` | string | 否 | 版本号 | +| `default_port` | int | 否 | 默认 UDP 端口 (默认 20777) | + +## parser.py 规范 + +必须导出 `get_parser()` 函数,返回的对象必须实现以下接口: + +```python +class MyParser: + def game_id(self) -> str: + """返回与 manifest.json 一致的 game id""" + return "my_game" + + def parse(self, data: bytes, addr: tuple) -> TelemetryData: + """ + 解析 UDP 数据包 + data: 原始字节数据 + addr: (host, port) 发送方地址 + 返回: server.telemetry.data.TelemetryData + """ +``` + +### 完整示例 + +```python +import struct +import time +from server.telemetry.data import TelemetryData + + +def get_parser(): + return MyGameParser() + + +class MyGameParser: + def game_id(self) -> str: + return "my_game" + + def parse(self, data: bytes, addr: tuple) -> TelemetryData: + td = TelemetryData( + game_id="my_game", + timestamp=time.time(), + ) + td.raw = { + "raw_hex": data.hex(), + "length": len(data), + "addr": f"{addr[0]}:{addr[1]}", + } + + # 解析你的游戏数据协议 + # 示例: 简单的二进制格式 + try: + td.speed_kmh = struct.unpack_from("