Skip to content

Commit f9f1133

Browse files
committed
Add support for load tester device
- Modify the existing configuration operator since it had implementation issues - Add frame reader and frame writer operators and support classes
1 parent e24f6db commit f9f1133

File tree

6 files changed

+232
-43
lines changed

6 files changed

+232
-43
lines changed

OpenEphys.Onix1/ConfigureLoadTester.cs

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
using System;
22
using System.ComponentModel;
3-
using System.Linq;
4-
using System.Reactive.Disposables;
5-
using System.Reactive.Subjects;
63
using Bonsai;
74

85
namespace OpenEphys.Onix1
96
{
10-
// TODO: Add data IO operators, update XML comment to link to them (<see cref="LoadTesterData"/>)
117
/// <summary>
12-
/// Configures a load tester device.
8+
/// Configures a load tester device for measuring system performance.
139
/// </summary>
1410
/// <remarks>
15-
/// This configuration operator can be linked to a data IO operator, such as LoadTesterData,
16-
/// using a shared <c>DeviceName</c>. The load tester device can be configured
17-
/// to produce data at user-settable size and rate to stress test various communication links and test
18-
/// closed-loop response latency.
11+
/// This configuration operator can be linked to a data IO operator, such as <see cref="LoadTesterData"/>,
12+
/// using a shared <c>DeviceName</c>. The load tester device can be configured to produce and consume data
13+
/// at user-defined sizes and rates to stress test various communication links and measure closed-loop
14+
/// response latency using a high-resolution hardware timer.
1915
/// </remarks>
2016
[Description("Configures a load testing device.")]
2117
public class ConfigureLoadTester : SingleDeviceFactory
2218
{
23-
readonly BehaviorSubject<uint> frameHz = new(1000);
24-
2519
/// <summary>
2620
/// Initializes a new instance of the <see cref="ConfigureLoadTester"/> class.
2721
/// </summary>
@@ -38,31 +32,37 @@ public ConfigureLoadTester()
3832
public bool Enable { get; set; } = false;
3933

4034
/// <summary>
41-
/// Gets or sets the number of repetitions of the 16-bit unsigned integer 42 sent with each read-frame.
35+
/// Gets or sets the number of repetitions incrementing, unsigned 16-bit integers sent with each read frame.
4236
/// </summary>
37+
/// <remarks>
38+
/// These data are produced by the controller and are used to impose a load the controller to host
39+
/// communication. These data can be used in downstream computational operations that model the
40+
/// computational load imposed by a closed-loop algorithm.
41+
/// </remarks>
4342
[Category(ConfigurationCategory)]
4443
[Description("Number of repetitions of the 16-bit unsigned integer 42 sent with each read-frame.")]
4544
[Range(0, 10e6)]
4645
public uint ReceivedWords { get; set; }
4746

4847
/// <summary>
49-
/// Gets or sets the number of repetitions of the 32-bit integer 42 sent with each write frame.
48+
/// Gets or sets the number of repetitions of the 32-bit integer dummy words sent with each write
49+
/// frame.
5050
/// </summary>
51+
/// <remarks>
52+
/// These data are produced by the host and are used to impose a load on host to controller
53+
/// commutation. They are discarded by the controller when they are received.
54+
/// </remarks>
5155
[Category(ConfigurationCategory)]
5256
[Description("Number of repetitions of the 32-bit integer 42 sent with each write frame.")]
5357
[Range(0, 10e6)]
5458
public uint TransmittedWords { get; set; }
5559

5660
/// <summary>
57-
/// Gets or sets a value specifying the rate at which frames are produced, in Hz.
61+
/// Gets or sets a value specifying the rate at which frames are produced in Hz.
5862
/// </summary>
59-
[Category(AcquisitionCategory)]
63+
[Category(ConfigurationCategory)]
6064
[Description("Specifies the rate at which frames are produced (Hz).")]
61-
public uint FramesPerSecond
62-
{
63-
get { return frameHz.Value; }
64-
set { frameHz.OnNext(value); }
65-
}
65+
public uint FramesPerSecond { get; set; }
6666

6767
/// <summary>
6868
/// Configures a load testing device.
@@ -83,40 +83,38 @@ public override IObservable<ContextTask> Process(IObservable<ContextTask> source
8383
var deviceAddress = DeviceAddress;
8484
var receivedWords = ReceivedWords;
8585
var transmittedWords = TransmittedWords;
86+
var framesPerSecond = FramesPerSecond;
87+
8688
return source.ConfigureDevice((context, observer) =>
8789
{
8890
var device = context.GetDeviceContext(deviceAddress, DeviceType);
8991
device.WriteRegister(LoadTester.ENABLE, enable ? 1u : 0u);
9092

9193
var clockHz = device.ReadRegister(LoadTester.CLK_HZ);
9294

93-
// Assumes 8-byte timer
94-
uint ValidSize()
95+
const int OverheadCycles = 9; // 4 cycles to produce hub clock, and 5 state machine overhead per the datasheet
96+
97+
var maxFramesPerSecond = clockHz / OverheadCycles;
98+
if (framesPerSecond > maxFramesPerSecond)
9599
{
96-
var clkDiv = device.ReadRegister(LoadTester.CLK_DIV);
97-
return clkDiv - 4 - 10; // -10 is overhead hack
100+
throw new ArgumentOutOfRangeException(nameof(FramesPerSecond), $"{nameof(FramesPerSecond)} must be less than {maxFramesPerSecond}.");
98101
}
99102

100-
var maxSize = ValidSize();
101-
var bounded = receivedWords > maxSize ? maxSize : receivedWords;
102-
device.WriteRegister(LoadTester.DT0H16_WORDS, bounded);
103+
device.WriteRegister(LoadTester.CLK_DIV, clockHz / framesPerSecond);
103104

104-
var writeArray = Enumerable.Repeat((uint)42, (int)(transmittedWords + 2)).ToArray();
105-
device.WriteRegister(LoadTester.HTOD32_WORDS, transmittedWords);
106-
var frameHzSubscription = frameHz.SubscribeSafe(observer, newValue =>
105+
var maxSize = device.ReadRegister(LoadTester.CLK_DIV) - OverheadCycles;
106+
107+
if (receivedWords > maxSize)
107108
{
108-
device.WriteRegister(LoadTester.CLK_DIV, clockHz / newValue);
109-
var maxSize = ValidSize();
110-
if (receivedWords > maxSize)
111-
{
112-
receivedWords = maxSize;
113-
}
114-
});
115-
116-
return new CompositeDisposable(
117-
DeviceManager.RegisterDevice(deviceName, device, DeviceType),
118-
frameHzSubscription
119-
);
109+
throw new ArgumentOutOfRangeException(nameof(ReceivedWords),
110+
$"{nameof(ReceivedWords)} must be less than {maxSize} for the requested frame rate of {framesPerSecond} Hz.");
111+
}
112+
113+
device.WriteRegister(LoadTester.DT0H16_WORDS, receivedWords);
114+
device.WriteRegister(LoadTester.HTOD32_WORDS, transmittedWords);
115+
116+
var deviceInfo = new LoadTesterDeviceInfo(context, DeviceType, deviceAddress, ReceivedWords, TransmittedWords);
117+
return DeviceManager.RegisterDevice(deviceName, deviceInfo);
120118
});
121119
}
122120
}
@@ -132,12 +130,13 @@ static class LoadTester
132130
public const uint DT0H16_WORDS = 3; // Number of repetitions of 16-bit unsigned integer 42 sent with each frame.
133131
// Note: max here depends of CLK_HZ and CLK_DIV. There needs to be enough clock
134132
// cycles to push the data at the requested CLK_HZ. Specifically,
135-
// CLK_HZ / CLK_DIV >= TX16_WORDS + 9. Going above this will result in
133+
// CLK_HZ / CLK_DIV >= DT0H16_WORDS + 9. Going above this will result in
136134
// decreased bandwidth as samples will be skipped.
137135
public const uint HTOD32_WORDS = 4; // Number of 32-bit words in a write-frame. All write frame data is ignored except
138136
// the first 64-bits, which are looped back into the device to host data frame for
139137
// testing loop latency. This value must be at least 2.
140138

139+
141140
internal class NameConverter : DeviceNameConverter
142141
{
143142
public NameConverter()

OpenEphys.Onix1/LoadTesterData.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 heartbeat data frames.
11+
/// </summary>
12+
/// <remarks>
13+
/// This data IO operator must be linked to an appropriate configuration, such as a <see
14+
/// cref="ConfigureHeartbeat"/>, using a shared <c>DeviceName</c>.
15+
/// </remarks>
16+
[Description("Produces a sequence of load tester data frames.")]
17+
public class LoadTesterData : Source<LoadTesterDataFrame>
18+
{
19+
/// <inheritdoc cref = "SingleDeviceFactory.DeviceName"/>
20+
[TypeConverter(typeof(LoadTester.NameConverter))]
21+
[Description(SingleDeviceFactory.DeviceNameDescription)]
22+
[Category(DeviceFactory.ConfigurationCategory)]
23+
public string DeviceName { get; set; }
24+
25+
/// <summary>
26+
/// Generates a sequence of <see cref="LoadTesterDataFrame"/> objects, each of which contains period signal from the
27+
/// acquisition system indicating that it is active.
28+
/// </summary>
29+
/// <returns>A sequence of <see cref="LoadTesterDataFrame"/> objects.</returns>
30+
public override IObservable<LoadTesterDataFrame> Generate()
31+
{
32+
return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo =>
33+
{
34+
var info = (LoadTesterDeviceInfo)deviceInfo;
35+
var device = info.GetDeviceContext(typeof(LoadTester));
36+
return deviceInfo.Context
37+
.GetDeviceFrames(device.Address)
38+
.Select(frame => new LoadTesterDataFrame(frame, info.ReceivedWords));
39+
});
40+
}
41+
}
42+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace OpenEphys.Onix1
5+
{
6+
/// <summary>
7+
/// A load tester data frame.
8+
/// </summary>
9+
public class LoadTesterDataFrame : DataFrame
10+
{
11+
/// <summary>
12+
/// Gets the difference between the hub clock and the loopback value at the moment the loopback
13+
/// value was received.
14+
/// </summary>
15+
/// <remarks>
16+
/// This value is the result of subtracting the loopback value written by the <see
17+
/// cref="LoadTesterLoopback"/> operator from the device's hub clock counter value at the moment that
18+
/// the loop back value was received. Typically, the <see cref="DataFrame.HubClock"/> member is sent
19+
/// to <see cref="LoadTesterLoopback"/> operator such that this value is a hardware-timed measurement
20+
/// value of real-time latency. This value is only updated when a new loopback value is sent to the
21+
/// device using the <see cref="LoadTesterLoopback"/> operator.
22+
/// </remarks>
23+
public ulong HubClockDelta { get; }
24+
25+
/// <summary>
26+
/// Gets the counter array.
27+
/// </summary>
28+
/// <remarks>
29+
/// This is a <see cref="ConfigureLoadTester.ReceivedWords"/>-long array of incrementing integers that
30+
/// is used for simulating the bandwidth of physical data sources.
31+
/// </remarks>
32+
public ushort[] Counter { get; }
33+
34+
/// <summary>
35+
/// Initializes a new instance of the <see cref="LoadTesterDataFrame"/> class.
36+
/// </summary>
37+
/// <param name="frame">A data frame produced by a load tester device.</param>
38+
/// <param name="receivedWords">The number of counter words that appear at the end of the load test
39+
/// data frame. This number is determined by the value of <see
40+
/// cref="ConfigureLoadTester.ReceivedWords"/>.</param>
41+
public unsafe LoadTesterDataFrame(oni.Frame frame, uint receivedWords)
42+
: base(frame.Clock)
43+
{
44+
var payload = (LoadTesterPayload*)frame.Data.ToPointer();
45+
var counterPtr = (ushort*)((byte*)payload + sizeof(LoadTesterPayload));
46+
47+
HubClock = payload->HubClock;
48+
HubClockDelta = payload->HubClockDelta;
49+
Counter = new Span<ushort>(counterPtr, (int)receivedWords).ToArray();
50+
}
51+
}
52+
53+
[StructLayout(LayoutKind.Sequential, Pack = 1)]
54+
struct LoadTesterPayload
55+
{
56+
public ulong HubClock;
57+
public ulong HubClockDelta;
58+
// NB: The ushort Counter array may or may not reside here. Its size is determined by ReceivedWords.
59+
}
60+
}
61+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace OpenEphys.Onix1
4+
{
5+
class LoadTesterDeviceInfo : DeviceInfo
6+
{
7+
public LoadTesterDeviceInfo(ContextTask context, Type deviceType, uint deviceAddress, uint receivedWords, uint transmittedWords)
8+
: base(context, deviceType, deviceAddress)
9+
{
10+
ReceivedWords = receivedWords;
11+
TransmittedWords = transmittedWords;
12+
}
13+
14+
public uint ReceivedWords { get; }
15+
16+
public uint TransmittedWords { get; }
17+
}
18+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
10+
/// <summary>
11+
/// Sends loopback data to the load testing device for closed-loop latency measurement.
12+
/// </summary>
13+
/// <remarks>
14+
/// This data IO operator must be linked to an appropriate configuration, such as a <see
15+
/// cref="ConfigureLoadTester"/>, using a shared <c>DeviceName</c>.
16+
/// </remarks>
17+
[Description("Sends analog output data to an ONIX breakout board.")]
18+
public class LoadTesterLoopback : Sink<ulong>
19+
{
20+
/// <inheritdoc cref = "SingleDeviceFactory.DeviceName"/>
21+
[TypeConverter(typeof(LoadTester.NameConverter))]
22+
[Description(SingleDeviceFactory.DeviceNameDescription)]
23+
[Category(DeviceFactory.ConfigurationCategory)]
24+
public string DeviceName { get; set; }
25+
26+
/// <summary>
27+
/// Creates and sends an loopback frame to the load testing device.
28+
/// </summary>
29+
/// <remarks>
30+
/// A loopback frame consists the <c>ulong</c> loopback value provided by <paramref name="source"/>
31+
/// that is prepended to a <see cref="ConfigureLoadTester.TransmittedWords"/>-element <c>ushort</c>
32+
/// array of dummy data. When the frame is received by hardware, the loopback value is subtracted from
33+
/// the current hub clock count on the load testing device and stored. Therefore, if the loopback
34+
/// value is is that of a previous <see cref="DataFrame.HubClock"/> from the <see
35+
/// cref="LoadTesterData"/> with the same <see cref="DeviceName"/> as this operator, this difference will provide a
36+
/// hardware-timed measurement of real-time latency. The the variably-sized dummy data in the loopback
37+
/// frame is used for testing the effect of increasing the frame size, and thus the write
38+
/// communication bandwidth, on real-time latency.
39+
/// </remarks>
40+
/// <param name="source">A sequence of loopback values to send to the device</param>
41+
/// <returns> A sequence of loopback values to send to the device.</returns>
42+
public unsafe override IObservable<ulong> Process(IObservable<ulong> source)
43+
{
44+
return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo =>
45+
{
46+
var info = (LoadTesterDeviceInfo)deviceInfo;
47+
var device = info.GetDeviceContext(typeof(LoadTester));
48+
var transmittedWords = info.TransmittedWords;
49+
50+
if (transmittedWords > 0)
51+
{
52+
var payload = new uint[transmittedWords + 2];
53+
54+
return source.Do(loopbackValue =>
55+
{
56+
payload[0] = (uint)loopbackValue;
57+
payload[1] = (uint)(loopbackValue >> 32);
58+
device.Write(payload);
59+
});
60+
}
61+
else
62+
{
63+
return source.Do(device.Write);
64+
}
65+
});
66+
}
67+
}
68+
}

OpenEphys.Onix1/OpenEphys.Onix1.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
<PackageReference Include="Microsoft.Bcl.Numerics" Version="9.0.7" />
1717
<PackageReference Include="OpenCV.Net" Version="3.4.2" />
1818
<PackageReference Include="OpenEphys.ProbeInterface.NET" Version="0.3.0" />
19+
<PackageReference Include="System.Memory" Version="4.6.3" />
1920
</ItemGroup>
2021
</Project>

0 commit comments

Comments
 (0)