diff --git a/Bonsai/Bonsai.config b/.bonsai/Bonsai.config similarity index 77% rename from Bonsai/Bonsai.config rename to .bonsai/Bonsai.config index 851577f4..f859f006 100644 --- a/Bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -1,19 +1,19 @@  - - - + + + - + - + - + - - + + @@ -25,7 +25,12 @@ + + + + + @@ -45,19 +50,19 @@ - - - + + + - + - + - + - - + + @@ -67,11 +72,16 @@ + + + + + diff --git a/Bonsai/NuGet.config b/.bonsai/NuGet.config similarity index 100% rename from Bonsai/NuGet.config rename to .bonsai/NuGet.config diff --git a/Bonsai/Setup.cmd b/.bonsai/Setup.cmd similarity index 100% rename from Bonsai/Setup.cmd rename to .bonsai/Setup.cmd diff --git a/Bonsai/Setup.ps1 b/.bonsai/Setup.ps1 similarity index 100% rename from Bonsai/Setup.ps1 rename to .bonsai/Setup.ps1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..04f1da99 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,91 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + release: + types: [published] +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + ContinuousIntegrationBuild: true + CiRunNumber: ${{ github.run_number }} + CiRunPushSuffix: ${{ github.ref_name }}-ci${{ github.run_number }} + CiRunPullSuffix: pull-${{ github.event.number }}-ci${{ github.run_number }} +jobs: + setup: + runs-on: ubuntu-latest + outputs: + build-suffix: ${{ steps.setup-build.outputs.build-suffix }} + steps: + - name: Setup Build + id: setup-build + run: echo "build-suffix=${{ github.event_name == 'push' && env.CiRunPushSuffix || github.event_name == 'pull_request' && env.CiRunPullSuffix || null }}" >> "$GITHUB_OUTPUT" + + build: + needs: [setup] + strategy: + fail-fast: false + matrix: + configuration: [debug, release] + os: [ubuntu-latest, windows-latest] + include: + - os: windows-latest + configuration: release + collect-packages: true + runs-on: ${{ matrix.os }} + env: + CiBuildVersionSuffix: ${{ needs.setup.outputs.build-suffix }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration ${{ matrix.configuration }} + + - name: Pack + id: pack + if: matrix.collect-packages + run: dotnet pack --no-build --configuration ${{ matrix.configuration }} + + - name: Collect packages + uses: actions/upload-artifact@v4 + if: matrix.collect-packages && steps.pack.outcome == 'success' && always() + with: + name: Packages + if-no-files-found: error + path: artifacts/package/${{matrix.configuration}}/** + + publish-github: + runs-on: ubuntu-latest + permissions: + packages: write + needs: [build] + if: github.event_name == 'push' || github.event_name == 'release' + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.x + + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: Packages + + - name: Push to GitHub Packages + run: dotnet nuget push "Packages/*.nupkg" --skip-duplicate --no-symbols --api-key ${{secrets.GITHUB_TOKEN}} --source https://nuget.pkg.github.com/${{github.repository_owner}} + env: + # This is a workaround for https://github.com/NuGet/Home/issues/9775 + DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER: 0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6c985e7b..0bd1fb74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ -.vs -bin -obj -Packages -*.user -*.exe -*.exe.settings -*.exe.WebView2 \ No newline at end of file +.vs/ +/artifacts/ +.bonsai/Packages/ +.bonsai/*.exe +.bonsai/*.exe.settings +.bonsai/*.exe.WebView2/ \ No newline at end of file diff --git a/OpenEphys.Onix/Directory.Build.props b/Directory.Build.props similarity index 50% rename from OpenEphys.Onix/Directory.Build.props rename to Directory.Build.props index 57999231..64baf9c1 100644 --- a/OpenEphys.Onix/Directory.Build.props +++ b/Directory.Build.props @@ -3,23 +3,27 @@ Open Ephys Copyright © Open Ephys and Contributors 2024 - snupkg + https://open-ephys.github.io/onix1-bonsai-docs true - ..\bin\$(Configuration) - - true - true - + snupkg + true + https://github.com/open-ephys/onix-bonsai-onix1 git + README.md LICENSE + true icon.png + 0.1.0 9.0 strict + + - - + + + \ No newline at end of file diff --git a/OpenEphys.Onix/NuGet.config b/NuGet.config similarity index 100% rename from OpenEphys.Onix/NuGet.config rename to NuGet.config diff --git a/OpenEphys.Onix/OpenEphys.Onix.sln b/OpenEphys.Onix/OpenEphys.Onix.sln deleted file mode 100644 index e63035c6..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix.sln +++ /dev/null @@ -1,56 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.3.32825.248 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenEphys.Onix", "OpenEphys.Onix\OpenEphys.Onix.csproj", "{353B1EBC-F8EB-4D99-8331-9FF15EC17F38}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenEphys.Onix.Design", "OpenEphys.Onix.Design\OpenEphys.Onix.Design.csproj", "{149E86EC-B865-463D-81A8-8290CA7F8871}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F8644FAC-94E5-4E73-B809-925ABABE35B1}" - ProjectSection(SolutionItems) = preProject - Directory.Build.props = Directory.Build.props - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|ARM64 = Debug|ARM64 - Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU - Release|ARM64 = Release|ARM64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|Any CPU.ActiveCfg = Debug|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|Any CPU.Build.0 = Debug|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|ARM64.ActiveCfg = Debug|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|ARM64.Build.0 = Debug|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|x64.ActiveCfg = Debug|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|x64.Build.0 = Debug|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|Any CPU.ActiveCfg = Release|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|Any CPU.Build.0 = Release|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|ARM64.ActiveCfg = Release|ARM64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|ARM64.Build.0 = Release|ARM64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|x64.ActiveCfg = Release|x64 - {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|x64.Build.0 = Release|x64 - {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|Any CPU.Build.0 = Debug|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|ARM64.Build.0 = Debug|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|x64.ActiveCfg = Debug|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|x64.Build.0 = Debug|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|Any CPU.ActiveCfg = Release|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|Any CPU.Build.0 = Release|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|ARM64.ActiveCfg = Release|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|ARM64.Build.0 = Release|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|x64.ActiveCfg = Release|Any CPU - {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|x64.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {86554706-612A-4283-B0DC-5477B01D58B6} - EndGlobalSection -EndGlobal diff --git a/OpenEphys.Onix/OpenEphys.Onix/AnalogInput.cs b/OpenEphys.Onix/OpenEphys.Onix/AnalogInput.cs deleted file mode 100644 index dc037fbb..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/AnalogInput.cs +++ /dev/null @@ -1,87 +0,0 @@ -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 AnalogInput : Source - { - [TypeConverter(typeof(AnalogIO.NameConverter))] - public string DeviceName { get; set; } - - public int BufferSize { get; set; } = 100; - - public AnalogIODataType DataType { get; set; } = AnalogIODataType.S16; - - static Mat CreateVoltageScale(int bufferSize, float[] voltsPerDivision) - { - using var scaleHeader = Mat.CreateMatHeader( - voltsPerDivision, - rows: voltsPerDivision.Length, - cols: 1, - depth: Depth.F32, - channels: 1); - var voltageScale = new Mat(scaleHeader.Rows, bufferSize, scaleHeader.Depth, scaleHeader.Channels); - CV.Repeat(scaleHeader, voltageScale); - return voltageScale; - } - - public unsafe override IObservable Generate() - { - var bufferSize = BufferSize; - var dataType = DataType; - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - Observable.Create(observer => - { - var device = deviceInfo.GetDeviceContext(typeof(AnalogIO)); - var ioDeviceInfo = (AnalogIODeviceInfo)deviceInfo; - - var sampleIndex = 0; - var voltageScale = dataType == AnalogIODataType.Volts - ? CreateVoltageScale(bufferSize, ioDeviceInfo.VoltsPerDivision) - : null; - var transposeBuffer = voltageScale != null - ? new Mat(AnalogIO.ChannelCount, bufferSize, Depth.S16, 1) - : null; - var analogDataBuffer = new short[AnalogIO.ChannelCount * bufferSize]; - var hubSyncCounterBuffer = new ulong[bufferSize]; - var clockBuffer = new ulong[bufferSize]; - - var frameObserver = Observer.Create( - frame => - { - var payload = (AnalogInputPayload*)frame.Data.ToPointer(); - Marshal.Copy(new IntPtr(payload->AnalogData), analogDataBuffer, sampleIndex * AnalogIO.ChannelCount, AnalogIO.ChannelCount); - hubSyncCounterBuffer[sampleIndex] = payload->HubSyncCounter; - clockBuffer[sampleIndex] = frame.Clock; - if (++sampleIndex >= bufferSize) - { - var analogData = BufferHelper.CopyTranspose( - analogDataBuffer, - bufferSize, - AnalogIO.ChannelCount, - Depth.S16, - voltageScale, - transposeBuffer); - observer.OnNext(new AnalogInputDataFrame(clockBuffer, hubSyncCounterBuffer, analogData)); - hubSyncCounterBuffer = new ulong[bufferSize]; - clockBuffer = new ulong[bufferSize]; - sampleIndex = 0; - } - }, - observer.OnError, - observer.OnCompleted); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .SubscribeSafe(frameObserver); - }))); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/AnalogInputDataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/AnalogInputDataFrame.cs deleted file mode 100644 index ccadf7b5..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/AnalogInputDataFrame.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Runtime.InteropServices; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class AnalogInputDataFrame - { - public AnalogInputDataFrame(ulong[] clock, ulong[] hubSyncCounter, Mat analogData) - { - Clock = clock; - HubSyncCounter = hubSyncCounter; - AnalogData = analogData; - } - - public ulong[] Clock { get; } - - public ulong[] HubSyncCounter { get; } - - public Mat AnalogData { get; } - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - unsafe struct AnalogInputPayload - { - public ulong HubSyncCounter; - public fixed short AnalogData[AnalogIO.ChannelCount]; - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/AnalogOutput.cs b/OpenEphys.Onix/OpenEphys.Onix/AnalogOutput.cs deleted file mode 100644 index 64f74e57..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/AnalogOutput.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class AnalogOutput : Sink - { - const AnalogIOVoltageRange OutputRange = AnalogIOVoltageRange.TenVolts; - - [TypeConverter(typeof(AnalogIO.NameConverter))] - public string DeviceName { get; set; } - - public AnalogIODataType DataType { get; set; } = AnalogIODataType.S16; - - public override IObservable Process(IObservable source) - { - var dataType = DataType; - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var bufferSize = 0; - var scaleBuffer = default(Mat); - var transposeBuffer = default(Mat); - var sampleScale = dataType == AnalogIODataType.Volts - ? 1 / AnalogIODeviceInfo.GetVoltsPerDivision(OutputRange) - : 1; - var device = deviceInfo.GetDeviceContext(typeof(AnalogIO)); - return source.Do(data => - { - if (dataType == AnalogIODataType.S16 && data.Depth != Depth.S16 || - dataType == AnalogIODataType.Volts && data.Depth != Depth.F32) - { - ThrowDataTypeException(data.Depth); - } - - AssertChannelCount(data.Rows); - if (bufferSize != data.Cols) - { - bufferSize = data.Cols; - transposeBuffer = bufferSize > 1 - ? new Mat(data.Cols, data.Rows, data.Depth, 1) - : null; - if (sampleScale != 1) - { - scaleBuffer = transposeBuffer != null - ? new Mat(data.Cols, data.Rows, Depth.S16, 1) - : new Mat(data.Rows, data.Cols, Depth.S16, 1); - } - else scaleBuffer = null; - } - - var outputBuffer = data; - if (transposeBuffer != null) - { - CV.Transpose(outputBuffer, transposeBuffer); - outputBuffer = transposeBuffer; - } - - if (scaleBuffer != null) - { - CV.ConvertScale(outputBuffer, scaleBuffer, sampleScale); - outputBuffer = scaleBuffer; - } - - var dataSize = outputBuffer.Step * outputBuffer.Rows; - device.Write(outputBuffer.Data, dataSize); - }); - })); - } - - public IObservable Process(IObservable source) - { - if (DataType != AnalogIODataType.S16) - ThrowDataTypeException(Depth.S16); - - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(AnalogIO)); - return source.Do(data => - { - AssertChannelCount(data.Length); - device.Write(data); - }); - })); - } - - public IObservable Process(IObservable source) - { - if (DataType != AnalogIODataType.Volts) - ThrowDataTypeException(Depth.F32); - - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(AnalogIO)); - var divisionsPerVolt = 1 / AnalogIODeviceInfo.GetVoltsPerDivision(OutputRange); - return source.Do(data => - { - AssertChannelCount(data.Length); - var samples = new short[data.Length]; - for (int i = 0; i < samples.Length; i++) - { - samples[i] = (short)(data[i] * divisionsPerVolt); - } - - device.Write(samples); - }); - })); - } - - static void AssertChannelCount(int channels) - { - if (channels != AnalogIO.ChannelCount) - { - throw new InvalidOperationException( - $"The input data must have exactly {AnalogIO.ChannelCount} channels." - ); - } - } - - static void ThrowDataTypeException(Depth depth) - { - throw new InvalidOperationException( - $"Invalid input data type '{depth}' for the specified analog IO configuration." - ); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/BitHelper.cs b/OpenEphys.Onix/OpenEphys.Onix/BitHelper.cs deleted file mode 100644 index 1bcd7b74..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/BitHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net; - -namespace OpenEphys.Onix -{ - static class BitHelper - { - internal static uint Replace(uint value, uint mask, uint bits) - { - return (value & ~mask) | (bits & mask); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Bno055Data.cs b/OpenEphys.Onix/OpenEphys.Onix/Bno055Data.cs deleted file mode 100644 index e012b71c..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/Bno055Data.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class Bno055Data : Source - { - [TypeConverter(typeof(Bno055.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Generate() - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(Bno055)); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .Select(frame => new Bno055DataFrame(frame)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureAnalogIO.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureAnalogIO.cs deleted file mode 100644 index d36805ed..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureAnalogIO.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; - -namespace OpenEphys.Onix -{ - [TypeConverter(typeof(SortedPropertyConverter))] - public class ConfigureAnalogIO : SingleDeviceFactory - { - public ConfigureAnalogIO() - : base(typeof(AnalogIO)) - { - DeviceAddress = 6; - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the analog IO device is enabled.")] - public bool Enable { get; set; } = true; - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 0.")] - public AnalogIOVoltageRange InputRange0 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 1.")] - public AnalogIOVoltageRange InputRange1 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 2.")] - public AnalogIOVoltageRange InputRange2 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 3.")] - public AnalogIOVoltageRange InputRange3 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 4.")] - public AnalogIOVoltageRange InputRange4 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 5.")] - public AnalogIOVoltageRange InputRange5 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 6.")] - public AnalogIOVoltageRange InputRange6 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 7.")] - public AnalogIOVoltageRange InputRange7 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 8.")] - public AnalogIOVoltageRange InputRange8 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 9.")] - public AnalogIOVoltageRange InputRange9 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 10.")] - public AnalogIOVoltageRange InputRange10 { get; set; } - - [Category(ConfigurationCategory)] - [Description("The input voltage range of channel 11.")] - public AnalogIOVoltageRange InputRange11 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 0.")] - public AnalogIODirection Direction0 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 1.")] - public AnalogIODirection Direction1 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 2.")] - public AnalogIODirection Direction2 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 3.")] - public AnalogIODirection Direction3 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 4.")] - public AnalogIODirection Direction4 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 5.")] - public AnalogIODirection Direction5 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 6.")] - public AnalogIODirection Direction6 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 7.")] - public AnalogIODirection Direction7 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 8.")] - public AnalogIODirection Direction8 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 9.")] - public AnalogIODirection Direction9 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 10.")] - public AnalogIODirection Direction10 { get; set; } - - [Category(AcquisitionCategory)] - [Description("The direction of channel 11.")] - public AnalogIODirection Direction11 { get; set; } - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(AnalogIO.ENABLE, Enable ? 1u : 0u); - device.WriteRegister(AnalogIO.CH00INRANGE, (uint)InputRange0); - device.WriteRegister(AnalogIO.CH01INRANGE, (uint)InputRange1); - device.WriteRegister(AnalogIO.CH02INRANGE, (uint)InputRange2); - device.WriteRegister(AnalogIO.CH03INRANGE, (uint)InputRange3); - device.WriteRegister(AnalogIO.CH04INRANGE, (uint)InputRange4); - device.WriteRegister(AnalogIO.CH05INRANGE, (uint)InputRange5); - device.WriteRegister(AnalogIO.CH06INRANGE, (uint)InputRange6); - device.WriteRegister(AnalogIO.CH07INRANGE, (uint)InputRange7); - device.WriteRegister(AnalogIO.CH08INRANGE, (uint)InputRange8); - device.WriteRegister(AnalogIO.CH09INRANGE, (uint)InputRange9); - device.WriteRegister(AnalogIO.CH10INRANGE, (uint)InputRange10); - device.WriteRegister(AnalogIO.CH11INRANGE, (uint)InputRange11); - - // Build the whole value for CHDIR and write it once - static uint SetIO(uint io_reg, int channel, AnalogIODirection direction) => - (io_reg & ~((uint)1 << channel)) | ((uint)(direction) << channel); - - var io_reg = 0u; - io_reg = SetIO(io_reg, 0, Direction0); - io_reg = SetIO(io_reg, 1, Direction1); - io_reg = SetIO(io_reg, 2, Direction2); - io_reg = SetIO(io_reg, 3, Direction3); - io_reg = SetIO(io_reg, 4, Direction4); - io_reg = SetIO(io_reg, 5, Direction5); - io_reg = SetIO(io_reg, 6, Direction6); - io_reg = SetIO(io_reg, 7, Direction7); - io_reg = SetIO(io_reg, 8, Direction8); - io_reg = SetIO(io_reg, 9, Direction9); - io_reg = SetIO(io_reg, 10, Direction10); - io_reg = SetIO(io_reg, 11, Direction11); - device.WriteRegister(AnalogIO.CHDIR, io_reg); - - var deviceInfo = new AnalogIODeviceInfo(device, this); - return DeviceManager.RegisterDevice(deviceName, deviceInfo); - }); - } - - class SortedPropertyConverter : ExpandableObjectConverter - { - public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) - { - var properties = base.GetProperties(context, value, attributes); - var sortedOrder = properties.Cast() - .Where(p => p.PropertyType == typeof(AnalogIOVoltageRange) - || p.PropertyType == typeof(AnalogIODirection)) - .OrderBy(p => p.PropertyType.MetadataToken) - .Select(p => p.Name) - .Prepend(nameof(Enable)) - .ToArray(); - return properties.Sort(sortedOrder); - } - } - } - - static class AnalogIO - { - public const int ID = 22; - - // constants - public const int ChannelCount = 12; - public const int NumberOfDivisions = 1 << 16; - - // managed registers - public const uint ENABLE = 0; - public const uint CHDIR = 1; - public const uint CH00INRANGE = 2; - public const uint CH01INRANGE = 3; - public const uint CH02INRANGE = 4; - public const uint CH03INRANGE = 5; - public const uint CH04INRANGE = 6; - public const uint CH05INRANGE = 7; - public const uint CH06INRANGE = 8; - public const uint CH07INRANGE = 9; - public const uint CH08INRANGE = 10; - public const uint CH09INRANGE = 11; - public const uint CH10INRANGE = 12; - public const uint CH11INRANGE = 13; - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(AnalogIO)) - { - } - } - } - - class AnalogIODeviceInfo : DeviceInfo - { - public AnalogIODeviceInfo(DeviceContext device, ConfigureAnalogIO deviceFactory) - : base(device, deviceFactory.DeviceType) - { - VoltsPerDivision = new[] - { - GetVoltsPerDivision(deviceFactory.InputRange0), - GetVoltsPerDivision(deviceFactory.InputRange1), - GetVoltsPerDivision(deviceFactory.InputRange2), - GetVoltsPerDivision(deviceFactory.InputRange3), - GetVoltsPerDivision(deviceFactory.InputRange4), - GetVoltsPerDivision(deviceFactory.InputRange5), - GetVoltsPerDivision(deviceFactory.InputRange6), - GetVoltsPerDivision(deviceFactory.InputRange7), - GetVoltsPerDivision(deviceFactory.InputRange8), - GetVoltsPerDivision(deviceFactory.InputRange9), - GetVoltsPerDivision(deviceFactory.InputRange10), - GetVoltsPerDivision(deviceFactory.InputRange11) - }; - } - - public static float GetVoltsPerDivision(AnalogIOVoltageRange voltageRange) - { - return voltageRange switch - { - AnalogIOVoltageRange.TenVolts => 20.0f / AnalogIO.NumberOfDivisions, - AnalogIOVoltageRange.TwoPointFiveVolts => 5.0f / AnalogIO.NumberOfDivisions, - AnalogIOVoltageRange.FiveVolts => 10.0f / AnalogIO.NumberOfDivisions, - _ => throw new ArgumentOutOfRangeException(nameof(voltageRange)), - }; - } - - public float[] VoltsPerDivision { get; } - } - - public enum AnalogIOVoltageRange - { - [Description("+/-10.0 V")] - TenVolts = 0, - [Description("+/-2.5 V")] - TwoPointFiveVolts = 1, - [Description("+/-5.0 V")] - FiveVolts, - } - - public enum AnalogIODirection - { - Input = 0, - Output = 1 - } - - public enum AnalogIODataType - { - S16, - Volts - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureBreakoutBoard.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureBreakoutBoard.cs deleted file mode 100644 index 779bdbe7..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureBreakoutBoard.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; - -namespace OpenEphys.Onix -{ - public class ConfigureBreakoutBoard : HubDeviceFactory - { - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureHeartbeat Heartbeat { get; set; } = new(); - - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureAnalogIO AnalogIO { get; set; } = new(); - - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureDigitalIO DigitalIO { get; set; } = new(); - - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureMemoryMonitor MemoryMonitor { get; set; } = new(); - - internal override IEnumerable GetDevices() - { - yield return Heartbeat; - yield return AnalogIO; - yield return DigitalIO; - yield return MemoryMonitor; - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureDigitalIO.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureDigitalIO.cs deleted file mode 100644 index c4996c0b..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureDigitalIO.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.ComponentModel; - -namespace OpenEphys.Onix -{ - public class ConfigureDigitalIO : SingleDeviceFactory - { - public ConfigureDigitalIO() - : base(typeof(DigitalIO)) - { - DeviceAddress = 7; - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the digital IO device is enabled.")] - public bool Enable { get; set; } = true; - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(DigitalIO.ENABLE, Enable ? 1u : 0); - return DeviceManager.RegisterDevice(deviceName, device, DeviceType); - }); - } - } - - static class DigitalIO - { - public const int ID = 18; - - // managed registers - public const uint ENABLE = 0x0; // Enable or disable the data output stream - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(DigitalIO)) - { - } - } - } - - [Flags] - public enum DigitalPortState : ushort - { - Pin0 = 0x1, - Pin1 = 0x2, - Pin2 = 0x4, - Pin3 = 0x8, - Pin4 = 0x10, - Pin5 = 0x20, - Pin6 = 0x40, - Pin7 = 0x80, - } - - [Flags] - public enum BreakoutButtonState : ushort - { - Moon = 0x1, - Triangle = 0x2, - X = 0x4, - Check = 0x8, - Circle = 0x10, - Square = 0x20, - Reserved0 = 0x40, - Reserved1 = 0x80, - PortDOn = 0x100, - PortCOn = 0x200, - PortBOn = 0x400, - PortAOn = 0x800, - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHarpSyncInput.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureHarpSyncInput.cs deleted file mode 100644 index 65412776..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHarpSyncInput.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.ComponentModel; - -namespace OpenEphys.Onix -{ - public class ConfigureHarpSyncInput : SingleDeviceFactory - { - public ConfigureHarpSyncInput() - : base(typeof(HarpSyncInput)) - { - DeviceAddress = 12; - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the Harp sync input device is enabled.")] - public bool Enable { get; set; } = true; - - [Category(ConfigurationCategory)] - [Description("Specifies the physical Harp clock input source.")] - public HarpSyncSource Source { get; set; } = HarpSyncSource.Breakout; - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(HarpSyncInput.ENABLE, Enable ? 1u : 0); - device.WriteRegister(HarpSyncInput.SOURCE, (uint)Source); - return DeviceManager.RegisterDevice(deviceName, device, DeviceType); - }); - } - } - - static class HarpSyncInput - { - public const int ID = 30; - - // managed registers - public const uint ENABLE = 0x0; // Enable or disable the data stream - public const uint SOURCE = 0x1; // Select the clock input source - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(HarpSyncInput)) - { - } - } - } - - public enum HarpSyncSource - { - Breakout = 0, - ClockAdapter = 1 - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64.cs deleted file mode 100644 index 6263a948..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Threading; - -namespace OpenEphys.Onix -{ - public class ConfigureHeadstage64 : HubDeviceFactory - { - PortName port; - readonly ConfigureHeadstage64LinkController LinkController = new(); - - public ConfigureHeadstage64() - { - // TODO: The issue with this headstage is that its locking voltage is far, far lower than the voltage required for full - // functionality. Locking occurs at around 2V on the headstage (enough to turn 1.8V on). Full functionality is at 5.0 volts. - // Whats worse: the port voltage can only go down to 3.3V, which means that its very hard to find the true lowest voltage - // for a lock and then add a large offset to that. - Port = PortName.PortA; - LinkController.HubConfiguration = HubConfiguration.Standard; - } - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureRhd2164 Rhd2164 { get; set; } = new(); - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureBno055 Bno055 { get; set; } = new(); - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureTS4231 TS4231 { get; set; } = new() { Enable = false }; - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureHeadstage64ElectricalStimulator ElectricalStimulator { get; set; } = new(); - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureHeadstage64OpticalStimulator OpticalStimulator { get; set; } = new(); - - public PortName Port - { - get { return port; } - set - { - port = value; - var offset = (uint)port << 8; - LinkController.DeviceAddress = (uint)port; - Rhd2164.DeviceAddress = offset + 0; - Bno055.DeviceAddress = offset + 1; - TS4231.DeviceAddress = offset + 2; - ElectricalStimulator.DeviceAddress = offset + 3; - OpticalStimulator.DeviceAddress = offset + 4; - } - } - - [Description("If defined, it will override automated voltage discovery and apply the specified voltage" + - "to the headstage. Warning: this device requires 5.5V 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 GetDevices() - { - yield return LinkController; - yield return Rhd2164; - yield return Bno055; - yield return TS4231; - yield return ElectricalStimulator; - yield return OpticalStimulator; - } - - class ConfigureHeadstage64LinkController : ConfigureFmcLinkController - { - protected override bool ConfigurePortVoltage(DeviceContext device) - { - // TODO: It takes a huge amount of time to get to 0, almost 10 seconds. - // The best we can do at the moment is drive port voltage to minimum which - // is an active process and then settle from there to zero volts. - const uint MinVoltage = 33; - const uint MaxVoltage = 60; - const uint VoltageOffset = 34; - const uint VoltageIncrement = 02; - - // Start with highest voltage and ramp it down to find lowest lock voltage - var voltage = MaxVoltage; - for (; voltage >= MinVoltage; voltage -= VoltageIncrement) - { - device.WriteRegister(FmcLinkController.PORTVOLTAGE, voltage); - Thread.Sleep(200); - if (!CheckLinkState(device)) - { - if (voltage == MaxVoltage) return false; - else break; - } - } - - device.WriteRegister(FmcLinkController.PORTVOLTAGE, MinVoltage); - device.WriteRegister(FmcLinkController.PORTVOLTAGE, 0); - Thread.Sleep(1000); - device.WriteRegister(FmcLinkController.PORTVOLTAGE, voltage + VoltageOffset); - Thread.Sleep(200); - return CheckLinkState(device); - } - } - } - - public enum PortName - { - PortA = 1, - PortB = 2 - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstageRhs2116.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstageRhs2116.cs new file mode 100644 index 00000000..387bbbac --- /dev/null +++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstageRhs2116.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; + +namespace OpenEphys.Onix +{ + public class ConfigureHeadstageRhs2116 : HubDeviceFactory + { + PortName port; + readonly ConfigureHeadstageRhs2116LinkController LinkController = new(); + + public ConfigureHeadstageRhs2116() + { + Port = PortName.PortA; + LinkController.HubConfiguration = HubConfiguration.Standard; + } + + [Category(ConfigurationCategory)] + [TypeConverter(typeof(HubDeviceConverter))] + public ConfigureRhs2116 Rhs2116A { get; set; } = new(); + + [Category(ConfigurationCategory)] + [TypeConverter(typeof(HubDeviceConverter))] + public ConfigureRhs2116 Rhs2116B { get; set; } = new(); + + [Category(ConfigurationCategory)] + [TypeConverter(typeof(HubDeviceConverter))] + public ConfigureRhs2116Trigger StimulusTrigger { get; set; } = new(); + + internal override void UpdateDeviceNames() + { + LinkController.DeviceName = GetFullDeviceName(nameof(LinkController)); + Rhs2116A.DeviceName = GetFullDeviceName(nameof(Rhs2116A)); + Rhs2116B.DeviceName = GetFullDeviceName(nameof(Rhs2116B)); + StimulusTrigger.DeviceName = GetFullDeviceName(nameof(StimulusTrigger)); + } + + public PortName Port + { + get { return port; } + set + { + port = value; + var offset = (uint)port << 8; + LinkController.DeviceAddress = (uint)port; + Rhs2116A.DeviceAddress = offset + 0; + Rhs2116B.DeviceAddress = offset + 1; + StimulusTrigger.DeviceAddress = offset + 2; + } + } + + + [Description("If defined, it will override automated voltage discovery and apply the specified voltage" + + "to the headstage. Warning: this device requires 3.4V to 4.4V 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 GetDevices() + { + yield return LinkController; + yield return Rhs2116A; + yield return Rhs2116B; + yield return StimulusTrigger; + } + + class ConfigureHeadstageRhs2116LinkController : ConfigureFmcLinkController + { + protected override bool ConfigurePortVoltage(DeviceContext device) + { + const double MinVoltage = 3.3; + const double MaxVoltage = 4.4; + const double VoltageOffset = 2.0; + const double VoltageIncrement = 0.2; + + for (var voltage = MinVoltage; voltage <= MaxVoltage; voltage += VoltageIncrement) + { + SetPortVoltage(device, voltage); + if (base.CheckLinkState(device)) + { + SetPortVoltage(device, voltage + VoltageOffset); + return CheckLinkState(device); + } + } + + return false; + } + + private void SetPortVoltage(DeviceContext device, double voltage) + { + device.WriteRegister(FmcLinkController.PORTVOLTAGE, 0); + Thread.Sleep(500); + device.WriteRegister(FmcLinkController.PORTVOLTAGE, (uint)(10 * voltage)); + Thread.Sleep(500); + } + + protected override bool CheckLinkState(DeviceContext device) + { + // NB: The RHS2116 headstage needs an additional reset after power on to provide its device table. + device.Context.Reset(); + var linkState = device.ReadRegister(FmcLinkController.LINKSTATE); + return (linkState & FmcLinkController.LINKSTATE_SL) != 0; + } + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeartbeat.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeartbeat.cs deleted file mode 100644 index 972ab53f..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeartbeat.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.ComponentModel; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class ConfigureHeartbeat : SingleDeviceFactory - { - readonly BehaviorSubject beatsPerSecond = new(10); - - public ConfigureHeartbeat() - : base(typeof(Heartbeat)) - { - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the heartbeat device is enabled.")] - public bool Enable { get; set; } = true; - - [Range(1, 10e6)] - [Category(ConfigurationCategory)] - [Description("Rate at which beats are produced.")] - public uint BeatsPerSecond - { - get => beatsPerSecond.Value; - set => beatsPerSecond.OnNext(value); - } - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(Heartbeat.ENABLE, 1); - var subscription = beatsPerSecond.Subscribe(newValue => - { - var clkHz = device.ReadRegister(Heartbeat.CLK_HZ); - device.WriteRegister(Heartbeat.CLK_DIV, clkHz / newValue); - }); - - return new CompositeDisposable( - DeviceManager.RegisterDevice(deviceName, device, DeviceType), - subscription - ); - }); - } - } - - static class Heartbeat - { - public const int ID = 12; - - public const uint ENABLE = 0; // Enable the heartbeat - public const uint CLK_DIV = 1; // Heartbeat clock divider ratio. Default results in 10 Hz heartbeat. Values less than CLK_HZ / 10e6 Hz will result in 1kHz. - public const uint CLK_HZ = 2; // The frequency parameter, CLK_HZ, used in the calculation of CLK_DIV - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(Heartbeat)) - { - } - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureMemoryMonitor.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureMemoryMonitor.cs deleted file mode 100644 index 9857d21a..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureMemoryMonitor.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.ComponentModel; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class ConfigureMemoryMonitor : SingleDeviceFactory - { - public ConfigureMemoryMonitor() - : base(typeof(MemoryMonitor)) - { - DeviceAddress = 10; - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the monitor device is enabled.")] - public bool Enable { get; set; } = false; - - [Range(1, 1000)] - [Category(ConfigurationCategory)] - [Description("Frequency at which hardware memory use is recorded (Hz).")] - public uint SamplesPerSecond { get; set; } = 10; - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(MemoryMonitor.ENABLE, 1); - device.WriteRegister(MemoryMonitor.CLK_DIV, device.ReadRegister(MemoryMonitor.CLK_HZ) / SamplesPerSecond); - - return DeviceManager.RegisterDevice(deviceName, device, DeviceType); - }); - } - } - - static class MemoryMonitor - { - public const int ID = 28; - - public const uint ENABLE = 0; // Enable the monitor - public const uint CLK_DIV = 1; // Sample clock divider ratio. Values less than CLK_HZ / 10e6 Hz will result in 1kHz. - public const uint CLK_HZ = 2; // The frequency parameter, CLK_HZ, used in the calculation of CLK_DIV - public const uint TOTAL_MEM = 3; // Total available memory in 32-bit words - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(MemoryMonitor)) - { - } - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBetaHeadstage.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBetaHeadstage.cs deleted file mode 100644 index 6356a6a8..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBetaHeadstage.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; - -namespace OpenEphys.Onix -{ - public class ConfigureNeuropixelsV2eBetaHeadstage : HubDeviceFactory - { - PortName port; - readonly ConfigureNeuropixelsV2eLinkController LinkController = new(); - - public ConfigureNeuropixelsV2eBetaHeadstage() - { - Port = PortName.PortA; - LinkController.HubConfiguration = HubConfiguration.Passthrough; - } - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureNeuropixelsV2eBeta NeuropixelsV2Beta { get; set; } = new(); - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureNeuropixelsV2eBno055 Bno055 { get; set; } = new(); - - public PortName Port - { - get { return port; } - set - { - port = value; - var offset = (uint)port << 8; - LinkController.DeviceAddress = (uint)port; - NeuropixelsV2Beta.DeviceAddress = offset + 0; - Bno055.DeviceAddress = offset + 1; - } - } - - [Description("If defined, overrides automated voltage discovery and applies " + - "the specified voltage to the headstage. Warning: this device requires 3.0V to 5.0V " + - "for proper operation. Higher voltages can damage the headstage.")] - public double? PortVoltage - { - get => LinkController.PortVoltage; - set => LinkController.PortVoltage = value; - } - - internal override IEnumerable GetDevices() - { - yield return LinkController; - yield return NeuropixelsV2Beta; - yield return Bno055; - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eHeadstage.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eHeadstage.cs deleted file mode 100644 index 710715f7..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eHeadstage.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; - -namespace OpenEphys.Onix -{ - public class ConfigureNeuropixelsV2eHeadstage : HubDeviceFactory - { - PortName port; - readonly ConfigureNeuropixelsV2eLinkController LinkController = new(); - - public ConfigureNeuropixelsV2eHeadstage() - { - Port = PortName.PortA; - LinkController.HubConfiguration = HubConfiguration.Passthrough; - } - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureNeuropixelsV2e NeuropixelsV2 { get; set; } = new(); - - [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureNeuropixelsV2eBno055 Bno055 { get; set; } = new(); - - public PortName Port - { - get { return port; } - set - { - port = value; - var offset = (uint)port << 8; - LinkController.DeviceAddress = (uint)port; - NeuropixelsV2.DeviceAddress = offset + 0; - Bno055.DeviceAddress = offset + 1; - } - } - - [Description("If defined, overrides automated voltage discovery and applies " + - "the specified voltage to the headstage. Warning: this device requires 3.0V to 5.5V " + - "for proper operation. Higher voltages can damage the headstage.")] - public double? PortVoltage - { - get => LinkController.PortVoltage; - set => LinkController.PortVoltage = value; - } - - internal override IEnumerable GetDevices() - { - yield return LinkController; - yield return NeuropixelsV2; - yield return Bno055; - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhs2116.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhs2116.cs new file mode 100644 index 00000000..c99cd62d --- /dev/null +++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhs2116.cs @@ -0,0 +1,265 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace OpenEphys.Onix +{ + public class ConfigureRhs2116 : SingleDeviceFactory + { + + readonly BehaviorSubject analogLowCutoff = new(Rhs2116AnalogLowCutoff.Low100mHz); + readonly BehaviorSubject analogLowCutoffRecovery = new(Rhs2116AnalogLowCutoff.Low250Hz); + readonly BehaviorSubject analogHighCutoff = new(Rhs2116AnalogHighCutoff.High10000Hz); + readonly BehaviorSubject respectExternalActiveStim = new(true); + readonly BehaviorSubject stimulusSequence = new(new Rhs2116StimulusSequence()); + + public ConfigureRhs2116() + : base(typeof(Rhs2116)) + { + } + + [Category(ConfigurationCategory)] + [Description("Specifies whether the RHS2116 device is enabled.")] + public bool Enable { get; set; } = true; + + //[Category(ConfigurationCategory)] + //[Description("Specifies the raw ADC output format used for amplifier conversions.")] + //public Rhs2116AmplifierDataFormat AmplifierDataFormat { get; set; } + + [Category(ConfigurationCategory)] + [Description("Specifies the cutoff frequency for the DSP high-pass filter used for amplifier offset removal.")] + public Rhs2116DspCutoff DspCutoff { get; set; } = Rhs2116DspCutoff.Off; + + [Category(AcquisitionCategory)] + [Description("Specifies the lower cutoff frequency of the pre-ADC amplifiers.")] + public Rhs2116AnalogLowCutoff AnalogLowCutoff + { + get => analogLowCutoff.Value; + set => analogLowCutoff.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("Specifies the lower cutoff frequency of the pre-ADC amplifiers during stimulus recovery.")] + public Rhs2116AnalogLowCutoff AnalogLowCutoffRecovery + { + get => analogLowCutoffRecovery.Value; + set => analogLowCutoffRecovery.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("Specifies the upper cutoff frequency of the pre-ADC amplifiers.")] + public Rhs2116AnalogHighCutoff AnalogHighCutoff + { + get => analogHighCutoff.Value; + set => analogHighCutoff.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("If true, this device will apply AnalogLowCutoffRecovery " + + "if stimulation occurs via any RHS chip the same headstage or others that are connected" + + "using StimActive pin. If false, this device will apply AnalogLowCutoffRecovery during its" + + "own stimuli.")] + public bool RespectExternalActiveStim + { + get => respectExternalActiveStim.Value; + set => respectExternalActiveStim.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("Stimulus sequence.")] + public Rhs2116StimulusSequence StimulusSequence + { + get => stimulusSequence.Value; + set => stimulusSequence.OnNext(value); + } + + public override IObservable Process(IObservable source) + { + var enable = Enable; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + // config register format following RHS2116 datasheet + // https://www.intantech.com/files/Intan_RHS2116_datasheet.pdf + var device = context.GetDeviceContext(deviceAddress, Rhs2116.ID); + + var format = device.ReadRegister(Rhs2116.FORMAT); + var dspCutoff = DspCutoff; + if (dspCutoff == Rhs2116DspCutoff.Off) + { + format &= ~(1u << 4); + } + else + { + format |= 1 << 4; + format &= ~0xFu; + format |= (uint)dspCutoff; + } + + device.WriteRegister(Rhs2116.FORMAT, format); // NB: DC data only provided in unsigned. Force amplifier data to use unsigned for consistency + device.WriteRegister(Rhs2116.ENABLE, enable ? 1u : 0); + + return new CompositeDisposable( + DeviceManager.RegisterDevice(deviceName, device, DeviceType), + analogLowCutoff.Subscribe(newValue => + { + var regs = Rhs2116Config.AnalogLowCutoffToRegisters[newValue]; + var reg = regs[2] << 13 | regs[1] << 7 | regs[0]; + device.WriteRegister(Rhs2116.BW2, reg); + }), + analogLowCutoffRecovery.Subscribe(newValue => + { + var regs = Rhs2116Config.AnalogLowCutoffToRegisters[newValue]; + var reg = regs[2] << 13 | regs[1] << 7 | regs[0]; + device.WriteRegister(Rhs2116.BW3, reg); + }), + analogHighCutoff.Subscribe(newValue => + { + var regs = Rhs2116Config.AnalogHighCutoffToRegisters[newValue]; + device.WriteRegister(Rhs2116.BW0, regs[1] << 6 | regs[0]); + device.WriteRegister(Rhs2116.BW1, regs[3] << 6 | regs[2]); + device.WriteRegister(Rhs2116.FASTSETTLESAMPLES, Rhs2116Config.AnalogHighCutoffToFastSettleSamples[newValue]); + }), + stimulusSequence.Subscribe(newValue => + { + // Step size + var reg = Rhs2116Config.StimulatorStepSizeToRegisters[newValue.CurrentStepSize]; + device.WriteRegister(Rhs2116.STEPSZ, reg[2] << 13 | reg[1] << 7 | reg[0]); + + // Anodic amplitudes + // TODO: cache last write and compare? + var a = newValue.AnodicAmplitudes; + for (int i = 0; i < a.Count(); i++) + { + device.WriteRegister(Rhs2116.POS00 + (uint)i, a.ElementAt(i)); + } + + // Cathodic amplitudes + // TODO: cache last write and compare? + var c = newValue.CathodicAmplitudes; + for (int i = 0; i < a.Count(); i++) + { + device.WriteRegister(Rhs2116.NEG00 + (uint)i, c.ElementAt(i)); + } + + // Create delta table and set length + var dt = newValue.DeltaTable; + device.WriteRegister(Rhs2116.NUMDELTAS, (uint)dt.Count); + + // TODO: If we want to do this efficently, we probably need a different data structure on the + // FPGA ram that allows columns to be out of order (e.g. linked list) + uint j = 0; + foreach (var d in dt) + { + uint indexAndTime = j++ << 22 | (d.Key & 0x003FFFFF); + device.WriteRegister(Rhs2116.DELTAIDXTIME, indexAndTime); + device.WriteRegister(Rhs2116.DELTAPOLEN, d.Value); + } + }) + ); + }); + } + } + + static class Rhs2116 + { + public const int ID = 31; + + // constants + public const int AmplifierChannelCount = 16; + public const int StimMemorySlotsAvailable = 1024; + public const double SampleFrequencyHz = 30.1932367151e3; + + // managed registers + public const uint ENABLE = 0x8000; // Enable or disable the data output stream (32767) + public const uint MAXDELTAS = 0x8001; // Maximum number of deltas in the delta table (32769) + public const uint NUMDELTAS = 0x8002; // Number of deltas in the delta table (32770) + public const uint DELTAIDXTIME = 0x8003; // The delta table index and corresponding application delta application time (32771) + public const uint DELTAPOLEN = 0x8004; // The polarity and enable vectors (32772) + public const uint SEQERROR = 0x8005; // Invalid sequence indicator (32773) + public const uint TRIGGER = 0x8006; // Writing 1 to this register will trigger a stimulation sequence for this device (32774) + public const uint FASTSETTLESAMPLES = 0x8007; // Number of round-robbin samples to apply charge balance following the conclusion of a stimulus pulse (32775) + public const uint RESPECTSTIMACTIVE = 0x8008; // Determines when artifact recovery sequence is applied to this chip (32776) + + // unmanaged registers + public const uint BIAS = 0x00; // Supply Sensor and ADC Buffer Bias Current + public const uint FORMAT = 0x01; // ADC Output Format, DSP Offset Removal, and Auxiliary Digital Outputs + public const uint ZCHECK = 0x02; // Impedance Check Control + public const uint DAC = 0x03; // Impedance Check DAC + public const uint BW0 = 0x04; // On-Chip Amplifier Bandwidth Select + public const uint BW1 = 0x05; // On-Chip Amplifier Bandwidth Select + public const uint BW2 = 0x06; // On-Chip Amplifier Bandwidth Select + public const uint BW3 = 0x07; // On-Chip Amplifier Bandwidth Select + public const uint PWR = 0x08; //Individual AC Amplifier Power + + public const uint SETTLE = 0x0a; // Amplifier Fast Settle + + public const uint LOWAB = 0x0c; // Amplifier Lower Cutoff Frequency Select + + public const uint STIMENA = 0x20; // Stimulation Enable A + public const uint STIMENB = 0x21; // Stimulation Enable B + public const uint STEPSZ = 0x22; // Stimulation Current Step Size + public const uint STIMBIAS = 0x23; // Stimulation Bias Voltages + public const uint RECVOLT = 0x24; // Current-Limited Charge Recovery Target Voltage + public const uint RECCUR = 0x25; // Charge Recovery Current Limit + public const uint DCPWR = 0x26; // Individual DC Amplifier Power + + public const uint COMPMON = 0x28; // Compliance Monitor + + public const uint STIMON = 0x2a; // Stimulator On + + public const uint STIMPOL = 0x2c; // Stimulator Polarity + + public const uint RECOV = 0x2e; // Charge Recovery Switch + + public const uint LIMREC = 0x30; // Current-Limited Charge Recovery Enable + + public const uint FAULTDET = 0x32; // Fault Current Detector + + public const uint NEG00 = 0x40; + public const uint NEG01 = 0x41; + public const uint NEG02 = 0x42; + public const uint NEG03 = 0x43; + public const uint NEG04 = 0x44; + public const uint NEG05 = 0x45; + public const uint NEG06 = 0x46; + public const uint NEG07 = 0x47; + public const uint NEG08 = 0x48; + public const uint NEG09 = 0x49; + public const uint NEG010 = 0x4a; + public const uint NEG011 = 0x4b; + public const uint NEG012 = 0x4c; + public const uint NEG013 = 0x4d; + public const uint NEG014 = 0x4e; + public const uint NEG015 = 0x4f; + + public const uint POS00 = 0x60; + public const uint POS01 = 0x61; + public const uint POS02 = 0x62; + public const uint POS03 = 0x63; + public const uint POS04 = 0x64; + public const uint POS05 = 0x65; + public const uint POS06 = 0x66; + public const uint POS07 = 0x67; + public const uint POS08 = 0x68; + public const uint POS09 = 0x69; + public const uint POS010 = 0x6a; + public const uint POS011 = 0x6b; + public const uint POS012 = 0x6c; + public const uint POS013 = 0x6d; + public const uint POS014 = 0x6e; + public const uint POS015 = 0x6f; + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(Rhs2116)) + { + } + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhs2116Trigger.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhs2116Trigger.cs new file mode 100644 index 00000000..b1adaac2 --- /dev/null +++ b/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhs2116Trigger.cs @@ -0,0 +1,56 @@ +using System; +using System.ComponentModel; + +namespace OpenEphys.Onix +{ + public class ConfigureRhs2116Trigger : SingleDeviceFactory + { + public ConfigureRhs2116Trigger() + : base(typeof(Rhs2116Trigger)) + { + } + + [Category(ConfigurationCategory)] + [Description("Specifies whether the RHS2116 device is enabled.")] + public Rhs2116TriggerSource TriggerSource { get; set; } = Rhs2116TriggerSource.Local; + + public override IObservable Process(IObservable source) + { + var triggerSource = TriggerSource; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, Rhs2116Trigger.ID); + device.WriteRegister(Rhs2116Trigger.TRIGGERSOURCE, (uint)triggerSource); + return DeviceManager.RegisterDevice(deviceName, device, DeviceType); + }); + } + } + + static class Rhs2116Trigger + { + public const int ID = 32; + + // managed registers + public const uint ENABLE = 0; // Writes and reads to ENABLE are ignored without error + public const uint TRIGGERSOURCE = 1; // The LSB is used to determine the trigger source + public const uint TRIGGER = 2; // Writing 0x1 to this register will trigger a stimulation sequence if the TRIGGERSOURCE is set to 0. + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(Rhs2116Trigger)) + { + } + } + } + + public enum Rhs2116TriggerSource + { + [Description("Respect local triggers (e.g. via GPIO or TRIGGER register) and broadcast via sync pin. ")] + Local = 0, + [Description("Receiver. Only resepct triggers received from sync pin")] + External = 1, + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureTS4231.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureTS4231.cs deleted file mode 100644 index 0c6577e1..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureTS4231.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.ComponentModel; - -namespace OpenEphys.Onix -{ - public class ConfigureTS4231 : SingleDeviceFactory - { - public ConfigureTS4231() - : base(typeof(TS4231)) - { - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the TS4231 device is enabled.")] - public bool Enable { get; set; } = true; - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(TS4231.ENABLE, Enable ? 1u : 0); - return DeviceManager.RegisterDevice(deviceName, device, DeviceType); - }); - } - } - - static class TS4231 - { - public const int ID = 25; - - // managed registers - public const uint ENABLE = 0x0; // Enable or disable the data output stream - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(TS4231)) - { - } - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureTest0.cs b/OpenEphys.Onix/OpenEphys.Onix/ConfigureTest0.cs deleted file mode 100644 index 4afa7ff8..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureTest0.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.ComponentModel; -using System.Reactive.Subjects; -using System.Xml.Serialization; - -namespace OpenEphys.Onix -{ - public class ConfigureTest0 : SingleDeviceFactory - { - readonly BehaviorSubject message = new(0); - - public ConfigureTest0() - : base(typeof(Test0)) - { - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the Test0 device is enabled.")] - public bool Enable { get; set; } = true; - - [Category(AcquisitionCategory)] - [Description("Specifies the first 16-bit word that appears in the device to host frame.")] - public short Message - { - get => message.Value; - set => message.OnNext(value); - } - - [XmlIgnore] - [Category(ConfigurationCategory)] - [Description("Indicates the number of 16-bit numbers, 0 to PayloadWords - 1, that follow Message in each frame.")] - public uint DummyCount { get; private set; } - - [XmlIgnore] - [Category(ConfigurationCategory)] - [Description("Indicates the rate at which frames are produced in Hz. 0 indicates that the frame rate is unspecified (variable or upstream controlled).")] - public uint FramesPerSecond { get; private set; } - - public override IObservable Process(IObservable source) - { - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(Test0.ENABLE, Enable ? 1u : 0); - FramesPerSecond = device.ReadRegister(Test0.FRAMERATE); - DummyCount = device.ReadRegister(Test0.NUMTESTWORDS); - return DeviceManager.RegisterDevice(deviceName, device, DeviceType); - }); - } - } - - static class Test0 - { - public const int ID = 10; - - public const uint ENABLE = 0x0; - public const uint MESSAGE = 0x1; - public const uint NUMTESTWORDS = 0x2; - public const uint FRAMERATE = 0x3; - - internal class NameConverter : DeviceNameConverter - { - public NameConverter() - : base(typeof(Test0)) - { - } - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ContextTask.cs b/OpenEphys.Onix/OpenEphys.Onix/ContextTask.cs deleted file mode 100644 index 883d36a5..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ContextTask.cs +++ /dev/null @@ -1,430 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Subjects; -using System.Threading; -using System.Threading.Tasks; - -namespace OpenEphys.Onix -{ - public class ContextTask : IDisposable - { - private oni.Context ctx; - - /// - /// Maximum amount of frames the reading queue will hold. If the queue fills or the read - /// thread is not performant enough to fill it faster than data is produced, frame reading - /// will throttle, filling host memory instead of userspace memory. - /// - private const int MaxQueuedFrames = 2_000_000; - - /// - /// Timeout in ms for queue reads. This should not be critical as the read operation will - /// cancel if the task is stopped - /// - private const int QueueTimeoutMilliseconds = 200; - - // NB: Decouple OnNext() form hadware reads - private Task readFrames; - private Task distributeFrames; - private BlockingCollection FrameQueue; - private CancellationTokenSource CollectFramesTokenSource; - private CancellationToken CollectFramesToken; - private IDisposable ContextConfiguration; - event Func configureHost; - event Func configureLink; - event Func configureDevice; - - // NOTE: There was a GC memory leak around here - internal Subject FrameReceived = new(); - - public static readonly string DefaultDriver = "riffa"; - public static readonly int DefaultIndex = 0; - - // TODO: These work for RIFFA implementation, but potentially not others!! - private readonly object readLock = new(); - private readonly object writeLock = new(); - private readonly object regLock = new(); - private readonly object disposeLock = new(); - private bool running = false; - - private readonly string contextDriver = DefaultDriver; - private readonly int contextIndex = DefaultIndex; - - public ContextTask(string driver, int index) - { - contextDriver = driver; - contextIndex = index; - Initialize(); - } - - private void Initialize() - { - ctx = new oni.Context(contextDriver, contextIndex); - SystemClockHz = ctx.SystemClockHz; - AcquisitionClockHz = ctx.AcquisitionClockHz; - MaxReadFrameSize = ctx.MaxReadFrameSize; - MaxWriteFrameSize = ctx.MaxWriteFrameSize; - DeviceTable = ctx.DeviceTable; - } - - public void Reset() - { - lock (disposeLock) - lock (regLock) - { - Stop(); - lock (readLock) - lock (writeLock) - { - ctx?.Dispose(); - Initialize(); - } - } - } - - public uint SystemClockHz { get; private set; } - public uint AcquisitionClockHz { get; private set; } - public uint MaxReadFrameSize { get; private set; } - public uint MaxWriteFrameSize { get; private set; } - public Dictionary DeviceTable { get; private set; } - - void AssertConfigurationContext() - { - if (running) - { - throw new InvalidOperationException("Configuration cannot be changed while acquisition context is running."); - } - } - - // NB: This is where actions that reconfigure the hub state, or otherwise - // change the device table should be executed - internal void ConfigureHost(Func configure) - { - lock (regLock) - { - AssertConfigurationContext(); - configureHost += configure; - } - } - - // NB: This is where actions that calibrate port voltage or otherwise - // check link lock state should be executed - internal void ConfigureLink(Func configure) - { - lock (regLock) - { - AssertConfigurationContext(); - configureLink += configure; - } - } - - // NB: Actions queued using this method should assume that the device table - // is finalized and cannot be changed - internal void ConfigureDevice(Func configure) - { - lock (regLock) - { - AssertConfigurationContext(); - configureDevice += configure; - } - } - - private IDisposable ConfigureContext() - { - var hostAction = Interlocked.Exchange(ref configureHost, null); - var linkAction = Interlocked.Exchange(ref configureLink, null); - var deviceAction = Interlocked.Exchange(ref configureDevice, null); - var disposable = new StackDisposable(); - ConfigureResources(disposable, hostAction); - ConfigureResources(disposable, linkAction); - ConfigureResources(disposable, deviceAction); - return disposable; - } - - void ConfigureResources(StackDisposable disposable, Func action) - { - if (action != null) - { - var invocationList = action.GetInvocationList(); - try - { - foreach (var selector in invocationList.Cast>()) - { - disposable.Push(selector(this)); - } - } - catch - { - disposable.Dispose(); - throw; - } - finally { Reset(); } - } - } - - internal void Start(int blockReadSize, int blockWriteSize) - { - lock (regLock) - { - if (running) return; - - // NB: Configure context before starting acquisition - ContextConfiguration = ConfigureContext(); - ctx.BlockReadSize = blockReadSize; - ctx.BlockWriteSize = blockWriteSize; - - // NB: Stuff related to sync mode is 100% ONIX, not ONI, so long term another place - // to do this separation might be needed - int addr = ctx.HardwareAddress; - int mode = (addr & 0x00FF0000) >> 16; - if (mode == 0) // Standalone mode - { - ctx.Start(true); - } - else // If synchronized mode, reset counter independently - { - ctx.ResetFrameClock(); - ctx.Start(false); - } - - CollectFramesTokenSource = new CancellationTokenSource(); - CollectFramesToken = CollectFramesTokenSource.Token; - - FrameQueue = new BlockingCollection(MaxQueuedFrames); - - readFrames = Task.Factory.StartNew(() => - { - try - { - while (!CollectFramesToken.IsCancellationRequested) - { - // NB: This is a blocking call and there is no safe way to terminate it - // other than ending the process. For this reason, it is the job of the - // hardware to provide enough data (e.g. through a HeartbeatDevice") for - // this call to return. - oni.Frame frame = ReadFrame(); - FrameQueue.Add(frame, CollectFramesToken); - - } - } - catch (OperationCanceledException) - { -#if DEBUG - // NB: If FrameQueue.Add has not been called, frame has ref count 0 when it exits - // while loop context and will be disposed. - Console.WriteLine("Frame collection task has been cancelled by " + this.GetType()); -#endif - }; - }, - CollectFramesToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - distributeFrames = Task.Factory.StartNew(() => - { - try - { - while (!CollectFramesToken.IsCancellationRequested) - { - if (FrameQueue.TryTake(out oni.Frame frame, QueueTimeoutMilliseconds, CollectFramesToken)) - { - FrameReceived.OnNext(frame); - frame.Dispose(); - } - } - } - catch (OperationCanceledException) - { -#if DEBUG - // NB: If the thread stops no frame has been collected - Console.WriteLine("Frame distribution task has been cancelled by " + this.GetType()); -#endif - } - }, - CollectFramesToken, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - - running = true; - } - } - - internal void Stop() - { - lock (regLock) - { - if (!running) return; - if ((distributeFrames != null || readFrames != null) && !distributeFrames.IsCanceled) - { - CollectFramesTokenSource.Cancel(); - Task.WaitAll(new Task[] { distributeFrames, readFrames }); - } - CollectFramesTokenSource?.Dispose(); - CollectFramesTokenSource = null; - - // Clear queue and free memory - while (FrameQueue?.Count > 0) - { - var frame = FrameQueue.Take(); - frame.Dispose(); - } - FrameQueue?.Dispose(); - FrameQueue = null; - ctx.Stop(); - running = false; - - ContextConfiguration?.Dispose(); - } - } - - #region oni.Context delegates - internal Action SetCustomOption => ctx.SetCustomOption; - internal Func GetCustomOption => ctx.GetCustomOption; - internal Action ResetFrameClock => ctx.ResetFrameClock; - - internal bool Running - { - get - { - return ctx.Running; - } - } - - public int HardwareAddress - { - get - { - return ctx.HardwareAddress; - } - set - { - ctx.HardwareAddress = value; - } - } - - public int BlockReadSize - { - get - { - return ctx.BlockReadSize; - } - } - - public int BlockWriteSize - { - get - { - return ctx.BlockWriteSize; - } - } - - public PassthroughState HubState - { - get - { - return (PassthroughState)ctx.GetCustomOption((int)oni.ONIXOption.PORTFUNC); - } - set - { - // PortA and PortB each have a bit in portfunc - ctx.SetCustomOption((int)oni.ONIXOption.PORTFUNC, (int)value); - } - } - - // NB: This is for actions that require synchronized register access and might - // be called asynchronously with context dispose - internal void EnsureContext(Action action) - { - lock (disposeLock) - { - if (ctx != null) - action(); - } - } - - internal uint ReadRegister(uint deviceAddress, uint registerAddress) - { - lock (regLock) - { - return ctx.ReadRegister(deviceAddress, registerAddress); - } - } - - internal void WriteRegister(uint deviceAddress, uint registerAddress, uint value) - { - lock (regLock) - { - ctx.WriteRegister(deviceAddress, registerAddress, value); - } - } - - public oni.Frame ReadFrame() - { - lock (readLock) - { - return ctx.ReadFrame(); - } - } - - public void Write(uint deviceAddress, T data) where T : unmanaged - { - lock (writeLock) - { - ctx.Write(deviceAddress, data); - } - } - - public void Write(uint deviceAddress, T[] data) where T : unmanaged - { - lock (writeLock) - { - ctx.Write(deviceAddress, data); - } - } - - public void Write(uint deviceAddress, IntPtr data, int dataSize) - { - lock (writeLock) - { - ctx.Write(deviceAddress, data, dataSize); - } - } - - public oni.Hub GetHub(uint deviceAddress) => ctx.GetHub(deviceAddress); - - public virtual uint GetPassthroughDeviceAddress(uint deviceAddress) - { - var hubAddress = (deviceAddress & 0xFF00u) >> 8; - if (hubAddress == 0) - { - throw new ArgumentException( - "Device addresses on hub zero cannot be used to create passthrough devices.", - nameof(deviceAddress)); - } - - return hubAddress + 7; - } - - #endregion - - public void Dispose() - { - lock (disposeLock) - lock (regLock) - { - Stop(); - lock (readLock) - lock (writeLock) - { - ctx?.Dispose(); - ctx = null; - } - } - - GC.SuppressFinalize(this); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/CreateContext.cs b/OpenEphys.Onix/OpenEphys.Onix/CreateContext.cs deleted file mode 100644 index eedc7c29..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/CreateContext.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Bonsai; -using System; -using System.ComponentModel; -using System.Reactive.Linq; - -namespace OpenEphys.Onix -{ - [Description("")] - [Combinator(MethodName = nameof(Generate))] - [WorkflowElementCategory(ElementCategory.Source)] - public class CreateContext - { - public string Driver { get; set; } = "riffa"; - - public int Index { get; set; } - - public IObservable Generate() - { - return Observable.Create(observer => - { - var driver = Driver; - var index = Index; - var context = new ContextTask(driver, index); - try - { - observer.OnNext(context); - return context; - } - catch - { - context.Dispose(); - throw; - } - }); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DeviceFactory.cs b/OpenEphys.Onix/OpenEphys.Onix/DeviceFactory.cs deleted file mode 100644 index cdd8ea09..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/DeviceFactory.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using Bonsai; - -namespace OpenEphys.Onix -{ - public abstract class DeviceFactory : Sink - { - internal const string ConfigurationCategory = "Configuration"; - internal const string AcquisitionCategory = "Acquisition"; - - internal abstract IEnumerable GetDevices(); - } - - public abstract class SingleDeviceFactory : DeviceFactory, IDeviceConfiguration - { - internal SingleDeviceFactory(Type deviceType) - { - DeviceType = deviceType ?? throw new ArgumentNullException(nameof(deviceType)); - } - - public string DeviceName { get; set; } - - public uint DeviceAddress { get; set; } - - [Browsable(false)] - public Type DeviceType { get; } - - internal override IEnumerable GetDevices() - { - yield return this; - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DeviceID.cs b/OpenEphys.Onix/OpenEphys.Onix/DeviceID.cs deleted file mode 100644 index 06af9e25..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/DeviceID.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace OpenEphys.Onix -{ - internal enum DeviceID - { - Null = 0, - Info = 1, - Rhd2132 = 2, - Rhd2164 = 3, - ElectricalStimulator = 4, - OpticalStimulator = 5, - TS4231 = 6, - DigitalInput32 = 7, - DigitalOutput32 = 8, - Bno055 = 9, - Test = 10, - NeuropixelsV1 = 11, - Heartbeat = 12, - AD51X2 = 13, - FmcVoltageController = 14, - AD7617 = 15, - AD576X = 16, - TestRegisterV0 = 17, - BreakoutDigitalIO = 18, - FmcClockInput = 19, - FmcClockOutput = 20, - TS4231V2Array = 21, - BreakoutAnalogIO = 22, - FmcLinkController = 23, - DS90UB9X = 24, - TS4231V1Array = 25, - Max10AdcCore = 26, - LoadTest = 27, - MemoryUsage = 28, - HarpSyncInput = 30, - Rhs2116 = 31, - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DigitalInput.cs b/OpenEphys.Onix/OpenEphys.Onix/DigitalInput.cs deleted file mode 100644 index fb47c51c..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/DigitalInput.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class DigitalInput : Source - { - [TypeConverter(typeof(DigitalIO.NameConverter))] - public string DeviceName { get; set; } - - public unsafe override IObservable Generate() - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(DigitalIO)); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .Select(frame => new DigitalInputDataFrame(frame)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DigitalInputDataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/DigitalInputDataFrame.cs deleted file mode 100644 index 185920e8..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/DigitalInputDataFrame.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Runtime.InteropServices; - -namespace OpenEphys.Onix -{ - public class DigitalInputDataFrame - { - public unsafe DigitalInputDataFrame(oni.Frame frame) - { - Clock = frame.Clock; - var payload = (DigitalInputPayload*)frame.Data.ToPointer(); - HubClock = payload->HubClock; - DigitalInputs = payload->DigitalInputs; - Buttons = payload->Buttons; - } - - public ulong Clock { get; } - - public ulong HubClock { get; } - - public DigitalPortState DigitalInputs { get; } - - public BreakoutButtonState Buttons { get; } - - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct DigitalInputPayload - { - public ulong HubClock; - public DigitalPortState DigitalInputs; - public BreakoutButtonState Buttons; - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DigitalOutput.cs b/OpenEphys.Onix/OpenEphys.Onix/DigitalOutput.cs deleted file mode 100644 index 209940fb..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/DigitalOutput.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class DigitalOutput : Sink - { - [TypeConverter(typeof(DigitalIO.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Process(IObservable source) - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(DigitalIO)); - return source.Do(value => device.Write((uint)value)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/HarpSyncInputData.cs b/OpenEphys.Onix/OpenEphys.Onix/HarpSyncInputData.cs deleted file mode 100644 index 3f6d981c..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/HarpSyncInputData.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class HarpSyncInputData : Source - { - [TypeConverter(typeof(HarpSyncInput.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Generate() - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(HarpSyncInput)); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .Select(frame => new HarpSyncInputDataFrame(frame)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/HarpSyncInputDataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/HarpSyncInputDataFrame.cs deleted file mode 100644 index e67d1212..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/HarpSyncInputDataFrame.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Runtime.InteropServices; - -namespace OpenEphys.Onix -{ - public class HarpSyncInputDataFrame - { - public unsafe HarpSyncInputDataFrame(oni.Frame frame) - { - Clock = frame.Clock; - var payload = (HarpSyncInputPayload*)frame.Data.ToPointer(); - HubClock = payload->HubClock; - HarpTime = payload->HarpTime; - } - - public ulong Clock { get; } - - public ulong HubClock { get; } - - public uint HarpTime { get; } - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct HarpSyncInputPayload - { - public ulong HubClock; - public uint HarpTime; - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Headstage64ElectricalStimulatorTrigger.cs b/OpenEphys.Onix/OpenEphys.Onix/Headstage64ElectricalStimulatorTrigger.cs deleted file mode 100644 index f1cbf28f..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/Headstage64ElectricalStimulatorTrigger.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.ComponentModel; -using System.Drawing.Design; -using System.Linq; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class Headstage64ElectricalStimulatorTrigger: Sink - { - readonly BehaviorSubject enable = new(true); - readonly BehaviorSubject phaseOneCurrent = new(0); - readonly BehaviorSubject interPhaseCurrent = new(0); - readonly BehaviorSubject phaseTwoCurrent = new(0); - readonly BehaviorSubject phaseOneDuration = new(0); - readonly BehaviorSubject interPhaseInterval = new(0); - readonly BehaviorSubject phaseTwoDuration = new(0); - readonly BehaviorSubject interPulseInterval = new(0); - readonly BehaviorSubject burstPulseCount = new(0); - readonly BehaviorSubject interBurstInterval = new(0); - readonly BehaviorSubject trainBurstCount = new(0); - readonly BehaviorSubject trainDelay = new(0); - readonly BehaviorSubject powerEnable = new(false); - - - [TypeConverter(typeof(Headstage64ElectricalStimulator.NameConverter))] - public string DeviceName { get; set; } - - [Description("Specifies whether the electrical stimulation subcircuit will respect triggers.")] - public bool Enable - { - get => enable.Value; - set => enable.OnNext(value); - } - - [Description("Phase 1 pulse current (uA).")] - [Range(-Headstage64ElectricalStimulator.AbsMaxMicroAmps, Headstage64ElectricalStimulator.AbsMaxMicroAmps)] - [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] - [Precision(3, 1)] - public double PhaseOneCurrent - { - get => phaseOneCurrent.Value; - set => phaseOneCurrent.OnNext(value); - } - - [Description("Interphase rest current (uA).")] - [Range(-Headstage64ElectricalStimulator.AbsMaxMicroAmps, Headstage64ElectricalStimulator.AbsMaxMicroAmps)] - [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] - [Precision(3, 1)] - public double InterPhaseCurrent - { - get => interPhaseCurrent.Value; - set => interPhaseCurrent.OnNext(value); - } - - [Description("Phase 2 pulse current (uA).")] - [Range(-Headstage64ElectricalStimulator.AbsMaxMicroAmps, Headstage64ElectricalStimulator.AbsMaxMicroAmps)] - [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] - [Precision(3, 1)] - public double PhaseTwoCurrent - { - get => phaseTwoCurrent.Value; - set => phaseTwoCurrent.OnNext(value); - } - - [Description("Pulse train start delay (uSec).")] - [Range(0, uint.MaxValue)] - public uint TrainDelay - { - get => trainDelay.Value; - set => trainDelay.OnNext(value); - } - - [Description("Phase 1 pulse duration (uSec).")] - [Range(0, uint.MaxValue)] - public uint PhaseOneDuration - { - get => phaseOneDuration.Value; - set => phaseOneDuration.OnNext(value); - } - - [Description("Inter-phase interval (uSec).")] - [Range(0, uint.MaxValue)] - public uint InterPhaseInterval - { - get => interPhaseInterval.Value; - set => interPhaseInterval.OnNext(value); - } - - [Description("Phase 2 pulse duration (uSec).")] - [Range(0, uint.MaxValue)] - public uint PhaseTwoDuration - { - get => phaseTwoDuration.Value; - set => phaseTwoDuration.OnNext(value); - } - - [Description("Inter-pulse interval (uSec).")] - [Range(0, uint.MaxValue)] - public uint InterPulseInterval - { - get => interPulseInterval.Value; - set => interPulseInterval.OnNext(value); - } - - [Description("Inter-burst interval (uSec).")] - [Range(0, uint.MaxValue)] - public uint InterBurstInterval - { - get => interBurstInterval.Value; - set => interBurstInterval.OnNext(value); - } - - [Description("Number of pulses in each burst.")] - [Range(0, uint.MaxValue)] - public uint BurstPulseCount - { - get => burstPulseCount.Value; - set => burstPulseCount.OnNext(value); - } - - [Description("Number of bursts in each train.")] - [Range(0, uint.MaxValue)] - public uint TrainBurstCount - { - get => trainBurstCount.Value; - set => trainBurstCount.OnNext(value); - } - - [Description("Stimulator power on/off.")] - [Range(0, uint.MaxValue)] - public bool PowerEnable - { - get => powerEnable.Value; - set => powerEnable.OnNext(value); - } - - public override IObservable Process(IObservable source) - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - Observable.Create(observer => - { - var device = deviceInfo.GetDeviceContext(typeof(Headstage64ElectricalStimulator)); - var triggerObserver = Observer.Create( - value => device.WriteRegister(Headstage64ElectricalStimulator.TRIGGER, value ? 1u : 0u), - observer.OnError, - observer.OnCompleted); - - static uint uAToCode(double currentuA) - { - var k = 1 / (2 * Headstage64ElectricalStimulator.AbsMaxMicroAmps / (Math.Pow(2, Headstage64ElectricalStimulator.DacBitDepth) - 1)); // static - return (uint)(k * (currentuA + Headstage64ElectricalStimulator.AbsMaxMicroAmps)); - } - - return new CompositeDisposable( - enable.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.ENABLE, value ? 1u : 0u)), - phaseOneCurrent.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT1, uAToCode(value))), - interPhaseCurrent.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.RESTCURR, uAToCode(value))), - phaseTwoCurrent.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT2, uAToCode(value))), - trainDelay.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.TRAINDELAY, value)), - phaseOneDuration.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.PULSEDUR1, value)), - interPhaseInterval.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.INTERPHASEINTERVAL, value)), - phaseTwoDuration.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.PULSEDUR2, value)), - interPulseInterval.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.INTERPULSEINTERVAL, value)), - interBurstInterval.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.INTERBURSTINTERVAL, value)), - burstPulseCount.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.BURSTCOUNT, value)), - trainBurstCount.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.TRAINCOUNT, value)), - powerEnable.Subscribe(value => device.WriteRegister(Headstage64ElectricalStimulator.POWERON, value ? 1u : 0u)), - source.SubscribeSafe(triggerObserver) - ); - }))); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Headstage64OpticalStimulatorTrigger.cs b/OpenEphys.Onix/OpenEphys.Onix/Headstage64OpticalStimulatorTrigger.cs deleted file mode 100644 index 3235c4b1..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/Headstage64OpticalStimulatorTrigger.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using System.ComponentModel; -using System.Drawing.Design; -using System.Linq; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class Headstage64OpticalStimulatorTrigger : Sink - { - readonly BehaviorSubject enable = new(true); - readonly BehaviorSubject maxCurrent = new(100); - readonly BehaviorSubject channelOneCurrent = new(100); - readonly BehaviorSubject channelTwoCurrent = new(0); - readonly BehaviorSubject pulseDuration = new(5); - readonly BehaviorSubject pulsesPerSecond = new(50); - readonly BehaviorSubject pulsesPerBurst = new(20); - readonly BehaviorSubject interBurstInterval = new(0); - readonly BehaviorSubject burstsPerTrain = new(1); - readonly BehaviorSubject delay = new(0); - - [TypeConverter(typeof(Headstage64OpticalStimulator.NameConverter))] - public string DeviceName { get; set; } - - [Description("Specifies whether the optical stimulation subcircuit will respect triggers.")] - public bool Enable - { - get => enable.Value; - set => enable.OnNext(value); - } - - [Description("Maximum current per channel per pulse (mA). " + - "This value is used by both channels. To get different amplitudes " + - "for each channel use the Channel0Level and Channel1Level parameters.")] - [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] - [Range(0, 300)] - [Precision(3, 0)] - public double MaxCurrent - { - get => maxCurrent.Value; - set => maxCurrent.OnNext(value); - } - - [Description("Channel 1 percent of MaxCurrent. If greater than 0, channel 1 will respond to triggers.")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(0, 100)] - [Precision(1, 12.5)] - public double ChannelOneCurrent - { - get => channelOneCurrent.Value; - set => channelOneCurrent.OnNext(value); - } - - [Description("Channel 2 percent of MaxCurrent. If greater than 0, channel 2 will respond to triggers.")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(0, 100)] - [Precision(1, 12.5)] - public double ChannelTwoCurrent - { - get => channelTwoCurrent.Value; - set => channelTwoCurrent.OnNext(value); - } - - [Description("Pulse duration (msec).")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(0.001, 1000.0)] - [Precision(3, 1)] - public double PulseDuration - { - get => pulseDuration.Value; - set => pulseDuration.OnNext(value); - } - - [Description("Pulse period (msec).")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(0.01, 10000.0)] - [Precision(3, 1)] - public double PulsesPerSecond - { - get => pulsesPerSecond.Value; - set => pulsesPerSecond.OnNext(value); - } - - [Description("Number of pulses to deliver in a burst.")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(1, int.MaxValue)] - [Precision(0, 1)] - public uint PulsesPerBurst - { - get => pulsesPerBurst.Value; - set => pulsesPerBurst.OnNext(value); - } - - [Description("Inter-burst interval (msec).")] - [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(0.0, 10000.0)] - [Precision(3, 1)] - public double InterBurstInterval - { - get => interBurstInterval.Value; - set => interBurstInterval.OnNext(value); - } - - [Description("Number of bursts to deliver in a train.")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(1, int.MaxValue)] - [Precision(0, 1)] - public uint BurstsPerTrain - { - get => burstsPerTrain.Value; - set => burstsPerTrain.OnNext(value); - } - - [Description("Delay between issue of trigger and start of train (msec).")] - [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] - [Range(0.0, 1000.0)] - [Precision(3, 1)] - public double Delay - { - get => delay.Value; - set => delay.OnNext(value); - } - - // TODO: Should this be checked before TRIGGER is written to below and an error thrown if - // DC current is too high? Or, should settings be forced too keep DC current under some value? - [Description("Direct current required during burst (mA). Should be less than 50 mA.")] - public double BurstCurrent - { - get - { - return PulsesPerSecond * 0.001 * PulseDuration * MaxCurrent * 0.01 * (ChannelOneCurrent + ChannelTwoCurrent); - } - } - - public override IObservable Process(IObservable source) - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - Observable.Create(observer => - { - var device = deviceInfo.GetDeviceContext(typeof(Headstage64OpticalStimulator)); - var triggerObserver = Observer.Create( - value => device.WriteRegister(Headstage64OpticalStimulator.TRIGGER, value ? 1u : 0u), - observer.OnError, - observer.OnCompleted); - - // NB: fit from Fig. 10 of CAT4016 datasheet - // x = (y/a)^(1/b) - // a = 3.833e+05 - // b = -0.9632 - static uint mAToPotSetting(double currentMa) - { - double R = Math.Pow(currentMa / 3.833e+05, 1 / -0.9632); - double s = 256 * (R - Headstage64OpticalStimulator.MinRheostatResistanceOhms) / Headstage64OpticalStimulator.PotResistanceOhms; - return s > 255 ? 255 : s < 0 ? 0 : (uint)s; - } - - uint currentSourceMask = 0; - static uint percentToPulseMask(int channel, double percent, uint oldMask) - { - uint mask = 0x00000000; - var p = 0.0; - while (p < percent) - { - mask = (mask << 1) | 1; - p += 12.5; - } - - return channel == 0 ? (oldMask & 0x0000FF00) | mask : (mask << 8) | (oldMask & 0x000000FF); - } - - static uint pulseDurationToRegister(double pulseDuration, double pulseHz) - { - var pulsePeriod = 1000.0 / pulseHz; - return pulseDuration > pulsePeriod ? (uint)(1000 * pulsePeriod - 1): (uint)(1000 * pulseDuration); - } - - static uint pulseFrequencyToRegister(double pulseHz, double pulseDuration) - { - var pulsePeriod = 1000.0 / pulseHz; - return pulsePeriod > pulseDuration ? (uint)(1000 * pulsePeriod) : (uint)(1000 * pulseDuration + 1); - } - - return new CompositeDisposable( - enable.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.ENABLE, value ? 1u : 0u)), - maxCurrent.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.MAXCURRENT, mAToPotSetting(value))), - channelOneCurrent.Subscribe(value => - { - currentSourceMask = percentToPulseMask(0, value, currentSourceMask); - device.WriteRegister(Headstage64OpticalStimulator.PULSEMASK, currentSourceMask); - }), - channelTwoCurrent.Subscribe(value => - { - currentSourceMask = percentToPulseMask(1, value, currentSourceMask); - device.WriteRegister(Headstage64OpticalStimulator.PULSEMASK, currentSourceMask); - }), - pulseDuration.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.PULSEDUR, pulseDurationToRegister(value, PulsesPerSecond))), - pulsesPerSecond.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.PULSEPERIOD, pulseFrequencyToRegister(value, PulseDuration))), - pulsesPerBurst.Subscribe(value =>device.WriteRegister(Headstage64OpticalStimulator.BURSTCOUNT, value)), - interBurstInterval.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.IBI, (uint)(1000 * value))), - burstsPerTrain.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.TRAINCOUNT, value)), - delay.Subscribe(value => device.WriteRegister(Headstage64OpticalStimulator.TRAINDELAY, (uint)(1000 * value))), - source.SubscribeSafe(triggerObserver) - ); - }))); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/HeartbeatCounter.cs b/OpenEphys.Onix/OpenEphys.Onix/HeartbeatCounter.cs deleted file mode 100644 index 88b6d8f9..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/HeartbeatCounter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class HeartbeatCounter : Source> - { - [TypeConverter(typeof(Heartbeat.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable> Generate() - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(Heartbeat)); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .Select(frame => new ManagedFrame(frame)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/HubConfiguration.cs b/OpenEphys.Onix/OpenEphys.Onix/HubConfiguration.cs deleted file mode 100644 index 3d2f4e40..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/HubConfiguration.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OpenEphys.Onix -{ - public enum HubConfiguration - { - Standard, - Passthrough - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/HubDeviceFactory.cs b/OpenEphys.Onix/OpenEphys.Onix/HubDeviceFactory.cs deleted file mode 100644 index 0682d008..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/HubDeviceFactory.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Bonsai; - -namespace OpenEphys.Onix -{ - public abstract class HubDeviceFactory : DeviceFactory, INamedElement - { - const string BaseTypePrefix = "Configure"; - string _name; - - protected HubDeviceFactory() - { - var baseName = GetType().Name; - var prefixIndex = baseName.IndexOf(BaseTypePrefix); - Name = prefixIndex >= 0 ? baseName.Substring(prefixIndex + BaseTypePrefix.Length) : baseName; - } - - public string Name - { - get { return _name; } - set - { - _name = value; - UpdateDeviceNames(); - } - } - - protected string GetFullDeviceName(string deviceName) - { - return !string.IsNullOrEmpty(_name) ? $"{_name}/{deviceName}" : string.Empty; - } - - internal virtual void UpdateDeviceNames() - { - foreach (var device in GetDevices()) - { - device.DeviceName = GetFullDeviceName(device.DeviceType.Name); - } - } - - public override IObservable Process(IObservable source) - { - if (string.IsNullOrEmpty(_name)) - { - throw new InvalidOperationException("A valid hub device name must be specified."); - } - - var output = source; - foreach (var device in GetDevices()) - { - output = device.Process(output); - } - - return output; - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ManagedFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/ManagedFrame.cs deleted file mode 100644 index 521d636a..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/ManagedFrame.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace OpenEphys.Onix -{ - /// - /// Managed copy of with strongly-typed data array. - /// - /// The data type of the Sample array - public class ManagedFrame where T : unmanaged - { - public ManagedFrame(oni.Frame frame) - { - Sample = frame.GetData(); - FrameClock = frame.Clock; - DeviceAddress = frame.DeviceAddress; - } - - public readonly T[] Sample; - - public ulong FrameClock { get; private set; } - - public uint DeviceAddress { get; private set; } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/MemoryUsage.cs b/OpenEphys.Onix/OpenEphys.Onix/MemoryUsage.cs deleted file mode 100644 index 045eb8c5..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/MemoryUsage.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class MemoryUsage : Source - { - [TypeConverter(typeof(MemoryMonitor.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Generate() - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(MemoryMonitor)); - var totalMemory = device.ReadRegister(MemoryMonitor.TOTAL_MEM); - - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .Select(frame => new MemoryUsageDataFrame(frame, totalMemory)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/MemoryUsageDataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/MemoryUsageDataFrame.cs deleted file mode 100644 index b2cb8c0d..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/MemoryUsageDataFrame.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Runtime.InteropServices; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class MemoryUsageDataFrame - { - public unsafe MemoryUsageDataFrame(oni.Frame frame, uint totalMemory) - { - var payload = (MemoryUsagePayload*)frame.Data.ToPointer(); - - FrameClock = frame.Clock; - DeviceAddress = frame.DeviceAddress; - HubClock = payload->HubClock; - PercentUsed = 100.0 * payload->Usage / totalMemory; - BytesUsed = payload->Usage * 4; - - } - - public ulong FrameClock { get; private set; } - - public uint DeviceAddress { get; private set; } - - public ulong HubClock { get; } - - public double PercentUsed { get; } - - public uint BytesUsed { get; } - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct MemoryUsagePayload - { - public ulong HubClock; - public uint Usage; - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eAdc.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eAdc.cs deleted file mode 100644 index 9bf3f4d3..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eAdc.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace OpenEphys.Onix -{ - public class NeuropixelsV1eAdc - { - public int CompP { get; set; } = 16; - public int CompN { get; set; } = 16; - public int Slope { get; set; } = 0; - public int Coarse { get; set; } = 0; - public int Fine { get; set; } = 0; - public int Cfix { get; set; } = 0; - public int Offset { get; set; } = 0; - public int Threshold { get; set; } = 512; - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eBno055Data.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eBno055Data.cs deleted file mode 100644 index 7a699148..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eBno055Data.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class NeuropixelsV1eBno055Data : Source - { - [TypeConverter(typeof(NeuropixelsV1eBno055.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Generate() - { - // Max of 100 Hz, but limited by I2C bus - var source = Observable.Interval(TimeSpan.FromSeconds(0.01)); - return Generate(source); - } - - public unsafe IObservable Generate(IObservable source) - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany( - deviceInfo => Observable.Create(observer => - { - var device = deviceInfo.GetDeviceContext(typeof(NeuropixelsV1eBno055)); - var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); - var i2c = new I2CRegisterContext(passthrough, NeuropixelsV1eBno055.BNO055Address); - - var pollingObserver = Observer.Create( - _ => - { - Bno055DataFrame frame = default; - device.Context.EnsureContext(() => - { - var data = i2c.ReadBytes(NeuropixelsV1eBno055.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/NeuropixelsV1eData.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eData.cs deleted file mode 100644 index 55d70f92..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eData.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using Bonsai; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class NeuropixelsV1eData : Source - { - [TypeConverter(typeof(NeuropixelsV1e.NameConverter))] - public string DeviceName { get; set; } - - int bufferSize = 36; - [Description("Number of super-frames (384 channels from spike band and 32 channels from " + - "LFP band) to buffer before propogating data. Must be a mulitple of 12.")] - public int BufferSize - { - get => bufferSize; - set => bufferSize = (int)(Math.Ceiling((double)value / NeuropixelsV1e.FramesPerRoundRobin) * NeuropixelsV1e.FramesPerRoundRobin); - } - - public unsafe override IObservable Generate() - { - var spikeBufferSize = BufferSize; - var lfpBufferSize = spikeBufferSize / NeuropixelsV1e.FramesPerRoundRobin; - - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var info = (NeuropixelsV1eDeviceInfo)deviceInfo; - var device = info.GetDeviceContext(typeof(NeuropixelsV1e)); - var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); - var probeData = device.Context.FrameReceived.Where(frame => frame.DeviceAddress == passthrough.Address); - - return Observable.Create(observer => - { - var sampleIndex = 0; - var spikeBuffer = new ushort[NeuropixelsV1e.ChannelCount, spikeBufferSize]; - var lfpBuffer = new ushort[NeuropixelsV1e.ChannelCount, lfpBufferSize]; - var frameCountBuffer = new int[spikeBufferSize * NeuropixelsV1e.FramesPerSuperFrame]; - var hubClockBuffer = new ulong[spikeBufferSize]; - var clockBuffer = new ulong[spikeBufferSize]; - - var frameObserver = Observer.Create( - frame => - { - var payload = (NeuropixelsV1ePayload*)frame.Data.ToPointer(); - NeuropixelsV1eDataFrame.CopyAmplifierBuffer(payload->AmplifierData, frameCountBuffer, spikeBuffer, lfpBuffer, sampleIndex, info.ApGainCorrection, info.LfpGainCorrection, info.AdcThresholds, info.AdcOffsets); - hubClockBuffer[sampleIndex] = payload->HubClock; - clockBuffer[sampleIndex] = frame.Clock; - if (++sampleIndex >= spikeBufferSize) - { - var spikeData = Mat.FromArray(spikeBuffer); - var lfpData = Mat.FromArray(lfpBuffer); - observer.OnNext(new NeuropixelsV1eDataFrame(clockBuffer, hubClockBuffer, frameCountBuffer, spikeData, lfpData)); - frameCountBuffer = new int[spikeBufferSize * NeuropixelsV1e.FramesPerSuperFrame]; - hubClockBuffer = new ulong[spikeBufferSize]; - clockBuffer = new ulong[spikeBufferSize]; - sampleIndex = 0; - } - }, - observer.OnError, - observer.OnCompleted); - return probeData.SubscribeSafe(frameObserver); - }); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2.cs deleted file mode 100644 index 3488fdc5..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace OpenEphys.Onix -{ - public enum NeuropixelsV2Probe - { - ProbeA = 0, - ProbeB = 1 - } - - [Flags] - enum NeuropixelsV2Status : uint - { - SR_OK = 1 << 7 // Indicates the SR chain comparison is OK - } - - static class NeuropixelsV2 - { - public const int ChannelCount = 384; - public const int BaseBitsPerChannel = 4; - public const int ElectrodePerShank = 1280; - public const int ReferencePixelCount = 4; - public const int DummyRegisterCount = 4; - public const int RegistersPerShank = ElectrodePerShank + ReferencePixelCount + DummyRegisterCount; - - // memory map - public const int STATUS = 0x09; - public const int SR_CHAIN6 = 0x0C; // Odd channel base config - public const int SR_CHAIN5 = 0x0D; // Even channel base config - public const int SR_CHAIN4 = 0x0E; // Shank 4 - public const int SR_CHAIN3 = 0x0F; // Shank 3 - public const int SR_CHAIN2 = 0x10; // Shank 2 - public const int SR_CHAIN1 = 0x11; // Shank 1 - public const int SR_LENGTH2 = 0x12; - public const int SR_LENGTH1 = 0x13; - public const int PROBE_ID = 0x14; - public const int SOFT_RESET = 0x15; - } - -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2RegisterContext.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2RegisterContext.cs deleted file mode 100644 index 5c0fcfc7..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2RegisterContext.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections; - -namespace OpenEphys.Onix -{ - class NeuropixelsV2RegisterContext : I2CRegisterContext - { - public NeuropixelsV2RegisterContext(I2CRegisterContext other, uint i2cAddress) - : base(other, i2cAddress) - { - } - - public NeuropixelsV2RegisterContext(DeviceContext deviceContext, uint i2cAddress) - : base(deviceContext, i2cAddress) - { - } - - public void WriteConfiguration(NeuropixelsV2QuadShankProbeConfiguration probe) - { - var baseBits = GenerateBaseBits(probe); - WriteShiftRegister(NeuropixelsV2.SR_CHAIN5, baseBits[0]); - WriteShiftRegister(NeuropixelsV2.SR_CHAIN6, baseBits[1]); - - var shankBits = GenerateShankBits(probe); - WriteShiftRegister(NeuropixelsV2.SR_CHAIN1, shankBits[0]); - WriteShiftRegister(NeuropixelsV2.SR_CHAIN2, shankBits[1]); - WriteShiftRegister(NeuropixelsV2.SR_CHAIN3, shankBits[2]); - WriteShiftRegister(NeuropixelsV2.SR_CHAIN4, shankBits[3]); - - } - - // Bits go into the shift registers MSB first - // This creates a *bit-reversed* byte array from a bit array - static byte[] BitArrayToBytes(BitArray bits) - { - if (bits.Length == 0) - { - throw new ArgumentException("Shift register data is empty", nameof(bits)); - } - - var bytes = new byte[(bits.Length - 1) / 8 + 1]; - bits.CopyTo(bytes, 0); - - for (int i = 0; i < bytes.Length; i++) - { - // NB: http://graphics.stanford.edu/~seander/bithacks.html - bytes[i] = (byte)((bytes[i] * 0x0202020202ul & 0x010884422010ul) % 1023); - } - - return bytes; - } - - // TODO: NeuropixelsV2.STATUS always fails. - private void WriteShiftRegister(uint srAddress, BitArray data) - { - var bytes = BitArrayToBytes(data); - - //var count = 2; - //while (count-- > 0) - //{ - // This allows Base shift registers to get a good STATUS, but does not help shank registers. - //WriteByte(NeuropixelsV2.SOFT_RESET, 0xFF); - //WriteByte(NeuropixelsV2.SOFT_RESET, 0x00); - - WriteByte(NeuropixelsV2.SR_LENGTH1, (uint)bytes.Length % 0x100); - WriteByte(NeuropixelsV2.SR_LENGTH2, (uint)bytes.Length / 0x100); - - foreach (var b in bytes) - { - WriteByte(srAddress, b); - } - //} - - //if (ReadByte(NeuropixelsV2.STATUS) != (uint)NeuropixelsV2Status.SR_OK) - //{ - // // TODO: This check always fails - // throw new InvalidOperationException($"Shift register {srAddress} status check failed."); - //} - } - - public static BitArray[] GenerateShankBits(NeuropixelsV2QuadShankProbeConfiguration probe) - { - BitArray[] shankBits = - { - new(NeuropixelsV2.RegistersPerShank, false), - new(NeuropixelsV2.RegistersPerShank, false), - new(NeuropixelsV2.RegistersPerShank, false), - new(NeuropixelsV2.RegistersPerShank, false) - }; - - // If tip reference is used, activate the tip electrodes - if (probe.Reference != NeuropixelsV2QuadShankReference.External) - { - shankBits[(int)probe.Reference - 1][643] = true; - shankBits[(int)probe.Reference - 1][644] = true; - } - - const int PixelOffset = (NeuropixelsV2.ElectrodePerShank - 1) / 2; - const int ReferencePixelOffset = 3; - foreach (var c in probe.ChannelMap) - { - var baseIndex = c.IntraShankElectrodeIndex % 2; - var pixelIndex = c.IntraShankElectrodeIndex / 2; - pixelIndex = baseIndex == 0 - ? pixelIndex + PixelOffset + 2 * ReferencePixelOffset - : PixelOffset - pixelIndex + ReferencePixelOffset; - - shankBits[c.Shank][pixelIndex] = true; - } - - return shankBits; - } - - public static BitArray[] GenerateBaseBits(NeuropixelsV2QuadShankProbeConfiguration probe) - { - BitArray[] baseBits = - { - new(NeuropixelsV2.ChannelCount * NeuropixelsV2.BaseBitsPerChannel / 2, false), - new(NeuropixelsV2.ChannelCount * NeuropixelsV2.BaseBitsPerChannel / 2, false) - }; - - var referenceBit = probe.Reference switch - { - NeuropixelsV2QuadShankReference.External => 1, - NeuropixelsV2QuadShankReference.Tip1 => 2, - NeuropixelsV2QuadShankReference.Tip2 => 2, - NeuropixelsV2QuadShankReference.Tip3 => 2, - NeuropixelsV2QuadShankReference.Tip4 => 2, - _ => throw new InvalidOperationException("Invalid reference selection."), - }; - - for (int i = 0; i < NeuropixelsV2.ChannelCount; i++) - { - var configIndex = i % 2; - var bitOffset = (382 - i + configIndex) / 2 * NeuropixelsV2.BaseBitsPerChannel; - baseBits[configIndex][bitOffset + 0] = false; // standby bit - baseBits[configIndex][bitOffset + referenceBit ] = true; - } - - return baseBits; - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaData.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaData.cs deleted file mode 100644 index 4de16e61..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaData.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using Bonsai; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class NeuropixelsV2eBetaData : Source - { - [TypeConverter(typeof(NeuropixelsV2eBeta.NameConverter))] - public string DeviceName { get; set; } - - public int BufferSize { get; set; } = 30; - - public NeuropixelsV2Probe ProbeIndex { get; set; } - - public unsafe override IObservable Generate() - { - var bufferSize = BufferSize; - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var info = (NeuropixelsV2eDeviceInfo)deviceInfo; - var device = info.GetDeviceContext(typeof(NeuropixelsV2eBeta)); - var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); - var probeData = device.Context.FrameReceived.Where(frame => - frame.DeviceAddress == passthrough.Address && - NeuropixelsV2eBetaDataFrame.GetProbeIndex(frame) == (int)ProbeIndex); - - var gainCorrection = ProbeIndex switch - { - NeuropixelsV2Probe.ProbeA => (ushort)info.GainCorrectionA, - NeuropixelsV2Probe.ProbeB => (ushort)info.GainCorrectionB, - _ => throw new ArgumentOutOfRangeException(nameof(ProbeIndex), $"Unexpected ProbeIndex value: {ProbeIndex}"), - }; - - return Observable.Create(observer => - { - var sampleIndex = 0; - var amplifierBuffer = new ushort[NeuropixelsV2.ChannelCount, bufferSize]; - var frameCounter = new int[NeuropixelsV2eBeta.FramesPerSuperFrame * bufferSize]; - var hubClockBuffer = new ulong[bufferSize]; - var clockBuffer = new ulong[bufferSize]; - - var frameObserver = Observer.Create( - frame => - { - var payload = (NeuropixelsV2BetaPayload*)frame.Data.ToPointer(); - NeuropixelsV2eBetaDataFrame.CopyAmplifierBuffer(payload->SuperFrame, amplifierBuffer, frameCounter, sampleIndex, gainCorrection); - hubClockBuffer[sampleIndex] = payload->HubClock; - clockBuffer[sampleIndex] = frame.Clock; - if (++sampleIndex >= bufferSize) - { - var amplifierData = Mat.FromArray(amplifierBuffer); - var dataFrame = new NeuropixelsV2eBetaDataFrame( - clockBuffer, - hubClockBuffer, - amplifierData, - frameCounter); - observer.OnNext(dataFrame); - frameCounter = new int[NeuropixelsV2eBeta.FramesPerSuperFrame * bufferSize]; - hubClockBuffer = new ulong[bufferSize]; - clockBuffer = new ulong[bufferSize]; - sampleIndex = 0; - } - }, - observer.OnError, - observer.OnCompleted); - return probeData.SubscribeSafe(frameObserver); - }); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBno055Data.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBno055Data.cs deleted file mode 100644 index 981ac1a7..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBno055Data.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class NeuropixelsV2eBno055Data : Source - { - [TypeConverter(typeof(NeuropixelsV2eBno055.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Generate() - { - // Max of 100 Hz, but limited by I2C bus - var source = Observable.Interval(TimeSpan.FromSeconds(0.01)); - return Generate(source); - } - - public unsafe IObservable Generate(IObservable source) - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany( - deviceInfo => Observable.Create(observer => - { - var device = deviceInfo.GetDeviceContext(typeof(NeuropixelsV2eBno055)); - var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); - var i2c = new I2CRegisterContext(passthrough, NeuropixelsV2eBno055.BNO055Address); - - var pollingObserver = Observer.Create( - _ => - { - Bno055DataFrame frame = default; - device.Context.EnsureContext(() => - { - var data = i2c.ReadBytes(NeuropixelsV2eBno055.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/NeuropixelsV2eData.cs b/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eData.cs deleted file mode 100644 index fcad11c2..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eData.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using Bonsai; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class NeuropixelsV2eData : Source - { - [TypeConverter(typeof(NeuropixelsV2e.NameConverter))] - public string DeviceName { get; set; } - - public int BufferSize { get; set; } = 30; - - public NeuropixelsV2Probe ProbeIndex { get; set; } - - public unsafe override IObservable Generate() - { - var bufferSize = BufferSize; - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var info = (NeuropixelsV2eDeviceInfo)deviceInfo; - var device = info.GetDeviceContext(typeof(NeuropixelsV2e)); - var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); - var probeData = device.Context.FrameReceived.Where(frame => - frame.DeviceAddress == passthrough.Address && - NeuropixelsV2eDataFrame.GetProbeIndex(frame) == (int)ProbeIndex); - - var gainCorrection = ProbeIndex switch - { - NeuropixelsV2Probe.ProbeA => (ushort)info.GainCorrectionA, - NeuropixelsV2Probe.ProbeB => (ushort)info.GainCorrectionB, - _ => throw new ArgumentOutOfRangeException(nameof(ProbeIndex), $"Unexpected ProbeIndex value: {ProbeIndex}"), - }; - - return Observable.Create(observer => - { - var sampleIndex = 0; - var amplifierBuffer = new ushort[NeuropixelsV2e.ChannelCount, bufferSize]; - var hubClockBuffer = new ulong[bufferSize]; - var clockBuffer = new ulong[bufferSize]; - - var frameObserver = Observer.Create( - frame => - { - var payload = (NeuropixelsV2Payload*)frame.Data.ToPointer(); - NeuropixelsV2eDataFrame.CopyAmplifierBuffer(payload->AmplifierData, amplifierBuffer, sampleIndex, gainCorrection); - hubClockBuffer[sampleIndex] = payload->HubClock; - clockBuffer[sampleIndex] = frame.Clock; - if (++sampleIndex >= bufferSize) - { - var amplifierData = Mat.FromArray(amplifierBuffer); - observer.OnNext(new NeuropixelsV2eDataFrame(clockBuffer, hubClockBuffer, amplifierData)); - hubClockBuffer = new ulong[bufferSize]; - clockBuffer = new ulong[bufferSize]; - sampleIndex = 0; - } - }, - observer.OnError, - observer.OnCompleted); - return probeData.SubscribeSafe(frameObserver); - }); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Rhd2164Config.cs b/OpenEphys.Onix/OpenEphys.Onix/Rhd2164Config.cs deleted file mode 100644 index 37547686..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/Rhd2164Config.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System.Collections.Generic; - -namespace OpenEphys.Onix -{ - public static class Rhd2164Config - { - public static readonly IReadOnlyDictionary> AnalogLowCutoffToRegisters = - new Dictionary>() - { - { Rhd2164AnalogLowCutoff.Low500Hz, new[] { 13, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low300Hz, new[] { 15, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low250Hz, new[] { 17, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low200Hz, new[] { 18, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low150Hz, new[] { 21, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low100Hz, new[] { 25, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low75Hz, new[] { 28, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low50Hz, new[] { 34, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low30Hz, new[] { 44, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low25Hz, new[] { 48, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low20Hz, new[] { 54, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low15Hz, new[] { 62, 0, 0 } }, - { Rhd2164AnalogLowCutoff.Low10Hz, new[] { 5, 1, 0 } }, - { Rhd2164AnalogLowCutoff.Low7500mHz, new[] { 18, 1, 0 } }, - { Rhd2164AnalogLowCutoff.Low5000mHz, new[] { 40, 1, 0 } }, - { Rhd2164AnalogLowCutoff.Low3090mHz, new[] { 20, 2, 0 } }, - { Rhd2164AnalogLowCutoff.Low2500mHz, new[] { 42, 2, 0 } }, - { Rhd2164AnalogLowCutoff.Low2000mHz, new[] { 8, 3, 0 } }, - { Rhd2164AnalogLowCutoff.Low1500mHz, new[] { 9, 4, 0 } }, - { Rhd2164AnalogLowCutoff.Low1000mHz, new[] { 44, 6, 0 } }, - { Rhd2164AnalogLowCutoff.Low750mHz, new[] { 49, 9, 0 } }, - { Rhd2164AnalogLowCutoff.Low500mHz, new[] { 35, 17, 0 } }, - { Rhd2164AnalogLowCutoff.Low300mHz, new[] { 1, 40, 0 } }, - { Rhd2164AnalogLowCutoff.Low250mHz, new[] { 56, 54, 0 } }, - { Rhd2164AnalogLowCutoff.Low100mHz, new[] { 16, 60, 1 } }, - }; - - public static readonly IReadOnlyDictionary> AnalogHighCutoffToRegisters = - new Dictionary>() - { - { Rhd2164AnalogHighCutoff.High20000Hz, new[] { 8, 0, 4, 0 } }, - { Rhd2164AnalogHighCutoff.High15000Hz, new[] { 11, 0, 8, 0 } }, - { Rhd2164AnalogHighCutoff.High10000Hz, new[] { 17, 0, 16, 0 } }, - { Rhd2164AnalogHighCutoff.High7500Hz, new[] { 22, 0, 23, 0 } }, - { Rhd2164AnalogHighCutoff.High5000Hz, new[] { 33, 0, 37, 0 } }, - { Rhd2164AnalogHighCutoff.High3000Hz, new[] { 3, 1, 13, 1 } }, - { Rhd2164AnalogHighCutoff.High2500Hz, new[] { 13, 1, 25, 1 } }, - { Rhd2164AnalogHighCutoff.High2000Hz, new[] { 27, 1, 44, 1 } }, - { Rhd2164AnalogHighCutoff.High1500Hz, new[] { 1, 2, 23, 2 } }, - { Rhd2164AnalogHighCutoff.High1000Hz, new[] { 46, 2, 30, 3 } }, - { Rhd2164AnalogHighCutoff.High750Hz, new[] { 41, 3, 36, 4 } }, - { Rhd2164AnalogHighCutoff.High500Hz, new[] { 30, 5, 43, 6 } }, - { Rhd2164AnalogHighCutoff.High300Hz, new[] { 6, 9, 2, 11 } }, - { Rhd2164AnalogHighCutoff.High250Hz, new[] { 42, 10, 5, 13 } }, - { Rhd2164AnalogHighCutoff.High200Hz, new[] { 24, 13, 7, 16 } }, - { Rhd2164AnalogHighCutoff.High150Hz, new[] { 44, 17, 8, 21 } }, - { Rhd2164AnalogHighCutoff.High100Hz, new[] { 38, 26, 5, 31 } }, - }; - - - } - - public enum Rhd2164AnalogLowCutoff - { - Low500Hz, - Low300Hz, - Low250Hz, - Low200Hz, - Low150Hz, - Low100Hz, - Low75Hz, - Low50Hz, - Low30Hz, - Low25Hz, - Low20Hz, - Low15Hz, - Low10Hz, - Low7500mHz, - Low5000mHz, - Low3090mHz, - Low2500mHz, - Low2000mHz, - Low1500mHz, - Low1000mHz, - Low750mHz, - Low500mHz, - Low300mHz, - Low250mHz, - Low100mHz - } - - public enum Rhd2164AnalogHighCutoff - { - High20000Hz, - High15000Hz, - High10000Hz, - High7500Hz, - High5000Hz, - High3000Hz, - High2500Hz, - High2000Hz, - High1500Hz, - High1000Hz, - High750Hz, - High500Hz, - High300Hz, - High250Hz, - High200Hz, - High150Hz, - High100Hz - } - - public enum Rhd2164DspCutoff - { - /// - /// - /// - Differential = 0, - - /// - /// 3310 Hz - /// - Dsp3309Hz, - - /// - /// 1370 Hz - /// - Dsp1374Hz, - - /// - /// 638 Hz - /// - Dsp638Hz, - - /// - /// 308 Hz - /// - Dsp308Hz, - - /// - /// 152 Hz - /// - Dsp152Hz, - - /// - /// 75.2 Hz - /// - Dsp75Hz, - - /// - /// 37.4 Hz - /// - Dsp37Hz, - - /// - /// 18.7 Hz - /// - Dsp19Hz, - - /// - /// 9.34 Hz - /// - Dsp9336mHz, - - /// - /// 4.67 Hz - /// - Dsp4665mHz, - - /// - /// 2.33 Hz - /// - Dsp2332mHz, - - /// - /// 1.17 Hz - /// - Dsp1166mHz, - - /// - /// 0.583 Hz - /// - Dsp583mHz, - - /// - /// 0.291 Hz - /// - Dsp291mHz, - - /// - /// 0.146 Hz - /// - Dsp146mHz, - - /// - /// - /// - Off - } - - public enum Rhd2164AmplifierDataFormat - { - Unsigned, - TwosComplement - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Rhs2116Config.cs b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116Config.cs new file mode 100644 index 00000000..b4c11bbd --- /dev/null +++ b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116Config.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Xml.Serialization; + +namespace OpenEphys.Onix +{ + public static class Rhs2116Config + { + public static readonly IReadOnlyDictionary> AnalogLowCutoffToRegisters = + new Dictionary>() + { + { Rhs2116AnalogLowCutoff.Low1000Hz, new uint[] { 10, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low500Hz, new uint[] { 13, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low300Hz, new uint[] { 15, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low250Hz, new uint[] { 17, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low200Hz, new uint[] { 18, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low150Hz, new uint[] { 21, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low100Hz, new uint[] { 25, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low75Hz, new uint[] { 28, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low50Hz, new uint[] { 34, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low30Hz, new uint[] { 44, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low25Hz, new uint[] { 48, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low20Hz, new uint[] { 54, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low15Hz, new uint[] { 62, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low10Hz, new uint[] { 5, 1, 0 } }, + { Rhs2116AnalogLowCutoff.Low7500mHz, new uint[] { 18, 1, 0 } }, + { Rhs2116AnalogLowCutoff.Low5000mHz, new uint[] { 40, 1, 0 } }, + { Rhs2116AnalogLowCutoff.Low3090mHz, new uint[] { 20, 2, 0 } }, + { Rhs2116AnalogLowCutoff.Low2500mHz, new uint[] { 42, 2, 0 } }, + { Rhs2116AnalogLowCutoff.Low2000mHz, new uint[] { 8, 3, 0 } }, + { Rhs2116AnalogLowCutoff.Low1500mHz, new uint[] { 9, 4, 0 } }, + { Rhs2116AnalogLowCutoff.Low1000mHz, new uint[] { 44, 6, 0 } }, + { Rhs2116AnalogLowCutoff.Low750mHz, new uint[] { 49, 9, 0 } }, + { Rhs2116AnalogLowCutoff.Low500mHz, new uint[] { 35, 17, 0 } }, + { Rhs2116AnalogLowCutoff.Low300mHz, new uint[] { 1, 40, 0 } }, + { Rhs2116AnalogLowCutoff.Low250mHz, new uint[] { 56, 54, 0 } }, + { Rhs2116AnalogLowCutoff.Low100mHz, new uint[] { 16, 60, 1 } }, + }; + + public static readonly IReadOnlyDictionary> AnalogHighCutoffToRegisters = + new Dictionary>() + { + { Rhs2116AnalogHighCutoff.High20000Hz, new uint[] { 8, 0, 4, 0 } }, + { Rhs2116AnalogHighCutoff.High15000Hz, new uint[] { 11, 0, 8, 0 } }, + { Rhs2116AnalogHighCutoff.High10000Hz, new uint[] { 17, 0, 16, 0 } }, + { Rhs2116AnalogHighCutoff.High7500Hz, new uint[] { 22, 0, 23, 0 } }, + { Rhs2116AnalogHighCutoff.High5000Hz, new uint[] { 33, 0, 37, 0 } }, + { Rhs2116AnalogHighCutoff.High3000Hz, new uint[] { 3, 1, 13, 1 } }, + { Rhs2116AnalogHighCutoff.High2500Hz, new uint[] { 13, 1, 25, 1 } }, + { Rhs2116AnalogHighCutoff.High2000Hz, new uint[] { 27, 1, 44, 1 } }, + { Rhs2116AnalogHighCutoff.High1500Hz, new uint[] { 1, 2, 23, 2 } }, + { Rhs2116AnalogHighCutoff.High1000Hz, new uint[] { 46, 2, 30, 3 } }, + { Rhs2116AnalogHighCutoff.High750Hz, new uint[] { 41, 3, 36, 4 } }, + { Rhs2116AnalogHighCutoff.High500Hz, new uint[] { 30, 5, 43, 6 } }, + { Rhs2116AnalogHighCutoff.High300Hz, new uint[] { 6, 9, 2, 11 } }, + { Rhs2116AnalogHighCutoff.High250Hz, new uint[] { 42, 10, 5, 13 } }, + { Rhs2116AnalogHighCutoff.High200Hz, new uint[] { 24, 13, 7, 16 } }, + { Rhs2116AnalogHighCutoff.High150Hz, new uint[] { 44, 17, 8, 21 } }, + { Rhs2116AnalogHighCutoff.High100Hz, new uint[] { 38, 26, 5, 31 } }, + }; + + public static readonly IReadOnlyDictionary AnalogHighCutoffToFastSettleSamples = + new Dictionary() + { + { Rhs2116AnalogHighCutoff.High20000Hz, 4 }, + { Rhs2116AnalogHighCutoff.High15000Hz, 5 }, + { Rhs2116AnalogHighCutoff.High10000Hz, 8 }, + { Rhs2116AnalogHighCutoff.High7500Hz, 10 }, + { Rhs2116AnalogHighCutoff.High5000Hz, 15 }, + { Rhs2116AnalogHighCutoff.High3000Hz, 25 }, + { Rhs2116AnalogHighCutoff.High2500Hz, 30 }, + { Rhs2116AnalogHighCutoff.High2000Hz, 30 }, + { Rhs2116AnalogHighCutoff.High1500Hz, 30 }, + { Rhs2116AnalogHighCutoff.High1000Hz, 30 }, + { Rhs2116AnalogHighCutoff.High750Hz, 30 }, + { Rhs2116AnalogHighCutoff.High500Hz, 30 }, + { Rhs2116AnalogHighCutoff.High300Hz, 30 }, + { Rhs2116AnalogHighCutoff.High250Hz, 30 }, + { Rhs2116AnalogHighCutoff.High200Hz, 30 }, + { Rhs2116AnalogHighCutoff.High150Hz, 30 }, + { Rhs2116AnalogHighCutoff.High100Hz, 30 }, + }; + + public static readonly IReadOnlyDictionary> StimulatorStepSizeToRegisters = + new Dictionary>() + { + { Rhs2116StepSize.Step10nA, new uint[] { 64, 19, 3 } }, + { Rhs2116StepSize.Step20nA, new uint[] { 40, 40, 1 } }, + { Rhs2116StepSize.Step50nA, new uint[] { 64, 40, 0 } }, + { Rhs2116StepSize.Step100nA, new uint[] { 30, 20, 0 } }, + { Rhs2116StepSize.Step200nA, new uint[] { 25, 10, 0 } }, + { Rhs2116StepSize.Step500nA, new uint[] { 101, 3, 0 } }, + { Rhs2116StepSize.Step1000nA, new uint[] { 98, 1, 0 } }, + { Rhs2116StepSize.Step2000nA, new uint[] { 94, 0, 0 } }, + { Rhs2116StepSize.Step5000nA, new uint[] { 38, 0, 0 } }, + { Rhs2116StepSize.Step10000nA, new uint[] { 15, 0, 0 } }, + }; + } + + public enum Rhs2116AnalogLowCutoff + { + Low1000Hz, + Low500Hz, + Low300Hz, + Low250Hz, + Low200Hz, + Low150Hz, + Low100Hz, + Low75Hz, + Low50Hz, + Low30Hz, + Low25Hz, + Low20Hz, + Low15Hz, + Low10Hz, + Low7500mHz, + Low5000mHz, + Low3090mHz, + Low2500mHz, + Low2000mHz, + Low1500mHz, + Low1000mHz, + Low750mHz, + Low500mHz, + Low300mHz, + Low250mHz, + Low100mHz, + } + + public enum Rhs2116AnalogHighCutoff + { + High20000Hz, + High15000Hz, + High10000Hz, + High7500Hz, + High5000Hz, + High3000Hz, + High2500Hz, + High2000Hz, + High1500Hz, + High1000Hz, + High750Hz, + High500Hz, + High300Hz, + High250Hz, + High200Hz, + High150Hz, + High100Hz, + } + + public enum Rhs2116DspCutoff + { + /// + /// out = samp[n] - samp[n-1] + /// + Differential = 0, + + /// + /// 3309 Hz + /// + Dsp3309Hz, + + /// + /// 1370 Hz + /// + Dsp1374Hz, + + /// + /// 638 Hz + /// + Dsp638Hz, + + /// + /// 308 Hz + /// + Dsp308Hz, + + /// + /// 152 Hz + /// + Dsp152Hz, + + /// + /// 75.2 Hz + /// + Dsp75Hz, + + /// + /// 37.4 Hz + /// + Dsp37Hz, + + /// + /// 18.7 Hz + /// + Dsp19Hz, + + /// + /// 9.34 Hz + /// + Dsp9336mHz, + + /// + /// 4.67 Hz + /// + Dsp4665mHz, + + /// + /// 2.33 Hz + /// + Dsp2332mHz, + + /// + /// 1.17 Hz + /// + Dsp1166mHz, + + /// + /// 0.583 Hz + /// + Dsp583mHz, + + /// + /// 0.291 Hz + /// + Dsp291mHz, + + /// + /// 0.146 Hz + /// + Dsp146mHz, + + /// + /// + /// + Off + } + + public enum Rhs2116StepSize + { + Step10nA, + Step20nA, + Step50nA, + Step100nA, + Step200nA, + Step500nA, + Step1000nA, + Step2000nA, + Step5000nA, + Step10000nA + } + + public class Rhs2116Stimulus + { + [DisplayName("Number of Stimuli")] + public uint NumberOfStimuli { get; set; } = 0; + + [DisplayName("Anodic First")] + public bool AnodicFirst { get; set; } = true; + + [DisplayName("Delay (samples)")] + public uint DelaySamples { get; set; } = 0; + + [DisplayName("Dwell (samples)")] + public uint DwellSamples { get; set; } = 0; + + [DisplayName("Anodic Current (steps)")] + public byte AnodicAmplitudeSteps { get; set; } = 0; + + [DisplayName("Anodic Width (samples)")] + public uint AnodicWidthSamples { get; set; } = 0; + + [DisplayName("Cathodic Current (steps)")] + public byte CathodicAmplitudeSteps { get; set; } = 0; + + [DisplayName("Cathodic Width (samples)")] + public uint CathodicWidthSamples { get; set; } = 0; + + [DisplayName("Inter Stimulus Interval (samples)")] + public uint InterStimulusIntervalSamples { get; set; } = 0; + + [XmlIgnore] + internal bool Valid + { + get + { + return NumberOfStimuli == 0 + ? DelaySamples == 0 && CathodicWidthSamples == 0 && InterStimulusIntervalSamples == 0 && AnodicAmplitudeSteps == 0 && CathodicAmplitudeSteps == 0 + : !(AnodicWidthSamples == 0 && AnodicAmplitudeSteps > 0) + && + !(AnodicWidthSamples > 0 && AnodicAmplitudeSteps == 0) + && + !(CathodicWidthSamples == 0 && CathodicAmplitudeSteps > 0) + && + !(CathodicWidthSamples > 0 && CathodicAmplitudeSteps == 0) + && + // Non-zero anodic or Non-zero cathodic + ((AnodicWidthSamples > 0 && AnodicAmplitudeSteps > 0) || (CathodicWidthSamples > 0 && CathodicAmplitudeSteps > 0)) + && + // Single pulse and possibly 0 ISI or Multiple pulse and positive ISI + ((NumberOfStimuli == 1 && InterStimulusIntervalSamples >= 0) || (NumberOfStimuli > 1 && InterStimulusIntervalSamples > 0)); + + } + } + } + + public class Rhs2116StimulusSequence + { + public Rhs2116StimulusSequence() + { + // TODO: is there a nicer way to initialize this array? + Stimuli = new Rhs2116Stimulus[16]; + for (var i = 0; i < Stimuli.Length; i++) + { + Stimuli[i] = new Rhs2116Stimulus(); + } + } + + public Rhs2116Stimulus[] Stimuli { get; set; } + + // TODO: Should be set automatically to fit the largest required stimulus applitude + public Rhs2116StepSize CurrentStepSize { get; set; } = Rhs2116StepSize.Step5000nA; + + /// + /// Maximum length of the sequence across all channels + /// + [XmlIgnore] + public uint SequenceLengthSamples + { + get + { + uint max = 0; + + foreach (var stim in Stimuli) + { + var len = stim.DelaySamples + stim.NumberOfStimuli * (stim.AnodicWidthSamples + stim.CathodicWidthSamples + stim.DwellSamples + stim.InterStimulusIntervalSamples); + max = len > max ? len : max; + + } + + return max; + } + } + + /// + /// Maximum peak to peak amplitude of the sequence across all channels. + /// + [XmlIgnore] + public int MaximumPeakToPeakAmplitudeSteps + { + get + { + int max = 0; + + foreach (var stim in Stimuli) + { + var p2p = stim.CathodicAmplitudeSteps + stim.AnodicAmplitudeSteps; + max = p2p > max ? p2p : max; + + } + + return max; + } + } + + /// + /// Is the stimulus sequence well define + /// + [XmlIgnore] + public bool Valid => Stimuli.ToList().All(s => s.Valid); + + /// + /// Does the sequence fit in hardware + /// + [XmlIgnore] + public bool FitsInHardware => StimulusSlotsRequired <= Rhs2116.StimMemorySlotsAvailable; + + /// + /// Number of hardware memory slots required by the sequence + /// + [XmlIgnore] + public int StimulusSlotsRequired => DeltaTable.Count; + + [XmlIgnore] + public double CurrentStepSizeuA + { + get + { + return CurrentStepSize switch + { + Rhs2116StepSize.Step10nA => 0.01, + Rhs2116StepSize.Step20nA => 0.02, + Rhs2116StepSize.Step50nA => 0.05, + Rhs2116StepSize.Step100nA => 0.1, + Rhs2116StepSize.Step200nA => 0.2, + Rhs2116StepSize.Step500nA => 0.5, + Rhs2116StepSize.Step1000nA => 1.0, + Rhs2116StepSize.Step2000nA => 2.0, + Rhs2116StepSize.Step5000nA => 5.0, + Rhs2116StepSize.Step10000nA => 10.0, + _ => throw new ArgumentException("Invalid stimulus step size selection."), + }; + } + } + + [XmlIgnore] + public double MaxPossibleAmplitudePerPhaseMicroAmps => CurrentStepSizeuA * 255; + + internal IEnumerable AnodicAmplitudes => Stimuli.ToList().Select(x => x.AnodicAmplitudeSteps); + + internal IEnumerable CathodicAmplitudes => Stimuli.ToList().Select(x => x.CathodicAmplitudeSteps); + + /// + /// Generate the delta-table representation of this stimulus sequence that can be uploaded to the RHS2116 device. + /// The resultant dictionary has a time, in samples as the key and a combimed [polary, enable] bit field as the value. + /// + [XmlIgnore] + internal Dictionary DeltaTable + { + get + { + var table = new Dictionary(); + + // Cycle through electrodes + for (int i = 0; i < Stimuli.Length; i++) + { + var s = Stimuli[i]; + + var e0 = s.AnodicFirst ? s.AnodicAmplitudeSteps > 0 : s.CathodicAmplitudeSteps > 0; + var e1 = s.AnodicFirst ? s.CathodicAmplitudeSteps > 0 : s.AnodicAmplitudeSteps > 0; + var d0 = s.AnodicFirst ? s.AnodicWidthSamples : s.CathodicWidthSamples; + var d1 = d0 + s.DwellSamples; + var d2 = d1 + (s.AnodicFirst ? s.CathodicWidthSamples : s.AnodicWidthSamples); + + var t0 = s.DelaySamples; + + for (int j = 0; j < s.NumberOfStimuli; j++) + { + AddOrInsert(ref table, i, t0, s.AnodicFirst, e0); + AddOrInsert(ref table, i, t0 + d0, s.AnodicFirst, false); + AddOrInsert(ref table, i, t0 + d1, !s.AnodicFirst, e1); + AddOrInsert(ref table, i, t0 + d2, !s.AnodicFirst, false); + + t0 += d2 + s.InterStimulusIntervalSamples; + } + } + + return table.ToDictionary(d => d.Key, d => + { + int[] i = new int[1]; + d.Value.CopyTo(i, 0); + return (uint)i[0]; + }); + } + } + + private static void AddOrInsert(ref Dictionary table, int channel, uint key, bool polarity, bool enable) + { + if (table.ContainsKey(key)) + { + table[key][channel] = enable; + table[key][channel + 16] = polarity; + } + else + { + table.Add(key, new BitArray(32, false)); + table[key][channel] = enable; + table[key][channel + 16] = polarity; + } + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Rhd2164Data.cs b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116Data.cs similarity index 67% rename from OpenEphys.Onix/OpenEphys.Onix/Rhd2164Data.cs rename to OpenEphys.Onix/OpenEphys.Onix/Rhs2116Data.cs index f37a7f7a..6a5533d6 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/Rhd2164Data.cs +++ b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116Data.cs @@ -9,41 +9,41 @@ namespace OpenEphys.Onix { - public class Rhd2164Data : Source + public class Rhs2116Data : Source { - [TypeConverter(typeof(Rhd2164.NameConverter))] + [TypeConverter(typeof(Rhs2116.NameConverter))] public string DeviceName { get; set; } public int BufferSize { get; set; } = 30; - public unsafe override IObservable Generate() + public unsafe override IObservable Generate() { var bufferSize = BufferSize; return Observable.Using( () => DeviceManager.ReserveDevice(DeviceName), disposable => disposable.Subject.SelectMany(deviceInfo => - Observable.Create(observer => + Observable.Create(observer => { var sampleIndex = 0; - var device = deviceInfo.GetDeviceContext(typeof(Rhd2164)); - var amplifierBuffer = new short[Rhd2164.AmplifierChannelCount * bufferSize]; - var auxBuffer = new short[Rhd2164.AuxChannelCount * bufferSize]; + var device = deviceInfo.GetDeviceContext(typeof(Rhs2116)); + var amplifierBuffer = new short[Rhs2116.AmplifierChannelCount * bufferSize]; + var dcBuffer = new short[Rhs2116.AmplifierChannelCount * bufferSize]; var hubClockBuffer = new ulong[bufferSize]; var clockBuffer = new ulong[bufferSize]; var frameObserver = Observer.Create( frame => { - var payload = (Rhd2164Payload*)frame.Data.ToPointer(); - Marshal.Copy(new IntPtr(payload->AmplifierData), amplifierBuffer, sampleIndex * Rhd2164.AmplifierChannelCount, Rhd2164.AmplifierChannelCount); - Marshal.Copy(new IntPtr(payload->AuxData), auxBuffer, sampleIndex * Rhd2164.AuxChannelCount, Rhd2164.AuxChannelCount); + var payload = (Rhs2116Payload*)frame.Data.ToPointer(); + Marshal.Copy(new IntPtr(payload->AmplifierData), amplifierBuffer, sampleIndex * Rhs2116.AmplifierChannelCount, Rhs2116.AmplifierChannelCount); + Marshal.Copy(new IntPtr(payload->DCData), dcBuffer, sampleIndex * Rhs2116.AmplifierChannelCount, Rhs2116.AmplifierChannelCount); hubClockBuffer[sampleIndex] = payload->HubClock; clockBuffer[sampleIndex] = frame.Clock; if (++sampleIndex >= bufferSize) { - var amplifierData = BufferHelper.CopyTranspose(amplifierBuffer, bufferSize, Rhd2164.AmplifierChannelCount, Depth.U16); - var auxData = BufferHelper.CopyTranspose(auxBuffer, bufferSize, Rhd2164.AuxChannelCount, Depth.U16); - observer.OnNext(new Rhd2164DataFrame(clockBuffer, hubClockBuffer, amplifierData, auxData)); + var amplifierData = BufferHelper.CopyTranspose(amplifierBuffer, bufferSize, Rhs2116.AmplifierChannelCount, Depth.U16); + var dcData = BufferHelper.CopyTranspose(dcBuffer, bufferSize, Rhs2116.AmplifierChannelCount, Depth.U16); + observer.OnNext(new Rhs2116DataFrame(clockBuffer, hubClockBuffer, amplifierData, dcData)); hubClockBuffer = new ulong[bufferSize]; clockBuffer = new ulong[bufferSize]; sampleIndex = 0; diff --git a/OpenEphys.Onix/OpenEphys.Onix/Rhd2164DataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116DataFrame.cs similarity index 55% rename from OpenEphys.Onix/OpenEphys.Onix/Rhd2164DataFrame.cs rename to OpenEphys.Onix/OpenEphys.Onix/Rhs2116DataFrame.cs index 72e592d5..c11fdd73 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/Rhd2164DataFrame.cs +++ b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116DataFrame.cs @@ -3,14 +3,14 @@ namespace OpenEphys.Onix { - public class Rhd2164DataFrame + public class Rhs2116DataFrame { - public Rhd2164DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat auxData) + public Rhs2116DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat dcData) { Clock = clock; HubClock = hubClock; AmplifierData = amplifierData; - AuxData = auxData; + DCData = dcData; } public ulong[] Clock { get; } @@ -19,14 +19,14 @@ public Rhd2164DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat public Mat AmplifierData { get; } - public Mat AuxData { get; } + public Mat DCData { get; } } [StructLayout(LayoutKind.Sequential, Pack = 1)] - unsafe struct Rhd2164Payload + unsafe struct Rhs2116Payload { public ulong HubClock; - public fixed ushort AmplifierData[Rhd2164.AmplifierChannelCount]; - public fixed ushort AuxData[Rhd2164.AuxChannelCount]; + public fixed ushort AmplifierData[Rhs2116.AmplifierChannelCount]; + public fixed ushort DCData[Rhs2116.AmplifierChannelCount]; } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/Rhs2116StimulusTrigger.cs b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116StimulusTrigger.cs new file mode 100644 index 00000000..93dcaaad --- /dev/null +++ b/OpenEphys.Onix/OpenEphys.Onix/Rhs2116StimulusTrigger.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix +{ + public class Rhs2116StimulusTrigger : Sink + { + [TypeConverter(typeof(Rhs2116Trigger.NameConverter))] + public string DeviceName { get; set; } + + public override IObservable Process(IObservable source) + { + return Observable.Using( + () => DeviceManager.ReserveDevice(DeviceName), + disposable => disposable.Subject.SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(Rhs2116Trigger)); + return source.Do(t => + { + const double SampleFrequencyMegaHz = Rhs2116.SampleFrequencyHz / 1e6; + var delaySamples = (int)(t * SampleFrequencyMegaHz); + device.WriteRegister(Rhs2116Trigger.TRIGGER, (uint)(delaySamples << 12 | 0x1)); + }); + + })); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/StartAcquisition.cs b/OpenEphys.Onix/OpenEphys.Onix/StartAcquisition.cs deleted file mode 100644 index e2a4468e..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/StartAcquisition.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class StartAcquisition : Combinator - { - public int ReadSize { get; set; } = 2048; - - public int WriteSize { get; set; } = 2048; - - public override IObservable Process(IObservable source) - { - return source.SelectMany(context => - { - return Observable.Create(observer => - { - var disposable = context.FrameReceived.SubscribeSafe(observer); - try - { - context.Start(ReadSize, WriteSize); - } - catch - { - disposable.Dispose(); - throw; - } - return Disposable.Create(() => - { - context.Stop(); - disposable.Dispose(); - }); - }); - }); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/TS4231Data.cs b/OpenEphys.Onix/OpenEphys.Onix/TS4231Data.cs deleted file mode 100644 index 6d0eb694..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/TS4231Data.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Bonsai; - -namespace OpenEphys.Onix -{ - public class TS4231Data : Source - { - [TypeConverter(typeof(TS4231.NameConverter))] - public string DeviceName { get; set; } - - public override IObservable Generate() - { - return Observable.Using( - () => DeviceManager.ReserveDevice(DeviceName), - disposable => disposable.Subject.SelectMany(deviceInfo => - { - var device = deviceInfo.GetDeviceContext(typeof(TS4231)); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .Select(frame => new TS4231DataFrame(frame)); - })); - } - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/TS4231DataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/TS4231DataFrame.cs deleted file mode 100644 index 47db0e85..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/TS4231DataFrame.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Runtime.InteropServices; - -namespace OpenEphys.Onix -{ - public class TS4231DataFrame - { - public unsafe TS4231DataFrame(oni.Frame frame) - { - Clock = frame.Clock; - var payload = (TS4231Payload*)frame.Data.ToPointer(); - HubClock = payload->HubClock; - SensorIndex = payload->SensorIndex; - EnvelopeWidth = payload->EnvelopeWidth; - EnvelopeType = payload->EnvelopeType; - } - - public ulong Clock { get; } - - public ulong HubClock { get; } - - public int SensorIndex { get; } - - public uint EnvelopeWidth { get; } - - public TS4231Envelope EnvelopeType { get; } - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct TS4231Payload - { - public ulong HubClock; - public ushort SensorIndex; - public uint EnvelopeWidth; - public TS4231Envelope EnvelopeType; - } - - public enum TS4231Envelope : short - { - Sweep, - J0, - K0, - J1, - K1, - J2, - K2 - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Test0Data.cs b/OpenEphys.Onix/OpenEphys.Onix/Test0Data.cs index a552e69c..ba312245 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/Test0Data.cs +++ b/OpenEphys.Onix/OpenEphys.Onix/Test0Data.cs @@ -60,10 +60,10 @@ public unsafe override IObservable Generate() observer.OnError, observer.OnCompleted); - return deviceInfo.Context.FrameReceived - .Where(frame => frame.DeviceAddress == device.Address) - .SubscribeSafe(frameObserver); - }))); + return deviceInfo.Context.FrameReceived + .Where(frame => frame.DeviceAddress == device.Address) + .SubscribeSafe(frameObserver); + }))); } } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/Test0DataFrame.cs b/OpenEphys.Onix/OpenEphys.Onix/Test0DataFrame.cs deleted file mode 100644 index 30f9bb01..00000000 --- a/OpenEphys.Onix/OpenEphys.Onix/Test0DataFrame.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Runtime.InteropServices; -using OpenCV.Net; - -namespace OpenEphys.Onix -{ - public class Test0DataFrame - { - public Test0DataFrame(ulong[] clock, ulong[] hubClock, Mat message, Mat dummy) - { - Clock = clock; - HubClock = hubClock; - Message = message; - Dummy = dummy; - } - - public ulong[] Clock { get; } - - public ulong[] HubClock { get; } - - public Mat Message { get; } - - public Mat Dummy { get; } - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct Test0PayloadHeader - { - public ulong HubClock; - public short Message; - } -} diff --git a/OpenEphys.Onix/OpenEphys.Onix.Design/OpenEphys.Onix.Design.csproj b/OpenEphys.Onix1.Design/OpenEphys.Onix1.Design.csproj similarity index 52% rename from OpenEphys.Onix/OpenEphys.Onix.Design/OpenEphys.Onix.Design.csproj rename to OpenEphys.Onix1.Design/OpenEphys.Onix1.Design.csproj index 2f8e34b8..9e3f8f88 100644 --- a/OpenEphys.Onix/OpenEphys.Onix.Design/OpenEphys.Onix.Design.csproj +++ b/OpenEphys.Onix1.Design/OpenEphys.Onix1.Design.csproj @@ -1,20 +1,17 @@ - + - OpenEphys.Onix.Design + OpenEphys.Onix1.Design Bonsai Library containing visual interfaces for configuring ONIX devices. Bonsai Rx Open Ephys Onix Design net472 true - 0.1.0 + false + x64 - - - - - + diff --git a/OpenEphys.Onix/OpenEphys.Onix/Properties/launchSettings.json b/OpenEphys.Onix1.Design/Properties/launchSettings.json similarity index 71% rename from OpenEphys.Onix/OpenEphys.Onix/Properties/launchSettings.json rename to OpenEphys.Onix1.Design/Properties/launchSettings.json index 8e6d143e..e8640d60 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/Properties/launchSettings.json +++ b/OpenEphys.Onix1.Design/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Bonsai": { "commandName": "Executable", - "executablePath": "$(SolutionDir)..\\Bonsai\\Bonsai.exe", + "executablePath": "$(SolutionDir).bonsai/Bonsai.exe", "commandLineArgs": "--lib:$(TargetDir).", "nativeDebugging": true } diff --git a/OpenEphys.Onix1.sln b/OpenEphys.Onix1.sln new file mode 100644 index 00000000..77983339 --- /dev/null +++ b/OpenEphys.Onix1.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32825.248 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenEphys.Onix1", "OpenEphys.Onix1\OpenEphys.Onix1.csproj", "{353B1EBC-F8EB-4D99-8331-9FF15EC17F38}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenEphys.Onix1.Design", "OpenEphys.Onix1.Design\OpenEphys.Onix1.Design.csproj", "{149E86EC-B865-463D-81A8-8290CA7F8871}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F8644FAC-94E5-4E73-B809-925ABABE35B1}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|x64.ActiveCfg = Debug|x64 + {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Debug|x64.Build.0 = Debug|x64 + {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|x64.ActiveCfg = Release|x64 + {353B1EBC-F8EB-4D99-8331-9FF15EC17F38}.Release|x64.Build.0 = Release|x64 + {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|x64.ActiveCfg = Debug|x64 + {149E86EC-B865-463D-81A8-8290CA7F8871}.Debug|x64.Build.0 = Debug|x64 + {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|x64.ActiveCfg = Release|x64 + {149E86EC-B865-463D-81A8-8290CA7F8871}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {86554706-612A-4283-B0DC-5477B01D58B6} + EndGlobalSection +EndGlobal diff --git a/OpenEphys.Onix1/BitHelper.cs b/OpenEphys.Onix1/BitHelper.cs new file mode 100644 index 00000000..875712e1 --- /dev/null +++ b/OpenEphys.Onix1/BitHelper.cs @@ -0,0 +1,46 @@ +using System.Collections; +using System; + +namespace OpenEphys.Onix1 +{ + static class BitHelper + { + /// + /// Replace a defined set of bits in unsigned integer with those from another. + /// + /// The value where bits will be replaced. + /// A mask defining which bits should be replaced. + /// A value containing the bits that will be assingned to the + /// positions in . + /// + internal static uint Replace(uint value, uint mask, uint bits) + { + return (value & ~mask) | (bits & mask); + } + + /// + /// Create a bit-reversed byte array from an bit array. + /// + /// Bit array to convert. + /// Byte array with where the MSB to LSB order has been reversed in each byte. + /// Thrown if the array is empty. + internal static byte[] ToBitReversedBytes(BitArray bits) + { + if (bits.Length == 0) + { + throw new ArgumentException("Shift register data is empty", nameof(bits)); + } + + var bytes = new byte[(bits.Length - 1) / 8 + 1]; + bits.CopyTo(bytes, 0); + + for (int i = 0; i < bytes.Length; i++) + { + // NB: http://graphics.stanford.edu/~seander/bithacks.html + bytes[i] = (byte)((bytes[i] * 0x0202020202ul & 0x010884422010ul) % 1023); + } + + return bytes; + } + } +} diff --git a/OpenEphys.Onix1/Bno055Data.cs b/OpenEphys.Onix1/Bno055Data.cs new file mode 100644 index 00000000..b2f1c523 --- /dev/null +++ b/OpenEphys.Onix1/Bno055Data.cs @@ -0,0 +1,40 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that generates a sequence of 3D orientation measurements produced by BNO055 9-axis inertial measurement unit. + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream 3D orientation data. + /// + [Description("Generates a sequence of 3D orientation measurements produced by a BNO055 9-axis inertial measurement unit.")] + public class Bno055Data : Source + { + /// + [TypeConverter(typeof(Bno055.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects, each of which contains a 3D orientation sample + /// in various formats along with device metadata. + /// + /// A sequence of objects. + public override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(Bno055)); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .Select(frame => new Bno055DataFrame(frame)); + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/Bno055DataFrame.cs b/OpenEphys.Onix1/Bno055DataFrame.cs similarity index 54% rename from OpenEphys.Onix/OpenEphys.Onix/Bno055DataFrame.cs rename to OpenEphys.Onix1/Bno055DataFrame.cs index e5178bab..c2bbd916 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/Bno055DataFrame.cs +++ b/OpenEphys.Onix1/Bno055DataFrame.cs @@ -2,10 +2,17 @@ using System.Numerics; using System.Runtime.InteropServices; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class Bno055DataFrame + /// + /// A class that contains 3D orientation data produced by a Bosch BNO055 9-axis inertial measurement unit (IMU). + /// + public class Bno055DataFrame : DataFrame { + /// + /// Initializes a new instance of the class. + /// + /// An ONI data frame containing BNO055 data. public unsafe Bno055DataFrame(oni.Frame frame) : this(frame.Clock, (Bno055Payload*)frame.Data.ToPointer()) { @@ -18,8 +25,8 @@ internal unsafe Bno055DataFrame(ulong clock, Bno055Payload* payload) } internal unsafe Bno055DataFrame(ulong clock, Bno055DataPayload* payload) + : base(clock) { - Clock = clock; EulerAngle = new Vector3( x: Bno055.EulerAngleScale * payload->EulerAngle[0], y: Bno055.EulerAngleScale * payload->EulerAngle[1], @@ -41,20 +48,42 @@ internal unsafe Bno055DataFrame(ulong clock, Bno055DataPayload* payload) Calibration = payload->Calibration; } - public ulong Clock { get; } - - public ulong HubClock { get; } - + /// + /// Gets the 3D orientation in Euler angle format with units of degrees. + /// + /// + /// The Tait-Bryan formalism is used: + /// + /// Yaw: 0 to 360 degrees. + /// Roll: -180 to 180 degrees + /// Pitch: -90 to 90 degrees + /// + /// public Vector3 EulerAngle { get; } + /// + /// Gets the 3D orientation represented as a Quaternion. + /// public Quaternion Quaternion { get; } + /// + /// Gets the linear acceleration vector in units of m / s^2. + /// public Vector3 Acceleration { get; } + /// + /// Gets the gravity acceleration vector in units of m / s^2. + /// public Vector3 Gravity { get; } + /// + /// Gets the chip temperature in Celsius. + /// public int Temperature { get; } + /// + /// Gets MEMS subsystem and sensor fusion calibration status. + /// public Bno055CalibrationFlags Calibration { get; } } @@ -76,13 +105,31 @@ unsafe struct Bno055DataPayload public Bno055CalibrationFlags Calibration; } + /// + /// Specifies the MEMS subsystem and sensor fusion calibration status. + /// [Flags] public enum Bno055CalibrationFlags : byte { + /// + /// Specifies that no sub-system is calibrated. + /// None = 0, + /// + /// Specifies all three sub-systems (gyroscope, accelerometer, and magnetometer) along with sensor fusion are calibrated. + /// System = 0x3, + /// + /// Specifies that the gyroscope is calibrated. + /// Gyroscope = 0xC, + /// + /// Specifies that the accelerometer is calibrated. + /// Accelerometer = 0x30, + /// + /// Specifies that the magnetometer is calibrated. + /// Magnetometer = 0xC0 } } diff --git a/OpenEphys.Onix1/BreakoutAnalogInput.cs b/OpenEphys.Onix1/BreakoutAnalogInput.cs new file mode 100644 index 00000000..3a09fd2f --- /dev/null +++ b/OpenEphys.Onix1/BreakoutAnalogInput.cs @@ -0,0 +1,117 @@ +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.Onix1 +{ + /// + /// Produces a sequence of analog input frames from an ONIX breakout board. + /// + [Description("Produces a sequence of analog input frames from an ONIX breakout board.")] + public class BreakoutAnalogInput : Source + { + /// + [TypeConverter(typeof(BreakoutAnalogIO.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the number of samples collected for each channel that are use to create a single . + /// + /// + /// This property determines the number of analog samples that are buffered for each channel before data is propagated. For instance, if this + /// value is set to 100, then 100 samples, along with corresponding clock values, will be collected from each of the input channels + /// and packed into each . Because channels are sampled at 100 kHz, this is equivalent to 1 + /// millisecond of data from each channel. + /// + [Description("The number of analog samples that are buffered for each channel before data is propagated.")] + public int BufferSize { get; set; } = 100; + + /// + /// Gets or sets the data type used to represent analog samples. + /// + /// + /// If is selected, each ADC sample is represented at a signed, twos-complement encoded + /// 16-bit integer. samples can be converted to a voltage using each channels' + /// selection. For instance, channel 0 can be converted using . + /// When is selected, the voltage conversion is performed automatically and samples + /// are represented as 32-bit floating point voltages. + /// + [Description("The data type used to represent analog samples.")] + public BreakoutAnalogIODataType DataType { get; set; } = BreakoutAnalogIODataType.S16; + + static Mat CreateVoltageScale(int bufferSize, float[] voltsPerDivision) + { + + using var scaleHeader = Mat.CreateMatHeader( + voltsPerDivision, + rows: voltsPerDivision.Length, + cols: 1, + depth: Depth.F32, + channels: 1); + var voltageScale = new Mat(scaleHeader.Rows, bufferSize, scaleHeader.Depth, scaleHeader.Channels); + CV.Repeat(scaleHeader, voltageScale); + return voltageScale; + } + + /// + /// Generates a sequence of . + /// + /// A sequence of + public unsafe override IObservable Generate() + { + var bufferSize = BufferSize; + var dataType = DataType; + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var device = deviceInfo.GetDeviceContext(typeof(BreakoutAnalogIO)); + var ioDeviceInfo = (BreakoutAnalogIODeviceInfo)deviceInfo; + + var sampleIndex = 0; + var voltageScale = dataType == BreakoutAnalogIODataType.Volts + ? CreateVoltageScale(bufferSize, ioDeviceInfo.VoltsPerDivision) + : null; + var transposeBuffer = voltageScale != null + ? new Mat(BreakoutAnalogIO.ChannelCount, bufferSize, Depth.S16, 1) + : null; + var analogDataBuffer = new short[BreakoutAnalogIO.ChannelCount * bufferSize]; + var hubClockBuffer = new ulong[bufferSize]; + var clockBuffer = new ulong[bufferSize]; + + var frameObserver = Observer.Create( + frame => + { + var payload = (BreakoutAnalogInputPayload*)frame.Data.ToPointer(); + Marshal.Copy(new IntPtr(payload->AnalogData), analogDataBuffer, sampleIndex * BreakoutAnalogIO.ChannelCount, BreakoutAnalogIO.ChannelCount); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= bufferSize) + { + var analogData = BufferHelper.CopyTranspose( + analogDataBuffer, + bufferSize, + BreakoutAnalogIO.ChannelCount, + Depth.S16, + voltageScale, + transposeBuffer); + observer.OnNext(new BreakoutAnalogInputDataFrame(clockBuffer, hubClockBuffer, analogData)); + hubClockBuffer = new ulong[bufferSize]; + clockBuffer = new ulong[bufferSize]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .SubscribeSafe(frameObserver); + })); + } + } +} diff --git a/OpenEphys.Onix1/BreakoutAnalogInputDataFrame.cs b/OpenEphys.Onix1/BreakoutAnalogInputDataFrame.cs new file mode 100644 index 00000000..bb62cd01 --- /dev/null +++ b/OpenEphys.Onix1/BreakoutAnalogInputDataFrame.cs @@ -0,0 +1,35 @@ +using System.Runtime.InteropServices; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Buffered analog data produced by the ONIX breakout board. + /// + public class BreakoutAnalogInputDataFrame : BufferedDataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// A buffered array of values. + /// A buffered array of hub clock counter values. + /// A buffered array of multi-channel analog data. + public BreakoutAnalogInputDataFrame(ulong[] clock, ulong[] hubClock, Mat analogData) + : base(clock, hubClock) + { + AnalogData = analogData; + } + + /// + /// Get the buffered analog data array. + /// + public Mat AnalogData { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct BreakoutAnalogInputPayload + { + public ulong HubClock; + public fixed short AnalogData[BreakoutAnalogIO.ChannelCount]; + } +} diff --git a/OpenEphys.Onix1/BreakoutAnalogOutput.cs b/OpenEphys.Onix1/BreakoutAnalogOutput.cs new file mode 100644 index 00000000..0b3c47b0 --- /dev/null +++ b/OpenEphys.Onix1/BreakoutAnalogOutput.cs @@ -0,0 +1,161 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Sends analog output data to an ONIX breakout board. + /// + [Description("Sends analog output data to an ONIX breakout board.")] + public class BreakoutAnalogOutput : Sink + { + const BreakoutAnalogIOVoltageRange OutputRange = BreakoutAnalogIOVoltageRange.TenVolts; + + /// + [TypeConverter(typeof(BreakoutAnalogIO.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the data type used to represent analog samples. + /// + /// + /// If is selected, each DAC value is represented by a signed, twos-complement encoded + /// 16-bit integer. In this case, the output voltage always corresponds to . + /// When is selected, 32-bit floating point voltages between -10 and 10 volts are sent + /// directly to the DACs. + /// + [Description("The data type used to represent analog samples.")] + public BreakoutAnalogIODataType DataType { get; set; } = BreakoutAnalogIODataType.S16; + + /// + /// Send samples to analog outputs. + /// + /// A sequence of 12xN sample matrices containing the analog data to write to channels 0 to 11. + /// A sequence of 12xN sample matrices containing the analog data that were written to channels 0 to 11. + public override IObservable Process(IObservable source) + { + var dataType = DataType; + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var bufferSize = 0; + var scaleBuffer = default(Mat); + var transposeBuffer = default(Mat); + var sampleScale = dataType == BreakoutAnalogIODataType.Volts + ? 1 / BreakoutAnalogIODeviceInfo.GetVoltsPerDivision(OutputRange) + : 1; + var device = deviceInfo.GetDeviceContext(typeof(BreakoutAnalogIO)); + return source.Do(data => + { + if (dataType == BreakoutAnalogIODataType.S16 && data.Depth != Depth.S16 || + dataType == BreakoutAnalogIODataType.Volts && data.Depth != Depth.F32) + { + ThrowDataTypeException(data.Depth); + } + + AssertChannelCount(data.Rows); + if (bufferSize != data.Cols) + { + bufferSize = data.Cols; + transposeBuffer = bufferSize > 1 + ? new Mat(data.Cols, data.Rows, data.Depth, 1) + : null; + if (sampleScale != 1) + { + scaleBuffer = transposeBuffer != null + ? new Mat(data.Cols, data.Rows, Depth.S16, 1) + : new Mat(data.Rows, data.Cols, Depth.S16, 1); + } + else scaleBuffer = null; + } + + var outputBuffer = data; + if (transposeBuffer != null) + { + CV.Transpose(outputBuffer, transposeBuffer); + outputBuffer = transposeBuffer; + } + + if (scaleBuffer != null) + { + CV.ConvertScale(outputBuffer, scaleBuffer, sampleScale); + outputBuffer = scaleBuffer; + } + + var dataSize = outputBuffer.Step * outputBuffer.Rows; + device.Write(outputBuffer.Data, dataSize); + }); + }); + } + + /// + /// Send samples to analog outputs. + /// + /// A sequence of 12x1 element arrays each containing the analog data to write to channels 0 to 11. + /// A sequence of 12x1 element arrays each containing the analog data to write to channels 0 to 11. + public IObservable Process(IObservable source) + { + if (DataType != BreakoutAnalogIODataType.S16) + ThrowDataTypeException(Depth.S16); + + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(BreakoutAnalogIO)); + return source.Do(data => + { + AssertChannelCount(data.Length); + device.Write(data); + }); + }); + } + + /// + /// Send samples to analog outputs. + /// + /// A sequence of 12x1 element arrays each containing the analog data to write to channels 0 to 11. + /// A sequence of 12x1 element arrays each containing the analog data to write to channels 0 to 11. + public IObservable Process(IObservable source) + { + if (DataType != BreakoutAnalogIODataType.Volts) + ThrowDataTypeException(Depth.F32); + + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(BreakoutAnalogIO)); + var divisionsPerVolt = 1 / BreakoutAnalogIODeviceInfo.GetVoltsPerDivision(OutputRange); + return source.Do(data => + { + AssertChannelCount(data.Length); + var samples = new short[data.Length]; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (short)(data[i] * divisionsPerVolt); + } + + device.Write(samples); + }); + }); + } + + static void AssertChannelCount(int channels) + { + if (channels != BreakoutAnalogIO.ChannelCount) + { + throw new InvalidOperationException( + $"The input data must have exactly {BreakoutAnalogIO.ChannelCount} channels." + ); + } + } + + static void ThrowDataTypeException(Depth depth) + { + throw new InvalidOperationException( + $"Invalid input data type '{depth}' for the specified analog IO configuration." + ); + } + } +} diff --git a/OpenEphys.Onix1/BreakoutDigitalInput.cs b/OpenEphys.Onix1/BreakoutDigitalInput.cs new file mode 100644 index 00000000..448925dd --- /dev/null +++ b/OpenEphys.Onix1/BreakoutDigitalInput.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that produces a sequence of digital input frames from an ONIX breakout board. + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream data. + /// + [Description("Produces a sequence of digital input frames from an ONIX breakout board.")] + public class BreakoutDigitalInput : Source + { + /// + [TypeConverter(typeof(BreakoutDigitalIO.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects, which contains information about breakout + /// board's digital input state. + /// + /// + /// Digital inputs are not regularly sampled. Instead, a new is produced each + /// whenever any digital state (i.e. a digital input pin, button, or switch state) changes. + /// + /// A sequence of objects. + public unsafe override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(BreakoutDigitalIO)); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .Select(frame => new BreakoutDigitalInputDataFrame(frame)); + }); + } + } +} diff --git a/OpenEphys.Onix1/BreakoutDigitalInputDataFrame.cs b/OpenEphys.Onix1/BreakoutDigitalInputDataFrame.cs new file mode 100644 index 00000000..c95122f6 --- /dev/null +++ b/OpenEphys.Onix1/BreakoutDigitalInputDataFrame.cs @@ -0,0 +1,41 @@ +using System.Runtime.InteropServices; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains information about a digital event on the ONIX breakout board. + /// + public class BreakoutDigitalInputDataFrame : DataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// A frame produced by an ONIX breakout board's digital IO device. + public unsafe BreakoutDigitalInputDataFrame(oni.Frame frame) + : base(frame.Clock) + { + var payload = (BreakoutDigitalInputPayload*)frame.Data.ToPointer(); + HubClock = payload->HubClock; + DigitalInputs = payload->DigitalInputs; + Buttons = payload->Buttons; + } + + /// + /// Gets the state of the breakout board's 8-bit digital input port. + /// + public BreakoutDigitalPortState DigitalInputs { get; } + + /// + /// Gets the state of the breakout board's buttons and switches. + /// + public BreakoutButtonState Buttons { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct BreakoutDigitalInputPayload + { + public ulong HubClock; + public BreakoutDigitalPortState DigitalInputs; + public BreakoutButtonState Buttons; + } +} diff --git a/OpenEphys.Onix1/BreakoutDigitalOutput.cs b/OpenEphys.Onix1/BreakoutDigitalOutput.cs new file mode 100644 index 00000000..14dc1d88 --- /dev/null +++ b/OpenEphys.Onix1/BreakoutDigitalOutput.cs @@ -0,0 +1,34 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Sends digital output data to an ONIX breakout board. + /// + [Description("Sends digital output data to an ONIX breakout board.")] + public class BreakoutDigitalOutput : Sink + { + /// + [TypeConverter(typeof(BreakoutDigitalIO.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Updates the digital output port state. + /// + /// A sequence of values indicating the state of the breakout board's 8 digital output pins + /// A sequence that is identical to . + public override IObservable Process(IObservable source) + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(BreakoutDigitalIO)); + return source.Do(value => device.Write((uint)value)); + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/BufferHelper.cs b/OpenEphys.Onix1/BufferHelper.cs similarity index 98% rename from OpenEphys.Onix/OpenEphys.Onix/BufferHelper.cs rename to OpenEphys.Onix1/BufferHelper.cs index 0d3f80f2..d2083865 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/BufferHelper.cs +++ b/OpenEphys.Onix1/BufferHelper.cs @@ -1,6 +1,6 @@ using OpenCV.Net; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { static class BufferHelper { diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureBno055.cs b/OpenEphys.Onix1/ConfigureBno055.cs similarity index 52% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureBno055.cs rename to OpenEphys.Onix1/ConfigureBno055.cs index 8605c9d0..95f35c8e 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureBno055.cs +++ b/OpenEphys.Onix1/ConfigureBno055.cs @@ -1,19 +1,45 @@ using System; using System.ComponentModel; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class for configuring a Bosch BNO055 9-axis inertial measurement unit (IMU). + /// + /// + /// This configuration class can be linked to a instance to stream orientation data from the IMU. + /// + [Description("Configures a Bosch BNO055 9-axis IMU device.")] public class ConfigureBno055 : SingleDeviceFactory { + /// + /// Initializes a new instance of the class. + /// public ConfigureBno055() : base(typeof(Bno055)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, a instance that is linked to this configuration will produce data. If set to false, + /// it will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the BNO055 device is enabled.")] public bool Enable { get; set; } = true; + /// + /// Configures a Bosch BNO055 9-axis IMU device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a BNO055 device. public override IObservable Process(IObservable source) { var deviceName = DeviceName; diff --git a/OpenEphys.Onix1/ConfigureBreakoutAnalogIO.cs b/OpenEphys.Onix1/ConfigureBreakoutAnalogIO.cs new file mode 100644 index 00000000..4a65ae10 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureBreakoutAnalogIO.cs @@ -0,0 +1,397 @@ +using System; +using System.ComponentModel; +using System.Linq; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for configuring the ONIX breakout board's analog inputs and outputs. + /// + [TypeConverter(typeof(SortedPropertyConverter))] + [Description("Configures the analog input and output device in the ONIX breakout board.")] + public class ConfigureBreakoutAnalogIO : SingleDeviceFactory + { + /// + /// Initialize a new instance of ConfigureAnalogIO. + /// + public ConfigureBreakoutAnalogIO() + : base(typeof(BreakoutAnalogIO)) + { + DeviceAddress = 6; + } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, will not produce data. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the analog IO device is enabled.")] + public bool Enable { get; set; } = true; + + /// + /// Gets or sets the input voltage range of channel 0. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 0.")] + public BreakoutAnalogIOVoltageRange InputRange0 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 1. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 1.")] + public BreakoutAnalogIOVoltageRange InputRange1 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 2. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 2.")] + public BreakoutAnalogIOVoltageRange InputRange2 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 3. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 3.")] + public BreakoutAnalogIOVoltageRange InputRange3 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 4. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 4.")] + public BreakoutAnalogIOVoltageRange InputRange4 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 5. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 5.")] + public BreakoutAnalogIOVoltageRange InputRange5 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 6. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 6.")] + public BreakoutAnalogIOVoltageRange InputRange6 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 7. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 7.")] + public BreakoutAnalogIOVoltageRange InputRange7 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 8. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 8.")] + public BreakoutAnalogIOVoltageRange InputRange8 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 9. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 9.")] + public BreakoutAnalogIOVoltageRange InputRange9 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 10. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 10.")] + public BreakoutAnalogIOVoltageRange InputRange10 { get; set; } + + /// + /// Gets or sets the input voltage range of channel 11. + /// + [Category(ConfigurationCategory)] + [Description("The input voltage range of channel 11.")] + public BreakoutAnalogIOVoltageRange InputRange11 { get; set; } + + /// + /// Gets or sets the direction of channel 0. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 0.")] + public BreakoutAnalogIODirection Direction0 { get; set; } + + /// + /// Gets or sets the direction of channel 1. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 1.")] + public BreakoutAnalogIODirection Direction1 { get; set; } + + /// + /// Gets or sets the direction of channel 2. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 2.")] + public BreakoutAnalogIODirection Direction2 { get; set; } + + /// + /// Gets or sets the direction of channel 3. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 3.")] + public BreakoutAnalogIODirection Direction3 { get; set; } + + /// + /// Gets or sets the direction of channel 4. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 4.")] + public BreakoutAnalogIODirection Direction4 { get; set; } + + /// + /// Gets or sets the direction of channel 5. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 5.")] + public BreakoutAnalogIODirection Direction5 { get; set; } + + /// + /// Gets or sets the direction of channel 6. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 6.")] + public BreakoutAnalogIODirection Direction6 { get; set; } + + /// + /// Gets or sets the direction of channel 7. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 7.")] + public BreakoutAnalogIODirection Direction7 { get; set; } + + /// + /// Gets or sets the direction of channel 8. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 8.")] + public BreakoutAnalogIODirection Direction8 { get; set; } + + /// + /// Gets or sets the direction of channel 9. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 9.")] + public BreakoutAnalogIODirection Direction9 { get; set; } + + /// + /// Gets or sets the direction of channel 10. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 10.")] + public BreakoutAnalogIODirection Direction10 { get; set; } + + /// + /// Gets or sets the direction of channel 11. + /// + [Category(AcquisitionCategory)] + [Description("The direction of channel 11.")] + public BreakoutAnalogIODirection Direction11 { get; set; } + + /// + /// Configures the analog input and output device in the ONIX breakout board. + /// + /// + /// This will schedule analog IO hardware configuration actions that can be applied by a + /// object prior to data collection. + /// + /// + /// The sequence of objects on which to apply the analog IO configuration. + /// + /// + /// A sequence of objects that is identical to + /// in which each has been instructed to apply the analog IO configuration. + /// + public override IObservable Process(IObservable source) + { + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(BreakoutAnalogIO.ENABLE, Enable ? 1u : 0u); + device.WriteRegister(BreakoutAnalogIO.CH00INRANGE, (uint)InputRange0); + device.WriteRegister(BreakoutAnalogIO.CH01INRANGE, (uint)InputRange1); + device.WriteRegister(BreakoutAnalogIO.CH02INRANGE, (uint)InputRange2); + device.WriteRegister(BreakoutAnalogIO.CH03INRANGE, (uint)InputRange3); + device.WriteRegister(BreakoutAnalogIO.CH04INRANGE, (uint)InputRange4); + device.WriteRegister(BreakoutAnalogIO.CH05INRANGE, (uint)InputRange5); + device.WriteRegister(BreakoutAnalogIO.CH06INRANGE, (uint)InputRange6); + device.WriteRegister(BreakoutAnalogIO.CH07INRANGE, (uint)InputRange7); + device.WriteRegister(BreakoutAnalogIO.CH08INRANGE, (uint)InputRange8); + device.WriteRegister(BreakoutAnalogIO.CH09INRANGE, (uint)InputRange9); + device.WriteRegister(BreakoutAnalogIO.CH10INRANGE, (uint)InputRange10); + device.WriteRegister(BreakoutAnalogIO.CH11INRANGE, (uint)InputRange11); + + // Build the whole value for CHDIR and write it once + static uint SetIO(uint io_reg, int channel, BreakoutAnalogIODirection direction) => + (io_reg & ~((uint)1 << channel)) | ((uint)(direction) << channel); + + var io_reg = 0u; + io_reg = SetIO(io_reg, 0, Direction0); + io_reg = SetIO(io_reg, 1, Direction1); + io_reg = SetIO(io_reg, 2, Direction2); + io_reg = SetIO(io_reg, 3, Direction3); + io_reg = SetIO(io_reg, 4, Direction4); + io_reg = SetIO(io_reg, 5, Direction5); + io_reg = SetIO(io_reg, 6, Direction6); + io_reg = SetIO(io_reg, 7, Direction7); + io_reg = SetIO(io_reg, 8, Direction8); + io_reg = SetIO(io_reg, 9, Direction9); + io_reg = SetIO(io_reg, 10, Direction10); + io_reg = SetIO(io_reg, 11, Direction11); + device.WriteRegister(BreakoutAnalogIO.CHDIR, io_reg); + + var deviceInfo = new BreakoutAnalogIODeviceInfo(device, this); + return DeviceManager.RegisterDevice(deviceName, deviceInfo); + }); + } + + class SortedPropertyConverter : ExpandableObjectConverter + { + public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) + { + var properties = base.GetProperties(context, value, attributes); + var sortedOrder = properties.Cast() + .Where(p => p.PropertyType == typeof(BreakoutAnalogIOVoltageRange) + || p.PropertyType == typeof(BreakoutAnalogIODirection)) + .OrderBy(p => p.PropertyType.MetadataToken) + .Select(p => p.Name) + .Prepend(nameof(Enable)) + .ToArray(); + return properties.Sort(sortedOrder); + } + } + } + + static class BreakoutAnalogIO + { + public const int ID = 22; + + // constants + public const int ChannelCount = 12; + public const int NumberOfDivisions = 1 << 16; + + // managed registers + public const uint ENABLE = 0; + public const uint CHDIR = 1; + public const uint CH00INRANGE = 2; + public const uint CH01INRANGE = 3; + public const uint CH02INRANGE = 4; + public const uint CH03INRANGE = 5; + public const uint CH04INRANGE = 6; + public const uint CH05INRANGE = 7; + public const uint CH06INRANGE = 8; + public const uint CH07INRANGE = 9; + public const uint CH08INRANGE = 10; + public const uint CH09INRANGE = 11; + public const uint CH10INRANGE = 12; + public const uint CH11INRANGE = 13; + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(BreakoutAnalogIO)) + { + } + } + } + + class BreakoutAnalogIODeviceInfo : DeviceInfo + { + public BreakoutAnalogIODeviceInfo(DeviceContext device, ConfigureBreakoutAnalogIO deviceFactory) + : base(device, deviceFactory.DeviceType) + { + VoltsPerDivision = new[] + { + GetVoltsPerDivision(deviceFactory.InputRange0), + GetVoltsPerDivision(deviceFactory.InputRange1), + GetVoltsPerDivision(deviceFactory.InputRange2), + GetVoltsPerDivision(deviceFactory.InputRange3), + GetVoltsPerDivision(deviceFactory.InputRange4), + GetVoltsPerDivision(deviceFactory.InputRange5), + GetVoltsPerDivision(deviceFactory.InputRange6), + GetVoltsPerDivision(deviceFactory.InputRange7), + GetVoltsPerDivision(deviceFactory.InputRange8), + GetVoltsPerDivision(deviceFactory.InputRange9), + GetVoltsPerDivision(deviceFactory.InputRange10), + GetVoltsPerDivision(deviceFactory.InputRange11) + }; + } + + public static float GetVoltsPerDivision(BreakoutAnalogIOVoltageRange voltageRange) + { + return voltageRange switch + { + BreakoutAnalogIOVoltageRange.TenVolts => 20.0f / BreakoutAnalogIO.NumberOfDivisions, + BreakoutAnalogIOVoltageRange.TwoPointFiveVolts => 5.0f / BreakoutAnalogIO.NumberOfDivisions, + BreakoutAnalogIOVoltageRange.FiveVolts => 10.0f / BreakoutAnalogIO.NumberOfDivisions, + _ => throw new ArgumentOutOfRangeException(nameof(voltageRange)), + }; + } + + public float[] VoltsPerDivision { get; } + } + + /// + /// Specifies the analog input ADC voltage range. + /// + public enum BreakoutAnalogIOVoltageRange + { + /// + /// ±10.0 volts. + /// + [Description("+/-10.0 volts")] + TenVolts = 0, + /// + /// ±2.5 volts. + /// + [Description("+/-2.5 volts")] + TwoPointFiveVolts = 1, + /// + /// ±5.0 volts. + /// + [Description("+/-5.0 volts")] + FiveVolts, + } + + /// + /// Specifies analog channel direction. + /// + public enum BreakoutAnalogIODirection + { + /// + /// Input to breakout board. + /// + Input = 0, + /// + /// Output from breakout board with loopback. + /// + Output = 1 + } + + /// + /// Specifies the analog sample representation. + /// + public enum BreakoutAnalogIODataType + { + /// + /// Twos-complement encoded signed 16-bit integer + /// + S16, + /// + /// 32-bit floating point voltage. + /// + Volts + } +} diff --git a/OpenEphys.Onix1/ConfigureBreakoutBoard.cs b/OpenEphys.Onix1/ConfigureBreakoutBoard.cs new file mode 100644 index 00000000..bdd68354 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureBreakoutBoard.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that configures an ONIX breakout board. + /// + [Description("Configures an ONIX breakout board.")] + public class ConfigureBreakoutBoard : MultiDeviceFactory + { + /// + /// Gets or sets the heartbeat configuration. + /// + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the heartbeat device in the ONIX breakout board.")] + public ConfigureHeartbeat Heartbeat { get; set; } = new(); + + /// + /// Gets or sets the breakout board's analog IO configuration. + /// + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the analog IO device in the ONIX breakout board.")] + public ConfigureBreakoutAnalogIO AnalogIO { get; set; } = new(); + + /// + /// Gets or sets the breakout board's digital IO configuration. + /// + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the digital IO device in the ONIX breakout board.")] + public ConfigureBreakoutDigitalIO DigitalIO { get; set; } = new(); + + /// + /// Gets or sets the hardware memory monitor configuration. + /// + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the memory monitor device in the ONIX breakout board.")] + public ConfigureMemoryMonitor MemoryMonitor { get; set; } = new(); + + internal override IEnumerable GetDevices() + { + yield return Heartbeat; + yield return AnalogIO; + yield return DigitalIO; + yield return MemoryMonitor; + } + } +} diff --git a/OpenEphys.Onix1/ConfigureBreakoutDigitalIO.cs b/OpenEphys.Onix1/ConfigureBreakoutDigitalIO.cs new file mode 100644 index 00000000..850a7a80 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureBreakoutDigitalIO.cs @@ -0,0 +1,166 @@ +using System; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for configuring the ONIX breakout board's digital inputs and outputs. + /// + [Description("Configures the digital input and output device in the ONIX breakout board.")] + public class ConfigureBreakoutDigitalIO : SingleDeviceFactory + { + /// + /// Initialize a new instance of . + /// + public ConfigureBreakoutDigitalIO() + : base(typeof(BreakoutDigitalIO)) + { + DeviceAddress = 7; + } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, will not produce data. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the digital IO device is enabled.")] + public bool Enable { get; set; } = true; + + /// + /// Configures the digital input and output device in the ONIX breakout board. + /// + /// + /// This will schedule digital IO hardware configuration actions that can be applied by a + /// object prior to data collection. + /// + /// A sequence of instances that hold configuration actions. + /// + /// The original sequence modified by adding additional configuration actions required to configure a digital IO device. + /// + public override IObservable Process(IObservable source) + { + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(BreakoutDigitalIO.ENABLE, Enable ? 1u : 0); + return DeviceManager.RegisterDevice(deviceName, device, DeviceType); + }); + } + } + + static class BreakoutDigitalIO + { + public const int ID = 18; + + // managed registers + public const uint ENABLE = 0x0; // Enable or disable the data output stream + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(BreakoutDigitalIO)) + { + } + } + } + + /// + /// Specifies the state of the ONIX breakout board's digital input pins. + /// + [Flags] + public enum BreakoutDigitalPortState : ushort + { + /// + /// Specifies that pin 0 is high. + /// + Pin0 = 0x1, + /// + /// Specifies that pin 1 is high. + /// + Pin1 = 0x2, + /// + /// Specifies that pin 2 is high. + /// + Pin2 = 0x4, + /// + /// Specifies that pin 3 is high. + /// + Pin3 = 0x8, + /// + /// Specifies that pin 4 is high. + /// + Pin4 = 0x10, + /// + /// Specifies that pin 5 is high. + /// + Pin5 = 0x20, + /// + /// Specifies that pin 6 is high. + /// + Pin6 = 0x40, + /// + /// Specifies that pin 7 is high. + /// + Pin7 = 0x80, + } + + /// + /// Specifies the state of the ONIX breakout board's switches and buttons. + /// + [Flags] + public enum BreakoutButtonState : ushort + { + /// + /// Specifies that the ☾ key is depressed. + /// + Moon = 0x1, + /// + /// Specifies that the △ key is depressed. + /// + Triangle = 0x2, + /// + /// Specifies that the × key is depressed. + /// + X = 0x4, + /// + /// Specifies that the ✓ key is depressed. + /// + Check = 0x8, + /// + /// Specifies that the ◯ key is depressed. + /// + Circle = 0x10, + /// + /// Specifies that the □ key is depressed. + /// + Square = 0x20, + /// + /// Specifies that reserved bit 0 is high. + /// + Reserved0 = 0x40, + /// + /// Specifies that reserved bit 1 is high. + /// + Reserved1 = 0x80, + /// + /// Specifies that port D power switch is set to on. + /// + PortDOn = 0x100, + /// + /// Specifies that port C power switch is set to on. + /// + PortCOn = 0x200, + /// + /// Specifies that port B power switch is set to on. + /// + PortBOn = 0x400, + /// + /// Specifies that port A power switch is set to on. + /// + PortAOn = 0x800, + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs b/OpenEphys.Onix1/ConfigureFmcLinkController.cs similarity index 89% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs rename to OpenEphys.Onix1/ConfigureFmcLinkController.cs index 93f579d9..e81f7402 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureFmcLinkController.cs +++ b/OpenEphys.Onix1/ConfigureFmcLinkController.cs @@ -3,9 +3,9 @@ using System.Reactive.Disposables; using System.Threading; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public abstract class ConfigureFmcLinkController : SingleDeviceFactory + internal abstract class ConfigureFmcLinkController : SingleDeviceFactory { public ConfigureFmcLinkController() : base(typeof(FmcLinkController)) @@ -67,16 +67,17 @@ public override IObservable Process(IObservable source } return Disposable.Create(dispose); }) - .ConfigureDevice(context => + .ConfigureDevice(context => { var deviceInfo = new DeviceInfo(context, DeviceType, deviceAddress); return DeviceManager.RegisterDevice(deviceName, deviceInfo); }); } + } - internal static class FmcLinkController - { - public const int ID = 23; + internal static class FmcLinkController + { + public const int ID = 23; public const uint ENABLE = 0; // The LSB is used to enable or disable the device data stream public const uint GPOSTATE = 1; // GPO output state (bits 31 downto 3: ignore. bits 2 downto 0: ‘1’ = high, ‘0’ = low) @@ -85,8 +86,13 @@ internal static class FmcLinkController public const uint SAVEVOLTAGE = 4; // Save link voltage to non-volatile EEPROM if greater than 0. This voltage will be applied after POR. public const uint LINKSTATE = 5; // bit 1 pass; bit 0 lock - public const uint LINKSTATE_PP = 0x2; // parity check pass bit - public const uint LINKSTATE_SL = 0x1; // SERDES lock bit - } + public const uint LINKSTATE_PP = 0x2; // parity check pass bit + public const uint LINKSTATE_SL = 0x1; // SERDES lock bit + } + + internal enum HubConfiguration + { + Standard, + Passthrough } } diff --git a/OpenEphys.Onix1/ConfigureHarpSyncInput.cs b/OpenEphys.Onix1/ConfigureHarpSyncInput.cs new file mode 100644 index 00000000..3b8ce24a --- /dev/null +++ b/OpenEphys.Onix1/ConfigureHarpSyncInput.cs @@ -0,0 +1,126 @@ +using System; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for configuring the ONIX breakout board Harp sync input device. + /// + /// + /// + /// Harp is a standard for asynchronous real-time data acquisition and experimental + /// control in neuroscience. It includes a clock synchronization protocol which allows + /// Harp devices to be connected to a shared clock line and continuously self-synchronize + /// their clocks to a precision of tens of microseconds. This means that all experimental + /// events are timestamped on the same clock and no post-hoc alignment of timing is necessary. + /// + /// + /// The Harp clock signal is transmitted over a serial line every second. + /// Every time the Harp sync input device in the ONIX breakout board detects a full Harp + /// synchronization packet, a new data frame is emitted pairing the current value of the + /// Harp clock with the local ONIX acquisition clock. + /// + /// + /// Logging the sequence of all Harp synchronization packets can greatly facilitate post-hoc + /// analysis and interpretation of timing signals. For more information see + /// . + /// + /// + [Description("Configures a ONIX breakout board Harp sync input device.")] + public class ConfigureHarpSyncInput : SingleDeviceFactory + { + /// + /// Initializes a new instance of the class. + /// + public ConfigureHarpSyncInput() + : base(typeof(HarpSyncInput)) + { + DeviceAddress = 12; + } + + /// + /// Gets or sets a value specifying whether the Harp sync input device is enabled. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the Harp sync input device is enabled.")] + public bool Enable { get; set; } = true; + + /// + /// Gets or sets a value specifying the physical Harp clock input source. + /// + /// + /// In standard ONIX breakout boards, the Harp mini-jack connector on the side of the + /// breakout is configured to receive Harp clock synchronization signals. + /// + /// In early access versions of the ONIX breakout board, the Harp mini-jack connector is + /// configured for output only, so a special adapter is needed to transmit the + /// Harp clock synchronization signal to the breakout clock input zero. + /// + [Category(ConfigurationCategory)] + [Description("Specifies the physical Harp clock input source.")] + public HarpSyncSource Source { get; set; } = HarpSyncSource.Breakout; + + /// + /// Configures a ONIX breakout board Harp sync input device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that hold configuration actions. + /// + /// The original sequence modified by adding additional configuration actions required to configure + /// a ONIX breakout board Harp sync input device. + /// + public override IObservable Process(IObservable source) + { + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(HarpSyncInput.ENABLE, Enable ? 1u : 0); + device.WriteRegister(HarpSyncInput.SOURCE, (uint)Source); + return DeviceManager.RegisterDevice(deviceName, device, DeviceType); + }); + } + } + + static class HarpSyncInput + { + public const int ID = 30; + + // managed registers + public const uint ENABLE = 0x0; // Enable or disable the data stream + public const uint SOURCE = 0x1; // Select the clock input source + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(HarpSyncInput)) + { + } + } + } + + /// + /// Specifies the physical Harp clock input source. + /// + public enum HarpSyncSource + { + /// + /// Specifies the Harp 3.5-mm audio jack connector on the side of the ONIX breakout board. + /// + Breakout = 0, + + /// + /// Specifies SMA clock input 0 on the ONIX breakout board. + /// + /// + /// In early access versions of the ONIX breakout board, Harp 3.5-mm audio jack connector was + /// configured for output only, so a special adapter was needed to transmit the Harp clock + /// synchronization signal to the breakout clock input zero. + /// + ClockAdapter = 1 + } +} diff --git a/OpenEphys.Onix1/ConfigureHeadstage64.cs b/OpenEphys.Onix1/ConfigureHeadstage64.cs new file mode 100644 index 00000000..9e7fd150 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureHeadstage64.cs @@ -0,0 +1,193 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that configures an ONIX headstage-64 in the specified port. + /// + [Description("Configures an ONIX headstage-64 in the specified port.")] + public class ConfigureHeadstage64 : MultiDeviceFactory + { + PortName port; + readonly ConfigureHeadstage64LinkController LinkController = new(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// Headstage-64 is a 1.5g serialized, multifunction headstage for small animals. This headstage is designed to function + /// with tetrode microdrives. Alternatively it can be used with other passive probes (e.g. silicon arrays, EEG/ECOG arrays, + /// etc.). It provides the following features on the headstage: + /// + /// 64 analog ephys channels and 3 auxiliary channels sampled at 30 kHz per channel. + /// A BNO055 9-axis IMU for real-time, 3D orientation tracking. + /// Three TS4231 light to digital converters for real-time, 3D position tracking with HTC Vive base stations. + /// A single electrical stimulator (current controlled, +/-15V compliance, automatic electrode discharge). + /// Two optical stimulators (800 mA peak current per channel). + /// + /// + public ConfigureHeadstage64() + { + // WONTFIX: The issue with this headstage is that its locking voltage is far, far lower than the voltage required for full + // functionality. Locking occurs at around 2V on the headstage (enough to turn 1.8V on). Full functionality is at 5.0 volts. + // The FMC port voltage can only go down to 3.3V, which means that its very hard to find the true lowest voltage + // for a lock and then add a large offset to that. Fixing this requires a hardware change. + Port = PortName.PortA; + LinkController.HubConfiguration = HubConfiguration.Standard; + } + + /// + /// Gets or sets the Rhd2164 configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the Rhd2164 device in the headstage-64.")] + public ConfigureRhd2164 Rhd2164 { get; set; } = new(); + + /// + /// Gets or sets the Bno055 9-axis inertial measurement unit configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the Bno055 device in the headstage-64.")] + public ConfigureBno055 Bno055 { get; set; } = new(); + + /// + /// Gets or sets the SteamVR V1 basestation 3D tracking array configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the TS4231 device in the headstage-64.")] + public ConfigureTS4231V1 TS4231 { get; set; } = new() { Enable = false }; + + /// + /// Gets or sets onboard electrical stimulator configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the ElectricalStimulator device in the headstage-64.")] + public ConfigureHeadstage64ElectricalStimulator ElectricalStimulator { get; set; } = new(); + + /// + /// Gets or sets onboard optical stimulator configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the OpticalStimulator device in the headstage-64.")] + public ConfigureHeadstage64OpticalStimulator OpticalStimulator { get; set; } = new(); + + /// + /// Gets or sets the port. + /// + /// + /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation. + /// + [Description("Specifies the physical connection of the headstage to the ONIX breakout board.")] + public PortName Port + { + get { return port; } + set + { + port = value; + var offset = (uint)port << 8; + LinkController.DeviceAddress = (uint)port; + Rhd2164.DeviceAddress = offset + 0; + Bno055.DeviceAddress = offset + 1; + TS4231.DeviceAddress = offset + 2; + ElectricalStimulator.DeviceAddress = offset + 3; + OpticalStimulator.DeviceAddress = offset + 4; + } + } + + /// + /// Gets or sets the port voltage override. + /// + /// + /// + /// If defined, it will override automated voltage discovery and apply the specified voltage to the headstage. + /// If left blank, an automated headstage detection algorithm will attempt to communicate with the headstage 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. + /// + /// + /// Warning: this device requires 5.5V to 6.0V, measured at the headstage, for proper operation. Supplying higher + /// voltages may result in damage. + /// + /// + [Description("If defined, it will override automated voltage discovery and apply the specified voltage" + + "to the headstage. Warning: this device requires 5.5V 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 GetDevices() + { + yield return LinkController; + yield return Rhd2164; + yield return Bno055; + yield return TS4231; + yield return ElectricalStimulator; + yield return OpticalStimulator; + } + + class ConfigureHeadstage64LinkController : ConfigureFmcLinkController + { + protected override bool ConfigurePortVoltage(DeviceContext device) + { + // WONTFIX: It takes a huge amount of time to get to 0, almost 10 seconds. + // The best we can do at the moment is drive port voltage to minimum which + // is an active process and then settle from there to zero volts. This requires + // a hardware revision that discharges the headstage between cycles to fix. + const uint MinVoltage = 33; + const uint MaxVoltage = 60; + const uint VoltageOffset = 34; + const uint VoltageIncrement = 02; + + // Start with highest voltage and ramp it down to find lowest lock voltage + var voltage = MaxVoltage; + for (; voltage >= MinVoltage; voltage -= VoltageIncrement) + { + device.WriteRegister(FmcLinkController.PORTVOLTAGE, voltage); + Thread.Sleep(200); + if (!CheckLinkState(device)) + { + if (voltage == MaxVoltage) return false; + else break; + } + } + + device.WriteRegister(FmcLinkController.PORTVOLTAGE, MinVoltage); + device.WriteRegister(FmcLinkController.PORTVOLTAGE, 0); + Thread.Sleep(1000); + device.WriteRegister(FmcLinkController.PORTVOLTAGE, voltage + VoltageOffset); + Thread.Sleep(200); + return CheckLinkState(device); + } + } + } + + /// + /// Specifies the physical port that a headstage is plugged into. + /// + /// + /// ONIX uses a common protocol to communicate with a variety of devices using the same physical connection. For this reason + /// lots of different headstage types can be plugged into a headstage port. + /// + public enum PortName + { + /// + /// Specifies Port A. + /// + PortA = 1, + /// + /// Specifies Port B. + /// + PortB = 2 + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64ElectricalStimulator.cs b/OpenEphys.Onix1/ConfigureHeadstage64ElectricalStimulator.cs similarity index 66% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64ElectricalStimulator.cs rename to OpenEphys.Onix1/ConfigureHeadstage64ElectricalStimulator.cs index 559d5e90..053afe34 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64ElectricalStimulator.cs +++ b/OpenEphys.Onix1/ConfigureHeadstage64ElectricalStimulator.cs @@ -1,14 +1,37 @@ using System; +using System.ComponentModel; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a headstage-64 onboard electrical stimulator. + /// + /// + /// This configuration class can be linked to a instance to deliver + /// current controlled electrical micro-stimulation through a contact on the probe connector on the bottom of the headstage + /// or the corresponding contact on a compatible electrode interface board. + /// + [Description("Configures a headstage-64 onboard electrical stimulator.")] public class ConfigureHeadstage64ElectricalStimulator : SingleDeviceFactory { + /// + /// Initializes a new instance of the class. + /// public ConfigureHeadstage64ElectricalStimulator() : base(typeof(Headstage64ElectricalStimulator)) { } + /// + /// Configure a headstage-64 onboard electrical stimulator. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a headstage-64 + /// onboard electrical stimulator. public override IObservable Process(IObservable source) { var deviceName = DeviceName; diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64OpticalStimulator.cs b/OpenEphys.Onix1/ConfigureHeadstage64OpticalStimulator.cs similarity index 66% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64OpticalStimulator.cs rename to OpenEphys.Onix1/ConfigureHeadstage64OpticalStimulator.cs index 736458b8..64de716d 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureHeadstage64OpticalStimulator.cs +++ b/OpenEphys.Onix1/ConfigureHeadstage64OpticalStimulator.cs @@ -1,14 +1,37 @@ using System; +using System.ComponentModel; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a headstage-64 dual-channel optical stimulator. + /// + /// + /// This configuration class can be linked to a instance to drive current + /// through laser diodes or LEDs connected to two contacts on the probe connector on the bottom of the headstage + /// or the corresponding contacts on a compatible electrode interface board. + /// + [Description("Configures a headstage-64 dual-channel optical stimulator.")] public class ConfigureHeadstage64OpticalStimulator : SingleDeviceFactory { + /// + /// Initializes a new instance of the class. + /// public ConfigureHeadstage64OpticalStimulator() : base(typeof(Headstage64OpticalStimulator)) { } + /// + /// Configure a headstage-64 dual-channel optical stimulator. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a + /// headstage-64 dual-channel optical stimulator. public override IObservable Process(IObservable source) { var deviceName = DeviceName; diff --git a/OpenEphys.Onix1/ConfigureHeadstageRhs2116.cs b/OpenEphys.Onix1/ConfigureHeadstageRhs2116.cs new file mode 100644 index 00000000..85738f0d --- /dev/null +++ b/OpenEphys.Onix1/ConfigureHeadstageRhs2116.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; + +namespace OpenEphys.Onix1 +{ + public class ConfigureHeadstageRhs2116 : MultiDeviceFactory + { + PortName port; + readonly ConfigureHeadstageRhs2116LinkController LinkController = new(); + + public ConfigureHeadstageRhs2116() + { + Port = PortName.PortA; + LinkController.HubConfiguration = HubConfiguration.Standard; + } + + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + public ConfigureRhs2116 Rhs2116A { get; set; } = new(); + + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + public ConfigureRhs2116 Rhs2116B { get; set; } = new(); + + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + public ConfigureRhs2116Trigger StimulusTrigger { get; set; } = new(); + + internal override void UpdateDeviceNames() + { + LinkController.DeviceName = GetFullDeviceName(nameof(LinkController)); + Rhs2116A.DeviceName = GetFullDeviceName(nameof(Rhs2116A)); + Rhs2116B.DeviceName = GetFullDeviceName(nameof(Rhs2116B)); + StimulusTrigger.DeviceName = GetFullDeviceName(nameof(StimulusTrigger)); + } + + public PortName Port + { + get { return port; } + set + { + port = value; + var offset = (uint)port << 8; + LinkController.DeviceAddress = (uint)port; + Rhs2116A.DeviceAddress = offset + 0; + Rhs2116B.DeviceAddress = offset + 1; + StimulusTrigger.DeviceAddress = offset + 2; + } + } + + + [Description("If defined, it will override automated voltage discovery and apply the specified voltage" + + "to the headstage. Warning: this device requires 3.4V to 4.4V 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 GetDevices() + { + yield return LinkController; + yield return Rhs2116A; + yield return Rhs2116B; + yield return StimulusTrigger; + } + + class ConfigureHeadstageRhs2116LinkController : ConfigureFmcLinkController + { + protected override bool ConfigurePortVoltage(DeviceContext device) + { + const double MinVoltage = 3.3; + const double MaxVoltage = 4.4; + const double VoltageOffset = 2.0; + const double VoltageIncrement = 0.2; + + for (var voltage = MinVoltage; voltage <= MaxVoltage; voltage += VoltageIncrement) + { + SetPortVoltage(device, voltage); + if (base.CheckLinkState(device)) + { + SetPortVoltage(device, voltage + VoltageOffset); + return CheckLinkState(device); + } + } + + return false; + } + + private void SetPortVoltage(DeviceContext device, double voltage) + { + device.WriteRegister(FmcLinkController.PORTVOLTAGE, 0); + Thread.Sleep(500); + device.WriteRegister(FmcLinkController.PORTVOLTAGE, (uint)(10 * voltage)); + Thread.Sleep(500); + } + + protected override bool CheckLinkState(DeviceContext device) + { + // NB: The RHS2116 headstage needs an additional reset after power on to provide its device table. + device.Context.Reset(); + var linkState = device.ReadRegister(FmcLinkController.LINKSTATE); + return (linkState & FmcLinkController.LINKSTATE_SL) != 0; + } + } + } +} diff --git a/OpenEphys.Onix1/ConfigureHeartbeat.cs b/OpenEphys.Onix1/ConfigureHeartbeat.cs new file mode 100644 index 00000000..a8d070e5 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureHeartbeat.cs @@ -0,0 +1,104 @@ +using System; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for configuring a heartbeat device. + /// + /// This configuration class can be linked to a instance to stream + /// heartbeats from the acquisition system. + /// + /// + [Description("Configures a heartbeat device.")] + public class ConfigureHeartbeat : SingleDeviceFactory + { + readonly BehaviorSubject beatsPerSecond = new(10); + + /// + /// Initializes and new instance of the class. + /// + public ConfigureHeartbeat() + : base(typeof(Heartbeat)) + { + } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, a instance that is linked to this configuration will produce data. + /// If set to false, it will not produce data. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the heartbeat device is enabled.")] + public bool Enable { get; set; } = true; + + /// + /// Gets or sets the rate at which beats are produced in Hz. + /// + /// + /// If set to true, a instance that is linked to this configuration will produce data. + /// If set to false, it will not produce data. + /// + [Range(1, 10e6)] + [Category(AcquisitionCategory)] + [Description("Rate at which beats are produced (Hz).")] + public uint BeatsPerSecond + { + get => beatsPerSecond.Value; + set => beatsPerSecond.OnNext(value); + } + + /// + /// Configures a heartbeat device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a heartbeat device./> + public override IObservable Process(IObservable source) + { + var enable = Enable; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice((context, observer) => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(Heartbeat.ENABLE, enable ? 1u : 0u); + var subscription = beatsPerSecond.SubscribeSafe(observer, newValue => + { + var clkHz = device.ReadRegister(Heartbeat.CLK_HZ); + device.WriteRegister(Heartbeat.CLK_DIV, clkHz / newValue); + }); + + return new CompositeDisposable( + DeviceManager.RegisterDevice(deviceName, device, DeviceType), + subscription + ); + }); + } + } + + static class Heartbeat + { + public const int ID = 12; + + public const uint ENABLE = 0; // Enable the heartbeat + public const uint CLK_DIV = 1; // Heartbeat clock divider ratio. Default results in 10 Hz heartbeat. Values less than CLK_HZ / 10e6 Hz will result in 1kHz. + public const uint CLK_HZ = 2; // The frequency parameter, CLK_HZ, used in the calculation of CLK_DIV + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(Heartbeat)) + { + } + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureLoadTester.cs b/OpenEphys.Onix1/ConfigureLoadTester.cs similarity index 57% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureLoadTester.cs rename to OpenEphys.Onix1/ConfigureLoadTester.cs index 5bfd3201..12259869 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureLoadTester.cs +++ b/OpenEphys.Onix1/ConfigureLoadTester.cs @@ -5,27 +5,54 @@ using System.Reactive.Subjects; using Bonsai; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class for configuring a load testing device. + /// + /// + /// The load tester device can be configured to produce data at user-settable size and rate + /// to stress test various communication links and test closed-loop response latency. + /// + [Description("Configures a load testing device.")] public class ConfigureLoadTester : SingleDeviceFactory { readonly BehaviorSubject frameHz = new(1000); + /// + /// Initializes a new instance of the class. + /// public ConfigureLoadTester() : base(typeof(LoadTester)) { } + /// + /// Gets or sets a value specifying whether the load testing device is enabled. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the load testing device is enabled.")] + public bool Enable { get; set; } = false; + + /// + /// Gets or sets the number of repetitions of the 16-bit unsigned integer 42 sent with each read-frame. + /// [Category(ConfigurationCategory)] [Description("Number of repetitions of the 16-bit unsigned integer 42 sent with each read-frame.")] [Range(0, 10e6)] public uint ReceivedWords { get; set; } + /// + /// Gets or sets the number of repetitions of the 32-bit integer 42 sent with each write frame. + /// [Category(ConfigurationCategory)] [Description("Number of repetitions of the 32-bit integer 42 sent with each write frame.")] [Range(0, 10e6)] public uint TransmittedWords { get; set; } + /// + /// Gets or sets a value specifying the rate at which frames are produced, in Hz. + /// [Category(AcquisitionCategory)] [Description("Specifies the rate at which frames are produced (Hz).")] public uint FramesPerSecond @@ -34,38 +61,52 @@ public uint FramesPerSecond set { frameHz.OnNext(value); } } + /// + /// Configures a load testing device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that hold configuration actions. + /// + /// The original sequence modified by adding additional configuration actions required to configure + /// a load testing device. + /// public override IObservable Process(IObservable source) { + var enable = Enable; var deviceName = DeviceName; var deviceAddress = DeviceAddress; var receivedWords = ReceivedWords; var transmittedWords = TransmittedWords; - return source.ConfigureDevice(context => + return source.ConfigureDevice((context, observer) => { var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(LoadTester.ENABLE, 1); + device.WriteRegister(LoadTester.ENABLE, enable ? 1u : 0u); + + var clockHz = device.ReadRegister(LoadTester.CLK_HZ); - var clk_hz = device.ReadRegister(LoadTester.CLK_HZ); // Assumes 8-byte timer uint ValidSize() { - var clk_div = device.ReadRegister(LoadTester.CLK_DIV); - return clk_div - 4 - 10; // -10 is overhead hack + var clkDiv = device.ReadRegister(LoadTester.CLK_DIV); + return clkDiv - 4 - 10; // -10 is overhead hack } - var max_size = ValidSize(); - var bounded = receivedWords > max_size ? max_size : receivedWords; + var maxSize = ValidSize(); + var bounded = receivedWords > maxSize ? maxSize : receivedWords; device.WriteRegister(LoadTester.DT0H16_WORDS, bounded); var writeArray = Enumerable.Repeat((uint)42, (int)(transmittedWords + 2)).ToArray(); device.WriteRegister(LoadTester.HTOD32_WORDS, transmittedWords); - var frameHzSubscription = frameHz.Subscribe(newValue => + var frameHzSubscription = frameHz.SubscribeSafe(observer, newValue => { - device.WriteRegister(LoadTester.CLK_DIV, clk_hz / newValue); - var max_size = ValidSize(); - if (receivedWords > max_size) + device.WriteRegister(LoadTester.CLK_DIV, clockHz / newValue); + var maxSize = ValidSize(); + if (receivedWords > maxSize) { - receivedWords = max_size; + receivedWords = maxSize; } }); diff --git a/OpenEphys.Onix1/ConfigureMemoryMonitor.cs b/OpenEphys.Onix1/ConfigureMemoryMonitor.cs new file mode 100644 index 00000000..037eea9d --- /dev/null +++ b/OpenEphys.Onix1/ConfigureMemoryMonitor.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for configuring a hardware memory monitor device. + /// + /// + /// The memory monitor produces periodic snapshots of the system's first in, first out (FIFO) data buffer. + /// This can be useful for: + /// + /// Ensuring that data is being read by the host PC quickly enough to prevent real-time delays or overflows. + /// In the case that the PC is not keeping up with data collection, FIFO memory use will increase monotonically. + /// Tuning the value of to optimize real-time performance. + /// For optimal real-time performance, should be as small as possible and the FIFO should be bypassed + /// (memory usage should remain at 0). However, these requirements are in conflict. The memory monitor provides a way to find the minimal value of + /// value of that does not result in excessive FIFO data buffering. This tradeoff will depend on the + /// bandwidth of data being acquired, the performance of the host PC, and downstream real-time processing. + /// + /// + [Description("Configures a hardware memory monitor device.")] + public class ConfigureMemoryMonitor : SingleDeviceFactory + { + /// + /// Initialize a new instance of . + /// + public ConfigureMemoryMonitor() + : base(typeof(MemoryMonitor)) + { + DeviceAddress = 10; + } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, will not produce data. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the memory monitor device is enabled.")] + public bool Enable { get; set; } = false; + + /// + /// Gets or sets the frequency at which memory use is recorded in Hz. + /// + [Range(1, 1000)] + [Category(ConfigurationCategory)] + [Description("Frequency at which memory use is recorded (Hz).")] + public uint SamplesPerSecond { get; set; } = 10; + + /// + /// Configures a memory monitor device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a memory monitor device./> + public override IObservable Process(IObservable source) + { + var enable = Enable; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + var samplesPerSecond = SamplesPerSecond; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(MemoryMonitor.ENABLE, enable ? 1u : 0u); + device.WriteRegister(MemoryMonitor.CLK_DIV, device.ReadRegister(MemoryMonitor.CLK_HZ) / samplesPerSecond); + return DeviceManager.RegisterDevice(deviceName, device, DeviceType); + }); + } + } + + static class MemoryMonitor + { + public const int ID = 28; + + public const uint ENABLE = 0; // Enable the monitor + public const uint CLK_DIV = 1; // Sample clock divider ratio. Values less than CLK_HZ / 10e6 Hz will result in 1kHz. + public const uint CLK_HZ = 2; // The frequency parameter, CLK_HZ, used in the calculation of CLK_DIV + public const uint TOTAL_MEM = 3; // Total available memory in 32-bit words + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(MemoryMonitor)) + { + } + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1e.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs similarity index 68% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1e.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs index eaa4fc87..578bb8d7 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1e.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs @@ -4,49 +4,124 @@ using System.Threading; using Bonsai; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a NeuropixelsV1e device. + /// + [Description("Configures a NeuropixelsV1e device.")] public class ConfigureNeuropixelsV1e : SingleDeviceFactory { + /// + /// Initialize a new instance of a class. + /// public ConfigureNeuropixelsV1e() : base(typeof(NeuropixelsV1e)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, + /// will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the Neuropixels data stream is enabled.")] public bool Enable { get; set; } = true; + /// + /// Gets or sets the LED enable state. + /// + /// + /// If true, the headstage LED will turn on during data acquisition. If false, the LED will not turn on. + /// [Category(ConfigurationCategory)] - [Description("If true, the headstage LED will illuminate during acquisition. Otherwise it will remain off.")] + [Description("If true, the headstage LED will turn on during data acquisition. If false, the LED will not turn on.")] public bool EnableLed { get; set; } = true; + /// + /// Gets or sets the amplifier gain for the spike-band. + /// + /// + /// The spike-band is from DC to 10 kHz if is set to false, while the + /// spike-band is from 300 Hz to 10 kHz if is set to true. + /// [Category(ConfigurationCategory)] [Description("Amplifier gain for spike-band.")] public NeuropixelsV1Gain SpikeAmplifierGain { get; set; } = NeuropixelsV1Gain.Gain1000; + /// + /// Gets or sets the amplifier gain for the LFP-band. + /// + /// + /// The LFP band is from 0.5 to 500 Hz. + /// [Category(ConfigurationCategory)] [Description("Amplifier gain for LFP-band.")] public NeuropixelsV1Gain LfpAmplifierGain { get; set; } = NeuropixelsV1Gain.Gain50; + /// + /// Gets or sets the reference for all electrodes. + /// + /// + /// All electrodes are set to the same reference, which can be either + /// or . + /// Setting to will use the external reference, while + /// sets the reference to the electrode at the tip of the probe. + /// [Category(ConfigurationCategory)] [Description("Reference selection.")] public NeuropixelsV1ReferenceSource Reference { get; set; } = NeuropixelsV1ReferenceSource.External; + /// + /// Gets or sets the state of the spike-band filter. + /// + /// + /// If set to true, the spike-band has a 300 Hz high-pass filter which will be activated. If set to + /// false, the high-pass filter will not to be activated. + /// [Category(ConfigurationCategory)] [Description("If true, activates a 300 Hz high-pass filter in the spike-band data stream.")] public bool SpikeFilter { get; set; } = true; + /// + /// Gets or sets the path to the gain calibration file. + /// + /// + /// Each probe must be provided with a gain calibration file that contains calibration data + /// specific to each probe. This file is mandatory for accurate recordings. + /// [FileNameFilter("Gain calibration files (*_gainCalValues.csv)|*_gainCalValues.csv")] [Description("Path to the Neuropixels 1.0 gain calibration file.")] [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] public string GainCalibrationFile { get; set; } + /// + /// Gets or sets the path to the ADC calibration file. + /// + /// + /// Each probe must be provided with an ADC calibration file that contains calibration data + /// specific to each probe. This file is mandatory for accurate recordings. + /// [FileNameFilter("ADC calibration files (*_ADCCalibration.csv)|*_ADCCalibration.csv")] [Description("Path to the Neuropixels 1.0 ADC calibration file.")] [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] public string AdcCalibrationFile { get; set; } + /// + /// Configures a NeuropixelsV1e device. + /// + /// + /// This will schedule configuration actions to be applied by a node + /// prior to data acquisition. + /// + /// A sequence of that holds all configuration actions. + /// + /// The original sequence with the side effect of an additional configuration action to configure + /// a NeuropixelsV1e device. + /// public override IObservable Process(IObservable source) { var enable = Enable; @@ -87,15 +162,14 @@ public override IObservable Process(IObservable source } var deviceInfo = new NeuropixelsV1eDeviceInfo(context, DeviceType, deviceAddress, probeControl); - var disposable = DeviceManager.RegisterDevice(deviceName, deviceInfo); var shutdown = Disposable.Create(() => { serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.GPIO10, NeuropixelsV1e.DefaultGPO10Config); serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.GPIO32, NeuropixelsV1e.DefaultGPO32Config); }); return new CompositeDisposable( - shutdown, - disposable); + DeviceManager.RegisterDevice(deviceName, deviceInfo), + shutdown); }); } @@ -165,7 +239,7 @@ static class NeuropixelsV1e public const int ChannelCount = 384; public const int FrameWords = 40; - // unmanaged regiseters + // unmanaged registers public const uint OP_MODE = 0X00; public const uint REC_MOD = 0X01; public const uint CAL_MOD = 0X02; @@ -235,21 +309,57 @@ enum NeuropixelsV1OperationRegisterValues : uint RECORD_AND_CALIBRATE = RECORD | CALIBRATE, }; + /// + /// Specifies the reference source for all electrodes. + /// public enum NeuropixelsV1ReferenceSource : byte { + /// + /// Specifies that the reference should be External. + /// External = 0b001, + /// + /// Specifies that the reference should be the Tip. + /// Tip = 0b010 } + /// + /// Specifies the gain for all electrodes + /// public enum NeuropixelsV1Gain : byte { + /// + /// Specifies that the gain should be 50x. + /// Gain50 = 0b000, + /// + /// Specifies that the gain should be 125x. + /// Gain125 = 0b001, + /// + /// Specifies that the gain should be 250x. + /// Gain250 = 0b010, + /// + /// Specifies that the gain should be 500x. + /// Gain500 = 0b011, + /// + /// Specifies that the gain should be 1000x. + /// Gain1000 = 0b100, + /// + /// Specifies that the gain should be 1500x. + /// Gain1500 = 0b101, + /// + /// Specifies that the gain should be 2000x. + /// Gain2000 = 0b110, + /// + /// Specifies that the gain should be 3000x. + /// Gain3000 = 0b111 } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1eBno055.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV1eBno055.cs similarity index 67% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1eBno055.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV1eBno055.cs index 9dc9e3ae..b5cbb9e9 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1eBno055.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV1eBno055.cs @@ -1,19 +1,45 @@ using System; using System.ComponentModel; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a NeuropixelsV1eBno055 device. + /// + [Description("Configures a NeuropixelsV1eBno055 device.")] public class ConfigureNeuropixelsV1eBno055 : SingleDeviceFactory { + /// + /// Initialize a new instance of a class. + /// public ConfigureNeuropixelsV1eBno055() : base(typeof(NeuropixelsV1eBno055)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, + /// will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the BNO055 device is enabled.")] public bool Enable { get; set; } = true; + /// + /// Configures a NeuropixelsV1eBno055 device. + /// + /// + /// This will schedule configuration actions to be applied by a node + /// prior to data acquisition. + /// + /// A sequence of that holds all configuration actions. + /// + /// The original sequence with the side effect of an additional configuration action to configure + /// a NeuropixelsV1eBno055 device. + /// public override IObservable Process(IObservable source) { var enable = Enable; diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1eHeadstage.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV1eHeadstage.cs similarity index 54% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1eHeadstage.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV1eHeadstage.cs index bb75b6e4..df88662e 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV1eHeadstage.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV1eHeadstage.cs @@ -2,27 +2,49 @@ using System.ComponentModel; using System.Threading; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class ConfigureNeuropixelsV1eHeadstage : HubDeviceFactory + /// + /// A class that configures a NeuropixelsV1e headstage. + /// + [Description("Configures a NeuropixelsV1e headstage.")] + public class ConfigureNeuropixelsV1eHeadstage : MultiDeviceFactory { PortName port; - readonly ConfigureNeuropixelsV1LinkController LinkController = new(); + readonly ConfigureNeuropixelsV1eLinkController LinkController = new(); + /// + /// Initialize a new instance of a class. + /// public ConfigureNeuropixelsV1eHeadstage() { Port = PortName.PortA; LinkController.HubConfiguration = HubConfiguration.Passthrough; } + /// + /// Gets or sets the NeuropixelsV1e configuration. + /// [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] - public ConfigureNeuropixelsV1e NeuropixelsV1 { get; set; } = new(); + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the NeuropixelsV1e device.")] + public ConfigureNeuropixelsV1e NeuropixelsV1e { get; set; } = new(); + /// + /// Gets or sets the Bno055 9-axis inertial measurement unit configuration. + /// [Category(ConfigurationCategory)] - [TypeConverter(typeof(HubDeviceConverter))] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the Bno055 device.")] public ConfigureNeuropixelsV1eBno055 Bno055 { get; set; } = new(); + /// + /// Gets or sets the port. + /// + /// + /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation. + /// + [Description("Specifies the physical connection of the headstage to the ONIX breakout board.")] public PortName Port { get { return port; } @@ -31,11 +53,20 @@ public PortName Port port = value; var offset = (uint)port << 8; LinkController.DeviceAddress = (uint)port; - NeuropixelsV1.DeviceAddress = offset + 0; + NeuropixelsV1e.DeviceAddress = offset + 0; Bno055.DeviceAddress = offset + 1; } } + /// + /// Gets or sets the port voltage. + /// + /// + /// If a port voltage is defined this will override the automated voltage discovery and applies + /// the specified voltage to the headstage. To enable automated voltage discovery, leave this field + /// empty. Warning: This device requires 3.8V to 5.0V for proper operation. Voltages higher than 5.0V can + /// damage the headstage + /// [Description("If defined, overrides automated voltage discovery and applies " + "the specified voltage to the headstage. Warning: this device requires 3.8V to 5.0V " + "for proper operation. Higher voltages can damage the headstage.")] @@ -48,11 +79,11 @@ public double? PortVoltage internal override IEnumerable GetDevices() { yield return LinkController; - yield return NeuropixelsV1; + yield return NeuropixelsV1e; yield return Bno055; } - class ConfigureNeuropixelsV1LinkController : ConfigureFmcLinkController + class ConfigureNeuropixelsV1eLinkController : ConfigureFmcLinkController { protected override bool ConfigurePortVoltage(DeviceContext device) { diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2e.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs similarity index 60% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2e.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs index 192adce0..b8893b62 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2e.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2e.cs @@ -3,37 +3,83 @@ using System.Reactive.Disposables; using Bonsai; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a NeuropixelsV2e device. + /// + [Description("Configures a NeuropixelsV2e device.")] public class ConfigureNeuropixelsV2e : SingleDeviceFactory { + /// + /// Initialize a new instance of a class. + /// public ConfigureNeuropixelsV2e() : base(typeof(NeuropixelsV2e)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, + /// will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the NeuropixelsV2 device is enabled.")] public bool Enable { get; set; } = true; + /// + /// Gets or sets the electrode configuration for Probe A. + /// [Category(ConfigurationCategory)] [Description("Probe A electrode configuration.")] - public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationA { get; set; } - + public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationA { get; set; } = new(); + + /// + /// Gets or sets the path to the gain calibration file for Probe A. + /// + /// + /// Each probe must be provided with a gain calibration file that contains calibration data + /// specific to each probe. This file is mandatory for accurate recordings. + /// [FileNameFilter("Gain calibration files (*_gainCalValues.csv)|*_gainCalValues.csv")] [Description("Path to the gain calibration file for probe A.")] [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] public string GainCalibrationFileA { get; set; } + /// + /// Gets or sets the electrode configuration for Probe B. + /// [Category(ConfigurationCategory)] [Description("Probe B electrode configuration.")] - public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationB { get; set; } - + public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationB { get; set; } = new(); + + /// + /// Gets or sets the path to the gain calibration file for Probe B. + /// + /// + /// Each probe must be provided with a gain calibration file that contains calibration data + /// specific to each probe. This file is mandatory for accurate recordings. + /// [FileNameFilter("Gain calibration files (*_gainCalValues.csv)|*_gainCalValues.csv")] [Description("Path to the gain calibration file for probe B.")] [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] public string GainCalibrationFileB { get; set; } + /// + /// Configures a NeuropixelsV2e device. + /// + /// + /// This will schedule configuration actions to be applied by a node + /// prior to data acquisition. + /// + /// A sequence of that holds all configuration actions. + /// + /// The original sequence with the side effect of an additional configuration action to configure + /// a NeuropixelsV2e device. + /// public override IObservable Process(IObservable source) { var enable = Enable; @@ -50,6 +96,10 @@ public override IObservable Process(IObservable source var serializer = new I2CRegisterContext(device, DS90UB9x.SER_ADDR); var gpo10Config = EnableProbeSupply(serializer); + // set I2C clock rate to ~400 kHz + serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.SCLHIGH, 20); + serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.SCLLOW, 20); + // read probe metadata var probeAMetadata = ReadProbeMetadata(serializer, NeuropixelsV2e.ProbeASelected); var probeBMetadata = ReadProbeMetadata(serializer, NeuropixelsV2e.ProbeBSelected); @@ -64,14 +114,15 @@ public override IObservable Process(IObservable source ResetProbes(serializer, gpo10Config); // configure probe streaming - ushort? gainCorrectionA = null; - ushort? gainCorrectionB = null; - var probeControl = new NeuropixelsV2RegisterContext(device, NeuropixelsV2e.ProbeAddress); + double? gainCorrectionA = null; + double? gainCorrectionB = null; + var probeControl = new NeuropixelsV2eRegisterContext(device, NeuropixelsV2e.ProbeAddress); // configure probe A streaming if (probeAMetadata.ProbeSerialNumber != null) { - gainCorrectionA = ReadGainCorrection(GainCalibrationFileA, (ulong)probeAMetadata.ProbeSerialNumber); + gainCorrectionA = NeuropixelsV2.ReadGainCorrection( + GainCalibrationFileA, (ulong)probeAMetadata.ProbeSerialNumber, NeuropixelsV2Probe.ProbeA); SelectProbe(serializer, NeuropixelsV2e.ProbeASelected); probeControl.WriteConfiguration(ProbeConfigurationA); ConfigureProbeStreaming(probeControl); @@ -80,22 +131,25 @@ public override IObservable Process(IObservable source // configure probe B streaming if (probeBMetadata.ProbeSerialNumber != null) { - gainCorrectionB = ReadGainCorrection(GainCalibrationFileB, (ulong)probeBMetadata.ProbeSerialNumber); + gainCorrectionB = NeuropixelsV2.ReadGainCorrection( + GainCalibrationFileB, (ulong)probeBMetadata.ProbeSerialNumber, NeuropixelsV2Probe.ProbeB); SelectProbe(serializer, NeuropixelsV2e.ProbeBSelected); probeControl.WriteConfiguration(ProbeConfigurationB); ConfigureProbeStreaming(probeControl); } + // disconnect i2c bus from both probes to prevent digital interference during acquisition + SelectProbe(serializer, NeuropixelsV2e.NoProbeSelected); + var deviceInfo = new NeuropixelsV2eDeviceInfo(context, DeviceType, deviceAddress, gainCorrectionA, gainCorrectionB); - var disposable = DeviceManager.RegisterDevice(deviceName, deviceInfo); var shutdown = Disposable.Create(() => { serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.GPIO10, NeuropixelsV2e.DefaultGPO10Config); SelectProbe(serializer, NeuropixelsV2e.NoProbeSelected); }); return new CompositeDisposable( - shutdown, - disposable); + DeviceManager.RegisterDevice(deviceName, deviceInfo), + shutdown); }); } @@ -147,26 +201,6 @@ static NeuropixelsV2eMetadata ReadProbeMetadata(I2CRegisterContext serializer, b SelectProbe(serializer, probeSelect); return new NeuropixelsV2eMetadata(serializer); } - - static ushort ReadGainCorrection(string gainCalibrationFile, ulong probeSerialNumber) - { - if (gainCalibrationFile == null) - { - throw new ArgumentException("Calibration file must be specified."); - } - - System.IO.StreamReader gainFile = new(gainCalibrationFile); - var sn = ulong.Parse(gainFile.ReadLine()); - - if (probeSerialNumber != sn) - { - throw new ArgumentException($"Probe serial number {probeSerialNumber} does not match calibration file serial number {sn}."); - } - - // Q1.14 fixed point conversion - return (ushort)(double.Parse(gainFile.ReadLine()) * (1 << 14)); - } - static void SelectProbe(I2CRegisterContext serializer, byte probeSelect) { serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.GPIO32, probeSelect); @@ -184,25 +218,25 @@ static void ResetProbes(I2CRegisterContext serializer, uint gpo10Config) static void ConfigureProbeStreaming(I2CRegisterContext i2cNP) { // Write super sync bits into ASIC - i2cNP.WriteByte(0x15, 0b00011000); - i2cNP.WriteByte(0x14, 0b01100001); - i2cNP.WriteByte(0x13, 0b10000110); - i2cNP.WriteByte(0x12, 0b00011000); - i2cNP.WriteByte(0x11, 0b01100001); - i2cNP.WriteByte(0x10, 0b10000110); - i2cNP.WriteByte(0x0F, 0b00011000); - i2cNP.WriteByte(0x0E, 0b01100001); - i2cNP.WriteByte(0x0D, 0b10000110); - i2cNP.WriteByte(0x0C, 0b00011000); - i2cNP.WriteByte(0x0B, 0b01100001); - i2cNP.WriteByte(0x0A, 0b10111001); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC11, 0b00011000); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC10, 0b01100001); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC9, 0b10000110); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC8, 0b00011000); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC7, 0b01100001); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC6, 0b10000110); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC5, 0b00011000); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC4, 0b01100001); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC3, 0b10000110); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC2, 0b00011000); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC1, 0b01100001); + i2cNP.WriteByte(NeuropixelsV2e.SUPERSYNC0, 0b10111001); // Activate recording mode on NP - i2cNP.WriteByte(0, 0b0100_0000); + i2cNP.WriteByte(NeuropixelsV2e.OP_MODE, 0b0100_0000); // Set global ADC settings // TODO: Undocumented - i2cNP.WriteByte(3, 0b0000_1000); + i2cNP.WriteByte(NeuropixelsV2e.ADC_CONFIG, 0b0000_1000); } } @@ -223,6 +257,40 @@ static class NeuropixelsV2e public const int ChannelCount = 384; public const int FrameWords = 36; // TRASH TRASH TRASH 0 ADC0 ADC8 ADC16 0 ADC1 ADC9 ADC17 0 ... ADC7 ADC15 ADC23 0 + // unmanaged register map + public const uint OP_MODE = 0x00; + public const uint REC_MODE = 0x01; + public const uint CAL_MODE = 0x02; + public const uint ADC_CONFIG = 0x03; + public const uint TEST_CONFIG1 = 0x04; + public const uint TEST_CONFIG2 = 0x05; + public const uint TEST_CONFIG3 = 0x06; + public const uint TEST_CONFIG4 = 0x07; + public const uint TEST_CONFIG5 = 0x08; + public const uint STATUS = 0x09; + public const uint SUPERSYNC0 = 0x0A; + public const uint SUPERSYNC1 = 0x0B; + public const uint SUPERSYNC2 = 0x0C; + public const uint SUPERSYNC3 = 0x0D; + public const uint SUPERSYNC4 = 0x0E; + public const uint SUPERSYNC5 = 0x0F; + public const uint SUPERSYNC6 = 0x10; + public const uint SUPERSYNC7 = 0x11; + public const uint SUPERSYNC8 = 0x12; + public const uint SUPERSYNC9 = 0x13; + public const uint SUPERSYNC10 = 0x14; + public const uint SUPERSYNC11 = 0x15; + public const uint SR_CHAIN6 = 0x16; // Odd channel base config + public const uint SR_CHAIN5 = 0x17; // Even channel base config + public const uint SR_CHAIN4 = 0x18; // Shank 4 + public const uint SR_CHAIN3 = 0x19; // Shank 3 + public const uint SR_CHAIN2 = 0x1A; // Shank 2 + public const uint SR_CHAIN1 = 0x1B; // Shank 1 + public const uint SR_LENGTH2 = 0x1C; + public const uint SR_LENGTH1 = 0x1D; + public const uint PROBE_ID = 0x1E; + public const uint SOFT_RESET = 0x1F; + internal class NameConverter : DeviceNameConverter { public NameConverter() diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBeta.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs similarity index 69% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBeta.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs index 65926a20..ce537096 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBeta.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBeta.cs @@ -3,41 +3,90 @@ using System.Reactive.Disposables; using Bonsai; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a NeuropixelsV2eBeta device. + /// + [Description("Configures a NeuropixelsV2eBeta device.")] public class ConfigureNeuropixelsV2eBeta : SingleDeviceFactory { + /// + /// Initialize a new instance of a class. + /// public ConfigureNeuropixelsV2eBeta() : base(typeof(NeuropixelsV2eBeta)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, + /// will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the NeuropixelsV2Beta device is enabled.")] public bool Enable { get; set; } = true; + /// + /// Gets or sets the LED enable state. + /// + /// + /// If true, the headstage LED will turn on during data acquisition. If false, the LED will not turn on. + /// [Category(ConfigurationCategory)] - [Description("Enable headstage LED when acquiring data.")] + [Description("If true, the headstage LED will turn on during data acquisition. If false, the LED will not turn on.")] public bool EnableLed { get; set; } = true; + /// + /// Gets or sets the electrode configuration for Probe A. + /// [Category(ConfigurationCategory)] [Description("Probe A electrode configuration.")] - public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationA { get; set; } = new NeuropixelsV2QuadShankProbeConfiguration(); - + public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationA { get; set; } = new(); + + /// + /// Gets or sets the path to the gain calibration file for Probe A. + /// + /// + /// Each probe must be provided with a gain calibration file that contains calibration data + /// specific to each probe. This file is mandatory for accurate recordings. + /// [FileNameFilter("Gain calibration files (*_gainCalValues.csv)|*_gainCalValues.csv")] - [Description("Path to the gain calibraiton file for probe A.")] + [Description("Path to the gain calibration file for probe A.")] [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] public string GainCalibrationFileA { get; set; } + /// + /// Gets or sets the electrode configuration for Probe B. + /// [Category(ConfigurationCategory)] [Description("Probe B electrode configuration.")] - public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationB { get; set; } = new NeuropixelsV2QuadShankProbeConfiguration(); - + public NeuropixelsV2QuadShankProbeConfiguration ProbeConfigurationB { get; set; } = new(); + + /// + /// Gets or sets the path to the gain calibration file for Probe B. + /// + /// + /// Each probe must be provided with a gain calibration file that contains calibration data + /// specific to each probe. This file is mandatory for accurate recordings. + /// [FileNameFilter("Gain calibration files (*_gainCalValues.csv)|*_gainCalValues.csv")] - [Description("Path to the gain calibraiton file for probe B.")] + [Description("Path to the gain calibration file for probe B.")] [Editor("Bonsai.Design.OpenFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] public string GainCalibrationFileB { get; set; } + /// + /// Configures a NeuropixelsV2eBeta device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a NeuropixelsV2eBeta device./> public override IObservable Process(IObservable source) { var enable = Enable; @@ -80,16 +129,17 @@ public override IObservable Process(IObservable source System.Threading.Thread.Sleep(20); // configure probe streaming - var probeControl = new NeuropixelsV2RegisterContext(device, NeuropixelsV2eBeta.ProbeAddress); + var probeControl = new NeuropixelsV2eBetaRegisterContext(device, NeuropixelsV2eBeta.ProbeAddress); - ushort? gainCorrectionA = null; - ushort? gainCorrectionB = null; + double? gainCorrectionA = null; + double? gainCorrectionB = null; // configure probe A streaming if (probeAMetadata.ProbeSerialNumber != null) { // read gain correction - gainCorrectionA = ReadGainCorrection(GainCalibrationFileA, (ulong)probeAMetadata.ProbeSerialNumber); + gainCorrectionA = NeuropixelsV2.ReadGainCorrection( + GainCalibrationFileA, (ulong)probeAMetadata.ProbeSerialNumber, NeuropixelsV2Probe.ProbeA); SelectProbe(serializer, ref gpo32Config, NeuropixelsV2eBeta.SelectProbeA); probeControl.WriteConfiguration(ProbeConfigurationA); ConfigureProbeStreaming(probeControl); @@ -98,7 +148,8 @@ public override IObservable Process(IObservable source // configure probe B streaming if (probeAMetadata.ProbeSerialNumber != null) { - gainCorrectionB = ReadGainCorrection(GainCalibrationFileB, (ulong)probeBMetadata.ProbeSerialNumber); + gainCorrectionB = NeuropixelsV2.ReadGainCorrection( + GainCalibrationFileB, (ulong)probeBMetadata.ProbeSerialNumber, NeuropixelsV2Probe.ProbeB); SelectProbe(serializer, ref gpo32Config, NeuropixelsV2eBeta.SelectProbeB); probeControl.WriteConfiguration(ProbeConfigurationB); ConfigureProbeStreaming(probeControl); @@ -115,15 +166,14 @@ public override IObservable Process(IObservable source SyncProbes(serializer, gpo10Config); var deviceInfo = new NeuropixelsV2eDeviceInfo(context, DeviceType, deviceAddress, gainCorrectionA, gainCorrectionB); - var disposable = DeviceManager.RegisterDevice(deviceName, deviceInfo); var shutdown = Disposable.Create(() => { serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.GPIO10, NeuropixelsV2eBeta.DefaultGPO10Config); serializer.WriteByte((uint)DS90UB9xSerializerI2CRegister.GPIO32, NeuropixelsV2eBeta.DefaultGPO32Config); }); return new CompositeDisposable( - shutdown, - disposable); + DeviceManager.RegisterDevice(deviceName, deviceInfo), + shutdown); }); } @@ -165,25 +215,6 @@ static NeuropixelsV2eBetaMetadata ReadProbeMetadata(I2CRegisterContext serialize return new NeuropixelsV2eBetaMetadata(serializer); } - static ushort ReadGainCorrection(string gainCalibrationFile, ulong probeSerialNumber) - { - if (gainCalibrationFile == null) - { - throw new ArgumentException("Calibraiton file must be specified."); - } - - System.IO.StreamReader gainFile = new(gainCalibrationFile); - var sn = ulong.Parse(gainFile.ReadLine()); - - if (probeSerialNumber != sn) - { - throw new ArgumentException($"Probe serial number {probeSerialNumber} does not match calibraiton file serial number {sn}."); - } - - // Q1.14 fixed point conversion - return (ushort)(double.Parse(gainFile.ReadLine()) * (1 << 14)); - } - static void SelectProbe(I2CRegisterContext serializer, ref uint gpo32Config, byte probeSelect) { gpo32Config = probeSelect switch @@ -208,10 +239,10 @@ static void SyncProbes(I2CRegisterContext serializer, uint gpo10Config) static void ConfigureProbeStreaming(I2CRegisterContext i2cNP) { // Activate recording mode on NP - i2cNP.WriteByte(0, 0b0100_0000); + i2cNP.WriteByte(NeuropixelsV2eBeta.REC_MODE, 0b0100_0000); // Set global ADC settings - i2cNP.WriteByte(3, 0b0000_1000); + i2cNP.WriteByte(NeuropixelsV2eBeta.ADC_CONFIG, 0b0000_1000); } } @@ -235,8 +266,29 @@ static class NeuropixelsV2eBeta public const int CountersPerFrame = 2; public const int FrameWords = 28; - - + // register map + public const int OP_MODE = 0x00; + public const int REC_MODE = 0x01; + public const int CAL_MODE = 0x02; + public const int ADC_CONFIG = 0x03; + public const int TEST_CONFIG1 = 0x04; + public const int TEST_CONFIG2 = 0x05; + public const int TEST_CONFIG3 = 0x06; + public const int TEST_CONFIG4 = 0x07; + public const int TEST_CONFIG5 = 0x08; + public const int STATUS = 0x09; + public const int SYNC2 = 0x0A; + public const int SYNC1 = 0x0B; + public const int SR_CHAIN6 = 0x0C; // Odd channel base config + public const int SR_CHAIN5 = 0x0D; // Even channel base config + public const int SR_CHAIN4 = 0x0E; // Shank 4 + public const int SR_CHAIN3 = 0x0F; // Shank 3 + public const int SR_CHAIN2 = 0x10; // Shank 2 + public const int SR_CHAIN1 = 0x11; // Shank 1 + public const int SR_LENGTH2 = 0x12; + public const int SR_LENGTH1 = 0x13; + public const int PROBE_ID = 0x14; + public const int SOFT_RESET = 0x15; internal class NameConverter : DeviceNameConverter { diff --git a/OpenEphys.Onix1/ConfigureNeuropixelsV2eBetaHeadstage.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBetaHeadstage.cs new file mode 100644 index 00000000..9c3c3b22 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBetaHeadstage.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that configures a NeuropixelsV2eBeta headstage. + /// + [Description("Configures a NeuropixelsV2eBeta headstage.")] + public class ConfigureNeuropixelsV2eBetaHeadstage : MultiDeviceFactory + { + PortName port; + readonly ConfigureNeuropixelsV2eLinkController LinkController = new(); + + /// + /// Initialize a new instance of a class. + /// + public ConfigureNeuropixelsV2eBetaHeadstage() + { + Port = PortName.PortA; + LinkController.HubConfiguration = HubConfiguration.Passthrough; + } + + /// + /// Gets or sets the NeuropixelsV2eBeta configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the NeuropixelsV2eBeta device.")] + public ConfigureNeuropixelsV2eBeta NeuropixelsV2eBeta { get; set; } = new(); + + /// + /// Gets or sets the Bno055 9-axis inertial measurement unit configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the Bno055 device.")] + public ConfigureNeuropixelsV2eBno055 Bno055 { get; set; } = new(); + + /// + /// Gets or sets the port. + /// + /// + /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation. + /// + [Description("Specifies the physical connection of the headstage to the ONIX breakout board.")] + public PortName Port + { + get { return port; } + set + { + port = value; + var offset = (uint)port << 8; + LinkController.DeviceAddress = (uint)port; + NeuropixelsV2eBeta.DeviceAddress = offset + 0; + Bno055.DeviceAddress = offset + 1; + } + } + + /// + /// Gets or sets the port voltage. + /// + /// + /// If a port voltage is defined this will override the automated voltage discovery and applies + /// the specified voltage to the headstage. To enable automated voltage discovery, leave this field + /// empty. Warning: This device requires 3.0V to 5.0V for proper operation. Voltages higher than 5.0V can + /// damage the headstage + /// + [Description("If defined, overrides automated voltage discovery and applies " + + "the specified voltage to the headstage. Warning: this device requires 3.0V to 5.0V " + + "for proper operation. Higher voltages can damage the headstage.")] + public double? PortVoltage + { + get => LinkController.PortVoltage; + set => LinkController.PortVoltage = value; + } + + internal override IEnumerable GetDevices() + { + yield return LinkController; + yield return NeuropixelsV2eBeta; + yield return Bno055; + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBno055.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBno055.cs similarity index 67% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBno055.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV2eBno055.cs index 1019a900..8a678573 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eBno055.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2eBno055.cs @@ -1,19 +1,45 @@ using System; using System.ComponentModel; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class that configures a NeuropixelsV2eBno055 device. + /// + [Description("Configures a NeuropixelsV2eBno055 device.")] public class ConfigureNeuropixelsV2eBno055 : SingleDeviceFactory { + /// + /// Initialize a new instance of a class. + /// public ConfigureNeuropixelsV2eBno055() : base(typeof(NeuropixelsV2eBno055)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, will produce data. If set to false, + /// will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the BNO055 device is enabled.")] public bool Enable { get; set; } = true; + /// + /// Configures a NeuropixelsV2eBno055 device. + /// + /// + /// This will schedule configuration actions to be applied by a node + /// prior to data acquisition. + /// + /// A sequence of that holds all configuration actions. + /// + /// The original sequence with the side effect of an additional configuration action to configure + /// a NeuropixelsV2eBno055 device. + /// public override IObservable Process(IObservable source) { var enable = Enable; diff --git a/OpenEphys.Onix1/ConfigureNeuropixelsV2eHeadstage.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2eHeadstage.cs new file mode 100644 index 00000000..d5a04d59 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2eHeadstage.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that configures a NeuropixelsV2e headstage. + /// + [Description("configures a NeuropixelsV2e headstage.")] + public class ConfigureNeuropixelsV2eHeadstage : MultiDeviceFactory + { + PortName port; + readonly ConfigureNeuropixelsV2eLinkController LinkController = new(); + + /// + /// Initialize a new instance of a class. + /// + public ConfigureNeuropixelsV2eHeadstage() + { + Port = PortName.PortA; + LinkController.HubConfiguration = HubConfiguration.Passthrough; + } + + /// + /// Gets or sets the NeuropixelsV2e configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the NeuropixelsV2e device.")] + public ConfigureNeuropixelsV2e NeuropixelsV2e { get; set; } = new(); + + /// + /// Gets or sets the Bno055 9-axis inertial measurement unit configuration. + /// + [Category(ConfigurationCategory)] + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the Bno055 device.")] + public ConfigureNeuropixelsV2eBno055 Bno055 { get; set; } = new(); + + /// + /// Gets or sets the port. + /// + /// + /// The port is the physical connection to the ONIX breakout board and must be specified prior to operation. + /// + [Description("Specifies the physical connection of the headstage to the ONIX breakout board.")] + public PortName Port + { + get { return port; } + set + { + port = value; + var offset = (uint)port << 8; + LinkController.DeviceAddress = (uint)port; + NeuropixelsV2e.DeviceAddress = offset + 0; + Bno055.DeviceAddress = offset + 1; + } + } + + /// + /// Gets or sets the port voltage. + /// + /// + /// If a port voltage is defined this will override the automated voltage discovery and applies + /// the specified voltage to the headstage. To enable automated voltage discovery, leave this field + /// empty. Warning: This device requires 3.0V to 5.5V for proper operation. Voltages higher than 5.5V can + /// damage the headstage + /// + [Description("If defined, overrides automated voltage discovery and applies " + + "the specified voltage to the headstage. Warning: this device requires 3.0V to 5.5V " + + "for proper operation. Higher voltages can damage the headstage.")] + public double? PortVoltage + { + get => LinkController.PortVoltage; + set => LinkController.PortVoltage = value; + } + + internal override IEnumerable GetDevices() + { + yield return LinkController; + yield return NeuropixelsV2e; + yield return Bno055; + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eLinkController.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV2eLinkController.cs similarity index 97% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eLinkController.cs rename to OpenEphys.Onix1/ConfigureNeuropixelsV2eLinkController.cs index 5f55c800..86cce964 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureNeuropixelsV2eLinkController.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV2eLinkController.cs @@ -1,6 +1,6 @@ using System.Threading; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class ConfigureNeuropixelsV2eLinkController : ConfigureFmcLinkController { diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhd2164.cs b/OpenEphys.Onix1/ConfigureRhd2164.cs similarity index 69% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureRhd2164.cs rename to OpenEphys.Onix1/ConfigureRhd2164.cs index 40ae4d30..3e106e4b 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureRhd2164.cs +++ b/OpenEphys.Onix1/ConfigureRhd2164.cs @@ -1,35 +1,67 @@ using System; using System.ComponentModel; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// A class for configuring an Intan RHD2164 bioamplifier chip. + /// + /// + /// This configuration class can be linked to a instance to stream + /// electrophysiology data from the chip. + /// + [Description("Configures a RHD2164 device.")] public class ConfigureRhd2164 : SingleDeviceFactory { + /// + /// Initializes a new instance of the class. + /// public ConfigureRhd2164() : base(typeof(Rhd2164)) { } + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, a instance that is linked to this configuration will produce data. + /// If set to false, it will not produce data. + /// [Category(ConfigurationCategory)] [Description("Specifies whether the RHD2164 device is enabled.")] public bool Enable { get; set; } = true; + /// + /// Gets or sets the cutoff frequency for the digital (post-ADC) high-pass filter used for amplifier offset removal. + /// [Category(ConfigurationCategory)] - [Description("Specifies the raw ADC output format used for amplifier conversions.")] - public Rhd2164AmplifierDataFormat AmplifierDataFormat { get; set; } - - [Category(ConfigurationCategory)] - [Description("Specifies the cutoff frequency for the DSP high-pass filter used for amplifier offset removal.")] + [Description("Specifies the cutoff frequency for the digital (post-ADC) high-pass filter used for amplifier offset removal.")] public Rhd2164DspCutoff DspCutoff { get; set; } = Rhd2164DspCutoff.Dsp146mHz; + /// + /// Gets or sets the low cutoff frequency of the analog (pre-ADC) bandpass filter. + /// [Category(ConfigurationCategory)] - [Description("Specifies the lower cutoff frequency of the pre-ADC amplifiers.")] + [Description("Specifies the low cutoff frequency of the analog (pre-ADC) bandpass filter.")] public Rhd2164AnalogLowCutoff AnalogLowCutoff { get; set; } = Rhd2164AnalogLowCutoff.Low100mHz; + /// + /// Gets or sets the high cutoff frequency of the analog (pre-ADC) bandpass filter. + /// [Category(ConfigurationCategory)] - [Description("Specifies the upper cutoff frequency of the pre-ADC amplifiers.")] + [Description("Specifies the high cutoff frequency of the analog (pre-ADC) bandpass filter.")] public Rhd2164AnalogHighCutoff AnalogHighCutoff { get; set; } = Rhd2164AnalogHighCutoff.High10000Hz; + /// + /// Configures a RHD2164 device. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a RHD2164 device. public override IObservable Process(IObservable source) { var enable = Enable; @@ -42,9 +74,7 @@ public override IObservable Process(IObservable source var device = context.GetDeviceContext(deviceAddress, DeviceType); var format = device.ReadRegister(Rhd2164.FORMAT); - var amplifierDataFormat = AmplifierDataFormat; - format &= ~(1u << 6); - format |= (uint)amplifierDataFormat << 6; + format &= ~(1u << 6); // hard-code amplifier data format to offset binary var dspCutoff = DspCutoff; if (dspCutoff == Rhd2164DspCutoff.Off) @@ -58,8 +88,8 @@ public override IObservable Process(IObservable source format |= (uint)dspCutoff; } - var highCutoff = Rhd2164Config.AnalogHighCutoffToRegisters[AnalogHighCutoff]; - var lowCutoff = Rhd2164Config.AnalogLowCutoffToRegisters[AnalogLowCutoff]; + var highCutoff = Rhd2164Config.ToHighCutoffToRegisters(AnalogHighCutoff); + var lowCutoff = Rhd2164Config.ToLowCutoffToRegisters(AnalogLowCutoff); var bw0 = device.ReadRegister(Rhd2164.BW0); var bw1 = device.ReadRegister(Rhd2164.BW1); var bw2 = device.ReadRegister(Rhd2164.BW2); diff --git a/OpenEphys.Onix1/ConfigureRhs2116.cs b/OpenEphys.Onix1/ConfigureRhs2116.cs new file mode 100644 index 00000000..2caf721e --- /dev/null +++ b/OpenEphys.Onix1/ConfigureRhs2116.cs @@ -0,0 +1,265 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace OpenEphys.Onix1 +{ + public class ConfigureRhs2116 : SingleDeviceFactory + { + + readonly BehaviorSubject analogLowCutoff = new(Rhs2116AnalogLowCutoff.Low100mHz); + readonly BehaviorSubject analogLowCutoffRecovery = new(Rhs2116AnalogLowCutoff.Low250Hz); + readonly BehaviorSubject analogHighCutoff = new(Rhs2116AnalogHighCutoff.High10000Hz); + readonly BehaviorSubject respectExternalActiveStim = new(true); + readonly BehaviorSubject stimulusSequence = new(new Rhs2116StimulusSequence()); + + public ConfigureRhs2116() + : base(typeof(Rhs2116)) + { + } + + [Category(ConfigurationCategory)] + [Description("Specifies whether the RHS2116 device is enabled.")] + public bool Enable { get; set; } = true; + + //[Category(ConfigurationCategory)] + //[Description("Specifies the raw ADC output format used for amplifier conversions.")] + //public Rhs2116AmplifierDataFormat AmplifierDataFormat { get; set; } + + [Category(ConfigurationCategory)] + [Description("Specifies the cutoff frequency for the DSP high-pass filter used for amplifier offset removal.")] + public Rhs2116DspCutoff DspCutoff { get; set; } = Rhs2116DspCutoff.Off; + + [Category(AcquisitionCategory)] + [Description("Specifies the lower cutoff frequency of the pre-ADC amplifiers.")] + public Rhs2116AnalogLowCutoff AnalogLowCutoff + { + get => analogLowCutoff.Value; + set => analogLowCutoff.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("Specifies the lower cutoff frequency of the pre-ADC amplifiers during stimulus recovery.")] + public Rhs2116AnalogLowCutoff AnalogLowCutoffRecovery + { + get => analogLowCutoffRecovery.Value; + set => analogLowCutoffRecovery.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("Specifies the upper cutoff frequency of the pre-ADC amplifiers.")] + public Rhs2116AnalogHighCutoff AnalogHighCutoff + { + get => analogHighCutoff.Value; + set => analogHighCutoff.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("If true, this device will apply AnalogLowCutoffRecovery " + + "if stimulation occurs via any RHS chip the same headstage or others that are connected" + + "using StimActive pin. If false, this device will apply AnalogLowCutoffRecovery during its" + + "own stimuli.")] + public bool RespectExternalActiveStim + { + get => respectExternalActiveStim.Value; + set => respectExternalActiveStim.OnNext(value); + } + + [Category(AcquisitionCategory)] + [Description("Stimulus sequence.")] + public Rhs2116StimulusSequence StimulusSequence + { + get => stimulusSequence.Value; + set => stimulusSequence.OnNext(value); + } + + public override IObservable Process(IObservable source) + { + var enable = Enable; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + // config register format following RHS2116 datasheet + // https://www.intantech.com/files/Intan_RHS2116_datasheet.pdf + var device = context.GetDeviceContext(deviceAddress, DeviceType); + + var format = device.ReadRegister(Rhs2116.FORMAT); + var dspCutoff = DspCutoff; + if (dspCutoff == Rhs2116DspCutoff.Off) + { + format &= ~(1u << 4); + } + else + { + format |= 1 << 4; + format &= ~0xFu; + format |= (uint)dspCutoff; + } + + device.WriteRegister(Rhs2116.FORMAT, format); // NB: DC data only provided in unsigned. Force amplifier data to use unsigned for consistency + device.WriteRegister(Rhs2116.ENABLE, enable ? 1u : 0); + + return new CompositeDisposable( + DeviceManager.RegisterDevice(deviceName, device, DeviceType), + analogLowCutoff.Subscribe(newValue => + { + var regs = Rhs2116Config.AnalogLowCutoffToRegisters[newValue]; + var reg = regs[2] << 13 | regs[1] << 7 | regs[0]; + device.WriteRegister(Rhs2116.BW2, reg); + }), + analogLowCutoffRecovery.Subscribe(newValue => + { + var regs = Rhs2116Config.AnalogLowCutoffToRegisters[newValue]; + var reg = regs[2] << 13 | regs[1] << 7 | regs[0]; + device.WriteRegister(Rhs2116.BW3, reg); + }), + analogHighCutoff.Subscribe(newValue => + { + var regs = Rhs2116Config.AnalogHighCutoffToRegisters[newValue]; + device.WriteRegister(Rhs2116.BW0, regs[1] << 6 | regs[0]); + device.WriteRegister(Rhs2116.BW1, regs[3] << 6 | regs[2]); + device.WriteRegister(Rhs2116.FASTSETTLESAMPLES, Rhs2116Config.AnalogHighCutoffToFastSettleSamples[newValue]); + }), + stimulusSequence.Subscribe(newValue => + { + // Step size + var reg = Rhs2116Config.StimulatorStepSizeToRegisters[newValue.CurrentStepSize]; + device.WriteRegister(Rhs2116.STEPSZ, reg[2] << 13 | reg[1] << 7 | reg[0]); + + // Anodic amplitudes + // TODO: cache last write and compare? + var a = newValue.AnodicAmplitudes; + for (int i = 0; i < a.Count(); i++) + { + device.WriteRegister(Rhs2116.POS00 + (uint)i, a.ElementAt(i)); + } + + // Cathodic amplitudes + // TODO: cache last write and compare? + var c = newValue.CathodicAmplitudes; + for (int i = 0; i < a.Count(); i++) + { + device.WriteRegister(Rhs2116.NEG00 + (uint)i, c.ElementAt(i)); + } + + // Create delta table and set length + var dt = newValue.DeltaTable; + device.WriteRegister(Rhs2116.NUMDELTAS, (uint)dt.Count); + + // TODO: If we want to do this efficently, we probably need a different data structure on the + // FPGA ram that allows columns to be out of order (e.g. linked list) + uint j = 0; + foreach (var d in dt) + { + uint indexAndTime = j++ << 22 | (d.Key & 0x003FFFFF); + device.WriteRegister(Rhs2116.DELTAIDXTIME, indexAndTime); + device.WriteRegister(Rhs2116.DELTAPOLEN, d.Value); + } + }) + ); + }); + } + } + + static class Rhs2116 + { + public const int ID = 31; + + // constants + public const int AmplifierChannelCount = 16; + public const int StimMemorySlotsAvailable = 1024; + public const double SampleFrequencyHz = 30.1932367151e3; + + // managed registers + public const uint ENABLE = 0x8000; // Enable or disable the data output stream (32767) + public const uint MAXDELTAS = 0x8001; // Maximum number of deltas in the delta table (32769) + public const uint NUMDELTAS = 0x8002; // Number of deltas in the delta table (32770) + public const uint DELTAIDXTIME = 0x8003; // The delta table index and corresponding application delta application time (32771) + public const uint DELTAPOLEN = 0x8004; // The polarity and enable vectors (32772) + public const uint SEQERROR = 0x8005; // Invalid sequence indicator (32773) + public const uint TRIGGER = 0x8006; // Writing 1 to this register will trigger a stimulation sequence for this device (32774) + public const uint FASTSETTLESAMPLES = 0x8007; // Number of round-robbin samples to apply charge balance following the conclusion of a stimulus pulse (32775) + public const uint RESPECTSTIMACTIVE = 0x8008; // Determines when artifact recovery sequence is applied to this chip (32776) + + // unmanaged registers + public const uint BIAS = 0x00; // Supply Sensor and ADC Buffer Bias Current + public const uint FORMAT = 0x01; // ADC Output Format, DSP Offset Removal, and Auxiliary Digital Outputs + public const uint ZCHECK = 0x02; // Impedance Check Control + public const uint DAC = 0x03; // Impedance Check DAC + public const uint BW0 = 0x04; // On-Chip Amplifier Bandwidth Select + public const uint BW1 = 0x05; // On-Chip Amplifier Bandwidth Select + public const uint BW2 = 0x06; // On-Chip Amplifier Bandwidth Select + public const uint BW3 = 0x07; // On-Chip Amplifier Bandwidth Select + public const uint PWR = 0x08; //Individual AC Amplifier Power + + public const uint SETTLE = 0x0a; // Amplifier Fast Settle + + public const uint LOWAB = 0x0c; // Amplifier Lower Cutoff Frequency Select + + public const uint STIMENA = 0x20; // Stimulation Enable A + public const uint STIMENB = 0x21; // Stimulation Enable B + public const uint STEPSZ = 0x22; // Stimulation Current Step Size + public const uint STIMBIAS = 0x23; // Stimulation Bias Voltages + public const uint RECVOLT = 0x24; // Current-Limited Charge Recovery Target Voltage + public const uint RECCUR = 0x25; // Charge Recovery Current Limit + public const uint DCPWR = 0x26; // Individual DC Amplifier Power + + public const uint COMPMON = 0x28; // Compliance Monitor + + public const uint STIMON = 0x2a; // Stimulator On + + public const uint STIMPOL = 0x2c; // Stimulator Polarity + + public const uint RECOV = 0x2e; // Charge Recovery Switch + + public const uint LIMREC = 0x30; // Current-Limited Charge Recovery Enable + + public const uint FAULTDET = 0x32; // Fault Current Detector + + public const uint NEG00 = 0x40; + public const uint NEG01 = 0x41; + public const uint NEG02 = 0x42; + public const uint NEG03 = 0x43; + public const uint NEG04 = 0x44; + public const uint NEG05 = 0x45; + public const uint NEG06 = 0x46; + public const uint NEG07 = 0x47; + public const uint NEG08 = 0x48; + public const uint NEG09 = 0x49; + public const uint NEG010 = 0x4a; + public const uint NEG011 = 0x4b; + public const uint NEG012 = 0x4c; + public const uint NEG013 = 0x4d; + public const uint NEG014 = 0x4e; + public const uint NEG015 = 0x4f; + + public const uint POS00 = 0x60; + public const uint POS01 = 0x61; + public const uint POS02 = 0x62; + public const uint POS03 = 0x63; + public const uint POS04 = 0x64; + public const uint POS05 = 0x65; + public const uint POS06 = 0x66; + public const uint POS07 = 0x67; + public const uint POS08 = 0x68; + public const uint POS09 = 0x69; + public const uint POS010 = 0x6a; + public const uint POS011 = 0x6b; + public const uint POS012 = 0x6c; + public const uint POS013 = 0x6d; + public const uint POS014 = 0x6e; + public const uint POS015 = 0x6f; + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(Rhs2116)) + { + } + } + } +} diff --git a/OpenEphys.Onix1/ConfigureRhs2116Trigger.cs b/OpenEphys.Onix1/ConfigureRhs2116Trigger.cs new file mode 100644 index 00000000..7ab998ef --- /dev/null +++ b/OpenEphys.Onix1/ConfigureRhs2116Trigger.cs @@ -0,0 +1,56 @@ +using System; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + public class ConfigureRhs2116Trigger : SingleDeviceFactory + { + public ConfigureRhs2116Trigger() + : base(typeof(Rhs2116Trigger)) + { + } + + [Category(ConfigurationCategory)] + [Description("Specifies whether the RHS2116 device is enabled.")] + public Rhs2116TriggerSource TriggerSource { get; set; } = Rhs2116TriggerSource.Local; + + public override IObservable Process(IObservable source) + { + var triggerSource = TriggerSource; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(Rhs2116Trigger.TRIGGERSOURCE, (uint)triggerSource); + return DeviceManager.RegisterDevice(deviceName, device, DeviceType); + }); + } + } + + static class Rhs2116Trigger + { + public const int ID = 32; + + // managed registers + public const uint ENABLE = 0; // Writes and reads to ENABLE are ignored without error + public const uint TRIGGERSOURCE = 1; // The LSB is used to determine the trigger source + public const uint TRIGGER = 2; // Writing 0x1 to this register will trigger a stimulation sequence if the TRIGGERSOURCE is set to 0. + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(Rhs2116Trigger)) + { + } + } + } + + public enum Rhs2116TriggerSource + { + [Description("Respect local triggers (e.g. via GPIO or TRIGGER register) and broadcast via sync pin. ")] + Local = 0, + [Description("Receiver. Only resepct triggers received from sync pin")] + External = 1, + } +} diff --git a/OpenEphys.Onix1/ConfigureTS4231V1.cs b/OpenEphys.Onix1/ConfigureTS4231V1.cs new file mode 100644 index 00000000..46603e90 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureTS4231V1.cs @@ -0,0 +1,73 @@ +using System; +using System.ComponentModel; + +namespace OpenEphys.Onix1 +{ + /// + /// A class for configuring an array of Triad Semiconductor TS4231 lighthouse receivers for 3D position tracking using + /// a pair of SteamVR V1 base stations. + /// + /// + /// This configuration class can be linked to a instance to stream 3D position data from + /// light-house receivers when SteamVR V1 base stations have been installed above the arena. + /// + [Description("Configures a TS4231 receiver array.")] + public class ConfigureTS4231V1 : SingleDeviceFactory + { + /// + /// Initializes a new instance of the class. + /// + public ConfigureTS4231V1() + : base(typeof(TS4231V1)) + { + } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, a instance that is linked to this configuration will produce data. If set to false, + /// it will not produce data. + /// + [Category(ConfigurationCategory)] + [Description("Specifies whether the TS4231 device is enabled.")] + public bool Enable { get; set; } = true; + + /// + /// Configures a TS4231 receiver array. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that holds configuration actions. + /// The original sequence modified by adding additional configuration actions required to configure a TS4231 array. + public override IObservable Process(IObservable source) + { + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + return source.ConfigureDevice(context => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + device.WriteRegister(TS4231V1.ENABLE, Enable ? 1u : 0); + return DeviceManager.RegisterDevice(deviceName, device, DeviceType); + }); + } + } + + static class TS4231V1 + { + public const int ID = 25; + + // managed registers + public const uint ENABLE = 0x0; // Enable or disable the data output stream + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(TS4231V1)) + { + } + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ContextHelper.cs b/OpenEphys.Onix1/ContextHelper.cs similarity index 99% rename from OpenEphys.Onix/OpenEphys.Onix/ContextHelper.cs rename to OpenEphys.Onix1/ContextHelper.cs index c8dff0bb..eb394b03 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ContextHelper.cs +++ b/OpenEphys.Onix1/ContextHelper.cs @@ -2,7 +2,7 @@ using System.Reflection; using oni; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { static class ContextHelper { diff --git a/OpenEphys.Onix1/ContextTask.cs b/OpenEphys.Onix1/ContextTask.cs new file mode 100644 index 00000000..9b66b002 --- /dev/null +++ b/OpenEphys.Onix1/ContextTask.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenEphys.Onix1 +{ + /// + /// Encapsulates an and orchestrates interaction with ONI hardware. + /// + /// + /// This class forms the basis for ONI hardware interaction within the library. It manages an . It + /// reads and distributes s using a dedicated acquisition thread. It allows s to + /// be written to devices that accept them. Finally, it exposes information about the underlying ONI hardware such as the device + /// table, clock rates, and block read and write sizes. + /// + public class ContextTask : IDisposable + { + oni.Context ctx; + + /// + /// Maximum amount of frames the reading queue will hold. If the queue fills or the read + /// thread is not performant enough to fill it faster than data is produced, frame reading + /// will throttle, filling host memory instead of user space memory. + /// + const int MaxQueuedFrames = 2_000_000; + + /// + /// Timeout in ms for queue reads. This should not be critical as the read operation will + /// cancel if the task is stopped + /// + const int QueueTimeoutMilliseconds = 200; + + /// + /// In this package most operators are tied in to the RIFFA PCIe backend used by the FMC host. + /// + internal const string DefaultDriver = "riffa"; + internal const int DefaultIndex = 0; + + // NB: Decouple OnNext() from hardware reads + bool disposed; + Task readFrames; + Task distributeFrames; + Task acquisition = Task.CompletedTask; + CancellationTokenSource collectFramesCancellation; + event Func ConfigureHostEvent; + event Func ConfigureLinkEvent; + event Func ConfigureDeviceEvent; + + // FrameReceived observable sequence + readonly Subject frameReceived = new(); + readonly IConnectableObservable> groupedFrames; + + // TODO: These work for RIFFA implementation, but potentially not others!! + readonly object readLock = new(); + readonly object writeLock = new(); + readonly object regLock = new(); + readonly object disposeLock = new(); + + readonly string contextDriver = DefaultDriver; + readonly int contextIndex = DefaultIndex; + + /// + /// Initializes a new instance of the class. + /// + /// A string specifying the device driver used to control hardware. + /// The index of the host interconnect between the ONI controller and host computer. For instance, 0 could + /// correspond to a particular PCIe slot or USB port as enumerated by the operating system and translated by an + /// ONI device driver translator. + /// A value of -1 will attempt to open the default hardware index and is useful if there is only a single ONI controller + /// managed by the specified in the host computer. + internal ContextTask(string driver, int index) + { + groupedFrames = frameReceived.GroupBy(frame => frame.DeviceAddress).Replay(); + groupedFrames.Connect(); + contextDriver = driver; + contextIndex = index; + Initialize(); + } + + private void Initialize() + { + ctx = new oni.Context(contextDriver, contextIndex); + SystemClockHz = ctx.SystemClockHz; + AcquisitionClockHz = ctx.AcquisitionClockHz; + MaxReadFrameSize = ctx.MaxReadFrameSize; + MaxWriteFrameSize = ctx.MaxWriteFrameSize; + DeviceTable = ctx.DeviceTable; + } + + internal void Reset() + { + lock (disposeLock) + lock (regLock) + { + AssertConfigurationContext(); + lock (readLock) + lock (writeLock) + { + ctx?.Dispose(); + Initialize(); + } + } + } + + /// + /// Gets the system clock rate in Hz. + /// + /// + /// This describes the frequency of the clock governing the ONI controller. + /// + public uint SystemClockHz { get; private set; } + + /// + /// Gets the acquisition clock rate in Hz. + /// + /// + /// This describes the frequency of the clock used to drive the ONI controller's acquisition clock which is used + /// to generate the clock counter values in and its derivative types (e.g. , + /// , etc.) + /// + public uint AcquisitionClockHz { get; private set; } + + /// + /// Gets the maximal size of a frame produced by a call to in bytes. + /// + /// + /// This number is the maximum sized frame that can be produced across every device within the device table + /// that generates data. + /// + public uint MaxReadFrameSize { get; private set; } + + /// + /// Gets the maximal size consumed by a call to in bytes. + /// + /// + /// This number is the maximum sized frame that can be consumed across every device within the device table + /// that accepts write data. + /// + public uint MaxWriteFrameSize { get; private set; } + + /// + /// Gets the device table containing the device hierarchy governed by the internal . + /// + /// + /// This dictionary maps a fully-qualified to an instance. + /// + public Dictionary DeviceTable { get; private set; } + + internal IObservable> GroupedFrames => groupedFrames; + + /// + /// Gets the sequence of s produced by a particular device. + /// + /// The fully qualified that will produce the frame sequence. + /// The frame sequence produced by the device at address . + public IObservable GetDeviceFrames(uint deviceAddress) + { + return groupedFrames.Where(deviceFrames => deviceFrames.Key == deviceAddress).Merge(); + } + + void AssertConfigurationContext() + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + + if (!acquisition.IsCompleted) + { + throw new InvalidOperationException("Configuration cannot be changed while acquisition context is running."); + } + } + + // NB: This is where actions that reconfigure the hub state, or otherwise + // change the device table should be executed + internal void ConfigureHost(Func configure) + { + lock (regLock) + { + AssertConfigurationContext(); + ConfigureHostEvent += configure; + } + } + + // NB: This is where actions that calibrate port voltage or otherwise + // check link lock state should be executed + internal void ConfigureLink(Func configure) + { + lock (regLock) + { + AssertConfigurationContext(); + ConfigureLinkEvent += configure; + } + } + + // NB: Actions queued using this method should assume that the device table + // is finalized and cannot be changed + internal void ConfigureDevice(Func configure) + { + lock (regLock) + { + AssertConfigurationContext(); + ConfigureDeviceEvent += configure; + } + } + + private IDisposable ConfigureContext() + { + var hostAction = Interlocked.Exchange(ref ConfigureHostEvent, null); + var linkAction = Interlocked.Exchange(ref ConfigureLinkEvent, null); + var deviceAction = Interlocked.Exchange(ref ConfigureDeviceEvent, null); + var disposable = new StackDisposable(); + ConfigureResources(disposable, hostAction); + ConfigureResources(disposable, linkAction); + ConfigureResources(disposable, deviceAction); + return disposable; + } + + void ConfigureResources(StackDisposable disposable, Func action) + { + if (action != null) + { + var invocationList = action.GetInvocationList(); + try + { + foreach (var selector in invocationList.Cast>()) + { + disposable.Push(selector(this)); + } + } + catch + { + disposable.Dispose(); + throw; + } + finally { Reset(); } + } + } + + internal Task StartAsync(int blockReadSize, int blockWriteSize, CancellationToken cancellationToken = default) + { + lock (disposeLock) + lock (regLock) + { + if (disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + + if (!acquisition.IsCompleted) + throw new InvalidOperationException("Acquisition already running in the current context."); + + // NB: Configure context before starting acquisition + var contextConfiguration = ConfigureContext(); + ctx.BlockReadSize = blockReadSize; + ctx.BlockWriteSize = blockWriteSize; + + // TODO: Stuff related to sync mode is 100% ONIX, not ONI. Therefore, in the long term, + // another place to do this separation might be needed + int address = ctx.HardwareAddress; + int mode = (address & 0x00FF0000) >> 16; + if (mode == 0) // Standalone mode + { + ctx.Start(true); + } + else // If synchronized mode, reset counter independently + { + ctx.ResetFrameClock(); + ctx.Start(false); + } + + collectFramesCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var collectFramesToken = collectFramesCancellation.Token; + var frameQueue = new BlockingCollection(MaxQueuedFrames); + + readFrames = Task.Factory.StartNew(() => + { + try + { + while (!collectFramesToken.IsCancellationRequested) + { + // NB: This is a blocking call and there is no safe way to terminate it + // other than ending the process. For this reason, it is the job of the + // hardware to provide enough data (e.g. through a HeartbeatDevice") for + // this call to return. + oni.Frame frame; + try { frame = ReadFrame(); } + catch (Exception) + { + collectFramesCancellation.Cancel(); + throw; + } + frameQueue.Add(frame, collectFramesToken); + + } + } + catch (OperationCanceledException) + { +#if DEBUG + // NB: If FrameQueue.Add has not been called, frame has ref count 0 when it exits + // while loop context and will be disposed. + Console.WriteLine("Frame collection task has been cancelled by " + this.GetType()); +#endif + }; + }, + collectFramesToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + distributeFrames = Task.Factory.StartNew(() => + { + try + { + while (!collectFramesToken.IsCancellationRequested) + { + if (frameQueue.TryTake(out oni.Frame frame, QueueTimeoutMilliseconds, collectFramesToken)) + { + frameReceived.OnNext(frame); + frame.Dispose(); + } + } + } + catch (OperationCanceledException) + { +#if DEBUG + // NB: If the thread stops no frame has been collected + Console.WriteLine("Frame distribution task has been cancelled by " + this.GetType()); +#endif + } + }, + collectFramesToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + return acquisition = Task.WhenAll(distributeFrames, readFrames).ContinueWith(task => + { + if (readFrames.IsFaulted && readFrames.Exception is AggregateException ex) + { + var error = ex.InnerExceptions.Count == 1 ? ex.InnerExceptions[0] : ex; + frameReceived.OnError(error); + } + + lock (regLock) + { + collectFramesCancellation?.Dispose(); + collectFramesCancellation = null; + + // Clear queue and free memory + while (frameQueue?.Count > 0) + { + var frame = frameQueue.Take(); + frame.Dispose(); + } + frameQueue?.Dispose(); + frameQueue = null; + ctx.Stop(); + + contextConfiguration.Dispose(); + acquisition = Task.CompletedTask; + } + }); + } + } + + #region oni.Context Properties + + /// + /// Gets the data acquisition state. + /// + /// + /// A value of true indicates that data is being acquired by the host computer from the host controller. + /// False indicates that the host computer is not collecting data from the controller and that the controller + /// memory remains cleared. + /// + internal bool Running => ctx.Running; + + internal int HardwareAddress + { + get => ctx.HardwareAddress; + set => ctx.HardwareAddress = value; + } + + /// + /// Gets the number of bytes read by the device driver access to the read channel. + /// + /// + /// This option allows control over a fundamental trade-off between closed-loop response time and overall bandwidth. + /// A minimal value, which is determined by , will provide the lowest response latency, + /// so long as data can be cleared from hardware memory fast enough to prevent buffering. Larger values will reduce system + /// call frequency, increase overall bandwidth, and may improve processing performance for high-bandwidth data sources. + /// The optimal value depends on the host computer and hardware configuration and must be determined via testing (e.g. + /// using ). + /// + public int BlockReadSize => ctx.BlockReadSize; + + /// + /// Gets the number of bytes that are pre-allocated for writing data to hardware. + /// + /// + /// This value determines the amount of memory pre-allocated for calls to , + /// , and . A larger size will reduce + /// the average amount of dynamic memory allocation system calls but increase the cost of each of those calls. The minimum + /// size of this option is determined by . The effect on real-timer performance is not as + /// large as that of . + /// + public int BlockWriteSize => ctx.BlockWriteSize; + + // Port A and Port B each have a bit in PORTFUNC + internal PassthroughState HubState + { + get => (PassthroughState)ctx.GetCustomOption((int)oni.ONIXOption.PORTFUNC); + set => ctx.SetCustomOption((int)oni.ONIXOption.PORTFUNC, (int)value); + } + + // NB: This is for actions that require synchronized register access and might + // be called asynchronously with context dispose + internal void EnsureContext(Action action) + { + lock (disposeLock) + { + if (!disposed) + action(); + } + } + + internal uint ReadRegister(uint deviceAddress, uint registerAddress) + { + lock (regLock) + { + return ctx.ReadRegister(deviceAddress, registerAddress); + } + } + + internal void WriteRegister(uint deviceAddress, uint registerAddress, uint value) + { + lock (regLock) + { + ctx.WriteRegister(deviceAddress, registerAddress, value); + } + } + + private oni.Frame ReadFrame() + { + lock (readLock) + { + return ctx.ReadFrame(); + } + } + + internal void Write(uint deviceAddress, T data) where T : unmanaged + { + lock (writeLock) + { + ctx.Write(deviceAddress, data); + } + } + + internal void Write(uint deviceAddress, T[] data) where T : unmanaged + { + lock (writeLock) + { + ctx.Write(deviceAddress, data); + } + } + + internal void Write(uint deviceAddress, IntPtr data, int dataSize) + { + lock (writeLock) + { + ctx.Write(deviceAddress, data, dataSize); + } + } + + internal oni.Hub GetHub(uint deviceAddress) => ctx.GetHub(deviceAddress); + + internal uint GetPassthroughDeviceAddress(uint deviceAddress) + { + var hubAddress = (deviceAddress & 0xFF00u) >> 8; + if (hubAddress == 0) + { + throw new ArgumentException( + "Device addresses on hub zero cannot be used to create passthrough devices.", + nameof(deviceAddress)); + } + + return hubAddress + 7; + } + + #endregion + + private void DisposeContext() + { + lock (disposeLock) + lock (regLock) + lock (readLock) + lock (writeLock) + { + ctx?.Dispose(); + ctx = null; + } + } + + /// + /// Dispose the and free all resources. + /// + public void Dispose() + { + lock (disposeLock) + lock (regLock) + { + disposed = true; + acquisition.ContinueWith(_ => DisposeContext()); + collectFramesCancellation?.Cancel(); + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/OpenEphys.Onix1/CreateContext.cs b/OpenEphys.Onix1/CreateContext.cs new file mode 100644 index 00000000..87b2ff84 --- /dev/null +++ b/OpenEphys.Onix1/CreateContext.cs @@ -0,0 +1,62 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Reactive.Linq; + +namespace OpenEphys.Onix1 +{ + /// + /// Creates a to orchestrate a single ONI-compliant controller + /// using the specified device driver and host interconnect. + /// + [Description("Creates a ContextTask to orchestrate a single ONI-compliant controller using the specified device driver and host interconnect.")] + [Combinator(MethodName = nameof(Generate))] + [WorkflowElementCategory(ElementCategory.Source)] + public class CreateContext + { + /// + /// Gets or sets a string specifying the device driver used to communicate with hardware. + /// + [Description("Specifies the device driver used to communicate with hardware.")] + public string Driver { get; set; } = ContextTask.DefaultDriver; + + /// + /// Gets or sets the index of the host interconnect between the ONI controller and host computer. + /// + /// + /// For instance, 0 could correspond to a particular PCIe slot or USB port as enumerated by the operating system and translated by an + /// ONI device driver translator. + /// A value of -1 will attempt to open the default index and is useful if there is only a single ONI controller + /// managed by the specified selected in the host computer. + /// + [Description("The index of the host interconnect between the ONI controller and host computer.")] + public int Index { get; set; } = ContextTask.DefaultIndex; + + /// + /// Generates a sequence that creates a new object. + /// + /// + /// A sequence containing a single instance of the class. Cancelling the sequence + /// will dispose of the created context. + /// + public IObservable Generate() + { + return Observable.Create(observer => + { + var driver = Driver; + var index = Index; + var context = new ContextTask(driver, index); + try + { + observer.OnNext(context); + return context; + } + catch + { + context.Dispose(); + throw; + } + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ConfigureDS90UB9x.cs b/OpenEphys.Onix1/DS90UB9x.cs similarity index 73% rename from OpenEphys.Onix/OpenEphys.Onix/ConfigureDS90UB9x.cs rename to OpenEphys.Onix1/DS90UB9x.cs index 67bcae7b..56a2eca0 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ConfigureDS90UB9x.cs +++ b/OpenEphys.Onix1/DS90UB9x.cs @@ -1,33 +1,5 @@ -using System; -using System.ComponentModel; - -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class ConfigureDS90UB9x : SingleDeviceFactory - { - public ConfigureDS90UB9x() - : base(typeof(DS90UB9x)) - { - } - - [Category(ConfigurationCategory)] - [Description("Specifies whether the DS90UB9x raw device is enabled.")] - public bool Enable { get; set; } = true; - - public override IObservable Process(IObservable source) - { - var enable = Enable; - var deviceName = DeviceName; - var deviceAddress = DeviceAddress; - return source.ConfigureDevice(context => - { - var device = context.GetDeviceContext(deviceAddress, DeviceType); - device.WriteRegister(DS90UB9x.ENABLE, enable ? 1u : 0); - return DeviceManager.RegisterDevice(deviceName, device, DeviceType); - }); - } - } - static class DS90UB9x { public const int ID = 24; diff --git a/OpenEphys.Onix1/DataFrame.cs b/OpenEphys.Onix1/DataFrame.cs new file mode 100644 index 00000000..536e5210 --- /dev/null +++ b/OpenEphys.Onix1/DataFrame.cs @@ -0,0 +1,68 @@ +namespace OpenEphys.Onix1 +{ + /// + /// An abstract class for representing objects in way that suits their use in this library. + /// + public abstract class DataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// Acquisition clock count. Generally provided by the underlying value. + internal DataFrame(ulong clock) + { + Clock = clock; + } + + internal DataFrame(ulong clock, ulong hubClock) + : this(clock) + { + HubClock = hubClock; + } + + /// + /// Gets the acquisition clock count. + /// + /// + /// Acquisition clock count that is synchronous for all frames collected within an ONI context created using . + /// The acquisition clock rate is given by . This clock value provides a common, synchronized + /// time base for all data collected with an single ONI context. + /// + public ulong Clock { get; } + + /// + /// Gets the hub clock count. + /// + /// + /// Local, potentially asynchronous, clock count. Aside from the synchronous value, data frames also contain a local clock + /// count produced within the that the data was actually produced within. For instance, a headstage may contain an onboard controller + /// for controlling devices and arbitrating data stream that runs asynchronously from the . This value + /// is therefore the most precise way to compare the sample time of data collected within a given . However, the delay between time of + /// data collection and synchronous time stamping by is very small (sub-microsecond) and this value can therefore + /// be disregarded in most scenarios in favor of . + /// + public ulong HubClock { get; internal set; } + } + + /// + /// An abstract class for representing buffered groups objects in way that suits their use in this library. + /// + public abstract class BufferedDataFrame + { + internal BufferedDataFrame(ulong[] clock, ulong[] hubClock) + { + Clock = clock; + HubClock = hubClock; + } + + /// + /// Gets the buffered array of values. + /// + public ulong[] Clock { get; } + + /// + /// Gets the buffered array of values. + /// + public ulong[] HubClock { get; } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DeviceContext.cs b/OpenEphys.Onix1/DeviceContext.cs similarity index 90% rename from OpenEphys.Onix/OpenEphys.Onix/DeviceContext.cs rename to OpenEphys.Onix1/DeviceContext.cs index 53f24b4a..5c059926 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/DeviceContext.cs +++ b/OpenEphys.Onix1/DeviceContext.cs @@ -1,8 +1,8 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class DeviceContext + internal class DeviceContext { readonly ContextTask _context; readonly oni.Device _device; @@ -19,6 +19,8 @@ public DeviceContext(ContextTask context, oni.Device device) public oni.Device DeviceMetadata => _device; + public oni.Hub Hub => _context.GetHub(_device.Address); + public uint ReadRegister(uint registerAddress) { return _context.ReadRegister(_device.Address, registerAddress); diff --git a/OpenEphys.Onix1/DeviceFactory.cs b/OpenEphys.Onix1/DeviceFactory.cs new file mode 100644 index 00000000..f1bd6bba --- /dev/null +++ b/OpenEphys.Onix1/DeviceFactory.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Provides an abstract base class for all device configuration operators. + /// + /// + /// ONI devices usually require a specific sequence of configuration and parameterization + /// steps before they can be interacted with. The provides + /// a modular abstraction for flexible assembly and sequencing of both single and multi- + /// device configuration. + /// + public abstract class DeviceFactory : Sink + { + internal const string ConfigurationCategory = "Configuration"; + internal const string AcquisitionCategory = "Acquisition"; + + internal abstract IEnumerable GetDevices(); + } + + /// + /// Provides an abstract base class for configuration operators responsible for + /// registering a single device within the internal device manager. + /// + /// + /// ONI devices usually require a specific sequence of configuration and parameterization + /// steps before they can be interacted with. The + /// provides a modular abstraction allowing flexible assembly and sequencing of + /// of all device-specific configuration code. + /// + public abstract class SingleDeviceFactory : DeviceFactory, IDeviceConfiguration + { + internal const string DeviceNameDescription = "The unique device name."; + internal const string DeviceAddressDescription = "The device address."; + + internal SingleDeviceFactory(Type deviceType) + { + DeviceType = deviceType ?? throw new ArgumentNullException(nameof(deviceType)); + } + + /// + /// Gets or sets a unique device name. + /// + /// + /// The device name provides a unique, human-readable identifier that is used to link software + /// elements for configuration, control, and data streaming to hardware. This is often a one-to-one + /// representation of a single , but can also represent abstract ONI device + /// aggregates or virtual devices. + /// + [Description(DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the device address. + /// + /// + /// This address provides a fully-qualified location of a device within the device table. This is often a one-to-one + /// representation of a , but can also represent abstract device addresses. + /// + [Description(DeviceAddressDescription)] + public uint DeviceAddress { get; set; } + + /// + /// Gets or sets the device identity. + /// + /// + /// This type provides a device identity to each device within the device table. This is often a one-to-one + /// representation of a a , but can also represent abstract device identities. + /// + [Browsable(false)] + public Type DeviceType { get; } + + internal override IEnumerable GetDevices() + { + yield return this; + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/DeviceInfo.cs b/OpenEphys.Onix1/DeviceInfo.cs similarity index 97% rename from OpenEphys.Onix/OpenEphys.Onix/DeviceInfo.cs rename to OpenEphys.Onix1/DeviceInfo.cs index 94fc8d74..3879942d 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/DeviceInfo.cs +++ b/OpenEphys.Onix1/DeviceInfo.cs @@ -1,6 +1,6 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { internal class DeviceInfo { diff --git a/OpenEphys.Onix/OpenEphys.Onix/DeviceManager.cs b/OpenEphys.Onix1/DeviceManager.cs similarity index 63% rename from OpenEphys.Onix/OpenEphys.Onix/DeviceManager.cs rename to OpenEphys.Onix1/DeviceManager.cs index 52274ddd..fa478cd8 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/DeviceManager.cs +++ b/OpenEphys.Onix1/DeviceManager.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class DeviceManager { - static readonly Dictionary deviceMap = new(); + static readonly Dictionary deviceMap = new(); static readonly object managerLock = new(); internal static IDisposable RegisterDevice(string name, DeviceContext device, Type deviceType) @@ -18,17 +19,15 @@ internal static IDisposable RegisterDevice(string name, DeviceContext device, Ty internal static IDisposable RegisterDevice(string name, DeviceInfo deviceInfo) { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("A valid device name must be specified.", nameof(name)); + } + lock (managerLock) { - var disposable = ReserveDevice(name); + var disposable = RegisterDevice(name); var subject = disposable.Subject; - if (subject.IsCompleted) - { - throw new ArgumentException( - $"A device with the same name '{name}' has already been configured.", - nameof(name) - ); - } foreach (var entry in deviceMap) { @@ -55,35 +54,53 @@ internal static IDisposable RegisterDevice(string name, DeviceInfo deviceInfo) } } - internal static DeviceDisposable ReserveDevice(string name) + static DeviceDisposable RegisterDevice(string name) { lock (managerLock) { - if (!deviceMap.TryGetValue(name, out var resourceHandle)) + if (deviceMap.ContainsKey(name)) { - var subject = new AsyncSubject(); - var dispose = Disposable.Create(() => - { - subject.Dispose(); - deviceMap.Remove(name); - }); - - resourceHandle.Subject = subject; - resourceHandle.RefCount = new RefCountDisposable(dispose); - deviceMap.Add(name, resourceHandle); - return new DeviceDisposable(subject, resourceHandle.RefCount); + throw new ArgumentException( + $"A device with the same name '{name}' has already been configured.", + nameof(name) + ); } - return new DeviceDisposable( - resourceHandle.Subject, - resourceHandle.RefCount.GetDisposable()); + var subject = new AsyncSubject(); + var dispose = Disposable.Create(() => + { + subject.Dispose(); + deviceMap.Remove(name); + }); + + var deviceDisposable = new DeviceDisposable(subject, dispose); + deviceMap.Add(name, deviceDisposable); + return deviceDisposable; } } - struct ResourceHandle + internal static IObservable GetDevice(string name) { - public AsyncSubject Subject; - public RefCountDisposable RefCount; + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("A valid device name must be specified.", nameof(name)); + } + + return Observable.Create(observer => + { + lock (managerLock) + { + if (!deviceMap.TryGetValue(name, out var deviceDisposable)) + { + throw new ArgumentException( + $"No device with the specified name '{name}' has been configured.", + nameof(name) + ); + } + + return deviceDisposable.Subject.SubscribeSafe(observer); + } + }); } internal sealed class DeviceDisposable : IDisposable diff --git a/OpenEphys.Onix/OpenEphys.Onix/DeviceNameConverter.cs b/OpenEphys.Onix1/DeviceNameConverter.cs similarity index 99% rename from OpenEphys.Onix/OpenEphys.Onix/DeviceNameConverter.cs rename to OpenEphys.Onix1/DeviceNameConverter.cs index 08ec5715..7150f475 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/DeviceNameConverter.cs +++ b/OpenEphys.Onix1/DeviceNameConverter.cs @@ -5,7 +5,7 @@ using Bonsai.Expressions; using Bonsai; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { /// /// Provides a type converter to convert a device name to and from other representations. diff --git a/OpenEphys.Onix1/HarpSyncInputData.cs b/OpenEphys.Onix1/HarpSyncInputData.cs new file mode 100644 index 00000000..6428420d --- /dev/null +++ b/OpenEphys.Onix1/HarpSyncInputData.cs @@ -0,0 +1,38 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that generates a sequence of Harp clock synchronization events produced by + /// the Harp sync input device in the ONIX breakout board. + /// + /// + [Description("Generates a sequence of Harp clock synchronization events produced by the Harp sync input device in the ONIX breakout board.")] + public class HarpSyncInputData : Source + { + /// + [TypeConverter(typeof(HarpSyncInput.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects, each of which contains + /// information about a single Harp clock synchronization event. + /// + /// A sequence of objects. + public override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(HarpSyncInput)); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .Select(frame => new HarpSyncInputDataFrame(frame)); + }); + } + } +} diff --git a/OpenEphys.Onix1/HarpSyncInputDataFrame.cs b/OpenEphys.Onix1/HarpSyncInputDataFrame.cs new file mode 100644 index 00000000..4940d5cc --- /dev/null +++ b/OpenEphys.Onix1/HarpSyncInputDataFrame.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains information about a Harp clock synchronization event. + /// + public class HarpSyncInputDataFrame : DataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// + /// A frame produced by the Harp sync input device of an ONIX breakout board. + /// + public unsafe HarpSyncInputDataFrame(oni.Frame frame) + : base(frame.Clock) + { + var payload = (HarpSyncInputPayload*)frame.Data.ToPointer(); + HubClock = payload->HubClock; + HarpTime = payload->HarpTime; + } + + /// + /// Gets the Harp clock time corresponding to the local acquisition ONIX clock count. + /// + public uint HarpTime { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct HarpSyncInputPayload + { + public ulong HubClock; + public uint HarpTime; + } +} diff --git a/OpenEphys.Onix1/Headstage64ElectricalStimulatorTrigger.cs b/OpenEphys.Onix1/Headstage64ElectricalStimulatorTrigger.cs new file mode 100644 index 00000000..e0cd6894 --- /dev/null +++ b/OpenEphys.Onix1/Headstage64ElectricalStimulatorTrigger.cs @@ -0,0 +1,240 @@ +using System; +using System.ComponentModel; +using System.Drawing.Design; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that controls a headstage-64 onboard electrical stimulus sequencer. + /// + /// + /// This class must be linked to an appropriate configuration, such as a , + /// in order to define and deliver electrical stimulation sequences. + /// + [Description("Controls a headstage-64 onboard electrical stimulus sequencer.")] + public class Headstage64ElectricalStimulatorTrigger: Sink + { + readonly BehaviorSubject enable = new(true); + readonly BehaviorSubject phaseOneCurrent = new(0); + readonly BehaviorSubject interPhaseCurrent = new(0); + readonly BehaviorSubject phaseTwoCurrent = new(0); + readonly BehaviorSubject phaseOneDuration = new(0); + readonly BehaviorSubject interPhaseInterval = new(0); + readonly BehaviorSubject phaseTwoDuration = new(0); + readonly BehaviorSubject interPulseInterval = new(0); + readonly BehaviorSubject burstPulseCount = new(0); + readonly BehaviorSubject interBurstInterval = new(0); + readonly BehaviorSubject trainBurstCount = new(0); + readonly BehaviorSubject triggerDelay = new(0); + readonly BehaviorSubject powerEnable = new(false); + + /// + [TypeConverter(typeof(Headstage64ElectricalStimulator.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, then the electrical stimulator circuit will respect triggers. If set to false, triggers will be ignored. + /// + [Description("Specifies whether the electrical stimulator will respect triggers.")] + public bool Enable + { + get => enable.Value; + set => enable.OnNext(value); + } + + /// + /// Gets or sets the electrical stimulator's power state. + /// + /// + /// If set to true, then the electrical stimulator's ±15V power supplies will be turned on. If set to false, + /// they will be turned off. It may be desirable to power down the electrical stimulator's power supplies outside + /// of stimulation windows to reduce power consumption and electrical noise. This property must be set to true + /// in order for electrical stimuli to be delivered properly. It takes ~10 milliseconds for these supplies to stabilize. + /// + [Description("Stimulator power on/off.")] + public bool PowerEnable + { + get => powerEnable.Value; + set => powerEnable.OnNext(value); + } + + /// + /// Gets or sets a delay from receiving a trigger to the start of stimulus sequence application in μsec + /// + [Description("A delay from receiving a trigger to the start of stimulus sequence application (uSec).")] + [Range(0, uint.MaxValue)] + public uint TriggerDelay + { + get => triggerDelay.Value; + set => triggerDelay.OnNext(value); + } + + + /// + /// Gets or sets the amplitude of the first phase of each pulse in μA. + /// + [Description("Amplitude of the first phase of each pulse (uA).")] + [Range(-Headstage64ElectricalStimulator.AbsMaxMicroAmps, Headstage64ElectricalStimulator.AbsMaxMicroAmps)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + [Precision(3, 1)] + public double PhaseOneCurrent + { + get => phaseOneCurrent.Value; + set => phaseOneCurrent.OnNext(value); + } + + /// + /// Gets or sets the amplitude of the interphase current of each pulse in μA. + /// + [Description("The amplitude of the inter-phase current of each pulse (uA).")] + [Range(-Headstage64ElectricalStimulator.AbsMaxMicroAmps, Headstage64ElectricalStimulator.AbsMaxMicroAmps)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + [Precision(3, 1)] + public double InterPhaseCurrent + { + get => interPhaseCurrent.Value; + set => interPhaseCurrent.OnNext(value); + } + + /// + /// Gets or sets the amplitude of the second phase of each pulse in μA. + /// + [Description("The amplitude of the second phase of each pulse (uA).")] + [Range(-Headstage64ElectricalStimulator.AbsMaxMicroAmps, Headstage64ElectricalStimulator.AbsMaxMicroAmps)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + [Precision(3, 1)] + public double PhaseTwoCurrent + { + get => phaseTwoCurrent.Value; + set => phaseTwoCurrent.OnNext(value); + } + + /// + /// Gets or sets the duration of the first phase of each pulse in μsec. + /// + [Description("The duration of the first phase of each pulse in μsec.")] + [Range(0, uint.MaxValue)] + public uint PhaseOneDuration + { + get => phaseOneDuration.Value; + set => phaseOneDuration.OnNext(value); + } + + /// + /// Gets or sets the duration of the interphase interval of each pulse in μsec. + /// + [Description("The duration of the interphase interval of each pulse (uSec).")] + [Range(0, uint.MaxValue)] + public uint InterPhaseInterval + { + get => interPhaseInterval.Value; + set => interPhaseInterval.OnNext(value); + } + + /// + /// Gets or sets the duration of the second phase of each pulse in μsec. + /// + [Description("The duration of the second phase of each pulse (uSec).")] + [Range(0, uint.MaxValue)] + public uint PhaseTwoDuration + { + get => phaseTwoDuration.Value; + set => phaseTwoDuration.OnNext(value); + } + + /// + /// Gets or sets the duration of the inter-pulse interval within a single burst in μsec. + /// + [Description("The duration of the inter-pulse interval within a single burst (uSec).")] + [Range(0, uint.MaxValue)] + public uint InterPulseInterval + { + get => interPulseInterval.Value; + set => interPulseInterval.OnNext(value); + } + + /// + /// Gets or sets the duration of the inter-burst interval within a stimulus train in μsec. + /// + [Description("The duration of the inter-burst interval within a stimulus train (uSec).")] + [Range(0, uint.MaxValue)] + public uint InterBurstInterval + { + get => interBurstInterval.Value; + set => interBurstInterval.OnNext(value); + } + + /// + /// Gets or sets the number of pulses per burst. + /// + [Description("The number of pulses per burst.")] + [Range(0, uint.MaxValue)] + public uint BurstPulseCount + { + get => burstPulseCount.Value; + set => burstPulseCount.OnNext(value); + } + + /// + /// Gets or sets the number of bursts in a stimulus train. + /// + [Description("The number of bursts in each train.")] + [Range(0, uint.MaxValue)] + public uint TrainBurstCount + { + get => trainBurstCount.Value; + set => trainBurstCount.OnNext(value); + } + + /// + /// Start an electrical stimulus sequence. + /// + /// A sequence of boolean values indicating the start of a stimulus sequence when true. + /// A sequence of boolean values that is identical to + public override IObservable Process(IObservable source) + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var device = deviceInfo.GetDeviceContext(typeof(Headstage64ElectricalStimulator)); + var triggerObserver = Observer.Create( + value => device.WriteRegister(Headstage64ElectricalStimulator.TRIGGER, value ? 1u : 0u), + observer.OnError, + observer.OnCompleted); + + static uint uAToCode(double currentuA) + { + var k = 1 / (2 * Headstage64ElectricalStimulator.AbsMaxMicroAmps / (Math.Pow(2, Headstage64ElectricalStimulator.DacBitDepth) - 1)); // static + return (uint)(k * (currentuA + Headstage64ElectricalStimulator.AbsMaxMicroAmps)); + } + + return new CompositeDisposable( + enable.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.ENABLE, value ? 1u : 0u)), + phaseOneCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT1, uAToCode(value))), + interPhaseCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.RESTCURR, uAToCode(value))), + phaseTwoCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.CURRENT2, uAToCode(value))), + triggerDelay.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.TRAINDELAY, value)), + phaseOneDuration.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.PULSEDUR1, value)), + interPhaseInterval.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.INTERPHASEINTERVAL, value)), + phaseTwoDuration.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.PULSEDUR2, value)), + interPulseInterval.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.INTERPULSEINTERVAL, value)), + interBurstInterval.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.INTERBURSTINTERVAL, value)), + burstPulseCount.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.BURSTCOUNT, value)), + trainBurstCount.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.TRAINCOUNT, value)), + powerEnable.SubscribeSafe(observer, value => device.WriteRegister(Headstage64ElectricalStimulator.POWERON, value ? 1u : 0u)), + source.SubscribeSafe(triggerObserver) + ); + })); + } + } +} diff --git a/OpenEphys.Onix1/Headstage64OpticalStimulatorTrigger.cs b/OpenEphys.Onix1/Headstage64OpticalStimulatorTrigger.cs new file mode 100644 index 00000000..34f6f060 --- /dev/null +++ b/OpenEphys.Onix1/Headstage64OpticalStimulatorTrigger.cs @@ -0,0 +1,270 @@ +using System; +using System.ComponentModel; +using System.Drawing.Design; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that controls a headstage-64 onboard optical stimulus sequencer. + /// + /// + /// This class must be linked to an appropriate configuration, such as a , + /// in order to define and deliver optical stimulation sequences. + /// + [Description("Controls a headstage-64 onboard optical stimulus sequencer.")] + public class Headstage64OpticalStimulatorTrigger : Sink + { + readonly BehaviorSubject enable = new(true); + readonly BehaviorSubject maxCurrent = new(100); + readonly BehaviorSubject channelOneCurrent = new(100); + readonly BehaviorSubject channelTwoCurrent = new(0); + readonly BehaviorSubject pulseDuration = new(5); + readonly BehaviorSubject pulsesPerSecond = new(50); + readonly BehaviorSubject pulsesPerBurst = new(20); + readonly BehaviorSubject interBurstInterval = new(0); + readonly BehaviorSubject burstsPerTrain = new(1); + readonly BehaviorSubject delay = new(0); + + /// + [TypeConverter(typeof(Headstage64OpticalStimulator.NameConverter))] + public string DeviceName { get; set; } + + /// + /// Gets or sets the device enable state. + /// + /// + /// If set to true, then the optical stimulator circuit will respect triggers. If set to false, triggers will be ignored. + /// + [Description("Specifies whether the optical stimulator will respect triggers.")] + public bool Enable + { + get => enable.Value; + set => enable.OnNext(value); + } + + /// + /// Gets or sets a delay from receiving a trigger to the start of stimulus sequence application in msec. + /// + [Description("A delay from receiving a trigger to the start of stimulus sequence application (msec).")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(0.0, 1000.0)] + [Precision(3, 1)] + public double Delay + { + get => delay.Value; + set => delay.OnNext(value); + } + + /// + /// Gets or sets the Maximum current per channel per pulse in mA. + /// + /// + /// This value defines the maximal possible current that can be delivered to each channel. + /// To get different amplitudes for each channel use the and + /// properties. + /// + [Description("Maximum current per channel per pulse (mA). " + + "This value is used by both channels. To get different amplitudes " + + "for each channel use the ChannelOneCurrent and ChannelTwoCurrent properties.")] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + [Range(0, 300)] + [Precision(3, 0)] + public double MaxCurrent + { + get => maxCurrent.Value; + set => maxCurrent.OnNext(value); + } + + /// + /// Gets or sets the percent of that will delivered to channel 1 in each pulse. + /// + [Description("Channel 1 percent of MaxCurrent. If greater than 0, channel 1 will respond to triggers.")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(0, 100)] + [Precision(1, 12.5)] + public double ChannelOneCurrent + { + get => channelOneCurrent.Value; + set => channelOneCurrent.OnNext(value); + } + + /// + /// Gets or sets the percent of that will delivered to channel 2 in each pulse. + /// + [Description("Channel 2 percent of MaxCurrent. If greater than 0, channel 2 will respond to triggers.")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(0, 100)] + [Precision(1, 12.5)] + public double ChannelTwoCurrent + { + get => channelTwoCurrent.Value; + set => channelTwoCurrent.OnNext(value); + } + + /// + /// Gets or sets the duration of each pulse in msec. + /// + [Description("The duration of each pulse (msec).")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(0.001, 1000.0)] + [Precision(3, 1)] + public double PulseDuration + { + get => pulseDuration.Value; + set => pulseDuration.OnNext(value); + } + + /// + /// Gets or sets the pulse period within a burst in msec. + /// + [Description("The pulse period within a burst (msec).")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(0.01, 10000.0)] + [Precision(3, 1)] + public double PulsesPerSecond + { + get => pulsesPerSecond.Value; + set => pulsesPerSecond.OnNext(value); + } + + /// + /// Gets or sets the number of pulses per burst. + /// + [Description("Number of pulses to deliver in a burst.")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(1, int.MaxValue)] + [Precision(0, 1)] + public uint PulsesPerBurst + { + get => pulsesPerBurst.Value; + set => pulsesPerBurst.OnNext(value); + } + + /// + /// Gets or sets the duration of the inter-burst interval within a stimulus train in msec. + /// + [Description("The duration of the inter-burst interval within a stimulus train (msec).")] + [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(0.0, 10000.0)] + [Precision(3, 1)] + public double InterBurstInterval + { + get => interBurstInterval.Value; + set => interBurstInterval.OnNext(value); + } + + /// + /// Gets or sets the number of bursts in a stimulus train. + /// + [Description("Number of bursts to deliver in a train.")] + [Editor(DesignTypes.NumericUpDownEditor, DesignTypes.UITypeEditor)] + [Range(1, int.MaxValue)] + [Precision(0, 1)] + public uint BurstsPerTrain + { + get => burstsPerTrain.Value; + set => burstsPerTrain.OnNext(value); + } + + // TODO: Should this be checked before TRIGGER is written to below and an error thrown if + // DC current is too high? Or, should settings be forced too keep DC current under some value? + /// + /// Gets total direct current required during the application of a burst. + /// + /// + /// This value should be kept below 50 mA to prevent excess head accumulation on the headstage. + /// + [Description("The total direct current required during the application of a burst (mA). Should be less than 50 mA.")] + public double BurstCurrent + { + get + { + return PulsesPerSecond * 0.001 * PulseDuration * MaxCurrent * 0.01 * (ChannelOneCurrent + ChannelTwoCurrent); + } + } + + /// + /// Start an optical stimulus sequence. + /// + /// A sequence of boolean values indicating the start of a stimulus sequence when true. + /// A sequence of boolean values that is identical to + public override IObservable Process(IObservable source) + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var device = deviceInfo.GetDeviceContext(typeof(Headstage64OpticalStimulator)); + var triggerObserver = Observer.Create( + value => device.WriteRegister(Headstage64OpticalStimulator.TRIGGER, value ? 1u : 0u), + observer.OnError, + observer.OnCompleted); + + // NB: fit from Fig. 10 of CAT4016 datasheet + // x = (y/a)^(1/b) + // a = 3.833e+05 + // b = -0.9632 + static uint mAToPotSetting(double currentMa) + { + double R = Math.Pow(currentMa / 3.833e+05, 1 / -0.9632); + double s = 256 * (R - Headstage64OpticalStimulator.MinRheostatResistanceOhms) / Headstage64OpticalStimulator.PotResistanceOhms; + return s > 255 ? 255 : s < 0 ? 0 : (uint)s; + } + + uint currentSourceMask = 0; + static uint percentToPulseMask(int channel, double percent, uint oldMask) + { + uint mask = 0x00000000; + var p = 0.0; + while (p < percent) + { + mask = (mask << 1) | 1; + p += 12.5; + } + + return channel == 0 ? (oldMask & 0x0000FF00) | mask : (mask << 8) | (oldMask & 0x000000FF); + } + + static uint pulseDurationToRegister(double pulseDuration, double pulseHz) + { + var pulsePeriod = 1000.0 / pulseHz; + return pulseDuration > pulsePeriod ? (uint)(1000 * pulsePeriod - 1): (uint)(1000 * pulseDuration); + } + + static uint pulseFrequencyToRegister(double pulseHz, double pulseDuration) + { + var pulsePeriod = 1000.0 / pulseHz; + return pulsePeriod > pulseDuration ? (uint)(1000 * pulsePeriod) : (uint)(1000 * pulseDuration + 1); + } + + return new CompositeDisposable( + enable.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.ENABLE, value ? 1u : 0u)), + maxCurrent.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.MAXCURRENT, mAToPotSetting(value))), + channelOneCurrent.SubscribeSafe(observer, value => + { + currentSourceMask = percentToPulseMask(0, value, currentSourceMask); + device.WriteRegister(Headstage64OpticalStimulator.PULSEMASK, currentSourceMask); + }), + channelTwoCurrent.SubscribeSafe(observer, value => + { + currentSourceMask = percentToPulseMask(1, value, currentSourceMask); + device.WriteRegister(Headstage64OpticalStimulator.PULSEMASK, currentSourceMask); + }), + pulseDuration.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.PULSEDUR, pulseDurationToRegister(value, PulsesPerSecond))), + pulsesPerSecond.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.PULSEPERIOD, pulseFrequencyToRegister(value, PulseDuration))), + pulsesPerBurst.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.BURSTCOUNT, value)), + interBurstInterval.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.IBI, (uint)(1000 * value))), + burstsPerTrain.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.TRAINCOUNT, value)), + delay.SubscribeSafe(observer, value => device.WriteRegister(Headstage64OpticalStimulator.TRAINDELAY, (uint)(1000 * value))), + source.SubscribeSafe(triggerObserver) + ); + })); + } + } +} diff --git a/OpenEphys.Onix1/HeartbeatData.cs b/OpenEphys.Onix1/HeartbeatData.cs new file mode 100644 index 00000000..f4c55227 --- /dev/null +++ b/OpenEphys.Onix1/HeartbeatData.cs @@ -0,0 +1,40 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that produces a sequence of heartbeat data frames. + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream heartbeat data. + /// + [Description("Produces a sequence of heartbeat data frames.")] + public class HeartbeatData : Source + { + /// + [TypeConverter(typeof(Heartbeat.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects, each of which contains period signal from the + /// acquisition system indicating that it is active. + /// + /// A sequence of objects. + public override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(Heartbeat)); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .Select(frame => new HeartbeatDataFrame(frame)); + }); + } + } +} diff --git a/OpenEphys.Onix1/HeartbeatDataFrame.cs b/OpenEphys.Onix1/HeartbeatDataFrame.cs new file mode 100644 index 00000000..9c22a27a --- /dev/null +++ b/OpenEphys.Onix1/HeartbeatDataFrame.cs @@ -0,0 +1,27 @@ +using System.Runtime.InteropServices; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains the time of a single heartbeat. + /// + public class HeartbeatDataFrame : DataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// A data frame produced by a heartbeat device. + public unsafe HeartbeatDataFrame(oni.Frame frame) + : base(frame.Clock) + { + var payload = (HeartbeatPayload*)frame.Data.ToPointer(); + HubClock = payload->HubClock; + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct HeartbeatPayload + { + public ulong HubClock; + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/I2CRegisterContext.cs b/OpenEphys.Onix1/I2CRegisterContext.cs similarity index 98% rename from OpenEphys.Onix/OpenEphys.Onix/I2CRegisterContext.cs rename to OpenEphys.Onix1/I2CRegisterContext.cs index d87f82f7..03075e87 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/I2CRegisterContext.cs +++ b/OpenEphys.Onix1/I2CRegisterContext.cs @@ -1,7 +1,7 @@ using System; using System.Text; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class I2CRegisterContext { diff --git a/OpenEphys.Onix/OpenEphys.Onix/IDeviceConfiguration.cs b/OpenEphys.Onix1/IDeviceConfiguration.cs similarity index 89% rename from OpenEphys.Onix/OpenEphys.Onix/IDeviceConfiguration.cs rename to OpenEphys.Onix1/IDeviceConfiguration.cs index 1e0bf4bf..73082f61 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/IDeviceConfiguration.cs +++ b/OpenEphys.Onix1/IDeviceConfiguration.cs @@ -1,6 +1,6 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { internal interface IDeviceConfiguration { diff --git a/OpenEphys.Onix/OpenEphys.Onix/MatHelper.cs b/OpenEphys.Onix1/MatHelper.cs similarity index 99% rename from OpenEphys.Onix/OpenEphys.Onix/MatHelper.cs rename to OpenEphys.Onix1/MatHelper.cs index 5c6f3d2c..60fe8754 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/MatHelper.cs +++ b/OpenEphys.Onix1/MatHelper.cs @@ -3,7 +3,7 @@ using System.Reactive.Linq; using OpenCV.Net; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { static class MatHelper { diff --git a/OpenEphys.Onix1/MemoryMonitorData.cs b/OpenEphys.Onix1/MemoryMonitorData.cs new file mode 100644 index 00000000..ed674142 --- /dev/null +++ b/OpenEphys.Onix1/MemoryMonitorData.cs @@ -0,0 +1,42 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that produces a sequence of memory usage data frames. + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream data. + /// + [Description("Produces a sequence of memory usage data frames.")] + public class MemoryMonitorData : Source + { + /// + [TypeConverter(typeof(MemoryMonitor.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects, which contains information + /// about the system's low-level first-in, first-out (FIFO) data buffer. + /// + /// A sequence of objects. + public override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(MemoryMonitor)); + var totalMemory = device.ReadRegister(MemoryMonitor.TOTAL_MEM); + + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .Select(frame => new MemoryMonitorDataFrame(frame, totalMemory)); + }); + } + } +} diff --git a/OpenEphys.Onix1/MemoryMonitorDataFrame.cs b/OpenEphys.Onix1/MemoryMonitorDataFrame.cs new file mode 100644 index 00000000..72ae2500 --- /dev/null +++ b/OpenEphys.Onix1/MemoryMonitorDataFrame.cs @@ -0,0 +1,43 @@ +using System.Runtime.InteropServices; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains hardware memory use information. + /// + public class MemoryMonitorDataFrame : DataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// A data frame produced by a memory monitor device. + /// + /// The total amount of memory, in 32-bit words, on the hardware that is available for data buffering. + /// + public unsafe MemoryMonitorDataFrame(oni.Frame frame, uint totalMemory) + : base(frame.Clock) + { + var payload = (MemoryUsagePayload*)frame.Data.ToPointer(); + HubClock = payload->HubClock; + PercentUsed = 100.0 * payload->Usage / totalMemory; + BytesUsed = payload->Usage * 4; + } + + /// + /// Gets the percent of available memory that is currently used. + /// + public double PercentUsed { get; } + + /// + /// Gets the number of bytes that are currently used. + /// + public uint BytesUsed { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct MemoryUsagePayload + { + public ulong HubClock; + public uint Usage; + } +} diff --git a/OpenEphys.Onix1/MultiDeviceFactory.cs b/OpenEphys.Onix1/MultiDeviceFactory.cs new file mode 100644 index 00000000..d7d3b3b8 --- /dev/null +++ b/OpenEphys.Onix1/MultiDeviceFactory.cs @@ -0,0 +1,99 @@ +using System; +using System.ComponentModel; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Provides an abstract base class for configuration operators responsible for + /// registering all devices within a logical group in the internal device manager. + /// + /// + /// + /// This class allows configuration of logical groups of devices that share some common functionality + /// and/or require a specific sequence of interdependent configuration steps prior to acquisition. For + /// instance, devices on a headstage can be combined with a device on the controller + /// that is used to set the port voltage and monitor headstage communication status + /// (e.g. ). Alternatively, devices that share some common functionality + /// from the user's perspective, but share no actual interdependent configuration from the perspective of + /// the hardware, can be grouped for ease of use (e.g. ). + /// + /// + /// These device groups are the most common starting point for configuration + /// of an ONIX system, and the provides a modular abstraction for flexible + /// assembly and sequencing of device groups. + /// + /// + public abstract class MultiDeviceFactory : DeviceFactory, INamedElement + { + const string BaseTypePrefix = "Configure"; + string _name; + + internal MultiDeviceFactory() + { + var baseName = GetType().Name; + var prefixIndex = baseName.IndexOf(BaseTypePrefix); + Name = prefixIndex >= 0 ? baseName.Substring(prefixIndex + BaseTypePrefix.Length) : baseName; + } + + /// + /// Gets or sets a unique device group name. + /// + /// + /// A human-readable identifier that is used as a prefix for + /// the of each device in the the group. + /// + [Description("The unique device group name.")] + public string Name + { + get { return _name; } + set + { + _name = value; + UpdateDeviceNames(); + } + } + + internal string GetFullDeviceName(string deviceName) + { + return !string.IsNullOrEmpty(_name) ? $"{_name}/{deviceName}" : string.Empty; + } + + internal virtual void UpdateDeviceNames() + { + foreach (var device in GetDevices()) + { + device.DeviceName = GetFullDeviceName(device.DeviceType.Name); + } + } + + /// + /// Configure all devices in the device group. + /// + /// + /// This will schedule configuration actions to be applied by a instance + /// prior to data acquisition. + /// + /// A sequence of instances that hold configuration actions. + /// + /// The original sequence modified by adding additional configuration actions required to configure + /// all the devices in the device group. + /// + public override IObservable Process(IObservable source) + { + if (string.IsNullOrEmpty(_name)) + { + throw new InvalidOperationException("A valid device group name must be specified."); + } + + var output = source; + foreach (var device in GetDevices()) + { + output = device.Process(output); + } + + return output; + } + } +} + diff --git a/OpenEphys.Onix1/NeuropixelsV1eAdc.cs b/OpenEphys.Onix1/NeuropixelsV1eAdc.cs new file mode 100644 index 00000000..f0ea1156 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV1eAdc.cs @@ -0,0 +1,41 @@ +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains ADC calibration values for a NeuropixelsV1e device. + /// + public class NeuropixelsV1eAdc + { + /// + /// Neuropixels 1.0 CompP calibration setting + /// + public int CompP { get; set; } = 16; + /// + /// Neuropixels 1.0 CompN calibration setting + /// + public int CompN { get; set; } = 16; + /// + /// Neuropixels 1.0 Slope calibration setting + /// + public int Slope { get; set; } = 0; + /// + /// Neuropixels 1.0 Coarse calibration setting + /// + public int Coarse { get; set; } = 0; + /// + /// Neuropixels 1.0 Fine calibration setting + /// + public int Fine { get; set; } = 0; + /// + /// Neuropixels 1.0 Cfix calibration setting + /// + public int Cfix { get; set; } = 0; + /// + /// Neuropixels 1.0 Offset calibration setting + /// + public int Offset { get; set; } = 0; + /// + /// Neuropixels 1.0 Threshold calibration setting + /// + public int Threshold { get; set; } = 512; + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV1eBno055Data.cs b/OpenEphys.Onix1/NeuropixelsV1eBno055Data.cs new file mode 100644 index 00000000..5f9b3917 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV1eBno055Data.cs @@ -0,0 +1,70 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence of objects from a NeuropixelsV1e headstage. + /// + [Description("Produces a sequence of Bno055DataFrame objects from a NeuropixelsV1e headstage.")] + public class NeuropixelsV1eBno055Data : Source + { + /// + [TypeConverter(typeof(NeuropixelsV1eBno055.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects at approximately 100 Hz. + /// + /// A sequence of objects. + /// + /// This will generate a sequence of objects at approximately 100 Hz. This rate + /// may be limited by the I2C bus. + /// + public override IObservable Generate() + { + // Max of 100 Hz, but limited by I2C bus + var source = Observable.Interval(TimeSpan.FromSeconds(0.01)); + return Generate(source); + } + + /// + /// Generates a sequence of objects. + /// + /// A sequence of objects. + public unsafe IObservable Generate(IObservable source) + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var device = deviceInfo.GetDeviceContext(typeof(NeuropixelsV1eBno055)); + var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); + var i2c = new I2CRegisterContext(passthrough, NeuropixelsV1eBno055.BNO055Address); + + return source.SubscribeSafe(observer, _ => + { + Bno055DataFrame frame = default; + device.Context.EnsureContext(() => + { + var data = i2c.ReadBytes(NeuropixelsV1eBno055.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); + } + }); + })); + } + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV1eData.cs b/OpenEphys.Onix1/NeuropixelsV1eData.cs new file mode 100644 index 00000000..b7513a11 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV1eData.cs @@ -0,0 +1,90 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence of objects from a NeuropixelsV1e headstage. + /// + [Description("Produces a sequence of NeuropixelsV1eDataFrame objects from a NeuropixelsV1e headstage.")] + public class NeuropixelsV1eData : Source + { + /// + [TypeConverter(typeof(NeuropixelsV1e.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + int bufferSize = 36; + + /// + /// Gets or sets the buffer size. + /// + /// + /// Buffer size sets the number of super frames that are buffered before propagating data. + /// A super frame consists of 384 channels from the spike-band and 32 channels from the LFP band. + /// The buffer size must be a multiple of 12. + /// + [Description("Number of super-frames (384 channels from spike band and 32 channels from " + + "LFP band) to buffer before propagating data. Must be a multiple of 12.")] + public int BufferSize + { + get => bufferSize; + set => bufferSize = (int)(Math.Ceiling((double)value / NeuropixelsV1e.FramesPerRoundRobin) * NeuropixelsV1e.FramesPerRoundRobin); + } + + /// + /// Generates a sequence of objects. + /// + /// A sequence of objects. + public unsafe override IObservable Generate() + { + var spikeBufferSize = BufferSize; + var lfpBufferSize = spikeBufferSize / NeuropixelsV1e.FramesPerRoundRobin; + + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var info = (NeuropixelsV1eDeviceInfo)deviceInfo; + var device = info.GetDeviceContext(typeof(NeuropixelsV1e)); + var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); + var probeData = device.Context.GetDeviceFrames(passthrough.Address); + + return Observable.Create(observer => + { + var sampleIndex = 0; + var spikeBuffer = new ushort[NeuropixelsV1e.ChannelCount, spikeBufferSize]; + var lfpBuffer = new ushort[NeuropixelsV1e.ChannelCount, lfpBufferSize]; + var frameCountBuffer = new int[spikeBufferSize * NeuropixelsV1e.FramesPerSuperFrame]; + var hubClockBuffer = new ulong[spikeBufferSize]; + var clockBuffer = new ulong[spikeBufferSize]; + + var frameObserver = Observer.Create( + frame => + { + var payload = (NeuropixelsV1ePayload*)frame.Data.ToPointer(); + NeuropixelsV1eDataFrame.CopyAmplifierBuffer(payload->AmplifierData, frameCountBuffer, spikeBuffer, lfpBuffer, sampleIndex, info.ApGainCorrection, info.LfpGainCorrection, info.AdcThresholds, info.AdcOffsets); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= spikeBufferSize) + { + var spikeData = Mat.FromArray(spikeBuffer); + var lfpData = Mat.FromArray(lfpBuffer); + observer.OnNext(new NeuropixelsV1eDataFrame(clockBuffer, hubClockBuffer, frameCountBuffer, spikeData, lfpData)); + frameCountBuffer = new int[spikeBufferSize * NeuropixelsV1e.FramesPerSuperFrame]; + hubClockBuffer = new ulong[spikeBufferSize]; + clockBuffer = new ulong[spikeBufferSize]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + return probeData.SubscribeSafe(frameObserver); + }); + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eDataFrame.cs b/OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs similarity index 71% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eDataFrame.cs rename to OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs index 9ca2e1cb..2941d4fc 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eDataFrame.cs +++ b/OpenEphys.Onix1/NeuropixelsV1eDataFrame.cs @@ -1,27 +1,55 @@ using System.Runtime.InteropServices; using OpenCV.Net; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class NeuropixelsV1eDataFrame + /// + /// Buffered data from a NeuropixelsV1e device. + /// + public class NeuropixelsV1eDataFrame : BufferedDataFrame { + /// + /// Initializes a new instance of the class. + /// + /// An array of values. + /// An array of hub clock counter values. + /// An array of frame count values. + /// An array of multi-channel spike data as a object. + /// An array of multi-channel LFP data as a object. public NeuropixelsV1eDataFrame(ulong[] clock, ulong[] hubClock, int[] frameCount, Mat spikeData, Mat lfpData) + : base(clock, hubClock) { - Clock = clock; - HubClock = hubClock; FrameCount = frameCount; SpikeData = spikeData; LfpData = lfpData; } - public ulong[] Clock { get; } - - public ulong[] HubClock { get; } - + /// + /// Gets the frame count value array. + /// + /// + /// Frame count is a 20-bit counter on the probe that increments its value for every frame produced. + /// The value ranges from 0 to 1048575 (2^20-1), and should always increment by 1 until it wraps around back to 0. + /// This can be used to detect dropped frames. + /// public int[] FrameCount { get; } + /// + /// Gets the spike-band data as a object. + /// + /// + /// Spike-band data has 384 rows (channels) with columns representing the samples acquired at 30 kHz. Each sample is a + /// 10-bit offset binary encoded as an unsigned short value. + /// public Mat SpikeData { get; } + /// + /// Gets the LFP band data as a object. + /// + /// + /// LFP data has 32 rows (channels) with columns representing the samples acquired at 2.5 kHz. Each sample is a + /// 10-bit offset binary encoded as an unsigned short value. + /// public Mat LfpData { get; } internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, int[] frameCountBuffer, ushort[,] spikeBuffer, ushort[,] lfpBuffer, int index, double apGainCorrection, double lfpGainCorrection, ushort[] thresholds, ushort[] offsets) @@ -38,7 +66,7 @@ internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, int[] fra for (int k = 0; k < NeuropixelsV1e.AdcCount; k++) { var a = amplifierData[adcToFrameIndex[k]]; - lfpBuffer[RawToChannel[k, lfpFrameIndex], lfpBufferIndex] = (ushort)(a > thresholds[k] ? a - offsets[k] : a); + lfpBuffer[RawToChannel[k, lfpFrameIndex], lfpBufferIndex] = (ushort)(lfpGainCorrection * (a > thresholds[k] ? a - offsets[k] : a)); } @@ -51,7 +79,7 @@ internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, int[] fra for (int k = 0; k < NeuropixelsV1e.AdcCount; k++) { var a = amplifierData[adcToFrameIndex[k] + adcDataOffset]; - spikeBuffer[RawToChannel[k, i], index] = (ushort)(a > thresholds[k] ? a - offsets[k] : a); + spikeBuffer[RawToChannel[k, i], index] = (ushort)(apGainCorrection * (a > thresholds[k] ? a - offsets[k] : a)); } frameCountBuffer[frameCountStartIndex + i + 1] = (amplifierData[adcDataOffset + 31] << 10) | (amplifierData[adcDataOffset + 39] << 0); diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eDeviceInfo.cs b/OpenEphys.Onix1/NeuropixelsV1eDeviceInfo.cs similarity index 96% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eDeviceInfo.cs rename to OpenEphys.Onix1/NeuropixelsV1eDeviceInfo.cs index afb00494..d9303449 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eDeviceInfo.cs +++ b/OpenEphys.Onix1/NeuropixelsV1eDeviceInfo.cs @@ -1,6 +1,6 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class NeuropixelsV1eDeviceInfo : DeviceInfo { diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eMetadata.cs b/OpenEphys.Onix1/NeuropixelsV1eMetadata.cs similarity index 98% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eMetadata.cs rename to OpenEphys.Onix1/NeuropixelsV1eMetadata.cs index d55f1a4d..92aa7fc7 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eMetadata.cs +++ b/OpenEphys.Onix1/NeuropixelsV1eMetadata.cs @@ -1,6 +1,6 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class NeuropixelsV1eMetadata { diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eRegisterContext.cs b/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs similarity index 87% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eRegisterContext.cs rename to OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs index 1ed024ad..1ccd9559 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV1eRegisterContext.cs +++ b/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs @@ -1,9 +1,8 @@ using System; using System.Collections; using System.Linq; -using Bonsai; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class NeuropixelsV1eRegisterContext : I2CRegisterContext { @@ -55,7 +54,7 @@ public NeuropixelsV1eRegisterContext(DeviceContext deviceContext, uint i2cAddres var gainCorrections = gainFile.ReadLine().Split(',').Skip(1); if (gainCorrections.Count() != 2 * NumberOfGains) - throw new ArgumentException("Incorrectly formmatted gain correction calibration file."); + throw new ArgumentException("Incorrectly formatted gain correction calibration file."); ApGainCorrection = double.Parse(gainCorrections.ElementAt(Array.IndexOf(Enum.GetValues(typeof(NeuropixelsV1Gain)), apGain))); LfpGainCorrection = double.Parse(gainCorrections.ElementAt(Array.IndexOf(Enum.GetValues(typeof(NeuropixelsV1Gain)), lfpGain) + 8)); @@ -66,7 +65,7 @@ public NeuropixelsV1eRegisterContext(DeviceContext deviceContext, uint i2cAddres var adcCal = adcFile.ReadLine().Split(',').Skip(1); if (adcCal.Count() != NumberOfGains) { - throw new ArgumentException("Incorrectly formmatted ADC calibration file."); + throw new ArgumentException("Incorrectly formatted ADC calibration file."); } Adcs[i] = new NeuropixelsV1eAdc @@ -134,7 +133,7 @@ public NeuropixelsV1eRegisterContext(DeviceContext deviceContext, uint i2cAddres var chanOptsIdx = BaseConfigurationConfigOffset + ((i - configIdx) * 4); - // MSB [Full, standby, LFPGain(3 downto 0), APGain(3 downto0)] LSB + // MSB [Full, standby, LFPGain(3 downto 0), APGain(3 downto 0)] LSB BaseConfigs[configIdx][chanOptsIdx + 0] = ((byte)apGain >> 0 & 0x1) == 1; BaseConfigs[configIdx][chanOptsIdx + 1] = ((byte)apGain >> 1 & 0x1) == 1; @@ -239,15 +238,11 @@ public void InitializeProbe() WriteByte(NeuropixelsV1e.OP_MODE, (uint)NeuropixelsV1OperationRegisterValues.RECORD); } - // TODO: There is an issue getting these SR write sequences to complete correctly. - // We have a suspicion it is due to the nature of the MCLK signal and that this - // headstage needs either a different oscillator with even more drive strength or - // a clock buffer (second might be easiest). public void WriteConfiguration() { - // shank - // NB: no read check because of ASIC bug - var shankBytes = BitArrayToBytes(ShankConfig); + // shank configuration + // NB: no read check because of ASIC bug that is documented in IMEC-API comments + var shankBytes = BitHelper.ToBitReversedBytes(ShankConfig); WriteByte(NeuropixelsV1e.SR_LENGTH1, (uint)shankBytes.Length % 0x100); WriteByte(NeuropixelsV1e.SR_LENGTH2, (uint)shankBytes.Length / 0x100); @@ -257,16 +252,15 @@ public void WriteConfiguration() WriteByte(NeuropixelsV1e.SR_CHAIN1, b); } - // base + // base configuration for (int i = 0; i < BaseConfigs.Length; i++) { var srAddress = i == 0 ? NeuropixelsV1e.SR_CHAIN2 : NeuropixelsV1e.SR_CHAIN3; for (int j = 0; j < 2; j++) { - // TODO: HACK HACK HACK - // If we do not do this, the ShiftRegisterSuccess check below will always fail - // on whatever the second shift register write sequnece regardless of order or + // WONTFIX: Without this reset, the ShiftRegisterSuccess check below will always fail + // on whatever the second shift register write sequence regardless of order or // contents. Could be increased current draw during internal process causes MCLK // to droop and mess up internal state. Or that MCLK is just not good enough to // prevent metastability in some logic in the ASIC that is only entered in between @@ -274,7 +268,7 @@ public void WriteConfiguration() WriteByte(NeuropixelsV1e.SOFT_RESET, 0xFF); WriteByte(NeuropixelsV1e.SOFT_RESET, 0x00); - var baseBytes = BitArrayToBytes(BaseConfigs[i]); + var baseBytes = BitHelper.ToBitReversedBytes(BaseConfigs[i]); WriteByte(NeuropixelsV1e.SR_LENGTH1, (uint)baseBytes.Length % 0x100); WriteByte(NeuropixelsV1e.SR_LENGTH2, (uint)baseBytes.Length / 0x100); @@ -294,32 +288,10 @@ public void WriteConfiguration() public void StartAcquisition() { - // TODO: Hack inside settings.WriteShiftRegisters() above puts probe in reset set that needs to be - // undone here + // WONTFIX: Soft reset inside settings.WriteShiftRegisters() above puts probe in reset set that + // needs to be undone here WriteByte(NeuropixelsV1e.OP_MODE, (uint)NeuropixelsV1OperationRegisterValues.RECORD); WriteByte(NeuropixelsV1e.REC_MOD, (uint)NeuropixelsV1RecordRegisterValues.ACTIVE); } - - - // Bits go into the shift registers MSB first - // This creates a *bit-reversed* byte array from a bit array - private static byte[] BitArrayToBytes(BitArray bits) - { - if (bits.Length == 0) - { - throw new ArgumentException("Shift register data is empty", nameof(bits)); - } - - var bytes = new byte[(bits.Length - 1) / 8 + 1]; - bits.CopyTo(bytes, 0); - - for (int i = 0; i < bytes.Length; i++) - { - // NB: http://graphics.stanford.edu/~seander/bithacks.html - bytes[i] = (byte)((bytes[i] * 0x0202020202ul & 0x010884422010ul) % 1023); - } - - return bytes; - } } } diff --git a/OpenEphys.Onix1/NeuropixelsV2.cs b/OpenEphys.Onix1/NeuropixelsV2.cs new file mode 100644 index 00000000..b6df4189 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections; + +namespace OpenEphys.Onix1 +{ + /// + /// Specifies the probe as A or B. + /// + public enum NeuropixelsV2Probe + { + /// + /// Specifies that this is Probe A. + /// + ProbeA = 0, + /// + /// Specifies that this is Probe B. + /// + ProbeB = 1 + } + + [Flags] + enum NeuropixelsV2Status : uint + { + SR_OK = 1 << 7 // Indicates the SR chain comparison is OK + } + + static class NeuropixelsV2 + { + public const int ChannelCount = 384; + public const int BaseBitsPerChannel = 4; + public const int ElectrodePerShank = 1280; + public const int ReferencePixelCount = 4; + public const int DummyRegisterCount = 4; + public const int RegistersPerShank = ElectrodePerShank + ReferencePixelCount + DummyRegisterCount; + + + internal static BitArray[] GenerateShankBits(NeuropixelsV2QuadShankProbeConfiguration probe) + { + BitArray[] shankBits = + { + new(NeuropixelsV2.RegistersPerShank, false), + new(NeuropixelsV2.RegistersPerShank, false), + new(NeuropixelsV2.RegistersPerShank, false), + new(NeuropixelsV2.RegistersPerShank, false) + }; + + + if (probe.Reference != NeuropixelsV2QuadShankReference.External) + { + // If tip reference is used, activate the tip electrodes + shankBits[(int)probe.Reference - 1][643] = true; + shankBits[(int)probe.Reference - 1][644] = true; + } + else + { + // TODO: is this the right approach or should only those + // connections to external reference on shanks with active + // electrodes be activated? + + // If external electrode is used, activate on each shank + shankBits[0][2] = true; + shankBits[0][1285] = true; + shankBits[1][2] = true; + shankBits[1][1285] = true; + shankBits[2][2] = true; + shankBits[2][1285] = true; + shankBits[3][2] = true; + shankBits[3][1285] = true; + } + + const int PixelOffset = (NeuropixelsV2.ElectrodePerShank - 1) / 2; + const int ReferencePixelOffset = 3; + foreach (var c in probe.ChannelMap) + { + var baseIndex = c.IntraShankElectrodeIndex % 2; + var pixelIndex = c.IntraShankElectrodeIndex / 2; + pixelIndex = baseIndex == 0 + ? pixelIndex + PixelOffset + 2 * ReferencePixelOffset + : PixelOffset - pixelIndex + ReferencePixelOffset; + + shankBits[c.Shank][pixelIndex] = true; + } + + return shankBits; + } + + internal static BitArray[] GenerateBaseBits(NeuropixelsV2QuadShankProbeConfiguration probe) + { + BitArray[] baseBits = + { + new(NeuropixelsV2.ChannelCount * NeuropixelsV2.BaseBitsPerChannel / 2, false), + new(NeuropixelsV2.ChannelCount * NeuropixelsV2.BaseBitsPerChannel / 2, false) + }; + + var referenceBit = probe.Reference switch + { + NeuropixelsV2QuadShankReference.External => 1, + NeuropixelsV2QuadShankReference.Tip1 => 2, + NeuropixelsV2QuadShankReference.Tip2 => 2, + NeuropixelsV2QuadShankReference.Tip3 => 2, + NeuropixelsV2QuadShankReference.Tip4 => 2, + _ => throw new InvalidOperationException("Invalid reference selection."), + }; + + for (int i = 0; i < NeuropixelsV2.ChannelCount; i++) + { + var configIndex = i % 2; + var bitOffset = (382 - i + configIndex) / 2 * NeuropixelsV2.BaseBitsPerChannel; + baseBits[configIndex][bitOffset + 0] = false; // standby bit + baseBits[configIndex][bitOffset + referenceBit] = true; + } + + return baseBits; + } + + internal static double ReadGainCorrection(string gainCalibrationFile, ulong probeSerialNumber, NeuropixelsV2Probe probe) + { + if (gainCalibrationFile == null) + { + throw new ArgumentException($"A calibration file must be specified for {probe} with serial number " + + $"{probeSerialNumber})"); + } + + System.IO.StreamReader gainFile = new(gainCalibrationFile); + var sn = ulong.Parse(gainFile.ReadLine()); + + if (probeSerialNumber != sn) + { + throw new ArgumentException($"{probe}'s serial number ({probeSerialNumber}) does not " + + $"match the calibration file serial number: {sn}."); + } + + return double.Parse(gainFile.ReadLine()); + } + } +} + diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2QuadShankProbeConfiguration.cs b/OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs similarity index 51% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2QuadShankProbeConfiguration.cs rename to OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs index e4c6c9da..97ba0ebc 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2QuadShankProbeConfiguration.cs +++ b/OpenEphys.Onix1/NeuropixelsV2QuadShankProbeConfiguration.cs @@ -4,28 +4,79 @@ using System.Linq; using System.Xml.Serialization; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { + /// + /// Specifies the reference for a quad-shank probe. + /// public enum NeuropixelsV2QuadShankReference : uint { + /// + /// Specifies that the External reference will be used. + /// External, + /// + /// Specifies that the tip reference of shank 1 will be used. + /// Tip1, + /// + /// Specifies that the tip reference of shank 2 will be used. + /// Tip2, + /// + /// Specifies that the tip reference of shank 3 will be used. + /// Tip3, + /// + /// Specifies that the tip reference of shank 4 will be used. + /// Tip4 } + + /// + /// Specifies the bank of electrodes within each shank. + /// public enum NeuropixelsV2QuadShankBank { + /// + /// Specifies that Bank A is the current bank. + /// + /// Bank A is defined as shank index 0 to 383 along each shank. A, + /// + /// Specifies that Bank B is the current bank. + /// + /// Bank B is defined as shank index 384 to 767 along each shank. B, + /// + /// Specifies that Bank C is the current bank. + /// + /// Bank C is defined as shank index 768 to 1151 along each shank. C, + /// + /// Specifies that Bank D is the current bank. + /// + /// + /// Bank D is defined as shank index 1152 to 1279 along each shank. Note that Bank D is not a full contingent + /// of 384 channels; to compensate for this, electrodes from Bank C (starting at shank index 896) are used to + /// generate a full 384 channel map. + /// D, } + /// + /// Defines a configuration for quad-shank, Neuropixels 2.0 and 2.0-beta probes. + /// public class NeuropixelsV2QuadShankProbeConfiguration { + /// + /// Creates a model of the probe with all electrodes instantiated. + /// public static readonly IReadOnlyList ProbeModel = CreateProbeModel(); + /// + /// Initializes a new instance of the class. + /// public NeuropixelsV2QuadShankProbeConfiguration() { ChannelMap = new List(NeuropixelsV2.ChannelCount); @@ -45,10 +96,30 @@ private static List CreateProbeModel() return electrodes; } + /// + /// Gets or sets the reference for all electrodes. + /// + /// + /// All electrodes are set to the same reference, which can be + /// or any of the tip references + /// (, , etc.). + /// Setting to will use the external reference, while + /// sets the reference to the electrode at the tip of the first shank. + /// public NeuropixelsV2QuadShankReference Reference { get; set; } = NeuropixelsV2QuadShankReference.External; + /// + /// Gets the existing channel map listing all currently enabled electrodes. + /// + /// + /// The channel map will always be 384 channels, and will return the 384 enabled electrodes. + /// public List ChannelMap { get; } + /// + /// Update the with the selected electrodes. + /// + /// List of selected electrodes that are being added to the public void SelectElectrodes(List electrodes) { foreach (var e in electrodes) @@ -58,10 +129,20 @@ public void SelectElectrodes(List electrodes) } } + /// + /// Defines a configuration for quad-shank electrodes. + /// public class NeuropixelsV2QuadShankElectrode { private int electrodeNumber = 0; + /// + /// Gets or sets the electrode number. + /// + /// + /// When the electrode number is updated, all other properties are automatically calculated based on + /// the number given. + /// public int ElectrodeNumber { get => electrodeNumber; @@ -128,14 +209,46 @@ public int ElectrodeNumber } } + /// + /// Gets the channel number of this electrode. + /// + /// + /// Channel number is automatically calculated from the electrode number, and will be between 0 and 383. + /// [XmlIgnore] public int Channel { get; private set; } = 0; + + /// + /// Gets the shank of this electrode. + /// + /// + /// Shank is automatically determined from the electrode number, and will be between 0 and 3. + /// [XmlIgnore] public int Shank { get; private set; } = 0; + + /// + /// Gets the index of the shank of this electrode. + /// + /// + /// Shank index is automatically determined from the electrode number, and will be between 0 and 1279. + /// [XmlIgnore] public int IntraShankElectrodeIndex { get; private set; } = 0; + + /// + /// Gets the of this electrode. + /// + /// + /// The bank is automatically determined from the electrode number, and corresponds to one of four logical + /// groupings along each shank. See for more details. + /// [XmlIgnore] public NeuropixelsV2QuadShankBank Bank { get; private set; } = NeuropixelsV2QuadShankBank.A; + + /// + /// Gets the position of the electrode in relation to the probe. + /// [XmlIgnore] public PointF Position { get; private set; } = new(0f, 0f); } diff --git a/OpenEphys.Onix1/NeuropixelsV2eBetaData.cs b/OpenEphys.Onix1/NeuropixelsV2eBetaData.cs new file mode 100644 index 00000000..5fd61bcf --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2eBetaData.cs @@ -0,0 +1,97 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence of objects from a NeuropixelsV2eBeta headstage. + /// + [Description("Produces a sequence of NeuropixelsV2eDataFrame objects from a NeuropixelsV2e headstage.")] + public class NeuropixelsV2eBetaData : Source + { + /// + [TypeConverter(typeof(NeuropixelsV2eBeta.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the buffer size. + /// + /// + /// Buffer size sets the number of frames that are buffered before propagating data. + /// + [Description("The number of samples collected for each channel that are used to create a single NeuropixelsV2eBetaDataFrame.")] + public int BufferSize { get; set; } = 30; + + /// + /// Gets or sets the probe index. + /// + [Description("The index of the probe from which to collect sample data")] + public NeuropixelsV2Probe ProbeIndex { get; set; } + + /// + /// Generates a sequence of objects. + /// + /// A sequence of objects. + public unsafe override IObservable Generate() + { + var bufferSize = BufferSize; + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var info = (NeuropixelsV2eDeviceInfo)deviceInfo; + var device = info.GetDeviceContext(typeof(NeuropixelsV2eBeta)); + var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); + var probeData = device.Context + .GetDeviceFrames(passthrough.Address) + .Where(frame => NeuropixelsV2eBetaDataFrame.GetProbeIndex(frame) == (int)ProbeIndex); + + var gainCorrection = ProbeIndex switch + { + NeuropixelsV2Probe.ProbeA => (double)info.GainCorrectionA, + NeuropixelsV2Probe.ProbeB => (double)info.GainCorrectionB, + _ => throw new ArgumentOutOfRangeException(nameof(ProbeIndex), $"Unexpected {nameof(ProbeIndex)} value: {ProbeIndex}"), + }; + + return Observable.Create(observer => + { + var sampleIndex = 0; + var amplifierBuffer = new ushort[NeuropixelsV2.ChannelCount, bufferSize]; + var frameCounter = new int[NeuropixelsV2eBeta.FramesPerSuperFrame * bufferSize]; + var hubClockBuffer = new ulong[bufferSize]; + var clockBuffer = new ulong[bufferSize]; + + var frameObserver = Observer.Create( + frame => + { + var payload = (NeuropixelsV2BetaPayload*)frame.Data.ToPointer(); + NeuropixelsV2eBetaDataFrame.CopyAmplifierBuffer(payload->SuperFrame, amplifierBuffer, frameCounter, sampleIndex, gainCorrection); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= bufferSize) + { + var amplifierData = Mat.FromArray(amplifierBuffer); + var dataFrame = new NeuropixelsV2eBetaDataFrame( + clockBuffer, + hubClockBuffer, + amplifierData, + frameCounter); + observer.OnNext(dataFrame); + frameCounter = new int[NeuropixelsV2eBeta.FramesPerSuperFrame * bufferSize]; + hubClockBuffer = new ulong[bufferSize]; + clockBuffer = new ulong[bufferSize]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + return probeData.SubscribeSafe(frameObserver); + }); + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaDataFrame.cs b/OpenEphys.Onix1/NeuropixelsV2eBetaDataFrame.cs similarity index 78% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaDataFrame.cs rename to OpenEphys.Onix1/NeuropixelsV2eBetaDataFrame.cs index c87b240d..e4e35b1a 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaDataFrame.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eBetaDataFrame.cs @@ -1,25 +1,41 @@ using System.Runtime.InteropServices; using OpenCV.Net; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class NeuropixelsV2eBetaDataFrame + /// + /// Buffered data from a NeuropixelsV2e device. + /// + public class NeuropixelsV2eBetaDataFrame : BufferedDataFrame { - public NeuropixelsV2eBetaDataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, int[] frameCounter) + /// + /// Initializes a new instance of the class. + /// + /// An array of values. + /// An array of hub clock counter values. + /// An array of multi-channel amplifier data. + /// An array of frame count values. + public NeuropixelsV2eBetaDataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, int[] frameCount) + : base(clock, hubClock) { - Clock = clock; - HubClock = hubClock; AmplifierData = amplifierData; - FrameCounter = frameCounter; + FrameCount = frameCount; } - public ulong[] Clock { get; } - - public ulong[] HubClock { get; } - + /// + /// Gets the amplifier data array. + /// public Mat AmplifierData { get; } - public int[] FrameCounter { get; } + /// + /// Gets the frame count array. + /// + /// + /// Frame count is a 20-bit counter on the probe that increments its value for every frame produced. + /// The value ranges from 0 to 1048575 (2^20-1), and should always increment by 1 until it wraps around back to 0. + /// This can be used to detect dropped frames. + /// + public int[] FrameCount { get; } internal static unsafe ushort GetProbeIndex(oni.Frame frame) { @@ -27,7 +43,7 @@ internal static unsafe ushort GetProbeIndex(oni.Frame frame) return data->ProbeIndex; } - internal static unsafe void CopyAmplifierBuffer(ushort* superFrame, ushort[,] amplifierBuffer, int[] frameCounter, int index, ushort gainCorrection) + internal static unsafe void CopyAmplifierBuffer(ushort* superFrame, ushort[,] amplifierBuffer, int[] frameCounter, int index, double gainCorrection) { // Loop over 16 "frames" within each "super frame" for (var i = 0; i < NeuropixelsV2eBeta.FramesPerSuperFrame; i++) @@ -42,7 +58,7 @@ internal static unsafe void CopyAmplifierBuffer(ushort* superFrame, ushort[,] am // Loop over ADC samples within each "frame" and map to channel position for (var k = 0; k < NeuropixelsV2eBeta.ADCsPerProbe; k++) { - amplifierBuffer[RawToChannel[k, i], index] = (ushort)(superFrame[adcDataOffset + k] * gainCorrection >> 14); // Q14.0 * Q1.14 -> Q14.0 + amplifierBuffer[RawToChannel[k, i], index] = (ushort)(gainCorrection * superFrame[adcDataOffset + k]); } } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaMetadata.cs b/OpenEphys.Onix1/NeuropixelsV2eBetaMetadata.cs similarity index 98% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaMetadata.cs rename to OpenEphys.Onix1/NeuropixelsV2eBetaMetadata.cs index dc6f2381..a85b044e 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eBetaMetadata.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eBetaMetadata.cs @@ -1,6 +1,6 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class NeuropixelsV2eBetaMetadata { diff --git a/OpenEphys.Onix1/NeuropixelsV2eBetaRegisterContext.cs b/OpenEphys.Onix1/NeuropixelsV2eBetaRegisterContext.cs new file mode 100644 index 00000000..48b30a26 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2eBetaRegisterContext.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; + +namespace OpenEphys.Onix1 +{ + class NeuropixelsV2eBetaRegisterContext : I2CRegisterContext + { + public NeuropixelsV2eBetaRegisterContext(I2CRegisterContext other, uint i2cAddress) + : base(other, i2cAddress) + { + } + + public NeuropixelsV2eBetaRegisterContext(DeviceContext deviceContext, uint i2cAddress) + : base(deviceContext, i2cAddress) + { + } + + public void WriteConfiguration(NeuropixelsV2QuadShankProbeConfiguration probe) + { + var baseBits = NeuropixelsV2.GenerateBaseBits(probe); + WriteShiftRegister(NeuropixelsV2eBeta.SR_CHAIN5, baseBits[0]); + WriteShiftRegister(NeuropixelsV2eBeta.SR_CHAIN6, baseBits[1]); + + var shankBits = NeuropixelsV2.GenerateShankBits(probe); + WriteShiftRegister(NeuropixelsV2eBeta.SR_CHAIN1, shankBits[0]); + WriteShiftRegister(NeuropixelsV2eBeta.SR_CHAIN2, shankBits[1]); + WriteShiftRegister(NeuropixelsV2eBeta.SR_CHAIN3, shankBits[2]); + WriteShiftRegister(NeuropixelsV2eBeta.SR_CHAIN4, shankBits[3]); + } + + + void WriteShiftRegister(uint srAddress, BitArray data) + { + var bytes = BitHelper.ToBitReversedBytes(data); + + var count = 2; + while (count-- > 0) + { + // This allows Base shift registers to get a good STATUS, but does not help shank registers. + WriteByte(NeuropixelsV2eBeta.SOFT_RESET, 0xFF); + WriteByte(NeuropixelsV2eBeta.SOFT_RESET, 0x00); + + WriteByte(NeuropixelsV2eBeta.SR_LENGTH1, (uint)(bytes.Length % 0x100)); + WriteByte(NeuropixelsV2eBeta.SR_LENGTH2, (uint)(bytes.Length / 0x100)); + + foreach (var b in bytes) + { + WriteByte(srAddress, b); + } + } + + if (ReadByte(NeuropixelsV2e.STATUS) != (uint) NeuropixelsV2Status.SR_OK) + { + Console.Error.WriteLine($"Warning: shift register {srAddress:X} status check failed. " + + $"{ShankName(srAddress)} may be damaged."); + } + } + + static string ShankName(uint shiftRegisterAddress) => shiftRegisterAddress switch + { + NeuropixelsV2eBeta.SR_CHAIN1 => "Shank 1", + NeuropixelsV2eBeta.SR_CHAIN2 => "Shank 2", + NeuropixelsV2eBeta.SR_CHAIN3 => "Shank 3", + NeuropixelsV2eBeta.SR_CHAIN4 => "Shank 4", + _ => throw new InvalidOperationException("Shift register address is not valid."), + }; + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV2eBno055Data.cs b/OpenEphys.Onix1/NeuropixelsV2eBno055Data.cs new file mode 100644 index 00000000..9028faf1 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2eBno055Data.cs @@ -0,0 +1,70 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence of objects from a NeuropixelsV2e headstage. + /// + [Description("Produces a sequence of Bno055DataFrame objects from a NeuropixelsV2e headstage.")] + public class NeuropixelsV2eBno055Data : Source + { + /// + [TypeConverter(typeof(NeuropixelsV2eBno055.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects at approximately 100 Hz. + /// + /// A sequence of objects. + /// + /// This will generate a sequence of objects at approximately 100 Hz. This rate + /// may be limited by the I2C bus. + /// + public override IObservable Generate() + { + // Max of 100 Hz, but limited by I2C bus + var source = Observable.Interval(TimeSpan.FromSeconds(0.01)); + return Generate(source); + } + + /// + /// Generates a sequence of objects. + /// + /// A sequence of objects. + public unsafe IObservable Generate(IObservable source) + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var device = deviceInfo.GetDeviceContext(typeof(NeuropixelsV2eBno055)); + var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); + var i2c = new I2CRegisterContext(passthrough, NeuropixelsV2eBno055.BNO055Address); + + return source.SubscribeSafe(observer, _ => + { + Bno055DataFrame frame = default; + device.Context.EnsureContext(() => + { + var data = i2c.ReadBytes(NeuropixelsV2eBno055.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); + } + }); + })); + } + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV2eData.cs b/OpenEphys.Onix1/NeuropixelsV2eData.cs new file mode 100644 index 00000000..2284ce97 --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2eData.cs @@ -0,0 +1,93 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence of objects from a NeuropixelsV2e headstage. + /// + [Description("Produces a sequence of NeuropixelsV2eDataFrame objects from a NeuropixelsV2e headstage.")] + public class NeuropixelsV2eData : Source + { + /// + [TypeConverter(typeof(NeuropixelsV2e.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the buffer size. + /// + /// + /// This property determines the number of super-frames that are buffered before data is propagated. A super-frame consists of 384 + /// channels from the spike-band and 32 channels from the LFP band. If this value is set to 30, then 30 super-frames, along with + /// corresponding clock values, will be collected and packed into each . Because channels are + /// sampled at 30 kHz, this is equivalent to 1 millisecond of data from each channel. + /// + [Description("The number of samples collected for each channel that are used to create a single NeuropixelsV2eDataFrame.")] + public int BufferSize { get; set; } = 30; + + /// + /// Gets or sets the probe index. + /// + [Description("The index of the probe from which to collect sample data")] + public NeuropixelsV2Probe ProbeIndex { get; set; } + + /// + /// Generates a sequence of objects. + /// + /// A sequence of objects. + public unsafe override IObservable Generate() + { + var bufferSize = BufferSize; + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var info = (NeuropixelsV2eDeviceInfo)deviceInfo; + var device = info.GetDeviceContext(typeof(NeuropixelsV2e)); + var passthrough = device.GetPassthroughDeviceContext(typeof(DS90UB9x)); + var probeData = device.Context + .GetDeviceFrames(passthrough.Address) + .Where(frame => NeuropixelsV2eDataFrame.GetProbeIndex(frame) == (int)ProbeIndex); + + var gainCorrection = ProbeIndex switch + { + NeuropixelsV2Probe.ProbeA => (double)info.GainCorrectionA, + NeuropixelsV2Probe.ProbeB => (double)info.GainCorrectionB, + _ => throw new ArgumentOutOfRangeException(nameof(ProbeIndex), $"Unexpected {nameof(ProbeIndex)} value: {ProbeIndex}"), + }; + + return Observable.Create(observer => + { + var sampleIndex = 0; + var amplifierBuffer = new ushort[NeuropixelsV2e.ChannelCount, bufferSize]; + var hubClockBuffer = new ulong[bufferSize]; + var clockBuffer = new ulong[bufferSize]; + + var frameObserver = Observer.Create( + frame => + { + var payload = (NeuropixelsV2Payload*)frame.Data.ToPointer(); + NeuropixelsV2eDataFrame.CopyAmplifierBuffer(payload->AmplifierData, amplifierBuffer, sampleIndex, gainCorrection); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= bufferSize) + { + var amplifierData = Mat.FromArray(amplifierBuffer); + observer.OnNext(new NeuropixelsV2eDataFrame(clockBuffer, hubClockBuffer, amplifierData)); + hubClockBuffer = new ulong[bufferSize]; + clockBuffer = new ulong[bufferSize]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + return probeData.SubscribeSafe(frameObserver); + }); + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eDataFrame.cs b/OpenEphys.Onix1/NeuropixelsV2eDataFrame.cs similarity index 85% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eDataFrame.cs rename to OpenEphys.Onix1/NeuropixelsV2eDataFrame.cs index 0a62749a..00f5c220 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eDataFrame.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eDataFrame.cs @@ -1,21 +1,28 @@ using System.Runtime.InteropServices; using OpenCV.Net; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - public class NeuropixelsV2eDataFrame + /// + /// Buffered data from a NeuropixelsV2e device. + /// + public class NeuropixelsV2eDataFrame : BufferedDataFrame { + /// + /// Initializes a new instance of the class. + /// + /// An array of values. + /// An array of hub clock counter values. + /// An array of multi-channel amplifier data. public NeuropixelsV2eDataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData) + : base(clock, hubClock) { - Clock = clock; - HubClock = hubClock; AmplifierData = amplifierData; } - public ulong[] Clock { get; } - - public ulong[] HubClock { get; } - + /// + /// Gets the amplifier data array. + /// public Mat AmplifierData { get; } internal static unsafe ushort GetProbeIndex(oni.Frame frame) @@ -24,7 +31,7 @@ internal static unsafe ushort GetProbeIndex(oni.Frame frame) return data->ProbeIndex; } - internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, ushort[,] amplifierBuffer, int index, ushort gainCorrection) + internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, ushort[,] amplifierBuffer, int index, double gainCorrection) { // Loop over 16 "frames" within each "super-frame" for (int i = 0; i < NeuropixelsV2e.FramesPerSuperFrame; i++) @@ -34,7 +41,7 @@ internal static unsafe void CopyAmplifierBuffer(ushort* amplifierData, ushort[,] for (int k = 0; k < NeuropixelsV2e.ADCsPerProbe; k++) { - amplifierBuffer[RawToChannel[k, i], index] = (ushort)(amplifierData[ADCIndices[k] + adcDataOffset] * gainCorrection >> 14); // Q14.0 * Q1.14 -> Q14.0 + amplifierBuffer[RawToChannel[k, i], index] = (ushort)(gainCorrection * amplifierData[ADCIndices[k] + adcDataOffset]); } } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eDeviceInfo.cs b/OpenEphys.Onix1/NeuropixelsV2eDeviceInfo.cs similarity index 60% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eDeviceInfo.cs rename to OpenEphys.Onix1/NeuropixelsV2eDeviceInfo.cs index d83997c8..6194fe14 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eDeviceInfo.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eDeviceInfo.cs @@ -1,18 +1,18 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class NeuropixelsV2eDeviceInfo : DeviceInfo { - public NeuropixelsV2eDeviceInfo(ContextTask context, Type deviceType, uint deviceAddress, ushort? gainCorrectionA, ushort? gainCorrectionB) + public NeuropixelsV2eDeviceInfo(ContextTask context, Type deviceType, uint deviceAddress, double? gainCorrectionA, double? gainCorrectionB) : base(context, deviceType, deviceAddress) { GainCorrectionA = gainCorrectionA; GainCorrectionB = gainCorrectionB; } - public ushort? GainCorrectionA { get; } + public double? GainCorrectionA { get; } - public ushort? GainCorrectionB { get; } + public double? GainCorrectionB { get; } } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eMetadata.cs b/OpenEphys.Onix1/NeuropixelsV2eMetadata.cs similarity index 98% rename from OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eMetadata.cs rename to OpenEphys.Onix1/NeuropixelsV2eMetadata.cs index 31588446..d33204ca 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/NeuropixelsV2eMetadata.cs +++ b/OpenEphys.Onix1/NeuropixelsV2eMetadata.cs @@ -1,6 +1,6 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { class NeuropixelsV2eMetadata { diff --git a/OpenEphys.Onix1/NeuropixelsV2eRegisterContext.cs b/OpenEphys.Onix1/NeuropixelsV2eRegisterContext.cs new file mode 100644 index 00000000..19c5b41a --- /dev/null +++ b/OpenEphys.Onix1/NeuropixelsV2eRegisterContext.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; + +namespace OpenEphys.Onix1 +{ + class NeuropixelsV2eRegisterContext : I2CRegisterContext + { + public NeuropixelsV2eRegisterContext(I2CRegisterContext other, uint i2cAddress) + : base(other, i2cAddress) + { + } + + public NeuropixelsV2eRegisterContext(DeviceContext deviceContext, uint i2cAddress) + : base(deviceContext, i2cAddress) + { + } + + public void WriteConfiguration(NeuropixelsV2QuadShankProbeConfiguration probe) + { + var baseBits = NeuropixelsV2.GenerateBaseBits(probe); + WriteShiftRegister(NeuropixelsV2e.SR_CHAIN5, baseBits[0]); + WriteShiftRegister(NeuropixelsV2e.SR_CHAIN6, baseBits[1]); + + var shankBits = NeuropixelsV2.GenerateShankBits(probe); + WriteShiftRegister(NeuropixelsV2e.SR_CHAIN1, shankBits[0]); + WriteShiftRegister(NeuropixelsV2e.SR_CHAIN2, shankBits[1]); + WriteShiftRegister(NeuropixelsV2e.SR_CHAIN3, shankBits[2]); + WriteShiftRegister(NeuropixelsV2e.SR_CHAIN4, shankBits[3]); + } + + void WriteShiftRegister(uint srAddress, BitArray data) + { + var bytes = BitHelper.ToBitReversedBytes(data); + + var count = 2; + while (count-- > 0) + { + // This allows Base shift registers to get a good STATUS + WriteByte(NeuropixelsV2e.SOFT_RESET, 0xFF); + WriteByte(NeuropixelsV2e.SOFT_RESET, 0x00); + + WriteByte(NeuropixelsV2e.SR_LENGTH1, (uint)(bytes.Length % 0x100)); + WriteByte(NeuropixelsV2e.SR_LENGTH2, (uint)(bytes.Length / 0x100)); + + foreach (var b in bytes) + { + WriteByte(srAddress, b); + } + } +; + if (ReadByte(NeuropixelsV2e.STATUS) != (uint)NeuropixelsV2Status.SR_OK) + { + Console.Error.WriteLine($"Warning: shift register {srAddress:X} status check failed. " + + $"{ShankName(srAddress)} may be damaged."); + } + } + + static string ShankName(uint shiftRegisterAddress) => shiftRegisterAddress switch + { + NeuropixelsV2e.SR_CHAIN1 => "Shank 1", + NeuropixelsV2e.SR_CHAIN2 => "Shank 2", + NeuropixelsV2e.SR_CHAIN3 => "Shank 3", + NeuropixelsV2e.SR_CHAIN4 => "Shank 4", + _ => throw new InvalidOperationException("Shift register address is not valid."), + }; + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/ObservableExtensions.cs b/OpenEphys.Onix1/ObservableExtensions.cs similarity index 70% rename from OpenEphys.Onix/OpenEphys.Onix/ObservableExtensions.cs rename to OpenEphys.Onix1/ObservableExtensions.cs index 15a87ba9..440ec082 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/ObservableExtensions.cs +++ b/OpenEphys.Onix1/ObservableExtensions.cs @@ -3,7 +3,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { internal static class ObservableExtensions { @@ -22,6 +22,13 @@ public static IObservable ConfigureDevice(this IObservable context.ConfigureDevice(action), configure); } + public static IObservable ConfigureDevice(this IObservable source, Func, IDisposable> configure) + { + return Observable.Create(observer => source + .ConfigureDevice(context => configure(context, observer)) + .SubscribeSafe(observer)); + } + static IObservable ConfigureContext( this IObservable source, Action> configureContext, @@ -59,5 +66,24 @@ static IObservable ConfigureContext( return source.SubscribeSafe(contextObserver); }); } + + public static IDisposable SubscribeSafe( + this IObservable source, + IObserver observer, + Action onNext) + { + var sourceObserver = Observer.Create( + value => + { + try { onNext(value); } + catch (Exception ex) + { + observer.OnError(ex); + } + }, + observer.OnError, + observer.OnCompleted); + return source.SubscribeSafe(sourceObserver); + } } } diff --git a/OpenEphys.Onix/OpenEphys.Onix/OpenEphys.Onix.csproj b/OpenEphys.Onix1/OpenEphys.Onix1.csproj similarity index 70% rename from OpenEphys.Onix/OpenEphys.Onix/OpenEphys.Onix.csproj rename to OpenEphys.Onix1/OpenEphys.Onix1.csproj index 43be3221..1a42ba9c 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/OpenEphys.Onix.csproj +++ b/OpenEphys.Onix1/OpenEphys.Onix1.csproj @@ -1,18 +1,17 @@  - OpenEphys.Onix + OpenEphys.Onix1 Bonsai library containing interfaces for data acquisition and control of ONIX devices. Bonsai Rx Open Ephys Onix net472 true - 0.1.0 x64 - - + + diff --git a/OpenEphys.Onix/OpenEphys.Onix/PassthroughState.cs b/OpenEphys.Onix1/PassthroughState.cs similarity index 60% rename from OpenEphys.Onix/OpenEphys.Onix/PassthroughState.cs rename to OpenEphys.Onix1/PassthroughState.cs index 3b31222f..e3448ded 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/PassthroughState.cs +++ b/OpenEphys.Onix1/PassthroughState.cs @@ -1,9 +1,9 @@ using System; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { [Flags] - public enum PassthroughState + internal enum PassthroughState { PortA = 1 << 0, PortB = 1 << 2 diff --git a/OpenEphys.Onix/OpenEphys.Onix/Properties/AssemblyInfo.cs b/OpenEphys.Onix1/Properties/AssemblyInfo.cs similarity index 76% rename from OpenEphys.Onix/OpenEphys.Onix/Properties/AssemblyInfo.cs rename to OpenEphys.Onix1/Properties/AssemblyInfo.cs index 24c8aefd..e8cc65f2 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/Properties/AssemblyInfo.cs +++ b/OpenEphys.Onix1/Properties/AssemblyInfo.cs @@ -3,5 +3,5 @@ // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: XmlNamespacePrefix("clr-namespace:OpenEphys.Onix", "onix")] +[assembly: XmlNamespacePrefix("clr-namespace:OpenEphys.Onix1", "onix1")] [assembly: WorkflowNamespaceIcon("")] diff --git a/OpenEphys.Onix/OpenEphys.Onix.Design/Properties/launchSettings.json b/OpenEphys.Onix1/Properties/launchSettings.json similarity index 71% rename from OpenEphys.Onix/OpenEphys.Onix.Design/Properties/launchSettings.json rename to OpenEphys.Onix1/Properties/launchSettings.json index 8e6d143e..e8640d60 100644 --- a/OpenEphys.Onix/OpenEphys.Onix.Design/Properties/launchSettings.json +++ b/OpenEphys.Onix1/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Bonsai": { "commandName": "Executable", - "executablePath": "$(SolutionDir)..\\Bonsai\\Bonsai.exe", + "executablePath": "$(SolutionDir).bonsai/Bonsai.exe", "commandLineArgs": "--lib:$(TargetDir).", "nativeDebugging": true } diff --git a/OpenEphys.Onix1/Rhd2164Config.cs b/OpenEphys.Onix1/Rhd2164Config.cs new file mode 100644 index 00000000..c684d20f --- /dev/null +++ b/OpenEphys.Onix1/Rhd2164Config.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; + +namespace OpenEphys.Onix1 +{ + internal static class Rhd2164Config + { + // Page 26 of RHD2000 datasheet + internal static IReadOnlyList ToLowCutoffToRegisters(Rhd2164AnalogLowCutoff lowCut) => lowCut switch + { + Rhd2164AnalogLowCutoff.Low500Hz => new[] { 13, 0, 0 }, + Rhd2164AnalogLowCutoff.Low300Hz => new[] { 15, 0, 0 }, + Rhd2164AnalogLowCutoff.Low250Hz => new[] { 17, 0, 0 }, + Rhd2164AnalogLowCutoff.Low200Hz => new[] { 18, 0, 0 }, + Rhd2164AnalogLowCutoff.Low150Hz => new[] { 21, 0, 0 }, + Rhd2164AnalogLowCutoff.Low100Hz => new[] { 25, 0, 0 }, + Rhd2164AnalogLowCutoff.Low75Hz => new[] { 28, 0, 0 }, + Rhd2164AnalogLowCutoff.Low50Hz => new[] { 34, 0, 0 }, + Rhd2164AnalogLowCutoff.Low30Hz => new[] { 44, 0, 0 }, + Rhd2164AnalogLowCutoff.Low25Hz => new[] { 48, 0, 0 }, + Rhd2164AnalogLowCutoff.Low20Hz => new[] { 54, 0, 0 }, + Rhd2164AnalogLowCutoff.Low15Hz => new[] { 62, 0, 0 }, + Rhd2164AnalogLowCutoff.Low10Hz => new[] { 5, 1, 0 }, + Rhd2164AnalogLowCutoff.Low7500mHz => new[] { 18, 1, 0 }, + Rhd2164AnalogLowCutoff.Low5000mHz => new[] { 40, 1, 0 }, + Rhd2164AnalogLowCutoff.Low3000mHz => new[] { 20, 2, 0 }, + Rhd2164AnalogLowCutoff.Low2500mHz => new[] { 42, 2, 0 }, + Rhd2164AnalogLowCutoff.Low2000mHz => new[] { 8, 3, 0 }, + Rhd2164AnalogLowCutoff.Low1500mHz => new[] { 9, 4, 0 }, + Rhd2164AnalogLowCutoff.Low1000mHz => new[] { 44, 6, 0 }, + Rhd2164AnalogLowCutoff.Low750mHz => new[] { 49, 9, 0 }, + Rhd2164AnalogLowCutoff.Low500mHz => new[] { 35, 17, 0 }, + Rhd2164AnalogLowCutoff.Low300mHz => new[] { 1, 40, 0 }, + Rhd2164AnalogLowCutoff.Low250mHz => new[] { 56, 54, 0 }, + Rhd2164AnalogLowCutoff.Low100mHz => new[] { 16, 60, 1 }, + _ => throw new ArgumentOutOfRangeException(nameof(lowCut), $"Unsupported low cutoff value : {lowCut}"), + }; + + // Page 25 of RHD2000 datasheet + internal static IReadOnlyList ToHighCutoffToRegisters(Rhd2164AnalogHighCutoff highCut) => highCut switch + { + Rhd2164AnalogHighCutoff.High20000Hz => new[] { 8, 0, 4, 0 }, + Rhd2164AnalogHighCutoff.High15000Hz => new[] { 11, 0, 8, 0 }, + Rhd2164AnalogHighCutoff.High10000Hz => new[] { 17, 0, 16, 0 }, + Rhd2164AnalogHighCutoff.High7500Hz => new[] { 22, 0, 23, 0 }, + Rhd2164AnalogHighCutoff.High5000Hz => new[] { 33, 0, 37, 0 }, + Rhd2164AnalogHighCutoff.High3000Hz => new[] { 3, 1, 13, 1 }, + Rhd2164AnalogHighCutoff.High2500Hz => new[] { 13, 1, 25, 1 }, + Rhd2164AnalogHighCutoff.High2000Hz => new[] { 27, 1, 44, 1 }, + Rhd2164AnalogHighCutoff.High1500Hz => new[] { 1, 2, 23, 2 }, + Rhd2164AnalogHighCutoff.High1000Hz => new[] { 46, 2, 30, 3 }, + Rhd2164AnalogHighCutoff.High750Hz => new[] { 41, 3, 36, 4 }, + Rhd2164AnalogHighCutoff.High500Hz => new[] { 30, 5, 43, 6 }, + Rhd2164AnalogHighCutoff.High300Hz => new[] { 6, 9, 2, 11 }, + Rhd2164AnalogHighCutoff.High250Hz => new[] { 42, 10, 5, 13 }, + Rhd2164AnalogHighCutoff.High200Hz => new[] { 24, 13, 7, 16 }, + Rhd2164AnalogHighCutoff.High150Hz => new[] { 44, 17, 8, 21 }, + Rhd2164AnalogHighCutoff.High100Hz => new[] { 38, 26, 5, 31 }, + _ => throw new ArgumentOutOfRangeException(nameof(highCut), $"Unsupported high cutoff value : {highCut}"), + }; + } + + /// + /// Specifies the lower cutoff frequency of the RHD2164 analog (pre-ADC) bandpass filter. + /// + public enum Rhd2164AnalogLowCutoff + { + /// + /// Specifies 500 Hz. + /// + Low500Hz, + /// + /// Specifies 300 Hz. + /// + Low300Hz, + /// + /// Specifies 250 Hz. + /// + Low250Hz, + /// + /// Specifies 200 Hz. + /// + Low200Hz, + /// + /// Specifies 150 Hz. + /// + Low150Hz, + /// + /// Specifies 100 Hz. + /// + Low100Hz, + /// + /// Specifies 75 Hz. + /// + Low75Hz, + /// + /// Specifies 50 Hz. + /// + Low50Hz, + /// + /// Specifies 30 Hz. + /// + Low30Hz, + /// + /// Specifies 25 Hz. + /// + Low25Hz, + /// + /// Specifies 20 Hz. + /// + Low20Hz, + /// + /// Specifies 15 Hz. + /// + Low15Hz, + /// + /// Specifies 10 Hz. + /// + Low10Hz, + /// + /// Specifies 7.5 Hz. + /// + Low7500mHz, + /// + /// Specifies 5 Hz. + /// + Low5000mHz, + /// + /// Specifies 3 Hz. + /// + Low3000mHz, + /// + /// Specifies 2.5 Hz. + /// + Low2500mHz, + /// + /// Specifies 2 Hz. + /// + Low2000mHz, + /// + /// Specifies 1.5 Hz. + /// + Low1500mHz, + /// + /// Specifies 1 Hz. + /// + Low1000mHz, + /// + /// Specifies 0.75 Hz. + /// + Low750mHz, + /// + /// Specifies 0.5 Hz. + /// + Low500mHz, + /// + /// Specifies 0.3 Hz. + /// + Low300mHz, + /// + /// Specifies 0.25 Hz. + /// + Low250mHz, + /// + /// Specifies 0.1 Hz. + /// + Low100mHz, + } + + /// + /// Specifies the upper cutoff frequency of the RHD2164 analog (pre-ADC) bandpass filter. + /// + public enum Rhd2164AnalogHighCutoff + { + /// + /// Specifies 20 kHz. + /// + High20000Hz, + /// + /// Specifies 15 kHz. + /// + High15000Hz, + /// + /// Specifies 10 kHz. + /// + High10000Hz, + /// + /// Specifies 7.5 kHz. + /// + High7500Hz, + /// + /// Specifies 5 kHz. + /// + High5000Hz, + /// + /// Specifies 3 kHz. + /// + High3000Hz, + /// + /// Specifies 2.5 kHz. + /// + High2500Hz, + /// + /// Specifies 2 kHz. + /// + High2000Hz, + /// + /// Specifies 1.5 kHz. + /// + High1500Hz, + /// + /// Specifies 1 kHz. + /// + High1000Hz, + /// + /// Specifies 750 Hz. + /// + High750Hz, + /// + /// Specifies 500 Hz. + /// + High500Hz, + /// + /// Specifies 300 Hz. + /// + High300Hz, + /// + /// Specifies 250 Hz. + /// + High250Hz, + /// + /// Specifies 200 Hz. + /// + High200Hz, + /// + /// Specifies 150 Hz. + /// + High150Hz, + /// + /// Specifies 100 Hz. + /// + High100Hz, + } + + /// + /// Specifies the cutoff frequency of the RHD2164 digital (post-ADC) high-pass filter. + /// + public enum Rhd2164DspCutoff + { + /// + /// Specifies differences between adjacent samples of each channel (approximate first-order derivative). + /// + Differential = 0, + /// + /// Specifies 3310 Hz. + /// + Dsp3309Hz, + /// + /// Specifies 1370 Hz. + /// + Dsp1374Hz, + /// + /// Specifies 638 Hz. + /// + Dsp638Hz, + /// + /// Specifies 308 Hz. + /// + Dsp308Hz, + /// + /// Specifies 152 Hz. + /// + Dsp152Hz, + /// + /// Specifies 75.2 Hz. + /// + Dsp75Hz, + /// + /// Specifies 37.4 Hz. + /// + Dsp37Hz, + /// + /// Specifies 18.7 Hz. + /// + Dsp19Hz, + /// + /// Specifies 9.34 Hz. + /// + Dsp9336mHz, + /// + /// Specifies 4.67 Hz. + /// + Dsp4665mHz, + /// + /// Specifies 2.33 Hz. + /// + Dsp2332mHz, + /// + /// Specifies 1.17 Hz. + /// + Dsp1166mHz, + /// + /// Specifies 0.583 Hz. + /// + Dsp583mHz, + /// + /// Specifies 0.291 Hz. + /// + Dsp291mHz, + /// + /// Specifies 0.146 Hz. + /// + Dsp146mHz, + /// + /// Specifies that no digital high-pass filtering should be applied. + /// + Off, + } +} diff --git a/OpenEphys.Onix1/Rhd2164Data.cs b/OpenEphys.Onix1/Rhd2164Data.cs new file mode 100644 index 00000000..de6c1686 --- /dev/null +++ b/OpenEphys.Onix1/Rhd2164Data.cs @@ -0,0 +1,82 @@ +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.Onix1 +{ + /// + /// A class that produces a sequence of objects. + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream electrophysiology data. + /// + [Description("produces a sequence of Rhd2164DataFrame objects.")] + public class Rhd2164Data : Source + { + /// + [TypeConverter(typeof(Rhd2164.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the number of samples collected for each channel that are used to create a single . + /// + /// + /// This property determines the number of samples that are buffered for each electrophysiology and auxiliary channel produced by the RHD2164 chip + /// before data is propagated. For instance, if this value is set to 30, then 30 samples, along with corresponding clock values, will be collected + /// from each of the electrophysiology and auxiliary channels and packed into each . Because channels are sampled at + /// 30 kHz, this is equivalent to 1 millisecond of data from each channel. + /// + [Description("The number of samples collected for each channel that are used to create a single Rhd2164DataFrame.")] + public int BufferSize { get; set; } = 30; + + /// + /// Generates a sequence of objects, each of which are a buffered set of multichannel samples an RHD2164 device. + /// + /// A sequence of objects. + public unsafe override IObservable Generate() + { + var bufferSize = BufferSize; + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var sampleIndex = 0; + var device = deviceInfo.GetDeviceContext(typeof(Rhd2164)); + var amplifierBuffer = new short[Rhd2164.AmplifierChannelCount * bufferSize]; + var auxBuffer = new short[Rhd2164.AuxChannelCount * bufferSize]; + var hubClockBuffer = new ulong[bufferSize]; + var clockBuffer = new ulong[bufferSize]; + + var frameObserver = Observer.Create( + frame => + { + var payload = (Rhd2164Payload*)frame.Data.ToPointer(); + Marshal.Copy(new IntPtr(payload->AmplifierData), amplifierBuffer, sampleIndex * Rhd2164.AmplifierChannelCount, Rhd2164.AmplifierChannelCount); + Marshal.Copy(new IntPtr(payload->AuxData), auxBuffer, sampleIndex * Rhd2164.AuxChannelCount, Rhd2164.AuxChannelCount); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= bufferSize) + { + var amplifierData = BufferHelper.CopyTranspose(amplifierBuffer, bufferSize, Rhd2164.AmplifierChannelCount, Depth.U16); + var auxData = BufferHelper.CopyTranspose(auxBuffer, bufferSize, Rhd2164.AuxChannelCount, Depth.U16); + observer.OnNext(new Rhd2164DataFrame(clockBuffer, hubClockBuffer, amplifierData, auxData)); + hubClockBuffer = new ulong[bufferSize]; + clockBuffer = new ulong[bufferSize]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .SubscribeSafe(frameObserver); + })); + } + } +} diff --git a/OpenEphys.Onix1/Rhd2164DataFrame.cs b/OpenEphys.Onix1/Rhd2164DataFrame.cs new file mode 100644 index 00000000..7df74d84 --- /dev/null +++ b/OpenEphys.Onix1/Rhd2164DataFrame.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains electrophysiology data produced by an RHD2164 bioamplifier chip. + /// + public class Rhd2164DataFrame : BufferedDataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// An array of values. + /// An array of hub clock counter values. + /// An array of RHD2164 multi-channel electrophysiology data. + /// An array of RHD2164 auxiliary channel data. + public Rhd2164DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat auxData) + : base(clock, hubClock) + { + AmplifierData = amplifierData; + AuxData = auxData; + } + + /// + /// Gets the buffered electrophysiology data array. + /// + /// + /// Each row corresponds to a channel. Each column corresponds to a sample whose time is indicated by + /// the corresponding element and . + /// + public Mat AmplifierData { get; } + + /// + /// Gets the buffered auxiliary data array. + /// + /// + /// Each row corresponds to a channel. Each column corresponds to a sample whose time is indicated by + /// the corresponding element and . + /// + public Mat AuxData { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct Rhd2164Payload + { + public ulong HubClock; + public fixed ushort AmplifierData[Rhd2164.AmplifierChannelCount]; + public fixed ushort AuxData[Rhd2164.AuxChannelCount]; + } +} diff --git a/OpenEphys.Onix1/Rhs2116Config.cs b/OpenEphys.Onix1/Rhs2116Config.cs new file mode 100644 index 00000000..cc020565 --- /dev/null +++ b/OpenEphys.Onix1/Rhs2116Config.cs @@ -0,0 +1,475 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Xml.Serialization; + +namespace OpenEphys.Onix1 +{ + public static class Rhs2116Config + { + public static readonly IReadOnlyDictionary> AnalogLowCutoffToRegisters = + new Dictionary>() + { + { Rhs2116AnalogLowCutoff.Low1000Hz, new uint[] { 10, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low500Hz, new uint[] { 13, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low300Hz, new uint[] { 15, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low250Hz, new uint[] { 17, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low200Hz, new uint[] { 18, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low150Hz, new uint[] { 21, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low100Hz, new uint[] { 25, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low75Hz, new uint[] { 28, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low50Hz, new uint[] { 34, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low30Hz, new uint[] { 44, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low25Hz, new uint[] { 48, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low20Hz, new uint[] { 54, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low15Hz, new uint[] { 62, 0, 0 } }, + { Rhs2116AnalogLowCutoff.Low10Hz, new uint[] { 5, 1, 0 } }, + { Rhs2116AnalogLowCutoff.Low7500mHz, new uint[] { 18, 1, 0 } }, + { Rhs2116AnalogLowCutoff.Low5000mHz, new uint[] { 40, 1, 0 } }, + { Rhs2116AnalogLowCutoff.Low3090mHz, new uint[] { 20, 2, 0 } }, + { Rhs2116AnalogLowCutoff.Low2500mHz, new uint[] { 42, 2, 0 } }, + { Rhs2116AnalogLowCutoff.Low2000mHz, new uint[] { 8, 3, 0 } }, + { Rhs2116AnalogLowCutoff.Low1500mHz, new uint[] { 9, 4, 0 } }, + { Rhs2116AnalogLowCutoff.Low1000mHz, new uint[] { 44, 6, 0 } }, + { Rhs2116AnalogLowCutoff.Low750mHz, new uint[] { 49, 9, 0 } }, + { Rhs2116AnalogLowCutoff.Low500mHz, new uint[] { 35, 17, 0 } }, + { Rhs2116AnalogLowCutoff.Low300mHz, new uint[] { 1, 40, 0 } }, + { Rhs2116AnalogLowCutoff.Low250mHz, new uint[] { 56, 54, 0 } }, + { Rhs2116AnalogLowCutoff.Low100mHz, new uint[] { 16, 60, 1 } }, + }; + + public static readonly IReadOnlyDictionary> AnalogHighCutoffToRegisters = + new Dictionary>() + { + { Rhs2116AnalogHighCutoff.High20000Hz, new uint[] { 8, 0, 4, 0 } }, + { Rhs2116AnalogHighCutoff.High15000Hz, new uint[] { 11, 0, 8, 0 } }, + { Rhs2116AnalogHighCutoff.High10000Hz, new uint[] { 17, 0, 16, 0 } }, + { Rhs2116AnalogHighCutoff.High7500Hz, new uint[] { 22, 0, 23, 0 } }, + { Rhs2116AnalogHighCutoff.High5000Hz, new uint[] { 33, 0, 37, 0 } }, + { Rhs2116AnalogHighCutoff.High3000Hz, new uint[] { 3, 1, 13, 1 } }, + { Rhs2116AnalogHighCutoff.High2500Hz, new uint[] { 13, 1, 25, 1 } }, + { Rhs2116AnalogHighCutoff.High2000Hz, new uint[] { 27, 1, 44, 1 } }, + { Rhs2116AnalogHighCutoff.High1500Hz, new uint[] { 1, 2, 23, 2 } }, + { Rhs2116AnalogHighCutoff.High1000Hz, new uint[] { 46, 2, 30, 3 } }, + { Rhs2116AnalogHighCutoff.High750Hz, new uint[] { 41, 3, 36, 4 } }, + { Rhs2116AnalogHighCutoff.High500Hz, new uint[] { 30, 5, 43, 6 } }, + { Rhs2116AnalogHighCutoff.High300Hz, new uint[] { 6, 9, 2, 11 } }, + { Rhs2116AnalogHighCutoff.High250Hz, new uint[] { 42, 10, 5, 13 } }, + { Rhs2116AnalogHighCutoff.High200Hz, new uint[] { 24, 13, 7, 16 } }, + { Rhs2116AnalogHighCutoff.High150Hz, new uint[] { 44, 17, 8, 21 } }, + { Rhs2116AnalogHighCutoff.High100Hz, new uint[] { 38, 26, 5, 31 } }, + }; + + public static readonly IReadOnlyDictionary AnalogHighCutoffToFastSettleSamples = + new Dictionary() + { + { Rhs2116AnalogHighCutoff.High20000Hz, 4 }, + { Rhs2116AnalogHighCutoff.High15000Hz, 5 }, + { Rhs2116AnalogHighCutoff.High10000Hz, 8 }, + { Rhs2116AnalogHighCutoff.High7500Hz, 10 }, + { Rhs2116AnalogHighCutoff.High5000Hz, 15 }, + { Rhs2116AnalogHighCutoff.High3000Hz, 25 }, + { Rhs2116AnalogHighCutoff.High2500Hz, 30 }, + { Rhs2116AnalogHighCutoff.High2000Hz, 30 }, + { Rhs2116AnalogHighCutoff.High1500Hz, 30 }, + { Rhs2116AnalogHighCutoff.High1000Hz, 30 }, + { Rhs2116AnalogHighCutoff.High750Hz, 30 }, + { Rhs2116AnalogHighCutoff.High500Hz, 30 }, + { Rhs2116AnalogHighCutoff.High300Hz, 30 }, + { Rhs2116AnalogHighCutoff.High250Hz, 30 }, + { Rhs2116AnalogHighCutoff.High200Hz, 30 }, + { Rhs2116AnalogHighCutoff.High150Hz, 30 }, + { Rhs2116AnalogHighCutoff.High100Hz, 30 }, + }; + + public static readonly IReadOnlyDictionary> StimulatorStepSizeToRegisters = + new Dictionary>() + { + { Rhs2116StepSize.Step10nA, new uint[] { 64, 19, 3 } }, + { Rhs2116StepSize.Step20nA, new uint[] { 40, 40, 1 } }, + { Rhs2116StepSize.Step50nA, new uint[] { 64, 40, 0 } }, + { Rhs2116StepSize.Step100nA, new uint[] { 30, 20, 0 } }, + { Rhs2116StepSize.Step200nA, new uint[] { 25, 10, 0 } }, + { Rhs2116StepSize.Step500nA, new uint[] { 101, 3, 0 } }, + { Rhs2116StepSize.Step1000nA, new uint[] { 98, 1, 0 } }, + { Rhs2116StepSize.Step2000nA, new uint[] { 94, 0, 0 } }, + { Rhs2116StepSize.Step5000nA, new uint[] { 38, 0, 0 } }, + { Rhs2116StepSize.Step10000nA, new uint[] { 15, 0, 0 } }, + }; + } + + public enum Rhs2116AnalogLowCutoff + { + Low1000Hz, + Low500Hz, + Low300Hz, + Low250Hz, + Low200Hz, + Low150Hz, + Low100Hz, + Low75Hz, + Low50Hz, + Low30Hz, + Low25Hz, + Low20Hz, + Low15Hz, + Low10Hz, + Low7500mHz, + Low5000mHz, + Low3090mHz, + Low2500mHz, + Low2000mHz, + Low1500mHz, + Low1000mHz, + Low750mHz, + Low500mHz, + Low300mHz, + Low250mHz, + Low100mHz, + } + + public enum Rhs2116AnalogHighCutoff + { + High20000Hz, + High15000Hz, + High10000Hz, + High7500Hz, + High5000Hz, + High3000Hz, + High2500Hz, + High2000Hz, + High1500Hz, + High1000Hz, + High750Hz, + High500Hz, + High300Hz, + High250Hz, + High200Hz, + High150Hz, + High100Hz, + } + + public enum Rhs2116DspCutoff + { + /// + /// out = samp[n] - samp[n-1] + /// + Differential = 0, + + /// + /// 3309 Hz + /// + Dsp3309Hz, + + /// + /// 1370 Hz + /// + Dsp1374Hz, + + /// + /// 638 Hz + /// + Dsp638Hz, + + /// + /// 308 Hz + /// + Dsp308Hz, + + /// + /// 152 Hz + /// + Dsp152Hz, + + /// + /// 75.2 Hz + /// + Dsp75Hz, + + /// + /// 37.4 Hz + /// + Dsp37Hz, + + /// + /// 18.7 Hz + /// + Dsp19Hz, + + /// + /// 9.34 Hz + /// + Dsp9336mHz, + + /// + /// 4.67 Hz + /// + Dsp4665mHz, + + /// + /// 2.33 Hz + /// + Dsp2332mHz, + + /// + /// 1.17 Hz + /// + Dsp1166mHz, + + /// + /// 0.583 Hz + /// + Dsp583mHz, + + /// + /// 0.291 Hz + /// + Dsp291mHz, + + /// + /// 0.146 Hz + /// + Dsp146mHz, + + /// + /// + /// + Off + } + + public enum Rhs2116StepSize + { + Step10nA, + Step20nA, + Step50nA, + Step100nA, + Step200nA, + Step500nA, + Step1000nA, + Step2000nA, + Step5000nA, + Step10000nA + } + + public class Rhs2116Stimulus + { + [DisplayName("Number of Stimuli")] + public uint NumberOfStimuli { get; set; } = 0; + + [DisplayName("Anodic First")] + public bool AnodicFirst { get; set; } = true; + + [DisplayName("Delay (samples)")] + public uint DelaySamples { get; set; } = 0; + + [DisplayName("Dwell (samples)")] + public uint DwellSamples { get; set; } = 0; + + [DisplayName("Anodic Current (steps)")] + public byte AnodicAmplitudeSteps { get; set; } = 0; + + [DisplayName("Anodic Width (samples)")] + public uint AnodicWidthSamples { get; set; } = 0; + + [DisplayName("Cathodic Current (steps)")] + public byte CathodicAmplitudeSteps { get; set; } = 0; + + [DisplayName("Cathodic Width (samples)")] + public uint CathodicWidthSamples { get; set; } = 0; + + [DisplayName("Inter Stimulus Interval (samples)")] + public uint InterStimulusIntervalSamples { get; set; } = 0; + + [XmlIgnore] + internal bool Valid + { + get + { + return NumberOfStimuli == 0 + ? DelaySamples == 0 && CathodicWidthSamples == 0 && InterStimulusIntervalSamples == 0 && AnodicAmplitudeSteps == 0 && CathodicAmplitudeSteps == 0 + : !(AnodicWidthSamples == 0 && AnodicAmplitudeSteps > 0) + && + !(AnodicWidthSamples > 0 && AnodicAmplitudeSteps == 0) + && + !(CathodicWidthSamples == 0 && CathodicAmplitudeSteps > 0) + && + !(CathodicWidthSamples > 0 && CathodicAmplitudeSteps == 0) + && + // Non-zero anodic or Non-zero cathodic + ((AnodicWidthSamples > 0 && AnodicAmplitudeSteps > 0) || (CathodicWidthSamples > 0 && CathodicAmplitudeSteps > 0)) + && + // Single pulse and possibly 0 ISI or Multiple pulse and positive ISI + ((NumberOfStimuli == 1 && InterStimulusIntervalSamples >= 0) || (NumberOfStimuli > 1 && InterStimulusIntervalSamples > 0)); + + } + } + } + + public class Rhs2116StimulusSequence + { + public Rhs2116StimulusSequence() + { + // TODO: is there a nicer way to initialize this array? + Stimuli = new Rhs2116Stimulus[16]; + for (var i = 0; i < Stimuli.Length; i++) + { + Stimuli[i] = new Rhs2116Stimulus(); + } + } + + public Rhs2116Stimulus[] Stimuli { get; set; } + + // TODO: Should be set automatically to fit the largest required stimulus applitude + public Rhs2116StepSize CurrentStepSize { get; set; } = Rhs2116StepSize.Step5000nA; + + /// + /// Maximum length of the sequence across all channels + /// + [XmlIgnore] + public uint SequenceLengthSamples + { + get + { + uint max = 0; + + foreach (var stim in Stimuli) + { + var len = stim.DelaySamples + stim.NumberOfStimuli * (stim.AnodicWidthSamples + stim.CathodicWidthSamples + stim.DwellSamples + stim.InterStimulusIntervalSamples); + max = len > max ? len : max; + + } + + return max; + } + } + + /// + /// Maximum peak to peak amplitude of the sequence across all channels. + /// + [XmlIgnore] + public int MaximumPeakToPeakAmplitudeSteps + { + get + { + int max = 0; + + foreach (var stim in Stimuli) + { + var p2p = stim.CathodicAmplitudeSteps + stim.AnodicAmplitudeSteps; + max = p2p > max ? p2p : max; + + } + + return max; + } + } + + /// + /// Is the stimulus sequence well define + /// + [XmlIgnore] + public bool Valid => Stimuli.ToList().All(s => s.Valid); + + /// + /// Does the sequence fit in hardware + /// + [XmlIgnore] + public bool FitsInHardware => StimulusSlotsRequired <= Rhs2116.StimMemorySlotsAvailable; + + /// + /// Number of hardware memory slots required by the sequence + /// + [XmlIgnore] + public int StimulusSlotsRequired => DeltaTable.Count; + + [XmlIgnore] + public double CurrentStepSizeuA + { + get + { + return CurrentStepSize switch + { + Rhs2116StepSize.Step10nA => 0.01, + Rhs2116StepSize.Step20nA => 0.02, + Rhs2116StepSize.Step50nA => 0.05, + Rhs2116StepSize.Step100nA => 0.1, + Rhs2116StepSize.Step200nA => 0.2, + Rhs2116StepSize.Step500nA => 0.5, + Rhs2116StepSize.Step1000nA => 1.0, + Rhs2116StepSize.Step2000nA => 2.0, + Rhs2116StepSize.Step5000nA => 5.0, + Rhs2116StepSize.Step10000nA => 10.0, + _ => throw new ArgumentException("Invalid stimulus step size selection."), + }; + } + } + + [XmlIgnore] + public double MaxPossibleAmplitudePerPhaseMicroAmps => CurrentStepSizeuA * 255; + + internal IEnumerable AnodicAmplitudes => Stimuli.ToList().Select(x => x.AnodicAmplitudeSteps); + + internal IEnumerable CathodicAmplitudes => Stimuli.ToList().Select(x => x.CathodicAmplitudeSteps); + + /// + /// Generate the delta-table representation of this stimulus sequence that can be uploaded to the RHS2116 device. + /// The resultant dictionary has a time, in samples as the key and a combimed [polary, enable] bit field as the value. + /// + [XmlIgnore] + internal Dictionary DeltaTable + { + get + { + var table = new Dictionary(); + + // Cycle through electrodes + for (int i = 0; i < Stimuli.Length; i++) + { + var s = Stimuli[i]; + + var e0 = s.AnodicFirst ? s.AnodicAmplitudeSteps > 0 : s.CathodicAmplitudeSteps > 0; + var e1 = s.AnodicFirst ? s.CathodicAmplitudeSteps > 0 : s.AnodicAmplitudeSteps > 0; + var d0 = s.AnodicFirst ? s.AnodicWidthSamples : s.CathodicWidthSamples; + var d1 = d0 + s.DwellSamples; + var d2 = d1 + (s.AnodicFirst ? s.CathodicWidthSamples : s.AnodicWidthSamples); + + var t0 = s.DelaySamples; + + for (int j = 0; j < s.NumberOfStimuli; j++) + { + AddOrInsert(ref table, i, t0, s.AnodicFirst, e0); + AddOrInsert(ref table, i, t0 + d0, s.AnodicFirst, false); + AddOrInsert(ref table, i, t0 + d1, !s.AnodicFirst, e1); + AddOrInsert(ref table, i, t0 + d2, !s.AnodicFirst, false); + + t0 += d2 + s.InterStimulusIntervalSamples; + } + } + + return table.ToDictionary(d => d.Key, d => + { + int[] i = new int[1]; + d.Value.CopyTo(i, 0); + return (uint)i[0]; + }); + } + } + + private static void AddOrInsert(ref Dictionary table, int channel, uint key, bool polarity, bool enable) + { + if (table.ContainsKey(key)) + { + table[key][channel] = enable; + table[key][channel + 16] = polarity; + } + else + { + table.Add(key, new BitArray(32, false)); + table[key][channel] = enable; + table[key][channel + 16] = polarity; + } + } + } +} diff --git a/OpenEphys.Onix1/Rhs2116Data.cs b/OpenEphys.Onix1/Rhs2116Data.cs new file mode 100644 index 00000000..3b503eaa --- /dev/null +++ b/OpenEphys.Onix1/Rhs2116Data.cs @@ -0,0 +1,58 @@ +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.Onix1 +{ + public class Rhs2116Data : Source + { + [TypeConverter(typeof(Rhs2116.NameConverter))] + public string DeviceName { get; set; } + + public int BufferSize { get; set; } = 30; + + public unsafe override IObservable Generate() + { + var bufferSize = BufferSize; + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var sampleIndex = 0; + var device = deviceInfo.GetDeviceContext(typeof(Rhs2116)); + var amplifierBuffer = new short[Rhs2116.AmplifierChannelCount * bufferSize]; + var dcBuffer = new short[Rhs2116.AmplifierChannelCount * bufferSize]; + var hubClockBuffer = new ulong[bufferSize]; + var clockBuffer = new ulong[bufferSize]; + + var frameObserver = Observer.Create( + frame => + { + var payload = (Rhs2116Payload*)frame.Data.ToPointer(); + Marshal.Copy(new IntPtr(payload->AmplifierData), amplifierBuffer, sampleIndex * Rhs2116.AmplifierChannelCount, Rhs2116.AmplifierChannelCount); + Marshal.Copy(new IntPtr(payload->DCData), dcBuffer, sampleIndex * Rhs2116.AmplifierChannelCount, Rhs2116.AmplifierChannelCount); + hubClockBuffer[sampleIndex] = payload->HubClock; + clockBuffer[sampleIndex] = frame.Clock; + if (++sampleIndex >= bufferSize) + { + var amplifierData = BufferHelper.CopyTranspose(amplifierBuffer, bufferSize, Rhs2116.AmplifierChannelCount, Depth.U16); + var dcData = BufferHelper.CopyTranspose(dcBuffer, bufferSize, Rhs2116.AmplifierChannelCount, Depth.U16); + observer.OnNext(new Rhs2116DataFrame(clockBuffer, hubClockBuffer, amplifierData, dcData)); + hubClockBuffer = new ulong[bufferSize]; + clockBuffer = new ulong[bufferSize]; + sampleIndex = 0; + } + }, + observer.OnError, + observer.OnCompleted); + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .SubscribeSafe(frameObserver); + })); + } + } +} diff --git a/OpenEphys.Onix1/Rhs2116DataFrame.cs b/OpenEphys.Onix1/Rhs2116DataFrame.cs new file mode 100644 index 00000000..5eb3e50e --- /dev/null +++ b/OpenEphys.Onix1/Rhs2116DataFrame.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + public class Rhs2116DataFrame + { + public Rhs2116DataFrame(ulong[] clock, ulong[] hubClock, Mat amplifierData, Mat dcData) + { + Clock = clock; + HubClock = hubClock; + AmplifierData = amplifierData; + DCData = dcData; + } + + public ulong[] Clock { get; } + + public ulong[] HubClock { get; } + + public Mat AmplifierData { get; } + + public Mat DCData { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct Rhs2116Payload + { + public ulong HubClock; + public fixed ushort AmplifierData[Rhs2116.AmplifierChannelCount]; + public fixed ushort DCData[Rhs2116.AmplifierChannelCount]; + } +} diff --git a/OpenEphys.Onix1/Rhs2116StimulusTrigger.cs b/OpenEphys.Onix1/Rhs2116StimulusTrigger.cs new file mode 100644 index 00000000..dacec97a --- /dev/null +++ b/OpenEphys.Onix1/Rhs2116StimulusTrigger.cs @@ -0,0 +1,29 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + public class Rhs2116StimulusTrigger : Sink + { + [TypeConverter(typeof(Rhs2116Trigger.NameConverter))] + public string DeviceName { get; set; } + + public override IObservable Process(IObservable source) + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(Rhs2116Trigger)); + return source.Do(t => + { + const double SampleFrequencyMegaHz = Rhs2116.SampleFrequencyHz / 1e6; + var delaySamples = (int)(t * SampleFrequencyMegaHz); + device.WriteRegister(Rhs2116Trigger.TRIGGER, (uint)(delaySamples << 12 | 0x1)); + }); + }); + } + } +} diff --git a/OpenEphys.Onix/OpenEphys.Onix/HubDeviceConverter.cs b/OpenEphys.Onix1/SingleDeviceFactoryConverter.cs similarity index 91% rename from OpenEphys.Onix/OpenEphys.Onix/HubDeviceConverter.cs rename to OpenEphys.Onix1/SingleDeviceFactoryConverter.cs index e83622b6..da34aaa7 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/HubDeviceConverter.cs +++ b/OpenEphys.Onix1/SingleDeviceFactoryConverter.cs @@ -3,9 +3,9 @@ using System.Globalization; using System.Linq; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { - internal class HubDeviceConverter : ExpandableObjectConverter + internal class SingleDeviceFactoryConverter : ExpandableObjectConverter { public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { diff --git a/OpenEphys.Onix/OpenEphys.Onix/StackDisposable.cs b/OpenEphys.Onix1/StackDisposable.cs similarity index 97% rename from OpenEphys.Onix/OpenEphys.Onix/StackDisposable.cs rename to OpenEphys.Onix1/StackDisposable.cs index e2a73353..f32b69ce 100644 --- a/OpenEphys.Onix/OpenEphys.Onix/StackDisposable.cs +++ b/OpenEphys.Onix1/StackDisposable.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Reactive.Disposables; -namespace OpenEphys.Onix +namespace OpenEphys.Onix1 { internal class StackDisposable : IDisposable { diff --git a/OpenEphys.Onix1/StartAcquisition.cs b/OpenEphys.Onix1/StartAcquisition.cs new file mode 100644 index 00000000..5bc60ab6 --- /dev/null +++ b/OpenEphys.Onix1/StartAcquisition.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Starts data acquisition and frame distribution on a . + /// + [Description("Starts data acquisition and frame distribution on a ContextTask.")] + public class StartAcquisition : Combinator> + { + /// + /// Gets or sets the number of bytes read per cycle of the 's acquisition thread. + /// + /// + /// This option allows control over a fundamental trade-off between closed-loop response time and overall bandwidth. + /// A minimal value, which is determined by , will provide the lowest response latency, + /// so long as data can be cleared from hardware memory fast enough to prevent buffering. Larger values will reduce system + /// call frequency, increase overall bandwidth, and may improve processing performance for high-bandwidth data sources. + /// The optimal value depends on the host computer and hardware configuration and must be determined via testing (e.g. + /// using ). + /// + [Description("Number of bytes read per cycle of the acquisition thread.")] + public int ReadSize { get; set; } = 2048; + + /// + /// Gets or sets the number of bytes that are pre-allocated for writing data to hardware. + /// + /// + /// This value determines the amount of memory pre-allocated for calls to , + /// , and . A larger size will reduce + /// the average amount of dynamic memory allocation system calls but increase the cost of each of those calls. The minimum + /// size of this option is determined by . The effect on real-timer performance is not as + /// large as that of . + /// + [Description("The number of bytes that are pre-allocated for writing data to hardware.")] + public int WriteSize { get; set; } = 2048; + + /// + /// Starts data acquisition and frame distribution on a and returns + /// the sequence of all received objects, grouped by device address. + /// + /// + /// The sequence of objects on which to start data acquisition + /// and frame distribution. + /// + /// + /// A sequence of objects for each , + /// grouped by device address. + /// + public override IObservable> Process(IObservable source) + { + return source.SelectMany(context => + { + return Observable.Create>((observer, cancellationToken) => + { + var frameSubscription = context.GroupedFrames.SubscribeSafe(observer); + try + { + return context.StartAsync(ReadSize, WriteSize, cancellationToken) + .ContinueWith(_ => frameSubscription.Dispose()); + } + catch + { + frameSubscription.Dispose(); + throw; + } + }); + }); + } + } +} diff --git a/OpenEphys.Onix1/TS4231V1Data.cs b/OpenEphys.Onix1/TS4231V1Data.cs new file mode 100644 index 00000000..e742ec8b --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1Data.cs @@ -0,0 +1,49 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that produces a sequence of decoded optical signals produced by a pair of SteamVR V1 base stations. + /// + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream 3D position data. + /// + /// + /// The data produced by this class contains individual base station pulse/sweep codes and timing information. These data provide + /// rapid updates that constrain the possible position of a sensor and therefore can be combined with orientation information + /// in a downstream predictive model (e.g. Kalman filter) for high-accuracy and robust position tracking. To produce naïve + /// position estimates, use the operator instead of this one. + /// + /// + [Description("Produces a sequence of decoded optical signals produced by a pair of SteamVR V1 base stations.")] + public class TS4231V1Data : Source + { + /// + [TypeConverter(typeof(TS4231V1.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence of objects, each of which contains information on a single + /// lighthouse optical sweep or pulse. + /// + /// A sequence of objects. + public override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany(deviceInfo => + { + var device = deviceInfo.GetDeviceContext(typeof(TS4231V1)); + var hubClockPeriod = 1e6 / device.Hub.ClockHz; + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .Select(frame => new TS4231V1DataFrame(frame, hubClockPeriod)); + }); + } + } +} diff --git a/OpenEphys.Onix1/TS4231V1DataFrame.cs b/OpenEphys.Onix1/TS4231V1DataFrame.cs new file mode 100644 index 00000000..3a7c8580 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1DataFrame.cs @@ -0,0 +1,97 @@ +using System.Runtime.InteropServices; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains information about a single synchronization pulse or light sweep from a SteamVR V1 base station. + /// + public class TS4231V1DataFrame : DataFrame + { + + /// + /// Initializes a new instance of the class. + /// + /// An produced by a TS4231 device + /// The period of the TS4231 devices local clock in Hz + public unsafe TS4231V1DataFrame(oni.Frame frame, double hubClockPeriod) + : base(frame.Clock) + { + var payload = (TS4231V1Payload*)frame.Data.ToPointer(); + HubClock = payload->HubClock; + SensorIndex = payload->SensorIndex; + EnvelopeWidth = 1e6 * hubClockPeriod * payload->EnvelopeWidth; + EnvelopeType = payload->EnvelopeType; + } + + /// + /// Gets the index of the TS4231 sensor that produced this data. + /// + public int SensorIndex { get; } + + /// + /// Gets the width of the envelope of the modulated optical pulse or sweep in microseconds. + /// + public double EnvelopeWidth { get; } + + /// + /// Gets the pulse or sweep classification. + /// + public TS4231V1Envelope EnvelopeType { get; } + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct TS4231V1Payload + { + public ulong HubClock; + public ushort SensorIndex; + public uint EnvelopeWidth; + public TS4231V1Envelope EnvelopeType; + } + + /// + /// Specifies the SteamVR V1 base station optical signal classification. + /// + public enum TS4231V1Envelope : short + { + /// + /// Specifies and invalid optical signal. + /// + Bad = -1, + /// + /// Specifies a synchronization pulse with 50.0 μS < width ≤ 62.5 μS + /// + J0, + /// + /// Specifies a synchronization pulse with 62.5 μS < width ≤ 72.9 μS + /// + K0, + /// + /// Specifies a synchronization pulse with 72.9 μS < width ≤ 83.3 μS + /// + J1, + /// + /// Specifies a synchronization pulse with 83.3 μS < width ≤ 93.8 μS + /// + K1, + /// + /// Specifies a synchronization pulse with 93.8 μS < width ≤ 104 μS + /// + J2, + /// + /// Specifies a synchronization pulse with 104 μS < width ≤ 115 μS + /// + K2, + /// + /// Specifies a synchronization pulse with 115 μS < width ≤ 125 μS + /// + J3, + /// + /// Specifies a synchronization pulse with 125 μS < width ≤ 135 μS + /// + K3, + /// + /// Specifies a light sheet sweep (width ≤ 50 μS) + /// + Sweep, + } +} diff --git a/OpenEphys.Onix1/TS4231V1PositionConverter.cs b/OpenEphys.Onix1/TS4231V1PositionConverter.cs new file mode 100644 index 00000000..c4b0e123 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1PositionConverter.cs @@ -0,0 +1,164 @@ +using OpenCV.Net; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reactive.Linq; + +namespace OpenEphys.Onix1 +{ + class TS4231V1PulseQueue + { + public Queue PulseFrameClock { get; } = new(new ulong[TS4231V1PositionConverter.ValidPulseSequenceTemplate.Length / 4]); + public Queue PulseHubClock { get; } = new(new ulong[TS4231V1PositionConverter.ValidPulseSequenceTemplate.Length / 4]); + public Queue PulseWidths { get; } = new(new double[TS4231V1PositionConverter.ValidPulseSequenceTemplate.Length / 4]); + public Queue PulseParse { get; } = new(new bool[TS4231V1PositionConverter.ValidPulseSequenceTemplate.Length]); + } + + class TS4231V1PositionConverter + { + const double SweepFrequencyHz = 60; + readonly double HubClockFrequencyPeriod; + readonly Mat p; + readonly Mat q; + + // Template pattern + internal static readonly bool[] ValidPulseSequenceTemplate = { + // bad skip axis sweep + false, false, false, false, + false, true, false, false, + false, false, false, true, // axis 0, station 0 + false, false, true, false, + false, true, true, false, + false, false, false, true, // axis 1, station 0 + false, true, false, false, + false, false, false, false, + false, false, false, true, // axis 0, station 1 + false, true, true, false, + false, false, true, false, + false, false, false, true // axis 1, station 1 + }; + + Dictionary PulseQueues = new(); + + public TS4231V1PositionConverter(uint hubClockFrequencyHz, Point3d baseStation1Origin, Point3d baseStation2Origin) + { + HubClockFrequencyPeriod = 1d / hubClockFrequencyHz; + + p = new Mat(3, 1, Depth.F64, 1); + p[0] = new Scalar(baseStation1Origin.X); + p[1] = new Scalar(baseStation1Origin.Y); + p[2] = new Scalar(baseStation1Origin.Z); + + q = new Mat(3, 1, Depth.F64, 1); + q[0] = new Scalar(baseStation2Origin.X); + q[1] = new Scalar(baseStation2Origin.Y); + q[2] = new Scalar(baseStation2Origin.Z); + } + + public unsafe TS4231V1PositionDataFrame Convert(oni.Frame frame) + { + var payload = (TS4231V1Payload*)frame.Data.ToPointer(); + + if (!PulseQueues.ContainsKey(payload->SensorIndex)) + PulseQueues.Add(payload->SensorIndex, new TS4231V1PulseQueue()); + + var queues = PulseQueues[payload->SensorIndex]; + + // Push pulse time into buffer and pop oldest + queues.PulseFrameClock.Dequeue(); + queues.PulseFrameClock.Enqueue(frame.Clock); + + queues.PulseHubClock.Dequeue(); + queues.PulseHubClock.Enqueue(payload->HubClock); + + // Push pulse width into buffer and pop oldest + queues.PulseWidths.Dequeue(); + queues.PulseWidths.Enqueue(HubClockFrequencyPeriod * payload->EnvelopeWidth); + + // push pulse code categorization into buffer and pop oldest 4x + queues.PulseParse.Dequeue(); + queues.PulseParse.Dequeue(); + queues.PulseParse.Dequeue(); + queues.PulseParse.Dequeue(); + queues.PulseParse.Enqueue(payload->EnvelopeType == TS4231V1Envelope.Bad); + queues.PulseParse.Enqueue(payload->EnvelopeType >= TS4231V1Envelope.J2 & payload->EnvelopeType != TS4231V1Envelope.Sweep); // skip + queues.PulseParse.Enqueue((int)payload->EnvelopeType % 2 == 1 & payload->EnvelopeType != TS4231V1Envelope.Sweep); // axis + queues.PulseParse.Enqueue(payload->EnvelopeType == TS4231V1Envelope.Sweep); // sweep + + // convert to arrays + var times = queues.PulseHubClock.Select(x => HubClockFrequencyPeriod * x).ToArray(); + var widths = queues.PulseWidths.ToArray(); + + // test template match and make sure time between pulses does not integrate to more than two periods + if (!queues.PulseParse.SequenceEqual(ValidPulseSequenceTemplate) || + times.Last() - times.First() > 2 / SweepFrequencyHz) + { + return null; + } + + var t11 = times[2] + widths[2] / 2 - times[0]; + var t21 = times[5] + widths[5] / 2 - times[3]; + var theta0 = 2 * Math.PI * SweepFrequencyHz * t11 - Math.PI / 2; + var gamma0 = 2 * Math.PI * SweepFrequencyHz * t21 - Math.PI / 2; + + var u = new Mat(3, 1, Depth.F64, 1); + u[0] = new Scalar(Math.Tan(theta0)); + u[1] = new Scalar(Math.Tan(gamma0)); + u[2] = new Scalar(1); + CV.Normalize(u, u); + + var t12 = times[8] + widths[8] / 2 - times[7]; + var t22 = times[11] + widths[11] / 2 - times[10]; + var theta1 = 2 * Math.PI * SweepFrequencyHz * t12 - Math.PI / 2; + var gamma1 = 2 * Math.PI * SweepFrequencyHz * t22 - Math.PI / 2; + + var v = new Mat(3, 1, Depth.F64, 1); + v[0] = new Scalar(Math.Tan(theta1)); + v[1] = new Scalar(Math.Tan(gamma1)); + v[2] = new Scalar(1); + CV.Normalize(v, v); + + // Base station origin vector + var d = q - p; + + // Linear transform + // A = [a11 a12] + // [a21 a22] + var a11 = 1.0; + var a12 = -CV.DotProduct(u, v); + var a21 = CV.DotProduct(u, v); + var a22 = -1.0; + + // Result + // B = [b1] + // [b2] + var b1 = CV.DotProduct(u, d); + var b2 = CV.DotProduct(v, d); + + // Solve Ax = B + var x2 = (b2 - (b1 * a21) / a11) / (a22 - (a12 * a21) / a11); + var x1 = (b1 - a12 * x2) / a11; + + // If singular, return null + if (double.IsNaN(x1) || + double.IsNaN(x2) || + double.IsInfinity(x1) || + double.IsInfinity(x2)) + { + return null; + } + + // calculate position + var p1 = p + x1 * u; + var q1 = q + x2 * v; + var position = 0.5 * (p1 + q1); + + return new TS4231V1PositionDataFrame( + queues.PulseHubClock.ElementAt(ValidPulseSequenceTemplate.Length / 8), + queues.PulseFrameClock.ElementAt(ValidPulseSequenceTemplate.Length / 8), + payload->SensorIndex, + new Vector3((float)position[0].Val0, (float)position[1].Val0, (float)position[2].Val0)); + } + } +} diff --git a/OpenEphys.Onix1/TS4231V1PositionData.cs b/OpenEphys.Onix1/TS4231V1PositionData.cs new file mode 100644 index 00000000..a7d27c72 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1PositionData.cs @@ -0,0 +1,95 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Bonsai; +using OpenCV.Net; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that produces a sequence of 3D positions from an array of Triad Semiconductor TS4231 receivers beneath + /// a pair of SteamVR V1 base stations. + /// + /// + /// + /// This data stream class must be linked to an appropriate configuration, such as a , + /// in order to stream data. + /// + /// + /// The data produced by this class contains naïve geometric estimates of positions of photodiodes attached to each TS4231 chip. + /// This operator makes the following assumptions about the setup: + /// + /// Two SteamVR V1 base stations are used. + /// The base stations have been synchronized with a patch cable and their modes set to ‘A’ and ‘b’, respectively. + /// The base stations are pointed in the same direction. + /// The Z-axis extends away the emitting face of lighthouses, X along the direction of the text on the back label, + /// and Y from bottom to top text on the back label. + /// + /// This operator collects a sequence of objects from each TS3231 receiver that are used to determine the ray from each + /// base station to the TS3231's photodiode. A simple geometric inversion is performed to determine the photodiodes 3D position from the values + /// and . It does not use a predictive model or integrate data from an IMU and is therefore quite sensitive to + /// obstructions in and will require post-hoc processing to correct systematic errors due to optical aberrations and nonlinearities. The the + /// operator provides access to individual lighthouse signals that is useful for a creating more robust position + /// estimates using downstream processing. + /// + /// + [Description("Produces a sequence of 3D positions from an array of Triad Semiconductor TS4231 receivers beneath a pair of SteamVR V1 base stations.")] + public class TS4231V1PositionData : Source + { + /// + [TypeConverter(typeof(TS4231V1.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + public string DeviceName { get; set; } + + /// + /// Gets or sets the position of the first base station in arbitrary units. + /// + /// + /// The units used will determine the units of and must match those used in . + /// Typically this value is used to define the origin and remains at (0, 0, 0). + /// + [Description("The position of the first base station in arbitrary units.")] + public Point3d P { get; set; } = new(0, 0, 0); + + /// + /// Gets or sets the position of the second base station in arbitrary units. + /// + /// + /// The units used will determine the units of and must match those used in . + /// + [Description("The position of the second base station in arbitrary units.")] + public Point3d Q { get; set; } = new(1, 0, 0); + + /// + /// Generates a sequence of objects, each of which contains the 3D position of single photodiode. + /// + /// A sequence of objects. + public unsafe override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => Observable.Create(observer => + { + var device = deviceInfo.GetDeviceContext(typeof(TS4231V1)); + var pulseConverter = new TS4231V1PositionConverter(device.Hub.ClockHz, P, Q); + + var frameObserver = Observer.Create( + frame => + { + var position = pulseConverter.Convert(frame); + if (position != null) + { + observer.OnNext(position); + } + }, + observer.OnError, + observer.OnCompleted); + + return deviceInfo.Context + .GetDeviceFrames(device.Address) + .SubscribeSafe(frameObserver); + })); + } + } +} diff --git a/OpenEphys.Onix1/TS4231V1PositionDataFrame.cs b/OpenEphys.Onix1/TS4231V1PositionDataFrame.cs new file mode 100644 index 00000000..2bddb468 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1PositionDataFrame.cs @@ -0,0 +1,48 @@ +using System.Numerics; + +namespace OpenEphys.Onix1 +{ + /// + /// A class that contains the 3D position of a photodiode in a TS4231 sensor array relative + /// to a given SteamVR V1 base station origin. + /// + /// + /// A sequence of 12 objects produced by a single TS4231 sensor are required to + /// geometrically calculate the position of the sensor's photodiode in 3D space. + /// + public class TS4231V1PositionDataFrame + { + /// + /// Initializes a new instance of the class. + /// + /// The median value of the 12 frames required to construct a single position. + /// The median value of the 12 frames required to construct a single position. + /// The index of the TS4231 sensor that the 3D position corresponds to. + /// The 3 dimensional position of the photodiode connected to the TS4231 sensor with units determined by + /// and . + public TS4231V1PositionDataFrame(ulong clock, ulong hubClock, int sensorIndex, Vector3 position) + { + Clock = clock; + HubClock = hubClock; + SensorIndex = sensorIndex; + Position = position; + } + + /// + public ulong Clock { get; } + + /// + public ulong HubClock { get; } + + /// + /// Gets the index of the TS4231 sensor that produced this data. + /// + public int SensorIndex { get; } + + /// + /// Gets rhe 3D position of the photodiode connected to the TS4231[] sensor with units determined by + /// and . + /// + public Vector3 Position { get; } + } +} diff --git a/README.md b/README.md index eed4c535..90e7baeb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ -# onix-refactor -A project for refactoring the ONIX bonsai library +# Onix1 Bonsai Library +[Bonsai](https://bonsai-rx.org/) library for the [Open Ephys Onix +Acquisition System](https://open-ephys.github.io/onix-docs). + +- Open Ephys store: https://open-ephys.org/onix +- Library documentation: https://open-ephys.github.io/onix1-bonsai-docs +- Hardware documentation: https://open-ephys.github.io/onix-docs + diff --git a/build/Version.props b/build/Version.props new file mode 100644 index 00000000..d7d1fa24 --- /dev/null +++ b/build/Version.props @@ -0,0 +1,20 @@ + + + + 0 + + dev$(DevVersion) + <_FileVersionRevision>$([MSBuild]::Add(60000, $(DevVersion))) + + + + $(CiBuildVersionSuffix) + <_FileVersionRevision>0 + <_FileVersionRevision Condition="'$(CiBuildVersionSuffix)' != '' and '$(CiRunNumber)' != ''">$(CiRunNumber) + + + + + $(WarningsAsErrors);CS7035 + + \ No newline at end of file