213 lines
6.5 KiB
Markdown
213 lines
6.5 KiB
Markdown
# 仪表盘开发指南
|
||
|
||
每个仪表盘是 `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/<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/
|
||
```
|