diff --git a/OpenEphys.Onix1/ConfigureBreakoutBoard.cs b/OpenEphys.Onix1/ConfigureBreakoutBoard.cs index ccbdd26..65e16db 100644 --- a/OpenEphys.Onix1/ConfigureBreakoutBoard.cs +++ b/OpenEphys.Onix1/ConfigureBreakoutBoard.cs @@ -47,6 +47,13 @@ public class ConfigureBreakoutBoard : MultiDeviceFactory public ConfigureBreakoutDigitalIO DigitalIO { get; set; } = new(); /// + /// Gets or sets the breakout board's output clock configuration. + /// + [TypeConverter(typeof(SingleDeviceFactoryConverter))] + [Description("Specifies the configuration for the clock output in the ONIX breakout board.")] + [Category(DevicesCategory)] + public ConfigureOutputClock ClockOutput { get; set; } = new(); + /// Gets or sets the the Harp synchronization input configuration. /// [TypeConverter(typeof(SingleDeviceFactoryConverter))] @@ -67,6 +74,7 @@ internal override IEnumerable GetDevices() yield return Heartbeat; yield return AnalogIO; yield return DigitalIO; + yield return ClockOutput; yield return HarpInput; yield return MemoryMonitor; } diff --git a/OpenEphys.Onix1/ConfigureOutputClock.cs b/OpenEphys.Onix1/ConfigureOutputClock.cs new file mode 100644 index 0000000..1d17d61 --- /dev/null +++ b/OpenEphys.Onix1/ConfigureOutputClock.cs @@ -0,0 +1,223 @@ +using System; +using System.ComponentModel; +using System.Drawing.Design; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Configures the ONIX breakout board's output clock. + /// + /// + /// The output clock provides a 3.3V logic level, 50 Ohm output impedance, frequency divided copy + /// of the Acquisition Clock that is used to generate + /// values for all data streams within an ONIX system. This clock runs at a + /// user defined rate, duty cycle, and start delay. It can be used to drive external hardware or can be + /// logged by external recording systems for post-hoc synchronization with ONIX data. + /// + [Description("Configures the ONIX breakout board's output clock.")] + public class ConfigureOutputClock : SingleDeviceFactory + { + readonly BehaviorSubject gate = new(false); + double frequencyHz = 1e6; + double dutyCycle = 50; + + /// + /// Initializes a new instance of the class. + /// + public ConfigureOutputClock() + : base(typeof(OutputClock)) + { + DeviceAddress = 5; + } + + /// + /// Gets or sets a value specifying if the output clock is active. + /// + /// + /// If set to true, the clock output will be connected to the clock output line. If set to false, the + /// clock output line will be held low. This value can be toggled in real time to gate acquisition of + /// external hardware. + /// + [Category(AcquisitionCategory)] + [Description("Clock gate control signal.")] + public bool ClockGate + { + get => gate.Value; + set => gate.OnNext(value); + } + + /// + /// Gets or sets the output clock frequency in Hz. + /// + /// + /// Valid values are between 0.1 Hz and 10 MHz. The output clock high and low times must each be an + /// integer multiple of the Acquisition Clock + /// frequency. Therefore, the true clock frequency will be set to a value that is as close as possible + /// to the requested setting while respecting this constraint. The value as actualized in hardware is + /// reported by . + /// + [Range(0.1, 10e6)] + [Category(ConfigurationCategory)] + [Description("Frequency of the output clock (Hz).")] + public double Frequency + { + get => frequencyHz; + set => frequencyHz = value >= 0.1 && value <= 10e6 + ? value + : throw new ArgumentOutOfRangeException(nameof(Frequency), value, + $"{nameof(Frequency)} must be between 0.1 Hz and 10 MHz."); + } + + /// + /// Gets or sets the output clock duty cycle in percent. + /// + /// + /// Valid values are between 10% and 90%. The output clock high and low times must each be an integer + /// multiple of the Acquisition Clock frequency. + /// Therefore, the true duty cycle will be set to a value that is as close as possible to the + /// requested setting while respecting this constraint. The value as actualized in hardware is + /// reported by . + /// + [Range(10, 90)] + [Editor(DesignTypes.SliderEditor, typeof(UITypeEditor))] + [Category(ConfigurationCategory)] + [Precision(1, 1)] + [Description("Duty cycle of output clock (%).")] + public double DutyCycle + { + get => dutyCycle; + set => dutyCycle = value >= 10 && value <= 90 + ? value + : throw new ArgumentOutOfRangeException(nameof(DutyCycle), value, + $"{nameof(DutyCycle)} must be between 10% and 90%."); + } + + /// + /// Gets or sets the delay following acquisition commencement before the clock becomes active in + /// seconds. + /// + /// + /// + /// Valid values are between 0 and and 3600 seconds. Setting to a value greater than 0 can be useful + /// for ensuring data sources that are driven by the output clock start significantly after ONIX has + /// begun acquisition for the purposes of ordering acquisition start times. + /// + /// + /// The delay must be an integer multiple of the Acquisition Clock frequency. Therefore, the true delay + /// cycle will be set to a value that is as close as possible to the requested setting while + /// respecting this constraint. The value as actualized in hardware is reported by . + /// + /// + [Category(ConfigurationCategory)] + [Description("Specifies a delay following acquisition start before the clock becomes active (sec).")] + [Range(0, 3600)] + public double Delay { get; set; } = 0; + + /// + /// Configures a clock output. + /// + /// + /// 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 clock output device./> + public override IObservable Process(IObservable source) + { + var clkFreqHz = Frequency; + var dutyCycle = DutyCycle; + var delaySeconds = Delay; + var deviceName = DeviceName; + var deviceAddress = DeviceAddress; + + return source.ConfigureDevice((context, observer) => + { + var device = context.GetDeviceContext(deviceAddress, DeviceType); + + var baseFreqHz = device.ReadRegister(OutputClock.BASE_FREQ_HZ); + var periodTicks = (uint)(baseFreqHz / clkFreqHz); + var h = (uint)(periodTicks * (dutyCycle / 100)); + var l = periodTicks - h; + var delayTicks = (uint)(delaySeconds * baseFreqHz); + device.WriteRegister(OutputClock.HIGH_CYCLES, h); + device.WriteRegister(OutputClock.LOW_CYCLES, l); + device.WriteRegister(OutputClock.DELAY_CYCLES, delayTicks); + + var deviceInfo = new OutputClockDeviceInfo(device, DeviceType, + new((double)baseFreqHz / periodTicks, 100.0 * h / periodTicks, delaySeconds, h + l, h, l, delayTicks)); + + var shutdown = Disposable.Create(() => + { + device.WriteRegister(OutputClock.CLOCK_GATE, 0u); + }); + + return new CompositeDisposable( + DeviceManager.RegisterDevice(deviceName, deviceInfo), + gate.SubscribeSafe(observer, value => device.WriteRegister(OutputClock.CLOCK_GATE, value ? 1u : 0u)), + shutdown + ); + }); + } + } + + static class OutputClock + { + public const int ID = 20; + + public const uint NULL = 0; // No command + public const uint CLOCK_GATE = 1; // Output gate. Bit 0 = 0 is disabled, Bit 0 = 1 is enabled. + public const uint HIGH_CYCLES = 2; // Number of input clock cycles output clock should be high. Valid values are 1 or greater. + public const uint LOW_CYCLES = 3; // Number of input clock cycles output clock should be low. Valid values are 1 or greater. + public const uint DELAY_CYCLES = 4; // Delay, in input clock cycles, following reset before clock becomes active. + public const uint GATE_RUN = 5; // LSB sets the gate using run status. Bit 0 = 0: Clock runs whenever CLOCK_GATE(0) is 1. Bit 0 = 1: Clock runs only when acquisition is in RUNNING state. + public const uint BASE_FREQ_HZ = 6; // Frequency of the input clock in Hz. + + internal class NameConverter : DeviceNameConverter + { + public NameConverter() + : base(typeof(OutputClock)) + { + } + } + } + + /// + /// Hardware-verified output clock parameters. + /// + /// Gets the exact clock frequency as actualized by the clock synthesizer in + /// Hz. + /// Gets the exact clock duty cycle as actualized by the clock synthesizer + /// in percent. + /// Gets the exact clock delay as actualized by the clock synthesizer in + /// seconds. + /// Gets the exact clock period as actualized by the clock synthesizer in units + /// of ticks of the Acquisition Clock. + /// Gets the exact clock high time per period as actualized by the clock + /// synthesizer in units of ticks of the Acquisition + /// Clock. + /// Gets the exact clock low time per period as actualized by the clock synthesizer + /// in units of ticks of the Acquisition + /// Clock. + /// Gets the exact clock delay as actualized by the clock synthesizer in units of + /// ticks of the Acquisition Clock. + public readonly record struct OutputClockParameters(double Frequency, + double DutyCycle, double Delay, uint PeriodTicks, uint HighTicks, uint LowTicks, uint DelayTicks); + + class OutputClockDeviceInfo : DeviceInfo + { + public OutputClockDeviceInfo(DeviceContext device, Type deviceType, OutputClockParameters parameters) + : base(device, deviceType) + { + Parameters = parameters; + } + + public OutputClockParameters Parameters { get; } + } +} diff --git a/OpenEphys.Onix1/OutputClockData.cs b/OpenEphys.Onix1/OutputClockData.cs new file mode 100644 index 0000000..417823e --- /dev/null +++ b/OpenEphys.Onix1/OutputClockData.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Produces a sequence with a single element containing the output clock's exact hardware parameters for each subscription. + /// + /// + /// This data IO operator must be linked to an appropriate configuration, such as a , using a shared DeviceName. + /// + [Description("Produces a sequence with a single element containing the output clock's hardware parameters for each subscription.")] + public class OutputClockData : Source + { + /// + [TypeConverter(typeof(OutputClock.NameConverter))] + [Description(SingleDeviceFactory.DeviceNameDescription)] + [Category(DeviceFactory.ConfigurationCategory)] + public string DeviceName { get; set; } + + /// + /// Generates a sequence containing a single structure. + /// + /// A sequence containing a single structure. + public override IObservable Generate() + { + return DeviceManager.GetDevice(DeviceName).SelectMany( + deviceInfo => + { + var clockOutDeviceInfo = (OutputClockDeviceInfo)deviceInfo; + return Observable.Defer(() => Observable.Return(clockOutDeviceInfo.Parameters)); + }); + } + } +}