diff --git a/DEPENDS b/DEPENDS
index 7d43690b3c115ea60f482031806a6dc36a6eef3b..b18c86bacb67525e328f47a25aab32e930d0ac6d 100644
--- a/DEPENDS
+++ b/DEPENDS
@@ -1,3 +1,3 @@
 TrainMatcher, 2.3.2-2.16.2
-calngDeps, 1.0.0-2.16.2
+calngDeps, 1.0.1-2.16.2
 calibrationClient, 11.0.0
diff --git a/setup.py b/setup.py
index 3b61604a455dc6afe5f1f63bd0fdf8c39594a143..92c44ccce81784e436beb57334c62c6be20b7307 100644
--- a/setup.py
+++ b/setup.py
@@ -32,10 +32,12 @@ setup(name='calng',
               'JungfrauCorrection = calng.corrections.JungfrauCorrection:JungfrauCorrection',
               'PnccdCorrection = calng.corrections.PnccdCorrection:PnccdCorrection',
               'LpdCorrection = calng.corrections.LpdCorrection:LpdCorrection',
+              'LpdminiCorrection = calng.corrections.LpdminiCorrection:LpdminiCorrection',
               'ShmemToZMQ = calng.ShmemToZMQ:ShmemToZMQ',
               'ShmemTrainMatcher = calng.ShmemTrainMatcher:ShmemTrainMatcher',
               'DetectorAssembler = calng.DetectorAssembler:DetectorAssembler',
               'Gotthard2Assembler = calng.Gotthard2Assembler:Gotthard2Assembler',
+              'LpdminiSplitter = calng.LpdminiSplitter:LpdminiSplitter',
           ],
 
           'karabo.middlelayer_device': [
@@ -44,10 +46,12 @@ setup(name='calng',
               'JungfrauCondition = calng.conditions.JungfrauCondition:JungfrauCondition',
               'LpdCondition = calng.conditions.LpdCondition:LpdCondition',
               'Agipd1MGeometry = calng.geometries.Agipd1MGeometry:Agipd1MGeometry',
+              'Agipd500KGeometry = calng.geometries.Agipd500KGeometry:Agipd500KGeometry',
               'Dssc1MGeometry = calng.geometries:Dssc1MGeometry.Dssc1MGeometry',
               'Epix100Geometry = calng.geometries:Epix100Geometry.Epix100Geometry',
               'JungfrauGeometry = calng.geometries:JungfrauGeometry.JungfrauGeometry',
               'Lpd1MGeometry = calng.geometries:Lpd1MGeometry.Lpd1MGeometry',
+              'LpdminiGeometry = calng.geometries:LpdminiGeometry.LpdminiGeometry',
               'PnccdGeometry = calng.geometries:PnccdGeometry.PnccdGeometry',
               'RoiTool = calng.RoiTool:RoiTool',
           ],
@@ -86,6 +90,12 @@ setup(name='calng',
                   extra_link_args=['-fopenmp'],
                   language="c++",
               ),
+              Extension(
+                  'calng.kernels.lpd_cython',
+                  ['src/calng/kernels/lpd_cpu.pyx'],
+                  extra_compile_args=['-O3', '-march=native', '-fopenmp'],
+                  extra_link_args=['-fopenmp'],
+              ),
           ],
           compiler_directives={
               'language_level': 3
diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py
index 2206ade34015937e2856678dda6ce2166a39407d..60f00fae7c877810b082890712599cb24f7ea12d 100644
--- a/src/calng/CalibrationManager.py
+++ b/src/calng/CalibrationManager.py
@@ -1135,8 +1135,11 @@ class CalibrationManager(DeviceClientBase, Device):
         if key_patterns:
             try:
                 # Try to obtain most recent configuration.
-                old_config = await getConfigurationFromPast(
-                    device_id, datetime.now().isoformat())
+                old_config = await wait_for(getConfigurationFromPast(
+                    device_id, datetime.now().isoformat()), 15.0)
+            except AsyncTimeoutError:
+                self.logger.warn(f'Timeout receiving previous configuration '
+                                 f'for {device_id}')
             except KaraboError as e:
                 self.logger.warn(f'Failed receiving previous configuration '
                                  f'for {device_id}: {e}')
@@ -1315,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/LpdminiSplitter.py b/src/calng/LpdminiSplitter.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed4c78a6086f1370716e9a0c2f402b1f3429a2fd
--- /dev/null
+++ b/src/calng/LpdminiSplitter.py
@@ -0,0 +1,242 @@
+from timeit import default_timer
+
+import numpy as np
+from karabo.bound import (
+    DOUBLE_ELEMENT,
+    INPUT_CHANNEL,
+    KARABO_CLASSINFO,
+    NODE_ELEMENT,
+    OUTPUT_CHANNEL,
+    OVERWRITE_ELEMENT,
+    UINT64_ELEMENT,
+    VECTOR_UINT32_ELEMENT,
+    VECTOR_STRING_ELEMENT,
+    ChannelMetaData,
+    Hash,
+    MetricPrefix,
+    PythonDevice,
+    State,
+    Timestamp,
+    Unit,
+)
+from karabo.common.api import KARABO_SCHEMA_DISPLAY_TYPE_SCENES as DT_SCENES
+
+from . import scenes, schemas, shmem_utils, utils
+from .base_correction import WarningLampType
+from ._version import version as deviceVersion
+
+# plan:
+# - split data into 8 modules
+# - copy each into shared memory and send on distinct channel
+
+PROCESSING_STATE_TIMEOUT = 10
+
+
+@KARABO_CLASSINFO("LpdminiSplitter", deviceVersion)
+class LpdminiSplitter(PythonDevice):
+    # up to 8 minis, using 1-indexing for naming
+    _output_channel_names = [f"output-{i+1}" for i in range(8)]
+
+    @staticmethod
+    def expectedParameters(expected):
+        (
+            OVERWRITE_ELEMENT(expected)
+            .key("state")
+            .setNewDefaultValue(State.INIT)
+            .commit(),
+
+            INPUT_CHANNEL(expected)
+            .key("input")
+            .commit(),
+
+            VECTOR_UINT32_ELEMENT(expected)
+            .key("outputDataShape")
+            .displayedName("Output data shape")
+            .readOnly()
+            .initialValue([512, 32, 256])
+            .commit(),
+
+            UINT64_ELEMENT(expected)
+            .key("trainId")
+            .displayedName("Train ID")
+            .description("ID of latest train processed by this device.")
+            .readOnly()
+            .initialValue(0)
+            .commit(),
+
+            NODE_ELEMENT(expected)
+            .key("performance")
+            .commit(),
+
+            DOUBLE_ELEMENT(expected)
+            .key("performance.rate")
+            .displayedName("Rate")
+            .description(
+                "Actual rate with which this device gets, processes, and sends trains. "
+                "This is a simple windowed moving average."
+            )
+            .unit(Unit.HERTZ)
+            .readOnly()
+            .initialValue(0)
+            .commit(),
+
+            DOUBLE_ELEMENT(expected)
+            .key("performance.processingTime")
+            .displayedName("Processing time")
+            .unit(Unit.SECOND)
+            .metricPrefix(MetricPrefix.MILLI)
+            .readOnly()
+            .initialValue(0)
+            .warnHigh(100)
+            .info("Processing too slow to reach 10 Hz")
+            .needsAcknowledging(False)
+            .commit(),
+
+            VECTOR_STRING_ELEMENT(expected)
+            .key("availableScenes")
+            .setSpecialDisplayType(DT_SCENES)
+            .readOnly()
+            .initialValue(["overview"])
+            .commit(),
+        )
+
+        for channel_name in LpdminiSplitter._output_channel_names:
+            (
+                OUTPUT_CHANNEL(expected)
+                .key(channel_name)
+                .dataSchema(schemas.xtdf_output_schema())
+                .commit(),
+            )
+
+    def __init__(self, config):
+        super().__init__(config)
+        self.registerInitialFunction(self._initialization)
+        self._output_image_shape = (512, 32, 256)  # will be updated based on frames
+        self.KARABO_SLOT(self.requestScene)
+        self._latest_log_message = None
+        self._latest_warn_type = None
+
+    def _initialization(self):
+        self.KARABO_ON_DATA("input", self.input_handler)
+        self._shmem_buffers = [
+            shmem_utils.ShmemCircularBuffer(
+                1 * 2**30,
+                (512, 32, 256),
+                np.uint16,
+                f"{self.getInstanceId()}:{channel_name}"
+            )
+            for channel_name in self._output_channel_names
+        ]
+
+        # performance measures and such
+        self._last_processing_started = 0  # used for processing time and timeout
+        self._buffered_status_update = Hash()
+        self._processing_time_tracker = utils.ExponentialMovingAverage(alpha=0.3)
+        self._rate_tracker = utils.WindowRateTracker()
+        self._input_delay_tracker = utils.ExponentialMovingAverage(alpha=0.3)
+        self._performance_measure_update_timer = utils.RepeatingTimer(
+            interval=1,
+            callback=self._update_performance_measures,
+        )
+        self.updateState(State.ON)
+
+    def input_handler(self, data_hash, metadata):
+        state = self.get("state")
+        if state is State.INIT:
+            return
+
+        # TODO: handle empty DAQ hash
+        if not data_hash.has("image.data"):
+            self.log_status_info("Input hash had no image data node")
+            if state is State.PROCESSING:
+                self.updateState(State.IGNORING)
+            return
+        try:
+            image_data = np.asarray(
+                data_hash.get("image.data")
+            )
+        except RuntimeError as err:
+            self.log_status_info(
+                f"Failed to load image data; probably empty hash from DAQ: {err}",
+                WarningLampType.EMPTY_HASH,
+            )
+            if state is State.PROCESSING:
+                self.updateState(State.IGNORING)
+            return
+
+        if state is not State.PROCESSING:
+            self.log_status_info("Processing data")
+            self.updateState(State.PROCESSING)
+
+        self._last_processing_started = default_timer()
+        timestamp = Timestamp.fromHashAttributes(metadata.getAttributes("timestamp"))
+        self._buffered_status_update.set("trainId", timestamp.getTrainId())
+        num_frames = data_hash["image.cellId"].size
+        image_data = image_data.reshape((num_frames, 256, 256))
+        # note: reshape is equivalent to overrideInputAxisOrder in BaseCorrection
+        if num_frames != self._output_image_shape:
+            self._output_image_shape = (num_frames, 32, 256)
+            for shmem_buffer in self._shmem_buffers:
+                shmem_buffer.change_shape(self._output_image_shape)
+            self.set("outputDataShape", list(self._output_image_shape))
+        # reshape to drop singleton axis + optionally fix DAQ shape shenanigans
+        receiver_source_name = metadata.get("source")
+        for i, (channel_name, shmem_buffer) in enumerate(
+            zip(self._output_channel_names, self._shmem_buffers)
+        ):
+            this_slice = image_data[:, i * 32 : (i + 1) * 32]
+            buffer_handle, buffer_array = shmem_buffer.next_slot()
+            buffer_array[:] = this_slice
+            data_hash.set("image.data", buffer_handle)
+            data_hash.set("calngShmemPaths", ["image.data"])
+            # TODO: consider source name; virtual indices like in offline?
+            channel = self.signalSlotable.getOutputChannel(channel_name)
+            channel.write(
+                data_hash,
+                ChannelMetaData(f"{receiver_source_name}-{i+1}", timestamp),
+                # adding 1-indexed suffix index to soruce name forwarded
+                copyAllData=False,
+            )
+            channel.update()
+        self._processing_time_tracker.update(
+            default_timer() - self._last_processing_started
+        )
+        self._rate_tracker.update()
+
+    def _update_performance_measures(self):
+        if self.get("state") in {State.PROCESSING, State.IGNORING}:
+            self._buffered_status_update.set(
+                "performance.rate", self._rate_tracker.get()
+            )
+            self._buffered_status_update.set(
+                "performance.processingTime", self._processing_time_tracker.get() * 1000
+            )
+            self.set(self._buffered_status_update)
+            if (
+                default_timer() - self._last_processing_started
+                > PROCESSING_STATE_TIMEOUT
+            ):
+                self.updateState(State.ON)
+
+    def requestScene(self, params):
+        payload = Hash()
+        payload["name"] = "overview"
+        payload["data"] = scenes.lpdmini_splitter_overview(
+            device_id=self.getInstanceId(),
+            schema=self.getFullSchema(),
+        )
+        payload["success"] = True
+        response = Hash()
+        response["type"] = "deviceScene"
+        response["origin"] = self.getInstanceId()
+        response["payload"] = payload
+        self.reply(response)
+
+    def log_status_info(self, msg, warn_type=None):
+        if (msg != self._latest_log_message) and (
+            warn_type is None or warn_type != self._latest_warn_type
+        ):
+            self.log.INFO(msg)
+            self.set("status", msg)
+            self._latest_log_message = msg
+            self._latest_warn_type = warn_type
diff --git a/src/calng/RoiTool.py b/src/calng/RoiTool.py
index a271f4bd9ea1cc40011107f4ca8ae127b9af5312..4f772c96f6423a95fa9bd723678684bfc2fa29db 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,
 )
 
@@ -167,10 +168,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
@@ -294,13 +295,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_correction.py b/src/calng/base_correction.py
index cc359a3f90b5e719fe8a8a49d0fb5d6829b94732..a205471d3d586ef378ab95554c77825ec1e2a621 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -723,16 +723,7 @@ class BaseCorrection(PythonDevice):
         self._update_correction_flags()
         self._update_frame_filter()
 
-        self._buffered_status_update = Hash(
-            "trainId",
-            0,
-            "performance.rate",
-            0,
-            "performance.processingTime",
-            0,
-            "performance.ratioOfRecentTrainsReceived",
-            0,
-        )
+        self._buffered_status_update = Hash()
         self._processing_time_tracker = utils.ExponentialMovingAverage(alpha=0.3)
         self._rate_tracker = utils.WindowRateTracker()
         self._input_delay_tracker = utils.ExponentialMovingAverage(alpha=0.3)
@@ -1005,6 +996,7 @@ class BaseCorrection(PythonDevice):
                 self.output_data_dtype,
                 shmem_buffer_name,
             )
+            self._shmem_receiver = shmem_utils.ShmemCircularBufferReceiver()
             if self._cuda_pin_buffers:
                 self.log.INFO("Trying to pin the shmem buffer memory")
                 self._shmem_buffer.cuda_pin()
@@ -1092,6 +1084,7 @@ class BaseCorrection(PythonDevice):
                 WarningLampType.EMPTY_HASH,
                 only_print_once=True,
             ) as warn:
+                self._shmem_receiver.dereference_shmem_handles(data_hash)
                 try:
                     image_data = np.asarray(data_hash.get(self._image_data_path))
                     cell_table = (
diff --git a/src/calng/base_geometry.py b/src/calng/base_geometry.py
index fcd24329a8e3c5e3c468d5494c8773eef0466760..f7c70e387e2f3f3a4c8bd5ba74bdbbc5d0e0e807 100644
--- a/src/calng/base_geometry.py
+++ b/src/calng/base_geometry.py
@@ -19,12 +19,14 @@ from karabo.middlelayer import (
     ImageData,
     Int32,
     Node,
+    Signal,
     Slot,
     State,
     String,
     Unit,
     VectorHash,
     VectorString,
+    sleep,
     slot,
 )
 from matplotlib.backends.backend_agg import FigureCanvasAgg
@@ -123,6 +125,7 @@ class TweakGeometryNode(Configurable):
         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
@@ -134,18 +137,16 @@ class TweakGeometryNode(Configurable):
     @Slot(displayedName="Undo")
     async def undo(self):
         assert len(self._undo_stack) > 0
-        parent = self.get_root()
-        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 = self.get_root()
         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)
 
@@ -153,14 +154,13 @@ class TweakGeometryNode(Configurable):
 
     @Slot(displayedName="Add offset")
     async def add(self):
-        parent = self.get_root()
-        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
@@ -233,7 +233,7 @@ class GeometryFileNode(Configurable):
 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",
@@ -246,11 +246,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)),
@@ -270,6 +266,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],
@@ -343,7 +352,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()
 
@@ -353,6 +362,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:
@@ -537,7 +548,7 @@ class ManualQuadrantsGeometryBase(ManualGeometryBase):
         self.quadrantCorners.offset.y = 0
 
 
-class ModuleListItem(Configurable):
+class OrientableModuleListItem(Configurable):
     posX = Double(
         assignment=Assignment.OPTIONAL,
         defaultValue=0,
@@ -550,20 +561,32 @@ class ModuleListItem(Configurable):
     orientY = Int32(assignment=Assignment.OPTIONAL, defaultValue=1)
 
 
-def make_manual_module_list_node(defaults):
-    class ManualModuleListNode(BaseManualGeometryConfigNode):
+class RotatableModuleListItem(Configurable):
+    posX = Double(
+        assignment=Assignment.OPTIONAL,
+        defaultValue=0,
+    )
+    posY = Double(
+        assignment=Assignment.OPTIONAL,
+        defaultValue=0,
+    )
+    rotate = Int32(assignment=Assignment.OPTIONAL, defaultValue=1)
+
+
+def make_manual_orientable_module_list_node(defaults):
+    class ManualOrientableModuleListNode(BaseManualGeometryConfigNode):
         modules = VectorHash(
             displayedName="Modules",
-            rows=ModuleListItem,
+            rows=OrientableModuleListItem,
             defaultValue=defaults,
             accessMode=AccessMode.RECONFIGURABLE,
             assignment=Assignment.OPTIONAL,
         )
 
-    return Node(ManualModuleListNode)
+    return Node(ManualOrientableModuleListNode)
 
 
-class ManualModuleListGeometryBase(ManualGeometryBase):
+class ManualOrientableModuleListGeometryBase(ManualGeometryBase):
     moduleList = None  # subclass must define (with nice defaults)
 
     @slot
@@ -591,3 +614,46 @@ class ManualModuleListGeometryBase(ManualGeometryBase):
             ).offset((self.moduleList.offset.x.value, self.moduleList.offset.y.value))
             await self._set_geometry(geometry)
             self.tweakGeometry._reset()
+
+
+def make_manual_rotatable_module_list_node(defaults):
+    class ManualRotatableModuleListNode(BaseManualGeometryConfigNode):
+        modules = VectorHash(
+            displayedName="Modules",
+            rows=RotatableModuleListItem,
+            defaultValue=defaults,
+            accessMode=AccessMode.RECONFIGURABLE,
+            assignment=Assignment.OPTIONAL,
+        )
+
+    return Node(ManualRotatableModuleListNode)
+
+
+class ManualRotatableModuleListGeometryBase(ManualGeometryBase):
+    moduleList = None  # subclass must define (with nice defaults)
+
+    @slot
+    def requestScene(self, params):
+        name = params.get("name", default="overview")
+        if name == "overview":
+            # Assumes there are correction devices known to manager
+            scene_data = scenes.modules_geometry_overview(
+                self.deviceId,
+                self.getDeviceSchema(),
+            )
+            payload = Hash("success", True, "name", name, "data", scene_data)
+
+        return Hash("type", "deviceScene", "origin", self.deviceId, "payload", payload)
+
+    async def _set_from_manual_config(self):
+        self._set_status("Updating geometry from manual configuration")
+        with self.push_state(State.CHANGING):
+            geometry = self.geometry_class.from_module_positions(
+                [(x, y) for (x, y, _) in self.moduleList.modules.value],
+                [
+                    rotation
+                    for (_, _, rotation) in self.moduleList.modules.value
+                ],
+            ).offset((self.moduleList.offset.x.value, self.moduleList.offset.y.value))
+            await self._set_geometry(geometry)
+            self.tweakGeometry._reset()
diff --git a/src/calng/conditions/JungfrauCondition.py b/src/calng/conditions/JungfrauCondition.py
index 2825a4d82978860f9514050ac805ba070931c040..30963793023deff7b8fa0fd01ada0f221992d36d 100644
--- a/src/calng/conditions/JungfrauCondition.py
+++ b/src/calng/conditions/JungfrauCondition.py
@@ -1,53 +1,29 @@
-import collections
 import operator
 
 from karabo.middlelayer import AccessMode, Assignment, String
 from .. import base_condition
-from ..corrections.JungfrauCorrection import GainModes
+from ..corrections.JungfrauCorrection import GainModes, GainSettings
 
 
-def old_settings_to_gain_mode(setting):
-    gain_mode = GainModes(setting)
-    if gain_mode in (GainModes.FIX_GAIN_1, GainModes.FIX_GAIN_2):
-        return 1
+def gain_mode_translator(gain_mode_string):
+    if gain_mode_string in {"dynamic", "forceswitchg1", "forceswitchg2"}:
+        return GainModes.ADAPTIVE_GAIN.name
+    elif gain_mode_string in {"fixg1", "fixg2"}:
+        return GainModes.FIXED_GAIN.name
     else:
-        return 0
+        raise ValueError(f"Unknown gain mode {gain_mode_string}")
 
 
-def old_settings_to_gain_setting(setting):
-    gain_mode = GainModes(setting)
-    if gain_mode is GainModes.DYNAMIC_GAIN_HG0:
-        return 1
-    else:
-        return 0
-
-
-def new_settings_to_gain_setting(setting):
+def gain_setting_translator(setting):
     if setting == "gain0":
-        return 0
+        return GainSettings.LOW_CDS.name
     elif setting == "highgain0":
-        return 1
+        return GainSettings.HIGH_CDS.name
     else:
         raise ValueError(f"Unknown gain setting {setting}")
 
 
-def new_gain_mode_to_gain_mode(gain_mode):
-    if gain_mode in {"dynamic", "forceswitchg1", "forceswitchg2"}:
-        return 0
-    elif gain_mode in {"fixg1", "fixg2"}:
-        return 1
-    else:
-        raise ValueError(f"Unknown gain_mode {gain_mode}")
-
-
 class JungfrauCondition(base_condition.ConditionBase):
-    detectorFirmwareVersion = String(
-        displayedName="Firmware version",
-        assignment=Assignment.OPTIONAL,
-        accessMode=AccessMode.INITONLY,
-        defaultValue="new",
-        options=["old", "new"],
-    )
     controlDeviceId = String(
         displayedName="Control device ID",
         assignment=Assignment.MANDATORY,
@@ -56,29 +32,17 @@ class JungfrauCondition(base_condition.ConditionBase):
 
     @property
     def keys_to_get(self):
-        key_map = [
-            # cells: 1.0 or 16.0
-            ("storageCells", "memoryCells", lambda n: n + 1),
-            (
-                "exposureTime",
-                "integrationTime",
-                lambda n: n * 1e6,
-            ),
-        ]
-        if self.detectorFirmwareVersion.value == "old":
-            key_map += [
-                # note: control device parameter is a vector
-                ("vHighVoltage", "biasVoltage", operator.itemgetter(0)),
-                # gain mode: omitted or 1.0
-                ("settings", "gainMode", old_settings_to_gain_mode),
-                # gain setting: 0.0 or 1.0 (derived from gain mode on device)
-                ("settings", "gainSetting", old_settings_to_gain_setting),
-            ]
-        else:
-            key_map += [
+        return {
+            self.controlDeviceId.value: [
+                # cells: 1.0 or 16.0
+                ("storageCells", "memoryCells", lambda n: n + 1),
+                (
+                    "exposureTime",
+                    "integrationTime",
+                    lambda n: n * 1e6,
+                ),
                 ("highVoltage", "biasVoltage", operator.itemgetter(0)),
-                ("settings", "gainSetting", new_settings_to_gain_setting),
-                ("gainMode", "gainMode", new_gain_mode_to_gain_mode),
+                ("settings", "gainSetting", gain_setting_translator),
+                ("gainMode", "gainMode", gain_mode_translator),
             ]
-
-        return {self.controlDeviceId.value: key_map}
+        }
diff --git a/src/calng/corrections/JungfrauCorrection.py b/src/calng/corrections/JungfrauCorrection.py
index bb40a97fc8661fe0b4d682e3ee0b2459dfe1204e..b7903d033143d77b4efc323789f23325ebbd774f 100644
--- a/src/calng/corrections/JungfrauCorrection.py
+++ b/src/calng/corrections/JungfrauCorrection.py
@@ -42,12 +42,13 @@ bad_pixel_constants = {
 
 # from pycalibration (TOOD: move to common shared lib)
 class GainModes(enum.Enum):
-    DYNAMIC_GAIN = "dynamicgain"
-    DYNAMIC_GAIN_HG0 = "dynamichg0"
-    FIX_GAIN_1 = "fixgain1"
-    FIX_GAIN_2 = "fixgain2"
-    FORCE_SWITCH_HG1 = "forceswitchg1"
-    FORCE_SWITCH_HG2 = "forceswitchg2"
+    ADAPTIVE_GAIN = 0
+    FIXED_GAIN = 1
+
+
+class GainSettings(enum.Enum):
+    LOW_CDS = 0
+    HIGH_CDS = 1
 
 
 class CorrectionFlags(enum.IntFlag):
@@ -380,7 +381,7 @@ class JungfrauCalcatFriend(base_calcat.BaseCalcatFriend):
             .reconfigurable()
             .commit(),
 
-            DOUBLE_ELEMENT(schema)
+            STRING_ELEMENT(schema)
             .key("constantParameters.gainMode")
             .displayedName("Gain mode")
             .description(
@@ -390,16 +391,18 @@ class JungfrauCalcatFriend(base_calcat.BaseCalcatFriend):
                 "for fixed gain (omitted otherwise)."
             )
             .assignmentOptional()
-            .defaultValue(0)
+            .defaultValue(GainModes.ADAPTIVE_GAIN.name)
+            .options(",".join(gain_mode.name for gain_mode in GainModes))
             .reconfigurable()
             .commit(),
 
-            DOUBLE_ELEMENT(schema)
+            STRING_ELEMENT(schema)
             .key("constantParameters.gainSetting")
             .displayedName("Gain setting")
             .description("See description of gainMode")
             .assignmentOptional()
-            .defaultValue(0)
+            .defaultValue(GainSettings.LOW_CDS.name)
+            .options(",".join(gain_setting.name for gain_setting in GainSettings))
             .reconfigurable()
             .commit(),
         )
@@ -421,10 +424,13 @@ class JungfrauCalcatFriend(base_calcat.BaseCalcatFriend):
         res["Integration Time"] = self._get_param("integrationTime")
         res["Sensor Temperature"] = self._get_param("sensorTemperature")
 
-        if self._get_param("gainMode") != 0:
-            # NOTE: always include if CalCat is updated for this
-            res["Gain mode"] = self._get_param("gainMode")
-        res["Gain Setting"] = self._get_param("gainSetting")
+        if (
+            gain_mode := GainModes[self._get_param("gainMode")]
+        ) is not GainModes.ADAPTIVE_GAIN:
+            # NOTE: currently only including if parameter for CalCat is 1
+            # change if conditions are tidied up in the database
+            res["Gain mode"] = gain_mode.value
+        res["Gain Setting"] = GainSettings[self._get_param("gainSetting")].value
 
         return res
 
diff --git a/src/calng/corrections/LpdCorrection.py b/src/calng/corrections/LpdCorrection.py
index 913f85498b79a35da74a399a261e48a630cddf73..62a9e42f1637f1341514ce4df3aa21df157b91a3 100644
--- a/src/calng/corrections/LpdCorrection.py
+++ b/src/calng/corrections/LpdCorrection.py
@@ -48,7 +48,7 @@ class CorrectionFlags(enum.IntFlag):
 
 class LpdGpuRunner(base_kernel_runner.BaseKernelRunner):
     _gpu_based = True
-    _corrected_axis_order = "fxy"
+    _corrected_axis_order = "fyx"
 
     @property
     def input_shape(self):
@@ -81,7 +81,7 @@ class LpdGpuRunner(base_kernel_runner.BaseKernelRunner):
         self.processed_data = cp.empty(self.processed_shape, dtype=output_data_dtype)
         self.cell_table = cp.empty(frames, dtype=np.uint16)
 
-        self.map_shape = (constant_memory_cells, pixels_x, pixels_y, 3)
+        self.map_shape = (constant_memory_cells, pixels_y, pixels_x, 3)
         self.offset_map = cp.empty(self.map_shape, dtype=np.float32)
         self.gain_amp_map = cp.empty(self.map_shape, dtype=np.float32)
         self.rel_gain_slopes_map = cp.empty(self.map_shape, dtype=np.float32)
@@ -153,7 +153,7 @@ class LpdGpuRunner(base_kernel_runner.BaseKernelRunner):
             Constants.Offset: ((2, 1, 0, 3), self.offset_map),
             Constants.GainAmpMap: ((2, 0, 1, 3), self.gain_amp_map),
             Constants.FFMap: ((2, 0, 1, 3), self.flatfield_map),
-            Constants.RelativeGain: ((2, 1, 0, 3), self.rel_gain_slopes_map),
+            Constants.RelativeGain: ((2, 0, 1, 3), self.rel_gain_slopes_map),
         }
         if constant_type in bad_pixel_loading:
             self.bad_pixel_map |= self._xp.asarray(
@@ -396,8 +396,8 @@ class LpdCorrection(BaseCorrection):
         return (
             self.unsafe_get("dataFormat.frames"),
             1,
-            self.unsafe_get("dataFormat.pixelsX"),
             self.unsafe_get("dataFormat.pixelsY"),
+            self.unsafe_get("dataFormat.pixelsX"),
         )
 
     def __init__(self, config):
diff --git a/src/calng/corrections/LpdminiCorrection.py b/src/calng/corrections/LpdminiCorrection.py
new file mode 100644
index 0000000000000000000000000000000000000000..688b174b23474f35346014e91bd3b9074879fb4a
--- /dev/null
+++ b/src/calng/corrections/LpdminiCorrection.py
@@ -0,0 +1,103 @@
+import numpy as np
+from karabo.bound import (
+    DOUBLE_ELEMENT,
+    KARABO_CLASSINFO,
+    OVERWRITE_ELEMENT,
+)
+
+from .._version import version as deviceVersion
+from ..base_correction import add_correction_step_schema
+from . import LpdCorrection
+
+
+class LpdminiGpuRunner(LpdCorrection.LpdGpuRunner):
+    def load_constant(self, constant_type, constant_data):
+        print(f"Given: {constant_type} with shape {constant_data.shape}")
+        # constant type → transpose order
+        constant_buffer_map = {
+            LpdCorrection.Constants.Offset: self.offset_map,
+            LpdCorrection.Constants.GainAmpMap: self.gain_amp_map,
+            LpdCorrection.Constants.FFMap: self.flatfield_map,
+            LpdCorrection.Constants.RelativeGain: self.rel_gain_slopes_map,
+        }
+        if constant_type in {
+            LpdCorrection.Constants.BadPixelsDark,
+            LpdCorrection.Constants.BadPixelsFF,
+        }:
+            self.bad_pixel_map |= self._xp.asarray(
+                constant_data,
+                dtype=np.uint32,
+            )[: self.constant_memory_cells]
+        else:
+            constant_buffer_map[constant_type].set(
+                constant_data.astype(np.float32)[: self.constant_memory_cells]
+            )
+
+
+class LpdminiCalcatFriend(LpdCorrection.LpdCalcatFriend):
+    @staticmethod
+    def add_schema(
+        schema,
+        managed_keys,
+    ):
+        super(LpdminiCalcatFriend, LpdminiCalcatFriend).add_schema(schema, managed_keys)
+        (
+            OVERWRITE_ELEMENT(schema)
+            .key("constantParameters.biasVoltage")
+            .setNewDisplayedName("Bias voltage (odd)")
+            .setNewDescription("Bias voltage apbplied to minis 1, 3, 5, and 7.")
+            .commit(),
+
+            DOUBLE_ELEMENT(schema)
+            .key("constantParameters.biasVoltage2")
+            .displayedName("Bias voltage (even)")
+            .description("Separate bias voltage used for minis 2, 4, 6, and 8.")
+            .assignmentOptional()
+            .defaultValue(300)
+            .reconfigurable()
+            .commit(),
+
+            OVERWRITE_ELEMENT(schema)
+            .key("constantParameters.pixelsY")
+            .setNewDefaultValue(32)
+            .commit(),
+        )
+        managed_keys.add("constantParameters.biasVoltage2")
+
+    def basic_condition(self):
+        res = super().basic_condition()
+        if int(self.device.get("fastSources")[0][-1]) % 2 == 0:
+            res["Sensor Bias Voltage"] = self._get_param("biasVoltage2")
+        if "category" in res:
+            del res["category"]
+        return res
+
+
+@KARABO_CLASSINFO("LpdminiCorrection", deviceVersion)
+class LpdminiCorrection(LpdCorrection.LpdCorrection):
+    _calcat_friend_class = LpdminiCalcatFriend
+    _kernel_runner_class = LpdminiGpuRunner
+    _managed_keys = LpdCorrection.LpdCorrection._managed_keys.copy()
+
+    @classmethod
+    def expectedParameters(cls, expected):
+        (
+            OVERWRITE_ELEMENT(expected)
+            .key("dataFormat.pixelsY")
+            .setNewDefaultValue(32)
+            .commit(),
+
+            OVERWRITE_ELEMENT(expected)
+            .key("dataFormat.frames")
+            .setNewDefaultValue(512)
+            .commit(),
+        )
+        cls._calcat_friend_class.add_schema(expected, cls._managed_keys)
+        # warning: this is redundant, but needed for now to get managed keys working
+        add_correction_step_schema(expected, cls._managed_keys, cls._correction_steps)
+        (
+            OVERWRITE_ELEMENT(expected)
+            .key("managedKeys")
+            .setNewDefaultValue(list(cls._managed_keys))
+            .commit()
+        )
diff --git a/src/calng/geometries/Agipd500KGeometry.py b/src/calng/geometries/Agipd500KGeometry.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2e9580050dc4e80d97d892018561fd9aa3d4242
--- /dev/null
+++ b/src/calng/geometries/Agipd500KGeometry.py
@@ -0,0 +1,8 @@
+import extra_geom
+
+from ..base_geometry import ManualOriginGeometryBase, make_origin_node
+
+
+class Agipd500KGeometry(ManualOriginGeometryBase):
+    geometry_class = extra_geom.AGIPD_500K2GGeometry
+    origin = make_origin_node(0, 0)
diff --git a/src/calng/geometries/JungfrauGeometry.py b/src/calng/geometries/JungfrauGeometry.py
index c447accc7dec7857a5c858e3cfac62e001d2af29..e0c0703cb394a9b3f51fa2f163a62cc119c6a15a 100644
--- a/src/calng/geometries/JungfrauGeometry.py
+++ b/src/calng/geometries/JungfrauGeometry.py
@@ -2,12 +2,15 @@ import extra_geom
 import numpy as np
 from karabo.middlelayer import Hash
 
-from ..base_geometry import ManualModuleListGeometryBase, make_manual_module_list_node
+from ..base_geometry import (
+    ManualOrientableModuleListGeometryBase,
+    make_manual_orientable_module_list_node,
+)
 
 
-class JungfrauGeometry(ManualModuleListGeometryBase):
+class JungfrauGeometry(ManualOrientableModuleListGeometryBase):
     geometry_class = extra_geom.JUNGFRAUGeometry
-    moduleList = make_manual_module_list_node(
+    moduleList = make_manual_orientable_module_list_node(
         [
             Hash("posX", x, "posY", y, "orientX", ox, "orientY", oy)
             for (x, y, ox, oy) in [
diff --git a/src/calng/geometries/LpdminiGeometry.py b/src/calng/geometries/LpdminiGeometry.py
new file mode 100644
index 0000000000000000000000000000000000000000..31846b953ccc949564686e221b631bf0bd8c866e
--- /dev/null
+++ b/src/calng/geometries/LpdminiGeometry.py
@@ -0,0 +1,24 @@
+import extra_geom
+from karabo.middlelayer import Hash
+
+from ..base_geometry import (
+    ManualRotatableModuleListGeometryBase,
+    make_manual_rotatable_module_list_node,
+)
+
+
+class LpdminiGeometry(ManualRotatableModuleListGeometryBase):
+    geometry_class = extra_geom.LPD_MiniGeometry
+
+    moduleList = make_manual_rotatable_module_list_node(
+        [
+            Hash("posX", x, "posY", y, "rotate", r)
+            for (x, y, r) in [
+                (0, 0, 0),
+                # TODO: appropriate defaults
+            ]
+        ]
+    )
+
+    def _update_manual_from_current(self):
+        raise NotImplementedError()
diff --git a/src/calng/geometries/__init__.py b/src/calng/geometries/__init__.py
index 8705bb0c5edf88aa6ffb570c71c4f54a4b059a6b..a2864497a2d9ee83b1f5b64536b2f89f9d8b814c 100644
--- a/src/calng/geometries/__init__.py
+++ b/src/calng/geometries/__init__.py
@@ -3,6 +3,8 @@ from . import (
     Agipd1MGeometry,
     Dssc1MGeometry,
     Epix100Geometry,
+    Lpd1MGeometry,
+    LpdminiGeometry,
     JungfrauGeometry,
     Lpd1MGeometry,
     PnccdGeometry,
diff --git a/src/calng/kernels/lpd_cpu.pyx b/src/calng/kernels/lpd_cpu.pyx
new file mode 100644
index 0000000000000000000000000000000000000000..0c3eb82d50c3e23d1414515fd967347ffe4a0876
--- /dev/null
+++ b/src/calng/kernels/lpd_cpu.pyx
@@ -0,0 +1,55 @@
+# cython: boundscheck=False
+# cython: cdivision=True
+# cython: wrapararound=False
+
+cdef unsigned char NONE = 0
+cdef unsigned char OFFSET = 1
+cdef unsigned char GAIN_AMP = 2
+cdef unsigned char REL_GAIN = 4
+cdef unsigned char FF_CORR = 8
+cdef unsigned char BPMASK = 16
+
+from cython.parallel import prange
+from libc.math cimport isinf, isnan
+
+def correct(
+    unsigned short[:, :, :, :] image_data,
+    unsigned short[:] cell_table,
+    unsigned char flags,
+    float[:, :, :, :] offset_map,
+    float[:, :, :, :] gain_amp_map,
+    float[:, :, :, :] rel_gain_slopes_map,
+    float[:, :, :, :] flatfield_map,
+    unsigned[:, :, :, :] bad_pixel_map,
+    float bad_pixel_mask_value,
+    # TODO: support spitting out gain map for preview purposes
+    float[:, :, :] output
+):
+    cdef int frame, map_cell, ss, fs
+    cdef unsigned char gain_stage
+    cdef float res
+    cdef unsigned short raw_data_value
+    for frame in prange(image_data.shape[0], nogil=True):
+        map_cell = cell_table[frame]
+        if map_cell >= offset_map.shape[0]:
+            for ss in range(image_data.shape[1]):
+                for fs in range(image_data.shape[2]):
+                    output[frame, ss, fs] = <float>image_data[frame, 0, ss, fs]
+            continue
+        for ss in range(image_data.shape[1]):
+            for fs in range(image_data.shape[3]):
+                raw_data_value = image_data[frame, 0, ss, fs]
+        gain_stage = (raw_data_value >> 12) & 0x0003
+        res = <float>(raw_data_value & 0x0fff)
+        if gain_stage > 2 or (flags & BPMASK and bad_pixel_map[map_cell, ss, fs, gain_stage] != 0):
+            res = bad_pixel_mask_value
+        else:
+            if flags & OFFSET:
+                res = res - offset_map[map_cell, ss, fs, gain_stage]
+            if flags & GAIN_AMP:
+                res = res * gain_amp_map[map_cell, ss, fs, gain_stage]
+            if flags & FF_CORR:
+                res = res * rel_gain_slopes_map[map_cell, ss, fs, gain_stage]
+            if res < 1e-7 or res > 1e7 or isnan(res) or isinf(res):
+                res = bad_pixel_mask_value
+        output[frame, ss, fs] = res
diff --git a/src/calng/scenes.py b/src/calng/scenes.py
index 01a97467908c839f81b36fff7aa1d6ba35079497..e48ae88b1d4598d58110e30666f02f3eca8b450c 100644
--- a/src/calng/scenes.py
+++ b/src/calng/scenes.py
@@ -9,6 +9,7 @@ from karabo.common.scenemodel.api import (
     DetectorGraphModel,
     DisplayCommandModel,
     DisplayLabelModel,
+    DisplayListModel,
     DisplayStateColorModel,
     DisplayTextLogModel,
     DoubleLineEditModel,
@@ -46,7 +47,13 @@ def DisplayRoundedFloat(*args, decimals=2, **kwargs):
     return EvaluatorModel(*args, expression=f"f'{{x:.{decimals}f}}'", **kwargs)
 
 
-_type_to_display_model = {"BOOL": CheckBoxModel, "FLOAT": DisplayRoundedFloat}
+_type_to_display_model = {
+    "BOOL": CheckBoxModel,
+    "FLOAT": DisplayRoundedFloat,
+    "STRING": DisplayLabelModel,
+    "UINT32": DisplayLabelModel,
+    "VECTOR_UINT32": DisplayListModel,
+}
 _type_to_line_editable = {
     "BOOL": (CheckBoxModel, {"klass": "EditableCheckBox"}),
     "DOUBLE": (DoubleLineEditModel, {}),
@@ -301,8 +308,13 @@ class DisplayAndEditableRow(HorizontalLayout):
         )
 
         if self.include_display(key_attr):
+            if value_type in _type_to_display_model:
+                model = _type_to_display_model[value_type]
+            else:
+                model = DisplayLabelModel
+                print(f"Scene generator would like to know more about {value_type}")
             self.children.append(
-                _type_to_display_model.get(value_type, DisplayLabelModel)(
+                model(
                     keys=[f"{device_id}.{key_path}"],
                     width=display_width * size_scale,
                     height=height,
@@ -551,7 +563,7 @@ class ManagerDeviceStatus(VerticalLayout):
 
 @titled("Device status", width=6 * NARROW_INC)
 @boxed
-class CorrectionDeviceStatus(VerticalLayout):
+class ProcessingDeviceStatus(VerticalLayout):
     def __init__(self, device_id, schema_hash):
         super().__init__(padding=0)
         name = DisplayLabelModel(
@@ -600,28 +612,29 @@ class CorrectionDeviceStatus(VerticalLayout):
                 ),
             ]
         )
-        self.children.append(
-            VerticalLayout(
-                children=[
-                    HorizontalLayout(
-                        LampModel(
-                            keys=[f"{device_id}.{warning_lamp}"],
-                            width=BASE_INC,
-                            height=BASE_INC,
-                        ),
-                        LabelModel(
-                            text=warning_lamp,
-                            width=8 * BASE_INC,
-                            height=BASE_INC,
-                        ),
-                    )
-                    for warning_lamp in schema_hash.getAttribute(
-                        "warningLamps", "defaultValue"
-                    )
-                ],
-                padding=0,
+        if schema_hash.has("warningLamps"):
+            self.children.append(
+                VerticalLayout(
+                    children=[
+                        HorizontalLayout(
+                            LampModel(
+                                keys=[f"{device_id}.{warning_lamp}"],
+                                width=BASE_INC,
+                                height=BASE_INC,
+                            ),
+                            LabelModel(
+                                text=warning_lamp,
+                                width=8 * BASE_INC,
+                                height=BASE_INC,
+                            ),
+                        )
+                        for warning_lamp in schema_hash.getAttribute(
+                            "warningLamps", "defaultValue"
+                        )
+                    ],
+                    padding=0,
+                )
             )
-        )
         self.children.append(status_log)
 
 
@@ -1309,7 +1322,7 @@ def scene_generator(fun):
 def correction_device_overview(device_id, schema):
     schema_hash = schema_to_hash(schema)
     main_overview = HorizontalLayout(
-        CorrectionDeviceStatus(device_id, schema_hash),
+        ProcessingDeviceStatus(device_id, schema_hash),
         VerticalLayout(
             recursive_editable(
                 device_id,
@@ -1355,6 +1368,15 @@ def correction_device_overview(device_id, schema):
     )
 
 
+@scene_generator
+def lpdmini_splitter_overview(device_id, schema):
+    schema_hash = schema_to_hash(schema)
+    return VerticalLayout(
+        ProcessingDeviceStatus(device_id, schema_hash),
+        DisplayRow(device_id, schema_hash, "outputDataShape", 6, 8),
+    )
+
+
 @scene_generator
 def correction_device_preview(device_id, schema, preview_channel):
     schema_hash = schema_to_hash(schema)
@@ -1519,6 +1541,39 @@ def manager_device_overview(
     mds_hash = schema_to_hash(manager_device_schema)
     cds_hash = schema_to_hash(correction_device_schema)
 
+    data_throttling_children = [
+        LabelModel(
+            text="Frame filter",
+            width=11 * BASE_INC,
+            height=BASE_INC,
+        ),
+        EditableRow(
+            manager_device_id,
+            mds_hash,
+            "managedKeys.frameFilter.type",
+            7,
+            4,
+        ),
+        EditableRow(
+            manager_device_id,
+            mds_hash,
+            "managedKeys.frameFilter.spec",
+            7,
+            4,
+        ),
+    ]
+
+    if "managedKeys.daqTrainStride" in mds_hash:
+        # Only add DAQ train stride if present on the schema, may be
+        # disabled on the manager.
+        data_throttling_children.insert(0, EditableRow(
+            manager_device_id,
+            mds_hash,
+            "managedKeys.daqTrainStride",
+            7,
+            4,
+        ))
+
     return VerticalLayout(
         HorizontalLayout(
             ManagerDeviceStatus(manager_device_id),
@@ -1540,34 +1595,7 @@ def manager_device_overview(
                     max_depth=2,
                 ),
                 titled("Data throttling")(boxed(VerticalLayout))(
-                    children=[
-                        EditableRow(
-                            manager_device_id,
-                            mds_hash,
-                            "managedKeys.daqTrainStride",
-                            7,
-                            4,
-                        ),
-                        LabelModel(
-                            text="Frame filter",
-                            width=11 * BASE_INC,
-                            height=BASE_INC,
-                        ),
-                        EditableRow(
-                            manager_device_id,
-                            mds_hash,
-                            "managedKeys.frameFilter.type",
-                            7,
-                            4,
-                        ),
-                        EditableRow(
-                            manager_device_id,
-                            mds_hash,
-                            "managedKeys.frameFilter.spec",
-                            7,
-                            4,
-                        ),
-                    ],
+                    children=data_throttling_children,
                     padding=0,
                 ),
             ),
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