adding state page
This commit is contained in:
parent
168204bdb3
commit
f2cf1675cb
@ -44,6 +44,10 @@ oven.set_ovenwatcher(ovenWatcher)
|
|||||||
def index():
|
def index():
|
||||||
return bottle.redirect('/picoreflow/index.html')
|
return bottle.redirect('/picoreflow/index.html')
|
||||||
|
|
||||||
|
@app.route('/state')
|
||||||
|
def state():
|
||||||
|
return bottle.redirect('/picoreflow/state.html')
|
||||||
|
|
||||||
@app.get('/api/stats')
|
@app.get('/api/stats')
|
||||||
def handle_api():
|
def handle_api():
|
||||||
log.info("/api/stats command received")
|
log.info("/api/stats command received")
|
||||||
|
|||||||
@ -342,6 +342,7 @@ class Oven(threading.Thread):
|
|||||||
self.heat_rate = 0
|
self.heat_rate = 0
|
||||||
self.heat_rate_temps = []
|
self.heat_rate_temps = []
|
||||||
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
|
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
|
||||||
|
self.catching_up = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_start_from_temperature(profile, temp):
|
def get_start_from_temperature(profile, temp):
|
||||||
@ -407,10 +408,15 @@ class Oven(threading.Thread):
|
|||||||
if self.target - temp > config.pid_control_window:
|
if self.target - temp > config.pid_control_window:
|
||||||
log.info("kiln must catch up, too cold, shifting schedule")
|
log.info("kiln must catch up, too cold, shifting schedule")
|
||||||
self.start_time = self.get_start_time()
|
self.start_time = self.get_start_time()
|
||||||
|
self.catching_up = True;
|
||||||
|
return
|
||||||
# kiln too hot, wait for it to cool down
|
# kiln too hot, wait for it to cool down
|
||||||
if temp - self.target > config.pid_control_window:
|
if temp - self.target > config.pid_control_window:
|
||||||
log.info("kiln must catch up, too hot, shifting schedule")
|
log.info("kiln must catch up, too hot, shifting schedule")
|
||||||
self.start_time = self.get_start_time()
|
self.start_time = self.get_start_time()
|
||||||
|
self.catching_up = True;
|
||||||
|
return
|
||||||
|
self.catching_up = False;
|
||||||
|
|
||||||
def update_runtime(self):
|
def update_runtime(self):
|
||||||
|
|
||||||
@ -473,6 +479,7 @@ class Oven(threading.Thread):
|
|||||||
'currency_type': config.currency_type,
|
'currency_type': config.currency_type,
|
||||||
'profile': self.profile.name if self.profile else None,
|
'profile': self.profile.name if self.profile else None,
|
||||||
'pidstats': self.pid.pidstats,
|
'pidstats': self.pid.pidstats,
|
||||||
|
'catching_up': self.catching_up,
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|||||||
47
public/assets/css/state.css
Normal file
47
public/assets/css/state.css
Normal 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
302
public/assets/js/state.js
Normal 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
74
public/state.html
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user