diff --git a/README.md b/README.md index 0397ff9..8a68108 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,32 @@ # Server for the Tecnai-Scripting interface -The "Tecnai-Server" is an additional TEM interface used to control a FEI Tecnai-TEM via the instamatic software. We have tested the server software on a FEI Tecnai G2 microscope. The program provides access for the instamatic software to the com-scripting interface of the TEM. +This "Tecnai-Server" is a supplementary TEM server used to control an FEI machine: Tecnai TEM, Titan TEM, or an integrated camera, via the instamatic software. It can be used in lieu of the full instamatic on computers that do not support modern Python. It has been tested on an FEI Tecnai G2 and an FEI Titan 80-300 microscope. The program provides access for the instamatic software to the com-scripting interface of the TEM. ## Installation and Requirement The server software was developed in a Python 3.4 software environment (Windows XP). Following you have to install the Python 3.4 software package on your microscope-PC. The additional needed software-side packages and their last versions confirmed to work on Windows XP are: - `comtypes` – version 1.2.1 ([link](https://pypi.org/project/comtypes/1.2.1/#comtypes-1.2.1-py2.py3-none-any.whl)); -- `PyYAML` – version 5.1.2 ([link](https://pypi.org/project/PyYAML/5.1.2/#PyYAML-5.1.2-cp34-cp34m-win32.whl), - might require [updating pip](https://pypi.org/project/pip/19.1.1/#pip-19.1.1-py2.py3-none-any.whl)) -- `typing` – version 3.10.0.0 ([link](https://pypi.org/project/typing/3.10.0.0/#typing-3.10.0.0-py3-none-any.whl)) +- `pip` – version 19.1.1 ([link](https://pypi.org/project/pip/19.1.1/#pip-19.1.1-py2.py3-none-any.whl)); +- `PyYAML` – version 5.1.2 ([link](https://pypi.org/project/PyYAML/5.1.2/#PyYAML-5.1.2-cp34-cp34m-win32.whl)); +- `typing` – version 3.10.0.0 ([link](https://pypi.org/project/typing/3.10.0.0/#typing-3.10.0.0-py3-none-any.whl)); -The packages can be installed using pip, either automatically (if connected to the internet) or manually, by downloading, copying, and pointing to the wheel files linked above. Furthermore, the FEI com-scripting interface should be available on your TEM-PC. +If you furthermore intend to use `instamatic-tecnai-server` to interface an integrated camera: -## Usage +- `numpy` – version 1.15.4 ([link](https://pypi.org/project/numpy/1.15.4/#files)). + +The packages can be installed using pip, either automatically (if connected to the internet) or manually, by downloading, copying, and pointing to the wheel files linked above. Furthermore, the FEI com-scripting interface should be available on your TEM-PC. It is most convenient to create an installation in a custom virtual environment, e.g. using conda, on a removable or shared drive. -Microscope PC: This script -Camera PC (or other): [instamatic software](https://github.com/instamatic-dev/instamatic) +## Usage -The instamatic instance on the camera PC communicates with "Tecnai-Server"-software [over the network](https://instamatic.readthedocs.io/en/latest/network/). +On the Microscope PC, install this script. On the camera and/or support PC, install [instamatic software](https://github.com/instamatic-dev/instamatic). Follow example configuration 2 or 3 from the documentation. The instamatic GUI instance on the camera PC will communicate with "Tecnai-Server"-software [over the network](https://instamatic.readthedocs.io/en/latest/network/). -The "Tecnai-Server"-software is provided as a standard python-program. You can download and install the software in your chosen directory on the microscope-PC. After you have opened a MS-DOS command prompt you are navigating to the installation directory. The server will be started by the usual python invocation `py tem_server.py`. A corresponding `start.bat` -file provided in the installation directory can be adapted and used to run the command. +The "Tecnai-Server"-software is provided as a standard python-program. You can download and install the software in your chosen directory on the microscope-PC. After you have opened an MS-DOS command prompt you are navigating to the installation directory. The server will be started by the usual python invocation `py tem_server.py`. An integrated camera, if used, can be accessed using analogous `py cam_server.py`. A corresponding `start.bat` file provided in the installation directory can be adapted and used to run the command(s). The software will be configured by the .yaml-files in the `utils`-subdirectory. For instance the correct network address of your microscope-PC is set in the `settings.yaml` file. The magnification table of your TEM or the scripting interface `tecnai` are saved in the `microscope.yaml` file. -In our experimental setup the [instamatic software](https://github.com/instamatic-dev/instamatic) is installed on a separate PC (camera PC). In this case the configuration files of Instamatic must be adapted like in the server software. Especially the `interface="tecnai"`, the microscope, the network address and the flag `use_tem_server"` should be verified. Afterwards, the instamatic software should be starting without errors on your PC. You can try it out in an IPython shell if the TEMController-object has access to TEM. +In our experimental setup the [instamatic software](https://github.com/instamatic-dev/instamatic) is installed on a separate PC (camera PC). In this case the configuration files of Instamatic must be adapted like in the server software. Especially the `interface=tecnai`, the microscope, the network address and the flag `use_tem_server` should be verified. Afterward, the instamatic software should be starting without errors on your PC. You can use an IPython shell (`instamatic.controller`) to test if the TEMController-object has access to TEM. ## Credits -Thanks to Steffen Schmidt ([CUP, LMU München](https://www.cup.uni-muenchen.de/)) for providing this script. +Thanks to Steffen Schmidt ([CUP, LMU München](https://www.cup.uni-muenchen.de/)) for providing the original script. diff --git a/cam_server.py b/cam_server.py new file mode 100644 index 0000000..f4d9008 --- /dev/null +++ b/cam_server.py @@ -0,0 +1,90 @@ +# IF YOUR VENV DOES NOT WORK CORRECTLY, A BOILERPLATE LIKE THIS MAY BE REQUIRED +# import sys +# sys.path.insert(0, r'Q:\DanielT\instamatic-tecnai-server\venv\Lib\site-packages') +# BOILERPLATE END + +import argparse +import logging +import queue +import threading +import time + +from instamaticServer.TEMController.camera import get_camera +from instamaticServer.utils.config import config +from tem_server import DeviceServer, listen, setup_logging + + +BUFSIZE = 1024 +TIMEOUT = 0.5 + + +class CamServer(DeviceServer): + """FEI Tecnai/Titan Acquisition camera communication server.""" + + device_abbr = 'cam' + device_kind = 'camera' + device_getter = staticmethod(get_camera) + requests = queue.Queue(maxsize=1) + responses = queue.Queue(maxsize=1) + stop_event = threading.Event() + host = config.default_settings['cam_server_host'] + port = config.default_settings['cam_server_port'] + + def __init__(self, name=None) -> None: + super(CamServer, self).__init__(name=name) + self.logger.setLevel(logging.INFO) + + +def main() -> None: + """ + Connects to the TEM and starts a server for camera communication. + Opens a socket on port {CamServer.host}:{CamServer.port}. + + This program initializes a connection to the TEM as defined in the config. + The purpose of this program is to isolate the camera connection in + a separate process for improved stability of the interface in case + instamatic crashes or is started and stopped frequently. + For running the GUI, the cam server is required. + Another reason is that it allows for remote connections from different PCs. + The connection goes over a TCP socket. + + The host and port are defined in `config/settings.yaml`. + + The data sent over the socket is a serialized dictionary with the following: + + - `attr_name`: Name of the function to call (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 serialized object. + """ + + parser = argparse.ArgumentParser(description=main.__doc__) + parser.add_argument('-c', '--camera', action='store', + help='Override camera to use.') + parser.set_defaults(camera=None) + options = parser.parse_args() + + logger = setup_logging(device_abbr='cam') + logger.info('Titan camera server starting') + + cam_server = CamServer(name=options.camera) + cam_server.start() + + cam_listener = threading.Thread(target=listen, args=(CamServer,), name='cam_listener') + cam_listener.start() + + try: + while not CamServer.stop_event.is_set(): time.sleep(TIMEOUT) + except KeyboardInterrupt: + logger.info("Received KeyboardInterrupt, shutting down...") + finally: + CamServer.stop_event.set() + cam_server.join() + cam_listener.join() + logger.info('Titan camera server terminating') + logging.shutdown() + + +if __name__ == '__main__': + main() diff --git a/instamaticServer/TEMController/camera.py b/instamaticServer/TEMController/camera.py new file mode 100644 index 0000000..4ab3d62 --- /dev/null +++ b/instamaticServer/TEMController/camera.py @@ -0,0 +1,37 @@ +from instamaticServer.utils.config import config + + +_cam_interfaces = ('simulate', 'tecnai') + +__all__ = ['get_camera', 'get_camera_class'] + + +def get_camera_class(interface: str): + """Grab the cam class with the given 'interface'.""" + + if interface == 'simulate': + from .simu_camera import SimuCamera as CamCls + elif interface == 'tecnai': + from .tecnai_camera import TecnaiCamera as CamCls + else: + raise ValueError("No such microscope interface: %s" % interface) + + return CamCls + + +def get_camera(name: str = None): + """Return an instance of camera interface `tecnai` or `simulate`""" + + if name in _cam_interfaces: + interface = name + else: + interface = config.camera.interface + name = config.default_settings['microscope'] + + cls = get_camera_class(interface=interface) + tem = cls(name=name) + + return tem + + + diff --git a/instamaticServer/TEMController/microscope.py b/instamaticServer/TEMController/microscope.py index 7b0f0af..6e6cb3b 100644 --- a/instamaticServer/TEMController/microscope.py +++ b/instamaticServer/TEMController/microscope.py @@ -1,6 +1,6 @@ -from utils.config import config +from instamaticServer.utils.config import config + -_conf = config() _tem_interfaces = ('simulate', 'tecnai') __all__ = ['get_microscope', 'get_microscope_class'] @@ -10,30 +10,23 @@ def get_microscope_class(interface: str): """Grab the tem class with the given 'interface'.""" if interface == 'simulate': - from .simu_microscope import SimuMicroscope as cls + from .simu_microscope import SimuMicroscope as TemCls elif interface == 'tecnai': - from .tecnai_microscope import TecnaiMicroscope as cls + from .tecnai_microscope import TecnaiMicroscope as TemCls else: - raise ValueError("No such microscope interface: %s" % (interface)) + raise ValueError("No such microscope interface: %s" % interface) - return cls + return TemCls def get_microscope(name: str = None): - """Generic class to load microscope interface class. - - name: str - Specify which microscope to use, must be one of `tecnai`, `simulate` - use_server: bool - Connect to microscope server running on the host/port defined in the config file + """Return an instance of microscope interface `tecnai` or `simulate`""" - returns: TEM interface class - """ if name in _tem_interfaces: interface = name else: - interface = _conf.micr_interface - name = _conf.default_settings['microscope'] + interface = config.micr_interface + name = config.default_settings['microscope'] cls = get_microscope_class(interface=interface) tem = cls(name=name) diff --git a/instamaticServer/TEMController/remote_movie.py b/instamaticServer/TEMController/remote_movie.py new file mode 100644 index 0000000..c0d25a9 --- /dev/null +++ b/instamaticServer/TEMController/remote_movie.py @@ -0,0 +1,21 @@ +from typing import Callable + + +class RemoteMovie: + """A lazy single-threaded image iterator, can't pass acq to other thread.""" + def __init__(self, get_image: Callable, **kwargs) -> None: + self._get_image = get_image + self._kwargs = dict(kwargs) + self._n_frames = self._kwargs.pop('n_frames') + self._i = 0 + self._started = False + + def __iter__(self): + return self + + def __next__(self): + if self._i >= self._n_frames: + raise StopIteration + self._started = True + self._i += 1 + return self._get_image(**self._kwargs) diff --git a/instamaticServer/TEMController/simu_camera.py b/instamaticServer/TEMController/simu_camera.py new file mode 100644 index 0000000..95d6cb1 --- /dev/null +++ b/instamaticServer/TEMController/simu_camera.py @@ -0,0 +1,104 @@ +import atexit +import logging +import time +from typing import Tuple, Any, Optional, List + +from instamaticServer.utils.config import config +from instamaticServer.utils.singleton import Singleton + +try: + import numpy as np +except ImportError: + np = False + + +logger = logging.getLogger('cam') + + +class SimuCamera(metaclass=Singleton): + """Simple class that simulates the camera interface and mocks the method calls.""" + + streamable = True + + # Set by `load_defaults` + camera_rotation_vs_stage_xy = None # type: float + default_binsize = None # type: int + default_exposure = None # type: float + dimensions = None # type: Tuple[int, int] + interface = None # type: str + possible_binsizes = None # type: List[int] + stretch_amplitude = None # type: float + stretch_azimuth = None # type: float + + def __init__(self, name='simulate'): + """Initialize camera module.""" + self.name = name + self.load_defaults() + self.establish_connection() + logger.info('Camera simulate initialized') + atexit.register(self.release_connection) + + def __enter__(self): + self.establish_connection() + return self + + def __exit__(self, kind, value, traceback) -> None: + self.release_connection() + + def get_binning(self) -> int: + return self.default_binsize + + def get_camera_dimensions(self) -> Tuple[int, int]: + return self.dimensions + + def get_name(self) -> str: + return self.name + + def load_defaults(self) -> None: + for key, val in config.camera.__dict__.items(): + setattr(self, key, val) + + def get_image(self, exposure: Optional[float] = None, binsize: int = 1): + """Image acquisition interface.""" + exposure = exposure or self.default_exposure + binsize = binsize or self.default_binsize + t0 = time.perf_counter() + dx, dy = self.dimensions + dx, dy = dx // binsize, dy // binsize + if np: + img = 256 * np.random.random_sample((dx, dy)) + else: + import random + img = [[random.randint(0, 255) for _ in range(dx)] for _ in range(dy)] + while time.perf_counter() - t0 < exposure: + time.sleep(0.001) + return img + + def get_image_dimensions(self) -> Tuple[int, int]: + """Get the binned dimensions reported by the camera.""" + dx, dy = self.dimensions + return dx // self.default_binsize, dy // self.default_binsize + + def get_movie( + self, + n_frames: int, + exposure: Optional[float] = None, + binsize: Optional[int] = None, + ): + """Unfortunately not designed to work with generators, as a server...""" + return [self.get_image(exposure, binsize) for _ in range(n_frames)] + + def establish_connection(self) -> Tuple[Any, Any]: + """Establish connection to the camera.""" + pass + + def release_connection(self) -> None: + """Release the connection to the camera.""" + pass + + +if __name__ == '__main__': + cam = SimuCamera() + from IPython import embed + + embed() diff --git a/instamaticServer/TEMController/simu_microscope.py b/instamaticServer/TEMController/simu_microscope.py index 62bce77..ac2f458 100644 --- a/instamaticServer/TEMController/simu_microscope.py +++ b/instamaticServer/TEMController/simu_microscope.py @@ -1,10 +1,14 @@ +import logging import random import time from typing import Optional, Tuple, Union -from .typing import StagePositionTuple, float_deg, int_nm -from utils.exceptions import TEMValueError -from utils.config import config +from instamaticServer.utils.config import Config +from instamaticServer.utils.exceptions import TEMValueError +from instamaticServer.utils.types import StagePositionTuple, float_deg, int_nm + + +logger = logging.getLogger('tem') NTRLMAPPING = { @@ -58,12 +62,18 @@ def __init__(self, name: str = None): self.BeamTilt_x = random.randint(MIN, MAX) self.BeamTilt_y = random.randint(MIN, MAX) + self.IlluminationTiltX = random.randint(MIN, MAX) + self.IlluminationTiltY = random.randint(MIN, MAX) + self.ImageShift1_x = random.randint(MIN, MAX) self.ImageShift1_y = random.randint(MIN, MAX) self.ImageShift2_x = random.randint(MIN, MAX) self.ImageShift2_y = random.randint(MIN, MAX) + self.ImageBeamShift_x = random.randint(MIN, MAX) + self.ImageBeamShift_y = random.randint(MIN, MAX) + # self.FunctionMode_value = random.randint(0, 2) self.FunctionMode_value = 0 @@ -82,11 +92,11 @@ def __init__(self, name: str = None): self.MAX = MAX self.MIN = MIN - self._conf = config(self.name) + self.config = Config(self.name) self._mic_ranges = None - if self._conf.micr_interface == 'simulate': - self._mic_ranges = self._conf.micr_ranges + if self.config.micr_interface == 'simulate': + self._mic_ranges = self.config.micr_ranges self._HT = 200000 # V @@ -140,13 +150,15 @@ def __init__(self, name: str = None): 'current': current, 'is_moving': False, 'speed': speed, - 'speed_setting': 12, + 'speed_setting': 1, 'direction': +1, 'start': 0.0, 'end': 0.0, 't0': 0.0, } + logger.info('Microscope simulate initialized') + ##self.goniotool_available = config.settings.use_goniotool self.goniotool_available = False ##auf Klasse GonioToolClient Obacht geben @@ -155,8 +167,8 @@ def __init__(self, name: str = None): try: self.goniotool = GonioToolClient() except Exception as e: - print('GonioToolClient:', e) - print('Could not connect to GonioToolServer, goniotool unavailable!') + logger.warning('GonioToolClient:', e) + logger.warning('Could not connect to GonioToolServer, goniotool unavailable!') self.goniotool_available = False #config.settings.use_goniotool = False @@ -255,12 +267,25 @@ def getCurrentDensity(self) -> float: rand_val = (random.random() - 0.5) * 10000 return self.CurrentDensity_value + rand_val + def getScreenCurrent(self): + rand_val = (random.random() - 0.5) * 10000 + return self.CurrentDensity_value + rand_val + + def isfocusscreenin(self) -> bool: + return False + def getBrightness(self) -> int: return self.Brightness_value def setBrightness(self, value: int): self.Brightness_value = value + def getBrightnessValue(self) -> int: + return self.Brightness_value + + def setBrightnessValue(self, value: int): + self.Brightness_value = value + def getMagnification(self) -> int: if self.getFunctionMode() == 'diff': return self.Magnification_value_diff @@ -356,6 +381,14 @@ def setGunShift(self, x: int, y: int): self.GunShift_x = x self.GunShift_y = y + def getBeamAlignShift(self) -> (float, float): + """get the Gun-Shift values.""" + return self.getGunShift() + + def setBeamAlignShift(self, x: float, y: float) -> None: + """set Gun-Shift values.""" + self.setGunShift(x, y) + def getGunTilt(self) -> Tuple[int, int]: return self.GunTilt_x, self.GunTilt_y @@ -377,6 +410,15 @@ def setBeamTilt(self, x: int, y: int): self.BeamTilt_x = x self.BeamTilt_y = y + def getDarkFieldTilt(self) -> (float, float): + """get the dark field tile value.""" + return self.IlluminationTiltX, self.IlluminationTiltX + + def setDarkFieldTilt(self, x: float, y: float) -> None: + """set the dark field tilt value.""" + self.IlluminationTiltX = x + self.IlluminationTiltY = y + def getImageShift1(self) -> Tuple[int, int]: return self.ImageShift1_x, self.ImageShift1_y @@ -391,13 +433,31 @@ def setImageShift2(self, x: int, y: int): self.ImageShift2_x = x self.ImageShift2_y = y + def getImageBeamShift(self): + return self.ImageBeamShift_x, self.ImageBeamShift_y + + def setImageBeamShift(self, x: float, y: float) -> None: + self.ImageBeamShift_x = x + self.ImageBeamShift_y = y + + def getHolderType(self) -> int: + return 0 + def getStagePosition(self) -> StagePositionTuple: return (self.StagePosition_x, self.StagePosition_y, self.StagePosition_z, self.StagePosition_a, self.StagePosition_b) + def isAThreadAlive(self) -> bool: + """Return goniotool status, always False.""" + return False + + def getStageSpeed(self) -> float: + """Return Stagespeed, can not be read on Tecnai = constant(0.5).""" + logger.info('StageSpeed can not be read on Tecnai') + return 0.5 + def isStageMoving(self) -> bool: self.getStagePosition() # trigger update of self._is_moving - # print(res, self._is_moving) return self._is_moving def waitForStage(self, delay: float = 0.1): @@ -468,7 +528,7 @@ def getRotationSpeed(self) -> int: def setRotationSpeed(self, value: int): self._stage_dict['a']['speed_setting'] = value - self._stage_dict['a']['speed'] = 10.0 * (value / 12) + self._stage_dict['a']['speed'] = value * 20 def getFunctionMode(self) -> str: """Mag1, mag2, lowmag, samag, diff.""" @@ -495,6 +555,30 @@ def setDiffFocus(self, value: int, confirm_mode: bool = True): raise TEMValueError("Must be in 'diff' mode to set DiffFocus") self.DiffractionFocus_value = value + def getDiffFocusValue(self, confirm_mode: bool = True) -> float: + """get the diffraction focus value.""" + if not self.getFunctionMode() == 'diff': + raise TEMValueError("Must be in 'diff' mode to get DiffFocus") + return self.DiffractionFocus_value + + def setDiffFocusValue(self, value: float, confirm_mode: bool = True) -> None: + """set the diffraction focus value.""" + if not self.getFunctionMode() == 'diff': + raise TEMValueError("Must be in 'diff' mode to set DiffFocus") + self.DiffractionFocus_value = value + + def getFocus(self) -> float: + """get the Defocus value.""" + if not self.getFunctionMode() in ['lowmag', 'mag1', 'samag', 'mag2']: + raise TEMValueError("Must be in 'mag' mode to get Focus") + return self.DiffractionFocus_value + + def setFocus(self, value: float) -> None: + """set the Defocus value.""" + if not self.getFunctionMode() in ['lowmag', 'mag1', 'samag', 'mag2']: + raise TEMValueError("Must be in 'mag' mode to set Focus") + self.DiffractionFocus_value = value + def setIntermediateLens1(self, value: int): """IL1.""" self.IntermediateLens1_value = value @@ -511,7 +595,7 @@ def setDiffShift(self, x: int, y: int): self.DiffractionShift_y = y def release_connection(self): - print('Connection to microscope released') + logger.info('Connection to microscope released') def isBeamBlanked(self) -> bool: return self.beamblank @@ -535,7 +619,7 @@ def setIntermediateLensStigmator(self, x: int, y: int): self.intermediatelensstigmator_y = y def getObjectiveLensStigmator(self) -> Tuple[int, int]: - return self.objectivelensstigmator_x, self.objectivelensstigmatir_y + return self.objectivelensstigmator_x, self.objectivelensstigmator_y def setObjectiveLensStigmator(self, x: int, y: int): self.objectivelensstigmator_x = x diff --git a/instamaticServer/TEMController/tecnai_camera.py b/instamaticServer/TEMController/tecnai_camera.py new file mode 100644 index 0000000..0aeb292 --- /dev/null +++ b/instamaticServer/TEMController/tecnai_camera.py @@ -0,0 +1,112 @@ +import atexit +from functools import partial + +import comtypes.client +import logging +from typing import Any, Generator, List, Optional, Tuple + +from instamaticServer.TEMController.remote_movie import RemoteMovie +from instamaticServer.utils.config import config +from instamaticServer.utils.singleton import Singleton + +try: + import numpy as np +except ImportError: + np = False + + +logger = logging.getLogger('cam') + + +class TecnaiCamera(metaclass=Singleton): + """Interfaces any camera on an FEI Tecnai/Titan microscope.""" + + streamable = True + + # Set by `load_defaults` + camera_rotation_vs_stage_xy = None # type: float + default_binsize = None # type: int + default_exposure = None # type: float + dimensions = None # type: Tuple[int, int] + interface = None # type: str + possible_binsizes = None # type: List[int] + stretch_amplitude = None # type: float + stretch_azimuth = None # type: float + + def __init__(self, name='tecnai'): + """Initialize camera module.""" + comtypes.CoInitialize() + logger.info('FEI Scripting initializing...') + self._tem = comtypes.client.CreateObject('TEMScripting.Instrument', comtypes.CLSCTX_ALL) + self.name = name + self.load_defaults() + self._acq, self._cam = self.establish_connection() + logger.info('Camera Tecnai connection established') + atexit.register(self.release_connection) + + def __enter__(self): + self.establish_connection() + return self + + def __exit__(self, kind, value, traceback) -> None: + self.release_connection() + + def get_binning(self) -> int: + return self.default_binsize + + def get_camera_dimensions(self) -> Tuple[int, int]: + return self.dimensions + + def get_name(self) -> str: + return self.name + + def load_defaults(self) -> None: + for key, val in config.camera.__dict__.items(): + setattr(self, key, val) + + def get_image(self, exposure: Optional[float] = None, binsize: Optional[int] = None): + """Image acquisition interface.""" + self._cam.AcqParams.ExposureTime = exposure or self.default_exposure + self._cam.AcqParams.Binning = binsize or self.default_binsize + img = self._acq.AcquireImages()[0] + sa = img.AsSafeArray + if np: + return np.stack(sa).T + return [[sa[r, c] for c in range(img.Height)] for r in range(img.Width)] + # try [[sa.GetElement([r, c]) or similar if direct indexing does not work... + + def get_image_dimensions(self) -> Tuple[int, int]: + """Get the binned dimensions of the camera.""" + dx, dy = self.dimensions + return dx // self.default_binsize, dy // self.default_binsize + + def get_movie( + self, + n_frames: int, + exposure: Optional[float] = None, + binsize: Optional[int] = None, + ) -> Generator: + """Yield n_frames images each collected using subsequent get_image""" + get_image = partial(self.get_image, self) + yield from RemoteMovie(get_image, n_frames=n_frames, exposure=exposure, binsize=binsize) + + def establish_connection(self) -> Tuple[Any, Any]: + """Establish connection to the camera.""" + acq = self._tem.Acquisition + acq.RemoveAllAcqDevices() + cam = acq.Cameras[0] + cam.AcqParams.ImageCorrection = 1 # bias and gain corr (0=off, 1=on) + cam.AcqParams.ImageSize = 0 # sub area centered (0=full, 1=half, 2=quarter) + acq.AddAcqDeviceByName(cam.Info.Name) + return acq, cam + + def release_connection(self) -> None: + """Release the connection to the camera.""" + pass + + +if __name__ == '__main__': + cam = TecnaiCamera() + from IPython import embed + + embed() diff --git a/instamaticServer/TEMController/tecnai_microscope.py b/instamaticServer/TEMController/tecnai_microscope.py index 457b01e..d0f2855 100644 --- a/instamaticServer/TEMController/tecnai_microscope.py +++ b/instamaticServer/TEMController/tecnai_microscope.py @@ -1,54 +1,30 @@ import atexit +import comtypes.client import logging import time -import comtypes.client from math import pi from typing import Optional -from .typing import StagePositionTuple, float_deg, int_nm -from utils.exceptions import FEIValueError, TEMCommunicationError -from utils.config import config -from TEMController.tecnai_stage_thread import TecnaiStageThread - - -_FUNCTION_MODES = {1: 'lowmag', 2: 'mag1', 3: 'samag', 4: 'mag2', 5: 'LAD', 6: 'diff'} - -#diff=D, LAD=LAD, lowmag=LM, mag1=Mi, samag=SA, mag2=Mh in Functionmodes - - +from instamaticServer.TEMController.tecnai_stage_thread import TecnaiStageThread +from instamaticServer.utils.config import Config +from instamaticServer.utils.exceptions import FEIValueError, TEMCommunicationError +from instamaticServer.utils.singleton import Singleton +from instamaticServer.utils.types import StagePositionTuple, float_deg, int_nm -#dict([('D', [0.0265, 0.035, 0.044, 0.062, 0.071, 0.089, 0.135, 0.175, 0.265, 0.43, 0.6, 0.86, 1.65, 2.65, 3.5, 4.1]), -# ('LAD', [4.5, 7.1, 9, 12.5, 18, 27, 36, 53, 71, 81, 130, 180, 245, 360, 530, 720, 790, 810, 960, 1100, 1300]), -# ('LM', [19, 25, 35, 50, 65, 82, 105, 145, 200, 300, 390, 500, 730, 980, 1350, 1850]), -# ('Mi', [2250, 3500, 4400]), -# ('SA', [6200, 8700, 13500, 17000, 26000, 34000, 38000, 63000, 86000, 125000, 175000, 250000, 350000, 400000]), -# ('Mh', [440000, 520000, 610000, 700000, 780000, 910000])]) +logger = logging.getLogger('tem') -class Singleton(type): - """Singleton Metaclass from Stack Overflow, stackoverflow.com/q/6760685""" - _instances = {} - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] +_FUNCTION_MODES = {1: 'lowmag', 2: 'mag1', 3: 'samag', 4: 'mag2', 5: 'LAD', 6: 'diff'} class TecnaiMicroscope(metaclass=Singleton): - """Python bindings to the Tecnai-G2 microscope using the COM scripting interface.""" + """Python bindings to the FEI microscope using the COM scripting interface.""" def __init__(self, name: str=None) -> None: - - try: - comtypes.CoInitialize() - except: - raise - - print('FEI Scripting initializing...') - ## TEM interfaces the GUN, stage etc + comtypes.CoInitialize() + logger.info('FEI Scripting initializing...') + ## TEM interfaces the GUN, stage etc. + enum constants self._tem = comtypes.client.CreateObject('TEMScripting.Instrument', comtypes.CLSCTX_ALL) - - ## TEM enum constants self._tem_constant = comtypes.client.Constants(self._tem) self._t = 0 @@ -59,21 +35,20 @@ def __init__(self, name: str=None) -> None: time.sleep(1) self._t += 1 if self._t > 3: - print('Waiting for microscope, t = %ss' % (self._t)) + logger.info('Waiting for microscope, t = %ss' % self._t) if self._t > 30: raise TEMCommunicationError('Cannot establish microscope connection (timeout).') - self._logger = logging.getLogger(__name__) - self._logger.info('Microscope connection established') + logger.info('Microscope Tecnai connection established') #close the network connection atexit.register(TecnaiMicroscope.release_connection) self.name = name - self._conf = config(self.name) + self.config = Config(self.name) self._mic_ranges = None - if self._conf.micr_interface == 'tecnai': - self._mic_ranges = self._conf.micr_ranges + if self.config.micr_interface == 'tecnai': + self._mic_ranges = self.config.micr_ranges self._rotation_speed = 1.0 self._tecnaiStage = TecnaiStageThread() #Thread für a-Movement @@ -96,7 +71,7 @@ def getStagePosition(self) -> StagePositionTuple: def getStageSpeed(self) -> float: """Return Stagespeed, can not be read on Tecnai = constant(0.5).""" - print('StageSpeed can not be read on Tecnai') + logger.info('StageSpeed can not be read on Tecnai') return 0.5 def is_goniotool_available(self) -> bool: @@ -195,7 +170,6 @@ def setStagePosition( #self._tem.Stage.GoToWithSpeed(pos, axis, 0.01) => 1grad in 4-5sec. - def setStageA(self, value: float=None, wait: bool=True) -> None: """Set the Stageposition alpha (A) in degrees.""" pos = self._tem.Stage.Position @@ -248,11 +222,11 @@ def waitForStage(self, delay: float=0.1) -> None: def setStageSpeed(self, value: float) -> None: """Set Stage speed, not available on Tecnai.""" - print('StageSpeed can not be set on Tecnai') + logger.info('StageSpeed can not be set on Tecnai') def stopStage(self) -> None: """Stop Stage, not available on Tecnai.""" - print('stopStage: not available on Tecnai.') + logger.info('stopStage: not available on Tecnai.') def setRotationSpeed(self, value: float) -> None: """Set rotationspeed of the alpha rotation.""" @@ -335,7 +309,6 @@ def getBeamAlignShift(self) -> (float, float): def setBeamAlignShift(self, x: float, y: float) -> None: """set Gun-Shift values.""" self.setGunShift(x, y) - ###Illumination def getSpotSize(self) -> int: @@ -411,7 +384,7 @@ def setCondensorLensStigmator(self, x: float, y: float) -> None: ###Projection def getCurrentDensity(self) -> float: """Get the current density, not available on Tecnai.""" - print('getCurrentDensity: not available on the Tecnai.') + logger.info('getCurrentDensity: not available on the Tecnai.') return 0 def getScreenCurrent(self) -> float: @@ -678,11 +651,11 @@ def setIntermediateLensStigmator(self, x: float, y: float) -> None: def release_connection() -> None: """release the COM-connection.""" comtypes.CoUninitialize() - print('Connection to microscope released') + logger.info('Connection to microscope released') def getApertureSize(self, aperture: str) -> None: """not available on Tecnai.""" - print('getApertureSize, not available on Tecnai.') + logger.info('getApertureSize, not available on Tecnai.') if __name__ == '__main__': diff --git a/instamaticServer/TEMController/tecnai_stage_thread.py b/instamaticServer/TEMController/tecnai_stage_thread.py index 518b733..51672dd 100644 --- a/instamaticServer/TEMController/tecnai_stage_thread.py +++ b/instamaticServer/TEMController/tecnai_stage_thread.py @@ -2,13 +2,23 @@ import comtypes from typing import Union + +from instamaticServer.utils.types import StagePositionTuple + + class TecnaiStageThread(threading.Thread): """ Stage communication with the Tecnai microscope over a separate thread. """ - def __init__(self, tem=None, pos:(float, float, float, float, float)=None, axis:int=None, speed:Union[int, float]=0): - super().__init__() + def __init__( + self, + tem = None, + pos: StagePositionTuple = None, + axis: int = None, + speed: Union[int, float] = 0, + ): + super().__init__(name='TecnaiStageThread') #TEM-Scriptinginterface self._tem = tem @@ -59,7 +69,7 @@ def run(self) -> None: self._tem.Stage.GoToWithSpeed(stagePos, self._axis, self._speed) class ContextManagedComtypes(): - '''The Context Manager Protocoll is used to initialize the COM connection again''' + """The Context Manager Protocol is used to initialize the COM connection again""" def __enter__(self): comtypes.CoInitialize() return self diff --git a/instamaticServer/serializer.py b/instamaticServer/serializer.py index ede6283..fcc7f4c 100644 --- a/instamaticServer/serializer.py +++ b/instamaticServer/serializer.py @@ -1,10 +1,9 @@ import json import pickle -from utils.config import config +from instamaticServer.utils.config import config -_conf = config() -PROTOCOL = _conf.default_settings['tem_communication_protocol'] +PROTOCOL = config.default_settings['tem_communication_protocol'] # %timeit ctrl.stage.get() # - pickle: 287 µs ± 10.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) @@ -50,5 +49,5 @@ def msgpack_dumper(data): loader = msgpack_loader dumper = msgpack_dumper else: - raise ValueError("No such protocol: %s" % (PROTOCOL)) + raise ValueError("No such protocol: %s" % PROTOCOL) diff --git a/instamaticServer/start.bat b/instamaticServer/start.bat deleted file mode 100755 index ba3ef5a..0000000 --- a/instamaticServer/start.bat +++ /dev/null @@ -1,3 +0,0 @@ -cd c:\instamatic\TEMserver &:: point to your installation directory -:: call .\venv\Scripts\activate.bat &:: uncomment and point to venv if using one -py tem_server.py &:: use `py`, `py3`, or `python` as needed diff --git a/instamaticServer/tem_server.py b/instamaticServer/tem_server.py deleted file mode 100644 index 13ee306..0000000 --- a/instamaticServer/tem_server.py +++ /dev/null @@ -1,172 +0,0 @@ -import datetime -import queue -import socket -import threading -import signal -import traceback -import logging - -from TEMController.microscope import get_microscope -from serializer import dumper, loader -from utils.config import config - -condition = threading.Condition() -stop_program_event = threading.Event() - -box = [] - -_conf = config() -HOST = _conf.default_settings['tem_server_host'] -PORT = _conf.default_settings['tem_server_port'] -BUFSIZE = 1024 - - -class TemServer(threading.Thread): - """TEM communcation server. - - Takes a logger object `log`, command queue `q`, and name of the - microscope `name` that is used to initialize the connection to the - microscope. Start the server using `TemServer.run` which will wait - for items to appear on `q` and execute them on the specified - microscope instance. - """ - - def __init__(self, log=None, q=None, name=None): - super().__init__() - - self._log = log - self._q = q - - # self.name is a reserved parameter for threads - self._name = name - - self.verbose = False - - def run(self): - """Start the server thread.""" - self.tem = get_microscope(name=self._name) - self._name = self.tem.name - print("Initialized connection to microscope: %s" % (self._name)) - - while True: - now = datetime.datetime.now().strftime('%H:%M:%S.%f') - - cmd = self._q.get() - - with condition: - func_name = cmd['func_name'] - args = cmd.get('args', ()) - kwargs = cmd.get('kwargs', {}) - - try: - ret = self.evaluate(func_name, args, kwargs) - status = 200 - except Exception as e: - traceback.print_exc() - if self._log: - self._log.exception(e) - ret = (e.__class__.__name__, e.args) - status = 500 - - box.append((status, ret)) - condition.notify() - print("%s | %s %s: %s" % (now, status, func_name, ret)) - - def evaluate(self, func_name: str, args: list, kwargs: dict): - """Evaluate the function `func_name` on `self.tem` and call it with - `args` and `kwargs`.""" - print(func_name, args, kwargs) - f = getattr(self.tem, func_name) - ret = f(*args, **kwargs) - return ret - -def handle(conn, q): - """Handle incoming connection, put command on the Queue `q`, which is then - handled by TEMServer.""" - with conn: - while True: - if stop_program_event.is_set(): - break - - data = conn.recv(BUFSIZE) - if not data: - break - - data = loader(data) - - if data == 'exit': - break - - if data == 'kill': - break - - with condition: - q.put(data) - condition.wait() - response = box.pop() - conn.send(dumper(response)) - - -def handle_kb_interrupt(sig, frame): - stop_program_event.set() - - -def main(): - - import argparse - description = """ -Connects to the TEM and starts a server for microscope communication. Opens a socket on port {HOST}:{PORT}. - -This program initializes a connection to the TEM as defined in the config. The purpose of this program is to isolate the microscope connection in a separate process for improved stability of the interface in case instamatic crashes or is started and stopped frequently. For running the GUI, the temserver is required. Another reason is that it allows for remote connections from different PCs. The connection goes over a TCP socket. - -The host and port are defined in `config/settings.yaml`. - -The data sent over the socket is a serialized dictionary with the following elements: - -- `func_name`: Name of the function to call (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 serialized object. -""" - - parser = argparse.ArgumentParser( - description=description, - formatter_class=argparse.RawDescriptionHelpFormatter) - - parser.add_argument('-t', '--microscope', action='store', dest='microscope', - help="""Override microscope to use.""") - - parser.set_defaults(microscope=None) - options = parser.parse_args() - microscope = options.microscope - - logging.basicConfig(filename='tem_server.log', level=logging.INFO) - - q = queue.Queue(maxsize=100) - - tem_reader = TemServer(name=microscope, log=None, q=q) - tem_reader.start() - - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind((HOST, PORT)) - s.listen(0) - - logging.info("Server listening on %s:%s" % (HOST, PORT)) - print ("Server listening on %s:%s" % (HOST, PORT)) - - signal.signal(signal.SIGINT, handle_kb_interrupt) - - with s: - while True: - conn, addr = s.accept() - #logging.info('Connected by %s' % (addr)) -# print('Connected by', addr) - command_thread = threading.Thread(target=handle, args=(conn, q)) - command_thread.start() - command_thread.join() - - - -if __name__ == '__main__': - main() diff --git a/instamaticServer/utils/config.py b/instamaticServer/utils/config.py index 52e7f84..8ae1cc3 100644 --- a/instamaticServer/utils/config.py +++ b/instamaticServer/utils/config.py @@ -1,39 +1,45 @@ from pathlib import Path +from types import SimpleNamespace +from typing import Dict + import yaml _settings_file = 'settings.yaml' -class config: +def dict_to_namespace(d: Dict) -> SimpleNamespace: + """Recursively converts a dictionary into a SimpleNamespace.""" + if isinstance(d, dict): + return SimpleNamespace(**{k: dict_to_namespace(v) for k, v in d.items()}) + return d + +class Config: def __init__(self, name:str=None): self.default_settings = self.settings() - if name != None: + if name is not None: self.default_settings['microscope'] = name self.micr_interface, self.micr_wavelength, self.micr_ranges = self.microscope() + try: + self.camera = self.load_camera_config() + except FileNotFoundError: + self.camera = SimpleNamespace() def settings(self) -> dict: """load the settings.yaml file.""" - default = None - - direc = Path(__file__).resolve().parent - file = direc.joinpath(_settings_file) + directory = Path(__file__).resolve().parent + file = directory.joinpath(_settings_file) with open(str(file), 'r') as stream: - default = yaml.safe_load(stream) - - return default + return yaml.safe_load(stream) def microscope(self): """load the microscope.yaml file.""" - default = None - - direc = Path(__file__).resolve().parent - microscope_file = '\\' + str(self.default_settings['microscope']) + '.yaml' - file = str(direc) + microscope_file - with open(file, 'r') as stream: + directory = Path(__file__).resolve().parent + file = directory / (str(self.default_settings['microscope']) + '.yaml') + with open(str(file), 'r') as stream: default = yaml.safe_load(stream) interface = default['interface'] @@ -42,9 +48,18 @@ def microscope(self): return interface, wavelength, micr_ranges + def load_camera_config(self) -> SimpleNamespace: + directory = Path(__file__).resolve().parent + file = directory / (str(self.default_settings['camera']) + '.yaml') + with open(str(file), 'r') as stream: + return dict_to_namespace(yaml.safe_load(stream)) + + +config = Config() + if __name__ == '__main__': - data = config() + data = Config() print(data.default_settings['microscope']) print(data.micr_ranges['Mh']) diff --git a/instamaticServer/utils/exceptions.py b/instamaticServer/utils/exceptions.py index 9371ff7..2800409 100644 --- a/instamaticServer/utils/exceptions.py +++ b/instamaticServer/utils/exceptions.py @@ -23,7 +23,7 @@ class TEMControllerError(Exception): 'TEMCommunicationError': TEMCommunicationError, 'JEOLValueError': JEOLValueError, 'FEIValueError': FEIValueError, - 'TEMControllerError ': TEMControllerError, + 'TEMControllerError': TEMControllerError, 'AttributeError': AttributeError, 'AssertionError': AssertionError, 'ConnectionError': ConnectionError, diff --git a/instamaticServer/utils/settings.yaml b/instamaticServer/utils/settings.yaml index 8e0ff49..3d07ff3 100644 --- a/instamaticServer/utils/settings.yaml +++ b/instamaticServer/utils/settings.yaml @@ -1,70 +1,21 @@ -microscope: tecnaiG2 -camera: simulate -calibration: simulate +microscope: simulate_tem # switch to e.g. 'tecnaiG2' after tests +camera: simulate_cam # switch to e.g. 'ultrascan' if desired # Global toggle to force simulated camera/microscope interface simulate: False data_directory: C:/instamatic -#flatfield: C:/instamatic/flatfield.tiff -flatfield: +flatfield: #C:/instamatic/flatfield.tiff # Run the TEM connection in a different process (recommended) use_tem_server: True -tem_server_host: '169.254.178.125' +tem_server_host: 'localhost' # '192.168.0.1' tem_server_port: 8088 tem_require_admin: False -tem_communication_protocol: 'pickle' # pickle, json, msgpack, yaml +tem_communication_protocol: pickle # pickle, json, msgpack, yaml # Run the Camera connection in a different process use_cam_server: False cam_server_host: 'localhost' cam_server_port: 8087 -cam_use_shared_memory: true - -# Submit collected data to an indexing server (CRED only) -use_indexing_server_exe: False -indexing_server_exe: 'instamatic.dialsserver.exe' -indexing_server_host: 'localhost' -indexing_server_port: 8089 -dials_script: 'E:/cctbx/dials_script.bat' - -# JEOL only, automatically set the rotation speed via Goniotool (instamatic.goniotool) -use_goniotool: False -goniotool_server_host: 'localhost' -goniotool_server_port: 8090 - -# For InsteaDMatic to control the rotation speed on a FEI/TFS system -fei_server_host: '192.168.12.1' -fei_server_port: 8091 - -# Automatically submit the data to an indexing server running in a VM (VirtualBox) -use_VM_server_exe: False -VM_server_exe: 'instamatic.VMserver.exe' -VM_server_host: 'localhost' -VM_server_port: 8092 -VM_ID: "Ubuntu 14.04.3" -VM_USERNAME: "lab6" -VM_PWD: "testtest" -VM_STARTUP_DELAY: 50 -VM_DESKTOP_DELAY: 20 -VM_SHARED_FOLDER: F:\SharedWithVM - -# Testing variables -cred_relax_beam_before_experiment: false -cred_track_stage_positions: false - -# Here the panels for the GUI can be turned on/off/reordered -modules: - - 'cred' - - 'cred_tvips' - - 'cred_fei' - - 'sed' - - 'autocred' - - 'red' - - 'machine_learning' - - 'ctrl' - - 'debug' - - 'about' - - 'console' - - 'io' +cam_use_shared_memory: False diff --git a/instamaticServer/utils/simulate_cam.yaml b/instamaticServer/utils/simulate_cam.yaml new file mode 100644 index 0000000..957c591 --- /dev/null +++ b/instamaticServer/utils/simulate_cam.yaml @@ -0,0 +1,21 @@ +calib_beamshift: + gridsize: 5 + stepsize: 500 +calib_directbeam: + BeamShift: + gridsize: 5 + stepsize: 75 + DiffShift: + gridsize: 5 + stepsize: 300 +camera_rotation_vs_stage_xy: -2.24 +default_binsize: 1 +default_exposure: 0.1 +dimensions: [512, 512] +dynamic_range: 255 +interface: simulate +physical_pixelsize: 0.055 +possible_binsizes: [1, 2, 4, 8, 16, 32, 64, 128, 256] +stretch_amplitude: 0.0 +stretch_azimuth: 0.0 +dead_time: 0.0 \ No newline at end of file diff --git a/instamaticServer/utils/simulate.yaml b/instamaticServer/utils/simulate_tem.yaml similarity index 98% rename from instamaticServer/utils/simulate.yaml rename to instamaticServer/utils/simulate_tem.yaml index eb8eb59..7944877 100644 --- a/instamaticServer/utils/simulate.yaml +++ b/instamaticServer/utils/simulate_tem.yaml @@ -1,10 +1,10 @@ -interface: simulate -ranges: - diff: [150, 200, 250, 300, 400, 500, 600, 800, 1000, 1200, 1500, 2000, 2500, 3000, - 3500, 4000, 4500] - lowmag: [50, 80, 100, 150, 200, 250, 300, 400, 500, 600, 800, 1000, 1200, 1500, - 2000, 2500, 3000, 5000, 6000, 8000, 10000, 12000, 15000] - mag1: [2500, 3000, 4000, 5000, 6000, 8000, 10000, 12000, 15000, 20000, 25000, 30000, - 40000, 50000, 60000, 80000, 100000, 120000, 150000, 200000, 250000, 300000, 400000, - 500000, 600000, 800000, 1000000, 1500000, 2000000] -wavelength: 0.025079 +interface: simulate +ranges: + diff: [150, 200, 250, 300, 400, 500, 600, 800, 1000, 1200, 1500, 2000, 2500, 3000, + 3500, 4000, 4500] + lowmag: [50, 80, 100, 150, 200, 250, 300, 400, 500, 600, 800, 1000, 1200, 1500, + 2000, 2500, 3000, 5000, 6000, 8000, 10000, 12000, 15000] + mag1: [2500, 3000, 4000, 5000, 6000, 8000, 10000, 12000, 15000, 20000, 25000, 30000, + 40000, 50000, 60000, 80000, 100000, 120000, 150000, 200000, 250000, 300000, 400000, + 500000, 600000, 800000, 1000000, 1500000, 2000000] +wavelength: 0.025079 diff --git a/instamaticServer/utils/singleton.py b/instamaticServer/utils/singleton.py new file mode 100644 index 0000000..42b593c --- /dev/null +++ b/instamaticServer/utils/singleton.py @@ -0,0 +1,7 @@ +class Singleton(type): + """Singleton Metaclass from Stack Overflow, stackoverflow.com/q/6760685""" + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/instamaticServer/utils/titan.yaml b/instamaticServer/utils/titan.yaml new file mode 100644 index 0000000..11c74c2 --- /dev/null +++ b/instamaticServer/utils/titan.yaml @@ -0,0 +1,9 @@ +interface: tecnai +ranges: + D: [34, 42, 53, 68, 90, 115, 140, 175, 215, 265, 330, 420, 530, 680, 830, 1050, 1350, 1700, 2100, 2700, 3700] + LAD: [] + LM: [45, 58, 73, 89, 115, 145, 185, 235, 300, 380, 470, 600, 760, 950, 1200, 1550] + Mi: [] + SA: [3400, 4400, 5600, 7200, 8800, 11500, 14500, 18500, 24000, 30000, 38000, 49000, 61000, 77000, 100000, 130000, 165000, 215000, 265000, 340000, 430000, 550000, ] # 700000, 890000, 1150000, 1250000, - can these magnifications exist in multiple modes? + Mh: [700000, 890000, 1150000, 1250000, 960000, 750000, 600000, 470000, 360000, 285000, 225000, 175000, 145000, 115000, 89000, 66000, 52000, 41000, 32000, 26000, 21000, 8300, 6200, 3100, ] +wavelength: 0.019687 diff --git a/instamaticServer/TEMController/typing.py b/instamaticServer/utils/types.py similarity index 100% rename from instamaticServer/TEMController/typing.py rename to instamaticServer/utils/types.py diff --git a/instamaticServer/utils/ultrascan.yaml b/instamaticServer/utils/ultrascan.yaml new file mode 100644 index 0000000..65f572e --- /dev/null +++ b/instamaticServer/utils/ultrascan.yaml @@ -0,0 +1,21 @@ +calib_beamshift: + gridsize: 5 + stepsize: 500 +calib_directbeam: + BeamShift: + gridsize: 5 + stepsize: 75 + DiffShift: + gridsize: 5 + stepsize: 300 +camera_rotation_vs_stage_xy: -2.24 +default_binsize: 4 +default_exposure: 0.1 +dimensions: [2048, 2048] +dynamic_range: 65535 +interface: tecnai +physical_pixelsize: 0.014 +possible_binsizes: [1, 2, 4] +stretch_amplitude: 0.0 +stretch_azimuth: 0.0 +dead_time: 0.5 diff --git a/requirements.txt b/requirements.txt index a567911..78690ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ comtypes PyYAML typing; python_version < '3.5' +# numpy # only used by camera server diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..a1fa251 --- /dev/null +++ b/start.bat @@ -0,0 +1,10 @@ +:: VARIANT 1 - IF YOU HAVE PROPERLY WORKING VENV +cd c:\instamatic\instamatic-tecnai-server &:: point to your installation directory +call .\venv\Scripts\activate.bat &:: uncomment and point to venv if using one +python tem_server.py &:: use `py`, `py3`, or `python` as needed + +:: VARIANT 2 - CALL DIRECTLY, REQUIRES UNCOMMENTED BOILERPLATE +:: cd Q:\DanielT\instamatic-tecnai-server +:: Q:\Malika\py\py34\python-3.4.4\python.exe Q:\DanielT\instamatic-tecnai-server\tem_server.py + +:: If using cam server, this file can be copied and also adapted for cam_server.py diff --git a/tem_server.py b/tem_server.py new file mode 100644 index 0000000..52cf05e --- /dev/null +++ b/tem_server.py @@ -0,0 +1,273 @@ +# IF YOUR VENV DOES NOT WORK CORRECTLY, A BOILERPLATE LIKE THIS MAY BE REQUIRED +# import sys +# sys.path.insert(0, r'Q:\DanielT\instamatic-tecnai-server\venv\Lib\site-packages') +# BOILERPLATE END + +import argparse +import datetime +import inspect +import logging +import queue +import socket +import sys +import threading +import time +import traceback +import uuid + +from typing import Any, Type, Callable + + +from instamaticServer.TEMController.microscope import get_microscope +from instamaticServer.serializer import dumper, loader +from instamaticServer.utils.config import config + + +logging.addLevelName(15, "EVAL") +_generators = {} +BUFSIZE = 1024 +TIMEOUT = 0.5 + + +def log_eval(logger, status, func_name, args, kwargs, ret): + if logger.isEnabledFor(15): + args_list = [repr(a) for a in args] + args_list += ['%s=%r' % (k, v) for k, v in kwargs.items()] + args_str = ', '.join(args_list) + logger.log(15, '%s | %s(%s): %s', status, func_name, args_str, ret) + + +class DeviceServer(threading.Thread): + """General microscope / camera (Acquisition) communication server. + + Takes a `name` of the microscope/camera and initializes appropriate device. + When `TemServer.start` thread method is called, `TemServer.run` starts. + The server will wait for cmd `requests` to appear in the queue, evaluate + them in order, and return the result via each client's `response_queue`. + """ + + device_abbr = None # type: str + device_kind = None # type: str + device_getter = None # type: Callable + requests = None # type: queue.Queue + responses = None # type: queue.Queue + stop_event = None # type: threading.Event + host = 'localhost' # type: str + port = None # type: int + + def __init__(self, name = None) -> None: + super().__init__(name=self.device_kind + '_server') + self.interface_name = name + self.logger = logging.getLogger(self.device_abbr) # temS/camS server + self.device = None + self.verbose = False + + def run(self) -> None: + """Start the server thread.""" + self.device = self.device_getter(name=self.interface_name) + self.device.get_attrs = self.get_attrs + self.logger.info('Initialized %s %s server thread', self.device_kind, self.device.name) + + while True: + try: + cmd = self.requests.get(timeout=TIMEOUT) + except queue.Empty: + if self.stop_event.is_set(): + break + continue + + func_name = cmd.get('func_name', cmd.get('attr_name')) + args = cmd.get('args', ()) + kwargs = cmd.get('kwargs', {}) + + try: + ret = self.evaluate(func_name, args, kwargs) + status = 200 + if inspect.isgenerator(ret): + gen_id = uuid.uuid4().hex + _generators[gen_id] = ret + ret = {'__generator__': gen_id} + except Exception as e: + traceback.print_exc() + self.logger.exception(e) + ret = (e.__class__.__name__, e.args) + status = 500 + + self.responses.put((status, ret)) + log_eval(self.logger, status, func_name, args, kwargs, ret) + + self.logger.info('Terminating %s %s server thread', self.device_kind, self.device.name) + + def evaluate(self, func_name: str, args: list, kwargs: dict) -> Any: + """Eval function `func_name` on `self.device` with `args` & `kwargs`.""" + self.logger.debug('evaluate(func_name=%s, args=%s, kwargs=%s)', func_name, args, kwargs) + + if func_name == '__gen_next__': + gen = _generators[kwargs['id']] + try: + return next(gen) + except StopIteration: + del _generators[kwargs['id']] + return + + if func_name == "__gen_close__": + _generators.pop(kwargs['id'], None) + return + + f = getattr(self.device, func_name) + return f(*args, **kwargs) if callable(f) else f + + def get_attrs(self): + """Get attributes from cam object to update __dict__ on client side.""" + attrs = {} + for item in dir(self.device): + if item.startswith('_'): + continue + obj = getattr(self.device, item) + if not callable(obj): + attrs[item] = type(obj) + + return attrs + + +class TemServer(DeviceServer): + """TEM communcation server.""" + + device_abbr = 'tem' + device_kind = 'microscope' + device_getter = staticmethod(get_microscope) + requests = queue.Queue(maxsize=1) + responses = queue.Queue(maxsize=1) + stop_event = threading.Event() + host = config.default_settings['tem_server_host'] + port = config.default_settings['tem_server_port'] + + +def handle(conn: socket.socket, server_type: Type[DeviceServer]) -> None: + """Handle incoming connection, put command on the Queue `q`, which is then + handled by TEMServer.""" + with conn: + conn.settimeout(TIMEOUT) + while True: + if server_type.stop_event.is_set(): + break + + try: + data = conn.recv(BUFSIZE) + except socket.timeout: + continue + + if not data: + break + + data = loader(data) + + if data == 'exit' or data == 'kill': + break + + server_type.requests.put(data) + response = server_type.responses.get() + serialized = dumper(response) + conn.sendall(serialized) + + +def listen(server_type: Type[DeviceServer]) -> None: + """Listen on a given server host/port and handle incoming instructions""" + + logger = logging.getLogger(server_type.device_abbr) # tem/cam listener + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as device_client: + device_client.bind((server_type.host, server_type.port)) + device_client.settimeout(TIMEOUT) + device_client.listen(1) + logger.info('Server listening on %s:%s', server_type.host, server_type.port) + while True: + if server_type.stop_event.is_set(): + break + try: + connection, _ = device_client.accept() + handle(connection, server_type) + except socket.timeout: + pass + except Exception as e: + logger.exception('Exception when handling connection: %s', e) + logger.info('Terminating %s listener thread', server_type.device_kind) + + +class MicrosecondFormatter(logging.Formatter): + def formatTime(self, record, datefmt=None): + t = datetime.datetime.fromtimestamp(record.created) + return t.strftime(datefmt) if datefmt else t.strftime("%H:%M:%S.%f") + + +def setup_logging(device_abbr='tem') -> logging.Logger: + logger = logging.getLogger(device_abbr) + logger.setLevel(15) + + logfile = '%s_server_%s.log' % (device_abbr, datetime.datetime.now().strftime('%Y-%m-%d')) + fh = logging.FileHandler(logfile) + sh = logging.StreamHandler(sys.stdout) + + fmt = MicrosecondFormatter('%(asctime)s %(name)-3s: %(levelname)-8s %(message)s') + fh.setFormatter(fmt) + sh.setFormatter(fmt) + + logger.addHandler(fh) + logger.addHandler(sh) + + logger.propagate = False + return logger + + +def main() -> None: + """ + Connects to the TEM and starts a server for microscope communication. + Opens a socket on port {TemServer.host}:{TemServer.port}. + + This program initializes a connection to the TEM as defined in the config. + The purpose of this program is to isolate the microscope connection in + a separate process for improved stability of the interface in case + instamatic crashes or is started and stopped frequently. + For running the GUI, the temserver is required. + Another reason is that it allows for remote connections from different PCs. + The connection goes over a TCP socket. + + The host and port are defined in `config/settings.yaml`. + + The data sent over the socket is a serialized dictionary with the following: + + - `func_name`: Name of the function to call (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 serialized object. + """ + + parser = argparse.ArgumentParser(description=main.__doc__) + parser.add_argument('-t', '--microscope', action='store', + help='Override microscope to use.') + parser.set_defaults(microscope=None) + options = parser.parse_args() + + logger = setup_logging(device_abbr='tem') + logger.info('Tecnai microscope server starting') + + tem_server = TemServer(name=options.microscope) + tem_server.start() + + tem_listener = threading.Thread(target=listen, args=(TemServer,), name='tem_listener') + tem_listener.start() + + try: + while not TemServer.stop_event.is_set(): time.sleep(TIMEOUT) + except KeyboardInterrupt: + logger.info("Received KeyboardInterrupt, shutting down...") + finally: + TemServer.stop_event.set() + tem_server.join() + tem_listener.join() + logger.info('Tecnai microscope server terminating') + logging.shutdown() + + +if __name__ == '__main__': + main() diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..44a4db1 --- /dev/null +++ b/tests.py @@ -0,0 +1,452 @@ +# IF YOUR VENV DOES NOT WORK CORRECTLY, A BOILERPLATE LIKE THIS MAY BE REQUIRED +# import sys +# sys.path.insert(0, r'Q:\DanielT\instamatic-tecnai-server\venv\Lib\site-packages') +# BOILERPLATE END + +import abc +import atexit +import pickle +import socket +import threading +import time +import unittest + +from typing import Type + +from instamaticServer.TEMController.simu_microscope import SimuMicroscope +from instamaticServer.utils.config import SimpleNamespace, config, dict_to_namespace +from cam_server import CamServer +from tem_server import DeviceServer, TemServer, listen + + +_conf_dict = {'a': 1, 'b': {'c': 3, 'd': 4}} +PRECISION_NM = 250 +PRECISION_DEG = 0.1 +TIMEOUT = 30 + + +class TestConfig(unittest.TestCase): + def test_namespace(self): + ns = SimpleNamespace(**_conf_dict) + self.assertEqual(ns.a, 1) + self.assertEqual(ns.b, {'c': 3, 'd': 4}) + + def test_dict_to_namespace(self): + ns = dict_to_namespace(_conf_dict) + self.assertEqual(ns.a, 1) + self.assertEqual(ns.b, SimpleNamespace(**{'c': 3, 'd': 4})) + + def test_config(self): + self.assertIn(config.micr_interface, {'tecnai', 'simulate'}) + self.assertIsInstance(config.camera, SimpleNamespace) + + +class TestSerializers(unittest.TestCase): + def test_json_serializer(self): + from instamaticServer.serializer import json_dumper, json_loader + self.assertEqual(_conf_dict, json_loader(json_dumper(_conf_dict))) + + def test_pickle_serializer(self): + from instamaticServer.serializer import pickle_dumper, pickle_loader + self.assertEqual(_conf_dict, pickle_loader(pickle_dumper(_conf_dict))) + + def test_msgpack_serializer(self): + try: + from instamaticServer.serializer import msgpack_dumper, msgpack_loader + self.assertEqual(_conf_dict, msgpack_loader(msgpack_dumper(_conf_dict))) + except ImportError: + pass + + +class TestDeviceServer(unittest.TestCase, metaclass=abc.ABCMeta): + DeviceServer = None # type: Type[DeviceServer] + device_abbr = None # type: str + + @classmethod + def setUpClass(cls): + cls.server = cls.DeviceServer() + cls.server.start() + ln = cls.device_abbr + '_listener' + cls.listener = threading.Thread(target=listen, args=(cls.DeviceServer,), name=ln) + cls.listener.start() + cls.threads = [cls.server, cls.listener] + atexit.register(cls.tearDownClass) + + t0 = time.perf_counter() + while getattr(cls.server, 'device', None) is None: + if time.perf_counter() - t0 > 5: + raise RuntimeError(cls.device_abbr.upper() + ' device did not initialize') + time.sleep(0.05) + + try: + cls.const = cls.server.device._tem_constant + except AttributeError: + cls.const = None + + host = config.default_settings[cls.device_abbr + '_server_host'] + port = config.default_settings[cls.device_abbr + '_server_port'] + cls.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + cls.socket.connect((host, port)) + cls.socket.settimeout(TIMEOUT) + + @classmethod + def tearDownClass(cls): + cls.server.stop_event.set() + while cls.threads: + thread = cls.threads.pop(0) + thread.join(timeout=5) + if thread.is_alive(): + print('Thread %s did not exit' % thread.name) + + if hasattr(cls, 'socket') and cls.socket: + try: + cls.socket.close() + except Exception as e: + print('Error closing %s socket:' % cls.device_abbr, e) + finally: + cls.socket = None + + def send(self, func, args = (), kwargs = None): + from instamaticServer.serializer import dumper, loader + from instamaticServer.utils.exceptions import TEMCommunicationError, exception_list + kwargs = kwargs or {} + d = {'func_name': func, 'args': args, 'kwargs': kwargs} + buffer_size = 1024 + if func in ('get_image', 'get_movie', '__gen_next__'): + buffer_size += 8 * config.camera.dimensions[0] * config.camera.dimensions[1] + self.socket.sendall(dumper(d)) + response = self.socket.recv(buffer_size) + if response: + for _ in range(100): # warrants all image/movie is collected + try: + status, data = loader(response) + except (pickle.UnpicklingError, EOFError, ValueError): + response += self.socket.recv(buffer_size) + time.sleep(0.01) + else: + break + else: + status = 500 + data = (None, None) + else: + raise RuntimeError('Received empty response when evaluating %s' % d) + if status == 200: + return data + elif status == 500: + error_code, args = data + raise exception_list.get(error_code, TEMCommunicationError)(*args) + else: + raise ConnectionError('Unknown status code: %s' % status) + + +class TestTemServer(TestDeviceServer): + DeviceServer = TemServer + device_abbr = 'tem' + + def test_20_getHolderType(self): + r = self.send('getHolderType') + if not isinstance(self.server.device, SimuMicroscope): + self.assertIsInstance(r, int) + self.assertIn(r,list(self.const.StageHolderType.values())) + + def test_21_getStagePosition(self): + r = self.send('getStagePosition') + self.assertIsInstance(r, tuple) + self.assertEqual(len(r), 5) + + def test_22_getStageSpeed(self): + r = self.send('getStageSpeed') + self.assertEqual(r, 0.5) + + def test_23_is_goniotool_available(self): + r = self.send('is_goniotool_available') + self.assertEqual(r, False) + + def test_24_isAThreadAlive(self): + r = self.send('isAThreadAlive') + self.assertEqual(r, False) + + def test_25_isStageMoving(self): + r = self.send('isStageMoving') + self.assertEqual(r, False) + + def test_30_setStagePosition(self): + p = (0, 0, 0, 0, 0) + self.send('setStagePosition', p) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[0], p[0], delta=PRECISION_NM) + self.assertAlmostEqual(r[1], p[1], delta=PRECISION_NM) + self.assertAlmostEqual(r[2], p[2], delta=PRECISION_NM) + self.assertAlmostEqual(r[3], p[3], delta=PRECISION_DEG) + self.assertAlmostEqual(r[4], p[4], delta=PRECISION_DEG) + + def test_31_setStagePosition(self): + self.send('setStagePosition', (0, 0, 0, 0, 0)) + self.send('setStagePosition', kwargs={'x': 10000, 'y': 10000}) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[0], 10000, delta=PRECISION_NM) + self.assertAlmostEqual(r[1], 10000, delta=PRECISION_NM) + self.assertAlmostEqual(r[2], 0, delta=PRECISION_NM) + self.assertAlmostEqual(r[3], 0, delta=PRECISION_DEG) + self.assertAlmostEqual(r[4], 0, delta=PRECISION_DEG) + + def test_32_setStagePosition(self): + self.send('setStagePosition', (10000, 10000, 0, 0, 0)) + self.send('getStagePosition') + self.send('setStagePosition', kwargs={'z': 10000}) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[2], 10000, delta=PRECISION_NM) + self.send('setStagePosition', kwargs={'z': 0}) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[2], 0, delta=PRECISION_NM) + self.send('setStagePosition', (10000, 10000, 10000, 0, 0)) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[0], 10000, delta=PRECISION_NM) + self.assertAlmostEqual(r[1], 10000, delta=PRECISION_NM) + self.assertAlmostEqual(r[2], 10000, delta=PRECISION_NM) + + def test_33_setStagePosition(self): + self.send('setStagePosition', (0, 0, 0, 0, 0)) + self.send('setStagePosition', kwargs={'a': 10}) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[3], 10, delta=PRECISION_DEG) + + def test_35_setStagePosition(self): + self.send('setStagePosition', (0, 0, 0, 0, 0)) + t0 = time.perf_counter() + self.send('setStagePosition', kwargs={'x': 5000}) + t1 = time.perf_counter() + self.send('setStagePosition', kwargs={'x': 0, 'speed': 0.1}) + t2 = time.perf_counter() + self.send('setStagePosition', kwargs={'x': 5000, 'speed': 0.05}) + t3 = time.perf_counter() + self.send('setStagePosition', kwargs={'x': 0, 'speed': 0.02}) + t4 = time.perf_counter() + if not isinstance(self.server.device, SimuMicroscope): + self.assertLess(t1 - t0, t2 - t1) + self.assertLess(t2 - t1, t3 - t2) + self.assertLess(t3 - t2, t4 - t3) + + def test_36_setStagePosition(self): + self.send('setStagePosition', (0, 0, 0, 0, 0)) + t0 = time.perf_counter() + self.send('setStagePosition', kwargs={'a': 2}) + t1 = time.perf_counter() + self.send('setStagePosition', kwargs={'a': 0, 'speed': 0.1}) + t2 = time.perf_counter() + self.send('setStagePosition', kwargs={'a': 2, 'speed': 0.05}) + t3 = time.perf_counter() + self.send('setStagePosition', kwargs={'a': 0, 'speed': 0.02}) + t4 = time.perf_counter() + if not isinstance(self.server.device, SimuMicroscope): + self.assertLess(t2 - t1, t3 - t2) + self.assertLess(t1 - t0, t2 - t1) + self.assertLess(t3 - t2, t4 - t3) + + def test_38_setStagePosition(self): + self.send('setStagePosition', (0, 0, 0, 0, 0)) + t0 = time.perf_counter() + self.send('setStagePosition', kwargs={'a': 30, 'wait': False}) + t1 = time.perf_counter() + time.sleep(0.1) + self.send('waitForStage') + t2 = time.perf_counter() + q = {'a': 0, 'wait': True} + self.send('setStagePosition', kwargs=q) + t3 = time.perf_counter() + self.assertLess(t1 - t0, t2 - t1) + self.assertLess(t1 - t0, t3 - t2) + + def test_40_setStageA(self): + p = (0, ) + self.send('setStageA', p) + r = self.send('getStagePosition') + self.assertAlmostEqual(r[3], p[0], delta=PRECISION_DEG) + + def test_46_setRotationSpeed(self): + self.send('setStageA', kwargs={'value': 0}) + t0 = time.perf_counter() + self.send('setRotationSpeed', (1.0,)) + self.send('setStageA', kwargs={'value': 5}) + t1 = time.perf_counter() + self.send('setRotationSpeed', (0.1,)) + self.send('setStageA', kwargs={'value': 0}) + t2 = time.perf_counter() + self.assertLess(t1 - t0, t2 - t1) + self.send('setRotationSpeed', (0.05,)) + self.send('setStageA', kwargs={'value': 5}) + t3 = time.perf_counter() + self.assertLess(t2 - t1, t3 - t2) + self.send('setRotationSpeed', (1.0,)) + self.send('setStageA', kwargs={'value': 0}) + + def test_48_setStageA(self): + self.send('setStageA', (0,)) + t0 = time.perf_counter() + self.send('setStageA', kwargs={'value': 10, 'wait': False}) + t1 = time.perf_counter() + time.sleep(0.1) + self.send('waitForStage', kwargs={'delay': 0.01}) + t2 = time.perf_counter() + self.send('setStageA', kwargs={'value': 0, 'wait': True}) + t3 = time.perf_counter() + self.assertLess(t1 - t0, t2 - t1) + self.assertLess(t1 - t0, t3 - t2) + + def test_50_getGunShift(self): + self.send('getGunShift') + + def test_51_getHTValue(self): + self.send('getHTValue') + + def test_52_isBeamBlanked(self): + self.send('isBeamBlanked') + + def test_53_getBeamAlignShift(self): + self.send('getBeamAlignShift') + + def test_54_getSpotSize(self): + self.send('getSpotSize') + + def test_55_getBrightness(self): + self.send('getBrightness') + + def test_56_getBrightnessValue(self): + self.send('getBrightnessValue') + + def test_57_getBeamShift(self): + self.send('getBeamShift') + + def test_58_getBeamTilt(self): + self.send('getBeamTilt') + + def test_59_getCondensorLensStigmator(self): + self.send('getCondensorLensStigmator') + + def test_61_getScreenCurrent(self): + self.send('getScreenCurrent') + + def test_62_isfocusscreenin(self): + r = self.send('isfocusscreenin') + self.assertIsInstance(r, bool) + + def test_63_getScreenPosition(self): + r = self.send('getScreenPosition') + self.assertIn(r, {'up', 'down', ''}) + + def test_64_getDiffFocus(self): + func_mode = self.send('getFunctionMode') + self.send('setFunctionMode', ('diff',)) + r = self.send('getDiffFocus') + self.send('setFunctionMode', (func_mode,)) + self.assertGreater(r, 0) + self.assertLess(r, 65536) + + def test_65_getDiffFocusValue(self): + func_mode = self.send('getFunctionMode') + self.send('setFunctionMode', ('diff',)) + r = self.send('getDiffFocusValue') + self.send('setFunctionMode', (func_mode,)) + if not isinstance(self.server.device, SimuMicroscope): + self.assertGreater(r, -1.0) + self.assertLess(r, 1.0) + + def test_65_getFocus(self): + r = self.send('getFocus') + if not isinstance(self.server.device, SimuMicroscope): + self.assertGreater(r, -1.0) + self.assertLess(r, 1.0) + + def test_67_FunctionMode(self): + r = self.send('getFunctionMode') + self.send('setFunctionMode', (r,)) + self.assertIn(r, ('lowmag', 'mag1', 'samag', 'mag2', 'diff')) + + def test_68_Magnification(self): + r = self.send('getMagnification') + self.send('setMagnification', (r,)) + self.assertIsInstance(r, (int, float)) + + def test_69_MagnificationIndex(self): + r = self.send('getMagnificationIndex') + self.send('setMagnificationIndex', (r,)) + self.assertIsInstance(r, int) + + def test_70_getDarkFieldTilt(self): + r = self.send('getDarkFieldTilt') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + def test_71_getImageShift1(self): + r = self.send('getImageShift1') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + def test_72_getImageShift2(self): + r = self.send('getImageShift2') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + def test_73_getImageBeamShift(self): + r = self.send('getImageBeamShift') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + def test_74_getDiffShift(self): + r = self.send('getDiffShift') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + def test_76_getObjectiveLensStigmator(self): + r = self.send('getObjectiveLensStigmator') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + def test_77_getIntermediateLensStigmator(self): + r = self.send('getIntermediateLensStigmator') + self.assertIsInstance(r[0], (int, float)) + self.assertIsInstance(r[1], (int, float)) + + +class TestCamServer(TestDeviceServer): + DeviceServer = CamServer + device_abbr = 'cam' + + def test_90_get_binning(self): + r = self.send('get_binning') + self.assertIsInstance(r, int) + + def test_91_default_binsize(self): + r = self.send('get_binning') + s = self.send('default_binsize') + self.assertEqual(r, s) + + def test_92_dimensions(self): + r = self.send('dimensions') + self.assertIsInstance(r[0], int) + + def test_93_get_camera_dimensions(self): + r = self.send('get_camera_dimensions') + s = self.send('dimensions') + self.assertEqual(r, s) + + def test_94_get_image_dimensions(self): + r = self.send('get_image_dimensions') + s = self.send('get_camera_dimensions') + self.assertEqual(r[0], s[0]) + self.assertEqual(r[1], s[1]) + + def test_96_get_image(self): + r = self.send('get_image') + s = self.send('get_image_dimensions') + self.assertEqual(len(r), s[0]) + + def test_97_get_image(self): + r = self.send('get_image') + s = self.send('get_image_dimensions') + self.assertEqual(len(r), s[0]) + + def test_98_get_movie(self): + r = self.send('get_movie', (1, )) + s = self.send('get_image_dimensions') + self.assertEqual(len(r[0]), s[0])