diff --git a/OpenEphys.Onix1.Design/ChannelConfigurationDialog.Designer.cs b/OpenEphys.Onix1.Design/ChannelConfigurationDialog.Designer.cs index 1d6990ac..e5dd97f1 100644 --- a/OpenEphys.Onix1.Design/ChannelConfigurationDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/ChannelConfigurationDialog.Designer.cs @@ -102,22 +102,22 @@ private void InitializeComponent() // dropDownOpenFile // this.dropDownOpenFile.Name = "dropDownOpenFile"; - this.dropDownOpenFile.Size = new System.Drawing.Size(265, 22); - this.dropDownOpenFile.Text = "Open Channel Configuration"; + this.dropDownOpenFile.Size = new System.Drawing.Size(218, 22); + this.dropDownOpenFile.Text = "Open ProbeInterface File"; this.dropDownOpenFile.Click += new System.EventHandler(this.MenuItemOpenFile); // // dropDownSaveFile // this.dropDownSaveFile.Name = "dropDownSaveFile"; - this.dropDownSaveFile.Size = new System.Drawing.Size(265, 22); - this.dropDownSaveFile.Text = "Save Channel Configuration"; + this.dropDownSaveFile.Size = new System.Drawing.Size(218, 22); + this.dropDownSaveFile.Text = "Save ProbeInterface File"; this.dropDownSaveFile.Click += new System.EventHandler(this.MenuItemSaveFile); // // dropDownLoadDefault // this.dropDownLoadDefault.Name = "dropDownLoadDefault"; - this.dropDownLoadDefault.Size = new System.Drawing.Size(265, 22); - this.dropDownLoadDefault.Text = "Load Default Channel Configuration"; + this.dropDownLoadDefault.Size = new System.Drawing.Size(218, 22); + this.dropDownLoadDefault.Text = "Load Default Configuration"; this.dropDownLoadDefault.Click += new System.EventHandler(this.MenuItemLoadDefaultConfig); // // tableLayoutPanel1 diff --git a/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs b/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs index 51dd1caf..35add677 100644 --- a/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs +++ b/OpenEphys.Onix1.Design/ChannelConfigurationDialog.cs @@ -313,7 +313,7 @@ internal T OpenAndParseConfigurationFile() where T : ProbeGroup if (ofd.ShowDialog() == DialogResult.OK && File.Exists(ofd.FileName)) { - var newConfiguration = DesignHelper.DeserializeString(File.ReadAllText(ofd.FileName)); + var newConfiguration = JsonHelper.DeserializeString(File.ReadAllText(ofd.FileName)); return newConfiguration; } @@ -927,7 +927,7 @@ private void MenuItemSaveFile(object sender, EventArgs e) if (sfd.ShowDialog() == DialogResult.OK) { - DesignHelper.SerializeObject(ProbeGroup, sfd.FileName); + JsonHelper.SerializeObject(ProbeGroup, sfd.FileName); } } diff --git a/OpenEphys.Onix1.Design/DesignHelper.cs b/OpenEphys.Onix1.Design/DesignHelper.cs index fc9f4619..8090455d 100644 --- a/OpenEphys.Onix1.Design/DesignHelper.cs +++ b/OpenEphys.Onix1.Design/DesignHelper.cs @@ -10,57 +10,6 @@ namespace OpenEphys.Onix1.Design { static class DesignHelper { - /// - /// Given a string with a valid JSON structure, deserialize the string to the given type. - /// - /// - /// - /// - #nullable enable - public static T? DeserializeString(string jsonString) - { - var errors = new List(); - - var serializerSettings = new JsonSerializerSettings() - { - Error = delegate(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args) - { - errors.Add(args.ErrorContext.Error.Message); - args.ErrorContext.Handled = true; - } - }; - - var obj = JsonConvert.DeserializeObject(jsonString, serializerSettings); - - if (errors.Count > 0) - { - MessageBox.Show($"There were errors encountered while parsing a JSON string. Check the console " + - $"for an error log.", "JSON Parse Error"); - - foreach (var e in errors) - { - Console.Error.WriteLine(e); - } - - return default; - } - - return obj; - } - #nullable disable - - public static void SerializeObject(object _object, string filepath) - { - var serializerSettings = new JsonSerializerSettings() - { - NullValueHandling = NullValueHandling.Ignore, - }; - - var stringJson = JsonConvert.SerializeObject(_object, Formatting.Indented, serializerSettings); - - File.WriteAllText(filepath, stringJson); - } - public static IEnumerable GetAllControls(this Control root) { var stack = new Stack(); diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1ChannelConfigurationDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV1ChannelConfigurationDialog.cs index afe9cdf9..41f85ce2 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1ChannelConfigurationDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1ChannelConfigurationDialog.cs @@ -37,7 +37,7 @@ public NeuropixelsV1ChannelConfigurationDialog(NeuropixelsV1ProbeConfiguration p ReferenceContacts.AddRange(ReferenceContactsList); - ProbeConfiguration = probeConfiguration; + ProbeConfiguration = new(probeConfiguration); HighlightEnabledContacts(); UpdateContactLabels(); @@ -52,7 +52,7 @@ internal override ProbeGroup DefaultChannelLayout() internal override void LoadDefaultChannelLayout() { - ProbeConfiguration = new(ProbeConfiguration.SpikeAmplifierGain, ProbeConfiguration.LfpAmplifierGain, ProbeConfiguration.Reference, ProbeConfiguration.SpikeFilter); + ProbeConfiguration.ProbeGroup = new(); ProbeGroup = ProbeConfiguration.ProbeGroup; OnFileOpenHandler(); @@ -62,8 +62,7 @@ internal override bool OpenFile() { if (base.OpenFile()) { - ProbeConfiguration = new((NeuropixelsV1eProbeGroup)ProbeGroup, ProbeConfiguration.SpikeAmplifierGain, ProbeConfiguration.LfpAmplifierGain, ProbeConfiguration.Reference, ProbeConfiguration.SpikeFilter); - ProbeGroup = ProbeConfiguration.ProbeGroup; + ProbeConfiguration.ProbeGroup = (NeuropixelsV1eProbeGroup)ProbeGroup; OnFileOpenHandler(); @@ -112,7 +111,7 @@ internal override void DrawScale() internal override void HighlightEnabledContacts() { - if (ProbeConfiguration == null || ProbeConfiguration.ChannelMap == null) + if (ProbeConfiguration == null) return; var contactObjects = zedGraphChannels.GraphPane.GraphObjList.OfType() @@ -125,11 +124,13 @@ internal override void HighlightEnabledContacts() contact.Fill.Color = DisabledContactFill; } + var channelMap = ProbeConfiguration.ChannelMap; + var contactsToEnable = contactObjects.Where(c => { var tag = c.Tag as ContactTag; var channel = NeuropixelsV1Electrode.GetChannelNumber(tag.ContactIndex); - return ProbeConfiguration.ChannelMap[channel].Index == tag.ContactIndex; + return channelMap[channel].Index == tag.ContactIndex; }); foreach (var contact in contactsToEnable) @@ -155,11 +156,13 @@ internal override void UpdateContactLabels() textObj.FontSpec.FontColor = DisabledContactTextColor; } + var channelMap = ProbeConfiguration.ChannelMap; + textObjsToUpdate = textObjs.Where(c => { var tag = c.Tag as ContactTag; var channel = NeuropixelsV1Electrode.GetChannelNumber(tag.ContactIndex); - return ProbeConfiguration.ChannelMap[channel].Index == tag.ContactIndex; + return channelMap[channel].Index == tag.ContactIndex; }); foreach (var textObj in textObjsToUpdate) diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1Dialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV1Dialog.cs index f41b6721..d7de267f 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1Dialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1Dialog.cs @@ -8,12 +8,13 @@ namespace OpenEphys.Onix1.Design /// public partial class NeuropixelsV1Dialog : Form { - readonly NeuropixelsV1ProbeConfigurationDialog ProbeConfigurationDialog; + internal readonly NeuropixelsV1ProbeConfigurationDialog ProbeConfigurationDialog; /// /// Public interface that is manipulated by /// . /// + [Obsolete] public IConfigureNeuropixelsV1 ConfigureNode { get; set; } /// @@ -25,24 +26,10 @@ public NeuropixelsV1Dialog(IConfigureNeuropixelsV1 configureNode) InitializeComponent(); Shown += FormShown; - if (configureNode is ConfigureNeuropixelsV1e configureV1e) - { - ConfigureNode = new ConfigureNeuropixelsV1e(configureV1e); - } - else if (configureNode is ConfigureNeuropixelsV1f configureV1f) - { - ConfigureNode = new ConfigureNeuropixelsV1f(configureV1f); - } - - ProbeConfigurationDialog = new(ConfigureNode.ProbeConfiguration, ConfigureNode.AdcCalibrationFile, ConfigureNode.GainCalibrationFile, ConfigureNode.InvertPolarity) - { - TopLevel = false, - FormBorderStyle = FormBorderStyle.None, - Dock = DockStyle.Fill, - Parent = this - }; - - panelProbe.Controls.Add(ProbeConfigurationDialog); + ProbeConfigurationDialog = new(configureNode.ProbeConfiguration); + ProbeConfigurationDialog + .SetChildFormProperties(this) + .AddDialogToPanel(panelProbe); this.AddMenuItemsFromDialogToFileOption(ProbeConfigurationDialog); } @@ -62,19 +49,7 @@ private void FormShown(object sender, EventArgs e) internal void Okay_Click(object sender, EventArgs e) { - SaveVariables(); - DialogResult = DialogResult.OK; } - - internal void SaveVariables() - { - ConfigureNode.ProbeConfiguration = ProbeConfigurationDialog.ProbeConfiguration; - - ConfigureNode.GainCalibrationFile = ProbeConfigurationDialog.textBoxGainCalibrationFile.Text; - ConfigureNode.AdcCalibrationFile = ProbeConfigurationDialog.textBoxAdcCalibrationFile.Text; - - ConfigureNode.InvertPolarity = ProbeConfigurationDialog.InvertPolarity; - } } } diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1Editor.cs b/OpenEphys.Onix1.Design/NeuropixelsV1Editor.cs index 5cb38007..440e8ab8 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1Editor.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1Editor.cs @@ -22,11 +22,7 @@ public override bool EditComponent(ITypeDescriptorContext context, object compon if (editorDialog.ShowDialog() == DialogResult.OK) { - configureNeuropixelsV1.Enable = editorDialog.ConfigureNode.Enable; - configureNeuropixelsV1.GainCalibrationFile = editorDialog.ConfigureNode.GainCalibrationFile; - configureNeuropixelsV1.AdcCalibrationFile = editorDialog.ConfigureNode.AdcCalibrationFile; - configureNeuropixelsV1.ProbeConfiguration = editorDialog.ConfigureNode.ProbeConfiguration; - configureNeuropixelsV1.InvertPolarity = editorDialog.ConfigureNode.InvertPolarity; + configureNeuropixelsV1.ProbeConfiguration = editorDialog.ProbeConfigurationDialog.ProbeConfiguration; return true; } diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1ProbeConfigurationDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV1ProbeConfigurationDialog.cs index 4a329309..f4aba201 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1ProbeConfigurationDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1ProbeConfigurationDialog.cs @@ -26,45 +26,36 @@ private enum ChannelPreset } /// - /// Public interface that is manipulated by - /// . + /// Gets or sets the probe configuration. /// - /// - /// When a is passed to - /// , it is copied and stored in this - /// variable so that any modifications made to configuration settings can be easily reversed - /// by not copying the new settings back to the original instance. - /// - public NeuropixelsV1ProbeConfiguration ProbeConfiguration { get; set; } + public NeuropixelsV1ProbeConfiguration ProbeConfiguration + { + get => ChannelConfiguration.ProbeConfiguration; + set => ChannelConfiguration.ProbeConfiguration = value; + } - /// - public bool InvertPolarity { get; set; } + /// + [Obsolete] + public bool InvertPolarity + { + get => ProbeConfiguration.InvertPolarity; + set => ProbeConfiguration.InvertPolarity = value; + } /// /// Initializes a new instance of . /// /// A object holding the current configuration settings. - /// String defining the path to the ADC calibration file. - /// String defining the path to the gain calibration file. - /// Boolean denoting whether or not to invert the polarity of neural data. - public NeuropixelsV1ProbeConfigurationDialog(NeuropixelsV1ProbeConfiguration probeConfiguration, string adcCalibrationFile, string gainCalibrationFile, bool invertPolarity) + public NeuropixelsV1ProbeConfigurationDialog(NeuropixelsV1ProbeConfiguration probeConfiguration) { InitializeComponent(); Shown += FormShown; - ProbeConfiguration = new(probeConfiguration); - - ChannelConfiguration = new(ProbeConfiguration) - { - TopLevel = false, - FormBorderStyle = FormBorderStyle.None, - Dock = DockStyle.Fill, - Parent = this, - }; - - InvertPolarity = invertPolarity; + ChannelConfiguration = new(probeConfiguration); + ChannelConfiguration + .SetChildFormProperties(this) + .AddDialogToPanel(panelProbe); - panelProbe.Controls.Add(ChannelConfiguration); this.AddMenuItemsFromDialogToFileOption(ChannelConfiguration); ChannelConfiguration.OnZoom += UpdateTrackBarLocation; @@ -85,12 +76,14 @@ public NeuropixelsV1ProbeConfigurationDialog(NeuropixelsV1ProbeConfiguration pro checkBoxSpikeFilter.Checked = ProbeConfiguration.SpikeFilter; checkBoxSpikeFilter.CheckedChanged += SpikeFilterIndexChanged; - checkBoxInvertPolarity.Checked = InvertPolarity; + checkBoxInvertPolarity.Checked = ProbeConfiguration.InvertPolarity; checkBoxInvertPolarity.CheckedChanged += InvertPolarityIndexChanged; - textBoxAdcCalibrationFile.Text = adcCalibrationFile; + textBoxAdcCalibrationFile.Text = ProbeConfiguration.AdcCalibrationFileName; + textBoxAdcCalibrationFile.TextChanged += (sender, e) => ProbeConfiguration.AdcCalibrationFileName = ((TextBox)sender).Text; - textBoxGainCalibrationFile.Text = gainCalibrationFile; + textBoxGainCalibrationFile.Text = ProbeConfiguration.GainCalibrationFileName; + textBoxGainCalibrationFile.TextChanged += (sender, e) => ProbeConfiguration.GainCalibrationFileName = ((TextBox)sender).Text; comboBoxChannelPresets.DataSource = Enum.GetValues(typeof(ChannelPreset)); CheckForExistingChannelPreset(); @@ -101,7 +94,7 @@ public NeuropixelsV1ProbeConfigurationDialog(NeuropixelsV1ProbeConfiguration pro private void InvertPolarityIndexChanged(object sender, EventArgs e) { - InvertPolarity = ((CheckBox)sender).Checked; + ProbeConfiguration.InvertPolarity = ((CheckBox)sender).Checked; } private void FormShown(object sender, EventArgs e) diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageDialog.cs index 36e1cd27..98517441 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageDialog.cs @@ -43,8 +43,6 @@ public NeuropixelsV1eHeadstageDialog(ConfigureNeuropixelsV1e configureNeuropixel private void Okay_Click(object sender, System.EventArgs e) { - DialogNeuropixelsV1e.SaveVariables(); - DialogResult = DialogResult.OK; } } diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageEditor.cs b/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageEditor.cs index 9071a247..122a19b9 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageEditor.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1eHeadstageEditor.cs @@ -23,13 +23,9 @@ public override bool EditComponent(ITypeDescriptorContext context, object compon if (editorDialog.ShowDialog() == DialogResult.OK) { - configureHeadstage.Bno055.Enable = ((ConfigurePolledBno055)editorDialog.DialogBno055.Device).Enable; + DesignHelper.CopyProperties((ConfigurePolledBno055)editorDialog.DialogBno055.Device, configureHeadstage.Bno055, DesignHelper.PropertiesToIgnore); - configureHeadstage.NeuropixelsV1e.AdcCalibrationFile = editorDialog.DialogNeuropixelsV1e.ConfigureNode.AdcCalibrationFile; - configureHeadstage.NeuropixelsV1e.GainCalibrationFile = editorDialog.DialogNeuropixelsV1e.ConfigureNode.GainCalibrationFile; - configureHeadstage.NeuropixelsV1e.Enable = editorDialog.DialogNeuropixelsV1e.ConfigureNode.Enable; - configureHeadstage.NeuropixelsV1e.ProbeConfiguration = editorDialog.DialogNeuropixelsV1e.ConfigureNode.ProbeConfiguration; - configureHeadstage.NeuropixelsV1e.InvertPolarity = editorDialog.DialogNeuropixelsV1e.ConfigureNode.InvertPolarity; + configureHeadstage.NeuropixelsV1e.ProbeConfiguration = editorDialog.DialogNeuropixelsV1e.ProbeConfigurationDialog.ProbeConfiguration; return true; } diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageDialog.cs b/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageDialog.cs index 52cd8c8a..12cb445f 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageDialog.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageDialog.cs @@ -61,9 +61,6 @@ public NeuropixelsV1fHeadstageDialog(ConfigureNeuropixelsV1f configureNeuropixel private void Okay_Click(object sender, System.EventArgs e) { - DialogNeuropixelsV1A.SaveVariables(); - DialogNeuropixelsV1B.SaveVariables(); - DialogResult = DialogResult.OK; } } diff --git a/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageEditor.cs b/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageEditor.cs index 18b26643..b2386028 100644 --- a/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageEditor.cs +++ b/OpenEphys.Onix1.Design/NeuropixelsV1fHeadstageEditor.cs @@ -23,19 +23,10 @@ public override bool EditComponent(ITypeDescriptorContext context, object compon if (editorDialog.ShowDialog() == DialogResult.OK) { - configureHeadstage.Bno055.Enable = ((ConfigureBno055)editorDialog.DialogBno055.Device).Enable; + DesignHelper.CopyProperties((ConfigureBno055)editorDialog.DialogBno055.Device, configureHeadstage.Bno055, DesignHelper.PropertiesToIgnore); - configureHeadstage.NeuropixelsV1A.AdcCalibrationFile = editorDialog.DialogNeuropixelsV1A.ConfigureNode.AdcCalibrationFile; - configureHeadstage.NeuropixelsV1A.GainCalibrationFile = editorDialog.DialogNeuropixelsV1A.ConfigureNode.GainCalibrationFile; - configureHeadstage.NeuropixelsV1A.Enable = editorDialog.DialogNeuropixelsV1A.ConfigureNode.Enable; - configureHeadstage.NeuropixelsV1A.ProbeConfiguration = editorDialog.DialogNeuropixelsV1A.ConfigureNode.ProbeConfiguration; - configureHeadstage.NeuropixelsV1A.InvertPolarity = editorDialog.DialogNeuropixelsV1A.ConfigureNode.InvertPolarity; - - configureHeadstage.NeuropixelsV1B.AdcCalibrationFile = editorDialog.DialogNeuropixelsV1B.ConfigureNode.AdcCalibrationFile; - configureHeadstage.NeuropixelsV1B.GainCalibrationFile = editorDialog.DialogNeuropixelsV1B.ConfigureNode.GainCalibrationFile; - configureHeadstage.NeuropixelsV1B.Enable = editorDialog.DialogNeuropixelsV1B.ConfigureNode.Enable; - configureHeadstage.NeuropixelsV1B.ProbeConfiguration = editorDialog.DialogNeuropixelsV1B.ConfigureNode.ProbeConfiguration; - configureHeadstage.NeuropixelsV1B.InvertPolarity = editorDialog.DialogNeuropixelsV1B.ConfigureNode.InvertPolarity; + configureHeadstage.NeuropixelsV1A.ProbeConfiguration = editorDialog.DialogNeuropixelsV1A.ProbeConfigurationDialog.ProbeConfiguration; + configureHeadstage.NeuropixelsV1B.ProbeConfiguration = editorDialog.DialogNeuropixelsV1B.ProbeConfigurationDialog.ProbeConfiguration; return true; } diff --git a/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs b/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs index 07f43ebf..946d5245 100644 --- a/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs +++ b/OpenEphys.Onix1.Design/Rhs2116StimulusSequenceDialog.cs @@ -914,7 +914,7 @@ int GetNumberOfSteps(double amplitude, Rhs2116StepSize stepSize) internal override void SerializeStimulusSequence(string fileName) { - DesignHelper.SerializeObject(Sequence, fileName); + JsonHelper.SerializeObject(Sequence, fileName); } internal override bool IsSequenceValid() @@ -924,7 +924,7 @@ internal override bool IsSequenceValid() internal override void DeserializeStimulusSequence(string fileName) { - var sequence = DesignHelper.DeserializeString(File.ReadAllText(fileName)); + var sequence = JsonHelper.DeserializeString(File.ReadAllText(fileName)); if (sequence != null && sequence.Stimuli.Length == 32) { diff --git a/OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs index 45e93f76..4f4fb519 100644 --- a/OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV1e.cs @@ -35,12 +35,9 @@ public ConfigureNeuropixelsV1e(ConfigureNeuropixelsV1e configureNeuropixelsV1e) { Enable = configureNeuropixelsV1e.Enable; EnableLed = configureNeuropixelsV1e.EnableLed; - GainCalibrationFile = configureNeuropixelsV1e.GainCalibrationFile; - AdcCalibrationFile = configureNeuropixelsV1e.AdcCalibrationFile; ProbeConfiguration = new(configureNeuropixelsV1e.ProbeConfiguration); DeviceName = configureNeuropixelsV1e.DeviceName; DeviceAddress = configureNeuropixelsV1e.DeviceAddress; - InvertPolarity = configureNeuropixelsV1e.InvertPolarity; } /// @@ -62,28 +59,83 @@ public ConfigureNeuropixelsV1e(ConfigureNeuropixelsV1e configureNeuropixelsV1e) [Description("Specifies whether the headstage LED will turn on during acquisition.")] public bool EnableLed { get; set; } = true; - /// - [Category(ConfigurationCategory)] - [Description("Invert the polarity of the electrode voltages acquired by the probe.")] - public bool InvertPolarity { get; set; } = true; + /// + /// Gets or sets a value determining if the polarity of the electrode voltages acquired by the probe + /// should be inverted. + /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// + [Browsable(false)] + [Externalizable(false)] + public bool InvertPolarity + { + get => ProbeConfiguration.InvertPolarity; + set => ProbeConfiguration.InvertPolarity = value; + } - /// - [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)] - [Category(ConfigurationCategory)] - public string GainCalibrationFile { get; set; } + /// + /// Prevent the InvertPolarity property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeInvertPolarity() + { + return false; + } - /// - [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)] - [Category(ConfigurationCategory)] - public string AdcCalibrationFile { get; set; } + /// + /// Gets or sets the path to the gain calibration file for this probe. + /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// + [Browsable(false)] + [Externalizable(false)] + public string GainCalibrationFile + { + get => ProbeConfiguration.GainCalibrationFileName; + set => ProbeConfiguration.GainCalibrationFileName = value; + } + + /// + /// Prevent the GainCalibrationFile property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeGainCalibrationFile() + { + return false; + } + + /// + /// Gets or sets the path to the ADC calibration file. + /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// + [Browsable(false)] + [Externalizable(false)] + public string AdcCalibrationFile + { + get => ProbeConfiguration.AdcCalibrationFileName; + set => ProbeConfiguration.AdcCalibrationFileName = value; + } + + /// + /// Prevent the AdcCalibrationFile property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeAdcCalibrationFile() + { + return false; + } /// [Category(ConfigurationCategory)] [Description("NeuropixelsV1 probe configuration")] + [TypeConverter(typeof(GenericPropertyConverter))] public NeuropixelsV1ProbeConfiguration ProbeConfiguration { get; set; } = new(); /// @@ -104,7 +156,7 @@ public override IObservable Process(IObservable source var deviceName = DeviceName; var deviceAddress = DeviceAddress; var ledEnabled = EnableLed; - var invertPolarity = InvertPolarity; + var invertPolarity = ProbeConfiguration.InvertPolarity; return source.ConfigureDevice(context => { // configure device via the DS90UB9x deserializer device @@ -127,7 +179,7 @@ public override IObservable Process(IObservable source // program shift registers var probeControl = new NeuropixelsV1eRegisterContext(device, NeuropixelsV1.ProbeI2CAddress, - probeMetadata.ProbeSerialNumber, ProbeConfiguration, GainCalibrationFile, AdcCalibrationFile); + probeMetadata.ProbeSerialNumber, ProbeConfiguration); probeControl.InitializeProbe(); probeControl.WriteConfiguration(); probeControl.StartAcquisition(); diff --git a/OpenEphys.Onix1/ConfigureNeuropixelsV1f.cs b/OpenEphys.Onix1/ConfigureNeuropixelsV1f.cs index 178249c8..e4be88ff 100644 --- a/OpenEphys.Onix1/ConfigureNeuropixelsV1f.cs +++ b/OpenEphys.Onix1/ConfigureNeuropixelsV1f.cs @@ -43,12 +43,9 @@ public ConfigureNeuropixelsV1f(ConfigureNeuropixelsV1f configureNeuropixelsV1f) { ProbeName = configureNeuropixelsV1f.ProbeName; Enable = configureNeuropixelsV1f.Enable; - GainCalibrationFile = configureNeuropixelsV1f.GainCalibrationFile; - AdcCalibrationFile = configureNeuropixelsV1f.AdcCalibrationFile; ProbeConfiguration = new(configureNeuropixelsV1f.ProbeConfiguration); DeviceName = configureNeuropixelsV1f.DeviceName; DeviceAddress = configureNeuropixelsV1f.DeviceAddress; - InvertPolarity = configureNeuropixelsV1f.InvertPolarity; } /// @@ -60,29 +57,84 @@ public ConfigureNeuropixelsV1f(ConfigureNeuropixelsV1f configureNeuropixelsV1f) [Description("Specifies whether the NeuropixelsV1 device is enabled.")] public bool Enable { get; set; } = true; - /// - [Category(ConfigurationCategory)] - [Description("Invert the polarity of the electrode voltages acquired by the probe.")] - public bool InvertPolarity { get; set; } = true; + /// + /// Gets or sets a value determining if the polarity of the electrode voltages acquired by the probe + /// should be inverted. + /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// + [Browsable(false)] + [Externalizable(false)] + public bool InvertPolarity + { + get => ProbeConfiguration.InvertPolarity; + set => ProbeConfiguration.InvertPolarity = value; + } + + /// + /// Prevent the InvertPolarity property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeInvertPolarity() + { + return false; + } /// [Category(ConfigurationCategory)] [Description("NeuropixelsV1 probe configuration.")] + [TypeConverter(typeof(GenericPropertyConverter))] public NeuropixelsV1ProbeConfiguration ProbeConfiguration { get; set; } = new(); - /// - [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)] - [Category(ConfigurationCategory)] - public string GainCalibrationFile { get; set; } + /// + /// Gets or sets the path to the gain calibration file for this probe. + /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// + [Browsable(false)] + [Externalizable(false)] + public string GainCalibrationFile + { + get => ProbeConfiguration.GainCalibrationFileName; + set => ProbeConfiguration.GainCalibrationFileName = value; + } - /// - [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)] - [Category(ConfigurationCategory)] - public string AdcCalibrationFile { get; set; } + /// + /// Prevent the GainCalibrationFile property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeGainCalibrationFile() + { + return false; + } + + /// + /// Gets or sets the path to the ADC calibration file. + /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// + [Browsable(false)] + [Externalizable(false)] + public string AdcCalibrationFile + { + get => ProbeConfiguration.AdcCalibrationFileName; + set => ProbeConfiguration.AdcCalibrationFileName = value; + } + + /// + /// Prevent the AdcCalibrationFile property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeAdcCalibrationFile() + { + return false; + } /// /// Gets or sets the for this probe. @@ -105,7 +157,7 @@ public ConfigureNeuropixelsV1f(ConfigureNeuropixelsV1f configureNeuropixelsV1f) public override IObservable Process(IObservable source) { var enable = Enable; - var invertPolarity = InvertPolarity; + var invertPolarity = ProbeConfiguration.InvertPolarity; var deviceName = DeviceName; var deviceAddress = DeviceAddress; return source.ConfigureDevice(context => @@ -115,7 +167,7 @@ public override IObservable Process(IObservable source if (enable) { - var probeControl = new NeuropixelsV1fRegisterContext(device, ProbeConfiguration, GainCalibrationFile, AdcCalibrationFile, invertPolarity); + var probeControl = new NeuropixelsV1fRegisterContext(device, ProbeConfiguration); probeControl.InitializeProbe(); probeControl.WriteShiftRegisters(); } diff --git a/OpenEphys.Onix1/GenericPropertyConverter.cs b/OpenEphys.Onix1/GenericPropertyConverter.cs new file mode 100644 index 00000000..dd60bd69 --- /dev/null +++ b/OpenEphys.Onix1/GenericPropertyConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; +using System.Linq; + +namespace OpenEphys.Onix1 +{ + /// + /// Generic property converter that expands to show all properties. + /// + internal class GenericPropertyConverter : ExpandableObjectConverter + { + public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) + { + var properties = (from property in base.GetProperties(context, value, attributes).Cast() + where !property.IsReadOnly + select property) + .ToArray(); + return new PropertyDescriptorCollection(properties).Sort(properties.Select(p => p.Name).ToArray()); + } + } +} diff --git a/OpenEphys.Onix1/IConfigureNeuropixelsV1.cs b/OpenEphys.Onix1/IConfigureNeuropixelsV1.cs index cae08f44..851940ac 100644 --- a/OpenEphys.Onix1/IConfigureNeuropixelsV1.cs +++ b/OpenEphys.Onix1/IConfigureNeuropixelsV1.cs @@ -14,54 +14,5 @@ public interface IConfigureNeuropixelsV1 /// Gets or sets the probe configuration. /// public NeuropixelsV1ProbeConfiguration ProbeConfiguration { get; set; } - - /// - /// Gets or sets the path to the gain calibration file. - /// - /// - /// - /// Each probe is linked to a gain calibration file that contains gain adjustments determined by IMEC during - /// factory testing. Electrode voltages are scaled using these values to ensure they can be accurately compared - /// across probes. Therefore, using the correct gain calibration file is mandatory to create standardized recordings. - /// - /// - /// Calibration files are probe-specific and not interchangeable across probes. Calibration files must contain the - /// serial number of the corresponding probe on their first line of text. If you have lost track of a calibration - /// file for your probe, email IMEC at neuropixels.info@imec.be with the probe serial number to retrieve a new copy. - /// - /// - 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 probe-specific hardware settings that is - /// created by IMEC during factory calibration. These files are used to set internal bias currents, correct for ADC - /// nonlinearities, correct ADC-zero crossing non-monotonicities, etc. Using the correct calibration file is mandatory - /// for the probe to operate correctly. - /// - /// - /// Calibration files are probe-specific and not interchangeable across probes. Calibration files must contain the - /// serial number of the corresponding probe on their first line of text. If you have lost track of a calibration - /// file for your probe, email IMEC at neuropixels.info@imec.be with the probe serial number to retrieve a new copy. - /// - /// - public string AdcCalibrationFile { get; set; } - - /// - /// Gets or sets a value determining if the polarity of the electrode voltages acquired by the probe - /// should be inverted. - /// - /// - /// - /// Neuropixels contain inverting amplifiers. This means that neural data that is captured by the probe - /// will be inverted compared to the physical signal that occurs at the electrode: e.g., extracellular - /// action potentials will tend to have positive deflections instead of negative. Setting this - /// property to true will apply a gain of -1 to undo this effect. - /// - /// - public bool InvertPolarity { get; set; } } } diff --git a/OpenEphys.Onix1/JsonHelper.cs b/OpenEphys.Onix1/JsonHelper.cs new file mode 100644 index 00000000..f04e2611 --- /dev/null +++ b/OpenEphys.Onix1/JsonHelper.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; + +namespace OpenEphys.Onix1 +{ + internal static class JsonHelper + { + public static T DeserializeString(string jsonString) where T : class + { + var errors = new List(); + + var serializerSettings = new JsonSerializerSettings() + { + Error = delegate (object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args) + { + errors.Add(args.ErrorContext.Error.Message); + args.ErrorContext.Handled = true; + } + }; + + try + { + var obj = JsonConvert.DeserializeObject(jsonString, serializerSettings); + + if (errors.Count > 0) + { + Console.WriteLine("There were errors encountered while parsing a JSON string.\n"); + foreach (var e in errors) + { + Console.Error.WriteLine(e); + } + return null; + } + + return obj; + } + catch (JsonReaderException e) + { + throw new InvalidDataException("Invalid JSON format", e); + } + catch (JsonSerializationException e) + { + throw new InvalidDataException("Failed to deserialize JSON", e); + } + } + + public static void SerializeObject(object obj, string filepath) + { + if (string.IsNullOrEmpty(filepath)) + return; + + var serializerSettings = new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore, + }; + + var stringJson = JsonConvert.SerializeObject(obj, Formatting.Indented, serializerSettings); + + try + { + File.WriteAllText(filepath, stringJson); + } + catch (UnauthorizedAccessException e) + { + throw new IOException($"Access denied writing to '{filepath}'. Check file permissions.", e); + } + catch (DirectoryNotFoundException e) + { + throw new IOException($"Directory not found for '{filepath}'. Ensure the directory exists.", e); + } + catch (PathTooLongException e) + { + throw new IOException($"File path '{filepath}' exceeds system maximum length.", e); + } + catch (IOException e) + { + throw new IOException($"Unable to write to '{filepath}'. The file may be in use.", e); + } + catch (Exception e) + { + throw new IOException($"Unexpected error writing to '{filepath}'.", e); + } + } + } +} diff --git a/OpenEphys.Onix1/NeuropixelsV1ProbeConfiguration.cs b/OpenEphys.Onix1/NeuropixelsV1ProbeConfiguration.cs index f1d7f1bf..3591138b 100644 --- a/OpenEphys.Onix1/NeuropixelsV1ProbeConfiguration.cs +++ b/OpenEphys.Onix1/NeuropixelsV1ProbeConfiguration.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Text; +using System.Threading.Tasks; using System.Xml.Serialization; using Bonsai; using Newtonsoft.Json; @@ -43,7 +44,6 @@ public class NeuropixelsV1ProbeConfiguration /// public NeuropixelsV1ProbeConfiguration() { - ChannelMap = NeuropixelsV1eProbeGroup.ToChannelMap(ProbeGroup); } /// @@ -53,15 +53,30 @@ public NeuropixelsV1ProbeConfiguration() /// Desired or current for the spike-band. /// Desired or current for the LFP-band. /// Desired or current . - /// Desired or current option to filer the spike-band. - public NeuropixelsV1ProbeConfiguration(NeuropixelsV1Gain spikeAmplifierGain, NeuropixelsV1Gain lfpAmplifierGain, NeuropixelsV1ReferenceSource reference, bool spikeFilter) + /// Desired or current option to filter the spike-band. + /// Desired or current filepath to the ADC calibration file. + /// Desired or current filepath to the gain calibration file. + /// Desired or current option to invert the polarity of the signal. + /// Desired or current filepath to the ProbeInterface file. + public NeuropixelsV1ProbeConfiguration( + NeuropixelsV1Gain spikeAmplifierGain, + NeuropixelsV1Gain lfpAmplifierGain, + NeuropixelsV1ReferenceSource reference, + bool spikeFilter, + string adcCalibrationFileName, + string gainCalibrationFileName, + bool invertPolarity, + string probeInterfaceFileName + ) { SpikeAmplifierGain = spikeAmplifierGain; LfpAmplifierGain = lfpAmplifierGain; Reference = reference; SpikeFilter = spikeFilter; - ProbeGroup = new(); - ChannelMap = NeuropixelsV1eProbeGroup.ToChannelMap(ProbeGroup); + AdcCalibrationFileName = adcCalibrationFileName; + GainCalibrationFileName = gainCalibrationFileName; + InvertPolarity = invertPolarity; + ProbeInterfaceFileName = probeInterfaceFileName; } /// @@ -72,16 +87,32 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1Gain spikeAmplifierGain, Neu /// Desired or current for the spike-band. /// Desired or current for the LFP-band. /// Desired or current . - /// Desired or current option to filer the spike-band. - public NeuropixelsV1ProbeConfiguration(NeuropixelsV1eProbeGroup probeGroup, NeuropixelsV1Gain spikeAmplifierGain, NeuropixelsV1Gain lfpAmplifierGain, NeuropixelsV1ReferenceSource reference, bool spikeFilter) + /// Desired or current option to filter the spike-band. + /// Desired or current filepath to the ADC calibration file. + /// Desired or current filepath to the gain calibration file. + /// Desired or current option to invert the polarity of the signal. + /// Desired or current filepath to the ProbeInterface file. + public NeuropixelsV1ProbeConfiguration( + NeuropixelsV1eProbeGroup probeGroup, + NeuropixelsV1Gain spikeAmplifierGain, + NeuropixelsV1Gain lfpAmplifierGain, + NeuropixelsV1ReferenceSource reference, + bool spikeFilter, + string adcCalibrationFileName, + string gainCalibrationFileName, + bool invertPolarity, + string probeInterfaceFileName + ) { SpikeAmplifierGain = spikeAmplifierGain; LfpAmplifierGain = lfpAmplifierGain; Reference = reference; SpikeFilter = spikeFilter; - ChannelMap = NeuropixelsV1eProbeGroup.ToChannelMap(probeGroup); - ProbeGroup = new(); - ProbeGroup.UpdateDeviceChannelIndices(ChannelMap); + ProbeGroup = probeGroup.Clone(); + AdcCalibrationFileName = adcCalibrationFileName; + GainCalibrationFileName = gainCalibrationFileName; + InvertPolarity = invertPolarity; + ProbeInterfaceFileName = probeInterfaceFileName; } /// @@ -95,9 +126,11 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1ProbeConfiguration probeConf LfpAmplifierGain = probeConfiguration.LfpAmplifierGain; Reference = probeConfiguration.Reference; SpikeFilter = probeConfiguration.SpikeFilter; - ProbeGroup = new(); - ProbeGroup.UpdateDeviceChannelIndices(probeConfiguration.ChannelMap); - ChannelMap = NeuropixelsV1eProbeGroup.ToChannelMap(ProbeGroup); + ProbeGroup = probeConfiguration.ProbeGroup.Clone(); + AdcCalibrationFileName = probeConfiguration.AdcCalibrationFileName; + GainCalibrationFileName = probeConfiguration.GainCalibrationFileName; + InvertPolarity = probeConfiguration.InvertPolarity; + ProbeInterfaceFileName = probeConfiguration.ProbeInterfaceFileName; } /// @@ -107,7 +140,7 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1ProbeConfiguration probeConf /// 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("Configuration")] + [Category(DeviceFactory.ConfigurationCategory)] [Description("Amplifier gain for spike-band.")] public NeuropixelsV1Gain SpikeAmplifierGain { get; set; } = NeuropixelsV1Gain.Gain1000; @@ -117,7 +150,7 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1ProbeConfiguration probeConf /// /// The LFP band is from 0.5 to 500 Hz. /// - [Category("Configuration")] + [Category(DeviceFactory.ConfigurationCategory)] [Description("Amplifier gain for LFP-band.")] public NeuropixelsV1Gain LfpAmplifierGain { get; set; } = NeuropixelsV1Gain.Gain50; @@ -130,7 +163,7 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1ProbeConfiguration probeConf /// Setting to will use the external reference, while /// sets the reference to the electrode at the tip of the probe. /// - [Category("Configuration")] + [Category(DeviceFactory.ConfigurationCategory)] [Description("Reference selection.")] public NeuropixelsV1ReferenceSource Reference { get; set; } = NeuropixelsV1ReferenceSource.External; @@ -141,7 +174,7 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1ProbeConfiguration probeConf /// 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("Configuration")] + [Category(DeviceFactory.ConfigurationCategory)] [Description("If true, activates a 300 Hz high-pass filter in the spike-band data stream.")] public bool SpikeFilter { get; set; } = true; @@ -152,42 +185,197 @@ public NeuropixelsV1ProbeConfiguration(NeuropixelsV1ProbeConfiguration probeConf /// The channel map will always be 384 channels, and will return the 384 enabled electrodes. /// [XmlIgnore] - public NeuropixelsV1Electrode[] ChannelMap { get; } + public NeuropixelsV1Electrode[] ChannelMap { get => NeuropixelsV1eProbeGroup.ToChannelMap(ProbeGroup); } /// - /// Update the with the selected electrodes. + /// Gets or sets the path to the gain calibration file. /// - /// List of selected electrodes that are being added to the + /// + /// + /// Each probe is linked to a gain calibration file that contains gain adjustments determined by IMEC during + /// factory testing. Electrode voltages are scaled using these values to ensure they can be accurately compared + /// across probes. Therefore, using the correct gain calibration file is mandatory to create standardized recordings. + /// + /// + /// Calibration files are probe-specific and not interchangeable across probes. Calibration files must contain the + /// serial number of the corresponding probe on their first line of text. If you have lost track of a calibration + /// file for your probe, email IMEC at neuropixels.info@imec.be with the probe serial number to retrieve a new copy. + /// + /// + [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)] + [Category(DeviceFactory.ConfigurationCategory)] + public string GainCalibrationFileName { get; set; } + + /// + /// Gets or sets the path to the ADC calibration file. + /// + /// + /// + /// Each probe must be provided with an ADC calibration file that contains probe-specific hardware settings that is + /// created by IMEC during factory calibration. These files are used to set internal bias currents, correct for ADC + /// nonlinearities, correct ADC-zero crossing non-monotonicities, etc. Using the correct calibration file is mandatory + /// for the probe to operate correctly. + /// + /// + /// Calibration files are probe-specific and not interchangeable across probes. Calibration files must contain the + /// serial number of the corresponding probe on their first line of text. If you have lost track of a calibration + /// file for your probe, email IMEC at neuropixels.info@imec.be with the probe serial number to retrieve a new copy. + /// + /// + [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)] + [Category(DeviceFactory.ConfigurationCategory)] + public string AdcCalibrationFileName { get; set; } + + string probeInterfaceFileName; + + /// + /// Gets or sets the file path where the ProbeInterface configuration will be saved. + /// + /// + /// If left empty, the ProbeInterface configuration will not be saved. + /// + [XmlIgnore] + [Category(DeviceFactory.ConfigurationCategory)] + [Description("File path to where the ProbeInterface file will be saved for this probe. If the file exists, it will be overwritten.")] + [FileNameFilter(ProbeInterfaceHelper.ProbeInterfaceFileNameFilter)] + [Editor("Bonsai.Design.SaveFileNameEditor, Bonsai.Design", DesignTypes.UITypeEditor)] + public string ProbeInterfaceFileName + { + get => probeInterfaceFileName; + set => probeInterfaceFileName = value; + } + + /// + /// Gets or sets the ProbeInterface file name, loading the given file asynchronously when set. + /// + [XmlIgnore] + [Browsable(false)] + [Externalizable(false)] + public string ProbeInterfaceLoadFileName + { + get => probeInterfaceFileName; + set + { + probeInterfaceFileName = value; + probeGroupTask = Task.Run(() => + { + if (string.IsNullOrEmpty(probeInterfaceFileName)) + return new NeuropixelsV1eProbeGroup(); + + return ProbeInterfaceHelper.LoadExternalProbeInterfaceFile(probeInterfaceFileName); + }); + } + } + + /// + /// Gets or sets a string defining the path to an external ProbeInterface JSON file. + /// This variable is needed to properly save a workflow in Bonsai, but it is not + /// directly accessible in the Bonsai editor. + /// + [Browsable(false)] + [Externalizable(false)] + [XmlElement(nameof(ProbeInterfaceFileName))] + public string ProbeInterfaceFileNameSerialize + { + get + { + if (string.IsNullOrEmpty(ProbeInterfaceFileName)) + return ""; + + if (probeGroup != null) + ProbeInterfaceHelper.SaveExternalProbeInterfaceFile(ProbeGroup, ProbeInterfaceFileName); + + return ProbeInterfaceFileName; + } + set => ProbeInterfaceLoadFileName = value; + } + + /// + /// Gets or sets a value determining if the polarity of the electrode voltages acquired by the probe + /// should be inverted. + /// + /// + /// + /// Neuropixels contain inverting amplifiers. This means that neural data that is captured by the probe + /// will be inverted compared to the physical signal that occurs at the electrode: e.g., extracellular + /// action potentials will tend to have positive deflections instead of negative. Setting this + /// property to true will apply a gain of -1 to undo this effect. + /// + /// + [Category(DeviceFactory.ConfigurationCategory)] + [Description("Invert the polarity of the electrode voltages acquired by the probe.")] + public bool InvertPolarity { get; set; } + + /// + /// Enable the selected electrodes. + /// + /// List of selected electrodes that are being enabled. public void SelectElectrodes(NeuropixelsV1Electrode[] electrodes) { + var channelMap = ChannelMap; + foreach (var e in electrodes) { try { - ChannelMap[e.Channel] = e; - } catch (IndexOutOfRangeException ex) + channelMap[e.Channel] = e; + } + catch (IndexOutOfRangeException ex) { throw new IndexOutOfRangeException($"Electrode {e.Index} specifies channel {e.Channel} but only channels " + - $"0 to {ChannelMap.Length - 1} are supported.", ex); + $"0 to {channelMap.Length - 1} are supported.", ex); } } - ProbeGroup.UpdateDeviceChannelIndices(ChannelMap); + ProbeGroup.UpdateDeviceChannelIndices(channelMap); } + Task probeGroupTask = null; + + NeuropixelsV1eProbeGroup probeGroup = null; + /// /// Gets or sets the channel configuration for this probe. /// [XmlIgnore] - [Category("Configuration")] + [Category(DeviceFactory.ConfigurationCategory)] [Description("Defines all aspects of the probe group, including probe contours, electrode size and location, enabled channels, etc.")] - public NeuropixelsV1eProbeGroup ProbeGroup { get; set; } = new(); + [Browsable(false)] + [Externalizable(false)] + public NeuropixelsV1eProbeGroup ProbeGroup + { + get + { + if (probeGroup == null) + { + try + { + probeGroup = probeGroupTask?.Result ?? new NeuropixelsV1eProbeGroup(); + } + catch (AggregateException ae) + { + probeGroup = new(); + throw new InvalidOperationException($"There was an error loading the ProbeInterface file, loading the default configuration instead.\n\nError: {ae.InnerException.Message}", ae.InnerException); + } + } + + return probeGroup; + } + set => probeGroup = value; + } /// /// Gets or sets a string defining the in Base64. /// This variable is needed to properly save a workflow in Bonsai, but it is not /// directly accessible in the Bonsai editor. /// + /// + /// [Obsolete]. Cannot tag this property with the Obsolete attribute due to https://github.com/dotnet/runtime/issues/100453 + /// [Browsable(false)] [Externalizable(false)] [XmlElement(nameof(ProbeGroup))] @@ -205,5 +393,15 @@ public string ProbeGroupString SelectElectrodes(NeuropixelsV1eProbeGroup.ToChannelMap(ProbeGroup)); } } + + /// + /// Prevent the ProbeGroup property from being serialized. + /// + /// False + [Obsolete] + public bool ShouldSerializeProbeGroupString() + { + return false; + } } } diff --git a/OpenEphys.Onix1/NeuropixelsV1eProbeGroup.cs b/OpenEphys.Onix1/NeuropixelsV1eProbeGroup.cs index ac9047f3..9135773b 100644 --- a/OpenEphys.Onix1/NeuropixelsV1eProbeGroup.cs +++ b/OpenEphys.Onix1/NeuropixelsV1eProbeGroup.cs @@ -64,6 +64,11 @@ public NeuropixelsV1eProbeGroup(NeuropixelsV1eProbeGroup probeGroup) { } + internal NeuropixelsV1eProbeGroup Clone() + { + return new NeuropixelsV1eProbeGroup(Specification, Version, Probes.Select(probe => new Probe(probe)).ToArray()); + } + /// /// Generates a 2D array of default contact positions based on the given number of channels. /// diff --git a/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs b/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs index 345c90a6..411ff539 100644 --- a/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs +++ b/OpenEphys.Onix1/NeuropixelsV1eRegisterContext.cs @@ -19,26 +19,26 @@ class NeuropixelsV1eRegisterContext : I2CRegisterContext readonly BitArray[] BaseConfigs; public NeuropixelsV1eRegisterContext(DeviceContext deviceContext, uint i2cAddress, ulong probeSerialNumber, - NeuropixelsV1ProbeConfiguration probeConfiguration, string gainCalibrationFile, string adcCalibrationFile) + NeuropixelsV1ProbeConfiguration probeConfiguration) : base(deviceContext, i2cAddress) { - if (!File.Exists(gainCalibrationFile)) + if (!File.Exists(probeConfiguration.GainCalibrationFileName)) { throw new ArgumentException($"A gain calibration file must be specified for the probe with serial number " + $"{probeSerialNumber}"); } - if (!File.Exists(adcCalibrationFile)) + if (!File.Exists(probeConfiguration.AdcCalibrationFileName)) { throw new ArgumentException($"An ADC calibration file must be specified for the probe with serial number " + $"{probeSerialNumber}"); } - var adcCalibration = NeuropixelsV1Helper.TryParseAdcCalibrationFile(adcCalibrationFile); + var adcCalibration = NeuropixelsV1Helper.TryParseAdcCalibrationFile(probeConfiguration.AdcCalibrationFileName); if (!adcCalibration.HasValue) { - throw new ArgumentException($"The calibration file \"{adcCalibrationFile}\" is invalid."); + throw new ArgumentException($"The calibration file \"{probeConfiguration.AdcCalibrationFileName}\" is invalid."); } if (adcCalibration.Value.SerialNumber != probeSerialNumber) @@ -47,12 +47,12 @@ public NeuropixelsV1eRegisterContext(DeviceContext deviceContext, uint i2cAddres $"match the ADC calibration file serial number ({adcCalibration.Value.SerialNumber})."); } - var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(gainCalibrationFile, + var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(probeConfiguration.GainCalibrationFileName, probeConfiguration.SpikeAmplifierGain, probeConfiguration.LfpAmplifierGain, NeuropixelsV1.ElectrodeCount); if (!gainCorrection.HasValue) { - throw new ArgumentException($"The calibration file \"{gainCalibrationFile}\" is invalid."); + throw new ArgumentException($"The calibration file \"{probeConfiguration.GainCalibrationFileName}\" is invalid."); } if (gainCorrection.Value.SerialNumber != probeSerialNumber) diff --git a/OpenEphys.Onix1/NeuropixelsV1fRegisterContext.cs b/OpenEphys.Onix1/NeuropixelsV1fRegisterContext.cs index 17d267ef..489f6c88 100644 --- a/OpenEphys.Onix1/NeuropixelsV1fRegisterContext.cs +++ b/OpenEphys.Onix1/NeuropixelsV1fRegisterContext.cs @@ -21,29 +21,29 @@ class NeuropixelsV1fRegisterContext : I2CRegisterContext readonly BitArray ShankConfig; readonly BitArray[] BaseConfigs; - public NeuropixelsV1fRegisterContext(DeviceContext deviceContext, NeuropixelsV1ProbeConfiguration configuration, string gainCalibrationFile, string adcCalibrationFile, bool invertPolarity) + public NeuropixelsV1fRegisterContext(DeviceContext deviceContext, NeuropixelsV1ProbeConfiguration configuration) : base(deviceContext, NeuropixelsV1.ProbeI2CAddress) { device = deviceContext; var metaData = new NeuropixelsV1fMetadata(device); - if (!File.Exists(gainCalibrationFile)) + if (!File.Exists(configuration.GainCalibrationFileName)) { throw new ArgumentException($"A gain calibration file must be specified for the probe with serial number " + $"{metaData.ProbeSerialNumber}"); } - if (!File.Exists(adcCalibrationFile)) + if (!File.Exists(configuration.AdcCalibrationFileName)) { throw new ArgumentException($"An ADC calibration file must be specified for the probe with serial number " + $"{metaData.ProbeSerialNumber}"); } - var adcCalibration = NeuropixelsV1Helper.TryParseAdcCalibrationFile(adcCalibrationFile); + var adcCalibration = NeuropixelsV1Helper.TryParseAdcCalibrationFile(configuration.AdcCalibrationFileName); if (!adcCalibration.HasValue) { - throw new ArgumentException($"The calibration file \"{adcCalibrationFile}\" is invalid."); + throw new ArgumentException($"The calibration file \"{configuration.AdcCalibrationFileName}\" is invalid."); } if (adcCalibration.Value.SerialNumber != metaData.ProbeSerialNumber) @@ -52,13 +52,13 @@ public NeuropixelsV1fRegisterContext(DeviceContext deviceContext, NeuropixelsV1P $"match the ADC calibration file serial number ({adcCalibration.Value.SerialNumber})."); } - var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(gainCalibrationFile, + var gainCorrection = NeuropixelsV1Helper.TryParseGainCalibrationFile(configuration.GainCalibrationFileName, configuration.SpikeAmplifierGain, configuration.LfpAmplifierGain, NeuropixelsV1.ElectrodeCount); if (!gainCorrection.HasValue) { - throw new ArgumentException($"The calibration file \"{gainCalibrationFile}\" is invalid."); + throw new ArgumentException($"The calibration file \"{configuration.GainCalibrationFileName}\" is invalid."); } if (gainCorrection.Value.SerialNumber != metaData.ProbeSerialNumber) @@ -67,8 +67,8 @@ public NeuropixelsV1fRegisterContext(DeviceContext deviceContext, NeuropixelsV1P $"match the gain calibration file serial number ({gainCorrection.Value.SerialNumber})."); } - ApGainCorrection = invertPolarity ? -gainCorrection.Value.ApGainCorrectionFactor : gainCorrection.Value.ApGainCorrectionFactor; - LfpGainCorrection = invertPolarity ? -gainCorrection.Value.LfpGainCorrectionFactor : gainCorrection.Value.LfpGainCorrectionFactor; + ApGainCorrection = configuration.InvertPolarity ? -gainCorrection.Value.ApGainCorrectionFactor : gainCorrection.Value.ApGainCorrectionFactor; + LfpGainCorrection = configuration.InvertPolarity ? -gainCorrection.Value.LfpGainCorrectionFactor : gainCorrection.Value.LfpGainCorrectionFactor; Adcs = adcCalibration.Value.Adcs; AdcThresholds = Adcs.ToList().Select(a => (ushort)a.Threshold).ToArray(); diff --git a/OpenEphys.Onix1/OpenEphys.Onix1.csproj b/OpenEphys.Onix1/OpenEphys.Onix1.csproj index 5526c48e..984dce3f 100644 --- a/OpenEphys.Onix1/OpenEphys.Onix1.csproj +++ b/OpenEphys.Onix1/OpenEphys.Onix1.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/OpenEphys.Onix1/ProbeInterfaceHelper.cs b/OpenEphys.Onix1/ProbeInterfaceHelper.cs new file mode 100644 index 00000000..ad408250 --- /dev/null +++ b/OpenEphys.Onix1/ProbeInterfaceHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using OpenEphys.ProbeInterface.NET; + +namespace OpenEphys.Onix1 +{ + internal static class ProbeInterfaceHelper + { + public const string ProbeInterfaceFileNameFilter = "ProbeInterface Files|*.json|All Files|*.*"; + + public static T LoadExternalProbeInterfaceFile(string probeInterfaceFileName) where T : ProbeGroup + { + if (string.IsNullOrEmpty(probeInterfaceFileName)) + { + throw new ArgumentNullException(nameof(probeInterfaceFileName), "ProbeInterface file path cannot be null or empty."); + } + + if (!File.Exists(probeInterfaceFileName)) + { + throw new FileNotFoundException($"The ProbeInterface file '{probeInterfaceFileName}' does not exist."); + } + + try + { + string jsonContent = File.ReadAllText(probeInterfaceFileName); + var result = JsonHelper.DeserializeString(jsonContent) ?? throw new InvalidDataException($"Failed to parse ProbeInterface file: {probeInterfaceFileName}"); + return result; + } + catch (UnauthorizedAccessException e) + { + throw new IOException($"Access denied reading '{probeInterfaceFileName}'. Check file permissions.", e); + } + catch (PathTooLongException e) + { + throw new IOException($"File path '{probeInterfaceFileName}' exceeds system maximum length.", e); + } + catch (IOException e) + { + throw new IOException($"Unable to read '{probeInterfaceFileName}'. The file may be in use.", e); + } + catch (Exception e) + { + throw new IOException($"Unexpected error reading '{probeInterfaceFileName}'.", e); + } + } + + public static void SaveExternalProbeInterfaceFile(ProbeGroup probeGroup, string probeInterfaceFile) + { + JsonHelper.SerializeObject(probeGroup, probeInterfaceFile); + } + } +} diff --git a/OpenEphys.Onix1/Rhs2116StimulusSequence.cs b/OpenEphys.Onix1/Rhs2116StimulusSequence.cs index 998e2ac6..f02e5800 100644 --- a/OpenEphys.Onix1/Rhs2116StimulusSequence.cs +++ b/OpenEphys.Onix1/Rhs2116StimulusSequence.cs @@ -5,8 +5,6 @@ using System.Xml.Serialization; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("OpenEphys.Onix1.Design")] - namespace OpenEphys.Onix1 { /// diff --git a/OpenEphys.Onix1/Rhs2116StimulusSequencePair.cs b/OpenEphys.Onix1/Rhs2116StimulusSequencePair.cs index 05bbb0f9..06015b62 100644 --- a/OpenEphys.Onix1/Rhs2116StimulusSequencePair.cs +++ b/OpenEphys.Onix1/Rhs2116StimulusSequencePair.cs @@ -3,8 +3,6 @@ using System.Xml.Serialization; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("OpenEphys.Onix1.Design")] - namespace OpenEphys.Onix1 { ///