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">&#9654</button>
+        <button id='stopBtn' type="button" class="btn btn-warning">&#9724</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">&times;</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">&#9654</button>
+        <button id='stopBtn' type="button" class="btn btn-warning">&#9724</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">&times;</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