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));
+ });
+ }
+ }
+}