Skip to content
Snippets Groups Projects
Commit efbb3351 authored by Robert L. Read's avatar Robert L. Read
Browse files

moving from ventmon repo

parent c41485d8
No related branches found
No related tags found
No related merge requests found
<!--
Breath Plot: COVID-19 Respiration Analysis Software
Copyright (C) 2020 Robert L. Read
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<!-- Load plotly.js into the DOM -->
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
<title>Public Invention Respiration Analysis</title>
</head>
<style>
#calcarea {
flex-direction:column;
background: AliceBlue;
}
.calcnum {
color: blue;
font-size: xx-large;
}
.fence {
width: 3em;
}
.value_and_fences {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.limit {
display: flex;
flex-direction: row;
}
.limit label {
width: 1em;
}
.alarmred {
background: red;
}
</style>
<!-- Style for toggle switch -->
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>
<body>
<div class="container-fluid">
<div class="jumbotron">
<h1 class="display-4">VentMon Respiration Analysis</h1>
<p class="lead">This is a work in progress of <a href="https://www.pubinv.org">Public Invention</a>. It can be attached to a data server to produce
an interactive or static analysis of a respiration. It&#39;s primary purpose is to test pandemic ventilators, but it is free software meant to be reused for other purposes.
</p>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon3">PIRDS data server url:</span>
</div>
<input type="text" class="form-control" id="dserverurl" aria-describedby="basic-addon3">
<div class="input-group-append">
<a class="btn btn-outline-dark btn-sm" href="#" role="button" id="useofficial">Use Ventmon Data Lake: ventmon.coslabs.com</a>
</div>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon3">Trace ID:</span>
</div>
<input type="text" class="form-control" id="traceid" aria-describedby="basic-addon3">
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" for="samples_to_plot">Number of Samples (~10s per 15000 samples):</span>
</div>
<input type="text" class="form-control" id="samples_to_plot" aria-describedby="samples_to_plot">
</div>
<label for="livetoggle">Plot Live:</label>
<label class="switch">
<input type="checkbox" id="livetoggle" checked>
<span class="slider round"></span>
</label>
<div class="container-fluid">
<div class="row">
<div class="col-9">
<div id='PFGraph'><!-- Pressure and Flow Graph --></div>
<div id='EventsGraph'><!-- Events --></div>
</div>
<div class="col-3">
<div class="alert alert-danger" role="alert" id="main_alert">
Danger! Flow limits exceeded; volumes will be incorrect.
</div>
<div class="container" id="calcarea">
<div>
<label for="max">PIP (max): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="max"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="max_h">H:</label>
<input class="fence" id="max_h" type='text'> </input>
</div>
<div class="limit min">
<label for="max_l">L:</label>
<input class="fence" id="max_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="avg">P. Mean: </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="avg"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="avg_h">H:</label>
<input class="fence" id="avg_h" type='text'> </input>
</div>
<div class="limit min">
<label for="avg_l">L:</label>
<input class="fence" id="avg_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="min">PEEP (min): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="min"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="min_h">H:</label>
<input class="fence" id="min_h" type='text'> </input>
</div>
<div class="limit min">
<label for="min_l">L:</label>
<input class="fence" id="min_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="mv">MVs (l/min): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="mv"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="mv_h">H:</label>
<input class="fence" id="mv_h" type='text'> </input>
</div>
<div class="limit min">
<label for="mv_l">L:</label>
<input class="fence" id="mv_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="bpm">RR: </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="bpm"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="bmp_h">H:</label>
<input class="fence" id="bpm_h" type='text'> </input>
</div>
<div class="limit min">
<label for="bpm_l">L:</label>
<input class="fence" id="bpm_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="ier">I:E ratio: </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="ier"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="ier_h">H:</label>
<input class="fence" id="ier_h" type='text'> </input>
</div>
<div class="limit min">
<label for="ier_l">L:</label>
<input class="fence" id="ier_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="tv">VTd (ml): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="tv"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="tv_h">H:</label>
<input class="fence" id="tv_h" type='text'> </input>
</div>
<div class="limit min">
<label for="tv_l">L:</label>
<input class="fence" id="tv_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="fio2">FiO2 Mean (%): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="fio2"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="fio2_h">H:</label>
<input class="fence" id="fio2_h" type='text'> </input>
</div>
<div class="limit min">
<label for="fio2_l">L:</label>
<input class="fence" id="fio2_l" type='text'> </input>
</div>
</div>
</div>
<div>
<label for="taip">TAIP (ms): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="taip"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="taip_h">H:</label>
<input class="fence" id="taip_h" type='text'> </input>
</div>
<div class="limit min">
<label for="taip_l">L:</label>
<input class="fence" id="taip_l" type='text'> </input>
</div>
</div>
</div>
</div>
<div>
<label for="taip">TRIP (ms): </label>
<div class="value_and_fences">
<div class="calcnum">
<label id="trip"> </label>
</div>
<div class="vertical_alarms">
<div class="limit max">
<label for="trip_h">H:</label>
<input class="fence" id="trip_h" type='text'> </input>
</div>
<div class="limit min">
<label for="trip_l">L:</label>
<input class="fence" id="trip_l" type='text'> </input>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> <!-- end main area -->
</div>
<div>
Parameters:
<div>
<label for="tarip_h">TAIP/TRIP High Pressure (cm H2O):</label>
<input class="fence" id="tarip_h" type='text'> </input>
</div>
<div>
<label for="tarip_l">TAIP/TRIP Low Pressure (cm H2O):</label>
<input class="fence" id="tarip_l" type='text'> </input>
</div>
</div>
<div>
<button id="import">Import Trace</button>
<button id="export">Export Trace</button>
</div>
<div>
<textarea id="json_trace" rows="30" cols="80"></textarea>
</div>
<p>
This is a work in progress of <a href="https://www.pubinv.org">Public Invention</a>.
<p>
This is a tester tool for open-source ventilators.
It uses the <a href="https://github.com/PubInv/respiration-data-standard">PIRDS data format</a>.
<p>
The basic operation is receive data from web server specified in the URL above.
Probably for now that will be the VentMon Python web server that
listens on a serial port for the VentMon device or any other
device that streams PIRDS events.
</div>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script>
var TAIP_AND_TRIP_MIN = 2;
var TAIP_AND_TRIP_MAX = 8;
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
// These are defined in PIRDS.h as "constant" key words of special meaning...
var FLOW_LIMITS_EXCEEDED = false;
const FLOW_TOO_HIGH = "FLOW OUT OF RANGE HIGH";
const FLOW_TOO_LOW = "FLOW OUT OF RANGE LOW";
const VENTMON_DATA_LAKE = "http://ventmon.coslabs.com";
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
var TRACE_ID = urlParams.get('i')
var DSERVER_URL = window.location.protocol + "//" + window.location.host;
var NUM_TO_READ = 500;
var DATA_RETRIEVAL_PERIOD = 50;
var RESPIRATION_RATE_WINDOW_SECONDS = 20;
var intervalID = null;
// This sould be better as time, but is easier
// to do as number of samples.
var MAX_SAMPLES_TO_STORE_S = 32000;
var samples = [];
var INITS_ONLY = true;
// This is just to get the party started!
function init_samples() {
return [
{
"event": "M",
"type": "P",
"ms": 19673310,
"loc": "A",
"num": "0",
"val": 10111
},
{
"event": "M",
"type": "D",
"ms": 19673310,
"loc": "A",
"num": "0",
"val": 4
},
{
"event": "M",
"type": "F",
"ms": 19673310,
"loc": "A",
"num": "0",
"val": 0
},
{
"event": "M",
"type": "P",
"ms": 19673376,
"loc": "A",
"num": "0",
"val": 10111
},
{
"event": "M",
"type": "D",
"ms": 19673376,
"loc": "A",
"num": "0",
"val": 4
},
{
"event": "M",
"type": "F",
"ms": 19673376,
"loc": "A",
"num": "0",
"val": 0
},
{
"event": "M",
"type": "P",
"ms": 19673442,
"loc": "A",
"num": "0",
"val": 10110
},
{
"event": "M",
"type": "D",
"ms": 19673442,
"loc": "A",
"num": "0",
"val": 3
},
];
}
function unpack(rows, key) {
return rows.map(function(row) { return row[key]; });
}
const CONVERT_PIRDS_TO_SLM = 1/1000;
// we have now changed this, there will be flow and
// pressure in the same samples, and we should filter.
// TODO: I need to add maximal start and end
// samples to equalize all the plots.
function samplesToLine(samples) {
var flows = samples.filter(s => s.event == 'M' && s.type == 'F');
// These are slm/1000, or ml/minute...
// so we multiply by 1000 to get liters per minute
var flow_values = unpack(flows,"val").map(v => v * CONVERT_PIRDS_TO_SLM);
var fmillis = unpack(flows, 'ms');
// Convert to seconds...
var fmin = Math.min(...fmillis);
var fzeroed = fmillis.map(m =>(m-fmin)/1000.0);
var pressures = samples.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
var pmillis = unpack(pressures, 'ms');
var pmin = Math.min(...pmillis);
var pzeroed = pmillis.map(m =>(m-pmin)/1000.0);
// the PIRDS standard is integral mm H2O, so we divide by 10
var delta_p = unpack(pressures, 'val').map(p => p / 10);
var diff_p = {type: "scatter", mode: "lines",
name: "pressure",
x: pzeroed,
y: delta_p,
line: {color: "#FF0000"}
};
var flow = {type: "scatter", mode: "lines",
name: "flow",
x: fzeroed,
y: flow_values,
xaxis: 'x2',
yaxis: 'y2',
fill: 'tozeroy',
line: {color: '#0000FF'}
};
var max_flow = flow_values.reduce(
function(a, b) {
return Math.max(Math.abs(a), Math.abs(b));
}
,0);
var scaled_flow = flow_values.map(f => 100.0 * (f / max_flow));
var flow_hollow = {type: "scatter", mode: "lines",
name: "flow ghost",
x: fzeroed,
// Convert to a percentage
y: scaled_flow,
line: {color: '#8888FF'}
};
return [diff_p,flow,flow_hollow];
}
function plot(samples, trans, breaths) {
var new_data = samplesToLine(samples);
var millis = unpack(samples, 'ms');
var min = Math.min(...millis);
var zeroed = millis.map(m =>(m-min)/1000.0);
{
var layout = {
title: 'VentMon Breath Analysis',
showlegend: false,
xaxis: {domain: [0.0,1.0]},
yaxis: {
title: 'Airway P(cm H2O)',
titlefont: {color: 'red'},
tickfont: {color: 'red'},
},
xaxis2: {domain: [0.0,1.0]},
yaxis2: {
title: 'Flow l/minute',
titlefont: {color: 'blue'},
tickfont: {color: 'blue'}
},
grid: {
rows: 2,
columns: 1,
pattern: 'independent',
roworder: 'top to bottom'}
}
var double_plot = [new_data[0],new_data[1]];
Plotly.newPlot('PFGraph', double_plot, layout);
}
// The Y-axis for the events will be percentage.
// This is somewhat abstract; each trace has
// a different meaning. In general it will be
// % as a function of some known value, which
// will be either a limit or a min or max.
{
var event_graph = [];
if (trans) {
// Transitions are simply scaled to 50%.
var tmillis = unpack(trans, 'ms');
var tzeroed = tmillis.map(m =>(m-min)/1000.0);
var tstates = unpack(trans, 'state');
var tstates_amped = tstates.map(m =>m*50);
var transPlot = {type: "scatter",
mode: "lines+markers",
name: "trans",
x: tzeroed,
y: tstates_amped,
line: {shape: 'hv',
color: 'dkGreen'},
};
// We add a hollow flow line to see in position..
event_graph.push(new_data[2]);
event_graph.push(transPlot);
}
if (breaths) {
var bmillis = unpack(breaths, 'ms');
var bzeroed = bmillis.map(m =>(m-min)/1000.0);
// I'm goint to a add start and end transition to make the plot
// come out right
var ys = breaths.map( b => 0);
var breathPlot = {type: "scatter", mode: "markers",
name: "Transitions",
x: bzeroed,
y: ys,
marker: { size: 8, color: "red",symbol: "diamond" },
textposition: 'bottom center',
text: bzeroed
};
event_graph.push(breathPlot);
// Now I attempt to extract volumes...
// Our breaths mark the END of a breath..
// so we want to draw the volumes that way.
const volume_ht_factor = 20;
var max_exh = unpack(breaths,'vol_e').reduce(
function(a, b) {
return Math.max(Math.abs(a), Math.abs(b));
}
,0);
var max_inh = unpack(breaths,'vol_i').reduce(
function(a, b) {
return Math.max(Math.abs(a), Math.abs(b));
}
,0);
var max_v = Math.max(max_inh,max_exh);
var exh_v = unpack(breaths, 'vol_e').map(e => 100 * e / max_v);
var t_exh_v = unpack(breaths, 'vol_e').map(e => Math.round(e*1000.0)+"ml exh");
var inh_v = unpack(breaths, 'vol_i').map(i => 100 * i / max_v);
var t_inh_v = unpack(breaths, 'vol_i').map(e => Math.round(e*1000.0)+"ml inh");
// now to graph properly, I must find the center of an inhalation.
// I have packed these into the breaths...
var inhale_centers = breaths.map(b => ((trans[b.trans_begin_inhale].ms + trans[b.trans_cross_zero].ms) - 2*min) / (2.0 * 1000.0));
var exhale_centers = breaths.map(b => ((trans[b.trans_cross_zero].ms + trans[b.trans_end_exhale].ms) - 2*min) / (2.0 * 1000.0));
var inhPlot = {type: "scatter", mode: "markers+text",
name: "Inh. ml",
textposition: 'bottom center',
x: inhale_centers,
y: inh_v,
text: t_inh_v,
marker: { size: 8,
color: 'black',
symbol: 'triangle-down'}
};
var exhPlot = {type: "scatter", mode: "markers+text",
name: "Exh. ml",
textposition: 'top center',
marker : {
sizer: 12,
color: 'green',
symbol: 'triangle-up' },
x: exhale_centers,
y: exh_v,
text: t_exh_v,
};
event_graph.push(inhPlot);
event_graph.push(exhPlot);
}
// Now I will attempt to add other markers, such as Humidity, Altitude, and other events,
// including possible warning events.
function gen_graph_measurement(samples, name, color, textposition, tp, lc, vf, tf) {
var selected = samples.filter(s => s.event == 'M' && s.type == tp && s.loc == lc);
return gen_graph(selected, name, color, textposition, vf, tf);
}
function gen_graph(selected,name,color, textposition, vf, tf) {
var millis = unpack(selected, 'ms');
var zeroed = millis.map(m =>(m-min)/1000.0);
var vs = selected.map(vf);
var vs_t = vs.map(tf);
var plot = {type: "scatter", mode: "markers+text",
name: name,
textposition: textposition,
x: zeroed,
y: vs,
text: vs_t,
marker: { size: 10, color: color }
};
return plot;
}
function gen_clock_graph(selected,name,color, textposition) {
var millis = unpack(selected, 'ms');
var zeroed = millis.map(m =>(m-min)/1000.0);
var vs = selected.map( s => -30)
var vs_t = selected.map(v => v.buff);
var plot = {type: "scatter", mode: "markers+text",
name: name,
textposition: textposition,
x: zeroed,
y: vs,
text: vs_t,
marker: { size: 10, color: color }
};
return plot;
}
function gen_message_events(samples,name,color, textposition) {
var messages = samples.filter(s => s.event == 'E' && s.type == 'M');
// Now I want to filter our all flow error messages...
// There are two special ones, "FLOW OUT OF RANGE LOW" and
// "FLOW OUT OF RANGE HIGH"
// Basically, we will show only the first of these in a "run"
// This is rather difficult; we will instead just use 200 ms
// as a window.
const time_window_ms = 200;
var lows = [];
var highs = [];
var others = [];
for(var i = 0; i < messages.length; i++) {
var m = messages[i];
if (m.buff == FLOW_TOO_LOW) {
if ((lows.length == 0) || ((lows.length > 0) && (lows[lows.length-1].ms < (m.ms - time_window_ms))))
lows.push(m);
} else if (m.buff == FLOW_TOO_HIGH) {
if ((highs.length == 0) || ((highs.length > 0) && (highs[highs.length-1].ms < (m.ms - time_window_ms))))
highs.push(m);
} else {
others.push(m);
}
}
if (lows.length > 0 || highs.length > 0) {
set_flow_alert();
} else {
unset_flow_alert();
}
var lowsPlot = gen_graph(lows,"Low Range","blue",'top center',
(s => -90.0),
(v => "LOW"));
var highsPlot = gen_graph(highs,"High Range","red",'bottom center',
(s => 90.0),
(v => "HIGH"));
var othersPlot = gen_graph(others,"messages","Aqua",'top center',
(s => -75.0),
(v => v.buff));
return [lowsPlot,highsPlot,othersPlot];
}
function gen_clock_events(samples,name,color, textposition) {
var messages = samples.filter(s => s.event == 'E' && s.type == 'C');
const time_window_ms = 200;
var clocks = [];
for(var i = 0; i < messages.length; i++) {
var m = messages[i];
if ((clocks.length == 0) || ((clocks.length > 0) && (clocks[clocks.length-1].ms < (m.ms - time_window_ms))))
clocks.push(m);
}
var clocksPlot = gen_clock_graph(clocks,name,color,textposition);
return clocksPlot;
}
{
var fio2AirwayPlot = gen_graph_measurement(samples,"FiO2 (%)","Black",'top center','O','A',
(s => s.val),
(v => "FiO2 (A): "+v.toFixed(1)+"%"));
event_graph.push(fio2AirwayPlot);
}
{
var humAirwayPlot = gen_graph_measurement(samples,"Hum (%)","Aqua",'top center','H','A',
(s => s.val/100.0),
(v => "H2O (A): "+v.toFixed(0)+"%"));
event_graph.push(humAirwayPlot);
}
{
var humAmbientPlot = gen_graph_measurement(samples,"Hum (%)","CornflowerBlue",'bottom center','H','B',
(s => -s.val/100.0),
(v => "H2O (B): "+(-v).toFixed(0)+"%"));
event_graph.push(humAmbientPlot);
}
{
var tempAirwayPlot = gen_graph_measurement(samples,"Temp A","red",'bottom center','T','A',
(s => s.val/100.0),
(v => "T (A): "+v.toFixed(1)+"C"));
event_graph.push(tempAirwayPlot);
}
{
var tempAmbientPlot = gen_graph_measurement(samples,"Temp B","orange",'top center','T','B',
(s => -s.val/100.0),
(v => "T (B): "+(-v).toFixed(1)+"C"));
event_graph.push(tempAmbientPlot);
}
// Altitude is really just a check that the system is working
{
var altAmbientPlot = gen_graph_measurement(samples,"Altitude","purple",'right','A','B',
(s => s.val/20.0),
(v => "Alt: "+(v*20.0).toFixed(0)+"m"));
event_graph.push(altAmbientPlot);
}
{
var [l,h,o] = gen_message_events(samples,"Message","red",'right');
event_graph.push(l);
event_graph.push(h);
event_graph.push(o);
}
{
var cs = gen_clock_events(samples,"Wall Clock","black",'top center');
event_graph.push(cs);
}
// The BME680 sensor detects VOC gases. I don't think has any clinical value
// compared
// {
// var gasAirwayPlot = gen_graph_measurement(samples,"Gas A","yellow",'bottom center','G','A',
// (s => s.val/1000.0),
// (v => "G (A): "+(v*100).toFixed(1)+"Ohms"));
// event_graph.push(gasAirwayPlot);
// }
// {
// var gasAmbientPlot = gen_graph_measurement(samples,"Gas B","gold",'top center','G','B',
// (s => -s.val/1000.0),
// (v => "G (B): "+(-v*100).toFixed(1)+"Ohms"));
// event_graph.push(gasAmbientPlot);
// }
// I'm going to try putting the pressure
// in faintly to make the graphs match
var event_layout = {
title: 'Events',
showlegend: false,
xaxis: {domain: [0.0,1.0]},
yaxis: {
range: [-100.0, 100.0]
},
};
Plotly.newPlot('EventsGraph', event_graph,event_layout);
}
}
function set_flow_alert() {
$("#main_alert").show();
}
function unset_flow_alert() {
$("#main_alert").hide();
}
function check_alarms(limits,key,limit,val,f,ms) {
var alarms = [];
if (limits[key][limit] && (f(val,limits[key][limit]))) {
alarms.push({param: key,
limit: limit,
val: val,
ms: ms});
}
return alarms;
}
// Return, [min,avg,max] pressures (no smoothing)!
function compute_pressures(secs,samples) {
var pressures = samples.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
if (pressures.length == 0) {
return [0,0,0,[]];
} else {
const recent_ms = pressures[pressures.length - 1].ms;
var cur_ms = recent_ms;
var cnt = 0.0;
var i = pressures.length - 1;
var cur_sample = pressures[i];
var min = Number.MAX_VALUE;
var max = Number.MIN_VALUE;
var sum = 0;
var alarms = [];
while((i >=0) && (pressures[i].ms > (cur_ms - secs*1000))) {
var p = pressures[i].val / 10.0; // this is now cm H2O
if (p < min ) {
min = p;
}
if (p > max) {
max = p;
}
sum += p;
cnt++;
alarms = alarms.concat(check_alarms(LIMITS,"max","h",p,(a,b) =>(a > b),pressures[i].ms));
alarms = alarms.concat(check_alarms(LIMITS,"max","l",p,(a,b) =>(a < b),pressures[i].ms));
i--;
}
return [min,sum/cnt,max,alarms];
}
}
function compute_fio2_mean(secs,samples) {
var oxygens = samples.filter(s => s.event == 'M' && s.type == 'O' && s.loc == 'A');
if (oxygens.length == 0) {
return null;
} else {
const recent_ms = oxygens[oxygens.length - 1].ms;
var cur_ms = recent_ms;
var cnt = 0.0;
var i = oxygens.length - 1;
var sum = 0;
var alarms = [];
while((i >=0) && (oxygens[i].ms > (cur_ms - secs*1000))) {
var oxy = oxygens[i].val; // oxygen concentration as a percentage
sum += oxy;
cnt++;
i--;
}
var fio2_avg = sum / cnt;
return fio2_avg;
}
}
function compute_respiration_rate(secs,samples,transitions,breaths) {
// In order to compute the number of breaths
// in the last s seconds, I compute those breaths
// whose time stamp is s seconds from the most recent sample
// We will compute respiration rate by counting breaths
// and dividing (cnt - 1) by time the first and last inhalation
var first_inhale_ms = -1;
var last_inhale_ms = -1;
if (breaths.length == 0) {
return [0,0,0,"NA"];
} else {
const recent_ms = samples[samples.length - 1].ms;
var cur_ms = recent_ms;
var cnt = 0.0;
var vol_i = 0.0;
var vol_e = 0.0;
var i = breaths.length - 1;
var time_inh = 0;
var time_exh = 0;
// fully completed inhalation volume does not include the
// most recent breath; we need it to be able to accurately
// divide by the inhlation_duration.
var vol_ci = 0.0;
while((i >=0) && (breaths[i].ms > (cur_ms - secs*1000))) {
cnt++;
vol_i += breaths[i].vol_i;
if (i < (breaths.length -1)) {
vol_ci += breaths[i].vol_i;
}
vol_e += breaths[i].vol_e;
const inh_ms = transitions[breaths[i].trans_begin_inhale].ms;
// note i is counting down in this loop...
if (last_inhale_ms < 0) last_inhale_ms = inh_ms;
first_inhale_ms = inh_ms;
const exh_ms = transitions[breaths[i].trans_end_exhale].ms;
const zero_ms = transitions[breaths[i].trans_cross_zero].ms;
time_inh += (zero_ms - inh_ms);
time_exh += (exh_ms - zero_ms);
i--;
}
if ((cnt > 1) && (first_inhale_ms != last_inhale_ms)) {
var inhalation_duration = last_inhale_ms - first_inhale_ms;
var inhalation_duration_min = inhalation_duration / (60.0 * 1000.0);
var rr = (cnt - 1) / inhalation_duration_min;
var duration_minutes = secs / 60.0;
// This is liters per minute. vol_ci is in liters.
// inhalation_duration is in ms.
var minute_volume = vol_ci / inhalation_duration_min;
var tidal_volume = 1000.0 * vol_i / cnt;
var EIratio = (time_inh == 0) ? null : time_exh / time_inh;
return [
rr,
tidal_volume,
minute_volume,
EIratio];
} else {
return [0,0,0,"NA"];
}
}
}
var LIMITS = {
max: { h: 40,
l: 0},
avg: { h: 30,
l: 0},
min: { h: 10,
l: -10},
mv: { h: 10,
l: 1},
bpm: { h: 40,
l: 5},
ier: { h: 4,
l: 0.25},
tv: { h: 1000,
l: 200},
fio2: { h: 100,
l: 20},
taip: { h: 100, // These are in ms
l: 0},
trip: { h: 100, // These are in ms
l: 0},
}
function load_ui_with_defaults(limits) {
var Lkeys = Object.keys(limits);
Lkeys.forEach((key,index) => {
["h","l"].forEach(limit => {
$("#"+key+"_"+limit).val(limits[key][limit]);
})
});
}
function set_rise_time_pressures()
{
$("#tarip_l").val(TAIP_AND_TRIP_MIN);
$('#tarip_l').change(function () {
TAIP_AND_TRIP_MIN = $("#tarip_l").val();
});
$("#tarip_h").val(TAIP_AND_TRIP_MAX);
$('#tarip_h').change(function () {
TAIP_AND_TRIP_MAX = $("#tarip_h").val();
});
}
function reflectAlarmsInGUI(alarms) {
var Lkeys = Object.keys(LIMITS);
Lkeys.forEach((key,index) => {
["h","l"].forEach(limit => {
var alarms_for_key = alarms.filter(s => s.param == key && s.limit == limit);
if (alarms_for_key.length > 0) {
$("#"+key).addClass("alarmred");
$("#"+key+"_"+limit).addClass("alarmred");
} else {
$("#"+key).removeClass("alarmred");
$("#"+key+"_"+limit).removeClass("alarmred");
}
})
});
}
function process(samples) {
const t = 200; // size of the window is 200ms
const v = 50; // min volume in ml
var [transitions,breaths] = computeMovingWindowTrace(samples,t,v);
plot(samples,transitions,breaths);
// How many seconds backwards should we look? Perhaps 20?
var [bpm,tv,mv,EIratio,fio2] = compute_respiration_rate(RESPIRATION_RATE_WINDOW_SECONDS,samples,transitions,breaths);
var alarms = [];
$("#bpm").text(bpm.toFixed(1));
$("#tv").text(tv.toFixed(0));
$("#mv").text((mv).toFixed(2));
if (EIratio == "NA") {
$("#ier").text("NA");
} else {
$("#ier").text((1.0 / EIratio).toFixed(1));
}
var final_ms = samples[samples.length -1].ms;
var b_ms = 0;
if (breaths.length > 0) {
b_ms = breaths[breaths.length -1].ms;
} else {
b_ms = final_ms;
}
function check_high_and_low(limits,key,v,ms) {
var al1 = check_alarms(limits,key,"h",v,(a,b) =>(a > b),ms);
var al2 = check_alarms(limits,key,"l",v,(a,b) =>(a < b),ms);
return al1.concat(al2);
}
alarms = alarms.concat(check_high_and_low(LIMITS,"bpm",bpm.toFixed(1),b_ms));
alarms = alarms.concat(check_high_and_low(LIMITS,"tv",tv.toFixed(0),b_ms));
alarms = alarms.concat(check_high_and_low(LIMITS,"mv",(mv).toFixed(2),b_ms));
alarms = alarms.concat(check_high_and_low(LIMITS,"ier",(1.0 / EIratio).toFixed(1),b_ms));
// These must be moved out; in fact,
// they must be made configurable!
var taip = compute_mean_TRIP_or_TAIP(
TAIP_AND_TRIP_MIN,
TAIP_AND_TRIP_MAX,
samples,
true);
if (taip != "NA") {
$("#taip").text(taip.toFixed(1));
alarms = alarms.concat(
check_high_and_low(LIMITS,"taip",taip,b_ms));
} else {
$("#taip").text("NA");
}
var trip = compute_mean_TRIP_or_TAIP(
TAIP_AND_TRIP_MIN,
TAIP_AND_TRIP_MAX,
samples,
false);
if (trip != "NA") {
$("#trip").text(trip.toFixed(1));
alarms = alarms.concat(
check_high_and_low(LIMITS,"trip",trip,b_ms));
} else {
$("#trip").text("NA");
}
var [min,avg,max,palarms] = compute_pressures(RESPIRATION_RATE_WINDOW_SECONDS,samples);
alarms = alarms.concat(palarms);
$("#min").text(min.toFixed(1));
$("#avg").text(avg.toFixed(1));
$("#max").text(max.toFixed(1));
var fio2 = compute_fio2_mean(RESPIRATION_RATE_WINDOW_SECONDS,samples);
if(fio2 == null){
$("#fio2").text("NA");
} else {
alarms = alarms.concat(check_high_and_low(LIMITS,"fio2",fio2.toFixed(1),b_ms));
$("#fio2").text(fio2.toFixed(1));
}
reflectAlarmsInGUI(alarms);
}
// WARNING: This is a hack...if the timestamp is negative,
// we treat it as a limited (beyond range of sensor) measurement.
// Our goal is to warn about this, but for now we will just
// ignore and correct.
function sanitize_samples(samples) {
samples.forEach(s =>
{
if (s.event == "M") {
if ("string" == (typeof s.ms))
s.ms = parseInt(s.ms);
if ("string" == (typeof s.val))
s.val = parseInt(s.val);
if ("string" == (typeof s.num))
s.num = parseInt(s.num);
if (s.ms < 0) {
s.ms = -s.ms;
} else if (s.event == "E") {
}
}
});
return samples;
}
function retrieveAndPlot(){
var trace_piece = (TRACE_ID) ? "/" + TRACE_ID : "";
DSERVER_URL = $("#dserverurl").val();
var url = DSERVER_URL + trace_piece + "/json?n="+ NUM_TO_READ;
$.ajax({url: url,
success: function(cur_sam){
// WARNING: This is a hack...if the timestamp is negative,
// we treat it as a limited (beyond range of sensor) measurement.
// Our goal is to warn about this, but for now we will just
// ignore and correct.
if (cur_sam && cur_sam.length == 0) {
console.log("no samples; potential misconfiguration!");
} else {
cur_sam = sanitize_samples(cur_sam);
if (INITS_ONLY) {
samples = cur_sam;
INITS_ONLY = false;
} else {
var discard = Math.max(0,
samples.length + cur_sam.length - MAX_SAMPLES_TO_STORE_S);
samples = samples.slice(discard);
}
samples = samples.concat(cur_sam);
// We are not guaranteeed to get samples in order
// we sort them....
samples = samples.sort((a,b) => a.ms - b.ms);
process(samples);
}
},
error: function(xhr, ajaxOptions, thrownError) {
console.log("Error!" + xhr.status);
console.log(thrownError);
// clearInterval(intervalID);
stop_interval_timer();
$("#livetoggle").prop("checked",false);
}
});
}
function compute_transitions(vm,flows) {
var transitions = [];
var state = 0; // Let 1 mean inspiration, -1 mean expiration, 0 neither
for(var i = 0; i < flows.length; i++) {
var f = flows[i].val * CONVERT_PIRDS_TO_SLM;
// var ms = flows[i].ms-first_time;
var ms = flows[i].ms;
if (state == 0) {
if (f > vm) {
state = 1;
transitions.push({ state: 1, sample: i, ms: ms})
} else if (f < -vm) {
state = -1;
transitions.push({ state: -1, sample: i, ms: ms})
}
} else if (state == 1) {
if (f < -vm) {
state = -1;
transitions.push({ state: -1, sample: i, ms: ms})
} else if (f < vm) {
state = 0;
transitions.push({ state: 0, sample: i, ms: ms})
}
} else if (state == -1) {
if (f > vm) {
state = 1;
transitions.push({ state: 1, sample: i, ms: ms})
} else if (f > 0) {
state = 0;
transitions.push({ state: 0, sample: i, ms: ms})
}
}
}
return transitions;
}
// produces a set of rising signals, time in ms of the leading edge of the rise and the trailing edge of the rise
// an array of 2-tuple
// taip == true implies compute TAIP, else compute TRIP
// Possibly this routine should be generalized to a general rise-time routine.
function compute_TAIP_or_TRIP_signals(min,max,pressures,taip) {
var pressures = pressures.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
const responseBegin = 0.1;
const responseEnd = 0.9;
var signals = [];
var foundMinSignal = false;
const highFence = (min + (responseEnd * (max - min)))*10;
const lowFence = (min + (responseBegin * (max - min)))*10;
var cur_signal_start;
var state = -1; // Let 1 mean rising, -1 mean fallen, 0 risen, but not fallen
var first_sample_index = taip ? 0 : pressures.length-1 ;
var last_sample_index = taip ? pressures.length-1 : 0;
var increment = taip ? 1 : -1;
for(var i = first_sample_index; i != last_sample_index; i+=increment) {
var p = pressures[i].val;
var ms = pressures[i].ms;
if (state == -1) {
if (p >= lowFence) {
state = 1;
cur_signal_start = pressures[i];
}
if (p >= highFence){
signals.push([ms,ms]);
}
} else if (state == 1) {
//console.log("state = 1",cur_signal_start); for debugging
if (p >= highFence) {
signals.push(taip ? [cur_signal_start.ms,ms] : [ms,cur_signal_start.ms])
state = 0;
} else if (p <= lowFence) {
state = -1;
cur_signal_start = null;
}
} else if (state == 0) {
if (p <= lowFence) {
state = -1;
}
}
}
return signals;
}
function compute_mean_TRIP_or_TAIP_sigs(sigs,min,max,pressures,taip){
if (sigs.length == 0){
return "NA";
} else {
var sum = 0;
for(var i = 0; i < sigs.length; i++) {
sum += sigs[i][1] - sigs[i][0]; // time in ms
}
return sum / sigs.length;
}
}
function testdata(){
//0 (not good enough), 10 (rising), 20 (above threshold) all 10 ms apart
//saw tooth function
var data = []; // pushing 50 things into it
for(var i = 0; i < 10; i++) {
var ms = i*5*10;
data[i*5+0] = {event:'M',loc:'A',ms:ms + 0,type:'D',val: 0};
data[i*5+1] = {event:'M',loc:'A',ms:ms + 10,type:'D',val: 100};
data[i*5+2] = {event:'M',loc:'A',ms:ms + 20,type:'D',val: 200};
data[i*5+3] = {event:'M',loc:'A',ms:ms + 30,type:'D',val: 100};
data[i*5+4] = {event:'M',loc:'A',ms:ms + 40,type:'D',val: 0};
}
return data;
}
function testdataSine(period_sm){ // period expressed in # of samples, each sample 10 ms
//0 (not good enough), 10 (rising), 20 (above threshold) all 10 ms apart
//sine tooth function
var data = []; // pushing 50 things into it
for(var i = 0; i < 1000; i++) {
var ms = i*10;
data[i] = {event:'M',loc:'A',ms:ms + 20,type:'D',val: 200*Math.sin(2*Math.PI*i/period_sm)};
}
return data;
}
function compute_mean_TRIP_or_TAIP(min,max,samples,taip) {
return compute_mean_TRIP_or_TAIP_sigs(
compute_TAIP_or_TRIP_signals(min,max,samples,taip),
min,max,samples,taip);
}
function test_compute_TAIP() {
var samples = testdata();
const TAIP_min = 0; // cm of H2O
const TAIP_max = 20; // cm of H2O
var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,true);
console.assert(TAIP_m == 10);
for (i = 50; i<150; i+=10) {
var sinewave = testdataSine(i);
var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,sinewave,true);
}
}
function compute_current_TAIP(TAIP_min,TAIP_max){ //uses samples from a global var
var TAIP_m = compute_mean_TRIP_or_TAIP(TAIP_min,TAIP_max,samples,true);
return TAIP_m;
}
// Because TAIP and TRIP are symmetric when viewed from
// from the direction of the samples; this tests that
// as a prelude to computing a single way.
function reverseArray(arr) {
var newArray = [];
for (var i = arr.length - 1; i >= 0; i--) {
newArray.push(arr[i]);
}
return newArray;
}
function test_TRIP_and_TAIP_are_symmetric() {
var samples = testdata();
var rsamples = reverseArray(samples);
const min = 0;
const max = 20;
var TRIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,false);
var TRIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,true);
console.assert(TRIP_m == TRIP_m_r);
console.assert(TRIP_m == 10);
for (i = 50; i<150; i+=10) {
var sinewave = testdataSine(i);
var rsinewave = reverseArray(sinewave);
var TRIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,false);
var TRIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,true);
console.assert(TRIP_m == TRIP_m_r);
var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,true);
var TAIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,false);
console.assert(TAIP_m == TAIP_m_r);
}
}
function compute_current_TRIP(TRIP_min,TRIP_max, samples)
{ //uses samples from a global var
if (samples.length == 0){
return "NA";
}
else {
var TRIP_m = compute_mean_TAIP_or_TRIP(TRIP_min,TRIP_max,false);
return TRIP_m;
}
}
// A simple computation of a moving window trace
// computing [A + -B], where A is volume to left
// of sample int time window t, and B is volume to right
// t is in milliseconds
function computeMovingWindowTrace(samples,t,v) {
var flows = samples.filter(s => s.event == 'M' && s.type == 'F');
var first_time = flows[0].ms;
var last_time = flows[flows.length - 1].ms;
var duration = last_time - first_time;
// Here is an idea...
// We define you to be in one of three states:
// Inspiring, expiring, or neither.
// Every transition between these states is logged.
// Having two inspirations between an expiration is
// weird but could happen.
// We record transitions.
// When the time series crossed a fixed threshold
// or zero, it causes a transition. If you are inspiring,
// you have to cross zero to transition to neither,
// and you start expiring when you cross the treshold.
// This is measured in standard liters per minute.
const vm = 10; // previously used 4
// We will model this as a list of transitions.
// A breath is any number of inspirations followed by
// any number of expirations. (I+)(E+)
var transitions = compute_transitions(vm,flows);
// Now that we have transitions, we can apply a
// diferrent algorithm to try to define "breaths".
// Because a breath is defined as an inspiration
// and then an expiration, we will define a breath
// as from the first inspiration, until there has
// been one expiration, until the next inspiration.
var breaths = [];
var expiring = false;
// This should be in liters...
function integrateSamples(a,z) {
// -1 for quadilateral approximation
var vol = 0;
for(var j = a; j < z-1; j++) {
// I'll use qadrilateral approximation.
// We'll form each quadrilateral between two samples.
var ms = flows[j+1].ms - flows[j].ms;
var ht = ((flows[j+1].val + flows[j].val )/2) * CONVERT_PIRDS_TO_SLM;
// Flow is actually in standard liters per minute,
// so to get liters we divide by 60 to it l/s,
// and and divde by 1000 to convert ms to seconds.
// We could do that here, but will move constants
// to end...
vol += ms * ht;
if (isNaN(vol)) {
debugger;
}
}
return vol/(60*1000);
}
function compute_breaths_based_on_exhalations(transitions) {
var beg = 0;
var zero = 0;
var last = 0;
var voli = 0;
var vole = 0;
for(var i = 0; i < transitions.length; i++) {
// We're looking for the end of the inhalation here!!
if (((i -1) >= 0) && transitions[i-1].state == 1 && (transitions[i].state == 0 || transitions[i].state == -1 )) {
zero = i;
}
if (expiring && transitions[i].state == 1) {
breaths.push({ ms: transitions[i].ms,
sample: transitions[i].sample,
vol_e: vole,
vol_i: voli,
trans_begin_inhale: beg,
trans_cross_zero: zero,
trans_end_exhale: i,
}
);
beg = i;
expiring = false;
vole = integrateSamples(last,transitions[i].sample);
last = transitions[i].sample;
}
if (!expiring && transitions[i].state == -1) {
expiring = true;
voli = integrateSamples(last,transitions[i].sample);
last = transitions[i].sample;
}
}
}
// This is based only on inhalations, and
// is therefore functional when there is a check valve
// in place. Such a system will rarely
// have negative flows, and we must mark
// the beginning of a breath from a transition to a "1"
// state from any other state.
// This algorithm is simple: A breath begins on a trasition
// to 1 from a not 1 state. This algorithm is susceptible
// to "stutter" near the boundary point, but if necessary
// a digital filter would sove that; we have not yet found
// that level of sophistication needed.
// We still want to track zeros, but now must strack them
// as a falling signal.
function compute_breaths_based_without_negative_flow(transitions) {
var beg = 0;
var zero = 0;
var last = 0;
var voli = 0;
var vole = 0;
var breaths = [];
var expiring = true;
for(var i = 0; i < transitions.length; i++) {
// We're looking for the end of the inhalation here!!
if (((i -1) >= 0) && transitions[i-1].state == 1 &&
(transitions[i].state == 0 || transitions[i].state == -1 )) {
zero = i;
}
if (expiring && transitions[i].state == 1) {
breaths.push({ ms: transitions[i].ms,
sample: transitions[i].sample,
vol_e: vole,
vol_i: voli,
trans_begin_inhale: beg,
trans_cross_zero: zero,
trans_end_exhale: i,
}
);
beg = i;
expiring = false;
vole = integrateSamples(last,transitions[i].sample);
last = transitions[i].sample;
}
if (!expiring && ((transitions[i].state == -1) || (transitions[i].state == 0))) {
expiring = true;
voli = integrateSamples(last,transitions[i].sample);
last = transitions[i].sample;
}
}
return breaths;
}
breaths = compute_breaths_based_without_negative_flow(transitions);
return [transitions,breaths];
}
$("#useofficial").click(function() {
DSERVER_URL = VENTMON_DATA_LAKE;
$("#dserverurl").val(DSERVER_URL);
});
$("#import").click(function() {
var input_trace = $("#json_trace").val();
samples = JSON.parse(input_trace);
});
$("#export").click(function() {
$("#json_trace").val(JSON.stringify(samples,null,2));
});
// Experimental timing against the data server
function stop_interval_timer() {
clearInterval(intervalID);
intervalID = null;
$("#livetoggle").prop("checked",false);
}
function start_interval_timer() {
if (intervalID) {
stop_interval_timer();
}
intervalID = setInterval(
function() {
retrieveAndPlot();
},
DATA_RETRIEVAL_PERIOD);
$("#livetoggle").prop("checked",true);
}
function toggle_interval_timer() {
if (intervalID) {
stop_interval_timer();
} else {
start_interval_timer();
}
}
function setLimit(ed) {
if(ed.which == 13) {
var id = ed.target.id;
var v = $("#"+id).val();
var vf = parseFloat(v);
var destruct = id.split("_");
LIMITS[destruct[0]][destruct[1]] = isNaN(vf) ? null : vf;
}
}
$(".fence").keypress(setLimit);
$("#livetoggle").change(toggle_interval_timer);
$("#startoperation").click(start_interval_timer);
$("#stopoperation").click(stop_interval_timer);
$( document ).ready(function() {
if (window.location.protocol == "http:")
DSERVER_URL = window.location.protocol + "//" + window.location.host;
else
DSERVER_URL = "http://localhost";
$( "#dserverurl" ).val( DSERVER_URL );
$('#dserverurl').change(function () {
DSERVER_URL = $("#dserverurl").val();
start_interval_timer();
});
// $( "#num_to_read" ).val( NUM_TO_READ );
// $('#num_to_read').change(function () {
// NUM_TO_READ = $("#num_to_read").val();
// });
$( "#traceid" ).val( TRACE_ID );
$('#traceid').change(function () {
TRACE_ID = $("#traceid").val();
start_interval_timer();
});
$( "#samples_to_plot" ).val( MAX_SAMPLES_TO_STORE_S );
$('#samples_to_plot').change(function () {
MAX_SAMPLES_TO_STORE_S = $("#samples_to_plot").val();
});
samples = init_samples();
process(samples);
start_interval_timer();
load_ui_with_defaults(LIMITS);
set_rise_time_pressures(
TAIP_AND_TRIP_MIN,
TAIP_AND_TRIP_MAX,
);
});
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment