четверг, 17 ноября 2011 г.

XML DA интерфейс OPC

Как говорилось в предыдущих заметках, серверы OPC могут предоставлять не только интерфейсы automation (который был рассмотрен в прошлой заметке) и custom, но и веб-интерфейс, что позволяет функционировать клиентам OPC-серверов на других платформах, используя для обмена с сервером SOAP сообщения.

В данной заметке рассмотрим, каким образом, используя веб-интерфейс XML DA (с процессом установки и настройки сервиса ICONICS OPC XML DA Wrapper можно ознакомиться в этой заметке), можно осуществлять обмен с OPC-сервером, используя средства платформы .NET на языке C#.

Перед тем, как рассматривать реализацию методов работы с OPC сервером, замечу, что с шаблонами запросами и ответами SOAP можно ознакомиться, перейдя в браузере по адресу сервиса OPC-сервера. На рисунке 1 представлен пример шаблона SOAP сообщений:

Рис. 1. Шаблон запроса и ответа SOAP статуса сервера.

На основании представленного шаблона создадим класс ServerStatus, который будет отображать основную информацию о текущем состоянии сервера. Ниже представлены основные члены класса состояния сервера ServerStatus:

public DateTime StartTime { get; set; }
public ServerState State { get; set; }
public string ProductVersion { get; set; }
public string ClientRequestHandle { get; set; }
public string RevisedLocaleID { get; set; }
public string VendorInfo { get; set; }
public string [] SupportedLocaleIDs { get; set; }
public string [] SupportedInterfaceVersions { get; set; }

Для реализации непосредственно обмена с XML DA с OPC сервером будем использовать класс WebRequest из пространства имен System.Net, собирая SOAP запросы и разбирая SOAP ответы. Ниже представлен код, формирующий SOAP запрос текущего состояния OPC-сервера на базе шаблона, который был представлен на рисунке 1.

// Address – это строка, которая представляет собой адрес Веб-сервиса, 
// в моем случае это - http://localhost/xmlda/Mitsubishi.MXOPC.6.asmx
WebRequest request = WebRequest.Create(Address);
request.Method = "POST";
request.ContentType = "text/xml; charset=utf-8";

// SOAP запрос о состоянии сервера.
string req_msg = "" 
   + ""
   + ""
   + ""
   + "";

request.ContentLength = req_msg.Length; // Длина сообщения.

После того, как запрос собран, его необходимо отправить и соответственно принять (или не принять) ответ:

Stream stream = request.GetRequestStream();
StreamWriter writer = new StreamWriter(stream);

writer.Write(req_msg); // Отправляем SOAP запрос о состоянии сервера.
writer.Close();

// Принимаем или не принимаем (тогда будет исключение) SOAP ответ.
StreamReader reader = new StreamReader (request.GetResponse().GetResponseStream());
string resp_msg = reader.ReadToEnd();
reader.Close();

Ответ будет также представлен в формате XML, поэтому дальнейшая задача заключается в том, что разобрать полученное сообщение и вернуть экземпляр класса ServerStatus. Для решения этой задачи воспользуемся существующими средствами пространства имен System.Xml, ниже представлен пример, как это можно осуществить:

… … …
// Создаем экземпляр документа XML, в который
// загружаем полученный ответ.
XmlDocument document = new XmlDocument();
document.LoadXml(resp_msg);

// Создаем экземпляр класса статуса сервера.
StatusServer status = new StatusServer();

// Извлекаем сведения о производителе.
XmlNode node = document.GetElementsByTagName("VendorInfo")[0];
status.VendorInfo = node.InnerText.Trim();

// Извлекаем сведения о поддерживаемых сервером
// региональных настроек.
XmlNodeList nodes = document.GetElementsByTagName("SupportedLocaleIDs");
status.SupportedLocaleIDs = new string[nodes.Count];
for (int i = 0; i < nodes.Count; i++) {
    status.SupportedLocaleIDs[i] = nodes[i].InnerText.Trim();
}

// Извлекаем сведения о поддерживаемых версиях XML DA
// интерфейсов, например XML_DA_Version_1_0.
nodes = document.GetElementsByTagName("SupportedInterfaceVersions");
status.SupportedInterfaceVersions = new string[nodes.Count];
for (int i = 0; i < nodes.Count; i++) {
    status.SupportedInterfaceVersions[i] = nodes[i].InnerText.Trim();
}

// Извлекаем время запуска OPC-сервера.
node = document.GetElementsByTagName("Status")[0];
XmlAttribute attr = node.Attributes["StartTime"];
status.StartTime = DateTime.Parse(attr.InnerText.Trim());

// Версия OPC-сервера.
attr = node.Attributes["ProductVersion"];
status.ProductVersion = attr.InnerText.Trim();

// Извлекаем идентификатор запроса.
node = document.GetElementsByTagName("GetStatusResult")[0];
attr = node.Attributes["ClientRequestHandle"];
status.ClientRequestHandle = attr.InnerText.Trim();

attr = node.Attributes["RevisedLocaleID"];
status.RevisedLocaleID = attr.InnerText.Trim();

// Извлекаем состояние сервера и преобразуем строковое
// значение в более удобное представление.
attr = node.Attributes["ServerState"];
if (attr.InnerText.Trim() == "running") { 
    status.State = ServerState.Running; 
}
if (attr.InnerText.Trim() == "failed") { 
    status.State = ServerState.Failed; 
}
if (attr.InnerText.Trim() == "noConfig") { 
    status.State = ServerState.NoConfig; 
}
if (attr.InnerText.Trim() == "suspended") { 
    status.State = ServerState.Suspended; 
}
if (attr.InnerText.Trim() == "test") { 
    status.State = ServerState.Test; 
}
if (attr.InnerText.Trim() == "commFault") { 
    status.State = ServerState.CommFault; 
}

// Возвращаем экземпляр класса о текущем статусе сервера.
return status;
Полученная в ходе запроса состояния сервера информация пригодится в дальнейшем, особенно важно обратить внимание на список поддерживаемых региональных настроек - узел SupportedLocaleIDs, при построении запросов в следующих шагах необходимо выбрать одну из поддерживаемых сервером региональных настроек, в нашем случае это будет "en". Данное строковое выражение будет использоваться в качестве фактического значения атрибута LocaleID.

Чтобы оперировать с переменными OPC-сервера, осуществляя чтение состония и значения переменных и запись знаний, необходимо какие переменные имеются в сервера и как минимум знать полный путь до них. Веб-сервис реализует метод Browse по аналогии с методом из automation интерфейса, который позволяет получить содержимое OPC-сервера.

Для хранения информации об элементе OPC-сервера, получаемой в ходе выполнения метода Browse будем использовать класс BrowseElement:

public class BrowseElement {
    public string Name { get; set; }
    public string ItemPath { get; set; }
    public bool IsItem { get; set; }
    public bool HasChildren { get; set; }

    public BrowseElement() {}
}
Ниже представлен фрагмент кода, осуществляющий запрос переменных OPC-сервера:
WebRequest request = WebRequest.Create(Address);
request.Method = "POST";
request.ContentType = "text/xml; charset=utf-8";

// Готовим фильтр элементов для запроса содержимого 
// OPC-сервера в зависимости от параметра.
string str_filter = "all";
switch(filter) {
    case BrowseFilter.Branch:
        str_filter = "branch";
        break;
    case BrowseFilter.Item:
        str_filter = "item";
        break;
    case BrowseFilter.All:
    default:
        str_filter = "all";
        break;
}

// Формируем непосредство тело запроса к сервера
// на получение содержимых элементов.
string req_msg = ""
       + ""
           + ""
           + ""
       + ""
    + "";

request.ContentLength = req_msg.Length;

// Отправляем SOAP запрос серверу.
Stream stream = request.GetRequestStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(req_msg);
writer.Close();

// Получаем SOAP ответ от сервера.
StreamReader reader = new StreamReader (request.GetResponse().GetResponseStream());
string resp_msg = reader.ReadToEnd();
reader.Close();

// Создаем экземпляр класса XML документа для 
// дальнейшего разбора документа.
XmlDocument document = new XmlDocument();
document.LoadXml(resp_msg);

// Вытаскиваем все узлы Elements - в них содержится
// информация об элементах сервера.
XmlNodeList nodes = document.GetElementsByTagName("Elements");
BrowseElement []elements = new BrowseElement[nodes.Count];
for (int i = 0; i < nodes.Count; i++) {
    elements[i] = new BrowseElement();

    // Извлекаем имя элемента OPC-сервера (переменной или
    // каталога или систеного элемента.
    XmlAttribute attr = nodes[i].Attributes["Name"];
    elements[i].Name = attr.InnerText.Trim();

    // Извлекаем полное имя включая путь элемента.
    attr = nodes[i].Attributes["ItemName"];
    elements[i].ItemPath = attr.InnerText.Trim();

    // Извлекаем свойство элемента, определяющее
    // является ли он переменной.
    attr = nodes[i].Attributes["IsItem"];
    elements[i].IsItem = bool.Parse(attr.InnerText.Trim());

    // Извлекаем свойство, опред. имееются ли потомки
    // у данного элемента.
    attr = nodes[i].Attributes["HasChildren"];
    elements[i].HasChildren = bool.Parse(attr.InnerText.Trim());
}

// Возвращаем массив переменных.
return elements;

Результат работы данного метода представлен на рисунке 2.

Рис. 2. Список переменных каталога Dev01 OPC-сервера.

Стоит заметить, что в отличие от своего собрата метод Browse в интерфейсе automation способен самостоятельно обойти дерево переменных в OPC-сервере. В нашем случае придется реализовать обход дерева переменных самостоятельно. Ниже представлен фрагмент кода, демонстрирующий рекурсивный обход дерева:
// Создаем экземпляр класса, в котором реализуем методы взаимодействия с 
// веб-сервисом OPC-сервера.
OpcXmlDaWrapper service = new OpcXmlDaWrapper("http://localhost/xmlda/Mitsubishi.MXOPC.6.asmx");

… … …

// Рекурсивный метод вывода на экран всех элементов OPC-сервера.
Browse(service, ""); 

… … …
// Аргумент dir - это начальный узел, с которой начинать обход дерева
// объектов OPC-сервера.
public static void Browse(OpcXmlDaWrapper service, string dir) {
   BrowseElement [] bri = service.Browse("", dir, BrowseFilter.All);

   for (int i = 0; i < bri.Length; i++) {
      if (bri[i].IsItem) {
         Console.WriteLine("{0}", bri[i].ItemPath);
      }
      else if (bri[i].HasChildren) {
         Browse(service, bri[i].ItemPath);
      }
   }
}

Теперь, когда у нас имеется список переменных OPC-сервера, реализуем метод, осуществляющий чтение переменных. За основу также берем шаблон из веб-сервиса и вместо заполнителей подставляем фактические значения:
// Создаем экземпляр запроса серверу
WebRequest request = WebRequest.Create(Address);
request.Method = "POST";
request.ContentType = "text/xml; charset=utf-8";

// Заполняем узлы переменных, которые хотим
// считать.
string req_items = "";
for (int i = 0; i < items.Length; i++) {
    req_items += """";
}

// Строим запрос SOAP в соответствие с шаблоном
// веб-сервиса, подставляя вместо заполнителей 
// фактические значения.
string req_msg = ""
    + ""
        + ""
        + ""
        + ""
        + req_items
        + ""
        + ""
    + ""
    + "";

// Длина SOAP запроса.
request.ContentLength = req_msg.Length;

// Отправляем запрос веб-сервису.
Stream stream = request.GetRequestStream();
StreamWriter writer = new StreamWriter(stream);
writer.Write(req_msg);
writer.Close();

// Считываем ответ от веб-сервиса.
StreamReader reader = new StreamReader (request.GetResponse().GetResponseStream());
string resp_msg = reader.ReadToEnd();
reader.Close();

// По аналогии с предыдущими примерами разбираем XML документ (ответ сервера).
... ... ...
Очень важно отметить, что в SOAP запросе в узлах Items атрибут ItemPath должен быть равен пустой строке! А в качестве значения атрибута ItemName принимается имя переменной с указанием полного пути! Это два очень важных момента. В старых версиях рассматриваемого веб-сервиса атрибут ItemPath игнорировался, в более новых версиях это не так, поэтому будьте внимательны.
     
Как при операциях чтения, так и при операциях записи, фактическому значению переменной соответствует строго определенный тип (строковый, целый, с плавающей точкой и так далее). Поэтому при разборе XML ответа с фактическими значениями переменных необходимо проверять атрибут xsi:type узла Value, в котором указан формат переменной, например, xsd:int или xsd:short или xsd:double. Аналогичная ситуация при присвоении значения переменным OPC-сервера - также необходимо указать тип записываемого значения.
    
В качестве логического заключения данной заметки рассмотрим запись значений в переменные OPC-сервера. Действия тут абсолютно такие же, отличие только в шаблоне, из информации в ответе нас интересует только наличие ошибок: если ошибок нет, то запись прошла успешно. В качестве примера присвоим переменной Dev01.StringVar значение «Hello Server!»:
WebRequest request = WebRequest.Create(Address);
request.Method = "POST";
request.ContentType = "text/xml; charset=utf-8";

// Формируем запрос на запись значения в переменную, поскольку планируем
// присвоить новое значение текстовой переменной, то указываем в качестве типа xsd:string.
// item_name – строковое значение, равное Dev01.StringVar, а value – строковое значение, 
// равное «Hello Server!», собственно, которое хотим присвоить.
string message = ""
   + ""
   + ""
   + ""
   + ""
   + ""
      + "" + value + ""
   + ""
   + ""
   + ""
   + ""
   + "";

// Указываем длину запроса.
request.ContentLength = message.Length;

// Получаем поток для отправки сформированного запроса сервису.
Stream stream = request.GetRequestStream();
StreamWriter writer = new StreamWriter(stream);

writer.Write(message); // Отправляем запрос серверу
writer.Close();

// Получаем ответ и анализируем на наличие ошибок.
StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream());
string resp_msg = reader.ReadToEnd(); 

// Приводить

// Разбираем XML документ (ответ от сервера).
… … …
На рисунке 3 представлен результат операции записи нового значения («Hello Server!») в строковую переменную OPC-сервера Dev01.StringVar.
             
Рис. 3. Значения переменных OPC-сервера после осуществления записи.

Таким образом, используя веб-интерфейс OPC-сервера можно реализовывать полноценные OPC-клиенты, по аналогии с тем, как это было показано в заметке про automation-интерфейс. Замечу, что в данной заметке при реализации методов чтения, записи и других методов использовались не все возможности, предоставляемые шаблонами. Используя другие опции, представленные в шаблонах, вы можете требовать в ответах большего количества всевозможной информации касательно переменных и OPC-сервера в зависимости от типа запроса.

Комментариев нет:

Отправить комментарий