diff --git a/src/instamatic/camera/camera_client.py b/src/instamatic/camera/camera_client.py index 6b320657..9f75314d 100644 --- a/src/instamatic/camera/camera_client.py +++ b/src/instamatic/camera/camera_client.py @@ -3,9 +3,9 @@ import atexit import socket import subprocess as sp +import threading import time from functools import wraps -from typing import Dict import numpy as np @@ -55,6 +55,7 @@ def __init__( self.name = name self.interface = interface self._bufsize = BUFSIZE + self._eval_lock = threading.Lock() self.verbose = False try: @@ -79,7 +80,7 @@ def __init__( ) print('Use shared memory:', self.use_shared_memory) - self.buffers: Dict[str, np.ndarray] = {} + self.buffers: dict[str, np.ndarray] = {} self.shms = {} self._attr_dct: dict = {} @@ -122,36 +123,37 @@ def wrapper(*args, **kwargs): def _eval_dct(self, dct): """Takes approximately 0.2-0.3 ms per call if HOST=='localhost'.""" - self.s.send(dumper(dct)) + with self._eval_lock: + self.s.send(dumper(dct)) - acquiring_image = dct['attr_name'] == 'get_image' - acquiring_movie = dct['attr_name'] == 'get_movie' + acquiring_image = dct['attr_name'] == 'get_image' + acquiring_movie = dct['attr_name'] == 'get_movie' - if acquiring_movie: - raise NotImplementedError('Acquiring movies over a socket is not supported.') + if acquiring_movie: + raise NotImplementedError('Acquiring movies over a socket is not supported.') - if acquiring_image and not self.use_shared_memory: - response = self.s.recv(self._imagebufsize) - else: - response = self.s.recv(self._bufsize) + if acquiring_image and not self.use_shared_memory: + response = self.s.recv(self._imagebufsize) + else: + response = self.s.recv(self._bufsize) - if response: - status, data = loader(response) - else: - raise RuntimeError(f'Received empty response when evaluating {dct=}') + if response: + status, data = loader(response) + else: + raise RuntimeError(f'Received empty response when evaluating {dct=}') - if self.use_shared_memory and acquiring_image: - data = self.get_data_from_shared_memory(**data) + if self.use_shared_memory and acquiring_image: + data = self.get_data_from_shared_memory(**data) - if status == 200: - return data + if status == 200: + return data - elif status == 500: - error_code, args = data - raise exception_list.get(error_code, TEMCommunicationError)(*args) + elif status == 500: + error_code, args = data + raise exception_list.get(error_code, TEMCommunicationError)(*args) - else: - raise ConnectionError(f'Unknown status code: {status}') + else: + raise ConnectionError(f'Unknown status code: {status}') def _init_dict(self): """Get list of functions and their doc strings from the uninitialized diff --git a/src/instamatic/gui/autocred_frame.py b/src/instamatic/gui/autocred_frame.py index fecaa50b..718775a2 100644 --- a/src/instamatic/gui/autocred_frame.py +++ b/src/instamatic/gui/autocred_frame.py @@ -13,10 +13,10 @@ from instamatic.calibrate import CalibBeamShift from instamatic.calibrate.filenames import * -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalautocRED(LabelFrame, HasQMixin): +class ExperimentalautocRED(LabelFrame, ModuleFrameMixin): """Data collection protocol for SerialRED data collection on a high-speed Timepix camera using automated screening and crystal tracking. diff --git a/src/instamatic/gui/base_module.py b/src/instamatic/gui/base_module.py index 29f97f01..971abf94 100644 --- a/src/instamatic/gui/base_module.py +++ b/src/instamatic/gui/base_module.py @@ -1,6 +1,7 @@ from __future__ import annotations from queue import Queue +from typing import Any class BaseModule: @@ -35,7 +36,8 @@ def initialize(self, parent): return frame -class HasQMixin: - """Asserts module.q remains reserved for DataCollectionController.q.""" +class ModuleFrameMixin: + """Asserts some class attributes i.e. module.q, app remain reserved.""" q: Queue + app: Any diff --git a/src/instamatic/gui/click_dispatcher.py b/src/instamatic/gui/click_dispatcher.py index 7b430cb2..b07ec574 100644 --- a/src/instamatic/gui/click_dispatcher.py +++ b/src/instamatic/gui/click_dispatcher.py @@ -82,9 +82,11 @@ def add_listener( self, name: str, callback: Optional[Callable[[ClickEvent], None]] = None, + active: bool = False, ) -> ClickListener: """Convenience method that adds and returns a new `ClickListener`""" listener = ClickListener(name, callback) + listener.active = active self.listeners[name] = listener return listener diff --git a/src/instamatic/gui/cred_fei_frame.py b/src/instamatic/gui/cred_fei_frame.py index d0fd8e9d..6fef0a23 100644 --- a/src/instamatic/gui/cred_fei_frame.py +++ b/src/instamatic/gui/cred_fei_frame.py @@ -5,10 +5,10 @@ from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalcRED_FEI(LabelFrame, HasQMixin): +class ExperimentalcRED_FEI(LabelFrame, ModuleFrameMixin): """Simple panel to assist cRED data collection (mainly rotation control) on a FEI microscope.""" diff --git a/src/instamatic/gui/cred_frame.py b/src/instamatic/gui/cred_frame.py index 65748ae9..b85b45aa 100644 --- a/src/instamatic/gui/cred_frame.py +++ b/src/instamatic/gui/cred_frame.py @@ -6,12 +6,12 @@ from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -ENABLE_FOOTFREE_OPTION = False +ENABLE_FOOTFREE_OPTION = True -class ExperimentalcRED(LabelFrame, HasQMixin): +class ExperimentalcRED(LabelFrame, ModuleFrameMixin): """GUI panel for doing cRED experiments on a Timepix camera.""" def __init__(self, parent): diff --git a/src/instamatic/gui/cred_tvips_frame.py b/src/instamatic/gui/cred_tvips_frame.py index b0aeac9a..dc7e20d0 100644 --- a/src/instamatic/gui/cred_tvips_frame.py +++ b/src/instamatic/gui/cred_tvips_frame.py @@ -9,12 +9,12 @@ from instamatic import config from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin barrier = threading.Barrier(2, timeout=60) -class ExperimentalTVIPS(LabelFrame, HasQMixin): +class ExperimentalTVIPS(LabelFrame, ModuleFrameMixin): """GUI panel for doing cRED / SerialRED experiments on a TVIPS camera.""" def __init__(self, parent): diff --git a/src/instamatic/gui/ctrl_frame.py b/src/instamatic/gui/ctrl_frame.py index 139fea0d..ee4a4360 100644 --- a/src/instamatic/gui/ctrl_frame.py +++ b/src/instamatic/gui/ctrl_frame.py @@ -2,18 +2,22 @@ import queue import threading -from threading import Event from tkinter import * from tkinter.ttk import * -from typing import Dict + +import numpy as np from instamatic import config +from instamatic.calibrate import CalibBeamShift +from instamatic.calibrate.filenames import CALIB_BEAMSHIFT +from instamatic.exceptions import TEMCommunicationError +from instamatic.gui.click_dispatcher import ClickEvent, MouseButton from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalCtrl(LabelFrame, HasQMixin): +class ExperimentalCtrl(LabelFrame, ModuleFrameMixin): """This panel holds some frequently used functions to control the electron microscope.""" @@ -74,6 +78,16 @@ def __init__(self, parent): ) b_wobble.grid(row=4, column=2, sticky='W', columnspan=2) + text = 'Move stage with LMB' + self.lmb_stage = Checkbutton(frame, text=text, variable=self.var_lmb_stage) + self.lmb_stage.grid(row=1, column=3, columnspan=3, sticky='W') + self.var_lmb_stage.trace_add('write', self.toggle_lmb_stage) + + text = 'Move beam with RMB' + self.rmb_beam = Checkbutton(frame, text=text, variable=self.var_rmb_beam) + self.rmb_beam.grid(row=2, column=3, columnspan=3, sticky='W') + self.var_rmb_beam.trace_add('write', self.toggle_rmb_beam) + e_stage_x = Spinbox(frame, textvariable=self.var_stage_x, **stage) e_stage_x.grid(row=6, column=1, sticky='EW') e_stage_y = Spinbox(frame, textvariable=self.var_stage_y, **stage) @@ -81,14 +95,14 @@ def __init__(self, parent): e_stage_z = Spinbox(frame, textvariable=self.var_stage_z, **stage) e_stage_z.grid(row=6, column=3, sticky='EW') + Label(frame, text='Rotation speed', width=20).grid(row=5, column=0, sticky='W') + e_goniotool_tx = Spinbox( + frame, width=10, textvariable=self.var_goniotool_tx, from_=1, to=12, increment=1 + ) + e_goniotool_tx.grid(row=5, column=1, sticky='EW') + b_goniotool_set = Button(frame, text='Set', command=self.set_goniotool_tx) + b_goniotool_set.grid(row=5, column=2, sticky='EW') if config.settings.use_goniotool: - Label(frame, text='Rot. Speed', width=20).grid(row=5, column=0, sticky='W') - e_goniotool_tx = Spinbox( - frame, width=10, textvariable=self.var_goniotool_tx, from_=1, to=12, increment=1 - ) - e_goniotool_tx.grid(row=5, column=1, sticky='EW') - b_goniotool_set = Button(frame, text='Set', command=self.set_goniotool_tx) - b_goniotool_set.grid(row=5, column=2, sticky='W') b_goniotool_default = Button( frame, text='Default', command=self.set_goniotool_tx_default ) @@ -203,7 +217,7 @@ def init_vars(self): self.var_stage_y = IntVar(value=0) self.var_stage_z = IntVar(value=0) - self.var_goniotool_tx = IntVar(value=1) + self.var_goniotool_tx = DoubleVar(value=1) self.var_brightness = IntVar(value=65535) self.var_difffocus = IntVar(value=65535) @@ -212,6 +226,8 @@ def init_vars(self): self.var_diff_defocus_on = BooleanVar(value=False) self.var_stage_wait = BooleanVar(value=True) + self.var_lmb_stage = BooleanVar(value=False) + self.var_rmb_beam = BooleanVar(value=False) def set_mode(self, event=None): self.ctrl.mode.set(self.var_mode.get()) @@ -257,7 +273,12 @@ def set_positive_angle(self): def set_goniotool_tx(self, event=None, value=None): if not value: value = self.var_goniotool_tx.get() - self.ctrl.stage.set_rotation_speed(value) + try: + self.ctrl.stage.set_rotation_speed(value) + except AttributeError: + print('This TEM does not implement `setRotationSpeed` method') + except TEMCommunicationError: + print('Could not connect to the stage rotation speed controller') def set_goniotool_tx_default(self, event=None): value = 12 @@ -301,6 +322,55 @@ def toggle_alpha_wobbler(self): if self.wobble_stop_event: self.wobble_stop_event.set() + def toggle_lmb_stage(self, _name, _index, _mode): + """If self.var_lmb_stage, move stage using Left Mouse Button.""" + + d = self.app.get_module('stream').click_dispatcher + if not self.var_lmb_stage.get(): + d.listeners.pop('lmb_stage', None) + return + + try: + stage_matrix = self.ctrl.get_stagematrix() + except KeyError: + print('No stage matrix for current mode and magnification found.') + print('Run `instamatic.calibrate_stagematrix` to use this feature.') + self.var_lmb_stage.set(False) + return + + def _callback(click: ClickEvent) -> None: + if click.button == MouseButton.LEFT: + cam_dim_x, cam_dim_y = self.ctrl.cam.get_camera_dimensions() + pixel_delta = np.array([click.y - cam_dim_y / 2, click.x - cam_dim_x / 2]) + stage_delta = np.dot(pixel_delta, stage_matrix) + self.ctrl.stage.move_in_projection(*stage_delta) + + d.add_listener('lmb_stage', _callback, active=True) + + def toggle_rmb_beam(self, _name, _index, _mode) -> None: + """If self.var_rmb_beam, move beam using Right Mouse Button.""" + + d = self.app.get_module('stream').click_dispatcher + if not self.var_rmb_beam.get(): + d.listeners.pop('rmb_beam', None) + return + + path = self.app.get_module('io').get_working_directory() / 'calib' + try: + calib_beamshift = CalibBeamShift.from_file(path / CALIB_BEAMSHIFT) + except OSError: + print(f'No {CALIB_BEAMSHIFT} file in directory {path} found.') + print('Run `instamatic.calibrate_beamshift` there to use this feature.') + self.var_rmb_beam.set(False) + return + + def _callback(click: ClickEvent) -> None: + if click.button == MouseButton.RIGHT: + bs = calib_beamshift.pixelcoord_to_beamshift((click.y, click.x)) + self.ctrl.beamshift.set(*[float(b) for b in bs]) + + d.add_listener('rmb_beam', _callback, active=True) + def stage_stop(self): self.q.put(('ctrl', {'task': 'stage.stop'})) diff --git a/src/instamatic/gui/debug_frame.py b/src/instamatic/gui/debug_frame.py index 304c8197..293bf665 100644 --- a/src/instamatic/gui/debug_frame.py +++ b/src/instamatic/gui/debug_frame.py @@ -8,7 +8,7 @@ from instamatic import config -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin scripts_drc = config.locations['scripts'] @@ -22,7 +22,7 @@ VMPORT = config.settings.VM_server_port -class DebugFrame(LabelFrame, HasQMixin): +class DebugFrame(LabelFrame, ModuleFrameMixin): """GUI panel with advanced / debugging functions.""" def __init__(self, parent): diff --git a/src/instamatic/gui/fast_adt_frame.py b/src/instamatic/gui/fast_adt_frame.py index 46638fbc..bfceb0e4 100644 --- a/src/instamatic/gui/fast_adt_frame.py +++ b/src/instamatic/gui/fast_adt_frame.py @@ -8,7 +8,7 @@ from instamatic import controller from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin pad0 = {'sticky': 'EW', 'padx': 0, 'pady': 1} pad10 = {'sticky': 'EW', 'padx': 10, 'pady': 1} @@ -82,7 +82,7 @@ def as_dict(self): return {n: v.get() for n, v in vars(self).items() if isinstance(v, Variable)} -class ExperimentalFastADT(LabelFrame, HasQMixin): +class ExperimentalFastADT(LabelFrame, ModuleFrameMixin): """GUI panel to perform selected FastADT-style (c)RED & PED experiments.""" def __init__(self, parent): diff --git a/src/instamatic/gui/gui.py b/src/instamatic/gui/gui.py index 3b7a4ccf..40d19489 100644 --- a/src/instamatic/gui/gui.py +++ b/src/instamatic/gui/gui.py @@ -43,6 +43,8 @@ def __init__(self, ctrl=None, stream=None, beam_ctrl=None, app=None, log=None): for module in self.app.modules.values(): if 'q' in get_type_hints(module.__class__): module.q = getattr(module, 'q', self.q) + if 'app' in get_type_hints(module.__class__): + module.app = getattr(module, 'app', self.app) self.exitEvent = threading.Event() atexit.register(self.exitEvent.set) @@ -136,11 +138,11 @@ def __init__(self, root, cam, modules: list = []): self.app = AppLoader() # the stream window is a special case, because it needs access - # to the cam module and the AppLoader itself + # to the cam module if cam: from .videostream_frame import module as stream_module - stream_module.set_kwargs(stream=cam, app=self.app) + stream_module.set_kwargs(stream=cam) modules.insert(0, stream_module) self.module_frame = Frame(root) diff --git a/src/instamatic/gui/machine_learning_frame.py b/src/instamatic/gui/machine_learning_frame.py index 59fe9d6d..c7777e85 100644 --- a/src/instamatic/gui/machine_learning_frame.py +++ b/src/instamatic/gui/machine_learning_frame.py @@ -10,7 +10,7 @@ from instamatic.formats import read_image -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin from .mpl_frame import ShowMatplotlibFig @@ -25,7 +25,7 @@ def treeview_sort_column(tv, col, reverse): tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse)) -class MachineLearningFrame(LabelFrame, HasQMixin): +class MachineLearningFrame(LabelFrame, ModuleFrameMixin): """GUI Panel to read in the results from the machine learning algorithm to identify good/poor crystals based on their diffraction pattern.""" diff --git a/src/instamatic/gui/red_frame.py b/src/instamatic/gui/red_frame.py index e3c0105f..ca6e64b9 100644 --- a/src/instamatic/gui/red_frame.py +++ b/src/instamatic/gui/red_frame.py @@ -5,10 +5,10 @@ from instamatic.utils.spinbox import Spinbox -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin -class ExperimentalRED(LabelFrame, HasQMixin): +class ExperimentalRED(LabelFrame, ModuleFrameMixin): """GUI panel to perform a simple RED experiment using discrete rotation steps.""" diff --git a/src/instamatic/gui/sed_frame.py b/src/instamatic/gui/sed_frame.py index 9f20fe6b..9fb9a41b 100644 --- a/src/instamatic/gui/sed_frame.py +++ b/src/instamatic/gui/sed_frame.py @@ -9,7 +9,7 @@ from instamatic.calibrate import CalibDirectBeam from instamatic.calibrate.filenames import CALIB_BEAMSHIFT, CALIB_DIRECTBEAM -from .base_module import BaseModule, HasQMixin +from .base_module import BaseModule, ModuleFrameMixin # import matplotlib # matplotlib.use('TkAgg') @@ -50,7 +50,7 @@ Press to start""" -class ExperimentalSED(LabelFrame, HasQMixin): +class ExperimentalSED(LabelFrame, ModuleFrameMixin): """GUI panel to start a SerialED experiment.""" def __init__(self, parent): diff --git a/src/instamatic/gui/videostream_frame.py b/src/instamatic/gui/videostream_frame.py index b4b4f45a..0ea39bb8 100644 --- a/src/instamatic/gui/videostream_frame.py +++ b/src/instamatic/gui/videostream_frame.py @@ -15,24 +15,23 @@ from instamatic._typing import AnyPath from instamatic.formats import read_tiff, write_tiff -from instamatic.gui.base_module import BaseModule, HasQMixin +from instamatic.gui.base_module import BaseModule, ModuleFrameMixin from instamatic.gui.click_dispatcher import ClickDispatcher from instamatic.gui.videostream_processor import VideoStreamProcessor from instamatic.processing import apply_flatfield_correction from instamatic.utils.spinbox import Spinbox -class VideoStreamFrame(LabelFrame, HasQMixin): +class VideoStreamFrame(LabelFrame, ModuleFrameMixin): """GUI panel to continuously display the last frame streamed from the camera.""" - def __init__(self, parent, stream, app=None): + def __init__(self, parent, stream): LabelFrame.__init__(self, parent, text='Stream') self.parent = parent self.stream = stream - self.app = app self.panel = None diff --git a/src/instamatic/microscope/interface/jeol_microscope.py b/src/instamatic/microscope/interface/jeol_microscope.py index 76c9e90f..a5156264 100644 --- a/src/instamatic/microscope/interface/jeol_microscope.py +++ b/src/instamatic/microscope/interface/jeol_microscope.py @@ -384,7 +384,7 @@ def getRotationSpeed(self) -> int: def setRotationSpeed(self, value: int): if self.goniotool_available: - self.goniotool.set_rate(value) + self.goniotool.set_rate(int(value)) else: raise TEMCommunicationError('Goniotool connection is not available.') diff --git a/src/instamatic/server/cam_server.py b/src/instamatic/server/cam_server.py index 2d3c926f..290fbf70 100644 --- a/src/instamatic/server/cam_server.py +++ b/src/instamatic/server/cam_server.py @@ -2,7 +2,6 @@ import datetime import logging -import pickle import queue import socket import threading @@ -12,10 +11,9 @@ from instamatic import config from instamatic.camera import get_camera +from instamatic.server.serializer import dumper, loader from instamatic.utils import high_precision_timers -from .serializer import dumper, loader - high_precision_timers.enable() if config.settings.cam_use_shared_memory: @@ -174,13 +172,13 @@ def main(): The host and port are defined in `config/settings.yaml`. -The data sent over the socket is a pickled dictionary with the following elements: +The data sent over the socket is a serialized dict with the following elements: - `attr_name`: Name of the function to call or attribute to return (str) - `args`: (Optional) List of arguments for the function (list) - `kwargs`: (Optiona) Dictionary of keyword arguments for the function (dict) -The response is returned as a pickle object. +The response is returned as a serialized object. """ parser = argparse.ArgumentParser(