Files
TurboSu/docs/DASHBOARD_DEV.md
T

6.5 KiB
Raw Blame History

仪表盘开发指南

每个仪表盘是 dashboards/ (内置) 或 data/dashboards/ (用户) 下的独立文件夹,启动时自动扫描。

文件夹结构

dashboards/my_dashboard/
├── manifest.json     # 元信息
├── index.html        # 完整独立页面
└── preview.png       # 预览图 (可选)

manifest.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 接收遥测数据
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);
};
  1. 数据绑定 — 使用 data-bind 属性自动绑定
<div data-bind="speed_kmh">0</div>
<span data-bind="rpm">0</span>
  1. 比例约束渲染 — 处理不同设备屏幕比例
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

快速模板

<!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 仪表盘页面导入或在社区分享。

zip -r my_dashboard.tsd my_dashboard/