ФБ «Скрипт C#» и его использование в MasterSCADA. Квитирование сообщений

ФБ «Скрипт C#» и его использование в MasterSCADA. Квитирование сообщений

В предыдущей статье мы рассмотрели отслеживание возникновения сообщений. Теперь рассмотрим ситуацию, когда сообщения нужно квитировать.
Квитирование из скрипта может потребоваться, например, если сигнал квитирования приходит с контроллера (оператор квитировал сообщение с локальной панели управления или кнопки на щите).
Сообщения в MasterSCADA находятся в кэше и в архиве. Поэтому в зависимости от задачи способ их квитирования будет разным. Квитирование в архиве требуется, если нужна возможность квитировать несколько сообщений от одного источника или выполнять квитирование из прошлой сессии (то есть после того как скада выключена). При этом для выполнения квитирования сообщений с прошлой сессии необходимо, чтобы архивация сообщений велась в базу данных (при архивации в файловый архив, квитирования таких сообщение не будет). Сначала рассмотрим квитирование из кэша – в рамках текущей сессии. Квитирование такое сообщение очень просто – для нужно:
1. Получить сообщение (или сообщения) используя уже знакомый по прошлой статье метод GetEvents.
2. Выполнить квитирование этого сообщения методом AckEvents, того же класса AlarmManager. Также можно выполнить квитирование методом AckEventsInternal – этот метод позволяет также указать нужную отметку времени и оператора (метод AckEvents всегда квитирует с текущей меткой и текущим оператором).
Для примера рассмотрим самый простой вариант квитирования – квитирование одного из сообщений, возникающих в объекте скрипта.
Например, имеем следующую структуру проекта:
По сигналу на входе Квитировать, мы должны выполнить квитирование сообщение возникшее от События 1.
Вот итоговый код скрипта:
public partial class ФБ : ScriptBase
{  
    bool? M=false;
    string NameEvent="Событие 1";
    List<ITreeObjectHlp> Source=new List<ITreeObjectHlp>(); 
     
    public override void Execute()    
    {            
  if (Квитировать==true && M==false)
  {         
      var AlarmEvents = HostFB.TreeItemHlp.Project.AlarmManager;         
         var Filter = new EventFilterData();                          
         Filter.OnlyNotAcked=true;        
         Source.Add(HostFB.TreeItemHlp.Parent.GetChild(NameEvent));         
         Filter.Sources=Source;
         var Events = AlarmEvents.GetEvents(HostFB.TreeItemHlp.Parent,Filter,50);                
   foreach (var Event in Events)
   {        
    MasterSCADA.Interfaces.EventID[] AckEvent =new[] {Event.EventID};     
    HostFB.TreeItemHlp.Project.AlarmManager.AckEvents(AckEvent,"Комментарий)");     
   }
     } 
   M=Квитировать;     
    }    
 
}



Рассмотрим его подробнее. Вначале мы объявляем имя переменной, которую будем квитировать – здесь оно задано как переменная в коде, но можно получить ее из входа, формировать в коде и т.д. Также объявляется коллекция Source – это коллекция источников сообщений для фильтра.

По переднему фронту входа Квитировать происходит подключение к менеджеру сообщений. Затем создается фильтр с выводом только неквитированных сообщений. Поскольку нас интересует конкретное сообщение, с конкретным источником, то можно сразу добавить этот источник в фильтр, а не производить фильтрацию затем в цикле. Для этого в этого в коллекцию Source добавляется переменная с именем NameEvent.
Затем с помощью метода GetEvents происходит получение всех сообщений, удовлетворяющих фильтру.
Получив сообщения, мы можем выполнять квитирование. Для этого в цикле мы создаем переменную AckEvent с ID нашего сообщения, а затем с помощью метода AckEvents выполняем его квитирование с комментарием.
Использовать цикл конкретно в данной задаче не обязательно – сообщение всегда будет одно. Но если вы добавите в коллекцию Source несколько источников, то тогда использовать цикл нужно обязательно.

Проверим работу нашего скрипта. Сгенерируем сообщения от всех событий.
А теперь выполним квитирование из скрипта – квитировалось только сообщение События 1.
Все корректно.

Но как быть если сообщений от одного источника несколько. Попробуем сгенерировать несколько сообщений, а затем квитируем из скрипта.
Как мы видим квитировалось только последнее сообщение.
Дело в том, что остальные сообщения уже переместились в архив, поэтому чтобы квитировать их там, необходимо обращаться к архиву – для этого применяется другой метод.

Добавим в проект новый объект и сделаем аналогичную структуру:
Теперь перейдем к скрипту. Скрипт квитирования всех сообщений источника будет выглядеть следующим образом:

public partial class ФБ : ScriptBase
{    
    bool? M=false;
    string NameEvent="Событие 1";
    List<ITreeObjectHlp> Source=new List<ITreeObjectHlp>(); 
     
    public override void Execute()    
    {    
 if (Квитировать==true && M==false)
 { 
     var project = HostFB.TreeItemHlp.Project;
        var filter = new EventFilterData();              
        Source.Add(HostFB.TreeItemHlp.Parent.GetChild(NameEvent));         
        filter.Sources=Source;
        filter.OnlyNotAcked=true;
        var server=project.GetService<EventServer>();        
        var enumerator = server.CreateEnumerator(HostFB.TreeItemHlp.Parent, filter, true);
        //получение последних 50 сообщений
        var archEvents = enumerator.Next(50);         
  foreach (var Event in archEvents)
   {        
    MasterSCADA.Interfaces.EventID[] AckEvent =new[] {Event.EventID};     
    HostFB.TreeItemHlp.Project.AlarmManager.AckEvents(AckEvent,"Комментарий)");     
   }
    }
    M= Квитировать;     
   


Как вы можете видеть скрипт практически идентичен предыдущему. Единственное отличие – в способе получения сообщений:
var server=project.GetService<EventServer>();        
var enumerator = server.CreateEnumerator(HostFB.TreeItemHlp.Parent, filter, true);
//получение последних 50 сообщений
var archEvents = enumerator.Next(50);



В данном коде происходит подключение к сервису архивных сообщений, при этом указывается интересующий нас объект и фильтр. Затем с помощью метода Next происходит запрос не более 50 последних сообщений. После этого полученные сообщения можно квитировать аналогичным методом.
В методе Next можно указать и большее количество сообщений, но лучше запрашивать поочередно – выполнять метод Next в цикле пока не будет возвращено <50 сообщений (то есть менее запрашиваемого количества).
Проверим работу скрипта.
Теперь квитировались все имеющиеся сообщения События 1.
Какой из методов получения сообщений для квитирования вы будете использовать – зависит от вашей задачи. Квитирование из архива – более универсальный метод, но он требует больших ресурсов чем квитирование из кэша (так как происходит обращение к базе, поиск сообщений и т.д.). Ссылка на проект с указанными скриптами находится в конце статьи.
Рассмотрим пример использования скрипта квитирования для конкретной задачи.
В контроллере существует ряд целочисленных переменных, взведение битов которых приводит к генерации сообщения. При этом квитирование сообщение может происходить с локальной панели управления, так и из SCADA системы. При этом необходима синхронизация квитирования – если квитирование было выполнено в SCADA, то сигнал об этом должен пойти в контроллер, и там сообщение также будет квитирование, и наоборот – квитирование в контроллере, должно вызывать квитирование сообщения в SCADA.
На рисунке представлено дерево системы, с шестью переменными. Переменная Alarm – это источник сообщений, Act_to_PLC переменная в которую мы должны записать номер бита сообщения, которое было квитирование в SCADA (после этого контроллер обнулит переменную), Ack_From_PLC – переменная биты которых указывают какие сообщения были квитированы (после квитирования мы должны обнулить переменную).
Задача состоит из двух частей:
1. Необходимо отслеживать квитирование сообщений и взводить в определенной команде соответствующий бит
2. Необходимо отслеживать изменение значения переменной Ack_From_PLC и выполнять квитирование соответствующих сообщений.
Дерево объектов будет выглядеть следующим образом.
Скрипт «Состояние сообщений» решает первую задачу – отслеживает квитирование сообщений в SCADA. При квитировании, скрипт определяет объект, в котором было выполнено квитирование и взводит в переменной ToPLC нужный бит. Значение этой переменной идет в контроллер, также переменная имеет обратную связь – чтобы переменная обнулилась, после того как контроллер квитирует сообщение у себя и сбросит все биты. Вот код данного скрипта:
public partial class ФБ : ScriptBase
{
    string path="";
    const string NameCommand="ToPLC"; //Имя команды в которую передается сигнал о квитировании
    const string NameEvent="Событие "; //Шаблон имени события от которого генерируется сообщение
    int CountEvents=16; //максимальное количество событий в объекте
     
    public override void Start()
    {   
     var project = HostFB.TreeItemHlp.Project;     
     path=project.ObjectTreeRootItem.Name+".";
        //подписка на изменение сообщений
        HostFB.TreeItemHlp.Project.AlarmManager.OnRecordsChangeEvent += AlarmManager_OnRecordsChangeEvent; 
        //подписка на добавление сообщений
        HostFB.TreeItemHlp.Project.AlarmManager.OnRecordsAddEvent += AlarmManager_OnRecordsChangeEvent; 
     }   
 
  
    void AlarmManager_OnRecordsChangeEvent(MasterSCADA.Hlp.Events.AlarmManagerHlp manager, MasterSCADA.Interfaces.EventID[] eventIDs)
    {            
        var project = HostFB.TreeItemHlp.Project;
        var filter = new EventFilterData();                       
        filter.EventIDs=eventIDs; //фильтруем по новым EventID                   
        var Events = project.AlarmManager.GetEvents(HostFB.TreeItemHlp.Parent, filter, 1000);                 
        //получаем атрибуты сообщения, поскольку сообщений может быть несколько, то в реальном скрипте перебор нужно делать через цикл                       
        foreach (var Event in Events)
        {       
         //проверяем что сообщение было квитировано
         if (Event.AckTime!=null && (Event.InactiveTime==null || Event.InactiveTime<Event.AckTime))
         {                          
          if (Event.Actor=="PLC") continue; //квитирование было выполнено контроллером - выходим                   
          string path1=string.Format("{0}{1}.{2}",path,Event.Object,NameCommand); //формируем путь до команды записи в контроллер    
          string Source=Event.Source;          
          //получаем значение переменной ToPLC
          var elem = (ITreePinHlp)HostFB.TreeItemHlp.Project.Item(path1);        
          if (elem==null) continue; //переменная не найдена - выходим
          var ItemValue=0;          
          if  (elem.GetRTPin().ObjectValue!=null)                   
          ItemValue=Convert.ToInt32(elem.GetRTPin().ObjectValue);
          //определяем номер сообщения
          string NumStr=Event.Source.Replace(NameEvent,"");//убираем маску имени
          int NumBit=0;
          try
          {
          NumBit=Convert.ToInt32(NumStr); //преобразуем в число
          }
          //неизвестный или некорретный номер сообщения - выходим
          catch {return;}          
          if (NumBit<CountEvents)
          {
           var NewVal=SetBit(ItemValue,NumBit,true);                      
           elem.GetRTPin(MasterSCADALib.PinTypes.PT_POUT).ObjectValue=NewVal;
           elem.GetRTPin(MasterSCADALib.PinTypes.PT_PIN).ObjectValue=NewVal;            
          }          
          
         }
     }   
    }
    //функция установки бита
    public int SetBit(int Val,int NumBit,bool BitValue)
    {
     byte[ ] byteArray = BitConverter.GetBytes( Val );
     BitArray bitArray = new BitArray(byteArray);
     bitArray[NumBit]=BitValue;
     int[] array = new int[1];
     bitArray.CopyTo(array, 0);
     return array[0];     
    }
 
}



В каждом объекте (их в проекте всего 2, но может быть любое количество) находится скрипт, который отслеживает состояние переменной FromPLC. Если переменная меняется, скрипт перебирает все биты переменной и находит взведенные. После этого выполняется квитирование указанного сообщения.

public partial class ФБ : ScriptBase
{
 const string NameCommand="FromPLC"; //имя команды с сигналом квитирования из контроллера
 const string NameEvent="Событие "; //шаблон имени события
 int? OldBitValue=0;
    public override void Execute()
    {
     //получаем путь до команды FromPLC
     string path1=string.Format("{0}.{1}",HostFB.TreeItemHlp.ParentObject.FullName,NameCommand);     
     var elem = (ITreePinHlp)HostFB.TreeItemHlp.Project.Item(path1);               
     var ItemValue=СостояниеКвитирования;
     if (ItemValue.HasValue==false || ItemValue.Value==0 || elem==null) return;  
     //разбираем переменную на биты
     byte[ ] byteArray = BitConverter.GetBytes(ItemValue.Value);
     BitArray bitArray = new BitArray(byteArray);
     for (int i=0;i<bitArray.Length;i++)
     {
      //перебираем все биты и если бит взведен - вызываем функцию квитирования сообщения
      if (bitArray[i]==true)
      AckMessage(NameEvent+i);
     }     
     //в конце сбрасываем значение
     elem.AddAssignValueTask(0,null);
     OldBitValue=ItemValue.Value;
    }
     
    //функция квитирования сообщения
    void AckMessage(string Name)
    {
        //Получения пути до текущего объекта
     var str = HostFB.TreeItemHlp.ParentObject.FullName;
     //получение пути до заданного сообщения
     str = string.Format("{0}.{1}",str,Name); 
     //проверяем существование переменной
     var elem = (ITreePinHlp)HostFB.TreeItemHlp.Project.Item(str);
     if (elem==null) return;
     //получаем ID переменной
     int _eventID = HostFB.TreeItemHlp.Project.SafeItem(str).ID;      
     var alarms_kvit = HostFB.TreeItemHlp.Project.AlarmManager;         
        var filter_kvit = new EventFilterData();        
        filter_kvit.OnlyNotAcked=true;        
        //получаем сообщения
        var events_kvit = alarms_kvit.GetEvents(HostFB.TreeItemHlp.Parent,filter_kvit,50);        
        //выполняем квитирование
  foreach (var id in events_kvit)
  {    
   if (id.EventID.SourceID != _eventID) continue;     
   MasterSCADA.Interfaces.EventID[] oo =new[] {id.EventID};     
   //получаем ID оператора
   var opers = HostFB.TreeItemHlp.Project.Computers[0].Operators["PLC"].Id;
   //квитируем сообщения от имени оператора PLC и с указанным текстом
   HostFB.TreeItemHlp.Project.AlarmManager.AckEventsInternal(oo, DateTime.UtcNow, opers, "Квитировано в контроллере");
  }
     
    }
}


Обратите внимание, что квитирование производится от имени оператора PLC (он должен быть создан в системе и иметь соответствующие права) – это позволяет в скрипте отслеживания понять, что сообщение квитировано из контроллера и не выполнять никаких действий.
Архив с двумя проектами и конфигурацией OPC сервера можно скачать по ссылке.