diff --git a/src/Aeon.Foraging/Aeon.Foraging.csproj b/src/Aeon.Foraging/Aeon.Foraging.csproj index 013a26f..cb386c4 100644 --- a/src/Aeon.Foraging/Aeon.Foraging.csproj +++ b/src/Aeon.Foraging/Aeon.Foraging.csproj @@ -7,7 +7,7 @@ Bonsai Rx Project Aeon Foraging net472 0.1.0 - build231009 + build231010 diff --git a/src/Aeon.Foraging/Aeon.Foraging.csproj.user b/src/Aeon.Foraging/Aeon.Foraging.csproj.user index 9c54aa0..746aabd 100644 --- a/src/Aeon.Foraging/Aeon.Foraging.csproj.user +++ b/src/Aeon.Foraging/Aeon.Foraging.csproj.user @@ -2,7 +2,7 @@ - + UserControl diff --git a/src/Aeon.Foraging/CreateDispenserEvent.cs b/src/Aeon.Foraging/CreateDispenserEvent.cs new file mode 100644 index 0000000..e6f9176 --- /dev/null +++ b/src/Aeon.Foraging/CreateDispenserEvent.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace Aeon.Foraging +{ + [Description("Create a sequence of dispenser event commands.")] + public class CreateDispenserEvent : Source + { + [Description("The number of dispenser units associated with the event command.")] + public int Value { get; set; } + + [Description("Specifies the type of dispenser event command to create.")] + public DispenserEventType EventType { get; set; } + + public override IObservable Generate() + { + return Observable.Return(new DispenserEventArgs(Value, EventType)); + } + + public IObservable Generate(IObservable source) + { + return source.Select(value => new DispenserEventArgs(Value, EventType)); + } + } +} diff --git a/src/Aeon.Foraging/DispenserAccumulate.cs b/src/Aeon.Foraging/DispenserAccumulate.cs new file mode 100644 index 0000000..c008f0e --- /dev/null +++ b/src/Aeon.Foraging/DispenserAccumulate.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Bonsai; + +namespace Aeon.Foraging +{ + [Combinator] + [Description("Generates a sequence of the estimated number of units in the specified dispenser.")] + public class DispenserAccumulate + { + public IObservable Process(IObservable source) + { + return source.Scan(new DispenserState(), Accumulate); + } + + public IObservable Process(IObservable source, IObservable seed) + { + return seed.Take(1).SelectMany(state => source.Scan(state, Accumulate)); + } + + static DispenserState Accumulate(DispenserState state, DispenserEventArgs evt) + { + return evt.EventType switch + { + DispenserEventType.Discount => new DispenserState { Count = state.Count - evt.Value }, + DispenserEventType.Refill => new DispenserState { Count = state.Count + evt.Value }, + DispenserEventType.Reset => new DispenserState { Count = evt.Value }, + _ => throw new InvalidOperationException("Invalid dispenser event type."), + }; + } + } + + public class DispenserState + { + public int Count { get; set; } + + public override string ToString() + { + return $"DispenserState(Total: {Count})"; + } + } +} diff --git a/src/Aeon.Foraging/DispenserController.cs b/src/Aeon.Foraging/DispenserController.cs new file mode 100644 index 0000000..b687454 --- /dev/null +++ b/src/Aeon.Foraging/DispenserController.cs @@ -0,0 +1,37 @@ +using Aeon.Acquisition; +using Bonsai; +using Bonsai.Harp; +using System; +using System.ComponentModel; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Aeon.Foraging +{ + [DefaultProperty(nameof(Name))] + [TypeVisualizer(typeof(DispenserEventVisualizer))] + [Description("Generates a sequence of event commands for the specified dispenser.")] + public class DispenserController : MetadataSource, INamedElement + { + [Description("The name of the dispenser.")] + public string Name { get; set; } + + string INamedElement.Name => $"{Name}{nameof(DispenserController)}"; + + internal BehaviorSubject State { get; } = new(value: default); + + public IObservable Process(IObservable source) + { + return Process().Merge(source + .Do(State).IgnoreElements() + .Cast()); + } + + public IObservable> Process(IObservable source, IObservable clockSource) + { + return Process(clockSource).Merge(source + .Do(State).IgnoreElements() + .Cast>()); + } + } +} diff --git a/src/Aeon.Foraging/DispenserStateMetadata.cs b/src/Aeon.Foraging/DispenserEventArgs.cs similarity index 58% rename from src/Aeon.Foraging/DispenserStateMetadata.cs rename to src/Aeon.Foraging/DispenserEventArgs.cs index d14e5e7..6d5aff9 100644 --- a/src/Aeon.Foraging/DispenserStateMetadata.cs +++ b/src/Aeon.Foraging/DispenserEventArgs.cs @@ -1,23 +1,20 @@ namespace Aeon.Foraging { - public class DispenserStateMetadata + public class DispenserEventArgs { - public DispenserStateMetadata(string name, int value, DispenserEventType eventType) + public DispenserEventArgs(int value, DispenserEventType eventType) { - Name = name; Value = value; EventType = eventType; } - public string Name { get; } - public int Value { get; } public DispenserEventType EventType { get; } public override string ToString() { - return $"DispenserState({Name}, {EventType}, Total:{Value})"; + return $"DispenserEvent({EventType}, Value:{Value})"; } } diff --git a/src/Aeon.Foraging/DispenserStateControl.Designer.cs b/src/Aeon.Foraging/DispenserEventControl.Designer.cs similarity index 82% rename from src/Aeon.Foraging/DispenserStateControl.Designer.cs rename to src/Aeon.Foraging/DispenserEventControl.Designer.cs index 755d965..017a26c 100644 --- a/src/Aeon.Foraging/DispenserStateControl.Designer.cs +++ b/src/Aeon.Foraging/DispenserEventControl.Designer.cs @@ -1,6 +1,6 @@ namespace Aeon.Foraging { - partial class DispenserStateControl + partial class DispenserEventControl { /// /// Required designer variable. @@ -29,12 +29,13 @@ protected override void Dispose(bool disposing) private void InitializeComponent() { this.dispenserPanel = new System.Windows.Forms.Panel(); + this.deliverButton = new System.Windows.Forms.Button(); + this.resetButton = new System.Windows.Forms.Button(); this.dispenserGroupBox = new System.Windows.Forms.GroupBox(); this.currentValueLabel = new System.Windows.Forms.Label(); this.currentLabel = new System.Windows.Forms.Label(); this.refillUpDown = new System.Windows.Forms.NumericUpDown(); this.refillButton = new System.Windows.Forms.Button(); - this.resetButton = new System.Windows.Forms.Button(); this.dispenserPanel.SuspendLayout(); this.dispenserGroupBox.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.refillUpDown)).BeginInit(); @@ -42,6 +43,7 @@ private void InitializeComponent() // // dispenserPanel // + this.dispenserPanel.Controls.Add(this.deliverButton); this.dispenserPanel.Controls.Add(this.resetButton); this.dispenserPanel.Controls.Add(this.dispenserGroupBox); this.dispenserPanel.Controls.Add(this.refillButton); @@ -49,13 +51,37 @@ private void InitializeComponent() this.dispenserPanel.Location = new System.Drawing.Point(0, 0); this.dispenserPanel.Margin = new System.Windows.Forms.Padding(6); this.dispenserPanel.Name = "dispenserPanel"; - this.dispenserPanel.Size = new System.Drawing.Size(400, 240); + this.dispenserPanel.Size = new System.Drawing.Size(400, 180); this.dispenserPanel.TabIndex = 10; // - // dispenserGroupBox + // deliverButton + // + this.deliverButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.deliverButton.Location = new System.Drawing.Point(213, 23); + this.deliverButton.Margin = new System.Windows.Forms.Padding(12); + this.deliverButton.Name = "deliverButton"; + this.deliverButton.Size = new System.Drawing.Size(175, 40); + this.deliverButton.TabIndex = 7; + this.deliverButton.Text = "Deliver"; + this.deliverButton.UseVisualStyleBackColor = true; + this.deliverButton.Click += new System.EventHandler(this.deliverButton_Click); + // + // resetButton // - this.dispenserGroupBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + this.resetButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.resetButton.Location = new System.Drawing.Point(214, 125); + this.resetButton.Margin = new System.Windows.Forms.Padding(12); + this.resetButton.Name = "resetButton"; + this.resetButton.Size = new System.Drawing.Size(175, 40); + this.resetButton.TabIndex = 6; + this.resetButton.Text = "Reset"; + this.resetButton.UseVisualStyleBackColor = true; + this.resetButton.Click += new System.EventHandler(this.resetButton_Click); + // + // dispenserGroupBox + // this.dispenserGroupBox.Controls.Add(this.currentValueLabel); this.dispenserGroupBox.Controls.Add(this.currentLabel); this.dispenserGroupBox.Controls.Add(this.refillUpDown); @@ -63,7 +89,7 @@ private void InitializeComponent() this.dispenserGroupBox.Margin = new System.Windows.Forms.Padding(12); this.dispenserGroupBox.Name = "dispenserGroupBox"; this.dispenserGroupBox.Padding = new System.Windows.Forms.Padding(12); - this.dispenserGroupBox.Size = new System.Drawing.Size(375, 153); + this.dispenserGroupBox.Size = new System.Drawing.Size(197, 153); this.dispenserGroupBox.TabIndex = 4; this.dispenserGroupBox.TabStop = false; this.dispenserGroupBox.Text = "Dispenser"; @@ -75,7 +101,7 @@ private void InitializeComponent() this.currentValueLabel.AutoSize = true; this.currentValueLabel.Location = new System.Drawing.Point(145, 43); this.currentValueLabel.Name = "currentValueLabel"; - this.currentValueLabel.Size = new System.Drawing.Size(31, 32); + this.currentValueLabel.Size = new System.Drawing.Size(24, 26); this.currentValueLabel.TabIndex = 2; this.currentValueLabel.Text = "0"; // @@ -84,7 +110,7 @@ private void InitializeComponent() this.currentLabel.AutoSize = true; this.currentLabel.Location = new System.Drawing.Point(15, 43); this.currentLabel.Name = "currentLabel"; - this.currentLabel.Size = new System.Drawing.Size(124, 32); + this.currentLabel.Size = new System.Drawing.Size(96, 26); this.currentLabel.TabIndex = 1; this.currentLabel.Text = "Current: "; // @@ -99,45 +125,31 @@ private void InitializeComponent() 0, 0}); this.refillUpDown.Name = "refillUpDown"; - this.refillUpDown.Size = new System.Drawing.Size(345, 38); + this.refillUpDown.Size = new System.Drawing.Size(167, 32); this.refillUpDown.TabIndex = 0; // // refillButton // - this.refillButton.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) + this.refillButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); - this.refillButton.Location = new System.Drawing.Point(12, 175); + this.refillButton.Location = new System.Drawing.Point(214, 75); this.refillButton.Margin = new System.Windows.Forms.Padding(12); this.refillButton.Name = "refillButton"; - this.refillButton.Size = new System.Drawing.Size(176, 52); + this.refillButton.Size = new System.Drawing.Size(175, 40); this.refillButton.TabIndex = 5; this.refillButton.Text = "Refill"; this.refillButton.UseVisualStyleBackColor = true; this.refillButton.Click += new System.EventHandler(this.refillButton_Click); // - // resetButton - // - this.resetButton.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Right))); - this.resetButton.Location = new System.Drawing.Point(212, 175); - this.resetButton.Margin = new System.Windows.Forms.Padding(12); - this.resetButton.Name = "resetButton"; - this.resetButton.Size = new System.Drawing.Size(175, 52); - this.resetButton.TabIndex = 6; - this.resetButton.Text = "Reset"; - this.resetButton.UseVisualStyleBackColor = true; - this.resetButton.Click += new System.EventHandler(this.resetButton_Click); - // - // DispenserStateControl + // DispenserEventControl // - this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F); + this.AutoScaleDimensions = new System.Drawing.SizeF(13F, 26F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Controls.Add(this.dispenserPanel); this.Font = new System.Drawing.Font("Microsoft Sans Serif", 16.2F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.Margin = new System.Windows.Forms.Padding(6); - this.Name = "DispenserStateControl"; - this.Size = new System.Drawing.Size(400, 240); + this.Name = "DispenserEventControl"; + this.Size = new System.Drawing.Size(400, 180); this.dispenserPanel.ResumeLayout(false); this.dispenserGroupBox.ResumeLayout(false); this.dispenserGroupBox.PerformLayout(); @@ -155,5 +167,6 @@ private void InitializeComponent() private System.Windows.Forms.Label currentValueLabel; private System.Windows.Forms.Label currentLabel; private System.Windows.Forms.Button resetButton; + private System.Windows.Forms.Button deliverButton; } } diff --git a/src/Aeon.Foraging/DispenserStateControl.cs b/src/Aeon.Foraging/DispenserEventControl.cs similarity index 53% rename from src/Aeon.Foraging/DispenserStateControl.cs rename to src/Aeon.Foraging/DispenserEventControl.cs index af768e0..b9b1e41 100644 --- a/src/Aeon.Foraging/DispenserStateControl.cs +++ b/src/Aeon.Foraging/DispenserEventControl.cs @@ -3,11 +3,11 @@ namespace Aeon.Foraging { - public partial class DispenserStateControl : UserControl + public partial class DispenserEventControl : UserControl { int currentValue; - public DispenserStateControl(DispenserState source) + public DispenserEventControl(DispenserController source) { Source = source ?? throw new ArgumentNullException(nameof(source)); InitializeComponent(); @@ -15,11 +15,11 @@ public DispenserStateControl(DispenserState source) var dispenserName = Source.Name; if (!string.IsNullOrEmpty(dispenserName)) { - dispenserGroupBox.Text = $"{dispenserName} Dispenser"; + dispenserGroupBox.Text = $"{dispenserName}"; } } - public DispenserState Source { get; } + public DispenserController Source { get; } public int Value { @@ -31,16 +31,24 @@ public int Value } } + private void deliverButton_Click(object sender, EventArgs e) + { + OnDispenserEvent(1m, DispenserEventType.Discount); + } + private void refillButton_Click(object sender, EventArgs e) { - var metadata = new DispenserStateMetadata(Source.Name, (int)refillUpDown.Value, DispenserEventType.Refill); - Source.OnNext(metadata); + OnDispenserEvent(refillUpDown.Value, DispenserEventType.Refill); } private void resetButton_Click(object sender, EventArgs e) { - var delta = (int)refillUpDown.Value - Value; - var metadata = new DispenserStateMetadata(Source.Name, delta, DispenserEventType.Reset); + OnDispenserEvent(refillUpDown.Value, DispenserEventType.Reset); + } + + private void OnDispenserEvent(decimal value, DispenserEventType eventType) + { + var metadata = new DispenserEventArgs((int)value, eventType); Source.OnNext(metadata); } } diff --git a/src/Aeon.Foraging/DispenserStateControl.resx b/src/Aeon.Foraging/DispenserEventControl.resx similarity index 100% rename from src/Aeon.Foraging/DispenserStateControl.resx rename to src/Aeon.Foraging/DispenserEventControl.resx diff --git a/src/Aeon.Foraging/DispenserStateVisualizer.cs b/src/Aeon.Foraging/DispenserEventVisualizer.cs similarity index 50% rename from src/Aeon.Foraging/DispenserStateVisualizer.cs rename to src/Aeon.Foraging/DispenserEventVisualizer.cs index bd658b1..f2237c3 100644 --- a/src/Aeon.Foraging/DispenserStateVisualizer.cs +++ b/src/Aeon.Foraging/DispenserEventVisualizer.cs @@ -1,43 +1,49 @@ using Bonsai.Design; using Bonsai.Expressions; -using Bonsai.Harp; using System; +using System.Drawing; using System.Windows.Forms; namespace Aeon.Foraging { - public class DispenserStateVisualizer : DialogTypeVisualizer + public class DispenserEventVisualizer : DialogTypeVisualizer { - DispenserStateControl control; + DispenserEventControl control; + IDisposable stateSubscription; public override void Load(IServiceProvider provider) { var context = (ITypeVisualizerContext)provider.GetService(typeof(ITypeVisualizerContext)); var visualizerElement = ExpressionBuilder.GetVisualizerElement(context.Source); - var source = (DispenserState)ExpressionBuilder.GetWorkflowElement(visualizerElement.Builder); + var source = (DispenserController)ExpressionBuilder.GetWorkflowElement(visualizerElement.Builder); - control = new DispenserStateControl(source); + control = new DispenserEventControl(source); control.Dock = DockStyle.Fill; - var state = source.State; - if (state != null) control.Value = state.Value; + control.HandleDestroyed += delegate { stateSubscription?.Dispose(); }; + control.HandleCreated += delegate + { + stateSubscription = source.State.ObserveOn(control).Subscribe(state => + { + if (state != null) + { + control.Value = state.Count; + } + }); + }; var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); if (visualizerService != null) { + using var graphics = Graphics.FromHwnd(IntPtr.Zero); + control.Scale(new SizeF( + graphics.DpiX / control.DeviceDpi, + graphics.DpiY / control.DeviceDpi)); visualizerService.AddControl(control); } } public override void Show(object value) { - if (value is DispenserStateMetadata metadata) - { - control.Value = metadata.Value; - } - else if (value is Timestamped timestampedMetadata) - { - control.Value = timestampedMetadata.Value.Value; - } } public override void Unload() diff --git a/src/Aeon.Foraging/DispenserState.cs b/src/Aeon.Foraging/DispenserState.cs deleted file mode 100644 index 874db3b..0000000 --- a/src/Aeon.Foraging/DispenserState.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Aeon.Acquisition; -using Bonsai; -using Bonsai.Harp; -using System; -using System.ComponentModel; -using System.Reactive.Linq; - -namespace Aeon.Foraging -{ - [DefaultProperty(nameof(Name))] - [TypeVisualizer(typeof(DispenserStateVisualizer))] - [Description("Generates a sequence of the estimated number of units in the specified dispenser.")] - public class DispenserState : MetadataSource, INamedElement - { - [Description("The name of the dispenser.")] - public string Name { get; set; } - - string INamedElement.Name => $"{Name}Dispenser"; - - internal DispenserStateRecovery State { get; set; } - - public override IObservable> Process(IObservable source) - { - var refill = Process(); - return Observable.Defer(() => - { - State = StateRecovery.Deserialize(Name); - var initialState = new DispenserStateMetadata(Name, State.Value, DispenserEventType.Reset); - return source.Publish(ps => - { - const int DigitalOutput = 35; - const int TriggerPellet = 0x80; - var discount = ps.Where(message => - message.Address == DigitalOutput && - message.MessageType == MessageType.Write && - message.GetPayloadByte() == TriggerPellet) - .Select(_ => new DispenserStateMetadata(Name, -1, DispenserEventType.Discount)); - return refill.Merge(discount).StartWith(initialState).Publish(changes => - changes.CombineLatest(ps, (data, message) => (data, message)) - .Sample(changes.MergeUnit(ps.Take(1))) - .Select((x, i) => - { - var data = x.data; - var timestamp = x.message.GetTimestamp(); - if (i > 0) State.Value += data.Value; - StateRecovery.Serialize(Name, State); - data = new DispenserStateMetadata(data.Name, State.Value, data.EventType); - return Timestamped.Create(data, timestamp); - })); - }); - }); - } - - public IObservable> Process(IObservable dispenser, IObservable source) - { - var refill = Process(); - var discount = dispenser.Select(x => new DispenserStateMetadata(Name, -x, DispenserEventType.Discount)); - return Observable.Defer(() => - { - State = StateRecovery.Deserialize(Name); - var initialState = new DispenserStateMetadata(Name, State.Value, DispenserEventType.Reset); - return discount.Merge(refill).StartWith(initialState).Select((data, i) => - { - if (i > 0) State.Value += data.Value; - StateRecovery.Serialize(Name, State); - data = new DispenserStateMetadata(data.Name, State.Value, data.EventType); - return data; - }).Timestamp(source); - }); - } - } - - public class DispenserStateRecovery - { - public int Value { get; set; } - } -} diff --git a/src/Aeon.Foraging/FormatDispenserState.cs b/src/Aeon.Foraging/FormatDispenserState.cs index c638ccc..9ce6f11 100644 --- a/src/Aeon.Foraging/FormatDispenserState.cs +++ b/src/Aeon.Foraging/FormatDispenserState.cs @@ -12,15 +12,16 @@ namespace Aeon.Foraging [WorkflowElementCategory(ElementCategory.Transform)] public class FormatDispenserState { - const int Address = 200; + [Description("The address of the virtual Harp register.")] + public int Address { get; set; } = 200; - public IObservable Process(IObservable> source) + public IObservable Process(IObservable> source) { return source.Select(input => HarpMessage.FromSingle( Address, input.Seconds, MessageType.Event, - input.Value.Value)); + input.Value.Count)); } } } diff --git a/src/Aeon.Foraging/PatchDispenser.bonsai b/src/Aeon.Foraging/PatchDispenser.bonsai new file mode 100644 index 0000000..4198202 --- /dev/null +++ b/src/Aeon.Foraging/PatchDispenser.bonsai @@ -0,0 +1,101 @@ + + + Provides an interface for controlling patch dispensers. Input sequence represents pellet discount notifications. + + + + + + + PatchController + + + NotDiscount + + + + Source1 + + + EventType + + + + Discount + + + + + + + + + + + + + Source1 + + + + 1 + Discount + + + + + + + + + + PatchState + + + + + + + + + PatchState + + + PatchState + + + + + + + + + PatchController + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file