class ScalarLiveViewPanel(wx.Panel): """ A panel to display a live view plot of a scalar resource. """ def __init__(self, parent, global_store, *args, **kwargs): wx.Panel.__init__(self, parent, *args, **kwargs) self.global_store = global_store self._measurement_resource_name = None # Defaults. self.plot_settings = PlotSettings() self.unit_conversion = 0 self.enabled = False self.capturing_data = False self.restart_live_view = False self.resource_backup = None # This lock blocks the acquisition thread from acquiring. self.running_lock = Lock() # Initialize recorded values. self.init_values() # Plot and toolbar. display_box = wx.BoxSizer(wx.VERTICAL) ## Plot. if plot_available: self.plot = TwoDimensionalPlot(self, color='blue') display_box.Add(self.plot.control, proportion=1, flag=wx.EXPAND) self.plot.x_label = 'Time (s)' else: display_box.Add((500, -1), proportion=1, flag=wx.EXPAND) ## Controls. if plot_available: controls_box = wx.BoxSizer(wx.HORIZONTAL) display_box.Add(controls_box, flag=wx.CENTER|wx.ALL, border=5) ### Numeric display. numeric_display_static_box = wx.StaticBox(self, label='Reading') numeric_display_box = wx.StaticBoxSizer(numeric_display_static_box, wx.HORIZONTAL) controls_box.Add(numeric_display_box, flag=wx.CENTER) self.numeric_display = wx.TextCtrl(self, size=(100, -1), style=wx.TE_READONLY) self.numeric_display.BackgroundColour = wx.LIGHT_GREY numeric_display_box.Add(self.numeric_display) ### Capture. capture_static_box = wx.StaticBox(self, label='Control') capture_box = wx.StaticBoxSizer(capture_static_box) controls_box.Add(capture_box, flag=wx.CENTER|wx.LEFT, border=10) self.run_button = wx.Button(self, label='Run') self.Bind(wx.EVT_BUTTON, self.OnRun, self.run_button) capture_box.Add(self.run_button, flag=wx.CENTER) self.pause_button = wx.Button(self, label='Pause') self.Bind(wx.EVT_BUTTON, self.OnPause, self.pause_button) capture_box.Add(self.pause_button, flag=wx.CENTER) self.reset_button = wx.Button(self, label='Reset') self.Bind(wx.EVT_BUTTON, self.OnReset, self.reset_button) capture_box.Add(self.reset_button, flag=wx.CENTER|wx.LEFT, border=10) ### Settings. settings_static_box = wx.StaticBox(self, label='Settings') settings_box = wx.StaticBoxSizer(settings_static_box, wx.HORIZONTAL) controls_box.Add(settings_box, flag=wx.CENTER|wx.LEFT, border=10) self.plot_settings_button = wx.Button(self, label='Plot...') self.Bind(wx.EVT_BUTTON, self.OnPlotSettings, self.plot_settings_button) settings_box.Add(self.plot_settings_button, flag=wx.CENTER) self.SetSizer(display_box) # Acquisition thread. callback = functools.partial(wx.CallAfter, self.add_value) self.acq_thread = AcquisitionThread(self.plot_settings.delay, callback, running_lock=self.running_lock) self.acq_thread.daemon = True self.acq_thread.start() # Wait for a resource to begin capturing. self.OnPause() self.run_button.Disable() # Subscriptions. pub.subscribe(self.msg_resource, 'resource.added') pub.subscribe(self.msg_resource, 'resource.removed') pub.subscribe(self.msg_data_capture_start, 'data_capture.start') pub.subscribe(self.msg_data_capture_data, 'data_capture.data') pub.subscribe(self.msg_data_capture_stop, 'data_capture.stop') @property def running(self): return self.pause_button.Enabled @property def resource(self): return self.acq_thread.resource @resource.setter def resource(self, value): # Ignore unreadable resources. if value is not None and not value.readable: value = None if self.running: # Currently running. running = True self.OnPause() else: running = False self.acq_thread.resource = value self.run_button.Enable(value is not None) # Resume if applicable. if running: self.OnRun() @property def measurement_resource_name(self): if self._measurement_resource_name is None: return '' else: return self._measurement_resource_name @measurement_resource_name.setter def measurement_resource_name(self, value): if value: self._measurement_resource_name = value try: self.resource = self.global_store.resources[self._measurement_resource_name] except KeyError: self.resource = None else: self._measurement_resource_name = None self.resource = None def init_values(self): """ Clear captured values. """ self._points = numpy.array([]) self._times = numpy.array([]) self._values = numpy.array([]) self.current_value = None self.start_time = None def update_plot(self): """ Redraw the plot. """ if not len(self._points) > 0: display_time = [0] display_values = [0] else: if self.plot_settings.time_value == 0: # Time. display_time = self._times if self.plot_settings.time_mode == 0: # Relative. # Calculate the number of seconds passed since each point. max_time = self._times[-1] display_time = [x - max_time for x in display_time] elif self.plot_settings.time_mode == 1: # Absolute. display_time = [x - self.start_time for x in display_time] elif self.plot_settings.time_value == 1: # Points. display_time = self._points if self.plot_settings.time_mode == 0: # Relative. # Calculate the number of seconds passed since each point. max_point = self._points[-1] display_time = [x - max_point for x in display_time] display_values = [x * 10 ** (self.plot_settings.y_scale + self.unit_conversion) for x in self._values] if self.plot_settings.update_x: self.plot.x_autoscale() if self.plot_settings.update_y: self.plot.y_autoscale() self.plot.x_data, self.plot.y_data = display_time, display_values def add_value(self, value): """ Update the plot with a new value. """ if not self.plot_settings.enabled: return # Extract the value of a Quantity. try: # Label with the base dimensions. if self.unit_conversion == 0: self.plot.y_label = '({0})'.format(value.original_units) value = value.original_value except AttributeError: pass # Update values. try: self._points = numpy.append(self._points, self._points[-1] + 1) except IndexError: self._points = numpy.append(self._points, 0) cur_time = time.time() self._times = numpy.append(self._times, cur_time) self._values = numpy.append(self._values, value) if self.start_time is None: self.start_time = cur_time cut_idx = len(self._points) - int(self.plot_settings.num_points) if cut_idx > 0: self._points = self._points[cut_idx:] self._times = self._times[cut_idx:] self._values = self._values[cut_idx:] # Set number display. self.current_value = value * 10 ** (self.plot_settings.y_scale + self.unit_conversion) self.numeric_display.Value = '{0:.6g}'.format(self.current_value) # Plot. self.update_plot() def close(self): """ Perform cleanup. """ # Unsubscriptions. pub.unsubscribe(self.msg_resource, 'resource.added') pub.unsubscribe(self.msg_resource, 'resource.removed') pub.unsubscribe(self.msg_data_capture_start, 'data_capture.start') pub.unsubscribe(self.msg_data_capture_data, 'data_capture.data') pub.unsubscribe(self.msg_data_capture_stop, 'data_capture.stop') # Ensure the thread exits. self.acq_thread.resource = None self.acq_thread.done = True if not self.running: self.running_lock.release() self.acq_thread.join() del self.acq_thread def OnRun(self, evt=None): """ Let the acquisition thread run. """ self.run_button.Disable() if self.acq_thread.resource is None: return self.running_lock.release() self.pause_button.Enable() def OnPause(self, evt=None): """ Block the acquisition thread. """ if not self.running: return self.running_lock.acquire() if self.acq_thread.resource is not None: self.run_button.Enable() self.pause_button.Disable() def OnReset(self, evt=None): self.init_values() self.update_plot() def OnPlotSettings(self, evt=None): """ Open the plot settings dialog. """ def ok_callback(dlg): self.plot_settings = dlg.GetValue() if self.plot_settings.units_from and self.plot_settings.units_to: try: quantity_from = Quantity(1, self.plot_settings.units_from) quantity_to = Quantity(1, self.plot_settings.units_to) except ValueError as e: self.unit_conversion = 0 MessageDialog(self, str(e), 'Invalid unit').Show() else: # We don't actually care about the units; just the prefix values. self.unit_conversion = math.log(quantity_from.value, 10) - math.log(quantity_to.value, 10) else: self.unit_conversion = 0 self.acq_thread.delay = self.plot_settings.delay if self.plot_settings.time_value == 0: self.plot.x_label = 'Time (s)' elif self.plot_settings.time_value == 1: self.plot.x_label = 'Points' if self.plot_settings.y_scale != 0: self.plot.y_label = '/ 10 ^ {0}'.format(self.plot_settings.y_scale) else: self.plot.y_label = '' if self.plot_settings.units_to: self.plot.y_label += ' ({0})'.format(self.plot_settings.units_to) self.update_plot() dlg = PlotSettingsDialog(self, ok_callback) dlg.SetValue(self.plot_settings) dlg.Show() def msg_resource(self, name, value=None): if self.measurement_resource_name is not None and name == self.measurement_resource_name: self.resource = value def msg_data_capture_start(self, name): if name == self.measurement_resource_name: if self.enabled: self.capturing_data = True # Keep track of whether to restart the capture afterwards. self.restart_live_view = self.running # Disable live view. self.resource_backup = self.resource self.resource = None def msg_data_capture_data(self, name, value): if name == self.measurement_resource_name: if self.capturing_data: self.add_value(value) def msg_data_capture_stop(self, name): if name == self.measurement_resource_name: if self.capturing_data: self.capturing_data = False # Re-enable live view. self.resource = self.resource_backup self.resource_backup = None if self.restart_live_view: self.OnRun()
class ListLiveViewPanel(wx.Panel): """ A panel to display a live view plot of a list resource. """ def __init__(self, parent, global_store, *args, **kwargs): wx.Panel.__init__(self, parent, *args, **kwargs) self.global_store = global_store self._measurement_resource_name = None # Defaults. self.plot_settings = PlotSettings() self.unit_conversion = 0 self.enabled = True self.capturing_data = False self.restart_live_view = False self.resource_backup = None # csv save file information self.save_path = '' self.defaultName = '' # Initialize recorded values. self.init_values() # This lock blocks the acquisition thread from acquiring. self.running_lock = Lock() # Plot and toolbar. display_box = wx.BoxSizer(wx.VERTICAL) ## Plot. if plot_available: self.plot = TwoDimensionalPlot(self, color='blue') display_box.Add(self.plot.control, proportion=1, flag=wx.EXPAND) # self.plot.x_label = 'Waveform time (s)' # self.plot.y_label = 'History' else: display_box.Add((500, -1), proportion=1, flag=wx.EXPAND) ## Controls. if plot_available: controls_box = wx.BoxSizer(wx.HORIZONTAL) display_box.Add(controls_box, flag=wx.CENTER | wx.ALL, border=5) ### Manual data export saveData_static_box = wx.StaticBox(self, label='Last trace') saveData_box = wx.StaticBoxSizer(saveData_static_box, wx.HORIZONTAL) controls_box.Add(saveData_box, flag=wx.CENTER) self.csv_button = wx.Button(self, label='Save to .csv') self.Bind(wx.EVT_BUTTON, self.onSave, self.csv_button) saveData_box.Add(self.csv_button, flag=wx.CENTER) ### Capture. capture_static_box = wx.StaticBox(self, label='Control') capture_box = wx.StaticBoxSizer(capture_static_box) controls_box.Add(capture_box, flag=wx.CENTER) self.run_button = wx.Button(self, label='Run') self.Bind(wx.EVT_BUTTON, self.OnRun, self.run_button) capture_box.Add(self.run_button, flag=wx.CENTER) self.pause_button = wx.Button(self, label='Pause') self.Bind(wx.EVT_BUTTON, self.OnPause, self.pause_button) capture_box.Add(self.pause_button, flag=wx.CENTER) self.reset_button = wx.Button(self, label='Reset') self.Bind(wx.EVT_BUTTON, self.OnReset, self.reset_button) capture_box.Add(self.reset_button, flag=wx.CENTER) ### Settings. settings_static_box = wx.StaticBox(self, label='Settings') settings_box = wx.StaticBoxSizer(settings_static_box, wx.HORIZONTAL) controls_box.Add(settings_box, flag=wx.CENTER | wx.LEFT, border=10) self.plot_settings_button = wx.Button(self, label='Plot...') self.Bind(wx.EVT_BUTTON, self.OnPlotSettings, self.plot_settings_button) settings_box.Add(self.plot_settings_button, flag=wx.CENTER) self.SetSizer(display_box) # Acquisition thread. callback = functools.partial(wx.CallAfter, self.add_values) self.acq_thread = AcquisitionThread(self.plot_settings.delay, callback, running_lock=self.running_lock) self.acq_thread.daemon = True self.acq_thread.start() # Wait for a resource to begin capturing. self.OnPause() self.run_button.Disable() # Subscriptions. pub.subscribe(self.msg_resource, 'resource.added') pub.subscribe(self.msg_resource, 'resource.removed') pub.subscribe(self.msg_data_capture_start, 'data_capture.start') pub.subscribe(self.msg_data_capture_data, 'data_capture.data') pub.subscribe(self.msg_data_capture_stop, 'data_capture.stop') @property def running(self): return self.pause_button.Enabled @property def resource(self): return self.acq_thread.resource @resource.setter def resource(self, value): # Ignore unreadable resources. if value is not None and not value.readable: value = None if self.running: # Currently running. running = True self.OnPause() else: running = False self.acq_thread.resource = value self.run_button.Enable(value is not None) # Resume if applicable. if running: self.OnRun() @property def measurement_resource_name(self): if self._measurement_resource_name is None: return '' else: return self._measurement_resource_name @measurement_resource_name.setter def measurement_resource_name(self, value): if value: self._measurement_resource_name = value try: self.resource = self.global_store.resources[ self._measurement_resource_name] except KeyError: self.resource = None else: self._measurement_resource_name = None self.resource = None def init_values(self): """ Clear captured values. """ self._times = numpy.array([]) self._values = numpy.array([]) def update_plot(self): """ Redraw the plot. """ # Wait for at least one line. if not len(self._times) > 0: display_time = [0] display_values = [0] else: display_time = self._times display_values = self._values if self.plot_settings.update_x: self.plot.x_autoscale() if self.plot_settings.update_y: self.plot.y_autoscale() self.plot.x_data, self.plot.y_data = display_time, display_values def add_values(self, values): """ Update the plot with a new list of values. """ if not self.plot_settings.enabled: return # Extract the times and the data values. times, values = list(zip(*values)) # Update values. self._values = numpy.append(numpy.array([]), values) self._times = numpy.append(numpy.array([]), times) # Plot. self.update_plot() def close(self): """ Perform cleanup. """ # Unsubscriptions. pub.unsubscribe(self.msg_resource, 'resource.added') pub.unsubscribe(self.msg_resource, 'resource.removed') pub.unsubscribe(self.msg_data_capture_start, 'data_capture.start') pub.unsubscribe(self.msg_data_capture_data, 'data_capture.data') pub.unsubscribe(self.msg_data_capture_stop, 'data_capture.stop') # Ensure the thread exits. self.acq_thread.resource = None self.acq_thread.done = True if not self.running: self.running_lock.release() self.acq_thread.join() del self.acq_thread def onSave(self, evt=None): """ save the latest trace to a manually named csv """ outArray = numpy.column_stack((self._times, self._values)) self.defaultName = 'listTrace_{0:04}-{1:02}-{2:02}_{3:02}-{4:02}-{5:02}.csv'.format( *localtime()) fdlg = wx.FileDialog(self, "Save last trace", "", self.defaultName, "CSV files(*.csv)|*.*", wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if fdlg.ShowModal() == wx.ID_OK: self.save_path = fdlg.GetPath() if not (self.save_path.endswith(".csv")): self.save_path = self.save_path + ".csv" numpy.savetxt(self.save_path, outArray, delimiter=',') def OnRun(self, evt=None): """ Let the acquisition thread run. """ self.run_button.Disable() if self.acq_thread.resource is None: return self.running_lock.release() self.pause_button.Enable() def OnPause(self, evt=None): """ Block the acquisition thread. """ if not self.running: return self.running_lock.acquire() if self.acq_thread.resource is not None: self.run_button.Enable() self.pause_button.Disable() def OnReset(self, evt=None): self.init_values() self.update_plot() def OnPlotSettings(self, evt=None): """ Open the plot settings dialog. """ def ok_callback(dlg): self.plot_settings = dlg.GetValue() dlg = PlotSettingsDialog(self, ok_callback) dlg.SetValue(self.plot_settings) dlg.Show() def msg_resource(self, name, value=None): if self.measurement_resource_name is not None and name == self.measurement_resource_name: self.resource = value def msg_data_capture_start(self, name): if name == self.measurement_resource_name: if self.enabled: self.capturing_data = True def msg_data_capture_data(self, name, value): if name == self.measurement_resource_name: if self.capturing_data: self.add_values(value) def msg_data_capture_stop(self, name): if name == self.measurement_resource_name: if self.capturing_data: self.capturing_data = False