jupyterpidaq.DAQinstance

  1# Tools for using a Jupyter notebook as a lab notebook that collects and
  2# displays data from an analog to digital converter in real time. The interface
  3# also allows for annotation, analysis and display of the data using common
  4# python tools. Common activities can be done using menus and buttons rather
  5# than typing python commands.
  6# J. Gutow <gutow@uwosh.edu> July 2021
  7# license GPL V3 or greater.
  8
  9######
 10# Environment setup
 11######
 12# Use os tools for file path and such
 13import os
 14import time
 15import logging
 16
 17# Start Logging
 18import JPSLUtils
 19
 20logname = 'DAQinstance_' + time.strftime('%y-%m-%d_%H%M%S',
 21                                         time.localtime()) + '.log'
 22logging.basicConfig(filename=logname, level=logging.WARN)
 23# logging.basicConfig(filename=logname, level=logging.DEBUG)
 24
 25# below is equivalent to %matplotlib notebook in a Jupyter cell
 26from IPython import get_ipython
 27
 28ipython = get_ipython()
 29print('Importing drivers and searching for available data acquisition '
 30      'hardware.',end='')
 31
 32# imports below must work. Allow normal python error response.
 33import ipywidgets as widgets
 34from pandas_GUI import *
 35print ('.',end='')
 36
 37from plotly import io as pio
 38pio.templates.default = "simple_white" #default plot format
 39from plotly import graph_objects as go
 40print ('.',end='')
 41
 42import numpy as np
 43print ('.',end='')
 44
 45import pandas as pd
 46print ('.',end='')
 47
 48from IPython.display import display, HTML
 49from IPython.display import Javascript as JS
 50
 51print ('.',end='')
 52
 53# Below allows asynchronous calls to get and plot the data in real time.
 54# Actually read the DAQ board on a different process.
 55import threading
 56from multiprocessing import Process, Pipe
 57
 58print('.',end='')
 59
 60# The process that monitors the board
 61from jupyterpidaq.DAQProc import DAQProc
 62
 63print('.',end='')
 64
 65# Board definitions
 66from jupyterpidaq import Boards as boards
 67
 68print('.',end='')
 69
 70global availboards
 71availboards = boards.load_boards()
 72
 73print('.',end='')
 74
 75# GUI for settings
 76from jupyterpidaq.ChannelSettings import ChannelSettings
 77
 78print('.',end='')
 79
 80# Sensor definitions
 81from jupyterpidaq.Sensors import sensors
 82
 83print('.',end='')
 84
 85# JPSL Utilities
 86from JPSLMenus import *
 87from JPSLUtils import *
 88
 89print('.',end='')
 90
 91# globals to put stuff in from threads.
 92data = []  # all data from DAQ tools avg_values
 93stdev = []  # all standard deviations
 94timestamp = []  # all timestamps
 95
 96# global list to keep track of runs
 97runs = []
 98
 99######
100# Interactive elements definitions
101######
102
103# Locate JupyterPiDAQ package directory
104mydir = os.path.dirname(
105    __file__)  # absolute path to directory containing this file.
106
107# Add a "DAQ Menu" to the notebook.
108tempJSfile = open(os.path.join(mydir, 'javascript', 'JupyterPiDAQmnu.js'))
109tempscript = '<script type="text/javascript">'
110tempscript += tempJSfile.read() + '</script>'
111tempJSfile.close()
112display(HTML(tempscript))
113JPSLUtils.OTJS('createCmdMenu()')
114
115print('Done with setup.')
116
117# cleanup log file if it is empty
118logging.shutdown()
119try:
120    if os.stat(logname).st_size == 0:
121        os.remove(logname)
122except FileNotFoundError:
123    pass
124
125# Data Aquistion Instance (a run).
126class DAQinstance():
127    def __init__(self, idno, title='None', ntraces=4, **kwargs):
128        """
129        Data Aquistion Instance (a run).
130
131        :param idno : id number you wish to use to keep track
132        :param title: optional name
133        :param ntraces: number of traces (default = 4) more than 4 easily
134            overwhelms a pi4.
135        :param kwargs:
136            :ignore_skew: bool (default: True) if True only a single average
137            collection time will be recorded for each time in a multichannel
138            data collection. If False a separate set of time will be
139            recorded for each channel.
140        """
141        from plotly import graph_objects as go
142        self.ignore_skew = kwargs.pop('ignore_skew',True)
143        self.idno = idno
144        self.livefig = go.FigureWidget(layout_template='simple_white')
145        self.PLTconn, self.DAQconn = Pipe()
146        self.DAQCTL, self.PLTCTL = Pipe()
147        self.pltthread = threading.Thread(target=self.updatingplot, args=(
148                                        self.PLTconn, self.PLTCTL))
149        self.title = str(title)
150        self.svname = title + '.jpidaq.html'
151        self.averaging_time = 0.1  # seconds adjusted based on collection rate
152        self.gain = [1] * ntraces
153        self.data = []
154        self.timestamp = []
155        self.stdev = []
156        self.pandadf = None
157        self.ntraces = ntraces
158        self.separate_plots = True
159        self.traces = []
160        # index map from returned data to trace,
161        self.tracemap = []
162        self.tracefrdatachn = []
163        self.tracelbls = []
164        self.units = []
165        for i in range(self.ntraces):
166            self.traces.append(ChannelSettings(i, availboards))
167        self.ratemax = 20.0  # Hz
168        self.rate = 1.0  # Hz
169        self.deltamin = 1 / self.ratemax
170        self.delta = 1.0 / self.rate
171        self.setupbtn = widgets.Button(
172            description='Set Parameters',
173            disabled=False,
174            button_style='info',
175            # 'success', 'info', 'warning', 'danger' or ''
176            tooltip='Click to set collection parameters to displayed values.',
177            icon='')
178        self.collectbtn = widgets.Button(
179            description='Start Collecting',
180            disabled=False,
181            button_style='success',
182            # 'success', 'info', 'warning', 'danger' or ''
183            tooltip='Start collecting data and plotting it. '
184                    'Will make new graph.',
185            icon='')
186        self.separate_traces_checkbox = widgets.Checkbox(
187            value = self.separate_plots,
188            description = 'Display multiple traces as stacked graphs. ' \
189                        'Uncheck to display all on the same graph.',
190            layout = widgets.Layout(width='80%'),
191            disabled = False)
192        self.rateinp = widgets.BoundedFloatText(
193            value=self.rate,
194            min=0,
195            max=self.ratemax,
196            step=self.ratemax / 1000.0,
197            description='Rate (Hz):',
198            disabled=False)
199        self.timelbl = widgets.Text(
200            value='Time(s)',
201            placeholder='Type something',
202            description='X-axis label (time):',
203            disabled=False)
204        self.runtitle = widgets.Text(
205            value=self.title,
206            placeholder='Type title/description',
207            description='Run title',
208            disabled=False)
209        self.defaultparamtxt = ''
210        self.defaultcollecttxt = '<span style="color:blue;"> To accurately '
211        self.defaultcollecttxt += 'read point location on graph you can zoom '
212        self.defaultcollecttxt += 'in. Drag to zoom. Additional controls show '
213        self.defaultcollecttxt += 'above plot when you hover over it.</span>'
214        self.collecttxt = widgets.HTML(
215            value=self.defaultcollecttxt,
216            placeholder='',
217            description='')
218        self.setup_layout_bottom = widgets.HBox(
219            [self.rateinp, self.timelbl, self.setupbtn])
220        self.setup_layout = widgets.VBox([self.separate_traces_checkbox,
221                                          self.setup_layout_bottom])
222        self.collect_layout = widgets.HBox([self.collectbtn, self.collecttxt])
223        self.output = widgets.Output()
224    def _make_defaultparamtxt(self):
225        """
226        Uses AdvancedHTMLParser (mimics javascript) to generate valid HTML for
227        the default parameter text.
228        :return: valid html string for the default parameter text.
229        """
230        from AdvancedHTMLParser import AdvancedTag as domel
231        run_info=domel('div')
232        run_info.setAttribute('id','DAQRun_' + str(self.idno) + '_info')
233        run_info.setAttribute('class','run_info')
234        run_id = domel('table')
235        run_id.setAttribute('id','run_id')
236        run_id.setAttribute('border', '1')
237        tr = domel('tr')
238        tr.appendInnerHTML('<th>Title</th><th>Id #</th>')
239        run_id.appendChild(tr)
240        tr = domel('tr')
241        tr.appendInnerHTML('<td>' + str(self.title) + '</td>' \
242                            '<td>' + str(self.idno) + '</td>')
243        run_id.appendChild(tr)
244        run_info.appendChild(run_id)
245        # table of run parameters
246        run_param = domel('table')
247        run_param.setAttribute('border','1')
248        run_param.setAttribute('id','run_param')
249        tr = domel('tr')
250        tr.setAttribute('style','text-align:center;')
251        tr.appendInnerHTML('<th>Approx. Rate (Hz)</th>' \
252                                '<th>Approx. Delta (s)</th>' \
253                                '<th>X-label </th>' \
254                                '<th>X-cols</th>' \
255                                '<th>Y-cols</th>' \
256                                '<th>err-cols<sup ' \
257                                'style="color:blue;">a</sup></th>' \
258                                '<th>One Plot</th>' )
259        run_param.appendChild(tr)
260        run_info.appendChild(run_param)
261        tr = domel('tr')
262        tr.setAttribute('style','text-align:center;')
263        tr.appendInnerHTML('<td>' + str(self.rate) + '</td>' \
264                            '<td>' + str(self.delta) + '</td>' \
265                            '<td>' + self.timelbl.value + '</td>')
266        xlist = '['
267        ylist = '['
268        errlist = '['
269        if self.ignore_skew:
270            xlist += '0]'
271            tempcount = 0
272            for k in self.traces:
273                if k.isactive:
274                    ylist += str(2 * tempcount + 1) + ','
275                    errlist += str(2 * tempcount + 2) + ','
276                    tempcount += 1
277            ylist = ylist[:-1] + ']'
278            errlist = errlist[:-1] + ']'
279        else:
280            tempcount = 0
281            for k in self.traces:
282                if k.isactive:
283                    xlist += str(3 * tempcount) + ','
284                    ylist += str(3 * tempcount + 1) + ','
285                    errlist += str(3 * tempcount + 2) + ','
286                    tempcount += 1
287            xlist = xlist[:-1] + ']'
288            ylist = ylist[:-1] + ']'
289            errlist = errlist[:-1] + ']'
290        tr.appendInnerHTML('<td>' + xlist + '</td><td>' + ylist + '</td>')
291        td = domel('td')
292        td.appendText(errlist)
293        tr.appendChild(td)
294        td = domel('td')
295        td.appendText(str(not (self.separate_plots)))
296        tr.appendChild(td)
297        run_param.appendChild(tr)
298        footer = domel('tfoot')
299        tr = domel('tr')
300        td = domel('td')
301        td.setAttribute('colspan','7')
302        td.appendInnerHTML('<sup style="color:blue;">a</sup>The ' \
303                            'standard deviation of the number in the ' \
304                            'column immediately to the left based on the ' \
305                            'variation in signal during the averaging time ' \
306                            'for the data point.')
307        tr.appendChild(td)
308        footer.appendChild(tr)
309        run_param.appendChild(footer)
310        # table of trace information
311        traceinfo = domel('table')
312        traceinfo.setAttribute('class','traceinfo')
313        traceinfo.setAttribute('id','traceinfo')
314        traceinfo.setAttribute('border','1')
315        tr = domel('tr')
316        tr.setAttribute('style','text-align:center;')
317        tr.appendInnerHTML('<th>Trace #</th><th>Title</th><th>Units</th>' \
318                            '<th>Board</th><th>Channel</th><th>Sensor</th>' \
319                            '<th>Gain</th>')
320        traceinfo.appendChild(tr)
321        for i in range(self.ntraces):
322            if (self.traces[i].isactive):
323                self.tracemap.append(i)
324                tr = domel('tr')
325                tr.setAttribute('style', 'text-align:center;')
326                tr.appendInnerHTML('<td>' + str(i) + '</td>' \
327                            '<td>' + self.traces[i].tracelbl.value + '</td>' \
328                            '<td>' + self.traces[i].units.value + '</td>' \
329                            '<td>' + str(self.traces[i].boardchoice.value) + \
330                            ' ' + self.traces[i].board.name + '</td>' \
331                            '<td>' + str(self.traces[i].channel) + '</td>' \
332                        '<td >' + self.traces[i].sensorchoice.value + '</td>' \
333                            '<td>' + str(self.traces[i].gains.value) + '</td>')
334                traceinfo.appendChild(tr)
335        run_info.appendChild(traceinfo)
336        return run_info.asHTML()
337    
338    def _load_from_html(self, file):
339        """
340        Loads data and parameters for a completed run from a saved html file.
341        :param file: filename or path.
342        :return:
343        """
344        import pandas as pd
345        from JPSLUtils import find_pandas_dataframe_names
346        from AdvancedHTMLParser import AdvancedHTMLParser as parser
347        from plotly import graph_objects as go
348        whichrun = pd.read_html(file, attrs={'id': 'run_id'})[0]
349        run_title = whichrun['Title'][0]
350        run_id = whichrun['Id #'][0]
351        svname = pd.read_html(file, attrs={'id': 'file_info'})[0]['Saved ' \
352                                                                  'as'][0]
353        self.pandadf = pd.read_html(file, attrs={'class': 'dataframe'},
354                                    index_col=0)[0]
355        self.title = run_title
356        self.svname = svname
357        run_param = \
358        pd.read_html(file, attrs={'id': 'run_param'}, skiprows=[2])[0]
359        self.rate = run_param['Approx. Rate (Hz)'][0]
360        self.delta = run_param['Approx. Delta (s)'][0]
361        # reassiging timelbl to a value from a widget
362        self.timelbl = run_param['X-label'][0]
363        self.separate_plots = not (run_param['One Plot'][0])
364        xcols = list(map(int,run_param['X-cols'][0].replace('[',
365                                        '').replace(']','').split(',')))
366        ycols = list(map(int,run_param['Y-cols'][0].replace('[',
367                                        '').replace(']','').split(',')))
368        errcols = list(map(int,run_param['err-colsa'][0].replace('[',
369                                        '').replace(']', '').split(',')))
370        htmldatafile = parser(file)
371        self.defaultparamtxt = htmldatafile.getElementsByClassName(
372            'run_info')[0].asHTML()
373        traceinfo = pd.read_html(file, attrs={'id': 'traceinfo'})[0]
374        for k in traceinfo.index:
375            # Do not refill the widgets. This truncates and changes the
376            # definitions of some things from widgets to values.
377            self.traces[k].isactive = True
378            self.traces[k].tracelbl= traceinfo['Title'][k]
379            self.traces[k].units = traceinfo['Units'][k]
380            boardchoice, boardname = (traceinfo['Board'][k]).split(' ',1)
381            self.traces[k].boardchoice = boardchoice
382            self.traces[k].board = boardname
383            self.traces[k].channel = traceinfo['Channel'][k]
384            self.traces[k].gains= traceinfo['Gain'][k]
385            self.traces[k].sensor = traceinfo['Sensor'][k]
386        # Plot
387        if self.separate_plots:
388            self.livefig.set_subplots(rows=len(ycols), cols=1,
389                                                  shared_xaxes=True)
390            self.livefig.update_xaxes(
391                title=self.timelbl, row=len(ycols), col=1)
392        else:
393            self.livefig.update_xaxes(title=self.timelbl)
394            self.livefig.update_yaxes(title="Values")
395        for i in range(len(ycols)):
396            namestr = self.pandadf.columns[ycols[i]]
397            xcol = None
398            if len(xcols) == 1:
399                xcol = xcols[0]
400            else:
401                xcol = xcols[i]
402            scat = go.Scatter(
403                y=self.pandadf.iloc[0:, ycols[i]],
404                x=self.pandadf.iloc[0:,xcol], name=namestr)
405            if self.separate_plots:
406                self.livefig.update_yaxes(title=self.traces[i].units,
407                    row=i+1, col=1)
408                self.livefig.add_trace(scat, row=i+1, col=1)
409            else:
410                self.livefig.add_trace(scat)
411        pass
412
413    def setupclick(self, btn):
414        # Could just use the values in widgets, but this forces intentional
415        # selection and locks them for the run.
416        from copy import copy
417        self.title = copy(self.runtitle.value)
418        self.rate = copy(self.rateinp.value)
419        self.delta = 1 / self.rate
420        self.separate_plots = copy(self.separate_traces_checkbox.value)
421        self.defaultparamtxt = self._make_defaultparamtxt()
422        self.runtitle.close()
423        del self.runtitle
424        self.setup_layout.close()
425        del self.setup_layout
426        self.output.clear_output()
427        doRun(runs[self.idno-1])
428        with self.output:
429            display(runs[self.idno - 1].livefig)
430        pass
431
432    def setup(self):
433        with self.output:
434            display(HTML("<h3 id ='RunSetUp' "
435                         "style='text-align:center;'>Set Run Parameters</h3>"))
436            self.setupbtn.on_click(self.setupclick)
437            display(self.runtitle)
438            for i in range(self.ntraces):
439                self.traces[i].setup()
440            display(self.setup_layout)
441        display(self.output)
442        pass
443
444    def collectclick(self, btn):
445        if (btn.description == 'Start Collecting'):
446            btn.description = 'Stop Collecting'
447            btn.button_style = 'danger'
448            btn.tooltip = 'Stop the data collection'
449            # do not allow parameters to be reset after starting run.
450            self.setupbtn.disabled = True
451            self.setupbtn.tooltip = 'Parameters locked. The run has started.'
452            self.rateinp.disabled = True
453            self.timelbl.disabled = True
454            nactive = 0
455            for k in self.traces:
456                if k.isactive:
457                    nactive += 1
458            whichchn = []
459            gains =[]
460            for i in range(self.ntraces):
461                if (self.traces[i].isactive):
462                    brd = self.traces[i].board
463                    chn = self.traces[i].channel
464                    newchn = True
465                    if len(whichchn) > 0:
466                        for k in range(len(whichchn)):
467                            if whichchn[k]['board'] == brd \
468                                and whichchn[k]['chnl'] == chn:
469                                self.tracefrdatachn.append(k)
470                                newchn = False
471                    if newchn:
472                        whichchn.append({'board': brd,
473                                         'chnl': chn})
474                        gains.append(self.traces[i].toselectedgain)
475                        self.tracefrdatachn.append(len(whichchn)-1)
476            # Use up to 30% of the time for averaging if channels were spaced
477            # evenly between data collection times (with DACQ2 they appear
478            # more synchronous than that).
479            self.averaging_time = self.delta / nactive / 3
480            DAQ = Process(target=DAQProc,
481                          args=(
482                              whichchn, gains, self.averaging_time, self.delta,
483                              self.DAQconn, self.DAQCTL))
484            DAQ.start()
485            self.pltthread.start()
486            # self.updatingplot() hangs up user interface
487        else:
488            btn.description = 'Done'
489            btn.button_style = ''
490            btn.tooltip = ''
491            # wait a plotting thread to terminate
492            self.pltthread.join()
493            self.data = data
494            self.timestamp = timestamp
495            self.stdev = stdev
496            self.fillpandadf()
497            # save data to html file so it is human readable and can be loaded
498            # elsewhere.
499            #self.svname = self.title + '_' + time.strftime('%y-%m-%d_%H%M%S',
500                                       # time.localtime()) + '.html'
501            svhtml = '<!DOCTYPE html>' \
502                     '<html><body>'+ self.defaultparamtxt + \
503                     '<table id="file_info" border="1"><tr><th>Saved as ' \
504                     '</th></tr><tr><td>' +  \
505                     self.svname+'</td></tr></table>' \
506                     '<h2>DATA</h2>'+ \
507                     self.pandadf.to_html() + '</body></html>'
508            f = open(self.svname,'w')
509            f.write(svhtml)
510            f.close()
511            self.collectbtn.close()
512            del self.collectbtn
513            with self.output:
514                display(HTML(
515                    '<span style="color:blue;font-weight:bold;">DATA SAVED TO:' +
516                    self.svname + '</span>'))
517        return
518
519    def fillpandadf(self):
520        datacolumns = []
521        temptimes = np.transpose(self.timestamp)
522        tempdata = np.transpose(self.data)
523        tempstdev = np.transpose(self.stdev)
524        chncnt = 0
525        for i in range(self.ntraces):
526            if (self.traces[i].isactive):
527                chncnt += 1
528        for i in range(chncnt):
529            if self.ignore_skew and i > 0:
530                pass
531            else:
532                datacolumns.append(temptimes[i])
533            datacolumns.append(tempdata[i])
534            datacolumns.append(tempstdev[i])
535        titles = []
536        # Column labels.
537        chncnt = 0
538        for i in range(self.ntraces):
539            if (self.traces[i].isactive):
540                chncnt += 1
541                if self.ignore_skew:
542                    if chncnt == 1:
543                        titles.append(self.timelbl.value)
544                    pass
545                else:
546                    titles.append(self.traces[
547                                  i].tracelbl.value + '_' + self.timelbl.value)
548                titles.append(
549                    self.traces[i].tracelbl.value + '(' + self.traces[
550                        i].units.value + ')')
551                titles.append(
552                    self.traces[i].tracelbl.value + '_' + 'stdev')
553        #print(str(titles))
554        #print(str(datacolumns))
555        self.pandadf = pd.DataFrame(np.transpose(datacolumns), columns=titles)
556
557    def updatingplot(self, PLTconn, PLTCTL):
558        """
559        Runs until a check of self.collectbtn.description does not return
560        'Stop Collecting'. This would probably be more efficient if set a
561        boolean.
562        Parameters
563        ----------
564        PLTconn: Pipe
565            connection plotter end
566        DAQconn: Pipe
567            connection DAQ end
568        PLTCTL: Pipe
569            control pipe plotter end
570        DAQCTL: Pipe
571            control pipe DAQ end
572        """
573        starttime = time.time()
574        global data
575        data = []
576        global timestamp
577        timestamp = []
578        global stdev
579        stdev = []
580        datalegend = []
581        timelegend = []
582        stdevlegend = []
583        whichchn = []
584        gains = []
585        toplotx = []
586        toploty = []
587        nactive = 0
588        def convert_pkg():
589            plttime = 0
590            if self.ignore_skew:
591                plttime = sum(pkg[0]) / len(pkg[0])
592            traceidx = 0
593            tmptime = []
594            tmpavg = []
595            tmpstd = []
596            tmpavg_std = []
597            for i, k in zip(self.tracemap, self.tracefrdatachn):
598
599                avg = pkg[1][k]
600                std = pkg[2][k]
601                avg_std = pkg[3][k]
602                avg_vdd = pkg[4][k]
603                avg, std, avg_std = self.traces[i].toselectedunits(avg,
604                                                                   std, avg_std,
605                                                                   avg_vdd)
606                avg, std, avg_std = sensors. \
607                    to_reasonable_significant_figures_fast(avg, std, avg_std)
608                tmptime.append(pkg[0][k])
609                tmpavg.append(avg)
610                tmpstd.append(std)
611                tmpavg_std.append(avg_std)
612                if self.ignore_skew:
613                    toplotx[traceidx].append(plttime)
614                else:
615                    toplotx[traceidx].append(pkg[0][k])
616                toploty[traceidx].append(avg)
617                traceidx += 1
618            timestamp.append(tmptime)
619            data.append(tmpavg)
620            stdev.append(tmpavg_std)
621            return
622
623        for k in self.traces:
624            if k.isactive:
625                nactive += 1
626        if self.separate_plots:
627            self.livefig.set_subplots(rows = nactive, cols = 1,
628                                      shared_xaxes= True)
629            self.livefig.update_xaxes(title = self.timelbl.value,
630                                      row = nactive, col = 1)
631        else:
632            self.livefig.update_yaxes(title = "Values")
633            self.livefig.update_xaxes(title = self.timelbl.value)
634        active_count = 0
635        for i in range(self.ntraces):
636            if (self.traces[i].isactive):
637                active_count += 1
638                tempstr = self.traces[i].tracelbl.value + '(' + \
639                          self.traces[i].units.value + ')'
640                timelegend.append('time_' + tempstr)
641                datalegend.append(tempstr)
642                stdevlegend.append('stdev_' + tempstr)
643                if self.separate_plots:
644                    scat = go.Scatter(y=[],x=[], name=tempstr)
645                    self.livefig.add_trace(scat, row = active_count,
646                                           col = 1)
647                    self.livefig.update_yaxes(title = self.traces[
648                        i].units.value, row = active_count, col = 1)
649                else:
650                    self.livefig.add_scatter(y=[],x=[], name=tempstr)
651                toplotx.append([])
652                toploty.append([])
653        lastupdatetime = time.time()
654
655        pts = 0
656        oldpts = 0
657        #print('about to enter while loop',end='')
658        while (self.collectbtn.description == 'Stop Collecting'):
659            #print('.',end='')
660            while PLTconn.poll():
661                pkg = PLTconn.recv()
662                self.lastpkgstr = str(pkg)
663                #print(self.lastpkgstr)
664                # convert voltage to requested units.
665                convert_pkg()
666            currenttime = time.time()
667            mindelay = 1.0
668            if self.separate_traces_checkbox.value:
669                mindelay = nactive*1.0
670            else:
671                mindelay = nactive*0.5
672            if (currenttime - lastupdatetime)>(mindelay+len(toplotx[0])*len(
673                    toplotx)/1000):
674                lastupdatetime = currenttime
675                for k in range(len(self.livefig.data)):
676                    self.livefig.data[k].x=toplotx[k]
677                    self.livefig.data[k].y=toploty[k]
678            #time.sleep(1)
679            PLTCTL.send('send')
680            time.sleep(self.delta)
681            # print ('btn.description='+str(btn.description))
682        endtime = time.time()
683        PLTCTL.send('stop')
684        time.sleep(0.5)  # wait 0.5 second to collect remaining data
685        PLTCTL.send('send')
686        time.sleep(0.5)
687        msg = ''
688        while (msg != 'done'):
689            while PLTconn.poll():
690                pkg = PLTconn.recv()
691                # print(str(pkg))
692                # convert voltage to requested units.
693                convert_pkg()
694            PLTCTL.send('send')
695            time.sleep(0.2)
696            if PLTCTL.poll():
697                msg = PLTCTL.recv()
698                # print (str(msg))
699                if (msg != 'done'):
700                    print('Received unexpected message: ' + str(msg))
701        for k in range(len(self.livefig.data)):
702            self.livefig.data[k].x = toplotx[k]
703            self.livefig.data[k].y = toploty[k]
704        return
705
706# TODO delete newRun once sure not needed.
707# def newRun(livefig):
708#     """
709#     Set up a new data collection run and add it to the list of runs.
710#     """
711#     nrun = len(runs) + 1
712#     runs.append(DAQinstance(nrun, livefig, title='Run-' + str(nrun)))
713#     runs[nrun - 1].setup()
714#     pass
715
716def Run(name):
717    """Load a run from stored data or start a new run if the local file for
718    the run does not exist.
719    Parameters
720    ----------
721    name: str
722        String name for the run. The data will be stored in a file of this
723        name with the extension of `.jpidaq.html`.
724    """
725    from pathlib import Path
726    from IPython import get_ipython
727    from IPython.display import display
728    global_dict = get_ipython().user_ns
729    runs = None
730    if 'runs' in global_dict and 'DAQinstance' in global_dict:
731        runs = global_dict['runs']
732    else:
733        return ('Initialization of JupyterPiDAQ required')
734    # Check if run completed, if so reload data, display and exit
735    datafilepath = Path.cwd() / Path(str(name) + '.jpidaq.html')
736    if datafilepath.exists():
737        # display the data as a live plotly plot.
738        svname = name + '.jpidaq.html'
739        runs.append(DAQinstance(len(runs)+1, title = name))
740        runs[-1]._load_from_html(svname)
741        display(HTML(runs[-1].defaultparamtxt))
742        display(HTML('<h3>Saved as: '+runs[-1].svname+'</h3>'))
743        display(runs[-1].livefig)
744        display(HTML(runs[-1].defaultcollecttxt))
745        return
746    nrun = len(runs) + 1
747    runs.append(DAQinstance(nrun, title=name))
748    runs[-1].setup()
749    return
750
751def doRun(whichrun):
752    with whichrun.output:
753        display(HTML('<span id="LiveRun_'+str(whichrun.idno)+'"></span>'))
754        display(HTML(whichrun.defaultparamtxt))
755        if hasattr(whichrun, "collectbtn"):
756            # only show if hasn't already collected data
757            whichrun.collectbtn.on_click(whichrun.collectclick)
758            display(whichrun.collectbtn)
759        display(HTML(whichrun.defaultcollecttxt))
760    pass
761
762# TODO delete displayRun once not needed.
763# def displayRun(runidx,file):
764#     """
765#     Displays a run. It can fall back to loading from a file if the outputarea
766#     is accidentally cleared.
767#     :param runidx: index+1 for the run in the runs array. Thus, the run id #.
768#     :param file: name of the file the run is saved to
769#     :return: A string warning if things are not initialized properly.
770#     """
771#     from IPython import get_ipython
772#     from JPSLUtils import find_pandas_dataframe_names, find_figure_names
773#     idxnum = runidx - 1
774#     run_id_table = pd.read_html(file, attrs={'id': 'run_id'})[0]
775#     run_title = run_id_table['Title'][0]
776#     run_id = run_id_table['Id #'][0]
777#     svname = pd.read_html(file, attrs={'id': 'file_info'})[0]['Saved as'][0]
778#     global_dict = get_ipython().user_ns
779#     runs = None
780#     if 'runs' in global_dict and 'DAQinstance' in global_dict:
781#         runs = global_dict['runs']
782#     else:
783#         return ('Initialization of JupyterPiDAQ required')
784#     exists = None
785#     if len(runs)>=runidx:
786#         if isinstance(runs[idxnum].livefig,go.FigureWidget) and runs[
787#             idxnum].idno == run_id and runs[idxnum].svname ==svname:
788#             exists = True
789#         else:
790#             exists = False
791#     if exists:
792#         display(HTML(runs[idxnum].defaultparamtxt))
793#         display(HTML('<h3>Saved as: '+runs[idxnum].svname+'</h3>'))
794#         runs[idxnum].livefig.show()
795#         display(HTML(runs[idxnum].defaultcollecttxt))
796#         JPSLUtils.select_containing_cell("LiveRun_"+str(runidx))
797#         JPSLUtils.delete_selected_cell()
798#     else:
799#         # Fall back on loading the data from the default save file.
800#         # Note: the file must be available.
801#         nrunfigs = 0
802#         for k in find_figure_names():
803#             if k.startswith('run_fig'):
804#                 nrunfigs+=1
805#         runfigname = 'run_fig'+str(nrunfigs+1)
806#         global_dict[runfigname] = go.FigureWidget()
807#         fig = global_dict[runfigname]
808#         runs.append(DAQinstance(run_id, fig, title=run_title))
809#         idxnum = len(runs)-1
810#         runs[idxnum]._load_from_html(file)
811#         display(HTML(runs[idxnum].defaultparamtxt))
812#         display(HTML('<h3>Saved as: ' + runs[idxnum].svname + '</h3>'))
813#         runs[idxnum].livefig.show()
814#         display(HTML(runs[idxnum].defaultcollecttxt))
815#     # protect the cell
816#     JPSLUtils.OTJS('protect_selected_cells();')
817#     pass
818
819def update_runsdrp():
820    # get list of runs
821    runlst = [('Choose Run', -1)]
822    for i in range(len(runs)):
823        runlst.append((str(i + 1) + ': ' + runs[i].title, i))
824    # buid selection menu
825    global runsdrp
826    runsdrp = widgets.Dropdown(
827        options=runlst,
828        value=-1,
829        description='Select Run #:',
830        disabled=False,
831    )
832    pass
833
834def showSelectedRunTable(change):
835    global runsdrp
836    global last_run_table_out
837    whichrun = runsdrp.value
838    runsdrp.close()
839    last_run_table_out.clear_output()
840    tbldiv = '<div style="height:10em;">' + str(runs[whichrun].title)
841    tbldiv += str(runs[whichrun].pandadf.to_html()) + '</div>'
842    with last_run_table_out:
843        display(HTML(tbldiv))
844    return
845
846def showDataTable():
847    """
848    Provides a menu to select which run. Then displays the run in a
849    10 em high scrolling table. Selection menu is removed after choice
850    is made.
851    """
852    from ipywidgets import Output
853    global last_run_table_out
854    last_run_table_out = Output()
855    update_runsdrp()
856    global runsdrp
857    runsdrp.observe(showSelectedRunTable, names='value')
858    with last_run_table_out:
859        display(runsdrp)
860    display(last_run_table_out)
861    # will display selected run and delete menu upon selection.
862    return
863def newCalculatedColumn():
864    """
865    Uses jupyter-pandas-GUI.new_pandas_column_GUI to provide a GUI expression
866    composer. This method finds the datasets and launches the GUI.
867    """
868    df_info = []
869    for i in range(len(runs)):
870        df_info.append([runs[i].pandadf, 'runs['+str(i)+'].pandadf',
871                        str(runs[i].title)])
872    new_pandas_column_GUI(df_info)
873    pass
874
875def newPlot():
876    """
877    Uses jupyter-pandas-GUI.plot_pandas_GUI to provide a GUI expression
878    composer. This method finds the datasets and launches the GUI.
879    """
880    df_info = []
881    for i in range(len(runs)):
882        if isinstance(runs[i].pandadf,pd.DataFrame):
883            df_info.append([runs[i].pandadf, 'runs['+str(i)+'].pandadf',
884                            str(runs[i].title)])
885    plot_pandas_GUI(df_info)
886    pass
887
888def newFit():
889    """
890    Uses jupyter-pandas-GUI.fit_pandas_GUI to provide a GUI expression
891    composer. This method finds the datasets and launches the GUI.
892    """
893    df_info = []
894    for i in range(len(runs)):
895        if isinstance(runs[i].pandadf,pd.DataFrame):
896            df_info.append([runs[i].pandadf, 'runs['+str(i)+'].pandadf',
897                            str(runs[i].title)])
898    fit_pandas_GUI(df_info)
899    pass
logname = 'DAQinstance_24-07-10_174850.log'
ipython = None
data = []
stdev = []
timestamp = []
runs = []
mydir = '/home/jonathan/GIT/JupyterPiDAQ/jupyterpidaq'
tempJSfile = <_io.TextIOWrapper name='/home/jonathan/GIT/JupyterPiDAQ/jupyterpidaq/javascript/JupyterPiDAQmnu.js' mode='r' encoding='UTF-8'>
tempscript = '<script type="text/javascript">// TODO: isolate under a name such as juputerPiDAQ.\nvar insertruncount = 0\nvar newrunstr = \'# EDIT THE COMMAND BELOW BY PROVIDING A RUN NAME.\\n\'\nnewrunstr += \'# The name should be surrounded by double quotes ("run_name").\\n\'\nnewrunstr += \'# using _ instead of spaces will avoid problems, especially on \'\nnewrunstr += \'Windows machines.\\n\'\nnewrunstr += \'Run("REPLACE_ME_WITH_NAME_FOR_RUN") # Initiate run or load a \'\nnewrunstr += \'completed run.\'\n\nfunction insertnewRun(){\n //Insert a cell below the current selection\n Jupyter.notebook.insert_cell_below();\n Jupyter.notebook.select_next(true);\n Jupyter.notebook.focus_cell();\n var currentcell = Jupyter.notebook.get_selected_cell();\n insertruncount += 1\n var cmdstr = newrunstr\n currentcell.set_text(cmdstr);\n //currentcell.execute();\n}\n\nfunction addnewRun(){\n //find the last cell in notebook\n var lastcellidx = Jupyter.notebook.ncells()-1;\n var lastcell=Jupyter.notebook.get_cell(lastcellidx);\n Jupyter.notebook.select(lastcellidx);\n //If the cell is empty put command in it. Otherwise\n //add another cell at the end of the worksheet. Then\n //put the command in the new lastcell.\n insertruncount += 1\n var cmdstr = newrunstr\n if(lastcell.get_text()==\'\'){\n lastcell.set_text(cmdstr);\n }else{\n Jupyter.notebook.insert_cell_below();\n Jupyter.notebook.select_next(true);\n Jupyter.notebook.focus_cell();\n lastcell=Jupyter.notebook.get_cell(lastcellidx+1);\n lastcell.set_text(cmdstr);\n }\n //lastcell.execute();\n}\n\nfunction showDataTable(){\n //find the currently active cell\n var currentcell = Jupyter.notebook.get_selected_cell();\n //Because we could destroy date created by having run\n //this cell previously do not use this cell if it contains\n //anything\n if (currentcell.get_text()==\'\'){\n currentcell.set_text(\'showDataTable()\');\n }else{\n Jupyter.notebook.insert_cell_below();\n Jupyter.notebook.select_next(true);\n Jupyter.notebook.focus_cell();\n currentcell = Jupyter.notebook.get_selected_cell();\n currentcell.set_text(\'showDataTable()\');\n }\n currentcell.execute();\n}\n\nfunction newCalculatedColumn(){\n //Insert a cell below the current selection\n Jupyter.notebook.insert_cell_below();\n Jupyter.notebook.select_next(true);\n Jupyter.notebook.focus_cell();\n var currentcell = Jupyter.notebook.get_selected_cell();\n currentcell.set_text(\'newCalculatedColumn()\');\n currentcell.execute();\n}\n\nfunction newPlot(){\n //Insert a cell below the current selection\n Jupyter.notebook.insert_cell_below();\n Jupyter.notebook.select_next(true);\n Jupyter.notebook.focus_cell();\n var currentcell = Jupyter.notebook.get_selected_cell();\n currentcell.set_text(\'newPlot()\');\n currentcell.execute();\n}\n\nfunction newFit(){\n //Insert a cell below the current selection\n Jupyter.notebook.insert_cell_below();\n Jupyter.notebook.select_next(true);\n Jupyter.notebook.focus_cell();\n var currentcell = Jupyter.notebook.get_selected_cell();\n currentcell.set_text(\'newFit()\');\n currentcell.execute();\n}\nfunction protect_selected_cells(){\n var celllist = Jupyter.notebook.get_selected_cells();\n for (var i = 0;i<celllist.length;i++){\n celllist[i].metadata.editable=false;\n }\n}\n\nfunction createCmdMenu(){\n if(JPSLUtils.env !=\'NBClassic\'){\n console.log(\'Not in classic notebook. DAQ_commands classic menu skipped.\');\n return;\n }\n if(!document.getElementById(\'DAQ_commands\')){\n var instrun = {\'type\':\'action\',\n \'title\':\'Insert New Run after selection...\',\n \'data\':"insertnewRun();"\n };\n var appendrun = {\'type\':\'action\',\n \'title\':\'Append New Run to end...\',\n \'data\':"addnewRun();"\n };\n var showdata = {\'type\':\'action\',\n \'title\':\'Show data in table...\',\n \'data\':"showDataTable();"\n };\n var calccol = {\'type\':\'action\',\n \'title\':\'Calculate new column...\',\n \'data\':"newCalculatedColumn();"\n };\n var istplt = {\'type\':\'action\',\n \'title\':\'Insert new plot after selection...\',\n \'data\':"newPlot();"\n };\n var istfit = {\'type\':\'action\',\n \'title\':\'Insert new fit after selection...\',\n \'data\':"newFit();"\n };\n var menu = {\'type\':\'menu\',\n \'title\':\'DAQ commands\',\n \'data\':[instrun, appendrun, showdata, calccol, istplt,\n istfit]\n };\n JPSLMenus.build(menu);\n }\n}\n\nfunction deleteCmdMenu(){\n if(document.getElementById(\'DAQ_commands\')){\n document.getElementById(\'DAQ_commands\').remove();\n }\n}</script>'
class DAQinstance:
127class DAQinstance():
128    def __init__(self, idno, title='None', ntraces=4, **kwargs):
129        """
130        Data Aquistion Instance (a run).
131
132        :param idno : id number you wish to use to keep track
133        :param title: optional name
134        :param ntraces: number of traces (default = 4) more than 4 easily
135            overwhelms a pi4.
136        :param kwargs:
137            :ignore_skew: bool (default: True) if True only a single average
138            collection time will be recorded for each time in a multichannel
139            data collection. If False a separate set of time will be
140            recorded for each channel.
141        """
142        from plotly import graph_objects as go
143        self.ignore_skew = kwargs.pop('ignore_skew',True)
144        self.idno = idno
145        self.livefig = go.FigureWidget(layout_template='simple_white')
146        self.PLTconn, self.DAQconn = Pipe()
147        self.DAQCTL, self.PLTCTL = Pipe()
148        self.pltthread = threading.Thread(target=self.updatingplot, args=(
149                                        self.PLTconn, self.PLTCTL))
150        self.title = str(title)
151        self.svname = title + '.jpidaq.html'
152        self.averaging_time = 0.1  # seconds adjusted based on collection rate
153        self.gain = [1] * ntraces
154        self.data = []
155        self.timestamp = []
156        self.stdev = []
157        self.pandadf = None
158        self.ntraces = ntraces
159        self.separate_plots = True
160        self.traces = []
161        # index map from returned data to trace,
162        self.tracemap = []
163        self.tracefrdatachn = []
164        self.tracelbls = []
165        self.units = []
166        for i in range(self.ntraces):
167            self.traces.append(ChannelSettings(i, availboards))
168        self.ratemax = 20.0  # Hz
169        self.rate = 1.0  # Hz
170        self.deltamin = 1 / self.ratemax
171        self.delta = 1.0 / self.rate
172        self.setupbtn = widgets.Button(
173            description='Set Parameters',
174            disabled=False,
175            button_style='info',
176            # 'success', 'info', 'warning', 'danger' or ''
177            tooltip='Click to set collection parameters to displayed values.',
178            icon='')
179        self.collectbtn = widgets.Button(
180            description='Start Collecting',
181            disabled=False,
182            button_style='success',
183            # 'success', 'info', 'warning', 'danger' or ''
184            tooltip='Start collecting data and plotting it. '
185                    'Will make new graph.',
186            icon='')
187        self.separate_traces_checkbox = widgets.Checkbox(
188            value = self.separate_plots,
189            description = 'Display multiple traces as stacked graphs. ' \
190                        'Uncheck to display all on the same graph.',
191            layout = widgets.Layout(width='80%'),
192            disabled = False)
193        self.rateinp = widgets.BoundedFloatText(
194            value=self.rate,
195            min=0,
196            max=self.ratemax,
197            step=self.ratemax / 1000.0,
198            description='Rate (Hz):',
199            disabled=False)
200        self.timelbl = widgets.Text(
201            value='Time(s)',
202            placeholder='Type something',
203            description='X-axis label (time):',
204            disabled=False)
205        self.runtitle = widgets.Text(
206            value=self.title,
207            placeholder='Type title/description',
208            description='Run title',
209            disabled=False)
210        self.defaultparamtxt = ''
211        self.defaultcollecttxt = '<span style="color:blue;"> To accurately '
212        self.defaultcollecttxt += 'read point location on graph you can zoom '
213        self.defaultcollecttxt += 'in. Drag to zoom. Additional controls show '
214        self.defaultcollecttxt += 'above plot when you hover over it.</span>'
215        self.collecttxt = widgets.HTML(
216            value=self.defaultcollecttxt,
217            placeholder='',
218            description='')
219        self.setup_layout_bottom = widgets.HBox(
220            [self.rateinp, self.timelbl, self.setupbtn])
221        self.setup_layout = widgets.VBox([self.separate_traces_checkbox,
222                                          self.setup_layout_bottom])
223        self.collect_layout = widgets.HBox([self.collectbtn, self.collecttxt])
224        self.output = widgets.Output()
225    def _make_defaultparamtxt(self):
226        """
227        Uses AdvancedHTMLParser (mimics javascript) to generate valid HTML for
228        the default parameter text.
229        :return: valid html string for the default parameter text.
230        """
231        from AdvancedHTMLParser import AdvancedTag as domel
232        run_info=domel('div')
233        run_info.setAttribute('id','DAQRun_' + str(self.idno) + '_info')
234        run_info.setAttribute('class','run_info')
235        run_id = domel('table')
236        run_id.setAttribute('id','run_id')
237        run_id.setAttribute('border', '1')
238        tr = domel('tr')
239        tr.appendInnerHTML('<th>Title</th><th>Id #</th>')
240        run_id.appendChild(tr)
241        tr = domel('tr')
242        tr.appendInnerHTML('<td>' + str(self.title) + '</td>' \
243                            '<td>' + str(self.idno) + '</td>')
244        run_id.appendChild(tr)
245        run_info.appendChild(run_id)
246        # table of run parameters
247        run_param = domel('table')
248        run_param.setAttribute('border','1')
249        run_param.setAttribute('id','run_param')
250        tr = domel('tr')
251        tr.setAttribute('style','text-align:center;')
252        tr.appendInnerHTML('<th>Approx. Rate (Hz)</th>' \
253                                '<th>Approx. Delta (s)</th>' \
254                                '<th>X-label </th>' \
255                                '<th>X-cols</th>' \
256                                '<th>Y-cols</th>' \
257                                '<th>err-cols<sup ' \
258                                'style="color:blue;">a</sup></th>' \
259                                '<th>One Plot</th>' )
260        run_param.appendChild(tr)
261        run_info.appendChild(run_param)
262        tr = domel('tr')
263        tr.setAttribute('style','text-align:center;')
264        tr.appendInnerHTML('<td>' + str(self.rate) + '</td>' \
265                            '<td>' + str(self.delta) + '</td>' \
266                            '<td>' + self.timelbl.value + '</td>')
267        xlist = '['
268        ylist = '['
269        errlist = '['
270        if self.ignore_skew:
271            xlist += '0]'
272            tempcount = 0
273            for k in self.traces:
274                if k.isactive:
275                    ylist += str(2 * tempcount + 1) + ','
276                    errlist += str(2 * tempcount + 2) + ','
277                    tempcount += 1
278            ylist = ylist[:-1] + ']'
279            errlist = errlist[:-1] + ']'
280        else:
281            tempcount = 0
282            for k in self.traces:
283                if k.isactive:
284                    xlist += str(3 * tempcount) + ','
285                    ylist += str(3 * tempcount + 1) + ','
286                    errlist += str(3 * tempcount + 2) + ','
287                    tempcount += 1
288            xlist = xlist[:-1] + ']'
289            ylist = ylist[:-1] + ']'
290            errlist = errlist[:-1] + ']'
291        tr.appendInnerHTML('<td>' + xlist + '</td><td>' + ylist + '</td>')
292        td = domel('td')
293        td.appendText(errlist)
294        tr.appendChild(td)
295        td = domel('td')
296        td.appendText(str(not (self.separate_plots)))
297        tr.appendChild(td)
298        run_param.appendChild(tr)
299        footer = domel('tfoot')
300        tr = domel('tr')
301        td = domel('td')
302        td.setAttribute('colspan','7')
303        td.appendInnerHTML('<sup style="color:blue;">a</sup>The ' \
304                            'standard deviation of the number in the ' \
305                            'column immediately to the left based on the ' \
306                            'variation in signal during the averaging time ' \
307                            'for the data point.')
308        tr.appendChild(td)
309        footer.appendChild(tr)
310        run_param.appendChild(footer)
311        # table of trace information
312        traceinfo = domel('table')
313        traceinfo.setAttribute('class','traceinfo')
314        traceinfo.setAttribute('id','traceinfo')
315        traceinfo.setAttribute('border','1')
316        tr = domel('tr')
317        tr.setAttribute('style','text-align:center;')
318        tr.appendInnerHTML('<th>Trace #</th><th>Title</th><th>Units</th>' \
319                            '<th>Board</th><th>Channel</th><th>Sensor</th>' \
320                            '<th>Gain</th>')
321        traceinfo.appendChild(tr)
322        for i in range(self.ntraces):
323            if (self.traces[i].isactive):
324                self.tracemap.append(i)
325                tr = domel('tr')
326                tr.setAttribute('style', 'text-align:center;')
327                tr.appendInnerHTML('<td>' + str(i) + '</td>' \
328                            '<td>' + self.traces[i].tracelbl.value + '</td>' \
329                            '<td>' + self.traces[i].units.value + '</td>' \
330                            '<td>' + str(self.traces[i].boardchoice.value) + \
331                            ' ' + self.traces[i].board.name + '</td>' \
332                            '<td>' + str(self.traces[i].channel) + '</td>' \
333                        '<td >' + self.traces[i].sensorchoice.value + '</td>' \
334                            '<td>' + str(self.traces[i].gains.value) + '</td>')
335                traceinfo.appendChild(tr)
336        run_info.appendChild(traceinfo)
337        return run_info.asHTML()
338    
339    def _load_from_html(self, file):
340        """
341        Loads data and parameters for a completed run from a saved html file.
342        :param file: filename or path.
343        :return:
344        """
345        import pandas as pd
346        from JPSLUtils import find_pandas_dataframe_names
347        from AdvancedHTMLParser import AdvancedHTMLParser as parser
348        from plotly import graph_objects as go
349        whichrun = pd.read_html(file, attrs={'id': 'run_id'})[0]
350        run_title = whichrun['Title'][0]
351        run_id = whichrun['Id #'][0]
352        svname = pd.read_html(file, attrs={'id': 'file_info'})[0]['Saved ' \
353                                                                  'as'][0]
354        self.pandadf = pd.read_html(file, attrs={'class': 'dataframe'},
355                                    index_col=0)[0]
356        self.title = run_title
357        self.svname = svname
358        run_param = \
359        pd.read_html(file, attrs={'id': 'run_param'}, skiprows=[2])[0]
360        self.rate = run_param['Approx. Rate (Hz)'][0]
361        self.delta = run_param['Approx. Delta (s)'][0]
362        # reassiging timelbl to a value from a widget
363        self.timelbl = run_param['X-label'][0]
364        self.separate_plots = not (run_param['One Plot'][0])
365        xcols = list(map(int,run_param['X-cols'][0].replace('[',
366                                        '').replace(']','').split(',')))
367        ycols = list(map(int,run_param['Y-cols'][0].replace('[',
368                                        '').replace(']','').split(',')))
369        errcols = list(map(int,run_param['err-colsa'][0].replace('[',
370                                        '').replace(']', '').split(',')))
371        htmldatafile = parser(file)
372        self.defaultparamtxt = htmldatafile.getElementsByClassName(
373            'run_info')[0].asHTML()
374        traceinfo = pd.read_html(file, attrs={'id': 'traceinfo'})[0]
375        for k in traceinfo.index:
376            # Do not refill the widgets. This truncates and changes the
377            # definitions of some things from widgets to values.
378            self.traces[k].isactive = True
379            self.traces[k].tracelbl= traceinfo['Title'][k]
380            self.traces[k].units = traceinfo['Units'][k]
381            boardchoice, boardname = (traceinfo['Board'][k]).split(' ',1)
382            self.traces[k].boardchoice = boardchoice
383            self.traces[k].board = boardname
384            self.traces[k].channel = traceinfo['Channel'][k]
385            self.traces[k].gains= traceinfo['Gain'][k]
386            self.traces[k].sensor = traceinfo['Sensor'][k]
387        # Plot
388        if self.separate_plots:
389            self.livefig.set_subplots(rows=len(ycols), cols=1,
390                                                  shared_xaxes=True)
391            self.livefig.update_xaxes(
392                title=self.timelbl, row=len(ycols), col=1)
393        else:
394            self.livefig.update_xaxes(title=self.timelbl)
395            self.livefig.update_yaxes(title="Values")
396        for i in range(len(ycols)):
397            namestr = self.pandadf.columns[ycols[i]]
398            xcol = None
399            if len(xcols) == 1:
400                xcol = xcols[0]
401            else:
402                xcol = xcols[i]
403            scat = go.Scatter(
404                y=self.pandadf.iloc[0:, ycols[i]],
405                x=self.pandadf.iloc[0:,xcol], name=namestr)
406            if self.separate_plots:
407                self.livefig.update_yaxes(title=self.traces[i].units,
408                    row=i+1, col=1)
409                self.livefig.add_trace(scat, row=i+1, col=1)
410            else:
411                self.livefig.add_trace(scat)
412        pass
413
414    def setupclick(self, btn):
415        # Could just use the values in widgets, but this forces intentional
416        # selection and locks them for the run.
417        from copy import copy
418        self.title = copy(self.runtitle.value)
419        self.rate = copy(self.rateinp.value)
420        self.delta = 1 / self.rate
421        self.separate_plots = copy(self.separate_traces_checkbox.value)
422        self.defaultparamtxt = self._make_defaultparamtxt()
423        self.runtitle.close()
424        del self.runtitle
425        self.setup_layout.close()
426        del self.setup_layout
427        self.output.clear_output()
428        doRun(runs[self.idno-1])
429        with self.output:
430            display(runs[self.idno - 1].livefig)
431        pass
432
433    def setup(self):
434        with self.output:
435            display(HTML("<h3 id ='RunSetUp' "
436                         "style='text-align:center;'>Set Run Parameters</h3>"))
437            self.setupbtn.on_click(self.setupclick)
438            display(self.runtitle)
439            for i in range(self.ntraces):
440                self.traces[i].setup()
441            display(self.setup_layout)
442        display(self.output)
443        pass
444
445    def collectclick(self, btn):
446        if (btn.description == 'Start Collecting'):
447            btn.description = 'Stop Collecting'
448            btn.button_style = 'danger'
449            btn.tooltip = 'Stop the data collection'
450            # do not allow parameters to be reset after starting run.
451            self.setupbtn.disabled = True
452            self.setupbtn.tooltip = 'Parameters locked. The run has started.'
453            self.rateinp.disabled = True
454            self.timelbl.disabled = True
455            nactive = 0
456            for k in self.traces:
457                if k.isactive:
458                    nactive += 1
459            whichchn = []
460            gains =[]
461            for i in range(self.ntraces):
462                if (self.traces[i].isactive):
463                    brd = self.traces[i].board
464                    chn = self.traces[i].channel
465                    newchn = True
466                    if len(whichchn) > 0:
467                        for k in range(len(whichchn)):
468                            if whichchn[k]['board'] == brd \
469                                and whichchn[k]['chnl'] == chn:
470                                self.tracefrdatachn.append(k)
471                                newchn = False
472                    if newchn:
473                        whichchn.append({'board': brd,
474                                         'chnl': chn})
475                        gains.append(self.traces[i].toselectedgain)
476                        self.tracefrdatachn.append(len(whichchn)-1)
477            # Use up to 30% of the time for averaging if channels were spaced
478            # evenly between data collection times (with DACQ2 they appear
479            # more synchronous than that).
480            self.averaging_time = self.delta / nactive / 3
481            DAQ = Process(target=DAQProc,
482                          args=(
483                              whichchn, gains, self.averaging_time, self.delta,
484                              self.DAQconn, self.DAQCTL))
485            DAQ.start()
486            self.pltthread.start()
487            # self.updatingplot() hangs up user interface
488        else:
489            btn.description = 'Done'
490            btn.button_style = ''
491            btn.tooltip = ''
492            # wait a plotting thread to terminate
493            self.pltthread.join()
494            self.data = data
495            self.timestamp = timestamp
496            self.stdev = stdev
497            self.fillpandadf()
498            # save data to html file so it is human readable and can be loaded
499            # elsewhere.
500            #self.svname = self.title + '_' + time.strftime('%y-%m-%d_%H%M%S',
501                                       # time.localtime()) + '.html'
502            svhtml = '<!DOCTYPE html>' \
503                     '<html><body>'+ self.defaultparamtxt + \
504                     '<table id="file_info" border="1"><tr><th>Saved as ' \
505                     '</th></tr><tr><td>' +  \
506                     self.svname+'</td></tr></table>' \
507                     '<h2>DATA</h2>'+ \
508                     self.pandadf.to_html() + '</body></html>'
509            f = open(self.svname,'w')
510            f.write(svhtml)
511            f.close()
512            self.collectbtn.close()
513            del self.collectbtn
514            with self.output:
515                display(HTML(
516                    '<span style="color:blue;font-weight:bold;">DATA SAVED TO:' +
517                    self.svname + '</span>'))
518        return
519
520    def fillpandadf(self):
521        datacolumns = []
522        temptimes = np.transpose(self.timestamp)
523        tempdata = np.transpose(self.data)
524        tempstdev = np.transpose(self.stdev)
525        chncnt = 0
526        for i in range(self.ntraces):
527            if (self.traces[i].isactive):
528                chncnt += 1
529        for i in range(chncnt):
530            if self.ignore_skew and i > 0:
531                pass
532            else:
533                datacolumns.append(temptimes[i])
534            datacolumns.append(tempdata[i])
535            datacolumns.append(tempstdev[i])
536        titles = []
537        # Column labels.
538        chncnt = 0
539        for i in range(self.ntraces):
540            if (self.traces[i].isactive):
541                chncnt += 1
542                if self.ignore_skew:
543                    if chncnt == 1:
544                        titles.append(self.timelbl.value)
545                    pass
546                else:
547                    titles.append(self.traces[
548                                  i].tracelbl.value + '_' + self.timelbl.value)
549                titles.append(
550                    self.traces[i].tracelbl.value + '(' + self.traces[
551                        i].units.value + ')')
552                titles.append(
553                    self.traces[i].tracelbl.value + '_' + 'stdev')
554        #print(str(titles))
555        #print(str(datacolumns))
556        self.pandadf = pd.DataFrame(np.transpose(datacolumns), columns=titles)
557
558    def updatingplot(self, PLTconn, PLTCTL):
559        """
560        Runs until a check of self.collectbtn.description does not return
561        'Stop Collecting'. This would probably be more efficient if set a
562        boolean.
563        Parameters
564        ----------
565        PLTconn: Pipe
566            connection plotter end
567        DAQconn: Pipe
568            connection DAQ end
569        PLTCTL: Pipe
570            control pipe plotter end
571        DAQCTL: Pipe
572            control pipe DAQ end
573        """
574        starttime = time.time()
575        global data
576        data = []
577        global timestamp
578        timestamp = []
579        global stdev
580        stdev = []
581        datalegend = []
582        timelegend = []
583        stdevlegend = []
584        whichchn = []
585        gains = []
586        toplotx = []
587        toploty = []
588        nactive = 0
589        def convert_pkg():
590            plttime = 0
591            if self.ignore_skew:
592                plttime = sum(pkg[0]) / len(pkg[0])
593            traceidx = 0
594            tmptime = []
595            tmpavg = []
596            tmpstd = []
597            tmpavg_std = []
598            for i, k in zip(self.tracemap, self.tracefrdatachn):
599
600                avg = pkg[1][k]
601                std = pkg[2][k]
602                avg_std = pkg[3][k]
603                avg_vdd = pkg[4][k]
604                avg, std, avg_std = self.traces[i].toselectedunits(avg,
605                                                                   std, avg_std,
606                                                                   avg_vdd)
607                avg, std, avg_std = sensors. \
608                    to_reasonable_significant_figures_fast(avg, std, avg_std)
609                tmptime.append(pkg[0][k])
610                tmpavg.append(avg)
611                tmpstd.append(std)
612                tmpavg_std.append(avg_std)
613                if self.ignore_skew:
614                    toplotx[traceidx].append(plttime)
615                else:
616                    toplotx[traceidx].append(pkg[0][k])
617                toploty[traceidx].append(avg)
618                traceidx += 1
619            timestamp.append(tmptime)
620            data.append(tmpavg)
621            stdev.append(tmpavg_std)
622            return
623
624        for k in self.traces:
625            if k.isactive:
626                nactive += 1
627        if self.separate_plots:
628            self.livefig.set_subplots(rows = nactive, cols = 1,
629                                      shared_xaxes= True)
630            self.livefig.update_xaxes(title = self.timelbl.value,
631                                      row = nactive, col = 1)
632        else:
633            self.livefig.update_yaxes(title = "Values")
634            self.livefig.update_xaxes(title = self.timelbl.value)
635        active_count = 0
636        for i in range(self.ntraces):
637            if (self.traces[i].isactive):
638                active_count += 1
639                tempstr = self.traces[i].tracelbl.value + '(' + \
640                          self.traces[i].units.value + ')'
641                timelegend.append('time_' + tempstr)
642                datalegend.append(tempstr)
643                stdevlegend.append('stdev_' + tempstr)
644                if self.separate_plots:
645                    scat = go.Scatter(y=[],x=[], name=tempstr)
646                    self.livefig.add_trace(scat, row = active_count,
647                                           col = 1)
648                    self.livefig.update_yaxes(title = self.traces[
649                        i].units.value, row = active_count, col = 1)
650                else:
651                    self.livefig.add_scatter(y=[],x=[], name=tempstr)
652                toplotx.append([])
653                toploty.append([])
654        lastupdatetime = time.time()
655
656        pts = 0
657        oldpts = 0
658        #print('about to enter while loop',end='')
659        while (self.collectbtn.description == 'Stop Collecting'):
660            #print('.',end='')
661            while PLTconn.poll():
662                pkg = PLTconn.recv()
663                self.lastpkgstr = str(pkg)
664                #print(self.lastpkgstr)
665                # convert voltage to requested units.
666                convert_pkg()
667            currenttime = time.time()
668            mindelay = 1.0
669            if self.separate_traces_checkbox.value:
670                mindelay = nactive*1.0
671            else:
672                mindelay = nactive*0.5
673            if (currenttime - lastupdatetime)>(mindelay+len(toplotx[0])*len(
674                    toplotx)/1000):
675                lastupdatetime = currenttime
676                for k in range(len(self.livefig.data)):
677                    self.livefig.data[k].x=toplotx[k]
678                    self.livefig.data[k].y=toploty[k]
679            #time.sleep(1)
680            PLTCTL.send('send')
681            time.sleep(self.delta)
682            # print ('btn.description='+str(btn.description))
683        endtime = time.time()
684        PLTCTL.send('stop')
685        time.sleep(0.5)  # wait 0.5 second to collect remaining data
686        PLTCTL.send('send')
687        time.sleep(0.5)
688        msg = ''
689        while (msg != 'done'):
690            while PLTconn.poll():
691                pkg = PLTconn.recv()
692                # print(str(pkg))
693                # convert voltage to requested units.
694                convert_pkg()
695            PLTCTL.send('send')
696            time.sleep(0.2)
697            if PLTCTL.poll():
698                msg = PLTCTL.recv()
699                # print (str(msg))
700                if (msg != 'done'):
701                    print('Received unexpected message: ' + str(msg))
702        for k in range(len(self.livefig.data)):
703            self.livefig.data[k].x = toplotx[k]
704            self.livefig.data[k].y = toploty[k]
705        return
DAQinstance(idno, title='None', ntraces=4, **kwargs)
128    def __init__(self, idno, title='None', ntraces=4, **kwargs):
129        """
130        Data Aquistion Instance (a run).
131
132        :param idno : id number you wish to use to keep track
133        :param title: optional name
134        :param ntraces: number of traces (default = 4) more than 4 easily
135            overwhelms a pi4.
136        :param kwargs:
137            :ignore_skew: bool (default: True) if True only a single average
138            collection time will be recorded for each time in a multichannel
139            data collection. If False a separate set of time will be
140            recorded for each channel.
141        """
142        from plotly import graph_objects as go
143        self.ignore_skew = kwargs.pop('ignore_skew',True)
144        self.idno = idno
145        self.livefig = go.FigureWidget(layout_template='simple_white')
146        self.PLTconn, self.DAQconn = Pipe()
147        self.DAQCTL, self.PLTCTL = Pipe()
148        self.pltthread = threading.Thread(target=self.updatingplot, args=(
149                                        self.PLTconn, self.PLTCTL))
150        self.title = str(title)
151        self.svname = title + '.jpidaq.html'
152        self.averaging_time = 0.1  # seconds adjusted based on collection rate
153        self.gain = [1] * ntraces
154        self.data = []
155        self.timestamp = []
156        self.stdev = []
157        self.pandadf = None
158        self.ntraces = ntraces
159        self.separate_plots = True
160        self.traces = []
161        # index map from returned data to trace,
162        self.tracemap = []
163        self.tracefrdatachn = []
164        self.tracelbls = []
165        self.units = []
166        for i in range(self.ntraces):
167            self.traces.append(ChannelSettings(i, availboards))
168        self.ratemax = 20.0  # Hz
169        self.rate = 1.0  # Hz
170        self.deltamin = 1 / self.ratemax
171        self.delta = 1.0 / self.rate
172        self.setupbtn = widgets.Button(
173            description='Set Parameters',
174            disabled=False,
175            button_style='info',
176            # 'success', 'info', 'warning', 'danger' or ''
177            tooltip='Click to set collection parameters to displayed values.',
178            icon='')
179        self.collectbtn = widgets.Button(
180            description='Start Collecting',
181            disabled=False,
182            button_style='success',
183            # 'success', 'info', 'warning', 'danger' or ''
184            tooltip='Start collecting data and plotting it. '
185                    'Will make new graph.',
186            icon='')
187        self.separate_traces_checkbox = widgets.Checkbox(
188            value = self.separate_plots,
189            description = 'Display multiple traces as stacked graphs. ' \
190                        'Uncheck to display all on the same graph.',
191            layout = widgets.Layout(width='80%'),
192            disabled = False)
193        self.rateinp = widgets.BoundedFloatText(
194            value=self.rate,
195            min=0,
196            max=self.ratemax,
197            step=self.ratemax / 1000.0,
198            description='Rate (Hz):',
199            disabled=False)
200        self.timelbl = widgets.Text(
201            value='Time(s)',
202            placeholder='Type something',
203            description='X-axis label (time):',
204            disabled=False)
205        self.runtitle = widgets.Text(
206            value=self.title,
207            placeholder='Type title/description',
208            description='Run title',
209            disabled=False)
210        self.defaultparamtxt = ''
211        self.defaultcollecttxt = '<span style="color:blue;"> To accurately '
212        self.defaultcollecttxt += 'read point location on graph you can zoom '
213        self.defaultcollecttxt += 'in. Drag to zoom. Additional controls show '
214        self.defaultcollecttxt += 'above plot when you hover over it.</span>'
215        self.collecttxt = widgets.HTML(
216            value=self.defaultcollecttxt,
217            placeholder='',
218            description='')
219        self.setup_layout_bottom = widgets.HBox(
220            [self.rateinp, self.timelbl, self.setupbtn])
221        self.setup_layout = widgets.VBox([self.separate_traces_checkbox,
222                                          self.setup_layout_bottom])
223        self.collect_layout = widgets.HBox([self.collectbtn, self.collecttxt])
224        self.output = widgets.Output()

Data Aquistion Instance (a run).

Parameters
  • idno: id number you wish to use to keep track
  • title: optional name
  • ntraces: number of traces (default = 4) more than 4 easily overwhelms a pi4.
  • kwargs: :ignore_skew: bool (default: True) if True only a single average collection time will be recorded for each time in a multichannel data collection. If False a separate set of time will be recorded for each channel.
ignore_skew
idno
livefig
pltthread
title
svname
averaging_time
gain
data
timestamp
stdev
pandadf
ntraces
separate_plots
traces
tracemap
tracefrdatachn
tracelbls
units
ratemax
rate
deltamin
delta
setupbtn
collectbtn
separate_traces_checkbox
rateinp
timelbl
runtitle
defaultparamtxt
defaultcollecttxt
collecttxt
setup_layout_bottom
setup_layout
collect_layout
output
def setupclick(self, btn):
414    def setupclick(self, btn):
415        # Could just use the values in widgets, but this forces intentional
416        # selection and locks them for the run.
417        from copy import copy
418        self.title = copy(self.runtitle.value)
419        self.rate = copy(self.rateinp.value)
420        self.delta = 1 / self.rate
421        self.separate_plots = copy(self.separate_traces_checkbox.value)
422        self.defaultparamtxt = self._make_defaultparamtxt()
423        self.runtitle.close()
424        del self.runtitle
425        self.setup_layout.close()
426        del self.setup_layout
427        self.output.clear_output()
428        doRun(runs[self.idno-1])
429        with self.output:
430            display(runs[self.idno - 1].livefig)
431        pass
def setup(self):
433    def setup(self):
434        with self.output:
435            display(HTML("<h3 id ='RunSetUp' "
436                         "style='text-align:center;'>Set Run Parameters</h3>"))
437            self.setupbtn.on_click(self.setupclick)
438            display(self.runtitle)
439            for i in range(self.ntraces):
440                self.traces[i].setup()
441            display(self.setup_layout)
442        display(self.output)
443        pass
def collectclick(self, btn):
445    def collectclick(self, btn):
446        if (btn.description == 'Start Collecting'):
447            btn.description = 'Stop Collecting'
448            btn.button_style = 'danger'
449            btn.tooltip = 'Stop the data collection'
450            # do not allow parameters to be reset after starting run.
451            self.setupbtn.disabled = True
452            self.setupbtn.tooltip = 'Parameters locked. The run has started.'
453            self.rateinp.disabled = True
454            self.timelbl.disabled = True
455            nactive = 0
456            for k in self.traces:
457                if k.isactive:
458                    nactive += 1
459            whichchn = []
460            gains =[]
461            for i in range(self.ntraces):
462                if (self.traces[i].isactive):
463                    brd = self.traces[i].board
464                    chn = self.traces[i].channel
465                    newchn = True
466                    if len(whichchn) > 0:
467                        for k in range(len(whichchn)):
468                            if whichchn[k]['board'] == brd \
469                                and whichchn[k]['chnl'] == chn:
470                                self.tracefrdatachn.append(k)
471                                newchn = False
472                    if newchn:
473                        whichchn.append({'board': brd,
474                                         'chnl': chn})
475                        gains.append(self.traces[i].toselectedgain)
476                        self.tracefrdatachn.append(len(whichchn)-1)
477            # Use up to 30% of the time for averaging if channels were spaced
478            # evenly between data collection times (with DACQ2 they appear
479            # more synchronous than that).
480            self.averaging_time = self.delta / nactive / 3
481            DAQ = Process(target=DAQProc,
482                          args=(
483                              whichchn, gains, self.averaging_time, self.delta,
484                              self.DAQconn, self.DAQCTL))
485            DAQ.start()
486            self.pltthread.start()
487            # self.updatingplot() hangs up user interface
488        else:
489            btn.description = 'Done'
490            btn.button_style = ''
491            btn.tooltip = ''
492            # wait a plotting thread to terminate
493            self.pltthread.join()
494            self.data = data
495            self.timestamp = timestamp
496            self.stdev = stdev
497            self.fillpandadf()
498            # save data to html file so it is human readable and can be loaded
499            # elsewhere.
500            #self.svname = self.title + '_' + time.strftime('%y-%m-%d_%H%M%S',
501                                       # time.localtime()) + '.html'
502            svhtml = '<!DOCTYPE html>' \
503                     '<html><body>'+ self.defaultparamtxt + \
504                     '<table id="file_info" border="1"><tr><th>Saved as ' \
505                     '</th></tr><tr><td>' +  \
506                     self.svname+'</td></tr></table>' \
507                     '<h2>DATA</h2>'+ \
508                     self.pandadf.to_html() + '</body></html>'
509            f = open(self.svname,'w')
510            f.write(svhtml)
511            f.close()
512            self.collectbtn.close()
513            del self.collectbtn
514            with self.output:
515                display(HTML(
516                    '<span style="color:blue;font-weight:bold;">DATA SAVED TO:' +
517                    self.svname + '</span>'))
518        return
def fillpandadf(self):
520    def fillpandadf(self):
521        datacolumns = []
522        temptimes = np.transpose(self.timestamp)
523        tempdata = np.transpose(self.data)
524        tempstdev = np.transpose(self.stdev)
525        chncnt = 0
526        for i in range(self.ntraces):
527            if (self.traces[i].isactive):
528                chncnt += 1
529        for i in range(chncnt):
530            if self.ignore_skew and i > 0:
531                pass
532            else:
533                datacolumns.append(temptimes[i])
534            datacolumns.append(tempdata[i])
535            datacolumns.append(tempstdev[i])
536        titles = []
537        # Column labels.
538        chncnt = 0
539        for i in range(self.ntraces):
540            if (self.traces[i].isactive):
541                chncnt += 1
542                if self.ignore_skew:
543                    if chncnt == 1:
544                        titles.append(self.timelbl.value)
545                    pass
546                else:
547                    titles.append(self.traces[
548                                  i].tracelbl.value + '_' + self.timelbl.value)
549                titles.append(
550                    self.traces[i].tracelbl.value + '(' + self.traces[
551                        i].units.value + ')')
552                titles.append(
553                    self.traces[i].tracelbl.value + '_' + 'stdev')
554        #print(str(titles))
555        #print(str(datacolumns))
556        self.pandadf = pd.DataFrame(np.transpose(datacolumns), columns=titles)
def updatingplot(self, PLTconn, PLTCTL):
558    def updatingplot(self, PLTconn, PLTCTL):
559        """
560        Runs until a check of self.collectbtn.description does not return
561        'Stop Collecting'. This would probably be more efficient if set a
562        boolean.
563        Parameters
564        ----------
565        PLTconn: Pipe
566            connection plotter end
567        DAQconn: Pipe
568            connection DAQ end
569        PLTCTL: Pipe
570            control pipe plotter end
571        DAQCTL: Pipe
572            control pipe DAQ end
573        """
574        starttime = time.time()
575        global data
576        data = []
577        global timestamp
578        timestamp = []
579        global stdev
580        stdev = []
581        datalegend = []
582        timelegend = []
583        stdevlegend = []
584        whichchn = []
585        gains = []
586        toplotx = []
587        toploty = []
588        nactive = 0
589        def convert_pkg():
590            plttime = 0
591            if self.ignore_skew:
592                plttime = sum(pkg[0]) / len(pkg[0])
593            traceidx = 0
594            tmptime = []
595            tmpavg = []
596            tmpstd = []
597            tmpavg_std = []
598            for i, k in zip(self.tracemap, self.tracefrdatachn):
599
600                avg = pkg[1][k]
601                std = pkg[2][k]
602                avg_std = pkg[3][k]
603                avg_vdd = pkg[4][k]
604                avg, std, avg_std = self.traces[i].toselectedunits(avg,
605                                                                   std, avg_std,
606                                                                   avg_vdd)
607                avg, std, avg_std = sensors. \
608                    to_reasonable_significant_figures_fast(avg, std, avg_std)
609                tmptime.append(pkg[0][k])
610                tmpavg.append(avg)
611                tmpstd.append(std)
612                tmpavg_std.append(avg_std)
613                if self.ignore_skew:
614                    toplotx[traceidx].append(plttime)
615                else:
616                    toplotx[traceidx].append(pkg[0][k])
617                toploty[traceidx].append(avg)
618                traceidx += 1
619            timestamp.append(tmptime)
620            data.append(tmpavg)
621            stdev.append(tmpavg_std)
622            return
623
624        for k in self.traces:
625            if k.isactive:
626                nactive += 1
627        if self.separate_plots:
628            self.livefig.set_subplots(rows = nactive, cols = 1,
629                                      shared_xaxes= True)
630            self.livefig.update_xaxes(title = self.timelbl.value,
631                                      row = nactive, col = 1)
632        else:
633            self.livefig.update_yaxes(title = "Values")
634            self.livefig.update_xaxes(title = self.timelbl.value)
635        active_count = 0
636        for i in range(self.ntraces):
637            if (self.traces[i].isactive):
638                active_count += 1
639                tempstr = self.traces[i].tracelbl.value + '(' + \
640                          self.traces[i].units.value + ')'
641                timelegend.append('time_' + tempstr)
642                datalegend.append(tempstr)
643                stdevlegend.append('stdev_' + tempstr)
644                if self.separate_plots:
645                    scat = go.Scatter(y=[],x=[], name=tempstr)
646                    self.livefig.add_trace(scat, row = active_count,
647                                           col = 1)
648                    self.livefig.update_yaxes(title = self.traces[
649                        i].units.value, row = active_count, col = 1)
650                else:
651                    self.livefig.add_scatter(y=[],x=[], name=tempstr)
652                toplotx.append([])
653                toploty.append([])
654        lastupdatetime = time.time()
655
656        pts = 0
657        oldpts = 0
658        #print('about to enter while loop',end='')
659        while (self.collectbtn.description == 'Stop Collecting'):
660            #print('.',end='')
661            while PLTconn.poll():
662                pkg = PLTconn.recv()
663                self.lastpkgstr = str(pkg)
664                #print(self.lastpkgstr)
665                # convert voltage to requested units.
666                convert_pkg()
667            currenttime = time.time()
668            mindelay = 1.0
669            if self.separate_traces_checkbox.value:
670                mindelay = nactive*1.0
671            else:
672                mindelay = nactive*0.5
673            if (currenttime - lastupdatetime)>(mindelay+len(toplotx[0])*len(
674                    toplotx)/1000):
675                lastupdatetime = currenttime
676                for k in range(len(self.livefig.data)):
677                    self.livefig.data[k].x=toplotx[k]
678                    self.livefig.data[k].y=toploty[k]
679            #time.sleep(1)
680            PLTCTL.send('send')
681            time.sleep(self.delta)
682            # print ('btn.description='+str(btn.description))
683        endtime = time.time()
684        PLTCTL.send('stop')
685        time.sleep(0.5)  # wait 0.5 second to collect remaining data
686        PLTCTL.send('send')
687        time.sleep(0.5)
688        msg = ''
689        while (msg != 'done'):
690            while PLTconn.poll():
691                pkg = PLTconn.recv()
692                # print(str(pkg))
693                # convert voltage to requested units.
694                convert_pkg()
695            PLTCTL.send('send')
696            time.sleep(0.2)
697            if PLTCTL.poll():
698                msg = PLTCTL.recv()
699                # print (str(msg))
700                if (msg != 'done'):
701                    print('Received unexpected message: ' + str(msg))
702        for k in range(len(self.livefig.data)):
703            self.livefig.data[k].x = toplotx[k]
704            self.livefig.data[k].y = toploty[k]
705        return

Runs until a check of self.collectbtn.description does not return 'Stop Collecting'. This would probably be more efficient if set a boolean.

Parameters

PLTconn: Pipe connection plotter end DAQconn: Pipe connection DAQ end PLTCTL: Pipe control pipe plotter end DAQCTL: Pipe control pipe DAQ end

def Run(name):
717def Run(name):
718    """Load a run from stored data or start a new run if the local file for
719    the run does not exist.
720    Parameters
721    ----------
722    name: str
723        String name for the run. The data will be stored in a file of this
724        name with the extension of `.jpidaq.html`.
725    """
726    from pathlib import Path
727    from IPython import get_ipython
728    from IPython.display import display
729    global_dict = get_ipython().user_ns
730    runs = None
731    if 'runs' in global_dict and 'DAQinstance' in global_dict:
732        runs = global_dict['runs']
733    else:
734        return ('Initialization of JupyterPiDAQ required')
735    # Check if run completed, if so reload data, display and exit
736    datafilepath = Path.cwd() / Path(str(name) + '.jpidaq.html')
737    if datafilepath.exists():
738        # display the data as a live plotly plot.
739        svname = name + '.jpidaq.html'
740        runs.append(DAQinstance(len(runs)+1, title = name))
741        runs[-1]._load_from_html(svname)
742        display(HTML(runs[-1].defaultparamtxt))
743        display(HTML('<h3>Saved as: '+runs[-1].svname+'</h3>'))
744        display(runs[-1].livefig)
745        display(HTML(runs[-1].defaultcollecttxt))
746        return
747    nrun = len(runs) + 1
748    runs.append(DAQinstance(nrun, title=name))
749    runs[-1].setup()
750    return

Load a run from stored data or start a new run if the local file for the run does not exist.

Parameters

name: str String name for the run. The data will be stored in a file of this name with the extension of .jpidaq.html.

def doRun(whichrun):
752def doRun(whichrun):
753    with whichrun.output:
754        display(HTML('<span id="LiveRun_'+str(whichrun.idno)+'"></span>'))
755        display(HTML(whichrun.defaultparamtxt))
756        if hasattr(whichrun, "collectbtn"):
757            # only show if hasn't already collected data
758            whichrun.collectbtn.on_click(whichrun.collectclick)
759            display(whichrun.collectbtn)
760        display(HTML(whichrun.defaultcollecttxt))
761    pass
def update_runsdrp():
820def update_runsdrp():
821    # get list of runs
822    runlst = [('Choose Run', -1)]
823    for i in range(len(runs)):
824        runlst.append((str(i + 1) + ': ' + runs[i].title, i))
825    # buid selection menu
826    global runsdrp
827    runsdrp = widgets.Dropdown(
828        options=runlst,
829        value=-1,
830        description='Select Run #:',
831        disabled=False,
832    )
833    pass
def showSelectedRunTable(change):
835def showSelectedRunTable(change):
836    global runsdrp
837    global last_run_table_out
838    whichrun = runsdrp.value
839    runsdrp.close()
840    last_run_table_out.clear_output()
841    tbldiv = '<div style="height:10em;">' + str(runs[whichrun].title)
842    tbldiv += str(runs[whichrun].pandadf.to_html()) + '</div>'
843    with last_run_table_out:
844        display(HTML(tbldiv))
845    return
def showDataTable():
847def showDataTable():
848    """
849    Provides a menu to select which run. Then displays the run in a
850    10 em high scrolling table. Selection menu is removed after choice
851    is made.
852    """
853    from ipywidgets import Output
854    global last_run_table_out
855    last_run_table_out = Output()
856    update_runsdrp()
857    global runsdrp
858    runsdrp.observe(showSelectedRunTable, names='value')
859    with last_run_table_out:
860        display(runsdrp)
861    display(last_run_table_out)
862    # will display selected run and delete menu upon selection.
863    return

Provides a menu to select which run. Then displays the run in a 10 em high scrolling table. Selection menu is removed after choice is made.

def newCalculatedColumn():
864def newCalculatedColumn():
865    """
866    Uses jupyter-pandas-GUI.new_pandas_column_GUI to provide a GUI expression
867    composer. This method finds the datasets and launches the GUI.
868    """
869    df_info = []
870    for i in range(len(runs)):
871        df_info.append([runs[i].pandadf, 'runs['+str(i)+'].pandadf',
872                        str(runs[i].title)])
873    new_pandas_column_GUI(df_info)
874    pass

Uses jupyter-pandas-GUI.new_pandas_column_GUI to provide a GUI expression composer. This method finds the datasets and launches the GUI.

def newPlot():
876def newPlot():
877    """
878    Uses jupyter-pandas-GUI.plot_pandas_GUI to provide a GUI expression
879    composer. This method finds the datasets and launches the GUI.
880    """
881    df_info = []
882    for i in range(len(runs)):
883        if isinstance(runs[i].pandadf,pd.DataFrame):
884            df_info.append([runs[i].pandadf, 'runs['+str(i)+'].pandadf',
885                            str(runs[i].title)])
886    plot_pandas_GUI(df_info)
887    pass

Uses jupyter-pandas-GUI.plot_pandas_GUI to provide a GUI expression composer. This method finds the datasets and launches the GUI.

def newFit():
889def newFit():
890    """
891    Uses jupyter-pandas-GUI.fit_pandas_GUI to provide a GUI expression
892    composer. This method finds the datasets and launches the GUI.
893    """
894    df_info = []
895    for i in range(len(runs)):
896        if isinstance(runs[i].pandadf,pd.DataFrame):
897            df_info.append([runs[i].pandadf, 'runs['+str(i)+'].pandadf',
898                            str(runs[i].title)])
899    fit_pandas_GUI(df_info)
900    pass

Uses jupyter-pandas-GUI.fit_pandas_GUI to provide a GUI expression composer. This method finds the datasets and launches the GUI.