diff --git a/config.py b/config.py index 1588eb240e075a9b990926b356dc5d25529995d7..41bb94707fcf8c4a83a6033f824ee4c6a2b6a61a 100644 --- a/config.py +++ b/config.py @@ -28,7 +28,7 @@ OHMPI_CONFIG = { 'board_addresses': {'A': 0x73, 'B': 0x72, 'M': 0x71, 'N': 0x70}, # def. {'A': 0x76, 'B': 0x71, 'M': 0x74, 'N': 0x70} 'settings': 'ohmpi_settings.json', - 'board_version': '22.10' + 'board_version': '22.11' } # TODO: add a dictionary with INA models and associated gain values # Execution logging configuration diff --git a/http_interface.py b/http_interface.py index 7fd04e37d9e8ae509f56a8e85ea043bd6b22ac73..07548a555122db9b47bfbef60177f470d401b622 100644 --- a/http_interface.py +++ b/http_interface.py @@ -151,16 +151,16 @@ class MyServer(SimpleHTTPRequestHandler): shutil.rmtree('data') os.mkdir('data') elif dic['cmd'] == 'update_settings': - if 'sequence' in dic['config'].keys() and dic['config']['sequence'] is not None: - sequence = dic['config'].pop('sequence', None) + if 'sequence' in dic['settings'].keys() and dic['settings']['sequence'] is not None: + sequence = dic['settings'].pop('sequence', None) sequence = np.loadtxt(StringIO(sequence)).astype(int).tolist() # list of list # we pass the sequence as a list of list as this object is easier to parse for the json.loads() # of ohmpi._process_commands() payload = json.dumps({'cmd_id': cmd_id, 'cmd': 'set_sequence', 'kwargs': {'sequence': sequence}}) print('payload ===', payload) publish.single(payload=payload, **publisher_config) - payload = json.dumps({'cmd_id': cmd_id + '_settings', 'cmd': 'update_settings', 'kwargs': {'config': dic['config']}}) - cdic = dic['config'] + payload = json.dumps({'cmd_id': cmd_id + '_settings', 'cmd': 'update_settings', 'kwargs': {'settings': dic['settings']}}) + cdic = dic['settings'] publish.single(payload=payload, **publisher_config) elif dic['cmd'] == 'invert': pass diff --git a/index-mqtt.html b/index-mqtt.html index 070d4fe13ea81700590c04d23493fbab8dcb512a..b94e8aea13be41bf8b4419e51f1f20bc733d1f30 100755 --- a/index-mqtt.html +++ b/index-mqtt.html @@ -12,7 +12,6 @@ <!-- <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> </head> <body> @@ -28,7 +27,7 @@ <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 + Automaticaly get data every 1 second </label> </div> <div id='output'>Status: idle</div> @@ -36,10 +35,11 @@ <!-- Pseudo section --> <select id='surveySelect' class='custom-select'> </select> - <input id="cmin" type="number" value="0"/> - <input id="cmax" type="number" value="150"/> + <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"> @@ -54,6 +54,7 @@ <!-- 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> @@ -62,12 +63,12 @@ <!-- <button id="invertBtn" type="button" class="btn btn-primary">Invert</button> --> <a id="download"></a> - <!-- Modal for configuration --> + <!-- 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 configuration</h5> + <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> @@ -77,13 +78,13 @@ <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"> + <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"> + <input type="number" class="form-control-number" id="injectionDuration" value=0.2> </div> </div> <div class="form-group row"> @@ -188,8 +189,21 @@ let ddic = JSON.parse(payload.split('INFO:')[1]) // check cmd_id is any - processData(ddic) - + if ('data' in ddic) { + processData(ddic) + } else if ('rsdata' in ddic) { + // update the bar plot + // TODO make a nice rs check like data with stored past values and legend according to time, not just last survey + let key = Object.keys(ddic['rsdata'])[0] + rsdata.push({ + x: ddic['rsdata'][key]['AB'], + y: ddic['rsdata'][key]['res'], + name: 'RS', + type: 'bar' + }) + Plotly.redraw('rs') + } + // usually these don't have a cmd_id so we are not sure when } else if (message.topic == topic_exec) { @@ -253,11 +267,11 @@ client.send(message) } - // run button - function runBtnFunc() { + // run button + function runBtnFunc() { sendCommand('{"cmd": "run_multiple_sequences"}', function(x) { - console.log(x['status']) - if (x['status'] == 'running') { + console.log(x['ohmpi_status']) + if (x['ohmpi_status'] == 'running') { output.innerHTML = 'Status: measuring...' } }) @@ -277,26 +291,27 @@ stopBtn.addEventListener('click', stopBtnFunc) // set configuration - function saveConfigBtnFunc() { + function saveSettingsBtnFunc() { // collect values from modal let formVals = {} - for (let field of ['nbElectrodes', 'injectionDuration', - 'nbMeasurements', 'sequenceDelay', 'nbStack']) { - formVals[field] = document.getElementById(field).value - } + 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 settings to Pi - function configCallback() { - sendCommand(JSON.stringify({ - 'cmd': 'update_settings', - 'kwargs': { - 'config': formVals - } - }), function(x) { - console.log('update_settings', x) - }) - } + // send settings to the Pi (except sequence) + sendCommand(JSON.stringify({ + 'cmd': 'update_settings', + 'kwargs': { + 'settings': 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 @@ -309,61 +324,110 @@ const reader = new FileReader() reader.readAsText(input.files[0]) reader.addEventListener('load', () => { - formVals['sequence'] = reader.result - console.log('file==', reader.result) - configCallback() + // formVals['sequence'] = reader.result + let fcontent = reader.result + let sequence = [] + for (a of fcontent.split('\n')) { + b = a.split(' ') + if (b.length > 1) { + sequence.push(b.map(parseFloat)) + } + } + sendCommand(JSON.stringify({ + 'cmd': 'set_sequence', + 'kwargs': { + 'sequence': sequence + } + }), function(x) { + console.log('set_sequence', x) + }) + }, false) } else { console.log('no sequence uploaded') - formVals['sequence'] = '' - configCallback() } } let saveConfigBtn = document.getElementById('saveConfigBtn') - saveConfigBtn.addEventListener('click', saveConfigBtnFunc) + saveConfigBtn.addEventListener('click', saveSettingsBtnFunc) // 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) - - // make time-serie plot + var trace = {} + let layout = {} let tdata = [] - let layout2 = { - title: 'Time-serie', - yaxis: { - title: 'App. res. [Ohm.m]' - }, - xaxis: { - title: 'Sampling time' - } - } - Plotly.newPlot('ts', tdata, layout2) + 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() { @@ -395,30 +459,135 @@ 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 ab = (a[i] + b[i])/2 - let mn = (m[i] + n[i])/2 + 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) - 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) - } - console.log(app) + 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 @@ -429,35 +598,19 @@ } } 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) - - // run RS check - function rsBtnFunc() { - sendCommand('{"cmd": "rsCheck"}', function (res) { - // update the bar plot - rsdata.push({ - x: res['data']['AB'], - y: res['data']['res'], - name: 'RS', - type: 'bar' - }) - Plotly.redraw('rs') - }) + // run RS check + function rsBtnFunc() { + sendCommand('{"cmd": "rs_check"}', function (a) {}) } let rsBtn = document.getElementById('rsBtn') rsBtn.addEventListener('click', rsBtnFunc) + + // get RS check data + function getRsBtnFunc() { + sendCommand('{"cmd": "get_rs_check"}') + } + let getRsBtn = document.getElementById('getRsBtn') + getRsBtn.addEventListener('click', getRsBtnFunc) // clear RS graph function rsClearBtnFunc() { diff --git a/index.html b/index.html index b334d18b47d1f8ab64e32028a15fac4c0b513d7e..26a62c729bf622462d6bc0d5fe007f5914193cdd 100644 --- a/index.html +++ b/index.html @@ -204,7 +204,7 @@ function settingsCallback() { sendCommand(JSON.stringify({ 'cmd': 'update_settings', - 'config': formVals + 'settings': formVals }), function(x) { console.log('update_settings', x) }) diff --git a/ohmpi.py b/ohmpi.py index a91b9057a9326980e80def836db9c49852089294..63575cba8533bcb2a1d8b3ae2692adcec421d0cb 100644 --- a/ohmpi.py +++ b/ohmpi.py @@ -468,6 +468,28 @@ class OhmPi(object): self.data_logger.info(json.dumps(rdic)) return ddic + def get_rs_check(self, cmd_id=None): + """Get results from RS check. + """ + if cmd_id is None: + cmd_id = 'unknown' + ddic = {} + fnames = sorted([fname for fname in os.listdir('data/') if fname[-7:] == '_rs.csv']) + if len(fnames) > 0: + fname = fnames[-1] + try: + data = np.loadtxt('data/' + fname, delimiter=',', + skiprows=1) + data = data[None, :] if len(data.shape) == 1 else data + ddic[fname.replace('.csv', '')] = { + 'AB': [str(int(a)) + '-' + str(int(b)) for a, b in zip(data[:, 0], data[:, 1])], + 'res': data[:, 2].astype(float).tolist(), + } + except Exception as e: + print(fname, ':', e) + rdic = {'cmd_id': cmd_id, 'rsdata': ddic} + self.data_logger.info(json.dumps(rdic)) + def interrupt(self, cmd_id=None): """Interrupts the acquisition. """ self.status = 'stopping' @@ -952,10 +974,14 @@ class OhmPi(object): # sleeping time between sequence dt = sequence_delay - (time.time() - t0) - if dt < 0: - dt = 0 + dt = 0 if dt < 0 else dt if nb_meas > 1: - time.sleep(dt) # waiting for next measurement (time-lapse) + tt0 = time.time() + while time.time() < tt0 + dt: # infinite loop :o + if self.status == 'stopping': + break # break the while, enter the for and break the for + time.sleep(0.5) + # time.sleep(dt) # waiting for next measurement (time-lapse) self.status = 'idle' self.thread = threading.Thread(target=func) @@ -965,9 +991,6 @@ class OhmPi(object): """Runs sequence synchronously (=blocking on main thread). Additional arguments are passed to run_measurement(). """ - self.status = 'running' - self.exec_logger.debug(f'Status: {self.status}') - self.exec_logger.debug(f'Measuring sequence: {self.sequence}') t0 = time.time() # create filename with timestamp @@ -1014,8 +1037,6 @@ class OhmPi(object): self.append_and_save(filename, acquired_data) self.exec_logger.debug(f'quadrupole {i + 1:d}/{n:d}') - 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()'. Additional arguments are passed to run_measurement(). @@ -1024,7 +1045,10 @@ class OhmPi(object): ---------- cmd_id: """ - + self.status = 'running' + self.exec_logger.debug(f'Status: {self.status}') + self.exec_logger.debug(f'Measuring sequence: {self.sequence}') + def func(): self.run_sequence(**kwargs) @@ -1287,7 +1311,7 @@ class OhmPi(object): with open(settings) as json_file: dic = json.load(json_file) self.settings.update(dic) - self.exec_logger.debug('Acquisition parameters updated: ' + str(self.settings)) + self.exec_logger.info('Acquisition parameters updated: ' + str(self.settings)) status = True except Exception as e: # noqa self.exec_logger.warning('Unable to update settings.') diff --git a/run_http_interface.sh b/run_http_interface.sh index 956067d067460056cf3d8ac18b6d519bc7413dfe..3a92c363514ef353ca1b30816508b3c059a507f5 100755 --- a/run_http_interface.sh +++ b/run_http_interface.sh @@ -1,5 +1,5 @@ #!bin/bash -USER="pi" # change if other username +#USER="pi" # change if other username cd /home/$USER/OhmPi source /home/$USER/OhmPi/ohmpy/bin/activate python ohmpi.py & # run ohmpi.py to capture the commands