Показаны сообщения с ярлыком OPC. Показать все сообщения
Показаны сообщения с ярлыком OPC. Показать все сообщения

четверг, 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-сервера в зависимости от типа запроса.

среда, 5 октября 2011 г.

Настройка веб-сервиса OPC-сервера

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

В качестве веб-сервиса OPC-сервера, будем использовать сервис ICONICS OPC XML DA Wrapper. Для его функционирования необходим сервер IIS (Microsoft Internet Information Server), который может быть не установлен в системе по-умолчанию. Если IIS не установлен, то перейдите ‘Пуск -> Панель управления -> Установка и удаление программ’ и в появившемся окне, перейдя во вкладку ‘Установка компонентов Windows’, запустите процесс установки IIS (может потребоваться диск с дистрибутивом Windows).

Рис. 1. Установка IIS.

После того, как установлен Internet Information Server (IIS), необходимо зарегистрировать его с ASP.NET, используя утилиту aspnet_regiis.exe, которая расположена в директории .NET Framework:

%WINDIR%\Microsoft.NET\Framework\<версия .NET Framework>\aspnet_regiis.exe -i

Теперь добавьте виртуальную директорию, установленного сервиса XML DA в IIS, для этого перейдите ‘Панель управления -> Администрирование -> Internet Information Services’. В появившемся окне раскройте список сервисов на вашем локальном компьютере и затем, щелкнув левой кнопкой мыши по ‘Веб-узел по умолчанию’, добавьте виртуальную директорию. При создании виртуальной директории укажите полный путь к папке, куда вы установили веб-сервис. Настраивая права доступа к виртуального каталогу, разрешите чтение, запуск сценариев и выполнение, как показано на рисунке 2.

Рис. 2. Настройка прав доступа к виртуальному каталогу.

Теперь перейдем к настройке веб-сервиса OPC-сервера. Чтобы ассоциировать XML DA с необходимым вам OPC-сервером, необходимо в каталоге, в который была установлена служба, переименовать файл ‘OPC_XML-DA_WrapperService.asmx’ в ProgID вашего OPC-сервера, в моем случае это ‘Mitsubishi.MXOPC.6.asmx’. Важно отметить, что в данном случае подразумевается, что OPC-сервер и веб-служба будут расположены на одном хосте.

Если все действия были выполнены правильно, то набрав адрес веб-службы в браузере (в моем случае это http://localhost/xmlda/Mitsubishi.MXOPC.6.asmx), вы увидите страницу, похожую на ту, что представлена на рисунке 3 ниже.

Рис. 3. Внешний вид страницы описания веб-службы.

Чтобы выяснить осуществляется ли обмен между веб-сервисом и OPC-сервером, можно воспользоваться html страничкой с java-скриптом, который поставляется вместе с ICONICS OPC XML DA Wrapper, и имеет название js_sample.html. Скопируйте этот файл в каталог, куда вы установили веб-сервис и затем, набрав в браузере его адрес, в моем случае это http://localhost/xmlda/js_sample.htm, убедитесь в наличии связи между веб-службой и OPC-сервером.

Рис. 4. Сведения об OPC-сервере через веб-сервис.

понедельник, 5 сентября 2011 г.

Automation-интерфейс OPC

Сервера OPC в соответствие со спецификацией OPC DA 3.0 предоставляют два типа интерфейсов, так называемый automation-интерфейc и custom-интерфейс. На рис. 1 представлена архитектура OPC взаимодействия.

Рис. 1. Архитектура OPC взаимодействия.

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

В данной заметке будет кратко рассмотрен обмен с OPC-сервером через automation-интерфейс.

Клиентское приложение, используя automation-интерфейс, осуществляет обмен по custom-интерфейсу через библиотеку посредника (wrapper), как представлено на рис. 1. В данной заметке в качестве DLL библиотеки automation-интерфейса будет использован библиотека OPCDAAuto.dll.

Первым делом не плохо бы узнать, какие OPC-сервера зарегистрированы на том или ином узле. Для этого можно воспользоваться методом GetOPCServers класса OPCServer, ниже представлен фрагмент кода, который выведет список имен зарегистрированных на хосте OPC-серверов:

OPCServer opc_server = new OPCServerClass();

// запрашиваем список имен зарегестрированных OPC-серверов на локальном узоле.
object list_servers = opc_server.GetOPCServers("127.0.0.1");

// выводим на консоль список имен зарегестрированных OPC-серверов.
System.Array y = ((System.Array)(list_servers));
for (int i = 1; i <= y.Length; i++) {
    System.Console.WriteLine(y.GetValue(i).ToString());
}
Чтобы получить дополнительные сведения об OPC-сервере, можно воспользоваться методами класса OPCServer:
// подключаемся к серверу с именем serv_name (например, Mitsubishi.MXOPC.4) на локальном хосте
serv.Connect(serv_name, "localhost");

// выводим имя OPC-сервера
Console.WriteLine("Name: " + serv.ServerName);

// информацию о производителе
Console.WriteLine("Vendor: " + serv.VendorInfo);

// версия OPC-сервера
Console.WriteLine("Ver: " + serv.MajorVersion.ToString() + "." + serv.MinorVersion.ToString());

// состояние OPC-сервера
int state = serv.ServerState;
string msg = "Unknown";
switch((OPCServerState) state) {
    case OPCServerState.OPCRunning:
        msg = "Running"; break;
    case OPCServerState.OPCSuspended:
        msg = "Suspended"; break;
    case OPCServerState.OPCNoconfig:
        msg = "Has no configuration"; break;
    case OPCServerState.OPCFailed:
        msg = "Fail"; break;
    case OPCServerState.OPCTest:
        msg = "Test mode"; break;
    case OPCServerState.OPCDisconnected:
        msg = "Disconnected"; break;
    default: break;
}

Console.WriteLine("State: " + msg);

// время запуска OPC сервера
Console.WriteLine("Started: " + serv.StartTime.ToString("dd.MM.yyyy hh:mm"));
Подключившись к серверу, мы имеем также возможность получить сведения о его переменных и узлах, хранящих переменные (устройства или группы). На рис. 2 представлен пример древовидной организации хранения переменных в OPC-сервере.

Рис. 2. Древовидная организация хранения переменных в OPC-сервере. 

Вывести все имена переменных сервера можно двумя способами: первый способ, наиболее простой, вывести все имена с полным путем. Второй способ, самостоятельно обойдя дерево (может потребоваться, если вы, к примеру, собираетесь заполнить древовидную структуру, например, тот же графический элемент TreeView). Ниже представлен пример того, как вывести все имена с полным путем:
// подключаемся по имени к OPC-серверу на локальном хосте.
opc_server.Connect(serv_name, "localhost");

// создаем экземпляр браузера.
OPCBrowser browser = opc_server.CreateBrowser(); 

// переход к корню дерева переменных OPC-сервера
browser.MoveToRoot();

// получить имена всех переменных, включая те, что расположены
// в нижележащих узлах (аргумент true).
browser.ShowLeafs(true);
    
// выводим список имен переменных OPC-сервера
// важно: счет начинается с единицы!
for (int i = 1; i < browser.Count + 1; i++) {
    Console.WriteLine(browser.Item(i));
}
На рис. 3 представлен результат исполнения представленного выше фрагмента кода.

Рис. 3. Список имен переменных OPC-сервера с полным указанием к ним пути. 

При реализации второго способа (самостоятельный обход дерева), после вызова метода браузера для перехода к корню дерева вызываем рекурсивную функцию:
// вызываем рекурсивный метод печати дерева переменных
PrintBranch(browser, 0);

… … …

// Рекурсивная функция, которая обходит дерево с уровня lvl
public static void PrintBranch(OPCBrowser browser, int level) {
    string spaces = "";  // для визуальн. отлич. потомков от родител.
    for (int i = 0; i < level; i++) { spaces += "\t"; }

    // получить имена переменных только этого уровня (не вложенных)
    browser.ShowLeafs(false);
    for (int i = 1; i < browser.Count + 1; i++) {
        Console.WriteLine(spaces + browser.Item(i));
    }

    // получить имена узлов этого уровня
    browser.ShowBranches();
    
    for (int i = 1; i < browser.Count + 1; i++) {
        Console.WriteLine(spaces + ":: " + browser.Item(i));

        // опускаемся для обхода нижележащего уровня
        browser.MoveDown(browser.Item(i));
        PrintBranch(browser, level + 1);

        // возвращаемся на прежний уровень
        browser.MoveUp();

        // и вновь запрашиваем все узлы на этом уровне
        browser.ShowBranches();
    }
}
Разобравшись с тем, как выводить информацию об OPC-сервере и его содержимом, имеет смысл перейти к рассмотрению к тому, как осуществляется чтение и запись переменных OPC-сервера. 

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

Чтобы выполнить чтение переменных, необходимо создать группу опрашиваемых переменных на сервере, занести в нее представляющие интерес переменные, после чего осуществить чтение данных:
// подключаемся к OPC-серверу на локальном хосте
OPCServer opc_server = new OPCServerClass();
opc_server.Connect(serv_name, "127.0.0.1");

// создаем экземпляр браузера
OPCBrowser browser = opc_server.CreateBrowser();
browser.ShowLeafs(true);

// создаем группу в OPC-сервере
OPCGroups groups = opc_server.OPCGroups;
groups.DefaultGroupIsActive = true;
OPCGroup group = groups.Add("MyGroup");

// указываем с какими переменными будем работать
OPCItems items = group.OPCItems;
int [] hserv = new int[browser.Count + 1];
int idtrans = 2000;

// добавляем переменные в группу и сохр. их идентифик.
// помните! индексы все с единицы!
for (int i = 1; i < browser.Count + 1; i++) {
    hserv[i] = items.AddItem(browser.Item(i), idtrans).ServerHandle;
}

Array handlers = (Array) hserv;
Array errors;   // массив отображающий статус считан. перемен.
Array values;   // массив значений переменных
object qualities;   // массив качества переменных
object timestamps;  // массив временных штампом

// синхронно считываем с устройства все значения переменных
group.SyncRead((short) OPCDataSource.OPCDevice, browser.Count, ref handlers, out values, out errors, out qualities, out timestamps); 

// выводим на печать результаты (индексы с единицы!)
for (int i = 1; i < browser.Count + 1; i++) {
    Console.WriteLine(browser.Item(i) + ":");
    Console.WriteLine("\tValue: " + values.GetValue(i).ToString());
    
    // выясняем качество переменной
    switch(int.Parse(((Array) qualities).GetValue(i).ToString())) {
        case (int) OPCQuality.OPCQualityGood:
            Console.WriteLine("\tQuality: Good"); break;
        default:
            Console.WriteLine("\tQuality: Bad"); break;
    }
    Console.WriteLine("\tTimeStamp: " + ((DateTime)((Array) timestamps).GetValue(i)).ToString("dd.MM.yyyy hh:mm:ss"));
    Console.WriteLine();
}

opc_server.Disconnect(); // разрываем соединение
На рис. 4 представлен результат деятельности представленного фрагмента программы, выполняющей чтение значений переменных OPC-сервера.

Рис. 4. Результат чтения значений переменных OPC-сервера. 

В заключение данной заметки, в качестве демонстрации работы с методами записи данных, осуществим запись строки «HELLO SERVER» в строковую переменную Dev01.StringVar:
// подключаемся к OPC-серверу
OPCServer serv = new OPCServerClass();
serv.Connect(serv_name, "127.0.0.1");

// создаем группу и добавляем в нее переменную
// счет идет с единицы!
OPCGroup group = serv.OPCGroups.Add("MyGroup");
int [] hserv = new int[2];
hserv[1] = group.OPCItems.AddItem("Dev01.StringVar", 2000).ServerHandle;

// Записываем значение переменной в OPC-сервер
object [] values = new object[2]; // одно значение будем писать
values[1] = "HELLO SERVER";

Array hs = (Array) hserv;
Array vals = (Array) values;
group.SyncWrite(1, ref hs, ref vals, out err);

Thread.Sleep(500); // даем серверу время на исполнение

// Выводим переменную сервера после изменений
group.SyncRead((short) OPCDataSource.OPCDevice, 1, ref hs, out outval, out err, out qualities, out stamps);
Console.WriteLine(outval.GetValue(1).ToString());

Таким образом, используя automation-интерфейс, можно создавать полноценные клиенты к OPC-серверам. В данной заметке были рассмотрены далеко не все возможности (сервисы), предоставляемые automation-интерфейсом, более полно ознакомиться с automation-интерфейсом можно непосредственно в соответствующей спецификации «OPC Data Access Automation Specification».