From 36ef308c7349eacd188c04d8f414940d34f52aef Mon Sep 17 00:00:00 2001 From: jkl <sagitta1618@gmail.com> Date: Thu, 19 Oct 2023 12:47:59 +0200 Subject: [PATCH] Fix interrupt in OhmPi for UI --- index.html | 789 +++++++++++++++++++++++++++++++++++++++++++++++++ index_old.html | 704 +++++++++++++++++++++++++++++++++++++++++++ ohmpi/ohmpi.py | 35 ++- 3 files changed, 1510 insertions(+), 18 deletions(-) create mode 100755 index.html create mode 100644 index_old.html diff --git a/index.html b/index.html new file mode 100755 index 00000000..65ed3d53 --- /dev/null +++ b/index.html @@ -0,0 +1,789 @@ +<!DOCTYPE html> +<!-- + To access the interface, + 1. connect to the 'raspi-webgui' wifi with the given password + Open a browserdebugging instruction to see the messages passing on the RPi +mosquitto_sub -h raspberrypi.local -t ohmpi_0001/ctrl +--> +<html> +<head> + <meta charset="utf8"/> + <title>OhmPi Acquisition Board</title> + <link rel="shortcut icon" type="image/jpg" href="logo_ohmpi.jpg"/> + + <!-- dependencies (need to be local as no internet in AP mode)--> + <script src="js/plotly-2.26.0.min.js"></script> + <script src="js/jquery-3.4.1.min.js"></script> + <link type="text/css" href="css/bootstrap.min.css" rel="stylesheet"> + <!-- <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> --> + <!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous"> --> + <!-- <script src="js/danfojs/bundle.min.js"></script> --> + <!-- <script src="js/mqtt.min.js"></script> --> + <script src="js/paho/paho-mqtt.js"></script> + <meta name="viewport" content="width=device-width, initial-scale=1"> +</head> +<body> + <div class='container-fluid'> + <h1>OhmPi Acquisition Board</h1> + <!-- nb stacks, on-time --> + <button id="update_settingsBtn" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#exampleModal">Settings</button> + <button id='runBtn' type="button" class="btn btn-primary">▶</button> + <button id='stopBtn' type="button" class="btn btn-warning">◼</button> + <!-- upload button for csv which display the table ABMN --> + <button id="removeDataBtn" type="button" class="btn btn-danger">Clear data</button> + <button id="getDataBtn" type="button" class="btn btn-info">Get data</button> + <div class="form-check"> + <input id="dataRetrievalCheck" class="form-check-input" type="checkbox" value=""> + <label class="form-check-label" for="dataRetrievalCheck"> + Automaticaly get data every 1 secondStart + </label> + </div> + <div id='output'>Status: idle</div> + + <!-- Pseudo section --> + <select id='surveySelect' class='custom-select'> + </select> + <input id="cmin" type="number" value=""/> + <input id="cmax" type="number" value=""/> + <button id="capplyBtn" type="button" class="btn btn-info">Apply</button> + <div id="gd"></div> + + <!-- trace figure --> + <div class="mb3 row"> + <label for="quadSelect">Quadrupole:</label> + <div class="col-sm-10"> + <select id='quadSelect' class='custom-select'></select> + </div> + </div> + <button id="addTraceBtn" type="button" class="btn btn-info">Add trace</button> + <button id="removeTracesBtn" type="button" class="btn btn-info">Remove all traces</button> + <div id="ts"></div> + + <!-- RS check --> + <button id="rsBtn" type="button" class="btn btn-info">Check contact resistance</button> + <button id="rsClearBtn" type="button" class="btn btn-info">Clear plot</button> + <div id="rs"></div> + + <!-- Inversion plot --> + <select id="surveySelectInv" class='custom-select'> + </select> + <button id="invertBtn" type="button" class="btn btn-info">Run inversion</button> + <input id="cminInv" type="number" value=""/> + <input id="cmaxInv" type="number" value=""/> + <button id="capplyBtnInv" type="button" class="btn btn-info">Apply</button> + <div id="inv"></div> + + <!-- Additional buttons --> + <button id="downloadBtn" type="button" class="btn btn-primary">Download data</button> + <a id="download"></a> + + <button id="restartBtn" type="button" class="btn btn-danger">Restart</button> + <button id="shutdownBtn" type="button" class="btn btn-danger">Shutdown</button> + + <!-- Modal for configuration --> + <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="exampleModalLabel">OhmPi configuration</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <form> + <div class="form-group row"> + <label for="nb_electrodes" class="col-sm-2 col-form-label">Nb electrodes</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="nb_electrodes" value="64"> + </div> + </div> + <div class="form-group row"> + <label for="injection_duration" class="col-sm-2 col-form-label">Injection duration [s]</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="injection_duration" value="0.2"> + </div> + </div> + <div class="form-group row"> + <label for="nb_meas" class="col-sm-2 col-form-label">Nb Measurements</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="nb_meas" value="1"> + </div> + </div> + <div class="form-group row"> + <label for="sequence_delay" class="col-sm-2 col-form-label">Sequence delay [s]</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="sequence_delay" value="100"> + </div> + </div> + <div class="form-group row"> + <label for="nb_stack" class="col-sm-2 col-form-label">Nb stack</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="nb_stack" value="1"> + </div> + </div> + <div class="form-group row"> + <label for="duty_cycle" class="col-sm-2 col-form-label">Duty cycle [0 -> 1]</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="duty_cycle" value="0.5"> + </div> + </div> + <div class="form-group row"> + <label for="sequence" class="col-sm-2 col-form-label">Sequence</label> + <div class="col-sm-10"> + <input type="file" class="form-control" id="sequence"> + </div> + </div> + <div class="form-group row"> + <label for="elecSpacing" class="col-sm-2 col-form-label">Electrode spacing [m]</label> + <div class="col-sm-10"> + <input type="number" class="form-control" id="elecSpacing", value="1"> + </div> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> + <button id="saveConfigBtn" type="button" data-dismiss="modal" class="btn btn-primary">Save</button> + </div> + </div> + </div> + </div> + <footer>v0.3.0</footer> + </div> + + <script type="text/javascript"> + //let serverUrl = 'http://10.3.141.1:8080' + //let serverUrl = 'http://0.0.0.0:8080' + //let serverUrl = 'http://localhost:8080' + let serverUrl = 'http://' + window.location.host + console.log('serverUrl =', serverUrl) + let output = document.getElementById('output') + let data = {} // hold data of all surveys + let interv = null // hold interval for automatic data retrieval + let quads = [] // available quadrupoles for time-serie figure + let squads = [] // selected quadrupoles for time-serie figure + let commands = {} // store commands and their id + let callbacks = {} // store callback (might not be needed) + let invertedData = [{ + rho: [[10, 10.625, 12.5, 15.625, 20], + [5.625, 6.25, 8.125, 11.25, 15.625], + [2.5, 3.125, 5., 8.125, 12.5], + [0.625, 1.25, 3.125, 6.25, 10.625], + [0, 0.625, 2.5, 5.625, 10]], + x: [-9, -6, -5 , -3, -1], + y: [0, 1, 4, 5, 7], + }] // store inverted data + + // function with MQTT + let topic = 'ohmpi_0001' // we could change this through a drop-down to connect to a different ohmpi + let topic_ctrl = topic + '/ctrl' + let topic_exec = topic + '/exec' + let topic_data = topic + '/data' + let hostname = location.hostname + let port = 9001 + let clientId = 'ohmpi_0001_html' + let message = null + let msg = '' + let userName = 'jkl' // can be ask to the user + let password = 'jkl' + + // create client + client = new Paho.MQTT.Client(hostname, port, clientId) + client.onConnectionLost = onConnectionLost + client.onMessageArrived = onMessageArrived + client.connect({onSuccess: onConnect}) +// client.connect({userName: username, password: password, onSuccess:onConnect}) + + function onConnect() { + console.log("onConnect") + client.subscribe(topic_data) + client.subscribe(topic_exec) + + // send welcome message + message = new Paho.MQTT.Message("Hello from index.html") + message.destinationName = topic_ctrl + client.send(message) + } + + function onConnectionLost(responseObject) { + if (responseObject.errorCode !== 0) + console.log("onConnectionLost:" + responseObject.errorMessage) + } + + function onMessageArrived(message) { + try { + let payload = message.payloadString + if (message.topic == topic_data) { + // process data + msg = payload // for accessing the variable from the console + console.log('DATA LOG:', payload) + let ddic = JSON.parse(payload.split('INFO:')[1]) + + // check cmd_id is any + processMessage(ddic) + + // usually these don't have a cmd_id so we are not sure when + + } else if (message.topic == topic_exec) { + // display it in the log + console.log('EXEC LOG:', payload) + } + + // let response = JSON.parse(message.payloadString) + // console.log('response=', response) + // // check ID of message against our dictionnary of callback + // let cmd_id = response['cmd_id'] + // if (callbacks.hasOwnProperty(cmd_id)) { + // console.log('++ execute callback') + // callbacks[cmd_id](response['content']) // execute callback + // } + } catch (e) { + console.log(e) + } + // client.disconnect() + } + + // useful functions + function generateUniqSerial() { + return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => { + const r = Math.floor(Math.random() * 16); + return r.toString(16); + }); + } + + // sending commands to the OhmPi + function sendCommand(query, callback=null) { + // dic in the form: {'cmd': X, ...} as JSON + if (callback == null) { + function callback(x) { + console.log('default callback:', x) + } + } + + /* + let xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + if (xhr.status == 200) { + callback(JSON.parse(xhr.response)) + } + } + } + xhr.open('POST', serverUrl) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(query) + */ + + // generate a unique command id to be associated with the commands + let uuid = generateUniqSerial() + commands[uuid] = query + callbacks[uuid] = callback // store the callback to be processed later when message arrives + let payload = '{"cmd_id": "' + uuid + '",' + query.slice(1) + console.log('sendCommand()', payload) + message = new Paho.MQTT.Message(payload) + message.destinationName = topic_ctrl + client.send(message) + } + + // run button + function runBtnFunc() { + sendCommand('{"cmd": "run_multiple_sequences"}', function(x) { + console.log(x['status']) + if (x['status'] == 'running') { + output.innerHTML = 'Status: measuring...' + } + }) + } + let runBtn = document.getElementById('runBtn') + runBtn.addEventListener('click', runBtnFunc) + + // interrupt button + function stopBtnFunc() { + sendCommand('{"cmd": "interrupt"}', function(x) { + output.innerHTML = 'Status: ' + x['status'] + clearInterval(interv) + getData() + }) + } + let stopBtn = document.getElementById('stopBtn') + stopBtn.addEventListener('click', stopBtnFunc) + + // set configuration + function saveConfigBtnFunc() { + // collect values from modal + let formVals = { + 'nb_electrodes': parseInt(document.getElementById('nb_electrodes').value), + 'injection_duration': parseFloat(document.getElementById('injection_duration').value), + 'nb_meas': parseInt(document.getElementById('nb_meas').value), + 'sequence_delay': parseFloat(document.getElementById('sequence_delay').value), + 'nb_stack': parseInt(document.getElementById('nb_stack').value), + 'duty_cycle': parseFloat(document.getElementById('duty_cycle').value), + } + console.log(formVals) + + // deal with the potential file containing the sequence + // https://stackoverflow.com/questions/19038919/is-it-possible-to-upload-a-text-file-to-input-in-html-js + if (!window.FileReader) { + alert('Your browser is not supported'); + return false; + } + let input = document.getElementById('sequence') + if (input.files.length) { + const reader = new FileReader() + reader.readAsText(input.files[0]) + reader.addEventListener('load', () => { + // parse the file and make it a list of list of int + var a = reader.result.split(/\r?\n|\r|\n/g) + var seq = new Array() + var eof = false + for (var i = 0; i < a.length; i++) { + b = a[i].split(' ') + var arr = new Array(4) + for (var j = 0; j < b.length; j++) { + val = parseInt(b[j]) + if (isNaN(val) == true) { + eof = true + break + } + arr[j] = val + } + if (eof == true) { + break + } + seq.push(arr) + } + formVals['sequence'] = seq + configCallback() + }, false) + } else { + console.log('no sequence uploaded') + formVals['sequence'] = '' + configCallback() + } + + // define callback to send settings to Pi + function configCallback() { + sendCommand(JSON.stringify({ + 'cmd': 'update_settings', + 'kwargs': { + 'settings': formVals + } + }), function(x) { + console.log('update_settings', x) + }) + } + + } + let saveConfigBtn = document.getElementById('saveConfigBtn') + saveConfigBtn.addEventListener('click', saveConfigBtnFunc) + + // make pseudo plot + var trace = { + x: [], + y: [], + mode: 'markers', + marker: { + size: 40, + color: [], + colorbar: { + title: 'App. res. [Ohm.m]', + cmin: 0, + cmax: 100, + } + } + } + let layout = { + title: 'Pseudo-section', + yaxis: { + title: 'Pseudo-depth', + autorange: 'reversed' + }, + xaxis: { + title: 'X' + } + + } + Plotly.newPlot('gd', [trace], layout, {responsive: true}) + + // make time-serie plot + let tdata = [] + let layout2 = { + title: 'Time-serie', + yaxis: { + title: 'App. res. [Ohm.m]' + }, + xaxis: { + title: 'Sampling time' + } + } + Plotly.newPlot('ts', tdata, layout2, {responsive: true}) + + // add trace to time-serie plot + function addTraceBtnFunc() { + let val = document.getElementById('quadSelect').value + squads.push(val.split(', ')) + tdata.push({ + x: [], + y: [], + name: val, + type: 'scatter' + }) + Plotly.newPlot('ts', tdata, layout2, {responsive: true}) + getData() + } + let addTraceBtn = document.getElementById('addTraceBtn') + addTraceBtn.addEventListener('click', addTraceBtnFunc) + + // remove all traces from time-serie plot + function removeTracesBtnFunc() { + squads = [] + tdata = [] + Plotly.newPlot('ts', tdata, layout2, {responsive: true}) + } + let removeTracesBtn = document.getElementById('removeTracesBtn') + removeTracesBtn.addEventListener('click', removeTracesBtnFunc) + + // callback function to draw the plot + function surveySelectFunc(el) { + let surveyName = el['target'].value + let df = data[surveyName] + if (df != undefined) { + let a = df['a'] + let b = df['b'] + let m = df['m'] + let n = df['n'] + // let's assume electrodes are 1 m distance + // compute pseudo-depth (assume no topo) + // compute app res (assumping flat, line survey) + let xpos = [] + let ypos = [] + let app = [] + for (let i = 0; i < a.length; i++) { + let emin = Math.min(...[a[i], b[i], m[i], n[i]]) + let emax = Math.max(...[a[i], b[i], m[i], n[i]]) + let dist = Math.abs(emax - emin) + xpos.push(emin + dist/2) + ypos.push(Math.sqrt(2)/2 * dist) + // let ab = (a[i] + b[i])/2 + // let mn = (m[i] + n[i])/2 + // xpos.push(Math.min(ab, mn) + dist/2) + // ypos.push(Math.sqrt(2)/2*dist) + let am = Math.abs(a[i] - m[i]) + let bm = Math.abs(b[i] - m[i]) + let an = Math.abs(a[i] - n[i]) + let bn = Math.abs(a[i] - n[i]) + let K = (2*Math.PI)/((1/am)-(1/an)-(1/an)+(1/bn)) + app.push(df['rho'][i]*K) + } + // update the trace and redraw the figure + trace['x'] = xpos + trace['y'] = ypos + trace['marker']['color'] = app + trace['marker']['cmax'] = document.getElementById('cmax').value + trace['marker']['cmin'] = document.getElementById('cmin').value + Plotly.redraw('gd') + } + } + let surveySelect = document.getElementById('surveySelect') + + // bar chart for contact resistance + let rsdata = [] + let rslayout = { + title: 'Contact resistances', + yaxis: { + title: 'Resistance [kOhm]' + }, + xaxis: { + title: 'Consecutive electrodes' + } + } + Plotly.newPlot('rs', rsdata, rslayout, {responsive: true}) + + // run RS check + function rsBtnFunc() { + sendCommand('{"cmd": "rs_check"}', function (res) { + // update the bar plot + rsdata.push({ + x: res['data']['AB'], + y: res['data']['res'], + name: 'RS', + type: 'bar' + }) + Plotly.redraw('rs') + }) + } + let rsBtn = document.getElementById('rsBtn') + rsBtn.addEventListener('click', rsBtnFunc) + + // clear RS graph + function rsClearBtnFunc() { + rsdata = [] + Plotly.newPlot('rs', rsdata, rslayout, {responsive: true}) + } + let rsClearBtn = document.getElementById('rsClearBtn') + rsClearBtn.addEventListener('click', rsClearBtnFunc) + + // getData + function getData() { + sendCommand(JSON.stringify({ + 'cmd': 'get_data', + 'survey_names': Object.keys(data).slice(0, -1) + // last survey is often partial so we download it again + }), console.log('processData(ddic)') + ) + } + + // processMessage + function processMessage(ddic) { + //if (('status' in ddic) | ('data' in ddic)) { + if (ddic.constructor == Object) { // it's a dictionnary + // acquisition related + processData(ddic) + } else { + // inversion related + invertedData = ddic + showInvFunc() + } + } + + // processData + function processData(ddic) { + // update status + output.innerHTML = 'Status: ' + ddic['status'] + + // update data dic with new data + data = { // destructuring assignement (magic! :o) + ...data, + ...ddic['data'] // value from second dic are preferred + } + + // dropdown with number of surveys and +++ + let surveyNames = Object.keys(data).sort() + + // remove listener as we will replace the choices + surveySelect.removeEventListener('change', surveySelectFunc) + surveySelect.innerHTML = '' // clearing all child nodes + + // add choices again + for (let surveyName of surveyNames) { + let option = document.createElement('option') + option.innerText = surveyName + option.value = surveyName + surveySelect.appendChild(option) + } + + // listener again + surveySelect.addEventListener('change', surveySelectFunc) + + // plot last one by default + surveySelect.value = surveyNames[surveyNames.length - 1] + + // call the function directly + // (as progammatically chaging the value does not trigger the event) + surveySelectFunc({'target': surveySelect}) + + // update list of survey for inversion + surveySelectInv.removeEventListener('change', showInvFunc) + surveySelectInv.innerHTML = '' // clearing all child nodes + for (let surveyName of surveyNames) { + let option = document.createElement('option') + option.innerText = surveyName + option.value = surveyName + surveySelectInv.appendChild(option) + } + surveySelectInv.addEventListener('change', showInvFunc) + surveySelectInv.value = surveyNames[surveyNames.length - 1] + + + // update list of quadrupoles if any + if (quads.length == 0) { + console.log('updating list of quadrupoles') + let df = data[surveyNames[0]] + let quadSelect = document.getElementById('quadSelect') + quadSelect.innerHTML = '' + for (let i = 0; i < df['a'].length; i++) { + quad = [df['a'][i], df['b'][i], df['m'][i], df['n'][i]] + quads.push(quad) + let option = document.createElement('option') + option.value = quad.join(', ') + option.innerText = quad.join(', ') + quadSelect.appendChild(option) + } + console.log('quads=', quads) + } + + // update time-serie figure + if (squads.length > 0) { + + // transform all surveyNames to datetime + let xt = [] + for (surveyName of surveyNames) { + let a = surveyName.split('_').slice(-1)[0] + xt.push(a.slice(0, 4) + '-' + + a.slice(4, 6) + '-' + + a.slice(6, 8) + ' ' + + a.slice(9, 11) + ':' + + a.slice(11, 13) + ':' + + a.slice(13, 15)) + } + //console.log(xt) + + // create one new trace per selected quadrupole + for (let k = 0; k < squads.length; k++) { + squad = squads[k] + let x = [] + let y = [] + for (let i = 0; i < surveyNames.length; i++) { + df = data[surveyNames[i]] + for (let j = 0; j < df['a'].length; j++) { + if (df['a'][j] == squad[0] + && df['b'][j] == squad[1] + && df['m'][j] == squad[2] + && df['n'][j] == squad[3]) { + y.push(df['rho'][j]) + x.push(xt[i]) + break + } + } + } + + // update trace dictionnary + tdata[k]['x'] = x + tdata[k]['y'] = y + } + //console.log(tdata) + Plotly.redraw('ts') + } + } + + let getDataBtn = document.getElementById('getDataBtn') + getDataBtn.addEventListener('click', getData) + + // apply new colorscale + let capplyBtn = document.getElementById('capplyBtn') + capplyBtn.addEventListener('click', function() { + surveySelectFunc({'target': surveySelect}) + }) + + // plot inverted data + function showInvFunc() { + let cmin = document.getElementById('cminInv').value + let cmax = document.getElementById('cmaxInv').value + + var invData = [{ + z: invertedData[0]['rho'], + x: invertedData[0]['x'], + y: invertedData[0]['z'], + type: 'contour', + colorscale: 'Viridis', + autocontour: true, // set to false if cmin is given + contours: { + start: cmin, + end: cmax, + size: 10 + }, + }] + + var invLayout = { + title: 'Inverted section', + yaxis: { + title: 'Z [m]', + }, + xaxis: { + title: 'X [m]' + } + } + + Plotly.newPlot('inv', invData, invLayout, {responsive: true}) + var btn = document.getElementById('invertBtn') + btn.innerText = 'Run inversion' + btn.className = 'btn btn-info' + + } + showInvFunc() + + // invert data + function invertBtnFunc() { + let survey_name = document.getElementById('surveySelectInv').value + var btn = document.getElementById('invertBtn') + btn.innerText = 'Inverting...' + btn.className = 'btn btn-warning' + sendCommand(JSON.stringify({ + 'cmd': 'run_inversion', + 'kwargs': { + 'survey_names': [survey_name + '.csv'] + } + }), function(x) { + console.log('inversion results', x) + }) + } + let invertBtn = document.getElementById('invertBtn') + invertBtn.addEventListener('click', invertBtnFunc) + + // apply new colorscale for inversion + let capplyBtnInv = document.getElementById('capplyBtnInv') + capplyBtnInv.addEventListener('click', function() { + showInvFunc() + }) + + // checkbox interaction for data download + function dataRetrievalCheckFunc(x) { + if (x['target'].checked == true) { + interv = setInterval(getData, 1000) // every 5s + } else { + clearInterval(interv) + } + } + let dataRetrievalCheck = document.getElementById('dataRetrievalCheck') + dataRetrievalCheck.addEventListener('change', dataRetrievalCheckFunc) + + // remove data + function removeDataBtnFunc() { + sendCommand('{"cmd": "remove_data"}',function(x) { + data = {} + output.innerHTML = 'Status: ' + x['status'] + ' (all data cleared)' + console.log('all data removed') + }) + } + let removeDataBtn = document.getElementById('removeDataBtn') + removeDataBtn.addEventListener('click', removeDataBtnFunc) + + // shutdown Pi + function shutdownBtnFunc() { + sendCommand('{"cmd": "shutdown"}', function(x) { + console.log('shuting down...') + }) + } + let shutdownBtn = document.getElementById('shutdownBtn') + shutdownBtn.addEventListener('click', shutdownBtnFunc) + + // restart Pi + function restartBtnFunc() { + sendCommand('{"cmd": "restart"}', function(x) { + console.log('rebooting...') + }) + } + let restartBtn = document.getElementById('restartBtn') + restartBtn.addEventListener('click', restartBtnFunc) + + // download data + function downloadBtnFunc() { + sendCommand('{"cmd": "download"}', function(x) { + let dwl = document.getElementById('download') + dwl.setAttribute('href', serverUrl + '/data.zip') + dwl.setAttribute('download', 'data.zip') + dwl.click() + }) + } + let downloadBtn = document.getElementById('downloadBtn') + downloadBtn.addEventListener('click', downloadBtnFunc) + + + </script> + + <!-- Boostrap scripts (at the end of the page for faster loading time)--> + <script src="js/bootstrap.bundle.min.js"></script> + <!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script> --> +</body> +</html> diff --git a/index_old.html b/index_old.html new file mode 100644 index 00000000..b334d18b --- /dev/null +++ b/index_old.html @@ -0,0 +1,704 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf8"/> + <title>OhmPi Acquisition Board</title> + <link rel="shortcut icon" type="image/jpg" href="logo_ohmpi.jpg"/> + + <!-- dependencies (need to be local as no internet in AP mode)--> + <script src="js/plotly-basic-2.8.3.min.js"></script> + <script src="js/jquery-3.4.1.min.js"></script> + <link type="text/css" href="css/bootstrap.min.css" rel="stylesheet"> + <!-- <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> --> + <!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous"> --> + <!-- <script src="js/danfojs/bundle.min.js"></script> --> +</head> +<body> + <div class='container'> + <h1>OhmPi Acquisition Board</h1> + <!-- nb stacks, on-time --> + <button id="update_settingsBtn" type="button" class="btn btn-secondary" data-toggle="modal" data-target="#exampleModal">Settings</button> + <button id='runBtn' type="button" class="btn btn-primary">▶</button> + <button id='stopBtn' type="button" class="btn btn-warning">◼</button> + <!-- upload button for csv which display the table ABMN --> + <button id="removeDataBtn" type="button" class="btn btn-danger">Clear data</button> + <button id="getDataBtn" type="button" class="btn btn-info">Get data</button> + <div class="form-check"> + <input id="dataRetrievalCheck" class="form-check-input" type="checkbox" value=""> + <label class="form-check-label" for="dataRetrievalCheck"> + Automaticaly get data every 1 second + </label> + </div> + <div id='output'>Status: idle</div> + + <!-- Pseudo section --> + <select id='surveySelect' class='custom-select'> + </select> + <input id="cmin" type="number" value=""/> + <input id="cmax" type="number" value=""/> + <button id="capplyBtn" type="button" class="btn btn-info">Apply</button> + <div id="gd"></div> + <div id="hoverinfo" style="margin-left:80px;"></div> + <div class="mb3 row"> + <label for="quadSelect">Quadrupole:</label> + <div class="col-sm-10"> + <select id='quadSelect' class='custom-select'></select> + </div> + </div> + + <!-- trace figure --> + <button id="addTraceBtn" type="button" class="btn btn-info">Add trace</button> + <button id="removeTracesBtn" type="button" class="btn btn-info">Remove all traces</button> + <div id="ts"></div> + + <!-- RS check --> + <button id="rsBtn" type="button" class="btn btn-info">Check contact resistance</button> + <button id="getRsBtn" type="button" class="btn btn-info">Get contact resistance</button> + <button id="rsClearBtn" type="button" class="btn btn-info">Clear plot</button> + <div id="rs"></div> + + <!-- Additional buttons --> + <button id="downloadBtn" type="button" class="btn btn-primary">Download data</button> + <!-- <button id="invertBtn" type="button" class="btn btn-primary">Invert</button> --> + <a id="download"></a> + + <!-- Modal for settings --> + <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="exampleModalLabel">OhmPi settings</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <form> + <div class="form-group row"> + <label for="nbElectrodes" class="col-sm-2 col-form-label">Nb electrodes</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="nbElectrodes" value=64> + </div> + </div> + <div class="form-group row"> + <label for="injectionDuration" class="col-sm-2 col-form-label">Injection duration [s]</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="injectionDuration" value=0.2> + </div> + </div> + <div class="form-group row"> + <label for="nbMeasurements" class="col-sm-2 col-form-label">Nb Measurements</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="nbMeasurements" value="1"> + </div> + </div> + <div class="form-group row"> + <label for="sequenceDelay" class="col-sm-2 col-form-label">Sequence delay [s]</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="sequenceDelay" value="100"> + </div> + </div> + <div class="form-group row"> + <label for="nbStack" class="col-sm-2 col-form-label">Nb stack</label> + <div class="col-sm-10"> + <input type="number" class="form-control-number" id="nbStack" value="1"> + </div> + </div> + <div class="form-group row"> + <label for="sequence" class="col-sm-2 col-form-label">Sequence</label> + <div class="col-sm-10"> + <input type="file" class="form-control" id="sequence"> + </div> + </div> + <div class="form-group row"> + <label for="elecSpacing" class="col-sm-2 col-form-label">Electrode spacing [m]</label> + <div class="col-sm-10"> + <input type="number" class="form-control" id="elecSpacing", value="1"> + </div> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> + <button id="saveConfigBtn" type="button" data-dismiss="modal" class="btn btn-primary">Save</button> + </div> + </div> + </div> + </div> + <button id="restartBtn" type="button" class="btn btn-danger">Restart</button> + <button id="shutdownBtn" type="button" class="btn btn-danger">Shutdown</button> + <footer>v0.2.0</footer> + </div> + + <script type="text/javascript"> + //let serverUrl = 'http://10.3.141.1:8080' + //let serverUrl = 'http://0.0.0.0:8080' + //let serverUrl = 'http://localhost:8080' + let serverUrl = 'http://' + window.location.host + console.log('serverUrl =', serverUrl) + let output = document.getElementById('output') + let data = {} // hold data of all surveys + let interv = null // hold interval for automatic data retrieval + let quads = [] // available quadrupoles for time-serie figure + let squads = [] // selected quadrupoles for time-serie figure + let elecSpacing = 1 // 1 m + + // useful functions + function sendCommand(query, callback=null) { + // dic in the form: {'cmd': X, ...} as JSON + if (callback == null) { + function callback(x) { + console.log('default callback:', x) + } + } + let xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + if (xhr.status == 200) { + callback(JSON.parse(xhr.response)) + } + } + } + xhr.open('POST', serverUrl) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.send(query) + } + + // run button + function runBtnFunc() { + sendCommand('{"cmd": "run_multiple_sequences"}', function(x) { + console.log(x['ohmpi_status']) + if (x['ohmpi_status'] == 'running') { + output.innerHTML = 'Status: measuring...' + } + }) + } + let runBtn = document.getElementById('runBtn') + runBtn.addEventListener('click', runBtnFunc) + + // interrupt button + function stopBtnFunc() { + sendCommand('{"cmd": "interrupt"}', function(x) { + output.innerHTML = 'Status: ' + x['ohmpi_status'] + clearInterval(interv) + getData() + }) + } + let stopBtn = document.getElementById('stopBtn') + stopBtn.addEventListener('click', stopBtnFunc) + + // set configuration + function saveSettingsBtnFunc() { + // collect values from modal + let formVals = {} + formVals['nb_electrodes'] = parseInt(document.getElementById('nbElectrodes').value) + formVals['injection_duration'] = parseFloat(document.getElementById('injectionDuration').value) + formVals['nb_meas'] = parseInt(document.getElementById('nbMeasurements').value) + formVals['sequence_delay'] = parseInt(document.getElementById('sequenceDelay').value) + formVals['nb_stack'] = parseInt(document.getElementById('nbStack').value) + formVals['elec_spacing'] = parseFloat(document.getElementById('elecSpacing').value) + console.log(formVals) + elecSpacing = formVals['elec_spacing'] + + // define callback to send settigs to Pi + function settingsCallback() { + sendCommand(JSON.stringify({ + 'cmd': 'update_settings', + 'config': formVals + }), function(x) { + console.log('update_settings', x) + }) + } + + // deal with the potential file containing the sequence + // https://stackoverflow.com/questions/19038919/is-it-possible-to-upload-a-text-file-to-input-in-html-js + if (!window.FileReader) { + alert('Your browser is not supported'); + return false; + } + let input = document.getElementById('sequence') + if (input.files.length) { + const reader = new FileReader() + reader.readAsText(input.files[0]) + reader.addEventListener('load', () => { + formVals['sequence'] = reader.result + console.log('file==', reader.result) + settingsCallback() + }, false) + } else { + console.log('no sequence uploaded') + formVals['sequence'] = '' + settingsCallback() + } + + + } + let saveConfigBtn = document.getElementById('saveConfigBtn') + saveConfigBtn.addEventListener('click', saveSettingsBtnFunc) + + // make pseudo plot + var trace = {} + let layout = {} + let tdata = [] + let layout2 = {} + let rsdata = [] + let rslayout = {} + + // initialize all plots + function initPlots() { + trace = { + x: [], + y: [], + mode: 'markers', + marker: { + size: 40, + color: [], + colorbar: { + title: 'App. res. [Ohm.m]', + cmin: 0, + cmax: 100, + } + } + } + layout = { + title: 'Pseudo-section', + yaxis: { + title: 'Pseudo-depth', + autorange: 'reversed' + }, + xaxis: { + title: 'X' + } + + } + Plotly.newPlot('gd', [trace], layout) + + // make time-serie plot + tdata = [] + layout2 = { + title: 'Time-serie', + yaxis: { + title: 'App. res. [Ohm.m]' + }, + xaxis: { + title: 'Sampling time' + } + } + Plotly.newPlot('ts', tdata, layout2) + + // bar chart for contact resistance + rsdata = [] + rslayout = { + title: 'Contact resistances', + yaxis: { + title: 'Resistance [kOhm]' + }, + xaxis: { + title: 'Consecutive electrodes' + } + } + Plotly.newPlot('rs', rsdata, rslayout) + } + initPlots() + + // hover function + var hoverInfo = document.getElementById('hoverinfo') + document.getElementById('gd').on('plotly_hover', function(data){ + var infotext = data.points.map(function(d){ + return (Math.round(d.data.marker.color[d.pointIndex], 2) + ' Ohm.m'); + }); + hoverInfo.innerHTML = infotext.join('<br/>'); + }) + .on('plotly_unhover', function(data){ + hoverInfo.innerHTML = ''; + }); + + // add trace to time-serie plot + function addTraceBtnFunc() { + let val = document.getElementById('quadSelect').value + squads.push(val.split(', ')) + tdata.push({ + x: [], + y: [], + name: val, + type: 'scatter' + }) + Plotly.newPlot('ts', tdata, layout2) + getData() + } + let addTraceBtn = document.getElementById('addTraceBtn') + addTraceBtn.addEventListener('click', addTraceBtnFunc) + + // remove all traces from time-serie plot + function removeTracesBtnFunc() { + squads = [] + tdata = [] + Plotly.newPlot('ts', tdata, layout2) + } + let removeTracesBtn = document.getElementById('removeTracesBtn') + removeTracesBtn.addEventListener('click', removeTracesBtnFunc) + + // callback function to draw the plot + function surveySelectFunc(el) { + let surveyName = el['target'].value + let df = data[surveyName] + if (df != undefined) { + // let's assume electrodes are 1 m distance + // compute pseudo-depth (assume no topo) + // compute app res (assumping flat, line survey) + let xpos = [] + let ypos = [] + let app = [] + for (let i = 0; i < df['a'].length; i++) { + let a = df['a'][i] + let b = df['b'][i] + let m = df['m'][i] + let n = df['n'][i] + + // compute geometric factor assuming flat 2D surface + let am = Math.abs(a - m)*elecSpacing + let bm = Math.abs(b - m)*elecSpacing + let an = Math.abs(a - n)*elecSpacing + let bn = Math.abs(b - n)*elecSpacing + let K = 2*Math.PI/((1/am)-(1/bm)-(1/an)+(1/bn)) + app.push(df['rho'][i]*K) + //console.log(K) // same as resipy for the wenner case + + // computing pseudo-depth assuming 2D flat array + // let's sort the electrodes AB are the two left, MN, the two right + let abmn = [a, b, m, n] + abmn = abmn.sort((a, b) => a - b) + let ab = (abmn[0] + abmn[1])/2 + let mn = (abmn[2] + abmn[3])/2 + let dist = Math.abs(ab - mn) + xpos.push((Math.min(ab, mn) + dist/2)*elecSpacing) + ypos.push((Math.sqrt(2)/2*dist)*elecSpacing) + + /* + lookupDict = dict(zip(self.elec['label'], np.arange(self.elec.shape[0]))) + array = self.df[['a','b','m','n']].replace(lookupDict).values.astype(int) + elecm = self.elec[['x','y','z']].values.astype(float).copy() # electrode matrix - should be array of floats so np.inf work properly + + ### first determine if measurements are nested ### + #find mid points of AB + AB = (elecm[array[:,0]] + elecm[array[:,1]]) / 2 # mid points of AB + MN = (elecm[array[:,2]] + elecm[array[:,3]]) / 2 # mid points of MN + ABrad = np.sqrt(np.sum((elecm[array[:,0]] - AB)**2,axis=1)) # radius of AB circle + MNrad = np.sqrt(np.sum((elecm[array[:,2]] - MN)**2,axis=1)) # radius of MN circle + + Amn = np.sqrt(np.sum((elecm[array[:,0]] - MN)**2,axis=1)) # distance of A to mid point of MN + Bmn = np.sqrt(np.sum((elecm[array[:,1]] - MN)**2,axis=1)) # distance of B to mid point of MN + Nab = np.sqrt(np.sum((elecm[array[:,2]] - AB)**2,axis=1)) # distance of N to mid point of AB + Mab = np.sqrt(np.sum((elecm[array[:,3]] - AB)**2,axis=1)) # distance of M to mid point of AB + + iABinMN = (Amn < MNrad) & (Bmn < MNrad) + iMNinAB = (Nab < ABrad) & (Mab < ABrad) + inested = iABinMN | iMNinAB #if AB encompasses MN or MN encompasses AB + + # so it will never be taken as minimium + elecm[self.elec['remote'].values,:] = np.inf + + # compute midpoint position of AB and MN dipoles + elecx = elecm[:,0] + elecy = elecm[:,1] + + #CURRENT ELECTRODE MIDPOINTS + caddx = np.abs(elecx[array[:,0]]-elecx[array[:,1]])/2 + caddy = np.abs(elecy[array[:,0]]-elecy[array[:,1]])/2 + caddx[np.isinf(caddx)] = 0 + caddy[np.isinf(caddy)] = 0 + cmiddlex = np.min([elecx[array[:,0]], elecx[array[:,1]]], axis=0) + caddx + cmiddley = np.min([elecy[array[:,0]], elecy[array[:,1]]], axis=0) + caddy + + #POTENTIAL ELECTRODE MIDPOINTS + paddx = np.abs(elecx[array[:,2]]-elecx[array[:,3]])/2 + paddy = np.abs(elecy[array[:,2]]-elecy[array[:,3]])/2 + paddx[np.isinf(paddx)] = 0 + paddy[np.isinf(paddy)] = 0 + pmiddlex = np.min([elecx[array[:,2]], elecx[array[:,3]]], axis=0) + paddx + pmiddley = np.min([elecy[array[:,2]], elecy[array[:,3]]], axis=0) + paddy + + + # for non-nested measurements + xposNonNested = np.min([cmiddlex, pmiddlex], axis=0) + np.abs(cmiddlex-pmiddlex)/2 + yposNonNested = np.min([cmiddley, pmiddley], axis=0) + np.abs(cmiddley-pmiddley)/2 + pcdist = np.sqrt((cmiddlex-pmiddlex)**2 + (cmiddley-pmiddley)**2) + + # zposNonNested = np.sqrt(2)/2*pcdist + zposNonNested = pcdist/4 + + if np.all(cmiddley-pmiddley == 0): + zposNonNested = 0.25*pcdist + else: # for 3D arrays where there are mid-line measurements, this works closer to inversion results + zposNonNested = np.sqrt(2)/2*pcdist + + # for nested measurements use formula of Dalhin 2006 + xposNested = np.zeros(len(pmiddlex)) + yposNested = np.zeros(len(pmiddlex)) + outerElec1 = np.zeros((len(pmiddlex), 2)) # position of one electrode of outer dipole + outerElec2 = np.zeros((len(pmiddlex), 2)) # position of one electrode of outer dipole + # innerMid = np.zeros((len(pmiddlex), 2)) # middle of inner dipole + if np.sum(iMNinAB) > 0: + xposNested[iMNinAB] = pmiddlex[iMNinAB] + yposNested[iMNinAB] = pmiddley[iMNinAB] + outerElec1[iMNinAB] = np.c_[elecx[array[iMNinAB,0]], elecy[array[iMNinAB,0]]] + outerElec2[iMNinAB] = np.c_[elecx[array[iMNinAB,1]], elecy[array[iMNinAB,1]]] + + if np.sum(iABinMN) > 0: + xposNested[iABinMN] = cmiddlex[iABinMN] + yposNested[iABinMN] = cmiddley[iABinMN] + outerElec1[iABinMN] = np.c_[elecx[array[iABinMN,2]], elecy[array[iABinMN,2]]] + outerElec2[iABinMN] = np.c_[elecx[array[iABinMN,3]], elecy[array[iABinMN,3]]] + + innerMid = np.c_[pmiddlex, pmiddley] # always use potential dipole + + apdist = np.sqrt(np.sum((outerElec1-innerMid)**2, axis=1)) + bpdist = np.sqrt(np.sum((outerElec2-innerMid)**2, axis=1)) + zposNested = np.min([apdist, bpdist], axis=0)/3 + + xpos = np.zeros_like(pmiddlex) + ypos = np.zeros_like(pmiddlex) + zpos = np.zeros_like(pmiddlex) + + xpos[~inested] = xposNonNested[~inested] + xpos[inested] = xposNested[inested] + + ypos[~inested] = yposNonNested[~inested] + ypos[inested] = yposNested[inested] + + zpos[~inested] = zposNonNested[~inested] + zpos[inested] = zposNested[inested] + + */ + } + //console.log(app) + // update the trace and redraw the figure + trace['x'] = xpos + trace['y'] = ypos + trace['marker']['color'] = app + trace['marker']['cmax'] = document.getElementById('cmax').value + trace['marker']['cmin'] = document.getElementById('cmin').value + Plotly.redraw('gd') + } + } + let surveySelect = document.getElementById('surveySelect') + + // run RS check + function rsBtnFunc() { + sendCommand('{"cmd": "rsCheck"}', function (a) {}) + } + let rsBtn = document.getElementById('rsBtn') + rsBtn.addEventListener('click', rsBtnFunc) + + // get RS check data + function getRsBtnFunc() { + sendCommand('{"cmd": "getRsCheck"}', function(res) { + // update the bar plot + rsdata.push({ + x: res['data']['AB'], + y: res['data']['res'], + name: 'RS', + type: 'bar' + }) + Plotly.redraw('rs') + }) + } + let getRsBtn = document.getElementById('getRsBtn') + getRsBtn.addEventListener('click', getRsBtnFunc) + + // clear RS graph + function rsClearBtnFunc() { + rsdata = [] + Plotly.newPlot('rs', rsdata, rslayout) + } + let rsClearBtn = document.getElementById('rsClearBtn') + rsClearBtn.addEventListener('click', rsClearBtnFunc) + + // getData + function getData() { + let surveyNames = [] + sendCommand(JSON.stringify({ + 'cmd': 'getData', + 'surveyNames': surveyNames + // last survey is often partial so we download it again + }), function(ddic) { + // update status + //output.innerHTML = 'Status: ' + ddic['status'] + + // update data dic with new data + data = { // destructuring assignement (magic! :o) + ...data, + ...ddic['data'] // value from second dic are preferred + } + + // dropdown with number of surveys + surveyNames = Object.keys(data).sort() + + // remove listener as we will replace the choices + surveySelect.removeEventListener('change', surveySelectFunc) + surveySelect.innerHTML = '' // clearing all child nodes + + // add choices again + for (let surveyName of surveyNames) { + let option = document.createElement('option') + option.innerText = surveyName + option.value = surveyName + surveySelect.appendChild(option) + } + + // listener again + surveySelect.addEventListener('change', surveySelectFunc) + + // plot last one by default + surveySelect.value = surveyNames[surveyNames.length - 1] + + // call the function directly + // (as progammatically chaging the value does not trigger the event) + surveySelectFunc({'target': surveySelect}) + + // update list of quadrupoles if any + let idiff = false + if (data[surveyNames[0]] != undefined) { + idiff = quads.length != data[surveyNames[0]]['a'].length + } + //console.log('idiff=', idiff, quads.length, data[surveyNames[0]]['a'].length) + if (((quads.length == 0) | idiff) & (data[surveyNames[0]] != undefined)){ + console.log('updating list of quadrupoles') + quads = [] + let df = data[surveyNames[0]] + let quadSelect = document.getElementById('quadSelect') + quadSelect.innerHTML = '' + for (let i = 0; i < df['a'].length; i++) { + quad = [df['a'][i], df['b'][i], df['m'][i], df['n'][i]] + quads.push(quad) + let option = document.createElement('option') + option.value = quad.join(', ') + option.innerText = quad.join(', ') + quadSelect.appendChild(option) + } + console.log('quads=', quads) + } + + // update time-serie figure + if (squads.length > 0) { + + // transform all surveyNames to datetime + let xt = [] + for (surveyName of surveyNames) { + let a = surveyName.split('_').slice(-1)[0] + xt.push(a.slice(0, 4) + '-' + + a.slice(4, 6) + '-' + + a.slice(6, 8) + ' ' + + a.slice(9, 11) + ':' + + a.slice(11, 13) + ':' + + a.slice(13, 15)) + } + //console.log(xt) + + // create one new trace per selected quadrupole + for (let k = 0; k < squads.length; k++) { + squad = squads[k] + let x = [] + let y = [] + for (let i = 0; i < surveyNames.length; i++) { + df = data[surveyNames[i]] + for (let j = 0; j < df['a'].length; j++) { + if (df['a'][j] == squad[0] + && df['b'][j] == squad[1] + && df['m'][j] == squad[2] + && df['n'][j] == squad[3]) { + y.push(df['rho'][j]) + x.push(xt[i]) + break + } + } + } + + // update trace dictionnary + tdata[k]['x'] = x + tdata[k]['y'] = y + } + //console.log(tdata) + Plotly.redraw('ts') + } + }) + } + let getDataBtn = document.getElementById('getDataBtn') + getDataBtn.addEventListener('click', getData) + + // apply new colorscale + let capplyBtn = document.getElementById('capplyBtn') + capplyBtn.addEventListener('click', function() { + surveySelectFunc({'target': surveySelect}) + }) + + // checkbox interaction for data download + function dataRetrievalCheckFunc(x) { + if (x['target'].checked == true) { + interv = setInterval(getData, 1000) // every 5s + } else { + clearInterval(interv) + } + } + let dataRetrievalCheck = document.getElementById('dataRetrievalCheck') + dataRetrievalCheck.addEventListener('change', dataRetrievalCheckFunc) + + // remove data + function removeDataBtnFunc() { + sendCommand('{"cmd": "removeData"}',function(x) { + data = {} + output.innerHTML = 'Status: ' + x['ohmpi_status'] + ' (all data cleared)' + console.log('all data removed') + initPlots() // reset all plots + }) + } + let removeDataBtn = document.getElementById('removeDataBtn') + removeDataBtn.addEventListener('click', removeDataBtnFunc) + + // shutdown Pi + function shutdownBtnFunc() { + sendCommand('{"cmd": "shutdown"}', function(x) { + console.log('shuting down...') + }) + } + let shutdownBtn = document.getElementById('shutdownBtn') + shutdownBtn.addEventListener('click', shutdownBtnFunc) + + // restart Pi + function restartBtnFunc() { + sendCommand('{"cmd": "restart"}', function(x) { + console.log('rebooting...') + }) + } + let restartBtn = document.getElementById('restartBtn') + restartBtn.addEventListener('click', restartBtnFunc) + + // invert data + // function invertBtnFunc() { + // sendCommand('{"cmd": "invert"}', function(x) { + // console.log('inversion results', x) + // }) + // } + // let invertBtn = document.getElementById('invertBtn') + // invertBtn.addEventListener('click', invertBtnFunc) + + // download data + function downloadBtnFunc() { + sendCommand('{"cmd": "download"}', function(x) { + let dwl = document.getElementById('download') + dwl.setAttribute('href', serverUrl + '/data.zip') + dwl.setAttribute('download', 'data.zip') + dwl.click() + }) + } + let downloadBtn = document.getElementById('downloadBtn') + downloadBtn.addEventListener('click', downloadBtnFunc) + + + </script> + + <!-- Boostrap scripts (at the end of the page for faster loading time)--> + <script src="js/bootstrap.bundle.min.js"></script> + <!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script> --> +</body> +</html> diff --git a/ohmpi/ohmpi.py b/ohmpi/ohmpi.py index 8672af17..d75f0766 100644 --- a/ohmpi/ohmpi.py +++ b/ohmpi/ohmpi.py @@ -273,10 +273,6 @@ class OhmPi(object): if header == 'R [ohm]': headers[i] = 'R [Ohm]' icols = list(np.where(np.in1d(headers, ['A', 'B', 'M', 'N', 'R [Ohm]']))[0]) - print(headers) - print('+++++', icols) - print(np.array(headers)[np.array(icols)]) - print(np.loadtxt(os.path.join(ddir, fname), delimiter=',', skiprows=1, usecols=icols).shape) data = np.loadtxt(os.path.join(ddir, fname), delimiter=',', skiprows=1, usecols=icols) data = data[None, :] if len(data.shape) == 1 else data @@ -307,6 +303,7 @@ class OhmPi(object): self.exec_logger.debug('Interrupted sequence acquisition...') else: self.exec_logger.debug('No sequence measurement thread to interrupt.') + self.status = 'idle' self.exec_logger.debug(f'Status: {self.status}') def load_sequence(self, filename: str, cmd_id=None): @@ -353,7 +350,7 @@ class OhmPi(object): message : str message containing a command and arguments or keywords and arguments """ - status = False + self.status = 'idle' cmd_id = '?' try: decoded_message = json.loads(message) @@ -374,12 +371,10 @@ class OhmPi(object): except Exception as e: self.exec_logger.error( f"Unable to execute {cmd}({str(kwargs) if kwargs is not None else ''}): {e}") - status = False except Exception as e: self.exec_logger.warning(f'Unable to decode command {message}: {e}') - status = False finally: - reply = {'cmd_id': cmd_id, 'status': status} + reply = {'cmd_id': cmd_id, 'status': self.status} reply = json.dumps(reply) self.exec_logger.debug(f'Execution report: {reply}') @@ -596,6 +591,12 @@ class OhmPi(object): self.exec_logger.debug(f'Status: {self.status}') self.exec_logger.debug(f'Measuring sequence: {self.sequence}') + # # kill previous running thread + # if self.thread is not None: + # self.exec_logger.info('Removing previous thread') + # self.thread.stop() + # self.thread.join() + def func(): for g in range(0, nb_meas): # for time-lapse monitoring if self.status == 'stopping': @@ -603,12 +604,13 @@ class OhmPi(object): break t0 = time.time() self.run_sequence(**kwargs) - # sleeping time between sequence dt = sequence_delay - (time.time() - t0) if dt < 0: dt = 0 if nb_meas > 1: + if self.status == 'stopping': + break time.sleep(dt) # waiting for next measurement (time-lapse) self.status = 'idle' @@ -706,9 +708,11 @@ class OhmPi(object): # save data and print in a text file self.append_and_save(filename, acquired_data) self.exec_logger.debug(f'quadrupole {i + 1:d}/{n:d}') - self._hw.pwr_state = 'off' - self.status = 'idle' + + # reset to idle if we didn't interrupt the sequence + if self.status != 'stopping': + self.status = 'idle' def run_sequence_async(self, cmd_id=None, **kwargs): """Runs the sequence in a separate thread. Can be stopped by 'OhmPi.interrupt()'. @@ -828,10 +832,8 @@ class OhmPi(object): try: self.sequence = np.array(sequence).astype(int) # self.sequence = np.loadtxt(StringIO(sequence)).astype('uint32') - status = True except Exception as e: self.exec_logger.warning(f'Unable to set sequence: {e}') - status = False def switch_mux_on(self, quadrupole, bypass_check=False, cmd_id=None): """Switches on multiplexer relays for given quadrupole. @@ -920,7 +922,6 @@ class OhmPi(object): cmd_id : str, optional Unique command identifier """ - status = False if settings is not None: try: if isinstance(settings, dict): @@ -932,10 +933,10 @@ class OhmPi(object): dic = json.load(json_file) self.settings.update(dic) self.exec_logger.debug('Acquisition parameters updated: ' + str(self.settings)) - status = True + self.status = 'idle (acquisition updated)' except Exception as e: # noqa self.exec_logger.warning('Unable to update settings.') - status = False + self.status = 'idle (unable to update settings)' else: self.exec_logger.warning('Settings are missing...') @@ -945,8 +946,6 @@ class OhmPi(object): self.settings['export_dir'] = os.path.split(self.settings['export_path'])[0] self.settings['export_name'] = os.path.split(self.settings['export_path'])[1] - return status - def run_inversion(self, survey_names=[], elec_spacing=1, **kwargs): """Run a simple 2D inversion using ResIPy. -- GitLab