Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fddfb3d
feat(camera): add best-effort set_frame_rate hint to AbstractCamera
hongquanli Jun 21, 2026
1bb5e69
feat(camera): SimulatedCamera honors set_frame_rate in continuous stream
hongquanli Jun 21, 2026
1a81c53
feat(camera): ToupcamCamera.set_frame_rate via PRECISE_FRAMERATE
hongquanli Jun 21, 2026
c0fb841
refactor: extract MultiPointWorkerBase (no behavior change)
hongquanli Jun 21, 2026
4b8cc1f
refactor: extract create_experiment_dir helper into acquisition_setup…
hongquanli Jun 21, 2026
9df259a
feat(streaming): add CountStop + RecordingRouter (pure)
hongquanli Jun 21, 2026
deae236
feat(streaming): RecordingWriter (bounded queue + thread -> ZarrWriter)
hongquanli Jun 21, 2026
83fa68a
fix(streaming): drain thread solely owns ZarrWriter lifecycle (no abo…
hongquanli Jun 21, 2026
3be2db4
feat(streaming): ContinuousFrameSource + StreamingCapture orchestrator
hongquanli Jun 21, 2026
5d78773
fix(streaming): move CameraAcquisitionMode import to top (no real cyc…
hongquanli Jun 21, 2026
aac4518
feat(record-zstack): params + pure planning helpers
hongquanli Jun 21, 2026
e0eb6bd
test(record-zstack): cover validation paths + document epsilon
hongquanli Jun 21, 2026
691369d
feat(record-zstack): RecordZStackWorker (record + zstack per FOV)
hongquanli Jun 21, 2026
fb93711
feat(record-zstack): RecordZStackController + ZarrWriter daemon-threa…
hongquanli Jun 21, 2026
3d2a62c
feat(record-zstack): RecordZStackMultiPointWidget + validation
hongquanli Jun 21, 2026
d66885c
fix(record-zstack): correct laser-AF reference check + dedup zstack r…
hongquanli Jun 21, 2026
ae5af71
feat(record-zstack): inline channel editors, Copy-from-Live, computed…
hongquanli Jun 21, 2026
ba30f7d
test(record-zstack): tighten invalid-range plane-label assertion + do…
hongquanli Jun 21, 2026
8431755
feat(record-zstack): Start/Stop handoff to RecordZStackController
hongquanli Jun 21, 2026
e644787
feat(record-zstack): add Record + Z-Stack tab to gui_hcs (ENABLE_RECO…
hongquanli Jun 21, 2026
3014893
feat(record-zstack): widget finished/progress slots for gui_hcs wiring
hongquanli Jun 21, 2026
25131f4
fix(record-zstack): add recordZStackWidget None sentinel (gui_hcs)
hongquanli Jun 21, 2026
27694cb
fix(record-zstack): illumination, trigger mode, streaming & live-view…
hongquanli Jun 21, 2026
da939bf
fix: harden streaming_capture (fps order, start error path, OOB gatin…
hongquanli Jun 21, 2026
2e9cf57
fix(record-zstack): batch-3 correctness, threading, and simplificatio…
hongquanli Jun 21, 2026
103bcd9
refactor: DRY batch 4 — lift _wait_for_outstanding_callback_images to…
hongquanli Jun 21, 2026
82d8166
fix: Wire signal_acquisition_started for Record+Z-Stack UI lockout (f…
hongquanli Jun 21, 2026
d4dc9d5
fix: emit signal_acquisition_started(True) before spawning worker thread
hongquanli Jun 21, 2026
df68321
refactor: Remove setter shims and duplicate attrs from RecordZStackCo…
hongquanli Jun 22, 2026
2aacdb6
style(record-zstack): compact widget layout to match WellplateMultiPoint
hongquanli Jun 22, 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
46 changes: 46 additions & 0 deletions software/control/camera_toupcam.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import threading
import control.toupcam as toupcam
import control.toupcam_exceptions
from control.toupcam_exceptions import hresult_checker

log = squid.logging.get_logger(__name__)
Expand Down Expand Up @@ -50,6 +51,21 @@ def get_sn_by_model(camera_model: ToupcamCameraModel):
return None # return None if no device with the specified model_name is connected


def clamp_precise_framerate_tenths(fps: float, min_tenths: int, max_tenths: int) -> int:
"""Clamp fps (in frames per second) to the allowed range in tenths.

Args:
fps: Desired frame rate in frames per second
min_tenths: Minimum allowed value in tenths (0.1 fps units)
max_tenths: Maximum allowed value in tenths (0.1 fps units)

Returns:
Clamped value in tenths of fps
"""
tenths = int(round(fps * 10.0))
return max(min_tenths, min(max_tenths, tenths))


class ToupcamCamera(AbstractCamera):
TOUPCAM_OPTION_RAW_RAW_VAL = 1
TOUPCAM_OPTION_RAW_RGB_VAL = 0
Expand Down Expand Up @@ -556,6 +572,36 @@ def get_exposure_limits(self) -> Tuple[float, float]:
(min_exposure, max_exposure, default_exposure) = self._camera.get_ExpTimeRange()
return min_exposure / 1000.0, max_exposure / 1000.0 # us -> ms

def set_frame_rate(self, fps: float) -> float:
"""Set the frame rate via PRECISE_FRAMERATE option.

_calculate_strobe_info (~:128-140) drives PRECISE_FRAMERATE to MAX on mode
switch; set_frame_rate must be called **after** entering CONTINUOUS to take
effect, and recording restores nothing (next acquisition resets exposure → MAX again).

Args:
fps: Desired frame rate in frames per second. If None or <= 0, returns
current achievable frame rate without changing settings.

Returns:
The achievable frame rate in frames per second, or current rate if not changed.
"""
if fps is None or fps <= 0:
return 1000.0 / self.get_total_frame_time()
try:
max_tenths = self._camera.get_Option(toupcam.TOUPCAM_OPTION_MAX_PRECISE_FRAMERATE)
min_tenths = self._camera.get_Option(toupcam.TOUPCAM_OPTION_MIN_PRECISE_FRAMERATE)
except toupcam.HRESULTException as ex:
self._log.warning(f"precise-framerate range read failed: {control.toupcam_exceptions.explain(ex)}")
return 1000.0 / self.get_total_frame_time()
tenths = clamp_precise_framerate_tenths(fps, min_tenths, max_tenths)
try:
self._camera.put_Option(toupcam.TOUPCAM_OPTION_PRECISE_FRAMERATE, tenths)
except toupcam.HRESULTException as ex:
self._log.warning(f"set precise-framerate failed: {control.toupcam_exceptions.explain(ex)}")
return 1000.0 / self.get_total_frame_time()
return tenths / 10.0

@staticmethod
def _user_gain_to_toupcam(user_gain):
"""
Expand Down
59 changes: 59 additions & 0 deletions software/control/core/acquisition_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Shared acquisition setup helpers.

Free functions used by MultiPointController (and future controllers such as
RecordZStackController) to set up experiment directories without duplicating
logic across controller classes.
"""

import os
from datetime import datetime
from typing import Optional, Tuple

from control import utils


def compute_pixel_size_um(objective_store, camera) -> Optional[float]:
"""Compute the physical pixel size in µm from objective and camera metadata.

Returns the product of the objective's pixel-size factor and the camera's
binned pixel size in µm, or None if either value is unavailable or an
exception is raised.

Args:
objective_store: ObjectiveStore (or compatible object) with
``get_pixel_size_factor() -> Optional[float]``.
camera: AbstractCamera (or compatible) with
``get_pixel_size_binned_um() -> Optional[float]``.

Returns:
Pixel size in µm, or None.
"""
try:
pixel_factor = objective_store.get_pixel_size_factor()
sensor_pixel_um = camera.get_pixel_size_binned_um()
if pixel_factor is not None and sensor_pixel_um is not None:
return float(pixel_factor) * float(sensor_pixel_um)
return None
except Exception:
return None


def create_experiment_dir(base_path: str, experiment_id: str) -> Tuple[str, str]:
"""Resolve a unique experiment ID and create its output directory.

Appends a timestamp to *experiment_id* (spaces replaced with underscores)
to guarantee uniqueness, then creates the directory tree under *base_path*.

Args:
base_path: Root directory for all experiments.
experiment_id: Human-readable experiment name supplied by the user.

Returns:
A ``(resolved_id, dir_path)`` tuple where *resolved_id* is the
timestamped identifier and *dir_path* is the absolute path of the
newly created directory.
"""
resolved_id = experiment_id.replace(" ", "_") + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f")
dir_path = os.path.join(base_path, resolved_id)
utils.ensure_directory_exists(dir_path)
return resolved_id, dir_path
9 changes: 3 additions & 6 deletions software/control/core/multi_point_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import tempfile
import time
import yaml
from datetime import datetime
from enum import Enum
from threading import Thread
from typing import Optional, Tuple, Any
Expand All @@ -17,6 +16,7 @@
from control import utils, utils_acquisition
import control._def
from control.core.auto_focus_controller import AutoFocusController
from control.core.acquisition_setup import create_experiment_dir
from control.core.multi_point_utils import MultiPointControllerFunctions, ScanPositionInformation, AcquisitionParameters
from control.core.scan_coordinates import ScanCoordinates
from control.core.laser_auto_focus_controller import LaserAutofocusController
Expand Down Expand Up @@ -437,12 +437,9 @@ def set_overlap_percent(self, overlap_percent: float):
self.overlap_percent = overlap_percent

def start_new_experiment(self, experiment_ID): # @@@ to do: change name to prepare_folder_for_new_experiment
# generate unique experiment ID
self.experiment_ID = experiment_ID.replace(" ", "_") + "_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f")
# generate unique experiment ID and create its output directory
self.experiment_ID, experiment_dir = create_experiment_dir(self.base_path, experiment_ID)
self.recording_start_time = time.time()
# create a new folder
experiment_dir = os.path.join(self.base_path, self.experiment_ID)
utils.ensure_directory_exists(experiment_dir)
# Save acquisition configuration via ConfigRepository
self.liveController.microscope.config_repo.save_acquisition_output(
output_dir=experiment_dir,
Expand Down
Loading
Loading