diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py index 5650707c3de8ce1629ba0560bdcbc7d758114d1f..60f00fae7c877810b082890712599cb24f7ea12d 100644 --- a/src/calng/CalibrationManager.py +++ b/src/calng/CalibrationManager.py @@ -1318,6 +1318,7 @@ class CalibrationManager(DeviceClientBase, Device): await gather(*awaitables) awaitables.clear() + callNoWait(self.geometryDevice.value, "sendGeometry") self._set_status('All devices instantiated') self.state = State.ACTIVE diff --git a/src/calng/DetectorAssembler.py b/src/calng/DetectorAssembler.py index 899dc9b1e45fd3000e077ab840f70f9a9d03cd2c..8a8c9e9be22cfe66c8fffa13f61bcb70b4029798 100644 --- a/src/calng/DetectorAssembler.py +++ b/src/calng/DetectorAssembler.py @@ -109,6 +109,7 @@ class DetectorAssembler(TrainMatcher.TrainMatcher): def __init__(self, conf): super().__init__(conf) self.info.merge(Hash("timeOfFlight", 0)) + self.registerSlot(self.slotReceiveGeometry) def initialization(self): super().initialization() @@ -122,22 +123,12 @@ class DetectorAssembler(TrainMatcher.TrainMatcher): self.KARABO_SLOT(self.requestScene) - geometry_device = self.get("geometryDevice") - client = self.remote() - try: - initial_geometry = client.get(geometry_device, "serializedGeometry") - except RuntimeError: - self.log.WARN( - "Failed to get initial geometry, maybe geometry device is down" - ) - else: - self.log.INFO("Got geometry immediately after init :D") - self._receive_geometry( - geometry_device, - Hash("pickledGeometry", initial_geometry), - ) - - self.remote().registerDeviceMonitor(geometry_device, self._receive_geometry) + self.signalSlotable.connect( + self.get("geometryDevice"), + "signalNewGeometry", + "", # slot device ID (default: self) + "slotReceiveGeometry", + ) self.assembled_output = self.signalSlotable.getOutputChannel("assembledOutput") self.start() @@ -168,15 +159,12 @@ class DetectorAssembler(TrainMatcher.TrainMatcher): ) ) - def _receive_geometry(self, device_id, config): - if not config.has("serializedGeometry"): - return - self.log.INFO(f"Found geometry on {device_id}") - serialized_geometry = config["serializedGeometry"] - if len(serialized_geometry) == 0: - self.log.INFO("New geometry empty, will ignore update.") - return - self._geometry = geom_utils.deserialize_geometry(serialized_geometry) + def slotReceiveGeometry(self, device_id, serialized_geometry): + self.log.INFO(f"Received geometry from {device_id}") + try: + self._geometry = geom_utils.deserialize_geometry(serialized_geometry) + except Exception as e: + self.log.WARN(f"Failed to deserialize geometry; {e}") # TODO: allow multiple memory cells (extra geom notion of extra dimensions) self._stack_input_buffer = np.ma.masked_array( data=np.zeros(self._geometry.expected_data_shape, dtype=np.float32), diff --git a/src/calng/RoiTool.py b/src/calng/RoiTool.py index 8f8f47886d2d6e869befd67c40b0ca8b13b0fe98..4618aebe91af57eaebfdd08c88bb3cfa506b1565 100644 --- a/src/calng/RoiTool.py +++ b/src/calng/RoiTool.py @@ -26,6 +26,7 @@ from karabo.middlelayer import ( VectorDouble, VectorInt32, VectorString, + get_property, slot, ) @@ -168,10 +169,10 @@ class RoiTool(Device): async def imageInput(self, data, meta): # TODO: handle streams without explicit mask? image = np.ma.masked_array( - data=rec_getattr(data, self.imageDataPath.value).astype( + data=get_property(data, self.imageDataPath.value).astype( np.float32, copy=False ), - mask=rec_getattr(data, self.maskPath.value).astype(np.uint8, copy=False), + mask=get_property(data, self.maskPath.value).astype(np.uint8, copy=False), ) # TODO: make handling of extra dimension(s) configurable @@ -295,13 +296,6 @@ class RoiTool(Device): return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload) -def rec_getattr(obj, path): - res = obj - for part in path.split("."): - res = getattr(res, part) - return res - - def _histogram_plot_helper(counts, bin_edges): x = np.zeros(bin_edges.size * 2) y = np.zeros_like(x) diff --git a/src/calng/base_condition.py b/src/calng/base_condition.py index de9dc177d598eeb1c36dcb2bfd9ea397e9bb23bd..96614475fe157e8b742c92cdca4043f1ea54dd53 100644 --- a/src/calng/base_condition.py +++ b/src/calng/base_condition.py @@ -19,11 +19,12 @@ from karabo.middlelayer import ( connectDevice, disconnectDevice, getConfiguration, + get_property, slot, waitUntilNew, ) from ._version import version as deviceVersion -from . import scenes, utils +from . import scenes class PipelineOperationMode(enum.Enum): @@ -134,7 +135,7 @@ class ConditionBase(Device): while True: await waitUntilNew( *( - utils.rec_getattr(control_devs[control_id], control_key) + get_property(control_devs[control_id], control_key) for control_id, v in self.keys_to_get.items() for control_key, *_ in v # "v" for "variable naming is hard" ) @@ -225,7 +226,7 @@ class ConditionBase(Device): unchecked_parameter, "", "", - utils.rec_getattr( + get_property( self._manager_parameters_node, unchecked_parameter ).value, State.IGNORING.value, @@ -251,7 +252,7 @@ class ConditionBase(Device): if isinstance(control_dev, Proxy): # device proxy via connectDevice try: - control_value = utils.rec_getattr(control_dev, control_key).value + control_value = get_property(control_dev, control_key).value except AttributeError: control_value = "key not found" could_look_up = False @@ -277,7 +278,7 @@ class ConditionBase(Device): ideal_value = "control key not found" try: - manager_value = utils.rec_getattr( + manager_value = get_property( self._manager_parameters_node, manager_key ).value except AttributeError: diff --git a/src/calng/base_geometry.py b/src/calng/base_geometry.py index 3489a90c683e24635676be7dc2913a85781b8839..e300162bed53a32e7521b0ec03ef1205ceb9c3d9 100644 --- a/src/calng/base_geometry.py +++ b/src/calng/base_geometry.py @@ -18,13 +18,14 @@ from karabo.middlelayer import ( ImageData, Int32, Node, + Signal, Slot, State, String, Unit, VectorHash, VectorString, - get_instance_parent, + sleep, slot, ) from matplotlib.backends.backend_agg import FigureCanvasAgg @@ -69,21 +70,13 @@ def make_x_y_offset_node(): ) -def get_my_device(me): - parent = me - while not isinstance(parent, Device): - new_parent = get_instance_parent(parent) - assert new_parent is not parent, "Circular parent reference" - parent = new_parent - return parent - - # TODO: consider other history models (could be fun) class TweakGeometryNode(Configurable): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._undo_stack = [] self._redo_stack = [] + self._device = self.get_root() def _reset(self): # clear history when new geometry is set from manual / file @@ -95,18 +88,16 @@ class TweakGeometryNode(Configurable): @Slot(displayedName="Undo") async def undo(self): assert len(self._undo_stack) > 0 - parent = get_my_device(self) - self._redo_stack.append(parent.geometry) - await parent._set_geometry(self._undo_stack.pop()) + self._redo_stack.append(self._device.geometry) + await self._device._set_geometry(self._undo_stack.pop()) self.undoLength = len(self._undo_stack) self.redoLength = len(self._redo_stack) @Slot(displayedName="Redo") async def redo(self): - parent = get_my_device(self) assert len(self._redo_stack) > 0 - self._undo_stack.append(parent.geometry) - await parent._set_geometry(self._redo_stack.pop()) + self._undo_stack.append(self._device.geometry) + await self._device._set_geometry(self._redo_stack.pop()) self.undoLength = len(self._undo_stack) self.redoLength = len(self._redo_stack) @@ -114,14 +105,13 @@ class TweakGeometryNode(Configurable): @Slot(displayedName="Add offset") async def add(self): - parent = get_my_device(self) - current_geometry = parent.geometry + current_geometry = self._device.geometry new_geometry = current_geometry.offset( (self.offset.x.value, self.offset.y.value) ) self._undo_stack.append(current_geometry) self.undoLength = len(self._undo_stack) - await parent._set_geometry(new_geometry) + await self._device._set_geometry(new_geometry) if self._redo_stack: self._redo_stack.clear() self.redoLength = 0 @@ -147,7 +137,7 @@ class BaseManualGeometryConfigNode(Configurable): allowedStates=[State.ACTIVE], ) async def setManual(self): - await get_my_device(self)._set_from_manual_config() + await self.get_root()._set_from_manual_config() class GeometryFileNode(Configurable): @@ -188,13 +178,13 @@ class GeometryFileNode(Configurable): allowedStates=[State.ACTIVE], ) async def loadFromFile(self): - await get_my_device(self)._load_from_file() + await self.get_root()._load_from_file() class ManualGeometryBase(Device): __version__ = deviceVersion geometry_class = None # subclass must set - # subclass must add slot manualConfigsetManual + # subclass must add slot setManual availableScenes = VectorString( displayedName="Available scenes", @@ -207,11 +197,7 @@ class ManualGeometryBase(Device): daqPolicy=DaqPolicy.OMIT, ) - serializedGeometry = String( - displayedName="Serialized geometry", - defaultValue="", - accessMode=AccessMode.READONLY, - ) + signalNewGeometry = Signal(String(), String()) geometryPreview = Image( ImageData(np.empty(shape=(0, 0, 0), dtype=np.uint32)), @@ -231,6 +217,19 @@ class ManualGeometryBase(Device): displayedName="Tweak geometry", ) + @Slot( + displayedName="Send geometry", + allowedStates=[State.ACTIVE], + description="Will send 'signalNewGeometry' to connected slots. These will for " + "example be DetectorAssembler. Note that signal is sent automatically when new " + "geometry is set - this slot is mostly to be called by manager after " + "(re)starting assemblers while geometry device is still up." + ) + async def sendGeometry(self): + self.signalNewGeometry( + self.deviceId, geom_utils.serialize_geometry(self.geometry) + ) + @Slot( displayedName="Update preview", allowedStates=[State.ACTIVE], @@ -304,7 +303,7 @@ class ManualGeometryBase(Device): async def _set_geometry(self, geometry, update_preview=True): self.geometry = geometry - self.serializedGeometry = geom_utils.serialize_geometry(geometry) + await self.sendGeometry() if update_preview: await self.updatePreview() @@ -314,6 +313,8 @@ class ManualGeometryBase(Device): async def onInitialization(self): self.state = State.INIT + self.log.INFO("Waiting a second to let slots connect to signal") + await sleep(1) if self.geometryFile.filePath.value and await self._load_from_file(): ... else: diff --git a/src/calng/utils.py b/src/calng/utils.py index 48e98452261ab7c4eec8a5057a0490ebfbd77740..61e632a98c0eb3109c62f228ddeb68f4365bc685 100644 --- a/src/calng/utils.py +++ b/src/calng/utils.py @@ -78,13 +78,6 @@ class PreviewIndexSelectionMode(enum.Enum): PULSE = "pulse" -def rec_getattr(obj, path): - res = obj - for part in path.split("."): - res = getattr(res, part) - return res - - def pick_frame_index(selection_mode, index, cell_table, pulse_table): """When selecting a single frame to preview, an obvious question is whether the number the operator provides is a frame index, a cell ID, or a pulse ID. This