Квитирование из скрипта может потребоваться, например, если сигнал квитирования приходит с контроллера (оператор квитировал сообщение с локальной панели управления или кнопки на щите).
Сообщения в MasterSCADA находятся в кэше и в архиве. Поэтому в зависимости от задачи способ их квитирования будет разным. Квитирование в архиве требуется, если нужна возможность квитировать несколько сообщений от одного источника или выполнять квитирование из прошлой сессии (то есть после того как скада выключена). При этом для выполнения квитирования сообщений с прошлой сессии необходимо, чтобы архивация сообщений велась в базу данных (при архивации в файловый архив, квитирования таких сообщение не будет). Сначала рассмотрим квитирование из кэша – в рамках текущей сессии. Квитирование такое сообщение очень просто – для нужно:
1. Получить сообщение (или сообщения) используя уже знакомый по прошлой статье метод GetEvents.
2. Выполнить квитирование этого сообщения методом AckEvents, того же класса AlarmManager. Также можно выполнить квитирование методом AckEventsInternal – этот метод позволяет также указать нужную отметку времени и оператора (метод AckEvents всегда квитирует с текущей меткой и текущим оператором).
Для примера рассмотрим самый простой вариант квитирования – квитирование одного из сообщений, возникающих в объекте скрипта.
Например, имеем следующую структуру проекта:

Вот итоговый код скрипта:
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 несколько источников, то тогда использовать цикл нужно обязательно.
Проверим работу нашего скрипта. Сгенерируем сообщения от всех событий.


Но как быть если сообщений от одного источника несколько. Попробуем сгенерировать несколько сообщений, а затем квитируем из скрипта.

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

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 сообщений (то есть менее запрашиваемого количества).
Проверим работу скрипта.

Какой из методов получения сообщений для квитирования вы будете использовать – зависит от вашей задачи. Квитирование из архива – более универсальный метод, но он требует больших ресурсов чем квитирование из кэша (так как происходит обращение к базе, поиск сообщений и т.д.). Ссылка на проект с указанными скриптами находится в конце статьи.
Рассмотрим пример использования скрипта квитирования для конкретной задачи.
В контроллере существует ряд целочисленных переменных, взведение битов которых приводит к генерации сообщения. При этом квитирование сообщение может происходить с локальной панели управления, так и из SCADA системы. При этом необходима синхронизация квитирования – если квитирование было выполнено в SCADA, то сигнал об этом должен пойти в контроллер, и там сообщение также будет квитирование, и наоборот – квитирование в контроллере, должно вызывать квитирование сообщения в SCADA.
На рисунке представлено дерево системы, с шестью переменными. Переменная Alarm – это источник сообщений, Act_to_PLC переменная в которую мы должны записать номер бита сообщения, которое было квитирование в SCADA (после этого контроллер обнулит переменную), Ack_From_PLC – переменная биты которых указывают какие сообщения были квитированы (после квитирования мы должны обнулить переменную).

1. Необходимо отслеживать квитирование сообщений и взводить в определенной команде соответствующий бит
2. Необходимо отслеживать изменение значения переменной Ack_From_PLC и выполнять квитирование соответствующих сообщений.
Дерево объектов будет выглядеть следующим образом.

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 сервера можно скачать по ссылке.