diff --git a/labscript_devices/GeniCam/__init__.py b/labscript_devices/GeniCam/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/labscript_devices/GeniCam/_genicam/_feature_tree.py b/labscript_devices/GeniCam/_genicam/_feature_tree.py
new file mode 100644
index 0000000..29f1089
--- /dev/null
+++ b/labscript_devices/GeniCam/_genicam/_feature_tree.py
@@ -0,0 +1,171 @@
+from genicam.genapi import NodeMap
+from genicam.genapi import EInterfaceType, EAccessMode, EVisibility
+
+from ._feature_value_tuple import (FeatureValueTuple, FeatureType,
+ FeatureAccessMode, FeatureVisibility)
+
+from ._tree import TreeNode
+
+_readable_nodes = [
+ EInterfaceType.intfIBoolean,
+ EInterfaceType.intfIEnumeration,
+ EInterfaceType.intfIFloat,
+ EInterfaceType.intfIInteger,
+ EInterfaceType.intfIString,
+ EInterfaceType.intfIRegister,
+]
+
+_readable_access_modes = [EAccessMode.RW, EAccessMode.RO]
+
+
+def populate_feature_value_tuple(feature):
+ interface_type = feature.node.principal_interface_type
+ value = None
+ if interface_type in [EInterfaceType.intfIBoolean,
+ EInterfaceType.intfIFloat,
+ EInterfaceType.intfIInteger]:
+ value = feature.value
+ else:
+ try:
+ value = str(feature.value)
+ except AttributeError:
+ try:
+ value = feature.to_string()
+ except AttributeError:
+ return None
+
+ visibility = feature.node.visibility
+ access_mode = feature.node.get_access_mode()
+ entries = None
+ if interface_type == EInterfaceType.intfIEnumeration:
+ entries = [ item.symbolic for item in feature.entries ]
+
+ return FeatureValueTuple(
+ name=feature.node.display_name,
+ value=value,
+ type=FeatureType(int(interface_type)),
+ entries=entries,
+ access_mode=FeatureAccessMode(int(access_mode)),
+ visibility=FeatureVisibility(int(visibility))
+ )
+
+
+class GeniCamFeatureTreeNode(TreeNode):
+ def __init__(self, name, parent_node=None, data=None, child_nodes=None):
+ super().__init__(name, parent_node, data, child_nodes)
+
+ @property
+ def feature(self):
+ return self.data
+
+ @classmethod
+ def get_tree_from_genicam_root_node(cls, root_node):
+ root = cls("Root", child_nodes={})
+ features = root_node.features
+
+ cls.populate_feature_tree(features, root)
+
+ return root
+
+ @classmethod
+ def populate_feature_tree(cls, features, parent_item):
+ for feature in features:
+ interface_type = feature.node.principal_interface_type
+
+ if interface_type == EInterfaceType.intfICategory:
+ item = cls(feature.node.display_name, parent_node=parent_item, child_nodes={})
+ cls.populate_feature_tree(feature.features, item)
+ else:
+ item = cls(feature.node.display_name, parent_node=parent_item, data=feature)
+
+ parent_item.add_child(item)
+
+ @staticmethod
+ def _eval_feature(feature):
+ value_tuple = populate_feature_value_tuple(feature)
+
+ return value_tuple
+
+ def __getitem__(self, k):
+ if self.child_nodes:
+ return self.child_nodes[k]
+ else:
+ raise KeyError(f"Node `{self.name}` is a data node that has no children.")
+
+ def set_attributes(self, attr_dict, set_if_changed=False):
+ return self._set_attributes(attr_dict, [], set_if_changed)
+
+ def _set_attributes(self, attr_dict, parent_paths, set_if_changed=False):
+ for k in attr_dict.keys():
+ v = attr_dict[k]
+ if isinstance(v, dict):
+ attr_dict[k] = self._set_attributes(v, parent_paths + [k])
+ else:
+ feature = self.access(parent_paths + [k]).data
+
+ interface_type = feature.node.principal_interface_type
+
+ try:
+ if interface_type == EInterfaceType.intfICommand:
+ if v:
+ feature.execute()
+ elif interface_type == EInterfaceType.intfIBoolean:
+ _v = True if v.lower() == 'true' else False
+ if not set_if_changed or feature.value != _v: # what's the point of doing this? should I cache?
+ feature.value = _v
+ elif interface_type == EInterfaceType.intfIFloat:
+ _v = float(v)
+ if not set_if_changed or feature.value != _v:
+ feature.value = _v
+ else:
+ if not set_if_changed or feature.value != v:
+ feature.value = v
+
+ except Exception:
+ pass
+
+ if interface_type == EInterfaceType.intfICommand:
+ attr_dict[k] = False
+ else:
+ attr_dict[k] = feature.value
+
+ return attr_dict
+
+ def filter_tree_with_visibility(self, visibility=None, must_writable=False):
+ visibility = EVisibility.Guru if visibility is None else visibility
+
+ if isinstance(visibility, str):
+ _visibility = {
+ "beginner": EVisibility.Beginner,
+ "expert": EVisibility.Expert,
+ "guru": EVisibility.Guru
+ }[visibility.lower()]
+ else:
+ _visibility = visibility
+
+ filtered_tree = GeniCamFeatureTreeNode.filter_tree(
+ self,
+ lambda n: n if (int(n.node.visibility) <= int(_visibility) and ((n.node.get_access_mode() == EAccessMode.RW) \
+ if must_writable else (n.node.get_access_mode() in [EAccessMode.RO, EAccessMode.RW]))) else None
+ )
+
+ return filtered_tree
+
+ def dump_value_dict(self, visibility=None, writable=False):
+ filtered_tree = self.filter_tree_with_visibility(visibility, writable)
+
+ if filtered_tree:
+ value_tree = TreeNode.eval_tree(filtered_tree,
+ lambda node: self._eval_feature(node).value)
+ return value_tree.dump_value_dict()
+ else:
+ return {}
+
+ def dump_value_tuple_dict(self, visibility=None, writable=False):
+ filtered_tree = self.filter_tree_with_visibility(visibility, writable)
+
+ if filtered_tree:
+ value_tree = TreeNode.eval_tree(filtered_tree, self._eval_feature)
+ return value_tree.dump_value_dict()
+ else:
+ return {}
diff --git a/labscript_devices/GeniCam/_genicam/_feature_value_tuple.py b/labscript_devices/GeniCam/_genicam/_feature_value_tuple.py
new file mode 100644
index 0000000..403cd90
--- /dev/null
+++ b/labscript_devices/GeniCam/_genicam/_feature_value_tuple.py
@@ -0,0 +1,39 @@
+from collections import namedtuple
+from enum import Enum
+
+
+# The purpose of provising these homemade types instead of using Harvester is to
+# not forcing the installation of Harvester on BLACS computer simply because I need
+# a few Enum types (only the worker computer needs Harvester)
+
+FeatureValueTuple = namedtuple("FeatureValueTuple", ["name", "value", "type", "entries",
+ "access_mode", "visibility"])
+
+
+class FeatureType(Enum):
+ Value = 0
+ Base = 1
+ Integer = 2
+ Boolean = 3
+ Command = 4
+ Float = 5
+ String = 6
+ Register = 7
+ Category = 8
+ Enumeration = 9
+ EnumEntry = 10
+ Port = 11
+
+class FeatureAccessMode(Enum):
+ NI = 0
+ NA = 1
+ WO = 2
+ RO = 3
+ RW = 4
+
+class FeatureVisibility(Enum):
+ Beginner = 0
+ Expert = 1
+ Guru = 2
+ Invisible = 3
+
diff --git a/labscript_devices/GeniCam/_genicam/_genicam.py b/labscript_devices/GeniCam/_genicam/_genicam.py
new file mode 100644
index 0000000..bc9e583
--- /dev/null
+++ b/labscript_devices/GeniCam/_genicam/_genicam.py
@@ -0,0 +1,151 @@
+from harvesters.core import Harvester
+from genicam.genapi import NodeMap
+from genicam.genapi import EInterfaceType, EAccessMode, EVisibility
+from genicam.gentl import TimeoutException, GenericException
+
+import time
+import logging
+import numpy as np
+
+from ._feature_tree import GeniCamFeatureTreeNode
+
+
+def nested_get(dct, keys):
+ for key in keys:
+ dct = dct[key]
+ return dct
+
+
+class GeniCamException(Exception):
+ pass
+
+
+class GeniCam:
+ _readable_nodes = [
+ EInterfaceType.intfIBoolean,
+ EInterfaceType.intfIEnumeration,
+ EInterfaceType.intfIFloat,
+ EInterfaceType.intfIInteger,
+ EInterfaceType.intfIString,
+ EInterfaceType.intfIRegister,
+ ]
+
+ _readable_access_modes = [EAccessMode.RW, EAccessMode.RO]
+
+ def __init__(self, serial_number, cti_path, logger=None):
+ self.logger = logger if logger else logging.getLogger()
+
+ self.raise_exception_on_failed_shot = False
+ self._image_fetch_polling_interval = 0.01 ## c.f. harvesters/core.py, `_timeout_on_client_fetch_call`
+
+ self.harvester = Harvester()
+
+ self.harvester.add_file(cti_path, check_validity=True)
+ self.harvester.update()
+
+ self.ia = None
+
+ try:
+ self.ia = self.harvester.create({'serial_number': serial_number})
+ except IndexError:
+ logging.error(f"Couldn't not find camera with serial number {serial_number}. List of available cameras:")
+ logging.error(self.harvester.device_info_list)
+
+ raise GeniCamException(f"Couldn't not find camera with serial number {serial_number}.")
+
+ self.feature_tree = GeniCamFeatureTreeNode.get_tree_from_genicam_root_node(
+ self.ia.remote_device.node_map.Root)
+
+ self._abort_acquisition = False
+
+ def snap(self, timeout):
+ mode_feat = self.feature_tree["Acquisition"]["AcquisitionMode"].data
+ old_mode = mode_feat.value
+ mode_feat.value = "SingleFrame"
+
+ self.ia.start()
+ img = self.fetch(timeout=timeout)
+ self.ia.stop()
+
+ mode_feat.value = old_mode
+
+ if img is None:
+ raise Exception("Acqusition timeout.")
+
+ return img
+
+ def fetch(self, timeout: float =0, raise_when_timeout=False):
+ try:
+ with self.ia.fetch(timeout=timeout) as buffer:
+ component = buffer.payload.components[0]
+ data = np.copy(component.data)
+ _2d = data.reshape(component.height, component.width)
+
+ return _2d
+ except TimeoutException as e:
+ if raise_when_timeout:
+ raise e
+ return None
+
+ def fetch_n_images(self, n_images, callback=None, timeout=0):
+ self._abort_acquisition = False
+
+ images = []
+
+ base = time.time()
+
+ poll_timeout = 100e-6 # polling every 100us
+
+ self.logger.debug(f"Polling for {n_images} images...")
+ for i in range(n_images):
+ while True:
+ elapsed = time.time() - base
+ if self._abort_acquisition:
+ self.logger.info("Received abort signal during acquisition.")
+ self._abort_acquisition = False
+ if callback:
+ callback(images)
+
+ return images
+
+ if timeout and elapsed > timeout:
+ raise Exception(f"Acqusition timeout while waiting for the {i+1}/{n_images} image.")
+
+ try:
+ img = self.fetch(poll_timeout, True)
+ break
+ except TimeoutException:
+ continue
+ except GenericException as e:
+ if self.raise_exception_on_failed_shot:
+ raise e
+ else:
+ self.logger.error(f"Error when acquiring image {i+1}/{n_images}:")
+ self.logger.exception(e)
+
+ if img is not None:
+ images.append(img)
+ self.logger.debug(f"Received image {i+1}/{n_images}.")
+
+ self.logger.debug(f"Received all {n_images} images.")
+
+ if callback:
+ callback(images)
+
+ return images
+
+ def stop_acquisition(self):
+ self.ia.stop()
+
+ def start_acquisition(self):
+ self._abort_acquisition = False
+ self.ia.start()
+
+ def abort_acquisition(self):
+ self._abort_acquisition = True
+ self.ia.stop()
+
+ def close(self):
+ if self.ia:
+ self.ia.destroy()
+
diff --git a/labscript_devices/GeniCam/_genicam/_tree.py b/labscript_devices/GeniCam/_genicam/_tree.py
new file mode 100644
index 0000000..e4e88ab
--- /dev/null
+++ b/labscript_devices/GeniCam/_genicam/_tree.py
@@ -0,0 +1,141 @@
+import textwrap
+from collections import OrderedDict
+
+
+class TreeNode:
+ def __init__(self, name, parent_node=None, data=None, child_nodes=None):
+ self.parent_node = parent_node
+ self.name = name
+ self.data = data
+ self.child_nodes = child_nodes # is a dict
+
+ assert (data is None or child_nodes is None), \
+ "A tree node can't has both data and children."
+
+ @classmethod
+ def _get_new_child_container(cls):
+ return {}
+
+ def _get_child_nodes_iterable(self):
+ return self.child_nodes.values()
+
+ def __getitem__(self, k):
+ if self.child_nodes:
+ return self.child_nodes[k]
+ else:
+ raise KeyError(f"Node `{self.name}` is a data node that has no children.")
+
+ def access(self, keys):
+ node = self
+ for key in keys:
+ node = node[key]
+ return node
+
+ def get_path_to_node(self):
+ if self.parent_node:
+ return self.parent_node.get_path_to_node() + [self.name]
+ else:
+ return []
+
+ def print(self, repr_func=None, indent=0):
+ if self.data:
+ if repr_func:
+ print(textwrap.indent(f"[{self.name}]", ' '*indent), repr_func(self.data))
+ else:
+ print(textwrap.indent(f"[{self.name}]", ' '*indent), self.data)
+ else:
+ print(textwrap.indent(f"[{self.name}]", ' '*indent))
+ for child in self._get_child_nodes_iterable():
+ child.print(repr_func, indent+1)
+
+ @classmethod
+ def eval_tree(cls, tree, eval_func, parent_node=None):
+ if tree.data:
+ data = eval_func(tree.data)
+ assert data is not None
+ return cls(tree.name, parent_node, data=data)
+ else:
+ node = cls(tree.name, parent_node=parent_node, data=None, child_nodes=cls._get_new_child_container())
+ for child in tree._get_child_nodes_iterable():
+ node.add_child(cls.eval_tree(child, eval_func, node))
+
+ return node
+
+ @classmethod
+ def filter_tree(cls, tree, filter_func, parent_node=None):
+ if tree.data is not None:
+ data = filter_func(tree.data)
+ if data is not None:
+ return TreeNode(tree.name, parent_node, data=filter_func(tree.data))
+ else:
+ return None
+ else:
+ node = cls(tree.name, parent_node=parent_node, data=None, child_nodes=cls._get_new_child_container())
+ for child in tree._get_child_nodes_iterable():
+ ret = cls.filter_tree(child, filter_func, node)
+ if ret:
+ node.add_child(cls.filter_tree(child, filter_func, node))
+
+ if len(node.child_nodes):
+ return node
+ else:
+ return None
+
+ def dump_value_dict(self, dump_func=None):
+ if self.data is not None:
+ if dump_func:
+ return dump_func(self.data)
+ else:
+ return self.data
+ else:
+ child_nodes = {}
+ if self.child_nodes:
+ for child in self._get_child_nodes_iterable():
+ value = child.dump_value_dict(dump_func)
+ if value is not None:
+ child_nodes[child.name] = value
+
+ return child_nodes
+
+ def add_child(self, child):
+ if self.data:
+ raise KeyError(f"Node `{self.name}` is a data node.")
+
+ self.child_nodes[child.name] = child
+
+
+class OrderedTreeNode(TreeNode):
+
+ @classmethod
+ def _get_new_child_container(cls):
+ return []
+
+ def _get_child_nodes_iterable(self):
+ return self.child_nodes
+
+ def add_child(self, child):
+ if self.data:
+ raise KeyError(f"Node `{self.name}` is a data node.")
+
+ self.child_nodes.append(child)
+
+ def __getitem__(self, k):
+ if self.child_nodes:
+ ret = list(filter(lambda node: node.name == k, self.child_nodes))
+ assert len(ret), f"Item `{k}` doesn't exist."
+ assert len(ret) == 1, f"More than one item `{k}` was found."
+
+ return ret[0]
+ else:
+ raise KeyError(f"Node `{self.name}` is a data node that has no children.")
+
+ def print(self, repr_func=None, indent=0):
+ if self.data:
+ if repr_func:
+ print(textwrap.indent(f"[{self.name}]", ' '*indent), repr_func(self.data))
+ else:
+ print(textwrap.indent(f"[{self.name}]", ' '*indent), self.data)
+ else:
+ print(textwrap.indent(f"[{self.name}]", ' '*indent))
+ for child in self.child_nodes:
+ child.print(repr_func, indent+1)
diff --git a/labscript_devices/GeniCam/attributes_dialog.ui b/labscript_devices/GeniCam/attributes_dialog.ui
new file mode 100644
index 0000000..5eb62af
--- /dev/null
+++ b/labscript_devices/GeniCam/attributes_dialog.ui
@@ -0,0 +1,178 @@
+
+
+ attributeTreeViewDialog
+
+
+
+ 0
+ 0
+ 674
+ 744
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 674
+ 744
+
+
+
+ Form
+
+
+
+ :/qtutils/fugue/table-import.png:/qtutils/fugue/table-import.png
+
+
+ -
+
+
+ These are the current attribute tree of the camera. Click "Copy yo clipboard" to copy the python dictionary representation of the current attributes.
+
+
+ true
+
+
+
+ -
+
+
+ 6
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Visibility level
+
+
+
+ -
+
+
+ false
+
+
+ Beginner
+
+
-
+
+ Beginner
+
+
+
+ :/qtutils/fugue/table.png:/qtutils/fugue/table.png
+
+
+ -
+
+ Expert
+
+
+
+ :/qtutils/fugue/tables.png:/qtutils/fugue/tables.png
+
+
+ -
+
+ Guru
+
+
+
+ :/qtutils/fugue/tables-stacks.png:/qtutils/fugue/tables-stacks.png
+
+
+
+
+
+
+ -
+
+
+ 8
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ 10
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Copy to clipboard
+
+
+
+ :/qtutils/fugue/clipboard--arrow.png:/qtutils/fugue/clipboard--arrow.png
+
+
+
+ -
+
+
+ Close
+
+
+
+ ..
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/labscript_devices/GeniCam/blacs_tab.ui b/labscript_devices/GeniCam/blacs_tab.ui
new file mode 100644
index 0000000..a942df4
--- /dev/null
+++ b/labscript_devices/GeniCam/blacs_tab.ui
@@ -0,0 +1,168 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 846
+ 659
+
+
+
+ Form
+
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+
+ 6
+
+ -
+
+
-
+
+
-
+
+
+ View IMAQdx attributes
+
+
+ Attributes
+
+
+
+ :/qtutils/fugue/table-export.png:/qtutils/fugue/table-export.png
+
+
+
+ -
+
+
+ Take a single image
+
+
+ Snap
+
+
+
+ :/qtutils/fugue/control-stop.png:/qtutils/fugue/control-stop.png
+
+
+
+ -
+
+
+ Acquire images continuously
+
+
+ Continuous
+
+
+
+ :/qtutils/fugue/control.png:/qtutils/fugue/control.png
+
+
+
+ -
+
+
+ Stop acquiring images
+
+
+ Stop
+
+
+
+ :/qtutils/fugue/control-stop-square.png:/qtutils/fugue/control-stop-square.png
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+ <html><head/><body><p>Maximum frame rate in continuous acquisition mode. Use this to reduce load on the computer during continuous acquisition,otherwise frames are acquired as quickly as possible, which may not be desirable on slower computers.</p></body></html>
+
+
+ No max rate
+
+
+
+
+
+ fps
+
+
+ 1
+
+
+ 9999.000000000000000
+
+
+ 0.000000000000000
+
+
+
+ -
+
+
+ No max rate
+
+
+ ...
+
+
+
+ :/qtutils/fugue/arrow-turn-180-left.png:/qtutils/fugue/arrow-turn-180-left.png
+
+
+
+
+
+ -
+
+
+ TextLabel
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/labscript_devices/GeniCam/blacs_tabs.py b/labscript_devices/GeniCam/blacs_tabs.py
new file mode 100644
index 0000000..46f4274
--- /dev/null
+++ b/labscript_devices/GeniCam/blacs_tabs.py
@@ -0,0 +1,202 @@
+import os
+import json
+import time
+import ast
+import zmq
+import threading
+
+import labscript_utils.h5_lock
+import h5py
+
+import numpy as np
+
+import qtutils.icons
+from qtutils.qt import QtWidgets, QtGui, QtCore
+
+from blacs.tab_base_classes import define_state, MODE_MANUAL
+from blacs.device_base_class import DeviceTab
+
+import labscript_utils.properties
+
+from .genicam_widget import GeniCamWidget
+
+
+
+def exp_av(av_old, data_new, dt, tau):
+ """Compute the new value of an exponential moving average based on the previous
+ average av_old, a new value data_new, a time interval dt and an averaging timescale
+ tau. Returns data_new if dt > tau"""
+ if dt > tau:
+ return data_new
+ k = dt / tau
+ return k * data_new + (1 - k) * av_old
+
+
+class GeniCamTab(DeviceTab):
+ # Subclasses may override this if all they do is replace the worker class with a
+ # different one:
+ worker_class = 'labscript_devices.GeniCam.blacs_workers.GeniCamWorker'
+ # Subclasses may override this to False if camera attributes should be set every
+ # shot even if the same values have previously been set:
+ use_smart_programming = True
+
+ def initialise_GUI(self):
+ layout = self.get_tab_layout()
+
+ self.widget = GeniCamWidget(layout.parentWidget(), self.device_name)
+
+ layout.addWidget(self.widget)
+
+ self.acquiring = False
+
+ self.supports_smart_programming(self.use_smart_programming)
+
+ # image receive params
+ self.last_frame_time = None
+ self.frame_rate = None
+
+ self.widget.on_continuous_requested.connect(self.start_continuous)
+ self.widget.on_stop_requested.connect(self.stop_continuous)
+ self.widget.on_continuous_max_change_requested.connect(self.update_max_fps)
+ self.widget.on_snap_requested.connect(self.snap)
+ self.widget.on_show_attribute_tree_requested.connect(self.get_and_show_attribute_tree)
+ self.widget.on_change_attribute_requested.connect(self.set_attributes)
+
+ self.zmq_ctx = zmq.Context()
+ self.image_socket = self.zmq_ctx.socket(zmq.REP)
+ self.image_socket_port = self.image_socket.bind_to_random_port('tcp://*', min_port=50000, max_port=59999, max_tries=100)
+
+ self.image_recv_thread = threading.Thread(target=self.image_recv_handler, daemon=True)
+ self.image_recv_thread.start()
+
+ def get_save_data(self):
+ return {
+ # TODO 'attribute_visibility': self.attributes_dialog.visibilityComboBox.currentText(),
+ 'acquiring': self.acquiring,
+ 'max_rate': self.widget.max_fps,
+ 'colormap': repr(self.widget.colormap)
+ }
+
+ def restore_save_data(self, save_data):
+ # TODO
+ # self.attributes_dialog.visibilityComboBox.setCurrentText(
+ # save_data.get('attribute_visibility', 'Beginner')
+ # )
+ self.widget.request_update_max_fps.emit(save_data.get('max_rate', 0))
+
+ if 'colormap' in save_data:
+ self.widget.request_update_colormap.emit(ast.literal_eval(save_data['colormap']))
+
+ if save_data.get('acquiring', False):
+ # Begin acquisition
+ self.start_continuous()
+
+ def initialise_workers(self):
+ table = self.settings['connection_table']
+ connection_table_properties = table.find_by_name(self.device_name).properties
+ # The device properties can vary on a shot-by-shot basis, but at startup we will
+ # initially set the values that are configured in the connection table, so they
+ # can be used for manual mode acquisition:
+ with h5py.File(table.filepath, 'r') as f:
+ device_properties = labscript_utils.properties.get(
+ f, self.device_name, "device_properties"
+ )
+
+ worker_initialisation_kwargs = {
+ 'cti_file': connection_table_properties['cti_file'],
+ 'serial_number': connection_table_properties['serial_number'],
+ 'camera_attributes': device_properties['camera_attributes'],
+ 'manual_mode_camera_attributes': connection_table_properties[
+ 'manual_mode_camera_attributes'
+ ],
+ 'image_receiver_port': self.image_socket_port,
+ }
+ self.create_worker(
+ 'main_worker', self.worker_class, worker_initialisation_kwargs
+ )
+ self.primary_worker = "main_worker"
+
+ @define_state(MODE_MANUAL, queue_state_indefinitely=True, delete_stale_states=True)
+ def snap(self):
+ yield (self.queue_work(self.primary_worker, 'snap'))
+
+ def update_max_fps(self):
+ if self.acquiring:
+ self.stop_continuous()
+ self.start_continuous()
+
+ @define_state(MODE_MANUAL, queue_state_indefinitely=True, delete_stale_states=True)
+ def start_continuous(self):
+ self.acquiring = True
+ max_fps = self.widget.max_fps
+ dt = 1 / max_fps if max_fps else 0
+ yield (self.queue_work(self.primary_worker, 'start_continuous', dt))
+ self.widget.request_enter_continuous_mode.emit()
+
+ @define_state(MODE_MANUAL, queue_state_indefinitely=True, delete_stale_states=True)
+ def stop_continuous(self):
+ yield (self.queue_work(self.primary_worker, 'stop_continuous'))
+ self.acquiring = False
+ self.widget.request_exit_continuous_mode.emit()
+
+ @define_state(MODE_MANUAL, queue_state_indefinitely=True, delete_stale_states=True)
+ def get_and_show_attribute_tree(self):
+ attr_tree = yield (self.queue_work(self.primary_worker, 'get_attribute_tuples_as_dict'))
+ self.widget.request_show_attribute_tree.emit(attr_tree)
+
+ @define_state(MODE_MANUAL, queue_state_indefinitely=True, delete_stale_states=True)
+ def set_attributes(self, attr_dict):
+ attr_dict = yield (self.queue_work(self.primary_worker, 'set_attributes', attr_dict))
+ self.widget.request_update_attribute_tree.emit(attr_dict)
+
+ def restart(self, *args, **kwargs):
+ # Must manually stop the receiving server upon tab restart, otherwise it does
+ # not get cleaned up:
+ # if self.image_socket:
+ # self.image_socket.close()
+ return DeviceTab.restart(self, *args, **kwargs)
+
+ def update_control(self):
+ if self.mode == 2:
+ # Transitioning to buffered
+ # TODO
+ pass
+ elif self.mode == 4:
+ # Transitioning to manual
+ # TODO
+ pass
+
+ def image_recv_handler(self):
+ while True:
+ # Acknowledge immediately so that the worker process can begin acquiring the
+ # next frame. This increases the possible frame rate since we may render a frame
+ # whilst acquiring the next, but does not allow us to accumulate a backlog since
+ # only one call to this method may occur at a time.
+ md_json, image_data = self.image_socket.recv_multipart()
+
+ self.image_socket.send(b'ok')
+
+ md = json.loads(md_json)
+ image = np.frombuffer(memoryview(image_data), dtype=md['dtype'])
+
+ image = image.reshape(md['shape'])
+ if len(image.shape) == 3 and image.shape[0] == 1:
+ # If only one image given as a 3D array, convert to 2D array:
+ image = image.reshape(image.shape[1:])
+
+ this_frame_time = time.time()
+ if self.last_frame_time is not None:
+ dt = this_frame_time - self.last_frame_time
+ if self.frame_rate is not None:
+ # Exponential moving average of the frame rate over 1 second:
+ self.frame_rate = exp_av(self.frame_rate, 1 / dt, dt, 1.0)
+ else:
+ self.frame_rate = 1 / dt
+
+ self.last_frame_time = this_frame_time
+
+ self.widget.request_update_image.emit(image)
+
+ # Update fps indicator:
+ if self.frame_rate is not None:
+ self.widget.request_update_fps.emit(self.frame_rate)
diff --git a/labscript_devices/GeniCam/blacs_workers.py b/labscript_devices/GeniCam/blacs_workers.py
new file mode 100644
index 0000000..850bd7c
--- /dev/null
+++ b/labscript_devices/GeniCam/blacs_workers.py
@@ -0,0 +1,384 @@
+#####################################################################
+# #
+# /labscript_devices/IMAQdxCamera/blacs_workers.py #
+# #
+# Copyright 2019, Monash University and contributors #
+# #
+# This file is part of labscript_devices, in the labscript suite #
+# (see http://labscriptsuite.org), and is licensed under the #
+# Simplified BSD License. See the license.txt file in the root of #
+# the project for the full license. #
+# #
+#####################################################################
+
+
+import sys
+import time
+import threading
+import numpy as np
+import labscript_utils.h5_lock
+import h5py
+import labscript_utils.properties
+import zmq
+import time
+import json
+
+from zprocess import RichStreamHandler
+
+from blacs.tab_base_classes import Worker
+
+from labscript_utils.shared_drive import path_to_local
+import labscript_utils.properties
+
+from ._genicam._genicam import GeniCam
+
+import logging
+
+
+class GeniCamWorker(Worker):
+ # Parameters passing down from BLACS
+ # - parent_host
+ # - serial_number
+ # - cti_file
+ # - manual_acquisition_timeout
+ # - manual_mode_camera_attributes
+ # - camera_attributes
+ # - image_receiver_port
+
+ # Camera properties saved in h5 file
+ # - stop_acquisition_timeout
+ # - exception_on_failed_shot
+ # - saved_attribute_visibility_level
+
+ def init(self):
+ self.logger = logging.getLogger("BLACS_GeniCam")
+ self.logger.setLevel(logging.DEBUG)
+ self.logger.addHandler(RichStreamHandler())
+
+ self.camera = GeniCam(self.serial_number, self.cti_file, self.logger)
+
+ self.logger.info("Setting attributes...")
+ self.set_attributes(self.camera_attributes)
+ self.set_attributes(self.manual_mode_camera_attributes, set_if_changed=True)
+
+ self.default_continuous_polling_interval = 0.01
+
+ self.images = None
+ self.n_images = None
+ self.attributes_to_save = None
+ self.exposures = None
+ self.acquisition_thread = None
+ self.h5_filepath = None
+
+ self.mode_before_continuous_started = None
+ self.continuous_stop = threading.Event()
+ self.continuous_thread = None
+ self.continuous_dt = None
+
+ self.fetch_image_finished = threading.Event()
+
+ self.zmq_context = zmq.Context()
+
+ self.image_socket = self.zmq_context.socket(zmq.REQ)
+ self.image_socket.connect(
+ f'tcp://{self.parent_host}:{self.image_receiver_port}'
+ )
+
+ def set_attributes(self, attributes, set_if_changed=False):
+ return self.camera.feature_tree.set_attributes(attributes, set_if_changed)
+
+ def get_attributes_as_dict(self, visibility=None):
+ """Return a dict of the attributes of the camera"""
+
+ attributes_dict = self.camera.feature_tree.dump_value_dict(visibility)
+
+ return attributes_dict
+
+ def get_attribute_tuples_as_dict(self):
+ """Return a dict of the FeatureValueTuple of the camera"""
+
+ attributes_dict = self.camera.feature_tree.dump_value_tuple_dict()
+
+ return attributes_dict
+
+ def get_attributes_as_text(self, visibility_level):
+ """Return a string representation of the attributes of the camera for
+ the given visibility level
+
+ visibility_level: one of "beginner", "expert", or "guru"
+ """
+
+ attrs = self.get_attributes_as_dict(visibility_level)
+ dict_repr = json.dumps(attrs, indent=4)
+
+ return self.device_name + '_camera_attributes = ' + dict_repr
+
+ def snap(self):
+ """Acquire one frame in manual mode. Send it to the parent via
+ self.image_socket. Wait for a response from the parent."""
+
+ image = self.camera.snap(self.manual_acquisition_timeout)
+ if image is not None:
+ self._send_image_to_parent(image)
+
+ def _send_image_to_parent(self, image):
+ """Send the image to the GUI to display. This will block if the parent process
+ is lagging behind in displaying frames, in order to avoid a backlog."""
+
+ metadata = dict(dtype=str(image.dtype), shape=image.shape)
+
+ self.image_socket.send_json(metadata, zmq.SNDMORE)
+ self.image_socket.send(image, copy=False)
+
+ response = self.image_socket.recv()
+ assert response == b'ok', response
+
+ def continuous_loop(self, dt):
+ """Acquire continuously in a loop, with minimum repetition interval dt"""
+ t = time.time()
+ while True:
+ t = time.time()
+ image = self.camera.fetch(self.default_continuous_polling_interval)
+
+ if image is not None:
+ self._send_image_to_parent(image)
+
+ if dt is None:
+ timeout = 0
+ else:
+ timeout = t + dt - time.time()
+
+ if self.continuous_stop.wait(timeout):
+ self.continuous_stop.clear()
+ break
+
+ def start_continuous(self, dt):
+ """Begin continuous acquisition in a thread with minimum repetition interval
+ dt"""
+ # TODO: set and store trigger mode
+ assert self.continuous_thread is None
+ # self.mode_before_continuous_started = self.camera.feature_tree["Acquisition"]["AcquisitionMode"].feature.value
+ # self.camera.feature_tree["Acquisition"]["AcquisitionMode"].feature.value = "Continuous"
+
+ self.camera.start_acquisition()
+ self.continuous_thread = threading.Thread(
+ target=self.continuous_loop, args=(dt,), daemon=True
+ )
+ self.continuous_thread.start()
+ self.continuous_dt = dt
+
+ def stop_continuous(self, pause=False):
+ """Stop the continuous acquisition thread"""
+ if not self.continuous_thread:
+ return
+
+ # if self.mode_before_continuous_started:
+ # self.camera.feature_tree["Acquisition"]["AcquisitionMode"].feature.value = self.mode_before_continuous_started
+
+ self.continuous_stop.set()
+ self.continuous_thread.join()
+ self.continuous_thread = None
+ self.camera.stop_acquisition()
+ # If we're just 'pausing', then do not clear self.continuous_dt. That way
+ # continuous acquisition can be resumed with the same interval by calling
+ # start(self.continuous_dt), without having to get the interval from the parent
+ # again, and the fact that self.continuous_dt is not None can be used to infer
+ # that continuous acquisiton is paused and should be resumed after a buffered
+ # run is complete:
+ if not pause:
+ self.continuous_dt = None
+
+ def transition_to_buffered(self, device_name, h5_filepath, initial_values, fresh):
+ if getattr(self, 'is_remote', False):
+ h5_filepath = path_to_local(h5_filepath)
+
+ if self.continuous_thread is not None:
+ # Pause continuous acquistion during transition_to_buffered:
+ self.stop_continuous(pause=True)
+
+ with h5py.File(h5_filepath, 'r') as f:
+ group = f['devices'][self.device_name]
+ if not 'EXPOSURES' in group:
+ return {}
+
+ self.h5_filepath = h5_filepath
+ self.exposures = group['EXPOSURES'][:]
+ self.n_images = len(self.exposures)
+
+ # Get the camera_attributes from the device_properties
+ properties = labscript_utils.properties.get(
+ f, self.device_name, 'device_properties'
+ )
+
+ camera_attributes = properties['camera_attributes']
+ self.stop_acquisition_timeout = properties['stop_acquisition_timeout']
+ self.exception_on_failed_shot = properties['exception_on_failed_shot']
+ saved_attr_level = properties['saved_attribute_visibility_level']
+
+ self.camera.raise_exception_on_failed_shot = self.exception_on_failed_shot
+
+ # Only reprogram attributes that differ from those last programmed in, or all of
+ # them if a fresh reprogramming was requested:
+ self.set_attributes(camera_attributes, set_if_changed=(not fresh))
+
+ # Get the camera attributes, so that we can save them to the H5 file:
+ if saved_attr_level is not None:
+ self.attributes_to_save = self.get_attributes_as_dict(saved_attr_level)
+ else:
+ self.attributes_to_save = None
+
+ self.logger.info(f"Configuring camera for {self.n_images} images.")
+
+ self.fetch_image_finished.clear()
+
+ # self.camera.feature_tree["Acquisition"]["AcquisitionMode"].feature.value = "MultiFrame"
+ self.camera.feature_tree["Acquisition"]["AcquisitionFrameCount"].feature.value = self.n_images
+
+ self.camera.start_acquisition()
+
+ self.acquisition_thread = threading.Thread(
+ target=self.camera.fetch_n_images,
+ args=(self.n_images, self._image_fetch_callback, self.stop_acquisition_timeout),
+ daemon=True,
+ )
+
+ self.acquisition_thread.start()
+ return {}
+
+ def _image_fetch_callback(self, images):
+ self.images = images
+ self.fetch_image_finished.set()
+
+ def transition_to_manual(self):
+ if self.h5_filepath is None:
+ self.logger.info('No camera exposures in this shot.\n')
+ return True
+
+ assert self.acquisition_thread is not None
+
+ if self.fetch_image_finished.wait(self.stop_acquisition_timeout):
+ self.fetch_image_finished.clear()
+ else:
+
+ emsg = ("Acquisition thread did not finish. Likely did not acquire expected"
+ "number of images. Check triggering is connected/configured correctly.")
+
+ if self.exception_on_failed_shot:
+ self.abort()
+ raise RuntimeError(emsg)
+ else:
+ self.camera.abort_acquisition()
+ self.acquisition_thread.join()
+ self.logger.error(emsg)
+
+ self.acquisition_thread = None
+
+ self.logger.debug("Stop acquisition...")
+ self.camera.stop_acquisition()
+
+ self.logger.debug(f"Saving {len(self.images)}/{len(self.exposures)} images...")
+
+ with h5py.File(self.h5_filepath, 'r+') as f:
+ image_path = 'images/' + self.device_name
+ image_group = f.require_group(image_path)
+ image_group.attrs['camera'] = self.device_name
+
+ # Save camera attributes to the HDF5 file:
+ if self.attributes_to_save is not None:
+ labscript_utils.properties.set_attributes(image_group, self.attributes_to_save)
+
+ # Whether we failed to get all the expected exposures:
+ image_group.attrs['failed_shot'] = len(self.images) != len(self.exposures)
+
+ # key the images by name and frametype. Allow for the case of there being
+ # multiple images with the same name and frametype. In this case we will
+ # save an array of images in a single dataset.
+ images = {
+ (exposure['name'], exposure['frametype']): []
+ for exposure in self.exposures
+ }
+
+ # Iterate over expected exposures, sorted by acquisition time, to match them
+ # up with the acquired images:
+ self.exposures.sort(order='t')
+ for image, exposure in zip(self.images, self.exposures):
+ images[(exposure['name'], exposure['frametype'])].append(image)
+
+ # Save images to the HDF5 file:
+ for (name, frametype), imagelist in images.items():
+ data = imagelist[0] if len(imagelist) == 1 else np.array(imagelist)
+ self.logger.debug(f"Saving frame(s) {name}/{frametype}.")
+ group = image_group.require_group(name)
+ dset = group.create_dataset(
+ frametype, data=data.astype('uint16'), dtype='uint16', compression='gzip'
+ )
+ # Specify this dataset should be viewed as an image
+ dset.attrs['CLASS'] = np.string_('IMAGE')
+ dset.attrs['IMAGE_VERSION'] = np.string_('1.2')
+ dset.attrs['IMAGE_SUBCLASS'] = np.string_('IMAGE_GRAYSCALE')
+ dset.attrs['IMAGE_WHITE_IS_ZERO'] = np.uint8(0)
+
+ self.logger.info(f"{len(self.images)} images saved.")
+
+ # If the images are all the same shape, send them to the GUI for display:
+ try:
+ image_block = np.stack(self.images)
+ except ValueError:
+ self.logger.warning("Cannot display images in the GUI, they are not all the same shape")
+ else:
+ self._send_image_to_parent(image_block)
+
+ self.images = None
+ self.n_images = None
+ self.attributes_to_save = None
+ self.exposures = None
+ self.h5_filepath = None
+ self.stop_acquisition_timeout = None
+ self.exception_on_failed_shot = None
+
+ self.logger.info("Setting manual mode camera attributes.\n")
+
+ self.set_attributes(self.manual_mode_camera_attributes, set_if_changed=True)
+ if self.continuous_dt is not None:
+ # If continuous manual mode acquisition was in progress before the bufferd
+ # run, resume it:
+ self.start_continuous(self.continuous_dt)
+ return True
+
+ def abort(self):
+ if self.acquisition_thread is not None:
+ self.camera.abort_acquisition()
+ self.acquisition_thread.join()
+ self.acquisition_thread = None
+ self.camera.stop_acquisition()
+
+ self.camera._abort_acquisition = False
+ self.images = None
+ self.n_images = None
+ self.attributes_to_save = None
+ self.exposures = None
+ self.acquisition_thread = None
+ self.h5_filepath = None
+ self.stop_acquisition_timeout = None
+ self.exception_on_failed_shot = None
+
+ # Resume continuous acquisition, if any:
+ if self.continuous_dt is not None and self.continuous_thread is None:
+ self.start_continuous(self.continuous_dt)
+ return True
+
+ def abort_buffered(self):
+ return self.abort()
+
+ def abort_transition_to_buffered(self):
+ return self.abort()
+
+ def program_manual(self, values):
+ return {}
+
+ def shutdown(self):
+ if self.continuous_thread is not None:
+ self.stop_continuous()
+
+ self.image_socket.close() # forgetting to close the socket will crash blacs when restarting worker
+ self.camera.close()
diff --git a/labscript_devices/GeniCam/genicam_feature_tree_widget.py b/labscript_devices/GeniCam/genicam_feature_tree_widget.py
new file mode 100644
index 0000000..18f5076
--- /dev/null
+++ b/labscript_devices/GeniCam/genicam_feature_tree_widget.py
@@ -0,0 +1,511 @@
+import os
+import re
+import json
+import qtutils.icons
+
+from PyQt5 import uic, QtWidgets, QtGui, QtCore
+from PyQt5.Qt import Qt, QStyledItemDelegate, QColor
+from PyQt5.QtCore import pyqtSignal, QAbstractItemModel, QModelIndex, QSortFilterProxyModel
+from PyQt5.QtWidgets import (QSpinBox, QPushButton, QComboBox, QDialog, QLineEdit, QWidget,
+ QHBoxLayout)
+
+from ._genicam._feature_value_tuple import (FeatureValueTuple, FeatureType, FeatureAccessMode,
+ FeatureVisibility)
+from ._genicam._tree import OrderedTreeNode
+
+from typing import Union
+
+
+class FeatureTreeNode(OrderedTreeNode):
+ _readable_access_modes = [FeatureAccessMode.RW, FeatureAccessMode.RO]
+ _readable_nodes = [
+ FeatureType.Boolean,
+ FeatureType.Enumeration,
+ FeatureType.Float,
+ FeatureType.Integer,
+ FeatureType.String,
+ FeatureType.Register
+ ]
+
+ def __init__(self, name, parent_node=None, data: Union[FeatureValueTuple, None]=None, child_nodes=None):
+ super().__init__(name, parent_node, data, child_nodes)
+
+ def columnCount(self):
+ return 2 if self.data else 1
+
+ def value(self, column):
+ data = self.data
+
+ if not data: # this is a catagory node
+ if column == 0:
+ return self.name
+ else:
+ return None
+
+ if column == 0:
+ value = self.name
+ else:
+ if data.type == FeatureType.Command:
+ value = '[Click here]'
+ else:
+ if data.access_mode not in self._readable_access_modes:
+ value = '[Not accessible]'
+ elif data.type not in self._readable_nodes:
+ value = '[Not readable]'
+ else:
+ value = data.value
+
+ return value
+
+ def tooltip(self, column):
+ return None # I can't see this to be useful
+
+ def background(self, column):
+ if not self.data: # this is a catagory node
+ return QColor("grey")
+ else:
+ return None
+
+ def foreground(self, column):
+ if not self.data: # this is a catagory node
+ return QColor('white')
+ else:
+ return None
+
+ def parent(self):
+ return self.parent_node
+
+ def row(self):
+ if self.parent_node:
+ return self.parent_node.child_nodes.index(self)
+
+ return 0
+
+ def child(self, row):
+ return self.child_nodes[row]
+
+ def childCount(self):
+ return len(self.child_nodes)
+
+ def dump_value_dict(self):
+ value_tree = OrderedTreeNode.eval_tree(self, lambda data: data.value)
+ return value_tree.dump_value_dict()
+
+
+class FeatureTreeModel(QAbstractItemModel):
+ _capable_roles = [
+ Qt.DisplayRole, Qt.ToolTipRole, Qt.BackgroundColorRole,
+ Qt.ForegroundRole
+ ]
+
+ _editables = [FeatureAccessMode.RW, FeatureAccessMode.WO]
+
+ on_attribute_set = pyqtSignal(FeatureTreeNode)
+
+ def __init__(self, parent, feat_value_dict):
+ """
+ REMARKS: QAbstractItemModel might impact the performance and could
+ slow Harvester. As far as we've confirmed, QAbstractItemModel calls
+ its index() method for every item already shown. Especially, such
+ a call happens every time when (1) its view got/lost focus or (2)
+ its view was scrolled. If such slow performance makes people
+ irritating we should investigate how can we optimize it.
+ """
+
+ super().__init__()
+
+ self.root_item = FeatureTreeNode('root', child_nodes=[])
+ self.item_index_lookup = {}
+ self.populate_tree_items(feat_value_dict, self.root_item)
+
+
+ # ----- Methods required by Qt -----
+
+ def columnCount(self, parent=None, *args, **kwargs):
+ return 2
+
+ def data(self, index, role=None):
+ # index: QModelIndex
+ if not index.isValid():
+ return None
+
+ if role not in self._capable_roles:
+ return None
+
+ item = index.internalPointer() # FeatureValueTuple
+
+ if role == Qt.DisplayRole:
+ value = item.value(index.column())
+ elif role == Qt.ToolTipRole:
+ value = item.tooltip(index.column())
+ elif role == Qt.BackgroundColorRole:
+ value = item.background(index.column())
+ else:
+ value = item.foreground(index.column())
+
+ return value
+
+ def flags(self, index):
+ if not index.isValid():
+ return Qt.NoItemFlags
+
+ tree_item = index.internalPointer()
+ feature = tree_item.data
+ if feature is None:
+ return Qt.ItemIsEnabled
+
+ access_mode = feature.access_mode
+
+ if access_mode in self._editables:
+ ret = Qt.ItemIsEnabled | Qt.ItemIsEditable
+ else:
+ if index.column() == 1:
+ ret = Qt.NoItemFlags
+ else:
+ ret = Qt.ItemIsEnabled
+ return ret
+
+ def headerData(self, p_int, Qt_Orientation, role=None):
+ # p_int: section
+ if Qt_Orientation == Qt.Horizontal and role == Qt.DisplayRole:
+ if p_int == 0:
+ return "Feature name"
+ elif p_int == 1:
+ return "Value"
+ return None
+
+ def index(self, p_int, p_int_1, parent=None, *args, **kwargs):
+ # p_int: row
+ # p_int_1: column
+ if not self.hasIndex(p_int, p_int_1, parent):
+ return QModelIndex()
+
+ if not parent or not parent.isValid():
+ parent_item = self.root_item
+ else:
+ parent_item = parent.internalPointer()
+
+ child_item = parent_item.child(p_int)
+ if child_item:
+ index = self.createIndex(p_int, p_int_1, child_item)
+ self.item_index_lookup[child_item] = index
+ return index
+ else:
+ return QModelIndex()
+
+ def parent(self, index):
+ if not index.isValid():
+ return index
+
+ child_item = index.internalPointer()
+ parent_item = child_item.parent()
+
+ if parent_item == self.root_item:
+ return QModelIndex()
+
+ return self.createIndex(parent_item.row(), 0, parent_item)
+
+ def rowCount(self, parent, *args, **kwargs):
+ if parent.column() > 0:
+ return 0
+
+ if not parent.isValid():
+ parent_item = self.root_item
+ else:
+ parent_item = parent.internalPointer()
+
+ return len(parent_item.child_nodes) if parent_item.child_nodes else 0
+
+ def setData(self, index, value, role=Qt.EditRole):
+ # index: QModelIndex
+ if role == Qt.EditRole:
+ # TODO: Check the type of the target and convert the given value.
+ item = index.internalPointer()
+ feat = item.data
+
+ item.data = FeatureValueTuple(name=feat.name,
+ value=value,
+ type=feat.type,
+ entries=feat.entries,
+ access_mode=feat.access_mode,
+ visibility=feat.visibility)
+
+ self.dataChanged.emit(index, index)
+ self.on_attribute_set.emit(item)
+
+ return True
+ return False
+
+ # ----- My methods, not required by Qt -----
+
+ @classmethod
+ def populate_tree_items(cls, feature_dict, parent_node):
+ for name, feature in feature_dict.items():
+ if not isinstance(feature, dict):
+ item = FeatureTreeNode(name, parent_node, feature, None)
+ else:
+ item = FeatureTreeNode(name, parent_node, None, [])
+ cls.populate_tree_items(feature, item)
+
+ parent_node.add_child(item)
+
+ def update_attr_from_dict(self, attr_dict, parent_paths=[]):
+ for k in attr_dict.keys():
+ v = attr_dict[k]
+ if isinstance(v, dict):
+ attr_dict[k] = self.update_attr_from_dict(v, parent_paths + [k])
+ else:
+ node = self.root_item.access(parent_paths + [k])
+ feat = node.data
+ node.data = FeatureValueTuple(name=feat.name,
+ value=v,
+ type=feat.type,
+ entries=feat.entries,
+ access_mode=feat.access_mode,
+ visibility=feat.visibility)
+
+ #if node in self.item_index_lookup:
+ assert node in self.item_index_lookup
+ index = self.item_index_lookup[node]
+ self.dataChanged.emit(index, index)
+
+
+class FeatureEditDelegate(QStyledItemDelegate):
+ def __init__(self, proxy, parent=None):
+ super().__init__()
+
+ self._proxy = proxy
+
+ def createEditor(self, parent, QStyleOptionViewItem, proxy_index: QModelIndex):
+
+ # Get the actual source.
+ src_index = self._proxy.mapToSource(proxy_index)
+
+ # If it's the column #0, then immediately return.
+ if src_index.column() == 0:
+ return None
+
+ tree_item = src_index.internalPointer()
+ feature = tree_item.data
+ interface_type = feature.type
+
+ if interface_type == FeatureType.Integer:
+ w = QSpinBox(parent)
+ w.setRange(feature.min, feature.max)
+ w.setSingleStep(feature.inc)
+ w.setValue(feature.value)
+ elif interface_type == FeatureType.Command:
+ w = QPushButton(parent)
+ w.setText('Execute')
+ w.clicked.connect(lambda: self.on_button_clicked(proxy_index))
+ elif interface_type == FeatureType.Boolean:
+ w = QComboBox(parent)
+ boolean_ints = {'False': 0, 'True': 1}
+ w.addItem('False')
+ w.addItem('True')
+ proxy_index = boolean_ints['True'] if feature.value else boolean_ints['False']
+ w.setCurrentIndex(proxy_index)
+ elif interface_type == FeatureType.Enumeration:
+ w = QComboBox(parent)
+ for item in feature.entries:
+ w.addItem(item)
+ w.setCurrentText(feature.value)
+ elif interface_type == FeatureType.String:
+ w = QLineEdit(parent)
+ w.setText(feature.value)
+ elif interface_type == FeatureType.Float:
+ w = QLineEdit(parent)
+ w.setText(str(feature.value))
+ else:
+ return None
+
+ return w
+
+ def setEditorData(self, editor: QWidget, proxy_index: QModelIndex):
+ src_index = self._proxy.mapToSource(proxy_index)
+ value = src_index.data(Qt.DisplayRole)
+ tree_item = src_index.internalPointer()
+ feature = tree_item.data
+ interface_type = feature.type
+
+ if interface_type == FeatureType.Integer:
+ editor.setValue(int(value))
+ elif interface_type == FeatureType.Boolean:
+ editor.setCurrentIndex(1 if value else 0)
+ elif interface_type == FeatureType.Enumeration:
+ editor.setEditText(value)
+ elif interface_type == FeatureType.String:
+ editor.setText(value)
+ elif interface_type == FeatureType.Float:
+ editor.setText(str(value))
+
+ def setModelData(self, editor: QWidget, model: QAbstractItemModel, proxy_index: QModelIndex):
+ src_index = self._proxy.mapToSource(proxy_index)
+ tree_item = src_index.internalPointer()
+ feature = tree_item.data
+ interface_type = feature.type
+
+ if interface_type == FeatureType.Integer:
+ data = editor.value()
+ model.setData(proxy_index, data)
+ elif interface_type == FeatureType.Boolean:
+ data = editor.currentText()
+ model.setData(proxy_index, data)
+ elif interface_type == FeatureType.Enumeration:
+ data = editor.currentText()
+ model.setData(proxy_index, data)
+ elif interface_type == FeatureType.String:
+ data = editor.text()
+ model.setData(proxy_index, data)
+ elif interface_type == FeatureType.Float:
+ data = editor.text()
+ model.setData(proxy_index, data)
+
+ def on_button_clicked(self, proxy_index: QModelIndex):
+ src_index = self._proxy.mapToSource(proxy_index)
+ tree_item = src_index.internalPointer()
+ feature = tree_item.data
+ interface_type = feature.type
+
+ if interface_type == FeatureType.Command:
+ proxy_index.model().setData(proxy_index, True)
+
+
+class FilterProxyModel(QSortFilterProxyModel):
+ def __init__(self, visibility=FeatureVisibility.Beginner):
+ #
+ super().__init__()
+
+ #
+ self._visibility = visibility
+ self._keyword = ''
+
+ def filterVisibility(self, visibility):
+ beginner_items = {FeatureVisibility.Beginner}
+ expert_items = beginner_items.union({FeatureVisibility.Expert})
+ guru_items = expert_items.union({FeatureVisibility.Guru})
+ all_items = guru_items.union({FeatureVisibility.Invisible})
+
+ items_dict = {
+ FeatureVisibility.Beginner: beginner_items,
+ FeatureVisibility.Expert: expert_items,
+ FeatureVisibility.Guru: guru_items,
+ FeatureVisibility.Invisible: all_items
+ }
+
+ if visibility not in items_dict[self._visibility]:
+ return False
+ else:
+ return True
+
+ def filterPattern(self, name):
+ if not re.search(self._keyword, name, re.IGNORECASE):
+ print(name + ': refused')
+ return False
+ else:
+ print(name + ': accepted')
+ return True
+
+ def setVisibility(self, visibility: FeatureVisibility):
+ self._visibility = visibility
+ self.invalidateFilter()
+
+ def setKeyword(self, keyword: str):
+ self._keyword = keyword
+ self.invalidateFilter()
+
+ def filterAcceptsRow(self, src_row, src_parent: QModelIndex):
+ src_model = self.sourceModel()
+ src_index = src_model.index(src_row, 0, parent=src_parent)
+
+ tree_item = src_index.internalPointer()
+ name = tree_item.name
+ feature = tree_item.data
+ visibility = feature.visibility if feature else FeatureVisibility.Beginner
+ if tree_item.child_nodes and len(tree_item.child_nodes):
+ for child in tree_item.child_nodes:
+ if self.filterAcceptsRow(child.row(), src_index):
+ return True
+ return False
+ else:
+ matches = re.search(self._keyword, name, re.IGNORECASE)
+
+ if matches:
+ result = self.filterVisibility(visibility)
+ else:
+ result = False
+ return result
+
+
+class GeniCamFeatureTreeDialog(QDialog):
+ on_attr_change_requested = pyqtSignal(dict)
+
+ def __init__(self, parent, device_name, attr_dict):
+ super().__init__(parent)
+
+ self.device_name = device_name
+
+ ui_filepath = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'attributes_dialog.ui'
+ )
+
+ self.layout = QHBoxLayout()
+ self.setLayout(self.layout)
+
+ self.setWindowFlags(QtCore.Qt.Tool)
+ self.setWindowTitle(f"GeniCam attributes: {self.device_name}")
+
+ self.ui = uic.loadUi(ui_filepath)
+ self.ui.setParent(self)
+
+ self.layout.addWidget(self.ui)
+
+ self.ui.copyButton.clicked.connect(self.on_copy_clicked)
+ self.ui.visibilityComboBox.currentIndexChanged.connect(self.on_attr_visibility_level_changed)
+
+ self.model = FeatureTreeModel(parent, attr_dict)
+ self.proxy = FilterProxyModel()
+ self.proxy.setSourceModel(self.model)
+ self.delegate = FeatureEditDelegate(proxy=self.proxy)
+
+ self.ui.attributeTreeView.setModel(self.proxy)
+ self.ui.attributeTreeView.setItemDelegate(self.delegate)
+ self.ui.attributeTreeView.setUniformRowHeights(True)
+ self.ui.attributeTreeView.setColumnWidth(0, 260)
+
+ self.ui.closeButton.clicked.connect(lambda: self.accept())
+
+ self.model.on_attribute_set.connect(self.on_attribute_set)
+
+ def on_attr_visibility_level_changed(self, value):
+ self.proxy.setVisibility(FeatureVisibility(value))
+
+ def on_attribute_set(self, node):
+ feature = node.data
+ path = node.get_path_to_node()
+ value = feature.value
+
+ _dict = value
+ for p in reversed(path):
+ _dict = {p: _dict}
+
+ self.on_attr_change_requested.emit(_dict)
+
+ def on_copy_clicked(self, button):
+ _visibility = self.ui.visibilityComboBox.currentIndex()
+
+ filtered_tree = FeatureTreeNode.filter_tree(self.model.root_item,
+ lambda n: n if (n.visibility.value <= _visibility and n.access_mode == FeatureAccessMode.RW) else None
+ )
+
+ filtered_tree.print()
+
+ if filtered_tree:
+ text = json.dumps(filtered_tree.dump_value_dict(), indent=4)
+
+ clipboard = QtGui.QApplication.instance().clipboard()
+ clipboard.setText(text)
+
+ def updte_attributes(self, attr_dict):
+ return self.model.update_attr_from_dict(attr_dict)
+
diff --git a/labscript_devices/GeniCam/genicam_test.py b/labscript_devices/GeniCam/genicam_test.py
new file mode 100644
index 0000000..cba5bcf
--- /dev/null
+++ b/labscript_devices/GeniCam/genicam_test.py
@@ -0,0 +1,182 @@
+from harvesters.core import Harvester
+import numpy as np
+
+from genicam.genapi import NodeMap
+from genicam.genapi import EInterfaceType, EAccessMode, EVisibility
+
+import textwrap
+
+class TreeItem(object):
+ _readable_nodes = [
+ EInterfaceType.intfIBoolean,
+ EInterfaceType.intfIEnumeration,
+ EInterfaceType.intfIFloat,
+ EInterfaceType.intfIInteger,
+ EInterfaceType.intfIString,
+ EInterfaceType.intfIRegister,
+ ]
+
+ _readable_access_modes = [EAccessMode.RW, EAccessMode.RO]
+
+ def __init__(self, data=None, parent_item=None):
+ #
+ super().__init__()
+
+ #
+ self._parent_item = parent_item
+ self._own_data = data
+ self._child_items = []
+
+ def print(self, indent=0):
+ feature, name, interface_type, access_mode, value, entries = self.data()
+
+ if interface_type == -1 or access_mode == -1:
+ print(textwrap.indent(f"[----] {name}, {value}", ' '*indent))
+ elif entries:
+ print(textwrap.indent(f"[{str(EAccessMode(access_mode))} {str(EInterfaceType(interface_type))}] {name}, {value} ({entries})", ' '*indent))
+ else:
+ print(textwrap.indent(f"[{str(EAccessMode(access_mode))} {str(EInterfaceType(interface_type))}] {name}, {value}", ' '*indent))
+
+ for child in self._child_items:
+ child.print(indent+1)
+
+ def dump_tree(self):
+ _dict = {}
+
+ feature, name, interface_type, access_mode, value, entries = self.data()
+
+ if self._child_items:
+ for child in self._child_items:
+ _name, feat = child.dump_tree()
+ _dict[_name] = feat
+ else:
+ return name, feature
+
+ return name, _dict
+
+ @property
+ def parent_item(self):
+ return self._parent_item
+
+ @property
+ def own_data(self):
+ return self._own_data
+
+ @property
+ def child_items(self):
+ return self._child_items
+
+ def appendChild(self, item):
+ self.child_items.append(item)
+
+ def child(self, row):
+ return self.child_items[row]
+
+ def childCount(self):
+ return len(self.child_items)
+
+ def columnCount(self):
+ try:
+ ret = len(self.own_data)
+ except TypeError:
+ ret = 1
+ return ret
+
+ def data(self):
+ if isinstance(self.own_data[0], str):
+ return None, self.own_data[0], -1, -1, self.own_data[1], []
+
+ feature = self.own_data[0]
+ name = feature.node.display_name
+
+ value = ''
+
+ interface_type = feature.node.principal_interface_type
+
+ access_mode = feature.node.get_access_mode()
+
+ entries = []
+
+ if interface_type != EInterfaceType.intfICategory:
+ if interface_type == EInterfaceType.intfICommand:
+ value = '[Click here]'
+ else:
+ if feature.node.get_access_mode() not in \
+ self._readable_access_modes:
+ value = '[Not accessible]'
+ elif interface_type not in self._readable_nodes:
+ value = '[Not readable]'
+ else:
+ try:
+ value = str(feature.value)
+ except AttributeError:
+ try:
+ value = feature.to_string()
+ except AttributeError:
+ pass
+
+ if interface_type == EInterfaceType.intfIEnumeration:
+ entries = [ item.symbolic for item in feature.entries ]
+
+ return feature, name, interface_type, access_mode, value, entries
+
+ def parent(self):
+ return self._parent_item
+
+ def row(self):
+ if self._parent_item:
+ return self._parent_item.child_items.index(self)
+
+ return 0
+
+
+def populateTreeItems(features, parent_item):
+ for feature in features:
+ interface_type = feature.node.principal_interface_type
+ item = TreeItem([feature, feature], parent_item)
+ parent_item.appendChild(item)
+ if interface_type == EInterfaceType.intfICategory:
+ populateTreeItems(feature.features, item)
+
+def populateTreeItemsDict(features):
+ _dict = {}
+
+ for feature in features:
+ interface_type = feature.node.principal_interface_type
+ name = feature.node.display_name
+
+ _dict[name] = feature
+
+ if interface_type == EInterfaceType.intfICategory:
+ _dict[name] = populateTreeItems(feature.features, item)
+
+ return _dict
+
+
+if __name__ == "__main__":
+ h = Harvester()
+ h.add_file('/home/gengyd/Downloads/Vimba_6_0/VimbaGigETL/CTI/x86_64bit/VimbaGigETL.cti')
+ h.update()
+
+ assert len(h.device_info_list) == 1
+
+ print(h.device_info_list)
+
+ ia = h.create(0)
+
+ root = TreeItem(('Feature Name', 'Value'))
+
+ ia.remote_device.node_map.ImageFormat.PixelFormat = "Mono10"
+
+ # populateTreeItems(ia.remote_device.node_map.Root.features, root)
+ print(populateTreeItemsDict(ia.remote_device.node_map.Root.features))
+
+
+ #root.print()
+ #feat_dict = root.dump_tree()[1]
+
+ #print(feat_dict["ImageFormat"]["PixelFormat"].value)
+ #feat_dict["ImageFormat"]["PixelFormat"].value = "Mono10"
+ #print(feat_dict["ImageFormat"]["PixelFormat"].value)
+
+
diff --git a/labscript_devices/GeniCam/genicam_widget.py b/labscript_devices/GeniCam/genicam_widget.py
new file mode 100644
index 0000000..3e54a13
--- /dev/null
+++ b/labscript_devices/GeniCam/genicam_widget.py
@@ -0,0 +1,142 @@
+import os
+import qtutils.icons
+
+from PyQt5 import uic, QtWidgets, QtGui, QtCore
+from PyQt5.QtCore import pyqtSignal
+
+import pyqtgraph as pg
+
+from .genicam_feature_tree_widget import GeniCamFeatureTreeDialog
+
+
+class GeniCamWidget(QtWidgets.QWidget):
+ # signals emitted by this widget to outside world
+ on_continuous_requested = pyqtSignal()
+ on_stop_requested = pyqtSignal()
+ on_snap_requested = pyqtSignal()
+ on_show_attribute_tree_requested = pyqtSignal()
+ on_continuous_max_change_requested = pyqtSignal(float)
+ on_change_attribute_requested = pyqtSignal(dict)
+
+ # signals this widget received
+ request_enter_continuous_mode = pyqtSignal()
+ request_exit_continuous_mode = pyqtSignal()
+ request_update_colormap = pyqtSignal(object)
+ request_update_image = pyqtSignal(object)
+ request_update_fps = pyqtSignal(float)
+ request_update_max_fps = pyqtSignal(float)
+ request_show_attribute_tree = pyqtSignal(dict)
+ request_update_attribute_tree = pyqtSignal(dict) # TODO
+
+ def __init__(self, parent, device_name):
+ super().__init__(parent)
+
+ self.device_name = device_name
+
+ ui_filepath = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'blacs_tab.ui'
+ )
+ uic.loadUi(ui_filepath, self)
+
+ # emit signals to outside world
+ self.continuousButton.clicked.connect(lambda: self.on_continuous_requested.emit())
+ self.stopButton.clicked.connect(lambda: self.on_stop_requested.emit())
+ self.snapButton.clicked.connect(lambda: self.on_snap_requested.emit())
+ self.attributesButton.clicked.connect(lambda: self.on_show_attribute_tree_requested.emit())
+ self.noMaxButton.clicked.connect(self.on_reset_rate_clicked)
+ self.maxRateSpinBox.valueChanged.connect(lambda max_rate: self.on_continuous_max_change_requested.emit(max_rate))
+
+ # signal outside world sends to this widget
+ self.request_update_image.connect(self.update_image)
+ self.request_update_fps.connect(self.update_fps)
+ self.request_update_colormap.connect(self.set_colormap)
+ self.request_update_max_fps.connect(self.set_max_fps)
+ self.request_enter_continuous_mode.connect(self.setup_continuous_mode)
+ self.request_exit_continuous_mode.connect(self.exit_continuous_mode)
+ self.request_show_attribute_tree.connect(self.show_attribute_tree_dialog)
+
+ self.image = pg.ImageView()
+ self.image.setSizePolicy(
+ QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding
+ )
+ self.horizontalLayout.addWidget(self.image)
+
+ self.stopButton.hide()
+ self.maxRateSpinBox.hide()
+ self.noMaxButton.hide()
+ self.fpsLabel.hide()
+
+ # Ensure the GUI reserves space for these widgets even if they are hidden.
+ # This prevents the GUI jumping around when buttons are clicked:
+ for widget in [
+ self.stopButton,
+ self.maxRateSpinBox,
+ self.noMaxButton,
+ ]:
+ size_policy = widget.sizePolicy()
+ if hasattr(size_policy, 'setRetainSizeWhenHidden'): # Qt 5.2+ only
+ size_policy.setRetainSizeWhenHidden(True)
+ widget.setSizePolicy(size_policy)
+
+ @property
+ def max_fps(self):
+ return self.maxRateSpinBox.value()
+
+ @property
+ def colormap(self):
+ return self.image.ui.histogram.gradient.saveState()
+
+ def show_attribute_tree_dialog(self, attr_dict):
+ self.attributesButton.setEnabled(False)
+ self.attributes_dialog = GeniCamFeatureTreeDialog(self, self.device_name, attr_dict)
+ self.attributes_dialog.finished.connect(self._cleanup_attribute_tree_dialog)
+ self.attributes_dialog.on_attr_change_requested.connect(lambda _dict: self.on_change_attribute_requested.emit(_dict))
+ self.request_update_attribute_tree.connect(self.attributes_dialog.updte_attributes)
+ self.attributes_dialog.show()
+
+ def _cleanup_attribute_tree_dialog(self, result):
+ self.request_update_attribute_tree.disconnect()
+ self.attribute_dialog = None
+ self.attributesButton.setEnabled(True)
+
+ def on_reset_rate_clicked(self):
+ self.maxRateSpinBox.setValue(0)
+
+ def update_image(self, image):
+ if self.image.image is None:
+ self.image.setImage(image.swapaxes(-1, -2))
+ else:
+ self.image.setImage(
+ image.swapaxes(-1, -2), autoRange=False, autoLevels=False
+ )
+
+ # draw immediately
+ QtGui.QApplication.instance().sendPostedEvents()
+
+ def update_fps(self, fps):
+ self.fpsLabel.setText(f"{fps:.01f} fps")
+
+ def setup_continuous_mode(self):
+ self.snapButton.setEnabled(False)
+ self.attributesButton.setEnabled(False)
+ self.continuousButton.hide()
+ self.stopButton.show()
+ self.maxRateSpinBox.show()
+ self.noMaxButton.show()
+ self.fpsLabel.show()
+ self.fpsLabel.setText('? fps')
+
+ def exit_continuous_mode(self):
+ self.snapButton.setEnabled(True)
+ self.attributesButton.setEnabled(True)
+ self.continuousButton.show()
+ self.maxRateSpinBox.hide()
+ self.noMaxButton.hide()
+ self.stopButton.hide()
+ self.fpsLabel.hide()
+
+ def set_colormap(self, colormap):
+ self.image.ui.histogram.gradient.restoreState(colormap)
+
+ def set_max_fps(self, rate):
+ self.maxRateSpinBox.setValue(rate)
diff --git a/labscript_devices/GeniCam/labscript_devices.py b/labscript_devices/GeniCam/labscript_devices.py
new file mode 100644
index 0000000..6149be3
--- /dev/null
+++ b/labscript_devices/GeniCam/labscript_devices.py
@@ -0,0 +1,232 @@
+#####################################################################
+# #
+# /labscript_devices/IMAQdxCamera/labscript_devices.py #
+# #
+# Copyright 2019, Monash University and contributors #
+# #
+# This file is part of labscript_devices, in the labscript suite #
+# (see http://labscriptsuite.org), and is licensed under the #
+# Simplified BSD License. See the license.txt file in the root of #
+# the project for the full license. #
+# #
+#####################################################################
+import sys
+from labscript_utils import dedent
+from labscript import TriggerableDevice, set_passed_properties
+import numpy as np
+import labscript_utils.h5_lock
+import h5py
+
+
+class GeniCam(TriggerableDevice):
+ description = 'GeniCam-compatible Camera'
+
+ @set_passed_properties(
+ property_names={
+ "connection_table_properties": [
+ "cti_file",
+ "serial_number",
+ "orientation",
+ "manual_acquisition_timeout",
+ "manual_mode_camera_attributes"
+ ],
+ "device_properties": [
+ "camera_attributes",
+ "stop_acquisition_timeout",
+ "exception_on_failed_shot",
+ "saved_attribute_visibility_level"
+ ],
+ }
+ )
+ def __init__(
+ self,
+ name,
+ parent_device,
+ connection,
+ serial_number,
+ cti_file,
+ orientation=None,
+ trigger_edge_type='rising',
+ trigger_duration=None,
+ minimum_recovery_time=0.0,
+ camera_attributes=None,
+ manual_mode_camera_attributes=None,
+ manual_acquisition_timeout=0.1,
+ stop_acquisition_timeout=5.0,
+ exception_on_failed_shot=True,
+ saved_attribute_visibility_level='beginner',
+ **kwargs
+ ):
+ """A camera to be controlled using GeniCam interface and triggered with a digital edge.
+
+ Args:
+ name (str)
+ device name
+
+ parent_device (IntermediateDevice)
+ Device with digital outputs to be used to trigger acquisition
+
+ connection (str)
+ Name of digital output port on parent device.
+
+ cti_file (str)
+ Path to the GenTL Producer (.cti file) provided by the camera manufacturer.
+
+ serial_number (str or int)
+ string or integer (integer allows entering a hex literal) of the
+ camera's serial number. This will be used to idenitfy the camera.
+
+ orientation (str, optional), default: ``
+ Description of the camera's location or orientation. This will be used
+ to determine the location in the shot file where the images will be
+ saved. If not given, the device name will be used instead.
+
+ trigger_edge_type (str), default: `'rising'`
+ The direction of the desired edges to be generated on the parent
+ devices's digital output used for triggering. Must be 'rising' or
+ 'falling'. Note that this only determines the edges created on the
+ parent device, it does not program the camera to expect this type of
+ edge. If required, one must configure the camera separately via
+ `camera_attributes` to ensure it expects the type of edge being
+ generated. Default: `'rising'`
+
+ trigger_duration (float or None), default: `None`
+ Duration of digital pulses to be generated by the parent device. This
+ can also be specified as an argument to `expose()` - the value given
+ here will be used only if nothing is passed to `expose()`.
+
+ minimum_recovery_time (float), default: `0`
+ Minimum time between frames. This will be used for error checking during
+ compilation.
+
+ camera_attributes (dict, optional):
+ Dictionary of camera attribute names and values to be programmed into
+ the camera. The meaning of these attributes is model-specific.
+ Attributes will be programmed in the order they appear in this
+ dictionary. This can be important as some attributes may not be settable
+ unless another attrbiute has been set first. After adding this device to
+ your connection table, a dictionary of the camera's default attributes
+ can be obtained from the BLACS tab, appropriate for copying and pasting
+ into your connection table to customise the ones you are interested in.
+
+ manual_mode_camera_attributes (dict, optional):
+ Dictionary of attributes that will be programmed into the camera during
+ manual mode, that differ from their values in `camera_attributes`. This
+ can be useful for example, to have software triggering during manual
+ mode (allowing the acquisition of frames from the BLACS manual mode
+ interface) but hardware triggering during buffered runs. Any attributes
+ in this dictionary must also be present in `camera_attributes`.
+
+ manual_acquisition_timeout (float), default: `5.0`
+ How long, in seconds, to wait for the images when `snap` or `continuous`
+ is clicked before giving up.
+
+ stop_acquisition_timeout (float), default: `5.0`
+ How long, in seconds, to wait during `transition_to_buffered` for the
+ acquisition of images to complete before giving up. Whilst all triggers
+ should have been received, this can be used to allow for slow image
+ download time.
+
+ exception_on_failed_shot (bool), default: `True`.
+ If acquisition does not complete within the given timeout after the end
+ of a shot, whether to raise an exception. If False, instead prints a
+ warning to stderr (visible in the terminal output pane in the BLACS
+ tab), saves the images acquired so far, and continues. In the case of
+ such a 'failed shot', the HDF5 attribute
+ f['images'][orientation/name].attrs['failed_shot'] will be set to `True`
+ (otherwise it is set to `False`). This attribute is acessible in the
+ lyse dataframe as `df[orientation/name, 'failed_shot']`.
+
+ saved_attribute_visibility_level (str or None), default: 'intermediate'
+ The detail level of the camera attributes saved to the HDF5 file at the
+ end of each shot. If None, no attributes will be saved. Must be one of
+ `'simple'`, `'intermediate'`, `'advanced'`, or `None`. If `None`, no
+ attributes will be saved.
+
+ **kwargs: Further keyword arguments to be passed to the `__init__` method of
+ the parent class (TriggerableDevice).
+ """
+ self.trigger_edge_type = trigger_edge_type
+ self.minimum_recovery_time = minimum_recovery_time
+ self.trigger_duration = trigger_duration
+ self.orientation = orientation
+ self.cti_file = cti_file
+ self.serial_number = serial_number
+ self.BLACS_connection = serial_number
+ self.manual_acquisition_timeout = manual_acquisition_timeout
+ self.stop_acquisition_timeout = stop_acquisition_timeout
+ self.exception_on_failed_shot = exception_on_failed_shot
+
+ if camera_attributes is None:
+ camera_attributes = {}
+
+ if manual_mode_camera_attributes is None:
+ manual_mode_camera_attributes = {}
+
+ # TODO: is this useful?
+ # for attr_name in manual_mode_camera_attributes:
+ # if attr_name not in camera_attributes:
+ # msg = f"""attribute '{attr_name}' is present in
+ # manual_mode_camera_attributes but not in camera_attributes.
+ # Attributes that are to differ between manual mode and buffered
+ # mode must be present in both dictionaries."""
+ # raise ValueError(dedent(msg))
+
+ valid_attr_levels = ('beginner', 'expert', 'guru', None)
+ if saved_attribute_visibility_level not in valid_attr_levels:
+ raise ValueError(f"saved_attribute_visibility_level must be one of {valid_attr_levels}")
+
+ self.saved_attribute_visibility_level = saved_attribute_visibility_level
+ self.camera_attributes = camera_attributes
+ self.manual_mode_camera_attributes = manual_mode_camera_attributes
+ self.exposures = []
+
+ TriggerableDevice.__init__(self, name, parent_device, connection, **kwargs)
+
+ def expose(self, t, name, frametype='frame', trigger_duration=None):
+ """Request an exposure at the given time. A trigger will be produced by the
+ parent trigger object, with duration trigger_duration, or if not specified, of
+ self.trigger_duration. The frame should have a `name, and optionally a
+ `frametype`, both strings. These determine where the image will be stored in the
+ hdf5 file. `name` should be a description of the image being taken, such as
+ "insitu_absorption" or "fluorescence" or similar. `frametype` is optional and is
+ the type of frame being acquired, for imaging methods that involve multiple
+ frames. For example an absorption image of atoms might have three frames:
+ 'probe', 'atoms' and 'background'. For this one might call expose three times
+ with the same name, but three different frametypes.
+ """
+ # Backward compatibility with code that calls expose with name as the first
+ # argument and t as the second argument:
+ if isinstance(t, str) and isinstance(name, (int, float)):
+ msg = """expose() takes `t` as the first argument and `name` as the second
+ argument, but was called with a string as the first argument and a
+ number as the second. Swapping arguments for compatibility, but you are
+ advised to modify your code to the correct argument order."""
+ print(dedent(msg), file=sys.stderr)
+ t, name = name, t
+ if trigger_duration is None:
+ trigger_duration = self.trigger_duration
+ if trigger_duration is None:
+ msg = """%s %s has not had an trigger_duration set as an instantiation
+ argument, and none was specified for this exposure"""
+ raise ValueError(dedent(msg) % (self.description, self.name))
+ if not trigger_duration > 0:
+ msg = "trigger_duration must be > 0, not %s" % str(trigger_duration)
+ raise ValueError(msg)
+ self.trigger(t, trigger_duration)
+ self.exposures.append((t, name, frametype, trigger_duration))
+ return trigger_duration
+
+ def generate_code(self, hdf5_file):
+ self.do_checks()
+ vlenstr = h5py.special_dtype(vlen=str)
+ table_dtypes = [
+ ('t', float),
+ ('name', vlenstr),
+ ('frametype', vlenstr),
+ ('trigger_duration', float),
+ ]
+ data = np.array(self.exposures, dtype=table_dtypes)
+ group = self.init_device_group(hdf5_file)
+ if self.exposures:
+ group.create_dataset('EXPOSURES', data=data)
diff --git a/labscript_devices/GeniCam/register_classes.py b/labscript_devices/GeniCam/register_classes.py
new file mode 100644
index 0000000..19a7a00
--- /dev/null
+++ b/labscript_devices/GeniCam/register_classes.py
@@ -0,0 +1,19 @@
+#####################################################################
+# #
+# /labscript_devices/IMAQdxCamera/register_classes.py #
+# #
+# Copyright 2019, Monash University and contributors #
+# #
+# This file is part of labscript_devices, in the labscript suite #
+# (see http://labscriptsuite.org), and is licensed under the #
+# Simplified BSD License. See the license.txt file in the root of #
+# the project for the full license. #
+# #
+#####################################################################
+from labscript_devices import register_classes
+
+register_classes(
+ 'GeniCam',
+ BLACS_tab='labscript_devices.GeniCam.blacs_tabs.GeniCamTab',
+ runviewer_parser=None,
+)