diff --git a/OpenEphys.Onix1/Bno055DataFrame.cs b/OpenEphys.Onix1/Bno055DataFrame.cs
index 148399f..0d4f559 100644
--- a/OpenEphys.Onix1/Bno055DataFrame.cs
+++ b/OpenEphys.Onix1/Bno055DataFrame.cs
@@ -51,14 +51,14 @@ internal unsafe Bno055DataFrame(ulong clock, Bno055DataPayload* payload)
///
/// Gets the 3D orientation in Euler angle format with units of degrees.
///
- ///
+ ///
/// The Tait-Bryan formalism is used:
///
/// - Yaw: 0 to 360 degrees.
/// - Roll: -180 to 180 degrees
/// - Pitch: -90 to 90 degrees
///
- ///
+ ///
public Vector3 EulerAngle { get; }
///
diff --git a/OpenEphys.Onix1/ConfigurePortController.cs b/OpenEphys.Onix1/ConfigurePortController.cs
index f9587c5..faf60bc 100644
--- a/OpenEphys.Onix1/ConfigurePortController.cs
+++ b/OpenEphys.Onix1/ConfigurePortController.cs
@@ -31,7 +31,7 @@ protected virtual bool CheckLinkState(DeviceContext device)
protected abstract bool ConfigurePortVoltage(DeviceContext device);
- private bool ConfigurePortVoltageOverride(DeviceContext device, double voltage)
+ protected virtual bool ConfigurePortVoltageOverride(DeviceContext device, double voltage)
{
device.WriteRegister(PortController.PORTVOLTAGE, (uint)(voltage * 10));
Thread.Sleep(500);
diff --git a/OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs
new file mode 100644
index 0000000..6ff9ba7
--- /dev/null
+++ b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Threading;
+using oni;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Configures a UCLA Miniscope V4 on the specified port.
+ ///
+ ///
+ /// The UCLA Miniscope V4 is a miniaturized fluorescent microscope for performing single-photon calcium
+ /// imaging in freely moving animals. It has the following features:
+ ///
+ /// - A Python-480 0.48 Megapixel CMOS image sensor.
+ /// - A BNO055 9-axis IMU for real-time, 3D orientation tracking.
+ /// - An electrowetting lens for remote focal plane adjustment.
+ /// - An excitation LED with adjustable brightness control and optional exposure-driven
+ /// interleaving to reduce photobleaching.
+ ///
+ ///
+ public class ConfigureUclaMiniscopeV4 : MultiDeviceFactory
+ {
+
+ const double MaxVoltage = 5.6;
+
+ PortName port;
+ readonly ConfigureUclaMiniscopeV4PortController PortControl = new();
+
+ ///
+ /// Initialize a new instance of a class.
+ ///
+ public ConfigureUclaMiniscopeV4()
+ {
+ Port = PortName.PortA;
+ PortControl.HubConfiguration = HubConfiguration.Passthrough;
+ }
+
+ ///
+ /// Gets or sets the Miniscope camera configuration.
+ ///
+ [Category(DevicesCategory)]
+ [TypeConverter(typeof(SingleDeviceFactoryConverter))]
+ public ConfigureUclaMiniscopeV4Camera Camera { get; set; } = new();
+
+ ///
+ /// Gets or sets the Bno055 9-axis inertial measurement unit configuration.
+ ///
+ [Category(DevicesCategory)]
+ [TypeConverter(typeof(PolledBno055SingleDeviceFactoryConverter))]
+ [Description("Specifies the configuration for the Bno055 device.")]
+ public ConfigurePolledBno055 Bno055 { get; set; } =
+ new ConfigurePolledBno055 { AxisMap = Bno055AxisMap.ZYX, AxisSign = Bno055AxisSign.MirrorX | Bno055AxisSign.MirrorY | Bno055AxisSign.MirrorZ };
+
+
+ ///
+ /// Gets or sets the port.
+ ///
+ ///
+ /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation.
+ ///
+ [Description("Specifies the physical connection of the miniscope to the ONIX breakout board.")]
+ [Category(ConfigurationCategory)]
+ public PortName Port
+ {
+ get { return port; }
+ set
+ {
+ port = value;
+ var offset = (uint)port << 8;
+ PortControl.DeviceAddress = (uint)port;
+ Camera.DeviceAddress = offset + 0;
+ Bno055.DeviceAddress = offset + 1;
+
+ // Hack: we configure the camera using the port controller below. configuration is super
+ // unreliable, so we do a bunch of retries in the logic PortController logic. We don't want to
+ // reperform configuration in the Camera object after this. So we capture a reference to the
+ // camera here in order to inform it we have already performed configuration.
+ PortControl.Camera = Camera;
+ }
+ }
+
+ ///
+ /// Gets or sets the port voltage override.
+ ///
+ ///
+ ///
+ /// If defined, it will override automated voltage discovery and apply the specified voltage to the miniscope.
+ /// If left blank, an automated headstage detection algorithm will attempt to communicate with the miniscope and
+ /// apply an appropriate voltage for stable operation. Because ONIX allows any coaxial tether to be used, some of
+ /// which are thin enough to result in a significant voltage drop, its may be required to manually specify the
+ /// port voltage.
+ ///
+ ///
+ /// Warning: this device requires 4.0 to 5.0V, measured at the miniscope, for proper operation. Supplying higher
+ /// voltages may result in damage.
+ ///
+ ///
+ [Description("If defined, it will override automated voltage discovery and apply the specified voltage " +
+ "to the miniscope. Warning: this device requires 4.0 to 5.0V, measured at the scope, for proper operation. " +
+ "Supplying higher voltages may result in damage to the miniscope.")]
+ [Category(ConfigurationCategory)]
+ public double? PortVoltage
+ {
+ get => PortControl.PortVoltage;
+ set => PortControl.PortVoltage = value;
+ }
+
+ internal override IEnumerable GetDevices()
+ {
+ yield return PortControl;
+ yield return Camera;
+ yield return Bno055;
+ }
+
+ class ConfigureUclaMiniscopeV4PortController : ConfigurePortController
+ {
+ internal ConfigureUclaMiniscopeV4Camera Camera;
+
+ protected override bool ConfigurePortVoltage(DeviceContext device)
+ {
+ const double MinVoltage = 5.2;
+ const double VoltageIncrement = 0.05;
+
+ for (var voltage = MinVoltage; voltage <= MaxVoltage; voltage += VoltageIncrement)
+ {
+ SetPortVoltage(device, voltage);
+ if (CheckLinkStateWithRetry(device))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ void SetPortVoltage(DeviceContext device, double voltage)
+ {
+ if (voltage > MaxVoltage)
+ {
+ throw new ArgumentException($"The port voltage must be set to a value less than {MaxVoltage} " +
+ $"volts to prevent damage to the miniscope.");
+ }
+
+ const int WaitUntilVoltageSettles = 400;
+ device.WriteRegister(PortController.PORTVOLTAGE, 0);
+ Thread.Sleep(WaitUntilVoltageSettles);
+ device.WriteRegister(PortController.PORTVOLTAGE, (uint)(10 * voltage));
+ Thread.Sleep(WaitUntilVoltageSettles);
+ }
+
+ override protected bool CheckLinkState(DeviceContext device)
+ {
+ var ds90ub9x = device.Context.GetPassthroughDeviceContext(DeviceAddress << 8, typeof(DS90UB9x));
+ ConfigureUclaMiniscopeV4Camera.ConfigureDeserializer(ds90ub9x);
+
+ const int FailureToWriteRegister = -6;
+ try
+ {
+ ConfigureUclaMiniscopeV4Camera.ConfigureCameraSystem(ds90ub9x, Camera.FrameRate, Camera.InterleaveLed);
+ }
+ catch (ONIException ex) when (ex.Number == FailureToWriteRegister)
+ {
+ return false;
+ }
+
+ var linkState = device.ReadRegister(PortController.LINKSTATE);
+ return (linkState & PortController.LINKSTATE_SL) != 0;
+ }
+
+ bool CheckLinkStateWithRetry(DeviceContext device)
+ {
+ const int TotalTries = 10;
+ for (int i = 0; i < TotalTries; i++)
+ {
+ if (CheckLinkState(device))
+ {
+ Camera.Configured = true;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ override protected bool ConfigurePortVoltageOverride(DeviceContext device, double voltage)
+ {
+ const int TotalTries = 3;
+ for (int i = 0; i < TotalTries; i++)
+ {
+ SetPortVoltage(device, voltage);
+ if (CheckLinkStateWithRetry(device))
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/OpenEphys.Onix1/ConfigureUclaMiniscopeV4Camera.cs b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4Camera.cs
new file mode 100644
index 0000000..bd78385
--- /dev/null
+++ b/OpenEphys.Onix1/ConfigureUclaMiniscopeV4Camera.cs
@@ -0,0 +1,358 @@
+using System;
+using System.ComponentModel;
+using System.Drawing.Design;
+using System.Reactive.Disposables;
+using System.Reactive.Subjects;
+using System.Threading;
+using System.Xml.Serialization;
+using Bonsai;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Configures the camera on a UCLA Miniscope V4.
+ ///
+ public class ConfigureUclaMiniscopeV4Camera : SingleDeviceFactory
+ {
+ readonly BehaviorSubject ledBrightness = new(0);
+ readonly BehaviorSubject sensorGain = new(UclaMiniscopeV4SensorGain.Low);
+ readonly BehaviorSubject liquidLensVoltage = new(47); // NB: middle of range
+
+ ///
+ /// Initialize a new instance of a class.
+ ///
+ public ConfigureUclaMiniscopeV4Camera()
+ : base(typeof(UclaMiniscopeV4))
+ {
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the camera will produce image data.
+ ///
+ ///
+ /// If set to true, will produce image data. If set to false, will not produce image data.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("Specifies whether the camera is enabled.")]
+ public bool Enable { get; set; } = true;
+
+ ///
+ /// Gets or sets the camera video rate in frames per second.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("Camera video rate in frames per second.")]
+ public UclaMiniscopeV4FramesPerSecond FrameRate { get; set; } = UclaMiniscopeV4FramesPerSecond.Fps30Hz;
+
+ ///
+ /// Gets or sets the camera sensor's analog gain.
+ ///
+ [Description("Camera sensor analog gain.")]
+ [Category(AcquisitionCategory)]
+ [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
+ public UclaMiniscopeV4SensorGain SensorGain
+ {
+ get => sensorGain.Value;
+ set => sensorGain.OnNext(value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the excitation LED should turn on only when the camera
+ /// shutter is open.
+ ///
+ ///
+ /// If set to true, the excitation LED will turn on briefly before, and turn off briefly after, the
+ /// camera begins photon collection on its photodiode array. If set to false, the excitation LED will
+ /// remain on at all times.
+ ///
+ [Category(ConfigurationCategory)]
+ [Description("Only turn on excitation LED during camera exposures.")]
+ public bool InterleaveLed { get; set; } = false;
+
+ ///
+ /// Gets or sets the excitation LED brightness level (0-100%).
+ ///
+ [Description("Excitation LED brightness level (0-100%).")]
+ [Category(AcquisitionCategory)]
+ [Range(0, 100)]
+ [Precision(1, 1)]
+ [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
+ public double LedBrightness
+ {
+ get => ledBrightness.Value;
+ set => ledBrightness.OnNext(value);
+ }
+
+ ///
+ /// Gets or sets the liquid lens driver voltage (Volts RMS).
+ ///
+ ///
+ /// The imaging focal plane is controlled by using a MAX14574 high-voltage liquid lens driver. This
+ /// chip produces pulse-width modulated, 5 kHz alternative electric field that deforms the miniscope's
+ /// liquid lens in order to change the focal plane. The strength of this field determines the degree
+ /// of deformation and therefore the focal depth. The default setting of 47 Volts RMS corresponds to
+ /// approximately mid-range.
+ ///
+ [Description("Liquid lens driver voltage (Volts RMS).")]
+ [Category(AcquisitionCategory)]
+ [Range(24.4, 69.7)]
+ [Precision(1, 1)]
+ [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))]
+ public double LiquidLensVoltage
+ {
+ get => liquidLensVoltage.Value;
+ set => liquidLensVoltage.OnNext(value);
+ }
+
+ // This is a hack. The hardware is quite unreliable and requires special assistance in order to
+ // operate. We do a lot of retries to make this happen. Once we have successful configuration, redoing
+ // it can cause failure. Therefore, we allow ConfigureMiniscopeV4 to set this variable in order to
+ // skip redundant configuration in Process(). This is not a great solution.
+ [XmlIgnore]
+ internal bool Configured { get; set; } = false;
+
+ ///
+ /// Configures the camera on a UCLA Miniscope V4.
+ ///
+ ///
+ /// This will schedule configuration actions to be applied by a node
+ /// prior to data acquisition.
+ ///
+ /// A sequence of instances that holds all
+ /// configuration actions.
+ ///
+ /// The original sequence but with each instance now containing
+ /// configuration actions required to use the miniscope's camera.
+ ///
+ public override IObservable Process(IObservable source)
+ {
+ var enable = Enable;
+ var deviceName = DeviceName;
+ var deviceAddress = DeviceAddress;
+ var frameRate = FrameRate;
+ var interleaveLed = InterleaveLed;
+
+ return source.ConfigureDevice(context =>
+ {
+ try
+ {
+ // configure device via the DS90UB9x deserializer device
+ var device = context.GetPassthroughDeviceContext(deviceAddress, typeof(DS90UB9x));
+
+ // TODO: early exit if false?
+ device.WriteRegister(DS90UB9x.ENABLE, enable ? 1u : 0);
+
+ // check if configuration was already performed on this object
+ if (!Configured)
+ {
+ ConfigureDeserializer(device);
+ ConfigureCameraSystem(device, frameRate, interleaveLed);
+ }
+
+ var deviceInfo = new DeviceInfo(context, DeviceType, deviceAddress);
+ 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)),
+ DeviceManager.RegisterDevice(deviceName, deviceInfo),
+ shutdown);
+ }
+ finally
+ {
+ // needs to be reconfigured past this point, cannot rely on putting this action in
+ // shutdown disposable since any exception before this function returns will not enforce
+ // this setting
+ Configured = false;
+ }
+ });
+ }
+
+ internal static void ConfigureDeserializer(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 guaranteed to capture every frame at the start of
+ // acquisition. For this reason, the frame start needs to be marked.
+ device.WriteRegister(DS90UB9x.MARK, (uint)DS90UB9xMarkMode.VsyncRising);
+
+ // set I2C clock rate to ~100 kHz
+ var deserializer = new I2CRegisterContext(device, DS90UB9x.DES_ADDR);
+ deserializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.SCLHIGH, 0x7A);
+ deserializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.SCLLOW, 0x7A);
+
+ // configure deserializer I2C aliases
+ 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);
+ }
+
+ internal static void ConfigureCameraSystem(DeviceContext device, UclaMiniscopeV4FramesPerSecond frameRate, bool interleaveLed)
+ {
+ const int WaitUntilPllSettles = 200;
+
+ // set up Python480
+ var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress);
+ WriteCameraRegister(atMega, 16, 3); // Turn on PLL
+ Thread.Sleep(WaitUntilPllSettles);
+ WriteCameraRegister(atMega, 32, 0x7007); // Turn on clock management
+ Thread.Sleep(WaitUntilPllSettles);
+ 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
+
+ // 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(0x03, 0x03);
+
+ // configuration properties
+ uint shutterWidth = frameRate switch
+ {
+ UclaMiniscopeV4FramesPerSecond.Fps10Hz => 10000,
+ UclaMiniscopeV4FramesPerSecond.Fps15Hz => 6667,
+ UclaMiniscopeV4FramesPerSecond.Fps20Hz => 5000,
+ UclaMiniscopeV4FramesPerSecond.Fps25Hz => 4000,
+ UclaMiniscopeV4FramesPerSecond.Fps30Hz => 3300,
+ _ => 3300
+ };
+
+ atMega.WriteByte(0x04, (uint)(interleaveLed ? 0x00 : 0x03));
+ WriteCameraRegister(atMega, 200, shutterWidth);
+ }
+
+ 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 atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress);
+ atMega.WriteByte(0x01, (uint)((percent == 0) ? 0xFF : 0x08));
+
+ var tpl0102 = new I2CRegisterContext(device, UclaMiniscopeV4.Tpl0102Address);
+ tpl0102.WriteByte(0x01, (uint)(255 * ((100 - percent) / 100.0)));
+ }
+
+ internal static void SetSensorGain(DeviceContext device, UclaMiniscopeV4SensorGain gain)
+ {
+ var atMega = new I2CRegisterContext(device, UclaMiniscopeV4.AtMegaAddress);
+ WriteCameraRegister(atMega, 204, (uint)gain);
+ }
+
+ internal static void SetLiquidLensVoltage(DeviceContext device, double voltage)
+ {
+ var max14574 = new I2CRegisterContext(device, 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))
+ {
+ }
+ }
+ }
+
+ ///
+ /// Specifies analog gain of the Python-480 image sensor on a UCLA Miniscope V4.
+ ///
+ public enum UclaMiniscopeV4SensorGain
+ {
+ ///
+ /// Specifies low gain.
+ ///
+ Low = 0x00E1,
+
+ ///
+ /// Specifies medium gain.
+ ///
+ Medium = 0x00E4,
+
+ ///
+ /// Specifies high gain.
+ ///
+ High = 0x0024,
+ }
+
+ ///
+ /// Specifies the video frame rate of the Python-480 image sensor on a UCLA Miniscope V4.
+ ///
+ public enum UclaMiniscopeV4FramesPerSecond
+ {
+ ///
+ /// Specifies 10 frames per second.
+ ///
+ Fps10Hz,
+
+ ///
+ /// Specifies 15 frames per second.
+ ///
+ Fps15Hz,
+
+ ///
+ /// Specifies 20 frames per second.
+ ///
+ Fps20Hz,
+
+ ///
+ /// Specifies 25 frames per second.
+ ///
+ Fps25Hz,
+
+ ///
+ /// Specifies 30 frames per second.
+ ///
+ Fps30Hz,
+ }
+}
diff --git a/OpenEphys.Onix1/UclaMiniscopeV4CameraData.cs b/OpenEphys.Onix1/UclaMiniscopeV4CameraData.cs
new file mode 100644
index 0000000..7baaabb
--- /dev/null
+++ b/OpenEphys.Onix1/UclaMiniscopeV4CameraData.cs
@@ -0,0 +1,123 @@
+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.Onix1
+{
+ ///
+ /// Produces a sequence of s from the Python-480 image sensor on a
+ /// UCLA Miniscope V4.
+ ///
+ public class UclaMiniscopeV4CameraData : Source
+ {
+ ///
+ [TypeConverter(typeof(UclaMiniscopeV4.NameConverter))]
+ [Description(SingleDeviceFactory.DeviceNameDescription)]
+ [Category(DeviceFactory.ConfigurationCategory)]
+ public string DeviceName { get; set; }
+
+ ///
+ /// Gets or sets the data type used to represent pixel intensity values.
+ ///
+ ///
+ /// The UCLA Miniscope V4 uses a 10-bit image sensor. To capture images that use the full
+ /// ADC resolution, this value can be set to .
+ /// This comes at the cost of limited codec support and larger file sizes. If is selected, the two least significant bits of
+ /// each pixel sample will be discarded, which greatly increases codec options and reduces
+ /// file sizes.
+ ///
+ [Description("The bit-depth used to represent pixel intensity values.")]
+ [Category(DeviceFactory.ConfigurationCategory)]
+ public UclaMiniscopeV4ImageDepth DataType { get; set; } = UclaMiniscopeV4ImageDepth.U8;
+
+ ///
+ /// Generates a sequence of s at a rate determined by .
+ ///
+ /// A sequence of s
+ public unsafe override IObservable Generate()
+ {
+ return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo =>
+ {
+ var device = deviceInfo.GetDeviceContext(typeof(UclaMiniscopeV4));
+ var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x));
+ var scopeData = device.Context.GetDeviceFrames(passthrough.Address);
+ var dataType = DataType;
+
+ return Observable.Create(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 sampleRect = new Rect(0, 1, UclaMiniscopeV4.SensorColumns, UclaMiniscopeV4.SensorRows - 1);
+
+ var frameObserver = Observer.Create(
+ frame =>
+ {
+ var payload = (UclaMiniscopeV4ImagerPayload*)frame.Data.ToPointer();
+
+ // Wait for first row
+ if (sampleIndex == 0 && (payload->ImageRow[0] & 0x8000) == 0)
+ return;
+
+ 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, -32768f); // Get rid first row's mark bit
+
+ switch (dataType)
+ {
+ case UclaMiniscopeV4ImageDepth.U8:
+ {
+ var eightBitImageData = new Mat(imageData.Size, Depth.U8, 1);
+ CV.ConvertScale(imageData, eightBitImageData, 0.25);
+ observer.OnNext(new UclaMiniscopeV4CameraFrame(clockBuffer, hubClockBuffer, eightBitImageData.GetImage()));
+ break;
+ }
+ case UclaMiniscopeV4ImageDepth.U10:
+ {
+ observer.OnNext(new UclaMiniscopeV4CameraFrame(clockBuffer, hubClockBuffer, imageData.GetImage()));
+ break;
+ }
+ }
+
+ hubClockBuffer = new ulong[UclaMiniscopeV4.SensorRows];
+ clockBuffer = new ulong[UclaMiniscopeV4.SensorRows];
+ sampleIndex = 0;
+ }
+ },
+ observer.OnError,
+ observer.OnCompleted);
+
+ return scopeData.SubscribeSafe(frameObserver);
+ });
+ });
+ }
+
+ ///
+ /// Specifies the bit-depth used to represent pixel intensity values.
+ ///
+ public enum UclaMiniscopeV4ImageDepth
+ {
+ ///
+ /// 8-bit pixel values encoded as bytes.
+ ///
+ U8,
+ ///
+ /// 10-bit pixel values encoded as unsigned 16-bit integers
+ ///
+ U10
+ }
+ }
+}
diff --git a/OpenEphys.Onix1/UclaMiniscopeV4CameraFrame.cs b/OpenEphys.Onix1/UclaMiniscopeV4CameraFrame.cs
new file mode 100644
index 0000000..5b3f2ca
--- /dev/null
+++ b/OpenEphys.Onix1/UclaMiniscopeV4CameraFrame.cs
@@ -0,0 +1,35 @@
+using System.Runtime.InteropServices;
+using OpenCV.Net;
+
+namespace OpenEphys.Onix1
+{
+ ///
+ /// Image data produced by the Python-480 CMOS image sensor on a UCLA Miniscope V4.
+ ///
+ public class UclaMiniscopeV4CameraFrame : BufferedDataFrame
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An array of values.
+ /// An array of hub clock counter values.
+ /// A image produced by the Python-480 on a UCLA Miniscope V4.
+ public UclaMiniscopeV4CameraFrame(ulong[] clock, ulong[] hubClock, IplImage image)
+ : base (clock, hubClock)
+ {
+ Image = image;
+ }
+
+ ///
+ /// Gets the 608x608 pixel, 10-bit, monochrome image.
+ ///
+ public IplImage Image { get; }
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ unsafe struct UclaMiniscopeV4ImagerPayload
+ {
+ public ulong HubClock;
+ public fixed short ImageRow[UclaMiniscopeV4.SensorColumns];
+ }
+}