Skip to content

Commit da6ef61

Browse files
authored
Merge pull request #504 from open-ephys/issue-384
Add support for stimulus reports on headstage-64
2 parents a2b1bde + debe53f commit da6ef61

9 files changed

+439
-46
lines changed

OpenEphys.Onix1/ConfigureHeadstage64.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ protected override bool ConfigurePortVoltage(DeviceContext device, out double vo
167167
// between cycles to fix.
168168
const double MinVoltage = 3.3;
169169
const double MaxVoltage = 6.0;
170-
const double VoltageOffset = 3.4;
170+
const double VoltageOffset = 4.0;
171171
const double VoltageIncrement = 0.2;
172172

173173
// NB: Wait for 1 second to discharge the headstage in the case that they have e.g. just

OpenEphys.Onix1/ConfigureHeadstage64ElectricalStimulator.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ public ConfigureHeadstage64ElectricalStimulator()
2222
{
2323
}
2424

25+
/// <summary>
26+
/// Gets or sets the data enable state.
27+
/// </summary>
28+
/// <remarks>
29+
/// If set to true, <see cref="Headstage64ElectricalStimulatorData"/> will produce data. If set to
30+
/// false, <see cref="Headstage64ElectricalStimulatorData"/> will not produce data.
31+
/// </remarks>
32+
[Category(ConfigurationCategory)]
33+
[Description("Specifies whether the headstage-64 electrical stimulator will produce stimulus reports.")]
34+
public bool Enable { get; set; }
35+
2536
/// <summary>
2637
/// Configure a headstage-64 onboard electrical stimulator.
2738
/// </summary>
@@ -37,10 +48,12 @@ public override IObservable<ContextTask> Process(IObservable<ContextTask> source
3748
{
3849
var deviceName = DeviceName;
3950
var deviceAddress = DeviceAddress;
51+
var enable = Enable;
4052
return source.ConfigureDevice(context =>
4153
{
4254
var device = context.GetDeviceContext(deviceAddress, DeviceType);
43-
device.WriteRegister(Headstage64ElectricalStimulator.ENABLE, 0);
55+
device.WriteRegister(Headstage64ElectricalStimulator.ENABLE, enable ? 1u : 0u);
56+
device.WriteRegister(Headstage64ElectricalStimulator.STIMENABLE, 0);
4457
return DeviceManager.RegisterDevice(deviceName, device, DeviceType);
4558
});
4659
}
@@ -55,7 +68,7 @@ static class Headstage64ElectricalStimulator
5568
public const double AbsMaxMicroAmps = 2500;
5669

5770
// managed registers
58-
public const uint NULLPARM = 0; // No command
71+
public const uint ENABLE = 0; // Enable stimulus report stream
5972
public const uint BIPHASIC = 1; // Biphasic pulse (0 = monophasic, 1 = biphasic; NB: currently ignored)
6073
public const uint CURRENT1 = 2; // Phase 1 current
6174
public const uint CURRENT2 = 3; // Phase 2 current
@@ -69,11 +82,23 @@ static class Headstage64ElectricalStimulator
6982
public const uint TRAINDELAY = 11; // Pulse train delay, microseconds
7083
public const uint TRIGGER = 12; // Trigger stimulation (1 = deliver)
7184
public const uint POWERON = 13; // Control estim sub-circuit power (0 = off, 1 = on)
72-
public const uint ENABLE = 14; // If 0 then stimulation triggers will be ignored, otherwise they will be applied
85+
public const uint STIMENABLE = 14; // If 0 then stimulation triggers will be ignored, otherwise they will be applied
7386
public const uint RESTCURR = 15; // Resting current between pulse phases
7487
public const uint RESET = 16; // Reset all parameters to default
7588
public const uint REZ = 17; // Internal DAC resolution in bits
7689

90+
internal static uint MicroampsToCode(double currentuA)
91+
{
92+
var k = 1 / (2 * AbsMaxMicroAmps / (Math.Pow(2, DacBitDepth) - 1)); // NB: constexpr, if we get support for it.
93+
return (uint)(k * (currentuA + AbsMaxMicroAmps));
94+
}
95+
96+
internal static double CodeToMicroamps(uint code)
97+
{
98+
var k = 2 * AbsMaxMicroAmps / (Math.Pow(2, DacBitDepth) - 1); // NB: constexpr, if we get support for it.
99+
return k * code - AbsMaxMicroAmps;
100+
}
101+
77102
internal class NameConverter : DeviceNameConverter
78103
{
79104
public NameConverter()

OpenEphys.Onix1/ConfigureHeadstage64OpticalStimulator.cs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
namespace OpenEphys.Onix1
55
{
66
/// <summary>
7-
/// Configures a headstage-64 dual-channel optical stimulator.
7+
/// Configures a headstage-64 optical stimulator.
88
/// </summary>
99
/// <remarks>
1010
/// This configuration operator can be linked to a data IO operator, such as <see
1111
/// cref="Headstage64OpticalStimulatorTrigger"/>, using a shared
1212
/// <c>DeviceName</c>.
1313
/// </remarks>
14-
[Description("Configures a headstage-64 dual-channel optical stimulator.")]
14+
[Description("Configures a headstage-64 optical stimulator.")]
1515
public class ConfigureHeadstage64OpticalStimulator : SingleDeviceFactory
1616
{
1717
/// <summary>
@@ -22,6 +22,17 @@ public ConfigureHeadstage64OpticalStimulator()
2222
{
2323
}
2424

25+
/// <summary>
26+
/// Gets or sets the data enable state.
27+
/// </summary>
28+
/// <remarks>
29+
/// If set to true, <see cref="Headstage64OpticalStimulatorData"/> will produce data. If set to
30+
/// false, <see cref="Headstage64OpticalStimulatorData"/> will not produce data.
31+
/// </remarks>
32+
[Category(ConfigurationCategory)]
33+
[Description("Specifies whether the headstage-64 optical stimulator will produce stimulus reports.")]
34+
public bool Enable { get; set; }
35+
2536
/// <summary>
2637
/// Configure a headstage-64 dual-channel optical stimulator.
2738
/// </summary>
@@ -37,10 +48,12 @@ public override IObservable<ContextTask> Process(IObservable<ContextTask> source
3748
{
3849
var deviceName = DeviceName;
3950
var deviceAddress = DeviceAddress;
51+
var enable = Enable;
4052
return source.ConfigureDevice(context =>
4153
{
4254
var device = context.GetDeviceContext(deviceAddress, DeviceType);
43-
device.WriteRegister(Headstage64OpticalStimulator.ENABLE, 0);
55+
device.WriteRegister(Headstage64OpticalStimulator.ENABLE, enable ? 1u : 0u);
56+
device.WriteRegister(Headstage64OpticalStimulator.STIMENABLE, 0u);
4457
return DeviceManager.RegisterDevice(deviceName, device, DeviceType);
4558
});
4659
}
@@ -55,7 +68,7 @@ static class Headstage64OpticalStimulator
5568
public const uint PotResistanceOhms = 100_000;
5669

5770
// managed registers
58-
public const uint NULLPARM = 0; // No command
71+
public const uint ENABLE = 0; // Enable stimulus report stream
5972
public const uint MAXCURRENT = 1; // Max LED/LD current, (0 to 255 = 800mA to 0 mA.See fig XX of CAT4016 datasheet)
6073
public const uint PULSEMASK = 2; // Bitmask determining which of the(up to 32) channels is affected by trigger
6174
public const uint PULSEDUR = 3; // Pulse duration, microseconds
@@ -65,12 +78,29 @@ static class Headstage64OpticalStimulator
6578
public const uint TRAINCOUNT = 7; // Number of bursts in train
6679
public const uint TRAINDELAY = 8; // Stimulus start delay, microseconds
6780
public const uint TRIGGER = 9; // Trigger stimulation (0 = off, 1 = deliver)
68-
public const uint ENABLE = 10; // 1: enables the stimulator, 0: stimulator ignores triggers (so that a common trigger can be used)
81+
public const uint STIMENABLE = 10; // 1: enables the stimulator, 0: stimulator ignores triggers (so that a common trigger can be used)
6982
public const uint RESTMASK = 11; // Bitmask determining the off state of the up to 32 current channels
7083
public const uint RESET = 12; // None If 1, Reset all parameters to default (not implemented)
7184
public const uint MINRHEOR = 13; // The series resistor between the potentiometer (rheostat) and RSET bin on the CAT4016
7285
public const uint POTRES = 14; // The resistance value of the potentiometer connected in rheostat config to RSET on CAT4016
7386

87+
// NB: fit from Fig. 10 of CAT4016 datasheet
88+
// x = (y/a)^(1/b)
89+
// a = 3.833e+05
90+
// b = -0.9632
91+
internal static uint MilliampsToPotSetting(double currentMa)
92+
{
93+
double R = Math.Pow(currentMa / 3.833e+05, 1 / -0.9632);
94+
uint s = (uint)Math.Round(256 * (R - MinRheostatResistanceOhms) / PotResistanceOhms);
95+
return s > 255 ? 255 : s < 0 ? 0 :s;
96+
}
97+
98+
internal static double PotSettingToMilliamps(uint potSetting)
99+
{
100+
var R = MinRheostatResistanceOhms + PotResistanceOhms * potSetting / 256;
101+
return 3.833e+05 * Math.Pow(R, -0.9632);
102+
}
103+
74104
internal class NameConverter : DeviceNameConverter
75105
{
76106
public NameConverter()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Linq;
4+
using System.Reactive.Linq;
5+
using Bonsai;
6+
7+
namespace OpenEphys.Onix1
8+
{
9+
/// <summary>
10+
/// Produces a sequence of <see
11+
/// cref="Headstage64ElectricalStimulatorDataFrame">Headstage64ElectricalStimulatorDataFrames</see>
12+
/// objects indicating the time and parameters of stimuli.
13+
/// </summary>
14+
/// <remarks>
15+
/// This data IO operator must be linked to an appropriate configuration, such as a <see
16+
/// cref="ConfigureHeadstage64ElectricalStimulator"/>, using a shared <c>DeviceName</c>.
17+
/// </remarks>
18+
[Description("Produces a sequence of stimulus reports containing time, trigger origin, and parameters of stimuli delivered by the headstage-64 onboard electrical stimulator.")]
19+
public class Headstage64ElectricalStimulatorData : Source<Headstage64ElectricalStimulatorDataFrame>
20+
{
21+
/// <inheritdoc cref = "SingleDeviceFactory.DeviceName"/>
22+
[TypeConverter(typeof(Headstage64ElectricalStimulator.NameConverter))]
23+
[Description(SingleDeviceFactory.DeviceNameDescription)]
24+
[Category(DeviceFactory.ConfigurationCategory)]
25+
public string DeviceName { get; set; }
26+
27+
/// <summary>
28+
/// Generates a sequence of <see
29+
/// cref="Headstage64ElectricalStimulatorDataFrame">Headstage64ElectricalStimulatorDataFrames</see>.
30+
/// </summary>
31+
/// <returns>A sequence of <see
32+
/// cref="Headstage64ElectricalStimulatorDataFrame">Headstage64ElectricalStimulatorDataFrames</see>.</returns>
33+
public override IObservable<Headstage64ElectricalStimulatorDataFrame> Generate()
34+
{
35+
return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo =>
36+
{
37+
var device = deviceInfo.GetDeviceContext(typeof(Headstage64ElectricalStimulator));
38+
return deviceInfo.Context
39+
.GetDeviceFrames(device.Address)
40+
.Select(frame => new Headstage64ElectricalStimulatorDataFrame(frame));
41+
});
42+
}
43+
}
44+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace OpenEphys.Onix1
5+
{
6+
/// <summary>
7+
/// A headstage-64 electrical stimulus report.
8+
/// </summary>
9+
/// <remarks>
10+
/// These frames provide synchronized information about the stimulus timing, trigger source, and stimulus
11+
/// parameters.
12+
/// </remarks>
13+
public class Headstage64ElectricalStimulatorDataFrame : DataFrame
14+
{
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="Headstage64ElectricalStimulatorDataFrame"/> class.
17+
/// </summary>
18+
/// <param name="frame">An ONI frame containing a electrical stimulus report.</param>
19+
public unsafe Headstage64ElectricalStimulatorDataFrame(oni.Frame frame)
20+
: base(frame.Clock)
21+
{
22+
var payload = (Headstage64ElectricalStimulatorPayload*)frame.Data.ToPointer();
23+
HubClock = payload->HubClock;
24+
Origin = (Headstage64StimulatorTriggerOrigin)(payload->DelayAndOrigin & 0x000F);
25+
Delay = (payload->DelayAndOrigin & 0xFFF0) >> 8;
26+
RestCurrent = Headstage64ElectricalStimulator.CodeToMicroamps(payload->RestCurrent);
27+
PhaseOneCurrent = Headstage64ElectricalStimulator.CodeToMicroamps(payload->PhaseOneCurrent);
28+
PhaseTwoCurrent = Headstage64ElectricalStimulator.CodeToMicroamps(payload->PhaseTwoCurrent);
29+
PhaseOneDuration = payload->PhaseOneDuration;
30+
InterPhaseInterval = payload->InterPhaseInterval;
31+
PhaseTwoDuration = payload->PhaseTwoDuration;
32+
InterPulseInterval = payload->InterPulseInterval;
33+
PulsesPerBurst = payload->PulsesPerBurst;
34+
InterBurstInterval = payload->InterBurstInterval;
35+
BurstsPerTrain = payload->BurstsPerTrain;
36+
}
37+
38+
/// <summary>
39+
/// Gets the stimulus trigger origin.
40+
/// </summary>
41+
public Headstage64StimulatorTriggerOrigin Origin {get; }
42+
43+
/// <summary>
44+
/// Gets the delay, in microseconds, from the time of trigger receipt (the <see
45+
/// cref="DataFrame.HubClock"/> value) to the physical application of the stimulus sequence.
46+
/// </summary>
47+
public uint Delay { get; }
48+
49+
/// <summary>
50+
/// Gets the rest current in microamps.
51+
/// </summary>
52+
public double RestCurrent { get; }
53+
54+
/// <summary>
55+
/// Gets the phase one current in microamps.
56+
/// </summary>
57+
public double PhaseOneCurrent { get; }
58+
59+
/// <summary>
60+
/// Gets the phase two current in microamps.
61+
/// </summary>
62+
public double PhaseTwoCurrent { get; }
63+
64+
/// <summary>
65+
/// Gets the phase one duration in microseconds.
66+
/// </summary>
67+
public uint PhaseOneDuration { get; }
68+
69+
/// <summary>
70+
/// Gets the inter-phase interval duration in microseconds.
71+
/// </summary>
72+
public uint InterPhaseInterval { get; }
73+
74+
/// <summary>
75+
/// Gets the phase two duration in microseconds.
76+
/// </summary>
77+
public uint PhaseTwoDuration { get; }
78+
79+
/// <summary>
80+
/// Gets the inter-pulse interval duration in microseconds.
81+
/// </summary>
82+
public uint InterPulseInterval { get; }
83+
84+
/// <summary>
85+
/// Gets the number of pulses per burst.
86+
/// </summary>
87+
public uint PulsesPerBurst { get; }
88+
89+
/// <summary>
90+
/// Gets the inter-burst interval duration in microseconds.
91+
/// </summary>
92+
public uint InterBurstInterval { get; }
93+
94+
/// <summary>
95+
/// Gets the number of bursts per train.
96+
/// </summary>
97+
public uint BurstsPerTrain { get; }
98+
99+
}
100+
101+
[StructLayout(LayoutKind.Sequential, Pack = 1)]
102+
unsafe struct Headstage64ElectricalStimulatorPayload
103+
{
104+
public ulong HubClock;
105+
public uint DelayAndOrigin;
106+
public uint RestCurrent;
107+
public uint PhaseOneCurrent;
108+
public uint PhaseTwoCurrent;
109+
public uint PhaseOneDuration;
110+
public uint InterPhaseInterval;
111+
public uint PhaseTwoDuration;
112+
public uint InterPulseInterval;
113+
public uint PulsesPerBurst;
114+
public uint InterBurstInterval;
115+
public uint BurstsPerTrain;
116+
}
117+
118+
/// <summary>
119+
/// Specifies the origin of the trigger.
120+
/// </summary>
121+
[Flags]
122+
public enum Headstage64StimulatorTriggerOrigin : byte
123+
{
124+
/// <summary>
125+
/// Specifies the source of the trigger is unknown.
126+
/// </summary>
127+
Unknown = 0,
128+
129+
/// <summary>
130+
/// Specifies the source of the trigger is a local Gpio pin.
131+
/// </summary>
132+
Gpio = 0x1,
133+
134+
/// <summary>
135+
/// Specifies the source of the trigger is a register.
136+
/// </summary>
137+
Register = 0x2,
138+
}
139+
}

OpenEphys.Onix1/Headstage64ElectricalStimulatorTrigger.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,11 @@ public override IObservable<bool> Process(IObservable<bool> source)
233233
observer.OnError,
234234
observer.OnCompleted);
235235

236-
static uint uAToCode(double currentuA)
237-
{
238-
var k = 1 / (2 * Headstage64ElectricalStimulator.AbsMaxMicroAmps / (Math.Pow(2, Headstage64ElectricalStimulator.DacBitDepth) - 1)); // static
239-
return (uint)(k * (currentuA + Headstage64ElectricalStimulator.AbsMaxMicroAmps));
240-
}
241-
242236
return new CompositeDisposable(
243-
enable.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.ENABLE, value ? 1u : 0u)),
244-
phaseOneCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT1, uAToCode(value))),
245-
interPhaseCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.RESTCURR, uAToCode(value))),
246-
phaseTwoCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT2, uAToCode(value))),
237+
enable.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.STIMENABLE, value ? 1u : 0u)),
238+
phaseOneCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT1, Headstage64ElectricalStimulator.MicroampsToCode(value))),
239+
interPhaseCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.RESTCURR, Headstage64ElectricalStimulator.MicroampsToCode(value))),
240+
phaseTwoCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT2, Headstage64ElectricalStimulator.MicroampsToCode(value))),
247241
triggerDelay.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.TRAINDELAY, value)),
248242
phaseOneDuration.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.PULSEDUR1, value)),
249243
interPhaseInterval.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.INTERPHASEINTERVAL, value)),

0 commit comments

Comments
 (0)