From 5c01e84c56b318060ed1dc90c4cc66bc123231f8 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Tue, 1 Jun 2021 22:33:31 +0200 Subject: [PATCH 01/31] Redesigend complex widgets system --- docs/.apidoc/pylablib.core.gui.widgets.rst | 18 + docs/changelog.rst | 1 + pylablib/core/gui/utils.py | 79 +++- pylablib/core/gui/value_handling.py | 157 ++++--- pylablib/core/gui/widgets/combo_box.py | 5 +- pylablib/core/gui/widgets/container.py | 403 ++++++++++++++++++ pylablib/core/gui/widgets/layout_manager.py | 220 ++++++++++ pylablib/core/gui/widgets/param_table.py | 270 +++++------- pylablib/gui/gui_managers.py | 8 +- pylablib/gui/widgets/plotters/__init__.py | 4 +- .../gui/widgets/plotters/image_plotter.py | 300 +++++++------ pylablib/gui/widgets/plotters/mpl_plotter.py | 12 +- .../gui/widgets/plotters/trace_plotter.py | 145 +++---- pylablib/gui/widgets/range_controls.py | 369 ++++++---------- 14 files changed, 1276 insertions(+), 715 deletions(-) create mode 100644 pylablib/core/gui/widgets/container.py create mode 100644 pylablib/core/gui/widgets/layout_manager.py diff --git a/docs/.apidoc/pylablib.core.gui.widgets.rst b/docs/.apidoc/pylablib.core.gui.widgets.rst index 424c2d6..f2f4b19 100644 --- a/docs/.apidoc/pylablib.core.gui.widgets.rst +++ b/docs/.apidoc/pylablib.core.gui.widgets.rst @@ -22,6 +22,15 @@ pylablib.core.gui.widgets.combo\_box module :undoc-members: :show-inheritance: +pylablib.core.gui.widgets.container module +------------------------------------------ + +.. automodule:: pylablib.core.gui.widgets.container + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + pylablib.core.gui.widgets.edit module ------------------------------------- @@ -40,6 +49,15 @@ pylablib.core.gui.widgets.label module :undoc-members: :show-inheritance: +pylablib.core.gui.widgets.layout\_manager module +------------------------------------------------ + +.. automodule:: pylablib.core.gui.widgets.layout_manager + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + pylablib.core.gui.widgets.param\_table module --------------------------------------------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 1bb540a..0a3c176 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,7 @@ you can write import pylablib.legacy as pll from pylablib.legacy.aux_libs.devices import Lakeshore +.. + renamed ``setupUi`` -> ``setup`` for all widgets 1.0.0 diff --git a/pylablib/core/gui/utils.py b/pylablib/core/gui/utils.py index 57d2b12..773d312 100644 --- a/pylablib/core/gui/utils.py +++ b/pylablib/core/gui/utils.py @@ -1,15 +1,28 @@ from . import qdelete +def find_layout_element(layout, element): + """ + Find a layout element. + + Can be a widget, a sublayout, or a layout element + Return item index within the layout. + If layout is empty or item is not present, return ``None`` + """ + if layout: + for idx in range(layout.count()): + item=layout.itemAt(idx) + if item is element or item.widget() is element or item.layout() is element: + return idx + def delete_layout_item(layout, idx): """Remove and item with the given index (completely delete it)""" if layout: item=layout.takeAt(idx) - layout.removeItem(item) - if item.layout(): - clean_layout(item.layout(),delete_layout=True) if item.widget(): clean_layout(item.widget().layout(),delete_layout=True) item.widget().deleteLater() + elif item.layout(): + clean_layout(item.layout(),delete_layout=True) def clean_layout(layout, delete_layout=False): """ Delete all items from the layout. @@ -49,15 +62,17 @@ def get_first_empty_row(layout, start_row=0): if is_layout_row_empty(layout,r): return r return layout.rowCount() -def insert_layout_row(layout, row): +def insert_layout_row(layout, row, compress=False): """ - Insert row in a grid layout at a given tow index. + Insert row in a grid layout at a given index. - Any multi-column item spanning over the row (i.e., starting at least one row before `row` and ending at least on row after `row`) gets stretched. + Any multi-column item spanning over the row (i.e., starting at least one row before `row` and ending at least one row after `row`) gets stretched. Anything else either stays in place (if it's above `row`), or gets moved one row down. + If ``compress==True``, try to find an empty row below the inserted position and shit it to the new row's place; + otherwise, add a completely new row. """ if layout: - free_row=get_first_empty_row(layout,row+1) + free_row=get_first_empty_row(layout,row+1) if compress else layout.rowCount() items_to_shift=[] for i in range(layout.count()): pos=layout.getItemPosition(i) @@ -69,6 +84,56 @@ def insert_layout_row(layout, row): row_shift=1 if p[0]>=row else 0 layout.addItem(i,p[0]+row_shift,p[1],p[2]+(1-row_shift),p[3]) + +def is_layout_column_empty(layout, col): + """Check if the given column in a grid layout is empty""" + if layout: + if colcol: + items_to_shift.append((layout.itemAt(i),pos)) + for i,_ in items_to_shift: + layout.removeItem(i) + for i,p in items_to_shift: + col_shift=1 if p[0]>=col else 0 + layout.addItem(i,p[0],p[1]+col_shift,p[2],p[3]+(1-col_shift)) + + def compress_grid_layout(layout): """Find all empty rows in a grid layout and shift them to the bottom""" if layout: diff --git a/pylablib/core/gui/value_handling.py b/pylablib/core/gui/value_handling.py index a3a9534..18d1d6f 100644 --- a/pylablib/core/gui/value_handling.py +++ b/pylablib/core/gui/value_handling.py @@ -50,7 +50,8 @@ def get_method_kind(method, add_args=0): if method is None: return None fsig=FunctionSignature.from_function(method) - if len(fsig.arg_names)>=1+add_args and fsig.arg_names[0]=="name": + arg_names=fsig.arg_names if fsig.obj is None else fsig.arg_names[1:] + if len(arg_names)>=1+add_args and arg_names[0]=="name": return "named" else: return "simple" @@ -178,7 +179,7 @@ class StandardValueHandler(IValueHandler): default_name(str): default name to be supplied to ``get/set_value`` and ``get/set_all_values`` methods if they require a name argument. """ def __init__(self, widget, default_name=None): - IValueHandler.__init__(self,widget) + super().__init__(widget) self.get_value_kind=get_method_kind(getattr(self.widget,"get_value",None)) self.get_all_values_kind="simple" if hasattr(self.widget,"get_all_values") else None if not (self.get_value_kind or self.get_all_values_kind): @@ -191,12 +192,12 @@ def __init__(self, widget, default_name=None): self.default_name=default_name def get_value(self, name=None): if name is None: - if self.get_value_kind=="simple": + if self.get_all_values_kind=="simple": + return self.widget.get_all_values() + elif self.get_value_kind=="simple": return self.widget.get_value() - elif self.get_value_kind=="named": - return self.widget.get_value(self.default_name) else: - return self.widget.get_all_values() + return self.widget.get_value(self.default_name) else: if self.get_value_kind=="named": return self.widget.get_value(name) @@ -205,12 +206,12 @@ def get_value(self, name=None): raise ValueError("can't find getter for widget {} with name {}".format(self.widget,name)) def set_value(self, value, name=None): if name is None: - if self.set_value_kind=="simple": + if self.set_all_values_kind=="simple": + return self.widget.set_all_values(value) + elif self.set_value_kind=="simple": return self.widget.set_value(value) - elif self.set_value_kind=="named": - return self.widget.set_value(self.default_name,value) else: - return self.widget.set_all_values(value) + return self.widget.set_value(self.default_name,value) else: if self.set_value_kind=="named": return self.widget.set_value(name,value) @@ -241,8 +242,6 @@ class ISingleValueHandler(IValueHandler): Args: widget: handled widget """ - def __init__(self, widget): - IValueHandler.__init__(self,widget) def get_single_value(self): """Get the widget value""" raise ValueError("can't find default getter for widget {}".format(self.widget)) @@ -394,7 +393,7 @@ def create_value_handler(widget): return ComboBoxValueHandler(widget) if isinstance(widget,QtWidgets.QProgressBar): return ProgressBarValueHandler(widget) - return IValueHandler(widget) + raise ValueError("can not determine widget handler type for widget {}".format(widget)) @@ -518,18 +517,21 @@ def get_value(self, name=None): return self.label_handler.get_value() def repr_value(self, value, name=None): """Represent a value with a given name""" - if self.widget_handler is not None: - return self.widget_handler.repr_value(value,name=self.repr_value_name if name is None else name) - elif self.repr_func is not None: - if name is None: - if self.repr_func_kind=="simple": - return self.repr_func(value) - else: - return self.repr_func(self.repr_value_name,value) - elif self.repr_func_kind=="named": - return self.repr_func(name,value) - if name: - raise KeyError("no indicator value with name {}".format(name)) + try: + if self.widget_handler is not None: + return self.widget_handler.repr_value(value,name=self.repr_value_name if name is None else name) + elif self.repr_func is not None: + if name is None: + if self.repr_func_kind=="simple": + return self.repr_func(value) + else: + return self.repr_func(self.repr_value_name,value) + elif self.repr_func_kind=="named": + return self.repr_func(name,value) + if name: + raise KeyError("no indicator value with name {}".format(name)) + except ValueError: + pass return str(value) def set_value(self, value, name=None): return self.label_handler.set_value(self.repr_value(value,name=name)) @@ -586,9 +588,11 @@ def add_handler(self, name, handler): """Add a value handler under a given name""" self.handlers[name]=handler return handler - def remove_handler(self, name): + def remove_handler(self, name, remove_indicator=True): """Remove the value handler with a given name""" del self.handlers[name] + if remove_indicator and name in self.indicator_handlers: + self.remove_indicator_handler(name) def get_handler(self, name): """Get the value handler with the given name""" return self.handlers[name] @@ -699,40 +703,39 @@ def add_label_indicator(self, name, label, formatter=None, ind_name="__default__ return self.add_indicator_handler(name,LabelIndicatorHandler(label,formatter=formatter),ind_name=ind_name) @gui_thread_method - def get_value(self, name=None, include=None): + def get_value(self, name=None, include=None, exclude=None): """ Get a value or a set of values in a subtree under a given name (all values by default). Automatically handles complex widgets and sub-names. If `name` refers to a branch, return a :class:`.Dictionary` object containing tree structure of the names. - If supplied, `include` is a container specifies which specifies names (relative to the root) to include in the result; by default, include everything. + If supplied, `include` and `exclude` are containers specifying included and excluded names (relative to the root); by default, include everything and exclude nothing. """ name=name or "" path,subpath=self.handlers.get_max_prefix(name,kind="leaf") if path: # path is in handlers and handlers are not empty - if include is None or "/".join(path) in include: - return self.handlers[path].get_value(subpath or None) + return self.handlers[path].get_value(subpath or None) elif name in self.handlers: values=dictionary.Dictionary() subtree=self.handlers[name] for n in subtree.paths(): - if include is None or "/".join(n) in include: + if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): values[n]=subtree[n].get_value() if values: return values raise KeyError("missing handler '{}'".format(name)) - def get_all_values(self, name=None, include=None): + def get_all_values(self, root=None, include=None, exclude=None): """ Get all values in the given sub-branch. Same as :meth:`get_value`, but returns an empty dictionary if the `name` is missing. """ try: - return self.get_value(name,include=include) + return self.get_value(root,include=include,exclude=exclude) except KeyError: return dictionary.Dictionary() @gui_thread_method - def set_value(self, name, value, include=None): + def set_value(self, name, value, include=None, exclude=None): """ Set value under a given name. @@ -741,85 +744,84 @@ def set_value(self, name, value, include=None): name=name or "" path,subpath=self.handlers.get_max_prefix(name,kind="leaf") if path: # path is in handlers and handlers are not empty - if include is None or "/".join(path) in include: - return self.handlers[path].set_value(value,subpath or None) + return self.handlers[path].set_value(value,subpath or None) elif name in self.handlers: # assigning to a branch subtree=self.handlers[name] for n,v in dictionary.as_dictionary(value).iternodes(to_visit="all",topdown=True,include_path=True): if subtree.has_entry(n,kind="leaf"): - if (include is None) or ("/".join(n) in include): + if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): subtree[n].set_value(v) return elif not dictionary.as_dict(value): # assign empty values return raise KeyError("missing handler '{}'".format(name)) - def set_all_values(self, value, root="", include=None): - return self.set_value(root,value,include=include) + def set_all_values(self, value, root="", include=None, exclude=None): + return self.set_value(root,value,include=include,exclude=exclude) @gui_thread_method - def get_indicator(self, name=None, ind_name="__default__", include=None): + def get_indicator(self, name=None, ind_name="__default__", include=None, exclude=None): """ Get indicator value with a given name. `ind_name` can distinguish different sub-indicators with the same name, if the same value has multiple indicators. + If supplied, `include` and `exclude` are containers specifying included and excluded names (relative to the root); by default, include everything and exclude nothing. """ name=name or "" path,subpath=self.indicator_handlers.get_max_prefix(name,kind="leaf") if path: # path is in indicator_handlers and indicator_handlers are not empty - if include is None or "/".join(path) in include: - ind_set=self.indicator_handlers[path].ind - if ind_name not in ind_set: - raise KeyError("missing indicator handler '{}' for with sub-name '{}'".format(name,ind_name)) - return ind_set[ind_name].get_value(subpath or None) + ind_set=self.indicator_handlers[path].ind + if ind_name not in ind_set: + raise KeyError("missing indicator handler '{}' for with sub-name '{}'".format(name,ind_name)) + return ind_set[ind_name].get_value(subpath or None) elif name in self.indicator_handlers: # getting branch values values=dictionary.Dictionary() subtree=self.indicator_handlers[name] for n in subtree.paths(): - if (include is None) or ("/".join(n) in include): + if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): ind_set=subtree[n].ind if ind_name in ind_set: values[n]=ind_set[ind_name].get_value() if values: return values raise KeyError("missing indicator handler '{}'".format(name)) - def get_all_indicators(self, name=None, ind_name="__default__", include=None): + def get_all_indicators(self, root=None, ind_name="__default__", include=None, exclude=None): """ Get all indicator values in the given sub-branch. - Same as :meth:`get_indicator`, but returns an empty dictionary if the `name` is missing. + Same as :meth:`get_indicator`, but returns an empty dictionary if the `root` is missing. """ try: - return self.get_indicator(name,ind_name=ind_name,include=include) + return self.get_indicator(root,ind_name=ind_name,include=include,exclude=exclude) except KeyError: return dictionary.Dictionary() @gui_thread_method - def set_indicator(self, name, value, ind_name=None, include=None, ignore_missing=True): + def set_indicator(self, name, value, ind_name=None, include=None, exclude=None, ignore_missing=True): """ Set indicator value with a given name. `ind_name` can distinguish different sub-indicators with the same name, if the same value has multiple indicators. By default, set all sub-indicators to the given value. + If supplied, `include` and `exclude` are containers specifying included and excluded names (relative to the root); by default, include everything and exclude nothing. If ``ignore_missing==True`` and the given indicator and sub-indicator names are missing, raise an error; otherwise, do nothing. """ name=name or "" path,subpath=self.indicator_handlers.get_max_prefix(name,kind="leaf") if path: # path is in indicator_handlers and indicator_handlers are not empty - if include is None or "/".join(path) in include: - ind_set=self.indicator_handlers[path].ind - if ind_name is None: - for ind in ind_set.values(): - ind.set_value(value,subpath or None) - return - elif ind_name in ind_set: - return ind_set[ind_name].set_value(value,subpath or None) - elif not ignore_missing: - raise KeyError("missing handler '{}' for indicator with sub-name '{}'".format(name,ind_name)) + ind_set=self.indicator_handlers[path].ind + if ind_name is None: + for ind in ind_set.values(): + ind.set_value(value,subpath or None) + return + elif ind_name in ind_set: + return ind_set[ind_name].set_value(value,subpath or None) + elif not ignore_missing: + raise KeyError("missing handler '{}' for indicator with sub-name '{}'".format(name,ind_name)) elif name in self.indicator_handlers: # assigning to a branch subtree=self.indicator_handlers[name] for n,v in dictionary.as_dictionary(value).iternodes(to_visit="all",topdown=True,include_path=True): if subtree.has_entry(n,kind="leaf"): - if (include is None) or ("/".join(n) in include): + if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): ind_set=subtree[n].ind if ind_name is None: for ind in ind_set.values(): @@ -829,17 +831,17 @@ def set_indicator(self, name, value, ind_name=None, include=None, ignore_missing return elif not ignore_missing: raise KeyError("missing handler '{}'".format(name)) - def set_all_indicators(self, value, root="", ind_name=None, include=None, ignore_missing=True): - return self.set_indicator(root,value,ind_name=ind_name,include=include,ignore_missing=ignore_missing) + def set_all_indicators(self, value, root="", ind_name=None, include=None, exclude=None, ignore_missing=True): + return self.set_indicator(root,value,ind_name=ind_name,include=include,exclude=exclude,ignore_missing=ignore_missing) @gui_thread_method - def update_indicators(self, root="", include=None): + def update_indicators(self, root="", include=None, exclude=None): """ Update all indicators in a subtree with the given root (all values by default) to represent current values. - If supplied, `include` is a container specifies which specifies names (relative to the root) to include in the result; by default, include everything. + If supplied, `include` and `exclude` are containers specifying included and excluded names (relative to the root); by default, include everything and exclude nothing. """ for n in self.handlers[root].paths(): - if (include is None) or ("/".join(n) in include): + if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): p=(root,n) if p in self.indicator_handlers: try: @@ -859,11 +861,30 @@ def repr_value(self, name, value): return self.handlers[path].repr_value(value,subpath) def get_value_changed_signal(self, name): """Get changed events for a value under a given name""" - return self.handlers[name].get_value_changed_signal() + return self.get_handler(name).get_value_changed_signal() +def get_gui_values(gui_values=None, gui_values_path=""): + """ + Get new or existing :class:`GUIValues` object and the sub-branch path inside it based on the supplied arguments. + + If `gui_values` is ``None`` or ``"new"``, create a new object and set empty root path. + If `gui_values` itself has ``gui_values`` attribute, get this attribute, and prepend object's ``gui_values_path`` attrbiute to the given path. + Otherwise, assume that `gui_values` is :class:`GUIValues` object, and use the supplied root. + """ + if gui_values is None or gui_values=="new": + return GUIValues(),"" + else: + root_path=[] + while hasattr(gui_values,"gui_values"): # covers for container widgets + if hasattr(gui_values,"gui_values_path"): + root_path.append(gui_values.gui_values_path) + gui_values=gui_values.gui_values + root_path.append(gui_values_path) + return gui_values,"/".join(root_path) + -def create_virtual_values(**kwargs): +def virtual_gui_values(**kwargs): """ Create a gui values set with all virtual values. diff --git a/pylablib/core/gui/widgets/combo_box.py b/pylablib/core/gui/widgets/combo_box.py index 941a178..f28135d 100644 --- a/pylablib/core/gui/widgets/combo_box.py +++ b/pylablib/core/gui/widgets/combo_box.py @@ -20,7 +20,10 @@ def index_to_value(self, idx): return self._index_values[idx] def value_to_index(self, value): """Turn value into a numerical index""" - return value if self._index_values is None else self._index_values.index(value) + try: + return value if self._index_values is None else self._index_values.index(value) + except ValueError as err: + raise ValueError("value {} is not among available option {}".format(value,self._index_values)) from err def _on_index_changed(self, index): if self._index!=index: self._index=index diff --git a/pylablib/core/gui/widgets/container.py b/pylablib/core/gui/widgets/container.py new file mode 100644 index 0000000..0683edf --- /dev/null +++ b/pylablib/core/gui/widgets/container.py @@ -0,0 +1,403 @@ +from ...utils import general, dictionary +from .. import value_handling +from .. import QtCore, QtWidgets +from ...thread import controller +from .layout_manager import QLayoutManagedWidget + +import collections + + + + +TTimer=collections.namedtuple("TTimer",["name","period","timer"]) +TTimerEvent=collections.namedtuple("TTimerEvent",["start","loop","stop","timer"]) +TWidget=collections.namedtuple("TWidget",["name","widget","gui_values_path"]) +class QContainer(QtCore.QObject): + """ + Basic controller object which combines and controls several other widget. + + Can either corresponds to a widget (e.g., a frame or a group box), or simply be an organizing entity. + + Args: + name: entity name (used by default when adding this object to a values table) + """ + TimerUIDGenerator=general.NamedUIDGenerator(thread_safe=True) + def __init__(self, *args, name=None, **kwargs): + super().__init__(*args,**kwargs) + self.name=name + self._timers={} + self._timer_events={} + self._running=False + self._widgets=dictionary.Dictionary() + self.setup_gui_values("new") + self.ctl=None + self.w=dictionary.ItemAccessor(self.get_widget) + self.v=dictionary.ItemAccessor(self.get_value,self.set_value) + self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) + + _ignore_set_values=[] + _ignore_get_values=[] + def setup_gui_values(self, gui_values="new", gui_values_path=""): + """ + Setup container's GUI values storage. + + `gui_values` is a :class:`.GUIValues`` object, an object which has ``gui_values`` attribute, + or ``"new"`` (make a new storage; in this case `gui_values_path` is ignored), and + `gui_values_path` is the container's path within this storage. + """ + if self._widgets: + raise RuntimeError("can not change gui values after widgets have been added") + if gui_values is not None: + self.gui_values,self.gui_values_path=value_handling.get_gui_values(gui_values,gui_values_path) + def setup_name(self, name): + """Set the object's name""" + self.name=name + self.setObjectName(name) + def setup(self, gui_values=None, gui_values_path=""): + """ + Setup the container by intializing its GUI values and setting the ``ctl`` attribute. + + `gui_values` is a :class:`.GUIValues`` object, an object which has ``gui_values`` attribute, + or ``"new"`` (make a new storage; in this case `gui_values_path` is ignored), and + `gui_values_path` is the container's path within this storage. + If ``gui_values`` is ``None``, skip the setup (assume that it's already done). + """ + self.setup_gui_values(gui_values=gui_values,gui_values_path=gui_values_path) + self.ctl=controller.get_gui_controller() + + def add_timer(self, name, period, autostart=True): + """ + Add a periodic timer with the given `name` and `period`. + + Rarely needs to be called explicitly (one is created automatically if timer event is created). + If ``autostart==True`` and the container has been started (by calling :meth:`start` method), start the timer as well. + """ + if name is None: + while True: + name=self.TimerUIDGenerator("timer") + if name not in self._timers: + break + if name in self._timers: + raise ValueError("timer {} already exists".format(name)) + timer=QtCore.QTimer(self) + timer.timeout.connect(controller.exsafe(lambda : self._on_timer(name))) + self._timers[name]=TTimer(name,period,timer) + if self._running and autostart: + self.start_timer(name) + return name + def _get_timer(self, name): + if name not in self._timers: + raise KeyError("timer {} does not exist".format(name)) + return self._timers[name] + def start_timer(self, name): + """Start the timer with the given name (also called automatically on :meth:`start` method)""" + if not self.is_timer_running(name): + self._call_timer_events(name,"start") + _,period,timer=self._get_timer(name) + timer.start(max(int(period*1000),1)) + def stop_timer(self, name): + """Stop the timer with the given name (also called automatically on :meth:`stop` method)""" + running=self.is_timer_running(name) + _,_,timer=self._get_timer(name) + if running: + timer.stop() + self._call_timer_events(name,"stop") + def is_timer_running(self, name): + """Check if the timer with the given name is running""" + _,_,timer=self._get_timer(name) + return timer.isActive() + def _on_timer(self, name): + self._call_timer_events(name,"loop") + + def add_timer_event(self, name, loop=None, start=None, stop=None, period=None, timer=None, autostart=True): + """ + Add timer event with the given `name`. + + Add an event which should be called periodically (e.g., a GUI update). Internally implemented through Qt timers. + `loop`, `start` and `stop` are the functions called, correspondingly, on timer (periodically), when timer is start, and when it's finished. + One can either specify the timer by name (`timer` parameter), or create a new one with the given `period`. + If ``autostart==True`` and the container has been started (by calling :meth:`start` method), start the timer as well. + """ + if timer is None and period is None: + raise ValueError("either a period or a timer name should be provided") + if timer is None: + timer=self.add_timer(None,period,autostart=autostart) + if start is not None and self.is_timer_running(timer): + start() + self._timer_events[name]=TTimerEvent(start,loop,stop,timer) + return timer + def _call_timer_events(self, timer, meth="loop"): + t=self._get_timer(timer).timer + for evt in self._timer_events.values(): + if evt.timer==timer: + if meth=="start" and evt.start is not None: + evt.start() + elif meth=="stop" and evt.stop is not None: + evt.stop() + elif evt.loop is not None and t.isActive(): # discard all possible after-stop queued events + evt.loop() + + def add_widget_values(self, path, widget): + """ + Add widget's values to the container's table. + + If `widget` is a container and ``path==""``, + use its :meth:`setup_gui_values` to make it share the same GUI values; + otherwise, simply add it to the GUI values under the given path. + """ + if path=="": + if hasattr(widget,"setup_gui_values"): + widget.setup_gui_values(self,"") + else: + raise ValueError("can not store a non-container widget under an empty path") + else: + self.gui_values.add_widget(path,widget) + def add_widget(self, name, widget, gui_values_path=True): + """ + Add a contained widget. + + If `gui_values_path` is ``False`` or ``None``, do not add it to the GUI values table; + if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not; + otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. + """ + if name in self._widgets: + raise ValueError("widget {} is already present") + if gui_values_path!=False and gui_values_path is not None: + if gui_values_path==True: + gui_values_path="" if hasattr(widget,"setup_gui_values") else name + self.add_widget_values(gui_values_path,widget) + self._widgets[name]=TWidget(name,widget,gui_values_path) + def get_widget(self, name): + """Get the widget with the given name""" + path,subpath=self._widgets.get_max_prefix(name,kind="leaf") + if path: + return self._widgets[path].widget.get_widget(subpath) if subpath else self._widgets[path].widget + raise KeyError("can't find widget {}".format(name)) + def _clear_widget(self, widget): + if hasattr(widget.widget,"clear"): + widget.widget.clear() + if widget.gui_values_path is not None: + self.gui_values.remove_handler((self.gui_values_path,widget.gui_values_path),remove_indicator=True) + def remove_widget(self, name): + """Remove widget from the container and clear it""" + path,subpath=self._widgets.get_max_prefix(name,kind="leaf") + if path: + if subpath: + return self._widgets[path].widget.remove_widget(subpath) + w=self._widgets.pop(path) + self._clear_widget(w) + else: + raise KeyError("can't find widget {}".format(name)) + + @controller.exsafe + def start(self): + """ + Start the container. + + Starts all the internal timers, and calls ``start`` method for all the contained widgets. + """ + if self._running: + raise RuntimeError("widget '{}' loop is already running".format(self.name)) + for w in self._widgets.iternodes(): + if hasattr(w.widget,"start"): + w.widget.start() + for n in self._timers: + self.start_timer(n) + self._running=True + @controller.exsafe + def stop(self): + """ + Stop the container. + + Stops all the internal timers, and calls ``stop`` method for all the contained widgets. + """ + if not self._running: + raise RuntimeError("widget '{}' loop is not running".format(self.name)) + self._running=False + for n in self._timers: + self.stop_timer(n) + for w in self._widgets.iternodes(): + if hasattr(w.widget,"stop"): + w.widget.stop() + + def clear(self): + """ + Clear the container. + + Stop all timers and widgets, and call ``clear`` methods of all contained widgets, + remove all widgets from the values table, remove all widgets from the table. + """ + if self._running: + self.stop() + for w in self._widgets.iternodes(): + self._clear_widget(w) + self._widgets=dictionary.Dictionary() + + def get_value(self, name=None): + """Get value of a widget with the given name (``None`` means all values)""" + return self.gui_values.get_value((self.gui_values_path,name or "")) + def get_all_values(self): + """Get values of all widget in the container""" + return self.gui_values.get_all_values(self.gui_values_path,exclude=self._ignore_get_values) + def set_value(self, name, value): + """Set value of a widget with the given name (``None`` means all values)""" + return self.gui_values.set_value((self.gui_values_path,name or ""),value) + def set_all_values(self, values): + """Set values of all widgets in the container""" + return self.gui_values.set_all_values(values,self.gui_values_path,exclude=self._ignore_set_values) + + def get_indicator(self, name=None): + """Get indicator value for a widget with the given name (``None`` means all indicators)""" + return self.gui_values.get_indicator((self.gui_values_path,name or "")) + def get_all_indicators(self): + """Get indicator values of all widget in the container""" + return self.gui_values.get_all_indicators(self.gui_values_path) + def set_indicator(self, name, value, ignore_missing=True): + """Set indicator value for a widget or a branch with the given name""" + return self.gui_values.set_indicator((self.gui_values_path,name or ""),value,ignore_missing=ignore_missing) + set_all_indicators=set_indicator + def update_indicators(self): + """Update all indicators to represent current values""" + return self.gui_values.update_indicators(root=self.gui_values_path) + + + + + +class QWidgetContainer(QLayoutManagedWidget, QContainer): + """ + Generic widget container. + + Combines :class:`QContainer` management of GUI values and timers + with :class:`.QLayoutManagedWidget` management of the contained widget's layout. + + Typically, adding widget adds them both to the container values and to the layout; + however, this can be skipped by either using :meth:`.QLayoutManagedWidget.add_layout_element` + (only add to the layout), or specifying ``location="skip"`` in :meth:`add_widget` (only add to the container). + """ + def setup(self, layout_kind="vbox", no_margins=False, gui_values=None, gui_values_path=""): + QContainer.setup(self,gui_values=gui_values,gui_values_path=gui_values_path) + QLayoutManagedWidget.setup(self,layout_kind=layout_kind,no_margins=no_margins) + def add_widget(self, name, widget, location=None, gui_values_path=True): + """ + Add a contained widget. + + `location` specifies the layout location to which the widget is added; + if it is ``"skip"``, skip adding it to the layout (can be manually added later). + Note that if the widget is added to the layout, it will be completely deleted when :meth:`clear` method is called; + otherwise, simply its ``clear`` method will be called, and its GUI values will be deleted. + + If `gui_values_path` is ``False`` or ``None``, do not add it to the GUI values table; + if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not; + otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. + """ + QContainer.add_widget(self,name=name,widget=widget,gui_values_path=gui_values_path) + if isinstance(widget,QtWidgets.QWidget): + QLayoutManagedWidget.add_layout_element(self,widget,location=location) + def remove_widget(self, name): + """Remove widget from the container and the layout, clear it, and remove it""" + if name in self._widgets: + widget=self._widgets[name].widget + QContainer.remove_widget(self,name) + QLayoutManagedWidget.remove_layout_element(self,widget) + else: + QContainer.remove_widget(self,name) + def add_frame(self, name, layout_kind="vbox", location=None, gui_values_path=True, no_margins=True): + """ + Add a new frame container to the layout. + + `layout_kind` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, + and `location` specifies its location within the container layout. + If ``no_margins==True``, the frame will have no inner layout margins. + The other parameters are the same as in :meth:`add_widget` method. + """ + frame=QFrameContainer(self) + frame.setup(layout_kind=layout_kind,no_margins=no_margins) + self.add_widget(name,frame,location=location,gui_values_path=gui_values_path) + return frame + def add_group_box(self, name, caption, layout_kind="vbox", location=None, gui_values_path=True, no_margins=True): + """ + Add a new group box container with the given `caption` to the layout. + + `layout_kind` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, + and `location` specifies its location within the container layout. + If ``no_margins==True``, the frame will have no inner layout margins. + The other parameters are the same as in :meth:`add_widget` method. + """ + group_box=QGroupBoxContainer(self) + group_box.setup(caption=caption,layout_kind=layout_kind, no_margins=no_margins) + self.add_widget(name,group_box,location=location,gui_values_path=gui_values_path) + return group_box + def clear(self): + """ + Clear the container. + + All the timers are stopped, all the contained widgets are cleared and removed. + """ + QContainer.clear(self) + QLayoutManagedWidget.clear(self) + + + + +class QFrameContainer(QtWidgets.QFrame, QWidgetContainer): + """An extention of :class:`QWidgetContainer` for a ``QFrame`` Qt base class""" + +class QGroupBoxContainer(QtWidgets.QGroupBox, QWidgetContainer): + """An extention of :class:`QWidgetContainer` for a ``QGroupBox`` Qt base class""" + def setup(self, caption=None, layout_kind="vbox", no_margins=False, gui_values=None, gui_values_path=""): + QWidgetContainer.setup(self,layout_kind=layout_kind,no_margins=no_margins,gui_values=gui_values,gui_values_path=gui_values_path) + if caption is not None: + self.setTitle(caption) + + + + +class QTabContainer(QtWidgets.QTabWidget, QContainer): + """ + Container which manages tab widget. + + Does not have its own layout, but can add or remove tabs, which are represented as :class:`QFrameContainer` widgets. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args,**kwargs) + self._tabs={} + def add_tab(self, name, caption, index=None, layout_kind="vbox", gui_values_path=True, no_margins=True): + """ + Add a new tab container with the given `caption` to the widget. + + `index` specifies the new tab's index (``None`` means adding to the end, negative values count from the end). + `layout_kind` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, + and `location` specifies its location within the container layout. + If ``no_margins==True``, the frame will have no inner layout margins. + The other parameters are the same as in :meth:`add_widget` method. + """ + if name in self._tabs: + raise ValueError("tab {} already exists".format(name)) + frame=QFrameContainer(self) + frame.setup(layout_kind=layout_kind,no_margins=no_margins) + self.add_widget(name=name,widget=frame,gui_values_path=gui_values_path) + if index is None: + index=self.count() + elif index<0: + index=index%self.count() + else: + index=min(index,self.count()) + self.insertTab(index,frame,caption) + self._tabs[name]=frame + return frame + def remove_tab(self, name): + """ + Remove a tab with the given name. + + Clear it, remove its GUI values, and delete it and all contained widgets. + """ + super().remove_widget(name) + frame=self._tabs.pop(name) + idx=self.indexOf(frame) + self.removeTab(idx) + frame.deleteLater() + def clear(self): + for n in list(self._tabs): + self.remove_tab(n) + super().clear() \ No newline at end of file diff --git a/pylablib/core/gui/widgets/layout_manager.py b/pylablib/core/gui/widgets/layout_manager.py new file mode 100644 index 0000000..bc50eda --- /dev/null +++ b/pylablib/core/gui/widgets/layout_manager.py @@ -0,0 +1,220 @@ +from .. import utils +from ...utils import py3, funcargparse + +from .. import QtCore, QtWidgets + +import contextlib + + +def _make_layout(kind, *args, **kwargs): + """Make a layout of the given kind""" + if kind=="grid": + return QtWidgets.QGridLayout(*args,**kwargs) + if kind=="vbox": + return QtWidgets.QVBoxLayout(*args,**kwargs) + if kind=="hbox": + return QtWidgets.QHBoxLayout(*args,**kwargs) + raise ValueError("unrecognized layout kind: {}".format(kind)) +class QLayoutManagedWidget(QtWidgets.QWidget): + """ + GUI widget which can manage layouts. + + Typically, first it is set up using :meth:`setup` method to specify the master layout kind; + afterwards, widgets and sublayout can be added using :meth:`add_layout_element`. + In addition, it can directly add named sublayouts using :meth:`add_sublayout` method. + """ + def __init__(self, parent=None): + super().__init__(parent) + self.main_layout=None + self._default_layout="main" + def _set_main_layout(self): + self.main_layout=_make_layout(self.main_layout_kind,self) + self.main_layout.setObjectName(self.name+"_main_layout" if hasattr(self,"name") and self.name else "main_layout") + if self.no_margins: + self.main_layout.setContentsMargins(0,0,0,0) + def setup(self, layout_kind="grid", no_margins=False): + """ + Setup the layout. + + Args: + kind: layout kind; can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box). + no_margins: if ``True``, set all layout margins to zero (useful when the widget is in the middle of layout hierarchy) + """ + self.main_layout_kind=layout_kind + self.no_margins=no_margins + self._set_main_layout() + self._sublayouts={"main":(self.main_layout,self.main_layout_kind)} + self._spacers=[] + + @contextlib.contextmanager + def using_layout(self, name): + """Use a different sublayout as default inside the ``with`` block""" + current_layout,self._default_layout=self._default_layout,name + try: + yield + finally: + self._default_layout=current_layout + def _normalize_location(self, location, default_location=None, default_layout=None): + if location=="skip": + return None,"skip" + if not isinstance(location,(list,tuple)): + location=() if location is None else (location,) + if location and isinstance(location[0],py3.textstring) and location[0]!="next": + lname,location=location[0],location[1:] + else: + lname=default_layout or self._default_layout + layout,lkind=self._sublayouts[lname] + if default_location is None: + default_location=("next",0,1,1) + location+=(None,)*(4-len(location)) + location=[d if l is None else l for (l,d) in zip(location,default_location)] + row,col,rowspan,colspan=location + if lkind=="grid": + row_cnt,col_cnt=layout.rowCount(),layout.columnCount() + elif lkind=="vbox": + col,colspan=0,1 + row_cnt,col_cnt=layout.count(),1 + else: + if col==0: + col=row + if colspan==1: + colspan=rowspan + row,rowspan=0,1 + row_cnt,col_cnt=1,layout.count() + if lkind in {"grid","vbox"}: + row=row_cnt if row=="next" else (row%row_cnt if row<0 else row) + if rowspan=="end": + rowspan=max(row_cnt-row,1) + if lkind in {"grid","hbox"}: + col=col_cnt if col=="next" else (col%col_cnt if col<0 else col) + if colspan=="end": + colspan=max(col_cnt-col,1) + return lname,(row,col,rowspan,colspan) + def _insert_layout_element(self, lname, element, location, kind="widget"): + layout,lkind=self._sublayouts[lname] + if lkind=="grid": + if kind=="widget": + layout.addWidget(element,*location) + elif kind=="item": + layout.addItem(element,*location) + elif kind=="layout": + layout.addLayout(element,*location) + else: + raise ValueError("unrecognized element kind: {}".format(kind)) + else: + idx=location[0] if lkind=="vbox" else location[1] + if lkind=="vbox" and (location[1]!=0 or location[3]!=1): + raise ValueError("vbox layout only has one column") + if lkind=="hbox" and (location[0]!=0 or location[2]!=1): + raise ValueError("hbox layout only has one row") + if kind=="widget": + layout.insertWidget(idx,element) + elif kind=="item": + layout.insertItem(idx,element) + elif kind=="layout": + layout.insertLayout(idx,element) + else: + raise ValueError("unrecognized element kind: {}".format(kind)) + def add_layout_element(self, element, location=None, kind="widget"): + """ + Add an existing `element` to the layout at the given `location`. + + `kind` can be ``"widget"`` for widgets, ``"layout"`` for other layouts, or ``"item"`` for layout items (spacers). + """ + lname,location=self._normalize_location(location) + if location!="skip": + self._insert_layout_element(lname,element,location,kind=kind) + def remove_layout_element(self, element): + """Remove a previously added layout element""" + for layout,_ in self._sublayouts.values(): + idx=utils.find_layout_element(layout,element) + if idx is not None: + utils.delete_layout_item(layout,idx) + return True + return False + def add_sublayout(self, name, kind="grid", location=None): + """ + Add a sublayout to the given location. + + `name` specifies the sublayout name, which can be used to refer to it in specifying locations later. + `kind` can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box). + """ + if name in self._sublayouts: + raise ValueError("sublayout {} already exists".format(name)) + layout=_make_layout(kind) + layout.setContentsMargins(0,0,0,0) + layout.setObjectName(name) + self.add_layout_element(layout,location,kind="layout") + self._sublayouts[name]=(layout,kind) + return layout + @contextlib.contextmanager + def using_new_sublayout(self, name, kind="grid", location=None): + """ + Create a different sublayout and use it as default inside the ``with`` block. + + `kind` can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box). + """ + self.add_sublayout(name,kind=kind,location=location) + with self.using_layout(name): + yield + def get_sublayout(self, name=None): + """Get the previously added sublayout""" + return self._sublayouts[name or self._default_layout][0] + + def add_spacer(self, height=0, width=0, stretch_height=False, stretch_width=False, location="next"): + """ + Add a spacer with the given width and height to the given location. + + If ``stretch_height==True`` or ``stretch_width==True``, the widget will stretch in these directions; otherwise, the widget size is fixed. + """ + spacer=QtWidgets.QSpacerItem(width,height, + QtWidgets.QSizePolicy.MinimumExpanding if stretch_width else QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.MinimumExpanding if stretch_height else QtWidgets.QSizePolicy.Minimum) + self.add_layout_element(spacer,location,kind="item") + self._spacers.append(spacer) # otherwise the reference is lost, and the object might be deleted + return spacer + def add_padding(self, kind="auto", location="next"): + """ + Add a padding (expandable spacer) of the given kind to the given location. + + `kind` can be ``"vertical"``, ``"horizontal"``, ``"auto"`` (vertical for ``grid`` and ``vbox`` layouts, horizontal for ``hbox``), + or ``"both"`` (stretches in both directions). + """ + funcargparse.check_parameter_range(kind,"kind",{"auto","horizontal","vertical","both"}) + if kind=="auto": + lname,_=self._normalize_location(location) + if lname is None: + kind="vertical" + else: + _,lkind=self._sublayouts[lname] + kind="horizontal" if lkind=="hbox" else "vertical" + stretch_height=kind in {"vertical","both"} + stretch_width=kind in {"horizontal","both"} + return self.add_spacer(stretch_height=stretch_height,stretch_width=stretch_width,location=location) + def add_decoration_label(self, text, location="next"): + """Add a decoration text label with the given text""" + label=QtWidgets.QLabel(self) + label.setText(str(text)) + label.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.add_layout_element(label,location) + return label + def insert_row(self, row, sublayout=None): + """Insert a new row at the given location in the grid layout""" + layout,kind=self._sublayouts[sublayout or self._default_layout] + if kind!="grid": + raise ValueError("can add rows only to grid layouts (vbox layouts work automatically)") + utils.insert_layout_row(layout,row%(layout.rowCount() or 1)) + def insert_column(self, col, sublayout=None): + """Insert a new column at the given location in the grid layout""" + layout,kind=self._sublayouts[sublayout or self._default_layout] + if kind!="grid": + raise ValueError("can add columns only to grid layouts (hbox layouts work automatically)") + utils.insert_layout_column(layout,col%(layout.colCount() or 1)) + + def clear(self): + """Clear the layout and remove all the added elements""" + utils.clean_layout(self.main_layout,delete_layout=True) + if self.main_layout is not None: + self._set_main_layout() + self._sublayouts={"main":(self.main_layout,self.main_layout_kind)} + self._spacers=[] \ No newline at end of file diff --git a/pylablib/core/gui/widgets/param_table.py b/pylablib/core/gui/widgets/param_table.py index 05990c3..57e7e19 100644 --- a/pylablib/core/gui/widgets/param_table.py +++ b/pylablib/core/gui/widgets/param_table.py @@ -1,14 +1,16 @@ from . import edit, label as widget_label, combo_box, button as widget_button +from . import layout_manager from ...thread import threadprop, controller -from .. import value_handling as value_handling, utils +from .. import value_handling from ...utils import py3, dictionary -from .. import QtCore, QtWidgets, Signal +from .. import QtWidgets, Signal import collections +import contextlib -class ParamTable(QtWidgets.QWidget): +class ParamTable(layout_manager.QLayoutManagedWidget): """ GUI parameter table. @@ -16,7 +18,7 @@ class ParamTable(QtWidgets.QWidget): Has methods for adding various kinds of controls (labels, edit boxes, combo boxes, check boxes), automatically creates values table for easy settings/getting. - By default supports 2-column (label-control) and 3-column (label-control-indicator) layout, depending on the parameters given to :meth:`setupUi`. + By default supports 2-column (label-control) and 3-column (label-control-indicator) layout, depending on the parameters given to :meth:`setup`. Similar to :class:`.GUIValues`, has three container-like accessor: ``.h`` for getting the value handler @@ -30,20 +32,34 @@ class ParamTable(QtWidgets.QWidget): ``.vs`` for getting the value changed Qt signal (i.e., ``self.get_value_changed_signal(name)`` is equivalent to ``self.s[name]``), - Like most widgets, requires calling :meth:`setupUi` to set up before usage. + Like most widgets, requires calling :meth:`setup` to set up before usage. Args: parent: parent widget """ - def __init__(self, parent=None): + def __init__(self, parent=None, name=None): super().__init__(parent) + self.name=name self.params={} self.h=dictionary.ItemAccessor(self.get_handler) self.w=dictionary.ItemAccessor(self.get_widget) self.v=dictionary.ItemAccessor(self.get_value,self.set_value) self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) self.vs=dictionary.ItemAccessor(self.get_value_changed_signal) - def setupUi(self, name, add_indicator=True, gui_values=None, gui_values_root=None, gui_thread_safe=False, cache_values=False, change_focused_control=False): + self.setup_gui_values("new") + def _set_main_layout(self): + super()._set_main_layout() + self.main_layout.setContentsMargins(5,5,5,5) + self.main_layout.setSpacing(5) + self.main_layout.setColumnStretch(1,1) + def setup_gui_values(self, gui_values=None, gui_values_path=""): + if self.params: + raise RuntimeError("can not change gui values after widgets have been added") + if gui_values is not None: + if gui_values_path is None: + gui_values_path=self.name + self.gui_values,self.gui_values_path=value_handling.get_gui_values(gui_values,gui_values_path) + def setup(self, name, add_indicator=True, gui_values=None, gui_values_path="", gui_thread_safe=False, cache_values=False, change_focused_control=False): """ Setup the table. @@ -51,7 +67,7 @@ def setupUi(self, name, add_indicator=True, gui_values=None, gui_values_root=Non name (str): table widget name add_indicator (bool): if ``True``, add indicators for all added widgets by default. gui_values (bool): as :class:`.GUIValues` object used to access table values; by default, create one internally - gui_values_root (str): if not ``None``, specify root (i.e., path prefix) for values inside the table; + gui_values_path (str): if not ``None``, specifies the path prefix for values inside the table; if not specified, then there's no additional root for internal table (``gui_values is None``), or it is equal to `name` if there is an external table (``gui_values is not None``) gui_thread_safe (bool): if ``True``, all value-access and indicator-access calls @@ -66,19 +82,9 @@ def setupUi(self, name, add_indicator=True, gui_values=None, gui_values_root=Non """ self.name=name self.setObjectName(self.name) - self.formLayout=QtWidgets.QGridLayout(self) - self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) - self.formLayout.setContentsMargins(5,5,5,5) - self.formLayout.setSpacing(5) - self.formLayout.setObjectName(self.name+"_formLayout") - self._sublayouts={} + super().setup() self.add_indicator=add_indicator - if gui_values is None: - self.gui_values=value_handling.GUIValues() - self.gui_values_root="" - else: - self.gui_values=gui_values - self.gui_values_root=gui_values_root if gui_values_root is not None else self.name + self.setup_gui_values(gui_values=gui_values,gui_values_path=gui_values_path) self.gui_thread_safe=gui_thread_safe self.change_focused_control=change_focused_control self.cache_values=cache_values @@ -93,88 +99,22 @@ def _update_cache_values(self, name=None, value=None): # pylint: disable=unused else: self.current_values[name]=self.get_value(name) - def _normalize_location(self, location, default=("next",0,1,1)): - if location=="skip": - return "skip" - if not isinstance(location,(list,tuple)): - location=(location,) - if isinstance(location[0],py3.textstring) and location[0]!="next": - lname,location=location[0],location[1:] - layout,lkind=self._sublayouts[lname] - else: - lname=None - layout,lkind=self.formLayout,"grid" - location+=(None,)*(4-len(location)) - location=[d if l is None else l for (l,d) in zip(location,default)] - row,col,rowspan,colspan=location - if lkind=="grid": - row_cnt,col_cnt=layout.rowCount(),layout.columnCount() - elif lkind=="vbox": - col,colspan,rowspan=0,1,1 - row_cnt,col_cnt=layout.count(),1 - else: - if location[1:]==(None,None,None): - row,col,rowspan,colspan=0,location[0],1,1 - row,colspan,rowspan=0,1,1 - row_cnt,col_cnt=1,layout.count() - row=row_cnt if row=="next" else (row%row_cnt if row<0 else row) - if rowspan=="end": - rowspan=max(row_cnt-row,1) - col=col_cnt if col=="next" else (col%col_cnt if col<0 else col) - if colspan=="end": - colspan=max(col_cnt-col,1) - return lname,(row,col,rowspan,colspan) - def _insert_layout_element(self, lname, element, location, kind="widget"): - if lname is None: - layout,lkind=self.formLayout,"grid" - else: - layout,lkind=self._sublayouts[lname] - if lkind=="grid": - if kind=="widget": - layout.addWidget(element,*location) - elif kind=="item": - layout.addItem(element,*location) - elif kind=="layout": - layout.addLayout(element,*location) - else: - raise ValueError("unrecognized element kind: {}".format(kind)) - else: - idx=location[0] if lkind=="vbox" else location[1] - if lkind=="vbox" and location[0]!=0: - raise ValueError("can't space widgets vertically in a vbox environment") - if kind=="widget": - layout.insertWidget(idx,element) - elif kind=="item": - layout.insertItem(idx,element) - elif kind=="layout": - layout.insertLayout(idx,element) - else: - raise ValueError("unrecognized element kind: {}".format(kind)) - def add_sublayout(self, name, kind="grid", location=("next",0,1,"end")): - """ - Add a sublayout to the given location. - - `name` specifies the sublayout name, which can be used to refer to it in specifying locations later. - `kind` can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box). - """ - if name in self._sublayouts: - raise ValueError("sublayout {} already exists".format(name)) - if kind=="grid": - layout=QtWidgets.QGridLayout(self) - elif kind=="vbox": - layout=QtWidgets.QVBoxLayout(self) - elif kind=="hbox": - layout=QtWidgets.QHBoxLayout(self) - else: - raise ValueError("unrecognized layout kind: {}".format(kind)) - lname,location=self._normalize_location(location) - self._insert_layout_element(lname,layout,location,kind="layout") - self._sublayouts[name]=(layout,kind) - + def add_sublayout(self, name, kind="grid", location=("next",0,1,2)): + return super().add_sublayout(name,kind=kind,location=location) + @contextlib.contextmanager + def using_new_sublayout(self, name, kind="grid", location=("next",0,1,2)): + with super().using_new_sublayout(name,kind=kind,location=location): + yield + + def _normalize_name(self, name): + if isinstance(name,(list,tuple)): + return "/".join(name) + return name ParamRow=collections.namedtuple("ParamRow",["widget","label","indicator","value_handler","indicator_handler"]) def _add_widget(self, name, params, add_change_event=True): + name=self._normalize_name(name) self.params[name]=params - path=(self.gui_values_root,name) + path=(self.gui_values_path,name) self.gui_values.add_handler(path,params.value_handler) if params.indicator_handler: self.gui_values.add_indicator_handler(path,params.indicator_handler) @@ -183,7 +123,7 @@ def _add_widget(self, name, params, add_change_event=True): if self.cache_values: params.value_handler.connect_value_changed_handler(lambda value: self._update_cache_values(name,value),only_signal=False) self._update_cache_values() - def add_simple_widget(self, name, widget, label=None, value_handler=None, add_indicator=None, location="next", tooltip=None, add_change_event=True): + def add_simple_widget(self, name, widget, label=None, value_handler=None, add_indicator=None, location=None, tooltip=None, add_change_event=True): """ Add a 'simple' (single-spaced, single-valued) widget to the table. @@ -193,7 +133,7 @@ def add_simple_widget(self, name, widget, label=None, value_handler=None, add_in label (str): if not ``None``, specifies label to put in front of the widget in the layout value_handler: value handler of the widget; by default, use auto-detected value handler (works for many simple built-in or custom widgets) add_indicator: if ``True``, add an indicator label in the third column and a corresponding indicator handler in the built-in values table; - by default, use the default value supplied to :meth:`setupUi` + by default, use the default value supplied to :meth:`setup` location (tuple): tuple ``(row, column)`` specifying location of the widget (or widget label, if it is specified); by default, add to a new row in the end and into the first column can also be a string ``"skip"``, which means that the widget is added to some other location manually later @@ -203,17 +143,26 @@ def add_simple_widget(self, name, widget, label=None, value_handler=None, add_in Return the widget's value handler """ + name=self._normalize_name(name) if name in self.params: raise KeyError("widget {} already exists".format(name)) if add_indicator is None: add_indicator=self.add_indicator - lname,location=self._normalize_location(location,default=("next",0,1,3 if add_indicator else 2)) + if isinstance(location,dict): + llocation=location.get("label",None) + ilocation=location.get("indicator",None) + location=location.get("widget",None) + else: + ilocation=llocation=None + lname,location=self._normalize_location(location,default_location=("next",0,1,3 if add_indicator else 2)) if location!="skip": row,col,rowspan,colspan=location labelspan=1 if label is not None else 0 indspan=1 if add_indicator else 0 if colspan1 - for ln in self.imgVBLines+self.imgHBLines: + for ln in self.vblines+self.hblines: ln.setPen(self.linecut_boundary_pen if show_boundary_lines else None) if show_boundary_lines: - self.imgVBLines[0].setPos(vpos-cut_width/2) - self.imgVBLines[1].setPos(vpos+cut_width/2) - self.imgHBLines[0].setPos(hpos-cut_width/2) - self.imgHBLines[1].setPos(hpos+cut_width/2) + self.vblines[0].setPos(vpos-cut_width/2) + self.vblines[1].setPos(vpos+cut_width/2) + self.hblines[0].setPos(hpos-cut_width/2) + self.hblines[1].setPos(hpos+cut_width/2) # Update image controls based on PyQtGraph image window @controller.exsafeSlot() @@ -450,8 +438,8 @@ def update_image_controls(self, levels=None): values=self._get_values() if levels is not None: values.v["minlim"],values.v["maxlim"]=levels - values.v["vlinepos"]=self.imgVLine.getPos()[0] - values.v["hlinepos"]=self.imgHLine.getPos()[1] + values.v["vlinepos"]=self.vline.getPos()[0] + values.v["hlinepos"]=self.hline.getPos()[1] self._update_linecut_boundaries(values) def _get_min_nonzero(self, img, default=0): img=img[img!=0] @@ -469,9 +457,9 @@ def _sanitize_img(self, img): # PyQtGraph histogram has an unfortunate failure m img[0,0]+=1 return img def _check_paint_done(self, dt=0): - items=[self.imageWindow.imageItem]+self.cut_lines + items=[self.image_window.imageItem]+self.cut_lines counters=[self._last_img_paint_cnt]+self._last_curve_paint_cnt - if self._last_img_paint_cnt==self.imageWindow.imageItem.paint_cnt: + if self._last_img_paint_cnt==self.image_window.imageItem.paint_cnt: return False for itm,cnt in zip(items,counters): if itm.paint_cnt==cnt: @@ -522,13 +510,13 @@ def update_image(self, update_controls=False, do_redraw=False, only_new_image=Tr draw_img=self._sanitize_img(draw_img) levels=img_levels if autoscale else (values.v["minlim"],values.v["maxlim"]) if self.isVisible(): - self.imageWindow.setImage(draw_img,levels=levels,autoHistogramRange=False) + self.image_window.setImage(draw_img,levels=levels,autoHistogramRange=False) if values.v["auto_histogram_range"]: hist_range=min(img_levels[0],levels[0]),max(img_levels[1],levels[1]) if hist_range[0]==hist_range[1]: hist_range=hist_range[0]-.5,hist_range[1]+.5 - self.imageWindow.ui.histogram.setHistogramRange(*hist_range) - self._last_img_paint_cnt=self.imageWindow.imageItem.paint_cnt + self.image_window.ui.histogram.setHistogramRange(*hist_range) + self._last_img_paint_cnt=self.image_window.imageItem.paint_cnt if update_controls: with self._while_updating(False): self.update_image_controls(levels=levels if autoscale else None) @@ -537,16 +525,16 @@ def update_image(self, update_controls=False, do_redraw=False, only_new_image=Tr values.i["maxlim"]=img_levels[1] values.v["size"]="{} x {}".format(*img_shape) show_lines=values.v["show_lines"] - for ln in [self.imgVLine,self.imgHLine]: + for ln in [self.vline,self.hline]: ln.setPen("g" if show_lines else None) ln.setHoverPen("y" if show_lines else None) ln.setMovable(show_lines) - for ln in [self.imgVLine]+self.imgVBLines: + for ln in [self.vline]+self.vblines: ln.setBounds([0,draw_img.shape[0]]) - for ln in [self.imgHLine]+self.imgHBLines: + for ln in [self.hline]+self.hblines: ln.setBounds([0,draw_img.shape[1]]) - self.imgVLine.setPos(values.v["vlinepos"]) - self.imgHLine.setPos(values.v["hlinepos"]) + self.vline.setPos(values.v["vlinepos"]) + self.hline.setPos(values.v["hlinepos"]) self._update_linecut_boundaries(values) if values.v["show_lines"] and values.v["show_linecuts"]: cut_width=values.v["linecut_width"] @@ -568,16 +556,46 @@ def update_image(self, update_controls=False, do_redraw=False, only_new_image=Tr hmin-=1 x_cut=draw_img[:,hmin:hmax].mean(axis=1) y_cut=draw_img[vmin:vmax,:].mean(axis=0) - autorange=self.plotWindow.getViewBox().autoRangeEnabled() - self.plotWindow.disableAutoRange() + autorange=self.cut_plot_window.getViewBox().autoRangeEnabled() + self.cut_plot_window.disableAutoRange() self.cut_lines[0].setData(np.arange(len(x_cut)),x_cut) self.cut_lines[1].setData(np.arange(len(y_cut)),y_cut) self._last_img_paint_cnt=[cl.paint_cnt for cl in self.cut_lines] if any(autorange): - self.plotWindow.enableAutoRange(x=autorange[0],y=autorange[1]) - self.plotWindow.setVisible(True) + self.cut_plot_window.enableAutoRange(x=autorange[0],y=autorange[1]) + self.cut_plot_window.setVisible(True) else: - self.plotWindow.setVisible(False) + self.cut_plot_window.setVisible(False) self.update_rectangles() self._last_paint_time=time.time() - return values \ No newline at end of file + return values + + + + + + +class ImagePlotterCombined(QWidgetContainer): + """ + A combined panel which includes :class:`ImagePlotter` and :class:`ImagePlotterCtl` in the sidebar. + + The :meth:`setup` method takes parameters both for plotter and controller setup. + In addition, it takes ``ctl_caption`` argument, which, if not ``None``, sets the caption of a group box made around the controller panel. + The plotter can be accessed as ``.plt`` attribute, and the controller as ``.ctl`` attribute. + The ``"sidebar"`` sublayout can be used to add additional elements if necessary. + """ + def setup(self, name, img_size=(1024,1024), min_size=None, ctl_caption=None, gui_values=None, gui_values_path=None, save_values=("colormap","img_lim_preset")): + self.name=name + super().setup("hbox",no_margins=True,gui_values=gui_values,gui_values_path=gui_values_path) + self.plt=ImagePlotter(self) + self.add_layout_element(self.plt) + self.plt.setup("{}_plotter".format(name),img_size=img_size,min_size=min_size) + with self.using_new_sublayout("sidebar","vbox"): + self.ctl=ImagePlotterCtl(self) + if ctl_caption is None: + self.add_widget("{}_control".format(name),self.ctl) + else: + self.add_group_box("{}_control_box".format(name),caption=ctl_caption).add_widget("{}_control".format(name),self.ctl) + self.ctl.setup("{}_control".format(name),self.plt,save_values=save_values) + self.add_padding() + self.get_sublayout().setStretch(0,1) \ No newline at end of file diff --git a/pylablib/gui/widgets/plotters/mpl_plotter.py b/pylablib/gui/widgets/plotters/mpl_plotter.py index a56250f..fe68894 100644 --- a/pylablib/gui/widgets/plotters/mpl_plotter.py +++ b/pylablib/gui/widgets/plotters/mpl_plotter.py @@ -12,7 +12,7 @@ class MPLFigureCanvas(FigureCanvasQTAgg): Simple widget wrapper for MPL plotting canvas. """ def __init__(self, parent=None): - FigureCanvasQTAgg.__init__(self,mpl.Figure()) + super().__init__(mpl.Figure()) if parent: self.setParent(parent) self.redraw_period=0.01 @@ -31,18 +31,18 @@ def redraw(self, force=False): -class MPLFigureToolbarCanvas(QtWidgets.QWidget): +class MPLFigureToolbarCanvas(QtWidgets.QFrame): """ Simple widget wrapper for MPL plotting canvas with the toolbar (for plot zooming/panning) """ def __init__(self, parent=None): - QtWidgets.QFrame.__init__(self,parent) - self.layout=QtWidgets.QVBoxLayout(self) + super().__init__(parent) + self.main_layout=QtWidgets.QVBoxLayout(self) self.canvas=MPLFigureCanvas(self) - self.layout.addWidget(self.canvas) + self.main_layout.addWidget(self.canvas) self.figure=self.canvas.figure self.toolbar=NavigationToolbar(self.canvas,self) - self.layout.addWidget(self.toolbar) + self.main_layout.addWidget(self.toolbar) @property def redraw_period(self): """Set redraw period""" diff --git a/pylablib/gui/widgets/plotters/trace_plotter.py b/pylablib/gui/widgets/plotters/trace_plotter.py index ffd5f27..f00456f 100644 --- a/pylablib/gui/widgets/plotters/trace_plotter.py +++ b/pylablib/gui/widgets/plotters/trace_plotter.py @@ -4,34 +4,32 @@ Has 2 parts: :class:`TracePlotter` which displays the plots, and :class:`TracePlotterCtl` which controls the channels (X-axis, enabled channels) and the plotting (buffer size, updating, etc.) :class:`TracePlotter` can also operate alone without a controller. -When both are used, :class:`TracePlotter` is created and set up first, and then supplied to :meth:`TracePlotterCtl.setupUi` method. +When both are used, :class:`TracePlotter` is created and set up first, and then supplied to :meth:`TracePlotterCtl.setup` method. """ from ....core.gui.widgets.param_table import ParamTable +from ....core.gui.widgets.container import QWidgetContainer +from ....core.gui.widgets.layout_manager import QLayoutManagedWidget from ....core.thread import controller -from ....core.gui import value_handling from ....core.dataproc import utils as trace_utils from ....thread.stream.table_accum import TableAccumulator, TableAccumulatorThread import pyqtgraph -from ....core.gui import QtWidgets, QtCore, Signal +from ....core.gui import QtCore, Signal import numpy as np -class TracePlotterCtl(QtWidgets.QWidget): +class TracePlotterCtl(QWidgetContainer): """ Class for controlling traces inside :class:`TracePlotter`. - Like most widgets, requires calling :meth:`setupUi` to set up before usage. + Like most widgets, requires calling :meth:`setup` to set up before usage. Args: parent: parent widget """ - def __init__(self, parent=None): - super().__init__(parent) - - def setupUi(self, name, plotter, gui_values=None, gui_values_root=None): + def setup(self, name, plotter, gui_values=None, gui_values_path=None): """ Setup the trace plotter controller. @@ -39,60 +37,26 @@ def setupUi(self, name, plotter, gui_values=None, gui_values_root=None): name (str): widget name plotter (TracePlotter): controlled image plotter gui_values (bool): as :class:`.GUIValues` object used to access table values; by default, create one internally - gui_values_root (str): if not ``None``, specify root (i.e., path prefix) for values inside the table. + gui_values_path (str): if not ``None``, specifies the path prefix for values inside the control """ - self.gui_values=gui_values or value_handling.GUIValues() - self.gui_values_root=gui_values_root or "" + self.name=name + super().setup(gui_values=gui_values,gui_values_path=gui_values_path,no_margins=True) self.plotter=plotter self.plotter._attach_controller(self) - - self.name=name - self.setObjectName(self.name) - self.hLayout=QtWidgets.QVBoxLayout(self) - self.hLayout.setContentsMargins(0,0,0,0) - self.hLayout.setObjectName("hLayout") - self.channelsGroupBox=QtWidgets.QGroupBox(self) - self.channelsGroupBox.setObjectName("channelsGroupBox") - self.channelsGroupBox.setTitle("Channels") - self.channelsGroupLayout=QtWidgets.QVBoxLayout(self.channelsGroupBox) - self.channelsGroupLayout.setContentsMargins(0, 0, 0, -1) - self.channelsGroupLayout.setObjectName("channelsGroupLayout") - self.channels_table=ParamTable(self.channelsGroupBox) - self.channels_table.setMinimumSize(QtCore.QSize(20, 20)) - self.channels_table.setObjectName("channels_table") - self.channelsGroupLayout.addWidget(self.channels_table) - self.hLayout.addWidget(self.channelsGroupBox) - self.plottingGroupBox=QtWidgets.QGroupBox(self) - self.plottingGroupBox.setObjectName("plottingGroupBox") - self.plottingGroupBox.setTitle("Plotting") - self.plottingGroupLayout=QtWidgets.QHBoxLayout(self.plottingGroupBox) - self.plottingGroupLayout.setContentsMargins(0, 0, 0, -1) - self.plottingGroupLayout.setObjectName("plottingGroupLayout") - self.plot_params_table=ParamTable(self.plottingGroupBox) - self.plot_params_table.setMinimumSize(QtCore.QSize(20, 20)) - self.plot_params_table.setObjectName("plot_params_table") - self.plottingGroupLayout.addWidget(self.plot_params_table) - self.hLayout.addWidget(self.plottingGroupBox) - spacerItem=QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.hLayout.addItem(spacerItem) - self.hLayout.setStretch(2, 1) - - self.channels_table.setupUi("channels",add_indicator=False,gui_values=self.gui_values,gui_values_root=self.gui_values_root+"/channels") + self.channels_table=ParamTable(self) + self.add_group_box("channels_group_box",caption="Channels").add_widget("channels",self.channels_table,gui_values_path="channels") + self.channels_table.setMinimumSize(QtCore.QSize(20,20)) + self.channels_table.setup("channels",add_indicator=False) self.setup_channels() - self.plot_params_table.setupUi("plotting_params",add_indicator=False,gui_values=self.gui_values,gui_values_root=self.gui_values_root+"/plotting") + self.plot_params_table=ParamTable(self) + self.add_group_box("plotting_group_box",caption="Plotting").add_widget("plotting",self.plot_params_table,gui_values_path="plotting") + self.plot_params_table.setMinimumSize(QtCore.QSize(20,20)) + self.plot_params_table.setup("plotting_params",add_indicator=False) self.plot_params_table.add_toggle_button("update_plot","Updating") self.plot_params_table.add_num_edit("disp_last",1,limiter=(1,None,"coerce","int"),formatter=("int"),label="Display last: ") self.plot_params_table.add_button("reset_history","Reset").get_value_changed_signal().connect(self.plotter.reset_history) - - def get_all_values(self): - """Get all control values""" - return self.gui_values.get_all_values(root=self.gui_values_root) - def set_all_values(self, values): - """Set all control values""" - self.gui_values.set_all_values(values,root=self.gui_values_root) - def get_all_indicators(self): - """Get all GUI indicators as a dictionary""" - return self.gui_values.get_all_indicators(root=self.gui_values_root) + self.add_padding() + self.main_layout.setStretch(2,1) def setup_channels(self): """ @@ -114,7 +78,7 @@ def get_enabled_channels(self): return [idx for idx in self.plotter.channel_indices if self.channels_table.v[idx+"_enabled"]] -class TracePlotter(QtWidgets.QWidget): +class TracePlotter(QLayoutManagedWidget): """ Trace plotter object. @@ -126,10 +90,7 @@ class TracePlotter(QtWidgets.QWidget): Args: parent: parent widget """ - def __init__(self, parent=None): - super().__init__(parent) - - def setupUi(self, name, add_end_marker=False, update_only_on_visible=True): + def setup(self, name, add_end_marker=False, update_only_on_visible=True): """ Setup the image view. @@ -139,16 +100,12 @@ def setupUi(self, name, add_end_marker=False, update_only_on_visible=True): update_only_on_visible (bool): if ``True``, only update plot if the widget is visible. """ self.name=name - self.setObjectName(self.name) - self.hLayout=QtWidgets.QVBoxLayout(self) - self.hLayout.setContentsMargins(0,0,0,0) - self.hLayout.setObjectName("layout") - self.plotWidget=pyqtgraph.PlotWidget(self) - self.plotWidget.setObjectName("plotWidget") - self.hLayout.addWidget(self.plotWidget) - self.plotWidget.addLegend() - self.plotWidget.setLabel("left","Signal") - self.plotWidget.showGrid(True,True,0.7) + super().setup("vbox",no_margins=True) + self.plot_widget=pyqtgraph.PlotWidget(self) + self.add_layout_element(self.plot_widget) + self.plot_widget.addLegend() + self.plot_widget.setLabel("left","Signal") + self.plot_widget.showGrid(True,True,0.7) self.ctl=None self.channels={} @@ -165,7 +122,7 @@ def _attach_controller(self, ctl): """ Attach :class:`TracePlotterCtl` object. - Called automatically in :meth:`TracePlotterCtl.setupUi`, doesn't need to be called explicitly. + Called automatically in :meth:`TracePlotterCtl.setup`, doesn't need to be called explicitly. """ self.ctl=ctl @@ -180,7 +137,7 @@ def setup_channels(self, channels, channel_indices=None): ``"factor"`` - rescaling factor applied before plotting. """ self.channels=channels.copy() - self.channel_indices=channel_indices or sorted(channels.keys()) + self.channel_indices=channel_indices or list(channels.keys()) mpl_colors=['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#bcbd22','#17becf'] old_style_colors=['#40FF40','#4040FF','#FF4040','#FFFF00','#00FFFF','#FF00FF','#C0C0C0','#404040'] colors=(mpl_colors+old_style_colors)*len(channels) @@ -218,16 +175,16 @@ def _update_plot_lines(self): """ for el in self.vlines+self.vmarks: if el is not None: - self.plotWidget.plotItem.legend.removeItem(el.name()) - self.plotWidget.removeItem(el) + self.plot_widget.plotItem.legend.removeItem(el.name()) + self.plot_widget.removeItem(el) self.vlines=[] self.vmarks=[] for idx in self.displayed: ch=self.channels[idx] - vl=self.plotWidget.plot([],[],pen=ch["color"],name=ch["legend_name"]) + vl=self.plot_widget.plot([],[],pen=ch["color"],name=ch["legend_name"]) self.vlines.append(vl) if ch.get("end_marker",self.add_end_marker): - vm=self.plotWidget.plot([],[],symbolBrush=ch["color"],symbol="o",symbolSize=5,pxMode=True) + vm=self.plot_widget.plot([],[],symbolBrush=ch["color"],symbol="o",symbolSize=5,pxMode=True) self.vmarks.append(vm) else: self.vmarks.append(None) @@ -334,8 +291,8 @@ def update_plot(self, data=None, idx_column=None): norm_data=np.column_stack(norm_data) norm_data=trace_utils.sort_by(norm_data,x_column=1,stable=True) norm_data=[norm_data[:,c] for c in range(norm_data.shape[1])] - autorange=self.plotWidget.plotItem.getViewBox().autoRangeEnabled() - self.plotWidget.plotItem.disableAutoRange() + autorange=self.plot_widget.plotItem.getViewBox().autoRangeEnabled() + self.plot_widget.plotItem.disableAutoRange() for vl,col in zip(self.vlines,norm_data[2:]): vl.setData(norm_data[0],col) if last_pts: @@ -343,5 +300,31 @@ def update_plot(self, data=None, idx_column=None): if vm is not None: vm.setData([last_pts[0]],[pt]) if any(autorange): - self.plotWidget.plotItem.enableAutoRange(x=autorange[0],y=autorange[1]) - self.plotWidget.setLabel("bottom",self.channels[xaxis]["legend_name"]) \ No newline at end of file + self.plot_widget.plotItem.enableAutoRange(x=autorange[0],y=autorange[1]) + self.plot_widget.setLabel("bottom",self.channels[xaxis]["legend_name"]) + + + + + + +class TracePlotterCombined(QWidgetContainer): + """ + A combined panel which includes :class:`TracePlotter` and :class:`TracePlotterCtl` in the sidebar. + + The :meth:`setup` method takes parameters both for plotter and controller setup. + The plotter can be accessed as ``.plt`` attribute, and the controller as ``.ctl`` attribute. + The ``"sidebar"`` sublayout can be used to add additional elements if necessary. + """ + def setup(self, name, add_end_marker=False, update_only_on_visible=True, gui_values=None, gui_values_path=None): + self.name=name + super().setup("hbox",no_margins=True,gui_values=gui_values,gui_values_path=gui_values_path) + self.plt=TracePlotter(self) + self.add_layout_element(self.plt) + self.plt.setup("{}_plotter".format(name),add_end_marker=add_end_marker,update_only_on_visible=update_only_on_visible) + with self.using_new_sublayout("sidebar","vbox"): + self.ctl=TracePlotterCtl(self) + self.add_widget("{}_control".format(name),self.ctl) + self.ctl.setup("{}_control".format(name),self.plt) + self.add_padding() + self.get_sublayout().setStretch(0,1) \ No newline at end of file diff --git a/pylablib/gui/widgets/range_controls.py b/pylablib/gui/widgets/range_controls.py index 71f4442..f0efade 100644 --- a/pylablib/gui/widgets/range_controls.py +++ b/pylablib/gui/widgets/range_controls.py @@ -1,6 +1,6 @@ from ...core.gui import QtCore, QtWidgets, Signal -from ...core.gui.widgets.edit import NumEdit +from ...core.gui.widgets.param_table import ParamTable from ...core.utils.numerical import limit_to_range import collections @@ -16,7 +16,7 @@ class RangeCtl(QtWidgets.QWidget): Can have any subset of 3 rows: specifying min-max, specifying center-span (connected to min-max), and specifying step. - Like most widgets, requires calling :meth:`setupUi` to set up before usage. + Like most complex widgets, requires calling :meth:`setup` to set up before usage. Args: parent: parent widget @@ -25,9 +25,9 @@ class RangeCtl(QtWidgets.QWidget): - ``value_changed``: emitted when the value is changed """ def __init__(self, parent=None): - QtWidgets.QWidget.__init__(self,parent) + super().__init__(parent) - def setupUi(self, name, lim=(None,None), order=True, formatter="float", labels=("Min","Max","Center","Span","Step"), elements=("minmax","cspan","step")): + def setup(self, name, lim=(None,None), order=True, formatter=".1f", labels=("Min","Max","Center","Span","Step"), elements=("minmax","cspan","step")): """ Setup the range control. @@ -44,68 +44,27 @@ def setupUi(self, name, lim=(None,None), order=True, formatter="float", labels=( self.order=order self.rng=(0,0,0) if "step" in elements else (0,0) self.setObjectName(self.name) - self.gridLayout=QtWidgets.QGridLayout(self) - self.gridLayout.setObjectName("gridLayout") - row=0 + self.main_layout=QtWidgets.QGridLayout(self) + self.main_layout.setObjectName("main_layout") + self.main_layout.setContentsMargins(0,0,0,0) + self.params=ParamTable(self) + self.main_layout.addWidget(self.params) + self.params.setup("params",add_indicator=False,change_focused_control=True) + self.params.main_layout.setContentsMargins(2,2,2,2) + self.params.main_layout.setSpacing(4) if "minmax" in elements: - self.labelMin=QtWidgets.QLabel(self) - self.labelMin.setObjectName("labelMin") - self.labelMin.setText(labels[0]) - self.gridLayout.addWidget(self.labelMin, row, 0, 1, 1) - self.labelMax=QtWidgets.QLabel(self) - self.labelMax.setObjectName("labelMax") - self.labelMax.setText(labels[1]) - self.gridLayout.addWidget(self.labelMax, row, 2, 1, 1) - self.e_min=NumEdit(self) - self.e_min.setObjectName("e_min") - self.gridLayout.addWidget(self.e_min, row, 1, 1, 1) - self.e_max=NumEdit(self) - self.e_max.setObjectName("e_max") - self.gridLayout.addWidget(self.e_max, row, 3, 1, 1) - self.e_min.value_changed.connect(self._minmax_changed) - self.e_max.value_changed.connect(self._minmax_changed) - row+=1 - else: - self.e_min=None - self.e_max=None + self.params.add_num_edit("min",formatter=formatter,label=labels[0]) + self.params.add_num_edit("max",formatter=formatter,label=labels[1],location=(-1,2)) + self.params.vs["min"].connect(self._minmax_changed) + self.params.vs["max"].connect(self._minmax_changed) if "cspan" in elements: - self.labelCent=QtWidgets.QLabel(self) - self.labelCent.setObjectName("labelCent") - self.labelCent.setText(labels[2]) - self.gridLayout.addWidget(self.labelCent, row, 0, 1, 1) - self.labelSpan=QtWidgets.QLabel(self) - self.labelSpan.setObjectName("labelSpan") - self.labelSpan.setText(labels[3]) - self.gridLayout.addWidget(self.labelSpan, row, 2, 1, 1) - self.e_cent=NumEdit(self) - self.e_cent.setObjectName("e_cent") - self.gridLayout.addWidget(self.e_cent, row, 1, 1, 1) - self.e_span=NumEdit(self) - self.e_span.setObjectName("e_span") - self.gridLayout.addWidget(self.e_span, row, 3, 1, 1) - self.e_cent.value_changed.connect(self._cspan_changed) - self.e_span.value_changed.connect(self._cspan_changed) - row+=1 - else: - self.e_cent=None - self.e_span=None + self.params.add_num_edit("cent",formatter=formatter,label=labels[2]) + self.params.add_num_edit("span",formatter=formatter,label=labels[3],location=(-1,2)) + self.params.vs["cent"].connect(self._cspan_changed) + self.params.vs["span"].connect(self._cspan_changed) if "step" in elements: - self.labelStep=QtWidgets.QLabel(self) - self.labelStep.setObjectName("labelStep") - self.labelStep.setText(labels[4]) - self.gridLayout.addWidget(self.labelStep, row, 0, 1, 1) - self.e_step=NumEdit(self) - self.e_step.setObjectName("e_step") - self.gridLayout.addWidget(self.e_step, row, 1, 1, 1) - self.e_step.value_changed.connect(self._step_changed) - self.e_step.set_limiter((0,None)) - row+=1 - else: - self.e_step=None - self.gridLayout.setContentsMargins(2,2,2,2) - for v in [self.e_min,self.e_max,self.e_cent,self.e_span,self.e_step]: - if v: - v.change_formatter(formatter) + self.params.add_num_edit("step",formatter=formatter,limiter=(0,None),label=labels[4]) + self.params.vs["step"].connect(self._step_changed) self._show_values(self.rng) self.set_limit(lim) @@ -114,20 +73,20 @@ def _limit_range(self, rng): vmax=limit_to_range(rng[1],*self.lim) if self.order: vmin,vmax=min(vmin,vmax),max(vmin,vmax) - if self.e_step is None: - return (vmin,vmax) - else: + if "step" in self.params: step=max(0,rng[2]) if len(rng)>2 else 0 return (vmin,vmax,step) + else: + return (vmin,vmax) def _minmax_changed(self): - rng=(self.e_min.get_value(),self.e_max.get_value())+self.rng[2:] + rng=(self.params.v["min"],self.params.v["max"])+self.rng[2:] self.set_value(rng) def _cspan_changed(self): - cent,span=self.e_cent.get_value(),self.e_span.get_value() + cent,span=self.params.v["cent"],self.params.v["span"] rng=((cent-span/2.),(cent+span/2.))+self.rng[2:] self.set_value(rng) def _step_changed(self): - rng=self.rng[0],self.rng[1],self.e_step.get_value() + rng=self.rng[0],self.rng[1],self.params.v["step"] self.set_value(rng) def set_limit(self, lim): @@ -140,14 +99,14 @@ def get_value(self): """Get current range value (3-tuple ``(left, right, step)`` if step is included, or 2-tuple ``(left, right)`` if it's not)""" return self.rng def _show_values(self, rng): - if self.e_min: - self.e_min.set_value(rng[0],notify_value_change=False) - self.e_max.set_value(rng[1],notify_value_change=False) - if self.e_cent: - self.e_cent.set_value((rng[0]+rng[1])/2.,notify_value_change=False) - self.e_span.set_value(rng[1]-rng[0],notify_value_change=False) - if self.e_step: - self.e_step.set_value(rng[2],notify_value_change=False) + if "min" in self.params: + self.params.w["min"].set_value(rng[0],notify_value_change=False) + self.params.w["max"].set_value(rng[1],notify_value_change=False) + if "cent" in self.params: + self.params.w["cent"].set_value((rng[0]+rng[1])/2.,notify_value_change=False) + self.params.w["span"].set_value(rng[1]-rng[0],notify_value_change=False) + if "step" in self.params: + self.params.w["step"].set_value(rng[2],notify_value_change=False) def set_value(self, rng, notify_value_change=True): """ Get current range value @@ -156,9 +115,9 @@ def set_value(self, rng, notify_value_change=True): If ``notify_value_change==True``, emit the `value_changed` signal; otherwise, change value silently. """ rng=self._limit_range(rng) + self._show_values(rng) if self.rng!=rng: self.rng=rng - self._show_values(rng) if notify_value_change: self.value_changed.emit(rng) @@ -176,9 +135,9 @@ class ROICtl(QtWidgets.QWidget): """ Class for ROI control. - Has 2 rows (for X and Y coordinates), each with 2 numerical edits: min and max (or width, depending on :func:`setupUi` parameters). + Has 2 rows (for X and Y coordinates), each with 2 numerical edits: min and max (or width, depending on :func:`setup` parameters). - Like most widgets, requires calling :meth:`setupUi` to set up before usage. + Like most complex widgets, requires calling :meth:`setup` to set up before usage. Args: parent: parent widget @@ -187,7 +146,7 @@ class ROICtl(QtWidgets.QWidget): value_changed: signal emitted when the ROI value is changed """ def __init__(self, parent=None): - QtWidgets.QWidget.__init__(self,parent) + super().__init__(parent) self.xparams=TAxisParams(0,1) self.yparams=TAxisParams(0,1) self.validate=None @@ -217,7 +176,7 @@ def validateROI(self, xparams, yparams): xparams=TAxisParams(*xparams) yparams=TAxisParams(*yparams) return xparams,yparams - def setupUi(self, name, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): + def setup(self, name, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): """ Setup the ROI control. @@ -236,57 +195,31 @@ def setupUi(self, name, xlim=(0,None), ylim=None, minsize=0, maxsize=None, label self.setObjectName(self.name) self.setMinimumSize(QtCore.QSize(100,60)) self.setMaximumSize(QtCore.QSize(2**16,60)) - self.gridLayout=QtWidgets.QGridLayout(self) - self.gridLayout.setObjectName("gridLayout") - self.labelROI=QtWidgets.QLabel(self) - self.labelROI.setObjectName("labelROI") - self.labelROI.setText("ROI") - self.gridLayout.addWidget(self.labelROI,0,0,1,1) - self.labelMin=QtWidgets.QLabel(self) - self.labelMin.setObjectName("labelMin") - self.labelMin.setText("Min") - self.gridLayout.addWidget(self.labelMin,0,1,1,1) - self.labelMax=QtWidgets.QLabel(self) - self.labelMax.setObjectName("labelMax") - self.labelMax.setText("Max" if kind=="minmax" else "Size") - self.gridLayout.addWidget(self.labelMax,0,2,1,1) - self.labelX=QtWidgets.QLabel(self) - self.labelX.setObjectName("labelX") - self.labelX.setText(labels[0]) - self.gridLayout.addWidget(self.labelX,1,0,1,1) - self.labelY=QtWidgets.QLabel(self) - self.labelY.setObjectName("labelY") - self.labelY.setText(labels[1]) - self.gridLayout.addWidget(self.labelY,2,0,1,1) - self.x_min=NumEdit(self) - self.x_min.setObjectName("x_min") - self.gridLayout.addWidget(self.x_min,1,1,1,1) - self.x_max=NumEdit(self) - self.x_max.setObjectName("x_max") - self.gridLayout.addWidget(self.x_max,1,2,1,1) - self.y_min=NumEdit(self) - self.y_min.setObjectName("y_min") - self.gridLayout.addWidget(self.y_min,2,1,1,1) - self.y_max=NumEdit(self) - self.y_max.setObjectName("y_max") - self.gridLayout.addWidget(self.y_max,2,2,1,1) - self.gridLayout.setContentsMargins(0,0,0,0) + self.main_layout=QtWidgets.QVBoxLayout(self) + self.main_layout.setObjectName("main_layout") + self.main_layout.setContentsMargins(0,0,0,0) self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) - self.gridLayout.setSpacing(4) - self.gridLayout.setColumnStretch(1,2) - self.gridLayout.setColumnStretch(2,2) + self.params=ParamTable(self) + self.main_layout.addWidget(self.params) + self.params.setup("params",add_indicator=False) + self.params.main_layout.setContentsMargins(0,0,0,0) + self.params.main_layout.setSpacing(4) + self.params.add_decoration_label("ROI",(0,0)) + self.params.add_decoration_label("Min",(0,1)) + self.params.add_decoration_label("Max" if kind=="minmax" else "Size",(0,2)) + self.params.add_decoration_label(labels[0],(1,0)) + self.params.add_decoration_label(labels[1],(2,0)) + self.params.add_num_edit("x_min",value=0,formatter="int",limiter=(None,None,"coerce","int"),location=(1,1,1,1)) + self.params.add_num_edit("x_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(1,2,1,1)) + self.params.add_num_edit("y_min",value=0,formatter="int",limiter=(None,None,"coerce","int"),location=(2,1,1,1)) + self.params.add_num_edit("y_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,2,1,1)) + self.params.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) + self.params.main_layout.setColumnStretch(1,1) + self.params.main_layout.setColumnStretch(2,1) self.validate=validate - for v in [self.x_min,self.y_min]: - v.set_formatter("int") - v.set_limiter((None,None,"coerce","int")) - v.set_value(0) - for v in [self.x_max,self.x_bin,self.y_max]: - v.set_formatter("int") - v.set_limiter((None,None,"coerce","int")) - v.set_value(1) - for v in [self.x_min,self.x_max,self.x_bin,self.y_min,self.y_max]: - v.setMinimumWidth(40) - v.value_changed.connect(self._on_edit) + for n in ["x_min","x_max","y_min","y_max"]: + self.params.w[n].setMinimumWidth(40) + self.params.vs[n].connect(self._on_edit) self.set_limits(xlim,ylim,minsize=minsize,maxsize=maxsize) @@ -305,12 +238,10 @@ def set_limits(self, xlim="keep", ylim="keep", minsize="keep", maxsize="keep"): self.minsize=minsize if maxsize!="keep": self.maxsize=maxsize - for v in [self.x_min,self.x_max]: - v.set_limiter((self.xlim[0],self.xlim[1],"coerce","int")) - for v in [self.y_min,self.y_max]: - v.set_limiter((self.ylim[0],self.ylim[1],"coerce","int")) - for v in [self.x_bin,self.y_bin]: - v.set_limiter((1,self.maxbin,"coerce","int")) + for n in ["x_min","x_max"]: + self.params.w[n].set_limiter((self.xlim[0],self.xlim[1],"coerce","int")) + for n in ["y_min","y_max"]: + self.params.w[n].set_limiter((self.ylim[0],self.ylim[1],"coerce","int")) self._show_values(*self.get_value()) value_changed=Signal(object) @@ -326,23 +257,23 @@ def get_value(self): Return tuple ``(xparams, yparams)`` of two axes parameters (each is a 2-tuple ``(min, max)``). """ if self.kind=="minmax": - xparams=TAxisParams(self.x_min.get_value(),self.x_max.get_value()) - yparams=TAxisParams(self.y_min.get_value(),self.y_max.get_value()) + xparams=TAxisParams(self.params.v["x_min"],self.params.v["x_max"]) + yparams=TAxisParams(self.params.v["y_min"],self.params.v["y_max"]) else: - xmin=self.x_min.get_value() - ymin=self.y_min.get_value() - xparams=TAxisParams(xmin,xmin+self.x_max.get_value()) - yparams=TAxisParams(ymin,ymin+self.y_max.get_value()) + xmin=self.params.v["x_min"] + ymin=self.params.v["y_min"] + xparams=TAxisParams(xmin,xmin+self.params.v["x_max"]) + yparams=TAxisParams(ymin,ymin+self.params.v["y_max"]) return self.validateROI(xparams,yparams) def _show_values(self, xparams, yparams): if self.kind=="minmax": xmax,ymax=xparams.max,yparams.max else: xmax,ymax=xparams.max-xparams.min,yparams.max-yparams.min - self.x_min.set_value(xparams.min,notify_value_change=False) - self.x_max.set_value(xmax,notify_value_change=False) - self.y_min.set_value(yparams.min,notify_value_change=False) - self.y_max.set_value(ymax,notify_value_change=False) + self.params.w["x_min"].set_value(xparams.min,notify_value_change=False) + self.params.w["x_max"].set_value(xmax,notify_value_change=False) + self.params.w["y_min"].set_value(yparams.min,notify_value_change=False) + self.params.w["y_max"].set_value(ymax,notify_value_change=False) def set_value(self, roi, notify_value_change=True): """ Set ROI value. @@ -360,13 +291,13 @@ def set_value(self, roi, notify_value_change=True): TBinAxisParams=collections.namedtuple("TBinAxisParams",["min","max","bin"]) -class BinROICtl(ROICtl): +class BinROICtl(QtWidgets.QWidget): """ Class for ROI control with binning. - Has 2 rows (for X and Y coordinates), each with 3 numerical edits: min, max (or width, depending on :func:`setupUi` parameters), and bin. + Has 2 rows (for X and Y coordinates), each with 3 numerical edits: min, max (or width, depending on :func:`setup` parameters), and bin. - Like most widgets, requires calling :meth:`setupUi` to set up before usage. + Like most complex widgets, requires calling :meth:`setup` to set up before usage. Args: parent: parent widget @@ -375,9 +306,13 @@ class BinROICtl(ROICtl): value_changed: signal emitted when the ROI value is changed """ def __init__(self, parent=None): - ROICtl.__init__(self,parent) + super().__init__(parent) self.xparams=TBinAxisParams(0,1,1) self.yparams=TBinAxisParams(0,1,1) + self.validate=None + self.xlim=(0,None) + self.ylim=(0,None) + self.minsize=0 self.maxbin=None def _limit_range(self, rng, lim, maxbin, minsize, maxsize): @@ -403,7 +338,7 @@ def validateROI(self, xparams, yparams): xparams=TBinAxisParams(*xparams) yparams=TBinAxisParams(*yparams) return xparams,yparams - def setupUi(self, name, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, kind="minmax", validate=None): + def setup(self, name, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): """ Setup the ROI control. @@ -423,68 +358,34 @@ def setupUi(self, name, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsiz self.setObjectName(self.name) self.setMinimumSize(QtCore.QSize(100,60)) self.setMaximumSize(QtCore.QSize(2**16,60)) - self.gridLayout=QtWidgets.QGridLayout(self) - self.gridLayout.setObjectName("gridLayout") - self.labelROI=QtWidgets.QLabel(self) - self.labelROI.setObjectName("labelROI") - self.labelROI.setText("ROI") - self.gridLayout.addWidget(self.labelROI,0,0,1,1) - self.labelMin=QtWidgets.QLabel(self) - self.labelMin.setObjectName("labelMin") - self.labelMin.setText("Min") - self.gridLayout.addWidget(self.labelMin,0,1,1,1) - self.labelMax=QtWidgets.QLabel(self) - self.labelMax.setObjectName("labelMax") - self.labelMax.setText("Max" if kind=="minmax" else "Size") - self.gridLayout.addWidget(self.labelMax,0,2,1,1) - self.labelBin=QtWidgets.QLabel(self) - self.labelBin.setObjectName("labelBin") - self.labelBin.setText("Bin") - self.gridLayout.addWidget(self.labelBin,0,3,1,1) - self.labelX=QtWidgets.QLabel(self) - self.labelX.setObjectName("labelX") - self.labelX.setText("X") - self.gridLayout.addWidget(self.labelX,1,0,1,1) - self.labelY=QtWidgets.QLabel(self) - self.labelY.setObjectName("labelY") - self.labelY.setText("Y") - self.gridLayout.addWidget(self.labelY,2,0,1,1) - self.x_min=NumEdit(self) - self.x_min.setObjectName("x_min") - self.gridLayout.addWidget(self.x_min,1,1,1,1) - self.x_max=NumEdit(self) - self.x_max.setObjectName("x_max") - self.gridLayout.addWidget(self.x_max,1,2,1,1) - self.x_bin=NumEdit(self) - self.x_bin.setObjectName("x_bin") - self.gridLayout.addWidget(self.x_bin,1,3,1,1) - self.y_min=NumEdit(self) - self.y_min.setObjectName("y_min") - self.gridLayout.addWidget(self.y_min,2,1,1,1) - self.y_max=NumEdit(self) - self.y_max.setObjectName("y_max") - self.gridLayout.addWidget(self.y_max,2,2,1,1) - self.y_bin=NumEdit(self) - self.y_bin.setObjectName("y_bin") - self.gridLayout.addWidget(self.y_bin,2,3,1,1) - self.gridLayout.setContentsMargins(0,0,0,0) - self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) - self.gridLayout.setSpacing(4) - self.gridLayout.setColumnStretch(1,2) - self.gridLayout.setColumnStretch(2,2) - self.gridLayout.setColumnStretch(3,1) + self.main_layout=QtWidgets.QGridLayout(self) + self.main_layout.setObjectName(self.name+"_main_layout") + self.main_layout.setContentsMargins(0,0,0,0) + self.params=ParamTable(self) + self.main_layout.addWidget(self.params) + self.params.setup("params",add_indicator=False) + self.params.main_layout.setContentsMargins(0,0,0,0) + self.params.main_layout.setSpacing(4) + self.params.add_decoration_label("ROI",(0,0)) + self.params.add_decoration_label("Min",(0,1)) + self.params.add_decoration_label("Max" if kind=="minmax" else "Size",(0,2)) + self.params.add_decoration_label("Bin",(0,3)) + self.params.add_decoration_label(labels[0],(1,0)) + self.params.add_decoration_label(labels[1],(2,0)) + self.params.add_num_edit("x_min",value=0,formatter="int",limiter=(None,None,"coerce","int"),location=(1,1,1,1)) + self.params.add_num_edit("x_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(1,2,1,1)) + self.params.add_num_edit("x_bin",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(1,3,1,1)) + self.params.add_num_edit("y_min",value=0,formatter="int",limiter=(None,None,"coerce","int"),location=(2,1,1,1)) + self.params.add_num_edit("y_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,2,1,1)) + self.params.add_num_edit("y_bin",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,3,1,1)) + self.params.main_layout.setColumnStretch(1,2) + self.params.main_layout.setColumnStretch(2,2) + self.params.main_layout.setColumnStretch(3,1) + self.params.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) self.validate=validate - for v in [self.x_min,self.y_min]: - v.set_formatter("int") - v.set_limiter((None,None,"coerce","int")) - v.set_value(0) - for v in [self.x_max,self.x_bin,self.y_max,self.y_bin]: - v.set_formatter("int") - v.set_limiter((None,None,"coerce","int")) - v.set_value(1) - for v in [self.x_min,self.x_max,self.x_bin,self.y_min,self.y_max,self.y_bin]: - v.setMinimumWidth(40) - v.value_changed.connect(self._on_edit) + for n in ["x_min","x_max","x_bin","y_min","y_max","y_bin"]: + self.params.w[n].setMinimumWidth(30) + self.params.vs[n].connect(self._on_edit) self.set_limits(xlim,ylim,maxbin=maxbin,minsize=minsize,maxsize=maxsize) @@ -505,14 +406,20 @@ def set_limits(self, xlim="keep", ylim="keep", maxbin="keep", minsize="keep", ma self.minsize=minsize if maxsize!="keep": self.maxsize=maxsize - for v in [self.x_min,self.x_max]: - v.set_limiter((self.xlim[0],self.xlim[1],"coerce","int")) - for v in [self.y_min,self.y_max]: - v.set_limiter((self.ylim[0],self.ylim[1],"coerce","int")) - for v in [self.x_bin,self.y_bin]: - v.set_limiter((1,self.maxbin,"coerce","int")) + for n in ["x_min","x_max"]: + self.params.w[n].set_limiter((self.xlim[0],self.xlim[1],"coerce","int")) + for n in ["y_min","y_max"]: + self.params.w[n].set_limiter((self.ylim[0],self.ylim[1],"coerce","int")) + for n in ["x_bin","y_bin"]: + self.params.w[n].set_limiter((1,self.maxbin,"coerce","int")) self._show_values(*self.get_value()) + value_changed=Signal(object) + def _on_edit(self): + params=self.get_value() + self._show_values(*params) + self.value_changed.emit(params) + def get_value(self): """ Get ROI value. @@ -520,25 +427,25 @@ def get_value(self): Return tuple ``(xparams, yparams)`` of two axes parameters (each is a 3-tuple ``(min, max, bin)``). """ if self.kind=="minmax": - xparams=TBinAxisParams(self.x_min.get_value(),self.x_max.get_value(),self.x_bin.get_value()) - yparams=TBinAxisParams(self.y_min.get_value(),self.y_max.get_value(),self.y_bin.get_value()) + xparams=TBinAxisParams(self.params.v["x_min"],self.params.v["x_max"],self.params.v["x_bin"]) + yparams=TBinAxisParams(self.params.v["y_min"],self.params.v["y_max"],self.params.v["y_bin"]) else: - xmin=self.x_min.get_value() - ymin=self.y_min.get_value() - xparams=TBinAxisParams(xmin,xmin+self.x_max.get_value(),self.x_bin.get_value()) - yparams=TBinAxisParams(ymin,ymin+self.y_max.get_value(),self.y_bin.get_value()) + xmin=self.params.v["x_min"] + ymin=self.params.v["y_min"] + xparams=TBinAxisParams(xmin,xmin+self.params.v["x_max"],self.params.v["x_bin"]) + yparams=TBinAxisParams(ymin,ymin+self.params.v["y_max"],self.params.v["y_bin"]) return self.validateROI(xparams,yparams) def _show_values(self, xparams, yparams): if self.kind=="minmax": xmax,ymax=xparams.max,yparams.max else: xmax,ymax=xparams.max-xparams.min,yparams.max-yparams.min - self.x_min.set_value(xparams.min,notify_value_change=False) - self.x_max.set_value(xmax,notify_value_change=False) - self.x_bin.set_value(xparams.bin,notify_value_change=False) - self.y_min.set_value(yparams.min,notify_value_change=False) - self.y_max.set_value(ymax,notify_value_change=False) - self.y_bin.set_value(yparams.bin,notify_value_change=False) + self.params.w["x_min"].set_value(xparams.min,notify_value_change=False) + self.params.w["x_max"].set_value(xmax,notify_value_change=False) + self.params.w["x_bin"].set_value(xparams.bin,notify_value_change=False) + self.params.w["y_min"].set_value(yparams.min,notify_value_change=False) + self.params.w["y_max"].set_value(ymax,notify_value_change=False) + self.params.w["y_bin"].set_value(yparams.bin,notify_value_change=False) def set_value(self, roi, notify_value_change=True): """ Set ROI value. From 6ae8982130b42abe4d3e411d387d99abf8a32d7f Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Tue, 1 Jun 2021 23:14:05 +0200 Subject: [PATCH 02/31] Restructured shortcut package imports --- pylablib/__init__.py | 7 +++- pylablib/core/__init__.py | 6 ---- pylablib/core/dataproc/__export__.py | 16 +++++++++ pylablib/core/dataproc/__init__.py | 33 ----------------- pylablib/core/devio/__export__.py | 5 +++ pylablib/core/devio/__init__.py | 12 ------- pylablib/core/fileio/__export__.py | 7 ++++ pylablib/core/fileio/__init__.py | 16 --------- pylablib/core/gui/__export__.py | 6 ++++ pylablib/core/gui/__init__.py | 19 +++------- pylablib/core/gui/widgets/__export__.py | 5 +++ pylablib/core/gui/widgets/__init__.py | 5 --- pylablib/core/thread/__export__.py | 8 +++++ pylablib/core/thread/__init__.py | 13 ------- pylablib/core/utils/__export__.py | 24 +++++++++++++ pylablib/core/utils/__init__.py | 35 ------------------- pylablib/devices/AWG/generic.py | 6 ++-- pylablib/devices/Andor/base.py | 2 +- .../devices/Arcus/ArcusPerformaxDriver_lib.py | 2 +- pylablib/devices/Arduino/base.py | 6 ++-- pylablib/devices/Attocube/base.py | 2 +- pylablib/devices/Conrad/base.py | 6 ++-- pylablib/devices/Cryomagnetics/base.py | 6 ++-- pylablib/devices/DCAM/dcamapi4_lib.py | 2 +- pylablib/devices/HighFinesse/wlmData_lib.py | 2 +- pylablib/devices/IMAQ/niimaq_lib.py | 2 +- pylablib/devices/IMAQdx/NIIMAQdx_lib.py | 2 +- pylablib/devices/Lakeshore/base.py | 6 ++-- pylablib/devices/LaserQuantum/base.py | 6 ++-- pylablib/devices/LighthousePhotonics/base.py | 6 ++-- pylablib/devices/M2/solstis.py | 6 ++-- pylablib/devices/NI/daq.py | 6 ++-- pylablib/devices/OZOptics/base.py | 6 ++-- pylablib/devices/Ophir/base.py | 6 ++-- pylablib/devices/PCO/sc2_camexport_lib.py | 2 +- pylablib/devices/Pfeiffer/base.py | 6 ++-- pylablib/devices/PhotonFocus/PhotonFocus.py | 2 +- pylablib/devices/PhotonFocus/pfcam_lib.py | 2 +- pylablib/devices/SmarAct/SCU3DControl_lib.py | 2 +- pylablib/devices/Tektronix/base.py | 6 ++-- pylablib/devices/Thorlabs/base.py | 2 +- .../devices/Thorlabs/tl_camera_sdk_lib.py | 2 +- pylablib/devices/Trinamic/base.py | 6 ++-- pylablib/devices/interface/camera.py | 8 ++--- pylablib/devices/uc480/uc480_lib.py | 2 +- pylablib/gui/widgets/__export__.py | 4 +++ pylablib/gui/widgets/__init__.py | 2 -- pylablib/gui/widgets/plotters/__init__.py | 3 -- pylablib/widgets.py | 4 +-- 49 files changed, 147 insertions(+), 203 deletions(-) create mode 100644 pylablib/core/dataproc/__export__.py create mode 100644 pylablib/core/devio/__export__.py create mode 100644 pylablib/core/fileio/__export__.py create mode 100644 pylablib/core/gui/__export__.py create mode 100644 pylablib/core/gui/widgets/__export__.py create mode 100644 pylablib/core/thread/__export__.py create mode 100644 pylablib/core/utils/__export__.py create mode 100644 pylablib/gui/widgets/__export__.py diff --git a/pylablib/__init__.py b/pylablib/__init__.py index 8d820fd..200dccb 100644 --- a/pylablib/__init__.py +++ b/pylablib/__init__.py @@ -3,7 +3,12 @@ from .core.utils import library_parameters from .core.utils.library_parameters import library_parameters as par, temp_library_parameters as temp_par -from .core import * +from .core.dataproc.__export__ import * +from .core.devio.__export__ import * +from .core.fileio.__export__ import * +from .core.gui.__export__ import * +from .core.thread.__export__ import * +from .core.utils.__export__ import * _load_path=os.path.abspath(os.curdir) diff --git a/pylablib/core/__init__.py b/pylablib/core/__init__.py index 911a990..e69de29 100644 --- a/pylablib/core/__init__.py +++ b/pylablib/core/__init__.py @@ -1,6 +0,0 @@ -from .utils import * -from .dataproc import * -from .fileio import * -from .devio import * -from .thread import * -from .gui import * \ No newline at end of file diff --git a/pylablib/core/dataproc/__export__.py b/pylablib/core/dataproc/__export__.py new file mode 100644 index 0000000..26e9c6a --- /dev/null +++ b/pylablib/core/dataproc/__export__.py @@ -0,0 +1,16 @@ +from .utils import is_ascending, is_descending, is_ordered, is_linear +from .utils import get_x_column, get_y_column +from .utils import sort_by, filter_by, unique_slices +from .utils import find_closest_arg, find_closest_value, get_range_indices, cut_to_range, cut_out_regions +from .utils import find_discrete_step, unwrap_mod_data +from .utils import xy2c, c2xy +from .fourier import fourier_transform, inverse_fourier_transform, power_spectral_density +from .filters import convolution_filter, gaussian_filter, gaussian_filter_nd, low_pass_filter, high_pass_filter, sliding_average, median_filter, sliding_filter +from .filters import decimate, binning_average, decimate_datasets, decimate_full, collect_into_bins, split_into_bins +from .filters import fourier_filter, fourier_filter_bandpass, fourier_filter_bandstop, fourier_make_response_real +from .fitting import Fitter, get_best_fit +from .callable import to_callable, MultiplexedCallable, JoinedCallable +from .interpolate import interpolate1D_func, interpolate1D, interpolate2D, interpolateND, regular_grid_from_scatter, interpolate_trace +from .specfunc import get_kernel_func, get_window_func +from .feature import get_baseline_simple, subtract_baseline, find_peaks_cutoff, multi_scale_peakdet, rescale_peak, peaks_sum_func, find_local_extrema, latching_trigger +from .image import ROI, get_region, get_region_sum \ No newline at end of file diff --git a/pylablib/core/dataproc/__init__.py b/pylablib/core/dataproc/__init__.py index c69622e..e69de29 100644 --- a/pylablib/core/dataproc/__init__.py +++ b/pylablib/core/dataproc/__init__.py @@ -1,33 +0,0 @@ -from . import utils -from .utils import is_ascending, is_descending, is_ordered, is_linear -from .utils import get_x_column, get_y_column -from .utils import sort_by, filter_by, unique_slices -from .utils import find_closest_arg, find_closest_value, get_range_indices, cut_to_range, cut_out_regions -from .utils import find_discrete_step, unwrap_mod_data -from .utils import xy2c, c2xy - -from . import fourier -from .fourier import fourier_transform, inverse_fourier_transform, power_spectral_density - -from . import filters -from .filters import convolution_filter, gaussian_filter, gaussian_filter_nd, low_pass_filter, high_pass_filter, sliding_average, median_filter, sliding_filter -from .filters import decimate, binning_average, decimate_datasets, decimate_full, collect_into_bins, split_into_bins -from .filters import fourier_filter, fourier_filter_bandpass, fourier_filter_bandstop, fourier_make_response_real - -from . import fitting -from .fitting import Fitter, get_best_fit - -from . import callable as callable_func -from .callable import to_callable, MultiplexedCallable, JoinedCallable - -from . import interpolate -from .interpolate import interpolate1D_func, interpolate1D, interpolate2D, interpolateND, regular_grid_from_scatter, interpolate_trace - -from . import specfunc -from .specfunc import get_kernel_func, get_window_func - -from . import feature as feature_detect -from .feature import get_baseline_simple, subtract_baseline, find_peaks_cutoff, multi_scale_peakdet, rescale_peak, peaks_sum_func, find_local_extrema, latching_trigger - -from . import image as image_utils -from .image import ROI, get_region, get_region_sum \ No newline at end of file diff --git a/pylablib/core/devio/__export__.py b/pylablib/core/devio/__export__.py new file mode 100644 index 0000000..66293f2 --- /dev/null +++ b/pylablib/core/devio/__export__.py @@ -0,0 +1,5 @@ +from .base import DeviceError +from .comm_backend import list_backend_resources, new_backend, DeviceBackendError +from .interface import IDevice, EnumParameterClass, RangeParameterClass, use_parameters +from .SCPI import SCPIDevice +from .data_format import DataFormat \ No newline at end of file diff --git a/pylablib/core/devio/__init__.py b/pylablib/core/devio/__init__.py index 2ded589..e69de29 100644 --- a/pylablib/core/devio/__init__.py +++ b/pylablib/core/devio/__init__.py @@ -1,12 +0,0 @@ -from .base import DeviceError - -from . import comm_backend -from .comm_backend import list_backend_resources, DeviceBackendError - -from .interface import IDevice, EnumParameterClass, RangeParameterClass, use_parameters - -from . import SCPI -from .SCPI import SCPIDevice - -from . import data_format -from .data_format import DataFormat \ No newline at end of file diff --git a/pylablib/core/fileio/__export__.py b/pylablib/core/fileio/__export__.py new file mode 100644 index 0000000..dd17d37 --- /dev/null +++ b/pylablib/core/fileio/__export__.py @@ -0,0 +1,7 @@ +from .loadfile import load_generic, load_csv, load_csv_desc, load_bin, load_bin_desc, load_dict +from .savefile import save_generic, save_csv, save_csv_desc, save_bin, save_bin_desc, save_dict +from .location import LocationName, LocationFile, get_location +from .table_stream import TableStreamFile +from .dict_entry import IDictionaryEntry, ExternalTextTableDictionaryEntry, ExternalBinTableDictionaryEntry, \ + IExternalFileDictionaryEntry, ExternalNumpyDictionaryEntry, ExpandedContainerDictionaryEntry +from .dict_entry import add_dict_entry_builder, add_dict_entry_parser, add_dict_entry_class \ No newline at end of file diff --git a/pylablib/core/fileio/__init__.py b/pylablib/core/fileio/__init__.py index 2ab3c70..e69de29 100644 --- a/pylablib/core/fileio/__init__.py +++ b/pylablib/core/fileio/__init__.py @@ -1,16 +0,0 @@ -from . import loadfile -from .loadfile import load_generic, load_csv, load_csv_desc, load_bin, load_bin_desc, load_dict - -from . import savefile -from .savefile import save_generic, save_csv, save_csv_desc, save_bin, save_bin_desc, save_dict - -from . import location -from .location import LocationName, LocationFile, get_location - -from . import table_stream -from .table_stream import TableStreamFile - -from . import dict_entry -from .dict_entry import IDictionaryEntry, ExternalTextTableDictionaryEntry, ExternalBinTableDictionaryEntry, \ - IExternalFileDictionaryEntry, ExternalNumpyDictionaryEntry, ExpandedContainerDictionaryEntry -from .dict_entry import add_dict_entry_builder, add_dict_entry_parser, add_dict_entry_class \ No newline at end of file diff --git a/pylablib/core/gui/__export__.py b/pylablib/core/gui/__export__.py new file mode 100644 index 0000000..16ee859 --- /dev/null +++ b/pylablib/core/gui/__export__.py @@ -0,0 +1,6 @@ +from . import qt_present + +if qt_present: + from .value_handling import GUIValues, virtual_gui_values + from .formatter import FloatFormatter, IntegerFormatter, FmtStringFormatter, as_formatter + from .limiter import NumberLimit, as_limiter \ No newline at end of file diff --git a/pylablib/core/gui/__init__.py b/pylablib/core/gui/__init__.py index 35d8188..d094815 100644 --- a/pylablib/core/gui/__init__.py +++ b/pylablib/core/gui/__init__.py @@ -1,29 +1,18 @@ -qt_present=False is_pyqt5=False -iq_pyside2=False +is_pyside2=False try: from PyQt5 import QtGui, QtWidgets, QtCore from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot from sip import delete as qdelete - qt_present=is_pyqt5=True + is_pyqt5=True except ImportError: try: from PySide2 import QtGui, QtWidgets, QtCore from PySide2.QtCore import Signal, Slot from shiboken2 import delete as qdelete - qt_present=iq_pyside2=True + is_pyside2=True except ImportError: pass -if qt_present: - from . import value_handling - from .value_handling import GUIValues - - from . import formatter - from .formatter import FloatFormatter, IntegerFormatter, FmtStringFormatter, as_formatter - - from . import limiter - from .limiter import NumberLimit, as_limiter - - from . import utils as gui_utils \ No newline at end of file +qt_present=is_pyqt5 or is_pyside2 \ No newline at end of file diff --git a/pylablib/core/gui/widgets/__export__.py b/pylablib/core/gui/widgets/__export__.py new file mode 100644 index 0000000..9d613b7 --- /dev/null +++ b/pylablib/core/gui/widgets/__export__.py @@ -0,0 +1,5 @@ +from .edit import NumEdit, TextEdit +from .label import NumLabel +from .combo_box import ComboBox +from .button import ToggleButton +from .param_table import ParamTable, StatusTable \ No newline at end of file diff --git a/pylablib/core/gui/widgets/__init__.py b/pylablib/core/gui/widgets/__init__.py index 9d613b7..e69de29 100644 --- a/pylablib/core/gui/widgets/__init__.py +++ b/pylablib/core/gui/widgets/__init__.py @@ -1,5 +0,0 @@ -from .edit import NumEdit, TextEdit -from .label import NumLabel -from .combo_box import ComboBox -from .button import ToggleButton -from .param_table import ParamTable, StatusTable \ No newline at end of file diff --git a/pylablib/core/thread/__export__.py b/pylablib/core/thread/__export__.py new file mode 100644 index 0000000..8e8a145 --- /dev/null +++ b/pylablib/core/thread/__export__.py @@ -0,0 +1,8 @@ +from ..gui import qt_present +if qt_present: + from .threadprop import ThreadError, NoControllerThreadError, DuplicateControllerThreadError, TimeoutThreadError, NoMessageThreadError, SkippedCallError, InterruptExceptionStop + from .threadprop import is_gui_thread, current_controller + from .controller import exint, exsafe, exsafeSlot, toploopSlot, remote_call, call_in_thread, call_in_gui_thread, gui_thread_method + from .controller import QThreadController, QTaskThread, get_controller, sync_controller, get_gui_controller, stop_controller, stop_all_controllers, stop_app + from .synchronizing import QThreadNotifier, QMultiThreadNotifier + from .multicast_pool import MulticastPool \ No newline at end of file diff --git a/pylablib/core/thread/__init__.py b/pylablib/core/thread/__init__.py index 85d1c3b..e69de29 100644 --- a/pylablib/core/thread/__init__.py +++ b/pylablib/core/thread/__init__.py @@ -1,13 +0,0 @@ -from ..gui import qt_present -if qt_present: - from . import threadprop - from .threadprop import ThreadError, NoControllerThreadError, DuplicateControllerThreadError, TimeoutThreadError, NoMessageThreadError, SkippedCallError, InterruptExceptionStop - from .threadprop import is_gui_thread, current_controller - - from .controller import exint, exsafe, exsafeSlot, toploopSlot, remote_call, call_in_thread, call_in_gui_thread, gui_thread_method - from .controller import QThreadController, QTaskThread, get_controller, sync_controller, get_gui_controller, stop_controller, stop_all_controllers, stop_app - - from .synchronizing import QThreadNotifier, QMultiThreadNotifier - from .multicast_pool import MulticastPool - - from . import callsync \ No newline at end of file diff --git a/pylablib/core/utils/__export__.py b/pylablib/core/utils/__export__.py new file mode 100644 index 0000000..043da57 --- /dev/null +++ b/pylablib/core/utils/__export__.py @@ -0,0 +1,24 @@ +from .dictionary import Dictionary, PrefixShortcutTree, PrefixTree, is_dictionary, as_dictionary, as_dict +from .files import get_file_creation_time, get_file_modification_time, touch +from .files import generate_indexed_filename, generate_prefixed_filename +from .files import copy_file, move_file +from .files import retry_copy, retry_move, retry_remove +from .files import ensure_dir, remove_dir, clean_dir, remove_dir_if_empty, copy_dir, move_dir +from .files import list_dir, list_dir_recursive, dir_empty, walk_dir, cmp_dirs +from .files import retry_ensure_dir, retry_remove_dir, retry_clean_dir, retry_remove_dir_if_empty, retry_copy_dir, retry_move_dir +from .files import zip_file, zip_multiple_files, unzip_file, zip_folder, unzip_folder +from .funcargparse import check_parameter_range, is_sequence, as_sequence +from .functions import FunctionSignature, funcsig, getargsfrom, call_cut_args, obj_prop, as_obj_prop, delaydef +from .general import any_item, merge_dicts, filter_dict, map_dict_keys, map_dict_values, invert_dict +from .general import flatten_list, partition_list, split_in_groups, sort_set_by_list, compare_lists +from .general import RetryOnException, SilenceException, retry_wait +from .general import UIDGenerator, NamedUIDGenerator, Countdown, Timer, call_limit, AccessIterator +from .general import setbp +from .general import StreamFileLogger +from .general import muxcall +from .numerical import gcd, gcd_approx, limit_to_range, infinite_list, unity as f_unity, constant as f_constant, polynomial as f_polynomial +from .string import string_equal, find_list_string, find_dict_string +from .string import get_string_filter, sfglob, sfregex +from .string import escape_string, extract_escaped_string, unescape_string, to_string, from_string, from_string_partial, from_row_string, add_conversion_class, add_namedtuple_class +from .net import get_local_addr, get_all_local_addr, get_local_hostname +from .net import ClientSocket, recv_JSON, listen \ No newline at end of file diff --git a/pylablib/core/utils/__init__.py b/pylablib/core/utils/__init__.py index 8b888c0..e69de29 100644 --- a/pylablib/core/utils/__init__.py +++ b/pylablib/core/utils/__init__.py @@ -1,35 +0,0 @@ -from .dictionary import Dictionary, PrefixShortcutTree, PrefixTree, is_dictionary, as_dictionary, as_dict - -from .files import get_file_creation_time, get_file_modification_time, touch -from .files import generate_indexed_filename, generate_prefixed_filename -from .files import copy_file, move_file -from .files import retry_copy, retry_move, retry_remove -from .files import ensure_dir, remove_dir, clean_dir, remove_dir_if_empty, copy_dir, move_dir -from .files import list_dir, list_dir_recursive, dir_empty, walk_dir, cmp_dirs -from .files import retry_ensure_dir, retry_remove_dir, retry_clean_dir, retry_remove_dir_if_empty, retry_copy_dir, retry_move_dir -from .files import zip_file, zip_multiple_files, unzip_file, zip_folder, unzip_folder - -from .funcargparse import check_parameter_range, is_sequence, as_sequence - -from .functions import FunctionSignature, funcsig, getargsfrom, call_cut_args, obj_prop, as_obj_prop, delaydef - -from .general import any_item, merge_dicts, filter_dict, map_dict_keys, map_dict_values, invert_dict -from .general import flatten_list, partition_list, split_in_groups, sort_set_by_list, compare_lists -from .general import RetryOnException, SilenceException, retry_wait -from .general import UIDGenerator, NamedUIDGenerator, Countdown, Timer, call_limit, AccessIterator -from .general import setbp -from .general import StreamFileLogger -from .general import muxcall - -from .numerical import gcd, gcd_approx, limit_to_range, infinite_list, unity as f_unity, constant as f_constant, polynomial as f_polynomial - -from .string import string_equal, find_list_string, find_dict_string -from .string import get_string_filter, sfglob, sfregex -from .string import escape_string, extract_escaped_string, unescape_string, to_string, from_string, from_string_partial, from_row_string, add_conversion_class, add_namedtuple_class - -from . import module - -from .net import get_local_addr, get_all_local_addr, get_local_hostname -from .net import ClientSocket, recv_JSON, listen - -from . import units \ No newline at end of file diff --git a/pylablib/devices/AWG/generic.py b/pylablib/devices/AWG/generic.py index 42c2834..643c2d4 100644 --- a/pylablib/devices/AWG/generic.py +++ b/pylablib/devices/AWG/generic.py @@ -1,11 +1,11 @@ -from ...core.devio import SCPI, interface, DeviceError, DeviceBackendError +from ...core.devio import SCPI, interface, comm_backend from ...core.utils import units import numpy as np -class GenericAWGError(DeviceError): +class GenericAWGError(comm_backend.DeviceError): """Generic AWG error""" -class GenericAWGBackendError(GenericAWGError,DeviceBackendError): +class GenericAWGBackendError(GenericAWGError,comm_backend.DeviceBackendError): """AWG backend communication error""" class GenericAWG(SCPI.SCPIDevice): diff --git a/pylablib/devices/Andor/base.py b/pylablib/devices/Andor/base.py index 5ec0947..b6a451b 100644 --- a/pylablib/devices/Andor/base.py +++ b/pylablib/devices/Andor/base.py @@ -1,4 +1,4 @@ -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError class AndorError(DeviceError): """Generic Andor error""" diff --git a/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py b/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py index d69666d..b232f64 100644 --- a/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py +++ b/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py @@ -1,7 +1,7 @@ # pylint: disable=wrong-spelling-in-comment from ...core.utils import ctypes_wrap -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from .ArcusPerformaxDriver_defs import define_functions from ..utils import load_lib diff --git a/pylablib/devices/Arduino/base.py b/pylablib/devices/Arduino/base.py index 4a5c540..94a1f7a 100644 --- a/pylablib/devices/Arduino/base.py +++ b/pylablib/devices/Arduino/base.py @@ -1,11 +1,11 @@ -from ...core.devio import comm_backend, DeviceError, DeviceBackendError +from ...core.devio import comm_backend import time -class ArduinoError(DeviceError): +class ArduinoError(comm_backend.DeviceError): """Generic Arduino devices error""" -class ArduinoBackendError(ArduinoError,DeviceBackendError): +class ArduinoBackendError(ArduinoError,comm_backend.DeviceBackendError): """Generic Arduino backend communication error""" diff --git a/pylablib/devices/Attocube/base.py b/pylablib/devices/Attocube/base.py index 48334ab..1c09309 100644 --- a/pylablib/devices/Attocube/base.py +++ b/pylablib/devices/Attocube/base.py @@ -1,4 +1,4 @@ -from ...core.devio import DeviceError, DeviceBackendError +from ...core.devio.comm_backend import DeviceError, DeviceBackendError class AttocubeError(DeviceError): """Generic Attocube error""" diff --git a/pylablib/devices/Conrad/base.py b/pylablib/devices/Conrad/base.py index d1dbdf2..65edfc0 100644 --- a/pylablib/devices/Conrad/base.py +++ b/pylablib/devices/Conrad/base.py @@ -1,12 +1,12 @@ -from ...core.devio import comm_backend, DeviceError, DeviceBackendError +from ...core.devio import comm_backend import struct import collections -class ConradError(DeviceError): +class ConradError(comm_backend.DeviceError): """Generic Conrad devices error""" -class ConradBackendError(ConradError,DeviceBackendError): +class ConradBackendError(ConradError,comm_backend.DeviceBackendError): """Generic Conrad backend communication error""" diff --git a/pylablib/devices/Cryomagnetics/base.py b/pylablib/devices/Cryomagnetics/base.py index 8557129..c6a13ee 100644 --- a/pylablib/devices/Cryomagnetics/base.py +++ b/pylablib/devices/Cryomagnetics/base.py @@ -1,11 +1,11 @@ from ...core.utils.py3 import textstring from ...core.utils import general -from ...core.devio import SCPI, interface, DeviceError, DeviceBackendError +from ...core.devio import SCPI, interface, comm_backend -class CryomagneticsError(DeviceError): +class CryomagneticsError(comm_backend.DeviceError): """Generic Cryomagnetics devices error""" -class CryomagneticsBackendError(CryomagneticsError,DeviceBackendError): +class CryomagneticsBackendError(CryomagneticsError,comm_backend.DeviceBackendError): """Generic Cryomagnetics backend communication error""" class LM500(SCPI.SCPIDevice): diff --git a/pylablib/devices/DCAM/dcamapi4_lib.py b/pylablib/devices/DCAM/dcamapi4_lib.py index d28ecee..1fd1134 100644 --- a/pylablib/devices/DCAM/dcamapi4_lib.py +++ b/pylablib/devices/DCAM/dcamapi4_lib.py @@ -10,7 +10,7 @@ from .dcamapi4_defs import DCAMCAP_STATUS, DCAMWAIT_EVENT, DCAM_IDSTR # pylint: disable=unused-import from ...core.utils import ctypes_wrap -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/HighFinesse/wlmData_lib.py b/pylablib/devices/HighFinesse/wlmData_lib.py index 9cb8f39..d1e4f7f 100644 --- a/pylablib/devices/HighFinesse/wlmData_lib.py +++ b/pylablib/devices/HighFinesse/wlmData_lib.py @@ -1,7 +1,7 @@ # pylint: disable=wrong-spelling-in-comment from ...core.utils import ctypes_wrap, files -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from .wlmData_defs import EGetError, ESetError, drEGetError, drESetError # pylint: disable=unused-import from .wlmData_defs import EInst, ECtrlMode, EBaseOperation, EAddOperation # pylint: disable=unused-import from .wlmData_defs import EMeasUnit, ECalibration, EAutocalibration, EEvent # pylint: disable=unused-import diff --git a/pylablib/devices/IMAQ/niimaq_lib.py b/pylablib/devices/IMAQ/niimaq_lib.py index 066649e..b824992 100644 --- a/pylablib/devices/IMAQ/niimaq_lib.py +++ b/pylablib/devices/IMAQ/niimaq_lib.py @@ -10,7 +10,7 @@ from .niimaq_attrtypes import IMG_ATTR_DOUBLE, IMG_ATTR_UINT64, IMG_ATTR_NA # pylint: disable=unused-import from ...core.utils import ctypes_wrap, py3 -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/IMAQdx/NIIMAQdx_lib.py b/pylablib/devices/IMAQdx/NIIMAQdx_lib.py index bb010f9..1aeac37 100644 --- a/pylablib/devices/IMAQdx/NIIMAQdx_lib.py +++ b/pylablib/devices/IMAQdx/NIIMAQdx_lib.py @@ -11,7 +11,7 @@ from .NIIMAQdx_defs import define_functions from ...core.utils import ctypes_wrap, py3 -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/Lakeshore/base.py b/pylablib/devices/Lakeshore/base.py index c310324..7a9098a 100644 --- a/pylablib/devices/Lakeshore/base.py +++ b/pylablib/devices/Lakeshore/base.py @@ -1,12 +1,12 @@ -from ...core.devio import SCPI, interface, DeviceError, DeviceBackendError +from ...core.devio import SCPI, interface, comm_backend import numpy as np import collections -class LakeshoreError(DeviceError): +class LakeshoreError(comm_backend.DeviceError): """Generic Lakeshore devices error""" -class LakeshoreBackendError(LakeshoreError,DeviceBackendError): +class LakeshoreBackendError(LakeshoreError,comm_backend.DeviceBackendError): """Generic Lakeshore backend communication error""" TLakeshore218AnalogSettings=collections.namedtuple("TLakeshore218AnalogSettings",["bipolar","mode","channel","source","high_value","low_value","man_value"]) diff --git a/pylablib/devices/LaserQuantum/base.py b/pylablib/devices/LaserQuantum/base.py index 8e129df..9da1115 100644 --- a/pylablib/devices/LaserQuantum/base.py +++ b/pylablib/devices/LaserQuantum/base.py @@ -1,13 +1,13 @@ -from ...core.devio import comm_backend, DeviceError, DeviceBackendError +from ...core.devio import comm_backend from ...core.utils import py3 import re import collections -class LaserQuantumError(DeviceError): +class LaserQuantumError(comm_backend.DeviceError): """Generic Laser Quantum devices error""" -class LaserQuantumBackendError(LaserQuantumError,DeviceBackendError): +class LaserQuantumBackendError(LaserQuantumError,comm_backend.DeviceBackendError): """Generic Laser Quantum backend communication error""" TDeviceInfo=collections.namedtuple("TDeviceInfo",["serial","software_version","cal_date"]) diff --git a/pylablib/devices/LighthousePhotonics/base.py b/pylablib/devices/LighthousePhotonics/base.py index 573b2b9..d89e0a2 100644 --- a/pylablib/devices/LighthousePhotonics/base.py +++ b/pylablib/devices/LighthousePhotonics/base.py @@ -1,12 +1,12 @@ -from ...core.devio import comm_backend, DeviceError, DeviceBackendError +from ...core.devio import comm_backend from ...core.utils import py3, funcargparse import collections -class LighthousePhotonicsError(DeviceError): +class LighthousePhotonicsError(comm_backend.DeviceError): """Generic Lighthouse Photonics devices error""" -class LighthousePhotonicsBackendError(LighthousePhotonicsError,DeviceBackendError): +class LighthousePhotonicsBackendError(LighthousePhotonicsError,comm_backend.DeviceBackendError): """Generic Lighthouse Photonics backend communication error""" TDeviceInfo=collections.namedtuple("TDeviceInfo",["product","version","serial","configuration"]) diff --git a/pylablib/devices/M2/solstis.py b/pylablib/devices/M2/solstis.py index a9e7944..a34b27a 100644 --- a/pylablib/devices/M2/solstis.py +++ b/pylablib/devices/M2/solstis.py @@ -1,5 +1,5 @@ from ...core.utils import net, general -from ...core.devio import interface, DeviceError, DeviceBackendError +from ...core.devio import interface, comm_backend from ...core.devio.comm_backend import reraise try: @@ -20,9 +20,9 @@ def _check_websocket(): c=299792458. -class M2Error(DeviceError): +class M2Error(comm_backend.DeviceError): """Generic M2 error""" -class M2CommunicationError(M2Error,DeviceBackendError): +class M2CommunicationError(M2Error,comm_backend.DeviceBackendError): """M2 network communication error""" class Solstis(interface.IDevice): diff --git a/pylablib/devices/NI/daq.py b/pylablib/devices/NI/daq.py index cbf7fb8..313e75b 100644 --- a/pylablib/devices/NI/daq.py +++ b/pylablib/devices/NI/daq.py @@ -1,4 +1,4 @@ -from ...core.devio import interface, DeviceError, DeviceBackendError +from ...core.devio import interface, comm_backend from ...core.devio.comm_backend import reraise from ...core.utils import general, funcargparse @@ -6,9 +6,9 @@ import numpy as np import collections -class NIError(DeviceError): +class NIError(comm_backend.DeviceError): """Generic NI error""" -class NIDAQmxError(NIError,DeviceBackendError): +class NIDAQmxError(NIError,comm_backend.DeviceBackendError): """NI DAQmx backend operation error""" try: diff --git a/pylablib/devices/OZOptics/base.py b/pylablib/devices/OZOptics/base.py index ee95f16..7cedfd8 100644 --- a/pylablib/devices/OZOptics/base.py +++ b/pylablib/devices/OZOptics/base.py @@ -1,11 +1,11 @@ from ...core.utils import numerical -from ...core.devio import interface, comm_backend, DeviceError, DeviceBackendError +from ...core.devio import interface, comm_backend import re -class OZOpticsError(DeviceError): +class OZOpticsError(comm_backend.DeviceError): """Generic OZOptics devices error""" -class OZOpticsBackendError(OZOpticsError,DeviceBackendError): +class OZOpticsBackendError(OZOpticsError,comm_backend.DeviceBackendError): """Generic OZOptics backend communication error""" diff --git a/pylablib/devices/Ophir/base.py b/pylablib/devices/Ophir/base.py index 9621f31..31b9185 100644 --- a/pylablib/devices/Ophir/base.py +++ b/pylablib/devices/Ophir/base.py @@ -1,11 +1,11 @@ -from ...core.devio import comm_backend, interface, DeviceError, DeviceBackendError +from ...core.devio import comm_backend, interface from ...core.utils import py3, units import collections -class OphirError(DeviceError): +class OphirError(comm_backend.DeviceError): """Generic Ophir device error""" -class OphirBackendError(OphirError,DeviceBackendError): +class OphirBackendError(OphirError,comm_backend.DeviceBackendError): """Generic Ophir backend communication error""" class OphirDevice(comm_backend.ICommBackendWrapper): diff --git a/pylablib/devices/PCO/sc2_camexport_lib.py b/pylablib/devices/PCO/sc2_camexport_lib.py index eabe16a..48d6986 100644 --- a/pylablib/devices/PCO/sc2_camexport_lib.py +++ b/pylablib/devices/PCO/sc2_camexport_lib.py @@ -8,7 +8,7 @@ from .sc2_defs_defs import CAPS1, CAPS3 # pylint: disable=unused-import from ...core.utils import ctypes_wrap, py3 -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/Pfeiffer/base.py b/pylablib/devices/Pfeiffer/base.py index 1dffd84..00ee854 100644 --- a/pylablib/devices/Pfeiffer/base.py +++ b/pylablib/devices/Pfeiffer/base.py @@ -1,13 +1,13 @@ from ...core.utils import py3 -from ...core.devio import comm_backend, interface, DeviceError, DeviceBackendError +from ...core.devio import comm_backend, interface import collections -class PfeifferError(DeviceError): +class PfeifferError(comm_backend.DeviceError): """Generic Pfeiffer device error""" -class PfeifferBackendError(PfeifferError,DeviceBackendError): +class PfeifferBackendError(PfeifferError,comm_backend.DeviceBackendError): """Generic Pfeiffer backend communication error""" diff --git a/pylablib/devices/PhotonFocus/PhotonFocus.py b/pylablib/devices/PhotonFocus/PhotonFocus.py index 389dcdb..ab17845 100644 --- a/pylablib/devices/PhotonFocus/PhotonFocus.py +++ b/pylablib/devices/PhotonFocus/PhotonFocus.py @@ -3,7 +3,7 @@ from ..IMAQ.IMAQ import IMAQCamera, IMAQError from ...core.utils import py3, dictionary -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..interface import camera from ..utils import load_lib diff --git a/pylablib/devices/PhotonFocus/pfcam_lib.py b/pylablib/devices/PhotonFocus/pfcam_lib.py index 93f06b3..e51e14e 100644 --- a/pylablib/devices/PhotonFocus/pfcam_lib.py +++ b/pylablib/devices/PhotonFocus/pfcam_lib.py @@ -4,7 +4,7 @@ from .pfcam_defs import define_functions from ...core.utils import ctypes_wrap, py3 -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/SmarAct/SCU3DControl_lib.py b/pylablib/devices/SmarAct/SCU3DControl_lib.py index 1e13dfb..3e02fb8 100644 --- a/pylablib/devices/SmarAct/SCU3DControl_lib.py +++ b/pylablib/devices/SmarAct/SCU3DControl_lib.py @@ -5,7 +5,7 @@ from .SCU3DControl_defs import EConfiguration # pylint: disable=unused-import from .SCU3DControl_defs import define_functions -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/Tektronix/base.py b/pylablib/devices/Tektronix/base.py index a552f45..e9b8d41 100644 --- a/pylablib/devices/Tektronix/base.py +++ b/pylablib/devices/Tektronix/base.py @@ -1,14 +1,14 @@ import numpy as np -from ...core.devio import SCPI, data_format, interface, DeviceError, DeviceBackendError +from ...core.devio import SCPI, data_format, interface, comm_backend from ...core.utils import funcargparse, general import collections -class TektronixError(DeviceError): +class TektronixError(comm_backend.DeviceError): """Generic Tektronix devices error""" -class TektronixBackendError(TektronixError,DeviceBackendError): +class TektronixBackendError(TektronixError,comm_backend.DeviceBackendError): """Generic Tektronix backend communication error""" def muxchannel(*args, **kwargs): diff --git a/pylablib/devices/Thorlabs/base.py b/pylablib/devices/Thorlabs/base.py index f0b9bc8..4850d7e 100644 --- a/pylablib/devices/Thorlabs/base.py +++ b/pylablib/devices/Thorlabs/base.py @@ -1,4 +1,4 @@ -from ...core.devio import DeviceError, DeviceBackendError +from ...core.devio.comm_backend import DeviceError, DeviceBackendError class ThorlabsError(DeviceError): """Generic Thorlabs error""" diff --git a/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py b/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py index 0dc1665..596d2fc 100644 --- a/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py +++ b/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py @@ -5,7 +5,7 @@ from .tl_camera_sdk_defs import define_functions from ...core.utils import ctypes_wrap, py3 -from ...core.devio import DeviceError +from ...core.devio.comm_backend import DeviceError from ..utils import load_lib import ctypes diff --git a/pylablib/devices/Trinamic/base.py b/pylablib/devices/Trinamic/base.py index c2dc1bd..bb635d4 100644 --- a/pylablib/devices/Trinamic/base.py +++ b/pylablib/devices/Trinamic/base.py @@ -1,4 +1,4 @@ -from ...core.devio import comm_backend, interface, DeviceError, DeviceBackendError +from ...core.devio import comm_backend, interface, comm_backend from ..interface import stage @@ -7,9 +7,9 @@ import struct import math -class TrinamicError(DeviceError): +class TrinamicError(comm_backend.DeviceError): """Generic Trinamic error""" -class TrinamicBackendError(TrinamicError,DeviceBackendError): +class TrinamicBackendError(TrinamicError,comm_backend.DeviceBackendError): """Generic Trinamic backend communication error""" class TMCM1110(comm_backend.ICommBackendWrapper,stage.IStage): diff --git a/pylablib/devices/interface/camera.py b/pylablib/devices/interface/camera.py index f35cc03..82d8ccc 100644 --- a/pylablib/devices/interface/camera.py +++ b/pylablib/devices/interface/camera.py @@ -1,5 +1,5 @@ -from ...core.devio import interface, DeviceError -from ...core.dataproc import image_utils +from ...core.devio import interface, comm_backend +from ...core.dataproc import image as image_utils from ...core.utils import functions as function_utils, general as general_utils, dictionary import numpy as np @@ -24,8 +24,8 @@ class ICamera(interface.IDevice): _default_frameinfo_format="namedtuple" _default_image_dtype=" Date: Wed, 2 Jun 2021 22:40:03 +0200 Subject: [PATCH 03/31] More GUI work: names, property values, bugfixes --- pylablib/core/gui/value_handling.py | 141 +++++++++++++++--- pylablib/core/gui/widgets/combo_box.py | 4 +- pylablib/core/gui/widgets/container.py | 71 +++++---- pylablib/core/gui/widgets/layout_manager.py | 40 ++--- pylablib/core/gui/widgets/param_table.py | 48 +++++- .../gui/widgets/plotters/image_plotter.py | 37 +++-- .../gui/widgets/plotters/trace_plotter.py | 36 ++--- pylablib/gui/widgets/range_controls.py | 30 ++-- 8 files changed, 277 insertions(+), 130 deletions(-) diff --git a/pylablib/core/gui/value_handling.py b/pylablib/core/gui/value_handling.py index 18d1d6f..91d5b87 100644 --- a/pylablib/core/gui/value_handling.py +++ b/pylablib/core/gui/value_handling.py @@ -61,6 +61,8 @@ def get_method_kind(method, add_args=0): +class NoParameterError(KeyError): + """Error raised by some handlers to indicate that the parameter is missing""" class IValueHandler: """ @@ -95,6 +97,11 @@ def repr_value(self, value, name=None): # pylint: disable=unused-argument If ``name`` is not ``None``, it specifies the name of the value parameter inside the widget (for complex widgets). """ return str(value) + def get_handler(self, name=None): + """Get handler of a contained widget (or same widget, if ``name==None``)""" + if name is None: + return self + raise KeyError("can't find handler for widget {} with name {}".format(self.widget,name)) def get_value_changed_signal(self): """Get the Qt signal emitted when the value is changed""" if hasattr(self.widget,"value_changed"): @@ -163,6 +170,59 @@ def set_value(self, value, name=None): self._notify_value_changed_handlers(value) +class PropertyValueHandler(IValueHandler): + """ + Virtual value handler which uses custom getter/setter methods to simulate a value. + + If getter or setter are not supplied but are called, they raise :exc:`NoParameterError`; + this means that they are ignored in :meth:`GUIValues.get_all_values` and :meth:`GUIValues.set_all_values` methods, + but raise an error when access directly (e.g., using :meth:`GUIValues.get_values`). + + Args: + getter: value getter method; takes 0 or 1 (name) arguments and returns the value + setter: value setter method; takes 1 (value) or 2 (name and value) arguments and sets the value + default_name(str): default name to be supplied to ``getter`` and ``setter`` methods if they require a name argument + """ + def __init__(self, getter=None, setter=None, default_name=None): + IValueHandler.__init__(self,None) + self.getter=getter + self.getter_kind=get_method_kind(getter) + self.setter=setter + self.setter_kind=get_method_kind(setter) + self.default_name=default_name + def get_value(self, name=None): + if self.getter is None: + raise NoParameterError("no getter defined for the parameter") + if name is None: + if self.getter_kind=="simple": + return self.getter() + elif self.getter_kind=="named": + return self.getter(self.default_name) + else: + if self.getter_kind=="named": + return self.getter(name) + raise KeyError("can't find getter for with name {}".format(name)) + def _set_notify(self, *args): + result=self.setter(*args) + try: + self._notify_value_changed_handlers(self.get_value(*args[:-1])) + except NoParameterError: + pass + return result + def set_value(self, value, name=None): + if self.setter is None: + raise NoParameterError("no setter defined for the parameter") + if name is None: + if self.setter_kind=="simple": + return self._set_notify(value) + elif self.setter_kind=="named": + return self._set_notify(self.default_name,value) + else: + if self.setter_kind=="named": + return self._set_notify(name,value) + raise KeyError("can't find setter for widget {} with name {}".format(self.widget,name)) + + _default_getters=("get_value","get_all_values") _default_setters=("set_value","set_all_values") @@ -189,6 +249,7 @@ def __init__(self, widget, default_name=None): if not (self.set_value_kind or self.set_all_values_kind): raise ValueError("can't find default setter for widget {}".format(self.widget)) self.repr_value_kind=get_method_kind(getattr(self.widget,"repr_value",None),add_args=1) + self.get_handler_kind=get_method_kind(getattr(self.widget,"get_handler",None)) self.default_name=default_name def get_value(self, name=None): if name is None: @@ -196,21 +257,21 @@ def get_value(self, name=None): return self.widget.get_all_values() elif self.get_value_kind=="simple": return self.widget.get_value() - else: + elif self.get_value_kind=="named": return self.widget.get_value(self.default_name) else: if self.get_value_kind=="named": return self.widget.get_value(name) elif self.get_all_values_kind=="simple": return self.widget.get_all_values()[name] - raise ValueError("can't find getter for widget {} with name {}".format(self.widget,name)) + raise KeyError("can't find getter for widget {} with name {}".format(self.widget,name)) def set_value(self, value, name=None): if name is None: if self.set_all_values_kind=="simple": return self.widget.set_all_values(value) elif self.set_value_kind=="simple": return self.widget.set_value(value) - else: + elif self.set_value_kind=="named": return self.widget.set_value(self.default_name,value) else: if self.set_value_kind=="named": @@ -219,7 +280,7 @@ def set_value(self, value, name=None): if isinstance(name,list): name="/".join(name) return self.widget.set_all_values({name:value}) - raise ValueError("can't find setter for widget {} with name {}".format(self.widget,name)) + raise KeyError("can't find setter for widget {} with name {}".format(self.widget,name)) def repr_value(self, value, name=None): if name is None: if self.repr_value_kind=="simple": @@ -228,9 +289,12 @@ def repr_value(self, value, name=None): return self.widget.repr_value(self.default_name,value) else: if self.repr_value_kind=="named": - return self.widget.repr_value(value) + return self.widget.repr_value(name,value) return str(value) - + def get_handler(self, name=None): + if self.get_handler_kind=="named" and name is not None: + return self.widget.get_handler(name) + return super().get_handler(name) class ISingleValueHandler(IValueHandler): @@ -447,7 +511,7 @@ def __init__(self, widget, default_name=None): self.default_name=default_name def get_value(self, name=None): if not (self.get_indicator_kind or self.get_all_indicators_kind): - raise ValueError("can't find default indicator getter for widget {}".format(self.widget)) + raise KeyError("can't find default indicator getter for widget {}".format(self.widget)) if name is None: if self.get_indicator_kind=="simple": return self.widget.get_indicator() @@ -460,10 +524,10 @@ def get_value(self, name=None): return self.widget.get_indicator() elif self.get_all_indicators_kind=="simple": return self.widget.get_all_indicators()[name] - raise ValueError("can't find indicator getter for widget {} with name {}".format(self.widget,name)) + raise KeyError("can't find indicator getter for widget {} with name {}".format(self.widget,name)) def set_value(self, value, name=None): if not (self.set_indicator_kind or self.set_all_indicators_kind): - raise ValueError("can't find default indicator setter for widget {}".format(self.widget)) + raise KeyError("can't find default indicator setter for widget {}".format(self.widget)) if name is None: if self.set_indicator_kind=="simple": return self.widget.set_indicator(value) @@ -478,7 +542,7 @@ def set_value(self, value, name=None): if isinstance(name,list): name="/".join(name) return self.widget.set_all_indicators({name:value}) - raise ValueError("can't find indicator setter for widget {} with name {}".format(self.widget,name)) + raise KeyError("can't find indicator setter for widget {} with name {}".format(self.widget,name)) class LabelIndicatorHandler(IIndicatorHandler): """ Indicator handler which uses a label to show the value. @@ -530,7 +594,7 @@ def repr_value(self, value, name=None): return self.repr_func(name,value) if name: raise KeyError("no indicator value with name {}".format(name)) - except ValueError: + except (KeyError,ValueError): pass return str(value) def set_value(self, value, name=None): @@ -581,21 +645,39 @@ def __init__(self, gui_thread_safe=True): self.h=dictionary.ItemAccessor(self.get_handler,self.add_handler,self.remove_handler,contains_checker=self.__contains__) self.w=dictionary.ItemAccessor(self.get_widget,contains_checker=self.__contains__) self.v=dictionary.ItemAccessor(self.get_value,self.set_value,contains_checker=self.__contains__) - self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) + self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator,contains_checker=self.__contains__) self.vs=dictionary.ItemAccessor(self.get_value_changed_signal,contains_checker=self.__contains__) def add_handler(self, name, handler): """Add a value handler under a given name""" self.handlers[name]=handler return handler - def remove_handler(self, name, remove_indicator=True): - """Remove the value handler with a given name""" + def remove_handler(self, name, remove_indicator=True, disconnect=False): + """ + Remove the value handler with a given name. + + If ``remove_indicator==True``, also try to remove the indicator widget. + If ``disconnect==True``, also disconnect all slots connected to the ``value_changed`` signal. + Unlike most methods (e.g., :meth:`get_value` or :meth:`get_handler`), does not recursively query the children, + so it only works if the handler is contained in this table. + """ + if disconnect: + handler=self.get_handler(name) + try: + handler.get_value_changed_signal().disconnect() + except TypeError: # no signals connected or no handle + pass del self.handlers[name] if remove_indicator and name in self.indicator_handlers: self.remove_indicator_handler(name) def get_handler(self, name): """Get the value handler with the given name""" - return self.handlers[name] + path,subpath=self.handlers.get_max_prefix(name,kind="leaf") + if path is None: + raise KeyError("missing handler {}".format(name)) + if not subpath: + return self.handlers[path] + return self.handlers[path].get_handler(subpath) def __contains__(self, name): return name in self.handlers @@ -621,12 +703,25 @@ def add_virtual_element(self, name, value=None, multivalued=False, add_indicator Doesn't correspond to any actual widget, but behaves very similarly from the application point of view (its value can be set or read, it has on-change events, it can have indicator). + The element value is simply stored on set and retrieved on get. If ``add_indicator==True``, add default indicator handler as well. """ h=self.add_handler(name,VirtualValueHandler(value,multivalued=multivalued)) if add_indicator: self.add_indicator_handler(name,VirtualIndicatorHandler(value)) return h + def add_property_element(self, name, getter=None, setter=None, add_indicator=True): + """ + Add a property value element. + + Doesn't correspond to any actual widget, but behaves very similarly from the application point of view; + each time the value is set or get, the corresponding setter and getter methods are called. + If ``add_indicator==True``, add default (stored value) indicator handler as well. + """ + h=self.add_handler(name,PropertyValueHandler(getter=getter,setter=setter)) + if add_indicator: + self.add_indicator_handler(name,VirtualIndicatorHandler()) + return h _default_value_types=(edit.TextEdit,edit.NumEdit,QtWidgets.QLineEdit,QtWidgets.QCheckBox,QtWidgets.QPushButton,QtWidgets.QComboBox,QtWidgets.QProgressBar) def add_all_children(self, root, root_name=None, types_include=None, types_exclude=(), names_exclude=None): """ @@ -720,7 +815,10 @@ def get_value(self, name=None, include=None, exclude=None): subtree=self.handlers[name] for n in subtree.paths(): if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): - values[n]=subtree[n].get_value() + try: + values[n]=subtree[n].get_value() + except NoParameterError: + pass if values: return values raise KeyError("missing handler '{}'".format(name)) @@ -750,7 +848,10 @@ def set_value(self, name, value, include=None, exclude=None): for n,v in dictionary.as_dictionary(value).iternodes(to_visit="all",topdown=True,include_path=True): if subtree.has_entry(n,kind="leaf"): if (include is None or "/".join(n) in include) and (exclude is None or "/".join(n) not in exclude): - subtree[n].set_value(v) + try: + subtree[n].set_value(v) + except NoParameterError: + pass return elif not dictionary.as_dict(value): # assign empty values return @@ -846,7 +947,7 @@ def update_indicators(self, root="", include=None, exclude=None): if p in self.indicator_handlers: try: self.set_indicator(p,self.get_value(p)) - except ValueError: + except (KeyError,ValueError): pass def repr_value(self, name, value): @@ -858,7 +959,7 @@ def repr_value(self, name, value): path,subpath=self.handlers.get_max_prefix(name,kind="leaf") if path is None: raise KeyError("missing handler {}".format(name)) - return self.handlers[path].repr_value(value,subpath) + return self.handlers[path].repr_value(value,subpath or None) def get_value_changed_signal(self, name): """Get changed events for a value under a given name""" return self.get_handler(name).get_value_changed_signal() @@ -869,7 +970,7 @@ def get_gui_values(gui_values=None, gui_values_path=""): Get new or existing :class:`GUIValues` object and the sub-branch path inside it based on the supplied arguments. If `gui_values` is ``None`` or ``"new"``, create a new object and set empty root path. - If `gui_values` itself has ``gui_values`` attribute, get this attribute, and prepend object's ``gui_values_path`` attrbiute to the given path. + If `gui_values` itself has ``gui_values`` attribute, get this attribute, and prepend object's ``gui_values_path`` attribute to the given path. Otherwise, assume that `gui_values` is :class:`GUIValues` object, and use the supplied root. """ if gui_values is None or gui_values=="new": diff --git a/pylablib/core/gui/widgets/combo_box.py b/pylablib/core/gui/widgets/combo_box.py index f28135d..5c8606c 100644 --- a/pylablib/core/gui/widgets/combo_box.py +++ b/pylablib/core/gui/widgets/combo_box.py @@ -51,7 +51,9 @@ def set_value(self, value, notify_value_change=True): If ``notify_value_change==True``, emit the `value_changed` signal; otherwise, change value silently. """ - index=self.value_to_index(value) + if not self.count(): + return + index=max(0,min(self.value_to_index(value),self.count()-1)) if self._index!=index: self._index=index self.setCurrentIndex(self._index) diff --git a/pylablib/core/gui/widgets/container.py b/pylablib/core/gui/widgets/container.py index 0683edf..ebaac5c 100644 --- a/pylablib/core/gui/widgets/container.py +++ b/pylablib/core/gui/widgets/container.py @@ -24,7 +24,8 @@ class QContainer(QtCore.QObject): TimerUIDGenerator=general.NamedUIDGenerator(thread_safe=True) def __init__(self, *args, name=None, **kwargs): super().__init__(*args,**kwargs) - self.name=name + self.name=None + self.setup_name(name) self._timers={} self._timer_events={} self._running=False @@ -51,17 +52,20 @@ def setup_gui_values(self, gui_values="new", gui_values_path=""): self.gui_values,self.gui_values_path=value_handling.get_gui_values(gui_values,gui_values_path) def setup_name(self, name): """Set the object's name""" - self.name=name - self.setObjectName(name) - def setup(self, gui_values=None, gui_values_path=""): + if name is not None: + self.name=name + self.setObjectName(name) + def setup(self, name=None, gui_values=None, gui_values_path=""): """ - Setup the container by intializing its GUI values and setting the ``ctl`` attribute. + Setup the container by initializing its GUI values and setting the ``ctl`` attribute. `gui_values` is a :class:`.GUIValues`` object, an object which has ``gui_values`` attribute, or ``"new"`` (make a new storage; in this case `gui_values_path` is ignored), and `gui_values_path` is the container's path within this storage. If ``gui_values`` is ``None``, skip the setup (assume that it's already done). """ + if self.name is None: + self.setup_name(name) self.setup_gui_values(gui_values=gui_values,gui_values_path=gui_values_path) self.ctl=controller.get_gui_controller() @@ -152,6 +156,14 @@ def add_widget_values(self, path, widget): raise ValueError("can not store a non-container widget under an empty path") else: self.gui_values.add_widget(path,widget) + def _setup_widget_name(self, widget, name): + if name is None: + name=getattr(widget,"name",None) + if name is None: + raise ValueError("widget name must be provided") + elif hasattr(widget,"setup_name"): + widget.setup_name(name) + return name def add_widget(self, name, widget, gui_values_path=True): """ Add a contained widget. @@ -160,6 +172,7 @@ def add_widget(self, name, widget, gui_values_path=True): if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not; otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. """ + name=self._setup_widget_name(widget,name) if name in self._widgets: raise ValueError("widget {} is already present") if gui_values_path!=False and gui_values_path is not None: @@ -167,6 +180,7 @@ def add_widget(self, name, widget, gui_values_path=True): gui_values_path="" if hasattr(widget,"setup_gui_values") else name self.add_widget_values(gui_values_path,widget) self._widgets[name]=TWidget(name,widget,gui_values_path) + return widget def get_widget(self, name): """Get the widget with the given name""" path,subpath=self._widgets.get_max_prefix(name,kind="leaf") @@ -177,7 +191,7 @@ def _clear_widget(self, widget): if hasattr(widget.widget,"clear"): widget.widget.clear() if widget.gui_values_path is not None: - self.gui_values.remove_handler((self.gui_values_path,widget.gui_values_path),remove_indicator=True) + self.gui_values.remove_handler((self.gui_values_path,widget.gui_values_path),remove_indicator=True,disconnect=True) def remove_widget(self, name): """Remove widget from the container and clear it""" path,subpath=self._widgets.get_max_prefix(name,kind="leaf") @@ -233,6 +247,10 @@ def clear(self): self._clear_widget(w) self._widgets=dictionary.Dictionary() + def get_handler(self, name): + """Get value handler of a widget with the given name""" + return self.gui_values.get_handler((self.gui_values_path,name or "")) + def get_value(self, name=None): """Get value of a widget with the given name (``None`` means all values)""" return self.gui_values.get_value((self.gui_values_path,name or "")) @@ -272,16 +290,17 @@ class QWidgetContainer(QLayoutManagedWidget, QContainer): with :class:`.QLayoutManagedWidget` management of the contained widget's layout. Typically, adding widget adds them both to the container values and to the layout; - however, this can be skipped by either using :meth:`.QLayoutManagedWidget.add_layout_element` + however, this can be skipped by either using :meth:`.QLayoutManagedWidget.add_to_layout` (only add to the layout), or specifying ``location="skip"`` in :meth:`add_widget` (only add to the container). """ - def setup(self, layout_kind="vbox", no_margins=False, gui_values=None, gui_values_path=""): - QContainer.setup(self,gui_values=gui_values,gui_values_path=gui_values_path) - QLayoutManagedWidget.setup(self,layout_kind=layout_kind,no_margins=no_margins) + def setup(self, layout="vbox", no_margins=False, name=None, gui_values=None, gui_values_path=""): + QContainer.setup(self,name=name,gui_values=gui_values,gui_values_path=gui_values_path) + QLayoutManagedWidget.setup(self,layout=layout,no_margins=no_margins) def add_widget(self, name, widget, location=None, gui_values_path=True): """ Add a contained widget. + If ``name==False``, only add the widget to they layout, but not to the container. `location` specifies the layout location to which the widget is added; if it is ``"skip"``, skip adding it to the layout (can be manually added later). Note that if the widget is added to the layout, it will be completely deleted when :meth:`clear` method is called; @@ -291,9 +310,11 @@ def add_widget(self, name, widget, location=None, gui_values_path=True): if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not; otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. """ - QContainer.add_widget(self,name=name,widget=widget,gui_values_path=gui_values_path) + if name==False: + QContainer.add_widget(self,name=name,widget=widget,gui_values_path=gui_values_path) if isinstance(widget,QtWidgets.QWidget): - QLayoutManagedWidget.add_layout_element(self,widget,location=location) + QLayoutManagedWidget.add_to_layout(self,widget,location=location) + return widget def remove_widget(self, name): """Remove widget from the container and the layout, clear it, and remove it""" if name in self._widgets: @@ -302,31 +323,31 @@ def remove_widget(self, name): QLayoutManagedWidget.remove_layout_element(self,widget) else: QContainer.remove_widget(self,name) - def add_frame(self, name, layout_kind="vbox", location=None, gui_values_path=True, no_margins=True): + def add_frame(self, name, layout="vbox", location=None, gui_values_path=True, no_margins=True): """ Add a new frame container to the layout. - `layout_kind` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, + `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, and `location` specifies its location within the container layout. If ``no_margins==True``, the frame will have no inner layout margins. The other parameters are the same as in :meth:`add_widget` method. """ frame=QFrameContainer(self) - frame.setup(layout_kind=layout_kind,no_margins=no_margins) self.add_widget(name,frame,location=location,gui_values_path=gui_values_path) + frame.setup(layout=layout,no_margins=no_margins) return frame - def add_group_box(self, name, caption, layout_kind="vbox", location=None, gui_values_path=True, no_margins=True): + def add_group_box(self, name, caption, layout="vbox", location=None, gui_values_path=True, no_margins=True): """ Add a new group box container with the given `caption` to the layout. - `layout_kind` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, + `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, and `location` specifies its location within the container layout. If ``no_margins==True``, the frame will have no inner layout margins. The other parameters are the same as in :meth:`add_widget` method. """ group_box=QGroupBoxContainer(self) - group_box.setup(caption=caption,layout_kind=layout_kind, no_margins=no_margins) self.add_widget(name,group_box,location=location,gui_values_path=gui_values_path) + group_box.setup(caption=caption,layout=layout,no_margins=no_margins) return group_box def clear(self): """ @@ -341,12 +362,12 @@ def clear(self): class QFrameContainer(QtWidgets.QFrame, QWidgetContainer): - """An extention of :class:`QWidgetContainer` for a ``QFrame`` Qt base class""" + """An extension of :class:`QWidgetContainer` for a ``QFrame`` Qt base class""" class QGroupBoxContainer(QtWidgets.QGroupBox, QWidgetContainer): - """An extention of :class:`QWidgetContainer` for a ``QGroupBox`` Qt base class""" - def setup(self, caption=None, layout_kind="vbox", no_margins=False, gui_values=None, gui_values_path=""): - QWidgetContainer.setup(self,layout_kind=layout_kind,no_margins=no_margins,gui_values=gui_values,gui_values_path=gui_values_path) + """An extension of :class:`QWidgetContainer` for a ``QGroupBox`` Qt base class""" + def setup(self, caption=None, layout="vbox", no_margins=False, name=None, gui_values=None, gui_values_path=""): + QWidgetContainer.setup(self,layout=layout,no_margins=no_margins,name=name,gui_values=gui_values,gui_values_path=gui_values_path) if caption is not None: self.setTitle(caption) @@ -362,12 +383,12 @@ class QTabContainer(QtWidgets.QTabWidget, QContainer): def __init__(self, *args, **kwargs): super().__init__(*args,**kwargs) self._tabs={} - def add_tab(self, name, caption, index=None, layout_kind="vbox", gui_values_path=True, no_margins=True): + def add_tab(self, name, caption, index=None, layout="vbox", gui_values_path=True, no_margins=True): """ Add a new tab container with the given `caption` to the widget. `index` specifies the new tab's index (``None`` means adding to the end, negative values count from the end). - `layout_kind` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, + `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, and `location` specifies its location within the container layout. If ``no_margins==True``, the frame will have no inner layout margins. The other parameters are the same as in :meth:`add_widget` method. @@ -375,8 +396,8 @@ def add_tab(self, name, caption, index=None, layout_kind="vbox", gui_values_path if name in self._tabs: raise ValueError("tab {} already exists".format(name)) frame=QFrameContainer(self) - frame.setup(layout_kind=layout_kind,no_margins=no_margins) self.add_widget(name=name,widget=frame,gui_values_path=gui_values_path) + frame.setup(layout=layout,no_margins=no_margins) if index is None: index=self.count() elif index<0: diff --git a/pylablib/core/gui/widgets/layout_manager.py b/pylablib/core/gui/widgets/layout_manager.py index bc50eda..5ff3dc3 100644 --- a/pylablib/core/gui/widgets/layout_manager.py +++ b/pylablib/core/gui/widgets/layout_manager.py @@ -6,41 +6,42 @@ import contextlib -def _make_layout(kind, *args, **kwargs): - """Make a layout of the given kind""" - if kind=="grid": - return QtWidgets.QGridLayout(*args,**kwargs) - if kind=="vbox": - return QtWidgets.QVBoxLayout(*args,**kwargs) - if kind=="hbox": - return QtWidgets.QHBoxLayout(*args,**kwargs) - raise ValueError("unrecognized layout kind: {}".format(kind)) class QLayoutManagedWidget(QtWidgets.QWidget): """ GUI widget which can manage layouts. Typically, first it is set up using :meth:`setup` method to specify the master layout kind; - afterwards, widgets and sublayout can be added using :meth:`add_layout_element`. + afterwards, widgets and sublayout can be added using :meth:`add_to_layout`. In addition, it can directly add named sublayouts using :meth:`add_sublayout` method. """ def __init__(self, parent=None): super().__init__(parent) self.main_layout=None self._default_layout="main" + + def _make_new_layout(self, kind, *args, **kwargs): + """Make a layout of the given kind""" + if kind=="grid": + return QtWidgets.QGridLayout(*args,**kwargs) + if kind=="vbox": + return QtWidgets.QVBoxLayout(*args,**kwargs) + if kind=="hbox": + return QtWidgets.QHBoxLayout(*args,**kwargs) + raise ValueError("unrecognized layout kind: {}".format(kind)) def _set_main_layout(self): - self.main_layout=_make_layout(self.main_layout_kind,self) + self.main_layout=self._make_new_layout(self.main_layout_kind,self) self.main_layout.setObjectName(self.name+"_main_layout" if hasattr(self,"name") and self.name else "main_layout") if self.no_margins: self.main_layout.setContentsMargins(0,0,0,0) - def setup(self, layout_kind="grid", no_margins=False): + def setup(self, layout="grid", no_margins=False): """ Setup the layout. Args: - kind: layout kind; can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box). + layout: layout kind; can be ``"grid"``, ``"vbox"`` (vertical single-column box), or ``"hbox"`` (horizontal single-row box). no_margins: if ``True``, set all layout margins to zero (useful when the widget is in the middle of layout hierarchy) """ - self.main_layout_kind=layout_kind + self.main_layout_kind=layout self.no_margins=no_margins self._set_main_layout() self._sublayouts={"main":(self.main_layout,self.main_layout_kind)} @@ -115,7 +116,7 @@ def _insert_layout_element(self, lname, element, location, kind="widget"): layout.insertLayout(idx,element) else: raise ValueError("unrecognized element kind: {}".format(kind)) - def add_layout_element(self, element, location=None, kind="widget"): + def add_to_layout(self, element, location=None, kind="widget"): """ Add an existing `element` to the layout at the given `location`. @@ -124,6 +125,7 @@ def add_layout_element(self, element, location=None, kind="widget"): lname,location=self._normalize_location(location) if location!="skip": self._insert_layout_element(lname,element,location,kind=kind) + return element def remove_layout_element(self, element): """Remove a previously added layout element""" for layout,_ in self._sublayouts.values(): @@ -141,10 +143,10 @@ def add_sublayout(self, name, kind="grid", location=None): """ if name in self._sublayouts: raise ValueError("sublayout {} already exists".format(name)) - layout=_make_layout(kind) + layout=self._make_new_layout(kind) layout.setContentsMargins(0,0,0,0) layout.setObjectName(name) - self.add_layout_element(layout,location,kind="layout") + self.add_to_layout(layout,location,kind="layout") self._sublayouts[name]=(layout,kind) return layout @contextlib.contextmanager @@ -170,7 +172,7 @@ def add_spacer(self, height=0, width=0, stretch_height=False, stretch_width=Fals spacer=QtWidgets.QSpacerItem(width,height, QtWidgets.QSizePolicy.MinimumExpanding if stretch_width else QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding if stretch_height else QtWidgets.QSizePolicy.Minimum) - self.add_layout_element(spacer,location,kind="item") + self.add_to_layout(spacer,location,kind="item") self._spacers.append(spacer) # otherwise the reference is lost, and the object might be deleted return spacer def add_padding(self, kind="auto", location="next"): @@ -196,7 +198,7 @@ def add_decoration_label(self, text, location="next"): label=QtWidgets.QLabel(self) label.setText(str(text)) label.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.add_layout_element(label,location) + self.add_to_layout(label,location) return label def insert_row(self, row, sublayout=None): """Insert a new row at the given location in the grid layout""" diff --git a/pylablib/core/gui/widgets/param_table.py b/pylablib/core/gui/widgets/param_table.py index 57e7e19..8815c46 100644 --- a/pylablib/core/gui/widgets/param_table.py +++ b/pylablib/core/gui/widgets/param_table.py @@ -25,7 +25,12 @@ class ParamTable(layout_manager.QLayoutManagedWidget): (i.e., ``self.get_handler(name)`` is equivalent to ``self.h[name]``), ``.w`` for getting the underlying widget (i.e., ``self.get_widget(name)`` is equivalent to ``self.w[name]``), - ``.v`` for settings/getting values + ``.v`` for settings/getting values using the default getting method + (equivalent to ``.wv`` if ``cache_values=False`` in :meth:`setup`, and to ``.cv`` otherwise), + ``.wv`` for settings/getting current current widget values without caching + (i.e., ``self.get_value(name)`` is equivalent to ``self.v[name]``, and ``self.set_value(name, value)`` is equivalent to ``self.v[name]=value``), + ``.cv`` for settings/getting values using cached value's table for getting + (i.e., ``self.current_values[name]`` is equivalent to ``self.cv[name]``, and ``self.set_value(name, value)`` is equivalent to ``self.cv[name]=value``), (i.e., ``self.get_value(name)`` is equivalent to ``self.v[name]``, and ``self.set_value(name, value)`` is equivalent to ``self.v[name]=value``), ``.i`` for settings/getting indicator values (i.e., ``self.get_indicator(name)`` is equivalent to ``self.i[name]``, and ``self.set_indicator(name, value)`` is equivalent to ``self.i[name]=value``) @@ -39,18 +44,25 @@ class ParamTable(layout_manager.QLayoutManagedWidget): """ def __init__(self, parent=None, name=None): super().__init__(parent) - self.name=name + self.name=None + self.setup_name(name) self.params={} self.h=dictionary.ItemAccessor(self.get_handler) self.w=dictionary.ItemAccessor(self.get_widget) - self.v=dictionary.ItemAccessor(self.get_value,self.set_value) + self.wv=dictionary.ItemAccessor(self.get_value,self.set_value) + self.v=self.wv + self.cv=dictionary.ItemAccessor(lambda name: self.current_values[name],self.set_value) self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) self.vs=dictionary.ItemAccessor(self.get_value_changed_signal) self.setup_gui_values("new") + def _make_new_layout(self, kind, *args, **kwargs): + layout=super()._make_new_layout(kind,*args,**kwargs) + if kind=="grid": + layout.setSpacing(5) + return layout def _set_main_layout(self): super()._set_main_layout() self.main_layout.setContentsMargins(5,5,5,5) - self.main_layout.setSpacing(5) self.main_layout.setColumnStretch(1,1) def setup_gui_values(self, gui_values=None, gui_values_path=""): if self.params: @@ -59,7 +71,12 @@ def setup_gui_values(self, gui_values=None, gui_values_path=""): if gui_values_path is None: gui_values_path=self.name self.gui_values,self.gui_values_path=value_handling.get_gui_values(gui_values,gui_values_path) - def setup(self, name, add_indicator=True, gui_values=None, gui_values_path="", gui_thread_safe=False, cache_values=False, change_focused_control=False): + def setup_name(self, name): + """Set the table's name""" + if name is not None: + self.name=name + self.setObjectName(name) + def setup(self, name=None, add_indicator=True, gui_values=None, gui_values_path="", gui_thread_safe=False, cache_values=False, change_focused_control=False): """ Setup the table. @@ -74,21 +91,22 @@ def setup(self, name, add_indicator=True, gui_values=None, gui_values_path="", g (``get/set_value``, ``get/set_all_values``, ``get/set_indicator``, and ``update_indicators``) are automatically called in the GUI thread. cache_values (bool): if ``True`` or ``"update_one"``, store a dictionary with all the current values and update it every time a GUI value is changed; provides a thread-safe way to check current parameters without lag - (unlike :meth:`get_all_values` with ``gui_thread_safe==True``, which re-routes call to a GUI thread and may cause up to 50ms delay) + (unlike :meth:`get_value` or :meth:`get_all_values` with ``gui_thread_safe==True``, which re-route calls to a GUI thread and may cause up to 100ms delay) can also be set to ``"update_all"``, in which case change of any value will cause value update of all variables; otherwise, change of a value will only cause update of that same value (might potentially miss some value updates for custom controls). change_focused_control (bool): if ``False`` and :meth:`set_value` method is called while the widget has user focus, ignore the value; note that :meth:`set_all_values` will still set the widget value. """ - self.name=name - self.setObjectName(self.name) super().setup() + if self.name is None: + self.setup_name(name) self.add_indicator=add_indicator self.setup_gui_values(gui_values=gui_values,gui_values_path=gui_values_path) self.gui_thread_safe=gui_thread_safe self.change_focused_control=change_focused_control self.cache_values=cache_values self.current_values=dictionary.Dictionary() + self.v=self.cv if cache_values else self.wv value_changed=Signal(object,object) @controller.exsafeSlot() @@ -239,12 +257,26 @@ def add_virtual_element(self, name, value=None, add_indicator=None): Doesn't correspond to any actual widget, but behaves very similarly from the application point of view (its value can be set or read, it has on-change events, it can have indicator). + The element value is simply stored on set and retrieved on get. """ value_handler=value_handling.VirtualValueHandler(value) if add_indicator is None: add_indicator=self.add_indicator indicator_handler=value_handling.VirtualIndicatorHandler if add_indicator else None self._add_widget(name,self.ParamRow(None,None,None,value_handler,indicator_handler)) + def add_property_element(self, name, getter=None, setter=None, add_indicator=True): + """ + Add a property value element. + + Doesn't correspond to any actual widget, but behaves very similarly from the application point of view; + each time the value is set or get, the corresponding setter and getter methods are called. + If ``add_indicator==True``, add default (stored value) indicator handler as well. + """ + value_handler=value_handling.PropertyValueHandler(getter=getter,setter=setter) + if add_indicator is None: + add_indicator=self.add_indicator + indicator_handler=value_handling.VirtualIndicatorHandler if add_indicator else None + self._add_widget(name,self.ParamRow(None,None,None,value_handler,indicator_handler)) def add_button(self, name, caption, label=None, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False): """ Add a button to the table. diff --git a/pylablib/gui/widgets/plotters/image_plotter.py b/pylablib/gui/widgets/plotters/image_plotter.py index c9f765e..fe7168e 100644 --- a/pylablib/gui/widgets/plotters/image_plotter.py +++ b/pylablib/gui/widgets/plotters/image_plotter.py @@ -41,10 +41,7 @@ class ImagePlotterCtl(QWidgetContainer): Args: parent: parent widget """ - def __init__(self, parent=None): - super().__init__(parent) - - def setup(self, name, plotter, gui_values=None, gui_values_path=None, save_values=("colormap","img_lim_preset")): + def setup(self, plotter, name=None, gui_values=None, gui_values_path=None, save_values=("colormap","img_lim_preset")): """ Setup the image plotter controller. @@ -56,15 +53,14 @@ def setup(self, name, plotter, gui_values=None, gui_values_path=None, save_value save_values (tuple): optional parameters to include on :meth:`get_all_values`; can include ``"colormap"`` (colormap defined in the widget), and ``"img_lim_preset"`` (saved image limit preset) """ - self.name=name - super().setup(gui_values=gui_values,gui_values_path=gui_values_path,no_margins=True) + super().setup(name=name,gui_values=gui_values,gui_values_path=gui_values_path,no_margins=True) self.save_values=save_values self.setMaximumWidth(200) self.plotter=plotter self.plotter._attach_controller(self) self.params=ParamTable(self) - self.add_widget("img_settings",self.params) - self.params.setup("img_settings",add_indicator=False) + self.add_widget("params",self.params) + self.params.setup(add_indicator=False) self.img_lim=(0,65536) self.params.add_text_label("size",label="Image size:") self.params.add_check_box("flip_x","Flip X",value=False) @@ -196,6 +192,7 @@ class ImagePlotter(QLayoutManagedWidget): """ def __init__(self, parent=None): super().__init__(parent) + self.name=None self.ctl=None self._virtual_values=self._make_virtual_values() @@ -209,7 +206,7 @@ def update_parameters(self, center=None, size=None): self.center=center if size: self.size=size - def setup(self, name, img_size=(1024,1024), min_size=None): + def setup(self, name=None, img_size=(1024,1024), min_size=None): """ Setup the image plotter. @@ -218,8 +215,8 @@ def setup(self, name, img_size=(1024,1024), min_size=None): img_size (tuple): default image size (used only until actual image is supplied) min_size (tuple): minimal widget size (``None`` mean no minimal size) """ + super().setup(layout="vbox",no_margins=True) self.name=name - super().setup("vbox",no_margins=True) self.single_armed=False self.single_acquired=False self.img=np.zeros(img_size) @@ -234,7 +231,7 @@ def setup(self, name, img_size=(1024,1024), min_size=None): if min_size: self.setMinimumSize(QtCore.QSize(*min_size)) self.image_window=pyqtgraph.ImageView(self,imageItem=ImageItem()) - self.add_layout_element(self.image_window) + self.add_to_layout(self.image_window) self.main_layout.setStretch(0,4) self.set_colormap("hot_sat") self.image_window.ui.roiBtn.hide() @@ -260,7 +257,7 @@ def setup(self, name, img_size=(1024,1024), min_size=None): self.cut_lines=[PlotCurveItem(pen="#B0B000",name="Horizontal"), PlotCurveItem(pen="#B000B0",name="Vertical")] for c in self.cut_lines: self.cut_plot_window.addItem(c) - self.add_layout_element(self.cut_plot_window) + self.add_to_layout(self.cut_plot_window) self.main_layout.setStretch(1,1) self.cut_plot_window.setVisible(False) self.vline.sigPositionChanged.connect(self.update_image_controls,QtCore.Qt.DirectConnection) @@ -584,18 +581,18 @@ class ImagePlotterCombined(QWidgetContainer): The plotter can be accessed as ``.plt`` attribute, and the controller as ``.ctl`` attribute. The ``"sidebar"`` sublayout can be used to add additional elements if necessary. """ - def setup(self, name, img_size=(1024,1024), min_size=None, ctl_caption=None, gui_values=None, gui_values_path=None, save_values=("colormap","img_lim_preset")): - self.name=name - super().setup("hbox",no_margins=True,gui_values=gui_values,gui_values_path=gui_values_path) + def setup(self, img_size=(1024,1024), min_size=None, ctl_caption=None, name=None, gui_values=None, gui_values_path=None, save_values=("colormap","img_lim_preset")): + super().setup(layout="hbox",name=name,gui_values=gui_values,gui_values_path=gui_values_path) self.plt=ImagePlotter(self) - self.add_layout_element(self.plt) - self.plt.setup("{}_plotter".format(name),img_size=img_size,min_size=min_size) + self.add_to_layout(self.plt) + self.plt.setup(name="plt",img_size=img_size,min_size=min_size) with self.using_new_sublayout("sidebar","vbox"): self.ctl=ImagePlotterCtl(self) if ctl_caption is None: - self.add_widget("{}_control".format(name),self.ctl) + self.add_widget("ctl",self.ctl) else: - self.add_group_box("{}_control_box".format(name),caption=ctl_caption).add_widget("{}_control".format(name),self.ctl) - self.ctl.setup("{}_control".format(name),self.plt,save_values=save_values) + self.add_group_box("ctl_box",caption=ctl_caption).add_widget("ctl",self.ctl) + self.w["ctl_box"].setMaximumWidth(200) + self.ctl.setup(self.plt,save_values=save_values) self.add_padding() self.get_sublayout().setStretch(0,1) \ No newline at end of file diff --git a/pylablib/gui/widgets/plotters/trace_plotter.py b/pylablib/gui/widgets/plotters/trace_plotter.py index f00456f..e5d2d2b 100644 --- a/pylablib/gui/widgets/plotters/trace_plotter.py +++ b/pylablib/gui/widgets/plotters/trace_plotter.py @@ -29,7 +29,7 @@ class TracePlotterCtl(QWidgetContainer): Args: parent: parent widget """ - def setup(self, name, plotter, gui_values=None, gui_values_path=None): + def setup(self, plotter, name=None, gui_values=None, gui_values_path=None): """ Setup the trace plotter controller. @@ -39,19 +39,18 @@ def setup(self, name, plotter, gui_values=None, gui_values_path=None): gui_values (bool): as :class:`.GUIValues` object used to access table values; by default, create one internally gui_values_path (str): if not ``None``, specifies the path prefix for values inside the control """ - self.name=name - super().setup(gui_values=gui_values,gui_values_path=gui_values_path,no_margins=True) + super().setup(name=name,gui_values=gui_values,gui_values_path=gui_values_path,no_margins=True) self.plotter=plotter self.plotter._attach_controller(self) self.channels_table=ParamTable(self) - self.add_group_box("channels_group_box",caption="Channels").add_widget("channels",self.channels_table,gui_values_path="channels") + self.add_group_box("channels_box",caption="Channels").add_widget("channels",self.channels_table,gui_values_path="channels") self.channels_table.setMinimumSize(QtCore.QSize(20,20)) - self.channels_table.setup("channels",add_indicator=False) + self.channels_table.setup(add_indicator=False) self.setup_channels() self.plot_params_table=ParamTable(self) - self.add_group_box("plotting_group_box",caption="Plotting").add_widget("plotting",self.plot_params_table,gui_values_path="plotting") + self.add_group_box("plotting_box",caption="Plotting").add_widget("plotting",self.plot_params_table,gui_values_path="plotting") self.plot_params_table.setMinimumSize(QtCore.QSize(20,20)) - self.plot_params_table.setup("plotting_params",add_indicator=False) + self.plot_params_table.setup(add_indicator=False) self.plot_params_table.add_toggle_button("update_plot","Updating") self.plot_params_table.add_num_edit("disp_last",1,limiter=(1,None,"coerce","int"),formatter=("int"),label="Display last: ") self.plot_params_table.add_button("reset_history","Reset").get_value_changed_signal().connect(self.plotter.reset_history) @@ -90,7 +89,11 @@ class TracePlotter(QLayoutManagedWidget): Args: parent: parent widget """ - def setup(self, name, add_end_marker=False, update_only_on_visible=True): + def __init__(self, parent=None): + super().__init__(parent) + self.name=None + self.ctl=None + def setup(self, name=None, add_end_marker=False, update_only_on_visible=True): """ Setup the image view. @@ -100,9 +103,9 @@ def setup(self, name, add_end_marker=False, update_only_on_visible=True): update_only_on_visible (bool): if ``True``, only update plot if the widget is visible. """ self.name=name - super().setup("vbox",no_margins=True) + super().setup(layout="vbox",no_margins=True) self.plot_widget=pyqtgraph.PlotWidget(self) - self.add_layout_element(self.plot_widget) + self.add_to_layout(self.plot_widget) self.plot_widget.addLegend() self.plot_widget.setLabel("left","Signal") self.plot_widget.showGrid(True,True,0.7) @@ -316,15 +319,14 @@ class TracePlotterCombined(QWidgetContainer): The plotter can be accessed as ``.plt`` attribute, and the controller as ``.ctl`` attribute. The ``"sidebar"`` sublayout can be used to add additional elements if necessary. """ - def setup(self, name, add_end_marker=False, update_only_on_visible=True, gui_values=None, gui_values_path=None): - self.name=name - super().setup("hbox",no_margins=True,gui_values=gui_values,gui_values_path=gui_values_path) + def setup(self, add_end_marker=False, update_only_on_visible=True, name=None, gui_values=None, gui_values_path=None): + super().setup(layout="hbox",no_margins=True,name=name,gui_values=gui_values,gui_values_path=gui_values_path) self.plt=TracePlotter(self) - self.add_layout_element(self.plt) - self.plt.setup("{}_plotter".format(name),add_end_marker=add_end_marker,update_only_on_visible=update_only_on_visible) + self.add_to_layout(self.plt) + self.plt.setup(name="plt",add_end_marker=add_end_marker,update_only_on_visible=update_only_on_visible) with self.using_new_sublayout("sidebar","vbox"): self.ctl=TracePlotterCtl(self) - self.add_widget("{}_control".format(name),self.ctl) - self.ctl.setup("{}_control".format(name),self.plt) + self.add_widget("ctl",self.ctl) + self.ctl.setup(self.plt) self.add_padding() self.get_sublayout().setStretch(0,1) \ No newline at end of file diff --git a/pylablib/gui/widgets/range_controls.py b/pylablib/gui/widgets/range_controls.py index f0efade..7e95d43 100644 --- a/pylablib/gui/widgets/range_controls.py +++ b/pylablib/gui/widgets/range_controls.py @@ -27,12 +27,11 @@ class RangeCtl(QtWidgets.QWidget): def __init__(self, parent=None): super().__init__(parent) - def setup(self, name, lim=(None,None), order=True, formatter=".1f", labels=("Min","Max","Center","Span","Step"), elements=("minmax","cspan","step")): + def setup(self, lim=(None,None), order=True, formatter=".1f", labels=("Min","Max","Center","Span","Step"), elements=("minmax","cspan","step")): """ Setup the range control. Args: - name (str): widget name lim (tuple): limit containing min and max values order (bool): if ``True``, first value is always smaller than the second one (values are swapped otherwise) formatter (str): formatter for all edit boxes; see :func:`.format.as_formatter` for details @@ -40,16 +39,14 @@ def setup(self, name, lim=(None,None), order=True, formatter=".1f", labels=("Min elements (tuple): tuple specifying elements which are displayed for the control; can contain ``"minmax"`` (min-max row), ``"cspan"`` (center-span row), and ``"step"`` (step row) """ - self.name=name self.order=order self.rng=(0,0,0) if "step" in elements else (0,0) - self.setObjectName(self.name) self.main_layout=QtWidgets.QGridLayout(self) self.main_layout.setObjectName("main_layout") self.main_layout.setContentsMargins(0,0,0,0) self.params=ParamTable(self) self.main_layout.addWidget(self.params) - self.params.setup("params",add_indicator=False,change_focused_control=True) + self.params.setup(name="params",add_indicator=False,change_focused_control=True) self.params.main_layout.setContentsMargins(2,2,2,2) self.params.main_layout.setSpacing(4) if "minmax" in elements: @@ -176,12 +173,11 @@ def validateROI(self, xparams, yparams): xparams=TAxisParams(*xparams) yparams=TAxisParams(*yparams) return xparams,yparams - def setup(self, name, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): + def setup(self, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): """ Setup the ROI control. Args: - name (str): widget name xlim (tuple): limit for x-axis min and max values ylim (tuple): limit for y-axis min and max values sizelim (int or tuple): minimal allowed size (int implies same for both axes) @@ -190,18 +186,15 @@ def setup(self, name, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels= validate: if not ``None``, a function which takes tuple ``(xparams, yparams)`` of two axes parameters (each is a 3-tuple ``(min, max, bin)``) and return their constrained versions. """ - self.name=name self.kind=kind - self.setObjectName(self.name) self.setMinimumSize(QtCore.QSize(100,60)) self.setMaximumSize(QtCore.QSize(2**16,60)) self.main_layout=QtWidgets.QVBoxLayout(self) self.main_layout.setObjectName("main_layout") self.main_layout.setContentsMargins(0,0,0,0) - self.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) self.params=ParamTable(self) self.main_layout.addWidget(self.params) - self.params.setup("params",add_indicator=False) + self.params.setup(name="params",add_indicator=False) self.params.main_layout.setContentsMargins(0,0,0,0) self.params.main_layout.setSpacing(4) self.params.add_decoration_label("ROI",(0,0)) @@ -218,7 +211,7 @@ def setup(self, name, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels= self.params.main_layout.setColumnStretch(2,1) self.validate=validate for n in ["x_min","x_max","y_min","y_max"]: - self.params.w[n].setMinimumWidth(40) + self.params.w[n].setMinimumWidth(30) self.params.vs[n].connect(self._on_edit) self.set_limits(xlim,ylim,minsize=minsize,maxsize=maxsize) @@ -338,12 +331,11 @@ def validateROI(self, xparams, yparams): xparams=TBinAxisParams(*xparams) yparams=TBinAxisParams(*yparams) return xparams,yparams - def setup(self, name, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): + def setup(self, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, labels=("X","Y"), kind="minmax", validate=None): """ Setup the ROI control. Args: - name (str): widget name xlim (tuple): limit for x-axis min and max values ylim (tuple): limit for y-axis min and max values maxbin (int or tuple): maximal allowed binning (int implies same for both axes) @@ -353,17 +345,15 @@ def setup(self, name, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize= validate: if not ``None``, a function which takes tuple ``(xparams, yparams)`` of two axes parameters (each is a 3-tuple ``(min, max, bin)``) and return their constrained versions. """ - self.name=name self.kind=kind - self.setObjectName(self.name) self.setMinimumSize(QtCore.QSize(100,60)) self.setMaximumSize(QtCore.QSize(2**16,60)) - self.main_layout=QtWidgets.QGridLayout(self) - self.main_layout.setObjectName(self.name+"_main_layout") + self.main_layout=QtWidgets.QVBoxLayout(self) + self.main_layout.setObjectName("main_layout") self.main_layout.setContentsMargins(0,0,0,0) self.params=ParamTable(self) self.main_layout.addWidget(self.params) - self.params.setup("params",add_indicator=False) + self.params.setup(name="params",add_indicator=False) self.params.main_layout.setContentsMargins(0,0,0,0) self.params.main_layout.setSpacing(4) self.params.add_decoration_label("ROI",(0,0)) @@ -378,10 +368,10 @@ def setup(self, name, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize= self.params.add_num_edit("y_min",value=0,formatter="int",limiter=(None,None,"coerce","int"),location=(2,1,1,1)) self.params.add_num_edit("y_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,2,1,1)) self.params.add_num_edit("y_bin",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,3,1,1)) + self.params.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) self.params.main_layout.setColumnStretch(1,2) self.params.main_layout.setColumnStretch(2,2) self.params.main_layout.setColumnStretch(3,1) - self.params.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) self.validate=validate for n in ["x_min","x_max","x_bin","y_min","y_max","y_bin"]: self.params.w[n].setMinimumWidth(30) From c64227cfa8417a166265002147e0847ee40238a9 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Wed, 2 Jun 2021 22:41:27 +0200 Subject: [PATCH 04/31] Minor bugfixes --- .pylintdict | 5 ++++- build_distrib.py | 1 + pylablib/__init__.py | 3 ++- pylablib/core/fileio/loadfile.py | 13 ++++++------- pylablib/devices/utils/load_lib.py | 9 +++++---- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.pylintdict b/.pylintdict index ac8d669..828e531 100644 --- a/.pylintdict +++ b/.pylintdict @@ -350,4 +350,7 @@ cyclemode readonly enums devio -unpacker \ No newline at end of file +unpacker +vbox +hbox +sublayouts diff --git a/build_distrib.py b/build_distrib.py index 52c5743..ea0d931 100644 --- a/build_distrib.py +++ b/build_distrib.py @@ -9,6 +9,7 @@ def clear_build(): shutil.rmtree("build",ignore_errors=True) shutil.rmtree("dist",ignore_errors=True) shutil.rmtree("pylablib.egg-info",ignore_errors=True) + shutil.rmtree("pylablib_lightweight.egg-info",ignore_errors=True) def make(): subprocess.call(["python","setup.py","sdist","bdist_wheel"]) def upload(production=False): diff --git a/pylablib/__init__.py b/pylablib/__init__.py index 200dccb..4b7df2c 100644 --- a/pylablib/__init__.py +++ b/pylablib/__init__.py @@ -1,7 +1,8 @@ import os from .core.utils import module as module_utils from .core.utils import library_parameters -from .core.utils.library_parameters import library_parameters as par, temp_library_parameters as temp_par +par=library_parameters.library_parameters +temp_par=library_parameters.temp_library_parameters from .core.dataproc.__export__ import * from .core.devio.__export__ import * diff --git a/pylablib/core/fileio/loadfile.py b/pylablib/core/fileio/loadfile.py index 40a4838..8579f53 100644 --- a/pylablib/core/fileio/loadfile.py +++ b/pylablib/core/fileio/loadfile.py @@ -3,13 +3,12 @@ """ from . import datafile, location, dict_entry, parse_csv, loadfile_utils -from ..utils import funcargparse -from ..utils.library_parameters import library_parameters +from ..utils import funcargparse, library_parameters import numpy as np -library_parameters.update({"fileio/loadfile/csv/out_type":"pandas"}) -library_parameters.update({"fileio/loadfile/dict/inline_out_type":"pandas"}) +library_parameters.library_parameters.update({"fileio/loadfile/csv/out_type":"pandas"}) +library_parameters.library_parameters.update({"fileio/loadfile/dict/inline_out_type":"pandas"}) ##### File formats ##### @@ -75,7 +74,7 @@ class CSVTableInputFileFormat(ITextInputFileFormat): """ def __init__(self, out_type="default", dtype="numeric", columns=None, delimiters=None, empty_entry_substitute=None, ignore_corrupted_lines=True, skip_lines=0): ITextInputFileFormat.__init__(self) - self.out_type=library_parameters["fileio/loadfile/csv/out_type"] if out_type=="default" else out_type + self.out_type=library_parameters.library_parameters["fileio/loadfile/csv/out_type"] if out_type=="default" else out_type self.dtype=dtype self.columns=columns self.delimiters=delimiters or parse_csv._table_delimiters @@ -123,7 +122,7 @@ def __init__(self, case_normalization=None, inline_dtype="generic", inline_out_t ITextInputFileFormat.__init__(self) self.case_normalization=case_normalization self.inline_dtype=inline_dtype - self.inline_out_type=library_parameters["fileio/loadfile/dict/inline_out_type"] if inline_out_type=="default" else inline_out_type + self.inline_out_type=library_parameters.library_parameters["fileio/loadfile/dict/inline_out_type"] if inline_out_type=="default" else inline_out_type if not entry_format in {"branch","dict_entry","value"}: raise ValueError("unrecognized entry format: {0}".format(entry_format)) self.entry_format=entry_format @@ -178,7 +177,7 @@ class BinaryTableInputFileFormatter(IInputFileFormat): """ def __init__(self, out_type="default", dtype=" Date: Wed, 2 Jun 2021 22:43:59 +0200 Subject: [PATCH 05/31] GUI container typo --- pylablib/core/gui/widgets/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylablib/core/gui/widgets/container.py b/pylablib/core/gui/widgets/container.py index ebaac5c..f84eaf8 100644 --- a/pylablib/core/gui/widgets/container.py +++ b/pylablib/core/gui/widgets/container.py @@ -310,7 +310,7 @@ def add_widget(self, name, widget, location=None, gui_values_path=True): if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not; otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. """ - if name==False: + if name!=False: QContainer.add_widget(self,name=name,widget=widget,gui_values_path=gui_values_path) if isinstance(widget,QtWidgets.QWidget): QLayoutManagedWidget.add_to_layout(self,widget,location=location) From 2a78c44ee7b07d59eac8df4862be140c58747193 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Thu, 3 Jun 2021 19:39:26 +0200 Subject: [PATCH 06/31] GUI interface renamings --- pylablib/core/gui/value_handling.py | 11 +- pylablib/core/gui/widgets/container.py | 135 ++++++++++-------- pylablib/core/gui/widgets/param_table.py | 3 + .../gui/widgets/plotters/image_plotter.py | 8 +- .../gui/widgets/plotters/trace_plotter.py | 6 +- 5 files changed, 92 insertions(+), 71 deletions(-) diff --git a/pylablib/core/gui/value_handling.py b/pylablib/core/gui/value_handling.py index 91d5b87..1c578a1 100644 --- a/pylablib/core/gui/value_handling.py +++ b/pylablib/core/gui/value_handling.py @@ -650,6 +650,8 @@ def __init__(self, gui_thread_safe=True): def add_handler(self, name, handler): """Add a value handler under a given name""" + if name in self.handlers: + raise ValueError("handler {} already exists".format(name)) self.handlers[name]=handler return handler def remove_handler(self, name, remove_indicator=True, disconnect=False): @@ -661,10 +663,17 @@ def remove_handler(self, name, remove_indicator=True, disconnect=False): Unlike most methods (e.g., :meth:`get_value` or :meth:`get_handler`), does not recursively query the children, so it only works if the handler is contained in this table. """ + if not self.handlers.has_entry(name,kind="leaf"): + if name in self.handlers: + raise KeyError("can not delete handler branch '{}'".format(name)) + else: + raise KeyError("missing handler '{}'".format(name)) if disconnect: handler=self.get_handler(name) try: - handler.get_value_changed_signal().disconnect() + signal=handler.get_value_changed_signal() + if signal is not None: + handler.get_value_changed_signal().disconnect() except TypeError: # no signals connected or no handle pass del self.handlers[name] diff --git a/pylablib/core/gui/widgets/container.py b/pylablib/core/gui/widgets/container.py index f84eaf8..8b9dd7e 100644 --- a/pylablib/core/gui/widgets/container.py +++ b/pylablib/core/gui/widgets/container.py @@ -11,7 +11,7 @@ TTimer=collections.namedtuple("TTimer",["name","period","timer"]) TTimerEvent=collections.namedtuple("TTimerEvent",["start","loop","stop","timer"]) -TWidget=collections.namedtuple("TWidget",["name","widget","gui_values_path"]) +TChild=collections.namedtuple("TChild",["name","widget","gui_values_path"]) class QContainer(QtCore.QObject): """ Basic controller object which combines and controls several other widget. @@ -29,10 +29,11 @@ def __init__(self, *args, name=None, **kwargs): self._timers={} self._timer_events={} self._running=False - self._widgets=dictionary.Dictionary() + self._children=dictionary.Dictionary() self.setup_gui_values("new") self.ctl=None self.w=dictionary.ItemAccessor(self.get_widget) + self.c=dictionary.ItemAccessor(self.get_child) self.v=dictionary.ItemAccessor(self.get_value,self.set_value) self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) @@ -46,8 +47,8 @@ def setup_gui_values(self, gui_values="new", gui_values_path=""): or ``"new"`` (make a new storage; in this case `gui_values_path` is ignored), and `gui_values_path` is the container's path within this storage. """ - if self._widgets: - raise RuntimeError("can not change gui values after widgets have been added") + if self._children: + raise RuntimeError("can not change gui values after children have been added") if gui_values is not None: self.gui_values,self.gui_values_path=value_handling.get_gui_values(gui_values,gui_values_path) def setup_name(self, name): @@ -141,22 +142,24 @@ def _call_timer_events(self, timer, meth="loop"): elif evt.loop is not None and t.isActive(): # discard all possible after-stop queued events evt.loop() - def add_widget_values(self, path, widget): + def add_child_values(self, path, widget): """ - Add widget's values to the container's table. + Add child's values to the container's table. - If `widget` is a container and ``path==""``, + If `widget` is a container and ``path==""`` or ends in ``"/*"`` (e.g., ``"subpath/*"``), use its :meth:`setup_gui_values` to make it share the same GUI values; otherwise, simply add it to the GUI values under the given path. """ - if path=="": + if path=="" or path=="*" or path.endswith("/*"): + if path.endswith("*"): + path=path[:-1] if hasattr(widget,"setup_gui_values"): - widget.setup_gui_values(self,"") + widget.setup_gui_values(self,path) else: raise ValueError("can not store a non-container widget under an empty path") else: self.gui_values.add_widget(path,widget) - def _setup_widget_name(self, widget, name): + def _setup_child_name(self, widget, name): if name is None: name=getattr(widget,"name",None) if name is None: @@ -164,42 +167,45 @@ def _setup_widget_name(self, widget, name): elif hasattr(widget,"setup_name"): widget.setup_name(name) return name - def add_widget(self, name, widget, gui_values_path=True): + def add_child(self, name, widget, gui_values_path=True): """ - Add a contained widget. + Add a contained child widget. If `gui_values_path` is ``False`` or ``None``, do not add it to the GUI values table; if it is ``True``, add it under the same root (``path==""``) if it's a container, and under `name` if it's not; otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. """ - name=self._setup_widget_name(widget,name) - if name in self._widgets: - raise ValueError("widget {} is already present") + name=self._setup_child_name(widget,name) + if name in self._children: + raise ValueError("child {} is already present") if gui_values_path!=False and gui_values_path is not None: if gui_values_path==True: gui_values_path="" if hasattr(widget,"setup_gui_values") else name - self.add_widget_values(gui_values_path,widget) - self._widgets[name]=TWidget(name,widget,gui_values_path) + self.add_child_values(gui_values_path,widget) + self._children[name]=TChild(name,widget,gui_values_path) return widget - def get_widget(self, name): - """Get the widget with the given name""" - path,subpath=self._widgets.get_max_prefix(name,kind="leaf") + def get_child(self, name): + """Get the child widget with the given name""" + path,subpath=self._children.get_max_prefix(name,kind="leaf") if path: - return self._widgets[path].widget.get_widget(subpath) if subpath else self._widgets[path].widget + return self._children[path].widget.get_child(subpath) if subpath else self._children[path].widget raise KeyError("can't find widget {}".format(name)) - def _clear_widget(self, widget): - if hasattr(widget.widget,"clear"): - widget.widget.clear() - if widget.gui_values_path is not None: - self.gui_values.remove_handler((self.gui_values_path,widget.gui_values_path),remove_indicator=True,disconnect=True) - def remove_widget(self, name): - """Remove widget from the container and clear it""" - path,subpath=self._widgets.get_max_prefix(name,kind="leaf") + def _clear_child(self, child): + if hasattr(child.widget,"clear"): + child.widget.clear() + if child.gui_values_path is not None: + try: + self.gui_values.remove_handler((self.gui_values_path,child.gui_values_path),remove_indicator=True,disconnect=True) + except KeyError: + pass + def remove_child(self, name): + """Remove child from the container and clear it""" + path,subpath=self._children.get_max_prefix(name,kind="leaf") if path: if subpath: - return self._widgets[path].widget.remove_widget(subpath) - w=self._widgets.pop(path) - self._clear_widget(w) + return self._children[path].widget.remove_child(subpath) + ch=self._children.pop(path) + self._clear_child(ch) else: raise KeyError("can't find widget {}".format(name)) @@ -211,10 +217,10 @@ def start(self): Starts all the internal timers, and calls ``start`` method for all the contained widgets. """ if self._running: - raise RuntimeError("widget '{}' loop is already running".format(self.name)) - for w in self._widgets.iternodes(): - if hasattr(w.widget,"start"): - w.widget.start() + raise RuntimeError("container '{}' loop is already running".format(self.name)) + for ch in self._children.iternodes(): + if hasattr(ch.widget,"start"): + ch.widget.start() for n in self._timers: self.start_timer(n) self._running=True @@ -226,13 +232,13 @@ def stop(self): Stops all the internal timers, and calls ``stop`` method for all the contained widgets. """ if not self._running: - raise RuntimeError("widget '{}' loop is not running".format(self.name)) + raise RuntimeError("container '{}' loop is not running".format(self.name)) self._running=False for n in self._timers: self.stop_timer(n) - for w in self._widgets.iternodes(): - if hasattr(w.widget,"stop"): - w.widget.stop() + for ch in self._children.iternodes(): + if hasattr(ch.widget,"stop"): + ch.widget.stop() def clear(self): """ @@ -243,14 +249,15 @@ def clear(self): """ if self._running: self.stop() - for w in self._widgets.iternodes(): - self._clear_widget(w) - self._widgets=dictionary.Dictionary() + for ch in self._children.iternodes(): + self._clear_child(ch) + self._children=dictionary.Dictionary() def get_handler(self, name): """Get value handler of a widget with the given name""" return self.gui_values.get_handler((self.gui_values_path,name or "")) - + def get_widget(self, name): + return self.gui_values.get_widget((self.gui_values_path,name or "")) def get_value(self, name=None): """Get value of a widget with the given name (``None`` means all values)""" return self.gui_values.get_value((self.gui_values_path,name or "")) @@ -291,19 +298,21 @@ class QWidgetContainer(QLayoutManagedWidget, QContainer): Typically, adding widget adds them both to the container values and to the layout; however, this can be skipped by either using :meth:`.QLayoutManagedWidget.add_to_layout` - (only add to the layout), or specifying ``location="skip"`` in :meth:`add_widget` (only add to the container). + (only add to the layout), or specifying ``location="skip"`` in :meth:`add_child` (only add to the container). """ def setup(self, layout="vbox", no_margins=False, name=None, gui_values=None, gui_values_path=""): QContainer.setup(self,name=name,gui_values=gui_values,gui_values_path=gui_values_path) QLayoutManagedWidget.setup(self,layout=layout,no_margins=no_margins) - def add_widget(self, name, widget, location=None, gui_values_path=True): + def add_child(self, name, widget, location=None, gui_values_path=True): """ - Add a contained widget. + Add a contained child widget. - If ``name==False``, only add the widget to they layout, but not to the container. + `name` specifies the child storage name; + if ``name==False``, only add the widget to they layout, but not to the container. `location` specifies the layout location to which the widget is added; - if it is ``"skip"``, skip adding it to the layout (can be manually added later). - Note that if the widget is added to the layout, it will be completely deleted when :meth:`clear` method is called; + if ``location=="skip"``, skip adding it to the layout (can be manually added later). + Note that if the widget is added to the layout, it will be completely deleted + when :meth:`clear`or :meth:`remove_child` methods are called; otherwise, simply its ``clear`` method will be called, and its GUI values will be deleted. If `gui_values_path` is ``False`` or ``None``, do not add it to the GUI values table; @@ -311,18 +320,18 @@ def add_widget(self, name, widget, location=None, gui_values_path=True): otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. """ if name!=False: - QContainer.add_widget(self,name=name,widget=widget,gui_values_path=gui_values_path) + QContainer.add_child(self,name=name,widget=widget,gui_values_path=gui_values_path) if isinstance(widget,QtWidgets.QWidget): QLayoutManagedWidget.add_to_layout(self,widget,location=location) return widget - def remove_widget(self, name): + def remove_child(self, name): """Remove widget from the container and the layout, clear it, and remove it""" - if name in self._widgets: - widget=self._widgets[name].widget - QContainer.remove_widget(self,name) + if name in self._children: + widget=self._children[name].widget + QContainer.remove_child(self,name) QLayoutManagedWidget.remove_layout_element(self,widget) else: - QContainer.remove_widget(self,name) + QContainer.remove_child(self,name) def add_frame(self, name, layout="vbox", location=None, gui_values_path=True, no_margins=True): """ Add a new frame container to the layout. @@ -330,10 +339,10 @@ def add_frame(self, name, layout="vbox", location=None, gui_values_path=True, no `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, and `location` specifies its location within the container layout. If ``no_margins==True``, the frame will have no inner layout margins. - The other parameters are the same as in :meth:`add_widget` method. + The other parameters are the same as in :meth:`add_child` method. """ frame=QFrameContainer(self) - self.add_widget(name,frame,location=location,gui_values_path=gui_values_path) + self.add_child(name,frame,location=location,gui_values_path=gui_values_path) frame.setup(layout=layout,no_margins=no_margins) return frame def add_group_box(self, name, caption, layout="vbox", location=None, gui_values_path=True, no_margins=True): @@ -343,10 +352,10 @@ def add_group_box(self, name, caption, layout="vbox", location=None, gui_values_ `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, and `location` specifies its location within the container layout. If ``no_margins==True``, the frame will have no inner layout margins. - The other parameters are the same as in :meth:`add_widget` method. + The other parameters are the same as in :meth:`add_child` method. """ group_box=QGroupBoxContainer(self) - self.add_widget(name,group_box,location=location,gui_values_path=gui_values_path) + self.add_child(name,group_box,location=location,gui_values_path=gui_values_path) group_box.setup(caption=caption,layout=layout,no_margins=no_margins) return group_box def clear(self): @@ -391,12 +400,12 @@ def add_tab(self, name, caption, index=None, layout="vbox", gui_values_path=True `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, and `location` specifies its location within the container layout. If ``no_margins==True``, the frame will have no inner layout margins. - The other parameters are the same as in :meth:`add_widget` method. + The other parameters are the same as in :meth:`add_child` method. """ if name in self._tabs: raise ValueError("tab {} already exists".format(name)) frame=QFrameContainer(self) - self.add_widget(name=name,widget=frame,gui_values_path=gui_values_path) + self.add_child(name=name,widget=frame,gui_values_path=gui_values_path) frame.setup(layout=layout,no_margins=no_margins) if index is None: index=self.count() @@ -413,7 +422,7 @@ def remove_tab(self, name): Clear it, remove its GUI values, and delete it and all contained widgets. """ - super().remove_widget(name) + super().remove_child(name) frame=self._tabs.pop(name) idx=self.indexOf(frame) self.removeTab(idx) diff --git a/pylablib/core/gui/widgets/param_table.py b/pylablib/core/gui/widgets/param_table.py index 8815c46..3e94591 100644 --- a/pylablib/core/gui/widgets/param_table.py +++ b/pylablib/core/gui/widgets/param_table.py @@ -524,6 +524,9 @@ def get_value_changed_signal(self, name): """Get a value-changed signal for a widget with the given name""" return self.params[self._normalize_name(name)].value_handler.get_value_changed_signal() + get_child=get_widget # form compatibility with QContainer + remove_child=remove_widget + @controller.gui_thread_method def get_indicator(self, name=None): """Get indicator value for a widget with the given name""" diff --git a/pylablib/gui/widgets/plotters/image_plotter.py b/pylablib/gui/widgets/plotters/image_plotter.py index fe7168e..fce4ee0 100644 --- a/pylablib/gui/widgets/plotters/image_plotter.py +++ b/pylablib/gui/widgets/plotters/image_plotter.py @@ -59,7 +59,7 @@ def setup(self, plotter, name=None, gui_values=None, gui_values_path=None, save_ self.plotter=plotter self.plotter._attach_controller(self) self.params=ParamTable(self) - self.add_widget("params",self.params) + self.add_child("params",self.params) self.params.setup(add_indicator=False) self.img_lim=(0,65536) self.params.add_text_label("size",label="Image size:") @@ -589,10 +589,10 @@ def setup(self, img_size=(1024,1024), min_size=None, ctl_caption=None, name=None with self.using_new_sublayout("sidebar","vbox"): self.ctl=ImagePlotterCtl(self) if ctl_caption is None: - self.add_widget("ctl",self.ctl) + self.add_child("ctl",self.ctl) else: - self.add_group_box("ctl_box",caption=ctl_caption).add_widget("ctl",self.ctl) - self.w["ctl_box"].setMaximumWidth(200) + self.add_group_box("ctl_box",caption=ctl_caption).add_child("ctl",self.ctl) + self.c["ctl_box"].setMaximumWidth(200) self.ctl.setup(self.plt,save_values=save_values) self.add_padding() self.get_sublayout().setStretch(0,1) \ No newline at end of file diff --git a/pylablib/gui/widgets/plotters/trace_plotter.py b/pylablib/gui/widgets/plotters/trace_plotter.py index e5d2d2b..f966fb0 100644 --- a/pylablib/gui/widgets/plotters/trace_plotter.py +++ b/pylablib/gui/widgets/plotters/trace_plotter.py @@ -43,12 +43,12 @@ def setup(self, plotter, name=None, gui_values=None, gui_values_path=None): self.plotter=plotter self.plotter._attach_controller(self) self.channels_table=ParamTable(self) - self.add_group_box("channels_box",caption="Channels").add_widget("channels",self.channels_table,gui_values_path="channels") + self.add_group_box("channels_box",caption="Channels").add_child("channels",self.channels_table,gui_values_path="channels") self.channels_table.setMinimumSize(QtCore.QSize(20,20)) self.channels_table.setup(add_indicator=False) self.setup_channels() self.plot_params_table=ParamTable(self) - self.add_group_box("plotting_box",caption="Plotting").add_widget("plotting",self.plot_params_table,gui_values_path="plotting") + self.add_group_box("plotting_box",caption="Plotting").add_child("plotting",self.plot_params_table,gui_values_path="plotting") self.plot_params_table.setMinimumSize(QtCore.QSize(20,20)) self.plot_params_table.setup(add_indicator=False) self.plot_params_table.add_toggle_button("update_plot","Updating") @@ -326,7 +326,7 @@ def setup(self, add_end_marker=False, update_only_on_visible=True, name=None, gu self.plt.setup(name="plt",add_end_marker=add_end_marker,update_only_on_visible=update_only_on_visible) with self.using_new_sublayout("sidebar","vbox"): self.ctl=TracePlotterCtl(self) - self.add_widget("ctl",self.ctl) + self.add_child("ctl",self.ctl) self.ctl.setup(self.plt) self.add_padding() self.get_sublayout().setStretch(0,1) \ No newline at end of file From 7060b8518a02cdfeb98cf42cab8003f1569632f8 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Fri, 4 Jun 2021 22:11:12 +0200 Subject: [PATCH 07/31] Minor threading fixes and restruct --- pylablib/core/devio/SCPI.py | 4 - pylablib/core/thread/callsync.py | 12 +- pylablib/core/thread/controller.py | 211 +++++++++++++---------------- 3 files changed, 106 insertions(+), 121 deletions(-) diff --git a/pylablib/core/devio/SCPI.py b/pylablib/core/devio/SCPI.py index 45771cc..6ac37bf 100644 --- a/pylablib/core/devio/SCPI.py +++ b/pylablib/core/devio/SCPI.py @@ -223,7 +223,6 @@ def _read_retry(self, raw=False, size=None, timeout=None, wait_callback=None, re t.reraise() error_msg="read raises IOError; waiting {0} sec before trying to recover".format(self._retry_delay) warnings.warn(error_msg) - # log.default_log.info(error_msg,origin="devices/SCPI",level="warning") self.sleep(self._retry_delay) self._try_recover(t.try_number) def _write_retry(self, msg="", flush=False): @@ -241,7 +240,6 @@ def _write_retry(self, msg="", flush=False): return sent error_msg="write raises IOError; waiting {0} sec before trying to recover".format(self._retry_delay) warnings.warn(error_msg) - # log.default_log.info(error_msg,origin="devices/SCPI",level="warning") self.sleep(self._retry_delay) self._try_recover(t.try_number) def _ask_retry(self, msg, delay=0., raw=False, size=None, timeout=None, wait_callback=None, retry=None): @@ -258,7 +256,6 @@ def _ask_retry(self, msg, delay=0., raw=False, size=None, timeout=None, wait_cal t.reraise() error_msg="ask raises IOError; waiting {0} sec before trying to recover".format(self._retry_delay) warnings.warn(error_msg) - # log.default_log.info(error_msg,origin="devices/SCPI",level="warning") self.sleep(self._retry_delay) self._try_recover(t.try_number) @@ -510,7 +507,6 @@ def ask(self, msg, data_type="string", delay=0., timeout=None, read_echo=False): t.reraise() error_msg="ask error in instrument {} returned {}".format(self.instr,reply) warnings.warn(error_msg) - # log.default_log.info(error_msg,origin="devices/SCPI",level="warning") self.sleep(0.5) self.flush() self._try_recover(t.try_number) diff --git a/pylablib/core/thread/callsync.py b/pylablib/core/thread/callsync.py index a0ba8e5..f41611c 100644 --- a/pylablib/core/thread/callsync.py +++ b/pylablib/core/thread/callsync.py @@ -6,6 +6,9 @@ import time import collections + +### Remote call results ### + class QCallResultSynchronizer(QThreadNotifier): def get_progress(self): """ @@ -115,10 +118,17 @@ def waiting_state(self): def notifying_state(self): return "done" + + + +### Remote call ### + class QScheduledCall: """ Object representing a scheduled remote call. + Can be called, skipped, or failed in the target thread, in which case it notifies the result synchronizer (if supplied). + Args: func: callable to be invoked in the destination thread args: arguments to be passed to `func` @@ -279,7 +289,7 @@ class QQueueScheduler(QScheduler): - :meth:`call_added`: called when a new call has been added to the queue - :meth:`call_popped`: called when a call has been removed from the queue (either for execution, or for skipping) """ - def __init__(self, on_full_queue="current", call_info_argname=None): + def __init__(self, on_full_queue="skip_current", call_info_argname=None): QScheduler.__init__(self,call_info_argname=call_info_argname) self.call_queue=collections.deque() funcargparse.check_parameter_range(on_full_queue,"on_full_queue",{"skip_current","skip_newest","skip_oldest","call","wait"}) diff --git a/pylablib/core/thread/controller.py b/pylablib/core/thread/controller.py index c1ff65b..e3cd5b5 100644 --- a/pylablib/core/thread/controller.py +++ b/pylablib/core/thread/controller.py @@ -6,8 +6,10 @@ import threading import contextlib import time -import sys, traceback +import sys +import traceback import heapq +import warnings _default_multicast_pool=mpool.MulticastPool() @@ -86,7 +88,7 @@ class QThreadControllerThread(QtCore.QThread): finalized=Signal() _stop_request=Signal() def __init__(self, controller): - QtCore.QThread.__init__(self) + super().__init__() self.moveToThread(self) self.controller=controller self._stop_request.connect(self._do_quit) @@ -172,7 +174,7 @@ class QThreadController(QtCore.QObject): - ``finished``: emitted on thread finish (before :meth:`on_finish` is executed) """ def __init__(self, name=None, kind="loop", multicast_pool=None): - QtCore.QObject.__init__(self) + super().__init__() funcargparse.check_parameter_range(kind,"kind",{"loop","run","main"}) if kind=="main": name="gui" @@ -222,6 +224,7 @@ def __init__(self, name=None, kind="loop", multicast_pool=None): self._stop_notifiers=[] # set up life control self._stop_requested=(self.kind!="main") + self._suspend_stop_request=False self._lifetime_state_lock=threading.Lock() self._lifetime_state="stopped" # set up signals @@ -399,8 +402,7 @@ def _wait_in_process_loop(self, done_check, timeout=None, as_toploop=False): with self._inner_loop(as_toploop=as_toploop): ctd=general.Countdown(timeout) while True: - if self._stop_requested: - raise threadprop.InterruptExceptionStop() + self._check_stop_request() if timeout is not None: time_left=ctd.time_left() if time_left: @@ -499,20 +501,40 @@ def check_messages(self, top_loop=False): """ with self._inner_loop(as_toploop=top_loop): threadprop.get_app().processEvents(QtCore.QEventLoop.AllEvents) - if self._stop_requested: - raise threadprop.InterruptExceptionStop() - def sleep(self, timeout, top_loop=False): + self._check_stop_request() + def sleep(self, timeout, wake_on_message=False, top_loop=False): """ Sleep for a given time (in seconds). Unlike :func:`time.sleep`, constantly checks the event loop for new messages (e.g., if stop or interrupt commands are issued). + In addition, if ``wake_on_message==True``, wake up if any message has been received; + it this case. return ``True`` if the wait has been completed, and ``False`` if it has been interrupted by a message. If ``top_loop==True``, treat the waiting as the top message loop (i.e., any top loop message or signal can be executed here). Local call method. """ try: - self._wait_in_process_loop(lambda: (False,None),timeout=timeout,as_toploop=top_loop) + self._wait_in_process_loop(lambda: (wake_on_message,None),timeout=timeout,as_toploop=top_loop) + return False except threadprop.TimeoutThreadError: - pass + return True + def _check_stop_request(self): + if self._stop_requested and not self._suspend_stop_request: + raise threadprop.InterruptExceptionStop + @contextlib.contextmanager + def no_stopping(self): + """ + Context manager, which temporarily suspends stop requests (:exc:`.InterruptExceptionStop` exceptions). + + If the stop request has been made within this block, raise the excpetion on exit. + Note that :meth:`stop` method and, correspondingly, :func:`stop_controller` still work, when called from the controlled thread. + """ + try: + suspend_stop_request=self._suspend_stop_request + self._suspend_stop_request=True + yield + finally: + self._suspend_stop_request=suspend_stop_request + self._check_stop_request() ### Overloaded methods for thread events ### @@ -1023,24 +1045,44 @@ def call_in_thread_sync(self, func, args=None, kwargs=None, sync=True, callback= -class QMultiRepeatingThread(QThreadController): +class QTaskThread(QThreadController): """ Thread which allows to set up and run jobs and batch jobs with a certain time period, and execute commands in the meantime. - Mostly serves as a base to a much more flexible :class:`QTaskThread` class; should rarely be considered directly. - Args: name(str): thread name (by default, generate a new unique name) + args: args supplied to :meth:`setup_task` method + kwargs: keyword args supplied to :meth:`setup_task` method multicast_pool: :class:`.MulticastPool` for this thread (by default, use the default common pool) + Attributes: + ca: asynchronous command accessor, which makes calls more function-like; + ``ctl.ca.comm(*args,**kwarg)`` is equivalent to ``ctl.call_command("comm",args,kwargs,sync=False)`` + cs: synchronous command accessor, which makes calls more function-like; + ``ctl.cs.comm(*args,**kwarg)`` is equivalent to ``ctl.call_command("comm",args,kwargs,sync=True)`` + css: synchronous command accessor which is made 'exception-safe' via :func:`exsafe` wrapper (i.e., safe to directly connect to slots) + ``ctl.css.comm(*args,**kwarg)`` is equivalent to ``with exint(): ctl.call_command("comm",args,kwargs,sync=True)`` + csi: synchronous command accessor which ignores and silences any exceptions (including missing /stopped controller) + useful for sending queries during thread finalizing / application shutdown, when it's not guaranteed that the command recipient is running + (commands already ignore any errors, unless their results are specifically requested); + useful for synchronous commands in finalizing functions, where other threads might already be stopped + m: method accessor; directly calls the method corresponding to the command; + ``ctl.m.comm(*args,**kwarg)`` is equivalent to ``ctl.call_command("comm",*args,**kwargs)``, which is often also equivalent to ``ctl.comm(*args,**kwargs)``; + for most practical purposes it's the same as directly invoking the class method, but it makes intent more explicit + (as command methods are usually not called directly from other threads), and it doesn't invoke warning about calling method instead of command from another thread. + Methods to overload: - - ``on_start``: executed on the thread startup (between synchronization points ``"start"`` and ``"run"``) - - :meth:`on_finish`: executed on thread cleanup (attempts to execute in any case, including exceptions) - - :meth:`check_commands`: executed once a scheduling cycle to check for new commands / events and execute them + - :meth:`setup_task`: executed on the thread startup (between synchronization points ``"start"`` and ``"run"``) + - :meth:`finalize_task`: executed on thread cleanup (attempts to execute in any case, including exceptions) """ + ## Action performed when another thread explicitly calls a method corresponding to a command (which is usually a typo) + ## Can be used to overload default behavior in children classes or instances + ## Can be ``"warning"``, which prints warning about this call (default), + ## or one of the accessor names (e.g., ``"c"`` or ``"q"``), which routes the call through this accessor + _direct_comm_call_action="warning" _new_jobs_check_period=0.02 # command refresh period if no jobs are scheduled (otherwise, after every job) - def __init__(self, name=None, multicast_pool=None): - QThreadController.__init__(self,name,kind="run",multicast_pool=multicast_pool) + def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): + super().__init__(name=name,kind="run",multicast_pool=multicast_pool) self.sync_period=0 self._last_sync_time=0 self.jobs={} @@ -1048,7 +1090,18 @@ def __init__(self, name=None, multicast_pool=None): self._jobs_list=[] self.batch_jobs={} self._batch_jobs_args={} - + self.args=args or [] + self.kwargs=kwargs or {} + self._commands={} + self._sched_order=[] + self._multicast_schedulers={} + self._command_warned=set() + self.ca=self.CommandAccess(self,sync=False) + self.cs=self.CommandAccess(self,sync=True) + self.css=self.CommandAccess(self,sync=True,safe=True) + self.csi=self.CommandAccess(self,sync=True,safe=True,ignore_errors=True) + self.m=self.CommandAccess(self,sync=True,direct=True) + ### Job handling ### # Called only in the controlled thread # @@ -1188,15 +1241,6 @@ def stop_batch_job(self, name, error_on_stopped=False): if cleanup: cleanup(*args,**kwargs) - def check_commands(self): - """ - Check for commands to execute. - - Called once every scheduling cycle: after any recurrent or batch job, but at least every `self._new_jobs_check_period` seconds (by default 20ms). - Local method, called automatically. - """ - - def _get_next_job(self, ct): if not self._jobs_list: return None,None @@ -1219,6 +1263,9 @@ def _acknowledge_job(self, name): self.timers[name].acknowledge(nmin=1) except ValueError: pass + + ### Start/run/stop control (called automatically) ### + def run(self): while True: ct=time.time() @@ -1243,71 +1290,21 @@ def run(self): job() self.check_commands() + def on_start(self): + super().on_start() + self.setup_task(*self.args,**self.kwargs) def on_finish(self): - QThreadController.on_finish(self) + super().on_finish() for n in self.batch_jobs: if n in self.jobs: self.stop_batch_job(n) + self.finalize_task() + for name in self._commands: + self._commands[name][1].clear() + ### Command methods ### - - - -class QTaskThread(QMultiRepeatingThread): - """ - Thread which allows to set up and run jobs and batch jobs with a certain time period, and execute commands in the meantime. - - Extension of :class:`QMultiRepeatingThread` with more powerful command scheduling and more user-friendly interface. - - Args: - name(str): thread name (by default, generate a new unique name) - args: args supplied to :meth:`setup_task` method - kwargs: keyword args supplied to :meth:`setup_task` method - multicast_pool: :class:`.MulticastPool` for this thread (by default, use the default common pool) - - Attributes: - ca: asynchronous command accessor, which makes calls more function-like; - ``ctl.ca.comm(*args,**kwarg)`` is equivalent to ``ctl.call_command("comm",args,kwargs,sync=False)`` - cs: synchronous command accessor, which makes calls more function-like; - ``ctl.cs.comm(*args,**kwarg)`` is equivalent to ``ctl.call_command("comm",args,kwargs,sync=True)`` - css: synchronous command accessor which is made 'exception-safe' via :func:`exsafe` wrapper (i.e., safe to directly connect to slots) - ``ctl.csi.comm(*args,**kwarg)`` is equivalent to ``with exint(): ctl.call_command("comm",args,kwargs,sync=True)`` - csi: synchronous command accessor which ignores and silences any exceptions (including missing /stopped controller) - useful for sending queries during thread finalizing / application shutdown, when it's not guaranteed that the command recipient is running - (commands already ignore any errors, unless their results are specifically requested); - useful for synchronous commands in finalizing functions, where other threads might already be stopped - m: method accessor; directly calls the method corresponding to the command; - ``ctl.m.comm(*args,**kwarg)`` is equivalent to ``ctl.call_command("comm",*args,**kwargs)``, which is often also equivalent to ``ctl.comm(*args,**kwargs)``; - for most practical purposes it's the same as directly invoking the class method, but it makes intent more explicit - (as command methods are usually not called directly from other threads), and it doesn't invoke warning about calling method instead of command from another thread. - - Methods to overload: - - :meth:`setup_task`: executed on the thread startup (between synchronization points ``"start"`` and ``"run"``) - - :meth:`finalize_task`: executed on thread cleanup (attempts to execute in any case, including exceptions) - - :meth:`process_multicast`: process a directed multicast (multicast with ``dst`` equal to this thread name); by default, does nothing - """ - ## Action performed when another thread explicitly calls a method corresponding to a command (which is usually a typo) - ## Can be used to overload default behavior in children classes or instances - ## Can be ``"warning"``, which prints warning about this call (default), - ## or one of the accessor names (e.g., ``"c"`` or ``"q"``), which routes the call through this accessor - _direct_comm_call_action="warning" - def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): - QMultiRepeatingThread.__init__(self,name=name,multicast_pool=multicast_pool) - self.args=args or [] - self.kwargs=kwargs or {} - self._directed_multicast.connect(self._on_directed_multicast,QtCore.Qt.QueuedConnection) - self._commands={} - self._sched_order=[] - self._multicast_schedulers={} - self._command_warned=set() - self.ca=self.CommandAccess(self,sync=False) - self.cs=self.CommandAccess(self,sync=True) - self.css=self.CommandAccess(self,sync=True,safe=True) - self.csi=self.CommandAccess(self,sync=True,safe=True,ignore_errors=True) - self.m=self.CommandAccess(self,sync=True,direct=True) - - def _call_command_method(self, name, original_method, args, kwargs): """Call given method taking into account ``_direct_comm_call_action``""" if threadprop.current_controller() is not self: @@ -1318,7 +1315,7 @@ def _call_command_method(self, name, original_method, args, kwargs): name,self.name,threadprop.current_controller().name),file=sys.stderr) self._command_warned.add(name) else: - accessor=QMultiRepeatingThread.__getattribute__(self,action) + accessor=getattr(self,action) return accessor.__getattr__(name)(*args,**kwargs) return original_method(*args,**kwargs) def _override_command_method(self, name): @@ -1335,12 +1332,6 @@ def setup_task(self, *args, **kwargs): """ Setup the thread (called before the main task loop). - Local call method, called automatically. - """ - def process_multicast(self, src, tag, value): - """ - Process a named multicast (with `dst` equal to the thread name) from the multicast pool. - Local call method, called automatically. """ def finalize_task(self): @@ -1369,24 +1360,6 @@ def update_status(self, kind, status, text=None, notify=True): self.set_variable(status_str+"_text",text) self.send_multicast("any",status_str+"_text",text) - ### Start/stop control (called automatically) ### - def on_start(self): - QMultiRepeatingThread.on_start(self) - self.setup_task(*self.args,**self.kwargs) - self.subscribe_direct(self._recv_directed_multicast) - def on_finish(self): - QMultiRepeatingThread.on_finish(self) - self.finalize_task() - for name in self._commands: - self._commands[name][1].clear() - - _directed_multicast=Signal(object) - @toploopSlot(object) - def _on_directed_multicast(self, msg): - self.process_multicast(*msg) - def _recv_directed_multicast(self, tag, src, value): - self._directed_multicast.emit((tag,src,value)) - ### Command control ### def _add_scheduler(self, scheduler, priority): for i,(p,_) in enumerate(self._sched_order): @@ -1449,6 +1422,12 @@ def add_direct_call_command(self, name, command=None, error_on_async=True): command=getattr(self,name) self._commands[name]=(command,"direct_sync" if error_on_async else "direct") def check_commands(self): + """ + Check for commands to execute. + + Called once every scheduling cycle: after any recurrent or batch job, but at least every `self._new_jobs_check_period` seconds (by default 20ms). + Local method, called automatically. + """ while True: called=False for _,scheduler in self._sched_order: @@ -1501,7 +1480,7 @@ def subscribe_commsync(self, callback, srcs="any", tags=None, dsts=None, filt=No return sid def unsubscribe(self, sid): - QMultiRepeatingThread.unsubscribe(self,sid) + super().unsubscribe(sid) if sid in self._multicast_schedulers: self._remover_scheduler(self._multicast_schedulers[sid]) del self._multicast_schedulers[sid] @@ -1590,7 +1569,7 @@ def _store_created_controller(controller): """ with _running_threads_lock: if _running_threads_stopping: - raise threadprop.InterruptExceptionStop() + raise threadprop.InterruptExceptionStop name=controller.name if (name in _running_threads) or (name in _created_threads): raise threadprop.DuplicateControllerThreadError("thread with name {} already exists".format(name)) @@ -1603,7 +1582,7 @@ def _register_controller(controller): """ with _running_threads_lock: if _running_threads_stopping: - raise threadprop.InterruptExceptionStop() + raise threadprop.InterruptExceptionStop name=controller.name if name in _running_threads: raise threadprop.DuplicateControllerThreadError("thread with name {} already exists".format(name)) @@ -1678,9 +1657,9 @@ def get_gui_controller(sync=False, timeout=None, create_if_missing=True): return gui_ctl -def stop_controller(name, code=0, sync=True, require_controller=False): +def stop_controller(name=None, code=0, sync=True, require_controller=False): """ - Stop a controller with a given name. + Stop a controller with a given name (current controller by default). `code` specifies controller exit code (only applies to the main thread controller). If ``require_controller==True`` and the controller is not present, raise and error; otherwise, do nothing. From 8ee52ce9d6f7589bfdf576b8fe70eb3245a809bb Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Mon, 7 Jun 2021 22:20:07 +0200 Subject: [PATCH 08/31] Changed task thread scheduling --- pylablib/core/thread/callsync.py | 188 ++++++++---- pylablib/core/thread/controller.py | 378 +++++++++++++++---------- pylablib/core/thread/multicast_pool.py | 6 - pylablib/core/utils/general.py | 31 +- 4 files changed, 394 insertions(+), 209 deletions(-) diff --git a/pylablib/core/thread/callsync.py b/pylablib/core/thread/callsync.py index f41611c..9af27b5 100644 --- a/pylablib/core/thread/callsync.py +++ b/pylablib/core/thread/callsync.py @@ -69,6 +69,7 @@ class QDummyResultSynchronizer: """Dummy result synchronizer for call which don't require result synchronization (e.g., multicasts)""" def notify(self, value): pass +dummy_synchronizer=QDummyResultSynchronizer() class QDirectResultSynchronizer: """ @@ -127,7 +128,7 @@ class QScheduledCall: """ Object representing a scheduled remote call. - Can be called, skipped, or failed in the target thread, in which case it notifies the result synchronizer (if supplied). + Can be executed, skipped, or failed in the target thread, in which case it notifies the result synchronizer (if supplied). Args: func: callable to be invoked in the destination thread @@ -136,25 +137,27 @@ class QScheduledCall: result_synchronizer: result synchronizer object; can be ``None`` (create new :class:`QCallResultSynchronizer`), ``"async"`` (no result synchronization), or a :class:`QCallResultSynchronizer` object. """ - Callback=collections.namedtuple("Callback",["func","pass_result","call_on_fail"]) + Callback=collections.namedtuple("Callback",["func","pass_result","call_on_exception","call_on_unschedule"]) def __init__(self, func, args=None, kwargs=None, result_synchronizer=None): self.func=func self.args=args or [] self.kwargs=kwargs or {} if result_synchronizer=="async": - result_synchronizer=QDummyResultSynchronizer() + result_synchronizer=dummy_synchronizer elif result_synchronizer is None: result_synchronizer=QCallResultSynchronizer() self.result_synchronizer=result_synchronizer self.callbacks=[] self._notified=[0] # hack to avoid use of locks ([0] is False, [] is True, use .pop() to atomically check and change) + self.state="wait" def _check_notified(self): try: self._notified.pop() return False except IndexError: return True - def __call__(self): + def execute(self): + """Execute the call and notify the result synchronizer (invoked by the destination thread)""" if self._check_notified(): return try: @@ -164,36 +167,46 @@ def __call__(self): res=("exception",e) raise finally: + self.state=res[0] for c in self.callbacks: - if c.call_on_fail or res[0]=="result": + if c.call_on_exception or c.call_on_unschedule or res[0]=="result": if c.pass_result: c.func(res[1] if res[0]=="result" else None) else: c.func() self.result_synchronizer.notify(res) - def add_callback(self, callback, pass_result=True, call_on_fail=False, position=None): + def add_callback(self, callback, pass_result=True, call_on_exception=False, call_on_unschedule=False, front=False): """ Set the callback to be executed after the main call is done. - Callback is not provided with any arguments. If ``pass_result==True``, pass function result to the callback (or ``None`` if call failed); otherwise, pass no arguments. - If ``callback_on_fail==True``, call it even if the original call raised an exception. - `position` specifies callback position in the call list (by default, end of the list). + If ``call_on_exception==True``, call it even if the original call raised an exception. + If ``call_on_unschedule==True``, call it for any call unscheduling event, including using :meth:`skip` or :meth:`fail` methods + (this effectively ignores `call_on_exception`, since the callback is called regardless of the exception). + If ``front==True``, add the callback in the front of the line (executes first). """ - cb=self.Callback(callback,pass_result,call_on_fail) - if position is None: - self.callbacks.append(cb) + cb=self.Callback(callback,pass_result,call_on_exception,call_on_unschedule) + if front: + self.callbacks.insert(0,cb) else: - self.callbacks.insert(position,cb) + self.callbacks.append(cb) def fail(self): """Notify that the call is failed (invoked by the destination thread)""" if self._check_notified(): return + self.state="fail" + for c in self.callbacks: + if c.call_on_unschedule: + c.func() self.result_synchronizer.notify(("fail",None)) def skip(self): """Notify that the call is skipped (invoked by the destination thread)""" if self._check_notified(): return + self.state="skip" + for c in self.callbacks: + if c.call_on_unschedule: + c.func() self.result_synchronizer.notify(("skip",None)) @@ -219,7 +232,7 @@ def __init__(self, call_info_argname=None): def build_call_info(self): """Build call info tuple which can be passed to scheduled calls""" return TDefaultCallInfo(time.time()) - def build_call(self, func, args=None, kwargs=None, callback=None, pass_result=True, callback_on_fail=True, sync_result=True): + def build_call(self, func, args=None, kwargs=None, callback=None, pass_result=True, callback_on_exception=True, sync_result=True): """ Build :class:`QScheduledCall` for subsequent scheduling. @@ -229,7 +242,7 @@ def build_call(self, func, args=None, kwargs=None, callback=None, pass_result=Tr kwargs: keyword arguments to be passed to `func` callback: optional callback to be called when `func` is done pass_result (bool): if ``True``, pass `func` result as a single argument to the callback; otherwise, give no arguments - callback_on_fail (bool): if ``True``, execute the callback on call fail or skip (if it requires an argument, ``None`` is supplied); + callback_on_exception (bool): if ``True``, execute the callback on call fail or skip (if it requires an argument, ``None`` is supplied); otherwise, only execute it if the call was successful sync_result: if ``True``, the call has a default result synchronizer; otherwise, no synchronization is made. """ @@ -238,7 +251,7 @@ def build_call(self, func, args=None, kwargs=None, callback=None, pass_result=Tr if self.call_info_argname: scheduled_call.kwargs[self.call_info_argname]=self.build_call_info() if callback is not None: - scheduled_call.add_callback(callback,pass_result=pass_result,call_on_fail=callback_on_fail) + scheduled_call.add_callback(callback,pass_result=pass_result,call_on_exception=callback_on_exception) return scheduled_call def schedule(self, call): # pylint: disable=unused-argument """Schedule the call""" @@ -257,11 +270,11 @@ class QDirectCallScheduler(QScheduler): call_info_argname: if not ``None``, supplies a name of a keyword argument via which call info (generated by :meth:`QScheduler.build_call_info`) is passed on function call """ - def build_call(self, func, args=None, kwargs=None, callback=None, pass_result=True, callback_on_fail=True, sync_result=False): + def build_call(self, func, args=None, kwargs=None, callback=None, pass_result=True, callback_on_exception=True, sync_result=False): return QScheduler.build_call(self,func,args=args,kwargs=kwargs, - callback=callback,pass_result=pass_result,callback_on_fail=callback_on_fail,sync_result=sync_result) + callback=callback,pass_result=pass_result,callback_on_exception=callback_on_exception,sync_result=sync_result) def schedule(self, call): - call() + call.execute() return True @@ -276,11 +289,14 @@ class QQueueScheduler(QScheduler): Used as a default command scheduler. Args: - on_full_queue: action to be taken if the call can't be scheduled (i.e., :meth:`can_schedule` returns ``False``); - can be ``"skip_current"`` (skip the call which is being scheduled), ``"skip_newest"`` (skip the most recent call; place the current) + on_full_queue: action to be taken if the call can't be scheduled (i.e., :meth:`can_schedule` returns ``False``); can be + ``"skip_current"`` (skip the call which is being scheduled), + ``"skip_newest"`` (skip the most recent call; place the current) ``"skip_oldest"`` (skip the oldest call in the queue; place the current), - ``"wait"`` (wait until the call can be scheduled, which is checked after every call removal from the queue; place the call), - or ``"call"`` (execute the call directly in the calling thread; should be used with caution). + ``"call_current"`` (execute the call which is being scheduled immediately in the caller thread), + ``"call_newest"`` (execute the most recent call immediately in the caller thread), + ``"call_oldest"`` (execute the oldest call in the queue immediately in the caller thread), or + ``"wait"`` (wait until the call can be scheduled, which is checked after every call removal from the queue; place the call) call_info_argname: if not ``None``, supplies a name of a keyword argument via which call info (generated by :meth:`QScheduler.build_call_info`) is passed on function call @@ -292,39 +308,33 @@ class QQueueScheduler(QScheduler): def __init__(self, on_full_queue="skip_current", call_info_argname=None): QScheduler.__init__(self,call_info_argname=call_info_argname) self.call_queue=collections.deque() - funcargparse.check_parameter_range(on_full_queue,"on_full_queue",{"skip_current","skip_newest","skip_oldest","call","wait"}) + funcargparse.check_parameter_range(on_full_queue,"on_full_queue",{"skip_current","skip_newest","skip_oldest","call_current","call_newest","call_oldest","wait"}) self.on_full_queue=on_full_queue self.lock=threading.Lock() self.call_popped_notifier=QMultiThreadNotifier() if on_full_queue=="wait" else None self.working=True - # self._call_add_notifiers=[] - # self._call_pop_notifiers=[] + self._last_popped=[None] def can_schedule(self, call): # pylint: disable=unused-argument """Check if the call can be scheduled""" return True def call_added(self, call): """Called whenever `call` has been added to the queue""" - pass - def call_popped(self, call, head): + def call_popped(self, call, idx): """ Called whenever `call` has been removed from the queue - `head` determines whether the call has been removed from the queue head, or from the queue tail. + `idx` determines the call position within the queue. """ - pass def _add_call(self, call): self.call_queue.append(call) self.call_added(call) - # for f in self._call_add_notifiers: - # f(call) def _pop_call(self, head=False): try: call=self.call_queue.popleft() if head else self.call_queue.pop() if self.call_popped_notifier is not None: self.call_popped_notifier.notify() - self.call_popped(call,head) - # for f in self._call_pop_notifiers: - # f(call,head) + self.call_popped(call,0 if head else -1) + self._last_popped=[self._last_popped[-1],call] return call except IndexError: return None @@ -345,6 +355,7 @@ def schedule(self, call): self.call_popped_notifier.wait(wait_n) scheduled=True skipped_call=None + execute_call=None with self.lock: if self.can_schedule(call): self._add_call(call) @@ -357,13 +368,20 @@ def schedule(self, call): elif self.on_full_queue=="skip_current": skipped_call=call scheduled=False + elif self.on_full_queue=="call_newest": + execute_call=self._pop_call() + self._add_call(call) + elif self.on_full_queue=="call_oldest": + execute_call=self._pop_call(head=True) + self._add_call(call) + elif self.on_full_queue=="call_current": + execute_call=call else: scheduled=False if skipped_call is not None: skipped_call.skip() - if self.on_full_queue=="call" and not scheduled: - call() - scheduled=True + if execute_call is not None: + execute_call.execute() return scheduled def pop_call(self): """ @@ -373,6 +391,23 @@ def pop_call(self): """ with self.lock: return self._pop_call(head=True) + def unschedule(self, call): + """ + Unschedule a given call. + + Designed for joint queue operation, so the call is not notified (assume that it has been already notified elsewhere). + """ + if call in self._last_popped: + return + with self.lock: + try: + idx=self.call_queue.index(call) + del self.call_queue[idx] + if self.call_popped_notifier is not None: + self.call_popped_notifier.notify() + self.call_popped(call,idx) + except ValueError: + pass def has_calls(self): """Check if there are queued calls""" return bool(self.call_queue) @@ -405,10 +440,14 @@ class QQueueLengthLimitScheduler(QQueueScheduler): Args: max_len: maximal queue length; non-positive values are interpreted as no limit - on_full_queue: action to be taken if the call can't be scheduled (the queue is full); - can be ``"skip_current"`` (skip the call which is being scheduled), ``"skip_newest"`` (skip the most recent call; place the current) - ``"skip_oldest"`` (skip the oldest call in the queue; place the current), ``"wait"`` (wait until the queue has space; place the call), - or ``"call"`` (execute the call directly in the calling thread; should be used with caution). + on_full_queue: action to be taken if the call can't be scheduled (the queue is full); can be + ``"skip_current"`` (skip the call which is being scheduled), + ``"skip_newest"`` (skip the most recent call; place the current) + ``"skip_oldest"`` (skip the oldest call in the queue; place the current), + ``"call_current"`` (execute the call which is being scheduled immediately in the caller thread), + ``"call_newest"`` (execute the most recent call immediately in the caller thread), + ``"call_oldest"`` (execute the oldest call in the queue immediately in the caller thread), or + ``"wait"`` (wait until the call can be scheduled, which is checked after every call removal from the queue; place the call) call_info_argname: if not ``None``, supplies a name of a keyword argument via which call info (generated by :meth:`QScheduler.build_call_info`) is passed on function call """ @@ -435,10 +474,14 @@ class QQueueSizeLimitScheduler(QQueueScheduler): size_calc: function that takes a single argument (call to be placed) and returns its size; can be either a single number, or a tuple (if several different size metrics are involved); by default, simply returns 1, which makes the scheduler behavior identical to :class:`QQueueLengthLimitScheduler` - on_full_queue: action to be taken if the call can't be scheduled (the queue is full); - can be ``"skip_current"`` (skip the call which is being scheduled), ``"skip_newest"`` (skip the most recent call; place the current) - ``"skip_oldest"`` (skip the oldest call in the queue; place the current), ``"wait"`` (wait until the queue has space; place the call), - or ``"call"`` (execute the call directly in the calling thread; should be used with caution). + on_full_queue: action to be taken if the call can't be scheduled (the queue is full); can be + ``"skip_current"`` (skip the call which is being scheduled), + ``"skip_newest"`` (skip the most recent call; place the current) + ``"skip_oldest"`` (skip the oldest call in the queue; place the current), + ``"call_current"`` (execute the call which is being scheduled immediately in the caller thread), + ``"call_newest"`` (execute the most recent call immediately in the caller thread), + ``"call_oldest"`` (execute the oldest call in the queue immediately in the caller thread), or + ``"wait"`` (wait until the call can be scheduled, which is checked after every call removal from the queue; place the call) call_info_argname: if not ``None``, supplies a name of a keyword argument via which call info (generated by :meth:`QScheduler.build_call_info`) is passed on function call """ @@ -461,9 +504,9 @@ def call_added(self, call): size=funcargparse.as_sequence(self._get_size(call)) for s,q in zip(size,self.size_queues): q.append(s) - def call_popped(self, call, head): + def call_popped(self, call, idx): for q in self.size_queues: - q.pop(0 if head else -1) + q.pop(idx) def can_schedule(self, call): for ms,q in zip(self.max_size,self.size_queues): if ms>0 and sum(q)>=ms: @@ -472,6 +515,53 @@ def can_schedule(self, call): +def schedule_multiple_queues(call, queues): + """ + Schedule the call simultaneously in several queues. + + Go through queues in the given order and schedule call in every one of them. + If one of the schedules failed or the call has been executed there, unschedule it from all the previous queues + and return ``False``; otherwise, return ``True``. + """ + if len(queues)==0: + return True + if len(queues)==1: + return queues[0].schedule(call) and call.state=="wait" + def queue_unscheduler(queue): + return lambda: queue.unschedule(call) + added=[] + complete=False + try: + for q in queues: + call.add_callback(queue_unscheduler(q),pass_result=False,call_on_unschedule=True,front=True) + q.schedule(call) + if call.state!="wait": # skipped or executed + return False + added.append(q) + complete=True + return True + finally: + if not complete and added: + added[-1].unschedule(call) # the previous ones should be unscheduled via the callback + + +class QMultiQueueScheduler: + """ + Wrapper around :func:`schedule_multiple_queues` which acts as a single scheduler. + + Support additional notifiers, which are called if the scheduling is successful + (e.g., to notify and wake up the destination thread). + """ + def __init__(self, schedulers, notifiers): + self.schedulers=schedulers + self.notifiers=notifiers + def build_call(self, *args, **kwargs): + return self.schedulers[0].build_call(*args,**kwargs) + def schedule(self, call): + if schedule_multiple_queues(call,self.schedulers): + for n in self.notifiers: + n() + class QThreadCallScheduler(QScheduler): """ @@ -520,7 +610,7 @@ def schedule(self, call): if self.limit_queue<=0 or self.queue_cntself._last_sync_time+self.sync_period: + self._last_sync_time=t + self.check_messages(top_loop=True) + self._in_command_loop=False + self._check_priority_queues() ### Start/run/stop control (called automatically) ### def run(self): while True: ct=time.time() - name,to=self._get_next_job(ct) - if name is None: - self.sleep(self._new_jobs_check_period) - else: - run_job=True - if (self._last_sync_time is None) or (self._last_sync_time+self.sync_period<=ct): - self._last_sync_time=ct - if not to: - self.check_messages(top_loop=True) - if to: - if to>self._new_jobs_check_period: - run_job=False - self.sleep(self._new_jobs_check_period) - else: - self.sleep(to) - if run_job: - self._acknowledge_job(name) - job=self.jobs[name] - job() - self.check_commands() + to=self._schedule_pending_jobs(ct) + sleep_time=self._new_jobs_check_period if to is None else min(self._new_jobs_check_period,to) + if sleep_time>=0: + self.sleep(sleep_time,wake_on_message=True) + self._exhaust_queued_calls() def on_start(self): super().on_start() @@ -1299,11 +1398,11 @@ def on_finish(self): if n in self.jobs: self.stop_batch_job(n) self.finalize_task() - for name in self._commands: - self._commands[name][1].clear() + for q in self._priority_queues.values(): + q.clear() - ### Command methods ### + ### Command call methods ### def _call_command_method(self, name, original_method, args, kwargs): """Call given method taking into account ``_direct_comm_call_action``""" @@ -1361,17 +1460,7 @@ def update_status(self, kind, status, text=None, notify=True): self.send_multicast("any",status_str+"_text",text) ### Command control ### - def _add_scheduler(self, scheduler, priority): - for i,(p,_) in enumerate(self._sched_order): - if p Date: Sat, 12 Jun 2021 20:45:48 +0200 Subject: [PATCH 09/31] Include uc480 acq restart, add frame transfer exc --- docs/devices/Andor.rst | 2 +- docs/devices/uc480.rst | 7 +- pylablib/devices/Andor/AndorSDK3.py | 7 +- pylablib/devices/Andor/base.py | 2 + pylablib/devices/interface/camera.py | 17 +++- pylablib/devices/uc480/uc480.py | 115 ++++++++++++++++++++++----- 6 files changed, 123 insertions(+), 27 deletions(-) diff --git a/docs/devices/Andor.rst b/docs/devices/Andor.rst index 4f1d441..c99f926 100644 --- a/docs/devices/Andor.rst +++ b/docs/devices/Andor.rst @@ -126,4 +126,4 @@ The operation of these cameras is also relatively standard. They support all the The description of the attributes is given in `manual `__. - - USB cameras can, in principle, generate data at higher rate than about 320Mb/s that the USB3 bus supports. For example, Andor Zyla with 16 bit readout has a single full frame size of 8Mb, which puts the maximal USB throughput at about 40FPS. At the same time, the camera itself is capable of reading up to 100FPS at the full frame. Hence, it is possible to overflow the camera internal buffer (size on the order of 1Gb) regardless of the PC performance. If this happens, the acquisition process halts and needs to be restarted. To check for the number of buffer overflows, you can use :meth:`.AndorSDK3Camera.get_missed_frames_status`, and to reset this counter, :meth:`.AndorSDK3Camera.reset_overflows_counter` (it also automatically resets on acquisition clearing, but not stopping). In addition, the class can implement different strategies when encountering overflow while waiting for a new frame. It is set using :meth:`.AndorSDK3Camera.set_overflow_behavior`, and it can be ``"error"`` (raise :exc:`.AndorError`, which is the default behavior), ``"restart"`` (restart the acquisition and immediately raise timeout error), or ``"ignore"`` (ignore the overflow, which will eventually lead to a timeout error, as the new frames are no longer generated). \ No newline at end of file + - USB cameras can, in principle, generate data at higher rate than about 320Mb/s that the USB3 bus supports. For example, Andor Zyla with 16 bit readout has a single full frame size of 8Mb, which puts the maximal USB throughput at about 40FPS. At the same time, the camera itself is capable of reading up to 100FPS at the full frame. Hence, it is possible to overflow the camera internal buffer (size on the order of 1Gb) regardless of the PC performance. If this happens, the acquisition process halts and needs to be restarted. To check for the number of buffer overflows, you can use :meth:`.AndorSDK3Camera.get_missed_frames_status`, and to reset this counter, :meth:`.AndorSDK3Camera.reset_overflows_counter` (it also automatically resets on acquisition clearing, but not stopping). In addition, the class can implement different strategies when encountering overflow while waiting for a new frame. It is set using :meth:`.AndorSDK3Camera.set_overflow_behavior`, and it can be ``"error"`` (raise :exc:`.AndorFrameTransferError`, which is the default behavior), ``"restart"`` (restart the acquisition and immediately raise timeout error), or ``"ignore"`` (ignore the overflow, which will eventually lead to a timeout error, as the new frames are no longer generated). \ No newline at end of file diff --git a/docs/devices/uc480.rst b/docs/devices/uc480.rst index 0525b41..1207a3e 100644 --- a/docs/devices/uc480.rst +++ b/docs/devices/uc480.rst @@ -44,4 +44,9 @@ Operation The operation of these cameras is relatively standard. They support all the standard methods for dealing with ROI and exposure, starting and stopping acquisition, and operating the frame reading loop. However, there's a couple of differences from the standard libraries worth highlighting: - Some cameras support both binning (adding several pixels together) and subsampling (skipping some pixels). However, only one can be enabled at a time. They can be set independently using, correspondingly, :meth:`.UC480Camera.get_binning`/:meth:`.UC480Camera.set_binning` and :meth:`.UC480Camera.get_subsampling`/:meth:`.UC480Camera.set_subsampling`. They can also be set as binning factors in :meth:`.UC480Camera.get_roi`/:meth:`.UC480Camera.set_roi`. Whether binning or subsampling is set there can be determined by the ``roi_binning_mode`` parameter on creation. - - Uc480 API supports many different pixel modes, including packed ones. However, pylablib currently supports only unpacked modes. \ No newline at end of file + - Uc480 API supports many different pixel modes, including packed ones. However, pylablib currently supports only unpacked modes. + - Occasionally (especially at high frame rates) frames get skipped during transfer, before they are placed into the frame buffer by the camera driver. This can happen in two different ways. First, the frame is simply dropped without any indication. This typically can not be detected without using the framestamp contained in the frame info, as the frames flow appear to be uninterrupted. In the second way, the acquisition appears to get "restarted" (the internal number of acquired frames is dropped to zero), which is detected by the library. In this case there are several different ways the software can react, which are controlled using :meth:`.UC480Camera.set_frameskip_behavior`. + + The default way to address this "restart" event (``"ignore"``) is to ignore it and only adjust the internal acquired frame counter; this manifests as quietly dropped frames, exactly the same as the first kind of event. In the other method (``"skip"``), some number of frames are marked as skipped, so that the difference between the number of acquired frames and the internal framestamp is kept constant. This makes the gap explicit in the camera frame counters. Finally (``"error"``), the software can raise :exc:`.uc480.uc480FrameTransferError` when such event is detected, which can be used to, e.g., restart the acquisition. + + One needs to keep in mind, that while the last two methods make "restarts" more explicit, they do not address the first kind of events (quiet drops). The most direct way to deal with them is to use frame information by setting ``return_info=True`` in frame reading methods like ``read_multiple_images``. This information contains the internal camera framestamp, which lets one detect any skipped frames. \ No newline at end of file diff --git a/pylablib/devices/Andor/AndorSDK3.py b/pylablib/devices/Andor/AndorSDK3.py index cf91ac1..c5dc172 100644 --- a/pylablib/devices/Andor/AndorSDK3.py +++ b/pylablib/devices/Andor/AndorSDK3.py @@ -1,4 +1,4 @@ -from .base import AndorError, AndorTimeoutError, AndorNotSupportedError +from .base import AndorError, AndorTimeoutError, AndorFrameTransferError, AndorNotSupportedError from . import atcore_lib from .atcore_lib import lib, AndorSDK3LibError, feature_types, read_uint12 @@ -204,6 +204,7 @@ class AndorSDK3Camera(camera.IBinROICamera, camera.IExposureCamera, camera.IAttr """ Error=AndorError TimeoutError=AndorTimeoutError + FrameTransferError=AndorFrameTransferError _TFrameInfo=TFrameInfo _frameinfo_fields=general.make_flat_namedtuple(TFrameInfo,fields={"size":camera.TFrameSize})._fields def __init__(self, idx=0): @@ -629,7 +630,7 @@ def set_overflow_behavior(self, behavior): """ Choose the camera behavior if buffer overflow is encountered when waiting for a new frame. - Can be ``"error"`` (raise ``AndorError``), ``"restart"`` (restart the acquisition), or ``"ignore"`` (ignore the overflow, which will cause the wait to time out). + Can be ``"error"`` (raise ``AndorFrameTransferError``), ``"restart"`` (restart the acquisition), or ``"ignore"`` (ignore the overflow, which will cause the wait to time out). """ self._overflow_behavior=behavior @@ -758,7 +759,7 @@ def _check_buffer_overflow(self): return False if self._overflow_behavior=="error": self.stop_acquisition() - raise AndorError("buffer overflow") + raise self.FrameTransferError("buffer overflow while waiting for a new frame") self.start_acquisition() return True return False diff --git a/pylablib/devices/Andor/base.py b/pylablib/devices/Andor/base.py index b6a451b..1ae28e2 100644 --- a/pylablib/devices/Andor/base.py +++ b/pylablib/devices/Andor/base.py @@ -4,5 +4,7 @@ class AndorError(DeviceError): """Generic Andor error""" class AndorTimeoutError(AndorError): """Andor timeout error""" +class AndorFrameTransferError(AndorError): + """Andor frame transfer error""" class AndorNotSupportedError(AndorError): """Option not supported error""" \ No newline at end of file diff --git a/pylablib/devices/interface/camera.py b/pylablib/devices/interface/camera.py index 82d8ccc..033f1a6 100644 --- a/pylablib/devices/interface/camera.py +++ b/pylablib/devices/interface/camera.py @@ -10,6 +10,10 @@ import threading + +class DefaultFrameTransferError(comm_backend.DeviceError): + """Generic frame transfer error""" + TFramesStatus=collections.namedtuple("TFramesStatus",["acquired","unread","skipped","buffer_size"]) TFrameSize=collections.namedtuple("TFrameSize",["width","height"]) TFramePosition=collections.namedtuple("TFramePosition",["left","top"]) @@ -26,6 +30,7 @@ class ICamera(interface.IDevice): _clear_pausing_acquisition=False Error=comm_backend.DeviceError TimeoutError=comm_backend.DeviceError + FrameTransferError=DefaultFrameTransferError def __init__(self): super().__init__() self._acq_params=None @@ -290,6 +295,8 @@ def get_frame_info_fields(self): """ return list(self._frameinfo_fields) def _convert_frame_info(self, info, fmt=None): + if info is None: + return None if fmt is None: fmt=self._frameinfo_format if fmt=="namedtuple": @@ -502,6 +509,7 @@ def reset(self, buffer_size=None): self.last_acquired_frame=-1 self.last_wait_frame=-1 self.last_read_frame=-1 + self.first_valid_frame=-1 self.skipped_frames=0 def update_acquired_frames(self, acquired_frames): """Update the counter of acquired frames (needs to be called by the camera whenever necessary)""" @@ -553,7 +561,8 @@ def get_frames_status(self, acquired_frames=None): return (0,0,0,0) self.update_acquired_frames(acquired_frames) full_unread=self.last_acquired_frame-self.last_read_frame - unread=min(full_unread,self.buffer_size) + valid_chunk=max(0,min(self.buffer_size,self.last_acquired_frame-self.first_valid_frame)) + unread=min(full_unread,valid_chunk) skipped=self.skipped_frames+(full_unread-unread) return (self.last_acquired_frame+1,unread,skipped,self.buffer_size) @@ -581,7 +590,7 @@ def trim_frames_range(self, rng): rng[1]=min(rng[1],acquired_frames) if rng[1]<=rng[0]: rng=rng[0],rng[0] - oldest_valid_frame=self.last_acquired_frame-self.buffer_size+1 + oldest_valid_frame=max(self.first_valid_frame,self.last_acquired_frame-self.buffer_size+1) if rng[1]<=oldest_valid_frame: return (oldest_valid_frame,oldest_valid_frame),rng[1]-rng[0] else: @@ -593,6 +602,10 @@ def advance_read_frames(self, rng): return self.skipped_frames+=max(rng[0]-1-self.last_read_frame,0) self.last_read_frame=max(self.last_read_frame,rng[1]-1) + def set_first_valid_frame(self, first_valid_frame): + """Set the first valid frame; all frames older than it are considered invalid when calculating skipped frames and trimming ranges""" + if self.buffer_size is not None: + self.first_valid_frame=first_valid_frame diff --git a/pylablib/devices/uc480/uc480.py b/pylablib/devices/uc480/uc480.py index afcba71..4872a35 100644 --- a/pylablib/devices/uc480/uc480.py +++ b/pylablib/devices/uc480/uc480.py @@ -12,7 +12,9 @@ class uc480TimeoutError(uc480Error): - "uc480 frame timeout error" + """uc480 frame timeout error""" +class uc480FrameTransferError(uc480Error): + """uc480 frame transfer error""" TCameraInfo=collections.namedtuple("TCameraInfo",["cam_id","dev_id","sens_id","model","serial_number","in_use","status"]) @@ -35,7 +37,7 @@ def find_by_serial(serial_number): TDeviceInfo=collections.namedtuple("TDeviceInfo",["cam_id","model","manufacturer","serial_number","usb_version","date","dll_version","camera_type"]) -TAcquiredFramesStatus=collections.namedtuple("TAcquiredFramesStatus",["acquired","transfer_missed"]) +TAcquiredFramesStatus=collections.namedtuple("TAcquiredFramesStatus",["acquired","transfer_missed","frameskip_events"]) TTimestamp=collections.namedtuple("TTimestamp",["year","month","day","hour","minute","second","millisecond"]) TFrameInfo=collections.namedtuple("TFrameInfo",["frame_index","framestamp","timestamp","timestamp_dev","size","io_status","flags"]) class UC480Camera(camera.IBinROICamera,camera.IExposureCamera): @@ -54,6 +56,7 @@ class UC480Camera(camera.IBinROICamera,camera.IExposureCamera): """ Error=uc480Error TimeoutError=uc480TimeoutError + FrameTransferError=uc480FrameTransferError _TFrameInfo=TFrameInfo _frameinfo_fields=general.make_flat_namedtuple(TFrameInfo,fields={"timestamp":TTimestamp,"size":camera.TFrameSize})._fields def __init__(self, cam_id=0, roi_binning_mode="auto", dev_id=None): @@ -67,6 +70,10 @@ def __init__(self, cam_id=0, roi_binning_mode="auto", dev_id=None): self.is_dev_id=True self.hcam=None self._buffers=None + self._frameskip_behavior="skip" + self._acq_offset=0 # offset between old and new acquired frame counter (changed when 'acquisition restart' happens) + self._buff_offset=0 # offset between acquired frame counter and buffer counter (changed when 'acquisition restart' happens) + self._frameskip_events=0 self._acq_in_progress=False self.open() self._all_color_modes=self._check_all_color_modes() @@ -140,15 +147,49 @@ def _allocate_buffers(self, n): bpp=self._get_pixel_mode_settings()[0] self._buffers=[] for _ in range(n): - self._buffers.append(lib.is_AllocImageMem(1,frame_size[0],frame_size[1],bpp)) - lib.is_AddToSequence(self.hcam,*self._buffers[-1]) + self._buffers.append((lib.is_AllocImageMem(self.hcam,frame_size[0],frame_size[1],bpp),(frame_size[0],frame_size[1]),bpp)) + lib.is_AddToSequence(self.hcam,*self._buffers[-1][0]) return n def _deallocate_buffers(self): if self._buffers is not None: lib.is_ClearSequence(self.hcam) for b in self._buffers: - lib.is_FreeImageMem(self.hcam,*b) + lib.is_FreeImageMem(self.hcam,*b[0]) self._buffers=None + def _find_buffer(self, buff): + baddr=[ctypes.cast(b[0][0],ctypes.c_void_p).value for b in self._buffers] + buffaddr=ctypes.cast(buff,ctypes.c_void_p).value + return baddr.index(buffaddr) + def _get_buffer_state(self): + bs=lib.is_GetActSeqBuf(self.hcam) + return bs[0],self._find_buffer(bs[1]),self._find_buffer(bs[2]) + def _update_buffer_counter(self, timeout=None, skip_gap=False): + ctd=general.Countdown(timeout) + while True: + bs=self._get_buffer_state() + if bs!=(1,0,0): + break + if ctd.passed(): + return False + last_acq=lib.is_CameraStatus(self.hcam,uc480_defs.CAMINFO.IS_SEQUENCE_CNT,uc480_defs.CAMINFO.IS_GET_STATUS)+self._acq_offset-1 + last_buffer=bs[2] + frame_stat=self._frame_counter.get_frames_status() + prev_acq=frame_stat[0] + prev_buffer=prev_acq%frame_stat[3] + dbuff=(last_buffer-prev_buffer)%frame_stat[3] + dacq=last_acq-prev_acq + acq_shift=dbuff-dacq + self._acq_offset+=acq_shift + if skip_gap: + last_stamp=lib.is_GetImageInfo(self.hcam,self._buffers[last_buffer][0][1]).u64FrameNumber + prev_stamp=lib.is_GetImageInfo(self.hcam,self._buffers[prev_buffer][0][1]).u64FrameNumber + dstamp=last_stamp-prev_stamp + stamp_shift=dstamp-dbuff + self._acq_offset+=stamp_shift + self._buff_offset+=stamp_shift + self._frame_counter.set_first_valid_frame(self._acq_offset) + self._frameskip_events+=1 + return True ### Generic controls ### @@ -331,6 +372,7 @@ def setup_acquisition(self, nframes=100): def clear_acquisition(self): self.stop_acquisition() self._deallocate_buffers() + self._reset_skip_counter() super().clear_acquisition() def start_acquisition(self, *args, **kwargs): self.stop_acquisition() @@ -338,21 +380,51 @@ def start_acquisition(self, *args, **kwargs): lib.is_ResetCaptureStatus(self.hcam) lib.is_CaptureVideo(self.hcam,uc480_defs.LIVEFREEZE.IS_DONT_WAIT,check=True) self._acq_in_progress=True + self._reset_skip_counter() self._frame_counter.reset(self._acq_params["nframes"]) def stop_acquisition(self): if self.acquisition_in_progress(): - self._frame_counter.update_acquired_frames(self._get_acquired_frames()) + self._frame_counter.update_acquired_frames(self._get_acquired_frames(error_on_skip=False)) lib.is_StopLiveVideo(self.hcam,0) self._acq_in_progress=False def acquisition_in_progress(self): return self._acq_in_progress + def get_frames_status(self): + if self.acquisition_in_progress(): + self._frame_counter.update_acquired_frames(self._get_acquired_frames(error_on_skip=False)) + return self._TFramesStatus(*self._frame_counter.get_frames_status()) def get_acquired_frame_status(self): - acquired=self._get_acquired_frames() + acquired=self._get_acquired_frames(error_on_skip=False) cstat=lib.is_GetCaptureStatus(self.hcam).adwCapStatusCnt_Detail transfer_missed=sum([cstat[i] for i in [0xa2,0xa3,0xb2,0xc7]]) - return TAcquiredFramesStatus(acquired,transfer_missed) - def _get_acquired_frames(self): - return lib.is_CameraStatus(self.hcam,uc480_defs.CAMINFO.IS_SEQUENCE_CNT,uc480_defs.CAMINFO.IS_GET_STATUS) + return TAcquiredFramesStatus(acquired,transfer_missed,self._frameskip_events) + _p_frameskip_behavior=interface.EnumParameterClass("frameskip_behavior",["error","ignore","skip"]) + @interface.use_parameters(behavior="frameskip_behavior") + def set_frameskip_behavior(self, behavior): + """ + Choose the camera behavior if frame skip event is encountered when waiting for a new frame, reading frames, getting buffer status, etc. + + Can be ``"error"`` (raise ``uc480FrameTransferError``), ``"ignore"`` (continue acquisition, ignore the gap), + or ``"skip"`` (mark some number of frames as skipped, but keep the frame counters consistent). + """ + self._frameskip_behavior=behavior + def _reset_skip_counter(self): + self._acq_offset=0 + self._buff_offset=0 + self._frameskip_events=0 + def _get_acquired_frames(self, error_on_skip=True): + acq=lib.is_CameraStatus(self.hcam,uc480_defs.CAMINFO.IS_SEQUENCE_CNT,uc480_defs.CAMINFO.IS_GET_STATUS)+self._acq_offset + prev_acq=self._frame_counter.get_frames_status()[0] + if acq1 else ()) + def _read_buffer(self, n, return_info=False, nchan=None): + buff,dim,bpp=self._buffers[(n-self._buff_offset)%len(self._buffers)] + frame_info=lib.is_GetImageInfo(self.hcam,buff[1]) if return_info else None + if nchan is None: + nchan=self._get_pixel_mode_settings()[1] + shape=dim+((nchan,) if nchan>1 else ()) frame=np.empty(shape=shape,dtype=self._np_dtypes[bpp//nchan]) lib.is_CopyImageMem(self.hcam,buff[0],buff[1],frame.ctypes.data) frame=self._convert_indexing(frame,"rct") - ts=frame_info.TimestampSystem - ts=TTimestamp(ts.wYear,ts.wMonth,ts.wDay,ts.wHour,ts.wMinute,ts.wSecond,ts.wMilliseconds) - size=camera.TFrameSize(frame_info.dwImageWidth,frame_info.dwImageHeight) - frame_info=TFrameInfo(n,frame_info.u64FrameNumber,ts,frame_info.u64TimestampDevice,size,frame_info.dwIoStatus,frame_info.dwFlags) + if return_info: + ts=frame_info.TimestampSystem + ts=TTimestamp(ts.wYear,ts.wMonth,ts.wDay,ts.wHour,ts.wMinute,ts.wSecond,ts.wMilliseconds) + size=camera.TFrameSize(frame_info.dwImageWidth,frame_info.dwImageHeight) + frame_info=TFrameInfo(n,frame_info.u64FrameNumber,ts,frame_info.u64TimestampDevice,size,frame_info.dwIoStatus,frame_info.dwFlags) return frame,self._convert_frame_info(frame_info) def _read_frames(self, rng, return_info=False): - data=[self._read_buffer(n) for n in range(rng[0],rng[1])] + nchan=self._get_pixel_mode_settings()[1] + data=[self._read_buffer(n,return_info=return_info,nchan=nchan) for n in range(rng[0],rng[1])] return [d[0] for d in data],[d[1] for d in data] def _zero_frame(self, n): bpp,nchan=self._get_pixel_mode_settings() From c54249ce0a987194ef74771e9850845c48a21cd3 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Sat, 12 Jun 2021 21:08:41 +0200 Subject: [PATCH 10/31] Fileio takes file objects; minor additions --- pylablib/core/dataproc/filters.py | 2 + pylablib/core/fileio/loadfile.py | 12 +++--- pylablib/core/fileio/location.py | 41 +++++++++++++++++++ pylablib/core/fileio/savefile.py | 14 +++---- pylablib/core/utils/general.py | 7 +++- .../gui/widgets/plotters/image_plotter.py | 3 ++ 6 files changed, 65 insertions(+), 14 deletions(-) diff --git a/pylablib/core/dataproc/filters.py b/pylablib/core/dataproc/filters.py index 76d9856..e2f3d08 100644 --- a/pylablib/core/dataproc/filters.py +++ b/pylablib/core/dataproc/filters.py @@ -254,6 +254,8 @@ def _decimation_filter(a, decimation_function, width, axis=0, mode="drop"): raise ValueError("unrecognized binning mode: "+mode) width=int(width) shape=np.shape(a) + if axis<0: + axis=max(axis+len(shape),0) actual_len=shape[axis] dec_len=int(actual_len/width)*width if mode=="drop": diff --git a/pylablib/core/fileio/loadfile.py b/pylablib/core/fileio/loadfile.py index 8579f53..0ebae54 100644 --- a/pylablib/core/fileio/loadfile.py +++ b/pylablib/core/fileio/loadfile.py @@ -252,7 +252,7 @@ def load_csv(path=None, out_type="default", dtype="numeric", columns=None, delim Load data table from a CSV/table file. Args: - path (str): path to the file + path (str): path to the file of a file-like object out_type (str): type of the result: ``'array'`` for numpy array, ``'pandas'`` for pandas DataFrame, or ``'default'`` (determined by the library default; ``'pandas'`` by default) dtype: dtype of entries; can be either a single type, or a list of types (one per column). @@ -282,7 +282,7 @@ def load_csv_desc(path=None, loc="file", return_file=False): Analogous to :func:`load_dict`, but doesn't allow any additional parameters (which don't matter in this case). Args: - path (str): path to the file + path (str): path to the file of a file-like object loc (str): location type (``"file"`` means the usual file location; see :func:`.location.get_location` for details) return_file (bool): if ``True``, return :class:`.DataFile` object (contains some metainfo); otherwise, return just the file data """ @@ -293,7 +293,7 @@ def load_bin(path=None, out_type="default", dtype=" Date: Sun, 13 Jun 2021 19:14:37 +0200 Subject: [PATCH 11/31] Expanded task thread controller --- pylablib/core/thread/callsync.py | 5 +- pylablib/core/thread/controller.py | 135 +++++++++++++++++++++++++---- 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/pylablib/core/thread/callsync.py b/pylablib/core/thread/callsync.py index 9af27b5..26dd8b5 100644 --- a/pylablib/core/thread/callsync.py +++ b/pylablib/core/thread/callsync.py @@ -398,7 +398,7 @@ def unschedule(self, call): Designed for joint queue operation, so the call is not notified (assume that it has been already notified elsewhere). """ if call in self._last_popped: - return + return False with self.lock: try: idx=self.call_queue.index(call) @@ -406,8 +406,9 @@ def unschedule(self, call): if self.call_popped_notifier is not None: self.call_popped_notifier.notify() self.call_popped(call,idx) + return True except ValueError: - pass + return False def has_calls(self): """Check if there are queued calls""" return bool(self.call_queue) diff --git a/pylablib/core/thread/controller.py b/pylablib/core/thread/controller.py index 1ffb8b8..567c4ce 100644 --- a/pylablib/core/thread/controller.py +++ b/pylablib/core/thread/controller.py @@ -1,4 +1,4 @@ -from ..utils import general, funcargparse, dictionary, functions as func_utils +from ..utils import general, funcargparse, dictionary, functions as func_utils, py3 from . import multicast_pool as mpool, threadprop, synchronizing, callsync from ..gui import QtCore, Slot, Signal @@ -334,6 +334,7 @@ def _kill_poke_timer(self): def _on_finish_event(self): with self._lifetime_state_lock: self._lifetime_state="finishing" + self.notify_exec_point("cleanup") self._stop_requested=False self.finished.emit() try: @@ -669,12 +670,13 @@ def send_multicast(self, dst="any", tag=None, value=None, src=None): ### Variable management ### _variable_change_tag="#sync.wait.variable" - def set_variable(self, name, value, notify=False, notify_tag="changed/*"): + def set_variable(self, name, value, update=False, notify=False, notify_tag="changed/*"): """ Set thread variable. Can be called in any thread (controlled or external). If ``notify==True``, send an multicast with the given `notify_tag` (where ``"*"`` symbol is replaced by the variable name). + If ``update==True`` and the value is a dictionary, update the branch rather than overwrite it. Local call method. """ split_name=tuple(dictionary.normalize_path(name)) @@ -682,7 +684,10 @@ def set_variable(self, name, value, notify=False, notify_tag="changed/*"): with self._params_val_lock: if name in self._params_funcs: del self._params_funcs[name] - self._params_val.add_entry(name,value,force=True) + if update: + self._params_val.merge(name,value) + else: + self._params_val.add_entry(name,value,force=True) for exp_name in self._params_exp: if exp_name==split_name[:len(exp_name)] or split_name==exp_name[:len(split_name)]: notify_list.append((self._params_val[exp_name],self._params_exp[exp_name])) @@ -911,7 +916,8 @@ def notify_exec_point(self, point): """ Mark the given execution point as passed. - Automatically invoked points include ``"start"`` (thread starting), ``"run"`` (thread setup and ready to run), and ``"stop"`` (thread finished). + Automatically invoked points include ``"start"`` (thread starting), ``"run"`` (thread setup and ready to run), + ``"cleanup"`` (thread stopping is invoked, starting to clean up) and ``"stop"`` (thread finished). Can be extended for arbitrary points. Local call method. """ @@ -921,7 +927,7 @@ def fail_exec_point(self, point): Mark the given execution point as failed. Automatically invoked for ``"run"`` (thread setup and ready to run) if the startup raised an error before the thread properly started - (``"start"`` and ``"stop"`` are notified in any case) + (``"start"``, ``"cleanup"``, and ``"stop"`` are notified in any case) Can be extended for arbitrary points. Local call method. """ @@ -938,7 +944,8 @@ def sync_exec_point(self, point, timeout=None, counter=1): """ Wait for the given execution point. - Automatically invoked points include ``"start"`` (thread starting), ``"run"`` (thread setup and ready to run), and ``"stop"`` (thread finished). + Automatically invoked points include ``"start"`` (thread starting), ``"run"`` (thread setup and ready to run), + ``"cleanup"`` (thread stopping is invoked, starting to clean up) and ``"stop"`` (thread finished). If timeout is passed, raise :exc:`.threadprop.TimeoutThreadError`. `counter` specifies the minimal number of pre-requisite :meth:`notify_exec_point` calls to finish the waiting (by default, a single call is enough). Return actual number of notifier calls up to date. @@ -1095,12 +1102,14 @@ def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): self._jobs_list=[] self.batch_jobs={} self._batch_jobs_args={} + self._batch_jobs_namegen=general.NamedUIDGenerator() self.args=args or [] self.kwargs=kwargs or {} self._commands={} self._in_command_loop=False self._priority_queues={} self._priority_queues_order=[] + self._priority_queues_lock=threading.Lock() self._command_warned=set() self.ca=self.CommandAccess(self,sync=False) self.cad=self.CommandAccess(self,sync="delayed") @@ -1136,7 +1145,7 @@ def __init__(self, job, period, queue, jobs_order): def schedule(self): """Schedule the job""" if self.scheduled: - raise RuntimeError("command is already scheduled") + raise RuntimeError("job is already scheduled") self.call=self.queue.build_call(self.job,sync_result=False) self.call.add_callback(self.mark_unscheduled,pass_result=False,call_on_unschedule=True) self.scheduled=True @@ -1150,15 +1159,12 @@ def mark_unscheduled(self): Called automatically on job completion. """ - if not self.scheduled: - raise RuntimeError("unscheduled command can't be marked as popped") self.call=None self.scheduled=False def unschedule(self): """Manually unschedule the job (e.g., when paused or removed)""" if not self.scheduled: - raise RuntimeError("unscheduled command can't be unscheduled") - self.queue.unschedule(self.call) + raise RuntimeError("unscheduled job can't be unscheduled") self.call=None self.scheduled=False def clear(self): @@ -1269,6 +1275,14 @@ def change_batch_job_parameters(self, name, job="keep", cleanup="keep", min_runt self.batch_jobs[name]=self.TBatchJob(job,cleanup,min_runtime,priority) if restart and running: self.start_batch_job(name,period,*args,**kwargs) + def remove_batch_job(self, name): + """ + Remove the batch job `name`, stopping it if necessary. + + Local call method. + """ + self.stop_batch_job(name) + del self.batch_jobs[name] def start_batch_job(self, name, period, *args, **kwargs): """ Start the batch job with the given name. @@ -1316,19 +1330,55 @@ def stop_batch_job(self, name, error_on_stopped=False): raise ValueError("job {} doesn't exists".format(name)) if name not in self.jobs: if error_on_stopped: - raise ValueError("job {} doesn't exists".format(name)) + raise ValueError("job {} is not running".format(name)) return self.remove_job(name) _,args,kwargs,cleanup=self._batch_jobs_args.pop(name) if cleanup: cleanup(*args,**kwargs) + def restart_batch_job(self, name, error_on_stopped=False): + """ + Restart the running batch job with its current arguments. + + If ``error_on_stopped==True`` and the job is not currently running, raise an error. Otherwise, do nothing. + Local call method. + """ + period,args,kwargs,_=self._batch_jobs_args[name] + self.stop_batch_job(name,error_on_stopped=error_on_stopped) + self.start_batch_job(name,period,*args,**kwargs) + def run_as_batch_job(self, job, period, cleanup=None, name=None, priority=-10, args=None, kwargs=None): + """ + Create a temporarily batch job and immediately run it. + + If `name` is ``None``, generate a new unique name. + The job is removed after it is complete (i.e., after cleanup). + Note that this implies, that it can not be restarted using :meth:`restart_batch_job`, as it will be removed after the stopping before the restart. + All the parameters are the same as for :meth:`add_batch_job` and :meth:`start_batch_job`. + Return the batch job name (either supplied or newly generated). + """ + if name is None: + while True: + name=self._batch_jobs_namegen("temp_job") + if name not in self.batch_jobs: + break + if cleanup is None: + def full_cleanup(*args, **kwargs): # pylint: disable=unused-argument + self.remove_batch_job(name) + else: + def full_cleanup(*args, **kwargs): + cleanup(*args,**kwargs) + self.remove_batch_job(name) + self.add_batch_job(name,job,cleanup=full_cleanup,priority=priority) + self.start_batch_job(name,period,*(args or []),**(kwargs or {})) + return name def _get_priority_queue(self, priority): """Get the queue with the given priority, creating one if it does not exist""" if priority not in self._priority_queues: - q=callsync.QQueueScheduler() - self._priority_queues[priority]=q - self._priority_queues_order=[self._priority_queues[p] for p in sorted(self._priority_queues,reverse=True)] + with self._priority_queues_lock: + q=callsync.QQueueScheduler() + self._priority_queues[priority]=q + self._priority_queues_order=[self._priority_queues[p] for p in sorted(self._priority_queues,reverse=True)] return self._priority_queues[priority] def _check_priority_queues(self): """ @@ -1391,6 +1441,19 @@ def run(self): def on_start(self): super().on_start() + self.add_command("add_job",priority=10) + self.add_command("change_job_period",priority=10) + self.add_command("remove_job",priority=10) + self.add_command("add_batch_job",priority=10) + self.add_command("change_batch_job_parameters",priority=10) + self.add_command("remove_batch_job",priority=10) + self.add_command("start_batch_job",priority=10) + self.add_command("is_batch_job_running",priority=10) + self.add_command("stop_batch_job",priority=10) + self.add_command("restart_batch_job",priority=10) + self.add_command("run_as_batch_job",priority=10) + self.add_command("add_command",priority=10) + self.add_command("subscribe_commsync",priority=10) self.setup_task(*self.args,**self.kwargs) def on_finish(self): super().on_finish() @@ -1406,7 +1469,7 @@ def on_finish(self): def _call_command_method(self, name, original_method, args, kwargs): """Call given method taking into account ``_direct_comm_call_action``""" - if threadprop.current_controller() is not self: + if not self.is_in_controlled(): action=self._direct_comm_call_action if action=="warning": if name not in self._command_warned: @@ -1472,7 +1535,9 @@ def add_command(self, name, command=None, scheduler=None, limit_queue=None, on_f name: command name command: command function; if ``None``, look for the method with the given `name`. scheduler: a command scheduler; by default, it is a :class:`.QQueueLengthLimitScheduler`, - which maintains a call queue with the given length limit and full queue behavior + which maintains a call queue with the given length limit and full queue behavior; + can also be a name of a different command, with which it will share a single queue with the same limitations; + if supplied, `limit_queue` and `on_full_queue` parameters are ignored limit_queue: command call queue limit; ``None`` means no limit on_full_queue: action to be taken if the call can't be scheduled (the queue is full); can be ``"skip_current"`` (skip the call which is being scheduled), @@ -1491,6 +1556,9 @@ def add_command(self, name, command=None, scheduler=None, limit_queue=None, on_f psch=self._get_priority_queue(priority) if scheduler is None and limit_queue is not None: scheduler=callsync.QQueueLengthLimitScheduler(max_len=limit_queue or 0,on_full_queue=on_full_queue) + elif isinstance(scheduler,py3.textstring): + multischeduler=self._commands[scheduler].scheduler + scheduler=multischeduler.schedulers[0] multischeduler=callsync.QMultiQueueScheduler([psch] if scheduler is None else [scheduler,psch],[self._command_poke]) self._commands[name]=self.TCommand(command,multischeduler,priority) self._override_command_method(name) @@ -1586,7 +1654,7 @@ def call_command(self, name, args=None, kwargs=None, sync=False, callback=None, If ``sync==True``, pause caller thread execution (for at most `timeout` seconds) until the command has been executed by the target thread, and then return the command result. If ``sync=="delayed"``, return :class:`.QCallResultSynchronizer` object which can be used to wait for and read the command result; otherwise, return ``None``. - In the latter case, if ``ignore_errors==True``, ignore all possible problems with the call (controller stopped, call raised an exception, call was skipped) + In the ``sync==True`` case, if ``ignore_errors==True``, ignore all possible problems with the call (controller stopped, call raised an exception, call was skipped) and return ``None`` instead; otherwise, these problems raise exceptions in the caller thread. Universal call method. """ @@ -1605,6 +1673,37 @@ def call_command(self, name, args=None, kwargs=None, sync=False, callback=None, return synchronizer elif sync: return synchronizer.get_value_sync(timeout=timeout,error_on_fail=not ignore_errors,error_on_skip=not ignore_errors,pass_exception=not ignore_errors) + def call_in_thread_commsync(self, func, args=None, kwargs=None, sync=True, timeout=None, priority=0, ignore_errors=False, same_thread_shortcut=True): + """ + Call a function in this thread such that it is synchronous with other commands, and jobs. + + Mostly equivalent to calling a command, only the command function is supplied instead of its name, and the advanced scheduling + (maximal schedule size, sharing with different commands, etc.) is not used. + `args` and `kwargs` specify the function arguments. + If ``sync==True``, pause caller thread execution (for at most `timeout` seconds) until the command has been executed by the target thread, and then return the command result. + If ``sync=="delayed"``, return :class:`.QCallResultSynchronizer` object which can be used to wait for and read the command result; + otherwise, return ``None``. + `priority` sets the call priority (by default, the same as the standard commands). + In the ``sync==True`` case, if ``ignore_errors==True``, ignore all possible problems with the call (controller stopped, call raised an exception, call was skipped) + and return ``None`` instead; otherwise, these problems raise exceptions in the caller thread. + If ``same_thread_shortcut==True`` (default) and the caller thread is the same as the controlled thread, call the function directly. + Universal call method. + """ + if same_thread_shortcut and self.is_in_controlled(): + value=func(*(args or []),**(kwargs or {})) + if sync=="delayed": + return callsync.QDirectResultSynchronizer(value) + if sync: + return value + return None + sched=self._get_priority_queue(priority) + call=sched.build_call(func,args,kwargs,pass_result=True,callback_on_exception=False,sync_result=bool(sync)) + sched.schedule(call) + synchronizer=call.result_synchronizer + if sync=="delayed": + return synchronizer + elif sync: + return synchronizer.get_value_sync(timeout=timeout,error_on_fail=not ignore_errors,error_on_skip=not ignore_errors,pass_exception=not ignore_errors) class CommandAccess: """ From 6b8f573f9b0100c10235588f78928982832bafaa Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Wed, 16 Jun 2021 22:50:11 +0200 Subject: [PATCH 12/31] Minor bugfixes --- pylablib/core/gui/value_handling.py | 2 +- pylablib/core/utils/dictionary.py | 6 +++--- pylablib/devices/HighFinesse/wlm.py | 11 ++++++----- pylablib/devices/M2/__init__.py | 2 +- pylablib/devices/M2/solstis.py | 5 +++-- pylablib/gui/widgets/plotters/trace_plotter.py | 2 +- pylablib/thread/device_thread.py | 10 ++++++---- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/pylablib/core/gui/value_handling.py b/pylablib/core/gui/value_handling.py index 1c578a1..c17a9d3 100644 --- a/pylablib/core/gui/value_handling.py +++ b/pylablib/core/gui/value_handling.py @@ -990,7 +990,7 @@ def get_gui_values(gui_values=None, gui_values_path=""): if hasattr(gui_values,"gui_values_path"): root_path.append(gui_values.gui_values_path) gui_values=gui_values.gui_values - root_path.append(gui_values_path) + root_path+=dictionary.normalize_path(gui_values_path) return gui_values,"/".join(root_path) diff --git a/pylablib/core/utils/dictionary.py b/pylablib/core/utils/dictionary.py index 33612e9..93b1d48 100644 --- a/pylablib/core/utils/dictionary.py +++ b/pylablib/core/utils/dictionary.py @@ -29,7 +29,7 @@ def split_path(path, omit_empty=True, sep=None): if sep is None: path=[e for t in path for e in str(t).split("/")] else: - path=[e for t in path for e in re.split(sep,t)] + path=[e for t in path for e in re.split(sep,str(t))] if omit_empty: path=[p for p in path if p!=""] return path @@ -1400,11 +1400,11 @@ class ItemAccessor: contains_checker: method for checking if variable is present (``None`` means none is supplied, so checking containment raises an error; ``"auto"`` means that getter raising :exc:`KeyError` is used for checking) normalize_names: if ``True``, normalize a supplied path using the standard :class:`Dictionary` rules and join it into a single string using the supplied separator - path_separator: path separator used for splitting and joining the supplied paths + path_separator: path separator regex used for splitting and joining the supplied paths (by default, the standard ``"/"`` separator) missing_error: if not ``None``, specifies the error raised on the missing value; used in ``__contains__``, :meth:`get` and :meth:`setdefault` to determine if the value is missing """ - def __init__(self, getter=None, setter=None, deleter=None, contains_checker="auto", normalize_names=True, path_separator="/", missing_error=None): + def __init__(self, getter=None, setter=None, deleter=None, contains_checker="auto", normalize_names=True, path_separator=None, missing_error=None): self.getter=getter self.setter=setter self.deleter=deleter diff --git a/pylablib/devices/HighFinesse/wlm.py b/pylablib/devices/HighFinesse/wlm.py index 1a83540..ff1a947 100644 --- a/pylablib/devices/HighFinesse/wlm.py +++ b/pylablib/devices/HighFinesse/wlm.py @@ -19,7 +19,7 @@ def ch_func(self, *_, **__): TDeviceInfo=collections.namedtuple("TDeviceInfo",["model","serial_number","revision_number","compilation_number"]) class WLM(interface.IDevice): """ - Generic Arcus Performax translation stage. + Generic HighFinesse wavemeter. Args: version(int): wavemeter version; if ``None``, use any available version @@ -110,7 +110,8 @@ def set_default_channel(self, channel): """Set the default channel (starting from 1) which is used for querying""" self.dchannel=self._get_channel(channel) - _get_error_codes={ EGetError.ErrNoSignal:"nosig", + _get_error_codes={ EGetError.ErrNoValue:"noval", + EGetError.ErrNoSignal:"nosig", EGetError.ErrBadSignal:"badsig", EGetError.ErrLowSignal:"under", EGetError.ErrBigSignal:"over"} @@ -122,7 +123,7 @@ def get_frequency(self, channel=None, error_on_invalid=True): `channel` is the measurement channel (starting from 1); if ``None``, use the default channel. If ``error_on_invalid==True``, raise an error if the measurement is invalid (e.g., over- or underexposure); otherwise, the method can return ``"under"`` if the meter is underexposed or ``"over"`` is it is overexposed, - ``"badsig"`` if there is no calculable signal, or ``"nosig"`` if there is no signal. + ``"badsig"`` if there is no calculable signal, ``"noval"`` if there are no values acquire yet, or ``"nosig"`` if there is no signal. """ try: return self.lib.GetFrequencyNum(self._get_channel(channel),0.)*1E12 @@ -179,7 +180,7 @@ def get_exposure(self, sensor=1, channel=None): e2=self.get_exposure(2,channel=channel) return [e1,e2] except WlmDataLibError: - return e1 + return [e1] else: if self.auto_channel_tab: self._set_channel_tab(self._get_channel(channel)) @@ -199,7 +200,7 @@ def set_exposure(self, exposure, sensor=1, channel=None): e2=self.set_exposure(exposure,2,channel=channel) return [e1,e2] except WlmDataLibError: - return e1 + return [e1] else: self.lib.SetExposureNum(self._get_channel(channel),sensor,int(exposure*1E3)) return self.get_exposure(sensor=sensor,channel=channel) diff --git a/pylablib/devices/M2/__init__.py b/pylablib/devices/M2/__init__.py index edb8bf9..4f4f652 100644 --- a/pylablib/devices/M2/__init__.py +++ b/pylablib/devices/M2/__init__.py @@ -1 +1 @@ -from .solstis import M2Error, Solstis \ No newline at end of file +from .solstis import M2Error, Solstis, c \ No newline at end of file diff --git a/pylablib/devices/M2/solstis.py b/pylablib/devices/M2/solstis.py index a34b27a..aa3f7d7 100644 --- a/pylablib/devices/M2/solstis.py +++ b/pylablib/devices/M2/solstis.py @@ -181,8 +181,9 @@ def update_reports(self, timeout=0.): with self.socket.using_timeout(timeout): preport=self._recv_reply() raise M2Error("received reply while waiting for a report: '{}'".format(preport[0])) - except net.SocketTimeout: - pass + except M2CommunicationError as err: + if not isinstance(err.backend_exc,net.SocketTimeout): + raise def get_last_report(self, op): """Get the latest report for the given operation""" rep=self._last_status.get(op,None) diff --git a/pylablib/gui/widgets/plotters/trace_plotter.py b/pylablib/gui/widgets/plotters/trace_plotter.py index f966fb0..0fa4271 100644 --- a/pylablib/gui/widgets/plotters/trace_plotter.py +++ b/pylablib/gui/widgets/plotters/trace_plotter.py @@ -225,7 +225,7 @@ def get_data_from_accum_thread(self, table_accum_thread): """ channels=self.get_required_channels() maxlen=self.ctl.plot_params_table.v["disp_last"] if self.ctl else None - return table_accum_thread.get_data_sync(channels,maxlen=maxlen,fmt="dict") + return table_accum_thread.csi.get_data(channels,maxlen=maxlen,fmt="dict") def setup_data_source(self, src=None): diff --git a/pylablib/thread/device_thread.py b/pylablib/thread/device_thread.py index 1cf1e32..8137ee6 100644 --- a/pylablib/thread/device_thread.py +++ b/pylablib/thread/device_thread.py @@ -47,7 +47,7 @@ class DeviceThread(controller.QTaskThread): - ``get_full_info``: get full info of the device """ def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): - controller.QTaskThread.__init__(self,name=name,multicast_pool=multicast_pool,args=args,kwargs=kwargs) + super().__init__(name=name,multicast_pool=multicast_pool,args=args,kwargs=kwargs) self.device=None self.add_command("open",self.open) self.add_command("close",self.close) @@ -96,7 +96,7 @@ def close_device(self): """ self.device.close() - def rpyc_devclass(self, cls, host=None, port=18812): + def rpyc_devclass(self, cls, host=None, port=18812, timeout=3., attempts=2): """ Get a local or remote device class on a different PC via RPyC. @@ -109,16 +109,18 @@ def rpyc_devclass(self, cls, host=None, port=18812): host: address of the remote host (it should be running RPyC server; see :func:`.rpyc_utils.run_device_service` for details); if ``None`` (default), use local device class, which is exactly the same as simply creating device class without using this function port: port of the remote host + timeout: remote connection timeout per attempt + attempts: total number of connection attempts """ if host is None: - module,cls=cls.rsplit(".",maxplit=1) + module,cls=cls.rsplit(".",maxsplit=1) try: module=importlib.import_module(module) except ImportError: module=importlib.import_module(module_utils.get_library_name()+".devices."+module) return getattr(module,cls) else: - self.rpyc_serv=rpyc_utils.connect_device_service(host,port=port,error_on_fail=False) + self.rpyc_serv=rpyc_utils.connect_device_service(host,port=port,timeout=timeout,attempts=attempts,error_on_fail=False) if not self.rpyc_serv: return None return self.rpyc_serv.get_device_class(cls) From 432a0edcd4d1be0ff2b021cd8ccc343bfb6503ca Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Wed, 16 Jun 2021 23:00:11 +0200 Subject: [PATCH 13/31] Expanded GUI tools --- pylablib/core/gui/utils.py | 12 +++- pylablib/core/gui/widgets/container.py | 47 +++++++------ pylablib/core/gui/widgets/layout_manager.py | 76 ++++++++++++++++++--- pylablib/core/gui/widgets/param_table.py | 69 +++++++++++-------- pylablib/gui/widgets/range_controls.py | 14 ++-- 5 files changed, 152 insertions(+), 66 deletions(-) diff --git a/pylablib/core/gui/utils.py b/pylablib/core/gui/utils.py index 773d312..53aa12a 100644 --- a/pylablib/core/gui/utils.py +++ b/pylablib/core/gui/utils.py @@ -62,12 +62,13 @@ def get_first_empty_row(layout, start_row=0): if is_layout_row_empty(layout,r): return r return layout.rowCount() -def insert_layout_row(layout, row, compress=False): +def insert_layout_row(layout, row, stretch=0, compress=False): """ Insert row in a grid layout at a given index. Any multi-column item spanning over the row (i.e., starting at least one row before `row` and ending at least one row after `row`) gets stretched. Anything else either stays in place (if it's above `row`), or gets moved one row down. + `stretch` determines the stretch factor of the new row. If ``compress==True``, try to find an empty row below the inserted position and shit it to the new row's place; otherwise, add a completely new row. """ @@ -83,6 +84,9 @@ def insert_layout_row(layout, row, compress=False): for i,p in items_to_shift: row_shift=1 if p[0]>=row else 0 layout.addItem(i,p[0]+row_shift,p[1],p[2]+(1-row_shift),p[3]) + for r in range(free_row,row,-1): + layout.setRowStretch(r,layout.rowStretch(r-1)) + layout.setRowStretch(row,stretch) def is_layout_column_empty(layout, col): @@ -111,12 +115,13 @@ def get_first_empty_column(layout, start_col=0): if is_layout_column_empty(layout,c): return c return layout.colCount() -def insert_layout_column(layout, col, compress=False): +def insert_layout_column(layout, col, stretch=0, compress=False): """ Insert column in a grid layout at a given index. Any multi-row item spanning over the column (i.e., starting at least one column before `col` and ending at least one column after `col`) gets stretched. Anything else either stays in place (if it's above `col`), or gets moved one column to the right. + `stretch` determines the stretch factor of the new column. If ``compress==True``, try to find an empty column below the inserted position and shit it to the new column's place; otherwise, add a completely new column. """ @@ -132,6 +137,9 @@ def insert_layout_column(layout, col, compress=False): for i,p in items_to_shift: col_shift=1 if p[0]>=col else 0 layout.addItem(i,p[0],p[1]+col_shift,p[2],p[3]+(1-col_shift)) + for c in range(free_col,col,-1): + layout.setColumnsStretch(c,layout.columnStretch(c-1)) + layout.setColumnsStretch(col,stretch) def compress_grid_layout(layout): diff --git a/pylablib/core/gui/widgets/container.py b/pylablib/core/gui/widgets/container.py index 8b9dd7e..683afce 100644 --- a/pylablib/core/gui/widgets/container.py +++ b/pylablib/core/gui/widgets/container.py @@ -32,8 +32,8 @@ def __init__(self, *args, name=None, **kwargs): self._children=dictionary.Dictionary() self.setup_gui_values("new") self.ctl=None - self.w=dictionary.ItemAccessor(self.get_widget) self.c=dictionary.ItemAccessor(self.get_child) + self.w=dictionary.ItemAccessor(self.get_widget) self.v=dictionary.ItemAccessor(self.get_value,self.set_value) self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) @@ -85,7 +85,7 @@ def add_timer(self, name, period, autostart=True): if name in self._timers: raise ValueError("timer {} already exists".format(name)) timer=QtCore.QTimer(self) - timer.timeout.connect(controller.exsafe(lambda : self._on_timer(name))) + timer.timeout.connect(controller.exsafe(lambda: self._on_timer(name))) self._timers[name]=TTimer(name,period,timer) if self._running and autostart: self.start_timer(name) @@ -125,6 +125,8 @@ def add_timer_event(self, name, loop=None, start=None, stop=None, period=None, t """ if timer is None and period is None: raise ValueError("either a period or a timer name should be provided") + if name in self._timer_events: + raise ValueError("timer event {} already exists".format(name)) if timer is None: timer=self.add_timer(None,period,autostart=autostart) if start is not None and self.is_timer_running(timer): @@ -158,7 +160,7 @@ def add_child_values(self, path, widget): else: raise ValueError("can not store a non-container widget under an empty path") else: - self.gui_values.add_widget(path,widget) + self.gui_values.add_widget((self.gui_values_path,path),widget) def _setup_child_name(self, widget, name): if name is None: name=getattr(widget,"name",None) @@ -217,7 +219,7 @@ def start(self): Starts all the internal timers, and calls ``start`` method for all the contained widgets. """ if self._running: - raise RuntimeError("container '{}' loop is already running".format(self.name)) + return for ch in self._children.iternodes(): if hasattr(ch.widget,"start"): ch.widget.start() @@ -232,7 +234,7 @@ def stop(self): Stops all the internal timers, and calls ``stop`` method for all the contained widgets. """ if not self._running: - raise RuntimeError("container '{}' loop is not running".format(self.name)) + return self._running=False for n in self._timers: self.stop_timer(n) @@ -392,30 +394,35 @@ class QTabContainer(QtWidgets.QTabWidget, QContainer): def __init__(self, *args, **kwargs): super().__init__(*args,**kwargs) self._tabs={} - def add_tab(self, name, caption, index=None, layout="vbox", gui_values_path=True, no_margins=True): + def _insert_tab(self, tab, caption, index): + if index is None: + index=self.count() + elif index<0: + index=index%self.count() + else: + index=min(index,self.count()) + self.insertTab(index,tab,caption) + def add_tab(self, name, caption, index=None, widget=None, layout="vbox", gui_values_path=True, no_margins=True): """ Add a new tab container with the given `caption` to the widget. `index` specifies the new tab's index (``None`` means adding to the end, negative values count from the end). - `layout` specifies the layout (``"vbox"``, ``"hbox"``, or ``"grid"``) of the new frame, - and `location` specifies its location within the container layout. - If ``no_margins==True``, the frame will have no inner layout margins. + If `widget` is ``None``, create a new frame widget using the given `layout` (``"vbox"``, ``"hbox"``, or ``"grid"``) + and `no_margins` (specifies whether the frame has inner margins) arguments; + otherwise, use the supplied widget. The other parameters are the same as in :meth:`add_child` method. """ if name in self._tabs: raise ValueError("tab {} already exists".format(name)) - frame=QFrameContainer(self) - self.add_child(name=name,widget=frame,gui_values_path=gui_values_path) - frame.setup(layout=layout,no_margins=no_margins) - if index is None: - index=self.count() - elif index<0: - index=index%self.count() + if widget is None: + widget=QFrameContainer(self) + self.add_child(name=name,widget=widget,gui_values_path=gui_values_path) + widget.setup(layout=layout,no_margins=no_margins) else: - index=min(index,self.count()) - self.insertTab(index,frame,caption) - self._tabs[name]=frame - return frame + self.add_child(name=name,widget=widget,gui_values_path=gui_values_path) + self._insert_tab(widget,caption,index) + self._tabs[name]=widget + return widget def remove_tab(self, name): """ Remove a tab with the given name. diff --git a/pylablib/core/gui/widgets/layout_manager.py b/pylablib/core/gui/widgets/layout_manager.py index 5ff3dc3..2c7ab08 100644 --- a/pylablib/core/gui/widgets/layout_manager.py +++ b/pylablib/core/gui/widgets/layout_manager.py @@ -14,7 +14,13 @@ class QLayoutManagedWidget(QtWidgets.QWidget): afterwards, widgets and sublayout can be added using :meth:`add_to_layout`. In addition, it can directly add named sublayouts using :meth:`add_sublayout` method. """ - def __init__(self, parent=None): + def __init__(self, *args, **kwargs): + if args: + parent=args[0] + elif "parent" in kwargs: + parent=kwargs["parent"] + else: + parent=None super().__init__(parent) self.main_layout=None self._default_layout="main" @@ -163,24 +169,43 @@ def get_sublayout(self, name=None): """Get the previously added sublayout""" return self._sublayouts[name or self._default_layout][0] - def add_spacer(self, height=0, width=0, stretch_height=False, stretch_width=False, location="next"): + def add_spacer(self, height=0, width=0, stretch_height=False, stretch_width=False, stretch=0, location="next"): """ Add a spacer with the given width and height to the given location. If ``stretch_height==True`` or ``stretch_width==True``, the widget will stretch in these directions; otherwise, the widget size is fixed. + If `stretch` is not ``None``, it specifies stretch of the spacer the corresponding direction (applied to the upper row and leftmost column for multi-cell spacer); + if `kind=="both"``, it can also be a tuple with two stretches along vertical and horizontal directions. """ spacer=QtWidgets.QSpacerItem(width,height, QtWidgets.QSizePolicy.MinimumExpanding if stretch_width else QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding if stretch_height else QtWidgets.QSizePolicy.Minimum) + lname,lpos=self._normalize_location(location) self.add_to_layout(spacer,location,kind="item") self._spacers.append(spacer) # otherwise the reference is lost, and the object might be deleted + if lname is not None: + r,c=lpos[:2] + layout,lkind=self._sublayouts[lname] + if not isinstance(stretch,tuple): + stretch=(stretch,stretch) + if lkind=="grid": + if stretch_height: + layout.setRowStretch(r,stretch[0]) + if stretch_width: + layout.setColumnStretch(c,stretch[1]) + elif lkind=="vbox" and stretch_height: + layout.setStretch(r,stretch[0]) + elif lkind=="hbox" and stretch_width: + layout.setStretch(c,stretch[1]) return spacer - def add_padding(self, kind="auto", location="next"): + def add_padding(self, kind="auto", location="next", stretch=0): """ Add a padding (expandable spacer) of the given kind to the given location. `kind` can be ``"vertical"``, ``"horizontal"``, ``"auto"`` (vertical for ``grid`` and ``vbox`` layouts, horizontal for ``hbox``), or ``"both"`` (stretches in both directions). + If `stretch` is not ``None``, it specifies stretch of the spacer the corresponding direction (applied to the upper row and leftmost column for multi-cell spacer); + can also be a tuple with two stretches along vertical and horizontal directions. """ funcargparse.check_parameter_range(kind,"kind",{"auto","horizontal","vertical","both"}) if kind=="auto": @@ -192,7 +217,42 @@ def add_padding(self, kind="auto", location="next"): kind="horizontal" if lkind=="hbox" else "vertical" stretch_height=kind in {"vertical","both"} stretch_width=kind in {"horizontal","both"} - return self.add_spacer(stretch_height=stretch_height,stretch_width=stretch_width,location=location) + return self.add_spacer(stretch_height=stretch_height,stretch_width=stretch_width,location=location,stretch=stretch) + def _normalize_stretch(self, args): + if len(args)==1: + return list(enumerate(args[0])) + if len(args)==2: + return [(args[0],args[1])] + raise TypeError("method takes one or two positional arguments, {} supplied".format(len(args))) + def set_row_stretch(self, *args, layout=None): + """ + Set row stretch for a given layout. + + Takes either two arguments ``index`` and ``stretch``, or a single list of stretches for all rows. + """ + layout,lkind=self._sublayouts[layout or self._default_layout] + for i,s in self._normalize_stretch(args): + if lkind=="grid": + layout.setRowStretch(i,s) + elif lkind=="vbox": + layout.setStretch(i,s) + else: + raise ValueError("only gird and vbox layout support column stretch") + def set_column_stretch(self, *args, layout=None): + """ + Set column stretch for a given layout. + + Takes either two arguments ``index`` and ``stretch``, or a single list of stretches for all columns. + """ + layout,lkind=self._sublayouts[layout or self._default_layout] + for i,s in self._normalize_stretch(args): + if lkind=="grid": + layout.setColumnStretch(i,s) + elif lkind=="hbox": + layout.setStretch(i,s) + else: + raise ValueError("only gird and hbox layout support column stretch") + def add_decoration_label(self, text, location="next"): """Add a decoration text label with the given text""" label=QtWidgets.QLabel(self) @@ -200,18 +260,18 @@ def add_decoration_label(self, text, location="next"): label.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) self.add_to_layout(label,location) return label - def insert_row(self, row, sublayout=None): + def insert_row(self, row, sublayout=None, stretch=0): """Insert a new row at the given location in the grid layout""" layout,kind=self._sublayouts[sublayout or self._default_layout] if kind!="grid": raise ValueError("can add rows only to grid layouts (vbox layouts work automatically)") - utils.insert_layout_row(layout,row%(layout.rowCount() or 1)) - def insert_column(self, col, sublayout=None): + utils.insert_layout_row(layout,row%(layout.rowCount() or 1),stretch=stretch) + def insert_column(self, col, sublayout=None, stretch=0): """Insert a new column at the given location in the grid layout""" layout,kind=self._sublayouts[sublayout or self._default_layout] if kind!="grid": raise ValueError("can add columns only to grid layouts (hbox layouts work automatically)") - utils.insert_layout_column(layout,col%(layout.colCount() or 1)) + utils.insert_layout_column(layout,col%(layout.colCount() or 1),stretch=stretch) def clear(self): """Clear the layout and remove all the added elements""" diff --git a/pylablib/core/gui/widgets/param_table.py b/pylablib/core/gui/widgets/param_table.py index 3e94591..a0214ea 100644 --- a/pylablib/core/gui/widgets/param_table.py +++ b/pylablib/core/gui/widgets/param_table.py @@ -1,5 +1,5 @@ from . import edit, label as widget_label, combo_box, button as widget_button -from . import layout_manager +from . import container from ...thread import threadprop, controller from .. import value_handling from ...utils import py3, dictionary @@ -10,7 +10,7 @@ import contextlib -class ParamTable(layout_manager.QLayoutManagedWidget): +class ParamTable(container.QWidgetContainer): """ GUI parameter table. @@ -43,10 +43,8 @@ class ParamTable(layout_manager.QLayoutManagedWidget): parent: parent widget """ def __init__(self, parent=None, name=None): - super().__init__(parent) - self.name=None - self.setup_name(name) self.params={} + super().__init__(parent,name=name) self.h=dictionary.ItemAccessor(self.get_handler) self.w=dictionary.ItemAccessor(self.get_widget) self.wv=dictionary.ItemAccessor(self.get_value,self.set_value) @@ -54,7 +52,6 @@ def __init__(self, parent=None, name=None): self.cv=dictionary.ItemAccessor(lambda name: self.current_values[name],self.set_value) self.i=dictionary.ItemAccessor(self.get_indicator,self.set_indicator) self.vs=dictionary.ItemAccessor(self.get_value_changed_signal) - self.setup_gui_values("new") def _make_new_layout(self, kind, *args, **kwargs): layout=super()._make_new_layout(kind,*args,**kwargs) if kind=="grid": @@ -64,18 +61,10 @@ def _set_main_layout(self): super()._set_main_layout() self.main_layout.setContentsMargins(5,5,5,5) self.main_layout.setColumnStretch(1,1) - def setup_gui_values(self, gui_values=None, gui_values_path=""): + def setup_gui_values(self, gui_values="new", gui_values_path=""): if self.params: - raise RuntimeError("can not change gui values after widgets have been added") - if gui_values is not None: - if gui_values_path is None: - gui_values_path=self.name - self.gui_values,self.gui_values_path=value_handling.get_gui_values(gui_values,gui_values_path) - def setup_name(self, name): - """Set the table's name""" - if name is not None: - self.name=name - self.setObjectName(name) + raise RuntimeError("can not change gui values after parameter widgets have been added") + super().setup_gui_values(gui_values=gui_values,gui_values_path=gui_values_path) def setup(self, name=None, add_indicator=True, gui_values=None, gui_values_path="", gui_thread_safe=False, cache_values=False, change_focused_control=False): """ Setup the table. @@ -97,11 +86,8 @@ def setup(self, name=None, add_indicator=True, gui_values=None, gui_values_path= change_focused_control (bool): if ``False`` and :meth:`set_value` method is called while the widget has user focus, ignore the value; note that :meth:`set_all_values` will still set the widget value. """ - super().setup() - if self.name is None: - self.setup_name(name) + super().setup(name=name,layout="grid",gui_values=gui_values,gui_values_path=gui_values_path) self.add_indicator=add_indicator - self.setup_gui_values(gui_values=gui_values,gui_values_path=gui_values_path) self.gui_thread_safe=gui_thread_safe self.change_focused_control=change_focused_control self.cache_values=cache_values @@ -117,14 +103,33 @@ def _update_cache_values(self, name=None, value=None): # pylint: disable=unused else: self.current_values[name]=self.get_value(name) - def add_sublayout(self, name, kind="grid", location=("next",0,1,2)): + def add_sublayout(self, name, kind="grid", location=("next",0,1,"end")): return super().add_sublayout(name,kind=kind,location=location) @contextlib.contextmanager - def using_new_sublayout(self, name, kind="grid", location=("next",0,1,2)): + def using_new_sublayout(self, name, kind="grid", location=("next",0,1,"end")): with super().using_new_sublayout(name,kind=kind,location=location): yield + def pad_borders(self, kind="both", stretch=0): + """ + Add expandable paddings on the bottom and/or right border. + + `kind` can be ``"bottom"``, ``"right"``, ``"both"``, or ``"none"`` (do nothing). + Note that if more elements are added, they will be placed after the padding, so the table will be padded in the middle. + """ + if kind in ["bottom","both"]: + self.add_padding("vertical",location=("next",0),stretch=stretch) + if kind in ["right","both"]: + self.add_padding("horizontal",location=(0,"next"),stretch=stretch) + + def add_frame(self, name, layout="vbox", location=("next",0,1,"end"), gui_values_path=True, no_margins=True): + return super().add_frame(name,layout=layout,location=location,gui_values_path=gui_values_path,no_margins=no_margins) + def add_group_box(self, name, caption, layout="vbox", location=("next",0,1,"end"), gui_values_path=True, no_margins=True): + return super().add_group_box(name,caption,layout=layout,location=location,gui_values_path=gui_values_path,no_margins=no_margins) + def _normalize_name(self, name): + if isinstance(name,tuple): + name=dictionary.normalize_path(name) if isinstance(name,(list,tuple)): return "/".join(name) return name @@ -132,6 +137,8 @@ def _normalize_name(self, name): def _add_widget(self, name, params, add_change_event=True): name=self._normalize_name(name) self.params[name]=params + if params.widget is not None: + self.add_child(name,params.widget,location="skip",gui_values_path=False) path=(self.gui_values_path,name) self.gui_values.add_handler(path,params.value_handler) if params.indicator_handler: @@ -246,7 +253,7 @@ def remove_widget(self, name): par=self.params.pop(name) self.gui_values.remove_handler((self.gui_values_path,name),remove_indicator=True) if par.widget is not None: - self.remove_layout_element(par.widget) + self.remove_child(name) if par.label is not None: self.remove_layout_element(par.label) if par.indicator is not None: @@ -335,7 +342,7 @@ def add_check_box(self, name, caption, value=False, label=None, add_indicator=No widget.setObjectName(self.name+"_"+name) widget.setChecked(value) return self.add_simple_widget(name,widget,label=label,add_indicator=add_indicator,location=location,tooltip=tooltip,add_change_event=add_change_event) - def add_text_label(self, name, value=None, label=None, location=None, tooltip=None, add_change_event=False, virtual=False): + def add_text_label(self, name, value="", label=None, location=None, tooltip=None, add_change_event=False, virtual=False): """ Add a text label to the table. @@ -371,7 +378,7 @@ def add_num_label(self, name, value=0, limiter=None, formatter=None, label=None, widget=widget_label.NumLabel(self,value=value,limiter=limiter,formatter=formatter) widget.setObjectName(self.name+"_"+name) return self.add_simple_widget(name,widget,label=label,add_indicator=False,location=location,tooltip=tooltip,add_change_event=add_change_event) - def add_text_edit(self, name, value=None, label=None, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False): + def add_text_edit(self, name, value="", label=None, add_indicator=None, location=None, tooltip=None, add_change_event=True, virtual=False): """ Add a text edit to the table. @@ -524,8 +531,14 @@ def get_value_changed_signal(self, name): """Get a value-changed signal for a widget with the given name""" return self.params[self._normalize_name(name)].value_handler.get_value_changed_signal() - get_child=get_widget # form compatibility with QContainer - remove_child=remove_widget + def get_child(self, name): + if name in self.params: + return self.get_widget(name) + return super().get_child(name) + def remove_child(self, name): + if name in self.params: + return self.remove_widget(name) + return super().remove_child(name) @controller.gui_thread_method def get_indicator(self, name=None): diff --git a/pylablib/gui/widgets/range_controls.py b/pylablib/gui/widgets/range_controls.py index 7e95d43..dee51c1 100644 --- a/pylablib/gui/widgets/range_controls.py +++ b/pylablib/gui/widgets/range_controls.py @@ -47,7 +47,7 @@ def setup(self, lim=(None,None), order=True, formatter=".1f", labels=("Min","Max self.params=ParamTable(self) self.main_layout.addWidget(self.params) self.params.setup(name="params",add_indicator=False,change_focused_control=True) - self.params.main_layout.setContentsMargins(2,2,2,2) + self.params.main_layout.setContentsMargins(5,5,5,5) self.params.main_layout.setSpacing(4) if "minmax" in elements: self.params.add_num_edit("min",formatter=formatter,label=labels[0]) @@ -62,6 +62,7 @@ def setup(self, lim=(None,None), order=True, formatter=".1f", labels=("Min","Max if "step" in elements: self.params.add_num_edit("step",formatter=formatter,limiter=(0,None),label=labels[4]) self.params.vs["step"].connect(self._step_changed) + self.params.set_column_stretch([0,1,0,1]) self._show_values(self.rng) self.set_limit(lim) @@ -195,7 +196,7 @@ def setup(self, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X"," self.params=ParamTable(self) self.main_layout.addWidget(self.params) self.params.setup(name="params",add_indicator=False) - self.params.main_layout.setContentsMargins(0,0,0,0) + self.params.main_layout.setContentsMargins(5,5,5,5) self.params.main_layout.setSpacing(4) self.params.add_decoration_label("ROI",(0,0)) self.params.add_decoration_label("Min",(0,1)) @@ -207,8 +208,7 @@ def setup(self, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X"," self.params.add_num_edit("y_min",value=0,formatter="int",limiter=(None,None,"coerce","int"),location=(2,1,1,1)) self.params.add_num_edit("y_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,2,1,1)) self.params.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) - self.params.main_layout.setColumnStretch(1,1) - self.params.main_layout.setColumnStretch(2,1) + self.params.set_column_stretch([0,1,1]) self.validate=validate for n in ["x_min","x_max","y_min","y_max"]: self.params.w[n].setMinimumWidth(30) @@ -354,7 +354,7 @@ def setup(self, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, self.params=ParamTable(self) self.main_layout.addWidget(self.params) self.params.setup(name="params",add_indicator=False) - self.params.main_layout.setContentsMargins(0,0,0,0) + self.params.main_layout.setContentsMargins(5,5,5,5) self.params.main_layout.setSpacing(4) self.params.add_decoration_label("ROI",(0,0)) self.params.add_decoration_label("Min",(0,1)) @@ -369,9 +369,7 @@ def setup(self, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, self.params.add_num_edit("y_max",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,2,1,1)) self.params.add_num_edit("y_bin",value=1,formatter="int",limiter=(None,None,"coerce","int"),location=(2,3,1,1)) self.params.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,QtWidgets.QSizePolicy.Preferred)) - self.params.main_layout.setColumnStretch(1,2) - self.params.main_layout.setColumnStretch(2,2) - self.params.main_layout.setColumnStretch(3,1) + self.params.set_column_stretch([0,2,2,1]) self.validate=validate for n in ["x_min","x_max","x_bin","y_min","y_max","y_bin"]: self.params.w[n].setMinimumWidth(30) From 42f0eeadee5974b01d86f367fd2f0c0aa708b48f Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Wed, 16 Jun 2021 23:09:47 +0200 Subject: [PATCH 14/31] Minor threading additions --- pylablib/core/thread/controller.py | 81 ++++++++++++++++++++---------- pylablib/core/thread/threadprop.py | 18 +++---- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/pylablib/core/thread/controller.py b/pylablib/core/thread/controller.py index 567c4ce..7bd0acd 100644 --- a/pylablib/core/thread/controller.py +++ b/pylablib/core/thread/controller.py @@ -65,11 +65,12 @@ def wrapper(func): return wrapper def _toploop(func): @func_utils.getargsfrom(func,hide_outer_obj=True) # slots don't work well with bound methods - def tlfunc(self, *args, **kwargs): - if self._in_inner_loop(): - self._toploop_calls.append(lambda: func(self,*args,**kwargs)) + def tlfunc(*args, **kwargs): + ctl=threadprop.current_controller() + if ctl._in_inner_loop(): + ctl._toploop_calls.append(lambda: func(*args,**kwargs)) else: - func(self,*args,**kwargs) + func(*args,**kwargs) return tlfunc def toploopSlot(*slargs, **slkwargs): """Wrapper around Qt slot which intercepts exceptions and stops the execution in a controlled manner""" @@ -216,6 +217,8 @@ def __init__(self, name=None, kind="loop", multicast_pool=None): self._thread_methods={} self.v=dictionary.ItemAccessor(getter=lambda name:self.get_variable(name,missing_error=True), setter=self.set_variable,deleter=self.delete_variable,contains_checker=self._has_variable) + self.sv=dictionary.ItemAccessor(getter=lambda name:self.get_variable(name,missing_error=True,simple=True), + setter=lambda name,value:self.set_variable(name,value,simple=True)) # set up high-level synchronization self._exec_notes={} self._exec_notes_lock=threading.Lock() @@ -670,30 +673,38 @@ def send_multicast(self, dst="any", tag=None, value=None, src=None): ### Variable management ### _variable_change_tag="#sync.wait.variable" - def set_variable(self, name, value, update=False, notify=False, notify_tag="changed/*"): + def set_variable(self, name, value, update=False, notify=False, notify_tag="changed/*", simple=False): """ Set thread variable. Can be called in any thread (controlled or external). If ``notify==True``, send an multicast with the given `notify_tag` (where ``"*"`` symbol is replaced by the variable name). If ``update==True`` and the value is a dictionary, update the branch rather than overwrite it. + If ``simple==True``, assume that the result is a single atomic variable, in which case the lock is not used; + note that in this case the threads waiting on this variable (or branches containing it) will not be notified. Local call method. """ - split_name=tuple(dictionary.normalize_path(name)) - notify_list=[] - with self._params_val_lock: - if name in self._params_funcs: - del self._params_funcs[name] + if simple: if update: self._params_val.merge(name,value) else: self._params_val.add_entry(name,value,force=True) - for exp_name in self._params_exp: - if exp_name==split_name[:len(exp_name)] or split_name==exp_name[:len(split_name)]: - notify_list.append((self._params_val[exp_name],self._params_exp[exp_name])) - for val,lst in notify_list: - for ctl in lst: - ctl.send_interrupt(self._variable_change_tag,val) + else: + split_name=tuple(dictionary.normalize_path(name)) + notify_list=[] + with self._params_val_lock: + if name in self._params_funcs: + del self._params_funcs[name] + if update: + self._params_val.merge(name,value) + else: + self._params_val.add_entry(name,value,force=True) + for exp_name in self._params_exp: + if exp_name==split_name[:len(exp_name)] or split_name==exp_name[:len(split_name)]: + notify_list.append((self._params_val[exp_name],self._params_exp[exp_name])) + for val,lst in notify_list: + for ctl in lst: + ctl.send_interrupt(self._variable_change_tag,val) if notify: notify_tag.replace("*",name) self.send_multicast("any",notify_tag,value) @@ -790,14 +801,18 @@ def send_sync(self, tag, uid): ### Variables access ### - def get_variable(self, name, default=None, copy_branch=True, missing_error=False): + def get_variable(self, name, default=None, copy_branch=True, missing_error=False, simple=False): """ Get thread variable. If ``missing_error==False`` and no variable exists, return `default`; otherwise, raise and error. If ``copy_branch==True`` and the variable is a :class:`.Dictionary` branch, return its copy to ensure that it stays unaffected on possible further variable assignments. + If ``simple==True``, assume that the result is a single atomic variable, in which case the lock is not used; + this only works with actual variables and not function variables. Universal call method. """ + if simple: + return self._params_val[name] if missing_error else self._params_val.get(name,default) with self._params_val_lock: if name in self._params_val: var=self._params_val[name] @@ -1091,12 +1106,13 @@ class QTaskThread(QThreadController): ## Can be ``"warning"``, which prints warning about this call (default), ## or one of the accessor names (e.g., ``"c"`` or ``"q"``), which routes the call through this accessor _direct_comm_call_action="warning" - _new_jobs_check_period=.1 # maximal time to sleep in the scheduling loop if there are no pending calls + _loop_wait_period=1. # time to wait in the main loop between scheduling events, if no events come; exact value does not affect anything TBatchJob=collections.namedtuple("TBatchJob",["job","cleanup","min_run_time","priority"]) TCommand=collections.namedtuple("TCommand",["command","scheduler","priority"]) def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): super().__init__(name=name,kind="run",multicast_pool=multicast_pool) - self.sync_period=0 + self.sync_period=0 # minimal time to check on the Qt event loop while running the internal scheduling loop + self.min_schedule_time=0. # minimal time to sleep between scheduling checks; acts as a quantum of scheduling self._last_sync_time=0 self.jobs={} self._jobs_list=[] @@ -1107,6 +1123,7 @@ def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): self.kwargs=kwargs or {} self._commands={} self._in_command_loop=False + self._poked=False self._priority_queues={} self._priority_queues_order=[] self._priority_queues_lock=threading.Lock() @@ -1165,6 +1182,7 @@ def unschedule(self): """Manually unschedule the job (e.g., when paused or removed)""" if not self.scheduled: raise RuntimeError("unscheduled job can't be unscheduled") + self.queue.unschedule(self.call) self.call=None self.scheduled=False def clear(self): @@ -1372,11 +1390,12 @@ def full_cleanup(*args, **kwargs): self.start_batch_job(name,period,*(args or []),**(kwargs or {})) return name - def _get_priority_queue(self, priority): + def _get_priority_queue(self, priority, fast=False): """Get the queue with the given priority, creating one if it does not exist""" if priority not in self._priority_queues: with self._priority_queues_lock: - q=callsync.QQueueScheduler() + # q=callsync.QQueueScheduler() + q=callsync.QFastQueueScheduler() if fast else callsync.QQueueScheduler() self._priority_queues[priority]=q self._priority_queues_order=[self._priority_queues[p] for p in sorted(self._priority_queues,reverse=True)] return self._priority_queues[priority] @@ -1426,18 +1445,29 @@ def _exhaust_queued_calls(self): self._last_sync_time=t self.check_messages(top_loop=True) self._in_command_loop=False + self._poked=False self._check_priority_queues() + def _command_poke(self): + if not self._in_command_loop and not self._poked: + self.poke() + self._poked=True ### Start/run/stop control (called automatically) ### def run(self): + schedule_time=0 while True: ct=time.time() to=self._schedule_pending_jobs(ct) - sleep_time=self._new_jobs_check_period if to is None else min(self._new_jobs_check_period,to) - if sleep_time>=0: - self.sleep(sleep_time,wake_on_message=True) + sleep_time=self._loop_wait_period if to is None else min(self._loop_wait_period,to) + if schedule_time<=0 and sleep_time>=0: + if not self._poked: + self.sleep(sleep_time,wake_on_message=True) + self._poked=False self._exhaust_queued_calls() + schedule_time=ct+self.min_schedule_time-time.time() + if schedule_time>0: + self.sleep(schedule_time) def on_start(self): super().on_start() @@ -1630,9 +1660,6 @@ def subscribe_commsync(self, callback, srcs="any", tags=None, dsts=None, filt=No ## Methods to be called by functions executing in other thread ## ### Request calls ### - def _command_poke(self): - if not self._in_command_loop: - self.poke() def _schedule_comm(self, name, args, kwargs, callback=None, sync_result=True): comm,sched,_=self._commands[name] call=sched.build_call(comm,args,kwargs,callback=callback,pass_result=True,callback_on_exception=False,sync_result=sync_result) diff --git a/pylablib/core/thread/threadprop.py b/pylablib/core/thread/threadprop.py index dd69bb3..e57cf86 100644 --- a/pylablib/core/thread/threadprop.py +++ b/pylablib/core/thread/threadprop.py @@ -13,45 +13,45 @@ class ThreadError(RuntimeError): """Generic thread error""" def __init__(self, msg=None): msg=msg or "thread error" - RuntimeError.__init__(self, msg) + super().__init__(msg) class NoControllerThreadError(ThreadError): """Thread error for a case of thread having no controllers""" def __init__(self, msg=None): msg=msg or "thread has no controller" - ThreadError.__init__(self, msg) + super().__init__(msg) class DuplicateControllerThreadError(ThreadError): """Thread error for a case of a duplicate thread controller""" def __init__(self, msg=None): msg=msg or "trying to create a duplicate thread controller" - ThreadError.__init__(self, msg) -class TimeoutThreadError(ThreadError): + super().__init__(msg) +class TimeoutThreadError(ThreadError,TimeoutError): """Thread error for a case of a wait timeout""" def __init__(self, msg=None): msg=msg or "waiting has timed out" - ThreadError.__init__(self, msg) + super().__init__(msg) class NoMessageThreadError(ThreadError): """Thread error for a case of trying to get a non-existing message""" def __init__(self, msg=None): msg=msg or "no message available" - ThreadError.__init__(self, msg) + super().__init__(msg) class SkippedCallError(ThreadError): """Thread error for a case of external call getting skipped (unscheduled)""" def __init__(self, msg=None): msg=msg or "call has been skipped" - ThreadError.__init__(self, msg) + super().__init__(msg) ### Interrupts ### class InterruptException(Exception): """Generic interrupt exception (raised by some function to signal interrupts from other threads)""" def __init__(self, msg=None): msg=msg or "thread interrupt" - Exception.__init__(self, msg) + super().__init__(msg) class InterruptExceptionStop(InterruptException): """Interrupt exception denoting thread stop request""" def __init__(self, msg=None): msg=msg or "thread interrupt: stop" - InterruptException.__init__(self, msg) + super().__init__(msg) def get_app(): From 7e03a4ebd7556effbf41925d65ceee466bcb3271 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Thu, 17 Jun 2021 20:31:29 +0200 Subject: [PATCH 15/31] Net expansions, uc480 and gui bugfixes --- pylablib/core/thread/controller.py | 20 +++++++++++++++++++- pylablib/core/utils/net.py | 17 ++++++++++++++++- pylablib/core/utils/rpyc_utils.py | 2 ++ pylablib/devices/uc480/uc480.py | 4 ++-- pylablib/gui/widgets/range_controls.py | 4 ++-- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/pylablib/core/thread/controller.py b/pylablib/core/thread/controller.py index 7bd0acd..4268711 100644 --- a/pylablib/core/thread/controller.py +++ b/pylablib/core/thread/controller.py @@ -23,6 +23,7 @@ _exception_print_lock=threading.Lock() +_debug_mode=False @contextlib.contextmanager def exint(error_msg_template="{}:"): @@ -33,12 +34,21 @@ def exint(error_msg_template="{}:"): pass except: # pylint: disable=bare-except with _exception_print_lock: + ctl_name=None try: ctl_name=get_controller(sync=False).name print(error_msg_template.format("Exception raised in thread '{}'".format(ctl_name)),file=sys.stderr) except threadprop.NoControllerThreadError: print(error_msg_template.format("Exception raised in an uncontrolled thread"),file=sys.stderr) traceback.print_exc() + if _debug_mode: + running_threads=", ".join(["'{}'".format(k) for k in _running_threads]) + print("\nRunning threads: {}\n\n".format(running_threads),file=sys.stderr) + for thread_id,frame in sys._current_frames().items(): + ctl=_find_controller_by_id(thread_id) + if ctl and ctl.name!=ctl_name: + print("\n\n\nStack trace of thread '{}':".format(ctl.name),file=sys.stderr) + traceback.print_stack(frame,file=sys.stderr) sys.stderr.flush() try: stop_controller("gui",code=1,sync=False,require_controller=True) @@ -94,7 +104,9 @@ def __init__(self, controller): self.controller=controller self._stop_request.connect(self._do_quit) self._stop_requested=False + self.thread_id=None def run(self): + self.thread_id=threading.current_thread().ident with exint(): try: self.exec_() # main execution event loop @@ -189,6 +201,7 @@ def __init__(self, name=None, kind="loop", multicast_pool=None): if threadprop.current_controller(require_controller=False): raise threadprop.DuplicateControllerThreadError() self.thread=threadprop.get_gui_thread() + self.thread.thread_id=threading.current_thread().ident threadprop.local_data.controller=self _register_controller(self) else: @@ -1814,7 +1827,12 @@ def _unregister_controller(controller): _running_threads_notifier.notify() - +def _find_controller_by_id(thread_id): + """Get a running thread for a given thread ID, or ``None`` if no thread is found""" + with _running_threads_lock: + for t in _running_threads.values(): + if t.thread.thread_id==thread_id: + return t def get_controller(name=None, sync=True, timeout=None, sync_point=None): """ Find a controller with a given name. diff --git a/pylablib/core/utils/net.py b/pylablib/core/utils/net.py index 5508af9..b997842 100644 --- a/pylablib/core/utils/net.py +++ b/pylablib/core/utils/net.py @@ -3,7 +3,7 @@ """ -import socket, json, contextlib +import socket, json, contextlib, re from . import funcargparse, strpack, general, py3 @@ -40,6 +40,21 @@ def get_local_hostname(): """Get a local host name""" return socket.gethostbyname_ex(socket.gethostname())[0] +def as_addr_port(addr, port): + """ + Parse the given address and port combination. + + `addr` can be a host address, a tuple ``(addr, port)``, or a string ``"addr:port"``; + in the first case the given `port` is used, while in the other two it is ginore. + Return tuple ``(addr, port)``. + """ + if isinstance(addr, tuple): + return addr[:2] + m=re.match(r"^([\w.]+):(\d+)$",addr) + if m: + return m[1],int(m[2]) + return addr,port + class ClientSocket: """ A client socket (used to connect to a server socket). diff --git a/pylablib/core/utils/rpyc_utils.py b/pylablib/core/utils/rpyc_utils.py index 36ffdef..dbf4da6 100644 --- a/pylablib/core/utils/rpyc_utils.py +++ b/pylablib/core/utils/rpyc_utils.py @@ -14,6 +14,7 @@ import pickle import warnings import socket +import re _default_packers={"numpy":np.ndarray.tostring,"pickle":pickle.dumps} @@ -219,6 +220,7 @@ def connect_device_service(addr, port=18812, timeout=3, attempts=2, error_on_fai (RPyC default is 3 seconds timeout and 6 attempts). If ``error_on_fail==True``, raise error if the connection failed; otherwise, return ``None`` """ + addr,port=net.as_addr_port(addr,port) with warnings.catch_warnings(): warnings.simplefilter("ignore") try: diff --git a/pylablib/devices/uc480/uc480.py b/pylablib/devices/uc480/uc480.py index 4872a35..0ba2878 100644 --- a/pylablib/devices/uc480/uc480.py +++ b/pylablib/devices/uc480/uc480.py @@ -143,11 +143,11 @@ def _get_sensor_info(self): ### Buffer controls ### def _allocate_buffers(self, n): self._deallocate_buffers() - frame_size=self._get_data_dimensions_rc()[::-1] + frame_size=self._get_data_dimensions_rc() bpp=self._get_pixel_mode_settings()[0] self._buffers=[] for _ in range(n): - self._buffers.append((lib.is_AllocImageMem(self.hcam,frame_size[0],frame_size[1],bpp),(frame_size[0],frame_size[1]),bpp)) + self._buffers.append((lib.is_AllocImageMem(self.hcam,frame_size[1],frame_size[0],bpp),(frame_size[0],frame_size[1]),bpp)) lib.is_AddToSequence(self.hcam,*self._buffers[-1][0]) return n def _deallocate_buffers(self): diff --git a/pylablib/gui/widgets/range_controls.py b/pylablib/gui/widgets/range_controls.py index dee51c1..4da5f87 100644 --- a/pylablib/gui/widgets/range_controls.py +++ b/pylablib/gui/widgets/range_controls.py @@ -188,7 +188,7 @@ def setup(self, xlim=(0,None), ylim=None, minsize=0, maxsize=None, labels=("X"," and return their constrained versions. """ self.kind=kind - self.setMinimumSize(QtCore.QSize(100,60)) + self.setMinimumSize(QtCore.QSize(110,70)) self.setMaximumSize(QtCore.QSize(2**16,60)) self.main_layout=QtWidgets.QVBoxLayout(self) self.main_layout.setObjectName("main_layout") @@ -346,7 +346,7 @@ def setup(self, xlim=(0,None), ylim=None, maxbin=None, minsize=0, maxsize=None, and return their constrained versions. """ self.kind=kind - self.setMinimumSize(QtCore.QSize(100,60)) + self.setMinimumSize(QtCore.QSize(110,70)) self.setMaximumSize(QtCore.QSize(2**16,60)) self.main_layout=QtWidgets.QVBoxLayout(self) self.main_layout.setObjectName("main_layout") From 51bc40725ff51c0c3c4c891dd89b3aa4b39e5e18 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Thu, 17 Jun 2021 20:32:53 +0200 Subject: [PATCH 16/31] Added basic stream control tools --- pylablib/thread/stream/__init__.py | 2 + pylablib/thread/stream/stream_manager.py | 545 +++++++++++++++++++++++ pylablib/thread/stream/stream_message.py | 495 ++++++++++++++++++++ 3 files changed, 1042 insertions(+) create mode 100644 pylablib/thread/stream/stream_manager.py create mode 100644 pylablib/thread/stream/stream_message.py diff --git a/pylablib/thread/stream/__init__.py b/pylablib/thread/stream/__init__.py index e69de29..9f5ffad 100644 --- a/pylablib/thread/stream/__init__.py +++ b/pylablib/thread/stream/__init__.py @@ -0,0 +1,2 @@ +from .stream_message import IStreamMessage, DataStreamMessage, GenericDataStreamMessage, DataBlockMessage, FramesMessage +from .stream_manager import StreamIDCounter, MultiStreamIDCounter, StreamSource, AccumulatorStreamReceiver \ No newline at end of file diff --git a/pylablib/thread/stream/stream_manager.py b/pylablib/thread/stream/stream_manager.py new file mode 100644 index 0000000..4b3136d --- /dev/null +++ b/pylablib/thread/stream/stream_manager.py @@ -0,0 +1,545 @@ +from ...core.utils import general, funcargparse +from ...core.thread import controller + +import threading +import collections + +from . import stream_message + + + + +class StreamIDCounter: + """ + Counter which keeps track of the session and message IDs for incoming or generated messages. + + Args: + use_mid: if ``False``, do not use ``mid`` counter and always return ``mid=None`` + sid_ooo: behavior if supplied session ID in :meth:`next_session` or :meth:`update` is out of order (lower than the current count); + can be ``"ignore"`` (keep the current counter value), ``"set"`` (set the value to the new smaller one), or ``"error"`` (raise an exception) + mid_ooo: behavior if supplied message ID in :meth:`next_message` or :meth:`update` is out of order (lower than the current count); + can be ``"ignore"`` (keep the current counter value), ``"set"`` (set the value to the new smaller one), or ``"error"`` (raise an exception) + """ + def __init__(self, use_mid=True, sid_ooo="ignore", mid_ooo="ignore"): + funcargparse.check_parameter_range(sid_ooo,"sid_ooo",["ignore","set","error"]) + funcargparse.check_parameter_range(mid_ooo,"mid_ooo",["ignore","set","error"]) + self.sid_gen=general.UIDGenerator() + self.sid=self.sid_gen() + self.use_mid=use_mid + self.mid_gen=general.UIDGenerator() if self.use_mid else lambda: None + self.mid=self.mid_gen() + self.sid_ooo=sid_ooo + self.mid_ooo=mid_ooo + self.cutoff=(0,0) + + def update_session(self, sid): + """ + Update the session counter. + + If `sid` is ``None``, keep the counter the same. If `sid` is ``"next"``, advance the current counter. + If the session ID value was increased, reset the message ID counter. + Return the new session ID. + """ + if sid is None: + return self.sid + if sid=="next": + return self.next_session() + csid=self.sid + if sid>=csid: + self.sid_gen.reset(sid) + elif self.sid_ooo=="set": + self.sid_gen.reset(sid) + elif self.sid_ooo=="error": + raise ValueError("next session id {} is before the current id {}".format(sid,self.sid)) + self.sid=self.sid_gen() + if self.sid>csid: + self.mid_gen=general.UIDGenerator() if self.use_mid else lambda: None + self.mid=self.mid_gen() + return self.sid + def next_session(self): + """Mark the start of the next session and reset message ID counter""" + self.sid=self.sid_gen() + self.mid_gen=general.UIDGenerator() if self.use_mid else lambda: None + self.mid=self.mid_gen() + return self.sid + def update_message(self, mid): + """ + Update the message counter. + + If `mid` is ``None``, keep the counter the same. If `mid` is ``"next"``, advance the current counter. + Return the new message ID. + """ + if mid is None or not self.use_mid: + return self.mid + if mid=="next": + return self.next_message() + if mid>=self.mid: + self.mid_gen.reset(mid) + elif self.mid_ooo=="set": + self.mid_gen.reset(mid) + elif self.mid_ooo=="error": + raise ValueError("next message id {} is before the current id {}".format(mid,self.mid)) + self.mid=self.mid_gen() + return self.mid + def next_message(self): + """Mark the next message""" + self.mid=self.mid_gen() + return self.mid + def update(self, sid=None, mid=None): + """ + Update counters to the ones supplied. + + Return ``True`` if the session ID was incremented as a result. + """ + csid=self.sid + self.update_session(sid) + self.update_message(mid) + return self.sid>csid + def receive_message(self, msg, sn=None): + """ + Update counters to the ones stored in the message. + + `sn` specifies the stream name within the message. + Return ``True`` if the session ID was incremented as a result. + """ + return self.update(*msg.get_ids(sn)) + def get_ids(self): + """Get stored IDs as a tuple ``(sid, mid)``""" + return self.sid,self.mid + def set_cutoff(self, sid=None, mid=0): + """ + Set the ID cutoff. + + Used in conjunction with :meth:`check_cutoff` to check if session and message IDs are above the cutoff. + A value of ``None`` keeps the current value. + Since IDs are normally non-negative, setting `sid` and `mid` to 0 effectively removes the cutoff. + Return the updated cutoff value. + """ + csid,cmid=self.cutoff + self.cutoff=(csid if sid is None else sid),(cmid if mid is None else mid) + return self.cutoff + def check_cutoff(self, sid=None, mid=None): + """ + Check if the supplied IDs pass the cutoff (i.e., above or equal to it). + + Values of ``None`` are not checked, i.e., assumed to always pass. + """ + if sid is not None and sid=cutoff: + ncut=i + break + del self.acc[:ncut] + if self.last_read is not None and self.last_readsince): + if cond is not None: + for evt in self.acc[::-1]: + if evt.ids<=since: + break + if cond(*evt): + do_wait=False + self.last_evt=evt + break + else: + do_wait=False + self.last_evt=self.acc[-1] + else: + do_wait=True + self.waiting=True + try: + if do_wait: + check=self.get_checker(since=since,cond=cond) + self.ctl.wait_until(check,timeout=timeout) + finally: + self.waiting=False + self.last_wait=self.last_evt.ids + return self.last_evt + def _get_acc_rng(self, i0, i1, peek, as_event): + if i1 is not None and i1<=i0 or not self.acc: + return [] + with self.lock: + if i1 is None: + i1=len(self.acc) + evts=self.acc[i0:i1] + if not peek: + del self.acc[:i1] + self.last_read=evts[-1].ids + return evts if as_event else [evt.msg for evt in evts] + def get_oldest(self, n=1, peek=False, as_event=False): + """ + Get the oldest `n` messages from the accumulator queue. + + If `n` is ``None``, return all messages. + If there are less than `n` message in the queue, return all of them. + If ``peek==True``, just return the messages; otherwise, pop the from the queue in mark the as read. + If ``as_event==True``, each message is represented as a tuple ``(src, tag, msg, ids)`` describing the received event; + otherwise, just messages are returned. + """ + return self._get_acc_rng(0,n,peek=peek,as_event=as_event) + def get_newest(self, n=1, peek=True, as_event=False): + """ + Get the newest `n` messages from the accumulator queue. + + If there are less than `n` message in the queue, return all of them. + If ``peek==True``, just return the messages; otherwise, clear the queue after reading. + If ``as_event==True``, each message is represented as a tuple ``(src, tag, msg, ids)`` describing the received event; + otherwise, just messages are returned. + """ + if n<=0: + return [] + return self._get_acc_rng(-n,-1,peek=peek,as_event=as_event) \ No newline at end of file diff --git a/pylablib/thread/stream/stream_message.py b/pylablib/thread/stream/stream_message.py new file mode 100644 index 0000000..abaefca --- /dev/null +++ b/pylablib/thread/stream/stream_message.py @@ -0,0 +1,495 @@ +from ...core.utils import functions + +import time + +import numpy as np + + + + + + +class IStreamMessage: + """ + A generic message belonging to a stream. + + Args: + sn: stream name (usually, references to the stream original source or purpose); could be ``None``, implying an "anonymous" stream + sid: session numerical ID, a unique ID (usually an incrementing number) which is different for different streaming sessions + mid: message numerical ID, a unique ID (usually an incrementing number) of the message within the stream + + Either `mid` or both IDs can be ``None``, indicating that the corresponding stream does not keep track of these IDs. + In addition, `sid` and `mid` can be dictionaries (preferrably with the same keys), which indicates that this message + inherits IDs from several streams with the given keys (e.g., it comes from a producer which join several streams together into one). + """ + _init_args=[] + def __init__(self, sn=None, sid=None, mid=None): + super().__init__() + self.sid=sid + self.mid=mid + self.sn=sn + self._setup_ids() + if not type(self)._init_args: + type(self)._init_args=functions.funcsig(self.__init__).arg_names[1:] + + def _setup_ids(self): + if isinstance(self.sid,dict): + if self.sn is None: + raise ValueError("anonymous streams can not have multi-named IDs: sid={}".format(self.sid)) + if not (self.sn in self.sid): + raise KeyError("stream name {} is not in sid {}".format(self.sn,self.sid)) + elif self.sn is not None: + self.sid={self.sn:self.sid} + if self.mid is not None: + self.mid={self.sn:self.mid} + def get_ids(self, name=None): + """ + Get message IDs as a tuple ``(sid, mid)``. + + If `name` is ``None``, assume that the message IDs are not named, and simply return them. + Otherwise, assume that they are names (i.e., dictionaries), and return IDs for the corresponding name; + in this case `name` can be either a single name, or a tuple of 2 names, one for ``sid`` and one for ``mid``. + If either of these names is ``None``, return ``None`` for the corresponding ID. + """ + if name is None: + name=self.sn + if name=="all" or name is None: + return self.sid,self.mid + sid=self.sid[name] + mid=None if self.mid is None else self.mid[name] + return sid,mid + def _build_copy_arg(self, name, *arg): + if arg: + return arg[0] + value=getattr(self,name) + if isinstance(value,dict): + return dict(value) + if isinstance(value,list): + return list(value) + return value + def copy(self, *args, **kwargs): + """ + Make a copy of the message + + Any specified keyword parameter replaces or adds to the current message parameter. + """ + kwargs.update(dict(zip(self._init_args,args))) + for ca in self._init_args: + try: + kwargs[ca]=self._build_copy_arg(ca,*([kwargs[ca]] if ca in kwargs else [])) + except KeyError: + pass + return type(self)(**kwargs) + + +class MetainfoAccessor: + """Accessor which exposes metainfo dictionary values as attributes""" + def __init__(self, msg): + self.msg=msg + def __getattr__(self, name): + return self.msg.metainfo[name] +class DataStreamMessage(IStreamMessage): + """ + A generic data stream message. + + In addition to :class:`IStreamMessage`, carries some information about the message and its source. + + Args: + source: data source, e.g., daq, camera, processor, etc. + tag: extra message tag + creation_time: message creation time (if ``None``, use current time) + metainfo: additional metainfo dictionary; the contents is arbitrary, but it's assumed to be message-wide, i.e., common for all frames in the message; + `source`, `tag`, and `creation_time` are stored there. + + All of the supplied additional data (source, tag, etc.) is automatically added in the metainfo dictionary. + It can be either directly, e.g., ``msg.metainfo["source"]``, or through the ``.mi`` accessor, e.g., ``msg.mi.source``. + """ + def __init__(self, source=None, tag=None, creation_time=None, metainfo=None, sn=None, sid=None, mid=None): + super().__init__(sn=sn,sid=sid,mid=mid) + self.metainfo=metainfo or {} + self._add_metainfo_args(source=source,tag=tag,creation_time=creation_time) + if "creation_time" not in self.metainfo: + self.metainfo["creation_time"]=time.time() + self.mi=MetainfoAccessor(self) + + _metainfo_args={"source","tag","creation_time"} # creation arguments which are automatically added to the metainfo + def _add_metainfo_args(self, **kwargs): + for k,v in kwargs.items(): + if v is not None: + self.metainfo[k]=v + def _build_copy_arg(self, name, *arg): + if name in self._metainfo_args: # already in metainfo + return arg[0] if arg else None + if name=="metainfo" and arg: + uvalue=dict(self.metainfo) + uvalue.update(arg[0]) + return uvalue + return super()._build_copy_arg(name,*arg) + +class GenericDataStreamMessage(DataStreamMessage): + """ + Generic data stream message, which contains arbitrary data. + + All methods and attributes (except ``data``) are the same as :class:`DataStreamMessage`. + """ + def __init__(self, data=None, source=None, tag=None, creation_time=None, metainfo=None, sn=None, sid=None, mid=None): + super().__init__(source=source,tag=tag,creation_time=creation_time,metainfo=metainfo,sn=sn,sid=sid,mid=mid) + self.data=data + + + + + + +class DataBlockMessage(DataStreamMessage): + """ + A message containing a block of several aligned streams of data (e.g., several daq channels, or aligned data streams). + + Also has methods for simple data extraction/modification. + + Args: + data: dictionary ``{name: values}`` of data chunks corresponding to each stream. All values should have the same length + order: default order of the data channels (used when they are returned as a list instead of a dictionary); by default, use the dictionary keys ord + source: data source, e.g., daq, camera, processor, etc. + tag: extra message tag + creation_time: message creation time (if ``None``, use current time) + metainfo: additional metainfo dictionary; the contents is arbitrary, but it's assumed to be message-wide, i.e., common for all frames in the message; + `source`, `tag`, and `creation_time` are stored there. + + All of the supplied additional data (source, tag, etc.) is automatically added in the metainfo dictionary. + It can be either directly, e.g., ``msg.metainfo["source"]``, or through the ``.mi`` accessor, e.g., ``msg.mi.source``. + """ + def __init__(self, data, order=None, source=None, tag=None, creation_time=None, metainfo=None, sn=None, sid=None, mid=None): + super().__init__(source=source,tag=tag,creation_time=creation_time,metainfo=metainfo,sn=sn,sid=sid,mid=mid) + self.data=data + self.order=order or list(data) + if set(self.order)!=set(self.data): + raise ValueError("data channels order doesn't agree with supplied data channels") + lch=None,None + for name,col in data.items(): + if lch[0] is None: + lch=name,len(col) + elif len(col)!=lch[1]: + raise ValueError("channel length doesn't agree: {} for {} vs {} for {}".format(name,len(col),*lch)) + + def __len__(self): + for ch in self.data: + return len(self.data[ch]) + return 0 + def __bool__(self): + return len(self)>0 + + def __getitem__(self, key): + return self.data[key] + + def filter_columns(self, include=None, exclude=None, strict=True): + """ + Filter data columns to include and excluded the columns with the given names. + + If ``strict==True`` some of column in `include` are not present in the message, raise an exception. + """ + if strict and include is not None: + for ch in include: + if ch not in self.data: + raise KeyError("included data channel {} is missing".format(ch)) + data=set() + for ch in self.data: + if include is None or ch in include: + if exclude is None or ch not in exclude: + data.add(ch) + self.data={ch:val for ch,val in self.data.items() if ch in data} + self.order=[ch for ch in self.order if ch in data] + + def cut_to_size(self, n, from_end=False): + """ + Cut the message to contain at most ``n`` rows. + + If ``from_end==True``, leave the last `n` rows; otherwise, leave the first `n` rows. + Return ``True`` if the cut message contains `n` row (i.e., its original length was ``>=n``), and ``False`` otherwise. + """ + if n==0: + self.data={ch:[] for ch in self.data} + return True + for ch in list(self.data): + col=self.data[ch] + if len(col)=0: + rng=0,rng + else: + rng=rng,l + return rng + def get_data_columns(self, channels=None, rng=None): + """ + Get table data as a list of columns. + + Args: + channels: list of channels to get; all channels by default (in which case, order is determined by internal ``order`` variable) + rng: optional cut range for the resulting data; can be a single number (positive to cut from the beginning, negative from the end), + or a tuple ``(start, end)``. + """ + order=channels or self.order + if rng is None: + return [self.data[ch] for ch in order] + rng=self._normalize_rng(rng) + return [self.data[ch][rng[0]:rng[1]] for ch in order] + def get_data_rows(self, channels=None, rng=None): + """ + Get table data as a list of rows. + + Args: + channels: list of channels to get; all channels by default + rng: optional cut range for the resulting data; can be a single number (positive to cut from the beginning, negative from the end), + or a tuple ``(start, end)``. + """ + return list(zip(*self.get_data_columns(channels=channels,rng=rng))) + def get_data_dict(self, channels=None, rng=None): + """ + Get table data as a dictionary ``{name: column}``. + + Args: + channels: list of channels to get; all channels by default + rng: optional cut range for the resulting data; can be a single number (positive to cut from the beginning, negative from the end), + or a tuple ``(start, end)``. + """ + order=channels or self.order + if rng is None: + return dict(self.data) + rng=self._normalize_rng(rng) + return {ch:self.data[ch][rng[0]:rng[1]] for ch in order} + + + + + + + +class FramesMessage(DataStreamMessage): + """ + A message containing a frame bundle and associated information (indices, etc.). + + Also has methods for simple data extraction/modification. + + Args: + frames: list of frames (2D or 3D numpy arrays for 1 or more frames) + indices: list of frame indices (if a corresponding array contains several frames, it can be the index of the first frame); + if ``None``, autofill starting from 0 + frame_info: list of frame chunk infos (one entry per chunk); + if ``None``, keep as ``None`` + source: frames source, e.g., camera or processor + tag: extra batch tag + creation_time: message creation time (if ``None``, use current time) + step: expected index step between the frames + chunks: if ``True``, force all `frames` elements to be chunks (of length 1 if necessary); + otherwise, if all of them are single-frame 2D arrays, keep them this way + metainfo: additional metainfo dictionary; the contents is arbitrary, but it's assumed to be message-wide, i.e., common for all frames in the message; + `source`, `tag`, `creation_time`, and `step` are stored there; + common additional keys are ``"status_line"`` (expected position of the status line), or ``"frame_info_field"`` (fields name for frame-info entries) + + All of the supplied additional data (source, tag, etc.) is automatically added in the metainfo dictionary. + It can be either directly, e.g., ``msg.metainfo["source"]``, or through the ``.mi`` accessor, e.g., ``msg.mi.source``. + """ + def __init__(self, frames, indices=None, frame_info=None, source=None, tag=None, creation_time=None, step=1, chunks=False, metainfo=None, sn=None, sid=None, mid=None): + super().__init__(source=source,tag=tag,creation_time=creation_time,metainfo=metainfo,sn=sn,sid=sid,mid=mid) + self._add_metainfo_args(step=step) + if isinstance(frames,tuple): + frames=list(frames) + elif not isinstance(frames,list): + frames=[frames] + self.frames=frames + if indices is not None: + if isinstance(indices,tuple): + indices=list(indices) + elif not isinstance(indices,list): + indices=[indices] + if len(indices)!=len(self.frames): + raise ValueError("index array length {} is different from the frames array length {}".format(len(indices),len(self.frames))) + self.indices=indices + if frame_info is not None: + if isinstance(frame_info,tuple): + frame_info=list(frame_info) + elif not isinstance(frame_info,list): + frame_info=[frame_info] + if len(frame_info)!=len(self.frames): + raise ValueError("frame info array length {} is different from the frames array length {}".format(len(frame_info),len(self.frames))) + self.frame_info=frame_info + self.chunks=chunks + self._setup_chunks() + + def __len__(self): + return len(self.frames) + def __bool__(self): + return len(self)>0 + + _metainfo_args=DataBlockMessage._metainfo_args|{"step"} + def _setup_chunks(self): + frames=self.frames + if not self.chunks: + self.chunks=any([f.ndim>2 for f in frames]) + if self.chunks: + self.frames=[f[None] if f.ndim==2 else f for f in frames] + if self.indices is None: + if self.chunks: + self.indices=[0]+list(np.cumsum([len(f) for f in self.frames])) + else: + self.indices=list(range(len(self.frames))) + elif self.chunks: + if self.indices is not None: + step=self.mi.step + for i,f in enumerate(self.frames): + idx=self.indices[i] + if np.ndim(idx)==0: + self.indices[i]=np.arange(idx,idx+f.shape[0]*step,step) + elif len(f)!=len(idx): + raise ValueError("frames and indices array lengths don't agree: {} vs {}".format(len(f),len(idx))) + + def nframes(self): + """Get total number of frames (taking into account that 3D array elements contain multiple frames)""" + return sum([len(f) for f in self.frames]) if self.chunks else len(self.frames) + def nbytes(self): + """Get total size of the frames in the message in bytes""" + return sum([f.nbytes for f in self.frames]) + + def get_missing_frames_number(self, last_frame=None): + """ + Check the message for missing frames. + + If `last_frame` is not ``None``, it should be the index of the frame preceding this block. + Assume that the missing frames are only between the blocks. + Return number of missing frames. + """ + chunks=self.chunks + missing=0 + for i in self.indices: + fi,li=(i[0],i[-1]) if chunks else (i,i) + if last_frame is not None: + missing+=fi-last_frame-self.mi.step + last_frame=li + return missing + + def get_frames_stack(self, n=None, reverse=False, add_indices=False, copy=True): + """ + Get a list of at most `n` frames from the message (if ``None``, return all frames). + + If ``reverse==True``, return last `n` frames (in the reversed order); otherwise, return first `n` frames. + If ``add_indices==True``, elements of the list are tuples ``(index, img)``; otherwise, they are just images. + If ``copy==True``, copy frames (otherwise changing the returned frames can affect the stored frames). + """ + if n==0: + return [] + if n is None: + n=self.nframes() + frames=[] + indices=[] + if self.chunks: + if reverse: + for i,f in zip(self.indices[::-1],self.frames[::-1]): + chunk=f[:-(n-len(frames))-1:-1] + add_frames=list(chunk.copy()) if copy else list(chunk) + frames+=add_frames + if add_indices: + indices+=list(i[:-(n-len(frames))-1:-1]) + if len(frames)>=n: + break + else: + for i,f in zip(self.indices,self.frames): + chunk=f[:n-len(frames)] + add_frames=list(chunk.copy()) if copy else list(chunk) + frames+=add_frames + if add_indices: + indices+=list(i[:n-len(frames)]) + if len(frames)>=n: + break + else: + frames=self.frames[-n:][::-1] if reverse else self.frames[:n] # pylint: disable=invalid-unary-operand-type + if copy: + frames=[f.copy() for f in frames] + if add_indices: + indices=self.indices[-n:][::-1] if reverse else self.indices[:n] # pylint: disable=invalid-unary-operand-type + return list(zip(indices,frames)) if add_indices else frames + def cut_to_size(self, n, from_end=False): + """ + Cut contained data to contain at most `n` frames. + + If ``from_end==True``, leave last `n` frames; otherwise, leave first `n` frames. + Return ``True`` if there are `n` frames after the cut, and ``False`` if there are less than `n`. + """ + if n==0: + self.data={k:[] for k in self.data} + return True + if self.chunks: + end=None + size=0 + d=-1 if from_end else 1 + frames=self.frames[::d] + indices=self.indices[::d] + for i,f in enumerate(frames): + if size+len(f)>n: + end=i + break + size+=len(f) + if end is None: + return size Date: Thu, 17 Jun 2021 20:46:05 +0200 Subject: [PATCH 17/31] Updated frame and block process with the new tools --- pylablib/devices/interface/camera.py | 124 ++++- pylablib/thread/script_thread.py | 105 +--- pylablib/thread/stream/blockstream.py | 208 ++------ pylablib/thread/stream/frameproc.py | 663 +++++++++++++++----------- pylablib/thread/stream/table_accum.py | 8 +- 5 files changed, 566 insertions(+), 542 deletions(-) diff --git a/pylablib/devices/interface/camera.py b/pylablib/devices/interface/camera.py index 033f1a6..8e78206 100644 --- a/pylablib/devices/interface/camera.py +++ b/pylablib/devices/interface/camera.py @@ -912,4 +912,126 @@ def get_roi_limits(self, hbin=1, vbin=1): # pylint: disable=unused-argument In some cameras, the step and the minimal size depend on the binning, which can be supplied. """ w,h=self.get_detector_size() - return TAxisROILimit(w,w,w,w,1),TAxisROILimit(h,h,h,h,1) \ No newline at end of file + return TAxisROILimit(w,w,w,w,1),TAxisROILimit(h,h,h,h,1) + + + + + + +def _get_partial_frame(frame, excl_area): + nr,nc=frame.shape[-2:] + r0,r1,c0,c1=excl_area + is_row=r1-r00 else frame[:,r1+1,c0:c1+1] + frame[:,r0:r1+1,c0:c1+1]=graft[:,None,:] + else: + graft=frame[:,r0:r1+1,c0-1] if c0>0 else frame[:,r0:r1+1,c1+1] + frame[:,r0:r1+1,c0:c1+1]=graft[:,:,None] + return frame[0] if frame_2d else frame +def extract_status_line(frame, status_line, copy=True): + """ + Extract status line, if present. + + Args: + frame: a frame to process (2D or 3D numpy array; if 3D, the first axis is the frame number) + status_line: status line descriptor (from the frames message) + copy: if ``True``, make copy of the original status line data. + """ + if status_line is None: + return None + if frame.ndim==2: + frame_2d=True + frame=frame[None,:,:] + else: + frame_2d=False + r0,r1,c0,c1=_normalize_sline_pos(status_line,frame.shape) + sline=frame[:,r0:r1+1,c0:c1+1] + if copy: + sline=sline.copy() + return sline[0] if frame_2d else sline +def insert_status_line(frame, status_line, value, copy=True): + """ + Insert status line, if present. + + Args: + frame: a frame to process (2D or 3D numpy array; if 3D, the first axis is the frame number) + status_line: status line descriptor (from the frames message) + value: status line value + copy: if ``True``, make copy of the original status line data. + """ + if status_line is None: + return frame.copy() if copy else frame + r0,r1,c0,c1=_normalize_sline_pos(status_line,frame.shape) + if value.ndim==2: + value=value[None,:,:] + value=value[:,:r1-r0+1,:c1-c0+1] + return remove_status_line(frame,status_line,policy="value",value=value,copy=copy) +def get_status_line_roi(frame, status_line): + """Return ROI taken by the status line in the given frame""" + if status_line is None: + return None + r0,r1,c0,c1=_normalize_sline_pos(status_line,frame.shape) + return image_utils.ROI(r0,r1+1,c0,c1+1) \ No newline at end of file diff --git a/pylablib/thread/script_thread.py b/pylablib/thread/script_thread.py index 2316d37..fee74f4 100644 --- a/pylablib/thread/script_thread.py +++ b/pylablib/thread/script_thread.py @@ -1,8 +1,6 @@ -from ..core.thread import controller, multicast_pool +from ..core.thread import controller from ..core.utils import functions -from ..core.gui import QtCore, Slot, Signal - import collections @@ -20,19 +18,13 @@ class ScriptThread(controller.QTaskThread): To do that, it provides a mechanism of multicast monitors: one can suspend execution until a multicast with certain properties has been received. This can be used to implement, e.g., waiting until the next stream_format/daq sample or a next camera frame. - Args: - name (str): thread name - args: args supplied to :meth:`setup_script` method - kwargs: keyword args supplied to :meth:`setup_script` method - multicast_pool: :class:`.MulticastPool` for this thread (by default, use the default common pool) - Attributes: - executing (bool): shows whether the script is executing right now; + - ``executing`` (bool): shows whether the script is executing right now; useful in :meth:`interrupt_script` to check whether it is while the script is running and is done / stopped by user / terminated (then it would be ``True``), or if the script was waiting to be executed / done executing (then it would be ``False``) Duplicates ``interrupt_reason`` attribute (``executing==False`` if and only if ``interrupt_reason=="shutdown"``) - stop_request (bool): shows whether stop has been requested from another thread (by calling :meth:`stop_execution`). - interrupt_reason (str): shows the reason for calling :meth:`interrupt_script`; + - ``stop_request`` (bool): shows whether stop has been requested from another thread (by calling :meth:`stop_execution`). + - ``interrupt_reason`` (str): shows the reason for calling :meth:`interrupt_script`; can be ``"done"`` (called in the end of regularly executed script), ``"stopped"`` (called if the script is forcibly stopped), ``"failed"`` (called if the thread is shut down while the script is active, e.g., due to error in the script or any other thread, or if the application is closing), @@ -45,16 +37,14 @@ class ScriptThread(controller.QTaskThread): - :meth:`interrupt_script`: executed when the script is finished or forcibly shut down (including due to exception or application shutdown) """ def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): - controller.QTaskThread.__init__(self,name=name,args=args,kwargs=kwargs,multicast_pool=multicast_pool) - self._monitor_signal.connect(self._on_monitor_signal,QtCore.Qt.QueuedConnection) - self._monitored_signals={} + super().__init__(name=name,args=args,kwargs=kwargs,multicast_pool=multicast_pool) self.executing=False self.interrupt_reason="shutdown" self.stop_request=False self.add_command("start_script",self._start_script) def process_interrupt(self, tag, value): - if controller.QTaskThread.process_interrupt(self,tag,value): + if super().process_interrupt(tag,value): return True if tag=="control.start": self.ca.start_script() @@ -119,89 +109,6 @@ def _start_script(self): self.interrupt_reason="failed" raise - _monitor_signal=Signal(object) - @Slot(object) - def _on_monitor_signal(self, value): - mon,msg=value - try: - signal=self._monitored_signals[mon] - if not signal.paused: - signal.messages.append(msg) - except KeyError: - pass - - class MonitoredSignal: # TODO: signal -> multicast; put in separate class? - def __init__(self, uid): - self.uid=uid - self.messages=[] - self.paused=True - def add_signal_monitor(self, mon, srcs="any", dsts="any", tags=None, filt=None): - """ - Add a new signal monitor. - - The monitoring isn't started until :meth:`start_monitoring` is called. - `mon` specifies monitor name; the rest of the arguments are the same as :meth:`.MulticastPool.subscribe` - """ - if mon in self._monitored_signals: - raise KeyError("signal monitor {} already exists".format(mon)) - uid=self.subscribe_nonsync(lambda *msg: self._monitor_signal.emit((mon,multicast_pool.TMulticast(*msg))),srcs=srcs,tags=tags,dsts=dsts,filt=filt) - self._monitored_signals[mon]=self.MonitoredSignal(uid) - def remove_signal_monitor(self, mon): - """Remove signal monitor with a given name""" - if mon not in self._monitored_signals: - raise KeyError("signal monitor {} doesn't exist".format(mon)) - uid,_=self._monitored_signals.pop(mon) - self.unsubscribe(uid) - def wait_for_signal_monitor(self, mons, timeout=None): - """ - Wait for a signal to be received on a given monitor or several monitors - - If several monitors are given (`mon` is a list), wait for a signal on any of them. - After waiting is done, pop and return signal value (see :meth:`pop_monitored_signal`). - """ - if not isinstance(mons,(list,tuple)): - mons=[mons] - for mon in mons: - if mon not in self._monitored_signals: - raise KeyError("signal monitor {} doesn't exist".format(mon)) - def check_monitors(pop=False): - for mon in mons: - if self._monitored_signals[mon].messages: - return TMulticastWaitResult(mon,self._monitored_signals[mon].messages.pop(0)) if pop else True - result=check_monitors(pop=True) - if result is not None: - return result - self.wait_until(check_monitors,timeout=timeout) - return check_monitors(pop=True) - def new_monitored_signals_number(self, mon): - """Get number of received signals at a given monitor""" - if mon not in self._monitored_signals: - raise KeyError("signal monitor {} doesn't exist".format(mon)) - return len(self._monitored_signals[mon].messages) - def pop_monitored_signal(self, mon, n=None): - """ - Pop data from the given signal monitor queue. - - `n` specifies number of signals to pop (by default, only one). - Each signal is a tuple ``(mon, sig)`` of monitor name and signal, - where `sig` is in turn tuple ``(src, tag, value)`` describing the signal. - """ - if self.new_monitored_signals_number(mon): - if n is None: - return self._monitored_signals[mon].messages.pop(0) - else: - return [self._monitored_signals[mon].messages.pop(0) for _ in range(n)] - return None - def reset_monitored_signal(self, mon): - """Reset monitored signal (clean its received signals queue)""" - self._monitored_signals[mon].messages.clear() - def pause_monitoring(self, mon, paused=True): - """Pause or un-pause signal monitoring""" - self._monitored_signals[mon].paused=paused - def start_monitoring(self, mon): - """Start signal monitoring""" - self.pause_monitoring(mon,paused=False) - def start_execution(self): """Request starting script execution""" diff --git a/pylablib/thread/stream/blockstream.py b/pylablib/thread/stream/blockstream.py index 4a4461e..51f3647 100644 --- a/pylablib/thread/stream/blockstream.py +++ b/pylablib/thread/stream/blockstream.py @@ -1,137 +1,13 @@ from ...core.thread import controller from ...core.utils import general, funcargparse +from . import stream_manager, stream_message + import numpy as np import collections - -class DataBlockMessage: - """ - A message containing a block of several aligned streams of data (e.g., several daq channels, or aligned data streams). - - Also has methods for simple data extraction/modification. - - Args: - channels: dictionary ``{name: values}`` of data chunks corresponding to each stream. All values should have the same length - order: default order of the channels (used when they are returned as a list instead of a dictionary); by default, use the dictionary keys order - metainfo: additional metainfo dictionary; the contents is arbitrary, but it's assumed to be message-wide, i.e., common for all frames in the message; - common keys are ``"source"`` (frames source, e.g., camera or processor), ``"rate"`` (data rate), ``"time"`` (creation time), or ``"tag"`` (additional batch tag) - """ - def __init__(self, channels, order=None, metainfo=None): - self.channels=channels - self.order=order or list(channels) - if set(self.order)!=set(self.channels): - raise ValueError("channels order doesn't agree with supplied channels") - self.metainfo=metainfo or {} - lch=None,None - for name,col in channels.items(): - if lch[0] is None: - lch=name,len(col) - elif len(col)!=lch[1]: - raise ValueError("channel length doesn't agree: {} for {} vs {} for {}".format(name,len(col),*lch)) - - def copy(self, **kwargs): - """ - Make a copy of the message - - Any specified keyword parameter replaces the current message parameter. - Channels are not deep copied. - """ - kwargs.setdefault("channels",dict(self.channels)) - kwargs.setdefault("order",self.order) - kwargs.setdefault("metainfo",self.metainfo) - return DataBlockMessage(**kwargs) - - def __len__(self): - for ch in self.channels: - return len(self.channels[ch]) - return 0 - def __bool__(self): - return self.__len__()>0 - - def __getitem__(self, key): - return self.channels[key] - - def filter_columns(self, include=None, exclude=None, strict=True): - if strict and include is not None: - for ch in include: - if ch not in self.channels: - raise ValueError("included channel {} is missing".format(ch)) - channels=set() - for ch in self.channels: - if include is None or ch in include: - if exclude is None or ch not in exclude: - channels.add(ch) - self.channels={ch:val for ch,val in self.channels.items() if ch in channels} - self.order=[ch for ch in self.order if ch in channels] - - def cut_to_size(self, n, reverse=False): - if n==0: - self.channels={ch:[] for ch in self.channels} - return True - for ch in list(self.channels): - col=self.channels[ch] - if len(col)0: - rng=0,rng - else: - rng=-rng,l - return rng - def get_data_columns(self, channels=None, rng=None): - """ - Get table data as a list of columns. - - Args: - channels: list of channels to get; all channels by default (in which case, order is determined by internal ``order`` variable) - maxlen: maximal column length (if stored length is larger, return last `maxlen` rows) - """ - order=channels or self.order - if rng is None: - return [self.channels[ch] for ch in order] - rng=self._normalize_rng(rng) - return [self.channels[ch][rng[0]:rng[1]] for ch in order] - def get_data_rows(self, channels=None, rng=None): - """ - Get table data as a list of rows. - - Args: - channels: list of channels to get; all channels by default - maxlen: maximal column length (if stored length is larger, return last `maxlen` rows) - """ - return list(zip(*self.get_data_columns(channels=channels,rng=rng))) - def get_data_dict(self, channels=None, rng=None): - """ - Get table data as a dictionary ``{name: column}``. - - Args: - channels: list of channels to get; all channels by default - maxlen: maximal column length (if stored length is larger, return last `maxlen` rows) - """ - order=channels or self.order - if rng is None: - return dict(self.channels) - rng=self._normalize_rng(rng) - return {ch:self.channels[ch][rng[0]:rng[1]] for ch in order} - - - - - - - - - TStreamFormerQueueStatus=collections.namedtuple("TStreamFormerQueueStatus",["enabled","queue_len","max_queue_len"]) class StreamFormerThread(controller.QTaskThread): """ @@ -141,65 +17,61 @@ class StreamFormerThread(controller.QTaskThread): When the block is complete (determined by ``block_period`` attribute), :meth:`on_new_block` is called. Accumulated data can be accessed with :meth:`get_data` and :meth:`pop_data`, or by default through ``"stream/data"`` multicast. - Args: - name: thread name - args: args supplied to :meth:`setup` method - kwargs: keyword args supplied to :meth:`setup` method - multicast_pool: :class:`.MulticastPool` for this thread (by default, use the default common pool) - Attributes: - block_period: size of a row block which causes :meth:`on_new_block` call + - ``block_period``: size of a row block which causes :meth:`on_new_block` call Commands: - ``get_data``: get the completed aligned data in a dictionary form - ``pop_data``: pop the completed aligned data (return the data and remove it from the internal storage) - ``clear_table``: clear the table with the completed aligned data - - ``clear_all``: remove all data (table and all filled channels) + - ``reset``: remove all data (table and all filled channels) + - ``set_cutoff``: set cutoff for session or message ID of a subscribed source - ``configure_channel``: configure a channel behavior (enable or disable) - ``get_channel_status``: get channel status (number of datapoints in the queue, maximal queue size, etc.) - ``get_source_status``: get lengths of multicast queues for all the data sources Methods to overload: - - :meth:`setup`: set up the thread - - :meth:`cleanup`: clean up the thread - :meth:`on_new_block`: called every time a new block is completed; by default, send an multicast with the new block's data - :meth:`prepare_new_data`: modify a new data chunk (dictionary of columns) before adding it to the storage """ - def setup(self): - """Set up the thread""" def prepare_new_data(self, columns): """ Prepare a newly acquired chunk. - `column` is a dictionary ``{name: data}`` of newly acquired data, + `columns` is a dictionary ``{name: data}`` of newly acquired data, where ``name`` is a channel name, and ``data`` is a list of one or more newly acquired values. Returned data should be in the same format. By default, no modifications are made. """ return columns + def _build_new_block(self): + data=self.pop_data() + sid,mid=self.cnt.get_ids() + self.cnt.next_message(self.sn) + block=stream_message.DataBlockMessage(data,source=self.name,sid=sid,mid=mid,sn=self.sn) + return block def on_new_block(self): """Gets called every time a new block is complete""" - data=self.pop_data() - self.send_multicast(tag="stream/data",value=DataBlockMessage(data,metainfo={"source":"stream_former"})) - def cleanup(self): - """Clean up the thread""" + self.send_multicast(tag="stream/data",value=self._build_new_block()) - def setup_task(self, *args, **kwargs): + def setup_task(self): self.channels={} self.table={} self.source_schedulers={} self.add_command("get_data") self.add_command("pop_data") self.add_command("clear_table") - self.add_command("clear_all") + self.add_command("reset") + self.add_command("set_cutoff") self.add_command("configure_channel") self.add_command("get_channel_status") self.add_command("get_source_status") + self.sn=self.name + self.cnt=stream_manager.MultiStreamIDCounter() + self.cnt.add_counter(self.sn) + self.source_sns={} self._row_cnt=0 self.block_period=1 - self.setup(*args,**kwargs) - def finalize_task(self): - self.cleanup() class ChannelQueue: """ @@ -356,7 +228,7 @@ def add_channel(self, name, func=None, max_queue_len=10, enabled=True, required= self.channels[name]=self.ChannelQueue(func,max_queue_len=max_queue_len,required=required,background=background,enabled=enabled, fill_on=fill_on,latching=latching,expand_list=expand_list,pure_func=pure_func,initial=initial) self.table[name]=[] - def subscribe_source(self, name, srcs, tags=None, dsts="any", filt=None, parse="default"): + def subscribe_source(self, name, srcs, tags=None, dsts="any", filt=None, parse="default", sn=None): """ Subscribe a source multicast to a channels. @@ -381,13 +253,30 @@ def subscribe_source(self, name, srcs, tags=None, dsts="any", filt=None, parse=" """ if parse=="default": parse=self._parse_default - def on_multicast(src, tag, value): - self._add_data(name,value,src=src,tag=tag,parse=parse) - uid=self.subscribe_commsync(on_multicast,srcs=srcs,tags=tags,dsts=dsts,filt=filt,limit_queue=-1) - self.source_schedulers[name]=self._multicast_schedulers[uid] + if sn is not None: + self.source_sns[name]=sn + def on_multicast(src, tag, value): + self.cnt.receive_message(value,sn=sn) + if self.cnt.check_cutoff(value,sn=sn): + self._add_data(name,value,src=src,tag=tag,parse=parse) + else: + def on_multicast(src, tag, value): + self._add_data(name,value,src=src,tag=tag,parse=parse) + uid=self.subscribe_commsync(on_multicast,srcs=srcs,tags=tags,dsts=dsts,filt=filt,limit_queue=None,priority=10) + # self.source_schedulers[name]=self._multicast_schedulers[uid] #TODO: save all schedulers in the task controller + if sn is not None: + self.cnt.add_counter(sn) + def set_cutoff(self, name, sid=None, mid=0): + """ + Set cutoffs for session and message IDs. + + Any arriving subscribed messages with IDs below the cutoff will be ignored. + If `sid` or `mid` are ``None``, it implies no cutoff. + """ + return self.cnt.set_cutoff(self.source_sns[name],sid=sid,mid=mid) def _parse_default(self, src, tag, value): - if isinstance(value,DataBlockMessage): + if isinstance(value,stream_message.DataBlockMessage): return value.get_data_dict() return value def _add_data(self, name, value, src=None, tag=None, parse=None): @@ -491,14 +380,15 @@ def pop_data(self, nrows=None, columns=None): def clear_table(self): """Clear table containing all complete rows""" self.table=dict([(n,[]) for n in self.table]) - def clear_all(self): + def reset(self): """Clear everything: table of complete rows and all channel queues""" self.table=dict([(n,[]) for n in self.table]) for _,ch in self.channels.items(): ch.clear() self._partial_rows=[] + self.cnt.next_session(self.sn) - def configure_channel(self, name, enable=True, required="auto", clear=True): + def configure_channel(self, name, enable=True, required="auto", reset=True): """ Reconfigure existing channel. @@ -507,12 +397,12 @@ def configure_channel(self, name, enable=True, required="auto", clear=True): enabled (bool): determines if the channel is enabled by default (disabled channel always returns ``None``) required: determines if the channel is required to receive the value to complete the row; by default, ``False`` if `func` is specified and ``True`` otherwise - clear (bool): if ``True``, clear all channels after reconfiguring + reset (bool): if ``True``, clear all channels after reconfiguring """ self.channels[name].enable(enable) self.channels[name].set_required(required) - if clear: - self.clear_all() + if reset: + self.reset() def get_channel_status(self): """ Get channel status. diff --git a/pylablib/thread/stream/frameproc.py b/pylablib/thread/stream/frameproc.py index deedb77..5a60896 100644 --- a/pylablib/thread/stream/frameproc.py +++ b/pylablib/thread/stream/frameproc.py @@ -1,31 +1,31 @@ -# from pylablib.aux_libs.gui import helpers - from ...core.thread import controller -from ...core.utils import dictionary -from ...core.dataproc import filters, image +from ...core.dataproc import filters -from . import framestream +from ...devices.interface import camera as camera_utils +from . import stream_manager, stream_message import numpy as np import time +import collections ########## Frame processing ########## -class FramePreprocessorThread(controller.QTaskThread): +class FrameBinningThread(controller.QTaskThread): """ - Frame preprocessor thread: receives frames and re-emit them after some simple preprocessing (binning in time or space). + Full frame binning thread: receives frames and re-emit them after binning along time or space axes. Setup args: - ``src``: name of the source thread (usually, a camera) - ``tag_in``: receiving multicast tag (for the source multicast) - - ``tag_out``: emitting multicast tag (for the multicast emitted by the processor) + - ``tag_out``: emitting multicast tag (for the multicast emitted by the processor); by default, same as ``tag_in`` Multicasts: - - ````: emitted with pre-processed frames + - ````: emitted with binned frames Variables: - - ``bin_params``: full binning parameters: tuple ``(spat_bin, spat_bin_mode, time_bin, time_bin_mode)`` - - ``result_type``: resulting frames type (see :meth:`setup_binning` for parameters) + - ``params/spat``: spatial binning parameters: ``"bin"`` for binning size (a 2-tuple) and ``"mode"`` for binning mode + - ``params/time``: temporal binning parameters: ``"bin"`` for binning size and ``"mode"`` for binning mode + - ``params/dtype``: resulting frames type (see :meth:`setup_binning` for parameters) - ``enabled``: indicates whether binning has been enabled Commands: @@ -33,27 +33,21 @@ class FramePreprocessorThread(controller.QTaskThread): - ``setup_binning``: setup binning parameters """ def setup_task(self, src, tag_in, tag_out=None): - self.subscribe_commsync(self.process_multicast,srcs=src,tags=tag_in,dsts="any",limit_queue=1,on_full_queue="wait",priority=-1) + self.subscribe_commsync(self.process_input_frames,srcs=src,tags=tag_in,dsts="any",limit_queue=2,on_full_queue="wait",subscription_priority=-5) self.tag_out=tag_out or tag_in - self.spat_bin=(1,1) - self.spat_bin_mode="skip" - self.time_bin=1 - self.time_bin_mode="skip" - self._setup_bin_params() - self.acc_frame=None - self.acc_frame_num=0 - self.result_type=None + self.v["params/spat"]={"bin":(1,1),"mode":"skip"} + self.v["params/time"]={"bin":1,"mode":"skip"} + self.v["params/dtype"]=None self.v["enabled"]=False + self._clear_buffer() + self.cnt=stream_manager.StreamIDCounter() self.add_command("setup_binning") self.add_command("enable_binning") - def _setup_bin_params(self): - self.v["bin_params"]=(self.spat_bin,self.spat_bin_mode,self.time_bin,self.time_bin_mode) - self.v["result_type"]=self.result_type def enable_binning(self, enabled=True): """Enable or disable the binning""" self.v["enabled"]=enabled - def setup_binning(self, spat_bin, spat_bin_mode, time_bin, time_bin_mode, result_type=None): + def setup_binning(self, spat_bin, spat_bin_mode, time_bin, time_bin_mode, dtype=None): """ Setup binning parameters. @@ -62,102 +56,115 @@ def setup_binning(self, spat_bin, spat_bin_mode, time_bin, time_bin_mode, result spat_bin_mode: mode for spatial binning; can be ``"skip"`` (simply take every n'th pixel), ``"sum"``, ``"min"``, ``"max"``, or ``"mean"``. time_bin: binning factor for the time axis time_bin_mode: same as `spat_bin_mode`, but for the time axis - result_type: if not ``None``, the resulting frames are converted to the given type; + dtype: if not ``None``, the resulting frames are converted to the given type; otherwise, they are converted into the same type as the source frames; note that if the source type is integer and binning mode is ``"mean"`` or ``"sum"``, some information might be lost through rounding or integer overflow; for the purposes of ``"mean"`` and ``"sum"`` binning the frames are always temporarily converted to float """ - if spat_bin!=self.spat_bin or spat_bin_mode!=self.spat_bin_mode or time_bin!=self.time_bin or time_bin_mode!=self.time_bin_mode: + par=self.v["params"] + if spat_bin!=par["spat/bin"] or spat_bin_mode!=par["spat/mode"] or time_bin!=par["time/bin"] or time_bin_mode!=par["time/mode"]: self._clear_buffer() - self.spat_bin=spat_bin - self.spat_bin_mode=spat_bin_mode - self.time_bin=time_bin - self.time_bin_mode=time_bin_mode - self.result_type=result_type - self._setup_bin_params() - - def _clear_buffer(self): + self.v["params/spat"]={"bin":spat_bin,"mode":spat_bin_mode} + self.v["params/time"]={"bin":time_bin,"mode":time_bin_mode} + self.v["params/dtype"]=dtype + + def _clear_buffer(self, clear_info=True): self.acc_frame=None self.acc_frame_num=0 - def _bin_spatial(self, frames, status_line): - if self.spat_bin!=(1,1): - sl=framestream.extract_status_line(frames,status_line,copy=False) - if self.spat_bin[0]>1: - frames=filters.decimate(frames,self.spat_bin[0],dec=self.spat_bin_mode,axis=1) - if self.spat_bin[1]>1: - frames=filters.decimate(frames,self.spat_bin[1],dec=self.spat_bin_mode,axis=2) + if clear_info: + self.last_frame_info=None + def _bin_spatial(self, frames, n, dec, status_line): + if n!=(1,1): + sl=camera_utils.extract_status_line(frames,status_line,copy=False) + if n[0]>1: + frames=filters.decimate(frames,n[0],dec=dec,axis=-2) + if n[1]>1: + frames=filters.decimate(frames,n[1],dec=dec,axis=-1) if sl is not None: - sl=sl[:,:frames.shape[1],:frames.shape[2]] - frames=framestream.insert_status_line(frames,status_line,sl,copy=(self.spat_bin_mode=="skip")) + frames=camera_utils.insert_status_line(frames,status_line,sl,copy=(dec=="skip")) return frames - def _decimate_with_status_line(self, frames, dec, status_line): - res=filters.decimate(frames,self.time_bin,dec=dec,axis=0) + def _decimate_with_status_line(self, frames, n, dec, status_line): + res=filters.decimate(frames,n,dec=dec,axis=0) if dec!="skip" and status_line is not None: - binned_n=self.time_bin*(len(frames)//self.time_bin) - sl=framestream.extract_status_line(frames[:binned_n:self.time_bin],status_line,copy=False) - res=framestream.insert_status_line(res,status_line,sl,copy=False) + binned_n=n*(len(frames)//n) + sl=camera_utils.extract_status_line(frames[:binned_n:n],status_line,copy=False) + res=camera_utils.insert_status_line(res,status_line,sl,copy=False) return res def _decimate_full_with_status_line(self, frames, dec, status_line): res=filters.decimate_full(frames,dec) if dec!="skip" and status_line is not None: - sl=framestream.extract_status_line(frames[0],status_line,copy=False) - res=framestream.insert_status_line(res,status_line,sl,copy=False) + sl=camera_utils.extract_status_line(frames[0],status_line,copy=False) + res=camera_utils.insert_status_line(res,status_line,sl,copy=False) return res def _update_buffer(self, frames, status_line): - res_dtype=frames.dtype if self.result_type is None else self.result_type - frames=self._bin_spatial(frames,status_line) - if self.time_bin>1: - if self.acc_frame is not None and frames.shape[1:]!=self.acc_frame.shape: + par=self.v["params"] + if frames.ndim==2: + frames=frames[None] + dtype=frames.dtype if par["dtype"] is None else par["dtype"] + frames=self._bin_spatial(frames,par["spat/bin"],par["spat/mode"],status_line) + time_bin,time_bin_mode=par["time/bin"],par["time/mode"] + if time_bin>1: + if self.acc_frame is not None and frames.shape[-2:]!=self.acc_frame.shape: self._clear_buffer() binned_frames=[] - time_dec=self.time_bin_mode if self.time_bin_mode!="mean" else "sum" - if time_dec=="sum": + time_dec_mode=time_bin_mode if time_bin_mode!="mean" else "sum" + if time_dec_mode=="sum": frames=frames.astype("float") - if self.acc_frame is not None and self.acc_frame_num+len(frames)>=self.time_bin: # complete current chunk - chunk=frames[:self.time_bin-self.acc_frame_num] - frames=frames[self.time_bin-self.acc_frame_num:] - chunk=filters.decimate_full(chunk,time_dec) if len(chunk)>1 else chunk[0] + if self.acc_frame is not None and self.acc_frame_num+len(frames)>=time_bin: # complete current chunk + chunk=frames[:time_bin-self.acc_frame_num] + frames=frames[time_bin-self.acc_frame_num:] + chunk=filters.decimate_full(chunk,time_dec_mode) if len(chunk)>1 else chunk[0] if self.acc_frame is not None: - chunk=self._decimate_full_with_status_line([self.acc_frame,chunk],time_dec,status_line) + chunk=self._decimate_full_with_status_line([self.acc_frame,chunk],time_dec_mode,status_line) binned_frames.append(chunk) - self._clear_buffer() + self._clear_buffer(clear_info=False) if len(frames): - binned_frames+=list(self._decimate_with_status_line(frames,time_dec,status_line)) # decimate all complete chunks - frames_left=len(frames)%self.time_bin + binned_frames+=list(self._decimate_with_status_line(frames,time_bin,time_dec_mode,status_line)) # decimate all complete chunks + frames_left=len(frames)%time_bin if frames_left: # update accumulator chunk=frames[-frames_left:] - chunk=self._decimate_full_with_status_line(chunk,time_dec,status_line) if len(chunk)>1 else chunk[0] + chunk=self._decimate_full_with_status_line(chunk,time_dec_mode,status_line) if len(chunk)>1 else chunk[0] if self.acc_frame is not None: - chunk=self._decimate_full_with_status_line([self.acc_frame,chunk],time_dec,status_line) + chunk=self._decimate_full_with_status_line([self.acc_frame,chunk],time_dec_mode,status_line) self.acc_frame=chunk self.acc_frame_num+=frames_left frames=np.asarray(binned_frames) - if self.time_bin_mode=="mean" and binned_frames: + if time_bin_mode=="mean" and binned_frames: if status_line is None: - frames/=self.time_bin + frames/=time_bin else: - sl=framestream.extract_status_line(frames,status_line,copy=True) - frames/=self.time_bin - frames=framestream.insert_status_line(frames,status_line,sl,copy=False) - frames=frames.astype(res_dtype) + sl=camera_utils.extract_status_line(frames,status_line,copy=True) + frames/=time_bin + frames=camera_utils.insert_status_line(frames,status_line,sl,copy=False) + frames=frames.astype(dtype) return frames - def process_multicast(self, src, tag, msg): - """Process frame multicast from the camera""" + def process_input_frames(self, src, tag, msg): # direct subscription + command for no-overhead forwarding? + """Process multicast message with input frames""" if not self.v["enabled"]: self.send_multicast(dst="any",tag=self.tag_out,value=msg) return processed=[] - if msg.first_frame_index()==0: + if self.cnt.receive_message(msg): self._clear_buffer() - for idx,chunk in zip(msg.indices,msg.frames): + frame_info_present=msg.frame_info is not None + for i,chunk in enumerate(msg.frames): + if frame_info_present and self.last_frame_info is None: + self.last_frame_info=msg.frame_info[i] frames_in_acc=self.acc_frame_num - proc_chunk=self._update_buffer(chunk,msg.status_line) + proc_chunk=self._update_buffer(chunk,msg.metainfo.get("status_line")) if len(proc_chunk): - processed.append((idx-frames_in_acc,proc_chunk)) # actual time bin chunk started `frames_in_acc` before + if not msg.chunks: + proc_chunk=proc_chunk[0] + index=msg.indices[i]-frames_in_acc + processed.append((proc_chunk,index,self.last_frame_info)) # actual time bin chunk started `frames_in_acc` before + if frame_info_present: + self.last_frame_info=msg.frame_info[i] if self.acc_frame_num else None if processed: - indices,frames=list(zip(*processed)) - msg=msg.copy(frames=frames,indices=indices,source="preprocessor",step=self.time_bin) + frames,indices,frame_info=list(zip(*processed)) + if msg.frame_info is None: + frame_info=None + msg=msg.copy(frames=frames,indices=indices,frame_info=frame_info,source=self.name,step=self.v["params/time/bin"]) self.send_multicast(dst="any",tag=self.tag_out,value=msg) @@ -166,46 +173,51 @@ def process_multicast(self, src, tag, msg): class FrameSlowdownThread(controller.QTaskThread): """ - Frame slowdown thread: receives frames and re-emit them with throttling FPS. + Frame slowdown thread: receives frames and re-emits them with reduced FPS. Setup args: - ``src``: name of the source thread (usually, a camera) - ``tag_in``: receiving multicast tag (for the source multicast) - - ``tag_out``: emitting show multicast tag (for the multicast emitted by the processor) + - ``tag_out``: emitting multicast tag (for the multicast emitted by the processor); by default, same as ``tag_in`` Multicasts: - - ````: emitted with slowed frames + - ````: emitted with slowed frames; emitted with the maximal period controlled by the :meth:`set_output_period`, + or on every input message if ``output_period`` is ``None`` Variables: + - ``enabled``: indicate whether the slowdown is on + - ``output_period``: period for outputting the slowed frame; if ``None``, output on every input message - ``buffer/filled``: the number of frames in the buffer - ``buffer/used``: the number of frames in the buffer which have already been shown - ``buffer/empty``: indicates whether the buffer has already been completely filled and used (in this case further frames are not shown, and the processor must be stopped) - - ``enabled``: indicate whether the slowdown is on - ``fps/in``: FPS of the incoming stream - ``fps/out``: FPS of the outgoing stream Commands: - - ``enable_slowdown``: enable or disable slowdown mode + - ``enable``: enable or disable slowdown mode - ``setup_slowdown``: setup slowdown parameters + - ``set_output_period``: set the period of output frames generation """ def setup_task(self, src, tag_in, tag_out=None): - self.subscribe_commsync(self.process_multicast,srcs=src,tags=tag_in,dsts="any",limit_queue=1) + self.subscribe_commsync(self.process_input_frames,srcs=src,tags=tag_in,dsts="any",limit_queue=10) self.tag_out=tag_out or tag_in self.frames_buffer=[] self.buffer_size=1 + self.v["enabled"]=False + self.v["output_period"]=None self.v["buffer/filled"]=0 self.v["buffer/used"]=0 self.v["buffer/empty"]=False - self.v["enabled"]=False self._last_emitted_time=None self._in_fps_calc=[None,0] self._out_fps_calc=[None,0] self.fps_period=1. self.v["fps/in"]=0 self.v["fps/out"]=0 - self.add_command("enable_slowdown") + self.add_command("enable") self.add_command("setup_slowdown") + self.add_job("output_frame",self.output_frame,1.) def _reset(self): self.frames_buffer=[] @@ -228,7 +240,7 @@ def _reset_fps(self, fps_calc, key): fps_calc[:]=[None,0] self.v[key]=0 - def enable_slowdown(self, enabled=True): + def enable(self, enabled=True): """Enable or disable the slowdown""" if enabled!=self.v["enabled"]: self._reset() @@ -245,18 +257,8 @@ def setup_slowdown(self, target_fps, buffer_size): self._reset() self.buffer_size=buffer_size self.target_fps=target_fps - - def process_multicast(self, src, tag, msg): - """Process frame multicast from the camera""" - self._update_fps(self._in_fps_calc,"fps/in",msg.nframes()) - if not self.v["enabled"]: - self.send_multicast(dst="any",tag=self.tag_out,value=msg) - return - if self.v["buffer/filled"]self.buffer_size-self.v["buffer/filled"]: - self.frames_buffer[-1].cut_to_size(self.buffer_size-self.v["buffer/filled"]) - self.v["buffer/filled"]+=self.frames_buffer[-1].nframes() + + def _emit_frames(self): t=time.time() if self._last_emitted_time is None: self._last_emitted_time=t @@ -267,7 +269,7 @@ def process_multicast(self, src, tag, msg): if self.frames_buffer[0].nframes()>nframes: out_msg=self.frames_buffer[0].copy() out_msg.cut_to_size(nframes) - self.frames_buffer[0].cut_to_size(self.frames_buffer[0].nframes()-nframes,reversed=True) + self.frames_buffer[0].cut_to_size(self.frames_buffer[0].nframes()-nframes,from_end=True) else: out_msg=self.frames_buffer.pop(0) nframes_out=out_msg.nframes() @@ -279,191 +281,288 @@ def process_multicast(self, src, tag, msg): self.v["buffer/empty"]=True self._last_emitted_time=t + def set_output_period(self, period=None): + """ + Set the period of the display frame generation. + + If ``None``, output on every input frame message. + """ + self.v["output_period"]=period + self.change_job_period("output_frame",1. if period is None else period) + def output_frame(self): + if self.v["output_period"] is not None: + self._emit_frames() + def process_input_frames(self, src, tag, msg): + """Process multicast message with input frames""" + self._update_fps(self._in_fps_calc,"fps/in",msg.nframes()) + msg=msg.copy(mid=None) + if not self.v["enabled"]: + self.send_multicast(dst="any",tag=self.tag_out,value=msg) + return + if self.v["buffer/filled"]self.buffer_size-self.v["buffer/filled"]: + self.frames_buffer[-1].cut_to_size(self.buffer_size-self.v["buffer/filled"]) + self.v["buffer/filled"]+=self.frames_buffer[-1].nframes() + if self.v["output_period"] is None: + self._emit_frames() -# ##### Camera channel calculation ##### - -# class CamChannelAccumulator(controller.QTaskThread): -# """ -# Camera channel accumulator. - -# Receives frames from a source, calculate time series and accumulates in a table together with the frame indices. - -# Setup args: -# src: name of the source thread (usually, a camera) -# tag: receiving multicast tag (for the source multicast) -# settings: dictionary with the accumulator settings - -# Commands: -# enable: enable or disable accumulation -# add_source: add a frame source -# select_source: select one of frame sources for calculation -# setup_processing: setup processing parameters -# setup_roi: setup averaging ROI -# reset_roi: reset averaging ROI to the whole image -# get_data: get the accumulated data as a dictionary of 1D numpy arrays -# reset: clear the accumulation table -# """ -# def setup_task(self, settings=None): -# self.settings=settings or {} -# self.frame_channels=["idx","mean"] -# self.memsize=self.settings.get("memsize",100000) -# self.table_accum=helpers.TableAccumulator(channels=self.frame_channels,memsize=self.memsize) -# self.enabled=False -# self.current_source=None -# self.sources={} -# self.skip_count=1 -# self._skip_accum=0 -# self.reset_time=time.time() -# self.roi=None -# self.roi_enabled=False -# self._last_roi=None -# self.add_command("enable") -# self.add_command("add_source") -# self.add_command("select_source") -# self.add_command("setup_processing") -# self.add_command("setup_roi") -# self.add_command("reset_roi") -# self.add_command("get_data") -# self.add_command("reset") - -# def enable(self, enabled=True): -# """Enable or disable trace accumulation""" -# self.enabled=enabled -# self._skip_accum=0 -# def setup_processing(self, skip_count=1): -# """ -# Setup processing parameters. - -# Args: -# skip_count: the accumulated values are calculated for every `skip_count` frame. -# """ -# self.skip_count=skip_count -# self._skip_accum=0 - -# TSource=collections.namedtuple("TSource",["src","tag","kind","sync"]) -# def add_source(self, name, src, tag, sync=False, kind="raw"): -# """ -# Add a frame source. - -# Args: -# name: source name (used for switching source) -# src: frame multicast source -# tag: frame multicast tag -# sync: if ``True``, the subscription is synchronized to the source (i.e, if processing takes too much time, the frame source waits); -# otherwise, the subscription is not synchronized (if processing takes too much time, frames are skipped) -# kind: source kind; can be ``"raw"`` (plotted vs. frame index, reset on source restart), ``"show"`` (plotted vs. time, only reset explicitly), -# or ``"points"`` (source sends directly a dictionary of trace values rather than frames) -# """ -# self.sources[name]=self.TSource(src,tag,kind,sync) -# callback=lambda s,t,v: self.process_source(s,t,v,source=name) -# if sync: -# self.subscribe_commsync(callback,srcs=src,dsts="any",tags=tag,limit_queue=2,on_full_queue="wait") -# else: -# self.subscribe_commsync(callback,srcs=src,dsts="any",tags=tag,limit_queue=10) -# def select_source(self, name): -# """Select a source with a given name""" -# if self.current_source!=name: -# self.reset() -# self.current_source=name -# if self.sources[name].kind in {"raw","show"}: -# self.table_accum.change_channels(self.frame_channels) -# else: -# self.table_accum.change_channels([]) +class BackgroundSubtractionThread(controller.QTaskThread): + """ + Frame background subtraction thread: receives frame streams and re-emits individual frames after background subtraction. + + Not all frames are re-emitted, so the stream is generally only useful for display. + + Setup args: + - ``src``: name of the source thread (usually, a camera) + - ``tag_in``: receiving multicast tag (for the source multicast) + - ``tag_out``: emitting multicast tag (for the multicast emitted by the processor) for frames intended to be shown; + by default, ``tag_in+"/show"`` + + Multicasts: + - ````: emitted with background-subtracted frames; emitted with the maximal period controlled by the :meth:`set_output_period`, + or on every input message if ``output_period`` is ``None`` + + Variables: + - ``enabled``: whether processing is enabled (if disabled, the display frame is emitted without any background subtraction) + - ``overridden``: whether the thread is overridden (if it is, the display frame message is not emitted at all) + - ``method``: subtraction method: either ``'snapshot"``, or ``"running"`` + - ``output_period``: maximal period for outputting the processed frame; if ``None``, output on every input message + - ``snapshot/parameters``: parameters of the snapshot background subtraction: ``"count"`` for number of frames to combine for the background, + ``"mode"`` for the combination mode (``"min"``, ``"mean"``, etc.), ``"dtype"`` for the final dtype, ``"offset"`` to enable or disable background offset + - ``snapshot/grabbed``: number of grabbed frames in the snapshot background buffer + - ``snapshot/background``: status of the snapshot background: ``"frame"`` for the final frame and ``"offset"`` for the final offset, + ``"buffer"`` for the whole background buffer, ``"state"`` for the buffer state, and ``"saving"`` for the saving method; + state can be ``"none"`` (none acquired), ``"acquiring"`` (accumulation in progress), ``"valid"`` (acquired and valid), or ``"wrong_size"`` (size mismatch); + saving method can be ``"none"`` (don't save background), ``"only_bg"`` (only save background frame), or ``"all"`` (save background + all comprising frames). + - ``running/parameters``: parameters of the snapshot background subtraction: ``"count"`` for number of frames to combine for the background, + ``"mode"`` for the combination mode (``"min"``, ``"mean"``, etc.), ``"dtype"`` for the final dtype, ``"offset"`` to enable or disable background offset + - ``running/grabbed``: number of grabbed frames in the running background buffer + - ``running/background``: status of the running background: ``"frame"`` for the final frame and ``"offset"`` for the final offset + + Commands: + - ``setup_snapshot_subtraction``: setup snapshot background calculation parameters + - ``grab_snapshot_background``: initiate snapshot background grab + - ``setup_snapshot_saving``: setup snapshot background saving method + - ``setup_running_subtraction``: setup parameters for the running background subtraction + - ``setup_subtraction_method``: set the subtraction method (running or snapshot) and whether the subtraction is enabled at all + - ``set_output_period``: set the period of output frames generation + """ + TStoredFrame=collections.namedtuple("TStoredFrame",["frame","index","info","status_line"]) + def setup_task(self, src, tag_in, tag_out=None): + self.frames_src=stream_manager.StreamSource(builder=stream_message.FramesMessage,use_mid=False) + self.subscribe_commsync(self.process_input_frames,srcs=src,tags=tag_in,dsts="any",limit_queue=10,on_full_queue="skip_oldest") + self.tag_out=tag_out or tag_in+"/show" + self.v["enabled"]=False + self.v["overridden"]=False + self.v["method"]="snapshot" + self.last_frame=self.TStoredFrame(None,None,None,None) + self._new_show_frame=False + self.snapshot_buffer=[] + self.v["snapshot/parameters"]={"count":1,"mode":"mean","dtype":None,"offset":False} + self.v["snapshot/grabbed"]=0 + self.v["snapshot/background/frame"]=None + self.v["snapshot/background/offset"]=None + self.v["snapshot/background/buffer"]=None + self.v["snapshot/background/state"]="none" + self.v["snapshot/background/saving"]="none" + self.running_buffer=[] + self.v["running/parameters"]={"count":1,"mode":"mean","dtype":None,"offset":False} + self.v["running/grabbed"]=0 + self.v["running/background/frame"]=None + self.v["running/background/offset"]=None + self.status_line_policy="duplicate" + self.add_command("setup_snapshot_subtraction") + self.add_command("grab_snapshot_background") + self.add_command("setup_snapshot_saving") + self.add_command("setup_running_subtraction") + self.add_command("setup_subtraction_method") + self.add_command("set_output_period") + self.v["output_period"]=None + self.add_job("output_frame",self.output_frame,1.) + + def _calculate_background(self, buffer, mode, dtype, use_offset): + if dtype is None: + dtype="i4" if buffer[0].dtype.kind in "ui" else "f" + background=filters.decimate_full(buffer,mode,axis=0) + background=background.astype(dtype) + status_line=self.last_frame.status_line + if status_line is not None: + background=camera_utils.remove_status_line(background,status_line,"zero",copy=False) + if use_offset: + offset=np.median(background).astype(dtype) + if status_line is not None: + background=camera_utils.remove_status_line(background,status_line,"value",value=offset,copy=False) + else: + offset=0 + return background,offset + def _calculate_snapshot_background(self, buffer): + par=self.v["snapshot/parameters"] + background,offset=self._calculate_background(buffer,par["mode"],par["dtype"],par["offset"]) + self.v["snapshot/background/frame"]=background + self.v["snapshot/background/offset"]=offset + if self.v["snapshot/background/state"]=="acquiring": + self.v["snapshot/background/state"]="valid" + + def _update_snapshot_buffer(self, msg): + """Update snapshot background buffer and calculate the background if the buffer is filled""" + if self.v["snapshot/background/state"]=="acquiring": + par=self.v["snapshot/parameters"] + count=par["count"] + if len(self.snapshot_buffer)=n: + buffer=self.v["snapshot/background/buffer"][-n:] + else: + self.grab_snapshot_background() + if self.v["snapshot/background/frame"] is not None: + self._calculate_snapshot_background(self.v["snapshot/background/buffer"]) + def grab_snapshot_background(self): + """Initiate snapshot background acquisition""" + self.v["snapshot/grabbed"]=0 + self.v["snapshot/background/frame"]=None + self.v["snapshot/background/state"]="acquiring" + def setup_snapshot_saving(self, mode): + """ + Enable shnapshot background subtraction and saving + + `mode` can be ``"none"`` (don't save background), ``"only_bg"`` (only save background frame), or ``"all"`` (save background + all comprising frames). + """ + self.v["snapshot/background/saving"]=mode + def get_background_to_save(self): + """Get the background to save, taking saving parameters into account""" + enabled=self._is_enabled() and (self.v["method"]=="snapshot") + background=self.v["snapshot/background"] + snapshot_background=background["frame"] + background_saving=background["saving"] if enabled else "none" + if background_saving=="none" or snapshot_background is None: + return None + else: + return [snapshot_background]+(background["buffer"] if background_saving=="all" else []) + + def _update_running_buffer(self, msg): + count=self.v["running/parameters/count"] + updated_frames=msg.get_frames_stack(count+1,reverse=True) # need to take one extra frame, since the last frame in the buffer shouldn't be subtracted + if self.running_buffer and updated_frames and self.running_buffer[0].shape!=updated_frames[0].shape: + self.running_buffer=[] + if len(updated_frames)==count+1: + self.running_buffer=updated_frames + else: + self.running_buffer=updated_frames+self.running_buffer[:count+1-len(updated_frames)] + self.v["running/grabbed"]=max(len(self.running_buffer)-1,0) + def setup_running_subtraction(self, n=1, mode="mean", dtype=None, offset=False): + """ + Setup running background parameters. + + Args: + n: number of frames in the buffer + mode: calculation mode; can be ``"mean"``, ``"median"``, ``"min"``, or ``"max"`` + dtype: numpy dtype of the final background and the output frames; ``None`` means ``int32`` for integer input frames and ``float`` otherwise + offset: if ``True``, subtract the median background value from it, so that the background subtracted frames stay roughly in the same + range as the original; otherwise, keep it the same, which shifts the background subtracted frames range towards zero. + """ + self.v["running/parameters"]={"count":n,"mode":mode,"dtype":dtype,"offset":offset} -# def setup_roi(self, center=None, size=None, enabled=True): -# """ -# Setup averaging ROI parameters. - -# `center` and `size` specify ROI parameters (if ``None``, keep current values). -# `enabled` specifies whether ROI is applied for averaging (``enabled==True``), or the whole frame is averaged (``enabled==False``) -# Return the new ROI (or ``None`` if no ROI can be specified). -# """ -# if center is not None or size is not None: -# if center is None and self.roi is not None: -# center=self.roi.center() -# if size is None and self.roi is not None: -# size=self.roi.size() -# if center is not None and size is not None: -# self.roi=image.ROI.from_centersize(center,size) -# self.roi_enabled=enabled -# return self.roi -# def reset_roi(self): -# """ -# Reset ROI to the whole image - -# Return the new ROI (or ``None`` if no frames have been acquired, so no ROI can specified) -# """ -# self.roi=self._last_roi -# return self.roi -# def process_frame(self, value, kind): -# """Process raw frames data""" -# if not value.has_frames(): -# return -# if value.first_frame_index()==0 and kind=="raw": -# self.reset() -# for i,f in zip(value.indices,value.frames): -# if len(f)+self._skip_accum>=self.skip_count: -# start=self.skip_count-self._skip_accum-1 -# calc_frames=f[start::self.skip_count] -# calc_roi=self.roi if (self.roi and self.roi_enabled) else image.ROI(0,calc_frames.shape[1],0,calc_frames.shape[2]) -# sums,area=image.get_region_sum(calc_frames,calc_roi.center(),calc_roi.size()) -# if value.status_line: -# sl_roi=get_status_line_roi(calc_frames,value.status_line) -# sl_roi=image.ROI.intersect(sl_roi,calc_roi) -# if sl_roi: -# sl_sums,sl_area=image.get_region_sum(calc_frames,sl_roi.center(),sl_roi.size()) -# sums-=sl_sums -# area-=sl_area -# means=sums/area if area>0 else sums -# if kind=="raw": -# x_axis=np.arange(i+start,i+len(f)*value.step,value.step*self.skip_count) -# else: -# x_axis=[time.time()-self.reset_time]*len(means) -# self.table_accum.add_data([x_axis,means]) -# self._skip_accum=(self._skip_accum+len(f))%self.skip_count -# shape=value.first_frame().shape -# self._last_roi=image.ROI(0,shape[0],0,shape[1]) -# def process_points(self, value): -# """Process trace dictionary data""" -# table={} -# min_len=None -# for k in value: -# v=value[k] -# if not isinstance(v,(list,np.ndarray)): -# v=[v] -# table[k]=v -# min_len=len(v) if min_len is None else min(len(v),min_len) -# if min_len>0: -# for k in table: -# table[k]=table[k][:min_len] -# if "idx" not in table: -# table["idx"]=[time.time()-self.reset_time]*min_len -# if not self.table_accum.channels: -# self.table_accum.change_channels(list(table.keys())) -# self.table_accum.add_data(table) -# def process_source(self, src, tag, value, source): -# """Receive the source data (frames or traces), process and add to the accumulator table""" -# if not self.enabled or source!=self.current_source: -# return -# kind=self.sources[source].kind -# if kind in {"raw","show"}: -# self.process_frame(value,kind) -# elif kind=="points": -# self.process_points(value) -# def get_data(self, maxlen=None): -# """ -# Get the accumulated data as a dictionary of 1D numpy arrays. + def setup_subtraction_method(self, method=None, enabled=None, overridden=None): + """ + Set the subtraction method, whether the background subtraction is enabled, and whether it is overridden. + + Values set to ``None`` are unchanged. + """ + if method is not None: + if method not in {"snapshot","running"}: + raise ValueError("unrecognized subtraction method: {}".format(method)) + self.v["method"]=method + if enabled is not None: + self.v["enabled"]=enabled + if overridden is not None: + self.v["overridden"]=overridden + def _is_enabled(self): + return self.v["enabled"] and not self.v["overridden"] + + def set_output_period(self, period=None): + """ + Set the period of the display frame generation. -# If `maxlen` is specified, get at most `maxlen` datapoints from the end. -# """ -# return self.table_accum.get_data_dict(maxlen=maxlen) -# def reset(self): -# """Clear all data in the table""" -# self.table_accum.reset_data() -# self._skip_accum=0 -# self.reset_time=time.time() + If ``None``, output on every input frame message. + """ + self.v["output_period"]=period + self.change_job_period("output_frame",1. if period is None else period) + def process_frame(self, frame, status_line=None): + """ + Process a frame (or stack of frames). + + `status_line` is a status line descriptor (defined in :class:`FramesMessage`) + """ + method=self.v["method"] + enabled=self._is_enabled() + background,offset=None,None + if self.v["snapshot/background/frame"] is not None: + if self.v["snapshot/background/frame"].shape!=frame.shape: + self.v["snapshot/background/state"]="wrong_size" + else: + self.v["snapshot/background/state"]="valid" + if enabled and method=="snapshot": + background,offset=self.v["snapshot/background/frame"],self.v["snapshot/background/offset"] + if enabled and method=="running": + par=self.v["running/parameters"] + if len(self.running_buffer)==par["count"]+1: + background,offset=self._calculate_background(self.running_buffer[1:],par["mode"],par["dtype"],par["offset"]) + if background.shape!=frame.shape: + background,offset=None,None + self.v["running/background/frame"]=background + self.v["running/background/offset"]=offset + if background is not None: + frame=frame-(background-offset) + if status_line is not None: + frame=camera_utils.remove_status_line(frame,status_line,policy=self.status_line_policy) + return frame + + def output_frame(self): + """Process and emit new frame""" + if self._new_show_frame and not self.v["overridden"]: + show_frame=self.process_frame(self.last_frame.frame,status_line=self.last_frame.status_line) + self.send_multicast(dst="any",tag=self.tag_out,value=self.frames_src.build_message(show_frame,self.last_frame.index,[self.last_frame.info],source=self.name)) + self._new_show_frame=False + + def process_input_frames(self, src, tag, msg): + """Process multicast message with input frames""" + self.frames_src.receive_message(msg) + self.last_frame=self.TStoredFrame(msg.last_frame(),msg.last_frame_index(),msg.last_frame_info(),msg.metainfo.get("status_line")) + self._update_running_buffer(msg) + self._update_snapshot_buffer(msg) + if self.v["output_period"] is None: + self._new_show_frame=True + self.output_frame() + else: + self._new_show_frame=True \ No newline at end of file diff --git a/pylablib/thread/stream/table_accum.py b/pylablib/thread/stream/table_accum.py index e29067e..56aa381 100644 --- a/pylablib/thread/stream/table_accum.py +++ b/pylablib/thread/stream/table_accum.py @@ -1,4 +1,5 @@ from ...core.thread import controller +from . import stream_message, stream_manager import threading @@ -60,6 +61,8 @@ def add_data(self, data): Data can either be a list of columns, or a dictionary ``{name: [data]}`` with named columns. """ + if isinstance(data,stream_message.DataBlockMessage): + data=data.data if isinstance(data,dict): table_data=[] for ch in self.channels: @@ -141,9 +144,10 @@ def setup_task(self, channels, src, tag, reset_tag=None, memsize=10**6): self.channels=channels self.fmt=[None]*len(channels) self.table_accum=TableAccumulator(channels=channels,memsize=memsize) - self.subscribe_sync(self._accum_data,srcs=src,tags=tag,dsts="any",limit_queue=100) + self.subscribe_direct(self._accum_data,srcs=src,tags=tag,dsts="any") if reset_tag is not None: self.subscribe_sync(self._on_source_reset,srcs=src,tags=reset_tag,dsts="any") + self.cnt=stream_manager.StreamIDCounter() self.data_lock=threading.Lock() self.add_direct_call_command("get_data") self.add_direct_call_command("reset",error_on_async=False) @@ -158,6 +162,8 @@ def _on_source_reset(self, src, tag, value): def _accum_data(self, src, tag, value): with self.data_lock: + if hasattr(value,"get_ids") and self.cnt.receive_message(value): + self.table_accum.reset_data() value=self.preprocess_data(value) self.table_accum.add_data(value) From ba8b6130fb58ffff198f28bc849793d349003389 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Mon, 21 Jun 2021 20:57:43 +0200 Subject: [PATCH 18/31] Added tox, updated device tests --- tests/conftest.py | 1 + tests/devices/conftest.py | 10 +++++-- tests/devices/test_basic_camera.py | 6 +++- tests/devices/test_cameras.py | 3 +- tests/devices/test_lasers.py | 25 +++++++++++++++++ tests/devices/test_meters.py | 12 +++++++- tests/devices/test_misc.py | 31 +++++++++++++++++++- tests/devices/test_stages.py | 6 +++- tox.ini | 45 ++++++++++++++++++++++++++++++ 9 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 tests/devices/test_lasers.py create mode 100644 tox.ini diff --git a/tests/conftest.py b/tests/conftest.py index f318796..17db355 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ def pytest_addoption(parser): parser.addoption("--devices",action="append",default=[],help="list of device names to test",) parser.addoption("--devlists",action="append",default=[],help="list of device list files",) parser.addoption("--maxchange",default=2,help="maximal allowed change to the device state while testing",) + parser.addoption("--dev-no-conn-fail",action="store_true",help="xfail instead of fail if could not connect to the device",) parser.addoption("--stressfactor",default=1,help="stress multiplication factor",) parser.addoption("--full",action="store_true",help="run maximally complete tests",) diff --git a/tests/devices/conftest.py b/tests/devices/conftest.py index 72554a7..99eabfa 100644 --- a/tests/devices/conftest.py +++ b/tests/devices/conftest.py @@ -4,6 +4,7 @@ import os import re +import time def parse_dev(dev): m=re.match(r"(\w+)(\(.*\))?$",dev) @@ -64,9 +65,13 @@ def device(request, library_parameters): except Exception: if i==open_retry: raise + time.sleep(1.) except Exception: - devargstr=", ".join([str(a) for a in devargs]) - pytest.xfail("couldn't connect to the device {}({})".format(devcls.__name__,devargstr)) + if request.config.getoption("dev_no_conn_fail"): + devargstr=", ".join([str(a) for a in devargs]) + pytest.xfail("couldn't connect to the device {}({})".format(devcls.__name__,devargstr)) + else: + raise opened=True for _ in range(open_rep): dev.close() @@ -78,6 +83,7 @@ def device(request, library_parameters): except Exception: if i==open_retry: raise + time.sleep(1.) opened=True yield dev finally: diff --git a/tests/devices/test_basic_camera.py b/tests/devices/test_basic_camera.py index 837985a..5b1fb76 100644 --- a/tests/devices/test_basic_camera.py +++ b/tests/devices/test_basic_camera.py @@ -20,10 +20,11 @@ class CameraTester(DeviceTester): In addition to the basic device tests, also performs basic camera testing. """ grab_size=10 + default_roi=() @pytest.mark.devchange(2) def test_snap_grab(self, device): """Test snapping and grabbing functions""" - device.set_roi() + device.set_roi(*self.default_roi) img=device.snap() assert isinstance(img,np.ndarray) assert img.ndim==2 @@ -40,11 +41,13 @@ def test_multisnap(self, device, stress_factor): device.snap() def test_multigrab(self, device, stress_factor): """Test snapping and grabbing functions""" + device.set_roi(*self.default_roi) for _ in range(stress_factor): device.grab(self.grab_size) @pytest.mark.devchange(2) def test_get_full_info_acq(self, device): """Test info getting errors during acquisition""" + device.set_roi(*self.default_roi) device.start_acquisition() try: info=device.get_full_info(self.include) @@ -73,6 +76,7 @@ def test_get_full_info_acq(self, device): @pytest.mark.devchange(1) def test_frame_info(self, device): """Test frame info consistency""" + device.set_roi(*self.default_roi) frames,infos=device.grab(self.grab_size,return_info=True) assert len(frames)==self.grab_size assert len(frames)==len(infos) diff --git a/tests/devices/test_cameras.py b/tests/devices/test_cameras.py index ba4b314..1eab2af 100644 --- a/tests/devices/test_cameras.py +++ b/tests/devices/test_cameras.py @@ -133,4 +133,5 @@ class TestUC480(ROICameraTester): """Testing class for UC480 camera interface""" devname="uc480" devcls=uc480.UC480Camera - rois=gen_rois(128,((1,1),(1,2),(2,2),((0,0),False),((3,3),False),((10,10),False),((100,100),False))) \ No newline at end of file + rois=gen_rois(128,((1,1),(1,2),(2,2),((0,0),False),((3,3),False),((10,10),False),((100,100),False))) + default_roi=(0,512,0,512) \ No newline at end of file diff --git a/tests/devices/test_lasers.py b/tests/devices/test_lasers.py new file mode 100644 index 0000000..bb55e90 --- /dev/null +++ b/tests/devices/test_lasers.py @@ -0,0 +1,25 @@ +from .test_basic import DeviceTester + +from pylablib.devices import LighthousePhotonics +from pylablib.devices import LaserQuantum +from pylablib.devices import M2 + + +class TestLPSproutG(DeviceTester): + """Testing class for Lighthouse Photonics SproutG laser""" + devname="lp_sprout" + devcls=LighthousePhotonics.SproutG + open_retry=3 + + +class TestLQFinesse(DeviceTester): + """Testing class for LaserQuantum Finesse laser""" + devname="lq_finesse" + devcls=LaserQuantum.Finesse + open_retry=3 + + +class TestM2Solstis(DeviceTester): + """Testing class for M2 Solstis laser""" + devname="m2_solstis" + devcls=M2.Solstis \ No newline at end of file diff --git a/tests/devices/test_meters.py b/tests/devices/test_meters.py index 15a70c2..fb0b7c0 100644 --- a/tests/devices/test_meters.py +++ b/tests/devices/test_meters.py @@ -3,22 +3,32 @@ from pylablib.devices import Ophir from pylablib.devices import Lakeshore from pylablib.devices import Pfeiffer +from pylablib.devices import HighFinesse class TestOphirVega(DeviceTester): """Testing class for Ophir Vega power meter""" devname="ophir_vega" devcls=Ophir.VegaPowerMeter + open_retry=3 class TestLakeshore218(DeviceTester): """Testing class for Lakeshore 218 temperature sensor""" devname="lakeshore218" devcls=Lakeshore.Lakeshore218 + open_retry=3 class TestPfeifferTPG260(DeviceTester): """Testing class for TPG260 pressure sensor""" devname="pfeiffer_tpg260" devcls=Pfeiffer.TPG260 - get_set_all_exclude=["calibration_factor"] \ No newline at end of file + open_retry=3 + get_set_all_exclude=["calibration_factor"] + + +class TestWLMWavemeter(DeviceTester): + """Testing class for High Finesse WLM wavemeter""" + devname="high_finesse_wlm" + devcls=HighFinesse.WLM \ No newline at end of file diff --git a/tests/devices/test_misc.py b/tests/devices/test_misc.py index 4fb0bab..ecce4a0 100644 --- a/tests/devices/test_misc.py +++ b/tests/devices/test_misc.py @@ -1,4 +1,5 @@ from pylablib.devices import Thorlabs, OZOptics +from pylablib.devices import NI from .test_basic import DeviceTester @@ -22,4 +23,32 @@ def test_motion(self, device): class TestOZOpticsEPC04(DeviceTester): """Testing class for OZOptics EPC04 polarization controller interface""" devname="ozoptics_epc04" - devcls=OZOptics.EPC04 \ No newline at end of file + devcls=OZOptics.EPC04 + + +class TestNIDAQ(DeviceTester): + """Testing class for NI DAQ interface""" + devname="nidaq" + devcls=NI.NIDAQ + + ai_rate=1000 + samples=1000 + @pytest.fixture(scope="class") + def device(self, device): + device.add_voltage_input("in0","ai0") + device.add_voltage_input("in1","ai1") + device.add_digital_input("din1","port0/line1") + device.add_counter_input("c0","ctr0","pfi0") + device.setup_clock(self.ai_rate) + return device + + def test_open_close(self, device): # otherwise it removes preset channels + pass + def test_read(self, device): + """Test samples reading""" + v=device.read(self.samples,timeout=self.samples/self.ai_rate*2+2) + assert v.shape==(self.samples,4) + device.start() + v=device.read(self.samples,timeout=self.samples/self.ai_rate*2+2) + device.stop() + assert v.shape==(self.samples,4) \ No newline at end of file diff --git a/tests/devices/test_stages.py b/tests/devices/test_stages.py index 444acbf..f49709e 100644 --- a/tests/devices/test_stages.py +++ b/tests/devices/test_stages.py @@ -27,6 +27,7 @@ class TestArcusPerformax(DeviceTester): """Testing class for Arcus Performax stage interface""" devname="arcus_performax" devcls=Arcus.Performax4EXStage + open_retry=3 get_set_all_exclude=["analog_input"] @pytest.fixture(scope="class") @@ -38,6 +39,7 @@ class TestSmarActSCUPerformax(DeviceTester): """Testing class for SmarAct SCU stage interface""" devname="smaract_scu3d" devcls=SmarAct.SCU3D + open_retry=3 @pytest.fixture(scope="class") def library_parameters(self): @@ -48,9 +50,11 @@ class TestTrinamicTMCM1110(DeviceTester): """Testing class for Trinamic TMCM1110 stage interface""" devname="trinamic_tmcm1110" devcls=Trinamic.TMCM1110 + open_retry=3 class TestKinesisMotor(DeviceTester): """Testing class for Thorlabs Kinesis stage interface""" devname="thorlabs_kinesis_motor" - devcls=Thorlabs.KinesisMotor \ No newline at end of file + devcls=Thorlabs.KinesisMotor + open_retry=3 \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5f05c88 --- /dev/null +++ b/tox.ini @@ -0,0 +1,45 @@ +# tox (https://tox.readthedocs.io/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = lpy{36,37,38,39}, lpy{36,37,38,39}_32, lpy{37,38}-serial_{2_7,3_0,3_1,3_2,3_3,3_4}, lpy{37,38}-ft232_{5,6,7,8,9,10,11}, lpy{37,38}-visa_{1_6,1_7,1_8,1_9,1_10}, lpy{37,38}-usb_{1_0_0,1_1_0} + +[testenv] +basepython = + lpy36: ../python-3.6.8.amd64/python.exe + lpy37: ../python-3.7.7.amd64/python.exe + lpy38: ../python-3.8.9.amd64/python.exe + lpy39: ../python-3.9.4.amd64/python.exe + lpy36_32: ../python-3.6.8/python.exe + lpy37_32: ../python-3.7.7/python.exe + lpy38_32: ../python-3.8.9/python.exe + lpy39_32: ../python-3.9.4/python.exe +changedir = tests +setenv = + PYTHONPATH = +deps = + pytest + serial_3_4: pyserial<3.5 + serial_3_3: pyserial<3.4 + serial_3_2: pyserial<3.3 + serial_3_1: pyserial<3.2 + serial_3_0: pyserial<3.1 + serial_2_7: pyserial<3.0 + ft232_5: pyft232==0.5 + ft232_6: pyft232==0.6 + ft232_7: pyft232==0.7 + ft232_8: pyft232==0.8 + ft232_9: pyft232==0.9 + ft232_10: pyft232==0.10 + ft232_11: pyft232==0.11 + visa_1_6: pyvisa<1.07 + visa_1_7: pyvisa<1.08 + visa_1_8: pyvisa<1.09 + visa_1_9: pyvisa<1.10 + visa_1_10: pyvisa<1.11 + usb_1_0_0: pyusb==1.0.0 + usb_1_1_0: pyusb==1.1.0 +commands = + pytest {posargs} \ No newline at end of file From d6728fad3afba9aeef5bee63ad3787a706eedced Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Mon, 21 Jun 2021 21:02:25 +0200 Subject: [PATCH 19/31] Fixed some 32-bit device issues --- docs/devices/Arcus_performax.rst | 3 +++ pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py | 14 ++++++++++++-- pylablib/devices/Arcus/performax.py | 1 + pylablib/devices/DCAM/dcamapi4_lib.py | 2 +- pylablib/devices/PhotonFocus/pfcam_lib.py | 2 +- pylablib/devices/Thorlabs/tl_camera_sdk_lib.py | 2 +- pylablib/devices/uc480/uc480_lib.py | 2 +- 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/devices/Arcus_performax.rst b/docs/devices/Arcus_performax.rst index 100f2ca..9842f2e 100644 --- a/docs/devices/Arcus_performax.rst +++ b/docs/devices/Arcus_performax.rst @@ -23,6 +23,9 @@ The controller has several communication modes: USB, R485, and Ethernet. USB mod The controller has only been tested with USB communication. +.. warning:: + There appear to be some issues with Python 3.6 which result in out-of-bounds write, memory corruption, and undefined behavior. Hence, Python 3.7+ is required to work with this device. + Connection ----------------------- diff --git a/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py b/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py index b232f64..b80b480 100644 --- a/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py +++ b/pylablib/devices/Arcus/ArcusPerformaxDriver_lib.py @@ -2,9 +2,11 @@ from ...core.utils import ctypes_wrap from ...core.devio.comm_backend import DeviceError -from .ArcusPerformaxDriver_defs import define_functions +from .ArcusPerformaxDriver_defs import define_functions, AR_HANDLE from ..utils import load_lib +import ctypes + class ArcusError(DeviceError): """Generic Arcus error""" @@ -46,7 +48,10 @@ def initlib(self): argprep={"lpDeviceString":strprep}) # AR_BOOL fnPerformaxComOpen(AR_DWORD dwDeviceNum, ctypes.POINTER(AR_HANDLE) pHandle) - self.fnPerformaxComOpen=wrapper(lib.fnPerformaxComOpen,rvals=["pHandle"]) + # self.fnPerformaxComOpen=wrapper(lib.fnPerformaxComOpen,rvals=["pHandle"]) + lib.fnPerformaxComOpen.argtypes=[ctypes.c_long,ctypes.c_void_p] + lib.fnPerformaxComOpen.errcheck=errchecker + self.fnPerformaxComOpen_lib=lib.fnPerformaxComOpen # AR_BOOL fnPerformaxComClose(AR_HANDLE pHandle) self.fnPerformaxComClose=wrapper(lib.fnPerformaxComClose) @@ -60,6 +65,11 @@ def initlib(self): self._initialized=True + def fnPerformaxComOpen(self, dwDeviceNum): # some problems with argtypes conversion on Python 3.6 + buff=ctypes.create_string_buffer(32) + self.fnPerformaxComOpen_lib(dwDeviceNum,buff) + rtype=ctypes.POINTER(AR_HANDLE) + return ctypes.cast(buff,rtype).contents lib=ArcusPerformaxLib() \ No newline at end of file diff --git a/pylablib/devices/Arcus/performax.py b/pylablib/devices/Arcus/performax.py index b6480c5..0451542 100644 --- a/pylablib/devices/Arcus/performax.py +++ b/pylablib/devices/Arcus/performax.py @@ -55,6 +55,7 @@ def _get_connection_parameters(self): def open(self): """Open the connection to the stage""" self.close() + lib.fnPerformaxComGetNumDevices() # sometimes needed to set up the dll lib.fnPerformaxComSetTimeouts(5000,5000) for _ in range(5): try: diff --git a/pylablib/devices/DCAM/dcamapi4_lib.py b/pylablib/devices/DCAM/dcamapi4_lib.py index 1fd1134..9e36169 100644 --- a/pylablib/devices/DCAM/dcamapi4_lib.py +++ b/pylablib/devices/DCAM/dcamapi4_lib.py @@ -120,7 +120,7 @@ def initlib(self): if self._initialized: return error_message="The library is automatically supplied with Hamamatsu HOKAWO or DCAM-API software\n"+load_lib.par_error_message.format("dcamapi") - self.lib=load_lib.load_lib("dcamapi.dll",locations=("parameter/dcamapi","global"),error_message=error_message,call_conv="stdcall") + self.lib=load_lib.load_lib("dcamapi.dll",locations=("parameter/dcamapi","global"),error_message=error_message,call_conv="cdecl") lib=self.lib define_functions(lib) diff --git a/pylablib/devices/PhotonFocus/pfcam_lib.py b/pylablib/devices/PhotonFocus/pfcam_lib.py index e51e14e..5a04107 100644 --- a/pylablib/devices/PhotonFocus/pfcam_lib.py +++ b/pylablib/devices/PhotonFocus/pfcam_lib.py @@ -110,7 +110,7 @@ def initlib(self): if self._initialized: return error_message="The library is automatically supplied with PhotonFocus PFRemote software\n"+load_lib.par_error_message.format("pfcam") - self.lib=load_lib.load_lib("pfcam.dll",locations=("parameter/pfcam","global"),error_message=error_message) + self.lib=load_lib.load_lib("pfcam.dll",locations=("parameter/pfcam","global"),error_message=error_message,call_conv="cdecl") lib=self.lib define_functions(lib) diff --git a/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py b/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py index 596d2fc..c126691 100644 --- a/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py +++ b/pylablib/devices/Thorlabs/tl_camera_sdk_lib.py @@ -56,7 +56,7 @@ def initlib(self): thorcam_path=load_lib.get_program_files_folder("Thorlabs/Scientific Imaging/ThorCam") error_message="The library is automatically supplied with Thorcam software\n"+load_lib.par_error_message.format("thorlabs_tlcam") depends=["thorlabs_unified_sdk_kernel.dll","thorlabs_unified_sdk_main.dll","thorlabs_tsi_usb_driver.dll","thorlabs_tsi_usb_hotplug_monitor.dll","thorlabs_tsi_cs_camera_device.dll","tsi_sdk.dll","tsi_usb.dll"] - self.lib=load_lib.load_lib("thorlabs_tsi_camera_sdk.dll",locations=("parameter/thorlabs_tlcam",thorcam_path,"global"),depends=depends,error_message=error_message,call_conv="stdcall") + self.lib=load_lib.load_lib("thorlabs_tsi_camera_sdk.dll",locations=("parameter/thorlabs_tlcam",thorcam_path,"global"),depends=depends,error_message=error_message,call_conv="cdecl") lib=self.lib define_functions(lib) diff --git a/pylablib/devices/uc480/uc480_lib.py b/pylablib/devices/uc480/uc480_lib.py index c1a7d03..13fe559 100644 --- a/pylablib/devices/uc480/uc480_lib.py +++ b/pylablib/devices/uc480/uc480_lib.py @@ -68,7 +68,7 @@ def initlib(self): thorcam_path=load_lib.get_program_files_folder("Thorlabs/Scientific Imaging/ThorCam") error_message="The library is automatically supplied with Thorcam software\n"+load_lib.par_error_message.format("uc480") lib_name="uc480.dll" if platform.architecture()[0][:2]=="32" else "uc480_64.dll" - self.lib=load_lib.load_lib(lib_name,locations=("parameter/uc480",thorcam_path,"global"),error_message=error_message,call_conv="stdcall") + self.lib=load_lib.load_lib(lib_name,locations=("parameter/uc480",thorcam_path,"global"),error_message=error_message,call_conv="cdecl") lib=self.lib define_functions(lib) From cee0214b38502d3398d61679b6eb6082b777a7b3 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Mon, 21 Jun 2021 21:07:00 +0200 Subject: [PATCH 20/31] Minor bugfixes, mostly devices --- pylablib/core/dataproc/utils.py | 7 +++++-- pylablib/core/devio/comm_backend.py | 18 ++++++++++++------ pylablib/core/utils/dictionary.py | 8 ++++---- pylablib/devices/LighthousePhotonics/base.py | 2 +- pylablib/devices/M2/solstis.py | 6 ++++-- pylablib/devices/NI/daq.py | 12 +++++++----- pylablib/devices/PhotonFocus/PhotonFocus.py | 2 +- pylablib/devices/Trinamic/base.py | 2 +- pylablib/devices/utils/load_lib.py | 17 +++++++++++------ 9 files changed, 46 insertions(+), 28 deletions(-) diff --git a/pylablib/core/dataproc/utils.py b/pylablib/core/dataproc/utils.py index dafe352..b0b0dac 100644 --- a/pylablib/core/dataproc/utils.py +++ b/pylablib/core/dataproc/utils.py @@ -479,10 +479,13 @@ def find_discrete_step(trace, min_fraction=1E-8, tolerance=1E-5): if len(trace)<2: raise ValueError('trace length should be at least 2') diffs=trace[1:]-trace[:-1] - q=diffs[0] + diffs=diffs[np.abs(diffs)!=0] + if len(diffs)==0: + return 0 + q=abs(diffs[0]) for d in diffs[1:]: if d!=0: - q=numerical.gcd_approx(q, abs(d), min_fraction, tolerance) + q=numerical.gcd_approx(q,abs(d),min_fraction,tolerance) return q def unwrap_mod_data(trace, wrap_range): diff --git a/pylablib/core/devio/comm_backend.py b/pylablib/core/devio/comm_backend.py index 4ec980a..23191fc 100644 --- a/pylablib/core/devio/comm_backend.py +++ b/pylablib/core/devio/comm_backend.py @@ -2,7 +2,7 @@ Routines for defining a unified interface across multiple backends. """ -from ..utils import funcargparse, general, net, py3, module +from ..utils import funcargparse, general, net, py3, module, functions as func_utils from . import interface from . import backend_logger from .base import DeviceError @@ -20,7 +20,7 @@ class DeviceBackendError(DeviceError): """Generic exception relaying a backend error""" def __init__(self, exc): - msg="backend exception: {}".format(repr(exc)) + msg="backend exception: {} ('{}')".format(repr(exc),str(exc)) super().__init__(msg) self.backend_exc=exc @@ -361,7 +361,7 @@ def _read_raw(self, size): with self.instr.ignore_warning(visa.constants.VI_SUCCESS_DEV_NPRESENT,visa.constants.VI_SUCCESS_MAX_CNT): while len(data)BBBBI",addr,comm,comm_type,bank,int(value)) + data_str=struct.pack(">BBBBi",addr,comm,comm_type,bank,int(value)) chksum=sum([b for b in data_str])%0x100 return data_str+struct.pack(" Date: Mon, 21 Jun 2021 21:25:49 +0200 Subject: [PATCH 21/31] PySide2 compatibility and minor bugfixes --- pylablib/core/gui/__init__.py | 13 +++- pylablib/core/gui/utils.py | 15 ++-- pylablib/core/gui/value_handling.py | 21 +++++- pylablib/core/gui/widgets/container.py | 84 +++++++++++++++------ pylablib/core/gui/widgets/layout_manager.py | 36 ++++++--- pylablib/core/gui/widgets/param_table.py | 7 +- pylablib/core/thread/controller.py | 6 +- 7 files changed, 126 insertions(+), 56 deletions(-) diff --git a/pylablib/core/gui/__init__.py b/pylablib/core/gui/__init__.py index d094815..2fde350 100644 --- a/pylablib/core/gui/__init__.py +++ b/pylablib/core/gui/__init__.py @@ -1,16 +1,25 @@ +import warnings + is_pyqt5=False is_pyside2=False try: from PyQt5 import QtGui, QtWidgets, QtCore from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot - from sip import delete as qdelete + try: + from PyQt5.sip import delete as qdelete # pylint: disable=no-name-in-module + except ImportError: # PyQt5<5.11 versions require separate sip + try: + from sip import delete as qdelete # pylint: disable=no-name-in-module + except ImportError: + warnings.warn("could not find sip required for some PyQt5 functionality; you need to either install it explicitly from PyPi, or update your PyQt5 version to 5.11 or above") + qdelete=None is_pyqt5=True except ImportError: try: from PySide2 import QtGui, QtWidgets, QtCore from PySide2.QtCore import Signal, Slot - from shiboken2 import delete as qdelete + from shiboken2 import delete as qdelete # pylint: disable=no-name-in-module is_pyside2=True except ImportError: pass diff --git a/pylablib/core/gui/utils.py b/pylablib/core/gui/utils.py index 53aa12a..7134847 100644 --- a/pylablib/core/gui/utils.py +++ b/pylablib/core/gui/utils.py @@ -78,9 +78,8 @@ def insert_layout_row(layout, row, stretch=0, compress=False): for i in range(layout.count()): pos=layout.getItemPosition(i) if pos[0]row: - items_to_shift.append((layout.itemAt(i),pos)) - for i,_ in items_to_shift: - layout.removeItem(i) + items_to_shift.append((i,pos)) + items_to_shift=[(layout.takeAt(i),p) for (i,p) in items_to_shift[::-1]][::-1] # remove starting from the end for i,p in items_to_shift: row_shift=1 if p[0]>=row else 0 layout.addItem(i,p[0]+row_shift,p[1],p[2]+(1-row_shift),p[3]) @@ -131,9 +130,8 @@ def insert_layout_column(layout, col, stretch=0, compress=False): for i in range(layout.count()): pos=layout.getItemPosition(i) if pos[1]col: - items_to_shift.append((layout.itemAt(i),pos)) - for i,_ in items_to_shift: - layout.removeItem(i) + items_to_shift.append((i,pos)) + items_to_shift=[(layout.takeAt(i),p) for (i,p) in items_to_shift[::-1]][::-1] # remove starting from the end for i,p in items_to_shift: col_shift=1 if p[0]>=col else 0 layout.addItem(i,p[0],p[1]+col_shift,p[2],p[3]+(1-col_shift)) @@ -153,9 +151,8 @@ def compress_grid_layout(layout): for i in range(layout.count()): pos=layout.getItemPosition(i) if pos[0]>curr_row: - items_to_shift.append((layout.itemAt(i),pos)) - for i,_ in items_to_shift: - layout.removeItem(i) + items_to_shift.append((i,pos)) + items_to_shift=[(layout.takeAt(i),p) for (i,p) in items_to_shift[::-1]][::-1] # remove starting from the end for i,p in items_to_shift: layout.addItem(i,p[0]-1,p[1],p[2],p[3]) filled_rows-=1 diff --git a/pylablib/core/gui/value_handling.py b/pylablib/core/gui/value_handling.py index c17a9d3..3d64876 100644 --- a/pylablib/core/gui/value_handling.py +++ b/pylablib/core/gui/value_handling.py @@ -176,7 +176,7 @@ class PropertyValueHandler(IValueHandler): If getter or setter are not supplied but are called, they raise :exc:`NoParameterError`; this means that they are ignored in :meth:`GUIValues.get_all_values` and :meth:`GUIValues.set_all_values` methods, - but raise an error when access directly (e.g., using :meth:`GUIValues.get_values`). + but raise an error when access directly (e.g., using :meth:`GUIValues.get_value`). Args: getter: value getter method; takes 0 or 1 (name) arguments and returns the value @@ -674,7 +674,7 @@ def remove_handler(self, name, remove_indicator=True, disconnect=False): signal=handler.get_value_changed_signal() if signal is not None: handler.get_value_changed_signal().disconnect() - except TypeError: # no signals connected or no handle + except (TypeError,RuntimeError): # no signals connected or no handle pass del self.handlers[name] if remove_indicator and name in self.indicator_handlers: @@ -905,7 +905,7 @@ def get_all_indicators(self, root=None, ind_name="__default__", include=None, ex except KeyError: return dictionary.Dictionary() @gui_thread_method - def set_indicator(self, name, value, ind_name=None, include=None, exclude=None, ignore_missing=True): + def set_indicator(self, name, value, ind_name=None, include=None, exclude=None, ignore_missing=False): """ Set indicator value with a given name. @@ -972,6 +972,21 @@ def repr_value(self, name, value): def get_value_changed_signal(self, name): """Get changed events for a value under a given name""" return self.get_handler(name).get_value_changed_signal() + def update_value(self, name=None): + """ + Send update signal for a handler with a given name or list of names. + + Emit a value changed signal with the current value to notify the subscribed slots. + If `name` is ``None``, emit for all values in the table. + """ + if name is None: + name=self.handlers.keys(leafs=True) + elif not isinstance(name,list): + name=[name] + for n in name: + changed_event=self.get_value_changed_signal(n) + if changed_event: + changed_event.emit(self.get_value(n)) def get_gui_values(gui_values=None, gui_values_path=""): diff --git a/pylablib/core/gui/widgets/container.py b/pylablib/core/gui/widgets/container.py index 683afce..0a31a2b 100644 --- a/pylablib/core/gui/widgets/container.py +++ b/pylablib/core/gui/widgets/container.py @@ -2,7 +2,7 @@ from .. import value_handling from .. import QtCore, QtWidgets from ...thread import controller -from .layout_manager import QLayoutManagedWidget +from .layout_manager import IQLayoutManagedWidget import collections @@ -12,7 +12,7 @@ TTimer=collections.namedtuple("TTimer",["name","period","timer"]) TTimerEvent=collections.namedtuple("TTimerEvent",["start","loop","stop","timer"]) TChild=collections.namedtuple("TChild",["name","widget","gui_values_path"]) -class QContainer(QtCore.QObject): +class IQContainer: """ Basic controller object which combines and controls several other widget. @@ -20,9 +20,14 @@ class QContainer(QtCore.QObject): Args: name: entity name (used by default when adding this object to a values table) + + Abstract mix-in class, which needs to be added to a class inheriting from ``QObject``. + Alternatively, one can directly use :class:`QContainer`, which already inherits from ``QObject``. """ TimerUIDGenerator=general.NamedUIDGenerator(thread_safe=True) def __init__(self, *args, name=None, **kwargs): + if not isinstance(self,QtCore.QObject): + raise RuntimeError("IQContainer should be mixed with a QObject class or subclass") super().__init__(*args,**kwargs) self.name=None self.setup_name(name) @@ -43,7 +48,7 @@ def setup_gui_values(self, gui_values="new", gui_values_path=""): """ Setup container's GUI values storage. - `gui_values` is a :class:`.GUIValues`` object, an object which has ``gui_values`` attribute, + `gui_values` is a :class:`.GUIValues` object, an object which has ``gui_values`` attribute, or ``"new"`` (make a new storage; in this case `gui_values_path` is ignored), and `gui_values_path` is the container's path within this storage. """ @@ -55,12 +60,12 @@ def setup_name(self, name): """Set the object's name""" if name is not None: self.name=name - self.setObjectName(name) + self.setObjectName(name) # pylint: disable=no-member def setup(self, name=None, gui_values=None, gui_values_path=""): """ Setup the container by initializing its GUI values and setting the ``ctl`` attribute. - `gui_values` is a :class:`.GUIValues`` object, an object which has ``gui_values`` attribute, + `gui_values` is a :class:`.GUIValues` object, an object which has ``gui_values`` attribute, or ``"new"`` (make a new storage; in this case `gui_values_path` is ignored), and `gui_values_path` is the container's path within this storage. If ``gui_values`` is ``None``, skip the setup (assume that it's already done). @@ -279,32 +284,48 @@ def get_indicator(self, name=None): def get_all_indicators(self): """Get indicator values of all widget in the container""" return self.gui_values.get_all_indicators(self.gui_values_path) - def set_indicator(self, name, value, ignore_missing=True): + def set_indicator(self, name, value, ignore_missing=False): """Set indicator value for a widget or a branch with the given name""" return self.gui_values.set_indicator((self.gui_values_path,name or ""),value,ignore_missing=ignore_missing) - set_all_indicators=set_indicator + def set_all_indicators(self, name, value, ignore_missing=True): + return self.set_indicator(name,value,ignore_missing=ignore_missing) def update_indicators(self): """Update all indicators to represent current values""" return self.gui_values.update_indicators(root=self.gui_values_path) +class QContainer(IQContainer, QtCore.QObject): + """ + Basic controller object which combines and controls several other widget. + + Can either corresponds to a widget (e.g., a frame or a group box), or simply be an organizing entity. + + Args: + name: entity name (used by default when adding this object to a values table) + + Simply a combination of :class:`IQContainer` and ``QObject``. + """ + -class QWidgetContainer(QLayoutManagedWidget, QContainer): +class IQWidgetContainer(IQLayoutManagedWidget, IQContainer): """ Generic widget container. - Combines :class:`QContainer` management of GUI values and timers - with :class:`.QLayoutManagedWidget` management of the contained widget's layout. + Combines :class:`IQContainer` management of GUI values and timers + with :class:`.IQLayoutManagedWidget` management of the contained widget's layout. Typically, adding widget adds them both to the container values and to the layout; however, this can be skipped by either using :meth:`.QLayoutManagedWidget.add_to_layout` (only add to the layout), or specifying ``location="skip"`` in :meth:`add_child` (only add to the container). + + Abstract mix-in class, which needs to be added to a class inheriting from ``QWidget``. + Alternatively, one can directly use :class:`QWidgetContainer`, which already inherits from ``QWidget``. """ def setup(self, layout="vbox", no_margins=False, name=None, gui_values=None, gui_values_path=""): - QContainer.setup(self,name=name,gui_values=gui_values,gui_values_path=gui_values_path) - QLayoutManagedWidget.setup(self,layout=layout,no_margins=no_margins) + IQContainer.setup(self,name=name,gui_values=gui_values,gui_values_path=gui_values_path) + IQLayoutManagedWidget.setup(self,layout=layout,no_margins=no_margins) def add_child(self, name, widget, location=None, gui_values_path=True): """ Add a contained child widget. @@ -314,7 +335,7 @@ def add_child(self, name, widget, location=None, gui_values_path=True): `location` specifies the layout location to which the widget is added; if ``location=="skip"``, skip adding it to the layout (can be manually added later). Note that if the widget is added to the layout, it will be completely deleted - when :meth:`clear`or :meth:`remove_child` methods are called; + when ``clear`` or ``remove_child`` methods are called; otherwise, simply its ``clear`` method will be called, and its GUI values will be deleted. If `gui_values_path` is ``False`` or ``None``, do not add it to the GUI values table; @@ -322,18 +343,18 @@ def add_child(self, name, widget, location=None, gui_values_path=True): otherwise, ``gui_values_path`` specifies the path under which the widget values are stored. """ if name!=False: - QContainer.add_child(self,name=name,widget=widget,gui_values_path=gui_values_path) + IQContainer.add_child(self,name=name,widget=widget,gui_values_path=gui_values_path) if isinstance(widget,QtWidgets.QWidget): - QLayoutManagedWidget.add_to_layout(self,widget,location=location) + IQLayoutManagedWidget.add_to_layout(self,widget,location=location) return widget def remove_child(self, name): """Remove widget from the container and the layout, clear it, and remove it""" if name in self._children: widget=self._children[name].widget - QContainer.remove_child(self,name) - QLayoutManagedWidget.remove_layout_element(self,widget) + IQContainer.remove_child(self,name) + IQLayoutManagedWidget.remove_layout_element(self,widget) else: - QContainer.remove_child(self,name) + IQContainer.remove_child(self,name) def add_frame(self, name, layout="vbox", location=None, gui_values_path=True, no_margins=True): """ Add a new frame container to the layout. @@ -366,17 +387,30 @@ def clear(self): All the timers are stopped, all the contained widgets are cleared and removed. """ - QContainer.clear(self) - QLayoutManagedWidget.clear(self) + IQContainer.clear(self) + IQLayoutManagedWidget.clear(self) + +class QWidgetContainer(IQWidgetContainer, QtWidgets.QWidget): + """ + Generic widget container. + + Combines :class:`IQContainer` management of GUI values and timers + with :class:`.IQLayoutManagedWidget` management of the contained widget's layout. + Typically, adding widget adds them both to the container values and to the layout; + however, this can be skipped by either using :meth:`.QLayoutManagedWidget.add_to_layout` + (only add to the layout), or specifying ``location="skip"`` in :meth:`add_child` (only add to the container). + + Simply a combination of :class:`IQWidgetContainer` and ``QWidget``. + """ -class QFrameContainer(QtWidgets.QFrame, QWidgetContainer): - """An extension of :class:`QWidgetContainer` for a ``QFrame`` Qt base class""" +class QFrameContainer(IQWidgetContainer, QtWidgets.QFrame): + """An extension of :class:`IQWidgetContainer` for a ``QFrame`` Qt base class""" -class QGroupBoxContainer(QtWidgets.QGroupBox, QWidgetContainer): - """An extension of :class:`QWidgetContainer` for a ``QGroupBox`` Qt base class""" +class QGroupBoxContainer(IQWidgetContainer, QtWidgets.QGroupBox): + """An extension of :class:`IQWidgetContainer` for a ``QGroupBox`` Qt base class""" def setup(self, caption=None, layout="vbox", no_margins=False, name=None, gui_values=None, gui_values_path=""): QWidgetContainer.setup(self,layout=layout,no_margins=no_margins,name=name,gui_values=gui_values,gui_values_path=gui_values_path) if caption is not None: @@ -385,7 +419,7 @@ def setup(self, caption=None, layout="vbox", no_margins=False, name=None, gui_va -class QTabContainer(QtWidgets.QTabWidget, QContainer): +class QTabContainer(IQContainer, QtWidgets.QTabWidget): """ Container which manages tab widget. diff --git a/pylablib/core/gui/widgets/layout_manager.py b/pylablib/core/gui/widgets/layout_manager.py index 2c7ab08..086a262 100644 --- a/pylablib/core/gui/widgets/layout_manager.py +++ b/pylablib/core/gui/widgets/layout_manager.py @@ -6,22 +6,21 @@ import contextlib -class QLayoutManagedWidget(QtWidgets.QWidget): +class IQLayoutManagedWidget: """ GUI widget which can manage layouts. Typically, first it is set up using :meth:`setup` method to specify the master layout kind; afterwards, widgets and sublayout can be added using :meth:`add_to_layout`. In addition, it can directly add named sublayouts using :meth:`add_sublayout` method. + + Abstract mix-in class, which needs to be added to a class inheriting from ``QWidget``. + Alternatively, one can directly use :class:`QLayoutManagedWidget`, which already inherits from ``QWidget``. """ def __init__(self, *args, **kwargs): - if args: - parent=args[0] - elif "parent" in kwargs: - parent=kwargs["parent"] - else: - parent=None - super().__init__(parent) + if not isinstance(self,QtWidgets.QWidget): + raise RuntimeError("IQLayoutManagedWidget should be mixed with a QWidget class or subclass") + super().__init__(*args,**kwargs) self.main_layout=None self._default_layout="main" @@ -36,7 +35,8 @@ def _make_new_layout(self, kind, *args, **kwargs): raise ValueError("unrecognized layout kind: {}".format(kind)) def _set_main_layout(self): self.main_layout=self._make_new_layout(self.main_layout_kind,self) - self.main_layout.setObjectName(self.name+"_main_layout" if hasattr(self,"name") and self.name else "main_layout") + name=getattr(self,"name") + self.main_layout.setObjectName(name+"_main_layout" if name else "main_layout") if self.no_margins: self.main_layout.setContentsMargins(0,0,0,0) def setup(self, layout="grid", no_margins=False): @@ -89,11 +89,11 @@ def _normalize_location(self, location, default_location=None, default_layout=No row,rowspan=0,1 row_cnt,col_cnt=1,layout.count() if lkind in {"grid","vbox"}: - row=row_cnt if row=="next" else (row%row_cnt if row<0 else row) + row=row_cnt if row=="next" else (row%max(row_cnt,1) if row<0 else row) if rowspan=="end": rowspan=max(row_cnt-row,1) if lkind in {"grid","hbox"}: - col=col_cnt if col=="next" else (col%col_cnt if col<0 else col) + col=col_cnt if col=="next" else (col%max(col_cnt,1) if col<0 else col) if colspan=="end": colspan=max(col_cnt-col,1) return lname,(row,col,rowspan,colspan) @@ -279,4 +279,16 @@ def clear(self): if self.main_layout is not None: self._set_main_layout() self._sublayouts={"main":(self.main_layout,self.main_layout_kind)} - self._spacers=[] \ No newline at end of file + self._spacers=[] + + +class QLayoutManagedWidget(IQLayoutManagedWidget, QtWidgets.QWidget): + """ + GUI widget which can manage layouts. + + Typically, first it is set up using :meth:`setup` method to specify the master layout kind; + afterwards, widgets and sublayout can be added using :meth:`add_to_layout`. + In addition, it can directly add named sublayouts using :meth:`add_sublayout` method. + + Simply a combination of :class:`IQLayoutManagedWidget` and ``QWidget``. + """ \ No newline at end of file diff --git a/pylablib/core/gui/widgets/param_table.py b/pylablib/core/gui/widgets/param_table.py index a0214ea..b1e1a93 100644 --- a/pylablib/core/gui/widgets/param_table.py +++ b/pylablib/core/gui/widgets/param_table.py @@ -549,10 +549,11 @@ def get_all_indicators(self, name=None): """Get indicator values of all widget in the given branch""" return self.gui_values.get_all_indicators((self.gui_values_path,name or ""),include=self.params) @controller.gui_thread_method - def set_indicator(self, name, value, ignore_missing=True): + def set_indicator(self, name, value, ignore_missing=False): """Set indicator value for a widget or a branch with the given name""" return self.gui_values.set_indicator((self.gui_values_path,name or ""),value,include=self.params,ignore_missing=ignore_missing) - set_all_indicators=set_indicator + def set_all_indicators(self, name, value, ignore_missing=True): + return self.set_indicator(name,value,ignore_missing=ignore_missing) @controller.gui_thread_method def update_indicators(self): """Update all indicators to represent current values""" @@ -568,7 +569,7 @@ def clear(self, disconnect=False): if disconnect: try: self.value_changed.disconnect() - except TypeError: # no signals connected + except (TypeError,RuntimeError): # no signals connected pass for name in self.params: path=(self.gui_values_path,name) diff --git a/pylablib/core/thread/controller.py b/pylablib/core/thread/controller.py index 4268711..8b058c5 100644 --- a/pylablib/core/thread/controller.py +++ b/pylablib/core/thread/controller.py @@ -807,7 +807,7 @@ def send_sync(self, tag, uid): """ Send a synchronization signal with the given tag and UID. - This method is rarely invoked directly, and is usually used by synchronizers code (e.g., class:`QThreadNotifier`). + This method is rarely invoked directly, and is usually used by synchronizers code (e.g., :class:`.QThreadNotifier`). External call method. """ self._control_sent.emit(("sync",tag,0,uid)) @@ -1474,7 +1474,9 @@ def run(self): to=self._schedule_pending_jobs(ct) sleep_time=self._loop_wait_period if to is None else min(self._loop_wait_period,to) if schedule_time<=0 and sleep_time>=0: - if not self._poked: + if self._poked: + self.check_messages() + else: self.sleep(sleep_time,wake_on_message=True) self._poked=False self._exhaust_queued_calls() From 36d62c50be7bfd6026b27df1d4b5d71c6605e33f Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Mon, 21 Jun 2021 21:26:45 +0200 Subject: [PATCH 22/31] Dependencies updates, docs bugfixes --- docs/conf.py | 3 +++ docs/devices/uc480.rst | 2 +- docs/install.rst | 6 +++--- setup.py | 7 ++++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4322dad..ead7280 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,6 +57,9 @@ sys.modules['pyvisa']=mock.Mock(VisaIOError=object, __version__='1.9.0') sys.modules['serial']=mock.Mock(SerialException=object) sys.modules['ft232']=mock.Mock(Ft232Exception=object) +sys.modules['PyQt5.QtCore']=mock.Mock(QObject=object,QThread=object) +sys.modules['PyQt5.QtWidgets']=mock.Mock(QWidget=object,QFrame=object,QGroupBox=object,QTabWidget=object,QPushButton=object,QComboBox=object,QLineEdit=object,QLabel=object) +sys.modules['PyQt5']=mock.Mock(QtCore=sys.modules['PyQt5.QtCore'],QtWidgets=sys.modules['PyQt5.QtWidgets']) if os.path.exists(".skipped_apidoc"): with open(".skipped_apidoc","r") as f: for ln in f.readlines(): diff --git a/docs/devices/uc480.rst b/docs/devices/uc480.rst index 1207a3e..9000e0a 100644 --- a/docs/devices/uc480.rst +++ b/docs/devices/uc480.rst @@ -47,6 +47,6 @@ The operation of these cameras is relatively standard. They support all the stan - Uc480 API supports many different pixel modes, including packed ones. However, pylablib currently supports only unpacked modes. - Occasionally (especially at high frame rates) frames get skipped during transfer, before they are placed into the frame buffer by the camera driver. This can happen in two different ways. First, the frame is simply dropped without any indication. This typically can not be detected without using the framestamp contained in the frame info, as the frames flow appear to be uninterrupted. In the second way, the acquisition appears to get "restarted" (the internal number of acquired frames is dropped to zero), which is detected by the library. In this case there are several different ways the software can react, which are controlled using :meth:`.UC480Camera.set_frameskip_behavior`. - The default way to address this "restart" event (``"ignore"``) is to ignore it and only adjust the internal acquired frame counter; this manifests as quietly dropped frames, exactly the same as the first kind of event. In the other method (``"skip"``), some number of frames are marked as skipped, so that the difference between the number of acquired frames and the internal framestamp is kept constant. This makes the gap explicit in the camera frame counters. Finally (``"error"``), the software can raise :exc:`.uc480.uc480FrameTransferError` when such event is detected, which can be used to, e.g., restart the acquisition. + The default way to address this "restart" event (``"ignore"``) is to ignore it and only adjust the internal acquired frame counter; this manifests as quietly dropped frames, exactly the same as the first kind of event. In the other method (``"skip"``), some number of frames are marked as skipped, so that the difference between the number of acquired frames and the internal framestamp is kept constant. This makes the gap explicit in the camera frame counters. Finally (``"error"``), the software can raise ``uc480FrameTransferError`` when such event is detected, which can be used to, e.g., restart the acquisition. One needs to keep in mind, that while the last two methods make "restarts" more explicit, they do not address the first kind of events (quiet drops). The most direct way to deal with them is to use frame information by setting ``return_info=True`` in frame reading methods like ``read_multiple_images``. This information contains the internal camera framestamp, which lets one detect any skipped frames. \ No newline at end of file diff --git a/docs/install.rst b/docs/install.rst index 7be2d52..601de75 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -39,8 +39,8 @@ With this, the basic functionality (such as data processing or file IO) will wor - ``[extra]`` extra packages used in some situations: ``numba`` (speeds up some data processing) and ``rpyc`` (communication between different PCs) - ``[devio]`` basic devio packages: ``pyft232``, ``pyvisa``, ``pyserial``, and ``pyusb`` - ``[devio-extra]`` additional devio packages: ``nidaqmx`` and ``websocket-client`` - - ``[gui-pyqt5]`` `PyQt5 `_-based GUI: ``pyqt5``, ``sip``, and ``pyqtgraph``. Should not be used together with ``[gui-pyside2]`` - - ``[gui-pyside2]`` `PySide2 `_-based GUI: ``pyside2``, ``shiboken2``, and ``pyqtgraph``. Should not be used together with ``[gui-pyqt5]`` + - ``[gui-pyqt5]`` `PyQt5 `_-based GUI: ``pyqt5`` and ``pyqtgraph``. Should not be used together with ``[gui-pyside2]`` + - ``[gui-pyside2]`` `PySide2 `_-based GUI: ``pyside2`` and ``pyqtgraph``. Should not be used together with ``[gui-pyqt5]`` The options can be combined. For example, @@ -75,7 +75,7 @@ The basic package dependencies are `NumPy `_ The main device communication packages are `PyVISA `_ and `pySerial `_, which cover the majority of devices. Several devices (e.g., :ref:`Thorlabs Kinesis ` and :ref:`Attocube ANC 350 `) require additional communication packages: `pyft232 `_ and `PyUSB `_. Finally, some particular devices completely or partially rely on specific packages: `NI-DAQmx `_ for :ref:`NIDAQ ` and `websocket-client `_ for additional :ref:`M2 Solstis ` functionality. -Finally, GUI and advanced multi-threading relies on Qt5, which has two possible options. The first (default) option is `PyQt5 `_ with `sip `_ for some memory management functionality. The second possible option is `PySide2 `_ with `shiboken `_. Both options should work equally well, and the choice mostly depends on what is already installed (since having both PyQt5 and PySide2 installed might lead to conflicts). Finally, plotting relies on `pyqtgraph `_, which (starting with 0.11) is compatible with both PySide2 and PyQt5. +Finally, GUI and advanced multi-threading relies on Qt5, which has two possible options. The first (default) option is `PyQt5 `_ with `sip `_ for some memory management functionality (newer PyQt5 versions ``>=5.11`` already come with ``PyQt5-sip``, but if you use an older version, you need to install ``sip`` separately). The second possible option is `PySide2 `_ with `shiboken `_. Both options should work equally well, and the choice mostly depends on what is already installed (since having both PyQt5 and PySide2 installed might lead to conflicts). Finally, plotting relies on `pyqtgraph `_, which (starting with 0.11) is compatible with both PySide2 and PyQt5. The package has been tested with Python 3.6 through 3.9, and is incompatible with Python 2. The last version officially supporting Python 2.7 is 0.4.0. Furthermore, testing has been mostly performed on 64-bit Python. This is the recommended option, as 32-bit version limitations (most notably, limited amount of accessible RAM) mean that it should only be used when absolutely necessary, e.g., when some required packages or libraries are only available in 32-bit version. diff --git a/setup.py b/setup.py index 024742b..7f964d8 100644 --- a/setup.py +++ b/setup.py @@ -16,10 +16,10 @@ dep_base=['numpy','scipy','pandas'] dep_extra=['rpyc','numba'] -dep_devio=['pyft232','pyvisa','pyserial','pyusb'] +dep_devio=['pyft232','pyvisa>=1.6','pyserial','pyusb'] dep_devio_extra=['nidaqmx','websocket-client'] -dep_pyqt5=['pyqt5','sip','pyqtgraph'] -dep_pyside2=['pyside2','shiboken2','pyqtgraph'] +dep_pyqt5=['pyqt5>5.10','pyqtgraph'] +dep_pyside2=['pyside2','shiboken2','pyqtgraph>0.10'] setup( name='pylablib', # name='pylablib-lightweight', @@ -41,6 +41,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Operating System :: Microsoft :: Windows' ], project_urls={ From 998217acd8a3083bfe4be2cb21d19aa16e4efc5a Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Mon, 21 Jun 2021 21:31:11 +0200 Subject: [PATCH 23/31] Streaming and device thread controller updates --- pylablib/thread/device_thread.py | 47 +++- pylablib/thread/stream/blockstream.py | 18 +- pylablib/thread/stream/frameproc.py | 2 +- pylablib/thread/stream/stream_manager.py | 300 +++++++++++++---------- pylablib/thread/stream/table_accum.py | 10 +- 5 files changed, 207 insertions(+), 170 deletions(-) diff --git a/pylablib/thread/device_thread.py b/pylablib/thread/device_thread.py index 8137ee6..73e5902 100644 --- a/pylablib/thread/device_thread.py +++ b/pylablib/thread/device_thread.py @@ -2,6 +2,7 @@ from ..core.utils import rpyc_utils, module as module_utils import importlib +import contextlib class DeviceThread(controller.QTaskThread): @@ -10,18 +11,12 @@ class DeviceThread(controller.QTaskThread): Contains methods to open/close the device, obtaining device settings and info, and dealing with remote devices (e.g., connected to other PCs). - Args: - name: thread name - args: args supplied to :meth:`setup_task` method - kwargs: keyword args supplied to :meth:`setup_task` method - multicast_pool: :class:`.MulticastPool` for this thread (by default, use the default common pool) - Attributes: device: managed device. Its opening should be specified in an overloaded :meth:`connect_device` method, and it is actually opened by calling :meth:`open_device` method (which also handles status updates and duplicate opening issues) - qd: device query accessor, which routes device method call through a command - ``ctl.qd.method(*args,**kwarg)`` is equivalent to ``ctl.device.method(args,kwargs)`` called as a synchronous command in the device thread - qdi: device query accessor, ignores and silences any exceptions (including missing /stopped controller); similar to ``.csi`` accessor for synchronous commands + csd: device query accessor, which routes device method call through a command + ``ctl.csd.method(*args,**kwarg)`` is equivalent to ``ctl.device.method(args,kwargs)`` called as a synchronous command in the device thread + csdi: device query accessor, ignores and silences any exceptions (including missing /stopped controller); similar to ``.csi`` accessor for synchronous commands device_reconnect_tries: number of attempts to connect to the device before when calling :meth:`open` before giving up and declaring it unavailable settings_variables: list of variables to list when requesting full info (e.g., using ``get_settings`` command); by default, read all variables, but if it takes too long, some can be omitted @@ -60,8 +55,9 @@ def __init__(self, name=None, args=None, kwargs=None, multicast_pool=None): self.device_reconnect_tries=0 self._tried_device_connect=0 self.rpyc_serv=None - self.qd=self.DeviceMethodAccessor(self,ignore_errors=False) - self.qdi=self.DeviceMethodAccessor(self,ignore_errors=True) + self.remote=None + self.csd=self.DeviceMethodAccessor(self,ignore_errors=False) + self.csdi=self.DeviceMethodAccessor(self,ignore_errors=True) def finalize_task(self): self.close() @@ -136,6 +132,30 @@ def rpyc_obtain(self, obj): return rpyc_utils.obtain(obj,serv=self.rpyc_serv) return obj + class ConnectionFailError(Exception): + """Error which can be raised on opening failure""" + + @contextlib.contextmanager + def using_devclass(self, cls, host, timeout=3., attempts=2): + """ + Context manager for simplifying device opening. + + Creates a class based on `cls` and `host` parameters, catches device opening errors, + and automatically closes device and sets it to ``None`` if that happens. + If `host` is ``"disconnect"``, skip device connection (can be used for e.g., temporarily unavailable or buggy device). + """ + if cls=="disconnect": + raise self.ConnectionFailError + try: + cls=self.rpyc_devclass(cls,host=host,timeout=timeout,attempts=attempts) + except IOError: + raise self.ConnectionFailError + try: + yield cls + except cls.Error: + if self.device is not None: + self.device.close() + self.device=None def open(self): """ Open the device by calling :meth:`connect_device`. @@ -148,7 +168,10 @@ def open(self): return False self.update_status("connection","opening","Connecting...") if self.device is None: - self.connect_device() + try: + self.connect_device() + except self.ConnectionFailError: + pass if self.device is not None: if not self.device.is_opened(): self.open_device() diff --git a/pylablib/thread/stream/blockstream.py b/pylablib/thread/stream/blockstream.py index 51f3647..77b9873 100644 --- a/pylablib/thread/stream/blockstream.py +++ b/pylablib/thread/stream/blockstream.py @@ -65,7 +65,7 @@ def setup_task(self): self.add_command("set_cutoff") self.add_command("configure_channel") self.add_command("get_channel_status") - self.add_command("get_source_status") + self.add_command("get_scheduled_status") self.sn=self.name self.cnt=stream_manager.MultiStreamIDCounter() self.cnt.add_counter(self.sn) @@ -262,8 +262,7 @@ def on_multicast(src, tag, value): else: def on_multicast(src, tag, value): self._add_data(name,value,src=src,tag=tag,parse=parse) - uid=self.subscribe_commsync(on_multicast,srcs=srcs,tags=tags,dsts=dsts,filt=filt,limit_queue=None,priority=10) - # self.source_schedulers[name]=self._multicast_schedulers[uid] #TODO: save all schedulers in the task controller + self.subscribe_commsync(on_multicast,srcs=srcs,tags=tags,dsts=dsts,filt=filt,limit_queue=None,priority=-5) if sn is not None: self.cnt.add_counter(sn) @@ -413,13 +412,6 @@ def get_channel_status(self): for n,ch in self.channels.items(): status[n]=ch.get_status() return status - def get_source_status(self): - """ - Get source incoming queues status. - - Return dictionary ``{name: queue_len}``. - """ - status={} - for n,sch in self.source_schedulers.items(): - status[n]=sch.get_current_len() - return status \ No newline at end of file + def get_scheduled_status(self): + """Get the total number of pending calls in the queues (includes both commands and source multicasts)""" + return sum(len(sch) for sch in self._priority_queues.values()) \ No newline at end of file diff --git a/pylablib/thread/stream/frameproc.py b/pylablib/thread/stream/frameproc.py index 5a60896..4097cd2 100644 --- a/pylablib/thread/stream/frameproc.py +++ b/pylablib/thread/stream/frameproc.py @@ -33,7 +33,7 @@ class FrameBinningThread(controller.QTaskThread): - ``setup_binning``: setup binning parameters """ def setup_task(self, src, tag_in, tag_out=None): - self.subscribe_commsync(self.process_input_frames,srcs=src,tags=tag_in,dsts="any",limit_queue=2,on_full_queue="wait",subscription_priority=-5) + self.subscribe_commsync(self.process_input_frames,srcs=src,tags=tag_in,dsts="any",limit_queue=2,on_full_queue="wait") self.tag_out=tag_out or tag_in self.v["params/spat"]={"bin":(1,1),"mode":"skip"} self.v["params/time"]={"bin":1,"mode":"skip"} diff --git a/pylablib/thread/stream/stream_manager.py b/pylablib/thread/stream/stream_manager.py index 4b3136d..4fed1fe 100644 --- a/pylablib/thread/stream/stream_manager.py +++ b/pylablib/thread/stream/stream_manager.py @@ -19,10 +19,14 @@ class StreamIDCounter: can be ``"ignore"`` (keep the current counter value), ``"set"`` (set the value to the new smaller one), or ``"error"`` (raise an exception) mid_ooo: behavior if supplied message ID in :meth:`next_message` or :meth:`update` is out of order (lower than the current count); can be ``"ignore"`` (keep the current counter value), ``"set"`` (set the value to the new smaller one), or ``"error"`` (raise an exception) + on_invalid: action on an invalid message (the one not having ``get_ids`` message); + can be ``"ignore"`` (ignore on :meth:`receive_message`), ``"advance"`` (advance message ID on ``default_sn`` with :meth:`receive_message`), + or ``"error"`` (raise an error) """ - def __init__(self, use_mid=True, sid_ooo="ignore", mid_ooo="ignore"): + def __init__(self, use_mid=True, sid_ooo="ignore", mid_ooo="ignore", on_invalid="ignore"): funcargparse.check_parameter_range(sid_ooo,"sid_ooo",["ignore","set","error"]) funcargparse.check_parameter_range(mid_ooo,"mid_ooo",["ignore","set","error"]) + funcargparse.check_parameter_range(on_invalid,"on_invalid",["ignore","advance","error"]) self.sid_gen=general.UIDGenerator() self.sid=self.sid_gen() self.use_mid=use_mid @@ -30,6 +34,7 @@ def __init__(self, use_mid=True, sid_ooo="ignore", mid_ooo="ignore"): self.mid=self.mid_gen() self.sid_ooo=sid_ooo self.mid_ooo=mid_ooo + self.on_invalid=on_invalid self.cutoff=(0,0) def update_session(self, sid): @@ -102,7 +107,15 @@ def receive_message(self, msg, sn=None): `sn` specifies the stream name within the message. Return ``True`` if the session ID was incremented as a result. """ - return self.update(*msg.get_ids(sn)) + try: + return self.update(*msg.get_ids(sn)) + except (AttributeError,TypeError): + if self.on_invalid=="ignore": + return False + if self.on_invalid=="advance": + self.next_message() + return False + raise def get_ids(self): """Get stored IDs as a tuple ``(sid, mid)``""" return self.sid,self.mid @@ -122,20 +135,41 @@ def check_cutoff(self, sid=None, mid=None): """ Check if the supplied IDs pass the cutoff (i.e., above or equal to it). - Values of ``None`` are not checked, i.e., assumed to always pass. + Values of ``None`` are set to the current counter values; values of ``"skip"`` always pass. """ - if sid is not None and sid=cutoff: - ncut=i - break - del self.acc[:ncut] - if self.last_read is not None and self.last_readsince): - if cond is not None: - for evt in self.acc[::-1]: - if evt.ids<=since: - break - if cond(*evt): - do_wait=False - self.last_evt=evt - break - else: - do_wait=False - self.last_evt=self.acc[-1] - else: - do_wait=True - self.waiting=True + if cond is None: + if len(self.acc)>=nacc: + return 0 + elif self.acc: + for i,evt in enumerate(self.acc): + if cond(*evt): + return i + self.acc_checked=len(self.acc) + self.waiting=True try: - if do_wait: - check=self.get_checker(since=since,cond=cond) - self.ctl.wait_until(check,timeout=timeout) + def check(): + with self.lock: + if cond is None: + self._passing_msg=0 + return len(self.acc)>=nacc + if len(self.acc)>self.acc_checked: + for i,evt in enumerate(self.acc[self.acc_checked:]): + if cond(*evt): + self._passing_msg=self.acc_checked+i + return True + self.acc_checked=len(self.acc) + return False + self.ctl.wait_until(check,timeout=timeout) finally: self.waiting=False - self.last_wait=self.last_evt.ids - return self.last_evt + self.acc_checked=0 + return self._passing_msg def _get_acc_rng(self, i0, i1, peek, as_event): if i1 is not None and i1<=i0 or not self.acc: return [] @@ -518,8 +536,16 @@ def _get_acc_rng(self, i0, i1, peek, as_event): evts=self.acc[i0:i1] if not peek: del self.acc[:i1] - self.last_read=evts[-1].ids return evts if as_event else [evt.msg for evt in evts] + def peek_message(self, n=0, as_event=None): + """ + Peek at the `n`th message in the accumulated queue. + + If ``as_event==True``, return tuple ``(src, tag, msg)`` describing the received event; + otherwise, just message is returned. + """ + evt=self.acc[n] + return evt if as_event else evt.msg def get_oldest(self, n=1, peek=False, as_event=False): """ Get the oldest `n` messages from the accumulator queue. @@ -527,9 +553,11 @@ def get_oldest(self, n=1, peek=False, as_event=False): If `n` is ``None``, return all messages. If there are less than `n` message in the queue, return all of them. If ``peek==True``, just return the messages; otherwise, pop the from the queue in mark the as read. - If ``as_event==True``, each message is represented as a tuple ``(src, tag, msg, ids)`` describing the received event; + If ``as_event==True``, each message is represented as a tuple ``(src, tag, msg)`` describing the received event; otherwise, just messages are returned. """ + if not peek: + self._check_waiting() return self._get_acc_rng(0,n,peek=peek,as_event=as_event) def get_newest(self, n=1, peek=True, as_event=False): """ @@ -537,9 +565,11 @@ def get_newest(self, n=1, peek=True, as_event=False): If there are less than `n` message in the queue, return all of them. If ``peek==True``, just return the messages; otherwise, clear the queue after reading. - If ``as_event==True``, each message is represented as a tuple ``(src, tag, msg, ids)`` describing the received event; + If ``as_event==True``, each message is represented as a tuple ``(src, tag, msg)`` describing the received event; otherwise, just messages are returned. """ + if not peek: + self._check_waiting() if n<=0: return [] return self._get_acc_rng(-n,-1,peek=peek,as_event=as_event) \ No newline at end of file diff --git a/pylablib/thread/stream/table_accum.py b/pylablib/thread/stream/table_accum.py index 56aa381..f76659f 100644 --- a/pylablib/thread/stream/table_accum.py +++ b/pylablib/thread/stream/table_accum.py @@ -133,20 +133,16 @@ class TableAccumulatorThread(controller.QTaskThread): - ``channels ([str])``: channel names - ``src (str)``: name of a source thread which emits new data signals (typically, a name of :class:`StreamFormerThread` thread) - ``tag (str)``: tag of the source multicast - - ``reset_tag (str)``: if not ``None``, defines the name of the reset multicast tag; if this multicast is received from the source, the table is automatically reset - ``memsize (int)``: maximal number of rows to store Commands: - ``get_data``: get some of the accumulated data - ``reset``: clear stored data """ - def setup_task(self, channels, src, tag, reset_tag=None, memsize=10**6): + def setup_task(self, channels, src, tag, memsize=10**6): self.channels=channels - self.fmt=[None]*len(channels) self.table_accum=TableAccumulator(channels=channels,memsize=memsize) self.subscribe_direct(self._accum_data,srcs=src,tags=tag,dsts="any") - if reset_tag is not None: - self.subscribe_sync(self._on_source_reset,srcs=src,tags=reset_tag,dsts="any") self.cnt=stream_manager.StreamIDCounter() self.data_lock=threading.Lock() self.add_direct_call_command("get_data") @@ -156,10 +152,6 @@ def preprocess_data(self, data): """Preprocess data before adding it to the table (to be overloaded)""" return data - def _on_source_reset(self, src, tag, value): - with self.data_lock: - self.table_accum.reset_data() - def _accum_data(self, src, tag, value): with self.data_lock: if hasattr(value,"get_ids") and self.cnt.receive_message(value): From cf49d084f9d088c10fd34f5cf59cdd1bfa44e909 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Thu, 24 Jun 2021 23:24:11 +0200 Subject: [PATCH 24/31] Minor bugs and compatibility issues --- docs/devices/devices_basics.rst | 4 +++- pylablib/core/utils/ipc.py | 4 ++-- pylablib/devices/IMAQdx/IMAQdx.py | 6 +---- pylablib/devices/IMAQdx/NIIMAQdx_lib.py | 1 + pylablib/devices/utils/load_lib.py | 29 +++++++++++++++++++++---- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/docs/devices/devices_basics.rst b/docs/devices/devices_basics.rst index 12e6df8..6db4ccf 100644 --- a/docs/devices/devices_basics.rst +++ b/docs/devices/devices_basics.rst @@ -182,12 +182,14 @@ Note that DLLs can have 32-bit and 64-bit version, and it should agree with the In addition, you need to provide pylablib with the path to the DLLs. In many cases it checks the standard locations such as the default ``System32`` folder (used, e.g., in DCAM or IMAQ cameras) or defaults paths for manufacturer software (such as ``C:/Program Files/Andor SOLIS`` for Andor cameras). If the software path is different, or if you choose to obtain DLLs elsewhere, you can also explicitly provide path by setting the library parameter:: import pylablib as pll - pll.par["devices/dlls/andor_sdk3"] = "path/to/dlls" + pll.par["devices/dlls/andor_sdk3"] = "D:/Program Files/Andor SOLIS" from pylablib.devices import Andor cam = Andor.AndorSDK3Camera() All of these requirements are described in detail for the specific devices. +Starting from Python 3.8 the DLL search path is changed to not include the files contained in ``PATH`` environment variable and in the script folder. By default, this behavior is still emulated when pylablib searches for the DLLs, since it is required in some cases (e.g., Photon Focus pfcam interface). If needed, it can be turned off (i.e., switched to the new default behavior of Python 3.8+) by setting ``pll.par["devices/dlls/add_environ_paths"]=False``. + Advanced examples -------------------------------------- diff --git a/pylablib/core/utils/ipc.py b/pylablib/core/utils/ipc.py index 70f95cb..d309641 100644 --- a/pylablib/core/utils/ipc.py +++ b/pylablib/core/utils/ipc.py @@ -105,7 +105,7 @@ def send_numpy(self, data, method="auto", timeout=None): return PipeIPCChannel.send_numpy(self,data) if not self._check_strides(data): # need continuous array to send data=data.copy() - buff_ptr,count=data.ctypes.get_data(),data.nbytes + buff_ptr,count=data.ctypes.data,data.nbytes self.conn.send(TPipeMsg(_sharedmem_start,(count,data.dtype.str,data.shape))) while count>0: chunk_size=min(count,self.arr_size) @@ -124,7 +124,7 @@ def recv_numpy(self, timeout=None): else: count,dtype,shape=msg.data data=np.empty(shape,dtype=dtype) - buff_ptr=data.ctypes.get_data() + buff_ptr=data.ctypes.data while count>0: chunk_size=self._recv_with_timeout(timeout) ctypes.memmove(buff_ptr,ctypes.addressof(self.arr.get_obj()),chunk_size) diff --git a/pylablib/devices/IMAQdx/IMAQdx.py b/pylablib/devices/IMAQdx/IMAQdx.py index c46a7db..dca6925 100644 --- a/pylablib/devices/IMAQdx/IMAQdx.py +++ b/pylablib/devices/IMAQdx/IMAQdx.py @@ -106,12 +106,8 @@ def __init__(self, sid, name): self.min=lib.IMAQdxGetAttributeMinimum(sid,name,self._attr_type_n) self.max=lib.IMAQdxGetAttributeMaximum(sid,name,self._attr_type_n) self.inc=lib.IMAQdxGetAttributeIncrement(sid,name,self._attr_type_n) - else: - self.min=self.max=self.inc=None if self._attr_type_n==IMAQdxAttributeType.IMAQdxAttributeTypeEnum: self.values=lib.IMAQdxEnumerateAttributeValues(sid,name) - else: - self.values=None def update_limits(self): """Update minimal and maximal attribute limits and return tuple ``(min, max, inc)``""" @@ -381,7 +377,7 @@ def _read_data_raw(self, buffer_num, size_bytes, dtype=" Date: Thu, 24 Jun 2021 23:30:55 +0200 Subject: [PATCH 25/31] Started SiSo framegrabber, reorganized PF code --- .../pylablib.devices.SiliconSoftware.rst | 24 + docs/.apidoc/pylablib.devices.rst | 1 + docs/.skipped_apidoc | 3 + pylablib/devices/IMAQ/IMAQ.py | 113 +- pylablib/devices/PhotonFocus/PhotonFocus.py | 116 +- pylablib/devices/PhotonFocus/__init__.py | 2 +- pylablib/devices/SiliconSoftware/__init__.py | 3 + pylablib/devices/SiliconSoftware/fgrab.py | 605 +++++++ .../SiliconSoftware/fgrab_define_defs.py | 1176 ++++++++++++++ .../SiliconSoftware/fgrab_prototyp_defs.py | 1403 +++++++++++++++++ .../SiliconSoftware/fgrab_prototyp_lib.py | 384 +++++ pylablib/devices/interface/camera.py | 196 ++- tests/devices/test_cameras.py | 29 + 13 files changed, 3930 insertions(+), 125 deletions(-) create mode 100644 docs/.apidoc/pylablib.devices.SiliconSoftware.rst create mode 100644 pylablib/devices/SiliconSoftware/__init__.py create mode 100644 pylablib/devices/SiliconSoftware/fgrab.py create mode 100644 pylablib/devices/SiliconSoftware/fgrab_define_defs.py create mode 100644 pylablib/devices/SiliconSoftware/fgrab_prototyp_defs.py create mode 100644 pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py diff --git a/docs/.apidoc/pylablib.devices.SiliconSoftware.rst b/docs/.apidoc/pylablib.devices.SiliconSoftware.rst new file mode 100644 index 0000000..20cc612 --- /dev/null +++ b/docs/.apidoc/pylablib.devices.SiliconSoftware.rst @@ -0,0 +1,24 @@ +pylablib.devices.SiliconSoftware package +======================================== + +Submodules +---------- + +pylablib.devices.SiliconSoftware.fgrab module +--------------------------------------------- + +.. automodule:: pylablib.devices.SiliconSoftware.fgrab + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: pylablib.devices.SiliconSoftware + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/docs/.apidoc/pylablib.devices.rst b/docs/.apidoc/pylablib.devices.rst index 1fe38fa..e23bd2f 100644 --- a/docs/.apidoc/pylablib.devices.rst +++ b/docs/.apidoc/pylablib.devices.rst @@ -28,6 +28,7 @@ Subpackages pylablib.devices.Pfeiffer pylablib.devices.PhotonFocus pylablib.devices.Rigol + pylablib.devices.SiliconSoftware pylablib.devices.SmarAct pylablib.devices.Tektronix pylablib.devices.Thorlabs diff --git a/docs/.skipped_apidoc b/docs/.skipped_apidoc index 9bed0d4..fd888f4 100644 --- a/docs/.skipped_apidoc +++ b/docs/.skipped_apidoc @@ -26,6 +26,9 @@ ..\pylablib\devices\PCO\sc2_sdkstructures_defs.py ..\pylablib\devices\PhotonFocus\pfcam_defs.py ..\pylablib\devices\PhotonFocus\pfcam_lib.py +..\pylablib\devices\SiliconSoftware\fgrab_define_defs.py +..\pylablib\devices\SiliconSoftware\fgrab_prototyp_defs.py +..\pylablib\devices\SiliconSoftware\fgrab_prototyp_lib.py ..\pylablib\devices\SmarAct\SCU3DControl_defs.py ..\pylablib\devices\SmarAct\SCU3DControl_lib.py ..\pylablib\devices\Thorlabs\tl_camera_sdk_defs.py diff --git a/pylablib/devices/IMAQ/IMAQ.py b/pylablib/devices/IMAQ/IMAQ.py index 67cf234..698e034 100644 --- a/pylablib/devices/IMAQ/IMAQ.py +++ b/pylablib/devices/IMAQ/IMAQ.py @@ -38,22 +38,26 @@ def get_cameras_number(): TDeviceInfo=collections.namedtuple("TDeviceInfo",["serial_number","interface"]) -class IMAQCamera(camera.IROICamera): +class IMAQFrameGrabber(camera.IROICamera): """ - Generic IMAQ camera interface. + Generic IMAQ frame grabber interface. + + Compared to :class:`IMAQCamera`, has more permissive initialization arguments, + which simplifies its use as a base class for expanded cameras. Args: - name: interface name (can be learned by :func:`list_cameras`; usually, but not always, starts with ``"cam"`` or ``"img"``) + imaq_name: interface name (can be learned by :func:`list_cameras`; usually, but not always, starts with ``"cam"`` or ``"img"``) + do_open: if ``False``, skip the last step of opening the device (should be opened in a subclass) """ Error=IMAQError TimeoutError=IMAQTimeoutError - def __init__(self, name="img0"): - super().__init__() + def __init__(self, imaq_name="img0", do_open=True, **kwargs): + super().__init__(**kwargs) lib.initlib() - self.name=name + self.imaq_name=imaq_name self.ifid=None self.sid=None - self._buffer_mgr=BufferManager() + self._buffer_mgr=camera.ChunkBufferManager() self._max_nbuff=None self._start_acq_count=None self._triggers_in={} @@ -61,20 +65,23 @@ def __init__(self, name="img0"): self._serial_term_write="" self._serial_datatype="bytes" - self.open() - self._add_info_variable("device_info",self.get_device_info) self._add_info_variable("grabber_attributes",self.get_all_grabber_attribute_values,priority=-5) + self._add_settings_variable("serial_params",self.get_serial_params,self.setup_serial_params) self._add_settings_variable("triggers_in_cfg",self._get_triggers_in_cfg,self._set_triggers_in_cfg) self._add_settings_variable("triggers_out_cfg",self._get_triggers_out_cfg,self._set_triggers_out_cfg) + if do_open: + self.open() + def _get_connection_parameters(self): - return self.name + return self.imaq_name def open(self): """Open connection to the camera""" + super().open() if self.sid is None: - self.ifid=lib.imgInterfaceOpen(self.name) + self.ifid=lib.imgInterfaceOpen(self.imaq_name) self.sid=lib.imgSessionOpen(self.ifid) self._check_grabber_attributes() def close(self): @@ -85,6 +92,7 @@ def close(self): self.sid=None lib.imgClose(self.ifid,1) self.ifid=None + super().close() def reset(self): """Reset connection to the camera""" if self.ifid is not None: @@ -193,9 +201,11 @@ def _get_data_dimensions_rc(self): def get_detector_size(self): _,_,mw,mh=lib.imgSessionFitROI(self.sid,0,0,0,2**31-1,2**31-1) return mw,mh + get_grabber_detector_size=get_detector_size def get_roi(self): t,l,h,w=lib.imgSessionGetROI(self.sid) return l,l+w,t,t+h + get_grabber_roi=get_roi @camera.acqcleared def set_roi(self, hstart=0, hend=None, vstart=0, vend=None): det_size=self.get_detector_size() @@ -207,12 +217,14 @@ def set_roi(self, hstart=0, hend=None, vstart=0, vend=None): if lib.imgSessionGetROI(self.sid)!=fit_roi: lib.imgSessionConfigureROI(self.sid,*fit_roi) return self.get_roi() + set_grabber_roi=set_roi def get_roi_limits(self, hbin=1, vbin=1): minp=lib.imgSessionFitROI(self.sid,0,0,0,1,1) detsize=self.get_detector_size() hlim=camera.TAxisROILimit(minp[2],detsize[0],1,1,1) vlim=camera.TAxisROILimit(minp[3],detsize[1],1,1,1) return hlim,vlim + get_grabber_roi_limits=get_roi_limits _trig_pol={ "high":niimaq_lib.IMG_TRIG_POL.IMG_TRIG_POLAR_ACTIVEH, "low":niimaq_lib.IMG_TRIG_POL.IMG_TRIG_POLAR_ACTIVEL} @@ -330,6 +342,9 @@ def setup_serial_params(self, write_term="", datatype="bytes"): """ self._serial_term_write=write_term self._serial_datatype=datatype + def get_serial_params(self): + """Return serial parameters as a tuple ``(write_term, datatype)``""" + return self._serial_term_write,self._serial_datatype def serial_write(self, msg, timeout=3., term=None): """ Write message into CameraLink serial port. @@ -444,7 +459,7 @@ def setup_acquisition(self, mode="sequence", nframes=100): self._max_nbuff=self._find_max_nbuff() nframes=min(nframes,self._max_nbuff) self._buffer_mgr.allocate(nframes,self._get_buffer_size()) - cbuffs=self._buffer_mgr.get_ctypes_frames_list() + cbuffs=self._buffer_mgr.get_ctypes_frames_list(ctype=ctypes.c_char_p) self._set_triggers_in_cfg(self._get_triggers_in_cfg()) # reapply trigger settings if mode=="sequence": lib.imgRingSetup(self.sid,len(cbuffs),cbuffs,0,0) @@ -596,74 +611,12 @@ def _get_grab_acquisition_parameters(self, nframes, buff_size): - - -class BufferManager: +class IMAQCamera(IMAQFrameGrabber): """ - Buffer manager, which takes care of creating and removing the buffer chunks, and reading out some parts of them. - + Generic IMAQ camera interface. + Args: - chunk_size: the minimal size of a single buffer chunk (continuous memory segment potentially containing several frames). + name: interface name (can be learned by :func:`list_cameras`; usually, but not always, starts with ``"cam"`` or ``"img"``) """ - def __init__(self, chunk_size=2**20): - self.chunks=None - self.nframes=None - self.frame_size=None - self.frames_per_chunk=None - self.chunk_size=chunk_size - - def __bool__(self): - return self.chunks is not None - def get_ctypes_frames_list(self): - """Get stored buffers as a ctypes array for pointers""" - if self.chunks: - cbuffs=(ctypes.c_char_p*self.nframes)() - for i,b in enumerate(self.chunks): - for j in range(self.frames_per_chunk): - nb=i*self.frames_per_chunk+j - if nb0: - ch=self.chunks[ibuff] - chunk_frames=self.frames_per_chunk if ibuffself.max: + value=self.max + else: + inc=self.inc + if inc>0: + value=((value-self.min)//inc)*inc+self.min + return value + + def get_value(self, enum_as_str=True): + """ + Get attribute value. + + If ``enum_as_str==True``, return enum-style values as strings; otherwise, return corresponding integer values. + """ + if self.system: + val=self._get_property(FgProperty.PROP_ID_VALUE) + if self.kind in ["i32","i64","u32","u64"]: + if val.startswith(b"0x"): + val=int(val[2:],base=16) + val=int(val) + elif self.kind=="f64": + val=float(val) + else: + val=lib.Fg_getParameterWithType_auto(self.fg,self.aid,self.siso_port,ptype=self._attr_type_n) + if self.kind=="str": + val=py3.as_str(val) + if enum_as_str and self.values: + val=self.values.get(val,val) + return val + def set_value(self, value, truncate=True): + """ + Get attribute value. + + If ``truncate==True``, automatically truncate value to lie within allowed range. + """ + if self.system: + raise ValueError("system property {} can not be set".format(self.name)) + if truncate: + value=self.truncate_value(value) + if self._ivalues: + value=self._ivalues.get(value,value) + lib.Fg_setParameterWithType_auto(self.fg,self.aid,value,self.siso_port,ptype=self._attr_type_n) + + def __repr__(self): + return "{}(name='{}', kind='{}')".format(self.__class__.__name__,self.name,self.kind) + + + + + + + +TDeviceInfo=collections.namedtuple("TDeviceInfo",["applet_info","system_info","software_version"]) +class SiliconSoftwareFrameGrabber(camera.IGrabberAttributeCamera,camera.IROICamera): + """ + Generic Silicon Software frame grabber interface. + + Compared to :class:`SiliconSoftwareCamera`, has more permissive initialization arguments, + which simplifies its use as a base class for expanded cameras. + + Args: + siso_board: board index, starting from 0; available boards can be learned by :func:`list_boards` + siso_applet: applet name, which can be learned by :func:`list_applets`; + usually, a simple applet like ``"DualLineGray16"`` or ``"MediumLineGray16`` are most appropriate; + can be either an applet name, or a direct path to the applet DLL + siso_port: port number, if several ports are supported by the camera and the applet + siso_detector_size: if not ``None``, can specify the maximal detector size; + by default, use the maximal available for the frame grabber (usually, 16384x16384) + """ + Error=SiliconSoftwareError + TimeoutError=SiliconSoftwareTimeoutError + def __init__(self, siso_board=0, siso_applet="DualAreaGray16", siso_port=0, siso_detector_size=None, do_open=True, **kwargs): + super().__init__(**kwargs) + lib.initlib() + self.siso_board=siso_board + self.siso_applet=siso_applet + self.siso_port=siso_port + try: + self.siso_applet_path=get_applet_info(self.siso_board,name=self.siso_applet).path + except KeyError: + self.siso_applet_path=self.siso_applet + self.fg=None + self._buffer_mgr=camera.ChunkBufferManager() + self._buffer_head=None + self._frame_merge=1 + self._acq_in_progress=False + self._system_info=None + self.siso_detector_size=siso_detector_size + + self._add_info_variable("device_info",self.get_device_info) + + if do_open: + self.open() + + + def _normalize_grabber_attribute_name(self, name): + if name.startswith("FG_"): + return name[3:] + return name + def _list_grabber_attributes(self): + pnum=lib.Fg_getNrOfParameter(self.fg) + attrs=[FGrabAttribute(self.fg,lib.Fg_getParameterId(self.fg,i),port=self.siso_port) for i in range(pnum)] + return [a for a in attrs if a.kind in ["i32","u32","i64","u64","f64","str"]] + def _get_connection_parameters(self): + return self.siso_board,self.siso_applet,self.siso_port,self.siso_applet_path + def open(self): + """Open connection to the camera""" + super().open() + if self.fg is None: + self.fg=lib.Fg_Init(self.siso_applet_path,self.siso_board) + self._update_grabber_attributes() + def close(self): + """Close connection to the camera""" + if self.fg is not None: + self.clear_acquisition() + try: + self.fg=lib.Fg_FreeGrabber(self.fg) + finally: + self.fg=None + super().close() + def is_opened(self): + """Check if the device is connected""" + return self.fg is not None + + + def get_all_grabber_attribute_values(self, root="", **kwargs): + grabber_attributes=self.get_grabber_attribute(root) + values=dictionary.Dictionary() + for n,a in grabber_attributes.items(): + try: + values[n]=a.get_value(**kwargs) + except SiliconSoftwareError: + pass + return values + def set_all_grabber_attribute_values(self, settings, root="", **kwargs): + grabber_attributes=self.get_grabber_attribute(root) + settings=dictionary.as_dict(settings,style="flat",copy=False) + for k,v in settings.items(): + k=self._normalize_grabber_attribute_name(k) + if k in grabber_attributes: + try: + grabber_attributes[k].set_value(v,**kwargs) + except SiliconSoftwareError: + pass + + def get_system_info(self): + """Get the dictionary with all system information parameters""" + if self._system_info is None: + self._system_info={} + for aid in Fg_Info_Selector: + try: + attr=FGrabAttribute(self.fg,aid.value,system=True) + self._system_info[attr.name]=attr.get_value() + except SiliconSoftwareError: + pass + return self._system_info + def get_genicam_info_xml(self): + """Get description in Genicam-compatible XML format""" + return py3.as_str(lib.Fg_getParameterInfoXML(self.fg,self.siso_port)) + + def get_device_info(self): + """ + Get camera model data. + + Return tuple ``(applet_info, system_info, software_version)`` with the board serial number and an the interface type (e.g., ``"1430"`` for NI PCIe-1430) + """ + system_info=self.get_system_info() + applet_info=get_applet_info(self.siso_board,path=self.siso_applet_path) + software_version=py3.as_str(lib.Fg_getSWVersion()) + return TDeviceInfo(applet_info,system_info,software_version) + + + def set_frame_merge(self, frame_merge=1): + if self._frame_merge!=frame_merge: + roi=self.get_grabber_roi() + self.clear_acquisition() + self._frame_merge=frame_merge + self.set_grabber_roi(*roi) + def _get_data_dimensions_rc(self): + return self.gav["HEIGHT"]//self._frame_merge,self.gav["WIDTH"] + def get_detector_size(self): + return self.siso_detector_size or (self.get_grabber_attribute("WIDTH").max,self.get_grabber_attribute("HEIGHT").max) + get_grabber_detector_size=get_detector_size + def get_roi(self): + w,h=self.gav["WIDTH"],self.gav["HEIGHT"]//self._frame_merge + l,t=self.gav["XOFFSET"],self.gav["YOFFSET"] + return l,l+w,t,t+h + get_grabber_roi=get_roi + @camera.acqcleared + def set_roi(self, hstart=0, hend=None, vstart=0, vend=None): + hlim,vlim=self.get_grabber_roi_limits() + if hend is None: + hend=hlim.max + if vend is None: + vend=vlim.max + hstart,hend=self._truncate_roi_axis((hstart,hend),hlim) + vstart,vend=self._truncate_roi_axis((vstart,vend),vlim) + if self._frame_merge!=1 and vstart!=0: + raise ValueError("frame merging is only supported with full vertical frame size") + self.gav["XOFFSET"]=0 + self.gav["WIDTH"]=hend-hstart + self.gav["XOFFSET"]=hstart + self.gav["YOFFSET"]=0 + self.gav["HEIGHT"]=(vend-vstart)*self._frame_merge + self.gav["YOFFSET"]=vstart # non-zero only if self._frame_merge==1 + return self.get_grabber_roi() + set_grabber_roi=set_roi + def get_roi_limits(self, hbin=1, vbin=1): + w,h=self.get_grabber_attribute("WIDTH"),self.get_grabber_attribute("HEIGHT") + x,y=self.get_grabber_attribute("XOFFSET"),self.get_grabber_attribute("YOFFSET") + detsize=self.get_detector_size() + hlim=camera.TAxisROILimit(w.min,detsize[1],x.inc,w.inc,1) + vlim=camera.TAxisROILimit(h.min,detsize[0],y.inc,h.inc,1) + return hlim,vlim + get_grabber_roi_limits=get_roi_limits + + + def _get_acquired_frames(self): + if not self.acquisition_in_progress(): + return None + return lib.Fg_getStatusEx(self.fg,FG_GETSTATUS.NUMBER_OF_GRABBED_IMAGES,0,self.siso_port,self._buffer_head)*self._frame_merge + + def _setup_buffers(self, nframes): + self._clear_buffers() + nbuff=(nframes-1)//self._frame_merge+1 + buffer_size=self._get_buffer_size() + self._buffer_mgr.allocate(nbuff,buffer_size) + self._buffer_head=lib.Fg_AllocMemHead(self.fg,nbuff*buffer_size,nbuff) + cbuffs=self._buffer_mgr.get_ctypes_frames_list(ctype=ctypes.c_void_p) + for i,b in enumerate(cbuffs): + lib.Fg_AddMem(self.fg,b,buffer_size,i,self._buffer_head) + def _clear_buffers(self): + if self._buffer_mgr: + cbuffs=self._buffer_mgr.get_ctypes_frames_list(ctype=ctypes.c_void_p) + for i,_ in enumerate(cbuffs): + lib.Fg_DelMem(self.fg,self._buffer_head,i) + lib.Fg_FreeMemHead(self.fg,self._buffer_head) + self._buffer_head=None + self._buffer_mgr.deallocate() + def _ensure_buffers(self): + nbuff=self._buffer_mgr.nframes + buffer_size=self._get_buffer_size() + if buffer_size!=self._buffer_mgr.frame_size: + self._setup_buffers(nbuff) + + @interface.use_parameters(mode="acq_mode") + def setup_acquisition(self, mode="sequence", nframes=100): + """ + Setup acquisition mode. + + `mode` can be either ``"snap"`` (single frame or a fixed number of frames) or ``"sequence"`` (continuous acquisition). + (note that :meth:`.IMAQCamera.acquisition_in_progress` would still return ``True`` in this case, even though new frames are no longer acquired). + `nframes` sets up number of frame buffers. + """ + self.clear_acquisition() + super().setup_acquisition(mode=mode,nframes=nframes) + self._setup_buffers(nframes) + def clear_acquisition(self): + """Clear all acquisition details and free all buffers""" + if self._acq_params: + self.stop_acquisition() + self._clear_buffers() + super().clear_acquisition() + def start_acquisition(self, *args, **kwargs): + self.stop_acquisition() + super().start_acquisition(*args,**kwargs) + self._ensure_buffers() + mode=self._acq_params["mode"] + nframes=self._acq_params["nframes"] if mode=="snap" else -1 + self._frame_counter.reset(self._buffer_mgr.nframes*self._frame_merge) + lib.Fg_AcquireEx(self.fg,self.siso_port,nframes,FG_ACQ.ACQ_STANDARD,self._buffer_head) + self._acq_in_progress=True + def stop_acquisition(self): + if self.acquisition_in_progress(): + self._frame_counter.update_acquired_frames(self._get_acquired_frames()) + lib.Fg_stopAcquireEx(self.fg,self.siso_port,self._buffer_head,FG_ACQ.ACQ_STANDARD) + self._acq_in_progress=False + def acquisition_in_progress(self): + return self._acq_in_progress + + + + def _wait_for_next_frame(self, timeout=20., idx=None): + if timeout is None or timeout>0.1: + timeout=0.1 + try: + if idx is None: + idx=self._frame_counter.last_acquired_frame+1 + lib.Fg_getLastPicNumberBlockingEx(self.fg,idx,self.siso_port,int(timeout*1000),self._buffer_head) + except SIFgrabLibError as err: + if err.code==FG_STATUS.FG_TIMEOUT_ERR: + pass + elif err.code in [FG_STATUS.FG_INVALID_MEMORY,FG_STATUS.FG_TRANSFER_NOT_ACTIVE] and not self.acquisition_in_progress(): + raise + + + def _get_buffer_bpp(self): + bpp=self.gav["PIXELDEPTH"] + return (bpp-1)//8+1 + def _get_buffer_dtype(self): + return "rng[0] else [] + if not peek: + self._frame_counter.advance_read_frames(rng) + return rng[0],skipped_frames,raw_frames + + def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", return_info=None, fastbuff=False): + """ + Read multiple images specified by `rng` (by default, all un-read images). + + If `rng` is specified, it is a tuple ``(first, last)`` with images range (first inclusive). + If no new frames are available, return an empty list; if no acquisition is running, return ``None``. + If ``peek==True``, return images but not mark them as read. + `missing_frame` determines what to do with frames which are out of range (missing or lost): + can be ``"none"`` (replacing them with ``None``), ``"zero"`` (replacing them with zero-filled frame), or ``"skip"`` (skipping them). + If ``return_info==True``, return tuple ``(frames, infos)``, where ``infos`` is a list of ``TFrameInfo`` single-element tuples + containing frame index; if some frames are missing and ``missing_frame!="skip"``, the corresponding frame info is ``None``. + If ``fastbuff==False``, return a list of individual frames (2D numpy arrays). + Otherwise, return a list of 'chunks', which are 3D numpy arrays containing several frames; + in this case, ``frame_info`` will only have one entry per chunk corresponding to the first frame in the chunk. + Using ``fastbuff`` results in faster operation at high frame rates (>~1kFPS), at the expense of a more complicated frame processing in the following code. + """ + funcargparse.check_parameter_range(missing_frame,"missing_frame",["none","zero","skip"]) + if fastbuff and missing_frame=="none": + raise ValueError("'none' missing frames mode is not supported if fastbuff==True") + first_frame,skipped_frames,raw_data=self._read_multiple_images_raw(rng=rng,peek=peek) + if raw_data is None: + return (None,None) if return_info else None + dim=self._get_data_dimensions_rc() + dt=self._get_buffer_dtype() + parsed_data=[self._parse_buffer(b,nframes=n) for n,b in raw_data] + if not fastbuff: + parsed_data=[f for chunk in parsed_data for f in chunk] + if return_info: + if fastbuff: + frame_info=[] + idx=first_frame + for d in parsed_data: + frame_info.append(self._convert_frame_info(self._TFrameInfo(idx))) + idx+=len(d) + else: + frame_info=[self._TFrameInfo(first_frame+n) for n in range(len(parsed_data))] + if skipped_frames and missing_frame!="skip": + if fastbuff: # only missing_frame=="zero" is possible + parsed_data=[np.zeros((skipped_frames,)+dim,dtype=dt)]+parsed_data + else: + if missing_frame=="zero": + parsed_data=list(np.zeros((skipped_frames,)+dim,dtype=dt))+parsed_data + else: + parsed_data=[None]*skipped_frames+parsed_data + if return_info: + frame_info=[None]*(1 if fastbuff else skipped_frames)+frame_info + parsed_data=self._convert_indexing(parsed_data,"rct",axes=(-2,-1)) + return (parsed_data,frame_info) if return_info else parsed_data + + + + + + +class SiliconSoftwareCamera(SiliconSoftwareFrameGrabber): + """ + Generic Silicon Software frame grabber interface. + + Args: + board: board index, starting from 0; available boards can be learned by :func:`list_boards` + applet: applet name, which can be learned by :func:`list_applets`; + usually, a simple applet like ``"DualLineGray16"`` or ``"MediumLineGray16`` are most appropriate; + can be either an applet name, or a direct path to the applet DLL + port: port number, if several ports are supported by the camera and the applet + detector_size: if not ``None``, can specify the maximal detector size; + by default, use the maximal available for the frame grabber (usually, 16384x16384) + """ + def __init__(self, board, applet, port=0, detector_size=None): + super().__init__(siso_board=board,siso_applet=applet,siso_port=port,siso_detector_size=detector_size) \ No newline at end of file diff --git a/pylablib/devices/SiliconSoftware/fgrab_define_defs.py b/pylablib/devices/SiliconSoftware/fgrab_define_defs.py new file mode 100644 index 0000000..388cb41 --- /dev/null +++ b/pylablib/devices/SiliconSoftware/fgrab_define_defs.py @@ -0,0 +1,1176 @@ +########## This file is generated automatically based on fgrab_define.h ########## + +# pylint: disable=unused-import, unused-argument, wrong-spelling-in-comment + + +import ctypes +import enum +from ...core.utils import ctypes_wrap + + + + +def _int32(v): return (v+0x80000000)%0x100000000-0x80000000 + + + + +##### DEFINE GROUPS ##### + + +### CONST ### +FG_NO = _int32(0) +FG_YES = _int32(1) +FG_LOW = _int32(0) +FG_HIGH = _int32(1) +HIGH_ON_ZERO_LOW = _int32(1) +HIGH_ON_ZERO_HIGH = _int32(0) +FG_FALSE = _int32(0) +FG_TRUE = _int32(1) +FG_FALLING = _int32(1) +FG_RISING = _int32(0) +FG_ON = _int32(1) +FG_OFF = _int32(0) +FG_ZERO = _int32(0) +FG_ONE = _int32(1) +FG_APPLY = _int32(1) +FG_LEFT_ALIGNED = _int32(1) +FG_RIGHT_ALIGNED = _int32(0) +FG_SAVE_LUT_TO_FILE = _int32(1) +FG_LOAD_LUT_FROM_FILE = _int32(0) +FG_MSB = _int32(0) +FG_LSB = _int32(1) +MAX_BUF_NR = _int32(1048576) +GRAB_INFINITE = _int32(-1) +GRAB_ALL_BUFFERS = _int32(-2) +ATM_SYNCHRONIZED = _int32(3) +FG_SINGLE = _int32(1) +FG_DOUBLE = _int32(2) +FILTER_X1 = _int32(1) +FILTER_X2 = _int32(2) +FILTER_X4 = _int32(3) +HIGH_ACTIVE = _int32(0) +LOW_ACTIVE = _int32(1) +FG_CUSTOM_BIT_SHIFT_MODE = _int32(2) +ASYNC_TRIGGER_MULTIFRAME = _int32(8) +CAMERA_FVAL_MULTIFRAME = _int32(9) +FG_PULSE_IMMEDIATE = _int32(1) +FG_PULSE_WIDTH = _int32(23) + + +class FG_BIT(enum.IntEnum): + FG_0_BIT = _int32(0) + FG_1_BIT = _int32(1) + FG_2_BIT = _int32(2) + FG_3_BIT = _int32(3) + FG_4_BIT = _int32(4) + FG_5_BIT = _int32(5) + FG_6_BIT = _int32(6) + FG_7_BIT = _int32(7) + FG_8_BIT = _int32(8) + FG_9_BIT = _int32(9) + FG_10_BIT = _int32(10) + FG_11_BIT = _int32(11) + FG_12_BIT = _int32(12) + FG_13_BIT = _int32(13) + FG_14_BIT = _int32(14) + FG_15_BIT = _int32(15) + FG_16_BIT = _int32(16) + FG_17_BIT = _int32(17) + FG_18_BIT = _int32(18) + FG_19_BIT = _int32(19) + FG_20_BIT = _int32(20) + FG_21_BIT = _int32(21) + FG_22_BIT = _int32(22) + FG_23_BIT = _int32(23) + FG_24_BIT = _int32(24) + FG_25_BIT = _int32(25) + FG_26_BIT = _int32(26) + FG_27_BIT = _int32(27) + FG_28_BIT = _int32(28) + FG_29_BIT = _int32(29) + FG_30_BIT = _int32(30) + FG_31_BIT = _int32(31) + FG_32_BIT = _int32(32) + FG_36_BIT = _int32(36) + FG_48_BIT = _int32(48) +dFG_BIT={a.name:a.value for a in FG_BIT} +drFG_BIT={a.value:a.name for a in FG_BIT} + + +class FG_MODE(enum.IntEnum): + CONTMODE = _int32(0x10) + HANDSHAKEMODE = _int32(0x20) + BLOCKINGMODE = _int32(0x20) + PULSEMODE = _int32(0x30) +dFG_MODE={a.name:a.value for a in FG_MODE} +drFG_MODE={a.value:a.name for a in FG_MODE} + + +class FG_IMGFMT(enum.IntEnum): + FG_GRAY = _int32(3) + FG_GRAY_PLUS_PICNR = _int32(30) + FG_GRAY16 = _int32(1) + FG_GRAY16_PLUS_PICNR = _int32(10) + FG_GRAY10 = _int32(21) + FG_GRAY12 = _int32(22) + FG_GRAY14 = _int32(23) + FG_GRAY32 = _int32(20) + FG_COL24 = _int32(2) + FG_COL32 = _int32(4) + FG_COL30 = _int32(5) + FG_COL36 = _int32(7) + FG_COL42 = _int32(9) + FG_COL48 = _int32(6) + FG_RGBX32 = _int32(408) + FG_RGBX40 = _int32(410) + FG_RGBX48 = _int32(412) + FG_RGBX56 = _int32(414) + FG_RGBX64 = _int32(416) + FG_BINARY = _int32(8) + FG_RAW = _int32(50) +dFG_IMGFMT={a.name:a.value for a in FG_IMGFMT} +drFG_IMGFMT={a.value:a.name for a in FG_IMGFMT} + + +class FG_SYNC(enum.IntEnum): + FG_INIT_LIBRARIES_SINGLE = _int32(0) + FG_INIT_LIBRARIES_MASTER = _int32(1) + FG_INIT_LIBRARIES_SLAVE = _int32(2) + FG_INIT_LIBRARIES_WAIT_FOR_SERVICE = _int32(0x004) + FG_INIT_LIBRARIES_SEQUENTIAL = _int32(0x008) + FG_INIT_LIBRARIES_AUTOSTART_ON_INIT = _int32(0x010) +dFG_SYNC={a.name:a.value for a in FG_SYNC} +drFG_SYNC={a.value:a.name for a in FG_SYNC} + + +class FG_ACQ(enum.IntEnum): + ACQ_STANDARD = _int32(0x1) + ACQ_BLOCK = _int32(0x2) + ACQ_MODE_MASK = _int32(0xffff) + ACQ_NO_AUTOSTOP = _int32(0x10000) +dFG_ACQ={a.name:a.value for a in FG_ACQ} +drFG_ACQ={a.value:a.name for a in FG_ACQ} + + +class FG_GETSTATUS(enum.IntEnum): + NUMBER_OF_GRABBED_IMAGES = _int32(10) + NUMBER_OF_LOST_IMAGES = _int32(20) + NUMBER_OF_BLOCK_LOST_IMAGES = _int32(30) + NUMBER_OF_BLOCKED_IMAGES = _int32(40) + NUMBER_OF_ACT_IMAGE = _int32(50) + NUMBER_OF_LAST_IMAGE = _int32(60) + NUMBER_OF_NEXT_IMAGE = _int32(70) + NUMBER_OF_IMAGES_IN_PROGRESS = _int32(80) + BUFFER_STATUS = _int32(90) + GRAB_ACTIVE = _int32(100) +dFG_GETSTATUS={a.name:a.value for a in FG_GETSTATUS} +drFG_GETSTATUS={a.value:a.name for a in FG_GETSTATUS} + + +class FG_SETSTATUS(enum.IntEnum): + FG_BLOCK = _int32(0x100) + FG_UNBLOCK = _int32(0x200) + FG_UNBLOCK_ALL = _int32(0x220) + SEL_ACT_IMAGE = _int32(200) + SEL_LAST_IMAGE = _int32(210) + SEL_NEXT_IMAGE = _int32(220) + SEL_NUMBER = _int32(230) + SEL_NEW_IMAGE = _int32(240) +dFG_SETSTATUS={a.name:a.value for a in FG_SETSTATUS} +drFG_SETSTATUS={a.value:a.name for a in FG_SETSTATUS} + + +class FG_PARAM_PROPERTY(enum.IntEnum): + FG_PARAMETER_PROPERTY_ACCESS = _int32(0x80000000) + FG_PARAMETER_PROPERTY_MIN = _int32(0xC0000000) + FG_PARAMETER_PROPERTY_MAX = _int32(0x40000000) + FG_PARAMETER_PROPERTY_STEP = _int32(0xE0000000) + FP_PARAMETER_PROPERTY_ACCESS_READ = _int32(0x1) + FP_PARAMETER_PROPERTY_ACCESS_WRITE = _int32(0x2) + FP_PARAMETER_PROPERTY_ACCESS_MODIFY = _int32(0x4) + FP_PARAMETER_PROPERTY_ACCESS_LOCKED = _int32(0x8) +dFG_PARAM_PROPERTY={a.name:a.value for a in FG_PARAM_PROPERTY} +drFG_PARAM_PROPERTY={a.value:a.name for a in FG_PARAM_PROPERTY} + + +class FG_PARAM(enum.IntEnum): + FG_REVNR = _int32(99) + FG_WIDTH = _int32(100) + FG_HEIGHT = _int32(200) + FG_MAXWIDTH = _int32(6100) + FG_MAXHEIGHT = _int32(6200) + FG_ACTIVEPORT = _int32(6300) + FG_XOFFSET = _int32(300) + FG_YOFFSET = _int32(400) + FG_XSHIFT = _int32(500) + FG_TIMEOUT = _int32(600) + FG_APC_STOP_TIMEOUT = _int32(601) + FG_STOP_TIMEOUT = _int32(602) + FG_FORMAT = _int32(700) + FG_CAMSUBTYP = _int32(80) + FG_FRAMESPERSEC = _int32(90) + FG_MAXFRAMESPERSEC = _int32(91) + FG_MINFRAMESPERSEC = _int32(92) + FG_LINESPERSEC = _int32(95) + FG_LINEPERIOD = _int32(96) + FG_LINEPERIODE = _int32(96) + FG_EXPOSURE = _int32(10020) + FG_LINEEXPOSURE = _int32(10030) + FG_HDSYNC = _int32(10050) + FG_PRESCALER = _int32(10050) + FG_LINETRIGGER = _int32(10050) + FG_RS232PARON = _int32(10060) + FG_MTU_SIZE = _int32(200351) + FG_PIXELDEPTH = _int32(4000) + FG_BITALIGNMENT = _int32(4010) + FG_LINEALIGNMENT = _int32(4020) + FG_COLOR_SELECT = _int32(4030) + FG_SWAP_CHANNELS = _int32(200350) + FG_CAMBITWIDTH = _int32(5000) + FG_CAMBITSHIFT = _int32(5010) + FG_CAMERA_WIDTH = _int32(110133) + FG_SHIFTCAMDATARIGHT = _int32(5020) + FG_ROTATECAMDATA = _int32(5020) + FG_USEDVAL = _int32(5025) + FG_SWAPENDIAN = _int32(5028) + FG_MASKCAMDATA = _int32(5030) + FG_ADDOFFSET = _int32(5035) + FG_DROPPEDIMAGEES = _int32(5040) + FG_SENSORREADOUT = _int32(5050) + FG_SENSORREADOUT_TAPS = _int32(5051) + FG_SENSORREADOUT_DIREC = _int32(5052) + FG_TRIGGERMODE = _int32(8100) + FG_LINETRIGGERMODE = _int32(8102) + FG_IMGTRIGGERMODE = _int32(8104) + FG_IMGTRIGGERON = _int32(8106) + FG_TRIGGERINSRC = _int32(8110) + FG_LINETRIGGERINSRC = _int32(8112) + FG_IMGTRIGGERINSRC = _int32(8113) + FG_LINETRIGGERINPOLARITY = _int32(8115) + FG_IMGTRIGGERINPOLARITY = _int32(8116) + FG_TRIGGERINPOLARITY = _int32(8116) + FG_IMGTRIGGERGATEDELAY = _int32(8118) + FG_USEROUT = _int32(8120) + FG_EXSYNCINVERT = _int32(8200) + FG_EXSYNCON = _int32(8300) + FG_EXSYNCDELAY = _int32(8400) + FG_EXSYNCPOLARITY = _int32(8420) + FG_DEADTIME = _int32(8450) + FG_DEADTIME_OFFSET = _int32(8460) + FG_BGRRGBORDER = _int32(8500) + FG_FLASHON = _int32(8600) + FG_SENDSOFTWARETRIGGER = _int32(8800) + FG_SETSOFTWARETRIGGER = _int32(8801) + FG_SOFTWARETRIGGER_QUEUE_FILLLEVEL = _int32(8802) + FG_LINETRIGGERDELAY = _int32(8900) + FG_LIMIT_TRIGGER_PULSES = _int32(8950) + FG_TRIGGERMASTERSYNC = _int32(9000) + FG_SHAFTENCODERINSRC = _int32(9100) + FG_SHAFTENCODERON = _int32(9110) + FG_SHAFTENCODERLEADING = _int32(9120) + FG_SHAFTENCODER_COMPCOUNTER = _int32(9125) + FG_RGB_MAP_RED = _int32(9200) + FG_RGB_MAP_GREEN = _int32(9210) + FG_RGB_MAP_BLUE = _int32(9220) + FG_CAMSTATUS = _int32(2000) + FG_CAMSTATUS_EXTENDED = _int32(2050) + FG_TWOCAMMODEL = _int32(2100) + FG_PORT = _int32(3000) + FG_NR_OF_DMAS = _int32(3050) + FG_TURBO_DMA_MODE = _int32(3051) + FG_NR_OF_CAMS = _int32(3060) + FG_NR_OF_PROCESSES = _int32(3070) + FG_DMA_PORT = _int32(3080) + FG_DMA_STARTED = _int32(3081) + FG_CAM_PORT = _int32(3090) + FG_RESET_GIGE_PORT_0 = _int32(3100) + FG_RESET_GIGE_PORT_1 = _int32(3101) + FG_RESET_GIGE_PORT_2 = _int32(3102) + FG_RESET_GIGE_PORT_3 = _int32(3103) + FG_TRANSFER_LEN = _int32(5210) + FG_STROBEPULSEDELAY = _int32(8700) + FG_STROBEPULSEREDUCE = _int32(8710) + FG_STROBEPULSESRCSEL = _int32(8720) + FG_STROBEPULSEINVERT = _int32(8730) + FG_FLASHTIME = _int32(8740) + FG_FLASHTIME_SYNC = _int32(8750) + FG_CAMERA_LINK_CAMTYPE = _int32(11011) + FG_CAMERA_LINK_CAMTYP = _int32(11011) + FG_CL_CAMTYP = _int32(11011) + FG_CAMTYP = _int32(11011) + FG_GBE_CAMTYPE = _int32(11011) + FG_GBE_CAMTYP = _int32(11011) + FG_CAMERA_LINK_CORE_RESET = _int32(11012) + FG_CAMERA_LINK_PIXEL_CLOCK = _int32(11013) + FG_CAMERA_LINK_PIXEL_CLOCK_X = _int32(11014) + FG_CAMERA_LINK_PIXEL_CLOCK_Y = _int32(11015) + FG_CAMERA_LINK_PIXEL_CLOCK_Z = _int32(11016) + FG_LOOKUPTABLE = _int32(12000) + FG_LUT_FILE = _int32(12010) + FG_LUT_SAVE_LOAD_FILE = _int32(12020) + FG_LUT_ENABLE = _int32(12030) + FG_KNEE_LUT = _int32(12100) + FG_KNEE_LUT_FILE = _int32(12110) + FG_KNEE_LUT_SAVE_LOAD_FILE = _int32(12120) + FG_KNEE_LUT_MODE = _int32(12130) + FG_KNEE_LUT_ACCESS = _int32(12140) + FG_KNEE_LUT_SCALE = _int32(12101) + FG_KNEE_LUT_OFFSET = _int32(12102) + FG_KNEE_LUT_GAMMA = _int32(12103) + FG_KNEE_LUT_INVERT = _int32(12104) + FG_MEDIAN = _int32(12200) + FG_2DSHADINGPARAMETER = _int32(12500) + FG_SCALINGFACTOR_RED = _int32(13000) + FG_SCALINGFACTOR_BLUE = _int32(13010) + FG_BAYERINIT = _int32(13020) + FG_SCALINGFACTOR_GREEN = _int32(13030) + FG_CCSEL = _int32(14000) + FG_CCSEL0 = _int32(14001) + FG_CCSEL1 = _int32(14002) + FG_CCSEL2 = _int32(14003) + FG_CCSEL3 = _int32(14004) + FG_CCSEL_INVERT = _int32(14005) + FG_CCSEL_INVERT0 = _int32(14006) + FG_CCSEL_INVERT1 = _int32(14007) + FG_CCSEL_INVERT2 = _int32(14008) + FG_CCSEL_INVERT3 = _int32(14009) + FG_CC1_SOURCE = _int32(14041) + FG_CC1_POLARITY = _int32(14031) + FG_CC2_SOURCE = _int32(14042) + FG_CC2_POLARITY = _int32(14032) + FG_CC3_SOURCE = _int32(14043) + FG_CC3_POLARITY = _int32(14033) + FG_CC4_SOURCE = _int32(14044) + FG_CC4_POLARITY = _int32(14034) + FG_DIGIO_INPUT = _int32(14010) + FG_DIGIO_OUTPUT = _int32(14020) + FG_IMAGE_TAG = _int32(22000) + FG_TIMESTAMP = _int32(22020) + FG_TIMESTAMP_LONG = _int32(22030) + FG_TIMESTAMP_LONG_FREQUENCY = _int32(22031) + FG_LICENSESTRING0 = _int32(23000) + FG_LICENSESTRING1 = _int32(23010) + FG_LICENSESTRING2 = _int32(23020) + FG_ACCESS_POINTER = _int32(23030) + FG_ROIX = _int32(23100) + FG_ROIY = _int32(23110) + FG_SHADING_SUBIMAGE = _int32(23120) + FG_SHADING_MULTENABLE = _int32(23130) + FG_SHADING_OFFSETENABLE = _int32(23140) + FG_SHADING_SUBENABLE = _int32(23140) + FG_SHADING_MAX_MULT = _int32(23135) + FG_SHADING_RUNSUBIMAGE0 = _int32(23121) + FG_SHADING_RUNSUBIMAGE1 = _int32(23122) + FG_SHADING_RUNSUBIMAGE2 = _int32(23123) + FG_SHADING_RUNSUBIMAGE3 = _int32(23124) + FG_SHADING_ENABLEMULT0 = _int32(23131) + FG_SHADING_ENABLEMULT1 = _int32(23132) + FG_SHADING_ENABLEMULT2 = _int32(23133) + FG_SHADING_ENABLEMULT3 = _int32(23134) + FG_SHADING_ENABLESUB0 = _int32(23141) + FG_SHADING_ENABLESUB1 = _int32(23142) + FG_SHADING_ENABLESUB2 = _int32(23143) + FG_SHADING_ENABLESUB3 = _int32(23144) + FG_SHADING_FPNENABLE = _int32(23150) + FG_SHADING_ENABLEFPN0 = _int32(23151) + FG_SHADING_ENABLEFPN1 = _int32(23152) + FG_SHADING_ENABLEFPN2 = _int32(23153) + FG_SHADING_ENABLEFPN3 = _int32(23154) + FG_SHADING_THRESHOLD0 = _int32(23156) + FG_SHADING_THRESHOLD1 = _int32(23157) + FG_SHADING_THRESHOLD2 = _int32(23158) + FG_SHADING_THRESHOLD3 = _int32(23159) + FG_SHADING_MULTFILE0 = _int32(23160) + FG_SHADING_SUBFILE0 = _int32(23170) + FG_SHADING_FPNFILE0 = _int32(23180) + FG_SHADING_MULTFILE1 = _int32(23210) + FG_SHADING_SUBFILE1 = _int32(23225) + FG_SHADING_FPNFILE1 = _int32(23230) + FG_SHADING_MULTFILE2 = _int32(23240) + FG_SHADING_SUBFILE2 = _int32(23250) + FG_SHADING_FPNFILE2 = _int32(23260) + FG_SHADING_MULTFILE3 = _int32(23270) + FG_SHADING_SUBFILE3 = _int32(23280) + FG_SHADING_FPNFILE3 = _int32(23290) + FG_CONTRAST = _int32(23200) + FG_BRIGHTNESS = _int32(23220) + FG_DOWNSCALE = _int32(24040) + FG_LINE_DOWNSCALE = _int32(24040) + FG_LINE_DOWNSCALEINIT = _int32(24050) + FG_FLASH_POLARITY = _int32(24060) + FG_FLASHDELAY = _int32(8700) + FG_LOAD_SHADINGDATA = _int32(24070) + FG_CLEAR_SHADINGDATA = _int32(24080) + FG_LINESHADINGPARAMETER = _int32(24081) + FG_1DSHADINGPARAMETER = _int32(24081) + FG_LINESHADING_SUB_ENABLE = _int32(24082) + FG_LINESHADING_MULT_ENABLE = _int32(24083) + FG_ENABLEDISABLE_SHADING = _int32(24083) + FG_SHADING_WIDTH = _int32(24089) + FG_AUTO_SHADING_WIDTH = _int32(24090) + FG_WRITE_SHADING_12 = _int32(24091) + FG_LINESHADING_MULT_FILENAME = _int32(24084) + FG_LINESHADING_SUB_FILENAME = _int32(24085) + FG_LINESHADING_LOAD_FROM_FILE = _int32(24086) + FG_LINESHADING_MODE = _int32(24087) + FG_DMASTATUS = _int32(24092) + FG_LINEVALID_SIGNAL_COUNT = _int32(24093) + FG_FRAMEVALID_SIGNAL_COUNT = _int32(24094) + FG_1DSHADING_FILE = _int32(24084) + FG_LOAD_1DSHADINGDATA = _int32(24086) + FG_BURSTLENGTH = _int32(24097) + FG_SUPERFRAME = _int32(24098) + FG_PLX_CLK = _int32(24102) + FG_MEASURED_PCIE_CLK = _int32(24103) + FG_FPGA_CLK = _int32(24104) + FG_HAP_FILE = _int32(24108) + FG_GLOBAL_ACCESS = _int32(24110) + FG_DOC_URL = _int32(24112) + FG_PARAM_DESCR = _int32(24114) + FG_REG_VALUE_STRING = _int32(24115) + FG_CAMPORT_CONFIG = _int32(30000) + FG_CAMERA_TYPE = _int32(30001) + FG_COLOR_FLAVOUR = _int32(30002) + FG_GEN_ENABLE = _int32(30099) + FG_GEN_PASSIVE = _int32(30100) + FG_GEN_ACTIVE = _int32(30101) + FG_GEN_WIDTH = _int32(30102) + FG_GEN_LINE_WIDTH = _int32(30103) + FG_GEN_HEIGHT = _int32(30104) + FG_GEN_START = _int32(30113) + FG_GEN_LINE_GAP = _int32(30105) + FG_GEN_FREQ = _int32(30106) + FG_GEN_ACCURACY = _int32(30107) + FG_GEN_ROLL = _int32(30112) + FG_GEN_TAP1 = _int32(30108) + FG_GEN_TAP2 = _int32(30109) + FG_GEN_TAP3 = _int32(30110) + FG_GEN_TAP4 = _int32(30111) + FG_CAMERASIMULATOR_ENABLE = _int32(30099) + FG_CAMERASIMULATOR_WIDTH = _int32(30102) + FG_CAMERASIMULATOR_HEIGHT = _int32(200322) + FG_CAMERASIMULATOR_LINE_GAP = _int32(30105) + FG_CAMERASIMULATOR_FRAME_GAP = _int32(200325) + FG_CAMERASIMULATOR_PATTERN = _int32(200326) + FG_CAMERASIMULATOR_ROLL = _int32(200327) + FG_CAMERASIMULATOR_SELECT_MODE = _int32(200328) + FG_CAMERASIMULATOR_PIXEL_FREQUENCY = _int32(30106) + FG_CAMERASIMULATOR_LINERATE = _int32(200329) + FG_CAMERASIMULATOR_FRAMERATE = _int32(200352) + FG_CAMERASIMULATOR_ACTIVE = _int32(30101) + FG_CAMERASIMULATOR_PASSIVE = _int32(30100) + FG_CAMERASIMULATOR_TRIGGER_MODE = _int32(200355) + FG_CAMERASIMULATOR_PATTERN_OFFSET = _int32(200356) + FG_CAMERASIMULATOR_FPS = _int32(200358) + FG_APPLET_ID = _int32(24010) + FG_APPLET_VERSION = _int32(24020) + FG_APPLET_REVISION = _int32(24030) + FG_APPLET_BUILD_TIME = _int32(24011) + FG_DESIGNCLK = _int32(24040) + FG_ALL = _int32(24050) + FG_THRESHOLD_H_MIN = _int32(25000) + FG_THRESHOLD_H_MAX = _int32(25010) + FG_THRESHOLD_S_MIN = _int32(25020) + FG_THRESHOLD_S_MAX = _int32(25030) + FG_THRESHOLD_I_MIN = _int32(25040) + FG_THRESHOLD_I_MAX = _int32(25050) + FG_DO_THRESHOLD_S = _int32(25060) + FG_DO_THRESHOLD_I = _int32(25070) + FG_SHADING_H = _int32(25080) + FG_SHADING_S = _int32(25090) + FG_SHADING_I = _int32(25100) + FG_FASTCONFIG_SEQUENCE = _int32(30010) + FG_FASTCONFIG_PAGECMD = _int32(30020) + FG_FASTCONFIG_PAGECMD_PTR = _int32(30030) + FG_FASTCONFIG_PULSEDIGIO = _int32(30040) + FG_IMG_SELECT_PERIOD = _int32(25110) + FG_IMG_SELECT = _int32(25111) + FG_NROFEXTERN_TRIGGER = _int32(30110) + FG_ACTIVATE_EXTERN_TRIGGER = _int32(30120) + FG_READ_EXTERN_TRIGGER = _int32(30130) + FG_NB_QUAD_IMG = _int32(30300) + FG_NB_STD_IMG = _int32(30310) + FG_BOARD_INFORMATION = _int32(42042) + FG_LOGGING = _int32(43010) + FG_LOG_FLUSH = _int32(43020) + FG_LOG_CONSOLE = _int32(43030) + FG_CREATE_DUMP = _int32(43040) + FG_CABLE_SELECT = _int32(1001010) + FG_IMAGE_ENABLE = _int32(1001020) + FG_STAT_ENABLE = _int32(1001030) + FG_MIN_DX = _int32(1001040) + FG_THR1 = _int32(1001050) + FG_THR2 = _int32(1001060) + FG_MEDIAN_ON = _int32(1001070) + FG_DMA_WRITE = _int32(1001080) + FG_FAST_CONFIG = _int32(1001090) + FG_SYNC = _int32(1001100) + FG_NODMA1IR = _int32(1001110) + FG_FILLLEVEL = _int32(110086) + FG_OVERFLOW = _int32(110087) + FG_NOISEFILTER = _int32(110016) + FG_LUT_TYPE = _int32(110017) + FG_LUT_CUSTOM_FILE = _int32(300000) + FG_LUT_SAVE_FILE = _int32(110021) + FG_PROCESSING_GAIN = _int32(300002) + FG_PROCESSING_GAMMA = _int32(300003) + FG_PROCESSING_OFFSET = _int32(300004) + FG_PROCESSING_INVERT = _int32(300005) + FG_LUT_IMPLEMENTATION_TYPE = _int32(300006) + FG_SHADING_GAIN_ENABLE = _int32(300100) + FG_SHADING_GRAY_FILENAME = _int32(300101) + FG_SHADING_OFFSET_ENABLE = _int32(300102) + FG_SHADING_BLACK_FILENAME = _int32(300103) + FG_SHADING_GAIN_CORRECTION_MODE = _int32(300106) + FG_SHADING_APPLY_SETTINGS = _int32(300107) + FG_SHADING_GAIN_NORMALIZATION_VALUE = _int32(300108) + FG_AREATRIGGERMODE = _int32(300200) + FG_TRIGGERSTATE = _int32(300201) + FG_TRIGGER_FRAMESPERSECOND = _int32(90) + FG_TRIGGER_EXCEEDED_PERIOD_LIMITS = _int32(300202) + FG_TRIGGER_EXCEEDED_PERIOD_LIMITS_CLEAR = _int32(300203) + FG_TRIGGERIN_DEBOUNCE = _int32(300204) + FG_TRIGGERIN_SRC = _int32(8110) + FG_TRIGGERIN_POLARITY = _int32(8116) + FG_SOFTWARETRIGGER_IS_BUSY = _int32(110075) + FG_TRIGGERIN_DOWNSCALE = _int32(300205) + FG_TRIGGERIN_DOWNSCALE_PHASE = _int32(300206) + FG_TRIGGERIN_STATS_PULSECOUNT = _int32(300207) + FG_TRIGGERIN_STATS_PULSECOUNT_CLEAR = _int32(300208) + FG_TRIGGERIN_STATS_FREQUENCY = _int32(300209) + FG_TRIGGERIN_STATS_MINFREQUENCY = _int32(300210) + FG_TRIGGERIN_STATS_MAXFREQUENCY = _int32(300211) + FG_TRIGGERIN_STATS_MINMAXFREQUENCY_CLEAR = _int32(300212) + FG_TRIGGER_MULTIPLY_PULSES = _int32(300213) + FG_TRIGGERQUEUE_MODE = _int32(300214) + FG_TRIGGERQUEUE_FILLLEVEL = _int32(300215) + FG_TRIGGER_PULSEFORMGEN0_DOWNSCALE = _int32(300216) + FG_TRIGGER_PULSEFORMGEN0_DOWNSCALE_PHASE = _int32(300217) + FG_TRIGGER_PULSEFORMGEN0_DELAY = _int32(300218) + FG_TRIGGER_PULSEFORMGEN0_WIDTH = _int32(300219) + FG_TRIGGER_PULSEFORMGEN1_DOWNSCALE = _int32(300220) + FG_TRIGGER_PULSEFORMGEN1_DOWNSCALE_PHASE = _int32(300221) + FG_TRIGGER_PULSEFORMGEN1_DELAY = _int32(300222) + FG_TRIGGER_PULSEFORMGEN1_WIDTH = _int32(300223) + FG_TRIGGER_PULSEFORMGEN2_DOWNSCALE = _int32(300224) + FG_TRIGGER_PULSEFORMGEN2_DOWNSCALE_PHASE = _int32(300225) + FG_TRIGGER_PULSEFORMGEN2_DELAY = _int32(300226) + FG_TRIGGER_PULSEFORMGEN2_WIDTH = _int32(300227) + FG_TRIGGER_PULSEFORMGEN3_DOWNSCALE = _int32(300228) + FG_TRIGGER_PULSEFORMGEN3_DOWNSCALE_PHASE = _int32(300229) + FG_TRIGGER_PULSEFORMGEN3_DELAY = _int32(300230) + FG_TRIGGER_PULSEFORMGEN3_WIDTH = _int32(300231) + FG_TRIGGEROUT_SELECT0 = _int32(300232) + FG_TRIGGEROUT_SELECT1 = _int32(300233) + FG_TRIGGEROUT_SELECT2 = _int32(300234) + FG_TRIGGEROUT_SELECT3 = _int32(300235) + FG_TRIGGEROUT_STATS_SOURCE = _int32(300236) + FG_TRIGGEROUT_STATS_PULSECOUNT = _int32(300237) + FG_TRIGGEROUT_STATS_PULSECOUNT_CLEAR = _int32(300238) + FG_TRIGGERIN_EVENT_SRC = _int32(300239) + FG_TRIGGER_QUEUE_FILLLEVEL_EVENT_ON_THRESHOLD = _int32(300240) + FG_TRIGGER_QUEUE_FILLLEVEL_EVENT_OFF_THRESHOLD = _int32(300241) + FG_TRIGGER_OUTPUT_EVENT_SELECT = _int32(300242) + FG_TRIGGERIN_BYPASS_SRC = _int32(300243) + FG_TRIGGEROUT_SELECT4 = _int32(300244) + FG_TRIGGEROUT_SELECT5 = _int32(300245) + FG_TRIGGEROUT_SELECT6 = _int32(300246) + FG_TRIGGEROUT_SELECT7 = _int32(300247) + FG_LUT_BASEPOINTS = _int32(300012) + FG_LUT_IN_BITS = _int32(300007) + FG_LUT_OUT_BITS = _int32(300008) + FG_LUT_VALUE = _int32(300001) + FG_LUT_VALUE_RED = _int32(300009) + FG_LUT_VALUE_GREEN = _int32(300010) + FG_LUT_VALUE_BLUE = _int32(300011) + FG_SHADING_DEAD_PIXEL_INTERPOLATION_ENABLE = _int32(300104) + FG_SHADING_DEAD_PIXEL_INTERPOLATION_THRESHOLD = _int32(300105) + FG_MISSING_CAMERA_FRAME_RESPONSE = _int32(300248) + FG_MISSING_CAMERA_FRAME_RESPONSE_CLEAR = _int32(300249) + FG_TRIGGERCC_SELECT0 = _int32(300250) + FG_TRIGGERCC_SELECT1 = _int32(300251) + FG_TRIGGERCC_SELECT2 = _int32(300252) + FG_TRIGGERCC_SELECT3 = _int32(300253) + FG_TRIGGER_LEGACY_MODE = _int32(300254) + FG_SC_SUBSENSORCOUNT = _int32(110118) + FG_SC_SENSORLENGTH = _int32(110119) + FG_SC_TAPCOUNT = _int32(110120) + FG_SC_ROTATEDSENSOR = _int32(110121) + FG_SC_READOUTDIRECTION = _int32(110122) + FG_SC_PIXELORDER = _int32(110123) + FG_SC_UPDATESCHEME = _int32(110124) + FG_IMAGEHEIGHT = _int32(110061) + FG_SHAFTENCODERMODE = _int32(110065) + FG_IMGTRIGGER_IS_BUSY = _int32(110066) + FG_IMGTRIGGERDEBOUNCING = _int32(110064) + FG_TRIGGERCAMERA_GPO0 = _int32(200330) + FG_TRIGGERCAMERA_GPO1 = _int32(200331) + FG_TRIGGERCAMERA_GPO2 = _int32(200332) + FG_TRIGGERCAMERA_GPO3 = _int32(200333) + FG_TRIGGERCAMERA_GPO4 = _int32(200334) + FG_TRIGGERCAMERA_GPO5 = _int32(200335) + FG_TRIGGERCAMERA_GPO6 = _int32(200336) + FG_TRIGGERCAMERA_GPO7 = _int32(200337) + FG_TRIGGERCAMERA_OUT_SELECT = _int32(200338) + FG_TRIGGERCAMERA_SOURCE = _int32(200338) + FG_TRIGGERCAMERA_POLARITY = _int32(200354) + FG_TRIGGERCAMERA_INPUT_MONITOR = _int32(200339) + FG_TRIGGERCAMERA_GPI_MONITOR = _int32(200340) + FG_SYSTEMMONITOR_FPGA_TEMPERATURE = _int32(200341) + FG_SYSTEMMONITOR_FPGA_VCC_INT = _int32(200342) + FG_SYSTEMMONITOR_FPGA_VCC_AUX = _int32(200343) + FG_SYSTEMMONITOR_BOARD_POWER = _int32(200344) + FG_SYSTEMMONITOR_CXP_CHIP_TEMPERATURE = _int32(200345) + FG_SYSTEMMONITOR_RAM_CHIP_TEMPERATURE = _int32(200346) + FG_SYSTEMMONITOR_CXP_POWER_REGULATOR_TEMPERATURE = _int32(200347) + FG_SYSTEMMONITOR_POWER_REGULATOR_TEMPERATURE = _int32(200348) + FG_SYSTEMMONITOR_FPGA_DNA = _int32(200349) + FG_SYSTEMMONITOR_CHANNEL_CURRENT = _int32(200350) + FG_SYSTEMMONITOR_CHANNEL_VOLTAGE = _int32(200351) + FG_SYSTEMMONITOR_CHANNEL_STATE = _int32(200353) + FG_SC = _int32(110138) + FG_SAMPLING_RATE = _int32(200365) + FG_PIXELFORMAT = _int32(200368) + FG_CXP_TRIGGER_PACKET_MODE = _int32(200369) + FG_SHADING_ENABLE = _int32(300109) + FG_SHAFTENCODER_COMPENSATION_ENABLE = _int32(200370) + FG_SHAFTENCODER_COMPENSATION_COUNT = _int32(200371) + FG_REVERSE_X = _int32(300110) + FG_TAPGEOMETRY = _int32(300111) + FG_VANTAGEPOINT = _int32(300112) + FG_SENSORWIDTH = _int32(200220) + FG_SENSORHEIGHT = _int32(200221) + FG_SYSTEMMONITOR_FPGA_VCC_BRAM = _int32(200372) + FG_SYSTEMMONITOR_CURRENT_LINK_WIDTH = _int32(200373) + FG_SYSTEMMONITOR_CURRENT_LINK_SPEED = _int32(200374) + FG_SYSTEMMONITOR_PCIE_LINK_GEN2_CAPABLE = _int32(200375) + FG_SYSTEMMONITOR_PCIE_LINK_PARTNER_GEN2_CAPABLE = _int32(200376) + FG_SYSTEMMONITOR_PCIE_TRAINED_PAYLOAD_SIZE = _int32(200377) + FG_SYSTEMMONITOR_EXTENSION_CONNECTOR_PRESENT = _int32(200378) + FG_SYSTEMMONITOR_POCL_STATE_PORT_A = _int32(200379) + FG_SYSTEMMONITOR_POCL_STATE_PORT_B = _int32(200380) + FG_ALTERNATIVE_BOARD_DETECTION = _int32(200381) + FG_CUSTOM_BIT_SHIFT_RIGHT = _int32(200396) + FG_LINETRIGGERDEBOUNCING = _int32(110063) + FG_IMGTRIGGER_ASYNC_HEIGHT = _int32(110067) + FG_GPI = _int32(140100) + FG_FRONT_GPI = _int32(200382) + FG_TRIGGERIN_STATS_SOURCE = _int32(200398) + FG_TRIGGERIN_STATS_POLARITY = _int32(200399) + FG_TRIGGEROUT_SELECT_GPO_0 = _int32(200384) + FG_TRIGGEROUT_SELECT_GPO_1 = _int32(200385) + FG_TRIGGEROUT_SELECT_GPO_2 = _int32(200386) + FG_TRIGGEROUT_SELECT_GPO_3 = _int32(200387) + FG_TRIGGEROUT_SELECT_FRONT_GPO_0 = _int32(200392) + FG_TRIGGEROUT_SELECT_GPO_4 = _int32(200388) + FG_TRIGGEROUT_SELECT_GPO_5 = _int32(200389) + FG_TRIGGEROUT_SELECT_GPO_6 = _int32(200390) + FG_TRIGGEROUT_SELECT_GPO_7 = _int32(200391) + FG_TRIGGEROUT_SELECT_FRONT_GPO_1 = _int32(200393) + FG_OUTPUT_APPEND_NUMBER = _int32(200372) + FG_LINE_PAYLOAD_SIZE = _int32(200400) + FG_CLHS_TRIGGER_PULSE_MESSAGE = _int32(200401) + FG_TRIGGEROUT_GPO_0_SOURCE = _int32(200384) + FG_TRIGGEROUT_GPO_0_POLARITY = _int32(200410) + FG_TRIGGEROUT_GPO_1_SOURCE = _int32(200385) + FG_TRIGGEROUT_GPO_1_POLARITY = _int32(200411) + FG_TRIGGEROUT_GPO_2_SOURCE = _int32(200386) + FG_TRIGGEROUT_GPO_2_POLARITY = _int32(200412) + FG_TRIGGEROUT_GPO_3_SOURCE = _int32(200387) + FG_TRIGGEROUT_GPO_3_POLARITY = _int32(200413) + FG_TRIGGEROUT_GPO_4_SOURCE = _int32(200388) + FG_TRIGGEROUT_GPO_4_POLARITY = _int32(200414) + FG_TRIGGEROUT_GPO_5_SOURCE = _int32(200389) + FG_TRIGGEROUT_GPO_5_POLARITY = _int32(200415) + FG_TRIGGEROUT_GPO_6_SOURCE = _int32(200390) + FG_TRIGGEROUT_GPO_6_POLARITY = _int32(200416) + FG_TRIGGEROUT_GPO_7_SOURCE = _int32(200391) + FG_TRIGGEROUT_GPO_7_POLARITY = _int32(200417) + FG_TRIGGEROUT_FRONT_GPO_0_SOURCE = _int32(200392) + FG_TRIGGEROUT_FRONT_GPO_0_POLARITY = _int32(200418) + FG_TRIGGEROUT_FRONT_GPO_1_SOURCE = _int32(200393) + FG_TRIGGEROUT_FRONT_GPO_1_POLARITY = _int32(200419) + FG_SIGNAL_ANALYZER_0_SOURCE = _int32(200420) + FG_SIGNAL_ANALYZER_0_POLARITY = _int32(200421) + FG_SIGNAL_ANALYZER_0_PERIOD_CURRENT = _int32(200422) + FG_SIGNAL_ANALYZER_0_PERIOD_MAX = _int32(200423) + FG_SIGNAL_ANALYZER_0_PERIOD_MIN = _int32(200424) + FG_SIGNAL_ANALYZER_0_PULSE_COUNT = _int32(200425) + FG_SIGNAL_ANALYZER_1_SOURCE = _int32(200430) + FG_SIGNAL_ANALYZER_1_POLARITY = _int32(200431) + FG_SIGNAL_ANALYZER_1_PERIOD_CURRENT = _int32(200432) + FG_SIGNAL_ANALYZER_1_PERIOD_MAX = _int32(200433) + FG_SIGNAL_ANALYZER_1_PERIOD_MIN = _int32(200434) + FG_SIGNAL_ANALYZER_1_PULSE_COUNT = _int32(200435) + FG_SIGNAL_ANALYZER_PULSE_COUNT_DIFFERENCE = _int32(200439) + FG_SIGNAL_ANALYZER_CLEAR = _int32(200438) + FG_CUSTOM_SIGNAL_EVENT_0_SOURCE = _int32(200440) + FG_CUSTOM_SIGNAL_EVENT_0_POLARITY = _int32(200441) + FG_CUSTOM_SIGNAL_EVENT_1_SOURCE = _int32(200442) + FG_CUSTOM_SIGNAL_EVENT_1_POLARITY = _int32(200443) +dFG_PARAM={a.name:a.value for a in FG_PARAM} +drFG_PARAM={a.value:a.name for a in FG_PARAM} + + +class FG_SIM_PATTERN(enum.IntEnum): + FG_HORIZONTAL = _int32(1) + FG_VERTICAL = _int32(2) + FG_DIAGONAL = _int32(3) +dFG_SIM_PATTERN={a.name:a.value for a in FG_SIM_PATTERN} +drFG_SIM_PATTERN={a.value:a.name for a in FG_SIM_PATTERN} + + +class FG_SIM_SPEED(enum.IntEnum): + FG_PIXEL_FREQUENCY = _int32(0) + FG_LINERATE = _int32(1) + FG_FRAMERATE = _int32(2) +dFG_SIM_SPEED={a.name:a.value for a in FG_SIM_SPEED} +drFG_SIM_SPEED={a.value:a.name for a in FG_SIM_SPEED} + + +class FG_LUT(enum.IntEnum): + LUT_RED = _int32(0) + LUT_GREEN = _int32(1) + LUT_BLUE = _int32(2) + LUT_GRAY = _int32(3) +dFG_LUT={a.name:a.value for a in FG_LUT} +drFG_LUT={a.value:a.name for a in FG_LUT} + + +class FG_PORT(enum.IntEnum): + PORT_A = _int32(0) + PORT_B = _int32(1) + PORT_C = _int32(2) + PORT_D = _int32(3) + PORT_AB = _int32(4) +dFG_PORT={a.name:a.value for a in FG_PORT} +drFG_PORT={a.value:a.name for a in FG_PORT} + + +class FG_COLOR(enum.IntEnum): + FG_RED = _int32(0) + FG_GREEN = _int32(1) + FG_BLUE = _int32(2) +dFG_COLOR={a.name:a.value for a in FG_COLOR} +drFG_COLOR={a.value:a.name for a in FG_COLOR} + + +class FG_TRIGGER(enum.IntEnum): + TRGINSOFTWARE = _int32(-1) + TRGINSRC_0 = _int32(0) + TRGINSRC_1 = _int32(1) + TRGINSRC_2 = _int32(2) + TRGINSRC_3 = _int32(3) + TRGINSRC_4 = _int32(4) + TRGINSRC_5 = _int32(5) + TRGINSRC_6 = _int32(6) + TRGINSRC_7 = _int32(7) + TRGINSRC_GPI_0 = _int32(0) + TRGINSRC_GPI_1 = _int32(1) + TRGINSRC_GPI_2 = _int32(2) + TRGINSRC_GPI_3 = _int32(3) + TRGINSRC_GPI_4 = _int32(4) + TRGINSRC_GPI_5 = _int32(5) + TRGINSRC_GPI_6 = _int32(6) + TRGINSRC_GPI_7 = _int32(7) + TRGINSRC_FRONT_GPI_0 = _int32(16) + TRGINSRC_FRONT_GPI_1 = _int32(17) + TRGINSRC_FRONT_GPI_2 = _int32(18) + TRGINSRC_FRONT_GPI_3 = _int32(19) +dFG_TRIGGER={a.name:a.value for a in FG_TRIGGER} +drFG_TRIGGER={a.value:a.name for a in FG_TRIGGER} + + +class FG_STATUS(enum.IntEnum): + FG_OK = _int32(0) + FG_INIT_OK = _int32(1) + FG_SOFTWARE_TRIGGER_PENDING = _int32(8803) + FG_ERROR = _int32(-1) + FG_DUMMY_BUFFER = _int32(-1) + FG_NO_PICTURE_AVAILABLE = _int32(-2) + FG_SISODIR5_NOT_SET = _int32(-5) + FG_INVALID_HANDLE = _int32(-6) + FG_ALR_INIT = _int32(-10) + FG_NOT_AVAILABLE = _int32(-12) + FG_NO_BOARD_AVAILABLE = _int32(-20) + FG_INVALID_BOARD_NUMBER = _int32(-21) + FG_BOARD_INIT_FAILED = _int32(-22) + FG_INVALID_CLOCK = _int32(-23) + FG_INVALID_DESIGN_NAME = _int32(-26) + FG_SYSTEM_LOCKED = _int32(-27) + FG_RESSOURCES_STILL_IN_USE = _int32(-28) + FG_CLOCK_INIT_FAILED = _int32(-29) + FG_WRONG_ARCHITECTURE = _int32(-50) + FG_WRONG_FIRMWARE_VERSION = _int32(-51) + FG_WRONG_RUNTIME_VERSION = _int32(-52) + FG_SOFTWARE_TRIGGER_BUSY = _int32(-60) + FG_INVALID_PORT_NUMBER = _int32(-61) + FG_EXCEPTION_IN_APPLET = _int32(-99) + FG_HAP_FILE_NOT_LOAD = _int32(-100) + FG_FILE_NOT_FOUND = _int32(-101) + FG_APPLET_NOT_ACCEPTED = _int32(-102) + FG_RECONFIGURATION_DISABLED = _int32(-103) + FG_RESTART_REQUIRED = _int32(-104) + FG_POWERCYCLE_REQUIRED = _int32(-105) + FG_MICROENABLE_NOT_INIT = _int32(-110) + FG_DLL_NOT_LOAD = _int32(-120) + FG_REG_KEY_NOT_FOUND = _int32(-121) + FG_VASDLL_NOT_LOAD = _int32(-122) + FG_ERROR_LOADING_MODULE = _int32(-123) + FG_UNEXPECTED_HAPLOAD = _int32(-130) + FG_SIZE_ERROR = _int32(-200) + FG_PTR_INVALID = _int32(-300) + FG_RANGE_ERR = _int32(-400) + FG_OVERFLOW_ERR = _int32(-401) + FG_NOT_ENOUGH_MEM = _int32(-500) + FG_DMATRANSFER_INVALID = _int32(-600) + FG_HAP_FILE_DONT_MATCH = _int32(-700) + FG_VERSION_MISMATCH = _int32(-701) + FG_ACCESS_DENIED = _int32(-702) + FG_RUNTIME_VERSION_TOO_OLD = _int32(-703) + FG_NOT_INIT = _int32(-2001) + FG_WRONG_SIZE = _int32(-2002) + FG_WRONG_NUMBER_OF_BUFFER = _int32(-2010) + FG_TOO_MANY_BUFFER = _int32(-2011) + FG_NOT_ENOUGH_MEMORY = _int32(-2020) + FG_MEMORY_ALREADY_ALLOCATED = _int32(-2024) + FG_CANNOT_WRITE_MEM_CONFIG_FAILED = _int32(-2026) + FG_INTERNAL_STATUS_ERROR = _int32(-2030) + FG_INTERNAL_ERROR = _int32(-2031) + FG_CANNOT_START = _int32(-2040) + FG_CANNOT_STOP = _int32(-2042) + FG_SYNC_ACQUIRE_NOT_SUPPORTED = _int32(-2045) + FG_INVALID_DESIGN = _int32(-2050) + FG_CONFIGURE_FAILED = _int32(-2052) + FG_RECONFIGURE_FAILED = _int32(-2053) + FG_NO_APPLET_ID = _int32(-2055) + FG_INVALID_MEMORY = _int32(-2060) + FG_MEMORY_IN_USE = _int32(-2061) + FG_INVALID_PARAMETER = _int32(-2070) + FG_ILLEGAL_WHILE_APC = _int32(-2071) + FG_APC_PRIORITY_ERROR = _int32(-2072) + FG_APC_ALREADY_REGISTERED = _int32(-2073) + FG_INVALID_VALUE = _int32(-2075) + FG_INVALID_FILENAME = _int32(-2076) + FG_INVALID_FILESIZE = _int32(-2077) + FG_INVALID_TYPE = _int32(-2078) + FG_INVALID_REGISTER = _int32(-7040) + FG_INVALID_MODULO = _int32(-7080) + FG_INVALID_CONFIGFILE = _int32(-5000) + FG_INVALID_CONFIGFILEEXT = _int32(-5000) + FG_FILE_ACCESS_DENIED = _int32(-5001) + FG_ERROR_FREQUENCY_TOO_HIGH_FOR_PFG = _int32(-2077) + FG_ERROR_VALUE_TOO_LOW_FOR_FPS_OR_WIDTH_OR_DELAY = _int32(-2078) + FG_ERROR_VALUE_TOO_HIGH_FOR_FPS_OR_WIDTH_OR_DELAY = _int32(-2079) + FG_NOT_LOAD = _int32(-2080) + FG_ALREADY_STARTED = _int32(-2090) + FG_OPERATION_ABORTED = _int32(-2091) + FG_STILL_ACTIVE = _int32(-2100) + FG_NO_VALID_DESIGN = _int32(-2110) + FG_TIMEOUT_ERR = _int32(-2120) + FG_NOT_IMPLEMENTED = _int32(-2130) + FG_WRONG_TRIGGER_MODE = _int32(-2140) + FG_NOT_WRONG_TRIGGER_MODE = _int32(-2140) + FG_WRONG_TRIGGER_STATE = _int32(-2141) + FG_ALL_BUFFER_BLOCKED = _int32(-2150) + FG_NO_EVENTS_FOUND = _int32(-2160) + FG_CANNOT_COMBINE_DATA_EVENTS = _int32(-2161) + FG_INVALID_EVENTMASK = _int32(-2162) + FG_CANNOT_INIT_MICROENABLE = _int32(-3000) + FG_TRANSFER_NOT_ACTIVE = _int32(-3010) + FG_CLOCK_NOT_LOCKED = _int32(-3120) + FG_STILL_NOT_STARTED = _int32(-4000) + FG_VALUE_OUT_OF_RANGE = _int32(-6000) + FG_CANNOT_CHANGE_DISPLAY_WIDTH = _int32(-7000) + FG_CANNOT_CHANGE_DISPLAY_HEIGHT = _int32(-7005) + FG_CANNOT_CHANGE_DISPLAY_SIZE = _int32(-7010) + FG_NO_VALID_LICENSE = _int32(-7020) + FG_CANNOT_CHANGE_CAMERA_FORMAT = _int32(-7030) + FG_REGISTER_INIT_FAILED = _int32(-7050) + FG_INVALID_SHADING_CORRECTION_FILE = _int32(-7060) + FG_WRITE_LINE_SHADING_TIMEOUT = _int32(-7070) + FG_INVALID_IMAGE_DIMENSIONS = _int32(-7071) + FG_ERR_INVALID_FILE_DATA = _int32(-7072) + FG_ERR_RANGE_ERROR = _int32(-7073) + FG_CANNOT_CHANGE_DURING_ACQU = _int32(-7090) + FG_TOKEN_NOT_FOUND_ERROR = _int32(-8000) + FG_WRITE_ACCESS_DENIED = _int32(-8010) + FG_REGISTER_UPDATE_FAILED = _int32(-8020) + FG_DEVICE_IO_ERROR = _int32(-9000) + FG_INVALID_CONFIG_REGION = _int32(-9001) + FG_DEVICE_REMOVED = _int32(-9002) + FG_PARAMETER_NOT_IN_FILE = _int32(-9003) +dFG_STATUS={a.name:a.value for a in FG_STATUS} +drFG_STATUS={a.value:a.name for a in FG_STATUS} + + +class FG_APPLET(enum.IntEnum): + SINGLE_AREA_GRAY = _int32(0x10) + SINGLE_AREA_2DSHADING = _int32(0x11) + DUAL_AREA_GRAY = _int32(0x20) + SINGLE_AREA_BAYER = _int32(0x30) + DUAL_AREA_BAYER = _int32(0x31) + SINGLE_AREA_GRAY_SHADING = _int32(0x40) + SDRAM_ACCESS = _int32(0x41) + SINGLE_LINE_GRAY = _int32(0x50) + SINGLE_LINE_RGB = _int32(0x60) + DUAL_LINE_RGB = _int32(0x61) + DUAL_LINE_RGB_SHADING = _int32(0x62) + DUAL_LINE_GRAY = _int32(0x70) + VISIGLAS = _int32(0x80) + TRUMPFINESS = _int32(0x81) + SOUDRONIC = _int32(0x82) + SINGLEHIGHPRECISION = _int32(0x83) + SINGLE_AREA_GRAY_OFFSET = _int32(0x84) + SINGLE_AREA_HSI = _int32(0x90) + SINGLE_AREA_RGB = _int32(0xa0) + DUAL_AREA_RGB = _int32(0xb0) + SINGLE_AREA_RGB_SEPARATION = _int32(0xb1) + MEDIUM_LINE_RGB = _int32(0xb2) + MEDIUM_LINE_GRAY = _int32(0xb3) + SINGLE_FAST_CONFIG = _int32(0xb5) + FASTCONFIG_SINGLE_AREA_GRAY = _int32(0xb5) + SINGLE_AREA_GRAY_XXL = _int32(0x110) + SINGLE_AREA_2DSHADING_XXL = _int32(0x111) + DUAL_AREA_GRAY_XXL = _int32(0x120) + SINGLE_AREA_BAYER_XXL = _int32(0x130) + DUAL_AREA_BAYER_XXL = _int32(0x131) + SINGLE_AREA_GRAY_SHADING_XXL = _int32(0x140) + SDRAM_ACCESS_XXL = _int32(0x141) + SINGLE_LINE_GRAY_XXL = _int32(0x150) + SINGLE_LINE_RGB_XXL = _int32(0x160) + DUAL_LINE_RGB_XXL = _int32(0x161) + DUAL_LINE_RGB_SHADING_XXL = _int32(0x162) + DUAL_LINE_GRAY_XXL = _int32(0x170) + SINGLE_AREA_HSI_XXL = _int32(0x190) + SINGLE_AREA_RGB_XXL = _int32(0x1a0) + DUAL_AREA_RGB_XXL = _int32(0x1b0) + SINGLE_AREA_RGB_SEPARATION_XXL = _int32(0x1b1) + MEDIUM_LINE_RGB_XXL = _int32(0x1b2) + MEDIUM_LINE_GRAY_XXL = _int32(0x1b3) + MEDIUM_AREA_GRAY_XXL = _int32(0x1b4) + MEDIUM_AREA_RGB_XXL = _int32(0x1b5) + SINGLE_AREA_BAYER12_XXL = _int32(0x1c0) + DUAL_AREA_GRAY12_XXL = _int32(0x1d0) + SINGLE_LINE_GRAY12_XXL = _int32(0x1d1) + DUAL_AREA_RGB36_XXL = _int32(0x1d2) + DUAL_LINE_GRAY12_XXL = _int32(0x1d3) + MEDIUM_LINE_GRAY12_XXL = _int32(0x1d4) + SINGLE_AREA_GRAY12_XXL = _int32(0x1d5) + DUAL_LINE_RGB36_XXL = _int32(0x1d6) + SINGLE_AREA_RGB36_XXL = _int32(0x1d7) + SINGLE_LINE_RGB36_XXL = _int32(0x1d8) + DUAL_AREA_BAYER12_XXL = _int32(0x1d9) + SINGLE_AREA_2DSHADING12_XXL = _int32(0x1da) + SINGLE_LINE_RGB24_XXL = _int32(0x1db) + LSC1020XXL = _int32(0x500) + LSC1020JPGXXL = _int32(0x501) + CLSC2050 = _int32(0x502) + CLSC2050JPGXXL = _int32(0x503) + SEQUENCE_EXTRACTOR = _int32(0x510) + SAG_COMPRESSION = _int32(0x520) + MEDIUM_LINE_GRAY_FIR_XXL = _int32(0x530) + DUAL_LINE_RGB_SORTING_XXL = _int32(0x540) + SINGLE_LINE_GRAY_2X12_XXL = _int32(0x550) + MEDIUM_LINE_GRAY12 = _int32(0x560) + SINGLE_LINE_RGB36PIPELINE2_XXL = _int32(0x570) + DUAL_AREA_GRAY_16 = _int32(0x580) + DUAL_AREA_GRAY16_ME4BASEX1 = _int32(0xa400010) + DUAL_AREA_RGB48_ME4BASEX1 = _int32(0xa400020) + DUAL_LINE_GRAY16_ME4BASEX1 = _int32(0xa400030) + DUAL_LINE_RGB48_ME4BASEX1 = _int32(0xa400040) + MEDIUM_AREA_GRAY16_ME4BASEX1 = _int32(0xa400050) + MEDIUM_AREA_RGB36_ME4BASEX1 = _int32(0xa400060) + MEDIUM_LINE_GRAY16_ME4BASEX1 = _int32(0xa400070) + MEDIUM_LINE_RGB36_ME4BASEX1 = _int32(0xa400080) + DUAL_AREA_BAYER12_ME4FULLX1 = _int32(0xa410010) + DUAL_AREA_GRAY16_ME4FULLX1 = _int32(0xa410020) + DUAL_AREA_RGB48_ME4FULLX1 = _int32(0xa410030) + DUAL_LINE_GRAY16_ME4FULLX1 = _int32(0xa410040) + DUAL_LINE_RGB30_ME4FULLX1 = _int32(0xa410050) + FULL_AREA_GRAY8_ME4FULLX1 = _int32(0xa410060) + FULL_LINE_GRAY8_ME4FULLX1 = _int32(0xa410070) + MEDIUM_AREA_GRAY16_ME4FULLX1 = _int32(0xa410080) + MEDIUM_AREA_RGB36_ME4FULLX1 = _int32(0xa410090) + MEDIUM_LINE_GRAY16_ME4FULLX1 = _int32(0xa4100a0) + MEDIUM_LINE_RGB36_ME4FULLX1 = _int32(0xa4100b0) + SINGLE_AREA_BAYERHQ_ME4FULLX1 = _int32(0xa4100c0) + SINGLE_AREA_GRAY2DSHADING_ME4FULLX1 = _int32(0xa4100d0) + DUAL_AREA_BAYER12_ME4FULLX4 = _int32(0xa440010) + DUAL_AREA_GRAY16_ME4FULLX4 = _int32(0xa440020) + DUAL_AREA_RGB48_ME4FULLX4 = _int32(0xa440030) + DUAL_LINE_GRAY16_ME4FULLX4 = _int32(0xa440040) + DUAL_LINE_RGB30_ME4FULLX4 = _int32(0xa440050) + FULL_AREA_GRAY8_ME4FULLX4 = _int32(0xa440060) + FULL_LINE_GRAY8_ME4FULLX4 = _int32(0xa440070) + MEDIUM_AREA_GRAY16_ME4FULLX4 = _int32(0xa440080) + MEDIUM_AREA_RGB36_ME4FULLX4 = _int32(0xa440090) + MEDIUM_LINE_GRAY16_ME4FULLX4 = _int32(0xa4400a0) + MEDIUM_LINE_RGB36_ME4FULLX4 = _int32(0xa4400b0) + SINGLE_AREA_BAYERHQ_ME4FULLX4 = _int32(0xa4400c0) + SINGLE_AREA_GRAY2DSHADING_ME4FULLX4 = _int32(0xa4400d0) + QUAD_AREA_BAYER24_ME4GBEX4 = _int32(0xe440010) + QUAD_AREA_GRAY16_ME4GBEX4 = _int32(0xe440020) + QUAD_AREA_RG24_ME4GBEX4 = _int32(0xe440030) + QUAD_AREA_RGB48_ME4GBEX4 = _int32(0xe440040) + QUAD_AREA_GRAY8_ME4GBEX4 = _int32(0xe440050) + QUAD_LINE_GRAY16_ME4GBEX4 = _int32(0xe440060) + QUAD_LINE_RGB24_ME4GBEX4 = _int32(0xe440070) + QUAD_LINE_GRAY8_ME4GBEX4 = _int32(0xe440080) +dFG_APPLET={a.name:a.value for a in FG_APPLET} +drFG_APPLET={a.value:a.name for a in FG_APPLET} + + +class ACQAPPLET(enum.IntEnum): + LUT_TYPE_PROCESSING = _int32(3) + LUT_TYPE_CUSTOM = _int32(0) + LUT_IMPLEMENTATION_FULL_LUT = _int32(0) + LUT_IMPLEMENTATION_KNEELUT = _int32(1) + FG_MAX_VALUE = _int32(0) + FG_MEAN_VALUE = _int32(1) + FG_MAX_RANGE = _int32(2) + FG_CUSTOM_VALUE = _int32(3) + FG_APPLY = _int32(1) + ATM_GENERATOR = _int32(1) + ATM_EXTERNAL = _int32(2) + ATM_SOFTWARE = _int32(4) + TS_ACTIVE = _int32(0) + TS_ASYNC_STOP = _int32(1) + TS_SYNC_STOP = _int32(2) + FG_ONE = _int32(1) + FG_ZERO = _int32(0) + IS_BUSY = _int32(1) + IS_NOT_BUSY = _int32(0) +dACQAPPLET={a.name:a.value for a in ACQAPPLET} +drACQAPPLET={a.value:a.name for a in ACQAPPLET} + + +class FG_PULSEGEN(enum.IntEnum): + PULSEGEN0 = _int32(0) + PULSEGEN1 = _int32(1) + PULSEGEN2 = _int32(2) + PULSEGEN3 = _int32(3) + GND = _int32(4) + VCC = _int32(5) + NOT_PULSEGEN0 = _int32(6) + NOT_PULSEGEN1 = _int32(7) + NOT_PULSEGEN2 = _int32(8) + NOT_PULSEGEN3 = _int32(9) + CAM_A_PULSEGEN0 = _int32(50) + CAM_A_PULSEGEN1 = _int32(51) + CAM_A_PULSEGEN2 = _int32(52) + CAM_A_PULSEGEN3 = _int32(53) + CAM_A_NOT_PULSEGEN0 = _int32(60) + CAM_A_NOT_PULSEGEN1 = _int32(61) + CAM_A_NOT_PULSEGEN2 = _int32(62) + CAM_A_NOT_PULSEGEN3 = _int32(63) + CAM_B_PULSEGEN0 = _int32(54) + CAM_B_PULSEGEN1 = _int32(55) + CAM_B_PULSEGEN2 = _int32(56) + CAM_B_PULSEGEN3 = _int32(57) + CAM_B_NOT_PULSEGEN0 = _int32(64) + CAM_B_NOT_PULSEGEN1 = _int32(65) + CAM_B_NOT_PULSEGEN2 = _int32(66) + CAM_B_NOT_PULSEGEN3 = _int32(67) + CAM_C_PULSEGEN0 = _int32(68) + CAM_C_PULSEGEN1 = _int32(69) + CAM_C_PULSEGEN2 = _int32(70) + CAM_C_PULSEGEN3 = _int32(71) + CAM_C_NOT_PULSEGEN0 = _int32(76) + CAM_C_NOT_PULSEGEN1 = _int32(77) + CAM_C_NOT_PULSEGEN2 = _int32(78) + CAM_C_NOT_PULSEGEN3 = _int32(79) + CAM_D_PULSEGEN0 = _int32(72) + CAM_D_PULSEGEN1 = _int32(73) + CAM_D_PULSEGEN2 = _int32(74) + CAM_D_PULSEGEN3 = _int32(75) + CAM_D_NOT_PULSEGEN0 = _int32(80) + CAM_D_NOT_PULSEGEN1 = _int32(81) + CAM_D_NOT_PULSEGEN2 = _int32(82) + CAM_D_NOT_PULSEGEN3 = _int32(83) +dFG_PULSEGEN={a.name:a.value for a in FG_PULSEGEN} +drFG_PULSEGEN={a.value:a.name for a in FG_PULSEGEN} + + +class FG_BYPASS(enum.IntEnum): + INPUT_BYPASS = _int32(10) + NOT_INPUT_BYPASS = _int32(11) + BYPASS_GPI_0 = _int32(10) + NOT_BYPASS_GPI_0 = _int32(20) + BYPASS_GPI_1 = _int32(11) + NOT_BYPASS_GPI_1 = _int32(21) + BYPASS_GPI_2 = _int32(12) + NOT_BYPASS_GPI_2 = _int32(22) + BYPASS_GPI_3 = _int32(13) + NOT_BYPASS_GPI_3 = _int32(23) + BYPASS_GPI_4 = _int32(14) + NOT_BYPASS_GPI_4 = _int32(24) + BYPASS_GPI_5 = _int32(15) + NOT_BYPASS_GPI_5 = _int32(25) + BYPASS_GPI_6 = _int32(16) + NOT_BYPASS_GPI_6 = _int32(26) + BYPASS_GPI_7 = _int32(17) + NOT_BYPASS_GPI_7 = _int32(27) + BYPASS_FRONT_GPI_0 = _int32(30) + NOT_BYPASS_FRONT_GPI_0 = _int32(40) + BYPASS_FRONT_GPI_1 = _int32(31) + NOT_BYPASS_FRONT_GPI_1 = _int32(41) + BYPASS_FRONT_GPI_2 = _int32(32) + NOT_BYPASS_FRONT_GPI_2 = _int32(42) + BYPASS_FRONT_GPI_3 = _int32(33) + NOT_BYPASS_FRONT_GPI_3 = _int32(43) +dFG_BYPASS={a.name:a.value for a in FG_BYPASS} +drFG_BYPASS={a.value:a.name for a in FG_BYPASS} + + +class FG_POCL(enum.IntEnum): + FG_INITIALIZE = _int32(0) + FG_POCL_CONNECTION_SENSE = _int32(1) + FG_POCL_CAMERA_DETECTED = _int32(2) + FG_POCL_CAMERA_CLOCK_DETECTED = _int32(3) + FG_CL_CAMERA_DETECTED = _int32(4) + FG_CL_CAMERA_CLOCK_DETECTED = _int32(5) + FG_POCL_WAIT_FOR_CONNECTION = _int32(6) + FG_CL_WAIT_FOR_CONNECTION = _int32(7) + FG_POCL_DISABLED = _int32(8) +dFG_POCL={a.name:a.value for a in FG_POCL} +drFG_POCL={a.value:a.name for a in FG_POCL} + + +class FG_LINEORDER(enum.IntEnum): + FG_LINEORDER = _int32(920000) + FG_LINEORDER_RGB = _int32(0) + FG_LINEORDER_BGR = _int32(1) +dFG_LINEORDER={a.name:a.value for a in FG_LINEORDER} +drFG_LINEORDER={a.value:a.name for a in FG_LINEORDER} + + +class FG_ALARM(enum.IntEnum): + DEVICE_ALARM_TEMPERATURE = (0x00000001) + DEVICE_ALARM_PHY = (0x00000002) + DEVICE_ALARM_POE = (0x00000004) + DEVICE_ALARM_ACL_0 = (0x00000010) + DEVICE_ALARM_ACL_1 = (0x00000020) + DEVICE_ALARM_ACL_2 = (0x00000040) + DEVICE_ALARM_ACL_3 = (0x00000080) + DEVICE_STATUS_CONFIGURED = (0x00000001) + DEVICE_STATUS_LOCKED = (0x00000002) + DEVICE_STATUS_OVERTEMP = (0x40000000) + DEVICE_STATUS_DEAD = (0x80000000) +dFG_ALARM={a.name:a.value for a in FG_ALARM} +drFG_ALARM={a.value:a.name for a in FG_ALARM} + + + + + +##### TYPE DEFINITIONS ##### + + + + + +##### FUNCTION DEFINITIONS ##### + + diff --git a/pylablib/devices/SiliconSoftware/fgrab_prototyp_defs.py b/pylablib/devices/SiliconSoftware/fgrab_prototyp_defs.py new file mode 100644 index 0000000..08e22ef --- /dev/null +++ b/pylablib/devices/SiliconSoftware/fgrab_prototyp_defs.py @@ -0,0 +1,1403 @@ +########## This file is generated automatically based on fgrab_prototyp.h ########## + +# pylint: disable=unused-import, unused-argument, wrong-spelling-in-comment + + +import ctypes +import enum +from ...core.utils import ctypes_wrap + + + + +def _int32(v): return (v+0x80000000)%0x100000000-0x80000000 + + + + +##### TYPE DEFINITIONS ##### + + +BYTE=ctypes.c_ubyte +PBYTE=ctypes.POINTER(BYTE) +CHAR=ctypes.c_char +PCHAR=ctypes.c_char_p +UCHAR=ctypes.c_ubyte +PUCHAR=ctypes.POINTER(UCHAR) +ULONG_PTR=ctypes.c_uint64 +LONG_PTR=ctypes.c_int64 +WORD=ctypes.c_ushort +LPWORD=ctypes.POINTER(WORD) +LONGLONG=ctypes.c_int64 +LPLONG=ctypes.POINTER(ctypes.c_long) +SSIZE_T=LONG_PTR +HANDLE=ctypes.c_void_p +LPHANDLE=ctypes.POINTER(HANDLE) +HWND=ctypes.c_void_p +HGLOBAL=ctypes.c_void_p +HINSTANCE=ctypes.c_void_p +HDC=ctypes.c_void_p +HMODULE=ctypes.c_void_p +HKEY=ctypes.c_void_p +PVOID=ctypes.c_void_p +LPVOID=ctypes.c_void_p +MirEGLNativeWindowType=ctypes.c_void_p +MirEGLNativeDisplayType=ctypes.c_void_p +class siso_board_type(enum.IntEnum): + PN_MICROENABLE =_int32(0xa1) + PN_MICROENABLEII =_int32(0xa2) + PN_MICROENABLE3I =_int32(0xa3) + PN_MICROENABLE3IXXL =_int32(0xa31) + PN_MICROENABLE4AD1CL =_int32(0xa40) + PN_MICROENABLE4BASE =_int32(PN_MICROENABLE4AD1CL) + PN_MICROENABLE4BASEx4 =_int32(0xa43) + PN_MICROENABLE4AD4CL =_int32(0xa42) + PN_MICROENABLE4VD1CL =_int32(0xa41) + PN_MICROENABLE4FULLx1 =_int32(PN_MICROENABLE4VD1CL) + PN_MICROENABLE4VD4CL =_int32(0xa44) + PN_MICROENABLE4FULLx4 =_int32(PN_MICROENABLE4VD4CL) + PN_MICROENABLE4AS1CL =_int32(0xa45) + PN_MICROENABLE4VQ4GE =_int32(0xe44) + PN_MICROENABLE4GIGEx4 =_int32(PN_MICROENABLE4VQ4GE) + PN_MICROENABLE4AQ4GE =_int32(0xe42) + PN_MICROENABLE4_H264CLx1 =_int32(0xb41) + PN_MICROENABLE4_H264pCLx1 =_int32(0xb42) + PN_PX100 =_int32(0xc41) + PN_PX200 =_int32(0xc42) + PN_PX210 =_int32(0xc43) + PN_PX300 =_int32(0xc44) + PN_MICROENABLE5A1CXP4 =_int32(0xa51) + PN_MICROENABLE5A1CLHSF2 =_int32(0xa52) + PN_MICROENABLE5AQ8CXP6B =_int32(0xa53) + PN_MICROENABLE5AQ8CXP4 =_int32(PN_MICROENABLE5AQ8CXP6B) + PN_MICROENABLE5VQ8CXP6B =_int32(0xa54) + PN_MICROENABLE5VQ8CXP4 =_int32(PN_MICROENABLE5VQ8CXP6B) + PN_MICROENABLE5AD8CLHSF2 =_int32(0xa55) + PN_MICROENABLE5VQ8CXP6D =_int32(0xa56) + PN_MICROENABLE5AQ8CXP6D =_int32(0xa57) + PN_MICROENABLE5VD8CL =_int32(0xa58) + PN_MICROENABLE5VF8CL =_int32(PN_MICROENABLE5VD8CL) + PN_MICROENABLE5A2CLHSF2 =_int32(0xa59) + PN_MICROENABLE5AD8CL =_int32(0xa5a) + PN_MICROENABLE5_LIGHTBRIDGE_VCL_PROTOTYPE=_int32(0x750) + PN_MICROENABLE5_LIGHTBRIDGE_MARATHON_VCL =_int32(0x751) + PN_MICROENABLE5_LIGHTBRIDGE_VCL =_int32(0x7510) + PN_MICROENABLE5_MARATHON_VCL =_int32(0x7511) + PN_MICROENABLE5_MARATHON_AF2_DP =_int32(0x752) + PN_MICROENABLE5_MARATHON_ACX_QP =_int32(0x753) + PN_MICROENABLE5_LIGHTBRIDGE_MARATHON_ACL =_int32(0x754) + PN_MICROENABLE5_LIGHTBRIDGE_ACL =_int32(0x7540) + PN_MICROENABLE5_MARATHON_ACL =_int32(0x7541) + PN_MICROENABLE5_MARATHON_ACX_SP =_int32(0x755) + PN_MICROENABLE5_MARATHON_ACX_DP =_int32(0x756) + PN_MICROENABLE5_MARATHON_VCX_QP =_int32(0x757) + PN_MICROENABLE5_MARATHON_VF2_DP =_int32(0x758) + PN_MICROENABLE5_LIGHTBRIDGE_MARATHON_VCLx=_int32(0x759) + PN_MICROENABLE5_MARATHON_VCLx =_int32(0x7591) + PN_TDI =_int32(0xb50) + PN_TDI_I =_int32(0xb500) + PN_TDI_II =_int32(0xb501) + PN_TGATE_USB =_int32(0xb57) + PN_TGATE_I_USB =_int32(0xb570) + PN_TGATE_II_USB =_int32(0xb571) + PN_TGATE =_int32(0xb5e) + PN_TGATE_I =_int32(0xb5e0) + PN_TGATE_II =_int32(0xb5e1) + PN_TGATE_35 =_int32(0xb58) + PN_TGATE_I_35 =_int32(0xb580) + PN_TGATE_II_35 =_int32(0xb581) + PN_TGATE_35_USB =_int32(0xb59) + PN_TGATE_I_35_USB =_int32(0xb590) + PN_TGATE_II_35_USB =_int32(0xb591) + PN_TTDI =_int32(0xb5f) + PN_MICROENABLE5_ABACUS_4G_PROTOTYPE =_int32(0xb51) + PN_MICROENABLE5_ABACUS_4G =_int32(PN_MICROENABLE5_ABACUS_4G_PROTOTYPE) + PN_MICROENABLE5_ABACUS_4G_BASE =_int32(0xb52) + PN_MICROENABLE5_ABACUS_4G_BASE_II =_int32(0xb53) + PN_MICROENABLE6_KCU105 =_int32(0xA60) + PN_UNKNOWN =_int32(0xffff) + PN_GENERIC_EVA =_int32(0x10000000) + PN_NONE =_int32(0) +dsiso_board_type={a.name:a.value for a in siso_board_type} +drsiso_board_type={a.value:a.name for a in siso_board_type} + + +frameindex_t=ctypes.c_int64 +class RowFilterModes(enum.IntEnum): + _NON_TRIGGERED_EOF_CONTROLLED =_int32(0) + _NON_TRIGGERED_LINE_COUNT_CONTROLLED=_int32(0x1) + _TRIGGERED_EOF_CONTROLLED =_int32(0x2) + _TRIGGERED_LINE_COUNT_CONTROLLED =_int32(0x3) +dRowFilterModes={a.name:a.value for a in RowFilterModes} +drRowFilterModes={a.value:a.name for a in RowFilterModes} + + +class TriggerMode(enum.IntEnum): + _GRABBER_CONTROLLED =_int32(0) + _GRABBER_CONTROLLED_STROBE =_int32(1) + _GRABBER_CONTROLLED_TRIGGER=_int32(2) + _SINGLE_SHOT =_int32(4) + _EXTERNAL_TRIGGER =_int32(6) +dTriggerMode={a.name:a.value for a in TriggerMode} +drTriggerMode={a.value:a.name for a in TriggerMode} + + +class LineTriggerMode(enum.IntEnum): + _LRM_AUTO =_int32(0) + _LRM_EXT_TRG=_int32(1) +dLineTriggerMode={a.name:a.value for a in LineTriggerMode} +drLineTriggerMode={a.value:a.name for a in LineTriggerMode} + + +class LineTriggerGateMode(enum.IntEnum): + _LRM_NON_GATED =_int32(0) + _LRM_GATED_COUNT =_int32(1) + _LRM_GATED_PARTIAL=_int32(2) + _LRM_GATED =_int32(3) +dLineTriggerGateMode={a.name:a.value for a in LineTriggerGateMode} +drLineTriggerGateMode={a.value:a.name for a in LineTriggerGateMode} + + +class TriggerSync(enum.IntEnum): + _LVAL =_int32(0) + _HDSYNC=_int32(1) +dTriggerSync={a.name:a.value for a in TriggerSync} +drTriggerSync={a.value:a.name for a in TriggerSync} + + +class MeTriggerMode(enum.IntEnum): + FREE_RUN =_int32(0) + GRABBER_CONTROLLED =_int32(1) + ASYNC_TRIGGER =_int32(2) + GRABBER_CONTROLLED_SYNCHRON =_int32(3) + ASYNC_SOFTWARE_TRIGGER =_int32(4) + ASYNC_GATED =_int32(5) + ASYNC_GATED_MULTIFRAME =_int32(6) + ASYNC_SOFTWARE_TRIGGER_QUEUED=_int32(7) +dMeTriggerMode={a.name:a.value for a in MeTriggerMode} +drMeTriggerMode={a.value:a.name for a in MeTriggerMode} + + +class MeLineTriggerMode(enum.IntEnum): + GRABBER_CONTROLLED_GATED=_int32(6) +dMeLineTriggerMode={a.name:a.value for a in MeLineTriggerMode} +drMeLineTriggerMode={a.value:a.name for a in MeLineTriggerMode} + + +class MeShaftMode(enum.IntEnum): + SOURCE_A=_int32(0) + SOURCE_B=_int32(1) +dMeShaftMode={a.name:a.value for a in MeShaftMode} +drMeShaftMode={a.value:a.name for a in MeShaftMode} + + +class MeLineShadingMode(enum.IntEnum): + SHADING_OFF =_int32(0) + SHADING_SUB =_int32(1) + SHADING_MULT =_int32(2) + SHADING_SUB_MULT=_int32(3) +dMeLineShadingMode={a.name:a.value for a in MeLineShadingMode} +drMeLineShadingMode={a.value:a.name for a in MeLineShadingMode} + + +class MeKneeLutMode(enum.IntEnum): + FG_INDEP=_int32(0) + FG_DEP =_int32(1) +dMeKneeLutMode={a.name:a.value for a in MeKneeLutMode} +drMeKneeLutMode={a.value:a.name for a in MeKneeLutMode} + + +class MeAreaTriggerMode(enum.IntEnum): + AREA_FREE_RUN =_int32(0) + AREA_GRABBER_CONTROLLED =_int32(1) + AREA_ASYNC_TRIGGER =_int32(2) + AREA_GRABBER_CONTROLLED_SYNCHRON=_int32(3) + AREA_ASYNC_SOFTWARE_TRIGGER =_int32(4) +dMeAreaTriggerMode={a.name:a.value for a in MeAreaTriggerMode} +drMeAreaTriggerMode={a.value:a.name for a in MeAreaTriggerMode} + + +class MeLineTriggerModeLine(enum.IntEnum): + LINE_FREE_RUN_LINE =_int32(0) + LINE_GRABBER_CONTROLLED_LINE =_int32(1) + LINE_ASYNC_TRIGGER_LINE =_int32(2) + LINE_ASYNC_GATED_LINE =_int32(5) + LINE_GRABBER_CONTROLLED_GATED_LINE=_int32(6) +dMeLineTriggerModeLine={a.name:a.value for a in MeLineTriggerModeLine} +drMeLineTriggerModeLine={a.value:a.name for a in MeLineTriggerModeLine} + + +class MeLineTriggerModeImage(enum.IntEnum): + LINE_FREE_RUN_IMAGE =_int32(0) + LINE_GRABBER_CONTROLLED_IMAGE =_int32(1) + LINE_ASYNC_TRIGGER_IMAGE =_int32(2) + LINE_GRABBER_CONTROLLED_GATED_IMAGE=_int32(5) + LINE_ASYNC_GATED_MULTIBUFFERS_IMAGE=_int32(6) +dMeLineTriggerModeImage={a.name:a.value for a in MeLineTriggerModeImage} +drMeLineTriggerModeImage={a.value:a.name for a in MeLineTriggerModeImage} + + +class MeRgbComponentMapping(enum.IntEnum): + FG_MAP_PIXEL0=_int32(0) + FG_MAP_PIXEL1=_int32(1) + FG_MAP_PIXEL2=_int32(2) + FG_MAP_PIXEL3=_int32(3) +dMeRgbComponentMapping={a.name:a.value for a in MeRgbComponentMapping} +drMeRgbComponentMapping={a.value:a.name for a in MeRgbComponentMapping} + + +class MeCameraLinkFormat(enum.IntEnum): + FG_CL_SINGLETAP_8_BIT =_int32(8) + FG_CL_SINGLETAP_10_BIT =_int32(10) + FG_CL_SINGLETAP_12_BIT =_int32(12) + FG_CL_SINGLETAP_14_BIT =_int32(14) + FG_CL_SINGLETAP_16_BIT =_int32(16) + FG_CL_DUALTAP_8_BIT =_int32(108) + FG_CL_DUALTAP_10_BIT =_int32(110) + FG_CL_DUALTAP_12_BIT =_int32(112) + FG_CL_TRIPLETAP_8_BIT =_int32(120) + FG_CL_LITE_8_BIT =_int32(130) + FG_CL_LITE_10_BIT =_int32(140) + FG_CL_RGB =_int32(500) + FG_CL_MEDIUM_8_BIT =_int32(208) + FG_CL_MEDIUM_10_BIT =_int32(210) + FG_CL_MEDIUM_12_BIT =_int32(212) + FG_CL_MEDIUM_3_TAP_10_BIT=_int32(219) + FG_CL_MEDIUM_3_TAP_12_BIT=_int32(220) + FG_CL_MEDIUM_RGB_24 =_int32(214) + FG_CL_MEDIUM_RGB_30 =_int32(216) + FG_CL_MEDIUM_RGB_36 =_int32(218) + FG_CL_8BIT_FULL_8 =_int32(308) + FG_CL_8BIT_FULL_10 =_int32(310) + FG_CL_FULL_8_TAP_10_BIT =_int32(311) + FG_CL_FULL_8_TAP_RGB_24 =_int32(320) + FG_CL_FULL_10_TAP_RGB_24 =_int32(321) + FG_CL_FULL_8_TAP_RGB_30 =_int32(322) +dMeCameraLinkFormat={a.name:a.value for a in MeCameraLinkFormat} +drMeCameraLinkFormat={a.value:a.name for a in MeCameraLinkFormat} + + +class MeCameraTypes(enum.IntEnum): + FG_AREA_GRAY =_int32(0) + FG_AREA_BAYER =_int32(1) + FG_LINE_GRAY =_int32(2) + FG_SINGLE_LINE_RGB =_int32(3) + FG_DUAL_LINE_RGB =_int32(4) + FG_SINGLE_AREA_RGB =_int32(5) + FG_DUAL_AREA_RGB =_int32(6) + FG_AREA_HSI =_int32(7) + FG_DUAL_LINE_RGB_SHADING =_int32(8) + FG_SINGLE_LINE_RGBHSI =_int32(9) + FG_SINGLE_AREA_RGB_SEPARATION=_int32(10) + FG_MEDIUM_LINE_RGB =_int32(11) + FG_MEDIUM_LINE_GRAY =_int32(12) + FG_MEDIUM_AREA_GRAY =_int32(13) + FG_MEDIUM_AREA_RGB =_int32(14) + FG_AREA_GRAY12 =_int32(15) + FG_SEQUENCE_EXTRACTOR_A =_int32(16) + FG_SEQUENCE_EXTRACTOR_B =_int32(17) + FG_LINE_GRAY12 =_int32(18) + FG_AREA_RGB36 =_int32(19) + FG_DUAL_LINE_RGB_SORTING =_int32(20) + FG_DUAL_LINE_GRAY12 =_int32(21) + FG_MEDIUM_LINE_GRAY12 =_int32(22) + FG_SINGLE_AREA_GRAY12 =_int32(23) + FG_2D_SHADING_12 =_int32(24) + DIVISOR_1 =_int32(25) + DIVISOR_2 =_int32(26) + DIVISOR_4 =_int32(27) + DIVISOR_8 =_int32(28) + DIVISOR_3 =_int32(29) + DIVISOR_16 =_int32(30) + DIVISOR_6 =_int32(31) +dMeCameraTypes={a.name:a.value for a in MeCameraTypes} +drMeCameraTypes={a.value:a.name for a in MeCameraTypes} + + +class MeSensorReadoutModes2(enum.IntEnum): + SMODE_UNCHANGED=_int32(0) + SMODE_REVERSE =_int32(3) + SMODE_TAB2_0 =_int32(1) + SMODE_TAB2_1 =_int32(4) + SMODE_TAB2_2 =_int32(6) + SMODE_TAB4_0 =_int32(2) + SMODE_TAB4_1 =_int32(5) + SMODE_TAB4_2 =_int32(7) + SMODE_TAB4_5 =_int32(8) + SMODE_TAB4_3 =_int32(9) + SMODE_TAB4_4 =_int32(10) + SMODE_TAB4_6 =_int32(11) + SMODE_TAB8_0 =_int32(30) + SMODE_TAB8_1 =_int32(31) + SMODE_TAB8_2 =_int32(32) + SMODE_TAB8_3 =_int32(33) + SMODE_TAB8_4 =_int32(34) + SMODE_TAB8_5 =_int32(35) + SMODE_TAB8_6 =_int32(36) + SMODE_TAB8_7 =_int32(37) + SMODE_TAB8_8 =_int32(38) + SMODE_TAB8_9 =_int32(39) + SMODE_TAB8_10 =_int32(40) + SMODE_TAB8_11 =_int32(41) + SMODE_TAB8_12 =_int32(42) + SMODE_TAB8_13 =_int32(43) + SMODE_TAB8_14 =_int32(44) + SMODE_TAB8_15 =_int32(45) + SMODE_TAB8_16 =_int32(46) + SMODE_TAB8_17 =_int32(47) + SMODE_TAB8_18 =_int32(48) + SMODE_TAB8_19 =_int32(49) + SMODE_TAB8_20 =_int32(50) + SMODE_TAB8_21 =_int32(51) + SMODE_TAB8_22 =_int32(52) + SMODE_TAB8_23 =_int32(53) + SMODE_TAB8_24 =_int32(54) + SMODE_TAB10_1 =_int32(60) + SMODE_TAB10_2 =_int32(61) + SMODE_TAB10_4 =_int32(63) + SMODE_TAB10_3 =_int32(62) +dMeSensorReadoutModes2={a.name:a.value for a in MeSensorReadoutModes2} +drMeSensorReadoutModes2={a.value:a.name for a in MeSensorReadoutModes2} + + +class FgParamTypes(enum.IntEnum): + FG_PARAM_TYPE_INVALID =_int32(0x0) + FG_PARAM_TYPE_INT32_T =_int32(0x1) + FG_PARAM_TYPE_UINT32_T =_int32(0x2) + FG_PARAM_TYPE_INT64_T =_int32(0x3) + FG_PARAM_TYPE_UINT64_T =_int32(0x4) + FG_PARAM_TYPE_DOUBLE =_int32(0x5) + FG_PARAM_TYPE_CHAR_PTR =_int32(0x6) + FG_PARAM_TYPE_SIZE_T =_int32(0x7) + FG_PARAM_TYPE_CHAR_PTR_PTR =_int32(0x16) + FG_PARAM_TYPE_STRUCT_FIELDPARAMACCESS=_int32(0x1000) + FG_PARAM_TYPE_STRUCT_FIELDPARAMINT =_int32(0x1002) + FG_PARAM_TYPE_STRUCT_FIELDPARAMINT64 =_int32(0x1003) + FG_PARAM_TYPE_STRUCT_FIELDPARAMDOUBLE=_int32(0x1005) + FG_PARAM_TYPE_COMPLEX_DATATYPE =_int32(0x2000) + FG_PARAM_TYPE_AUTO =_int32((-1)) +dFgParamTypes={a.name:a.value for a in FgParamTypes} +drFgParamTypes={a.value:a.name for a in FgParamTypes} + + +class MeInitFlags(enum.IntEnum): + FG_INIT_FLAG_DEFAULT =_int32(0x00) + FG_INIT_FLAG_SLAVE =_int32(0x01) + FG_INIT_FLAGS_VALID_MASK=_int32((~(FG_INIT_FLAG_DEFAULT|FG_INIT_FLAG_SLAVE))) +dMeInitFlags={a.name:a.value for a in MeInitFlags} +drMeInitFlags={a.value:a.name for a in MeInitFlags} + + +class FgImageSourceTypes(enum.IntEnum): + FG_CAMPORT =_int32(0) + FG_CAMERASIMULATOR=_int32(1) + FG_GENERATOR =_int32(1) +dFgImageSourceTypes={a.name:a.value for a in FgImageSourceTypes} +drFgImageSourceTypes={a.value:a.name for a in FgImageSourceTypes} + + +class FgParamEnumGbeCamType(enum.IntEnum): + RGB8_PACKED =_int32(0) + BGR8_PACKED =_int32(1) + RGBA8_PACKED=_int32(2) + BGRA8_PACKED=_int32(3) +dFgParamEnumGbeCamType={a.name:a.value for a in FgParamEnumGbeCamType} +drFgParamEnumGbeCamType={a.value:a.name for a in FgParamEnumGbeCamType} + + +class CameraSimulatorTriggerMode(enum.IntEnum): + SIMULATION_FREE_RUN =_int32(0) + RISING_EDGE_TRIGGERS_LINE =_int32(8) + RISING_EDGE_TRIGGERS_FRAME=_int32(9) +dCameraSimulatorTriggerMode={a.name:a.value for a in CameraSimulatorTriggerMode} +drCameraSimulatorTriggerMode={a.value:a.name for a in CameraSimulatorTriggerMode} + + +class BOARD_INFORMATION_SELECTOR(enum.IntEnum): + BINFO_BOARDTYPE =_int32(0) + BINFO_POCL =_int32(1) + BINFO_PCIE_PAYLOAD=_int32(2) +dBOARD_INFORMATION_SELECTOR={a.name:a.value for a in BOARD_INFORMATION_SELECTOR} +drBOARD_INFORMATION_SELECTOR={a.value:a.name for a in BOARD_INFORMATION_SELECTOR} + + +class Fg_Info_Selector(enum.IntEnum): + INFO_APPLET_CAPABILITY_TAGS =_int32(1) + INFO_TIMESTAMP_FREQUENCY =_int32(100) + INFO_OWN_BOARDINDEX =_int32(101) + INFO_NR_OF_BOARDS =_int32(1000) + INFO_MAX_NR_OF_BOARDS =_int32(1001) + INFO_BOARDNAME =_int32(1010) + INFO_BOARDTYPE =_int32(1011) + INFO_BOARDSERIALNO =_int32(1012) + INFO_BOARDSUBTYPE =_int32(1013) + INFO_FIRMWAREVERSION =_int32(1015) + INFO_HARDWAREVERSION =_int32(1016) + INFO_PHYSICAL_LOCATION =_int32(1017) + INFO_BOARDSTATUS =_int32(1020) + INFO_PIXELPLANT_PRESENT =_int32(1030) + INFO_CAMERA_INTERFACE =_int32(1040) + INFO_DRIVERVERSION =_int32(1100) + INFO_DRIVERARCH =_int32(1101) + INFO_DRIVERFULLVERSION =_int32(1102) + INFO_BOARDNODENUMBER =_int32(1103) + INFO_DRIVERGROUPAFFINITY =_int32(1104) + INFO_DRIVERAFFINITYMASK =_int32(1105) + INFO_DESIGN_ID =_int32(1200) + INFO_BITSTREAM_ID =_int32(1201) + INFO_APPLET_DESIGN_ID =_int32(1202) + INFO_APPLET_BITSTREAM_ID =_int32(1203) + INFO_FPGA_BITSTREAM_ID =_int32(1204) + INFO_APPLET_FULL_PATH =_int32(1300) + INFO_APPLET_FILE_NAME =_int32(1301) + INFO_APPLET_TYPE =_int32(1302) + INFO_STATUS_PCI_LINK_WIDTH =_int32(2001) + INFO_STATUS_PCI_PAYLOAD_MODE =_int32(2002) + INFO_STATUS_PCI_LINK_SPEED =_int32(2003) + INFO_STATUS_PCI_PAYLOAD_SIZE =_int32(2004) + INFO_STATUS_PCI_EXPECTED_LINK_WIDTH =_int32(2005) + INFO_STATUS_PCI_EXPECTED_LINK_SPEED =_int32(2006) + INFO_STATUS_PCI_NATIVE_LINK_SPEED =_int32(2007) + INFO_STATUS_PCI_REQUEST_SIZE =_int32(2008) + INFO_STATUS_PCI_NROF_INVALID_8B10B_CHARS =_int32(2101) + INFO_STATUS_PCI_NROF_8B10B_DISPARITY_ERRORS=_int32(2102) + INFO_SERVICE_ISRUNNING =_int32(3001) +dFg_Info_Selector={a.name:a.value for a in Fg_Info_Selector} +drFg_Info_Selector={a.value:a.name for a in Fg_Info_Selector} + + +class Fg_BoardStatus_Bits(enum.IntEnum): + INFO_BOARDSTATUS_CONFIGURED =_int32(0x00000001) + INFO_BOARDSTATUS_LOCKED =_int32(0x00000002) + INFO_BOARDSTATUS_DEAD_1 =_int32(0x00008000) + INFO_BOARDSTATUS_RECONFIGURING =_int32(0x10000000) + INFO_BOARDSTATUS_REBOOT_REQUIRED=_int32(0x20000000) + INFO_BOARDSTATUS_OVERTEMP =_int32(0x40000000) + INFO_BOARDSTATUS_DEAD_2 =_int32(0x80000000) + INFO_BOARDSTATUS_DEAD =_int32((INFO_BOARDSTATUS_DEAD_1|INFO_BOARDSTATUS_DEAD_2)) +dFg_BoardStatus_Bits={a.name:a.value for a in Fg_BoardStatus_Bits} +drFg_BoardStatus_Bits={a.value:a.name for a in Fg_BoardStatus_Bits} + + +class FgProperty(enum.IntEnum): + PROP_ID_VALUE =_int32(0) + PROP_ID_DATATYPE =_int32(1) + PROP_ID_NAME =_int32(2) + PROP_ID_PARAMETERNAME=_int32(3) + PROP_ID_VALUELLEN =_int32(4) + PROP_ID_ACCESS_ID =_int32(5) + PROP_ID_MIN_ID =_int32(6) + PROP_ID_MAX_ID =_int32(7) + PROP_ID_STEP_ID =_int32(8) + PROP_ID_ACCESS =_int32(9) + PROP_ID_MIN =_int32(10) + PROP_ID_MAX =_int32(11) + PROP_ID_STEP =_int32(12) + PROP_ID_IS_ENUM =_int32(13) + PROP_ID_ENUM_VALUES =_int32(14) + PROP_ID_FIELD_SIZE =_int32(15) +dFgProperty={a.name:a.value for a in FgProperty} +drFgProperty={a.value:a.name for a in FgProperty} + + +class FgPropertyEnumValues(ctypes.Structure): + _fields_=[ ("value",ctypes.c_int32), + ("name",ctypes.c_char*1) ] +PFgPropertyEnumValues=ctypes.POINTER(FgPropertyEnumValues) +class CFgPropertyEnumValues(ctypes_wrap.CStructWrapper): + _struct=FgPropertyEnumValues + + +class FgStopAcquireFlags(enum.IntEnum): + STOP_ASYNC =_int32(0x00) + STOP_SYNC_TO_APC =_int32(0x04) + STOP_ASYNC_FALLBACK=_int32(0x40000000) + STOP_SYNC =_int32(0x80000000) +dFgStopAcquireFlags={a.name:a.value for a in FgStopAcquireFlags} +drFgStopAcquireFlags={a.value:a.name for a in FgStopAcquireFlags} + + +Fg_ApcFunc_t=ctypes.c_void_p +class Fg_Apc_Flag(enum.IntEnum): + FG_APC_DEFAULTS =_int32(0x0) + FG_APC_BATCH_FRAMES =_int32(0x1) + FG_APC_IGNORE_TIMEOUTS =_int32(0x2) + FG_APC_IGNORE_APCFUNC_RETURN=_int32(0x4) + FG_APC_IGNORE_STOP =_int32(0x8) + FG_APC_HIGH_PRIORITY =_int32(0x10) + FG_APC_DELIVER_ERRORS =_int32(0x20) +dFg_Apc_Flag={a.name:a.value for a in Fg_Apc_Flag} +drFg_Apc_Flag={a.value:a.name for a in Fg_Apc_Flag} + + +class FgApcControlFlags(enum.IntEnum): + FG_APC_CONTROL_BASIC=_int32(0) +dFgApcControlFlags={a.name:a.value for a in FgApcControlFlags} +drFgApcControlFlags={a.value:a.name for a in FgApcControlFlags} + + +Fg_EventFunc_t=ctypes.c_void_p +class FgEventControlFlags(enum.IntEnum): + FG_EVENT_DEFAULT_FLAGS=_int32(0) + FG_EVENT_BATCHED =_int32(0x1) +dFgEventControlFlags={a.name:a.value for a in FgEventControlFlags} +drFgEventControlFlags={a.value:a.name for a in FgEventControlFlags} + + +class FgEventNotifiers(enum.IntEnum): + FG_EVENT_NOTIFY_JOINED =_int32(0x1) + FG_EVENT_NOTIFY_TIMESTAMP=_int32(0x2) + FG_EVENT_NOTIFY_PAYLOAD =_int32(0x4) + FG_EVENT_NOTIFY_LOST =_int32(0x8) +dFgEventNotifiers={a.name:a.value for a in FgEventNotifiers} +drFgEventNotifiers={a.value:a.name for a in FgEventNotifiers} + + +Fg_AsyncNotifyFunc_t=ctypes.c_void_p +class CCsel(enum.IntEnum): + CC_EXSYNC =_int32(0) + CC_PRESCALER =_int32(1) + CC_HDSYNC =_int32(CC_PRESCALER) + CC_EXSYNC2 =_int32(CC_PRESCALER) + CC_STROBEPULSE =_int32(2) + CC_CLK =_int32(3) + CC_GND =_int32(4) + CC_VCC =_int32(5) + CC_NOT_EXSYNC =_int32(6) + CC_NOT_PRESCALER =_int32(7) + CC_NOT_HDSYNC =_int32(CC_NOT_PRESCALER) + CC_NOT_EXSYNC2 =_int32(CC_NOT_PRESCALER) + CC_NOT_STROBEPULSE=_int32(8) + FG_OTHER =_int32((-1)) +dCCsel={a.name:a.value for a in CCsel} +drCCsel={a.value:a.name for a in CCsel} + + +class SignalSelectLine(enum.IntEnum): + FG_SIGNAL_CAM0_EXSYNC =_int32(2000) + FG_SIGNAL_CAM0_EXSYNC2 =_int32(2001) + FG_SIGNAL_CAM0_FLASH =_int32(2002) + FG_SIGNAL_CAM0_LVAL =_int32(2007) + FG_SIGNAL_CAM0_FVAL =_int32(2008) + FG_SIGNAL_CAM0_LINE_START =_int32(2100) + FG_SIGNAL_CAM0_LINE_END =_int32(2101) + FG_SIGNAL_CAM0_FRAME_START=_int32(2102) + FG_SIGNAL_CAM0_FRAME_END =_int32(2103) + FG_SIGNAL_CAM1_EXSYNC =_int32(2010) + FG_SIGNAL_CAM1_EXSYNC2 =_int32(2011) + FG_SIGNAL_CAM1_FLASH =_int32(2012) + FG_SIGNAL_CAM1_LVAL =_int32(2017) + FG_SIGNAL_CAM1_FVAL =_int32(2018) + FG_SIGNAL_CAM1_LINE_START =_int32(2110) + FG_SIGNAL_CAM1_LINE_END =_int32(2111) + FG_SIGNAL_CAM1_FRAME_START=_int32(2112) + FG_SIGNAL_CAM1_FRAME_END =_int32(2113) + FG_SIGNAL_CAM2_EXSYNC =_int32(2020) + FG_SIGNAL_CAM2_EXSYNC2 =_int32(2021) + FG_SIGNAL_CAM2_FLASH =_int32(2022) + FG_SIGNAL_CAM2_LVAL =_int32(2027) + FG_SIGNAL_CAM2_FVAL =_int32(2028) + FG_SIGNAL_CAM2_LINE_START =_int32(2120) + FG_SIGNAL_CAM2_LINE_END =_int32(2121) + FG_SIGNAL_CAM2_FRAME_START=_int32(2122) + FG_SIGNAL_CAM2_FRAME_END =_int32(2123) + FG_SIGNAL_CAM3_EXSYNC =_int32(2030) + FG_SIGNAL_CAM3_EXSYNC2 =_int32(2031) + FG_SIGNAL_CAM3_FLASH =_int32(2032) + FG_SIGNAL_CAM3_LVAL =_int32(2037) + FG_SIGNAL_CAM3_FVAL =_int32(2038) + FG_SIGNAL_CAM3_LINE_START =_int32(2130) + FG_SIGNAL_CAM3_LINE_END =_int32(2131) + FG_SIGNAL_CAM3_FRAME_START=_int32(2132) + FG_SIGNAL_CAM3_FRAME_END =_int32(2133) + FG_SIGNAL_GPI_0 =_int32(1001) + FG_SIGNAL_GPI_1 =_int32(1011) + FG_SIGNAL_GPI_2 =_int32(1021) + FG_SIGNAL_GPI_3 =_int32(1031) + FG_SIGNAL_GPI_4 =_int32(1041) + FG_SIGNAL_GPI_5 =_int32(1051) + FG_SIGNAL_GPI_6 =_int32(1061) + FG_SIGNAL_GPI_7 =_int32(1071) + FG_SIGNAL_FRONT_GPI_0 =_int32(1081) + FG_SIGNAL_FRONT_GPI_1 =_int32(1091) + FG_SIGNAL_FRONT_GPI_2 =_int32(1101) + FG_SIGNAL_FRONT_GPI_3 =_int32(1111) +dSignalSelectLine={a.name:a.value for a in SignalSelectLine} +drSignalSelectLine={a.value:a.name for a in SignalSelectLine} + + +class CcSignalMappingArea(enum.IntEnum): + CC_PULSEGEN0 =_int32(0) + CC_PULSEGEN1 =_int32(1) + CC_PULSEGEN2 =_int32(2) + CC_PULSEGEN3 =_int32(3) + CC_NOT_PULSEGEN0 =_int32(6) + CC_NOT_PULSEGEN1 =_int32(7) + CC_NOT_PULSEGEN2 =_int32(8) + CC_NOT_PULSEGEN3 =_int32(9) + CC_INPUT_BYPASS =_int32(10) + CC_NOT_INPUT_BYPASS=_int32(11) +dCcSignalMappingArea={a.name:a.value for a in CcSignalMappingArea} +drCcSignalMappingArea={a.value:a.name for a in CcSignalMappingArea} + + +class CcSignalMappingLineExtended(enum.IntEnum): + CC_GPI_0 =_int32(1001) + CC_NOT_GPI_0 =_int32(1000) + CC_GPI_1 =_int32(1011) + CC_NOT_GPI_1 =_int32(1010) + CC_GPI_2 =_int32(1021) + CC_NOT_GPI_2 =_int32(1020) + CC_GPI_3 =_int32(1031) + CC_NOT_GPI_3 =_int32(1030) + CC_GPI_4 =_int32(1041) + CC_NOT_GPI_4 =_int32(1040) + CC_GPI_5 =_int32(1051) + CC_NOT_GPI_5 =_int32(1050) + CC_GPI_6 =_int32(1061) + CC_NOT_GPI_6 =_int32(1060) + CC_GPI_7 =_int32(1071) + CC_NOT_GPI_7 =_int32(1070) + CC_FRONT_GPI_0 =_int32(1081) + CC_NOT_FRONT_GPI_0=_int32(1080) + CC_FRONT_GPI_1 =_int32(1091) + CC_NOT_FRONT_GPI_1=_int32(1090) + CC_FRONT_GPI_2 =_int32(1101) + CC_NOT_FRONT_GPI_2=_int32(1100) + CC_FRONT_GPI_3 =_int32(1111) + CC_NOT_FRONT_GPI_3=_int32(1110) +dCcSignalMappingLineExtended={a.name:a.value for a in CcSignalMappingLineExtended} +drCcSignalMappingLineExtended={a.value:a.name for a in CcSignalMappingLineExtended} + + +class Fg_PoCXPState(enum.IntEnum): + BOOTING =_int32(0x001) + NOCABLE =_int32(0x002) + NOPOCXP =_int32(0x004) + POCXPOK =_int32(0x008) + MIN_CURR =_int32(0x010) + MAX_CURR =_int32(0x020) + LOW_VOLT =_int32(0x040) + OVER_VOLT =_int32(0x080) + ADC_Chip_Error=_int32(0x100) +dFg_PoCXPState={a.name:a.value for a in Fg_PoCXPState} +drFg_PoCXPState={a.value:a.name for a in Fg_PoCXPState} + + +class VantagePointNamingConvention(enum.IntEnum): + FG_VANTAGEPOINT_TOP_LEFT =_int32(0) + FG_VANTAGEPOINT_TOP_RIGHT =_int32(1) + FG_VANTAGEPOINT_BOTTOM_LEFT =_int32(2) + FG_VANTAGEPOINT_BOTTOM_RIGHT=_int32(3) +dVantagePointNamingConvention={a.name:a.value for a in VantagePointNamingConvention} +drVantagePointNamingConvention={a.value:a.name for a in VantagePointNamingConvention} + + +class TapGeometryNamingConvention(enum.IntEnum): + FG_GEOMETRY_1X =_int32(0x01100000) + FG_GEOMETRY_1X2 =_int32(0x01200000) + FG_GEOMETRY_2X =_int32(0x02100000) + FG_GEOMETRY_2XE =_int32(0x02110000) + FG_GEOMETRY_2XM =_int32(0x02120000) + FG_GEOMETRY_1X3 =_int32(0x01300000) + FG_GEOMETRY_3X =_int32(0x03100000) + FG_GEOMETRY_1X4 =_int32(0x01400000) + FG_GEOMETRY_4X =_int32(0x04100000) + FG_GEOMETRY_4XE =_int32(0x04110000) + FG_GEOMETRY_2X2 =_int32(0x02200000) + FG_GEOMETRY_2X2E =_int32(0x02210000) + FG_GEOMETRY_2X2M =_int32(0x02220000) + FG_GEOMETRY_1X8 =_int32(0x01800000) + FG_GEOMETRY_8X =_int32(0x08100000) + FG_GEOMETRY_1X10 =_int32(0x01A00000) + FG_GEOMETRY_10X =_int32(0x0A100000) + FG_GEOMETRY_4X2 =_int32(0x04200000) + FG_GEOMETRY_4X2E =_int32(0x04210000) + FG_GEOMETRY_5X2 =_int32(0x05200000) + FG_GEOMETRY_1X_1Y =_int32(0x01100110) + FG_GEOMETRY_1X_2Y =_int32(0x01100210) + FG_GEOMETRY_1X_2YE =_int32(0x01100211) + FG_GEOMETRY_2X_1Y =_int32(0x02100110) + FG_GEOMETRY_2XE_1Y =_int32(0x02110110) + FG_GEOMETRY_2XM_1Y =_int32(0x02120110) + FG_GEOMETRY_2X_2Y =_int32(0x02100210) + FG_GEOMETRY_2X_2YE =_int32(0x02100211) + FG_GEOMETRY_2XE_2Y =_int32(0x02110210) + FG_GEOMETRY_2XE_2YE=_int32(0x02110211) + FG_GEOMETRY_2XM_2Y =_int32(0x02120210) + FG_GEOMETRY_2XM_2YE=_int32(0x02120211) + FG_GEOMETRY_4X_1Y =_int32(0x04100110) + FG_GEOMETRY_1X2_1Y =_int32(0x01200110) + FG_GEOMETRY_1X3_1Y =_int32(0x01300110) + FG_GEOMETRY_1X4_1Y =_int32(0x01400110) + FG_GEOMETRY_2X2_1Y =_int32(0x02200110) + FG_GEOMETRY_2X2E_1Y=_int32(0x02210110) + FG_GEOMETRY_2X2M_1Y=_int32(0x02220110) + FG_GEOMETRY_1X2_2YE=_int32(0x01200211) +dTapGeometryNamingConvention={a.name:a.value for a in TapGeometryNamingConvention} +drTapGeometryNamingConvention={a.value:a.name for a in TapGeometryNamingConvention} + + +class PixelFormatNamingConvention(enum.IntEnum): + Mono8 =_int32(257) + Mono10 =_int32(258) + Mono12 =_int32(259) + Mono14 =_int32(260) + Mono16 =_int32(261) + BayerGR8 =_int32(785) + BayerGR10=_int32(786) + BayerGR12=_int32(787) + BayerGR14=_int32(788) + BayerRG8 =_int32(801) + BayerRG10=_int32(802) + BayerRG12=_int32(803) + BayerRG14=_int32(804) + BayerGB8 =_int32(817) + BayerGB10=_int32(818) + BayerGB12=_int32(819) + BayerGB14=_int32(820) + BayerBG8 =_int32(833) + BayerBG10=_int32(834) + BayerBG12=_int32(835) + BayerBG14=_int32(836) + RGB8 =_int32(1025) + RGB10 =_int32(1026) + RGB12 =_int32(1027) + RGB14 =_int32(1028) + RGB16 =_int32(1029) + RGBA8 =_int32(1281) + RGBA10 =_int32(1282) + RGBA12 =_int32(1283) + RGBA14 =_int32(1284) + RGBA16 =_int32(1285) +dPixelFormatNamingConvention={a.name:a.value for a in PixelFormatNamingConvention} +drPixelFormatNamingConvention={a.value:a.name for a in PixelFormatNamingConvention} + + +class BayerOrdering(enum.IntEnum): + GreenFollowedByRed =_int32(3) + GreenFollowedByBlue=_int32(0) + RedFollowedByGreen =_int32(2) + BlueFollowedByGreen=_int32(1) +dBayerOrdering={a.name:a.value for a in BayerOrdering} +drBayerOrdering={a.value:a.name for a in BayerOrdering} + + +class BayerBilinearLineOrdering(enum.IntEnum): + RedBlueLineFollowedByGreenLine=_int32(10) + BlueRedLineFollowedByGreenLine=_int32(11) + GreenLineFollowedByRedBlueLine=_int32(12) + GreenLineFollowedByBlueRedLine=_int32(13) +dBayerBilinearLineOrdering={a.name:a.value for a in BayerBilinearLineOrdering} +drBayerBilinearLineOrdering={a.value:a.name for a in BayerBilinearLineOrdering} + + +class GigEPixelFormat(enum.IntEnum): + MONO8 =_int32(0) + MONO8_SIGNED =_int32(1) + MONO10 =_int32(2) + MONO10_PACKED=_int32(3) + MONO12 =_int32(4) + MONO12_PACKED=_int32(5) + MONO14 =_int32(7) + MONO16 =_int32(6) +dGigEPixelFormat={a.name:a.value for a in GigEPixelFormat} +drGigEPixelFormat={a.value:a.name for a in GigEPixelFormat} + + +class CXPTriggerPackedModes(enum.IntEnum): + FG_STANDARD =_int32(0) + FG_RISING_EDGE_ONLY=_int32(1) +dCXPTriggerPackedModes={a.name:a.value for a in CXPTriggerPackedModes} +drCXPTriggerPackedModes={a.value:a.name for a in CXPTriggerPackedModes} + + +Fg_AppletIteratorType=ctypes.c_void_p +Fg_AppletIteratorItem=ctypes.c_void_p +class FgAppletIteratorSource(enum.IntEnum): + FG_AIS_BOARD =_int32(0) + FG_AIS_FILESYSTEM=enum.auto() +dFgAppletIteratorSource={a.name:a.value for a in FgAppletIteratorSource} +drFgAppletIteratorSource={a.value:a.name for a in FgAppletIteratorSource} + + +class FgAppletIntProperty(enum.IntEnum): + FG_AP_INT_FLAGS =_int32(0) + FG_AP_INT_INFO =enum.auto() + FG_AP_INT_PARTITION =enum.auto() + FG_AP_INT_NR_OF_DMA =enum.auto() + FG_AP_INT_NR_OF_CAMS =enum.auto() + FG_AP_INT_GROUP_CODE =enum.auto() + FG_AP_INT_USER_CODE =enum.auto() + FG_AP_INT_BOARD_GROUP_MASK=enum.auto() + FG_AP_INT_BOARD_USER_CODE =enum.auto() + FG_AP_INT_ICON_SIZE =enum.auto() + FG_AP_INT_LAG =enum.auto() + FG_AP_INT_DESIGN_VERSION =enum.auto() + FG_AP_INT_DESIGN_REVISION =enum.auto() +dFgAppletIntProperty={a.name:a.value for a in FgAppletIntProperty} +drFgAppletIntProperty={a.value:a.name for a in FgAppletIntProperty} + + +class FgAppletStringProperty(enum.IntEnum): + FG_AP_STRING_APPLET_UID =_int32(0) + FG_AP_STRING_BITSTREAM_UID =enum.auto() + FG_AP_STRING_DESIGN_NAME =enum.auto() + FG_AP_STRING_APPLET_NAME =enum.auto() + FG_AP_STRING_DESCRIPTION =enum.auto() + FG_AP_STRING_CATEGORY =enum.auto() + FG_AP_STRING_APPLET_PATH =enum.auto() + FG_AP_STRING_ICON =enum.auto() + FG_AP_STRING_SUPPORTED_PLATFORMS=enum.auto() + FG_AP_STRING_TAGS =enum.auto() + FG_AP_STRING_VERSION =enum.auto() + FG_AP_STRING_APPLET_FILE =enum.auto() + FG_AP_STRING_RUNTIME_VERSION =enum.auto() +dFgAppletStringProperty={a.name:a.value for a in FgAppletStringProperty} +drFgAppletStringProperty={a.value:a.name for a in FgAppletStringProperty} + + +class LookupTable(ctypes.Structure): + _fields_=[ ("lut",ctypes.POINTER(ctypes.c_uint)), + ("id",ctypes.c_uint), + ("nrOfElements",ctypes.c_uint), + ("format",ctypes.c_uint), + ("number",ctypes.c_ubyte) ] +PLookupTable=ctypes.POINTER(LookupTable) +class CLookupTable(ctypes_wrap.CStructWrapper): + _struct=LookupTable + + +class KneeLookupTable(ctypes.Structure): + _fields_=[ ("value",ctypes.POINTER(ctypes.c_double)), + ("reserved",ctypes.POINTER(ctypes.c_double)), + ("id",ctypes.c_uint), + ("nrOfElements",ctypes.c_uint), + ("format",ctypes.c_uint), + ("number",ctypes.c_ubyte) ] +PKneeLookupTable=ctypes.POINTER(KneeLookupTable) +class CKneeLookupTable(ctypes_wrap.CStructWrapper): + _struct=KneeLookupTable + + +class ShadingParameter(ctypes.Structure): + _fields_=[ ("offset",ctypes.POINTER(ctypes.c_ubyte)), + ("cmult",ctypes.POINTER(ctypes.c_ubyte)), + ("mult",ctypes.POINTER(ctypes.c_float)), + ("nrOfElements",ctypes.c_uint), + ("width",ctypes.c_int), + ("height",ctypes.c_int), + ("set",ctypes.c_int) ] +PShadingParameter=ctypes.POINTER(ShadingParameter) +class CShadingParameter(ctypes_wrap.CStructWrapper): + _struct=ShadingParameter + + +class LineShadingParameter(ctypes.Structure): + _fields_=[ ("mShadingData",ctypes.c_uint*4096), + ("mNoOfPixelsInit",ctypes.c_int) ] +PLineShadingParameter=ctypes.POINTER(LineShadingParameter) +class CLineShadingParameter(ctypes_wrap.CStructWrapper): + _struct=LineShadingParameter + + +class FieldParameterInt(ctypes.Structure): + _fields_=[ ("value",ctypes.c_uint32), + ("index",ctypes.c_uint) ] +PFieldParameterInt=ctypes.POINTER(FieldParameterInt) +class CFieldParameterInt(ctypes_wrap.CStructWrapper): + _struct=FieldParameterInt + + +class FieldParameterDouble(ctypes.Structure): + _fields_=[ ("value",ctypes.c_double), + ("index",ctypes.c_uint) ] +PFieldParameterDouble=ctypes.POINTER(FieldParameterDouble) +class CFieldParameterDouble(ctypes_wrap.CStructWrapper): + _struct=FieldParameterDouble + + +class FieldParameterAccess(ctypes.Structure): + _fields_=[ ("vtype",ctypes.c_int), + ("index",ctypes.c_uint), + ("count",ctypes.c_uint), + ("p_int32_t",ctypes.POINTER(ctypes.c_int32)) ] +PFieldParameterAccess=ctypes.POINTER(FieldParameterAccess) +class CFieldParameterAccess(ctypes_wrap.CStructWrapper): + _struct=FieldParameterAccess + + +class FgApcControl(ctypes.Structure): + _fields_=[ ("version",ctypes.c_uint), + ("func",Fg_ApcFunc_t), + ("data",ctypes.c_void_p), + ("timeout",ctypes.c_uint), + ("flags",ctypes.c_uint) ] +PFgApcControl=ctypes.POINTER(FgApcControl) +class CFgApcControl(ctypes_wrap.CStructWrapper): + _struct=FgApcControl + + + + + +##### FUNCTION DEFINITIONS ##### + + + + + +def addfunc(lib, name, restype, argtypes=None, argnames=None): + if getattr(lib,name,None) is None: + setattr(lib,name,None) + else: + func=getattr(lib,name) + func.restype=restype + if argtypes is not None: + func.argtypes=argtypes + if argnames is not None: + func.argnames=argnames + +def define_functions(lib): + # ctypes.c_int CreateDisplay(ctypes.c_uint nDepth, ctypes.c_uint nWidth, ctypes.c_uint nHeight) + addfunc(lib, "CreateDisplay", restype = ctypes.c_int, + argtypes = [ctypes.c_uint, ctypes.c_uint, ctypes.c_uint], + argnames = ["nDepth", "nWidth", "nHeight"] ) + # None SetBufferWidth(ctypes.c_int nId, ctypes.c_uint nWidth, ctypes.c_uint nHeight) + addfunc(lib, "SetBufferWidth", restype = None, + argtypes = [ctypes.c_int, ctypes.c_uint, ctypes.c_uint], + argnames = ["nId", "nWidth", "nHeight"] ) + # None DrawBuffer(ctypes.c_int nId, ctypes.c_void_p ulpBuf, ctypes.c_int nNr, ctypes.c_char_p cpStr) + addfunc(lib, "DrawBuffer", restype = None, + argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p], + argnames = ["nId", "ulpBuf", "nNr", "cpStr"] ) + # None CloseDisplay(ctypes.c_int nId) + addfunc(lib, "CloseDisplay", restype = None, + argtypes = [ctypes.c_int], + argnames = ["nId"] ) + # ctypes.c_int SetDisplayDepth(ctypes.c_int nId, ctypes.c_uint depth) + addfunc(lib, "SetDisplayDepth", restype = ctypes.c_int, + argtypes = [ctypes.c_int, ctypes.c_uint], + argnames = ["nId", "depth"] ) + # ctypes.c_int Fg_InitLibraries(ctypes.c_char_p sisoDir) + addfunc(lib, "Fg_InitLibraries", restype = ctypes.c_int, + argtypes = [ctypes.c_char_p], + argnames = ["sisoDir"] ) + # ctypes.c_int Fg_InitLibrariesEx(ctypes.c_char_p sisoDir, ctypes.c_uint flags, ctypes.c_char_p id, ctypes.c_uint timeout) + addfunc(lib, "Fg_InitLibrariesEx", restype = ctypes.c_int, + argtypes = [ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.c_uint], + argnames = ["sisoDir", "flags", "id", "timeout"] ) + # None Fg_AbortInitLibraries() + addfunc(lib, "Fg_AbortInitLibraries", restype = None, + argtypes = [], + argnames = [] ) + # None Fg_InitLibrariesStartNextSlave() + addfunc(lib, "Fg_InitLibrariesStartNextSlave", restype = None, + argtypes = [], + argnames = [] ) + # None Fg_FreeLibraries() + addfunc(lib, "Fg_FreeLibraries", restype = None, + argtypes = [], + argnames = [] ) + # ctypes.c_int Fg_findApplet(ctypes.c_uint BoardIndex, ctypes.c_char_p Path, ctypes.c_size_t Size) + addfunc(lib, "Fg_findApplet", restype = ctypes.c_int, + argtypes = [ctypes.c_uint, ctypes.c_char_p, ctypes.c_size_t], + argnames = ["BoardIndex", "Path", "Size"] ) + # ctypes.c_void_p Fg_Init(ctypes.c_char_p FileName, ctypes.c_uint BoardIndex) + addfunc(lib, "Fg_Init", restype = ctypes.c_void_p, + argtypes = [ctypes.c_char_p, ctypes.c_uint], + argnames = ["FileName", "BoardIndex"] ) + # ctypes.c_void_p Fg_InitConfig(ctypes.c_char_p Config_Name, ctypes.c_uint BoardIndex) + addfunc(lib, "Fg_InitConfig", restype = ctypes.c_void_p, + argtypes = [ctypes.c_char_p, ctypes.c_uint], + argnames = ["Config_Name", "BoardIndex"] ) + # ctypes.c_void_p Fg_InitEx(ctypes.c_char_p FileName, ctypes.c_uint BoardIndex, ctypes.c_int flags) + addfunc(lib, "Fg_InitEx", restype = ctypes.c_void_p, + argtypes = [ctypes.c_char_p, ctypes.c_uint, ctypes.c_int], + argnames = ["FileName", "BoardIndex", "flags"] ) + # ctypes.c_void_p Fg_InitConfigEx(ctypes.c_char_p Config_Name, ctypes.c_uint BoardIndex, ctypes.c_int flags) + addfunc(lib, "Fg_InitConfigEx", restype = ctypes.c_void_p, + argtypes = [ctypes.c_char_p, ctypes.c_uint, ctypes.c_int], + argnames = ["Config_Name", "BoardIndex", "flags"] ) + # ctypes.c_int Fg_FreeGrabber(ctypes.c_void_p Fg) + addfunc(lib, "Fg_FreeGrabber", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_void_p Fg_AllocMem(ctypes.c_void_p Fg, ctypes.c_size_t Size, frameindex_t BufCnt, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_AllocMem", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, ctypes.c_size_t, frameindex_t, ctypes.c_uint], + argnames = ["Fg", "Size", "BufCnt", "DmaIndex"] ) + # ctypes.c_void_p Fg_AllocMemEx(ctypes.c_void_p Fg, ctypes.c_size_t Size, frameindex_t BufCnt) + addfunc(lib, "Fg_AllocMemEx", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, ctypes.c_size_t, frameindex_t], + argnames = ["Fg", "Size", "BufCnt"] ) + # ctypes.c_int Fg_FreeMem(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_FreeMem", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint], + argnames = ["Fg", "DmaIndex"] ) + # ctypes.c_int Fg_FreeMemEx(ctypes.c_void_p Fg, ctypes.c_void_p mem) + addfunc(lib, "Fg_FreeMemEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "mem"] ) + # ctypes.c_void_p Fg_AllocMemHead(ctypes.c_void_p Fg, ctypes.c_size_t Size, frameindex_t BufCnt) + addfunc(lib, "Fg_AllocMemHead", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, ctypes.c_size_t, frameindex_t], + argnames = ["Fg", "Size", "BufCnt"] ) + # ctypes.c_int Fg_FreeMemHead(ctypes.c_void_p Fg, ctypes.c_void_p memHandle) + addfunc(lib, "Fg_FreeMemHead", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "memHandle"] ) + # ctypes.c_int Fg_AddMem(ctypes.c_void_p Fg, ctypes.c_void_p pBuffer, ctypes.c_size_t Size, frameindex_t bufferIndex, ctypes.c_void_p memHandle) + addfunc(lib, "Fg_AddMem", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, frameindex_t, ctypes.c_void_p], + argnames = ["Fg", "pBuffer", "Size", "bufferIndex", "memHandle"] ) + # ctypes.c_int Fg_DelMem(ctypes.c_void_p Fg, ctypes.c_void_p memHandle, frameindex_t bufferIndex) + addfunc(lib, "Fg_DelMem", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p, frameindex_t], + argnames = ["Fg", "memHandle", "bufferIndex"] ) + # ctypes.c_void_p Fg_NumaAllocDmaBuffer(ctypes.c_void_p Fg, ctypes.c_size_t Size) + addfunc(lib, "Fg_NumaAllocDmaBuffer", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, ctypes.c_size_t], + argnames = ["Fg", "Size"] ) + # ctypes.c_int Fg_NumaFreeDmaBuffer(ctypes.c_void_p Fg, ctypes.c_void_p Buffer) + addfunc(lib, "Fg_NumaFreeDmaBuffer", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "Buffer"] ) + # ctypes.c_int Fg_NumaPinThread(ctypes.c_void_p Fg) + addfunc(lib, "Fg_NumaPinThread", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_int Fg_getNrOfParameter(ctypes.c_void_p Fg) + addfunc(lib, "Fg_getNrOfParameter", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_int Fg_getParameterId(ctypes.c_void_p fg, ctypes.c_int index) + addfunc(lib, "Fg_getParameterId", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int], + argnames = ["fg", "index"] ) + # ctypes.c_char_p Fg_getParameterName(ctypes.c_void_p fg, ctypes.c_int index) + addfunc(lib, "Fg_getParameterName", restype = ctypes.c_char_p, + argtypes = [ctypes.c_void_p, ctypes.c_int], + argnames = ["fg", "index"] ) + # ctypes.c_int Fg_getParameterIdByName(ctypes.c_void_p fg, ctypes.c_char_p name) + addfunc(lib, "Fg_getParameterIdByName", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_char_p], + argnames = ["fg", "name"] ) + # ctypes.c_char_p Fg_getParameterNameById(ctypes.c_void_p fg, ctypes.c_uint id, ctypes.c_uint dma) + addfunc(lib, "Fg_getParameterNameById", restype = ctypes.c_char_p, + argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint], + argnames = ["fg", "id", "dma"] ) + # ctypes.c_int Fg_setParameter(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_setParameter", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint], + argnames = ["Fg", "Parameter", "Value", "DmaIndex"] ) + # ctypes.c_int Fg_setParameterWithType(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_int type) + addfunc(lib, "Fg_setParameterWithType", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint, ctypes.c_int], + argnames = ["Fg", "Parameter", "Value", "DmaIndex", "type"] ) + # ctypes.c_int Fg_getParameter(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_getParameter", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint], + argnames = ["Fg", "Parameter", "Value", "DmaIndex"] ) + # ctypes.c_int Fg_getParameterWithType(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_int type) + addfunc(lib, "Fg_getParameterWithType", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint, ctypes.c_int], + argnames = ["Fg", "Parameter", "Value", "DmaIndex", "type"] ) + # ctypes.c_int Fg_freeParameterStringWithType(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_int type) + addfunc(lib, "Fg_freeParameterStringWithType", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint, ctypes.c_int], + argnames = ["Fg", "Parameter", "Value", "DmaIndex", "type"] ) + # ctypes.c_int Fg_getParameterEx(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem, frameindex_t ImgNr) + addfunc(lib, "Fg_getParameterEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, frameindex_t], + argnames = ["Fg", "Parameter", "Value", "DmaIndex", "pMem", "ImgNr"] ) + # ctypes.c_int Fg_getParameterInfoXML(ctypes.c_void_p Fg, ctypes.c_int port, ctypes.c_char_p infoBuffer, ctypes.POINTER(ctypes.c_size_t) infoBufferSize) + addfunc(lib, "Fg_getParameterInfoXML", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.POINTER(ctypes.c_size_t)], + argnames = ["Fg", "port", "infoBuffer", "infoBufferSize"] ) + # ctypes.c_int Fg_getBitsPerPixel(ctypes.c_int format) + addfunc(lib, "Fg_getBitsPerPixel", restype = ctypes.c_int, + argtypes = [ctypes.c_int], + argnames = ["format"] ) + # ctypes.c_int Fg_saveConfig(ctypes.c_void_p Fg, ctypes.c_char_p Filename) + addfunc(lib, "Fg_saveConfig", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_char_p], + argnames = ["Fg", "Filename"] ) + # ctypes.c_int Fg_saveFieldParameterToFile(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_uint DmaIndex, ctypes.c_char_p FileName) + addfunc(lib, "Fg_saveFieldParameterToFile", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint, ctypes.c_char_p], + argnames = ["Fg", "Parameter", "DmaIndex", "FileName"] ) + # ctypes.c_int Fg_loadConfig(ctypes.c_void_p Fg, ctypes.c_char_p Filename) + addfunc(lib, "Fg_loadConfig", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_char_p], + argnames = ["Fg", "Filename"] ) + # ctypes.c_int Fg_loadFieldParameterFromFile(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_uint DmaIndex, ctypes.c_char_p FileName) + addfunc(lib, "Fg_loadFieldParameterFromFile", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint, ctypes.c_char_p], + argnames = ["Fg", "Parameter", "DmaIndex", "FileName"] ) + # ctypes.c_int Fg_Acquire(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, frameindex_t PicCount) + addfunc(lib, "Fg_Acquire", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint, frameindex_t], + argnames = ["Fg", "DmaIndex", "PicCount"] ) + # ctypes.c_int Fg_stopAcquire(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_stopAcquire", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint], + argnames = ["Fg", "DmaIndex"] ) + # frameindex_t Fg_getLastPicNumberBlocking(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_int Timeout) + addfunc(lib, "Fg_getLastPicNumberBlocking", restype = frameindex_t, + argtypes = [ctypes.c_void_p, frameindex_t, ctypes.c_uint, ctypes.c_int], + argnames = ["Fg", "PicNr", "DmaIndex", "Timeout"] ) + # frameindex_t Fg_getLastPicNumber(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_getLastPicNumber", restype = frameindex_t, + argtypes = [ctypes.c_void_p, ctypes.c_uint], + argnames = ["Fg", "DmaIndex"] ) + # frameindex_t Fg_getLastPicNumberBlockingEx(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_int Timeout, ctypes.c_void_p pMem) + addfunc(lib, "Fg_getLastPicNumberBlockingEx", restype = frameindex_t, + argtypes = [ctypes.c_void_p, frameindex_t, ctypes.c_uint, ctypes.c_int, ctypes.c_void_p], + argnames = ["Fg", "PicNr", "DmaIndex", "Timeout", "pMem"] ) + # frameindex_t Fg_getLastPicNumberEx(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + addfunc(lib, "Fg_getLastPicNumberEx", restype = frameindex_t, + argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "DmaIndex", "pMem"] ) + # ctypes.c_void_p Fg_getImagePtr(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_getImagePtr", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, frameindex_t, ctypes.c_uint], + argnames = ["Fg", "PicNr", "DmaIndex"] ) + # ctypes.c_void_p Fg_getImagePtrEx(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + addfunc(lib, "Fg_getImagePtrEx", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, frameindex_t, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "PicNr", "DmaIndex", "pMem"] ) + # ctypes.c_int Fg_AcquireEx(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, frameindex_t PicCount, ctypes.c_int nFlag, ctypes.c_void_p memHandle) + addfunc(lib, "Fg_AcquireEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint, frameindex_t, ctypes.c_int, ctypes.c_void_p], + argnames = ["Fg", "DmaIndex", "PicCount", "nFlag", "memHandle"] ) + # ctypes.c_int Fg_sendImage(ctypes.c_void_p Fg, frameindex_t startImage, frameindex_t PicCount, ctypes.c_int nFlag, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_sendImage", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, frameindex_t, frameindex_t, ctypes.c_int, ctypes.c_uint], + argnames = ["Fg", "startImage", "PicCount", "nFlag", "DmaIndex"] ) + # ctypes.c_int Fg_sendImageEx(ctypes.c_void_p Fg, frameindex_t startImage, frameindex_t PicCount, ctypes.c_int nFlag, ctypes.c_uint DmaIndex, ctypes.c_void_p memHandle) + addfunc(lib, "Fg_sendImageEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, frameindex_t, frameindex_t, ctypes.c_int, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "startImage", "PicCount", "nFlag", "DmaIndex", "memHandle"] ) + # ctypes.c_int Fg_stopAcquireEx(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, ctypes.c_void_p memHandle, ctypes.c_int nFlag) + addfunc(lib, "Fg_stopAcquireEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_int], + argnames = ["Fg", "DmaIndex", "memHandle", "nFlag"] ) + # frameindex_t Fg_getImage(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_uint Timeout) + addfunc(lib, "Fg_getImage", restype = frameindex_t, + argtypes = [ctypes.c_void_p, ctypes.c_int, frameindex_t, ctypes.c_uint, ctypes.c_uint], + argnames = ["Fg", "Param", "PicNr", "DmaIndex", "Timeout"] ) + # frameindex_t Fg_getImageEx(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_uint Timeout, ctypes.c_void_p pMem) + addfunc(lib, "Fg_getImageEx", restype = frameindex_t, + argtypes = [ctypes.c_void_p, ctypes.c_int, frameindex_t, ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "Param", "PicNr", "DmaIndex", "Timeout", "pMem"] ) + # ctypes.c_int Fg_registerApcHandler(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, ctypes.c_void_p control, ctypes.c_int flags) + addfunc(lib, "Fg_registerApcHandler", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_int], + argnames = ["Fg", "DmaIndex", "control", "flags"] ) + # ctypes.c_int Fg_getLastErrorNumber(ctypes.c_void_p Fg) + addfunc(lib, "Fg_getLastErrorNumber", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_char_p getErrorDescription(ctypes.c_int ErrorNumber) + addfunc(lib, "getErrorDescription", restype = ctypes.c_char_p, + argtypes = [ctypes.c_int], + argnames = ["ErrorNumber"] ) + # ctypes.c_char_p Fg_getLastErrorDescription(ctypes.c_void_p Fg) + addfunc(lib, "Fg_getLastErrorDescription", restype = ctypes.c_char_p, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_char_p Fg_getErrorDescription(ctypes.c_void_p Fg, ctypes.c_int ErrorNumber) + addfunc(lib, "Fg_getErrorDescription", restype = ctypes.c_char_p, + argtypes = [ctypes.c_void_p, ctypes.c_int], + argnames = ["Fg", "ErrorNumber"] ) + # ctypes.c_int Fg_getBoardType(ctypes.c_int BoardIndex) + addfunc(lib, "Fg_getBoardType", restype = ctypes.c_int, + argtypes = [ctypes.c_int], + argnames = ["BoardIndex"] ) + # ctypes.c_char_p Fg_getBoardNameByType(ctypes.c_int BoardType, ctypes.c_int UseShortName) + addfunc(lib, "Fg_getBoardNameByType", restype = ctypes.c_char_p, + argtypes = [ctypes.c_int, ctypes.c_int], + argnames = ["BoardType", "UseShortName"] ) + # ctypes.c_uint Fg_getSerialNumber(ctypes.c_void_p Fg) + addfunc(lib, "Fg_getSerialNumber", restype = ctypes.c_uint, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_char_p Fg_getSWVersion() + addfunc(lib, "Fg_getSWVersion", restype = ctypes.c_char_p, + argtypes = [], + argnames = [] ) + # ctypes.c_char_p Fg_getAppletVersion(ctypes.c_void_p Fg, ctypes.c_int AppletId) + addfunc(lib, "Fg_getAppletVersion", restype = ctypes.c_char_p, + argtypes = [ctypes.c_void_p, ctypes.c_int], + argnames = ["Fg", "AppletId"] ) + # ctypes.c_int Fg_getAppletId(ctypes.c_void_p Fg, ctypes.c_char_p ignored) + addfunc(lib, "Fg_getAppletId", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_char_p], + argnames = ["Fg", "ignored"] ) + # ctypes.c_int Fg_getParameterProperty(ctypes.c_void_p Fg, ctypes.c_int parameterId, ctypes.c_int propertyId, ctypes.c_void_p buffer, ctypes.POINTER(ctypes.c_int) bufLen) + addfunc(lib, "Fg_getParameterProperty", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)], + argnames = ["Fg", "parameterId", "propertyId", "buffer", "bufLen"] ) + # ctypes.c_int Fg_getParameterPropertyEx(ctypes.c_void_p Fg, ctypes.c_int parameterId, ctypes.c_int propertyId, ctypes.c_int DmaIndex, ctypes.c_void_p buffer, ctypes.POINTER(ctypes.c_int) bufLen) + addfunc(lib, "Fg_getParameterPropertyEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)], + argnames = ["Fg", "parameterId", "propertyId", "DmaIndex", "buffer", "bufLen"] ) + # ctypes.c_int Fg_getSystemInformation(ctypes.c_void_p Fg, ctypes.c_int selector, ctypes.c_int propertyId, ctypes.c_int param1, ctypes.c_void_p buffer, ctypes.POINTER(ctypes.c_uint) bufLen) + addfunc(lib, "Fg_getSystemInformation", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint)], + argnames = ["Fg", "selector", "propertyId", "param1", "buffer", "bufLen"] ) + # ctypes.c_int Fg_readUserDataArea(ctypes.c_void_p Fg, ctypes.c_int boardId, ctypes.c_uint offs, ctypes.c_uint size, ctypes.c_void_p buffer) + addfunc(lib, "Fg_readUserDataArea", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "boardId", "offs", "size", "buffer"] ) + # ctypes.c_int Fg_writeUserDataArea(ctypes.c_void_p Fg, ctypes.c_int boardId, ctypes.c_uint offs, ctypes.c_uint size, ctypes.c_void_p buffer) + addfunc(lib, "Fg_writeUserDataArea", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "boardId", "offs", "size", "buffer"] ) + # frameindex_t Fg_getStatus(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_getStatus", restype = frameindex_t, + argtypes = [ctypes.c_void_p, ctypes.c_int, frameindex_t, ctypes.c_uint], + argnames = ["Fg", "Param", "Data", "DmaIndex"] ) + # frameindex_t Fg_getStatusEx(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + addfunc(lib, "Fg_getStatusEx", restype = frameindex_t, + argtypes = [ctypes.c_void_p, ctypes.c_int, frameindex_t, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "Param", "Data", "DmaIndex", "pMem"] ) + # ctypes.c_int Fg_setStatus(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex) + addfunc(lib, "Fg_setStatus", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, frameindex_t, ctypes.c_uint], + argnames = ["Fg", "Param", "Data", "DmaIndex"] ) + # ctypes.c_int Fg_setStatusEx(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + addfunc(lib, "Fg_setStatusEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, frameindex_t, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "Param", "Data", "DmaIndex", "pMem"] ) + # ctypes.c_int Fg_getAppletIterator(ctypes.c_int boardIndex, ctypes.c_int src, ctypes.POINTER(Fg_AppletIteratorType) iter, ctypes.c_int flags) + addfunc(lib, "Fg_getAppletIterator", restype = ctypes.c_int, + argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(Fg_AppletIteratorType), ctypes.c_int], + argnames = ["boardIndex", "src", "iter", "flags"] ) + # ctypes.c_int Fg_freeAppletIterator(Fg_AppletIteratorType iter) + addfunc(lib, "Fg_freeAppletIterator", restype = ctypes.c_int, + argtypes = [Fg_AppletIteratorType], + argnames = ["iter"] ) + # Fg_AppletIteratorItem Fg_getAppletIteratorItem(Fg_AppletIteratorType iter, ctypes.c_int index) + addfunc(lib, "Fg_getAppletIteratorItem", restype = Fg_AppletIteratorItem, + argtypes = [Fg_AppletIteratorType, ctypes.c_int], + argnames = ["iter", "index"] ) + # Fg_AppletIteratorItem Fg_findAppletIteratorItem(Fg_AppletIteratorType iter, ctypes.c_char_p path) + addfunc(lib, "Fg_findAppletIteratorItem", restype = Fg_AppletIteratorItem, + argtypes = [Fg_AppletIteratorType, ctypes.c_char_p], + argnames = ["iter", "path"] ) + # Fg_AppletIteratorItem Fg_addAppletIteratorItem(Fg_AppletIteratorType iter, ctypes.c_char_p path, ctypes.POINTER(ctypes.c_int) numItems) + addfunc(lib, "Fg_addAppletIteratorItem", restype = Fg_AppletIteratorItem, + argtypes = [Fg_AppletIteratorType, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)], + argnames = ["iter", "path", "numItems"] ) + # ctypes.c_int64 Fg_getAppletIntProperty(Fg_AppletIteratorItem item, ctypes.c_int property) + addfunc(lib, "Fg_getAppletIntProperty", restype = ctypes.c_int64, + argtypes = [Fg_AppletIteratorItem, ctypes.c_int], + argnames = ["item", "property"] ) + # ctypes.c_char_p Fg_getAppletStringProperty(Fg_AppletIteratorItem item, ctypes.c_int property) + addfunc(lib, "Fg_getAppletStringProperty", restype = ctypes.c_char_p, + argtypes = [Fg_AppletIteratorItem, ctypes.c_int], + argnames = ["item", "property"] ) + # ctypes.c_uint64 Fg_getEventMask(ctypes.c_void_p Fg, ctypes.c_char_p name) + addfunc(lib, "Fg_getEventMask", restype = ctypes.c_uint64, + argtypes = [ctypes.c_void_p, ctypes.c_char_p], + argnames = ["Fg", "name"] ) + # ctypes.c_int Fg_getEventPayload(ctypes.c_void_p Fg, ctypes.c_uint64 mask) + addfunc(lib, "Fg_getEventPayload", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint64], + argnames = ["Fg", "mask"] ) + # ctypes.c_char_p Fg_getEventName(ctypes.c_void_p Fg, ctypes.c_uint64 mask) + addfunc(lib, "Fg_getEventName", restype = ctypes.c_char_p, + argtypes = [ctypes.c_void_p, ctypes.c_uint64], + argnames = ["Fg", "mask"] ) + # ctypes.c_int Fg_getEventCount(ctypes.c_void_p Fg) + addfunc(lib, "Fg_getEventCount", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p], + argnames = ["Fg"] ) + # ctypes.c_int Fg_activateEvents(ctypes.c_void_p Fg, ctypes.c_uint64 mask, ctypes.c_uint enable) + addfunc(lib, "Fg_activateEvents", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint], + argnames = ["Fg", "mask", "enable"] ) + # ctypes.c_int Fg_clearEvents(ctypes.c_void_p Fg, ctypes.c_uint64 mask) + addfunc(lib, "Fg_clearEvents", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint64], + argnames = ["Fg", "mask"] ) + # ctypes.c_uint64 Fg_eventWait(ctypes.c_void_p Fg, ctypes.c_uint64 mask, ctypes.c_uint timeout, ctypes.c_uint flags, ctypes.c_void_p info) + addfunc(lib, "Fg_eventWait", restype = ctypes.c_uint64, + argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "mask", "timeout", "flags", "info"] ) + # ctypes.c_int Fg_registerEventCallback(ctypes.c_void_p Fg, ctypes.c_uint64 mask, Fg_EventFunc_t handler, ctypes.c_void_p data, ctypes.c_uint flags, ctypes.c_void_p info) + addfunc(lib, "Fg_registerEventCallback", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint64, Fg_EventFunc_t, ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p], + argnames = ["Fg", "mask", "handler", "data", "flags", "info"] ) + # ctypes.c_int Fg_registerAsyncNotifyCallback(ctypes.c_void_p Fg, Fg_AsyncNotifyFunc_t handler, ctypes.c_void_p context) + addfunc(lib, "Fg_registerAsyncNotifyCallback", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, Fg_AsyncNotifyFunc_t, ctypes.c_void_p], + argnames = ["Fg", "handler", "context"] ) + # ctypes.c_int Fg_unregisterAsyncNotifyCallback(ctypes.c_void_p Fg, Fg_AsyncNotifyFunc_t handler, ctypes.c_void_p context) + addfunc(lib, "Fg_unregisterAsyncNotifyCallback", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, Fg_AsyncNotifyFunc_t, ctypes.c_void_p], + argnames = ["Fg", "handler", "context"] ) + # ctypes.c_int Fg_resetAsyncNotify(ctypes.c_void_p Fg, ctypes.c_ulong notification, ctypes.c_ulong pl, ctypes.c_ulong ph) + addfunc(lib, "Fg_resetAsyncNotify", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong], + argnames = ["Fg", "notification", "pl", "ph"] ) + # ctypes.c_int Fg_setExsync(ctypes.c_void_p Fg, ctypes.c_int Flag, ctypes.c_uint CamPort) + addfunc(lib, "Fg_setExsync", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint], + argnames = ["Fg", "Flag", "CamPort"] ) + # ctypes.c_int Fg_setFlash(ctypes.c_void_p Fg, ctypes.c_int Flag, ctypes.c_uint CamPort) + addfunc(lib, "Fg_setFlash", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint], + argnames = ["Fg", "Flag", "CamPort"] ) + # ctypes.c_int Fg_sendSoftwareTrigger(ctypes.c_void_p Fg, ctypes.c_uint CamPort) + addfunc(lib, "Fg_sendSoftwareTrigger", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint], + argnames = ["Fg", "CamPort"] ) + # ctypes.c_int Fg_sendSoftwareTriggerEx(ctypes.c_void_p Fg, ctypes.c_uint CamPort, ctypes.c_uint Triggers) + addfunc(lib, "Fg_sendSoftwareTriggerEx", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint], + argnames = ["Fg", "CamPort", "Triggers"] ) + # ctypes.c_void_p Fg_AllocShading(ctypes.c_void_p Fg, ctypes.c_int set, ctypes.c_uint CamPort) + addfunc(lib, "Fg_AllocShading", restype = ctypes.c_void_p, + argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint], + argnames = ["Fg", "set", "CamPort"] ) + # ctypes.c_int Fg_FreeShading(ctypes.c_void_p Fg, ctypes.c_void_p sh) + addfunc(lib, "Fg_FreeShading", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "sh"] ) + # ctypes.c_int Shad_GetAccess(ctypes.c_void_p Fg, ctypes.c_void_p sh) + addfunc(lib, "Shad_GetAccess", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "sh"] ) + # ctypes.c_int Shad_FreeAccess(ctypes.c_void_p Fg, ctypes.c_void_p sh) + addfunc(lib, "Shad_FreeAccess", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "sh"] ) + # ctypes.c_int Shad_GetMaxLine(ctypes.c_void_p Fg, ctypes.c_void_p sh) + addfunc(lib, "Shad_GetMaxLine", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p], + argnames = ["Fg", "sh"] ) + # ctypes.c_int Shad_SetSubValueLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int x, ctypes.c_int channel, ctypes.c_float sub) + addfunc(lib, "Shad_SetSubValueLine", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_float], + argnames = ["Fg", "sh", "x", "channel", "sub"] ) + # ctypes.c_int Shad_SetMultValueLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int x, ctypes.c_int channel, ctypes.c_float mult) + addfunc(lib, "Shad_SetMultValueLine", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_float], + argnames = ["Fg", "sh", "x", "channel", "mult"] ) + # ctypes.c_int Shad_SetFixedPatternNoiseLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int x, ctypes.c_int channel, ctypes.c_int on) + addfunc(lib, "Shad_SetFixedPatternNoiseLine", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_int], + argnames = ["Fg", "sh", "x", "channel", "on"] ) + # ctypes.c_int Shad_WriteActLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int Line) + addfunc(lib, "Shad_WriteActLine", restype = ctypes.c_int, + argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int], + argnames = ["Fg", "sh", "Line"] ) + # ctypes.c_int getLastErrorNumber() + addfunc(lib, "getLastErrorNumber", restype = ctypes.c_int, + argtypes = [], + argnames = [] ) + + diff --git a/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py b/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py new file mode 100644 index 0000000..befeb6a --- /dev/null +++ b/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py @@ -0,0 +1,384 @@ +# pylint: disable=wrong-spelling-in-comment + +from .fgrab_define_defs import FG_STATUS, drFG_STATUS, FG_PARAM +from .fgrab_prototyp_defs import FgParamTypes, FgProperty +from .fgrab_prototyp_defs import define_functions + +from ...core.utils import ctypes_wrap, functions as func_utils +from ...core.devio.comm_backend import DeviceError +from ..utils import load_lib + +import ctypes + + +class SiliconSoftwareError(DeviceError): + """Generic Silicon Software error""" +class SIFgrabLibError(SiliconSoftwareError): + """Generic SiliconSoftware frame grabber library error""" + def __init__(self, func, code, desc=""): + self.func=func + self.code=code + self.name=drFG_STATUS.get(self.code,"UNKNOWN") + self.desc=desc + self.msg="function '{}' raised error {}({}): {}".format(func,code,self.name,self.desc) + super().__init__(self.msg) +def last_error_desc(lib, s=None): + code=lib.Fg_getLastErrorNumber(s) + desc=lib.Fg_getLastErrorDescription(s) + return code,desc +def errcheck(lib, check=None, sarg=0): + """ + Build an error checking function. + + If `check` is not ``True``, it can be an additional checking method taking the result and the arguments; + otherwise, any result always passes. + `sarg` is the position of the frame grabber struct argument, which is used to get the error code and description. + """ + def checker(result, func, arguments): + s=None if sarg is None else arguments[sarg] + if check is not None: + valid=func_utils.call_cut_args(check,result,*arguments) + else: + valid=True + if not valid: + code,desc=last_error_desc(lib,s) + raise SIFgrabLibError(func.__name__,code=code,desc=desc) + return result + return checker + + + +# frameindex_t=ctypes.c_int32 if platform.architecture()[0]=="32bit" else ctypes.c_int64 # TODO: implement in defa + + + +class SIFgrabLib: + + def __init__(self): + self._initialized=False + + + def initlib(self): + if self._initialized: + return + error_message="The library is automatically supplied with Silicon Software Runtime software\n"+load_lib.par_error_message.format("sifgrab") + self.lib=load_lib.load_lib("fglib5.dll",locations=("parameter/sifgrab","global"),error_message=error_message,call_conv="cdecl") + lib=self.lib + define_functions(lib) + + max_applet_name_length=1024 + apstrprep=ctypes_wrap.strprep(max_applet_name_length) + wrapper=ctypes_wrap.CFunctionWrapper() + wrapper_rz=ctypes_wrap.CFunctionWrapper(errcheck=errcheck(lib,check=lambda v:v==0)) + wrapper_rnz=ctypes_wrap.CFunctionWrapper(errcheck=errcheck(lib,check=lambda v:bool(v))) + wrapper_rgez=ctypes_wrap.CFunctionWrapper(errcheck=errcheck(lib,check=lambda v:v>=0)) + wrapper_init_rnz=ctypes_wrap.CFunctionWrapper(errcheck=errcheck(lib,check=lambda v:bool(v),sarg=None)) + wrapper_init_rgez=ctypes_wrap.CFunctionWrapper(errcheck=errcheck(lib,check=lambda v:v>=0,sarg=None)) + + # ctypes.c_int Fg_getLastErrorNumber(ctypes.c_void_p Fg) + self.Fg_getLastErrorNumber=wrapper(lib.Fg_getLastErrorNumber) + # ctypes.c_char_p getErrorDescription(ctypes.c_int ErrorNumber) + self.getErrorDescription=wrapper(lib.getErrorDescription) + # ctypes.c_char_p Fg_getErrorDescription(ctypes.c_void_p Fg, ctypes.c_int ErrorNumber) + self.Fg_getErrorDescription=wrapper(lib.Fg_getErrorDescription) + # ctypes.c_char_p Fg_getLastErrorDescription(ctypes.c_void_p Fg) + self.Fg_getLastErrorDescription=wrapper(lib.Fg_getLastErrorDescription) + + # Optional methods # + # ctypes.c_int Fg_InitLibraries(ctypes.c_char_p sisoDir) + self.Fg_InitLibraries=wrapper_rnz(lib.Fg_InitLibraries) + # ctypes.c_int Fg_InitLibrariesEx(ctypes.c_char_p sisoDir, ctypes.c_uint flags, ctypes.c_char_p id, ctypes.c_uint timeout) + self.Fg_InitLibrariesEx=wrapper_rnz(lib.Fg_InitLibrariesEx) + # None Fg_AbortInitLibraries() + self.Fg_AbortInitLibraries=wrapper(lib.Fg_AbortInitLibraries) + # None Fg_InitLibrariesStartNextSlave() + self.Fg_InitLibrariesStartNextSlave=wrapper(lib.Fg_InitLibrariesStartNextSlave) + # None Fg_FreeLibraries() + self.Fg_FreeLibraries=wrapper(lib.Fg_FreeLibraries) + + # ctypes.c_int Fg_getAppletIterator(ctypes.c_int boardIndex, ctypes.c_int src, ctypes.POINTER(Fg_AppletIteratorType) iter, ctypes.c_int flags) + self.Fg_getAppletIterator=wrapper_rgez(lib.Fg_getAppletIterator, rvals=[None,"iter"]) + # ctypes.c_int Fg_freeAppletIterator(Fg_AppletIteratorType iter) + self.Fg_freeAppletIterator=wrapper_rz(lib.Fg_freeAppletIterator) + # Fg_AppletIteratorItem Fg_getAppletIteratorItem(Fg_AppletIteratorType iter, ctypes.c_int index) + self.Fg_getAppletIteratorItem=wrapper_rnz(lib.Fg_getAppletIteratorItem) + # ctypes.c_int64 Fg_getAppletIntProperty(Fg_AppletIteratorItem item, ctypes.c_int property) + self.Fg_getAppletIntProperty=wrapper(lib.Fg_getAppletIntProperty) + # ctypes.c_char_p Fg_getAppletStringProperty(Fg_AppletIteratorItem item, ctypes.c_int property) + self.Fg_getAppletStringProperty=wrapper(lib.Fg_getAppletStringProperty) + # Fg_AppletIteratorItem Fg_findAppletIteratorItem(Fg_AppletIteratorType iter, ctypes.c_char_p path) + self.Fg_findAppletIteratorItem=wrapper_rnz(lib.Fg_findAppletIteratorItem) + # Fg_AppletIteratorItem Fg_addAppletIteratorItem(Fg_AppletIteratorType iter, ctypes.c_char_p path, ctypes.POINTER(ctypes.c_int) numItems) + self.Fg_addAppletIteratorItem=wrapper_rnz(lib.Fg_addAppletIteratorItem) + # ctypes.c_int Fg_findApplet(ctypes.c_uint BoardIndex, ctypes.c_char_p Path, ctypes.c_size_t Size) + self.Fg_findApplet=wrapper(lib.Fg_findApplet, argprep={"Path":apstrprep,"Size":max_applet_name_length}, byref=None) + + # ctypes.c_char_p Fg_getSWVersion() + self.Fg_getSWVersion=wrapper(lib.Fg_getSWVersion) + # ctypes.c_int Fg_getBoardType(ctypes.c_int BoardIndex) + self.Fg_getBoardType=wrapper_init_rgez(lib.Fg_getBoardType) + # ctypes.c_uint Fg_getSerialNumber(ctypes.c_void_p Fg) + self.Fg_getSerialNumber=wrapper(lib.Fg_getSerialNumber) + # ctypes.c_int Fg_getAppletId(ctypes.c_void_p Fg, ctypes.c_char_p ignored) + self.Fg_getAppletId=wrapper(lib.Fg_getAppletId, args=["Fg"], rvals=[None]) + # ctypes.c_char_p Fg_getAppletVersion(ctypes.c_void_p Fg, ctypes.c_int AppletId) + self.Fg_getAppletVersion=wrapper(lib.Fg_getAppletVersion) + # ctypes.c_char_p Fg_getBoardNameByType(ctypes.c_int BoardType, ctypes.c_int UseShortName) + self.Fg_getBoardNameByType=wrapper_init_rnz(lib.Fg_getBoardNameByType) + # ctypes.c_int Fg_getSystemInformation(ctypes.c_void_p Fg, ctypes.c_int selector, ctypes.c_int propertyId, ctypes.c_int param1, ctypes.c_void_p buffer, ctypes.POINTER(ctypes.c_uint) bufLen) + self.Fg_getSystemInformation_lib=wrapper_rz(lib.Fg_getSystemInformation, args="all", rvals=["bufLen"]) + + + # ctypes.c_void_p Fg_Init(ctypes.c_char_p FileName, ctypes.c_uint BoardIndex) + self.Fg_Init=wrapper_init_rnz(lib.Fg_Init) + # ctypes.c_void_p Fg_InitConfig(ctypes.c_char_p Config_Name, ctypes.c_uint BoardIndex) + self.Fg_InitEx=wrapper_init_rnz(lib.Fg_InitEx) + # ctypes.c_void_p Fg_InitConfigEx(ctypes.c_char_p Config_Name, ctypes.c_uint BoardIndex, ctypes.c_int flags) + self.Fg_InitConfig=wrapper_init_rnz(lib.Fg_InitConfig) + # ctypes.c_void_p Fg_InitEx(ctypes.c_char_p FileName, ctypes.c_uint BoardIndex, ctypes.c_int flags) + self.Fg_InitConfigEx=wrapper_init_rnz(lib.Fg_InitConfigEx) + # ctypes.c_int Fg_FreeGrabber(ctypes.c_void_p Fg) + self.Fg_FreeGrabber=wrapper_rz(lib.Fg_FreeGrabber) + # ctypes.c_int Fg_loadConfig(ctypes.c_void_p Fg, ctypes.c_char_p Filename) + self.Fg_loadConfig=wrapper_rz(lib.Fg_loadConfig) + # ctypes.c_int Fg_saveConfig(ctypes.c_void_p Fg, ctypes.c_char_p Filename) + self.Fg_saveConfig=wrapper_rz(lib.Fg_saveConfig) + + # ctypes.c_int Fg_getNrOfParameter(ctypes.c_void_p Fg) + self.Fg_getNrOfParameter=wrapper_rgez(lib.Fg_getNrOfParameter) + # ctypes.c_int Fg_getParameterId(ctypes.c_void_p fg, ctypes.c_int index) + self.Fg_getParameterId=wrapper_rgez(lib.Fg_getParameterId) + # ctypes.c_char_p Fg_getParameterName(ctypes.c_void_p fg, ctypes.c_int index) + self.Fg_getParameterName=wrapper_rnz(lib.Fg_getParameterName) + # ctypes.c_int Fg_getParameterIdByName(ctypes.c_void_p fg, ctypes.c_char_p name) + self.Fg_getParameterIdByName=wrapper_rgez(lib.Fg_getParameterIdByName) + # ctypes.c_char_p Fg_getParameterNameById(ctypes.c_void_p fg, ctypes.c_uint id, ctypes.c_uint dma) + self.Fg_getParameterNameById=wrapper_rnz(lib.Fg_getParameterNameById) + + # ctypes.c_int Fg_setParameter(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex) + self.Fg_setParameter=wrapper_rz(lib.Fg_setParameter, byref=["Value"]) + # ctypes.c_int Fg_setParameterWithType(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_int type) + self.Fg_setParameterWithType=wrapper_rz(lib.Fg_setParameterWithType, byref=["Value"]) + # ctypes.c_int Fg_getParameter(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex) + self.Fg_getParameter=wrapper_rz(lib.Fg_getParameter) + # ctypes.c_int Fg_getParameterWithType(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_int type) + self.Fg_getParameterWithType=wrapper_rz(lib.Fg_getParameterWithType) + # ctypes.c_int Fg_freeParameterStringWithType(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_int type) + self.Fg_freeParameterStringWithType=wrapper(lib.Fg_freeParameterStringWithType) + # ctypes.c_int Fg_getParameterEx(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_void_p Value, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem, frameindex_t ImgNr) + self.Fg_getParameterEx=wrapper_rz(lib.Fg_getParameterEx) + # ctypes.c_int Fg_getParameterProperty(ctypes.c_void_p Fg, ctypes.c_int parameterId, ctypes.c_int propertyId, ctypes.c_void_p buffer, ctypes.POINTER(ctypes.c_int) bufLen) + self.Fg_getParameterProperty_lib=wrapper_rz(lib.Fg_getParameterProperty, args="all", rvals=["bufLen"]) + # ctypes.c_int Fg_getParameterPropertyEx(ctypes.c_void_p Fg, ctypes.c_int parameterId, ctypes.c_int propertyId, ctypes.c_int DmaIndex, ctypes.c_void_p buffer, ctypes.POINTER(ctypes.c_int) bufLen) + self.Fg_getParameterPropertyEx_lib=wrapper_rz(lib.Fg_getParameterPropertyEx, args="all", rvals=["bufLen"]) + # ctypes.c_int Fg_getParameterInfoXML(ctypes.c_void_p Fg, ctypes.c_int port, ctypes.c_char_p infoBuffer, ctypes.POINTER(ctypes.c_size_t) infoBufferSize) + self.Fg_getParameterInfoXML_lib=wrapper_rz(lib.Fg_getParameterInfoXML, args="all", rvals=["infoBufferSize"]) + # ctypes.c_int Fg_getBitsPerPixel(ctypes.c_int format) + self.Fg_getBitsPerPixel=wrapper_rgez(lib.Fg_getBitsPerPixel) + + # frameindex_t Fg_getStatus(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex) + self.Fg_getStatus=wrapper_rgez(lib.Fg_getStatus) + # frameindex_t Fg_getStatusEx(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + self.Fg_getStatusEx=wrapper_rgez(lib.Fg_getStatusEx) + # ctypes.c_int Fg_setStatus(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex) + self.Fg_setStatus=wrapper_rgez(lib.Fg_setStatus) + # ctypes.c_int Fg_setStatusEx(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t Data, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + self.Fg_setStatusEx=wrapper_rgez(lib.Fg_setStatusEx) + + # ctypes.c_void_p Fg_AllocMem(ctypes.c_void_p Fg, ctypes.c_size_t Size, frameindex_t BufCnt, ctypes.c_uint DmaIndex) + self.Fg_AllocMem=wrapper_rnz(lib.Fg_AllocMem) + # ctypes.c_void_p Fg_AllocMemEx(ctypes.c_void_p Fg, ctypes.c_size_t Size, frameindex_t BufCnt) + self.Fg_AllocMemEx=wrapper_rnz(lib.Fg_AllocMemEx) + # ctypes.c_int Fg_FreeMem(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex) + self.Fg_FreeMem=wrapper_rz(lib.Fg_FreeMem) + # ctypes.c_int Fg_FreeMemEx(ctypes.c_void_p Fg, ctypes.c_void_p mem) + self.Fg_FreeMemEx=wrapper_rz(lib.Fg_FreeMemEx) + # ctypes.c_void_p Fg_AllocMemHead(ctypes.c_void_p Fg, ctypes.c_size_t Size, frameindex_t BufCnt) + self.Fg_AllocMemHead=wrapper_rnz(lib.Fg_AllocMemHead) + # ctypes.c_int Fg_FreeMemHead(ctypes.c_void_p Fg, ctypes.c_void_p memHandle) + self.Fg_FreeMemHead=wrapper_rz(lib.Fg_FreeMemHead) + # ctypes.c_int Fg_AddMem(ctypes.c_void_p Fg, ctypes.c_void_p pBuffer, ctypes.c_size_t Size, frameindex_t bufferIndex, ctypes.c_void_p memHandle) + self.Fg_AddMem=wrapper_rgez(lib.Fg_AddMem) + # ctypes.c_int Fg_DelMem(ctypes.c_void_p Fg, ctypes.c_void_p memHandle, frameindex_t bufferIndex) + self.Fg_DelMem=wrapper_rz(lib.Fg_DelMem) + + # ctypes.c_int Fg_Acquire(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, frameindex_t PicCount) + self.Fg_Acquire=wrapper_rz(lib.Fg_Acquire) + # ctypes.c_int Fg_AcquireEx(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, frameindex_t PicCount, ctypes.c_int nFlag, ctypes.c_void_p memHandle) + self.Fg_AcquireEx=wrapper_rz(lib.Fg_AcquireEx) + # ctypes.c_int Fg_stopAcquire(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex) + self.Fg_stopAcquire=wrapper_rz(lib.Fg_stopAcquire) + # ctypes.c_int Fg_stopAcquireEx(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, ctypes.c_void_p memHandle, ctypes.c_int nFlag) + self.Fg_stopAcquireEx=wrapper_rz(lib.Fg_stopAcquireEx) + # frameindex_t Fg_getLastPicNumberBlocking(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_int Timeout) + self.Fg_getLastPicNumberBlocking=wrapper_rgez(lib.Fg_getLastPicNumberBlocking) + # frameindex_t Fg_getLastPicNumber(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex) + self.Fg_getLastPicNumber=wrapper_rgez(lib.Fg_getLastPicNumber) + # frameindex_t Fg_getLastPicNumberBlockingEx(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_int Timeout, ctypes.c_void_p pMem) + self.Fg_getLastPicNumberBlockingEx=wrapper_rgez(lib.Fg_getLastPicNumberBlockingEx) + # frameindex_t Fg_getLastPicNumberEx(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + self.Fg_getLastPicNumberEx=wrapper_rgez(lib.Fg_getLastPicNumberEx) + # ctypes.c_void_p Fg_getImagePtr(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex) + self.Fg_getImagePtr=wrapper_rnz(lib.Fg_getImagePtr) + # ctypes.c_void_p Fg_getImagePtrEx(ctypes.c_void_p Fg, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_void_p pMem) + self.Fg_getImagePtrEx=wrapper_rnz(lib.Fg_getImagePtrEx) + # frameindex_t Fg_getImage(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_uint Timeout) + self.Fg_getImage=wrapper_rgez(lib.Fg_getImage) + # frameindex_t Fg_getImageEx(ctypes.c_void_p Fg, ctypes.c_int Param, frameindex_t PicNr, ctypes.c_uint DmaIndex, ctypes.c_uint Timeout, ctypes.c_void_p pMem) + self.Fg_getImageEx=wrapper_rgez(lib.Fg_getImageEx) + + # ctypes.c_int Fg_sendSoftwareTrigger(ctypes.c_void_p Fg, ctypes.c_uint CamPort) + self.Fg_sendSoftwareTrigger=wrapper_rz(lib.Fg_sendSoftwareTrigger) + # ctypes.c_int Fg_sendSoftwareTriggerEx(ctypes.c_void_p Fg, ctypes.c_uint CamPort, ctypes.c_uint Triggers) + self.Fg_sendSoftwareTriggerEx=wrapper_rz(lib.Fg_sendSoftwareTriggerEx) + # ctypes.c_int Fg_setExsync(ctypes.c_void_p Fg, ctypes.c_int Flag, ctypes.c_uint CamPort) + self.Fg_setExsync=wrapper_rgez(lib.Fg_setExsync) + # ctypes.c_int Fg_setFlash(ctypes.c_void_p Fg, ctypes.c_int Flag, ctypes.c_uint CamPort) + self.Fg_setFlash=wrapper_rgez(lib.Fg_setFlash) + + # ctypes.c_void_p Fg_NumaAllocDmaBuffer(ctypes.c_void_p Fg, ctypes.c_size_t Size) + self.Fg_NumaAllocDmaBuffer=wrapper_rnz(lib.Fg_NumaAllocDmaBuffer) + # ctypes.c_int Fg_NumaFreeDmaBuffer(ctypes.c_void_p Fg, ctypes.c_void_p Buffer) + self.Fg_NumaFreeDmaBuffer=wrapper_rz(lib.Fg_NumaFreeDmaBuffer) + # ctypes.c_int Fg_NumaPinThread(ctypes.c_void_p Fg) + self.Fg_NumaPinThread=wrapper_rz(lib.Fg_NumaPinThread) + + # ctypes.c_int Fg_readUserDataArea(ctypes.c_void_p Fg, ctypes.c_int boardId, ctypes.c_uint offs, ctypes.c_uint size, ctypes.c_void_p buffer) + self.Fg_readUserDataArea=wrapper_rz(lib.Fg_readUserDataArea) + # ctypes.c_int Fg_writeUserDataArea(ctypes.c_void_p Fg, ctypes.c_int boardId, ctypes.c_uint offs, ctypes.c_uint size, ctypes.c_void_p buffer) + self.Fg_writeUserDataArea=wrapper_rz(lib.Fg_writeUserDataArea) + + + self._initialized=True + + + ## Applet-related ## + # # ctypes.c_int Fg_sendImage(ctypes.c_void_p Fg, frameindex_t startImage, frameindex_t PicCount, ctypes.c_int nFlag, ctypes.c_uint DmaIndex) + # self.Fg_sendImage=wrapper(lib.Fg_sendImage) + # # ctypes.c_int Fg_sendImageEx(ctypes.c_void_p Fg, frameindex_t startImage, frameindex_t PicCount, ctypes.c_int nFlag, ctypes.c_uint DmaIndex, ctypes.c_void_p memHandle) + # self.Fg_sendImageEx=wrapper(lib.Fg_sendImageEx) + # # ctypes.c_int Fg_saveFieldParameterToFile(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_uint DmaIndex, ctypes.c_char_p FileName) + # self.Fg_saveFieldParameterToFile=wrapper(lib.Fg_saveFieldParameterToFile) + # # ctypes.c_int Fg_loadFieldParameterFromFile(ctypes.c_void_p Fg, ctypes.c_int Parameter, ctypes.c_uint DmaIndex, ctypes.c_char_p FileName) + # self.Fg_loadFieldParameterFromFile=wrapper(lib.Fg_loadFieldParameterFromFile) + + ## Events and callbacks ## + # # ctypes.c_uint64 Fg_getEventMask(ctypes.c_void_p Fg, ctypes.c_char_p name) + # self.Fg_getEventMask=wrapper(lib.Fg_getEventMask) + # # ctypes.c_int Fg_getEventPayload(ctypes.c_void_p Fg, ctypes.c_uint64 mask) + # self.Fg_getEventPayload=wrapper(lib.Fg_getEventPayload) + # # ctypes.c_char_p Fg_getEventName(ctypes.c_void_p Fg, ctypes.c_uint64 mask) + # self.Fg_getEventName=wrapper(lib.Fg_getEventName) + # # ctypes.c_int Fg_getEventCount(ctypes.c_void_p Fg) + # self.Fg_getEventCount=wrapper(lib.Fg_getEventCount) + # # ctypes.c_int Fg_activateEvents(ctypes.c_void_p Fg, ctypes.c_uint64 mask, ctypes.c_uint enable) + # self.Fg_activateEvents=wrapper(lib.Fg_activateEvents) + # # ctypes.c_int Fg_clearEvents(ctypes.c_void_p Fg, ctypes.c_uint64 mask) + # self.Fg_clearEvents=wrapper(lib.Fg_clearEvents) + # # ctypes.c_uint64 Fg_eventWait(ctypes.c_void_p Fg, ctypes.c_uint64 mask, ctypes.c_uint timeout, ctypes.c_uint flags, ctypes.c_void_p info) + # self.Fg_eventWait=wrapper(lib.Fg_eventWait) + # # ctypes.c_int Fg_registerEventCallback(ctypes.c_void_p Fg, ctypes.c_uint64 mask, Fg_EventFunc_t handler, ctypes.c_void_p data, ctypes.c_uint flags, ctypes.c_void_p info) + # self.Fg_registerEventCallback=wrapper(lib.Fg_registerEventCallback) + # # ctypes.c_int Fg_registerAsyncNotifyCallback(ctypes.c_void_p Fg, Fg_AsyncNotifyFunc_t handler, ctypes.c_void_p context) + # self.Fg_registerAsyncNotifyCallback=wrapper(lib.Fg_registerAsyncNotifyCallback) + # # ctypes.c_int Fg_unregisterAsyncNotifyCallback(ctypes.c_void_p Fg, Fg_AsyncNotifyFunc_t handler, ctypes.c_void_p context) + # self.Fg_unregisterAsyncNotifyCallback=wrapper(lib.Fg_unregisterAsyncNotifyCallback) + # # ctypes.c_int Fg_resetAsyncNotify(ctypes.c_void_p Fg, ctypes.c_ulong notification, ctypes.c_ulong pl, ctypes.c_ulong ph) + # self.Fg_resetAsyncNotify=wrapper(lib.Fg_resetAsyncNotify) + # # ctypes.c_int Fg_registerApcHandler(ctypes.c_void_p Fg, ctypes.c_uint DmaIndex, ctypes.c_void_p control, ctypes.c_int flags) + # self.Fg_registerApcHandler=wrapper(lib.Fg_registerApcHandler) + + ## Shading ## + # # ctypes.c_void_p Fg_AllocShading(ctypes.c_void_p Fg, ctypes.c_int set, ctypes.c_uint CamPort) + # self.Fg_AllocShading=wrapper(lib.Fg_AllocShading) + # # ctypes.c_int Fg_FreeShading(ctypes.c_void_p Fg, ctypes.c_void_p sh) + # self.Fg_FreeShading=wrapper(lib.Fg_FreeShading) + # # ctypes.c_int Shad_GetAccess(ctypes.c_void_p Fg, ctypes.c_void_p sh) + # self.Shad_GetAccess=wrapper(lib.Shad_GetAccess) + # # ctypes.c_int Shad_FreeAccess(ctypes.c_void_p Fg, ctypes.c_void_p sh) + # self.Shad_FreeAccess=wrapper(lib.Shad_FreeAccess) + # # ctypes.c_int Shad_GetMaxLine(ctypes.c_void_p Fg, ctypes.c_void_p sh) + # self.Shad_GetMaxLine=wrapper(lib.Shad_GetMaxLine) + # # ctypes.c_int Shad_SetSubValueLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int x, ctypes.c_int channel, ctypes.c_float sub) + # self.Shad_SetSubValueLine=wrapper(lib.Shad_SetSubValueLine) + # # ctypes.c_int Shad_SetMultValueLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int x, ctypes.c_int channel, ctypes.c_float mult) + # self.Shad_SetMultValueLine=wrapper(lib.Shad_SetMultValueLine) + # # ctypes.c_int Shad_SetFixedPatternNoiseLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int x, ctypes.c_int channel, ctypes.c_int on) + # self.Shad_SetFixedPatternNoiseLine=wrapper(lib.Shad_SetFixedPatternNoiseLine) + # # ctypes.c_int Shad_WriteActLine(ctypes.c_void_p Fg, ctypes.c_void_p sh, ctypes.c_int Line) + # self.Shad_WriteActLine=wrapper(lib.Shad_WriteActLine) + + + + + + + def Fg_getParameterProperty(self, Fg, parameterId, propertyId): + l=self.Fg_getParameterProperty_lib(Fg,parameterId,propertyId,None,0) + buff=ctypes.create_string_buffer(l) + l=self.Fg_getParameterProperty_lib(Fg,parameterId,propertyId,ctypes.cast(buff,ctypes.c_void_p),l) + return buff[:l][:-1] + def Fg_getParameterPropertyEx(self, Fg, parameterId, propertyId, DmaIndex): + l=self.Fg_getParameterPropertyEx_lib(Fg,parameterId,propertyId,DmaIndex,None,0) + buff=ctypes.create_string_buffer(l) + l=self.Fg_getParameterPropertyEx_lib(Fg,parameterId,propertyId,DmaIndex,ctypes.cast(buff,ctypes.c_void_p),l) + return buff[:l][:-1] + def Fg_getSystemInformation(self, Fg, selector, propertyId, param1): + l=self.Fg_getSystemInformation_lib(Fg,selector,propertyId,param1,None,0) + buff=ctypes.create_string_buffer(l) + l=self.Fg_getSystemInformation_lib(Fg,selector,propertyId,param1,ctypes.cast(buff,ctypes.c_void_p),l) + return buff[:l][:-1] + def Fg_getParameterInfoXML(self, Fg, port): + l=self.Fg_getParameterInfoXML_lib(Fg,port,None,0) + buff=ctypes.create_string_buffer(l) + l=self.Fg_getParameterInfoXML_lib(Fg,port,ctypes.cast(buff,ctypes.c_char_p),l) + return buff[:l][:-1] + + _property_types={ FgParamTypes.FG_PARAM_TYPE_INT32_T: ctypes.c_int32, + FgParamTypes.FG_PARAM_TYPE_UINT32_T: ctypes.c_uint32, + FgParamTypes.FG_PARAM_TYPE_INT64_T: ctypes.c_int64, + FgParamTypes.FG_PARAM_TYPE_UINT64_T: ctypes.c_uint64, + FgParamTypes.FG_PARAM_TYPE_DOUBLE: ctypes.c_double, + FgParamTypes.FG_PARAM_TYPE_CHAR_PTR: ctypes.c_char_p, + # FgParamTypes.FG_PARAM_TYPE_CHAR_PTR_PTR: ctypes.POINTER(ctypes.c_char_p), + # FgParamTypes.FG_PARAM_TYPE_STRUCT_FIELDPARAMACCESS: FieldParameterAccess, + # FgParamTypes.FG_PARAM_TYPE_STRUCT_FIELDPARAMINT: FieldParameterInt, + # FgParamTypes.FG_PARAM_TYPE_STRUCT_FIELDPARAMINT64: FieldParameterAccess, + # FgParamTypes.FG_PARAM_TYPE_STRUCT_FIELDPARAMDOUBLE: FieldParameterDouble, + } + def Fg_getParameterWithType_auto(self, Fg, Parameter, DmaIndex, ptype=None): + if ptype is None: + ptype=int(self.Fg_getParameterPropertyEx(Fg,Parameter,FgProperty.PROP_ID_DATATYPE,DmaIndex)) + if ptype not in self._property_types: + raise SIFgrabLibError(self.Fg_getParameterPropertyEx,FG_STATUS.FG_ERROR,desc="can't deal with parameter type {}".format(ptype)) + if ptype==FgParamTypes.FG_PARAM_TYPE_CHAR_PTR: + l=int(self.Fg_getParameterPropertyEx(Fg,Parameter,FgProperty.PROP_ID_VALUELLEN,DmaIndex)) + v=ctypes.create_string_buffer(l) + pv=v + else: + v=self._property_types[ptype]() + pv=ctypes.byref(v) + self.Fg_getParameterWithType(Fg,Parameter,pv,DmaIndex,ptype) + return v.value + def Fg_setParameterWithType_auto(self, Fg, Parameter, Value, DmaIndex, ptype=None): + if ptype is None: + ptype=int(self.Fg_getParameterPropertyEx(Fg,Parameter,FgProperty.PROP_ID_DATATYPE,DmaIndex)) + if ptype not in self._property_types: + raise SIFgrabLibError(self.Fg_getParameterPropertyEx,FG_STATUS.FG_ERROR,desc="can't deal with parameter type {}".format(ptype)) + if ptype==FgParamTypes.FG_PARAM_TYPE_CHAR_PTR: + l=int(self.Fg_getParameterPropertyEx(Fg,Parameter,FgProperty.PROP_ID_VALUELLEN,DmaIndex)) + v=ctypes.create_string_buffer(l) + Value=Value[:l] + v[:len(Value)]=Value + else: + v=self._property_types[ptype](Value) + self.Fg_setParameterWithType(Fg,Parameter,v,DmaIndex,ptype) + + def Fg_getParameterEx_auto(self, Fg, Parameter, DmaIndex, pMem, ImgNr): + ptypes={FG_PARAM.FG_IMAGE_TAG:ctypes.c_uint, FG_PARAM.FG_TIMESTAMP:ctypes.c_uint, FG_PARAM.FG_TIMESTAMP_LONG:ctypes.c_uint64, FG_PARAM.FG_TRANSFER_LEN:ctypes.c_size_t} + v=ptypes[Parameter]() + self.Fg_getParameterEx(Fg,Parameter,v,DmaIndex,pMem,ImgNr) + return v.value + + +lib=SIFgrabLib() \ No newline at end of file diff --git a/pylablib/devices/interface/camera.py b/pylablib/devices/interface/camera.py index 8e78206..9fc9d57 100644 --- a/pylablib/devices/interface/camera.py +++ b/pylablib/devices/interface/camera.py @@ -8,6 +8,7 @@ import time import functools import threading +import ctypes @@ -31,7 +32,7 @@ class ICamera(interface.IDevice): Error=comm_backend.DeviceError TimeoutError=comm_backend.DeviceError FrameTransferError=DefaultFrameTransferError - def __init__(self): + def __init__(self, *args, **kwargs): super().__init__() self._acq_params=None self._default_acq_params=function_utils.funcsig(self.setup_acquisition).defaults @@ -649,6 +650,81 @@ def wait(self, idx=None, timeout=None): +class ChunkBufferManager: + """ + Buffer manager, which takes care of creating and removing the buffer chunks, and reading out some parts of them. + + Args: + chunk_size: the minimal size of a single buffer chunk (continuous memory segment potentially containing several frames). + """ + def __init__(self, chunk_size=2**20): + self.chunks=None + self.nframes=None + self.frame_size=None + self.frames_per_chunk=None + self.chunk_size=chunk_size + + def __bool__(self): + return self.chunks is not None + def get_ctypes_frames_list(self, ctype=ctypes.c_char_p): + """Get stored buffers as a ctypes array with pointer of the given type""" + if self.chunks: + cbuffs=(ctype*self.nframes)() + for i,b in enumerate(self.chunks): + for j in range(self.frames_per_chunk): + nb=i*self.frames_per_chunk+j + if nb0: + ch=self.chunks[ibuff] + chunk_frames=self.frames_per_chunk if ibuff Date: Fri, 25 Jun 2021 23:30:38 +0200 Subject: [PATCH 26/31] Docs and spelling bugfixes --- .pylintdict | 14 ++++++++++++++ docs/conf.py | 2 +- pylablib/core/thread/controller.py | 6 +++--- pylablib/core/utils/net.py | 2 +- pylablib/devices/uc480/uc480.py | 1 + pylablib/thread/stream/frameproc.py | 2 +- pylablib/thread/stream/stream_manager.py | 4 ++-- pylablib/thread/stream/stream_message.py | 2 +- 8 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.pylintdict b/.pylintdict index 828e531..1151d91 100644 --- a/.pylintdict +++ b/.pylintdict @@ -354,3 +354,17 @@ unpacker vbox hbox sublayouts +unscheduling +unschedule +callsync +genicam +grayscale +csd +csdi +ctl +sn +autofill +infos +paddings +fg +QQueueScheduler diff --git a/docs/conf.py b/docs/conf.py index ead7280..a4415e5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -85,7 +85,7 @@ 'rpyc': ('https://rpyc.readthedocs.io/en/latest/', None), 'pyqtgraph': ("https://pyqtgraph.readthedocs.io/en/latest/", None), 'pySerial': ("https://pythonhosted.org/pyserial/", None), - 'PyVISA': ("https://pyvisa.readthedocs.io/en/master/", None), + 'PyVISA': ("https://pyvisa.readthedocs.io/en/latest/", None), 'nidaqmx': ("https://nidaqmx-python.readthedocs.io/en/latest/", None),} diff --git a/pylablib/core/thread/controller.py b/pylablib/core/thread/controller.py index 8b058c5..5a7dc93 100644 --- a/pylablib/core/thread/controller.py +++ b/pylablib/core/thread/controller.py @@ -542,7 +542,7 @@ def no_stopping(self): """ Context manager, which temporarily suspends stop requests (:exc:`.InterruptExceptionStop` exceptions). - If the stop request has been made within this block, raise the excpetion on exit. + If the stop request has been made within this block, raise the exception on exit. Note that :meth:`stop` method and, correspondingly, :func:`stop_controller` still work, when called from the controlled thread. """ try: @@ -1160,7 +1160,7 @@ class Job: Args: job: job function period: job period - queue: thread controller's scheduling queue, to which the jub must be added + queue: thread controller's scheduling queue, to which the job must be added jobs_order: thread controller's job queue which determines the jobs scheduling order """ def __init__(self, job, period, queue, jobs_order): @@ -1429,7 +1429,7 @@ def _schedule_pending_jobs(self, t=None): Check if there are any pending jobs and schedule them. Return the time to wait until the next job needs to be scheduled. - Return time is 0 if a jos has been scheduled during that call, + Return time is 0 if a job has been scheduled during that call, and ``None`` if there are not jobs to schedule. """ wait_time=None diff --git a/pylablib/core/utils/net.py b/pylablib/core/utils/net.py index b997842..75df372 100644 --- a/pylablib/core/utils/net.py +++ b/pylablib/core/utils/net.py @@ -45,7 +45,7 @@ def as_addr_port(addr, port): Parse the given address and port combination. `addr` can be a host address, a tuple ``(addr, port)``, or a string ``"addr:port"``; - in the first case the given `port` is used, while in the other two it is ginore. + in the first case the given `port` is used, while in the other two it is ignore. Return tuple ``(addr, port)``. """ if isinstance(addr, tuple): diff --git a/pylablib/devices/uc480/uc480.py b/pylablib/devices/uc480/uc480.py index 0ba2878..bee8fa9 100644 --- a/pylablib/devices/uc480/uc480.py +++ b/pylablib/devices/uc480/uc480.py @@ -624,5 +624,6 @@ def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", retur describing frame index, framestamp, global timestamp (real time), device timestamp (time from camera restart, in 0.1us steps), frame size, digital input state, and additional flags; if some frames are missing and ``missing_frame!="skip"``, the corresponding frame info is ``None``. + Note that obtaining frame info might take about 2ms, so at high frame rates it will become a limiting factor. """ return super().read_multiple_images(rng=rng,peek=peek,missing_frame=missing_frame,return_info=return_info) \ No newline at end of file diff --git a/pylablib/thread/stream/frameproc.py b/pylablib/thread/stream/frameproc.py index 4097cd2..b9baeb6 100644 --- a/pylablib/thread/stream/frameproc.py +++ b/pylablib/thread/stream/frameproc.py @@ -453,7 +453,7 @@ def grab_snapshot_background(self): self.v["snapshot/background/state"]="acquiring" def setup_snapshot_saving(self, mode): """ - Enable shnapshot background subtraction and saving + Enable snapshot background subtraction and saving `mode` can be ``"none"`` (don't save background), ``"only_bg"`` (only save background frame), or ``"all"`` (save background + all comprising frames). """ diff --git a/pylablib/thread/stream/stream_manager.py b/pylablib/thread/stream/stream_manager.py index 4fed1fe..403f01a 100644 --- a/pylablib/thread/stream/stream_manager.py +++ b/pylablib/thread/stream/stream_manager.py @@ -206,7 +206,7 @@ def update_message(self, sn, mid): """ return self.cnts[sn].update_message(mid) def next_message(self, sn): - """Mark the next messagefor the given stream name""" + """Mark the next message for the given stream name""" return self.cnts[sn].next_message() def receive_message(self, msg, sn=None): """ @@ -539,7 +539,7 @@ def _get_acc_rng(self, i0, i1, peek, as_event): return evts if as_event else [evt.msg for evt in evts] def peek_message(self, n=0, as_event=None): """ - Peek at the `n`th message in the accumulated queue. + Peek at the message number `n` in the accumulated queue. If ``as_event==True``, return tuple ``(src, tag, msg)`` describing the received event; otherwise, just message is returned. diff --git a/pylablib/thread/stream/stream_message.py b/pylablib/thread/stream/stream_message.py index abaefca..d2761e2 100644 --- a/pylablib/thread/stream/stream_message.py +++ b/pylablib/thread/stream/stream_message.py @@ -19,7 +19,7 @@ class IStreamMessage: mid: message numerical ID, a unique ID (usually an incrementing number) of the message within the stream Either `mid` or both IDs can be ``None``, indicating that the corresponding stream does not keep track of these IDs. - In addition, `sid` and `mid` can be dictionaries (preferrably with the same keys), which indicates that this message + In addition, `sid` and `mid` can be dictionaries (preferably with the same keys), which indicates that this message inherits IDs from several streams with the given keys (e.g., it comes from a producer which join several streams together into one). """ _init_args=[] From 96db9e794ac0b77ab6d68e2d6424bd211602b7f8 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Fri, 25 Jun 2021 23:31:36 +0200 Subject: [PATCH 27/31] Tox update, minor bugfixes --- .gitignore | 2 ++ pylablib/core/fileio/parse_csv.py | 2 ++ pylablib/devices/IMAQ/IMAQ.py | 2 +- tox.ini | 46 +++++++++++++++++++++++-------- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 3dff1b6..2907e55 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,8 @@ nosetests.xml coverage.xml *.cover .hypothesis/ +.tox.* +run-tox-dev.py # Translations *.mo diff --git a/pylablib/core/fileio/parse_csv.py b/pylablib/core/fileio/parse_csv.py index 293d4c0..e7988a6 100644 --- a/pylablib/core/fileio/parse_csv.py +++ b/pylablib/core/fileio/parse_csv.py @@ -129,6 +129,8 @@ def _try_convert_column(column, dtype, min_dtype="int"): pass except OverflowError: # need to use the standard Python long integer type, which can't be stored in a numpy array break + except TypeError: # some numpy version can not convert string into complex + break column=[string.from_string(e) for e in column] if dtype=="numeric": for e in column: diff --git a/pylablib/devices/IMAQ/IMAQ.py b/pylablib/devices/IMAQ/IMAQ.py index 698e034..6d0c0ef 100644 --- a/pylablib/devices/IMAQ/IMAQ.py +++ b/pylablib/devices/IMAQ/IMAQ.py @@ -550,7 +550,7 @@ def _read_multiple_images_raw(self, rng=None, peek=False): self._frame_counter.advance_read_frames(rng) return rng[0],skipped_frames,raw_frames - def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", return_info=None, fastbuff=False): + def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", return_info=False, fastbuff=False): """ Read multiple images specified by `rng` (by default, all un-read images). diff --git a/tox.ini b/tox.ini index 5f05c88..d1a50fe 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = lpy{36,37,38,39}, lpy{36,37,38,39}_32, lpy{37,38}-serial_{2_7,3_0,3_1,3_2,3_3,3_4}, lpy{37,38}-ft232_{5,6,7,8,9,10,11}, lpy{37,38}-visa_{1_6,1_7,1_8,1_9,1_10}, lpy{37,38}-usb_{1_0_0,1_1_0} +envlist = lpy{36,37,38,39}, lpy{36,37,38,39}_32 [testenv] basepython = @@ -27,19 +27,41 @@ deps = serial_3_1: pyserial<3.2 serial_3_0: pyserial<3.1 serial_2_7: pyserial<3.0 - ft232_5: pyft232==0.5 - ft232_6: pyft232==0.6 - ft232_7: pyft232==0.7 - ft232_8: pyft232==0.8 - ft232_9: pyft232==0.9 - ft232_10: pyft232==0.10 ft232_11: pyft232==0.11 - visa_1_6: pyvisa<1.07 - visa_1_7: pyvisa<1.08 - visa_1_8: pyvisa<1.09 - visa_1_9: pyvisa<1.10 + ft232_10: pyft232==0.10 + ft232_9: pyft232==0.9 + ft232_8: pyft232==0.8 + ft232_7: pyft232==0.7 + ft232_6: pyft232==0.6 + ft232_5: pyft232==0.5 visa_1_10: pyvisa<1.11 - usb_1_0_0: pyusb==1.0.0 + visa_1_9: pyvisa<1.10 + visa_1_8: pyvisa<1.09 + visa_1_7: pyvisa<1.08 + visa_1_6: pyvisa<1.07 usb_1_1_0: pyusb==1.1.0 + usb_1_0_0: pyusb==1.0.0 + np_1_20: numpy==1.20.0 + np_1_19: numpy==1.19.0 + np_1_18: numpy==1.18.0 + np_1_17: numpy==1.17.0 + np_1_16: numpy==1.16.0 + np_1_15: numpy==1.15.0 + scp_1_7: scipy==1.7.0 + scp_1_6: scipy==1.6.0 + scp_1_5: scipy==1.5.0 + scp_1_4: scipy==1.4.0 + scp_1_3: scipy==1.3.0 + scp_1_2: scipy==1.2.0 + scp_1_1: scipy==1.1.0 + pd_1_2: pandas==1.2.0 + pd_1_1: pandas==1.1.0 + pd_1_0: pandas==1.0.0 + pd_0_25: pandas==0.25.0 + pd_0_24: pandas==0.24.0 + pd_0_23: pandas==0.23.0 + pd_0_22: pandas==0.22.0 + pd_0_21: pandas==0.21.0 + pd_0_20: pandas==0.20.0 commands = pytest {posargs} \ No newline at end of file From 63fd7d77fd92abefa1058db76a69b2d523543155 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Sat, 26 Jun 2021 20:27:07 +0200 Subject: [PATCH 28/31] Changed device testing layout --- tests/devices/conftest.py | 83 +++++++++++++++++------------ tests/devices/test_awg.py | 6 ++- tests/devices/test_basic.py | 17 ++++-- tests/devices/test_basic_camera.py | 27 ++++++---- tests/devices/test_cameras.py | 7 ++- tests/devices/test_misc.py | 15 +++--- tests/devices/test_oscilloscopes.py | 4 +- 7 files changed, 98 insertions(+), 61 deletions(-) diff --git a/tests/devices/conftest.py b/tests/devices/conftest.py index 99eabfa..2fde529 100644 --- a/tests/devices/conftest.py +++ b/tests/devices/conftest.py @@ -43,52 +43,67 @@ def pytest_collection_modifyitems(config, items): def library_parameters(): pass + +class DeviceOpener: + def __init__(self, devcls, devargs, open_retry, no_conn_fail=False, post_open=False): + self.devcls=devcls + self.devargs=devargs + self.open_retry=open_retry + self.no_conn_fail=no_conn_fail + self.post_open=post_open + self.device=None + self.failed=False + def _fail(self): + devargstr=", ".join([str(a) for a in self.devargs]) + pytest.xfail("couldn't connect to the device {}({})".format(self.devcls.__name__,devargstr)) + def open(self): + if self.device is not None: + return self.device + if self.failed: + self._fail() + try: + for i in range(self.open_retry+1): + try: + self.device=self.devcls(*self.devargs) + if self.post_open: + self.post_open(self.device) + return self.device + except Exception: + if i==self.open_retry: + raise + time.sleep(1.) + except Exception: + self.failed=True + if self.no_conn_fail: + self._fail() + else: + raise + def close(self): + if self.device is not None: + self.device.close() + self.device=None + def __call__(self): + return self.open() + + @pytest.fixture(scope="class") -def device(request, library_parameters): +def devopener(request, library_parameters): devcls=request.cls.devcls devargs=getattr(request.cls,"devargs",()) open_retry=getattr(request.cls,"open_retry",0) + post_open=getattr(request.cls,"post_open",None) devname=request.cls.devname devlist=get_device_list(request.config) if devname not in devlist: pytest.skip("skipping test of missing device {}".format(devname)) elif devlist[devname] is not None: devargs=devlist[devname] - open_rep=getattr(request.cls,"open_rep",0) - opened=False + opener=DeviceOpener(devcls,devargs,open_retry,no_conn_fail=request.config.getoption("dev_no_conn_fail"),post_open=post_open) try: - try: - for i in range(open_retry+1): - try: - dev=devcls(*devargs) - break - except Exception: - if i==open_retry: - raise - time.sleep(1.) - except Exception: - if request.config.getoption("dev_no_conn_fail"): - devargstr=", ".join([str(a) for a in devargs]) - pytest.xfail("couldn't connect to the device {}({})".format(devcls.__name__,devargstr)) - else: - raise - opened=True - for _ in range(open_rep): - dev.close() - opened=False - for i in range(open_retry+1): - try: - dev.open() - break - except Exception: - if i==open_retry: - raise - time.sleep(1.) - opened=True - yield dev + yield opener finally: - if opened: - dev.close() + opener.close() + @pytest.fixture diff --git a/tests/devices/test_awg.py b/tests/devices/test_awg.py index efbf031..95618c5 100644 --- a/tests/devices/test_awg.py +++ b/tests/devices/test_awg.py @@ -7,7 +7,8 @@ class GenericAWGTester(DeviceTester): """Testing a generic AWG""" @pytest.mark.devchange(5) - def test_output(self, device): + def test_output(self, devopener): + device=devopener() for ch in range(1,device.get_channels_number()+1): device.enable_output(True,channel=ch) assert device.is_output_enabled(channel=ch) @@ -16,7 +17,8 @@ def test_output(self, device): devfunctions=["sine"] @pytest.mark.devchange(5) - def test_functions(self, device): + def test_functions(self, devopener): + device=devopener() for ch in range(1,device.get_channels_number()+1): for f in self.devfunctions: device.set_function(f,channel=ch) diff --git a/tests/devices/test_basic.py b/tests/devices/test_basic.py index 2e8477c..7bb73a4 100644 --- a/tests/devices/test_basic.py +++ b/tests/devices/test_basic.py @@ -6,12 +6,15 @@ class DeviceTester: Takes device class and creation arguments and implements some basic tests. """ - open_rep=0 test_open_rep=2 include=-10 get_set_all_exclude=() - def test_open_close(self, device): + @classmethod + def post_open(cls, device): + pass + def test_open_close(self, devopener): + device=devopener() assert device.is_opened() for _ in range(self.test_open_rep): device.close() @@ -19,20 +22,24 @@ def test_open_close(self, device): device.close() assert not device.is_opened() device.open() + self.post_open(device) assert device.is_opened() device.open() assert device.is_opened() - def test_opened(self, device): + def test_opened(self, devopener): """Test opening and closing errors""" + device=devopener() assert device.is_opened() - def test_get_full_info(self, device): + def test_get_full_info(self, devopener): """Test info getting errors""" + device=devopener() info=device.get_full_info(self.include) print(device,info) @pytest.mark.devchange(2) - def test_get_set_all(self, device): + def test_get_set_all(self, devopener): """Test getting and re-applying settings error""" + device=devopener() settings=device.get_settings(self.include) print(device,settings) for k in self.get_set_all_exclude: diff --git a/tests/devices/test_basic_camera.py b/tests/devices/test_basic_camera.py index 5b1fb76..ade8c52 100644 --- a/tests/devices/test_basic_camera.py +++ b/tests/devices/test_basic_camera.py @@ -22,8 +22,9 @@ class CameraTester(DeviceTester): grab_size=10 default_roi=() @pytest.mark.devchange(2) - def test_snap_grab(self, device): + def test_snap_grab(self, devopener): """Test snapping and grabbing functions""" + device=devopener() device.set_roi(*self.default_roi) img=device.snap() assert isinstance(img,np.ndarray) @@ -35,18 +36,21 @@ def test_snap_grab(self, device): assert isinstance(imgs[0],np.ndarray) assert imgs[0].ndim==2 assert imgs[0].shape==device.get_data_dimensions() - def test_multisnap(self, device, stress_factor): + def test_multisnap(self, devopener, stress_factor): """Test snapping and grabbing functions""" + device=devopener() for _ in range(5*stress_factor): device.snap() - def test_multigrab(self, device, stress_factor): + def test_multigrab(self, devopener, stress_factor): """Test snapping and grabbing functions""" + device=devopener() device.set_roi(*self.default_roi) for _ in range(stress_factor): device.grab(self.grab_size) @pytest.mark.devchange(2) - def test_get_full_info_acq(self, device): + def test_get_full_info_acq(self, devopener): """Test info getting errors during acquisition""" + device=devopener() device.set_roi(*self.default_roi) device.start_acquisition() try: @@ -55,8 +59,9 @@ def test_get_full_info_acq(self, device): finally: device.stop_acquisition() # @pytest.mark.devchange(3) - # def test_get_set_all_acq(self, device): + # def test_get_set_all_acq(self, devopener): # """Test getting and re-applying settings error during acquisition""" + # device=devopener() # stopped_settings=device.get_settings(self.include) # print(device,stopped_settings) # for k in self.get_set_all_exclude: @@ -74,8 +79,9 @@ def test_get_full_info_acq(self, device): # device.stop_acquisition() @pytest.mark.devchange(1) - def test_frame_info(self, device): + def test_frame_info(self, devopener): """Test frame info consistency""" + device=devopener() device.set_roi(*self.default_roi) frames,infos=device.grab(self.grab_size,return_info=True) assert len(frames)==self.grab_size @@ -88,8 +94,9 @@ def check_acq_params(self, device, setup, running, new_images=None): assert device.acquisition_in_progress()==running assert (device.get_new_images_range() is not None)==new_images @pytest.mark.devchange(3) - def test_acq_info(self, device): + def test_acq_info(self, devopener): """Test getting acquisition info""" + device=devopener() device.set_roi() device.clear_acquisition() self.check_acq_params(device,False,False) @@ -104,8 +111,9 @@ def test_acq_info(self, device): self.check_acq_params(device,False,False) @pytest.mark.devchange(3) - def test_frame_size(self, device): + def test_frame_size(self, devopener): """Test data dimensions and detector size relations""" + device=devopener() for idx in ["rct","rcb","xyt","xyb"]: self.check_get_set(device,"image_indexing",idx) device.set_image_indexing("rct") @@ -126,12 +134,13 @@ class ROICameraTester(CameraTester): # a list of 2-tuples ``(roi_set, roi_get)``, where the first is the ROI supplied to the camera, # and the second is the expected resulting ROI (can also be ``"same"``) @pytest.mark.devchange(3) - def test_roi(self, device): + def test_roi(self, devopener): """ Test ROI functions. Also test that the frame shape and size obeys the specified ROI. """ + device=devopener() # basic full ROI device.set_roi() rr=device.get_roi() diff --git a/tests/devices/test_cameras.py b/tests/devices/test_cameras.py index 9ce8d86..97bdbba 100644 --- a/tests/devices/test_cameras.py +++ b/tests/devices/test_cameras.py @@ -82,8 +82,9 @@ def check_status_line(self, frames): assert np.all(slines[1:,0]-slines[:-1,0]==1) @pytest.mark.devchange(5) - def test_large_acq(self, device): + def test_large_acq(self, devopener): """Test large fast acquisition""" + device=devopener() for roi,ngrab,nbuff in [((0,None,0,None),100,50),((0,32,0,32),10**5,5000)]: device.set_roi(*roi) device.set_exposure(0) @@ -109,8 +110,9 @@ def check_status_line(self, frames): assert np.all(slines[1:,0]-slines[:-1,0]==1) @pytest.mark.devchange(5) - def test_large_acq(self, device): + def test_large_acq(self, devopener): """Test large fast acquisition""" + device=devopener() device.gav["CAMERA_LINK_CAMTYP"]="FG_CL_DUALTAP_12_BIT" device.gav["FORMAT"]="FG_GRAY16" device.cav["DataResolution"]="12bit" @@ -143,6 +145,7 @@ class TestTLCam(ROICameraTester): devcls=Thorlabs.ThorlabsTLCamera grab_size=100 rois=gen_rois(128,((1,1),(1,2),(2,2),((0,0),False),((3,3),False),((10,10),False),((100,100),False))) + # rois=gen_rois(128,((1,1),)) diff --git a/tests/devices/test_misc.py b/tests/devices/test_misc.py index ecce4a0..41bd5ff 100644 --- a/tests/devices/test_misc.py +++ b/tests/devices/test_misc.py @@ -11,7 +11,8 @@ class TestFW102(DeviceTester): devname="thorlabs_fw102" devcls=Thorlabs.FW @pytest.mark.devchange(4) - def test_motion(self, device): + def test_motion(self, devopener): + device=devopener() pos=device.get_position() npos=pos-1 if pos>1 else pos+1 device.set_position(npos) @@ -33,19 +34,17 @@ class TestNIDAQ(DeviceTester): ai_rate=1000 samples=1000 - @pytest.fixture(scope="class") - def device(self, device): + @classmethod + def post_open(cls, device): device.add_voltage_input("in0","ai0") device.add_voltage_input("in1","ai1") device.add_digital_input("din1","port0/line1") device.add_counter_input("c0","ctr0","pfi0") - device.setup_clock(self.ai_rate) - return device + device.setup_clock(cls.ai_rate) - def test_open_close(self, device): # otherwise it removes preset channels - pass - def test_read(self, device): + def test_read(self, devopener): """Test samples reading""" + device=devopener() v=device.read(self.samples,timeout=self.samples/self.ai_rate*2+2) assert v.shape==(self.samples,4) device.start() diff --git a/tests/devices/test_oscilloscopes.py b/tests/devices/test_oscilloscopes.py index 9e51c8d..d052635 100644 --- a/tests/devices/test_oscilloscopes.py +++ b/tests/devices/test_oscilloscopes.py @@ -10,7 +10,9 @@ class GenericOscilloscopeTester(DeviceTester): """Testing a generic oscilloscope""" @pytest.mark.devchange(4) - def test_grab(self, device): + def test_grab(self, devopener): + """Test trace readout""" + device=devopener() channels=list(range(1,device.get_channels_number()+1)) for ch in channels: device.enable_channel(ch) From ba6e8b107bafc1d6166767cafc844d32e7fc6966 Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Sat, 26 Jun 2021 20:30:27 +0200 Subject: [PATCH 29/31] Device bugfixes, framegrabber frame_infos --- pylablib/devices/IMAQ/IMAQ.py | 3 +- pylablib/devices/NI/daq.py | 7 +- pylablib/devices/PhotonFocus/PhotonFocus.py | 52 ++++++-- pylablib/devices/SiliconSoftware/fgrab.py | 122 +++++++++++++++--- .../SiliconSoftware/fgrab_prototyp_lib.py | 2 +- pylablib/devices/interface/camera.py | 13 +- pylablib/devices/utils/load_lib.py | 2 +- 7 files changed, 165 insertions(+), 36 deletions(-) diff --git a/pylablib/devices/IMAQ/IMAQ.py b/pylablib/devices/IMAQ/IMAQ.py index 6d0c0ef..63312f3 100644 --- a/pylablib/devices/IMAQ/IMAQ.py +++ b/pylablib/devices/IMAQ/IMAQ.py @@ -582,7 +582,8 @@ def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", retur frame_info=[] idx=first_frame for d in parsed_data: - frame_info.append(self._convert_frame_info(self._TFrameInfo(idx))) + idx_data=np.arange(len(d))+idx if return_info=="all" else idx + frame_info.append(self._convert_frame_info(self._TFrameInfo(idx_data))) idx+=len(d) else: frame_info=[self._TFrameInfo(first_frame+n) for n in range(len(parsed_data))] diff --git a/pylablib/devices/NI/daq.py b/pylablib/devices/NI/daq.py index 02123a5..28352b4 100644 --- a/pylablib/devices/NI/daq.py +++ b/pylablib/devices/NI/daq.py @@ -69,6 +69,7 @@ def __init__(self, dev_name="dev0", rate=1E2, buffer_size=1E5, reset=False): self.rate=rate self.clk_src=None self.buffer_size=buffer_size + self.ai_task=None self.ai_channels={} self.ci_tasks={} self.ci_counters={} @@ -80,8 +81,6 @@ def __init__(self, dev_name="dev0", rate=1E2, buffer_size=1E5, reset=False): self.clk_channel_base=20E6 self.max_ao_write_rate=1000 # maximal rate of repeating ao waveform with continuous repetition self.open() - self._update_channel_names() - self._running=False self._add_info_variable("device_info",self.get_device_info) self._add_status_variable("input_channels",lambda: self.get_input_channels(include=("ai","ci","di","cpi"))) self._add_status_variable("voltage_input_parameters",self.get_voltage_input_parameters) @@ -100,11 +99,15 @@ def _get_connection_parameters(self): return self.dev_name @reraise def open(self): + if self.ai_task is not None: + return self.ai_task=nidaqmx.Task() self.di_task=nidaqmx.Task() self.do_task=nidaqmx.Task() self.ao_task=nidaqmx.Task() self.cpi_task=nidaqmx.Task() + self._update_channel_names() + self._running=False @reraise def close(self): if self.ai_task is not None: diff --git a/pylablib/devices/PhotonFocus/PhotonFocus.py b/pylablib/devices/PhotonFocus/PhotonFocus.py index 03b58c2..427d9af 100644 --- a/pylablib/devices/PhotonFocus/PhotonFocus.py +++ b/pylablib/devices/PhotonFocus/PhotonFocus.py @@ -11,6 +11,7 @@ import numpy as np import collections import re +import warnings @@ -189,7 +190,7 @@ class IPhotonFocusCamera(camera.IAttributeCamera): # pylint: disable=abstract-me Args: pfcam_port: port number for pfcam interface (can be learned by :func:`list_cameras`; port number is the first element of the camera data tuple) - kwargs: keyword arguments passed to the frame grabber initializer + kwargs: keyword arguments passed to the frame grabber initialization """ Error=DeviceError GrabberClass=None @@ -212,7 +213,7 @@ def __init__(self, pfcam_port=0, **kwargs): def setup_max_baudrate(self): """Setup the maximal available baudrate""" - brs=[115200,57600,38400,19200,9600,4800,2400,1200] + brs=[921600,460800,230400,115200,57600,38400,19200,9600,4800,2400,1200] try: for br in brs: if lib.pfIsBaudRateSupported(self.pfcam_port,br): @@ -398,7 +399,7 @@ def set_roi(self, hstart=0, hend=None, vstart=0, vend=None): self.ucav["Window/H"]=min(vend-vstart,grabber_detector_size[1]) self._update_grabber_roi() return self.get_roi() - def get_roi_limits(self, hbin=1, vbin=1): + def get_roi_limits(self, hbin=1, vbin=1): # pylint: disable=unused-argument params=[self.ca[p] for p in ["Window/W","Window/H"]] minp,maxp=[list(p) for p in zip(*[p.update_limits() for p in params])] hlim=camera.TAxisROILimit(minp[0],maxp[0],self._hstep,minp[0],1) @@ -407,13 +408,25 @@ def get_roi_limits(self, hbin=1, vbin=1): def _get_buffer_bpp(self): bpp=self.GrabberClass._get_buffer_bpp(self) if self.GrabberClass else 1 - attr=self.get_attribute("DataResolution",error_on_missing=False) - if attr: - res=attr.get_value() - m=re.match(r"Res(\d+)Bit",res) - if m: - bpp=(int(m.group(1))-1)//8+1 + ppbpp=self._get_camera_bytepp() + if ppbpp is not None: + bpp=(ppbpp-1)//8+1 return bpp + def _get_camera_bytepp(self): + fmt=self.get_attribute_value("DataResolution",error_on_missing=False) + if fmt is None: + return None + m=re.match(r"(?:res)?(\d+)bit",fmt.lower()) + if m: + return int(m[1]) + return None + def _set_camera_bytepp(self, bpp): + fmt=self.get_attribute_value("DataResolution",error_on_missing=False) + if fmt: + m=re.match(r"^([^\d]*)(\d+)([^\d]*)$",fmt) + if m: + fmt="{}{}{}".format(m[1],bpp,m[3]) + self.cav["DataResolution"]=fmt def _get_data_dimensions_rc(self): roi=self.get_roi() w,h=roi[1]-roi[0],roi[3]-roi[2] @@ -505,6 +518,19 @@ class PhotonFocusIMAQCamera(IPhotonFocusCamera,IMAQFrameGrabber): def __init__(self, imaq_name="img0", pfcam_port=0): super().__init__(pfcam_port=pfcam_port,name=imaq_name) + def open(self): + super().open() + self._ensure_pixel_format() + + def _ensure_pixel_format(self): + fgbpp=self.get_grabber_attribute_value("BITSPERPIXEL") + ppbpp=self._get_camera_bytepp() + if ppbpp is not None and ppbpp!=fgbpp: + msg=( "PhotonFocus pixel format {} does not agree with the frame grabber {} bits per pixel; " + "changing PhotonFocus pixel format accordingly; to use the original format, alter the camera file".format(self.cav["DataResolution"],fgbpp)) + warnings.warn(msg) + self._set_camera_bytepp(fgbpp) + class PhotonFocusSiSoCamera(IPhotonFocusCamera,SiliconSoftwareFrameGrabber): @@ -524,6 +550,14 @@ class PhotonFocusSiSoCamera(IPhotonFocusCamera,SiliconSoftwareFrameGrabber): def __init__(self, siso_board, siso_applet, siso_port=0, pfcam_port=0): super().__init__(pfcam_port=pfcam_port,siso_board=siso_board,siso_applet=siso_applet,siso_port=siso_port) + def open(self): + super().open() + self._ensure_pixel_format() + + def _ensure_pixel_format(self): + ppbpp=self._get_camera_bytepp() + if ppbpp is not None: + self.setup_camlink_pixel_format(ppbpp,2) ##### Dealing with status line ##### diff --git a/pylablib/devices/SiliconSoftware/fgrab.py b/pylablib/devices/SiliconSoftware/fgrab.py index 8f6ca2b..50d86a8 100644 --- a/pylablib/devices/SiliconSoftware/fgrab.py +++ b/pylablib/devices/SiliconSoftware/fgrab.py @@ -1,6 +1,7 @@ from . import fgrab_prototyp_lib from .fgrab_prototyp_defs import FgAppletIntProperty, FgAppletStringProperty, FgAppletIteratorSource, FgParamTypes, FgProperty, Fg_Info_Selector, drFg_Info_Selector -from .fgrab_define_defs import FG_STATUS, FG_GETSTATUS, FG_ACQ +from .fgrab_prototyp_defs import MeCameraLinkFormat +from .fgrab_define_defs import FG_STATUS, FG_GETSTATUS, FG_ACQ, FG_IMGFMT, FG_PARAM from .fgrab_prototyp_lib import lib, SiliconSoftwareError, SIFgrabLibError from ...core.utils import py3, funcargparse, general as general_utils, dictionary @@ -244,6 +245,7 @@ def __repr__(self): TDeviceInfo=collections.namedtuple("TDeviceInfo",["applet_info","system_info","software_version"]) +TFrameInfo=collections.namedtuple("TFrameInfo",["frame_index","framestamp","timestamp","timestamp_long"]) class SiliconSoftwareFrameGrabber(camera.IGrabberAttributeCamera,camera.IROICamera): """ Generic Silicon Software frame grabber interface. @@ -262,6 +264,7 @@ class SiliconSoftwareFrameGrabber(camera.IGrabberAttributeCamera,camera.IROICame """ Error=SiliconSoftwareError TimeoutError=SiliconSoftwareTimeoutError + _TFrameInfo=TFrameInfo def __init__(self, siso_board=0, siso_applet="DualAreaGray16", siso_port=0, siso_detector_size=None, do_open=True, **kwargs): super().__init__(**kwargs) lib.initlib() @@ -281,6 +284,8 @@ def __init__(self, siso_board=0, siso_applet="DualAreaGray16", siso_port=0, siso self.siso_detector_size=siso_detector_size self._add_info_variable("device_info",self.get_device_info) + self._add_info_variable("camlink_pixel_formats",self.get_available_camlink_pixel_formats) + self._add_status_variable("pixel_format",self.get_camlink_pixel_format,ignore_error=SiliconSoftwareError) if do_open: self.open() @@ -290,9 +295,11 @@ def _normalize_grabber_attribute_name(self, name): if name.startswith("FG_"): return name[3:] return name + _fixed_parameters=[FG_PARAM.FG_CAMSTATUS,FG_PARAM.FG_IMAGE_TAG,FG_PARAM.FG_TIMEOUT,FG_PARAM.FG_TIMESTAMP,FG_PARAM.FG_TIMESTAMP_LONG,FG_PARAM.FG_TRANSFER_LEN,FG_PARAM.FG_GLOBAL_ACCESS] def _list_grabber_attributes(self): pnum=lib.Fg_getNrOfParameter(self.fg) attrs=[FGrabAttribute(self.fg,lib.Fg_getParameterId(self.fg,i),port=self.siso_port) for i in range(pnum)] + attrs+=[FGrabAttribute(self.fg,aid,port=self.siso_port) for aid in self._fixed_parameters] return [a for a in attrs if a.kind in ["i32","u32","i64","u64","f64","str"]] def _get_connection_parameters(self): return self.siso_board,self.siso_applet,self.siso_port,self.siso_applet_path @@ -436,6 +443,59 @@ def _ensure_buffers(self): if buffer_size!=self._buffer_mgr.frame_size: self._setup_buffers(nbuff) + _camlink_fmts={ ( 8,1):MeCameraLinkFormat.FG_CL_SINGLETAP_8_BIT, + (10,1):MeCameraLinkFormat.FG_CL_SINGLETAP_10_BIT, + (12,1):MeCameraLinkFormat.FG_CL_SINGLETAP_12_BIT, + (14,1):MeCameraLinkFormat.FG_CL_SINGLETAP_14_BIT, + (16,1):MeCameraLinkFormat.FG_CL_SINGLETAP_16_BIT, + ( 8,2):MeCameraLinkFormat.FG_CL_DUALTAP_8_BIT, + (10,2):MeCameraLinkFormat.FG_CL_DUALTAP_10_BIT, + (12,2):MeCameraLinkFormat.FG_CL_DUALTAP_12_BIT,} + def setup_camlink_pixel_format(self, bits_per_pixel=8, taps=1, output_fmt=None, fmt=None): + """ + Set up CameraLink pixel format. + + If `fmt` is ``None``, use supplied `bits_per_pixel` (8, 10, 12, 14, or 16) and `taps` (1 or 2) to figure out the format; + otherwise, `fmt` should be a numerical (e.g., ``210``) or string (e.g., ``"FG_CL_MEDIUM_10_BIT"``) format. + `output_fmt` specifies the result frame format; if ``None``, use grayscale with the given `bits_per_pixel` + if `fmt` is ``None``, or 16 bit grayscale otherwise. + """ + if fmt is None: + try: + fmt=self._camlink_fmts[bits_per_pixel,taps] + if output_fmt is None: + if bits_per_pixel<=8: + output_fmt=FG_IMGFMT.FG_GRAY + elif bits_per_pixel<=16: + output_fmt=FG_IMGFMT.FG_GRAY16 + else: + output_fmt=FG_IMGFMT.FG_GRAY32 + except KeyError: + raise KeyError("combination for {} bits per pixel and {} taps is not supported".format(bits_per_pixel,taps)) + else: + if output_fmt is None: + output_fmt=FG_IMGFMT.FG_GRAY16 + self.gav["CAMERA_LINK_CAMTYP"]=fmt + self.gav["FORMAT"]=output_fmt + def get_camlink_pixel_format(self): + """Get CamLink pixel format and the output pixel format as a tuple""" + try: + return (self.gav["CAMERA_LINK_CAMTYP"],self.gav["FORMAT"]) + except KeyError: + return None + def get_available_camlink_pixel_formats(self): + """Get all available CamLink pixel formats and the output pixel formats as a tuple of 2 lists""" + try: + clfmts=list(self.get_grabber_attribute("CAMERA_LINK_CAMTYP").values.values()) + except KeyError: + clfmts=None + try: + pxfmts=list(self.get_grabber_attribute("FORMAT").values.values()) + except KeyError: + pxfmts=None + return pxfmts,clfmts + + @interface.use_parameters(mode="acq_mode") def setup_acquisition(self, mode="sequence", nframes=100): """ @@ -515,7 +575,7 @@ def _trim_images_range(self, rng): rng=(rng[0]//self._frame_merge)*self._frame_merge,((rng[1]-1)//self._frame_merge+1)*self._frame_merge rng,skipped=self._frame_counter.trim_frames_range(rng) return rng,skipped - def _read_multiple_images_raw(self, rng=None, peek=False): + def _read_multiple_images_raw(self, rng=None, peek=False, return_info=False): """ Read multiple images specified by `rng` (by default, all un-read images). @@ -525,16 +585,38 @@ def _read_multiple_images_raw(self, rng=None, peek=False): (array of tuples ``[(nframes,data)]`` with the number of frames and the raw buffer data). """ if not self._buffer_mgr: - return 0,0,None + return 0,0,None,None rng,skipped_frames=self._trim_images_range(rng) if rng is None: - return 0,0,[] + return 0,0,[],[] raw_frames=self._buffer_mgr.get_frames_data(rng[0]//self._frame_merge,rng[1]//self._frame_merge-rng[0]//self._frame_merge) if rng[1]>rng[0] else [] + if return_info: + params=[FG_PARAM.FG_IMAGE_TAG,FG_PARAM.FG_TIMESTAMP,FG_PARAM.FG_TIMESTAMP_LONG] + frame_info=[] + chn=[n for n,_ in raw_frames] + chidx=np.cumsum([rng[0]]+chn[:-1]) + if return_info in ["full","chunks"]: + frng=range(rng[0]//self._frame_merge,rng[1]//self._frame_merge) + else: + frng=chidx + frame_info.append(np.array(frng)*self._frame_merge) + for p in params: + try: + frame_info.append([lib.Fg_getParameterEx_auto(self.fg,p,self.siso_port,self._buffer_head,i+1) for i in frng]) + except SIFgrabLibError: + frame_info.append([0]*len(frng)) + frame_info=np.array(frame_info) + if return_info=="chunks": + frame_info=[frame_info[:,i:i+n].T for i,n in zip(chidx-chidx[0],chn)] + else: + frame_info=list(frame_info.T) + else: + frame_info=None if not peek: self._frame_counter.advance_read_frames(rng) - return rng[0],skipped_frames,raw_frames + return rng[0],skipped_frames,raw_frames,frame_info - def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", return_info=None, fastbuff=False): + def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", return_info=False, fastbuff=False): """ Read multiple images specified by `rng` (by default, all un-read images). @@ -543,17 +625,24 @@ def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", retur If ``peek==True``, return images but not mark them as read. `missing_frame` determines what to do with frames which are out of range (missing or lost): can be ``"none"`` (replacing them with ``None``), ``"zero"`` (replacing them with zero-filled frame), or ``"skip"`` (skipping them). - If ``return_info==True``, return tuple ``(frames, infos)``, where ``infos`` is a list of ``TFrameInfo`` single-element tuples - containing frame index; if some frames are missing and ``missing_frame!="skip"``, the corresponding frame info is ``None``. + If ``return_info==True``, return tuple ``(frames, infos)``, where ``infos`` is a list of ``TFrameInfo`` instances + describing frame index, framestamp, and two timestamps (lower and higher precision); + if some frames are missing and ``missing_frame!="skip"``, the corresponding frame info is ``None``. + Note that obtaining frame info takes about 100us, so ``return_info="all"`` should be avoided fro rates above 5-10kFPS. If ``fastbuff==False``, return a list of individual frames (2D numpy arrays). Otherwise, return a list of 'chunks', which are 3D numpy arrays containing several frames; - in this case, ``frame_info`` will only have one entry per chunk corresponding to the first frame in the chunk. + in this case, if `return_info` is ``True``, then ``frame_info`` will only have one entry per chunk corresponding to the first frame in the chunk, + and if `return_info` is ``"all"``, it will return info for each frame in the chunk, also in chunk form. Using ``fastbuff`` results in faster operation at high frame rates (>~1kFPS), at the expense of a more complicated frame processing in the following code. """ funcargparse.check_parameter_range(missing_frame,"missing_frame",["none","zero","skip"]) if fastbuff and missing_frame=="none": raise ValueError("'none' missing frames mode is not supported if fastbuff==True") - first_frame,skipped_frames,raw_data=self._read_multiple_images_raw(rng=rng,peek=peek) + if return_info and not fastbuff: + return_info="full" + elif return_info=="all": + return_info="chunks" + _,skipped_frames,raw_data,frame_info=self._read_multiple_images_raw(rng=rng,peek=peek,return_info=return_info) if raw_data is None: return (None,None) if return_info else None dim=self._get_data_dimensions_rc() @@ -561,15 +650,10 @@ def read_multiple_images(self, rng=None, peek=False, missing_frame="skip", retur parsed_data=[self._parse_buffer(b,nframes=n) for n,b in raw_data] if not fastbuff: parsed_data=[f for chunk in parsed_data for f in chunk] - if return_info: - if fastbuff: - frame_info=[] - idx=first_frame - for d in parsed_data: - frame_info.append(self._convert_frame_info(self._TFrameInfo(idx))) - idx+=len(d) - else: - frame_info=[self._TFrameInfo(first_frame+n) for n in range(len(parsed_data))] + if return_info=="chunks": + frame_info=[self._convert_frame_info(self._TFrameInfo(*list(fi.T))) for fi in frame_info] + elif return_info: + frame_info=[self._convert_frame_info(self._TFrameInfo(*fi)) for fi in frame_info] if skipped_frames and missing_frame!="skip": if fastbuff: # only missing_frame=="zero" is possible parsed_data=[np.zeros((skipped_frames,)+dim,dtype=dt)]+parsed_data diff --git a/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py b/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py index befeb6a..72ee3b4 100644 --- a/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py +++ b/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py @@ -377,7 +377,7 @@ def Fg_setParameterWithType_auto(self, Fg, Parameter, Value, DmaIndex, ptype=Non def Fg_getParameterEx_auto(self, Fg, Parameter, DmaIndex, pMem, ImgNr): ptypes={FG_PARAM.FG_IMAGE_TAG:ctypes.c_uint, FG_PARAM.FG_TIMESTAMP:ctypes.c_uint, FG_PARAM.FG_TIMESTAMP_LONG:ctypes.c_uint64, FG_PARAM.FG_TRANSFER_LEN:ctypes.c_size_t} v=ptypes[Parameter]() - self.Fg_getParameterEx(Fg,Parameter,v,DmaIndex,pMem,ImgNr) + self.Fg_getParameterEx(Fg,Parameter,ctypes.byref(v),DmaIndex,pMem,ImgNr) return v.value diff --git a/pylablib/devices/interface/camera.py b/pylablib/devices/interface/camera.py index 9fc9d57..99f18f0 100644 --- a/pylablib/devices/interface/camera.py +++ b/pylablib/devices/interface/camera.py @@ -32,7 +32,7 @@ class ICamera(interface.IDevice): Error=comm_backend.DeviceError TimeoutError=comm_backend.DeviceError FrameTransferError=DefaultFrameTransferError - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs): # pylint: disable=unused-argument super().__init__() self._acq_params=None self._default_acq_params=function_utils.funcsig(self.setup_acquisition).defaults @@ -273,10 +273,11 @@ def get_frame_info_format(self): Can be ``"namedtuple"`` (potentially nested named tuples; convenient to get particular values), ``"list"`` (flat list of values, with field names are given by :meth:`get_frame_info_fields`; convenient for building a table), + ``"array"`` (same as ``"list"``, but with a numpy array, which is easier to use for ``fastbuff`` readout supported by some cameras), or ``"dict"`` (flat dictionary with the same fields as the ``"list"`` format; more resilient to future format changes) """ return self._frameinfo_format - _p_frameinfo_format=interface.EnumParameterClass("frame_info_format",["namedtuple","list","dict"]) + _p_frameinfo_format=interface.EnumParameterClass("frame_info_format",["namedtuple","list","array","dict"]) @interface.use_parameters(fmt="frame_info_format") def set_frame_info_format(self, fmt): """ @@ -284,6 +285,7 @@ def set_frame_info_format(self, fmt): Can be ``"namedtuple"`` (potentially nested named tuples; convenient to get particular values), ``"list"`` (flat list of values, with field names are given by :meth:`get_frame_info_fields`; convenient for building a table), + ``"array"`` (same as ``"list"``, but with a numpy array, which is easier to use for ``fastbuff`` readout supported by some cameras), or ``"dict"`` (flat dictionary with the same fields as the ``"list"`` format; more resilient to future format changes) """ self._frameinfo_format=fmt @@ -304,6 +306,11 @@ def _convert_frame_info(self, info, fmt=None): return info if fmt=="list": return list(general_utils.flatten_list(info)) + if fmt=="array": + arr=np.array(list(general_utils.flatten_list(info))) + if arr.ndim==2: + arr=arr.T + return arr return dict(zip(self._frameinfo_fields,general_utils.flatten_list(info))) def get_new_images_range(self): @@ -831,7 +838,7 @@ class IGrabberAttributeCamera(ICamera): Camera class which supports frame grabber attributes. Essentially the same as :class:`IAttributeCamera`, but with relevant methods and attributes renamed - to support both frame grabber and camera attrbiutes handling simultaneously. + to support both frame grabber and camera attributes handling simultaneously. The method ``_list_grabber_attributes`` must be defined in a subclass; it should produce a list of camera attributes, which have ``name`` attribute for placing them into a dictionary. diff --git a/pylablib/devices/utils/load_lib.py b/pylablib/devices/utils/load_lib.py index 972fdbd..afe69b2 100644 --- a/pylablib/devices/utils/load_lib.py +++ b/pylablib/devices/utils/load_lib.py @@ -125,7 +125,7 @@ def load_lib(name, locations=("global",), call_conv="cdecl", locally=False, depe env_paths=old_env_path.split(os.pathsep) if old_env_path else [] if not any([files.paths_equal(loc_folder,ep) for ep in env_paths if ep]): os.environ["PATH"]=files.normalize_path(loc_folder)+(os.pathsep+old_env_path if old_env_path else "") - path=loc_name if folder=="" else "./"+loc_name + path=loc_name folder=loc_folder depends=depends or [] paths=[os.path.join(folder,dn) for dn in depends]+[path] From 38fee43604653f035bf7e80966493b9d27ca7f8d Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Sat, 26 Jun 2021 21:38:54 +0200 Subject: [PATCH 30/31] Added SiliconSoftware documentation --- docs/devices/IMAQ.rst | 4 +- docs/devices/PhotonFocus.rst | 2 +- docs/devices/SiliconSoftware.rst | 87 +++++++++++++++++++ docs/devices/cameras_basics.rst | 2 + docs/devices/cameras_list.txt | 1 + .../SiliconSoftware/fgrab_prototyp_lib.py | 4 +- 6 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 docs/devices/SiliconSoftware.rst diff --git a/docs/devices/IMAQ.rst b/docs/devices/IMAQ.rst index 84a8b02..c9c8c8b 100644 --- a/docs/devices/IMAQ.rst +++ b/docs/devices/IMAQ.rst @@ -28,7 +28,7 @@ The cameras are identified by their name, which usually looks like ``"img0"``. T >> from pylablib.devices import IMAQ >> IMAQ.list_cameras() - [`img0`, `img1`] + ['img0', 'img1'] >> cam1 = IMAQ.IMAQCamera('img0') >> cam2 = IMAQ.IMAQCamera('img1') >> cam1.close() @@ -74,4 +74,4 @@ Known issues - If you are unable to access full camera sensor size, check the camera file (it can be opened in the text editor). ``MaxImageSize`` parameter defines the maximal allowed image size, and it should be equal to the camera sensor size. - Same goes for bitness. If the camera bitness is higher than set up in the frame grabber, a single camera pixel gets treated as several pixels by the frame grabber, typically resulting in 1px-wide vertical stripes on the image. In the opposite case, the frame grabber expects more bytes than the camera sends, it never receives the full frame, and the acquisition times out. - Keep in mind that as long as the frame grabber is accessed in NI MAX, it is blocked from use in any other software. Hence, you need to close NI MAX before running your code. -- As mentioned above, ROI is defined within a frame transferred by the camera. Hence, if it includes pixels with positions outside of the transferred frame, the acquisition will time out. For example, suppose the camera sensor is 1024x1024px, and the *camera* ROI is selected to be central 512x512 region. As far as the frame grabber is concerned, now the camera sensor size is 512x512px. Hence, if you try to set the same *frame grabber* ROI in the (i.e., 512x512 starting at 256,256), it will expect 768x768px frame. Since the frame is, actually, 512x512px, the acquisition will time out. The correct solution is to set frame grabber ROI from 0 to 512px on both axes. In general, it is a good idea to always follow this pattern: control ROI only on camera, and always set frame grabber ROI to cover the whole transfer frame. \ No newline at end of file +- As mentioned above, ROI is defined within a frame transferred by the camera. Hence, if it includes pixels with positions outside of the transferred frame, the acquisition will time out. For example, suppose the camera sensor is 1024x1024px, and the *camera* ROI is selected to be central 512x512 region. As far as the frame grabber is concerned, now the camera sensor size is 512x512px. Hence, if you try to set the same *frame grabber* ROI (i.e., 512x512 starting at 256,256), it will expect 768x768px frame. Since the frame is, actually, 512x512px, the acquisition will time out. The correct solution is to set frame grabber ROI from 0 to 512px on both axes. In general, it is a good idea to always follow this pattern: control ROI only on camera, and always set frame grabber ROI to cover the whole transfer frame. \ No newline at end of file diff --git a/docs/devices/PhotonFocus.rst b/docs/devices/PhotonFocus.rst index cceea18..636264a 100644 --- a/docs/devices/PhotonFocus.rst +++ b/docs/devices/PhotonFocus.rst @@ -13,7 +13,7 @@ In principle, pfcam can work with any frame grabber. However, so far it has only Software requirements ----------------------- -These cameras require ``pfcam.dll``, which is installed with `PFInstaller `__, which is freely available, but requires a registration. After installation, the DLL is automatically added to the ``System32`` folder, where pylablib looks for it by default. If the DLL is located elsewhere, the path can be specified using the library parameter ``devices/dlls/pfcam``:: +These cameras require ``pfcam.dll``, which is installed with `PFInstaller `__, which is freely available, but requires a registration. After installation, the path to the DLL (located by default in ``Photonfocus/PFRemote/bin`` folder in ``Program Files``) is automatically added to system ``PATH`` variable, which is one of the places where pylablib looks for it by default. If the DLL is located elsewhere, the path can be specified using the library parameter ``devices/dlls/pfcam``:: import pylablib as pll pll.par["devices/dlls/pfcam"] = "path/to/dlls" diff --git a/docs/devices/SiliconSoftware.rst b/docs/devices/SiliconSoftware.rst new file mode 100644 index 0000000..02e90b6 --- /dev/null +++ b/docs/devices/SiliconSoftware.rst @@ -0,0 +1,87 @@ +.. _cameras_siso: + +.. note:: + General camera communication concepts are described on the corresponding :ref:`page ` + +Silicon Software frame grabbers interface +========================================= + +Silicon Software produces a range of frame grabbers, which can be used to control different cameras with a CameraLink interface. It has been tested with microEnable IV AD4-CL frame grabber together with PhotonFocus MV-D1024E camera. + +The code is located in :mod:`pylablib.devices.SiliconSoftware`, and the main camera class is :class:`pylablib.devices.SiliconSoftware.SiliconSoftwareCamera<.SiliconSoftware.SiliconSoftwareCamera>`. + +Software requirements +----------------------- + +This interfaces requires ``fglib5.dll``, which is installed with the freely available `Silicon Software Runtime Environment `__ (the newest version for 64-bit Windows is `5.7.0 `__), which also includes all the necessary drivers. After installation, the path to the DLL (located by default in ``SiliconSoftware/Runtime5.7.0/bin`` folder in ``Program Files``) is automatically added to system ``PATH`` variable, which is one of the places where pylablib looks for it by default. If the DLL is located elsewhere, the path can be specified using the library parameter ``devices/dlls/sisofgrab``:: + + import pylablib as pll + pll.par["devices/dlls/sisofgrab"] = "path/to/dlls" + from pylablib.devices import SiliconSoftware + cam = SiliconSoftware.SiliconSoftwareCamera() + + +Connection +----------------------- + +Figuring out the connection parameters is a multi-stage process. First, one must identify one of several boards. The boards can be identified using :func:`.SiliconSoftware.list_boards` function. Second, one must select an applet. These provide different board readout modes and, for Advanced Applets, various post-processing capabilities. These applets can be identified using :func:`.SiliconSoftware.list_applets` method, or directly from the Silicon Software RT microDisplay software supplied with the runtime. The choice depends on the color mode (color vs. grayscale and different bitness), readout mode (area or line), can camera connection (single, double, or quad). Finally, depending on the board and the camera connection, one of several ports must be selected. For example, if the frame grabber has two connectors, but the camera only uses a single interface, then the double camera applet (e.g., ``DualAreaGray16``) must be selected, and the port should specify the board connector (0 for ``A``, 1 for ``B``):: + + >> from pylablib.devices import SiliconSoftware + >> SiliconSoftware.list_boards() # first list the connected boards + [TBoardInfo(name='mE4AD4-CL', full_name='microEnable IV AD4-CL')] + >> SiliconSoftware.list_applets(0) # list all applets on the first board + [ ..., + TAppletInfo(name='DualAreaGray16', file='DualAreaGray16.dll'), + ... ] + >> cam = SiliconSoftware.SiliconSoftwareCamera(0, 'DualAreaGray16') # connect to the first board (port 0 by default) + >> cam.close() + +Note that currently the code is organized in such a way, that only one port on a single board can be in use at one time. + +Operation +------------------------ + +Unlike most cameras, the frame grabber interface only deals with the frame transfer between the camera and the PC over the CameraLink interface. Therefore, in can not directly control camera parameters such as exposure, frame rate, triggering, ROI, etc. Some similar-looking parameters are still present, but they have a different meaning: + + - External trigger controls frame *transfer*, not frame *acquisition*, which is defined by the camera. By default, when the internal frame grabber trigger is used, the frame grabber transfer rate is synchronized to the camera, so every frame gets transferred. However, if the external transfer trigger is used and it is out of sync with the camera, it can result in duplicate or missing frames. + - ROI is defined within the transferred image, whose size itself is determined by the camera ROI. Hence, e.g., if the camera chip is 1024x1024px and its roi is 512x512, then the frame grabber ROI can go only up to 512x512. Any attempts to set it higher result in frame being misshapen or having random data outside of the image area. + +The SDK also provides a universal interface for getting and setting various :ref:`camera attributes ` using their name. You can use :meth:`.SiliconSoftwareCamera.get_grabber_attribute_value` and :meth:`.SiliconSoftwareCamera.set_grabber_attribute_value` for that, as well as ``.gav`` attribute which gives a dictionary-like access:: + + >> cam = SiliconSoftware.SiliconSoftwareCamera() + >> cam.get_grabber_attribute_value("CAMERA_LINK_CAMTYP") # get the camera data format + 'FG_CL_SINGLETAP_8_BIT' + >> cam.set_grabber_attribute_value("WIDTH", 512) # set the readout frame width to 512px + >> cam.gav["WIDTH"] # get the width; could also use cam.get_grabber_attribute_value("WIDTH") + 512 + +To see all available attributes, you can call :meth:`.SiliconSoftwareCamera.get_all_grabber_attributes` to get a dictionary with attribute objects, and :meth:`.SiliconSoftwareCamera.get_all_grabber_attribute_values` to get the dictionary of attribute values. The attribute objects provide additional information: attribute kind (integer, string, etc.), range (either numerical range, or selection of values for enum attributes), description string, etc.:: + + >> cam = IMAQdx.IMAQdxCamera() + >> attr = cam.get_grabber_attribute("BITALIGNMENT") + >> attr.values + {1: 'FG_LEFT_ALIGNED', 0: 'FG_RIGHT_ALIGNED'} + +The parameter can also be inspected in the Silicon Software RT microDisplay software. + +Fast buffer readout mode +~~~~~~~~~~~~~~~~~~~~~~~~ + +At high frame rates (above ~10kFPS) dealing with each frame individually becomes too slow for Python. Hence, there is an option to read out and process frames in larger 'chunks', which are 3D numpy arrays with the first axis enumerating the frame index. This approach leverages the ability to store several frame buffers in the contiguous memory locations (resulting in a single 3D array), and it essentially eliminates the overhead for dealing with multiple frames at high frame rates, as long as the total data rate is manageable (typically below 600Mb/s). + +This option can be accessed by supplying ``fastbuff=True`` in :meth:`.SiliconSoftwareCamera.read_multiple_images`. In this case, instead of a list of individual frames (which is the standard behavior), the method returns list of chunks about 1Mb in size, which contain several consecutive frames. Otherwise the method behaves identically to the standard one. + + +Communication with the camera and camera files +-------------------------------------------------- + +The frame grabber needs some basic information about the camera (sensor size, bit depth, timeouts, aux lines mapping), which are contained in the camera files. These files can be assigned to cameras in the NI MAX, and are usually supplied by NI or by the camera manufacturer. In addition, NI MAX allows one to adjust some settings within these files, which are read-only within the NI IMAQ software. These include frame timeout and camera bit depth. + +The communication with the camera itself greatly varies between different cameras. Some will have additional connection to control the parameters. However, most use serial communication built into the CameraLink interface. This communication can be set up with :meth:`.IMAQCamera.setup_serial_params` and used via :meth:`.IMAQCamera.serial_read` and :meth:`.IMAQCamera.serial_write`. The communication protocols are camera-dependent. Yet some other cameras (e.g., Photon Focus) use proprietary communication protocol. In this case, the provide their own DLLs, which independently use NI-provided DLLs for serial communication (most notably, ``clallserial.dll``). In this case, one needs to maintain two independent connections: one directly to the NI frame grabber to obtain the frame data, and one to the manufacturer library to control the camera. This is the way it is implemented in PhotonFocus camera interface. + + +Known issues +-------------------- + +- The maximal frame rate (at least for the tested microEnable IV AD4-CL board) is limited by about 20kFPS. It seems to be relatively independent of the frame size, i.e., it is not the data transfer rate issue. One possible way to get around it is to use line readout applet, e.g., ``DualLineGray16``, and set the frame height to be the integer multiple of the camera frame. This will combine several camera frames into a single frame-grabber frame, effectively lowering the frame rate at avoiding the issue. However, this sometimes leads to incorrect frame splitting: the top line of the "combined" frame does not coincide with the top line of the original camera frame, so all frames are shifted cyclically by some number of rows. Hence, it might require some post-processing with frames merging and re-splitting. +- As mentioned above, ROI is defined within a frame transferred by the camera. Therefore, if it includes pixels with positions outside of the transferred frame, the acquisition will be faulty. For example, suppose the camera sensor is 1024x1024px, and the *camera* ROI is selected to be central 512x512 region. As far as the frame grabber is concerned, now the camera sensor size is 512x512px. Hence, if you try to set the same *frame grabber* ROI (i.e., 512x512 starting at 256,256), it will expect 768x768px frame. Since the frame is, actually, 512x512px, the returned frame will partially contain random data. The correct solution is to set frame grabber ROI from 0 to 512px on both axes. In general, it is a good idea to always follow this pattern: control ROI only on camera, and always set frame grabber ROI to cover the whole transfer frame. \ No newline at end of file diff --git a/docs/devices/cameras_basics.rst b/docs/devices/cameras_basics.rst index 60771a5..39d232e 100644 --- a/docs/devices/cameras_basics.rst +++ b/docs/devices/cameras_basics.rst @@ -248,6 +248,8 @@ Many cameras supply additional information together with the frames. Most freque There are several slightly different metainfo formats, which can be set using :meth:`.ICamera.set_frame_info_format` method. The default representation is a (possibly nested) named tuple, but it is also possible to represent it as a flat list, or a flat dictionary. The exact structure and values depend on the camera. +Keep in mind, that for some camera interfaces (e.g., :ref:`Uc480 ` or :ref:`Silicon Software `) obtaining the additional information might take relatively long, even longer than the proper frame readout. Hence, at higher frame rates it might become a bottleneck, and would need to be turned off. + Currently supported cameras ------------------------------------------- diff --git a/docs/devices/cameras_list.txt b/docs/devices/cameras_list.txt index d569367..92a9473 100644 --- a/docs/devices/cameras_list.txt +++ b/docs/devices/cameras_list.txt @@ -4,5 +4,6 @@ - :ref:`NI IMAQdx `: National Instruments universal camera interface. Tested with Ethernet-connected PhotonFocus HD1-D1312 camera. - :ref:`Photon Focus `: Photon Focus pfcam interface. Tested with NI PCI-1430 and PCI-1433 frame grabbers together with PhotonFocus MV-D1024E camera. - :ref:`PCO SC2 `: PCO cameras. Tested with pco.edge cameras with CLHS and regular CameraLink interfaces. +- :ref:`Silicon Software `: Silicon Software frame grabbers. Tested with microEnable IV AD4-CL frame grabbers together with PhotonFocus MV-D1024E camera. - :ref:`Thorlabs Scientific Cameras `: Thorlabs sCMOS cameras. Tested with Thorlabs Kiralux camera. - :ref:`Uc480 `: multiple cameras, including simple Thorlabs and IDS cameras. Tested with IDS SC2592R12M and Thorlabs DCC1545M. \ No newline at end of file diff --git a/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py b/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py index 72ee3b4..57566d1 100644 --- a/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py +++ b/pylablib/devices/SiliconSoftware/fgrab_prototyp_lib.py @@ -61,8 +61,8 @@ def __init__(self): def initlib(self): if self._initialized: return - error_message="The library is automatically supplied with Silicon Software Runtime software\n"+load_lib.par_error_message.format("sifgrab") - self.lib=load_lib.load_lib("fglib5.dll",locations=("parameter/sifgrab","global"),error_message=error_message,call_conv="cdecl") + error_message="The library is automatically supplied with Silicon Software Runtime software\n"+load_lib.par_error_message.format("sisofgrab") + self.lib=load_lib.load_lib("fglib5.dll",locations=("parameter/sisofgrab","global"),error_message=error_message,call_conv="cdecl") lib=self.lib define_functions(lib) From 630b4f5522fe16df04e37ef7e8dae720fd3edb0e Mon Sep 17 00:00:00 2001 From: Alexey Shkarin Date: Sat, 26 Jun 2021 23:53:04 +0200 Subject: [PATCH 31/31] Docs update for v1.1.0 --- docs/changelog.rst | 24 +++++++++++++++++++++++- docs/conf.py | 4 ++-- docs/devices/PhotonFocus.rst | 13 +++++++------ docs/devices/SiliconSoftware.rst | 14 +++++++------- docs/devices/cameras_root.rst | 1 + pylablib/devices/PhotonFocus/__init__.py | 2 +- setup.py | 4 ++-- 7 files changed, 43 insertions(+), 19 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0a3c176..45e2acf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,7 +23,29 @@ you can write import pylablib.legacy as pll from pylablib.legacy.aux_libs.devices import Lakeshore -.. + renamed ``setupUi`` -> ``setup`` for all widgets +1.1.0 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- General + + * Reorganized the core modules import structure: now ``__init__.py`` modules are mostly empty, and all the necessary imports are either exposed directly in ``pylablib`` (e.g., ``pylablib.Fitter``), or should be accessed directly by the module (e.g. ``pll.core.dataproc.fitting.Fitter``). Intermediate access (e.g., ``pll.core.dataproc.Fitter``) is no longer supported. + * File IO functions (e.g., ``read_csv``) can now take file-like objects in addition to paths. + +- Devices + + * Added Silicon Software frame grabbers interface and rearranged PhotonFocus code to include both IMAQ and SiliconSoftware frame grabbers. + * Fixed various compatibility bugs arising for specific versions of Python or dependency modules: Kinesis error with specific pyft232 versions, some DLL-dependent devices errors with Python 3.8+, DLL types in 32-bit Python + * Addressed issue with occasional uc480 acquisition restarts, fixed M2 communication report errors, + +- GUI and threading + + * Added container and layout management classes in addition to parameter tables for more consistent GUI structure organization. + * Added ``pylablib.widgets`` module which combines all custom widgets for the ease of using in layout managers or custom applications. + * Fixed support for ``PySide2`` Qt5 backed. + * Renamed ``setupUi`` -> ``setup`` for all widgets, and changed the GUI setup organization for many of them (the functioning stayed the same). + * Reorganized scheduling in ``QTaskThread`` to treat jobs, commands, and subscriptions more consistently. + * Added basic data stream management. + 1.0.0 diff --git a/docs/conf.py b/docs/conf.py index a4415e5..8b51c73 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,7 +29,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '1.0.0' +release = '1.1.0' # -- General configuration --------------------------------------------------- @@ -213,7 +213,7 @@ def setup(app): # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'pylablib', 'pylablib Documentation', - author, 'pylablib', 'One line description of project.', + author, 'pylablib', 'Code for use in lab environment: experiment automation, data acquisition, device communication.', 'Miscellaneous'), ] diff --git a/docs/devices/PhotonFocus.rst b/docs/devices/PhotonFocus.rst index 636264a..513bb6c 100644 --- a/docs/devices/PhotonFocus.rst +++ b/docs/devices/PhotonFocus.rst @@ -6,9 +6,9 @@ Photon Focus pfcam interface ============================ -Photon Focus cameras transfer their data to the PC using frame grabbers (e.g., via :ref:`NI IMAQ ` interface). Hence, the camera control is done through the serial port built into the CameraLink interface. However, the cameras use a closed binary protocol, so one needs to use a pfcam library provided by Photon Focus. It relies on the libraries exposed by the frame grabber manufacturers (e.g., the standard ``cl*serial.dll``) to communicate with the camera directly, meaning that the pfcam user simply calls its method, and all the communication happens behind the scenes. +Photon Focus cameras transfer their data to the PC using frame grabbers (e.g., via :ref:`NI IMAQ ` or :ref:`Silicon Photonics ` interfaces). Hence, the camera control is done through the serial port built into the CameraLink interface. However, the cameras use a closed binary protocol, so all the control is done through the pfcam library provided by Photon Focus. It relies on the libraries exposed by the frame grabber manufacturers (e.g., the standard ``cl*serial.dll``) to communicate with the camera directly, meaning that the pfcam user simply calls its method, and all the communication happens behind the scenes. -In principle, pfcam can work with any frame grabber. However, so far it has only been developed and tested in conjunction with National Instruments frame grabbers using the :ref:`NI IMAQ ` interface. Hence, the main camera class :class:`pylablib.devices.PhotonFocus.PhotonFocusIMAQCamera<.PhotonFocus.PhotonFocusIMAQCamera>` already incorporates IMAQ functionality. This makes it easier to use, but restricts the use of pfcam to IMAQ-compatible frame grabbers. +In principle, pfcam can work with any frame grabber. Because of that, there are two different kinds of classes for this camera. To start with, there is :class:`.PhotonFocus.IPhotonFocusCamera<.PhotonFocus.IPhotonFocusCamera>`, which provides interface for addressing camera properties, but can not handle actual frame acquisition. Using this class directly leads to errors in any frame data related methods (e.g., ``wait_for_frame``, or ``read_multiple_images``), and it is mostly intended to serve as a base class to be combined with the actual frame grabber. Two such classes are provided: :class:`.PhotonFocus.PhotonFocusIMAQCamera<.PhotonFocus.PhotonFocusIMAQCamera>` for National Instruments frame grabbers using the :ref:`NI IMAQ ` interface and :class:`.PhotonFocus.PhotonFocusSiSoCamera<.PhotonFocus.PhotonFocusSiSoCamera>` for :ref:`Silicon Photonics ` frame grabbers. Both are classes are complete and ready to use. In addition to combining camera and frame grabber control, they also implement basic consistency support, such as automatic adjustment of frame grabber ROI and data transfer format. Software requirements ----------------------- @@ -24,7 +24,7 @@ These cameras require ``pfcam.dll``, which is installed with `PFInstaller ` documentation. The second is the pfcam port, which is a number starting from zero. To list all of the connected pfcam-compatible cameras, you can run :func:`.PhotonFocus.list_cameras`:: +The camera class requires two pieces of information. First is the frame grabber interface connection: either NI IMAQ interface name (e.g., ``"img0"``) identified as described in the :ref:`NI IMAQ ` documentation, or Silicon Software board and applet described in :ref:`Silicon Software ` documentation. The second piece of information is the pfcam port, which is a number starting from zero. To list all of the connected pfcam-compatible cameras, you can use the PFRemote software (the interface number is given in parentheses after every connection option in the list) or run :func:`.PhotonFocus.list_cameras`:: >> from pylablib.devices import PhotonFocus, IMAQ >> IMAQ.list_cameras() # get all IMAQ frame grabber devices @@ -40,7 +40,7 @@ Operation The operation of these cameras is relatively standard. They support all the standard methods for dealing with ROI and exposure, starting and stopping acquisition, and operating the frame reading loop. However, there's a couple of differences from the standard libraries worth highlighting: - - The SDK also provides a universal interface for getting and setting various :ref:`camera attributes ` (called "properties" in the documentation) using their name. You can use :meth:`.PhotonFocusIMAQCamera.get_attribute_value` and :meth:`.PhotonFocusIMAQCamera.set_attribute_value` for that, as well as ``.cav`` attribute which gives a dictionary-like access:: + - The SDK also provides a universal interface for getting and setting various :ref:`camera attributes ` (called "properties" in the documentation) using their name. You can use :meth:`.IPhotonFocusCamera.get_attribute_value` and :meth:`.IPhotonFocusCamera.set_attribute_value` for that, as well as ``.cav`` attribute which gives a dictionary-like access:: >> cam = PhotonFocus.PhotonFocusIMAQCamera() >> cam.get_attribute_value("Window/W") # get the ROI width @@ -49,7 +49,7 @@ The operation of these cameras is relatively standard. They support all the stan >> cam.cav["ExposureTime"] # get the exposure; could also use cam.get_attribute_value("ExposureTime") 0.1 - Some values (e.g., ``Window.Max`` or ``Reset``) serve as commands; these can be invoked using :meth:`.PhotonFocusIMAQCamera.call_command` method. To see all available attributes, you can call :meth:`.DCAMCamera.get_all_attributes` to get a dictionary with attribute objects, and :meth:`.DCAMCamera.get_all_attribute_values` to get the dictionary of attribute values. The attribute objects provide additional information: attribute range, step, and units:: + Some values (e.g., ``Window.Max`` or ``Reset``) serve as commands; these can be invoked using :meth:`.PhotonFocusIMAQCamera.call_command` method. To see all available attributes, you can call :meth:`.IPhotonFocusCamera.get_all_attributes` to get a dictionary with attribute objects, and :meth:`.IPhotonFocusCamera.get_all_attribute_values` to get the dictionary of attribute values. The attribute objects provide additional information: attribute range, step, and units:: >> cam = PhotonFocus.PhotonFocusIMAQCamera() >> attr = cam.get_attribute("Window/W") @@ -58,5 +58,6 @@ The operation of these cameras is relatively standard. They support all the stan >> (attr.min, attr.max) (16, 1024) - - Being a subclass of :class:`.IMAQ.IMAQCamera` class, it supports all of its features, such as trigger control and fast buffer acquisition. Some methods have been modified to make them more convenient: e.g., :meth:`.PhotonFocusIMAQCamera.set_roi` method sets the camera ROI and automatically adjusts the frame grabber ROI to match. + - :class:`.PhotonFocus.PhotonFocusIMAQCamera` supports all of :class:`.IMAQ.IMAQCamera` features, such as trigger control and fast buffer acquisition. Some methods have been modified to make them more convenient: e.g., :meth:`.PhotonFocusIMAQCamera.set_roi` method sets the camera ROI and automatically adjusts the frame grabber ROI to match. + - Same is true for :class:`.PhotonFocus.PhotonFocusSiSoCamera`, which, e.g., provides access to all of the frame grabber variables. - The camera supports a status line, which replaces the bottom one or two rows of the frame with the encoded frame-related data such as frame number and timestamp. You can use :func:`.PhotonFocus.get_status_lines` function to identify and extract the data in the status lines from the supplied frames. In addition, you can use :func:`.PhotonFocus.remove_status_line` to remove the status lines in several possible ways: zeroing out, masking with the previous frame, cutting off entirely, etc. \ No newline at end of file diff --git a/docs/devices/SiliconSoftware.rst b/docs/devices/SiliconSoftware.rst index 02e90b6..4e69d08 100644 --- a/docs/devices/SiliconSoftware.rst +++ b/docs/devices/SiliconSoftware.rst @@ -8,12 +8,12 @@ Silicon Software frame grabbers interface Silicon Software produces a range of frame grabbers, which can be used to control different cameras with a CameraLink interface. It has been tested with microEnable IV AD4-CL frame grabber together with PhotonFocus MV-D1024E camera. -The code is located in :mod:`pylablib.devices.SiliconSoftware`, and the main camera class is :class:`pylablib.devices.SiliconSoftware.SiliconSoftwareCamera<.SiliconSoftware.SiliconSoftwareCamera>`. +The code is located in :mod:`pylablib.devices.SiliconSoftware`, and the main camera class is :class:`pylablib.devices.SiliconSoftware.SiliconSoftwareCamera<.fgrab.SiliconSoftwareCamera>`. Software requirements ----------------------- -This interfaces requires ``fglib5.dll``, which is installed with the freely available `Silicon Software Runtime Environment `__ (the newest version for 64-bit Windows is `5.7.0 `__), which also includes all the necessary drivers. After installation, the path to the DLL (located by default in ``SiliconSoftware/Runtime5.7.0/bin`` folder in ``Program Files``) is automatically added to system ``PATH`` variable, which is one of the places where pylablib looks for it by default. If the DLL is located elsewhere, the path can be specified using the library parameter ``devices/dlls/sisofgrab``:: +This interfaces requires ``fglib5.dll``, which is installed with the freely available (upon registration) `Silicon Software Runtime Environment `__ (the newest version for 64-bit Windows is `5.7.0 `__), which also includes all the necessary drivers. After installation, the path to the DLL (located by default in ``SiliconSoftware/Runtime5.7.0/bin`` folder in ``Program Files``) is automatically added to system ``PATH`` variable, which is one of the places where pylablib looks for it by default. If the DLL is located elsewhere, the path can be specified using the library parameter ``devices/dlls/sisofgrab``:: import pylablib as pll pll.par["devices/dlls/sisofgrab"] = "path/to/dlls" @@ -24,7 +24,7 @@ This interfaces requires ``fglib5.dll``, which is installed with the freely avai Connection ----------------------- -Figuring out the connection parameters is a multi-stage process. First, one must identify one of several boards. The boards can be identified using :func:`.SiliconSoftware.list_boards` function. Second, one must select an applet. These provide different board readout modes and, for Advanced Applets, various post-processing capabilities. These applets can be identified using :func:`.SiliconSoftware.list_applets` method, or directly from the Silicon Software RT microDisplay software supplied with the runtime. The choice depends on the color mode (color vs. grayscale and different bitness), readout mode (area or line), can camera connection (single, double, or quad). Finally, depending on the board and the camera connection, one of several ports must be selected. For example, if the frame grabber has two connectors, but the camera only uses a single interface, then the double camera applet (e.g., ``DualAreaGray16``) must be selected, and the port should specify the board connector (0 for ``A``, 1 for ``B``):: +Figuring out the connection parameters is a multi-stage process. First, one must identify one of several boards. The boards can be identified using :func:`.SiliconSoftware.list_boards<.fgrab.list_boards>` function. Second, one must select an applet. These provide different board readout modes and, for Advanced Applets, various post-processing capabilities. These applets can be identified using :func:`.SiliconSoftware.list_applets<.fgrab.list_applets>` method, or directly from the Silicon Software RT microDisplay software supplied with the runtime. The choice depends on the color mode (color vs. gray-scale and different bitness), readout mode (area or line), can camera connection (single, double, or quad). Finally, depending on the board and the camera connection, one of several ports must be selected. For example, if the frame grabber has two connectors, but the camera only uses a single interface, then the double camera applet (e.g., ``DualAreaGray16``) must be selected, and the port should specify the board connector (0 for ``A``, 1 for ``B``):: >> from pylablib.devices import SiliconSoftware >> SiliconSoftware.list_boards() # first list the connected boards @@ -57,7 +57,7 @@ The SDK also provides a universal interface for getting and setting various :ref To see all available attributes, you can call :meth:`.SiliconSoftwareCamera.get_all_grabber_attributes` to get a dictionary with attribute objects, and :meth:`.SiliconSoftwareCamera.get_all_grabber_attribute_values` to get the dictionary of attribute values. The attribute objects provide additional information: attribute kind (integer, string, etc.), range (either numerical range, or selection of values for enum attributes), description string, etc.:: - >> cam = IMAQdx.IMAQdxCamera() + >> cam = SiliconSoftware.SiliconSoftwareCamera() >> attr = cam.get_grabber_attribute("BITALIGNMENT") >> attr.values {1: 'FG_LEFT_ALIGNED', 0: 'FG_RIGHT_ALIGNED'} @@ -72,12 +72,12 @@ At high frame rates (above ~10kFPS) dealing with each frame individually becomes This option can be accessed by supplying ``fastbuff=True`` in :meth:`.SiliconSoftwareCamera.read_multiple_images`. In this case, instead of a list of individual frames (which is the standard behavior), the method returns list of chunks about 1Mb in size, which contain several consecutive frames. Otherwise the method behaves identically to the standard one. -Communication with the camera and camera files +Communication with the camera -------------------------------------------------- -The frame grabber needs some basic information about the camera (sensor size, bit depth, timeouts, aux lines mapping), which are contained in the camera files. These files can be assigned to cameras in the NI MAX, and are usually supplied by NI or by the camera manufacturer. In addition, NI MAX allows one to adjust some settings within these files, which are read-only within the NI IMAQ software. These include frame timeout and camera bit depth. +The frame grabber needs some basic information about the camera (sensor size, bit depth, timeouts, aux lines mapping), which can be set up using the grabber attributes. The most important transfer parameters are the number of taps and the bitness of the transferred data, which can be set up using :meth:`.SiliconSoftwareCamera.setup_camlink_pixel_format`. The values for this parameters can usually be obtained from the camera manuals. -The communication with the camera itself greatly varies between different cameras. Some will have additional connection to control the parameters. However, most use serial communication built into the CameraLink interface. This communication can be set up with :meth:`.IMAQCamera.setup_serial_params` and used via :meth:`.IMAQCamera.serial_read` and :meth:`.IMAQCamera.serial_write`. The communication protocols are camera-dependent. Yet some other cameras (e.g., Photon Focus) use proprietary communication protocol. In this case, the provide their own DLLs, which independently use NI-provided DLLs for serial communication (most notably, ``clallserial.dll``). In this case, one needs to maintain two independent connections: one directly to the NI frame grabber to obtain the frame data, and one to the manufacturer library to control the camera. This is the way it is implemented in PhotonFocus camera interface. +.. The communication with the camera itself greatly varies between different cameras. Some will have additional connection to control the parameters. However, most use serial communication built into the CameraLink interface. This communication can be set up with :meth:`.SiliconSoftwareCamera.setup_serial_params` and used via :meth:`.SiliconSoftwareCamera.serial_read` and :meth:`.IMAQCamera.serial_write`. The communication protocols are camera-dependent. Yet some other cameras (e.g., Photon Focus) use proprietary communication protocol. In this case, the provide their own DLLs, which independently use NI-provided DLLs for serial communication (most notably, ``clallserial.dll``). In this case, one needs to maintain two independent connections: one directly to the NI frame grabber to obtain the frame data, and one to the manufacturer library to control the camera. This is the way it is implemented in PhotonFocus camera interface. Known issues diff --git a/docs/devices/cameras_root.rst b/docs/devices/cameras_root.rst index 99cde5a..58b9d72 100644 --- a/docs/devices/cameras_root.rst +++ b/docs/devices/cameras_root.rst @@ -21,5 +21,6 @@ Currently supported cameras: IMAQdx PhotonFocus PCO_SC2 + SiliconSoftware Thorlabs_TLCamera uc480 \ No newline at end of file diff --git a/pylablib/devices/PhotonFocus/__init__.py b/pylablib/devices/PhotonFocus/__init__.py index bdb5391..e195743 100644 --- a/pylablib/devices/PhotonFocus/__init__.py +++ b/pylablib/devices/PhotonFocus/__init__.py @@ -1,4 +1,4 @@ from . import PhotonFocus -from .PhotonFocus import query_camera_name, list_cameras, get_cameras_number, PhotonFocusIMAQCamera, PhotonFocusSiSoCamera +from .PhotonFocus import query_camera_name, list_cameras, get_cameras_number, IPhotonFocusCamera, PhotonFocusIMAQCamera, PhotonFocusSiSoCamera from .PhotonFocus import PFCamError from .PhotonFocus import get_status_lines, get_status_line_position, remove_status_line, find_skipped_frames \ No newline at end of file diff --git a/setup.py b/setup.py index 7f964d8..ebfae1e 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,8 @@ setup( name='pylablib', # name='pylablib-lightweight', - version='1.0.0', - description='Collection of Python code for using in lab environment: experiment automation, data acquisition, device communication', + version='1.1.0', + description='Code for use in lab environment: experiment automation, data acquisition, device communication', long_description=long_description, long_description_content_type="text/x-rst", url='https://github.com/AlexShkarin/pyLabLib',