diff --git a/Interface/Harp.Olfactometer/ConfigureOdorMix.cs b/Interface/Harp.Olfactometer/ConfigureOdorMix.cs
new file mode 100644
index 0000000..a9b20d6
--- /dev/null
+++ b/Interface/Harp.Olfactometer/ConfigureOdorMix.cs
@@ -0,0 +1,219 @@
+using Bonsai;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Bonsai.Harp;
+using System.Collections.Generic;
+
+
+namespace Harp.Olfactometer
+{
+ ///
+ /// Represents an operator that generates a sequence of Harp messages to
+ /// configure the odor mixture in the olfactometer as corresponding odor
+ /// valves.
+ /// The configuration will set the target flow for each channel, assuming
+ /// a constant target full flow, target odor flow and percentage of each odor.
+ ///
+ [Description("Generates a sequence of Harp messages to configure an odor mixture.")]
+
+ public class ConfigureOdorMix : Source
+ {
+ ///
+ /// Gets or sets the concentration of Channel0.
+ ///
+ [Range(0, 1)]
+ [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)]
+ [Description("Odor dilution percentage for channel 0.")]
+ public float PercentageChannel0 { get; set; } = 0;
+
+ ///
+ /// Gets or sets the concentration of Channel1.
+ ///
+ [Range(0, 1)]
+ [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)]
+ [Description("Odor dilution percentage for channel 1.")]
+ public float PercentageChannel1 { get; set; } = 0;
+
+ ///
+ /// Gets or sets the concentration of Channel2.
+ ///
+ [Range(0, 1)]
+ [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)]
+ [Description("Odor dilution percentage for channel 2.")]
+ public float PercentageChannel2 { get; set; } = 0;
+
+ ///
+ /// Gets or sets the concentration of Channel3.
+ ///
+ [Range(0, 1)]
+ [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)]
+ [Description("Odor dilution percentage for channel 3. This Value will be ignored if Channel3AsCarrier is set to True.")]
+ public float PercentageChannel3 { get; set; } = 0;
+
+
+ public bool channel3AsCarrier = false;
+ ///
+ /// Gets or sets the operation mode of Channel3.
+ ///
+ [Description("Specifies if Channel3 should be used as an odor or carrier channel. If True, the flow value value of Channel3 will be set to TargetTotalFlow.")]
+ public bool Channel3AsCarrier
+ {
+ get { return channel3AsCarrier; }
+ set
+ {
+ channel3AsCarrier = value;
+ PercentageChannel3 = channel3AsCarrier ? float.NaN : 0;
+ }
+ }
+
+ ///
+ /// Gets or sets the target flow rate for all odor channels.
+ ///
+ [Range(0, 100)]
+ [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)]
+ [Description("The target odor flow for each channel, assuming PercentageChannelX = 1.")]
+ public int TargetOdorFlow { get; set; } = 100;
+
+
+ ///
+ /// Gets or sets the target total flow rate.
+ ///
+ [Range(0, 100)]
+ [Editor(DesignTypes.SliderEditor, DesignTypes.UITypeEditor)]
+ [Description("The total target flow rate. This value will be used to calculate the flow rate of the carrier automatically.")]
+ public int TargetTotalFlow { get; set; } = 1000;
+
+ private List ConstructMessages()
+ {
+ var adjustedOdorFlow0 = (int)(TargetOdorFlow * PercentageChannel0);
+ var adjustedOdorFlow1 = (int)(TargetOdorFlow * PercentageChannel1);
+ var adjustedOdorFlow2 = (int)(TargetOdorFlow * PercentageChannel2);
+ var adjustedOdorFlow3 = channel3AsCarrier ? 0 : (int)(TargetOdorFlow * PercentageChannel3);
+ var carrierFlow = TargetOdorFlow - (adjustedOdorFlow0 + adjustedOdorFlow1 + adjustedOdorFlow2 + adjustedOdorFlow3);
+
+ var channelsTargetFlow = ChannelsTargetFlow.FromPayload(MessageType.Write, new ChannelsTargetFlowPayload(
+ adjustedOdorFlow0,
+ adjustedOdorFlow1,
+ adjustedOdorFlow2,
+ channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3,
+ carrierFlow));
+
+ adjustedOdorFlow3 = channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3;
+ var odorValveState = OdorValveState.FromPayload(MessageType.Write,
+ (
+ (adjustedOdorFlow0 > 0 ? OdorValves.Valve0 : OdorValves.None) |
+ (adjustedOdorFlow1 > 0 ? OdorValves.Valve1 : OdorValves.None) |
+ (adjustedOdorFlow2 > 0 ? OdorValves.Valve2 : OdorValves.None) |
+ (adjustedOdorFlow3 > 0 ? OdorValves.Valve3 : OdorValves.None)
+ ));
+
+ return new List { channelsTargetFlow, odorValveState };
+ }
+
+ private List ConstructMessages(int odorIndex, double concentration)
+ {
+
+ var adjustedOdorFlow0 = 0;
+ var adjustedOdorFlow1 = 0;
+ var adjustedOdorFlow2 = 0;
+ var adjustedOdorFlow3 = 0;
+
+ switch (odorIndex)
+ {
+ case 0:
+ adjustedOdorFlow0 = (int)(TargetOdorFlow * concentration);
+ break;
+ case 1:
+ adjustedOdorFlow1 = (int)(TargetOdorFlow * concentration);
+ break;
+ case 2:
+ adjustedOdorFlow2 = (int)(TargetOdorFlow * concentration);
+ break;
+ case 3:
+ if (channel3AsCarrier)
+ {
+ throw new Exception("Channel 3 is set as carrier. Cannot set flow for this channel.");
+ }
+ adjustedOdorFlow3 = (int)(TargetOdorFlow * concentration);
+ break;
+ default:
+ throw new Exception("Invalid channel number. Must be between 0 and 3.");
+ }
+
+ var carrierFlow = TargetOdorFlow - (adjustedOdorFlow0 + adjustedOdorFlow1 + adjustedOdorFlow2 + adjustedOdorFlow3);
+
+ var channelsTargetFlow = ChannelsTargetFlow.FromPayload(MessageType.Write, new ChannelsTargetFlowPayload(
+ adjustedOdorFlow0,
+ adjustedOdorFlow1,
+ adjustedOdorFlow2,
+ channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3,
+ carrierFlow));
+
+ adjustedOdorFlow3 = channel3AsCarrier ? TargetOdorFlow : adjustedOdorFlow3;
+ var odorValveState = OdorValveState.FromPayload(MessageType.Write,
+ (
+ (adjustedOdorFlow0 > 0 ? OdorValves.Valve0 : OdorValves.None) |
+ (adjustedOdorFlow1 > 0 ? OdorValves.Valve1 : OdorValves.None) |
+ (adjustedOdorFlow2 > 0 ? OdorValves.Valve2 : OdorValves.None) |
+ (adjustedOdorFlow3 > 0 ? OdorValves.Valve3 : OdorValves.None)
+ ));
+
+ return new List { channelsTargetFlow, odorValveState };
+ }
+
+
+ ///
+ /// Generates an observable sequence of Harp messages to configure the
+ /// odor mixture whenever the input sequence produces an element.
+ ///
+ ///
+ /// The type of the elements in the sequence.
+ ///
+ ///
+ /// The sequence containing the notifications used to emit new configuration
+ /// messages.
+ ///
+ ///
+ /// A sequence of objects representing the commands
+ /// needed to fully configure odor mixture.
+ ///
+ public IObservable Generate(IObservable source)
+ {
+ return source.SelectMany(value => ConstructMessages().ToObservable());
+ }
+
+ ///
+ /// Generates an observable sequence of Harp messages to configure the
+ /// odor mixture.
+ ///
+ ///
+ /// A sequence of objects representing the commands
+ /// needed to fully configure the PWM feature.
+ ///
+ public override IObservable Generate()
+ {
+ return ConstructMessages().ToObservable();
+ }
+
+ ///
+ /// Generates an observable sequence of Harp messages to configure the
+ /// odor mixture whenever the input sequence produces an element. The
+ /// tuple values will be used to target a specific channel index (Item1)
+ /// and set its concentration (Item2).
+ ///
+ ///
+ /// The sequence containing a tuple with the channel index and concentration.
+ ///
+ ///
+ /// A sequence of objects representing the commands
+ /// needed to fully configure odor mixture.
+ ///
+ public IObservable Generate(IObservable> source)
+ {
+ return source.SelectMany(value => ConstructMessages(value.Item1, value.Item2));
+ }
+ }
+
+}