adding state page

This commit is contained in:
jbruce12000 2024-07-23 15:23:28 -04:00
parent 168204bdb3
commit f2cf1675cb
5 changed files with 434 additions and 0 deletions

View File

@ -44,6 +44,10 @@ oven.set_ovenwatcher(ovenWatcher)
def index():
return bottle.redirect('/picoreflow/index.html')
@app.route('/state')
def state():
return bottle.redirect('/picoreflow/state.html')
@app.get('/api/stats')
def handle_api():
log.info("/api/stats command received")

View File

@ -342,6 +342,7 @@ class Oven(threading.Thread):
self.heat_rate = 0
self.heat_rate_temps = []
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
self.catching_up = False
@staticmethod
def get_start_from_temperature(profile, temp):
@ -407,10 +408,15 @@ class Oven(threading.Thread):
if self.target - temp > config.pid_control_window:
log.info("kiln must catch up, too cold, shifting schedule")
self.start_time = self.get_start_time()
self.catching_up = True;
return
# kiln too hot, wait for it to cool down
if temp - self.target > config.pid_control_window:
log.info("kiln must catch up, too hot, shifting schedule")
self.start_time = self.get_start_time()
self.catching_up = True;
return
self.catching_up = False;
def update_runtime(self):
@ -473,6 +479,7 @@ class Oven(threading.Thread):
'currency_type': config.currency_type,
'profile': self.profile.name if self.profile else None,
'pidstats': self.pid.pidstats,
'catching_up': self.catching_up,
}
return state

View File

@ -0,0 +1,47 @@
.container {
border-radius: 5px;
background: #888888;
//background: #CCCCCC;
padding: 2px;
display: flex;
flex-direction: row;
align-items: center;
width: max-content;
font-family: sans-serif;
margin: 4px;
}
.stat {
padding: 2px;
border-radius: 5px;
font-size: 40pt;
color: #FFFFFF;
}
.stattxt {
padding: 7px;
border-radius: 5px;
font-size: 40pt;
background: #BBBBBB;
//color: #FFFFFF;
color: #888888;
margin: 1px 2px 1px 2px;
}
.top {
border-radius: 5px 5px 0px 0px;
background: #0000CC;
padding: 4px;
color: #CCCCCC;
text-align: center;
font-size: 18pt;
}
.bottom {
border-radius: 0px 0px 5px 5px;
background: #BBBBBB;
padding: 4px;
text-align: center;
color: #0000CC;
font-size: 20pt;
}

302
public/assets/js/state.js Normal file
View File

@ -0,0 +1,302 @@
config = "";
all = [];
var table = "";
var protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:';
}
var host = "" + protocol + "//" + window.location.hostname + ":" + window.location.port;
var ws_status = new WebSocket(host+"/status");
var ws_config = new WebSocket(host+"/config");
ws_status.onmessage = function(e) {
x = JSON.parse(e.data);
if (x.pidstats) {
x.pidstats["datetime"]=unix_to_yymmdd_hhmmss(x.pidstats.time);
x.pidstats.err = x.pidstats.err*-1;
x.pidstats.out = x.pidstats.out*100;
x.pidstats.catching_up = x.catching_up
if (x.catching_up == true) {
x.pidstats.catchingup = x.pidstats.ispoint;
}
all.push(x.pidstats);
}
var str = JSON.stringify(x, null, 2);
document.getElementById("state").innerHTML = "<pre>"+str+"</pre>"
table.replaceData(latest(20));
drawall(all);
document.getElementById("error-current").innerHTML = rnd(x.pidstats.err);
document.getElementById("error-1min").innerHTML = rnd(average("err",1,all));
document.getElementById("error-5min").innerHTML = rnd(average("err",5,all));
document.getElementById("error-15min").innerHTML = rnd(average("err",15,all));
document.getElementById("temp").innerHTML = rnd(x.pidstats.ispoint);
document.getElementById("target").innerHTML = rnd(x.pidstats.setpoint);
document.getElementById("heat-pct").innerHTML = rnd(x.pidstats.out);
document.getElementById("catching-up").innerHTML = rnd(percent_catching_up(all));
};
ws_config.onopen = function() {
ws_config.send('GET');
};
ws_config.onmessage = function(e) {
config = JSON.parse(e.data);
//console.log(e);
};
create_table(all);
//---------------------------------------------------------------------------
function rnd(number) {
return Number(number).toFixed(2);
}
//---------------------------------------------------------------------------
function average(field,minutes,data) {
if(data[0]!=null) {
var t = data[data.length - 1].time;
var oldest = t-(60*minutes);
var q = "SELECT AVG("+ field + ") from ? where time>=" + oldest.toString();
var avg = alasql(q,[data]);
return avg[0]["AVG(err)"];
}
return 0;
}
//---------------------------------------------------------------------------
function drawall(data) {
draw_temps(data);
draw_error(data);
draw_heat(data);
draw_p(data);
draw_i(data);
draw_d(data);
}
//---------------------------------------------------------------------------
function draw_heat(data) {
var traces=[];
var rows = alasql('SELECT datetime, out from ?',[data]);
var title = 'Heating Percent';
var trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'out'),
name: 'heat',
mode: 'lines',
line: { color: 'rgb(255,0,0)', width:2 }
};
traces.push(trace);
spot = document.getElementById('heat');
var layout = {
title: title,
showlegend: true,
};
Plotly.newPlot(spot, traces, layout, {displayModeBar: false});
}
//---------------------------------------------------------------------------
function draw_p(data) {
var traces=[];
var rows = alasql('SELECT datetime, p from ?',[data]);
var title = 'Proportional';
var trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'p'),
name: 'p',
mode: 'lines',
line: { color: 'rgb(0,0,255)', width:2 }
};
traces.push(trace);
spot = document.getElementById('p');
var layout = {
title: title,
showlegend: true,
};
Plotly.newPlot(spot, traces, layout, {displayModeBar: false});
}
//---------------------------------------------------------------------------
function draw_i(data) {
var traces=[];
var rows = alasql('SELECT datetime, i from ?',[data]);
var title = 'Integral';
var trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'i'),
name: 'i',
mode: 'lines',
line: { color: 'rgb(0,0,255)', width:2 }
};
traces.push(trace);
spot = document.getElementById('i');
var layout = {
title: title,
showlegend: true,
};
Plotly.newPlot(spot, traces, layout, {displayModeBar: false});
}
//---------------------------------------------------------------------------
function draw_d(data) {
var traces=[];
var rows = alasql('SELECT datetime, d from ?',[data]);
var title = 'Derivative';
var trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'd'),
name: 'd',
mode: 'lines',
line: { color: 'rgb(0,0,255)', width:2 }
};
traces.push(trace);
spot = document.getElementById('d');
var layout = {
title: title,
showlegend: true,
};
Plotly.newPlot(spot, traces, layout, {displayModeBar: false});
}
//---------------------------------------------------------------------------
function draw_error(data) {
var traces=[];
var rows = alasql('SELECT datetime, err from ?',[data]);
var title = 'Error';
var trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'err'),
name: 'error',
mode: 'lines',
line: { color: 'rgb(255,0,0)', width:2 }
};
traces.push(trace);
spot = document.getElementById('error');
var layout = {
title: title,
showlegend: true,
//xaxis : { tickformat:'%b' },
};
Plotly.newPlot(spot, traces, layout, {displayModeBar: false});
}
//---------------------------------------------------------------------------
function draw_temps(data) {
var traces=[];
var rows = alasql('SELECT datetime, ispoint, setpoint, catchingup from ?',[data]);
var title = 'Temperature and Target';
var trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'setpoint'),
name: 'target',
mode: 'lines',
line: { color: 'rgb(0,0,255)', width:2 }
};
traces.push(trace);
trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'ispoint'),
name: 'temp',
mode: 'lines',
line: { color: 'rgb(255,0,0)', width:2 }
};
traces.push(trace);
trace = {
x: unpack(rows, 'datetime'),
y: unpack(rows, 'catchingup'),
name: 'catchup',
mode: 'markers',
marker: { color: 'rgb(0,255,0)', width:3 }
};
traces.push(trace);
spot = document.getElementById('temps');
var layout = {
title: title,
showlegend: true,
//xaxis : { tickformat:'%b' },
};
Plotly.newPlot(spot, traces, layout, {displayModeBar: false});
}
//---------------------------------------------------------------------------
function unpack(rows, key) {
return rows.map(function(row) { return row[key]; });
}
//---------------------------------------------------------------------------
function unix_to_yymmdd_hhmmss(t) {
var date = new Date(t * 1000);
var newd = new Date(date.getTime() - date.getTimezoneOffset()*60000);
//return date.toLocaleString('en-US',{hour12:false}).replace(',','');
return newd.toISOString().replace("T"," ").substring(0, 19);
}
//---------------------------------------------------------------------------
function latest(n) {
//sql = "select * from ? order by time desc limit " + n;
sql = "select * from ? order by time desc";
results = alasql(sql,[all]);
return results;
}
//---------------------------------------------------------------------------
function percent_catching_up(data) {
var sql = "select sum(timeDelta) as slip from ? where catching_up=true";
var a = alasql(sql,[data]);
a = a[0]["slip"];
sql = "select sum(timeDelta) as [all] from ?";
var b = alasql(sql,[data]);
b = b[0]["all"];
return a/b*100;
}
//---------------------------------------------------------------------------
function create_table(data) {
table = new Tabulator("#state-table", {
height:300,
data:data, //assign data to table
//layout:"fitColumns", //fit columns to width of table (optional)
columns:[
{title:"DateTime", field:"datetime"},
{title:"Target", field:"setpoint"},
{title:"Temp", field:"ispoint"},
{title:"Error", field:"err"},
{title:"P", field:"p"},
{title:"I", field:"i"},
{title:"D", field:"d"},
{title:"Heat", field:"out"},
{title:"Catching Up", field:"catching_up"},
{title:"Time Delta", field:"timeDelta"},
]});
}
//---------------------------------------------------------------------------
function csv_string() {
table.download("csv", "kiln-state.csv");
}

74
public/state.html Normal file
View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Kiln Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/css/state.css"/>
</head>
<body>
<div class="container" id="temp-stats">
<div class="stattxt">TEMP</div>
<div class="stat">
<div class="top">temp</div>
<div class="bottom" id="temp"></div>
</div>
<div class="stat">
<div class="top">target</div>
<div class="bottom" id="target"></div>
</div>
</div>
<div class="container" id="error-stats">
<div class="stattxt">ERROR</div>
<div class="stat">
<div class="top">now</div>
<div class="bottom" id="error-current"></div>
</div>
<div class="stat">
<div class="top">1 min</div>
<div class="bottom" id="error-1min"></div>
</div>
<div class="stat">
<div class="top">5 min</div>
<div class="bottom" id="error-5min"></div>
</div>
<div class="stat">
<div class="top">15 min</div>
<div class="bottom" id="error-15min"></div>
</div>
</div>
<div class="container" id="heat-stats">
<div class="stattxt">HEAT</div>
<div class="stat">
<div class="top">%</div>
<div class="bottom" id="heat-pct"></div>
</div>
</div>
<div class="container" id="catchingup">
<div class="stattxt">CATCH UP</div>
<div class="stat">
<div class="top">%</div>
<div class="bottom" id="catching-up"></div>
</div>
</div>
<div id="temps"></div>
<div id="error"></div>
<div id="heat"></div>
<div id="p"></div>
<div id="i"></div>
<div id="d"></div>
<div id="state-table"></div>
<div id="state-table-controls"><a href="javascript: void(0)" onclick="csv_string();" >csv</a></div>
<div id="state"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/alasql/4.3.2/alasql.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.32.0/plotly.min.js" charset="utf-8"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.6.1/css/tabulator.min.css" rel="stylesheet">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/tabulator/5.6.1/js/tabulator.min.js"></script>
<script src="assets/js/state.js"></script>
</body>
</html>