Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
594 changes: 569 additions & 25 deletions dlclivegui/cameras/backends/basler_backend.py

Large diffs are not rendered by default.

170 changes: 157 additions & 13 deletions dlclivegui/cameras/backends/gentl_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,15 @@ def open(self) -> None:

self._acquirer.start()

try:
self._read_telemetry(node_map)
self._debug_frame_rate_nodes(node_map, context="after starting acquisition")
except Exception:
LOG.warning(
"Failed to read telemetry after starting acquisition; some 'actual' values may be missing.",
exc_info=True,
)

LOG.debug(
"Opened GenTL camera index=%s serial=%s label=%s",
selected_index,
Expand Down Expand Up @@ -1126,6 +1135,48 @@ def _node_symbolics(node) -> list[str]:
except Exception:
return []

@staticmethod
def _node_value(node_map, name: str, default=None):
"""Best-effort read of a GenICam node value."""
try:
node = getattr(node_map, name)
except Exception:
return default

try:
return node.value
except Exception:
return default

@classmethod
def _node_float(cls, node_map, *names: str, allow_zero: bool = False) -> float | None:
"""Return the first positive float value from a list of GenICam node names."""
for name in names:
value = cls._node_value(node_map, name, None)
try:
fvalue = float(value)
except Exception:
continue

if fvalue > 0 or (allow_zero and fvalue == 0):
return fvalue

return None

@classmethod
def _node_str(cls, node_map, *names: str) -> str | None:
"""Return the first non-empty string value from a list of GenICam node names."""
for name in names:
value = cls._node_value(node_map, name, None)
if value is None:
continue

text = str(value).strip()
if text:
return text

return None

def _set_enum_node(self, node_map, name: str, value: str, *, strict: bool = False) -> bool:
node = self._node(node_map, name)
if node is None:
Expand Down Expand Up @@ -1605,21 +1656,48 @@ def _configure_frame_rate(self, node_map) -> None:
return

target = float(self.settings.fps)
LOG.info("Configuring GenTL frame rate: requested %.3f FPS", target)

for attr in ("AcquisitionFrameRateEnable", "AcquisitionFrameRateControlEnable"):
try:
getattr(node_map, attr).value = True
node = getattr(node_map, attr)
before = getattr(node, "value", None)
node.value = True
after = getattr(node, "value", None)
LOG.info("Enabled GenTL %s: before=%r after=%r", attr, before, after)
break
except Exception:
pass

for attr in ("AcquisitionFrameRate", "ResultingFrameRate", "AcquisitionFrameRateAbs"):
for attr in ("AcquisitionFrameRate", "AcquisitionFrameRateAbs"):
try:
getattr(node_map, attr).value = target
node = getattr(node_map, attr)
before = getattr(node, "value", None)
node.value = target
after = getattr(node, "value", None)

LOG.info(
"Set GenTL %s: before=%r requested=%.3f after=%r",
attr,
before,
target,
after,
)

try:
accepted = float(after)
if accepted > 0:
self._actual_fps = accepted
except Exception:
pass

return

except AttributeError:
continue
except Exception as e:
LOG.warning("Failed to set frame rate via %s: %s", attr, e)

LOG.warning("Could not set frame rate to %s FPS", target)

def _read_telemetry(self, node_map) -> None:
Expand All @@ -1629,20 +1707,86 @@ def _read_telemetry(self, node_map) -> None:
except Exception:
pass

try:
self._actual_fps = float(node_map.ResultingFrameRate.value)
except Exception:
self._actual_fps = None
# Prefer true/resulting frame-rate readback nodes.
resulting_fps = self._node_float(
node_map,
"AcquisitionResultingFrameRate",
"ResultingFrameRate",
"AcquisitionFrameRateResulting",
"DeviceFrameRate",
)

try:
self._actual_exposure = float(node_map.ExposureTime.value)
except Exception:
self._actual_exposure = None
# Fallback to requested/accepted frame-rate nodes only if no resulting node exists.
requested_fps = self._node_float(
node_map,
"AcquisitionFrameRate",
"AcquisitionFrameRateAbs",
)

if resulting_fps is not None:
self._actual_fps = resulting_fps
elif requested_fps is not None:
self._actual_fps = requested_fps

exposure = self._node_float(
node_map,
"ExposureTime",
"ExposureTimeAbs",
"Exposure",
allow_zero=True,
)
if exposure is not None:
self._actual_exposure = exposure

gain = self._node_float(
node_map,
"Gain",
"GainRaw",
allow_zero=True,
)
if gain is not None:
self._actual_gain = gain

# Persist useful telemetry into properties["gentl"] for GUI/debugging.
try:
self._actual_gain = float(node_map.Gain.value)
ns = self._ensure_settings_ns()

if self._actual_width and self._actual_height:
ns["actual_resolution"] = [int(self._actual_width), int(self._actual_height)]

if self._actual_fps is not None:
ns["actual_fps"] = float(self._actual_fps)

if resulting_fps is not None:
ns["actual_resulting_frame_rate"] = float(resulting_fps)

if requested_fps is not None:
ns["actual_acquisition_frame_rate"] = float(requested_fps)

if self._actual_exposure is not None:
ns["actual_exposure"] = float(self._actual_exposure)

if self._actual_gain is not None:
ns["actual_gain"] = float(self._actual_gain)

exposure_auto = self._node_str(node_map, "ExposureAuto")
if exposure_auto is not None:
ns["actual_exposure_auto"] = exposure_auto

throughput = self._node_float(node_map, "DeviceLinkThroughputLimit", allow_zero=True)
if throughput is not None:
ns["actual_device_link_throughput_limit"] = float(throughput)

throughput_mode = self._node_str(node_map, "DeviceLinkThroughputLimitMode")
if throughput_mode is not None:
ns["actual_device_link_throughput_limit_mode"] = throughput_mode

pixel_format = self._node_str(node_map, "PixelFormat")
if pixel_format is not None:
ns["actual_pixel_format"] = pixel_format

except Exception:
self._actual_gain = None
pass

# ------------------------------------------------------------------
# Frame conversion / local helpers
Expand Down
12 changes: 10 additions & 2 deletions dlclivegui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@
TriggerStrobePolarity = Literal["ActiveHigh", "ActiveLow"]
TriggerStrobeOperation = Literal["Exposure", "FixedDuration"]

SINGLE_CAMERA_WORKER_DO_LOG_TIMING = False
MULTI_CAMERA_WORKER_DO_LOG_TIMING = True
# Global settings
## GUI
GUI_MAX_DISPLAY_FPS: float = 30.0


## Debug
### Timing logs
SINGLE_CAMERA_WORKER_DO_LOG_TIMING: bool = True
MULTI_CAMERA_WORKER_DO_LOG_TIMING: bool = True
Comment on lines +25 to +28
# MAIN_WINDOW_DO_LOG_TIMING: bool = False


class CameraSettings(BaseModel):
Expand Down
6 changes: 3 additions & 3 deletions dlclivegui/gui/camera_config/camera_config_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,16 +820,16 @@ def _on_active_camera_selected(self, row: int) -> None:

def _ensure_default_trigger_config(self, cam: CameraSettings) -> None:
backend = (cam.backend or "").lower()
if backend != "gentl":
if backend not in {"gentl", "basler"}:
return

if not isinstance(cam.properties, dict):
cam.properties = {}

ns = cam.properties.setdefault("gentl", {})
ns = cam.properties.setdefault(backend, {})
if not isinstance(ns, dict):
ns = {}
cam.properties["gentl"] = ns
cam.properties[backend] = ns

ns.setdefault("trigger", CameraTriggerSettings().model_dump(exclude_none=True))

Expand Down
Loading