From 9cf90471daa3838828e34036c20d5705597ca70d Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 14:14:24 +0100 Subject: [PATCH 1/8] Split dispenser state from dispenser UI controller --- src/Aeon.Foraging/Aeon.Foraging.csproj | 2 +- src/Aeon.Foraging/Aeon.Foraging.csproj.user | 2 +- src/Aeon.Foraging/DispenserController.cs | 36 +++++++++ ...StateMetadata.cs => DispenserEventArgs.cs} | 9 +-- ...r.cs => DispenserEventControl.Designer.cs} | 62 +++++++++------- ...ateControl.cs => DispenserEventControl.cs} | 22 ++++-- ...ontrol.resx => DispenserEventControl.resx} | 0 ...ualizer.cs => DispenserEventVisualizer.cs} | 31 ++++---- src/Aeon.Foraging/DispenserState.cs | 73 ++++++------------- src/Aeon.Foraging/FormatDispenserState.cs | 2 +- 10 files changed, 131 insertions(+), 108 deletions(-) create mode 100644 src/Aeon.Foraging/DispenserController.cs rename src/Aeon.Foraging/{DispenserStateMetadata.cs => DispenserEventArgs.cs} (58%) rename src/Aeon.Foraging/{DispenserStateControl.Designer.cs => DispenserEventControl.Designer.cs} (82%) rename src/Aeon.Foraging/{DispenserStateControl.cs => DispenserEventControl.cs} (57%) rename src/Aeon.Foraging/{DispenserStateControl.resx => DispenserEventControl.resx} (100%) rename src/Aeon.Foraging/{DispenserStateVisualizer.cs => DispenserEventVisualizer.cs} (57%) 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/DispenserController.cs b/src/Aeon.Foraging/DispenserController.cs new file mode 100644 index 0000000..509d09e --- /dev/null +++ b/src/Aeon.Foraging/DispenserController.cs @@ -0,0 +1,36 @@ +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..f2de2ed 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}, Total:{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..d00dbd5 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.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.deliverButton = 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,9 +51,21 @@ 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; // + // resetButton + // + this.resetButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | 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.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) @@ -63,7 +77,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 +89,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 +98,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 +113,42 @@ 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) - | System.Windows.Forms.AnchorStyles.Right))); - this.refillButton.Location = new System.Drawing.Point(12, 175); + this.refillButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + 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 + // deliverButton // - 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); + this.deliverButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | 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); // // DispenserStateControl // - 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.Size = new System.Drawing.Size(400, 180); this.dispenserPanel.ResumeLayout(false); this.dispenserGroupBox.ResumeLayout(false); this.dispenserGroupBox.PerformLayout(); @@ -155,5 +166,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 57% rename from src/Aeon.Foraging/DispenserStateControl.cs rename to src/Aeon.Foraging/DispenserEventControl.cs index af768e0..21e8904 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(); @@ -19,7 +19,7 @@ public DispenserStateControl(DispenserState source) } } - 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 57% rename from src/Aeon.Foraging/DispenserStateVisualizer.cs rename to src/Aeon.Foraging/DispenserEventVisualizer.cs index bd658b1..2574f79 100644 --- a/src/Aeon.Foraging/DispenserStateVisualizer.cs +++ b/src/Aeon.Foraging/DispenserEventVisualizer.cs @@ -1,25 +1,34 @@ using Bonsai.Design; using Bonsai.Expressions; -using Bonsai.Harp; using System; 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.Value; + } + }); + }; var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); if (visualizerService != null) @@ -30,14 +39,6 @@ public override void Load(IServiceProvider provider) 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 index 874db3b..7f45e82 100644 --- a/src/Aeon.Foraging/DispenserState.cs +++ b/src/Aeon.Foraging/DispenserState.cs @@ -1,71 +1,40 @@ -using Aeon.Acquisition; -using Bonsai; -using Bonsai.Harp; -using System; +using System; using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; +using Aeon.Acquisition; +using Bonsai; namespace Aeon.Foraging { + [Combinator] [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 + public class DispenserState : 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); - })); - }); - }); - } + string INamedElement.Name => $"{Name}{nameof(DispenserState)}"; - public IObservable> Process(IObservable dispenser, IObservable source) + public IObservable Process(IObservable source) { - var refill = Process(); - var discount = dispenser.Select(x => new DispenserStateMetadata(Name, -x, DispenserEventType.Discount)); + var name = Name; 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) => + var state = StateRecovery.Deserialize(name); + return Observable.Return(state).Concat(source.Select(evt => { - if (i > 0) State.Value += data.Value; - StateRecovery.Serialize(Name, State); - data = new DispenserStateMetadata(data.Name, State.Value, data.EventType); - return data; - }).Timestamp(source); + state = evt.EventType switch + { + DispenserEventType.Discount => new DispenserStateRecovery { Value = state.Value - evt.Value }, + DispenserEventType.Refill => new DispenserStateRecovery { Value = state.Value + evt.Value }, + DispenserEventType.Reset => new DispenserStateRecovery { Value = evt.Value }, + _ => throw new InvalidOperationException("Invalid dispenser event type."), + }; + StateRecovery.Serialize(name, state); + return state; + })); }); } } diff --git a/src/Aeon.Foraging/FormatDispenserState.cs b/src/Aeon.Foraging/FormatDispenserState.cs index c638ccc..a7333da 100644 --- a/src/Aeon.Foraging/FormatDispenserState.cs +++ b/src/Aeon.Foraging/FormatDispenserState.cs @@ -14,7 +14,7 @@ public class FormatDispenserState { const int Address = 200; - public IObservable Process(IObservable> source) + public IObservable Process(IObservable> source) { return source.Select(input => HarpMessage.FromSingle( Address, From f152ba5c504d60d48a99b62bd0cbed005905c726 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 14:47:13 +0100 Subject: [PATCH 2/8] Ensure correct control scaling factor --- src/Aeon.Foraging/DispenserEventVisualizer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Aeon.Foraging/DispenserEventVisualizer.cs b/src/Aeon.Foraging/DispenserEventVisualizer.cs index 2574f79..48fc839 100644 --- a/src/Aeon.Foraging/DispenserEventVisualizer.cs +++ b/src/Aeon.Foraging/DispenserEventVisualizer.cs @@ -1,6 +1,7 @@ using Bonsai.Design; using Bonsai.Expressions; using System; +using System.Drawing; using System.Windows.Forms; namespace Aeon.Foraging @@ -33,6 +34,10 @@ public override void Load(IServiceProvider provider) 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); } } From 7b4d5e410f6ab0888db11ea83b78da34ef6caf62 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 17:06:05 +0100 Subject: [PATCH 3/8] Refactor dispenser state property name --- src/Aeon.Foraging/DispenserEventVisualizer.cs | 2 +- src/Aeon.Foraging/DispenserState.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Aeon.Foraging/DispenserEventVisualizer.cs b/src/Aeon.Foraging/DispenserEventVisualizer.cs index 48fc839..f2237c3 100644 --- a/src/Aeon.Foraging/DispenserEventVisualizer.cs +++ b/src/Aeon.Foraging/DispenserEventVisualizer.cs @@ -26,7 +26,7 @@ public override void Load(IServiceProvider provider) { if (state != null) { - control.Value = state.Value; + control.Value = state.Count; } }); }; diff --git a/src/Aeon.Foraging/DispenserState.cs b/src/Aeon.Foraging/DispenserState.cs index 7f45e82..e01db47 100644 --- a/src/Aeon.Foraging/DispenserState.cs +++ b/src/Aeon.Foraging/DispenserState.cs @@ -27,9 +27,9 @@ public IObservable Process(IObservable new DispenserStateRecovery { Value = state.Value - evt.Value }, - DispenserEventType.Refill => new DispenserStateRecovery { Value = state.Value + evt.Value }, - DispenserEventType.Reset => new DispenserStateRecovery { Value = evt.Value }, + DispenserEventType.Discount => new DispenserStateRecovery { Count = state.Count - evt.Value }, + DispenserEventType.Refill => new DispenserStateRecovery { Count = state.Count + evt.Value }, + DispenserEventType.Reset => new DispenserStateRecovery { Count = evt.Value }, _ => throw new InvalidOperationException("Invalid dispenser event type."), }; StateRecovery.Serialize(name, state); @@ -41,6 +41,6 @@ public IObservable Process(IObservable Date: Tue, 17 Oct 2023 17:07:36 +0100 Subject: [PATCH 4/8] Update dispenser state formatter --- src/Aeon.Foraging/FormatDispenserState.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Aeon.Foraging/FormatDispenserState.cs b/src/Aeon.Foraging/FormatDispenserState.cs index a7333da..238b624 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)); } } } From 5e348a780316df74fce7c9e961897005a1444cc9 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 22:39:33 +0100 Subject: [PATCH 5/8] Externalize dispenser state representation --- src/Aeon.Foraging/CreateDispenserEvent.cs | 28 ++++++++++++++ src/Aeon.Foraging/DispenserAccumulate.cs | 44 ++++++++++++++++++++++ src/Aeon.Foraging/DispenserController.cs | 7 ++-- src/Aeon.Foraging/DispenserEventArgs.cs | 2 +- src/Aeon.Foraging/DispenserState.cs | 46 ----------------------- src/Aeon.Foraging/FormatDispenserState.cs | 2 +- 6 files changed, 78 insertions(+), 51 deletions(-) create mode 100644 src/Aeon.Foraging/CreateDispenserEvent.cs create mode 100644 src/Aeon.Foraging/DispenserAccumulate.cs delete mode 100644 src/Aeon.Foraging/DispenserState.cs 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 index 509d09e..b687454 100644 --- a/src/Aeon.Foraging/DispenserController.cs +++ b/src/Aeon.Foraging/DispenserController.cs @@ -18,15 +18,16 @@ public class DispenserController : MetadataSource, INamedEle string INamedElement.Name => $"{Name}{nameof(DispenserController)}"; - internal BehaviorSubject State { get; } = new(value: default); - public IObservable Process(IObservable source) + 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) + public IObservable> Process(IObservable source, IObservable clockSource) { return Process(clockSource).Merge(source .Do(State).IgnoreElements() diff --git a/src/Aeon.Foraging/DispenserEventArgs.cs b/src/Aeon.Foraging/DispenserEventArgs.cs index f2de2ed..6d5aff9 100644 --- a/src/Aeon.Foraging/DispenserEventArgs.cs +++ b/src/Aeon.Foraging/DispenserEventArgs.cs @@ -14,7 +14,7 @@ public DispenserEventArgs(int value, DispenserEventType eventType) public override string ToString() { - return $"DispenserEvent({EventType}, Total:{Value})"; + return $"DispenserEvent({EventType}, Value:{Value})"; } } diff --git a/src/Aeon.Foraging/DispenserState.cs b/src/Aeon.Foraging/DispenserState.cs deleted file mode 100644 index e01db47..0000000 --- a/src/Aeon.Foraging/DispenserState.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using Aeon.Acquisition; -using Bonsai; - -namespace Aeon.Foraging -{ - [Combinator] - [DefaultProperty(nameof(Name))] - [Description("Generates a sequence of the estimated number of units in the specified dispenser.")] - public class DispenserState : INamedElement - { - [Description("The name of the dispenser.")] - public string Name { get; set; } - - string INamedElement.Name => $"{Name}{nameof(DispenserState)}"; - - public IObservable Process(IObservable source) - { - var name = Name; - return Observable.Defer(() => - { - var state = StateRecovery.Deserialize(name); - return Observable.Return(state).Concat(source.Select(evt => - { - state = evt.EventType switch - { - DispenserEventType.Discount => new DispenserStateRecovery { Count = state.Count - evt.Value }, - DispenserEventType.Refill => new DispenserStateRecovery { Count = state.Count + evt.Value }, - DispenserEventType.Reset => new DispenserStateRecovery { Count = evt.Value }, - _ => throw new InvalidOperationException("Invalid dispenser event type."), - }; - StateRecovery.Serialize(name, state); - return state; - })); - }); - } - } - - public class DispenserStateRecovery - { - public int Count { get; set; } - } -} diff --git a/src/Aeon.Foraging/FormatDispenserState.cs b/src/Aeon.Foraging/FormatDispenserState.cs index 238b624..9ce6f11 100644 --- a/src/Aeon.Foraging/FormatDispenserState.cs +++ b/src/Aeon.Foraging/FormatDispenserState.cs @@ -15,7 +15,7 @@ public class FormatDispenserState [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, From f255076d0c289cd4ae712094939fb8d945742c7a Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 22:41:35 +0100 Subject: [PATCH 6/8] Add common patch dispenser interface workflow --- src/Aeon.Foraging/PatchDispenser.bonsai | 92 +++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/Aeon.Foraging/PatchDispenser.bonsai diff --git a/src/Aeon.Foraging/PatchDispenser.bonsai b/src/Aeon.Foraging/PatchDispenser.bonsai new file mode 100644 index 0000000..e28b3ba --- /dev/null +++ b/src/Aeon.Foraging/PatchDispenser.bonsai @@ -0,0 +1,92 @@ + + + Provides an interface for controlling patch dispensers. Input sequence represents pellet discount notifications. + + + + DispenserEvents + + + NotDiscount + + + + Source1 + + + EventType + + + + Discount + + + + + + + + + + + + + Source1 + + + + 1 + Discount + + + + + + + + + + PatchState + + + + + + + + + PatchState + + + PatchState + + + + + + DispenserEvents + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 2a6a02a5670fcc934637009376ca3b1f7416219c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 23:56:50 +0100 Subject: [PATCH 7/8] Externalize controller subject identifiers --- src/Aeon.Foraging/PatchDispenser.bonsai | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Aeon.Foraging/PatchDispenser.bonsai b/src/Aeon.Foraging/PatchDispenser.bonsai index e28b3ba..4198202 100644 --- a/src/Aeon.Foraging/PatchDispenser.bonsai +++ b/src/Aeon.Foraging/PatchDispenser.bonsai @@ -7,8 +7,11 @@ Provides an interface for controlling patch dispensers. Input sequence represents pellet discount notifications. + + + - DispenserEvents + PatchController NotDiscount @@ -64,29 +67,35 @@ PatchState + + + - DispenserEvents + PatchController - - - - - - - + + + + + + - - - - - + + + + + + + + + \ No newline at end of file From fcce2729d209cc0ceeae781c4c075d6518b4c945 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 17 Oct 2023 23:57:19 +0100 Subject: [PATCH 8/8] Allow tighter dispenser controller layouts --- .../DispenserEventControl.Designer.cs | 39 ++++++++++--------- src/Aeon.Foraging/DispenserEventControl.cs | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Aeon.Foraging/DispenserEventControl.Designer.cs b/src/Aeon.Foraging/DispenserEventControl.Designer.cs index d00dbd5..017a26c 100644 --- a/src/Aeon.Foraging/DispenserEventControl.Designer.cs +++ b/src/Aeon.Foraging/DispenserEventControl.Designer.cs @@ -29,13 +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.deliverButton = new System.Windows.Forms.Button(); this.dispenserPanel.SuspendLayout(); this.dispenserGroupBox.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.refillUpDown)).BeginInit(); @@ -54,9 +54,23 @@ private void InitializeComponent() this.dispenserPanel.Size = new System.Drawing.Size(400, 180); this.dispenserPanel.TabIndex = 10; // + // 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.resetButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + 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"; @@ -68,8 +82,6 @@ private void InitializeComponent() // // dispenserGroupBox // - this.dispenserGroupBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); this.dispenserGroupBox.Controls.Add(this.currentValueLabel); this.dispenserGroupBox.Controls.Add(this.currentLabel); this.dispenserGroupBox.Controls.Add(this.refillUpDown); @@ -118,7 +130,8 @@ private void InitializeComponent() // // refillButton // - this.refillButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + 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(214, 75); this.refillButton.Margin = new System.Windows.Forms.Padding(12); this.refillButton.Name = "refillButton"; @@ -128,26 +141,14 @@ private void InitializeComponent() this.refillButton.UseVisualStyleBackColor = true; this.refillButton.Click += new System.EventHandler(this.refillButton_Click); // - // deliverButton - // - this.deliverButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | 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); - // - // DispenserStateControl + // DispenserEventControl // 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.Name = "DispenserEventControl"; this.Size = new System.Drawing.Size(400, 180); this.dispenserPanel.ResumeLayout(false); this.dispenserGroupBox.ResumeLayout(false); diff --git a/src/Aeon.Foraging/DispenserEventControl.cs b/src/Aeon.Foraging/DispenserEventControl.cs index 21e8904..b9b1e41 100644 --- a/src/Aeon.Foraging/DispenserEventControl.cs +++ b/src/Aeon.Foraging/DispenserEventControl.cs @@ -15,7 +15,7 @@ public DispenserEventControl(DispenserController source) var dispenserName = Source.Name; if (!string.IsNullOrEmpty(dispenserName)) { - dispenserGroupBox.Text = $"{dispenserName} Dispenser"; + dispenserGroupBox.Text = $"{dispenserName}"; } }