diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs
index 5d3442c1..79b45d66 100644
--- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs
+++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs
@@ -21,7 +21,7 @@ public ConfigureFmcLinkController()
                      "Consult the device datasheet and documentation for allowable voltage ranges.")]
         public double? PortVoltage { get; set; } = null;
 
-        protected bool CheckLinkState(DeviceContext device)
+        protected virtual bool CheckLinkState(DeviceContext device)
         {
             var linkState = device.ReadRegister(FmcLinkController.LINKSTATE);
             return (linkState & FmcLinkController.LINKSTATE_SL) != 0;
diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4.cs
new file mode 100644
index 00000000..dd69e181
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+
+namespace OpenEphys.Onix
+{
+    public class ConfigureUclaMiniscopeV4 : HubDeviceFactory
+    {
+        PortName port;
+        readonly ConfigureUclaMiniscopeV4LinkController LinkController = new();
+
+        public ConfigureUclaMiniscopeV4()
+        {
+            Port = PortName.PortA;
+            LinkController.HubConfiguration = HubConfiguration.Passthrough;
+        }
+
+        [Category(ConfigurationCategory)]
+        [TypeConverter(typeof(HubDeviceConverter))]
+        public ConfigureUclaMiniscopeV4Camera Camera { get; set; } = new();
+
+        [Category(ConfigurationCategory)]
+        [TypeConverter(typeof(HubDeviceConverter))]
+        public ConfigureUclaMiniscopeV4Bno055 Bno055 { get; set; } = new();
+
+        public PortName Port
+        {
+            get { return port; }
+            set
+            {
+                port = value;
+                var offset = (uint)port << 8;
+                LinkController.DeviceAddress = (uint)port;
+                Camera.DeviceAddress = offset + 0;
+                Bno055.DeviceAddress = offset + 1;
+            }
+        }
+
+        [Description("If defined, it will override automated voltage discovery and apply the specified voltage" +
+                     "to the headstage. Warning: this device requires 5.0V to 6.0V for proper operation." +
+                     "Supplying higher voltages may result in damage to the headstage.")]
+        public double? PortVoltage
+        {
+            get => LinkController.PortVoltage;
+            set => LinkController.PortVoltage = value;
+        }
+
+        internal override IEnumerable<IDeviceConfiguration> GetDevices()
+        {
+            yield return LinkController;
+            yield return Camera;
+            yield return Bno055;
+        }
+    }
+}
diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4Bno055.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4Bno055.cs
new file mode 100644
index 00000000..7414cf53
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4Bno055.cs
@@ -0,0 +1,69 @@
+using System;
+using System.ComponentModel;
+
+namespace OpenEphys.Onix
+{
+    public class ConfigureUclaMiniscopeV4Bno055 : SingleDeviceFactory
+    {
+        public ConfigureUclaMiniscopeV4Bno055()
+            : base(typeof(UclaMiniscopeV4Bno055))
+        {
+        }
+
+        [Category(ConfigurationCategory)]
+        [Description("Specifies whether the BNO055 device is enabled.")]
+        public bool Enable { get; set; } = true;
+
+        public override IObservable<ContextTask> Process(IObservable<ContextTask> source)
+        {
+            var enable = Enable;
+            var deviceName = DeviceName;
+            var deviceAddress = DeviceAddress;
+            return source.ConfigureDevice(context =>
+            {
+                // configure device via the DS90UB9x deserializer device
+                var device = context.GetPassthroughDeviceContext(deviceAddress, DS90UB9x.ID);
+                ConfigureDeserializer(device);
+                ConfigureBno055(device);
+                var deviceInfo = new DeviceInfo(context, DeviceType, deviceAddress);
+                return DeviceManager.RegisterDevice(deviceName, deviceInfo);
+            });
+        }
+
+        static void ConfigureDeserializer(DeviceContext device)
+        {
+            // configure deserializer I2C aliases
+            var deserializer = new I2CRegisterContext(device, DS90UB9x.DES_ADDR);
+            uint alias = UclaMiniscopeV4Bno055.BNO055Address << 1;
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID4, alias);
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias4, alias);
+        }
+
+        static void ConfigureBno055(DeviceContext device)
+        {
+            // setup BNO055 device
+            // TODO: Correct orientation
+            var i2c = new I2CRegisterContext(device, UclaMiniscopeV4Bno055.BNO055Address);
+            i2c.WriteByte(0x3E, 0x00); // Power mode normal
+            i2c.WriteByte(0x07, 0x00); // Page ID address 0
+            i2c.WriteByte(0x3F, 0x00); // Internal oscillator
+            i2c.WriteByte(0x41, 0b00000110);  // Axis map config (configured to match hs64; X => Z, Y => -Y, Z => X)
+            i2c.WriteByte(0x42, 0b000000010); // Axis sign (negate Y)
+            i2c.WriteByte(0x3D, 8); // Operation mode is NOF
+        }
+    }
+
+    static class UclaMiniscopeV4Bno055
+    {
+        public const int BNO055Address = 0x28;
+        public const int DataAddress = 0x1A;
+
+        internal class NameConverter : DeviceNameConverter
+        {
+            public NameConverter()
+                : base(typeof(UclaMiniscopeV4Bno055))
+            {
+            }
+        }
+    }
+}
diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4Camera.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4Camera.cs
new file mode 100644
index 00000000..b18b93c8
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4Camera.cs
@@ -0,0 +1,236 @@
+using System;
+using System.ComponentModel;
+using System.Drawing.Design;
+using System.Reactive.Disposables;
+using System.Reactive.Subjects;
+using Bonsai;
+
+namespace OpenEphys.Onix
+{
+    public class ConfigureUclaMiniscopeV4Camera : SingleDeviceFactory
+    {
+        readonly BehaviorSubject<double> ledBrightness = new(0);
+        readonly BehaviorSubject<UclaMiniscopeV4SensorGain> sensorGain = new(UclaMiniscopeV4SensorGain.Low);
+        readonly BehaviorSubject<double> liquidLensVoltage = new(47); // NB: middle of range
+
+        public ConfigureUclaMiniscopeV4Camera()
+            : base(typeof(UclaMiniscopeV4))
+        {
+        }
+
+        [Category(ConfigurationCategory)]
+        [Description("Specifies whether the camera is enabled.")]
+        public bool Enable { get; set; } = true;
+
+        [Category(ConfigurationCategory)]
+        [Description("Camera video rate in frames per second.")]
+        public UclaMiniscopeV4FramesPerSecond FrameRate { get; set; } = UclaMiniscopeV4FramesPerSecond.Fps30Hz;
+
+        [Description("Camera sensor analog gain.")]
+        [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
+        public UclaMiniscopeV4SensorGain SensorGain
+        {
+            get => sensorGain.Value;
+            set => sensorGain.OnNext(value);
+        }
+
+        [Category(ConfigurationCategory)]
+        [Description("Only turn on excitation LED during camera exposures.")]
+        public bool InterleaveLed { get; set; } = false;
+
+        [Description("Excitation LED brightness (0-100%).")]
+        [Range(0, 100)]
+        [Precision(1, 1)]
+        [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
+        public double LedBrightness
+        {
+            get => ledBrightness.Value;
+            set => ledBrightness.OnNext(value);
+        }
+
+        [Description("Liquid lens voltage (Volts RMS).")]
+        [Range(24.4, 69.7)]
+        [Precision(1, 1)]
+        [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
+        public double LiquidLensVoltage
+        {
+            get => liquidLensVoltage.Value;
+            set => liquidLensVoltage.OnNext(value);
+        }
+
+        public override IObservable<ContextTask> Process(IObservable<ContextTask> source)
+        {
+            var enable = Enable;
+            var deviceName = DeviceName;
+            var deviceAddress = DeviceAddress;
+            uint shutterWidth = FrameRate switch
+            {
+                UclaMiniscopeV4FramesPerSecond.Fps10Hz => 10000,
+                UclaMiniscopeV4FramesPerSecond.Fps15Hz => 6667,
+                UclaMiniscopeV4FramesPerSecond.Fps20Hz => 5000,
+                UclaMiniscopeV4FramesPerSecond.Fps25Hz => 4000,
+                UclaMiniscopeV4FramesPerSecond.Fps30Hz => 3300,
+                _ => 3300
+            };
+            var interleaveLED = InterleaveLed;
+
+            return source.ConfigureDevice(context =>
+            {
+                // configure device via the DS90UB9x deserializer device
+                var device = context.GetPassthroughDeviceContext(deviceAddress, DS90UB9x.ID);
+                device.WriteRegister(DS90UB9x.ENABLE, enable ? 1u : 0);
+
+                // configure deserializer, chip states, and camera PLL
+                ConfigureMiniscope(device);
+
+                // configuration properties
+                var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress);
+                atMega.WriteByte(0x04, (uint)(interleaveLED ? 0x00 : 0x03));
+                WriteCameraRegister(atMega, 200, shutterWidth);
+
+                var deviceInfo = new DeviceInfo(context, DeviceType, deviceAddress);
+                var disposable = DeviceManager.RegisterDevice(deviceName, deviceInfo);
+                var shutdown = Disposable.Create(() =>
+                {
+                    // turn off EWL
+                    var max14574 = new I2CRegisterContext(device, UclaMiniscopeV4.Max14574Address);
+                    max14574.WriteByte(0x03, 0x00);
+
+                    // turn off LED
+                    var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress);
+                    atMega.WriteByte(1, 0xFF);
+                });
+                return new CompositeDisposable(
+                    ledBrightness.Subscribe(value => SetLedBrightness(device, value)),
+                    sensorGain.Subscribe(value => SetSensorGain(device, value)),
+                    liquidLensVoltage.Subscribe(value => SetLiquidLensVoltage(device, value)),
+                    shutdown,
+                    disposable);
+            });
+        }
+
+        internal static void ConfigureMiniscope(DeviceContext device)
+        {
+            // configure deserializer
+            device.WriteRegister(DS90UB9x.TRIGGEROFF, 0);
+            device.WriteRegister(DS90UB9x.READSZ, UclaMiniscopeV4.SensorColumns);
+            device.WriteRegister(DS90UB9x.TRIGGER, (uint)DS90UB9xTriggerMode.HsyncEdgePositive);
+            device.WriteRegister(DS90UB9x.SYNCBITS, 0);
+            device.WriteRegister(DS90UB9x.DATAGATE, (uint)DS90UB9xDataGate.VsyncPositive);
+
+            // NB: This is required because Bonsai is not garuenteed to capure every frame at the start of acqusition.
+            // For this reason, the frame start needs to be marked.
+            device.WriteRegister(DS90UB9x.MARK, (uint)DS90UB9xMarkMode.VsyncRising);
+
+            // configure deserializer I2C aliases
+            var deserializer = new I2CRegisterContext(device, DS90UB9x.DES_ADDR);
+            uint coaxMode = 0x4 + (uint)DS90UB9xMode.Raw12BitLowFrequency; // 0x4 maintains coax mode
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.PortMode, coaxMode);
+
+            uint i2cAlias = UclaMiniscopeV4.AtMegaAddress << 1;
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID1, i2cAlias);
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias1, i2cAlias);
+
+            i2cAlias = UclaMiniscopeV4.Tpl0102Address << 1;
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID2, i2cAlias);
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias2, i2cAlias);
+
+            i2cAlias = UclaMiniscopeV4.Max14574Address << 1;
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveID3, i2cAlias);
+            deserializer.WriteByte((uint)DS90UB9xDeserializerI2CRegister.SlaveAlias3, i2cAlias);
+
+            // set up potentiometer
+            var tpl0102 = new I2CRegisterContext(device, UclaMiniscopeV4.Tpl0102Address);
+            tpl0102.WriteByte(0x00, 0x72);
+            tpl0102.WriteByte(0x01, 0x00);
+
+            // turn on EWL
+            var max14574 = new I2CRegisterContext(device, UclaMiniscopeV4.Max14574Address);
+            max14574.WriteByte(0x08, 0x7F);
+            max14574.WriteByte(0x09, 0x02);
+
+            // turn on LED and setup Python480
+            var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress);
+            WriteCameraRegister(atMega, 16, 3); // Turn on PLL
+            WriteCameraRegister(atMega, 32, 0x7007); // Turn on clock managment
+            WriteCameraRegister(atMega, 199, 666); // Defines granularity (unit = 1/PLL clock) of exposure and reset_length
+            WriteCameraRegister(atMega, 200, 3300); // Set frame rate to 30 Hz
+            WriteCameraRegister(atMega, 201, 3000); // Set Exposure
+        }
+
+        private static void WriteCameraRegister(I2CRegisterContext i2c, uint register, uint value)
+        {
+            // ATMega -> Python480 passthrough protocol
+            var regLow = register & 0xFF;
+            var regHigh = (register >> 8) & 0xFF;
+            var valLow = value & 0xFF;
+            var valHigh = (value >> 8) & 0xFF;
+
+            i2c.WriteByte(0x05, regHigh);
+            i2c.WriteByte(regLow, valHigh);
+            i2c.WriteByte(valLow, 0x00);
+        }
+
+        internal static void SetLedBrightness(DeviceContext device, double percent)
+        {
+            var des = device.Context.GetPassthroughDeviceContext(device.Address, DS90UB9x.ID);
+
+            var atMega = new I2CRegisterContext(des, UclaMiniscopeV4.AtMegaAddress);
+            atMega.WriteByte(0x01, (uint)((percent == 0) ? 0xFF : 0x08));
+
+            var tpl0102 = new I2CRegisterContext(des, UclaMiniscopeV4.Tpl0102Address);
+            tpl0102.WriteByte(0x01, (uint)(255 * ((100 - percent) / 100.0)));
+        }
+
+        internal static void SetSensorGain(DeviceContext device, UclaMiniscopeV4SensorGain gain)
+        {
+            var des = device.Context.GetPassthroughDeviceContext(device.Address, DS90UB9x.ID);
+
+            var atMega = new I2CRegisterContext(des, UclaMiniscopeV4.AtMegaAddress);
+            WriteCameraRegister(atMega, 204, (uint)gain);
+        }
+
+        internal static void SetLiquidLensVoltage(DeviceContext device, double voltage)
+        {
+            var des = device.Context.GetPassthroughDeviceContext(device.Address, DS90UB9x.ID);
+
+            var max14574 = new I2CRegisterContext(des, UclaMiniscopeV4.Max14574Address);
+            max14574.WriteByte(0x08, (uint)((voltage - 24.4) / 0.0445) >> 2);
+            max14574.WriteByte(0x09, 0x02);
+        }
+    }
+
+    static class UclaMiniscopeV4
+    {
+        public const int AtMegaAddress = 0x10;
+        public const int Tpl0102Address = 0x50;
+        public const int Max14574Address = 0x77;
+
+        public const int SensorRows = 608;
+        public const int SensorColumns = 608;
+
+        internal class NameConverter : DeviceNameConverter
+        {
+            public NameConverter()
+                : base(typeof(UclaMiniscopeV4))
+            {
+            }
+        }
+    }
+
+    public enum UclaMiniscopeV4SensorGain
+    {
+        Low = 0x00E1,
+        Medium = 0x00E4,
+        High = 0x0024,
+    }
+
+    public enum UclaMiniscopeV4FramesPerSecond
+    {
+        Fps10Hz,
+        Fps15Hz,
+        Fps20Hz,
+        Fps25Hz,
+        Fps30Hz,
+    }
+}
diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4LinkController.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4LinkController.cs
new file mode 100644
index 00000000..a5244a11
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureUclaMiniscopeV4LinkController.cs
@@ -0,0 +1,59 @@
+using System.Threading;
+
+namespace OpenEphys.Onix
+{
+    class ConfigureUclaMiniscopeV4LinkController : ConfigureFmcLinkController
+    {
+        protected override bool ConfigurePortVoltage(DeviceContext device)
+        {
+            const uint MinVoltage = 50;
+            const uint MaxVoltage = 70;
+            const uint VoltageOffset = 02;
+            const uint VoltageIncrement = 02;
+
+            for (uint voltage = MinVoltage; voltage <= MaxVoltage; voltage += VoltageIncrement)
+            {
+                SetPortVoltage(device, voltage);
+                if (CheckLinkState(device))
+                {
+                    SetPortVoltage(device, voltage + VoltageOffset);
+                    return CheckLinkState(device);
+                }
+            }
+
+            return false;
+        }
+
+        private void SetPortVoltage(DeviceContext device, uint voltage)
+        {
+            const int WaitUntilVoltageSettles = 200;
+            device.WriteRegister(FmcLinkController.PORTVOLTAGE, 0);
+            Thread.Sleep(WaitUntilVoltageSettles);
+            device.WriteRegister(FmcLinkController.PORTVOLTAGE, voltage);
+            Thread.Sleep(WaitUntilVoltageSettles);
+        }
+
+        override protected bool CheckLinkState(DeviceContext device)
+        {
+            try
+            {
+                var ds90ub9x = device.Context.GetPassthroughDeviceContext(DeviceAddress << 8, DS90UB9x.ID);
+                ConfigureUclaMiniscopeV4Camera.ConfigureMiniscope(ds90ub9x);
+            }
+            catch (oni.ONIException ex)
+            {
+                // this can occur if power is too low, so we need to be able to try again
+                const int FailureToWriteRegister = -6;
+                if (ex.Number != FailureToWriteRegister)
+                {
+                    throw;
+                }
+            }
+
+            Thread.Sleep(200);
+
+            var linkState = device.ReadRegister(FmcLinkController.LINKSTATE);
+            return (linkState & FmcLinkController.LINKSTATE_SL) != 0;
+        }
+    }
+}
diff --git a/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Bno055Data.cs b/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Bno055Data.cs
new file mode 100644
index 00000000..fbefceca
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Bno055Data.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using Bonsai;
+
+namespace OpenEphys.Onix
+{
+    public class UclaMiniscopeV4Bno055Data : Source<Bno055DataFrame>
+    {
+        [TypeConverter(typeof(UclaMiniscopeV4Bno055.NameConverter))]
+        public string DeviceName { get; set; }
+
+        public override IObservable<Bno055DataFrame> Generate()
+        {
+            // Max of 100 Hz, but limited by I2C bus
+            var source = Observable.Interval(TimeSpan.FromSeconds(0.01));
+            return Generate(source);
+        }
+
+        public unsafe IObservable<Bno055DataFrame> Generate<TSource>(IObservable<TSource> source)
+        {
+            return Observable.Using(
+                () => DeviceManager.ReserveDevice(DeviceName),
+                disposable => disposable.Subject.SelectMany(
+                    deviceInfo => Observable.Create<Bno055DataFrame>(observer =>
+                    {
+                        var device = deviceInfo.GetDeviceContext(typeof(UclaMiniscopeV4Bno055));
+                        var passthrough = device.GetPassthroughDeviceContext(DS90UB9x.ID);
+                        var i2c = new I2CRegisterContext(passthrough, UclaMiniscopeV4Bno055.BNO055Address);
+
+                        var pollingObserver = Observer.Create<TSource>(
+                            _ =>
+                            {
+                                Bno055DataFrame frame = default;
+                                device.Context.EnsureContext(() =>
+                                {
+                                    var data = i2c.ReadBytes(UclaMiniscopeV4Bno055.DataAddress, sizeof(Bno055DataPayload));
+                                    ulong clock = passthrough.ReadRegister(DS90UB9x.LASTI2CL);
+                                    clock += (ulong)passthrough.ReadRegister(DS90UB9x.LASTI2CH) << 32;
+                                    fixed (byte* dataPtr = data)
+                                    {
+                                        frame = new Bno055DataFrame(clock, (Bno055DataPayload*)dataPtr);
+                                    }
+                                });
+
+                                if (frame != null)
+                                {
+                                    observer.OnNext(frame);
+                                }
+                            },
+                            observer.OnError,
+                            observer.OnCompleted);
+                        return source.SubscribeSafe(pollingObserver);
+                    })));
+        }
+    }
+}
diff --git a/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Camera.cs b/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Camera.cs
new file mode 100644
index 00000000..f7d0139e
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Camera.cs
@@ -0,0 +1,66 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using Bonsai;
+using OpenCV.Net;
+
+namespace OpenEphys.Onix
+{
+    public class UclaMiniscopeV4Camera : Source<UclaMiniscopeV4Image>
+    {
+        [TypeConverter(typeof(UclaMiniscopeV4.NameConverter))]
+        public string DeviceName { get; set; }
+
+        public unsafe override IObservable<UclaMiniscopeV4Image> Generate()
+        {
+            return Observable.Using(
+                () => DeviceManager.ReserveDevice(DeviceName),
+                disposable => disposable.Subject.SelectMany(deviceInfo =>
+                {
+                    var device = deviceInfo.GetDeviceContext(typeof(UclaMiniscopeV4));
+                    var passthrough = device.GetPassthroughDeviceContext(DS90UB9x.ID);
+                    var scopeData = device.Context.FrameReceived.Where(frame => frame.DeviceAddress == passthrough.Address);
+                    return Observable.Create<UclaMiniscopeV4Image>(observer =>
+                    {
+                        var sampleIndex = 0;
+                        var imageBuffer = new short[UclaMiniscopeV4.SensorRows * UclaMiniscopeV4.SensorColumns];
+                        var hubClockBuffer = new ulong[UclaMiniscopeV4.SensorRows];
+                        var clockBuffer = new ulong[UclaMiniscopeV4.SensorRows];
+                        var awaitingFrameStart = true;
+
+                        var frameObserver = Observer.Create<oni.Frame>(
+                            frame =>
+                            {
+                                var payload = (UclaMiniscopeV4ImagerPayload*)frame.Data.ToPointer();
+
+                                // Wait for first row
+                                if (awaitingFrameStart && (payload->ImageRow[0] & 0x8000) == 0)
+                                    return;
+
+                                awaitingFrameStart = false;
+                                Marshal.Copy(new IntPtr(payload->ImageRow), imageBuffer, sampleIndex * UclaMiniscopeV4.SensorColumns, UclaMiniscopeV4.SensorColumns);
+                                hubClockBuffer[sampleIndex] = payload->HubClock;
+                                clockBuffer[sampleIndex] = frame.Clock;
+                                if (++sampleIndex >= UclaMiniscopeV4.SensorRows)
+                                {
+                                    var imageData = Mat.FromArray(imageBuffer, UclaMiniscopeV4.SensorRows, UclaMiniscopeV4.SensorColumns,  Depth.U16, 1);
+                                    CV.ConvertScale(imageData.GetRow(0), imageData.GetRow(0), 1.0f, -32768.0f); // Get rid first row's mark bit
+                                    observer.OnNext(new UclaMiniscopeV4Image(clockBuffer, hubClockBuffer, imageData.GetImage()));
+                                    hubClockBuffer = new ulong[UclaMiniscopeV4.SensorRows];
+                                    clockBuffer = new ulong[UclaMiniscopeV4.SensorRows];
+                                    sampleIndex = 0;
+                                    awaitingFrameStart = true;
+                                }
+                            },
+                            observer.OnError,
+                            observer.OnCompleted);
+
+                        return scopeData.SubscribeSafe(frameObserver);
+                    });
+                }));
+        }
+    }
+}
diff --git a/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Image.cs b/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Image.cs
new file mode 100644
index 00000000..28d00f48
--- /dev/null
+++ b/OpenEphys.Onix/OpenEphys.Onix/UclaMiniscopeV4Image.cs
@@ -0,0 +1,28 @@
+using System.Runtime.InteropServices;
+using OpenCV.Net;
+
+namespace OpenEphys.Onix
+{
+    public class UclaMiniscopeV4Image
+    {
+        public UclaMiniscopeV4Image(ulong[] clock, ulong[] hubClock, IplImage image)
+        {
+            Clock = clock;
+            HubClock = hubClock;
+            Image = image;
+        }
+
+        public ulong[] Clock { get; }
+
+        public ulong[] HubClock { get; }
+
+        public IplImage Image { get; }
+    }
+
+    [StructLayout(LayoutKind.Sequential, Pack = 1)]
+    unsafe struct UclaMiniscopeV4ImagerPayload
+    {
+        public ulong HubClock;
+        public fixed short ImageRow[UclaMiniscopeV4.SensorColumns];
+    }
+}