Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UCLA miniscope V4 support #275

Merged
merged 18 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions OpenEphys.Onix1/Bno055DataFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ internal unsafe Bno055DataFrame(ulong clock, Bno055DataPayload* payload)
/// <summary>
/// Gets the 3D orientation in Euler angle format with units of degrees.
/// </summary>
/// <remark>
/// <remarks>
/// The Tait-Bryan formalism is used:
/// <list type="bullet">
/// <item><description>Yaw: 0 to 360 degrees.</description></item>
/// <item><description>Roll: -180 to 180 degrees</description></item>
/// <item><description>Pitch: -90 to 90 degrees</description></item>
/// </list>
/// </remark>
/// </remarks>
public Vector3 EulerAngle { get; }

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion OpenEphys.Onix1/ConfigurePortController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
198 changes: 198 additions & 0 deletions OpenEphys.Onix1/ConfigureUclaMiniscopeV4.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using oni;

namespace OpenEphys.Onix1
{
/// <summary>
/// Configures a UCLA Miniscope V4 on the specified port.
/// </summary>
/// <remarks>
/// The UCLA Miniscope V4 is a e miniaturized fluorescent microscope for performing single-photon calcium
jonnew marked this conversation as resolved.
Show resolved Hide resolved
/// imaging in freely moving animals. It has the following features:
/// <list type="bullet">
/// <item><description>A Python-480 0.48 Megapixel CMOS image sensor.</description></item>
/// <item><description>A BNO055 9-axis IMU for real-time, 3D orientation tracking.</description></item>
/// <item><description>An electrowetting lens for remote focal plane adjustment.</description></item>
/// <item><description>An excitation LED with adjustable brightness control and optional exposure-driven
/// interleaving to reduce photobleaching.</description></item>
/// </list>
/// </remarks>
public class ConfigureUclaMiniscopeV4 : MultiDeviceFactory
{

jonnew marked this conversation as resolved.
Show resolved Hide resolved
const double MaxVoltage = 5.6;

PortName port;
readonly ConfigureUclaMiniscopeV4PortController PortControl = new();

/// <summary>
/// Initialize a new instance of a <see cref="ConfigureUclaMiniscopeV4"/> class.
/// </summary>
public ConfigureUclaMiniscopeV4()
{
Port = PortName.PortA;
PortControl.HubConfiguration = HubConfiguration.Passthrough;
}

/// <summary>
/// Gets or sets the Miniscope camera configuration.
/// </summary>
[Category(DevicesCategory)]
[TypeConverter(typeof(SingleDeviceFactoryConverter))]
public ConfigureUclaMiniscopeV4Camera Camera { get; set; } = new();

/// <summary>
/// Gets or sets the Bno055 9-axis inertial measurement unit configuration.
/// </summary>
[Category(DevicesCategory)]
[TypeConverter(typeof(SingleDeviceFactoryConverter))]
public ConfigureUclaMiniscopeV4Bno055 Bno055 { get; set; } = new();

/// <summary>
/// Gets or sets the port.
/// </summary>
/// <remarks>
/// The port is the physical connection to the ONIX breakout board and must be specified prior to operation.
/// </remarks>
[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;
}
}

/// <summary>
/// Gets or sets the port voltage override.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// Warning: this device requires 4.0 to 5.0V, measured at the miniscope, for proper operation. Supplying higher
/// voltages may result in damage.
/// </para>
/// </remarks>
[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.")]
jonnew marked this conversation as resolved.
Show resolved Hide resolved
[Category(ConfigurationCategory)]
public double? PortVoltage
{
get => PortControl.PortVoltage;
set => PortControl.PortVoltage = value;
}

internal override IEnumerable<IDeviceConfiguration> 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;
}
}
}
}
104 changes: 104 additions & 0 deletions OpenEphys.Onix1/ConfigureUclaMiniscopeV4Bno055.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.ComponentModel;

namespace OpenEphys.Onix1
{
/// <summary>
/// Configures the Bno055 inertial measurement unit (IMU) on a UCLA Miniscope V4.
/// </summary>
public class ConfigureUclaMiniscopeV4Bno055 : SingleDeviceFactory
{
/// <summary>
/// Initialize a new instance of a <see cref="ConfigureUclaMiniscopeV4Bno055"/> class.
/// </summary>
public ConfigureUclaMiniscopeV4Bno055()
: base(typeof(UclaMiniscopeV4Bno055))
{
}

/// <summary>
/// Gets or sets the device enable state.
/// </summary>
/// <remarks>
/// If set to true, <see cref="UclaMiniscopeV4Bno055Data"/> will produce data. If set to false,
/// <see cref="UclaMiniscopeV4Bno055Data"/> will not produce data.
/// </remarks>
[Category(ConfigurationCategory)]
[Description("Specifies whether the BNO055 device is enabled.")]
public bool Enable { get; set; } = true;

/// <summary>
/// Configures the Bno055 inertial measurement unit (IMU) on a UCLA Miniscope V4.
/// </summary>
/// <remarks>
/// This will schedule configuration actions to be applied by a <see cref="StartAcquisition"/> node
/// prior to data acquisition.
/// </remarks>
/// <param name="source">A sequence of <see cref="ContextTask"/> instances that holds all configuration actions.</param>
/// <returns>
/// The original sequence but with each <see cref="ContextTask"/> instance now containing configuration actions required to use the miniscope's Bno055 IMU.
/// </returns>
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, typeof(DS90UB9x));
ConfigureDeserializer(device);
ConfigureBno055(device);
var deviceInfo = new UclaMiniscopeV4Bno055DeviceInfo(context, DeviceType, deviceAddress, enable);
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);
}

// TODO: This can fail, pretty randomly, during configuration causing the failure to write
// register and workflow to terminate. Not sure why, likely due to hardware design.
static void ConfigureBno055(DeviceContext device)
{
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))
{
}
}
}

class UclaMiniscopeV4Bno055DeviceInfo : DeviceInfo
{
public UclaMiniscopeV4Bno055DeviceInfo(ContextTask context, Type deviceType, uint deviceAddress, bool enable)
: base(context, deviceType, deviceAddress)
{
Enable = enable;
}

public bool Enable { get; }
}
}
Loading
Loading