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]; + } +}