C# Foreach 循环,帮助实现移动到 NEXT 和 PREVIOUS step
C# Foreach loop, help to implement move to NEXT and PRIVIOUS step
这是我到目前为止编写的代码,我还没有用真正的 PLC 测试过。请问有没有办法实现上一步,然后进入下一步?
public async void Run()
{
//recepi has many steps, and each step has one or many Nodes (commands to PLC)
foreach (Step step in recepi.Step)
{
var spStep = new TagItem(step.SPTag, daServer);//setpoint to the PLC
var pvStep = new TagItem(step.SPTag, daServer);//actual Process value from PLC
foreach (var node in step.Nodes)
{
// here i'm writing other setpoints to the PLC.
var spNode = new TagItem(node.SPTag, daServer);
await spNode.Write(node.Value);
}
//when all the commands are sent to the PLC, i have to wait for the prosess value to reach a certn
//value before i can move to next step and do the same again.
switch (step.Operator)
{
case Operator.Equal:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) == float.Parse(spStep.TagValue)));
break;
case Operator.Unequal:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) != float.Parse(spStep.TagValue)));
break;
case Operator.LessThan:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) < float.Parse(spStep.TagValue)));
break;
case Operator.LessThanOrEqual:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) <= float.Parse(spStep.TagValue)));
break;
case Operator.GreaterThan:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) > float.Parse(spStep.TagValue)));
break;
case Operator.GreaterThanOrEqual:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) >= float.Parse(spStep.TagValue)));
break;
}
}
}
谢谢你的帮助!
在 UI 中,操作员可以通过按相应的按钮停止、暂停、转到下一步和上一步。我想要实现的是对音乐播放器微笑。 运行 一个配方需要 2 个多小时才能完成一批,因为它是一个缓慢的过程,其中涉及加热。
这是我目前使用状态机的无状态库的代码。
using Formula.Models;
using Formula.UI;
using Stateless;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
public class FormulaEngine
{
private readonly StateMachine<RecepieState, Triggers> _machine;
StateMachine<RecepieState, Triggers>.TriggerWithParameters<Step> _setNextStep;
StateMachine<RecepieState, Triggers>.TriggerWithParameters<Step> _setPreviousStep;
private readonly DAServer _daServer;
private readonly IEventAggregator _ea;
const float Epsilon = 0.001f;
private int _index = 0;
StepList stepList;
public Recepi Recepi { get; }
Dictionary<Operator, Func<TagItem,Step, bool>> operatorItems = new Dictionary<Operator, Func<TagItem, Step, bool>>
{
{ Operator.Equal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.Unequal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.LessThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.LessThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) <= Epsilon) },
{ Operator.GreaterThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.GreaterThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) >= Epsilon) },
};
public enum RecepieState
{
Running,
Stoped,
Paused
}
public enum Triggers
{
Start,
Pause,
Stop,
GoToNextStep,
GoToPreviousStep
}
public FormulaEngine(Recepi recepi, DAServer daServer, IEventAggregator ea)
{
Recepi = recepi;
this._daServer = daServer;
this._ea = ea;
_machine = new StateMachine<RecepieState, Triggers>(RecepieState.Stoped);
_setNextStep = _machine.SetTriggerParameters<Step>(Triggers.GoToNextStep);
_setPreviousStep = _machine.SetTriggerParameters<Step>(Triggers.GoToPreviousStep);
ConfigureStateMachine();
stepList = new StepList();
stepList.AddSteps(Recepi.Step);
}
private void ConfigureStateMachine()
{
_machine.Configure(RecepieState.Running)
.OnEntryAsync(t => ExcecuteRecepi())
.InternalTransitionAsync(Triggers.GoToNextStep, t => OnNextStep())
.InternalTransitionAsync(Triggers.GoToPreviousStep, t => OnPreviousStep())
//.OnEntryFromAsync(Triggers.StartExcecuting, (t) => ExcecuteRecepi())
//.OnEntryAsync(t => StartTimer())
//.OnExitAsync(t => StopTimer())
//.InternalTransitionAsync(_setNextStep, (step, t) => OnNextStep(step))
//.InternalTransitionAsync(_setPreviousStep, (step, t) => OnPreviousStep(step))
.Permit(Triggers.Pause, RecepieState.Paused)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Paused)
.Permit(Triggers.Start, RecepieState.Running)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Stoped)
.Permit(Triggers.Start, RecepieState.Running);
}
public int Index
{
get { return _index; }
set
{
_index = value;
_ea.GetEvent<CurrentStepChangedEvent>().Publish(_index);
}
}
private async Task ExcecuteRecepi()
{
Index = 0;
stepList.CurrentStepIndex = Index;
if (Index <= stepList.Count())
{
var step = stepList[Index];
await RunStep(step);
}
await _machine.FireAsync(Triggers.GoToNextStep);
}
private async Task<bool> RunStep(Step step)
{
var pvStep = new TagItem(step.SPTag, _daServer);
foreach (var node in step.Nodes)
{
// here i'm writing other setpoints to the PLC.
var spNode = new TagItem(node.SPTag, _daServer);
await spNode.Write(node.Value);
}
var test = operatorItems[step.Operator];
while (!test(pvStep, step))
{
await Task.Delay(500);
}
return true;
}
private async Task OnPreviousStep()
{
if(Index > 0)
{
Index--;
if (Index <= stepList.Count())
{
var step = stepList[Index];
await RunStep(step);
}
}
}
private async Task<bool> OnNextStep()
{
Index++;
if (Index <= stepList.Count())
{
var step = stepList[Index];
await RunStep(step);
//var result = Task.Run(async () => await RunStep(step)).Result;
_machine.Fire(Triggers.GoToNextStep);
}
return true;
}
//private Task StopTimer()
//{
// throw new NotImplementedException();
//}
//private Task StartTimer()
//{
// Stopwatch stopWatch = new Stopwatch();
// stopWatch.Start();
//}
public void Stop()
{
_machine.Fire(Triggers.Stop);
}
public void Pause()
{
_machine.Fire(Triggers.Pause);
}
public void Start()
{
_machine.FireAsync(Triggers.Start);
}
}
using Formula.Models;
using Formula.UI;
using Stateless;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace FormulaEngine
{
public enum Activity
{
WAIT_UNTIL = 1
}
public enum RecepieState
{
Running,
Stoped,
Paused
}
public enum Triggers
{
Start,
Pause,
Stop,
GoToNextStep,
GoToPreviousStep,
StartExcecuting
}
public enum Operator
{
[Description("=")]
[EnumMember(Value = "=")]
Equal,
[Description("<>")]
[EnumMember(Value = "<>")]
Unequal,
[Description("<")]
[EnumMember(Value = "<")]
LessThan,
[Description("<=")]
[EnumMember(Value = "<=")]
LessThanOrEqual,
[Description(">")]
[EnumMember(Value = ">")]
GreaterThan,
[Description(">=")]
[EnumMember(Value = ">=")]
GreaterThanOrEqual
}
public class BaseModel
{
public short Id { get; set; }
public string Name { get; set; }
}
public class Recepi : BaseModel
{
public Recepi()
{
Step = new List<Step>();
}
public short FormulaGroupId { get; set; }
public DateTime DateCreated { get; set; }
public string Description { get; set; }
public string CreatedBy { get; set; }
[ForeignKey("RecepiId")]
public List<Step> Step { get; set; }
}
public class Step : BaseModel
{
public Step()
{
Nodes = new List<Node>();
}
public short RecepiId { get; set; }
public short SPTagId { get; set; }
public short PVTagId { get; set; }
public Operator Operator { get; set; }
public TagMetaData SPTag { get; set; }
public TagMetaData PVTag { get; set; }
public string Value { get; set; }
public Activity Activity { get; set; }
[ForeignKey("StepId")]
public List<Node> Nodes { get; set; }
}
public class Node : BaseModel
{
public short StepId { get; set; }
public short SPTagId { get; set; }
public string Value { get; set; }
public TagMetaData SPTag { get; set; }
public TagMetaData PVTag { get; set; }
public Operator Operator { get; set; }
}
public class TagMetaData : BaseModel, IComparable<TagMetaData>
{
//public GroupMetaData GroupMetaData { get; set; }
public short GroupId { get; set; }
public string EngUnit { get; set; }
public DateTime CurrentTimeStamp { get; set; }
public int CurrentQuality { get; set; }
public string Address { get; set; }
public DataType DataType { get; set; }
public string Description { get; set; }
public ushort Size { get; set; }
public float Maximum { get; set; }
public float Minimum { get; set; }
public int Cycle { get; set; }
public TagMetaData(short id, string name, short grpId, string address, DataType type, ushort size, float max = 0, float min = 0, int cycle = 0)
{
Id = id;
GroupId = grpId;
Name = name;
Address = address;
DataType = type;
Size = size;
Maximum = max;
Minimum = min;
Cycle = cycle;
}
public TagMetaData()
{
}
public int CompareTo(TagMetaData other)
{
return this.Id.CompareTo(other.Id);
}
public override string ToString()
{
return Name;
}
}
public class FormulaEngine
{
private readonly StateMachine<RecepieState, Triggers> _machine;
private readonly DAServer daServer;
const float Epsilon = 0.001f;
int Index = 0;
public Recepi Recepi { get; }
Dictionary<Operator, Func<TagItem, Step, bool>> operatorItems = new Dictionary<Operator, Func<TagItem, Step, bool>>
{
{ Operator.Equal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.Unequal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.LessThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.LessThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) <= Epsilon) },
{ Operator.GreaterThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.GreaterThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) >= Epsilon) },
};
public enum RecepieState
{
Running,
Stoped,
Paused
}
public enum Triggers
{
Start,
Pause,
Stop,
GoToNextStep,
GoToPreviousStep,
StartExcecuting
}
public FormulaEngine(Recepi recepi, DAServer daServer)
{
Recepi = recepi;
this.daServer = daServer;
_machine = new StateMachine<RecepieState, Triggers>(RecepieState.Stoped);
ConfigureStateMachine();
}
private void ConfigureStateMachine()
{
_machine.Configure(RecepieState.Running)
.OnEntryAsync(t => ExcecuteRecepi())
//.OnEntryFromAsync(Triggers.StartExcecuting, (t) => ExcecuteRecepi())
//.OnEntryAsync(t => StartTimer())
//.OnExitAsync(t => StopTimer())
.InternalTransitionAsync(Triggers.GoToNextStep, t => OnNextStep())
.InternalTransitionAsync(Triggers.GoToPreviousStep, t => OnPreviousStep())
.Permit(Triggers.Pause, RecepieState.Paused)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Paused)
.Permit(Triggers.Start, RecepieState.Running)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Stoped)
.Permit(Triggers.Start, RecepieState.Running);
}
private async Task ExcecuteRecepi()
{
var stepList = new StepList();
stepList.AddSteps(Recepi.Step);
var step = stepList[Index];
await RunStep(step);
}
private async Task RunStep(Step step)
{
var pvStep = new TagItem(step.SPTag, daServer);
foreach (var node in step.Nodes)
{
// here i'm writing other setpoints to the PLC.
var spNode = new TagItem(node.SPTag, daServer);
await spNode.Write(node.Value);
}
var test = operatorItems[step.Operator];
while (!test(pvStep, step))
{
await Task.Delay(500);
}
Index++;
await _machine.FireAsync(Triggers.GoToNextStep);
}
private async Task OnPreviousStep()
{
throw new NotImplementedException();
}
private async Task OnNextStep()
{
throw new NotImplementedException();
}
//private Task StopTimer()
//{
// throw new NotImplementedException();
//}
//private Task StartTimer()
//{
// Stopwatch stopWatch = new Stopwatch();
// stopWatch.Start();
//}
public void Stop()
{
_machine.Fire(Triggers.Stop);
}
public void Pause()
{
_machine.Fire(Triggers.Pause);
}
public void Start()
{
_machine.FireAsync(Triggers.Start);
_machine.FireAsync(Triggers.StartExcecuting);
}
}
public class StepList : IEnumerable<Step>
{
private Dictionary<int, Step> _stepList;
private readonly object cacheSyncLock;
public StepList()
{
this._stepList = new Dictionary<int, Step>();
this.cacheSyncLock = new object();
}
public void AddSteps(List<Step> stepList)
{
foreach (Step step in stepList)
{
int newIndex = this._stepList.Keys.Count == 0 ? 0 : this._stepList.Keys.Max() + 1;
this._stepList.Add(newIndex, step);
}
}
public IEnumerator<Step> GetEnumerator()
{
return this._stepList
.OrderBy(pair => pair.Key)
.Select(pair => pair.Value)
.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public Step this[int index]
{
get { return this._stepList[index]; }
}
public int? CurrentStepIndex { get; set; }
public bool CanRunNextStep
{
get { return this.CurrentStepIndex.HasValue && this._stepList.ContainsKey(this.CurrentStepIndex.Value + 1); }
}
public bool CanRunPreviousStep
{
get { return this.CurrentStepIndex.HasValue && this._stepList.ContainsKey(this.CurrentStepIndex.Value - 1); }
}
}
}
(首先注意:大多数读者都不知道什么是 PLC。我猜它是一个可编程逻辑控制器。你可能想把它说出来,并在你的问题中粗略描述它)
我将在本文底部留下我原来的答案 - 它仍然对您问题中的编程提出了很好的观点。新部分(紧接在下方)较少涉及编程,更多涉及设计。
另请注意,我在 20 多年的时间里没有做过任何类似的事情(控制系统、PLC 和批处理系统),而在这三个领域中,我在批处理系统方面的经验最少。对了,你的PLC支持IEC-61131-3/SFC这样的语言吗? 1131-3 顺序功能图语言是真正为批处理和配方系统编程而开发的。
为了允许 "The operator can Stop, Pause, Go to Next and Previous step by pressing respective buttons",您可能必须从根本上重构您的程序。您还需要使一切异步。
十年前,我建议在专用线程中执行控制部分,并让 UI 通过队列或其他一些异步机制进行通信。我怀疑你现在可以通过 everything awaitable.
来做到这一点
您可能想让您的食谱步骤更加丰富。据我了解,您的配方步骤现在由一组 tag/setpoint 对组成,然后是一个 step-completed 条件,该条件由一个标签、一个设定点和一个比较操作组成.
您可能需要在食谱步骤中考虑这一点:
- 时间的概念(保持特定状态多长时间)
- 一个步骤可能不仅涉及更改设定值,还涉及 starting/stopping 或 opening/closing 事情
- 步骤的结尾可能很复杂(时间、测量或测量)
- 那个尺寸 "equality" 很多都不够好。测量值超过设定值 20-25% 然后在稳定下来之前稍微响一下的情况并不少见。您可能需要考虑让 "equality" 可选地表示 "within N% of setpoint for T seconds".
此外,要获得您的 pause/stop/next/prev 行为,您不会只是循环执行您的步骤。相反,创建一个 class 维护有序的步骤集合并接受对 pause/stop/next/prev 的命令。然后实现一个涉及步骤、命令和一组中间状态的状态机。
我所说的 "intermediate states" 的意思是,例如暂停一个步骤的命令不会立即导致该步骤暂停。相反,它将经历 "waiting to pause" 状态,然后进入暂停状态。让你的命令 await
可用。当UI调用await Pause()
时,它returns的任务最终会在状态进入Paused状态时完成。停止、下一个和上一个相同。
我在原始答案中展示的小循环的变体:
while (!test(spStep, pvStep))
{
await Task.Delay(pollingTime);
}
可能足以 "power" 您的状态引擎。你 运行 一个简单的轮询循环。循环条件会比我展示的更复杂,并且在调用 Task.Delay
之后可能会有相当多的代码。但是像这样的东西会让你感觉到时间什么时候到了,什么时候应该转换等等。
你们很多人需要有防止操作员进入不安全状态的规则(编码在你的步骤中),然后有代码总是将一个步骤带到安全状态(例如,如果你混合两个组件和唯一安全的状态是 "neither A nor B" 在水箱中或 "all of A and B in the tank",你需要以某种方式对该信息进行编码。记住控制系统的第一级是安全。
当您处于自动模式时,所有发生的事情都是当一个步骤完成时,它只会调用一个 "Next" 自身的命令(直到它到达配方的末尾)。
关于平等的另一个说明
如果你仔细阅读比较浮点数和双精度数,你会看到很多关于如何根据 float
和 double
的范围适当地确定两个数字是否相等的信息。在控制系统中,测量结果很多是不精确的、有滞后现象并且有噪声。您可能不希望根据 float
的范围进行比较,而是根据测量范围进行比较。我似乎记得我工作过的一个系统有一个 "coarse"、"medium" 或 "fine" 属性 与每个测量相关联。例如,"coarse" 可能意味着在测量比例的 2% 以内的两个测量值将被视为 "equal"。
终于
别忘了,如果您觉得这有用,可以给它点赞。而且,如果它回答了您的问题,您可以 "accept" 它。
原答案:
我的理解是System.Threading.SpinWait.SpinUntil
很重,很容易被误用。我绝不是它的使用专家,我已经避免了它。
但是,无论如何,您可能想要做的事情是 awaitable。我不知道你的时间要求是什么(我接触 PLC 已经有几十年了)。但是,我猜如果您每 50 毫秒轮询一次您的值,您就会得到所需的响应。
首先,如评论中所述,您的函数应该是 async Task
而不是 async void
。您可能还想将其重命名为 RunAsync
。所以:
public async Task RunAsync()
由于您的核心操作代码非常相似,我很想用 table 来驱动它。所以考虑这样的事情:
Dictionary<Operator, Func<TagItem, TagItem, bool>> operatorItems =
new Dictionary<Operator, Func<TagItem, TagItem, bool>>
{
{ Operator.Equal, (p,s) => (float.Parse(p.TagValue) == float.Parse(s.TagValue)) },
{ Operator.Unequal, (p,s) => (float.Parse(p.TagValue) != float.Parse(s.TagValue)) },
{ Operator.LessThan, (p,s) => (float.Parse(p.TagValue) < float.Parse(s.TagValue)) },
{ Operator.LessThanOrEqual, (p,s) => (float.Parse(p.TagValue) <= float.Parse(s.TagValue)) },
{ Operator.GreaterThan, (p,s) => (float.Parse(p.TagValue) > float.Parse(s.TagValue)) },
{ Operator.GreaterThanOrEqual, (p,s) => (float.Parse(p.TagValue) >= float.Parse(s.TagValue)) },
};
我在这里构建了一个 table 将运算符映射到要测试的条件。
需要注意的一件事是您正在使用浮点数。比较浮点数是否相等(实际上是相等,不相等,并且在较小程度上,小于或大于)是错误的做法。在 Internet 上查看,您会发现很多关于为什么您应该 永远 这样做的讨论。
您应该定义一个 Epsilon 值,表示“足够接近以被视为相等*”。取决于您正在阅读的内容(即 PLC 信号的噪声程度(如果它是数字化模拟信号)),这可能相当小(或不是)。它还取决于您拥有的值的范围。此外,如果您正在读取的值的范围非常广泛,那么您可能不想使用一个常数,但使用的是您正在查看的值大小的一小部分。
但是,为了简单起见,让我们这样做:
const float Epsilon = 0.000001f; //change this to meet your needs
现在,我们需要更改相等性的定义,使 Epsilon 内任何相等的值(无论哪种方式)都相等:
{ Operator.Equal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.TagValue)) < Epsilon) },
您需要为 "Unequal" 做类似的事情,您应该在 >
、<
、>=
的计算中使用相同的值和 <=
运算符。
无论如何,完成后,您可以简单地执行以下操作:
const int pollingTime = 50; //milliseconds
还有这个:
var test = operatorItems[step.Operator];
while (!test(spStep, pvStep))
{
await Task.Delay(pollingTime);
}
这是我到目前为止编写的代码,我还没有用真正的 PLC 测试过。请问有没有办法实现上一步,然后进入下一步?
public async void Run()
{
//recepi has many steps, and each step has one or many Nodes (commands to PLC)
foreach (Step step in recepi.Step)
{
var spStep = new TagItem(step.SPTag, daServer);//setpoint to the PLC
var pvStep = new TagItem(step.SPTag, daServer);//actual Process value from PLC
foreach (var node in step.Nodes)
{
// here i'm writing other setpoints to the PLC.
var spNode = new TagItem(node.SPTag, daServer);
await spNode.Write(node.Value);
}
//when all the commands are sent to the PLC, i have to wait for the prosess value to reach a certn
//value before i can move to next step and do the same again.
switch (step.Operator)
{
case Operator.Equal:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) == float.Parse(spStep.TagValue)));
break;
case Operator.Unequal:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) != float.Parse(spStep.TagValue)));
break;
case Operator.LessThan:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) < float.Parse(spStep.TagValue)));
break;
case Operator.LessThanOrEqual:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) <= float.Parse(spStep.TagValue)));
break;
case Operator.GreaterThan:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) > float.Parse(spStep.TagValue)));
break;
case Operator.GreaterThanOrEqual:
System.Threading.SpinWait.SpinUntil(() => (float.Parse(pvStep.TagValue) >= float.Parse(spStep.TagValue)));
break;
}
}
}
谢谢你的帮助!
在 UI 中,操作员可以通过按相应的按钮停止、暂停、转到下一步和上一步。我想要实现的是对音乐播放器微笑。 运行 一个配方需要 2 个多小时才能完成一批,因为它是一个缓慢的过程,其中涉及加热。
这是我目前使用状态机的无状态库的代码。
using Formula.Models;
using Formula.UI;
using Stateless;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
public class FormulaEngine
{
private readonly StateMachine<RecepieState, Triggers> _machine;
StateMachine<RecepieState, Triggers>.TriggerWithParameters<Step> _setNextStep;
StateMachine<RecepieState, Triggers>.TriggerWithParameters<Step> _setPreviousStep;
private readonly DAServer _daServer;
private readonly IEventAggregator _ea;
const float Epsilon = 0.001f;
private int _index = 0;
StepList stepList;
public Recepi Recepi { get; }
Dictionary<Operator, Func<TagItem,Step, bool>> operatorItems = new Dictionary<Operator, Func<TagItem, Step, bool>>
{
{ Operator.Equal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.Unequal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.LessThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.LessThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) <= Epsilon) },
{ Operator.GreaterThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.GreaterThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) >= Epsilon) },
};
public enum RecepieState
{
Running,
Stoped,
Paused
}
public enum Triggers
{
Start,
Pause,
Stop,
GoToNextStep,
GoToPreviousStep
}
public FormulaEngine(Recepi recepi, DAServer daServer, IEventAggregator ea)
{
Recepi = recepi;
this._daServer = daServer;
this._ea = ea;
_machine = new StateMachine<RecepieState, Triggers>(RecepieState.Stoped);
_setNextStep = _machine.SetTriggerParameters<Step>(Triggers.GoToNextStep);
_setPreviousStep = _machine.SetTriggerParameters<Step>(Triggers.GoToPreviousStep);
ConfigureStateMachine();
stepList = new StepList();
stepList.AddSteps(Recepi.Step);
}
private void ConfigureStateMachine()
{
_machine.Configure(RecepieState.Running)
.OnEntryAsync(t => ExcecuteRecepi())
.InternalTransitionAsync(Triggers.GoToNextStep, t => OnNextStep())
.InternalTransitionAsync(Triggers.GoToPreviousStep, t => OnPreviousStep())
//.OnEntryFromAsync(Triggers.StartExcecuting, (t) => ExcecuteRecepi())
//.OnEntryAsync(t => StartTimer())
//.OnExitAsync(t => StopTimer())
//.InternalTransitionAsync(_setNextStep, (step, t) => OnNextStep(step))
//.InternalTransitionAsync(_setPreviousStep, (step, t) => OnPreviousStep(step))
.Permit(Triggers.Pause, RecepieState.Paused)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Paused)
.Permit(Triggers.Start, RecepieState.Running)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Stoped)
.Permit(Triggers.Start, RecepieState.Running);
}
public int Index
{
get { return _index; }
set
{
_index = value;
_ea.GetEvent<CurrentStepChangedEvent>().Publish(_index);
}
}
private async Task ExcecuteRecepi()
{
Index = 0;
stepList.CurrentStepIndex = Index;
if (Index <= stepList.Count())
{
var step = stepList[Index];
await RunStep(step);
}
await _machine.FireAsync(Triggers.GoToNextStep);
}
private async Task<bool> RunStep(Step step)
{
var pvStep = new TagItem(step.SPTag, _daServer);
foreach (var node in step.Nodes)
{
// here i'm writing other setpoints to the PLC.
var spNode = new TagItem(node.SPTag, _daServer);
await spNode.Write(node.Value);
}
var test = operatorItems[step.Operator];
while (!test(pvStep, step))
{
await Task.Delay(500);
}
return true;
}
private async Task OnPreviousStep()
{
if(Index > 0)
{
Index--;
if (Index <= stepList.Count())
{
var step = stepList[Index];
await RunStep(step);
}
}
}
private async Task<bool> OnNextStep()
{
Index++;
if (Index <= stepList.Count())
{
var step = stepList[Index];
await RunStep(step);
//var result = Task.Run(async () => await RunStep(step)).Result;
_machine.Fire(Triggers.GoToNextStep);
}
return true;
}
//private Task StopTimer()
//{
// throw new NotImplementedException();
//}
//private Task StartTimer()
//{
// Stopwatch stopWatch = new Stopwatch();
// stopWatch.Start();
//}
public void Stop()
{
_machine.Fire(Triggers.Stop);
}
public void Pause()
{
_machine.Fire(Triggers.Pause);
}
public void Start()
{
_machine.FireAsync(Triggers.Start);
}
}
using Formula.Models;
using Formula.UI;
using Stateless;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace FormulaEngine
{
public enum Activity
{
WAIT_UNTIL = 1
}
public enum RecepieState
{
Running,
Stoped,
Paused
}
public enum Triggers
{
Start,
Pause,
Stop,
GoToNextStep,
GoToPreviousStep,
StartExcecuting
}
public enum Operator
{
[Description("=")]
[EnumMember(Value = "=")]
Equal,
[Description("<>")]
[EnumMember(Value = "<>")]
Unequal,
[Description("<")]
[EnumMember(Value = "<")]
LessThan,
[Description("<=")]
[EnumMember(Value = "<=")]
LessThanOrEqual,
[Description(">")]
[EnumMember(Value = ">")]
GreaterThan,
[Description(">=")]
[EnumMember(Value = ">=")]
GreaterThanOrEqual
}
public class BaseModel
{
public short Id { get; set; }
public string Name { get; set; }
}
public class Recepi : BaseModel
{
public Recepi()
{
Step = new List<Step>();
}
public short FormulaGroupId { get; set; }
public DateTime DateCreated { get; set; }
public string Description { get; set; }
public string CreatedBy { get; set; }
[ForeignKey("RecepiId")]
public List<Step> Step { get; set; }
}
public class Step : BaseModel
{
public Step()
{
Nodes = new List<Node>();
}
public short RecepiId { get; set; }
public short SPTagId { get; set; }
public short PVTagId { get; set; }
public Operator Operator { get; set; }
public TagMetaData SPTag { get; set; }
public TagMetaData PVTag { get; set; }
public string Value { get; set; }
public Activity Activity { get; set; }
[ForeignKey("StepId")]
public List<Node> Nodes { get; set; }
}
public class Node : BaseModel
{
public short StepId { get; set; }
public short SPTagId { get; set; }
public string Value { get; set; }
public TagMetaData SPTag { get; set; }
public TagMetaData PVTag { get; set; }
public Operator Operator { get; set; }
}
public class TagMetaData : BaseModel, IComparable<TagMetaData>
{
//public GroupMetaData GroupMetaData { get; set; }
public short GroupId { get; set; }
public string EngUnit { get; set; }
public DateTime CurrentTimeStamp { get; set; }
public int CurrentQuality { get; set; }
public string Address { get; set; }
public DataType DataType { get; set; }
public string Description { get; set; }
public ushort Size { get; set; }
public float Maximum { get; set; }
public float Minimum { get; set; }
public int Cycle { get; set; }
public TagMetaData(short id, string name, short grpId, string address, DataType type, ushort size, float max = 0, float min = 0, int cycle = 0)
{
Id = id;
GroupId = grpId;
Name = name;
Address = address;
DataType = type;
Size = size;
Maximum = max;
Minimum = min;
Cycle = cycle;
}
public TagMetaData()
{
}
public int CompareTo(TagMetaData other)
{
return this.Id.CompareTo(other.Id);
}
public override string ToString()
{
return Name;
}
}
public class FormulaEngine
{
private readonly StateMachine<RecepieState, Triggers> _machine;
private readonly DAServer daServer;
const float Epsilon = 0.001f;
int Index = 0;
public Recepi Recepi { get; }
Dictionary<Operator, Func<TagItem, Step, bool>> operatorItems = new Dictionary<Operator, Func<TagItem, Step, bool>>
{
{ Operator.Equal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.Unequal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.LessThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) < Epsilon) },
{ Operator.LessThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) <= Epsilon) },
{ Operator.GreaterThan, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) > Epsilon) },
{ Operator.GreaterThanOrEqual, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.Value)) >= Epsilon) },
};
public enum RecepieState
{
Running,
Stoped,
Paused
}
public enum Triggers
{
Start,
Pause,
Stop,
GoToNextStep,
GoToPreviousStep,
StartExcecuting
}
public FormulaEngine(Recepi recepi, DAServer daServer)
{
Recepi = recepi;
this.daServer = daServer;
_machine = new StateMachine<RecepieState, Triggers>(RecepieState.Stoped);
ConfigureStateMachine();
}
private void ConfigureStateMachine()
{
_machine.Configure(RecepieState.Running)
.OnEntryAsync(t => ExcecuteRecepi())
//.OnEntryFromAsync(Triggers.StartExcecuting, (t) => ExcecuteRecepi())
//.OnEntryAsync(t => StartTimer())
//.OnExitAsync(t => StopTimer())
.InternalTransitionAsync(Triggers.GoToNextStep, t => OnNextStep())
.InternalTransitionAsync(Triggers.GoToPreviousStep, t => OnPreviousStep())
.Permit(Triggers.Pause, RecepieState.Paused)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Paused)
.Permit(Triggers.Start, RecepieState.Running)
.Permit(Triggers.Stop, RecepieState.Stoped);
_machine.Configure(RecepieState.Stoped)
.Permit(Triggers.Start, RecepieState.Running);
}
private async Task ExcecuteRecepi()
{
var stepList = new StepList();
stepList.AddSteps(Recepi.Step);
var step = stepList[Index];
await RunStep(step);
}
private async Task RunStep(Step step)
{
var pvStep = new TagItem(step.SPTag, daServer);
foreach (var node in step.Nodes)
{
// here i'm writing other setpoints to the PLC.
var spNode = new TagItem(node.SPTag, daServer);
await spNode.Write(node.Value);
}
var test = operatorItems[step.Operator];
while (!test(pvStep, step))
{
await Task.Delay(500);
}
Index++;
await _machine.FireAsync(Triggers.GoToNextStep);
}
private async Task OnPreviousStep()
{
throw new NotImplementedException();
}
private async Task OnNextStep()
{
throw new NotImplementedException();
}
//private Task StopTimer()
//{
// throw new NotImplementedException();
//}
//private Task StartTimer()
//{
// Stopwatch stopWatch = new Stopwatch();
// stopWatch.Start();
//}
public void Stop()
{
_machine.Fire(Triggers.Stop);
}
public void Pause()
{
_machine.Fire(Triggers.Pause);
}
public void Start()
{
_machine.FireAsync(Triggers.Start);
_machine.FireAsync(Triggers.StartExcecuting);
}
}
public class StepList : IEnumerable<Step>
{
private Dictionary<int, Step> _stepList;
private readonly object cacheSyncLock;
public StepList()
{
this._stepList = new Dictionary<int, Step>();
this.cacheSyncLock = new object();
}
public void AddSteps(List<Step> stepList)
{
foreach (Step step in stepList)
{
int newIndex = this._stepList.Keys.Count == 0 ? 0 : this._stepList.Keys.Max() + 1;
this._stepList.Add(newIndex, step);
}
}
public IEnumerator<Step> GetEnumerator()
{
return this._stepList
.OrderBy(pair => pair.Key)
.Select(pair => pair.Value)
.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public Step this[int index]
{
get { return this._stepList[index]; }
}
public int? CurrentStepIndex { get; set; }
public bool CanRunNextStep
{
get { return this.CurrentStepIndex.HasValue && this._stepList.ContainsKey(this.CurrentStepIndex.Value + 1); }
}
public bool CanRunPreviousStep
{
get { return this.CurrentStepIndex.HasValue && this._stepList.ContainsKey(this.CurrentStepIndex.Value - 1); }
}
}
}
(首先注意:大多数读者都不知道什么是 PLC。我猜它是一个可编程逻辑控制器。你可能想把它说出来,并在你的问题中粗略描述它)
我将在本文底部留下我原来的答案 - 它仍然对您问题中的编程提出了很好的观点。新部分(紧接在下方)较少涉及编程,更多涉及设计。
另请注意,我在 20 多年的时间里没有做过任何类似的事情(控制系统、PLC 和批处理系统),而在这三个领域中,我在批处理系统方面的经验最少。对了,你的PLC支持IEC-61131-3/SFC这样的语言吗? 1131-3 顺序功能图语言是真正为批处理和配方系统编程而开发的。
为了允许 "The operator can Stop, Pause, Go to Next and Previous step by pressing respective buttons",您可能必须从根本上重构您的程序。您还需要使一切异步。
十年前,我建议在专用线程中执行控制部分,并让 UI 通过队列或其他一些异步机制进行通信。我怀疑你现在可以通过 everything awaitable.
来做到这一点您可能想让您的食谱步骤更加丰富。据我了解,您的配方步骤现在由一组 tag/setpoint 对组成,然后是一个 step-completed 条件,该条件由一个标签、一个设定点和一个比较操作组成.
您可能需要在食谱步骤中考虑这一点:
- 时间的概念(保持特定状态多长时间)
- 一个步骤可能不仅涉及更改设定值,还涉及 starting/stopping 或 opening/closing 事情
- 步骤的结尾可能很复杂(时间、测量或测量)
- 那个尺寸 "equality" 很多都不够好。测量值超过设定值 20-25% 然后在稳定下来之前稍微响一下的情况并不少见。您可能需要考虑让 "equality" 可选地表示 "within N% of setpoint for T seconds".
此外,要获得您的 pause/stop/next/prev 行为,您不会只是循环执行您的步骤。相反,创建一个 class 维护有序的步骤集合并接受对 pause/stop/next/prev 的命令。然后实现一个涉及步骤、命令和一组中间状态的状态机。
我所说的 "intermediate states" 的意思是,例如暂停一个步骤的命令不会立即导致该步骤暂停。相反,它将经历 "waiting to pause" 状态,然后进入暂停状态。让你的命令 await
可用。当UI调用await Pause()
时,它returns的任务最终会在状态进入Paused状态时完成。停止、下一个和上一个相同。
我在原始答案中展示的小循环的变体:
while (!test(spStep, pvStep))
{
await Task.Delay(pollingTime);
}
可能足以 "power" 您的状态引擎。你 运行 一个简单的轮询循环。循环条件会比我展示的更复杂,并且在调用 Task.Delay
之后可能会有相当多的代码。但是像这样的东西会让你感觉到时间什么时候到了,什么时候应该转换等等。
你们很多人需要有防止操作员进入不安全状态的规则(编码在你的步骤中),然后有代码总是将一个步骤带到安全状态(例如,如果你混合两个组件和唯一安全的状态是 "neither A nor B" 在水箱中或 "all of A and B in the tank",你需要以某种方式对该信息进行编码。记住控制系统的第一级是安全。
当您处于自动模式时,所有发生的事情都是当一个步骤完成时,它只会调用一个 "Next" 自身的命令(直到它到达配方的末尾)。
关于平等的另一个说明
如果你仔细阅读比较浮点数和双精度数,你会看到很多关于如何根据 float
和 double
的范围适当地确定两个数字是否相等的信息。在控制系统中,测量结果很多是不精确的、有滞后现象并且有噪声。您可能不希望根据 float
的范围进行比较,而是根据测量范围进行比较。我似乎记得我工作过的一个系统有一个 "coarse"、"medium" 或 "fine" 属性 与每个测量相关联。例如,"coarse" 可能意味着在测量比例的 2% 以内的两个测量值将被视为 "equal"。
终于
别忘了,如果您觉得这有用,可以给它点赞。而且,如果它回答了您的问题,您可以 "accept" 它。
原答案:
我的理解是System.Threading.SpinWait.SpinUntil
很重,很容易被误用。我绝不是它的使用专家,我已经避免了它。
但是,无论如何,您可能想要做的事情是 awaitable。我不知道你的时间要求是什么(我接触 PLC 已经有几十年了)。但是,我猜如果您每 50 毫秒轮询一次您的值,您就会得到所需的响应。
首先,如评论中所述,您的函数应该是 async Task
而不是 async void
。您可能还想将其重命名为 RunAsync
。所以:
public async Task RunAsync()
由于您的核心操作代码非常相似,我很想用 table 来驱动它。所以考虑这样的事情:
Dictionary<Operator, Func<TagItem, TagItem, bool>> operatorItems =
new Dictionary<Operator, Func<TagItem, TagItem, bool>>
{
{ Operator.Equal, (p,s) => (float.Parse(p.TagValue) == float.Parse(s.TagValue)) },
{ Operator.Unequal, (p,s) => (float.Parse(p.TagValue) != float.Parse(s.TagValue)) },
{ Operator.LessThan, (p,s) => (float.Parse(p.TagValue) < float.Parse(s.TagValue)) },
{ Operator.LessThanOrEqual, (p,s) => (float.Parse(p.TagValue) <= float.Parse(s.TagValue)) },
{ Operator.GreaterThan, (p,s) => (float.Parse(p.TagValue) > float.Parse(s.TagValue)) },
{ Operator.GreaterThanOrEqual, (p,s) => (float.Parse(p.TagValue) >= float.Parse(s.TagValue)) },
};
我在这里构建了一个 table 将运算符映射到要测试的条件。
需要注意的一件事是您正在使用浮点数。比较浮点数是否相等(实际上是相等,不相等,并且在较小程度上,小于或大于)是错误的做法。在 Internet 上查看,您会发现很多关于为什么您应该 永远 这样做的讨论。
您应该定义一个 Epsilon 值,表示“足够接近以被视为相等*”。取决于您正在阅读的内容(即 PLC 信号的噪声程度(如果它是数字化模拟信号)),这可能相当小(或不是)。它还取决于您拥有的值的范围。此外,如果您正在读取的值的范围非常广泛,那么您可能不想使用一个常数,但使用的是您正在查看的值大小的一小部分。
但是,为了简单起见,让我们这样做:
const float Epsilon = 0.000001f; //change this to meet your needs
现在,我们需要更改相等性的定义,使 Epsilon 内任何相等的值(无论哪种方式)都相等:
{ Operator.Equal, (p,s) => (Math.Abs(float.Parse(p.TagValue) - float.Parse(s.TagValue)) < Epsilon) },
您需要为 "Unequal" 做类似的事情,您应该在 >
、<
、>=
的计算中使用相同的值和 <=
运算符。
无论如何,完成后,您可以简单地执行以下操作:
const int pollingTime = 50; //milliseconds
还有这个:
var test = operatorItems[step.Operator];
while (!test(spStep, pvStep))
{
await Task.Delay(pollingTime);
}