def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count)) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # True when acquisition occurs self.acquiring = False # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self._hidden_view = StreamView("Plugin View Hidden") self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams, static=True, ignore_view=True) # The streams currently displayed are the one visible self.add_all_streams() # The list of streams ready for acquisition (just used as a cache) self._acq_streams = {} # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) self._orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: self._orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(self._orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(self._orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) self.start_listening_to_va() # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det, opm=self._main_data_model.opm) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) # The TOOL_ROA is not present because we don't allow the user to change # the ROA), so we need to explicitly request the canvas to show the ROA. if hasattr(self._tab_data_model, "roa") and self._tab_data_model.roa is not None: cnvs = self.pnl_view_acq.canvas self.roa_overlay = RepetitionSelectOverlay(cnvs, self._tab_data_model.roa, self._tab_data_model.fovComp) cnvs.add_world_overlay(self.roa_overlay) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # To update the estimated time when streams are removed/added self._view.stream_tree.flat.subscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.subscribe(self.on_streams_changed)
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) # To turn on/off the fan self._orig_fan_speed = None orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams() # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det) if self._main_data_model.role == "delphi": self._main_data_model.fineAlignDwellTime.value = 0.5 self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # TODO: use the presets VAs and subscribe to each of them, instead of # using pub/sub messages pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. => listen to .flat once it's there # Currently just use view.lastUpdate which should be "similar" # (but doesn't work if the stream contains no image) self._view.lastUpdate.subscribe(self.on_streams_changed)
class AcquisitionDialog(xrcfr_acq): """ Wrapper class responsible for additional initialization of the Acquisition Dialog created in XRCed """ # TODO: share more code with cont.acquisition def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count)) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # True when acquisition occurs self.acquiring = False # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self._hidden_view = StreamView("Plugin View Hidden") self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams, static=True, ignore_view=True) # The streams currently displayed are the one visible self.add_all_streams() # The list of streams ready for acquisition (just used as a cache) self._acq_streams = {} # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) self._orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: self._orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(self._orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(self._orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) self.start_listening_to_va() # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det, opm=self._main_data_model.opm) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) # The TOOL_ROA is not present because we don't allow the user to change # the ROA), so we need to explicitly request the canvas to show the ROA. if hasattr(self._tab_data_model, "roa") and self._tab_data_model.roa is not None: cnvs = self.pnl_view_acq.canvas self.roa_overlay = RepetitionSelectOverlay(cnvs, self._tab_data_model.roa, self._tab_data_model.fovComp) cnvs.add_world_overlay(self.roa_overlay) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # To update the estimated time when streams are removed/added self._view.stream_tree.flat.subscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.subscribe(self.on_streams_changed) def start_listening_to_va(self): # Get all the VA's from the stream and subscribe to them for changes. for entry in self._orig_entries: if hasattr(entry, "vigilattr"): entry.vigilattr.subscribe(self.on_setting_change) def stop_listening_to_va(self): for entry in self._orig_entries: if hasattr(entry, "vigilattr"): entry.vigilattr.unsubscribe(self.on_setting_change) def duplicate_tab_data_model(self, orig): """ Duplicate a MicroscopyGUIData and adapt it for the acquisition window The streams will be shared, but not the views orig (MicroscopyGUIData) return (MicroscopyGUIData) """ # TODO: we'd better create a new view and copy the streams new = copy.copy(orig) # shallow copy new.streams = model.ListVA(orig.streams.value) # duplicate # create view (which cannot move or focus) view = guimodel.MicroscopeView("All") # differentiate it (only one view) new.views = model.ListVA() new.views.value.append(view) new.focussedView = model.VigilantAttribute(view) new.viewLayout = model.IntEnumerated(guimodel.VIEW_LAYOUT_ONE, choices={guimodel.VIEW_LAYOUT_ONE}) new.tool = model.IntEnumerated(TOOL_NONE, choices={TOOL_NONE}) return new def add_all_streams(self): """ Add all the streams present in the interface model to the stream panel. """ # the order the streams are added should not matter on the display, so # it's ok to not duplicate the streamTree literally # go through all the streams available in the interface model for s in self._tab_data_model.streams.value: if isinstance(s, NON_SPATIAL_STREAMS): v = self._hidden_view else: v = self._view self.streambar_controller.addStream(s, add_to_view=v) def remove_all_streams(self): """ Remove the streams we added to the view on creation Must be called in the main GUI thread """ # Ensure we don't update the view after the window is destroyed self.streambar_controller.clear() # TODO: need to have a .clear() on the settings_controller to clean up? self._settings_controller = None self._acq_streams = {} # also empty the cache gc.collect() # To help reclaiming some memory def get_acq_streams(self): """ return (list of Streams): the streams to be acquired """ # Only acquire the streams which are displayed streams = self._view.getStreams() + self._hidden_view.getStreams() # Add the overlay stream if requested, and folds all the streams if streams and self.chkbox_fine_align.Value: streams.append(self._ovrl_stream) self._acq_streams = acqmng.foldStreams(streams, self._acq_streams) return self._acq_streams def find_current_preset(self): """ find the name of the preset identical to the current settings (not including "Custom") returns (string): name of the preset raises KeyError: if no preset can be found """ # check each preset for n, settings in self._preset_values.items(): # compare each value between the current and proposed different = False for entry, value in settings.items(): if entry.vigilattr.value != value: different = True break if not different: return n raise KeyError() def _can_fine_align(self, streams): """ Return True if with the given streams it would make sense to fine align streams (iterable of Stream) return (bool): True if at least a SEM and an optical stream are present """ # check for a SEM stream for s in streams: if isinstance(s, EMStream): break else: return False # check for an optical stream # TODO: allow it also for ScannedFluoStream once fine alignment is supported # on confocal SECOM. for s in streams: if isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream): break else: return False return True @wxlimit_invocation(0.1) def update_setting_display(self): if not self: return # if gauge was left over from an error => now hide it if self.acquiring: self.gauge_acq.Show() return elif self.gauge_acq.IsShown(): self.gauge_acq.Hide() self.Layout() # Enable/disable Fine alignment check box streams = self._view.getStreams() + self._hidden_view.getStreams() can_fa = self._can_fine_align(streams) if self.chkbox_fine_align.Enabled: self._prev_fine_align = self.chkbox_fine_align.Value self.chkbox_fine_align.Enable(can_fa) # Uncheck if disabled, otherwise put same as previous value self.chkbox_fine_align.Value = (can_fa and self._prev_fine_align) self.update_acquisition_time() # update highlight for se, value in self._orig_settings.items(): se.highlight(se.vigilattr.value != value) def on_streams_changed(self, val): """ When the list of streams to acquire has changed """ self.update_setting_display() def on_setting_change(self, _=None): self.update_setting_display() # check presets and fall-back to custom try: preset_name = self.find_current_preset() logging.debug("Detected preset %s", preset_name) except KeyError: # should not happen with the current preset_no_change logging.exception("Couldn't match any preset") preset_name = u"Custom" wx.CallAfter(self.cmb_presets.SetValue, preset_name) def update_acquisition_time(self): """ Must be called in the main GUI thread. """ streams = self.get_acq_streams() if streams: acq_time = acqmng.estimateTime(streams) acq_time = math.ceil(acq_time) # round a bit pessimisticly txt = "The estimated acquisition time is {}." txt = txt.format(units.readable_time(acq_time)) else: txt = "No streams present." self.lbl_acqestimate.SetLabel(txt) @call_in_wx_main def _onFilename(self, name): """ updates the GUI when the filename is updated """ # decompose into path/file path, base = os.path.split(name) self.txt_destination.SetValue(str(path)) # show the end of the path (usually more important) self.txt_destination.SetInsertionPointEnd() self.txt_filename.SetValue(str(base)) def on_preset(self, evt): preset_name = self.cmb_presets.GetValue() try: new_preset = self._preset_values[preset_name] except KeyError: logging.debug("Not changing settings for preset %s", preset_name) return logging.debug("Changing setting to preset %s", preset_name) # TODO: presets should also be able to change the special stream settings # (eg: accumulation/interpolation) when we have them # apply the recorded values apply_preset(new_preset) # The hardware might not exactly apply the setting as computed in the # preset. We need the _exact_ same value to find back which preset is # currently selected. So update the values the first time. if preset_name not in self._presets_confirmed: for se in new_preset.keys(): new_preset[se] = se.vigilattr.value self._presets_confirmed.add(preset_name) self.update_setting_display() def on_key(self, evt): """ Dialog key press handler. """ if evt.GetKeyCode() == wx.WXK_ESCAPE: self.Close() else: evt.Skip() def on_change_file(self, evt): """ Shows a dialog to change the path, name, and format of the acquisition file. returns nothing, but updates .filename and .conf """ fn = create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count) new_name = ShowAcquisitionFileDialog(self, fn) if new_name is not None: self.filename.value = new_name self.conf.fn_ptn, self.conf.fn_count = guess_pattern(new_name) logging.debug("Generated filename pattern '%s'", self.conf.fn_ptn) # It means the user wants to do a new acquisition self.btn_secom_acquire.SetLabel("START") self.last_saved_file = None def terminate_listeners(self): """ Disconnect all the connections to the streams. Must be called in the main GUI thread. """ # stop listening to events self._view.stream_tree.flat.unsubscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.unsubscribe(self.on_streams_changed) self.stop_listening_to_va() self.remove_all_streams() def on_close(self, evt): """ Close event handler that executes various cleanup actions """ if self.acq_future: # TODO: ask for confirmation before cancelling? # What to do if the acquisition is done while asking for # confirmation? msg = "Cancelling acquisition due to closing the acquisition window" logging.info(msg) self.acq_future.cancel() self.terminate_listeners() self.EndModal(wx.ID_CANCEL) def _view_file(self): """ Called to open the file which was just acquired Must be called in the main GUI thread. """ self.terminate_listeners() self.EndModal(wx.ID_OPEN) logging.debug("My return code is %d", self.GetReturnCode()) def _pause_settings(self): """ Pause the settings of the GUI and save the values for restoring them later """ self._settings_controller.pause() self._settings_controller.enable(False) self.streambar_controller.pause() self.streambar_controller.enable(False) def _resume_settings(self): """ Resume the settings of the GUI and save the values for restoring them later """ self._settings_controller.enable(True) self._settings_controller.resume() self.streambar_controller.enable(True) self.streambar_controller.resume() def on_acquire(self, evt): """ Start the actual acquisition """ if self.last_saved_file: # This means the button is actually "View" self._view_file() return logging.info("Acquire button clicked, starting acquisition") self.acquiring = True self.btn_secom_acquire.Disable() # disable estimation time updates during acquisition self._view.lastUpdate.unsubscribe(self.on_streams_changed) # Freeze all the settings so that it's not possible to change anything self._pause_settings() self.gauge_acq.Show() self.Layout() # to put the gauge at the right place # For now, always indicate the best quality (even if the preset is set # to "live") if self._main_data_model.opm: self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_BEST) # Note: It should never be possible to reach here with no streams streams = self.get_acq_streams() v_streams = self._view.getStreams() # visible streams for s in streams: # Add extra viewable streams to view. However, do not add incompatible streams. if s not in v_streams and not isinstance(s, NON_SPATIAL_STREAMS): self._view.addStream(s) # Update the filename in the streams if hasattr(s, "filename"): pathname, base = os.path.split(self.filename.value) s.filename.value = base self.acq_future = acqmng.acquire(streams, self._main_data_model.settings_obs) self._acq_future_connector = ProgressiveFutureConnector(self.acq_future, self.gauge_acq, self.lbl_acqestimate) self.acq_future.add_done_callback(self.on_acquisition_done) self.btn_cancel.SetLabel("Cancel") self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) def on_cancel(self, evt): """ Handle acquisition cancel button click """ if not self.acq_future: logging.warning("Tried to cancel acquisition while it was not started") return logging.info("Cancel button clicked, stopping acquisition") self.acq_future.cancel() self.acquiring = False self.btn_cancel.SetLabel("Close") # all the rest will be handled by on_acquisition_done() @call_in_wx_main def on_acquisition_done(self, future): """ Callback called when the acquisition is finished (either successfully or cancelled) """ if self._main_data_model.opm: self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_FAST) # bind button back to direct closure self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self._resume_settings() self.acquiring = False # re-enable estimation time updates self._view.lastUpdate.subscribe(self.on_streams_changed) self.acq_future = None # To avoid holding the ref in memory self._acq_future_connector = None try: data, exp = future.result(1) # timeout is just for safety self.conf.fn_count = update_counter(self.conf.fn_count) except CancelledError: # put back to original state: # re-enable the acquire button self.btn_secom_acquire.Enable() # hide progress bar (+ put pack estimated time) self.update_acquisition_time() self.gauge_acq.Hide() self.Layout() return except Exception: # We cannot do much: just warn the user and pretend it was cancelled logging.exception("Acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Acquisition failed.") self.lbl_acqestimate.Parent.Layout() # leave the gauge, to give a hint on what went wrong. return # Handle the case acquisition failed "a bit" if exp: logging.warning("Acquisition failed (after %d streams): %s", len(data), exp) # save result to file self.lbl_acqestimate.SetLabel("Saving file...") self.lbl_acqestimate.Parent.Layout() try: thumb = acqmng.computeThumbnail(self._view.stream_tree, future) filename = self.filename.value exporter = dataio.get_converter(self.conf.last_format) exporter.export(filename, data, thumb) logging.info("Acquisition saved as file '%s'.", filename) # Allow to see the acquisition self.btn_secom_acquire.SetLabel("VIEW") self.last_saved_file = filename except Exception: logging.exception("Saving acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Saving acquisition file failed.") self.lbl_acqestimate.Parent.Layout() return if exp: self.lbl_acqestimate.SetLabel("Acquisition failed (partially).") else: self.lbl_acqestimate.SetLabel("Acquisition completed.") # As the action is complete, rename "Cancel" to "Close" self.btn_cancel.SetLabel("Close") self.lbl_acqestimate.Parent.Layout() # Make sure the file is not overridden self.btn_secom_acquire.Enable()
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) # To turn on/off the fan self._orig_fan_speed = None self._orig_fan_temp = None orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self.streambar_controller = StreamBarController( self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams() # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = { } # dict string -> dict (SettingEntries -> value) orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # TODO: use the presets VAs and subscribe to each of them, instead of # using pub/sub messages pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. => listen to .flat once it's there # Currently just use view.lastUpdate which should be "similar" # (but doesn't work if the stream contains no image) self._view.lastUpdate.subscribe(self.on_streams_changed)
class AcquisitionDialog(xrcfr_acq): """ Wrapper class responsible for additional initialization of the Acquisition Dialog created in XRCed """ # TODO: share more code with cont.acquisition def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) # To turn on/off the fan self._orig_fan_speed = None orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams() # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det) if self._main_data_model.role == "delphi": self._main_data_model.fineAlignDwellTime.value = 0.5 self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # TODO: use the presets VAs and subscribe to each of them, instead of # using pub/sub messages pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. => listen to .flat once it's there # Currently just use view.lastUpdate which should be "similar" # (but doesn't work if the stream contains no image) self._view.lastUpdate.subscribe(self.on_streams_changed) def duplicate_tab_data_model(self, orig): """ Duplicate a MicroscopyGUIData and adapt it for the acquisition window The streams will be shared, but not the views orig (MicroscopyGUIData) return (MicroscopyGUIData) """ # TODO: we'd better create a new view and copy the streams new = copy.copy(orig) # shallow copy new.streams = model.ListVA(orig.streams.value) # duplicate # create view (which cannot move or focus) view = guimodel.MicroscopeView("All") # differentiate it (only one view) new.views = model.ListVA() new.views.value.append(view) new.focussedView = model.VigilantAttribute(view) new.viewLayout = model.IntEnumerated(guimodel.VIEW_LAYOUT_ONE, choices={guimodel.VIEW_LAYOUT_ONE}) return new def add_all_streams(self): """ Add all the streams present in the interface model to the stream panel. """ # the order the streams are added should not matter on the display, so # it's ok to not duplicate the streamTree literally # go through all the streams available in the interface model for s in self._tab_data_model.streams.value: self._view.addStream(s) # Add first to the view, so "visible" button is correct self.streambar_controller.add_acquisition_stream_cont(s) # listen to changes in local VAs local_vas = set(s.emt_vas.values()) | set(s.det_vas.values()) for lva in local_vas: lva.subscribe(self.on_setting_change) def remove_all_streams(self): """ Remove the streams we added to the view on creation """ # Ensure we don't update the view after the window is destroyed self.streambar_controller.clear() for s in list(self._tab_data_model.streams.value): # copy, as it's modified by stream cont # Stop listening to changes in local VAs local_vas = set(s.emt_vas.values()) | set(s.det_vas.values()) for lva in local_vas: lva.unsubscribe(self.on_setting_change) def find_current_preset(self): """ find the name of the preset identical to the current settings (not including "Custom") returns (string): name of the preset raises KeyError: if no preset can be found """ # check each preset for n, settings in self._preset_values.items(): # compare each value between the current and proposed different = False for entry, value in settings.items(): if entry.vigilattr.value != value: different = True break if not different: return n raise KeyError() def _can_fine_align(self, streams): """ Return True if with the given streams it would make sense to fine align streams (iterable of Stream) return (bool): True if at least a SEM and an optical stream are present """ # check for a SEM stream for s in streams: if isinstance(s, EMStream): break else: return False # check for an optical stream for s in streams: if isinstance(s, OpticalStream): break else: return False return True @wxlimit_invocation(0.1) def update_setting_display(self): # if gauge was left over from an error => now hide it if self.gauge_acq.IsShown(): self.gauge_acq.Hide() self.Layout() # Enable/disable Fine alignment check box streams = self._view.getStreams() can_fa = self._can_fine_align(streams) if self.chkbox_fine_align.Enabled: self._prev_fine_align = self.chkbox_fine_align.Value self.chkbox_fine_align.Enable(can_fa) # Uncheck if disabled, otherwise put same as previous value self.chkbox_fine_align.Value = (can_fa and self._prev_fine_align) self.update_acquisition_time() # update highlight for se, value in self._orig_settings.items(): se.highlight(se.vigilattr.value != value) def on_streams_changed(self, val): """ When the list of streams to acquire has changed """ self.update_setting_display() def on_setting_change(self, _=None): self.update_setting_display() # check presets and fall-back to custom try: preset_name = self.find_current_preset() logging.debug("Detected preset %s", preset_name) except KeyError: # should not happen with the current preset_no_change logging.exception("Couldn't match any preset") preset_name = u"Custom" self.cmb_presets.SetValue(preset_name) def update_acquisition_time(self): streams = self._view.getStreams() if streams: if self.chkbox_fine_align.Value: streams.append(self._ovrl_stream) acq_time = acq.estimateTime(streams) acq_time = math.ceil(acq_time) # round a bit pessimistically txt = "The estimated acquisition time is {}." txt = txt.format(units.readable_time(acq_time)) else: txt = "No streams present." self.lbl_acqestimate.SetLabel(txt) def _get_default_filename(self): """ Return a good default filename """ # TODO: check the file doesn't yet exist (if the computer clock is # correct it's unlikely) return os.path.join( self.conf.last_path, u"%s%s" % (time.strftime("%Y%m%d-%H%M%S"), self.conf.last_extension) ) def _onFilename(self, name): """ updates the GUI when the filename is updated """ # decompose into path/file path, base = os.path.split(name) self.txt_destination.SetValue(unicode(path)) # show the end of the path (usually more important) self.txt_destination.SetInsertionPointEnd() self.txt_filename.SetValue(unicode(base)) def on_preset(self, evt): preset_name = self.cmb_presets.GetValue() try: new_preset = self._preset_values[preset_name] except KeyError: logging.debug("Not changing settings for preset %s", preset_name) return logging.debug("Changing setting to preset %s", preset_name) # TODO: presets should also be able to change the special stream settings # (eg: accumulation/interpolation) when we have them # apply the recorded values apply_preset(new_preset) # The hardware might not exactly apply the setting as computed in the # preset. We need the _exact_ same value to find back which preset is # currently selected. So update the values the first time. # TODO: this should not be necessary once the settings only change the # stream settings, and not directly the hardware. if preset_name not in self._presets_confirmed: for se in new_preset.keys(): new_preset[se] = se.vigilattr.value self._presets_confirmed.add(preset_name) self.update_setting_display() def on_key(self, evt): """ Dialog key press handler. """ if evt.GetKeyCode() == wx.WXK_ESCAPE: self.Close() else: evt.Skip() def on_change_file(self, evt): """ Shows a dialog to change the path, name, and format of the acquisition file. returns nothing, but updates .filename and .conf """ new_name = ShowAcquisitionFileDialog(self, self.filename.value) if new_name is not None: self.filename.value = new_name # It means the user wants to do a new acquisition self.btn_secom_acquire.SetLabel("START") self.last_saved_file = None def on_close(self, evt): """ Close event handler that executes various cleanup actions """ if self.acq_future: # TODO: ask for confirmation before cancelling? # What to do if the acquisition is done while asking for # confirmation? msg = "Cancelling acquisition due to closing the acquisition window" logging.info(msg) self.acq_future.cancel() self.remove_all_streams() # stop listening to events pub.unsubscribe(self.on_setting_change, 'setting.changed') self._view.lastUpdate.unsubscribe(self.on_streams_changed) self.EndModal(wx.ID_CANCEL) def _view_file(self): """ Called to open the file which was just acquired """ self.remove_all_streams() # stop listening to events pub.unsubscribe(self.on_setting_change, 'setting.changed') self._view.lastUpdate.unsubscribe(self.on_streams_changed) self.EndModal(wx.ID_OPEN) logging.debug("My return code is %d", self.GetReturnCode()) def _pause_settings(self): """ Pause the settings of the GUI and save the values for restoring them later """ self._settings_controller.pause() self._settings_controller.enable(False) self.streambar_controller.pause() self.streambar_controller.enable(False) def _resume_settings(self): """ Resume the settings of the GUI and save the values for restoring them later """ self._settings_controller.enable(True) self._settings_controller.resume() self.streambar_controller.enable(True) self.streambar_controller.resume() def _set_fan(self, enable): """ Turn on/off the fan of the CCD enable (boolean): True to turn on/restore the fan, and False to turn if off """ if model.hasVA(self._main_data_model.ccd, "fanSpeed"): return fs = self._main_data_model.ccd.fanSpeed if enable: if self._orig_fan_speed is not None: fs.value = max(fs.value, self._orig_fan_speed) else: self._orig_fan_speed = fs.value fs.value = 0 def on_acquire(self, evt): """ Start the actual acquisition """ if self.last_saved_file: # This means the button is actually "View" self._view_file() return logging.info("Acquire button clicked, starting acquisition") self.btn_secom_acquire.Disable() # disable estimation time updates during acquisition self._view.lastUpdate.unsubscribe(self.on_streams_changed) # TODO: freeze all the settings so that it's not possible to change anything self._pause_settings() self.gauge_acq.Show() self.Layout() # to put the gauge at the right place # start acquisition + connect events to callback streams = self._view.getStreams() # Add the overlay stream if the fine alignment check box is checked if self.chkbox_fine_align.Value: streams.append(self._ovrl_stream) # Turn off the fan to avoid vibrations (in all acquisitions) self._set_fan(False) # It should never be possible to reach here with no streams self.acq_future = acq.acquire(streams) self._acq_future_connector = ProgressiveFutureConnector(self.acq_future, self.gauge_acq, self.lbl_acqestimate) self.acq_future.add_done_callback(self.on_acquisition_done) self.btn_cancel.SetLabel("Cancel") self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) def on_cancel(self, evt): """ Handle acquisition cancel button click """ if not self.acq_future: logging.warning("Tried to cancel acquisition while it was not started") return logging.info("Cancel button clicked, stopping acquisition") self.acq_future.cancel() self.btn_cancel.SetLabel("Close") # all the rest will be handled by on_acquisition_done() @call_in_wx_main def on_acquisition_done(self, future): """ Callback called when the acquisition is finished (either successfully or cancelled) """ self._set_fan(True) # Turn the fan back on # bind button back to direct closure self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self._resume_settings() # re-enable estimation time updates self._view.lastUpdate.subscribe(self.on_streams_changed) try: data, exp = future.result(1) # timeout is just for safety except CancelledError: # put back to original state: # re-enable the acquire button self.btn_secom_acquire.Enable() # hide progress bar (+ put pack estimated time) self.update_acquisition_time() self.gauge_acq.Hide() self.Layout() return except Exception: # We cannot do much: just warn the user and pretend it was cancelled logging.exception("Acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Acquisition failed.") self.lbl_acqestimate.Parent.Layout() # leave the gauge, to give a hint on what went wrong. return # Handle the case acquisition failed "a bit" if exp: logging.error("Acquisition failed (after %d streams): %s", len(data), exp) # save result to file self.lbl_acqestimate.SetLabel("Saving file...") self.lbl_acqestimate.Parent.Layout() try: thumb = acq.computeThumbnail(self._view.stream_tree, future) filename = self.filename.value exporter = dataio.get_converter(self.conf.last_format) exporter.export(filename, data, thumb) logging.info("Acquisition saved as file '%s'.", filename) # Allow to see the acquisition self.btn_secom_acquire.SetLabel("VIEW") self.last_saved_file = filename except Exception: logging.exception("Saving acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Saving acquisition file failed.") self.lbl_acqestimate.Parent.Layout() return if exp: self.lbl_acqestimate.SetLabel("Acquisition failed (partially).") else: self.lbl_acqestimate.SetLabel("Acquisition completed.") # As the action is complete, rename "Cancel" to "Close" self.btn_cancel.SetLabel("Close") self.lbl_acqestimate.Parent.Layout() # Make sure the file is not overridden self.btn_secom_acquire.Enable()
class AcquisitionDialog(xrcfr_acq): """ Wrapper class responsible for additional initialization of the Acquisition Dialog created in XRCed """ # TODO: share more code with cont.acquisition def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) # To turn on/off the fan self._orig_fan_speed = None self._orig_fan_temp = None orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self.streambar_controller = StreamBarController( self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams() # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = { } # dict string -> dict (SettingEntries -> value) orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # TODO: use the presets VAs and subscribe to each of them, instead of # using pub/sub messages pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. => listen to .flat once it's there # Currently just use view.lastUpdate which should be "similar" # (but doesn't work if the stream contains no image) self._view.lastUpdate.subscribe(self.on_streams_changed) def duplicate_tab_data_model(self, orig): """ Duplicate a MicroscopyGUIData and adapt it for the acquisition window The streams will be shared, but not the views orig (MicroscopyGUIData) return (MicroscopyGUIData) """ # TODO: we'd better create a new view and copy the streams new = copy.copy(orig) # shallow copy new.streams = model.ListVA(orig.streams.value) # duplicate # create view (which cannot move or focus) view = guimodel.MicroscopeView("All") # differentiate it (only one view) new.views = model.ListVA() new.views.value.append(view) new.focussedView = model.VigilantAttribute(view) new.viewLayout = model.IntEnumerated( guimodel.VIEW_LAYOUT_ONE, choices={guimodel.VIEW_LAYOUT_ONE}) return new def add_all_streams(self): """ Add all the streams present in the interface model to the stream panel. """ # the order the streams are added should not matter on the display, so # it's ok to not duplicate the streamTree literally # go through all the streams available in the interface model for s in self._tab_data_model.streams.value: self._view.addStream( s) # Add first to the view, so "visible" button is correct self.streambar_controller.add_acquisition_stream_cont(s) # listen to changes in local VAs local_vas = set(s.emt_vas.values()) | set(s.det_vas.values()) for lva in local_vas: lva.subscribe(self.on_setting_change) def remove_all_streams(self): """ Remove the streams we added to the view on creation """ # Ensure we don't update the view after the window is destroyed self.streambar_controller.clear() for s in list(self._tab_data_model.streams.value ): # copy, as it's modified by stream cont # Stop listening to changes in local VAs local_vas = set(s.emt_vas.values()) | set(s.det_vas.values()) for lva in local_vas: lva.unsubscribe(self.on_setting_change) def find_current_preset(self): """ find the name of the preset identical to the current settings (not including "Custom") returns (string): name of the preset raises KeyError: if no preset can be found """ # check each preset for n, settings in self._preset_values.items(): # compare each value between the current and proposed different = False for entry, value in settings.items(): if entry.vigilattr.value != value: different = True break if not different: return n raise KeyError() def _can_fine_align(self, streams): """ Return True if with the given streams it would make sense to fine align streams (iterable of Stream) return (bool): True if at least a SEM and an optical stream are present """ # check for a SEM stream for s in streams: if isinstance(s, EMStream): break else: return False # check for an optical stream for s in streams: if isinstance(s, OpticalStream): break else: return False return True @wxlimit_invocation(0.1) def update_setting_display(self): # if gauge was left over from an error => now hide it if self.gauge_acq.IsShown(): self.gauge_acq.Hide() self.Layout() # Enable/disable Fine alignment check box streams = self._view.getStreams() can_fa = self._can_fine_align(streams) if self.chkbox_fine_align.Enabled: self._prev_fine_align = self.chkbox_fine_align.Value self.chkbox_fine_align.Enable(can_fa) # Uncheck if disabled, otherwise put same as previous value self.chkbox_fine_align.Value = (can_fa and self._prev_fine_align) self.update_acquisition_time() # update highlight for se, value in self._orig_settings.items(): se.highlight(se.vigilattr.value != value) def on_streams_changed(self, val): """ When the list of streams to acquire has changed """ self.update_setting_display() def on_setting_change(self, _=None): self.update_setting_display() # check presets and fall-back to custom try: preset_name = self.find_current_preset() logging.debug("Detected preset %s", preset_name) except KeyError: # should not happen with the current preset_no_change logging.exception("Couldn't match any preset") preset_name = u"Custom" self.cmb_presets.SetValue(preset_name) def update_acquisition_time(self): streams = self._view.getStreams() if streams: if self.chkbox_fine_align.Value: streams.append(self._ovrl_stream) acq_time = acq.estimateTime(streams) acq_time = math.ceil(acq_time) # round a bit pessimisticly txt = "The estimated acquisition time is {}." txt = txt.format(units.readable_time(acq_time)) else: txt = "No streams present." self.lbl_acqestimate.SetLabel(txt) def _get_default_filename(self): """ Return a good default filename """ # TODO: check the file doesn't yet exist (if the computer clock is # correct it's unlikely) return os.path.join( self.conf.last_path, u"%s%s" % (time.strftime("%Y%m%d-%H%M%S"), self.conf.last_extension)) def _onFilename(self, name): """ updates the GUI when the filename is updated """ # decompose into path/file path, base = os.path.split(name) self.txt_destination.SetValue(unicode(path)) # show the end of the path (usually more important) self.txt_destination.SetInsertionPointEnd() self.txt_filename.SetValue(unicode(base)) def on_preset(self, evt): preset_name = self.cmb_presets.GetValue() try: new_preset = self._preset_values[preset_name] except KeyError: logging.debug("Not changing settings for preset %s", preset_name) return logging.debug("Changing setting to preset %s", preset_name) # TODO: presets should also be able to change the special stream settings # (eg: accumulation/interpolation) when we have them # apply the recorded values apply_preset(new_preset) # The hardware might not exactly apply the setting as computed in the # preset. We need the _exact_ same value to find back which preset is # currently selected. So update the values the first time. if preset_name not in self._presets_confirmed: for se in new_preset.keys(): new_preset[se] = se.vigilattr.value self._presets_confirmed.add(preset_name) self.update_setting_display() def on_key(self, evt): """ Dialog key press handler. """ if evt.GetKeyCode() == wx.WXK_ESCAPE: self.Close() else: evt.Skip() def on_change_file(self, evt): """ Shows a dialog to change the path, name, and format of the acquisition file. returns nothing, but updates .filename and .conf """ new_name = ShowAcquisitionFileDialog(self, self.filename.value) if new_name is not None: self.filename.value = new_name # It means the user wants to do a new acquisition self.btn_secom_acquire.SetLabel("START") self.last_saved_file = None def on_close(self, evt): """ Close event handler that executes various cleanup actions """ if self.acq_future: # TODO: ask for confirmation before cancelling? # What to do if the acquisition is done while asking for # confirmation? msg = "Cancelling acquisition due to closing the acquisition window" logging.info(msg) self.acq_future.cancel() self.remove_all_streams() # stop listening to events pub.unsubscribe(self.on_setting_change, 'setting.changed') self._view.lastUpdate.unsubscribe(self.on_streams_changed) self.EndModal(wx.ID_CANCEL) def _view_file(self): """ Called to open the file which was just acquired """ self.remove_all_streams() # stop listening to events pub.unsubscribe(self.on_setting_change, 'setting.changed') self._view.lastUpdate.unsubscribe(self.on_streams_changed) self.EndModal(wx.ID_OPEN) logging.debug("My return code is %d", self.GetReturnCode()) def _pause_settings(self): """ Pause the settings of the GUI and save the values for restoring them later """ self._settings_controller.pause() self._settings_controller.enable(False) self.streambar_controller.pause() self.streambar_controller.enable(False) def _resume_settings(self): """ Resume the settings of the GUI and save the values for restoring them later """ self._settings_controller.enable(True) self._settings_controller.resume() self.streambar_controller.enable(True) self.streambar_controller.resume() def _set_fan(self, enable): """ Turn on/off the fan of the CCD enable (boolean): True to turn on/restore the fan, and False to turn if off """ if not model.hasVA(self._main_data_model.ccd, "fanSpeed"): return fs = self._main_data_model.ccd.fanSpeed if enable: if self._orig_fan_speed is not None: fs.value = max(fs.value, self._orig_fan_speed) else: self._orig_fan_speed = fs.value fs.value = 0 # Raise targetTemperature to max/ambient to avoid the fan from # automatically starting again. (Some hardware have this built-in when # the current temperature is too high compared to the target) if model.hasVA(self._main_data_model.ccd, "targetTemperature"): temp = self._main_data_model.ccd.targetTemperature if enable: if self._orig_fan_temp is not None: temp.value = min(temp.value, self._orig_fan_temp) else: self._orig_fan_temp = temp.value # TODO: handle choices try: temp.value = min(temp.range[1], 25) # don't set above ambient temperature except Exception: logging.warning( "Failed to change targetTemperature when disabling fan", exc_info=True) def on_acquire(self, evt): """ Start the actual acquisition """ if self.last_saved_file: # This means the button is actually "View" self._view_file() return logging.info("Acquire button clicked, starting acquisition") self.btn_secom_acquire.Disable() # disable estimation time updates during acquisition self._view.lastUpdate.unsubscribe(self.on_streams_changed) # TODO: freeze all the settings so that it's not possible to change anything self._pause_settings() self.gauge_acq.Show() self.Layout() # to put the gauge at the right place # start acquisition + connect events to callback streams = self._view.getStreams() # Add the overlay stream if the fine alignment check box is checked if self.chkbox_fine_align.Value: streams.append(self._ovrl_stream) # Turn off the fan to avoid vibrations (in all acquisitions) self._set_fan(False) # It should never be possible to reach here with no streams self.acq_future = acq.acquire(streams) self._acq_future_connector = ProgressiveFutureConnector( self.acq_future, self.gauge_acq, self.lbl_acqestimate) self.acq_future.add_done_callback(self.on_acquisition_done) self.btn_cancel.SetLabel("Cancel") self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) def on_cancel(self, evt): """ Handle acquisition cancel button click """ if not self.acq_future: logging.warning( "Tried to cancel acquisition while it was not started") return logging.info("Cancel button clicked, stopping acquisition") self.acq_future.cancel() self.btn_cancel.SetLabel("Close") # all the rest will be handled by on_acquisition_done() @call_in_wx_main def on_acquisition_done(self, future): """ Callback called when the acquisition is finished (either successfully or cancelled) """ self._set_fan(True) # Turn the fan back on # bind button back to direct closure self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self._resume_settings() # re-enable estimation time updates self._view.lastUpdate.subscribe(self.on_streams_changed) self.acq_future = None # To avoid holding the ref in memory self._acq_future_connector = None try: data, exp = future.result(1) # timeout is just for safety except CancelledError: # put back to original state: # re-enable the acquire button self.btn_secom_acquire.Enable() # hide progress bar (+ put pack estimated time) self.update_acquisition_time() self.gauge_acq.Hide() self.Layout() return except Exception: # We cannot do much: just warn the user and pretend it was cancelled logging.exception("Acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Acquisition failed.") self.lbl_acqestimate.Parent.Layout() # leave the gauge, to give a hint on what went wrong. return # Handle the case acquisition failed "a bit" if exp: logging.warning("Acquisition failed (after %d streams): %s", len(data), exp) # save result to file self.lbl_acqestimate.SetLabel("Saving file...") self.lbl_acqestimate.Parent.Layout() try: thumb = acq.computeThumbnail(self._view.stream_tree, future) filename = self.filename.value exporter = dataio.get_converter(self.conf.last_format) exporter.export(filename, data, thumb) logging.info("Acquisition saved as file '%s'.", filename) # Allow to see the acquisition self.btn_secom_acquire.SetLabel("VIEW") self.last_saved_file = filename except Exception: logging.exception("Saving acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Saving acquisition file failed.") self.lbl_acqestimate.Parent.Layout() return if exp: self.lbl_acqestimate.SetLabel("Acquisition failed (partially).") else: self.lbl_acqestimate.SetLabel("Acquisition completed.") # As the action is complete, rename "Cancel" to "Close" self.btn_cancel.SetLabel("Close") self.lbl_acqestimate.Parent.Layout() # Make sure the file is not overridden self.btn_secom_acquire.Enable()
def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController(self, self._tab_data_model, highlight_change=True) # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) orig_entries = self._settings_controller.entries self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) orig_view = orig_tab_data.focussedView.value view = self._tab_data_model.focussedView.value self.stream_controller = StreamController(self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams(orig_view.getStreams()) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True main_data = self._tab_data_model.main self._ovrl_stream = stream.OverlayStream("fine alignment", main_data.ccd, main_data.ebeam, main_data.sed) self._ovrl_stream.dwellTime.value = main_data.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating view.view_pos.value = orig_view.view_pos.value view.mpp.value = orig_view.mpp.value view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.setView(view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. # Currently just use view.last_update which should be "similar" view.lastUpdate.subscribe(self.on_streams_changed)
class AcquisitionDialog(xrcfr_acq): """ Wrapper class responsible for additional initialization of the Acquisition Dialog created in XRCed """ # TODO: share more code with cont.acquisition def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(self._get_default_filename()) self.filename.subscribe(self._onFilename, init=True) # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController(self, self._tab_data_model, highlight_change=True) # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) orig_entries = self._settings_controller.entries self._orig_settings = preset_as_is(orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) orig_view = orig_tab_data.focussedView.value view = self._tab_data_model.focussedView.value self.stream_controller = StreamController(self._tab_data_model, self.pnl_secom_streams) # The streams currently displayed are the one visible self.add_all_streams(orig_view.getStreams()) # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True main_data = self._tab_data_model.main self._ovrl_stream = stream.OverlayStream("fine alignment", main_data.ccd, main_data.ebeam, main_data.sed) self._ovrl_stream.dwellTime.value = main_data.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating view.view_pos.value = orig_view.view_pos.value view.mpp.value = orig_view.mpp.value view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.setView(view, self._tab_data_model) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset pub.subscribe(self.on_setting_change, 'setting.changed') # TODO: we should actually listen to the stream tree, but it's not # currently possible. # Currently just use view.last_update which should be "similar" view.lastUpdate.subscribe(self.on_streams_changed) def duplicate_tab_data_model(self, orig): """ Duplicate a MicroscopyGUIData and adapt it for the acquisition window The streams will be shared, but not the views orig (MicroscopyGUIData) return (MicroscopyGUIData) """ new = copy.copy(orig) # shallow copy # create view (which cannot move or focus) view = guimodel.MicroscopeView(orig.focussedView.value.name.value) # differentiate it (only one view) new.views = {"all": view} new.focussedView = model.VigilantAttribute(view) new.viewLayout = model.IntEnumerated(guimodel.VIEW_LAYOUT_ONE, choices=set([guimodel.VIEW_LAYOUT_ONE])) return new def add_all_streams(self, visible_streams): """ Add all the streams present in the interface model to the stream panel. visible_streams (list of streams): the streams that should be visible """ # the order the streams are added should not matter on the display, so # it's ok to not duplicate the streamTree literally view = self._tab_data_model.focussedView.value # Add stream to view first, so that the "visible" button is correct for s in visible_streams: view.addStream(s) # go through all the streams available in the interface model for s in self._tab_data_model.streams.value: sp = self.stream_controller.addStreamForAcquisition(s) def remove_all_streams(self): """ Remove the streams we added to the view on creation """ # Ensure we don't update the view after the window is destroyed view = self._tab_data_model.focussedView.value for s in view.getStreams(): view.removeStream(s) def find_current_preset(self): """ find the name of the preset identical to the current settings (not including "Custom") returns (string): name of the preset raises KeyError: if no preset can be found """ # check each preset for n, settings in self._preset_values.items(): # compare each value between the current and proposed different = False for entry, value in settings.items(): if entry.va.value != value: different = True break if not different: return n raise KeyError() def _can_fine_align(self, streams): """ Return True if with the given streams it would make sense to fine align streams (iterable of Stream) return (bool): True if at least a SEM and an optical stream are present """ # check for a SEM stream for s in streams: if isinstance(s, EM_STREAMS): break else: return False # check for an optical stream for s in streams: if isinstance(s, OPTICAL_STREAMS): break else: return False return True @call_after def update_setting_display(self): # if gauge was left over from an error => now hide it if self.gauge_acq.IsShown(): self.gauge_acq.Hide() self.Layout() # Enable/disable Fine alignment check box streams = self._tab_data_model.focussedView.value.getStreams() can_fa = self._can_fine_align(streams) if self.chkbox_fine_align.Enabled: self._prev_fine_align = self.chkbox_fine_align.Value self.chkbox_fine_align.Enable(can_fa) # Uncheck if disabled, otherwise put same as previous value self.chkbox_fine_align.Value = (can_fa and self._prev_fine_align) self.update_acquisition_time() # update highlight for se, value in self._orig_settings.items(): se.highlight(se.va.value != value) def on_streams_changed(self, val): """ When the list of streams to acquire has changed """ self.update_setting_display() def on_setting_change(self, setting_ctrl): self.update_setting_display() # check presets and fall-back to custom try: preset_name = self.find_current_preset() logging.debug("Detected preset %s", preset_name) except KeyError: # should not happen with the current preset_no_change logging.exception("Couldn't match any preset") preset_name = u"Custom" self.cmb_presets.SetValue(preset_name) def update_acquisition_time(self): streams = self._tab_data_model.focussedView.value.getStreams() if streams: if self.chkbox_fine_align.Value: streams.add(self._ovrl_stream) acq_time = acq.estimateTime(streams) acq_time = math.ceil(acq_time) # round a bit pessimistically txt = "The estimated acquisition time is {}." txt = txt.format(units.readable_time(acq_time)) else: txt = "No streams present." self.lbl_acqestimate.SetLabel(txt) def _get_default_filename(self): """ Return a good default filename """ # TODO: check the file doesn't yet exist (if the computer clock is # correct it's unlikely) return os.path.join(self.conf.last_path, u"%s%s" % (time.strftime("%Y%m%d-%H%M%S"), self.conf.last_extension) ) def _onFilename(self, name): """ updates the GUI when the filename is updated """ # decompose into path/file path, base = os.path.split(name) self.txt_destination.SetValue(unicode(path)) # show the end of the path (usually more important) self.txt_destination.SetInsertionPointEnd() self.txt_filename.SetValue(unicode(base)) def on_preset(self, evt): preset_name = self.cmb_presets.GetValue() try: new_preset = self._preset_values[preset_name] except KeyError: logging.debug("Not changing settings for preset %s", preset_name) return logging.debug("Changing setting to preset %s", preset_name) # TODO: presets should also be able to change the special stream settings # (eg: accumulation/interpolation) when we have them # apply the recorded values apply_preset(new_preset) # The hardware might not exactly apply the setting as computed in the # preset. We need the _exact_ same value to find back which preset is # currently selected. So update the values the first time. # TODO: this should not be necessary once the settings only change the # stream settings, and not directly the hardware. if not preset_name in self._presets_confirmed: for se in new_preset.keys(): new_preset[se] = se.va.value self._presets_confirmed.add(preset_name) self.update_setting_display() def on_key(self, evt): """ Dialog key press handler. """ if evt.GetKeyCode() == wx.WXK_ESCAPE: self.Close() else: evt.Skip() def on_change_file(self, evt): """ Shows a dialog to change the path, name, and format of the acquisition file. returns nothing, but updates .filename and .conf """ new_name = ShowAcquisitionFileDialog(self, self.filename.value) self.filename.value = new_name def on_close(self, evt): """ Close event handler that executes various cleanup actions """ if self.acq_future: # TODO: ask for confirmation before cancelling? # What to do if the acquisition is done while asking for # confirmation? msg = "Cancelling acquisition due to closing the acquisition window" logging.info(msg) self.acq_future.cancel() self.remove_all_streams() # stop listening to events pub.unsubscribe(self.on_setting_change, 'setting.changed') self.Destroy() def _pause_settings(self): """ Pause the settings of the GUI and save the values for restoring them later """ self._settings_controller.pause() self._settings_controller.enable(False) def _resume_settings(self): self._settings_controller.resume() self._settings_controller.enable(True) def on_acquire(self, evt): """ Start the acquisition (really) """ self.btn_secom_acquire.Disable() # disable estimation time updates during acquisition view = self._tab_data_model.focussedView.value view.lastUpdate.unsubscribe(self.on_streams_changed) # TODO: freeze all the settings so that it's not possible to change anything self._pause_settings() self.gauge_acq.Show() self.Layout() # to put the gauge at the right place # start acquisition + connect events to callback streams = self._tab_data_model.focussedView.value.getStreams() # Add the overlay stream if the fine alignment check box is checked if self.chkbox_fine_align.Value: streams.add(self._ovrl_stream) # It should never be possible to reach here with no streams self.acq_future = acq.acquire(streams) self._acq_future_connector = ProgessiveFutureConnector(self.acq_future, self.gauge_acq, self.lbl_acqestimate) self.acq_future.add_done_callback(self.on_acquisition_done) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) def on_cancel(self, evt): """ Called during acquisition when pressing the cancel button """ if not self.acq_future: msg = "Tried to cancel acquisition while it was not started" logging.warning(msg) return self.acq_future.cancel() # all the rest will be handled by on_acquisition_done() @call_after def on_acquisition_done(self, future): """ Callback called when the acquisition is finished (either successfully or cancelled) """ # bind button back to direct closure self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self._resume_settings() # reenable estimation time updates view = self._tab_data_model.focussedView.value view.lastUpdate.subscribe(self.on_streams_changed) try: data = future.result(1) # timeout is just for safety except CancelledError: # put back to original state: # re-enable the acquire button self.btn_secom_acquire.Enable() # hide progress bar (+ put pack estimated time) self.update_acquisition_time() self.gauge_acq.Hide() self.Layout() return except Exception: # We cannot do much: just warn the user and pretend it was cancelled logging.exception("Acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Acquisition failed.") # leave the gauge, to give a hint on what went wrong. return # save result to file self.lbl_acqestimate.SetLabel("Saving file...") try: thumb = acq.computeThumbnail( self._tab_data_model.focussedView.value.stream_tree, future) filename = self.filename.value exporter = dataio.get_exporter(self.conf.last_format) exporter.export(filename, data, thumb) logging.info("Acquisition saved as file '%s'.", filename) except Exception: logging.exception("Saving acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Saving acquisition file failed.") return self.lbl_acqestimate.SetLabel("Acquisition completed.") # We don't allow to acquire anymore => change button name self.btn_cancel.SetLabel("Close")
class AcquisitionDialog(xrcfr_acq): """ Wrapper class responsible for additional initialization of the Acquisition Dialog created in XRCed """ # TODO: share more code with cont.acquisition def __init__(self, parent, orig_tab_data): xrcfr_acq.__init__(self, parent) self.conf = get_acqui_conf() for n in presets: self.cmb_presets.Append(n) # TODO: record and reuse the preset used? self.cmb_presets.Select(0) self.filename = model.StringVA(create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count)) self.filename.subscribe(self._onFilename, init=True) # The name of the last file that got written to disk (used for auto viewing on close) self.last_saved_file = None # True when acquisition occurs self.acquiring = False # a ProgressiveFuture if the acquisition is going on self.acq_future = None self._acq_future_connector = None self._main_data_model = orig_tab_data.main # duplicate the interface, but with only one view self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data) # Create a new settings controller for the acquisition dialog self._settings_controller = SecomSettingsController( self, self._tab_data_model, highlight_change=True # also adds a "Reset" context menu ) orig_view = orig_tab_data.focussedView.value self._view = self._tab_data_model.focussedView.value self._hidden_view = StreamView("Plugin View Hidden") self.streambar_controller = StreamBarController(self._tab_data_model, self.pnl_secom_streams, static=True, ignore_view=True) # The streams currently displayed are the one visible self.add_all_streams() # The list of streams ready for acquisition (just used as a cache) self._acq_streams = {} # FIXME: pass the fold_panels # Compute the preset values for each preset self._preset_values = {} # dict string -> dict (SettingEntries -> value) self._orig_entries = get_global_settings_entries(self._settings_controller) for sc in self.streambar_controller.stream_controllers: self._orig_entries += get_local_settings_entries(sc) self._orig_settings = preset_as_is(self._orig_entries) # to detect changes for n, preset in presets.items(): self._preset_values[n] = preset(self._orig_entries) # Presets which have been confirmed on the hardware self._presets_confirmed = set() # (string) self.start_listening_to_va() # If it could be possible to do fine alignment, allow the user to choose if self._can_fine_align(self._tab_data_model.streams.value): self.chkbox_fine_align.Show() # Set to True to make it the default, but will be automatically # disabled later if the current visible streams don't allow it. self.chkbox_fine_align.Value = True for s in self._tab_data_model.streams.value: if isinstance(s, EMStream): em_det = s.detector em_emt = s.emitter elif isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream): opt_det = s.detector self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det, opm=self._main_data_model.opm) self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value else: self.chkbox_fine_align.Show(False) self.chkbox_fine_align.Value = False self._prev_fine_align = self.chkbox_fine_align.Value # make sure the view displays the same thing as the one we are # duplicating self._view.view_pos.value = orig_view.view_pos.value self._view.mpp.value = orig_view.mpp.value self._view.merge_ratio.value = orig_view.merge_ratio.value # attach the view to the viewport self.pnl_view_acq.canvas.fit_view_to_next_image = False self.pnl_view_acq.setView(self._view, self._tab_data_model) # The TOOL_ROA is not present because we don't allow the user to change # the ROA), so we need to explicitly request the canvas to show the ROA. if hasattr(self._tab_data_model, "roa") and self._tab_data_model.roa is not None: cnvs = self.pnl_view_acq.canvas self.roa_overlay = RepetitionSelectOverlay(cnvs, self._tab_data_model.roa, self._tab_data_model.fovComp) cnvs.add_world_overlay(self.roa_overlay) self.Bind(wx.EVT_CHAR_HOOK, self.on_key) self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file) self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire) self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset) self.Bind(wx.EVT_CLOSE, self.on_close) # on_streams_changed is compatible because it doesn't use the args self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed) self.on_preset(None) # will force setting the current preset # To update the estimated time when streams are removed/added self._view.stream_tree.flat.subscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.subscribe(self.on_streams_changed) def start_listening_to_va(self): # Get all the VA's from the stream and subscribe to them for changes. for entry in self._orig_entries: if hasattr(entry, "vigilattr"): entry.vigilattr.subscribe(self.on_setting_change) def stop_listening_to_va(self): for entry in self._orig_entries: if hasattr(entry, "vigilattr"): entry.vigilattr.unsubscribe(self.on_setting_change) def duplicate_tab_data_model(self, orig): """ Duplicate a MicroscopyGUIData and adapt it for the acquisition window The streams will be shared, but not the views orig (MicroscopyGUIData) return (MicroscopyGUIData) """ # TODO: we'd better create a new view and copy the streams new = copy.copy(orig) # shallow copy new.streams = model.ListVA(orig.streams.value) # duplicate # create view (which cannot move or focus) view = guimodel.MicroscopeView("All") # differentiate it (only one view) new.views = model.ListVA() new.views.value.append(view) new.focussedView = model.VigilantAttribute(view) new.viewLayout = model.IntEnumerated(guimodel.VIEW_LAYOUT_ONE, choices={guimodel.VIEW_LAYOUT_ONE}) new.tool = model.IntEnumerated(TOOL_NONE, choices={TOOL_NONE}) return new def add_all_streams(self): """ Add all the streams present in the interface model to the stream panel. """ # the order the streams are added should not matter on the display, so # it's ok to not duplicate the streamTree literally # go through all the streams available in the interface model for s in self._tab_data_model.streams.value: if isinstance(s, NON_SPATIAL_STREAMS): v = self._hidden_view else: v = self._view self.streambar_controller.addStream(s, add_to_view=v) def remove_all_streams(self): """ Remove the streams we added to the view on creation """ # Ensure we don't update the view after the window is destroyed self.streambar_controller.clear() # TODO: need to have a .clear() on the settings_controller to clean up? self._settings_controller = None self._acq_streams = {} # also empty the cache gc.collect() # To help reclaiming some memory def get_acq_streams(self): """ return (list of Streams): the streams to be acquired """ # Only acquire the streams which are displayed streams = self._view.getStreams() + self._hidden_view.getStreams() # Add the overlay stream if requested, and folds all the streams if streams and self.chkbox_fine_align.Value: streams.append(self._ovrl_stream) self._acq_streams = acq.foldStreams(streams, self._acq_streams) return self._acq_streams def find_current_preset(self): """ find the name of the preset identical to the current settings (not including "Custom") returns (string): name of the preset raises KeyError: if no preset can be found """ # check each preset for n, settings in self._preset_values.items(): # compare each value between the current and proposed different = False for entry, value in settings.items(): if entry.vigilattr.value != value: different = True break if not different: return n raise KeyError() def _can_fine_align(self, streams): """ Return True if with the given streams it would make sense to fine align streams (iterable of Stream) return (bool): True if at least a SEM and an optical stream are present """ # check for a SEM stream for s in streams: if isinstance(s, EMStream): break else: return False # check for an optical stream # TODO: allow it also for ScannedFluoStream once fine alignment is supported # on confocal SECOM. for s in streams: if isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream): break else: return False return True @wxlimit_invocation(0.1) def update_setting_display(self): if not self: return # if gauge was left over from an error => now hide it if self.acquiring: self.gauge_acq.Show() elif self.gauge_acq.IsShown(): self.gauge_acq.Hide() self.Layout() # Enable/disable Fine alignment check box streams = self._view.getStreams() + self._hidden_view.getStreams() can_fa = self._can_fine_align(streams) if self.chkbox_fine_align.Enabled: self._prev_fine_align = self.chkbox_fine_align.Value self.chkbox_fine_align.Enable(can_fa) # Uncheck if disabled, otherwise put same as previous value self.chkbox_fine_align.Value = (can_fa and self._prev_fine_align) self.update_acquisition_time() # update highlight for se, value in self._orig_settings.items(): se.highlight(se.vigilattr.value != value) def on_streams_changed(self, val): """ When the list of streams to acquire has changed """ self.update_setting_display() def on_setting_change(self, _=None): self.update_setting_display() # check presets and fall-back to custom try: preset_name = self.find_current_preset() logging.debug("Detected preset %s", preset_name) except KeyError: # should not happen with the current preset_no_change logging.exception("Couldn't match any preset") preset_name = u"Custom" self.cmb_presets.SetValue(preset_name) def update_acquisition_time(self): streams = self.get_acq_streams() if streams: acq_time = acq.estimateTime(streams) acq_time = math.ceil(acq_time) # round a bit pessimisticly txt = "The estimated acquisition time is {}." txt = txt.format(units.readable_time(acq_time)) else: txt = "No streams present." self.lbl_acqestimate.SetLabel(txt) def _onFilename(self, name): """ updates the GUI when the filename is updated """ # decompose into path/file path, base = os.path.split(name) self.txt_destination.SetValue(unicode(path)) # show the end of the path (usually more important) self.txt_destination.SetInsertionPointEnd() self.txt_filename.SetValue(unicode(base)) def on_preset(self, evt): preset_name = self.cmb_presets.GetValue() try: new_preset = self._preset_values[preset_name] except KeyError: logging.debug("Not changing settings for preset %s", preset_name) return logging.debug("Changing setting to preset %s", preset_name) # TODO: presets should also be able to change the special stream settings # (eg: accumulation/interpolation) when we have them # apply the recorded values apply_preset(new_preset) # The hardware might not exactly apply the setting as computed in the # preset. We need the _exact_ same value to find back which preset is # currently selected. So update the values the first time. if preset_name not in self._presets_confirmed: for se in new_preset.keys(): new_preset[se] = se.vigilattr.value self._presets_confirmed.add(preset_name) self.update_setting_display() def on_key(self, evt): """ Dialog key press handler. """ if evt.GetKeyCode() == wx.WXK_ESCAPE: self.Close() else: evt.Skip() def on_change_file(self, evt): """ Shows a dialog to change the path, name, and format of the acquisition file. returns nothing, but updates .filename and .conf """ fn = create_filename(self.conf.last_path, self.conf.fn_ptn, self.conf.last_extension, self.conf.fn_count) new_name = ShowAcquisitionFileDialog(self, fn) if new_name is not None: self.filename.value = new_name self.conf.fn_ptn, self.conf.fn_count = guess_pattern(new_name) logging.debug("Generated filename pattern '%s'", self.conf.fn_ptn) # It means the user wants to do a new acquisition self.btn_secom_acquire.SetLabel("START") self.last_saved_file = None def terminate_listeners(self): # stop listening to events self._view.stream_tree.flat.unsubscribe(self.on_streams_changed) self._hidden_view.stream_tree.flat.unsubscribe(self.on_streams_changed) self.stop_listening_to_va() self.remove_all_streams() def on_close(self, evt): """ Close event handler that executes various cleanup actions """ if self.acq_future: # TODO: ask for confirmation before cancelling? # What to do if the acquisition is done while asking for # confirmation? msg = "Cancelling acquisition due to closing the acquisition window" logging.info(msg) self.acq_future.cancel() self.terminate_listeners() self.EndModal(wx.ID_CANCEL) def _view_file(self): """ Called to open the file which was just acquired """ self.terminate_listeners() self.EndModal(wx.ID_OPEN) logging.debug("My return code is %d", self.GetReturnCode()) def _pause_settings(self): """ Pause the settings of the GUI and save the values for restoring them later """ self._settings_controller.pause() self._settings_controller.enable(False) self.streambar_controller.pause() self.streambar_controller.enable(False) def _resume_settings(self): """ Resume the settings of the GUI and save the values for restoring them later """ self._settings_controller.enable(True) self._settings_controller.resume() self.streambar_controller.enable(True) self.streambar_controller.resume() def on_acquire(self, evt): """ Start the actual acquisition """ if self.last_saved_file: # This means the button is actually "View" self._view_file() return logging.info("Acquire button clicked, starting acquisition") self.acquiring = True self.btn_secom_acquire.Disable() # disable estimation time updates during acquisition self._view.lastUpdate.unsubscribe(self.on_streams_changed) # Freeze all the settings so that it's not possible to change anything self._pause_settings() self.gauge_acq.Show() self.Layout() # to put the gauge at the right place # For now, always indicate the best quality (even if the preset is set # to "live") if self._main_data_model.opm: self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_BEST) # Note: It should never be possible to reach here with no streams streams = self.get_acq_streams() v_streams = self._view.getStreams() # visible streams for s in streams: # Add extra viewable streams to view. However, do not add incompatible streams. if s not in v_streams and not isinstance(s, NON_SPATIAL_STREAMS): self._view.addStream(s) # Update the filename in the streams if hasattr(s, "filename"): pathname, base = os.path.split(self.filename.value) s.filename.value = base self.acq_future = acq.acquire(streams) self._acq_future_connector = ProgressiveFutureConnector(self.acq_future, self.gauge_acq, self.lbl_acqestimate) self.acq_future.add_done_callback(self.on_acquisition_done) self.btn_cancel.SetLabel("Cancel") self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel) def on_cancel(self, evt): """ Handle acquisition cancel button click """ if not self.acq_future: logging.warning("Tried to cancel acquisition while it was not started") return logging.info("Cancel button clicked, stopping acquisition") self.acq_future.cancel() self.acquiring = False self.btn_cancel.SetLabel("Close") # all the rest will be handled by on_acquisition_done() @call_in_wx_main def on_acquisition_done(self, future): """ Callback called when the acquisition is finished (either successfully or cancelled) """ if self._main_data_model.opm: self._main_data_model.opm.setAcqQuality(path.ACQ_QUALITY_FAST) # bind button back to direct closure self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close) self._resume_settings() self.acquiring = False # re-enable estimation time updates self._view.lastUpdate.subscribe(self.on_streams_changed) self.acq_future = None # To avoid holding the ref in memory self._acq_future_connector = None try: data, exp = future.result(1) # timeout is just for safety self.conf.fn_count = update_counter(self.conf.fn_count) except CancelledError: # put back to original state: # re-enable the acquire button self.btn_secom_acquire.Enable() # hide progress bar (+ put pack estimated time) self.update_acquisition_time() self.gauge_acq.Hide() self.Layout() return except Exception: # We cannot do much: just warn the user and pretend it was cancelled logging.exception("Acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Acquisition failed.") self.lbl_acqestimate.Parent.Layout() # leave the gauge, to give a hint on what went wrong. return # Handle the case acquisition failed "a bit" if exp: logging.warning("Acquisition failed (after %d streams): %s", len(data), exp) # save result to file self.lbl_acqestimate.SetLabel("Saving file...") self.lbl_acqestimate.Parent.Layout() try: thumb = acq.computeThumbnail(self._view.stream_tree, future) filename = self.filename.value exporter = dataio.get_converter(self.conf.last_format) exporter.export(filename, data, thumb) logging.info("Acquisition saved as file '%s'.", filename) # Allow to see the acquisition self.btn_secom_acquire.SetLabel("VIEW") self.last_saved_file = filename except Exception: logging.exception("Saving acquisition failed") self.btn_secom_acquire.Enable() self.lbl_acqestimate.SetLabel("Saving acquisition file failed.") self.lbl_acqestimate.Parent.Layout() return if exp: self.lbl_acqestimate.SetLabel("Acquisition failed (partially).") else: self.lbl_acqestimate.SetLabel("Acquisition completed.") # As the action is complete, rename "Cancel" to "Close" self.btn_cancel.SetLabel("Close") self.lbl_acqestimate.Parent.Layout() # Make sure the file is not overridden self.btn_secom_acquire.Enable()