Skip to content

Commit ded8683

Browse files
authored
Merge pull request #442 from open-ephys/issue-384-rhs2116
Add support for stimulus trigger events from headstage-rhs2116
2 parents 1086513 + fab27d8 commit ded8683

10 files changed

+320
-26
lines changed

OpenEphys.Onix1/ConfigureRhs2116.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ public override IObservable<ContextTask> Process(IObservable<ContextTask> source
187187
device.WriteRegister(Rhs2116.BW1, regs[3] << 6 | regs[2]);
188188
device.WriteRegister(Rhs2116.FASTSETTLESAMPLES, Rhs2116Config.AnalogHighCutoffToFastSettleSamples[newValue]);
189189
}),
190+
respectExternalActiveStim.SubscribeSafe(observer, newValue =>
191+
{
192+
device.WriteRegister(Rhs2116.RESPECTSTIMACTIVE, newValue ? 1u : 0u);
193+
}),
190194
DeviceManager.RegisterDevice(deviceName, device, DeviceType)
191195
);
192196
});

OpenEphys.Onix1/ConfigureRhs2116Pair.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ static void ConfigureChip(DeviceContext device, bool enable, Rhs2116DspCutoff ds
149149
// consistency
150150
device.WriteRegister(Rhs2116.FORMAT, format);
151151
device.WriteRegister(Rhs2116.ENABLE, enable ? 1u : 0);
152+
153+
// Force each device to respect each other's stimulation state since they are being
154+
// treated, de-facto, as a single chip.
155+
device.WriteRegister(Rhs2116.RESPECTSTIMACTIVE, 1u);
152156
}
153157

154158
ConfigureChip(rhs2116A, enable, dspCutoff);

OpenEphys.Onix1/ConfigureRhs2116Trigger.cs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@
1010
namespace OpenEphys.Onix1
1111
{
1212
/// <summary>
13-
/// Configures an ONIX RHS 2116 Trigger device.
13+
/// Configures an Rhs2116 trigger device.
1414
/// </summary>
1515
/// <remarks>
16-
/// The RHS2116 Trigger device generates triggers for Intan RHS2116 bioamplifier and stimulator chip(s)
17-
/// either from a remote source via external SYNC pin or locally via GPIO or TRIGGER register. This
18-
/// device can be used to synchronize stimulus application and recovery across chips.
16+
/// The Rhs2116 Trigger device generates triggers for Intan Rhs2116 bioamplifier and stimulator chip(s)
17+
/// either from a remote source via external SYNC pin or locally via GPIO or TRIGGER register. This device
18+
/// can be used to synchronize stimulus application and recovery across chips. This configuration operator
19+
/// can be linked to a data IO operator, such as <see cref="Rhs2116TriggerData"/>, using a shared
20+
/// <c>DeviceName</c>.
1921
/// </remarks>
2022
[Editor("OpenEphys.Onix1.Design.Rhs2116StimulusSequenceEditor, OpenEphys.Onix1.Design", typeof(ComponentEditor))]
2123
public class ConfigureRhs2116Trigger : SingleDeviceFactory
2224
{
2325
readonly BehaviorSubject<Rhs2116StimulusSequencePair> stimulusSequence = new(new Rhs2116StimulusSequencePair());
26+
readonly BehaviorSubject<bool> triggerArmed = new(true);
2427

2528
/// <summary>
2629
/// Initializes a new instance of the <see cref="ConfigureRhs2116Trigger"/> class.
@@ -30,6 +33,19 @@ public ConfigureRhs2116Trigger()
3033
{
3134
}
3235

36+
/// <summary>
37+
/// Gets or sets the device enable state.
38+
/// </summary>
39+
/// <remarks>
40+
/// If set to true, a <see cref="Rhs2116TriggerData"/> instance that is linked to this configuration
41+
/// will produce data. If set to false, it will not produce data. Note that this does not affect the
42+
/// ability of the device to trigger stimuli, but only affects if trigger event information is
43+
/// streamed back from the device. To disable the trigger see the <see cref="Armed"/> property.
44+
/// </remarks>
45+
[Category(ConfigurationCategory)]
46+
[Description("Specifies whether the stimulus trigger device will stream stimulus delivery information.")]
47+
public bool Enable { get; set; } = true;
48+
3349
/// <summary>
3450
/// Gets or sets the trigger source.
3551
/// </summary>
@@ -52,6 +68,21 @@ public ConfigureRhs2116Trigger()
5268
[Description("Defines the channel configuration")]
5369
public Rhs2116ProbeGroup ProbeGroup { get; set; } = new();
5470

71+
/// <summary>
72+
/// Gets or sets if trigger is armed.
73+
/// </summary>
74+
/// <remarks>
75+
/// If true, this device will respect triggers from the selected <see cref="TriggerSource"/>.
76+
/// Otherwise, triggers will be ignored.
77+
/// </remarks>
78+
[Category(AcquisitionCategory)]
79+
[Description("If true, respect triggers. Otherwise, triggers will not be applied.")]
80+
public bool Armed
81+
{
82+
get => triggerArmed.Value;
83+
set => triggerArmed.OnNext(value);
84+
}
85+
5586
/// <summary>
5687
/// Gets or sets a string defining the <see cref="ProbeGroup"/> in Base64.
5788
/// </summary>
@@ -88,7 +119,7 @@ public Rhs2116StimulusSequencePair StimulusSequence
88119
}
89120

90121
/// <summary>
91-
/// Configures an RHS2116 Trigger device.
122+
/// Configures an Rhs2116 Trigger device.
92123
/// </summary>
93124
/// <remarks>
94125
/// This will schedule configuration actions to be applied by a <see cref="StartAcquisition"/> node
@@ -97,10 +128,11 @@ public Rhs2116StimulusSequencePair StimulusSequence
97128
/// <param name="source">A sequence of <see cref="ContextTask"/> that holds all configuration actions.</param>
98129
/// <returns>
99130
/// The original sequence with the side effect of an additional configuration action to configure
100-
/// aN RHS2116 Trigger device.
131+
/// aN Rhs2116 Trigger device.
101132
/// </returns>
102133
public override IObservable<ContextTask> Process(IObservable<ContextTask> source)
103134
{
135+
var enable = Enable;
104136
var triggerSource = TriggerSource;
105137
var deviceName = DeviceName;
106138
var deviceAddress = DeviceAddress;
@@ -113,6 +145,7 @@ public override IObservable<ContextTask> Process(IObservable<ContextTask> source
113145
var rhs2116B = context.GetDeviceContext(rhs2116BAddress, typeof(Rhs2116));
114146

115147
var device = context.GetDeviceContext(deviceAddress, DeviceType);
148+
device.WriteRegister(Rhs2116Trigger.ENABLE, enable ? 1u : 0u);
116149
device.WriteRegister(Rhs2116Trigger.TRIGGERSOURCE, (uint)triggerSource);
117150

118151
static void WriteStimulusSequence(DeviceContext device, Rhs2116StimulusSequence sequence)
@@ -164,6 +197,10 @@ static void WriteStimulusSequence(DeviceContext device, Rhs2116StimulusSequence
164197
WriteStimulusSequence(rhs2116A, newValue.StimulusSequenceA);
165198
WriteStimulusSequence(rhs2116B, newValue.StimulusSequenceB);
166199
}),
200+
triggerArmed.SubscribeSafe(observer, newValue =>
201+
{
202+
device.WriteRegister(Rhs2116Trigger.TRIGGERARMED, newValue ? 1u : 0u);
203+
}),
167204
DeviceManager.RegisterDevice(deviceName, device, DeviceType));
168205
});
169206
}
@@ -174,9 +211,11 @@ static class Rhs2116Trigger
174211
public const int ID = 32;
175212

176213
// managed registers
177-
public const uint ENABLE = 0; // Writes and reads to ENABLE are ignored without error
214+
public const uint ENABLE = 0; // Enable or disable the trigger event datastream
178215
public const uint TRIGGERSOURCE = 1; // The LSB is used to determine the trigger source
179216
public const uint TRIGGER = 2; // Writing 0x1 to this register will trigger a stimulation sequence if the TRIGGERSOURCE is set to 0.
217+
public const uint TRIGGERARMED = 3; // 0x0: Ignore all trigger inputs regardless of TRIGGERSOURCE.
218+
// 0x1: Respect the trigger input specified by TRIGGERSOURCE
180219

181220
internal class NameConverter : DeviceNameConverter
182221
{

OpenEphys.Onix1/Rhs2116Data.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public unsafe override IObservable<Rhs2116DataFrame> Generate()
5050
var device = deviceInfo.GetDeviceContext(typeof(Rhs2116));
5151
var amplifierBuffer = new short[Rhs2116.AmplifierChannelCount * bufferSize];
5252
var dcBuffer = new short[Rhs2116.AmplifierChannelCount * bufferSize];
53+
var recovery = new ushort[bufferSize];
5354
var hubClockBuffer = new ulong[bufferSize];
5455
var clockBuffer = new ulong[bufferSize];
5556

@@ -61,12 +62,14 @@ public unsafe override IObservable<Rhs2116DataFrame> Generate()
6162
Marshal.Copy(new IntPtr(payload->DCData), dcBuffer, sampleIndex * Rhs2116.AmplifierChannelCount, Rhs2116.AmplifierChannelCount);
6263
hubClockBuffer[sampleIndex] = payload->HubClock;
6364
clockBuffer[sampleIndex] = frame.Clock;
65+
recovery[sampleIndex] = payload->Recovery;
6466
if (++sampleIndex >= bufferSize)
6567
{
6668
var amplifierData = BufferHelper.CopyTranspose(amplifierBuffer, bufferSize, Rhs2116.AmplifierChannelCount, Depth.U16);
6769
var dcData = BufferHelper.CopyTranspose(dcBuffer, bufferSize, Rhs2116.AmplifierChannelCount, Depth.U16);
68-
observer.OnNext(new Rhs2116DataFrame(clockBuffer, hubClockBuffer, amplifierData, dcData));
70+
observer.OnNext(new Rhs2116DataFrame(clockBuffer, hubClockBuffer, amplifierData, dcData, recovery));
6971
hubClockBuffer = new ulong[bufferSize];
72+
recovery = new ushort[bufferSize];
7073
clockBuffer = new ulong[bufferSize];
7174
sampleIndex = 0;
7275
}

OpenEphys.Onix1/Rhs2116DataFrame.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
using System.Runtime.InteropServices;
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Specialized;
4+
using System.Numerics;
5+
using System.Runtime.InteropServices;
26
using OpenCV.Net;
37

48
namespace OpenEphys.Onix1
59
{
610
/// <summary>
7-
/// Buffered data from one or more Rhs2116 devices.
11+
/// Buffered data from a Rhs2116 device.
812
/// </summary>
913
public class Rhs2116DataFrame : BufferedDataFrame
1014
{
@@ -15,19 +19,21 @@ public class Rhs2116DataFrame : BufferedDataFrame
1519
/// <param name="hubClock">An array of hub clock counter values.</param>
1620
/// <param name="amplifierData">An array of multi-channel amplifier data.</param>
1721
/// <param name="dcData">An array of multi-channel DC data.</param>
18-
public Rhs2116DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat dcData)
22+
/// <param name="recoveryMask">An array of stimulus artifact recovery mask values.</param>
23+
public Rhs2116DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat dcData, ushort[] recoveryMask)
1924
: base(clock, hubClock)
2025
{
2126
AmplifierData = amplifierData;
2227
DCData = dcData;
28+
RecoveryMask = recoveryMask;
2329
}
2430

2531
/// <summary>
2632
/// Gets the high-gain electrophysiology data array.
2733
/// </summary>
2834
/// <remarks>
29-
/// Electrophysiology samples are organized in MxN matrix with M rows representing electrophysiology
30-
/// channel number and N columns representing sample index. Each column is a M-channel vector of ADC
35+
/// Electrophysiology samples are organized in a 16xN matrix with 16 rows representing electrophysiology
36+
/// channel and N columns representing sample index. Each column is a 16-channel vector of ADC
3137
/// samples whose acquisition time is indicated by the corresponding elements in <see
3238
/// cref="DataFrame.Clock"/> and <see cref="DataFrame.HubClock"/>. Each ADC sample is an 16-bit,
3339
/// offset binary value encoded as a <see cref="ushort"/>. The following equation can be used to
@@ -42,8 +48,8 @@ public Rhs2116DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat
4248
/// Gets the DC-coupled, low-gain amplifier data array for monitoring stimulation waveforms.
4349
/// </summary>
4450
/// <remarks>
45-
/// DC-coupled samples are organized in MxN matrix with M rows representing electrophysiology
46-
/// channel number and N columns representing sample index. Each column is a M-channel vector of ADC
51+
/// DC-coupled samples are organized in 16xN matrix with 16 rows representing electrophysiology
52+
/// channel and N columns representing sample index. Each column is a 16-channel vector of ADC
4753
/// samples whose acquisition time is indicated by the corresponding elements in <see
4854
/// cref="DataFrame.Clock"/> and <see cref="DataFrame.HubClock"/>. Each ADC sample is an 10-bit,
4955
/// offset binary value encoded as a <see cref="ushort"/>. The following equation can be used to
@@ -53,6 +59,20 @@ public Rhs2116DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat
5359
/// </code>
5460
/// </remarks>
5561
public Mat DCData { get; }
62+
63+
/// <summary>
64+
/// Gets the stimulus artifact recovery mask array.
65+
/// </summary>
66+
/// <remarks>
67+
/// During and following stimulus pulses, electrophysiology amplifiers can enter an artifact recovery
68+
/// mode which discharges the electrode and applies an aggressive high-pass filter to prevent
69+
/// amplifier saturation (see <see cref="ConfigureRhs2116.AnalogLowCutoffRecovery"/>). This 1xN vector
70+
/// provides a per-sample bit mask indicating if artifact recovery mode is active on each of the 16
71+
/// electrophysiology channels. The bit position in each value indicates the channel number and the
72+
/// logical state indicates if recovery is active. For instance a value of 0x0011 would indicate that
73+
/// channels 0 and 4 were in artifact recovery mode and all other channels were in normal operation.
74+
/// </remarks>
75+
public ushort[] RecoveryMask { get; }
5676
}
5777

5878
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -61,6 +81,6 @@ unsafe struct Rhs2116Payload
6181
public ulong HubClock;
6282
public fixed ushort AmplifierData[Rhs2116.AmplifierChannelCount];
6383
public fixed ushort DCData[Rhs2116.AmplifierChannelCount];
64-
public short Pad;
84+
public ushort Recovery;
6585
}
6686
}

OpenEphys.Onix1/Rhs2116PairData.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
namespace OpenEphys.Onix1
1111
{
1212
/// <summary>
13-
/// Produces a sequence of <see cref="Rhs2116DataFrame"/> objects from a synchronized pair of Intan
13+
/// Produces a sequence of <see cref="Rhs2116PairDataFrame"/> objects from a synchronized pair of Intan
1414
/// Rhs2116 bidirectional bioacquisition chips.
1515
/// chips.
1616
/// </summary>
1717
/// <remarks>
1818
/// This data IO operator must be linked to an appropriate configuration, such as a <see
1919
/// cref="ConfigureRhs2116Pair"/>, using a shared <c>DeviceName</c>.
2020
/// </remarks>
21-
public class Rhs2116PairData : Source<Rhs2116DataFrame>
21+
public class Rhs2116PairData : Source<Rhs2116PairDataFrame>
2222
{
2323
/// <inheritdoc cref = "SingleDeviceFactory.DeviceName"/>
2424
[TypeConverter(typeof(Rhs2116Pair.NameConverter))]
@@ -31,27 +31,28 @@ public class Rhs2116PairData : Source<Rhs2116DataFrame>
3131
/// This property determines the number of samples that are collected from each of the 32
3232
/// electrophysiology channels before data is propagated. For instance, if this value is set to 30,
3333
/// then 32x30 samples, along with 30 corresponding clock values, will be collected and packed into
34-
/// each <see cref="Rhs2116DataFrame"/>. Because channels are sampled at ~30 kHz, this is equivalent
34+
/// each <see cref="Rhs2116PairDataFrame"/>. Because channels are sampled at ~30 kHz, this is equivalent
3535
/// to ~1 millisecond of data from each channel.
3636
/// </remarks>
3737
public int BufferSize { get; set; } = 30;
3838

3939
/// <summary>
40-
/// Generates a sequence of <see cref="Rhs2116DataFrame"/>s.
40+
/// Generates a sequence of <see cref="Rhs2116PairDataFrame"/>s.
4141
/// </summary>
42-
/// <returns>A sequence of <see cref="Rhs2116DataFrame"/>s.</returns>
43-
public unsafe override IObservable<Rhs2116DataFrame> Generate()
42+
/// <returns>A sequence of <see cref="Rhs2116PairDataFrame"/>s.</returns>
43+
public unsafe override IObservable<Rhs2116PairDataFrame> Generate()
4444
{
4545
var bufferSize = BufferSize;
4646
return DeviceManager.GetDevice(DeviceName).SelectMany(
47-
deviceInfo => Observable.Create<Rhs2116DataFrame>(observer =>
47+
deviceInfo => Observable.Create<Rhs2116PairDataFrame>(observer =>
4848
{
4949
var sampleIndex = 0;
5050
var dualInfo = (Rhs2116PairDeviceInfo)deviceInfo;
5151
var rhs2116A = dualInfo.Rhs2116A;
5252
var rhs2116B = dualInfo.Rhs2116B;
5353
var amplifierBuffer = new short[Rhs2116Pair.TotalChannels * bufferSize];
5454
var dcBuffer = new short[Rhs2116Pair.TotalChannels * bufferSize];
55+
var recovery = new uint[bufferSize];
5556
var hubClockBuffer = new ulong[bufferSize];
5657
var clockBuffer = new ulong[bufferSize];
5758

@@ -64,24 +65,27 @@ public unsafe override IObservable<Rhs2116DataFrame> Generate()
6465
var payload = (Rhs2116Payload*)frame.Data.ToPointer();
6566
Marshal.Copy(new IntPtr(payload->AmplifierData), amplifierBuffer, offset, Rhs2116.AmplifierChannelCount);
6667
Marshal.Copy(new IntPtr(payload->DCData), dcBuffer, offset, Rhs2116.AmplifierChannelCount);
68+
recovery[sampleIndex] |= payload->Recovery;
6769

6870
if (++sampleIndex >= bufferSize)
6971
{
7072
var amplifierData = BufferHelper.CopyTranspose(amplifierBuffer, bufferSize, Rhs2116Pair.TotalChannels, Depth.U16);
7173
var dcData = BufferHelper.CopyTranspose(dcBuffer, bufferSize, Rhs2116Pair.TotalChannels, Depth.U16);
72-
observer.OnNext(new Rhs2116DataFrame(clockBuffer, hubClockBuffer, amplifierData, dcData));
74+
observer.OnNext(new Rhs2116PairDataFrame(clockBuffer, hubClockBuffer, amplifierData, dcData, recovery));
7375
hubClockBuffer = new ulong[bufferSize];
76+
recovery = new uint[bufferSize];
7477
clockBuffer = new ulong[bufferSize];
7578
sampleIndex = 0;
7679
}
7780

78-
} else
81+
} else // Chip B
7982
{
8083
var offset = sampleIndex * Rhs2116Pair.TotalChannels + Rhs2116Pair.ChannelsPerChip;
8184
var payload = (Rhs2116Payload*)frame.Data.ToPointer();
8285
Marshal.Copy(new IntPtr(payload->AmplifierData), amplifierBuffer, offset, Rhs2116.AmplifierChannelCount);
8386
Marshal.Copy(new IntPtr(payload->DCData), dcBuffer, offset, Rhs2116.AmplifierChannelCount);
8487
hubClockBuffer[sampleIndex] = payload->HubClock;
88+
recovery[sampleIndex] |= (uint)payload->Recovery << 16;
8589
clockBuffer[sampleIndex] = frame.Clock;
8690
}
8791

0 commit comments

Comments
 (0)