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