feat: #3 tire temperature matrix dashboard (FL/FR/RL/RR heatmap)
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
<!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 - 轮胎温度</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0f;color:#fff;
|
||||||
|
font-family:'SF Pro Display',-apple-system,system-ui,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}
|
||||||
|
#content{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#0a0a0f;gap:min(3vh,16px)}
|
||||||
|
.grid{display:grid;grid-template-columns:1fr 1fr;gap:min(3vw,20px);width:min(70vw,400px)}
|
||||||
|
.tire{display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.tire-box{width:min(20vw,120px);height:min(20vw,120px);border-radius:12px;background:rgba(255,255,255,.04);display:flex;flex-direction:column;align-items:center;justify-content:center;transition:background .3s}
|
||||||
|
.tire-temp{font-size:min(6vw,40px);font-weight:300;font-variant-numeric:tabular-nums}
|
||||||
|
.tire-label{font-size:min(2.5vw,14px);color:rgba(255,255,255,.3);text-transform:uppercase;letter-spacing:.1em}
|
||||||
|
.tire-status{font-size:min(2vw,11px);margin-top:2px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"><div id="box" class="contain"><div id="content">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="tire"><div class="tire-box" id="tfl"><div class="tire-temp" id="vfl">--</div><div class="tire-status" id="sfl"></div></div><div class="tire-label">FL 左前</div></div>
|
||||||
|
<div class="tire"><div class="tire-box" id="tfr"><div class="tire-temp" id="vfr">--</div><div class="tire-status" id="sfr"></div></div><div class="tire-label">FR 右前</div></div>
|
||||||
|
<div class="tire"><div class="tire-box" id="trl"><div class="tire-temp" id="vrl">--</div><div class="tire-status" id="srl"></div></div><div class="tire-label">RL 左后</div></div>
|
||||||
|
<div class="tire"><div class="tire-box" id="trr"><div class="tire-temp" id="vrr">--</div><div class="tire-status" id="srr"></div></div><div class="tire-label">RR 右后</div></div>
|
||||||
|
</div>
|
||||||
|
</div></div></div>
|
||||||
|
<script>
|
||||||
|
const M={aspect_ratio:"4:3",render_mode:"contain"};
|
||||||
|
(function(){
|
||||||
|
const box=document.getElementById('box');
|
||||||
|
const proto=location.protocol==='https:'?'wss:':'ws:',wsUrl=proto+'//'+location.host+'/ws';
|
||||||
|
let ws,rt;
|
||||||
|
const ids=['fl','fr','rl','rr'];
|
||||||
|
|
||||||
|
function pr(r){if(!r||r==='auto')return null;const p=r.split(':');if(p.length===2)return +p[0]/+p[1]}
|
||||||
|
|
||||||
|
function apply(){
|
||||||
|
const rt=pr(M.aspect_ratio||'4:3'),md=M.render_mode||'contain';
|
||||||
|
if(!rt){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(md==='cover'){cw=vw;ch=vw/rt;box.className='cover'}
|
||||||
|
else{box.className='contain';cw=vr>rt?vh*rt:vw;ch=vr>rt?vh:vw/rt}
|
||||||
|
box.style.width=cw+'px';box.style.height=ch+'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
function tempColor(t){
|
||||||
|
// 0=Cold(blue) 60=Optimal(green) 100+=Overheat(red)
|
||||||
|
if(t<20)return'rgba(52,130,255,.15)';
|
||||||
|
if(t<50)return'rgba(52,200,130,.2)';
|
||||||
|
if(t<80)return'rgba(52,200,50,.25)';
|
||||||
|
if(t<100)return'rgba(255,160,20,.25)';
|
||||||
|
return'rgba(255,50,40,.3)';
|
||||||
|
}
|
||||||
|
function tempStatus(t){
|
||||||
|
if(t<20)return{text:'太冷',color:'#3482FF'};
|
||||||
|
if(t<50)return{text:'预热',color:'#34C880'};
|
||||||
|
if(t<80)return{text:'最佳',color:'#34C830'};
|
||||||
|
if(t<100)return{text:'偏高',color:'#FFA014'};
|
||||||
|
return{text:'过热',color:'#FF3228'};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cn(){ws=new WebSocket(wsUrl);ws.onopen=()=>{if(rt){clearTimeout(rt);rt=null}}
|
||||||
|
ws.onmessage=(e)=>{try{const m=JSON.parse(e.data);if(m.type==='telemetry'){
|
||||||
|
const d=m.data,raw=d.raw||{};
|
||||||
|
// Forza parser puts tire temps in raw; alternately we can read direct fields
|
||||||
|
const temps=[raw.tire_temp_fl||0,raw.tire_temp_fr||0,raw.tire_temp_rl||0,raw.tire_temp_rr||0];
|
||||||
|
ids.forEach((id,i)=>{
|
||||||
|
const t=temps[i],el=document.getElementById('t'+id),vEl=document.getElementById('v'+id),sEl=document.getElementById('s'+id);
|
||||||
|
if(t>0){
|
||||||
|
el.style.background=tempColor(t);
|
||||||
|
vEl.textContent=Math.round(t)+'°';
|
||||||
|
const st=tempStatus(t);sEl.textContent=st.text;sEl.style.color=st.color;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}catch(e){}}
|
||||||
|
ws.onclose=()=>{rt=setTimeout(cn,2000)}}
|
||||||
|
|
||||||
|
apply();window.addEventListener('resize',apply);cn()
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"id": "tire_temp",
|
||||||
|
"name": "轮胎温度矩阵",
|
||||||
|
"category": "physics",
|
||||||
|
"description": "四轮实时温度热力图,蓝=冷 / 绿=最佳 / 红=过热",
|
||||||
|
"author": "Yei.J. (AskaEth)",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"supported_games": "forza_horizon,forza_horizon_6,forza_motorsport",
|
||||||
|
"config": {
|
||||||
|
"icon": "🌡",
|
||||||
|
"aspect_ratio": "4:3",
|
||||||
|
"render_mode": "contain"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,11 @@ class ForzaHorizonParser:
|
|||||||
td.handbrake = struct.unpack_from("<B", data, 318)[0] / 255.0
|
td.handbrake = struct.unpack_from("<B", data, 318)[0] / 255.0
|
||||||
td.gear = struct.unpack_from("<B", data, 319)[0]
|
td.gear = struct.unpack_from("<B", data, 319)[0]
|
||||||
td.steering = max(-1.0, min(1.0, struct.unpack_from("<b", data, 320)[0] / 127.0))
|
td.steering = max(-1.0, min(1.0, struct.unpack_from("<b", data, 320)[0] / 127.0))
|
||||||
|
|
||||||
|
td.raw["tire_temp_fl"] = struct.unpack_from("<f", data, 268)[0]
|
||||||
|
td.raw["tire_temp_fr"] = struct.unpack_from("<f", data, 272)[0]
|
||||||
|
td.raw["tire_temp_rl"] = struct.unpack_from("<f", data, 276)[0]
|
||||||
|
td.raw["tire_temp_rr"] = struct.unpack_from("<f", data, 280)[0]
|
||||||
except struct.error:
|
except struct.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user