Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a150689
Add a crude implementation to control beamshift with RMB (for FEI cRED)
Baharis Nov 10, 2025
24a369a
Add a crude implementation to control stage opsition with LMB
Baharis Nov 11, 2025
94fd7fb
Allow rotation speed control even if goniotool is not available
Baharis Nov 11, 2025
5f5fac3
Add more details, otherwise more difficult to reach, to pets input
Baharis Nov 20, 2025
0ffc01f
Add some more examples to the pets prefix
Baharis Nov 20, 2025
a73ed8f
Videoframe instance needs no app kwarg anymore since it is class attr…
Baharis Nov 21, 2025
0b94d82
Raname `HasQMixin` to `ModuleFrameMixin` to generalize it better
Baharis Nov 21, 2025
621d954
`Any` is a significantly better type hint that `None`
Baharis Nov 21, 2025
8eeda49
Do not change default `ENABLE_FOOTFREE_OPTION`
Baharis Nov 21, 2025
190d632
Improve lmb/rmb responsiveness, messages, GUI feedback
Baharis Nov 21, 2025
2f1d726
After all, why would you ever want to disable the footfree option???
Baharis Nov 21, 2025
9bfe76c
Change "Rotation Speed" to "Rotation speed" to align with other settings
Baharis Nov 21, 2025
d672d28
Update src/instamatic/gui/ctrl_frame.py
Baharis Nov 24, 2025
08048e2
Update src/instamatic/gui/ctrl_frame.py
Baharis Nov 24, 2025
dc96cd0
Make local config path reference more readable
Baharis Nov 24, 2025
31c28ea
Implement `toggle_lmb_stage` fallback using `move_in_projection`
Baharis Nov 24, 2025
f639dab
Implement fixes necessary to handle remote camera via server
Baharis Nov 24, 2025
560b318
Merge branch 'main' into remote_camera
Baharis Nov 24, 2025
c593bf4
Allow setting non-integer rotation speeds
Baharis Nov 24, 2025
99853eb
Merge branch 'remote_camera' into ctrl_tem_with_mouse
Baharis Nov 24, 2025
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
50 changes: 26 additions & 24 deletions src/instamatic/camera/camera_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -55,6 +55,7 @@ def __init__(
self.name = name
self.interface = interface
self._bufsize = BUFSIZE
self._eval_lock = threading.Lock()
self.verbose = False

try:
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/autocred_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions src/instamatic/gui/base_module.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from queue import Queue
from typing import Any


class BaseModule:
Expand Down Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/instamatic/gui/click_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/cred_fei_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
6 changes: 3 additions & 3 deletions src/instamatic/gui/cred_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/cred_tvips_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
96 changes: 83 additions & 13 deletions src/instamatic/gui/ctrl_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -74,21 +78,31 @@ 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)
e_stage_y.grid(row=6, column=2, sticky='EW')
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
)
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'}))

Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/debug_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/instamatic/gui/fast_adt_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 4 additions & 2 deletions src/instamatic/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading