Add embedded HTML header for curve editor page
Minified HTML/CSS/JS that will be served directly from code without needing filesystem upload
This commit is contained in:
parent
e2d1a8f74f
commit
b13dbb2897
339
usermods/GPU_Fan_Controller/gpu_fan_html.h
Normal file
339
usermods/GPU_Fan_Controller/gpu_fan_html.h
Normal file
@ -0,0 +1,339 @@
|
||||
#pragma once
|
||||
|
||||
// GPU Fan Controller - Embedded Web Page
|
||||
// This is served directly from code at /gpu-fan
|
||||
|
||||
const char GPU_FAN_HTML[] PROGMEM = R"=====(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>GPU Fan</title>
|
||||
<style>
|
||||
:root{--bg:#1a1a2e;--card:#16213e;--acc:#0f3460;--hl:#e94560;--txt:#eee;--dim:#888;--ok:#4ade80;--warn:#fbbf24;--err:#ef4444}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:system-ui,sans-serif;background:var(--bg);color:var(--txt);padding:15px}
|
||||
.c{max-width:700px;margin:0 auto}
|
||||
h1{text-align:center;margin-bottom:15px;color:var(--hl);font-size:1.5em}
|
||||
.card{background:var(--card);border-radius:10px;padding:15px;margin-bottom:15px}
|
||||
.card h2{font-size:.9em;margin-bottom:12px;color:var(--dim);text-transform:uppercase}
|
||||
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
|
||||
.stat{text-align:center;padding:10px;background:var(--acc);border-radius:6px}
|
||||
.val{font-size:1.5em;font-weight:bold}
|
||||
.lbl{font-size:.7em;color:var(--dim);margin-top:3px}
|
||||
.t-val{color:var(--warn)}.s-val{color:var(--ok)}.m-val{color:var(--hl)}
|
||||
.graph{position:relative;width:100%;height:200px;background:var(--acc);border-radius:6px;overflow:hidden}
|
||||
svg{width:100%;height:100%}
|
||||
.gline{stroke:#fff3;stroke-width:1}
|
||||
.albl{fill:var(--dim);font-size:9px}
|
||||
.curve{fill:none;stroke:var(--hl);stroke-width:2.5}
|
||||
.area{fill:url(#grad);opacity:.3}
|
||||
.point{fill:var(--hl);stroke:#fff;stroke-width:2;cursor:grab}
|
||||
.point:hover{r:9;fill:#fff}
|
||||
.tline{stroke:var(--warn);stroke-width:2;stroke-dasharray:4,4}
|
||||
.tdot{fill:var(--warn);stroke:#fff;stroke-width:2}
|
||||
.btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}
|
||||
.btn{padding:8px 16px;border:none;border-radius:5px;cursor:pointer;font-size:.85em}
|
||||
.btn-s{background:var(--acc);color:var(--txt)}
|
||||
.btn-g{background:var(--ok);color:#000}
|
||||
.mode{display:flex;background:var(--acc);border-radius:6px;padding:3px;margin-bottom:12px}
|
||||
.mode button{flex:1;padding:8px;border:none;background:0;color:var(--dim);cursor:pointer;border-radius:4px;font-size:.85em}
|
||||
.mode button.on{background:var(--hl);color:#fff}
|
||||
.slider{padding:12px 0}
|
||||
.slider.hide{display:none}
|
||||
.slider input{width:100%;height:6px;-webkit-appearance:none;background:var(--acc);border-radius:3px}
|
||||
.slider input::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;background:var(--hl);border-radius:50%;cursor:pointer}
|
||||
.slider .v{text-align:center;font-size:1.3em;margin-top:8px}
|
||||
table{width:100%;margin-top:12px;border-collapse:collapse;font-size:.85em}
|
||||
th,td{padding:6px;text-align:center;border-bottom:1px solid var(--acc)}
|
||||
table input{width:50px;padding:4px;border:1px solid var(--acc);border-radius:3px;background:var(--bg);color:var(--txt);text-align:center}
|
||||
.toast{position:fixed;bottom:15px;left:50%;transform:translateX(-50%);padding:10px 20px;background:var(--ok);color:#000;border-radius:6px;opacity:0;transition:opacity .3s;font-size:.9em}
|
||||
.toast.show{opacity:1}
|
||||
.toast.err{background:var(--err);color:#fff}
|
||||
.back{display:inline-block;margin-bottom:10px;color:var(--dim);text-decoration:none;font-size:.9em}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="c">
|
||||
<a href="/" class="back">← WLED</a>
|
||||
<h1>🌀 GPU Fan Controller</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>Status</h2>
|
||||
<div class="grid">
|
||||
<div class="stat"><div class="val t-val" id="temp">--</div><div class="lbl">Temp °C</div></div>
|
||||
<div class="stat"><div class="val s-val" id="speed">--</div><div class="lbl">Fan %</div></div>
|
||||
<div class="stat"><div class="val m-val" id="mode">--</div><div class="lbl">Mode</div></div>
|
||||
<div class="stat"><div class="val" id="status" style="color:var(--ok)">●</div><div class="lbl" id="stxt">OK</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Mode</h2>
|
||||
<div class="mode">
|
||||
<button id="btnF" onclick="setMode(0)">Fixed</button>
|
||||
<button id="btnC" class="on" onclick="setMode(1)">Curve</button>
|
||||
</div>
|
||||
|
||||
<div class="slider hide" id="fCtrl">
|
||||
<input type="range" id="fSlider" min="0" max="100" value="50" oninput="setFixed(this.value)">
|
||||
<div class="v"><span id="fVal">50</span>%</div>
|
||||
</div>
|
||||
|
||||
<div id="cCtrl">
|
||||
<div class="graph">
|
||||
<svg id="svg" viewBox="0 0 400 200">
|
||||
<defs><linearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#e94560;stop-opacity:.5"/>
|
||||
<stop offset="100%" style="stop-color:#e94560;stop-opacity:0"/>
|
||||
</linearGradient></defs>
|
||||
<g id="grid"></g>
|
||||
<g id="labels"></g>
|
||||
<line id="tLine" class="tline" x1="0" y1="0" x2="0" y2="200"/>
|
||||
<path id="area" class="area"/>
|
||||
<path id="curve" class="curve"/>
|
||||
<circle id="tDot" class="tdot" r="5" cx="0" cy="0"/>
|
||||
<g id="points"></g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>Temp</th><th>Speed</th></tr></thead>
|
||||
<tbody id="tbl"></tbody>
|
||||
</table>
|
||||
|
||||
<div class="btns">
|
||||
<button class="btn btn-s" onclick="addPt()">+ Add</button>
|
||||
<button class="btn btn-s" onclick="remPt()">- Remove</button>
|
||||
<button class="btn btn-s" onclick="reset()">Reset</button>
|
||||
<button class="btn btn-g" onclick="save()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
const P={l:40,r:15,t:15,b:25},W=400,H=200;
|
||||
const GW=W-P.l-P.r,GH=H-P.t-P.b;
|
||||
const TN=20,TX=100,SN=0,SX=100;
|
||||
|
||||
let pts=[{t:30,s:30},{t:50,s:50},{t:70,s:75},{t:85,s:100}];
|
||||
let cMode=1,fSpd=50,cTemp=25,drag=null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded',()=>{
|
||||
drawGrid();loadCfg();draw();
|
||||
setInterval(poll,2000);
|
||||
});
|
||||
|
||||
function tX(t){return P.l+((t-TN)/(TX-TN))*GW}
|
||||
function sY(s){return P.t+(1-(s-SN)/(SX-SN))*GH}
|
||||
function xT(x){return TN+((x-P.l)/GW)*(TX-TN)}
|
||||
function yS(y){return SX-((y-P.t)/GH)*(SX-SN)}
|
||||
|
||||
function drawGrid(){
|
||||
let g=document.getElementById('grid'),l=document.getElementById('labels');
|
||||
for(let t=20;t<=100;t+=20){
|
||||
let x=tX(t);
|
||||
g.innerHTML+=`<line class="gline" x1="${x}" y1="${P.t}" x2="${x}" y2="${H-P.b}"/>`;
|
||||
l.innerHTML+=`<text class="albl" x="${x}" y="${H-8}" text-anchor="middle">${t}</text>`;
|
||||
}
|
||||
for(let s=0;s<=100;s+=25){
|
||||
let y=sY(s);
|
||||
g.innerHTML+=`<line class="gline" x1="${P.l}" y1="${y}" x2="${W-P.r}" y2="${y}"/>`;
|
||||
l.innerHTML+=`<text class="albl" x="${P.l-5}" y="${y+3}" text-anchor="end">${s}</text>`;
|
||||
}
|
||||
}
|
||||
|
||||
function draw(){
|
||||
pts.sort((a,b)=>a.t-b.t);
|
||||
let path='',area='';
|
||||
pts.forEach((p,i)=>{
|
||||
let x=tX(p.t),y=sY(p.s);
|
||||
path+=(i?'L':'M')+x+','+y+' ';
|
||||
});
|
||||
if(pts.length){
|
||||
area=path+`L${tX(pts[pts.length-1].t)},${sY(0)} L${tX(pts[0].t)},${sY(0)} Z`;
|
||||
}
|
||||
document.getElementById('curve').setAttribute('d',path);
|
||||
document.getElementById('area').setAttribute('d',area);
|
||||
|
||||
let pg=document.getElementById('points');pg.innerHTML='';
|
||||
pts.forEach((p,i)=>{
|
||||
let c=document.createElementNS('http://www.w3.org/2000/svg','circle');
|
||||
c.setAttribute('class','point');
|
||||
c.setAttribute('cx',tX(p.t));
|
||||
c.setAttribute('cy',sY(p.s));
|
||||
c.setAttribute('r',7);
|
||||
c.setAttribute('data-i',i);
|
||||
c.onmousedown=c.ontouchstart=startDrag;
|
||||
pg.appendChild(c);
|
||||
});
|
||||
|
||||
drawTbl();
|
||||
drawTemp();
|
||||
}
|
||||
|
||||
function drawTbl(){
|
||||
let tb=document.getElementById('tbl');tb.innerHTML='';
|
||||
pts.forEach((p,i)=>{
|
||||
tb.innerHTML+=`<tr><td>${i+1}</td>
|
||||
<td><input type="number" value="${p.t}" min="0" max="100" onchange="setPt(${i},'t',this.value)"></td>
|
||||
<td><input type="number" value="${p.s}" min="0" max="100" onchange="setPt(${i},'s',this.value)"></td></tr>`;
|
||||
});
|
||||
}
|
||||
|
||||
function setPt(i,k,v){pts[i][k]=parseInt(v);draw()}
|
||||
|
||||
function drawTemp(){
|
||||
let x=tX(cTemp);
|
||||
document.getElementById('tLine').setAttribute('x1',x);
|
||||
document.getElementById('tLine').setAttribute('x2',x);
|
||||
let s=calcSpd(cTemp);
|
||||
document.getElementById('tDot').setAttribute('cx',x);
|
||||
document.getElementById('tDot').setAttribute('cy',sY(s));
|
||||
}
|
||||
|
||||
function calcSpd(t){
|
||||
if(pts.length<2)return 50;
|
||||
if(t<=pts[0].t)return pts[0].s;
|
||||
if(t>=pts[pts.length-1].t)return pts[pts.length-1].s;
|
||||
for(let i=0;i<pts.length-1;i++){
|
||||
if(t>=pts[i].t&&t<=pts[i+1].t){
|
||||
let r=(t-pts[i].t)/(pts[i+1].t-pts[i].t);
|
||||
return pts[i].s+r*(pts[i+1].s-pts[i].s);
|
||||
}
|
||||
}
|
||||
return 50;
|
||||
}
|
||||
|
||||
function startDrag(e){
|
||||
e.preventDefault();
|
||||
drag=parseInt(e.target.getAttribute('data-i'));
|
||||
document.onmousemove=document.ontouchmove=doDrag;
|
||||
document.onmouseup=document.ontouchend=endDrag;
|
||||
}
|
||||
|
||||
function doDrag(e){
|
||||
if(drag===null)return;
|
||||
e.preventDefault();
|
||||
let svg=document.getElementById('svg');
|
||||
let rect=svg.getBoundingClientRect();
|
||||
let cx=e.touches?e.touches[0].clientX:e.clientX;
|
||||
let cy=e.touches?e.touches[0].clientY:e.clientY;
|
||||
let x=(cx-rect.left)*(W/rect.width);
|
||||
let y=(cy-rect.top)*(H/rect.height);
|
||||
pts[drag].t=Math.max(TN,Math.min(TX,Math.round(xT(x))));
|
||||
pts[drag].s=Math.max(SN,Math.min(SX,Math.round(yS(y))));
|
||||
draw();
|
||||
}
|
||||
|
||||
function endDrag(){
|
||||
drag=null;
|
||||
document.onmousemove=document.ontouchmove=null;
|
||||
document.onmouseup=document.ontouchend=null;
|
||||
}
|
||||
|
||||
function addPt(){
|
||||
if(pts.length>=5){toast('Max 5',1);return}
|
||||
let mt=(pts[0].t+pts[pts.length-1].t)/2;
|
||||
pts.push({t:Math.round(mt),s:Math.round(calcSpd(mt))});
|
||||
draw();
|
||||
}
|
||||
|
||||
function remPt(){
|
||||
if(pts.length<=2){toast('Min 2',1);return}
|
||||
pts.pop();draw();
|
||||
}
|
||||
|
||||
function reset(){
|
||||
pts=[{t:30,s:30},{t:50,s:50},{t:70,s:75},{t:85,s:100}];
|
||||
draw();
|
||||
}
|
||||
|
||||
function setMode(m){
|
||||
cMode=m;
|
||||
document.getElementById('btnF').classList.toggle('on',m===0);
|
||||
document.getElementById('btnC').classList.toggle('on',m===1);
|
||||
document.getElementById('fCtrl').classList.toggle('hide',m!==0);
|
||||
document.getElementById('cCtrl').style.display=m===0?'none':'block';
|
||||
}
|
||||
|
||||
function setFixed(v){
|
||||
fSpd=parseInt(v);
|
||||
document.getElementById('fVal').textContent=fSpd;
|
||||
fetch('/json/state',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({'GPU-Fan':{speed:fSpd}})});
|
||||
}
|
||||
|
||||
async function loadCfg(){
|
||||
try{
|
||||
let r=await fetch('/cfg.json');
|
||||
let cfg=await r.json();
|
||||
let f=cfg.um?.['GPU-Fan'];
|
||||
if(f){
|
||||
cMode=f.mode||1;
|
||||
fSpd=f['fixed-speed']||50;
|
||||
pts=[];
|
||||
let n=f['curve-points']||4;
|
||||
for(let i=1;i<=n&&i<=5;i++){
|
||||
let t=f['curve-t'+i],s=f['curve-s'+i];
|
||||
if(t!==undefined&&s!==undefined)pts.push({t,s});
|
||||
}
|
||||
if(pts.length<2)reset();
|
||||
setMode(cMode);
|
||||
document.getElementById('fSlider').value=fSpd;
|
||||
document.getElementById('fVal').textContent=fSpd;
|
||||
draw();
|
||||
}
|
||||
}catch(e){console.error(e)}
|
||||
}
|
||||
|
||||
async function poll(){
|
||||
try{
|
||||
let r=await fetch('/json/info');
|
||||
let info=await r.json();
|
||||
let u=info.u;
|
||||
if(u){
|
||||
if(u['GPU Temp']){cTemp=parseFloat(u['GPU Temp'][0])||25;document.getElementById('temp').textContent=cTemp.toFixed(1)}
|
||||
if(u['Fan Speed'])document.getElementById('speed').textContent=parseInt(u['Fan Speed'][0])||0;
|
||||
if(u['Fan Mode'])document.getElementById('mode').textContent=u['Fan Mode'][0];
|
||||
}
|
||||
drawTemp();
|
||||
document.getElementById('status').style.color='var(--ok)';
|
||||
document.getElementById('stxt').textContent='OK';
|
||||
}catch(e){
|
||||
document.getElementById('status').style.color='var(--err)';
|
||||
document.getElementById('stxt').textContent='Err';
|
||||
}
|
||||
}
|
||||
|
||||
async function save(){
|
||||
pts.sort((a,b)=>a.t-b.t);
|
||||
let cfg={'GPU-Fan':{enabled:true,mode:cMode,'fixed-speed':fSpd,'curve-points':pts.length}};
|
||||
for(let i=0;i<5;i++){
|
||||
cfg['GPU-Fan']['curve-t'+(i+1)]=i<pts.length?pts[i].t:50;
|
||||
cfg['GPU-Fan']['curve-s'+(i+1)]=i<pts.length?pts[i].s:50;
|
||||
}
|
||||
try{
|
||||
let r=await fetch('/cfg.json',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({um:cfg})});
|
||||
if(r.ok){
|
||||
toast('Saved!');
|
||||
fetch('/json/state',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({'GPU-Fan':{mode:cMode}})});
|
||||
}else toast('Fail',1);
|
||||
}catch(e){toast('Err',1)}
|
||||
}
|
||||
|
||||
function toast(m,e){
|
||||
let t=document.getElementById('toast');
|
||||
t.textContent=m;
|
||||
t.classList.toggle('err',!!e);
|
||||
t.classList.add('show');
|
||||
setTimeout(()=>t.classList.remove('show'),2000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)=====";
|
||||
Loading…
Reference in New Issue
Block a user