Files
TurboSu/docs/DASHBOARD_DEV.md

215 lines
6.7 KiB
Markdown
Raw Permalink 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.
# 仪表盘开发指南
每个仪表盘是 `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",
"supported_games": "all",
"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 | 否 | 版本号 |
| `supported_games` | string | 否 | 支持的游戏: `"all"` 或 ID 逗号分隔 (如 `"forza_horizon,f1_24"`) |
| `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/<id>` 访问。
### 必须包含
1. **WebSocket 连接** — 连接到 `ws://<host>/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
<div data-bind="speed_kmh">0</div>
<span data-bind="rpm">0</span>
```
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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<title>TurboSu - My Dashboard</title>
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#000;color:#fff;
font-family:'SF Pro Text',-apple-system,'PingFang SC',sans-serif}
#root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
#box{position:relative;overflow:hidden}
#box.contain{max-width:100vw;max-height:100vh}
#box.cover{min-width:100vw;min-height:100vh}
#box.fill{width:100vw;height:100vh}
</style>
</head>
<body>
<div id="root"><div id="box" class="contain"><div id="content">
<div data-bind="speed_kmh" style="font-size:80px;font-weight:900;">0</div>
<div style="font-size:24px;color:rgba(255,255,255,.5);">KM/H</div>
</div></div></div>
<script>
const M={aspect_ratio:"auto",render_mode:"contain"};
(function(){
const box=document.getElementById('box');
const proto=location.protocol==='https:'?'wss:':'ws:';
let ws,rt;
function parseRatio(r){
if(!r||r==='auto')return null;
const p=r.split(':');if(p.length===2){const w=+p[0],h=+p[1];if(w>0&&h>0)return w/h}
return null
}
function apply(){
const ratio=parseRatio(M.aspect_ratio),mode=M.render_mode;
if(!ratio){box.style.width='100vw';box.style.height='100vh';box.className='fill';return}
const vw=window.innerWidth,vh=window.innerHeight,vr=vw/vh;let cw,ch;
if(mode==='cover'){if(vr>ratio){cw=vw;ch=vw/ratio}else{ch=vh;cw=vh*ratio};box.className='cover'}
else{box.className='contain';if(vr>ratio){ch=vh;cw=vh*ratio}else{cw=vw;ch=vw/ratio}}
box.style.width=cw+'px';box.style.height=ch+'px'
}
function connect(){
ws=new WebSocket(proto+'//'+location.host+'/ws');
ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}};
ws.onmessage=(e)=>{
try{const m=JSON.parse(e.data);if(m.type==='telemetry')
document.querySelectorAll('[data-bind]').forEach(el=>{
const v=m.data[el.getAttribute('data-bind')];
if(v!==undefined)el.textContent=typeof v==='number'?v.toFixed(1):v
})
}catch(e){}
};
ws.onclose=()=>{rt=setTimeout(connect,2000)}
}
apply();window.addEventListener('resize',apply);connect()
})();
</script>
</body>
</html>
```
## 打包分发
将整个文件夹压缩为 zip,改后缀为 `.tsd`,即可在 TurboSu 仪表盘页面导入或在社区分享。
```bash
zip -r my_dashboard.tsd my_dashboard/
```