Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58cb87a
Rewrite TEM server to allow second identical server that communicates…
Baharis Nov 25, 2025
ea01146
Read camera config, add config interpreter, adapt get_camera, server
Baharis Nov 27, 2025
065cffb
Write 62 rudimentary tests for basic library functions, to be tested
Baharis Nov 28, 2025
816e193
Reimplement the whole freaking system as a package to enable testing
Baharis Dec 2, 2025
607f371
Adapt simu microscope and fix all 50 tests
Baharis Dec 2, 2025
268d8b8
Try removing remaining Python 3.4 incompatibilities
Baharis Dec 3, 2025
f276cb3
Fix tests to do logging & printing neatly
Baharis Dec 3, 2025
b5d0665
Add atexit to be sure `TestServer` exits correctly
Baharis Dec 3, 2025
2284ae3
Run tests on tecnai – success, new interface works!
Baharis Dec 3, 2025
3c1cb14
Rewrite the start.bat to closer match the actual use case
Baharis Dec 3, 2025
b8ac789
E-RC: hotfixes to make things work on Titan
Jan 5, 2026
182d838
E-RC: split joint tem server into tem and cam servers working in sepa…
Baharis Jan 6, 2026
7c6d7d9
E-RC: address logging, stop event cross-contamination between two new…
Baharis Jan 6, 2026
085aa78
E-RC: get_image: respect default binsize, fix get_image_dimensions typo
Baharis Jan 6, 2026
fee7403
E-RC: get_image: fix get_image_dimensions bug (determine locally)
Baharis Jan 6, 2026
f1a1d7e
E-RC: non-callable remote camera attrs must be private if non-picklable
Baharis Jan 6, 2026
6954646
E-RC: any subclasses of simple namespace are not pickle-able and must…
Baharis Jan 6, 2026
ed3e6d3
E-RC: any subclasses of simple namespace are not pickle-able and must…
Baharis Jan 6, 2026
5dbdbe3
E-RC: fix get_image typo (binning -> binsize)
Baharis Jan 6, 2026
7a5ebc9
E-RC: patch instamatic RPC to allow get_movie generator
Baharis Jan 8, 2026
6405f65
E-RC: patch instamatic RPC to allow get_movie generator (add file)
Baharis Jan 8, 2026
3bba7cf
E-RC: patch instamatic RPC to allow get_movie, but only yield lazily …
Baharis Jan 8, 2026
5db5fb4
E-RC: patch instamatic RPC to allow get_movie, but only yield lazily …
Baharis Jan 8, 2026
ddd8ab3
ER-C: adapting patches for production
Baharis Jan 12, 2026
0ca1e00
Run Config ~once, update README, simplify settings, fix tests
Baharis Jan 14, 2026
1f0cd5e
Remove unused import statements
Baharis Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
90 changes: 90 additions & 0 deletions cam_server.py
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 37 additions & 0 deletions instamaticServer/TEMController/camera.py
Original file line number Diff line number Diff line change
@@ -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



25 changes: 9 additions & 16 deletions instamaticServer/TEMController/microscope.py
Original file line number Diff line number Diff line change
@@ -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']
Expand All @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions instamaticServer/TEMController/remote_movie.py
Original file line number Diff line number Diff line change
@@ -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)
104 changes: 104 additions & 0 deletions instamaticServer/TEMController/simu_camera.py
Original file line number Diff line number Diff line change
@@ -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()
Loading