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" 自身的命令(直到它到达配方的末尾)。


关于平等的另一个说明

如果你仔细阅读比较浮点数和双精度数,你会看到很多关于如何根据 floatdouble 的范围适当地确定两个数字是否相等的信息。在控制系统中,测量结果很多是不精确的、有滞后现象并且有噪声。您可能不希望根据 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);
}