diff --git a/ansible/README.md b/ansible/README.md index 386e7bf7fdf256daafa8683f1f0419774fbab5c2..288aee344794b32017a25a6c0a6d48d854bc828d 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -6,7 +6,8 @@ - Boot as default and set your preferred options for country, password, etc. > From command line on your local PC (not the pi): -- copy your ssh keys if you don't want a password every time +- generate and copy your ssh keys if you don't want a password every time + - ssh-keygen - ssh-copy-id pi@MY-RPI-IPADDRESS - then setup ansible: - install ansible on your local PC with your package manager @@ -18,7 +19,7 @@ cd hev-sw/ansible source hev-ansible.sh cd playbooks ``` -- add the address of your Raspberry Pi to the `hosts` file under the section `[hevpi]` +- add the address of your Raspberry Pi to the `hosts` file under the section `[hevpi]`. The default host file is found at /etc/ansible/hosts - example : ``` [hevpi] diff --git a/raspberry-backend/static/js/Chart-display.js b/raspberry-backend/static/js/Chart-display.js index 3fbc5f4cd4cc9ffb83b352921b1a36b3c0caff39..9b7a865d9288a7b3bbce4288936f3e37f17d0165 100644 --- a/raspberry-backend/static/js/Chart-display.js +++ b/raspberry-backend/static/js/Chart-display.js @@ -8,6 +8,8 @@ var initial_yaxis_pressure = []; var initial_yaxis_volume = []; var initial_yaxis_flow = []; +var fio_reading; + /** * Request data from the server, add it to the graph and set a timeout * to request again @@ -31,9 +33,8 @@ function init_results(){ url: '/last_N_data', success: function(data) { for (i=0; i<data.length; i++) { - var seconds = data[i]["timestamp"]; + var seconds = data[i]["timestamp"]/1000; if ( seconds == "" ) continue; - console.log(seconds); initial_yaxis_pressure.push({x : seconds, y : data[i]["pressure_buffer"]}); initial_yaxis_volume.push({x : seconds, y : data[i]["pressure_inhale"]}); initial_yaxis_flow.push({x : seconds, y : data[i]["temperature_buffer"]}); @@ -49,7 +50,7 @@ function init_results(){ initial_yaxis_pressure[i][0] = initial_yaxis_pressure[i][0] - current_timestamp; initial_yaxis_volume[i][0] = initial_yaxis_volume[i][0] - current_timestamp; initial_yaxis_flow[i][0] = initial_yaxis_flow[i][0] - current_timestamp; - } + } }, cache: false @@ -94,7 +95,11 @@ function requestChartVar() { $.ajax({ url: '/live-data', success: function(point) { - var seconds = point["timestamp"]; + fio_reading = (point["pressure_buffer"]).toFixed(0) ; + //console.log(fio_reading); + fio_gauge.data.datasets[0].gaugeData['value'] = fio_reading; + + var seconds = point["timestamp"]/1000; // get difference between last time stamp and this and apply to existing points var diff = 0; if ( current_timestamp == -1 ){ @@ -129,6 +134,8 @@ function requestChartVar() { chart_pressure.update(); chart_flow.update(); chart_volume.update(); + fio_gauge.update(); + }, cache: false }); @@ -353,4 +360,22 @@ $(document).ready(function() { }); +var ctx = document.getElementById("example_gauge").getContext("2d"); +fio_gauge = new Chart(ctx, { + type: "tsgauge", + data: { + datasets: [{ + backgroundColor: ["#0fdc63", "#fd9704", "#ff7143"], + borderWidth: 0, + gaugeData: { + value: 0, + valueColor: "#ff7143" + }, + gaugeLimits: [0, 50, 100] + }] + }, + options: { + events: [] + } +}); diff --git a/raspberry-backend/static/js/Chart-displayLoop.js b/raspberry-backend/static/js/Chart-displayLoop.js index db4298b9cefe16e918915ad5fa095f6e5bd12f3d..b9251278c9363485b412f60b551d902979a99156 100644 --- a/raspberry-backend/static/js/Chart-displayLoop.js +++ b/raspberry-backend/static/js/Chart-displayLoop.js @@ -13,34 +13,48 @@ function requestChartVar() { success: function(point) { if( chart_PV.data.datasets[1].data.length > 100 ){ chart_PV.data.datasets[1].data.shift(); - chart_FV.data.datasets[1].data.shift(); + chart_VF.data.datasets[1].data.shift(); chart_PF.data.datasets[1].data.shift(); } // point labelled as: (stealing definitions from Chart-display.js) // Pressure = pressure_buffer // Flow = pressure_inhale ?? // Volume = temperature_buffer !!? - chart_PV.data.datasets[1].data.push({x: point["temperature_buffer"], - y: point["pressure_buffer"]}); - chart_FV.data.datasets[1].data.push({x: point["temperature_buffer"], - y: point["pressure_inhale"]}); - chart_PF.data.datasets[1].data.push({x: point["pressure_inhale"], - y: point["pressure_buffer"]}); - chart_PV.data.datasets[0].data = - [{x: point["temperature_buffer"],y: point["pressure_buffer"]}]; + var pressure = point["pressure_buffer"]; + var volume = point["temperature_buffer"]; + var flow = point["pressure_inhale"]; - chart_FV.data.datasets[0].data = - [{x: point["temperature_buffer"], y: point["pressure_inhale"]}]; + // to quote the AAMI: + //For Pressure-Volume loops, the graph + //is required to use delivered volume on the vertical axis + //and airway pressure on the horizontal axis. Positive + //values should increase in up/right directions. Every + //breath resets the graph, setting the volume back at the + //origin. - chart_PF.data.datasets[0].data = - [{x: point["pressure_inhale"], y: point["pressure_buffer"]}]; + //For Flow-Volume loops the graph is required to use flow + //rate on the vertical axis and delivered volume on the + //horizontal axis. Positive values should increase in the + //up/right directions. Every breath resets the graph, + //setting the volume back at the origin. The document also + //suggests that there could be another version using + //exhalation flow, if possible. + // loops: + chart_PV.data.datasets[1].data.push({x: pressure, y: volume}); + chart_VF.data.datasets[1].data.push({x: volume, y: flow}); + chart_PF.data.datasets[1].data.push({x: pressure, y: flow}); + + // current data: + chart_PV.data.datasets[0].data = [{x: pressure, y: volume}]; + chart_VF.data.datasets[0].data = [{x: volume, y: flow}]; + chart_PF.data.datasets[0].data = [{x: pressure, y: flow}]; + + // now run chart updates chart_PV.update(); - chart_FV.update(); + chart_VF.update(); chart_PF.update(); - - console.info("chart_PV.data", chart_PV.data); - }, + }, cache: false }); setTimeout(requestChartVar, 200); @@ -53,55 +67,43 @@ $(document).ready(function() { chart_PV = new Chart(ctx_PV, { type: 'scatter', data: {datasets: [{data: [], - label: "Pressure:Volme (current)", + label: "Pressure (x) [mbar]: Volume (y) [ml] (current)", borderColor: "rgb(128,0,0)", pointBackgroundColor : "rgb(128,0,0)", fill: true}, {data: [], - label: "Loop Pressure:Volme (last 20s)", + label: "Loop (20s)", borderColor: "rgb(51,99,255)", pointBackgroundColor : "rgb(51,99,255)", fill: false }, ]}, options: {elements: { point: { radius: 5}}, - scales: {xAxes: [{type: 'linear', - position: 'bottom', - lablelString: 'temperature_buffer??', - display: true, - ticks: {min: -1000, max: 1500, - stepSize: 500, fontSize:25 }}], - yAxes: [{type: 'linear', - position: 'left', - lablelString: 'pressure_buffer??', - display: true, + scales: {xAxes: [{display: true, ticks: {min: 0, max: 25, - stepSize: 5, fontSize: 25 }}]}} + stepSize: 5, fontSize: 25 }}], + yAxes: [{display: true, + ticks: {min: -1000, max: 1500, + stepSize: 500, fontSize:25 }}]}} }); - var ctx_FV = document.getElementById('flow_volume_chart'); - chart_FV = new Chart(ctx_FV, { + var ctx_VF = document.getElementById('flow_volume_chart'); + chart_VF = new Chart(ctx_VF, { type: 'scatter', data: {datasets: [{data: [], - label: "Flow:Volme (current)", + label: "Volume (x) [ml]: Flow (y) [ml/min] (current)", borderColor: "rgb(128,0,0)", pointBackgroundColor : "rgb(128,0,0)", fill: true }, {data: [], - label: "Loop Flow:Volme (last 20s)", + label: "Loop (20s)", borderColor: "rgb(51,99,255)", pointBackgroundColor : "rgb(51,99,255)", fill: false }]} ,options: {elements: { point: { radius: 5}}, - scales: {xAxes: [{type: 'linear', - position: 'bottom', - lablelString: 'temperature_buffer??', - display: true, - ticks: {min: -1000, max: 1500, - stepSize: 500, fontSize: 25 }}], - yAxes: [{type: 'linear', - position: 'left', - lablelString: 'pressure_inhale??', - display: true, + scales: {xAxes: [{display: true, + ticks: {min: -1000, max: 1500, + stepSize: 500, fontSize: 25 }}], + yAxes: [{display: true, ticks: {min: 0, max: 300, stepSize: 100, fontSize: 25 }}]}} }); @@ -110,28 +112,22 @@ $(document).ready(function() { chart_PF = new Chart(ctx_PF, { type: 'scatter', data: {datasets: [{data: [], - label: "Pressure:Flow (current)", + label: "Pressure (x) [mbar]: Flow (y) [ml/min] (current)", borderColor: "rgb(128,0,0)", pointBackgroundColor : "rgb(128,0,0)", fill: true }, {data: [], - label: "Loop Pressure:Flow (last 20s)", + label: "Loop (20s)", borderColor: "rgb(51,99,255)", pointBackgroundColor : "rgb(51,99,255)", fill: false }]} ,options: {elements: { point: { radius: 5, fill: true}}, - scales: {xAxes: [{type: 'linear', - position: 'bottom', - lablelString: 'pressure_inhale??', - display: true, + scales: {xAxes: [{display: true, + ticks: {min: 0, max: 25 , + stepSize: 5 , fontSize: 25 }}], + yAxes: [{display: true, ticks: {min: 0, max: 300, - stepSize: 100, fontSize: 25 }}], - yAxes: [{type: 'linear', - position: 'left', - lablelString: 'pressure_buffer??', - display: true, - ticks: {min: 0, max: 25, - stepSize: 5, fontSize: 25 }}]}} + stepSize: 100, fontSize: 25 }}]}} }); }); diff --git a/raspberry-backend/static/js/Gauge.js b/raspberry-backend/static/js/Gauge.js new file mode 100644 index 0000000000000000000000000000000000000000..0994eeaae1c397bda77687a8efd678fd81a74900 --- /dev/null +++ b/raspberry-backend/static/js/Gauge.js @@ -0,0 +1,217 @@ +(function () { + if (!window.Chart) { + return; + } + function GaugeChartHelper() { + } + GaugeChartHelper.prototype.setup = function(chart, config) { + this.chart = chart; + this.ctx = chart.ctx; + this.limits = config.data.datasets[0].gaugeLimits; + this.data = config.data.datasets[0].gaugeData; + var options = chart.options; + this.fontSize = options.defaultFontSize; + this.fontStyle = options.defaultFontFamily; + this.fontColor = options.defaultFontColor; + this.ctx.textBaseline = "alphabetic"; + this.arrowAngle = 25 * Math.PI / 180; + this.arrowColor = config.options.indicatorColor || options.arrowColor; + this.showMarkers = typeof(config.options.showMarkers) === 'undefined' ? true : config.options.showMarkers; + if (config.options.markerFormatFn) { + this.markerFormatFn = config.options.markerFormatFn; + } else { + this.markerFormatFn = function(value) { + return value; + } + } + }; + GaugeChartHelper.prototype.applyGaugeConfig = function(chartConfig) { + this.calcLimits(); + chartConfig.data.datasets[0].data = this.doughnutData; + var ctx = this.ctx; + var labelsWidth = this.limits.map(function(label){ + var text = this.markerFormatFn(label); + return ctx.measureText(text).width; + }.bind(this)); + var padding = Math.max.apply(this, labelsWidth) + this.chart.width / 35; + var heightRatio = this.chart.height / 50; + chartConfig.options.layout.padding = { + top: this.fontSize + heightRatio, + left: padding, + right: padding, + bottom: heightRatio * 2 + }; + }; + GaugeChartHelper.prototype.calcLimits = function() { + var limits = this.limits; + var data = []; + var total = 0; + for (var i = 1, ln = limits.length; i < ln; i++) { + var dataValue = Math.abs(limits[i] - limits[i - 1]); + total += dataValue; + data.push(dataValue); + } + this.doughnutData = data; + var minValue = limits[0]; + var maxValue = limits[limits.length - 1]; + this.isRevers = minValue > maxValue; + this.minValue = this.isRevers ? maxValue : minValue; + this.totalValue = total; + }; + GaugeChartHelper.prototype.updateGaugeDimensions = function() { + var chartArea = this.chart.chartArea; + this.gaugeRadius = this.chart.innerRadius; + this.gaugeCenterX = (chartArea.left + chartArea.right) / 2; + this.gaugeCenterY = (chartArea.top + chartArea.bottom + this.chart.outerRadius) / 2; + this.arrowLength = this.chart.radiusLength * 2; + }; + GaugeChartHelper.prototype.getCoordOnCircle = function(r, alpha) { + return { + x: r * Math.cos(alpha), + y: r * Math.sin(alpha) + }; + }; + GaugeChartHelper.prototype.getAngleOfValue = function(value) { + var result = 0; + var gaugeValue = value - this.minValue; + if (gaugeValue <= 0) { + result = 0; + } else if (gaugeValue >= this.totalValue) { + result = Math.PI; + } else { + result = Math.PI * gaugeValue / this.totalValue; + } + if (this.isRevers) { + return Math.PI - result; + } else { + return result; + } + }; + GaugeChartHelper.prototype.renderLimitLabel = function(value) { + var ctx = this.ctx; + var angle = this.getAngleOfValue(value); + var coord = this.getCoordOnCircle(this.chart.outerRadius + (this.chart.radiusLength / 2), angle); + var align; + var diff = angle - (Math.PI / 2); + if (diff > 0) { + align = "left"; + } else if (diff < 0) { + align = "right"; + } else { + align = "center"; + } + ctx.textAlign = align; + ctx.font = this.fontSize + "px " + this.fontStyle; + ctx.fillStyle = this.fontColor; + var text = this.markerFormatFn(value); + ctx.fillText(text, this.gaugeCenterX - coord.x, this.gaugeCenterY - coord.y); + }; + GaugeChartHelper.prototype.renderLimits = function() { + for (var i = 0, ln = this.limits.length; i < ln; i++) { + this.renderLimitLabel(this.limits[i]); + } + }; + GaugeChartHelper.prototype.renderValueLabel = function() { + var label = this.data.value.toString(); + var ctx = this.ctx; + ctx.font = "30px " + this.fontStyle; + var stringWidth = ctx.measureText(label).width; + var elementWidth = 0.75 * this.gaugeRadius * 2; + var widthRatio = elementWidth / stringWidth; + var newFontSize = Math.floor(30 * widthRatio); + var fontSizeToUse = Math.min(newFontSize, this.gaugeRadius); + ctx.textAlign = "center"; + ctx.font = fontSizeToUse + "px " + this.fontStyle; + ctx.fillStyle = this.data.valueColor || this.fontColor; + ctx.fillText(label, this.gaugeCenterX, this.gaugeCenterY); + }; + GaugeChartHelper.prototype.renderValueArrow = function(value) { + var angle = this.getAngleOfValue(typeof value === "number" ? value : this.data.value); + this.ctx.globalCompositeOperation = "source-over"; + this.renderArrow(this.gaugeRadius, angle, this.arrowLength, this.arrowAngle, this.arrowColor); + }; + GaugeChartHelper.prototype.renderSmallValueArrow = function(value) { + var angle = this.getAngleOfValue(value); + this.ctx.globalCompositeOperation = "source-over"; + this.renderArrow(this.gaugeRadius - 1, angle, this.arrowLength - 1, this.arrowAngle, this.arrowColor); + }; + GaugeChartHelper.prototype.clearValueArrow = function(value) { + var angle = this.getAngleOfValue(value); + this.ctx.lineWidth = 2; + this.ctx.globalCompositeOperation = "destination-out"; + this.renderArrow(this.gaugeRadius - 1, angle, this.arrowLength + 1, this.arrowAngle, "#FFFFFF"); + this.ctx.stroke(); + }; + GaugeChartHelper.prototype.renderArrow = function(radius, angle, arrowLength, arrowAngle, arrowColor) { + var coord = this.getCoordOnCircle(radius, angle); + var arrowPoint = { + x: this.gaugeCenterX - coord.x, + y: this.gaugeCenterY - coord.y + }; + var ctx = this.ctx; + ctx.fillStyle = arrowColor; + ctx.beginPath(); + ctx.moveTo(arrowPoint.x, arrowPoint.y); + coord = this.getCoordOnCircle(arrowLength, angle + arrowAngle); + ctx.lineTo(arrowPoint.x + coord.x, arrowPoint.y + coord.y); + coord = this.getCoordOnCircle(arrowLength, angle - arrowAngle); + ctx.lineTo(arrowPoint.x + coord.x, arrowPoint.y + coord.y); + ctx.closePath(); + ctx.fill(); + }; + GaugeChartHelper.prototype.animateArrow = function() { + var stepCount = 30; + var animateTimeout = 300; + var gaugeValue = this.data.value - this.minValue; + var step = gaugeValue / stepCount; + var i = 0; + var currentValue = this.minValue; + var interval = setInterval(function() { + i++; + this.clearValueArrow(currentValue); + if (i > stepCount) { + clearInterval(interval); + this.renderValueArrow(); + } else { + currentValue += step; + this.renderSmallValueArrow(currentValue); + } + }.bind(this), animateTimeout / stepCount); + }; + Chart.defaults.tsgauge = { + animation: { + animateRotate: true, + animateScale: false + }, + cutoutPercentage: 95, + rotation: Math.PI, + circumference: Math.PI, + legend: { + display: false + }, + scales: {}, + arrowColor: "#444" + }; + Chart.controllers.tsgauge = Chart.controllers.doughnut.extend({ + initialize: function(chart) { + var gaugeHelper = this.gaugeHelper = new GaugeChartHelper(); + gaugeHelper.setup(chart, chart.config); + gaugeHelper.applyGaugeConfig(chart.config); + chart.config.options.animation.onComplete = function(chartElement) { + gaugeHelper.updateGaugeDimensions(); + gaugeHelper.animateArrow(); + }; + Chart.controllers.doughnut.prototype.initialize.apply(this, arguments); + }, + draw: function() { + Chart.controllers.doughnut.prototype.draw.apply(this, arguments); + var gaugeHelper = this.gaugeHelper; + gaugeHelper.updateGaugeDimensions(); + gaugeHelper.renderValueLabel(); + if (gaugeHelper.showMarkers) { + gaugeHelper.renderLimits(); + } + gaugeHelper.renderSmallValueArrow(gaugeHelper.minValue); + } + }); +})(); diff --git a/raspberry-backend/templates/base.html b/raspberry-backend/templates/base.html index c8b29c37378bfe9ba49bead039c27ae31248dfe6..42580894cd8afaa0551b78015c1861cc838b15f3 100644 --- a/raspberry-backend/templates/base.html +++ b/raspberry-backend/templates/base.html @@ -119,7 +119,8 @@ <script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script> <script src="{{ url_for('static', filename='js/Chart.min.js') }}"></script> <script src="{{ url_for('static', filename='js/moment.js') }}"></script> - <script src="{{ url_for('static', filename='js/Chart.js') }}"></script> + <script src="{{ url_for('static', filename='js/Chart.js') }}"></script> + <script src="{{ url_for('static', filename='js/Gauge.js') }}"></script> <script src="{{ url_for('static', filename='js/jquery.dataTables.min.js') }}"></script> <script src="{{ url_for('static', filename='js/dataTables.bootstrap4.min.js') }}"></script> diff --git a/raspberry-backend/templates/index.html b/raspberry-backend/templates/index.html index e12bba4667783dd9cbaf2b2205ecb5cf40f720ce..9c03789c022d079aa43dba2de7cc4501031da75e 100644 --- a/raspberry-backend/templates/index.html +++ b/raspberry-backend/templates/index.html @@ -29,9 +29,10 @@ <div class = "row"> <div class = "range-button-container ml-auto mr-auto"> + <input type="button" class="sb-nav-button" name="fivesecs" value="5s" onclick="setChartXaxisRange(-5,0)"> <input type="button" class="sb-nav-button" name="thirtysecs" value="30s" onclick="setChartXaxisRange(-30,0)"> - <input type="button" class="sb-nav-button" name="thirtysecs" value="60s" onclick="setChartXaxisRange(-60,0)"> - <input type="button" class="sb-nav-button" name="thirtysecs" value="90s" onclick="setChartXaxisRange(-90,0)"> + <input type="button" class="sb-nav-button" name="sixtysecs" value="60s" onclick="setChartXaxisRange(-60,0)"> + <input type="button" class="sb-nav-button" name="ninetysecs" value="90s" onclick="setChartXaxisRange(-90,0)"> </div> </div> </div> @@ -60,16 +61,15 @@ <div class="card-header d-flex align-items-center justify-content-between py-1 min-height-1b"> <a class="small text-dark col-center" href="#">FIO<sub>2</sub></a> </div> - <div class="card-body px-1 py-1 tiny ml-auto mr-auto br-red"> - <div class = "reading-wrapper br-red"><input class = "input-reading py-0" id = "fio2" value="62" type="number" readOnly disabled>%</div> - </div> + <!-- <div class="card-body px-1 py-1 tiny ml-auto mr-auto"><input class = "input-reading py-0" id = "fio2" value="62" type="number" readOnly disabled>%</div> --> + <div class = "main-chart-container"><canvas id="example_gauge"></canvas></div> </div> <div class="card-reading"> <div class="card-header d-flex align-items-center justify-content-between py-1 min-height-1b"> <a class="small text-dark col-center" href="#">P<sub>Peak</sub></a> </div> - <div class="card-body px-1 py-1 tiny ml-auto mr-auto"><input class = "input-reading" id="p_peak" readOnly disabled value=20.0>cmH2O</div> + <div class="card-body px-1 py-1 tiny ml-auto mr-auto"><input class = "input-reading" id="p_peak" readOnly disabled value=20.0>cmH2O</div> </div> <!--- end row 1 --> diff --git a/raspberry-dataserver/hevfromtxt.py b/raspberry-dataserver/hevfromtxt.py index a22417b7b4d0b23180601c608dbbe711aa7ac970..d25f317b2a5ab164eab46c4d94a17efce489b35e 100755 --- a/raspberry-dataserver/hevfromtxt.py +++ b/raspberry-dataserver/hevfromtxt.py @@ -39,7 +39,7 @@ class hevfromtxt(): # directly setting private member variables in this edge case payload = CommsCommon.DataFormat() payload._version = payload._RPI_VERSION - payload._timestamp = time_offset + self._timestamp[self._pos] + payload._timestamp = time_offset + self._timestamp[self._pos] * 1000 # timestamp in ms payload._pressure_buffer = self._pressure[self._pos] payload._pressure_inhale = self._volume[self._pos] payload._temperature_buffer = self._flow[self._pos]