diff --git a/breath_plot.html b/breath_plot.html new file mode 100644 index 0000000000000000000000000000000000000000..11aec55358c8183fe6ffe37e2a7c78d2f1034a41 --- /dev/null +++ b/breath_plot.html @@ -0,0 +1,1703 @@ + <!-- +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'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>