суббота, 7 апреля 2012 г.

Покрытие кода с gcov и gcovr

Одним из популярных средств измерения покрытия кода тестами служит утилита gcov. Данная утилита используется совместно с gcc для анализа программ с целью достижения наибольшей производительности и эффективности кода, и выявления не покрываемых тестами участков кода. Предоставляемая статистика утилитой gcov, позволяет ответить на следующие вопросы:

  • Как часто (количество прохождений) выполняется каждая строка кода.
  • Какие строки кода не выполняются.
  • Какой процент строк кода покрыт тестами для каждого файла.
  • Какой процент ветвлений (условий) покрыт тестами для каждого файла.

Использование утилиты gcov для анализа кода, требует наличия при компиляции флагов -fprofile-arcs -ftest-coverage, которые сообщают компилятору о необходимости дополнительной информацию требуемой для gcov (граф прохождения программы), а также о необходимости включить дополнительные инструкции кода непосредственно в объектные файлы, также необходимых для gcov.

В общем и простейшем случае процедура запуска компиляции следующая:
$ g++ -fprofile-arcs -ftest-coverage main.cpp

Рассмотрим использование gcov на чуть более сложном примере. Пусть в нашем распоряжении имеется тестируемый проект, который состоит:

  • main.cpp – в котором расположена инструкция запуска тестов.
  • gtest/ – каталог, в котором находится исходный код тестовой среды Google Test.
  • SRM204/ – каталог, в котором находятся заголовки с решениями и тестами задач.

В таком случае процесс запуска компиляции проекта с последующей возможностью анализа покрытия кода тестами будет выглядеть следующим образом:
$ mkdir Debug && cd Debug/
$ g++ -I.. -I../SRM204 -c -fprofile-arcs -ftest-coverage -o "main.o" "../main.cpp"
$ g++ -I.. -I../SRM204 -c -fprofile-arcs -ftest-coverage -o "gtest-all.o" "../gtest/gtest-all.cc"
$ g++ -fprofile-arcs -ftest-coverage -o "TopCoder.exe"  ./gtest-all.o  ./main.o

После успешной компиляции необходимо запустить полученный исполняемый файл, в ходе исполнения которого будут сгенерированы необходимые утилите gcov вспомогательные для последующего анализа:
$ ./TopCoder.exe

Далее, используя утилиту gcov, получим общие сведения о покрытии кода тестами, а также отчеты по покрытию кода для каждого файла проекта в отдельности, которые имеют расширение *.gcov:
$ gcov -b ../main.cpp

Фрагмент сведений выводимых, утилитой gcov в ходе формирования отчетов по каждому используемому файлу (с исходным кодом) в ходе исполнения работы тестируемой программы:
File '../SRM204/Apothecary.h'
Lines executed:100.00% of 67
Branches executed:62.16% of 222
Taken at least once:36.94% of 222
Calls executed:49.14% of 175
../SRM204/Apothecary.h:creating 'Apothecary.h.gcov'

Из представленного фрагмента, очевидно, что покрытие кода, в данном файле, составляет 100%, при этом вероятность прохождения всех веток составляет 62%.

Не стоит удивляться, если в ходе «профайлинга» будет представлена информация о покрытии кода подключаемых заголовков стандартной библиотеки C/C++:
$ ls . | grep ".gcov$"
...   ...   ...
new.gcov
new_allocator.h.gcov
ostream.gcov
sstream.gcov
...   ...   ...

Ниже представлен фрагмент сгенерированного отчета утилитой gcov по файлу с исходным кодом:
98:   77: for (int j = 0; j < (int) inputs[i].length(); j++) {
91:   78:    iter = elem->find(inputs[i][j]);
91:   79:    if (iter == elem->end()) {
73:   80:       if (inputs[i][j] != ' ') {
 -:   81:          // ... ...
 -:   82:       }
 -:   83:    }
 -:   84:    else {
18:   85:       iter->second++;
 -:   86:    }
 -:   87: }
Значения, расположенные слева отражают сколько раз была исполнена та или иная строка кода. В данном конкретном случае, очевидно, что строка с номером 85 была выполнена в ходе тестирования 18 раз, а 81-ая строка – 65 раз соответственно.

Сама по себе утилита gcov не предоставляет никаких возможностей по фильтрации тех исходных файлов, которые не тестируются или вообще не относятся к проекту (например, исходные коды стандартной библиотеки C/C++), кроме того, нет никаких возможностей по резюмированию и протоколированию результатов. Однако существует утилита gcovr, которая расширяет возможности gcov и представляет собой скрипт на языке Python, который решает обозначенные выше проблемы утилиты gcov.

Как было упомянуто, утилита gcovr базируется на gcov, поэтому все действия при компиляции и требование как минимум одного запуска тестов остаются в прежними, дальнейшие же действия похожи, но имеют ряд расширенных возможностей.

Поскольку синтаксис использования утилиты gcov не представляет из себя что-то из ряда вон выходящего, то перейдем непосредственно к рассмотрению примера работы с рассматриваемой утилиты. Запросим резюме на покрытие тестами кода в исходных файлах корневого каталога данного проекта (опция -r позволяет указать каталог, исходные файлы которого, в том числе и в нижележащих каталогах, будут анализироваться на предмет покрытия), за исключением каталога gtest/, в котором расположены исходные коды Google Test (опция -e позволяет указать, исходные файлы каких каталогов не подвергать анализу):
$ gcovr -r ../ -e '..*/gtest/'
------------------------------------------------------------------------------
File                                       Lines    Exec  Cover   Missing
------------------------------------------------------------------------------
SRM204/Aaagmnrs.h                             67      65    97%   44-45
SRM204/Apothecary.h                           67      67   100%
SRM204/Medici.h                               58      58   100%
main.cpp                                       4       4   100%
------------------------------------------------------------------------------
TOTAL                                        196     194    98%
------------------------------------------------------------------------------

Результат, выводимый утилитой на консоль, при отсутствии флага исключения (-e --exclude) включал бы в себя сведения о покрытии кода самой тестовой среды Google Test.

Для того, чтобы получить отчет по каждому файлу, как в gcov, необходимо использовать опцию -k --keep, поскольку по-умолчанию gcovr удаляет все отчеты:
$ gcovr -k -r ../ -e '..*/gtest/'

Если необходимо определить покрытие кода не по строкам, а по ветвям, достаточно указать соответствующую опцию -b --branches:
$ gcovr -b -r ../ -e '..*/gtest/'
------------------------------------------------------------------------------
File                                      Branch   Taken  Cover   Missing
------------------------------------------------------------------------------
SRM204/Aaagmnrs.h                            180      99    55%   33,34, ...
SRM204/Apothecary.h                          138      82    59%   67,74, ...
SRM204/Medici.h                              138      76    55%   30,49, ...
main.cpp                                       4       3    75%   31
------------------------------------------------------------------------------
TOTAL                                        460     260    56%
------------------------------------------------------------------------------

Но наиболее яркой особенностью утилиты gcovr является генерация отчетов о покрытии кода тестами в формате XML, пригодных для отображения результатов покрытия в таких сервисах, как Jenkins и Hudson:
$ gcovr -r ../ -e '..*/gtest/' -x > coverage-report.xml

Таким образом, использование утилит gcov и gcovr позволяет в полном объеме оценить в процентном соотношении покрытие кода тестами, а при соответствующей интеграции с сервисами Jenkins и Hudson отследить динамику изменений.

четверг, 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».

пятница, 2 сентября 2011 г.

Сокеты на сетевом уровне

В продолжении к заметке о "Сокетах на канальном уровне". Для создания сокета на сетевом уровне, необходимо указывать тип SOCK_RAW:

sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

В таком случае ethernet кадр будет собран ядром, и соответственно в поле "протокол" (h_proto) ethernet кадра будет указан IP-протокол (0x0800). Очевидно, что осуществить обмен по низкоуровневым протоколам уже не удастся, поскольку для обмена по тому же ARP, в поле "протокол" ethernet кадра должен быть указан 0x0806. Тем не менее, используя сокеты сетевого уровня, вы можете собрать ICMP (протокол сокета IPPROTO_ICMP), IGMP (протокол сокета IPPROTO_IGMP) пакеты.

Сбор пакетов начиная с заголовка IP можно осуществлять указав в качестве протокола сокета IPPROTO_RAW, а также параметр IP_HDRINCL:

int en = 1;
int sock;
sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &ena, sizeof(ena));

Подобные сокеты необходимы, когда требуется реализовать обмен по протоколу, который не использует протоколы UDP и TCP, и не поддерживается ядром, например, протокол OSPF (протокол маршрутизации).

В качестве примера работы с символьными сокетами создадим простое приложение, которое отправляет upd дейтаграмму, самостоятельно заполняя ip и udp заголовки.

Первоначально, заполним ip пакет, структура которого определена в файле ip.h:

struct hostent      *src_host, *dst_host;
struct ip           *iph;       // указатель на IP заголовок
char                datagram[1024];

... ... ...

// Инициализируем все структуры
memset(&src_host, 0, sizeof(src_host));
memset(&dst_host, 0, sizeof(dst_host));
memset(&param, 0, sizeof(param));
memset(&datagram, 0, sizeof(datagram));

dst_host = gethostbyname(argv[1]);  // IP адрес назначения
src_host = gethostbyname(argv[2]);  // исходящий IP адрес

// размещаем и заполняем заголовок IP-дейтаграммы
iph = (struct ip *) datagram;
iph->ip_v = 4;      // версия проток. - IPv4
iph->ip_hl = 5;     // длина заголовка IP пакета в 32-х разр. словах.
iph->ip_tos = 0;    // тип службы - обычно игнорируется
iph->ip_id = 0;     // игнорируем, т.к. не фрагментируем
iph->ip_off = 0;    // нет смещения, т.к. не фрагментируем
iph->ip_ttl = 64;   // время жизни - 64 перехода
iph->ip_p = 17;      // номер протокола - используем IP

// заполняем адреса отправителя и получателя соответственно
memcpy(&(iph->ip_src.s_addr), src_host->h_addr, src_host->h_length);
memcpy(&(iph->ip_dst.s_addr), dst_host->h_addr, dst_host->h_length);

/* Размер всей IP дейтаграммы и контрольная сумма обычно заполняется ядром.
iph->ip_len = 20;
iph->ip_sum = checksum((short unsigned int *) datagram, 10); */


Отмечу, что номера протоколов для IP заголовка можно посмотреть в RFC 1700, кроме того, обращу внимание на то, что IP адрес источника можно указывать ненастоящий - правильность этого поля ядром проверяться не будет, но в этом случае не стоит потом ждать ответа.

После того, как заполнен IP заголовок, разместим за ним заголовок UDP, определив его структуру:

// структура UDP заголовка
struct udp {
    unsigned short  dsp;    // порт источника
    unsigned short  scp;    // порт назначения
    unsigned short  len;    // длина UDP
    unsigned short  sum;    // контрольная сумма UDP
};

... ... ...

char *msg = "Hello World";

// размещаем и заполняем udp заголовок
udph = (struct udp *) (datagram + sizeof(struct ip));
udph->dsp = htons(14350);   // порт назначения
udph->scp = htons(14350);   // порт источника
udph->sum = 0;      // Для UDP не обязательна контр. сумма
    
// строку с приветствием добавляем в данные UDP пакета
strcpy((char *)(datagram + sizeof(struct ip) + sizeof(struct udp)), msg);
udph->len = htons(sizeof(struct udp) + strlen(msg)); // длина UDP

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

// адрес сокета
memcpy(&param.sin_addr.s_addr, dst_host->h_name, dst_host->h_length);
param.sin_family = AF_INET;

// создаем сокет на сетевом уровне (протокол IP)
ena = 1;
sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &ena, sizeof(ena));

    
if (sendto(sock, &datagram, sizeof(struct ip) + sizeof(struct udp), 0, (struct sockaddr *) &param, sizeof(param)) < 0) {
    printf("IP Packet was sent unsuccessfully...\n");
}
else { printf("IP Packet was sent (size %d)\n", iph->ip_len); 

Запустим анализатор трафика Wireshark и убедимся в том, что дейтаграмма была успешно отправлена - рис. 1.

Рис. 1. Отправленная UDP дейтаграмма.



Сокеты на канальном уровне

Начну данную заметку с определения понятия сырого сокета, встречаемого в литературе под названием символьный или неструктурированный сокет. Сырой сокет — это сокет, позволяет принимает пакеты, обходя уровни TCP и UDP в стеке TCP/IP и отправляя их непосредственно приложению или отправляя их аналогичным образом.

Ниже представлен пример создания сырого сокета, функционирующего на канальном уровне по протоколу ARP:
int sock = socket(AF_INET, SOCK_PACKET, ETH_P_ARP);

Тип сокета (2-й аргумент) — SOCK_PACKET позволяет осуществляет работу на канальном уровне.

В типе протокола (3-й аргумент) можно указать один из протоколов канального уровня, определения которых представлены в файле if_ether.h. Но стоит иметь ввиду, что имя протокола может быть определено, но при этом не поддерживаться ядром. Кроме того важно помнить, что если при создании сырого сокета определено не нулевое значение протокола, то значение поля протокола полученной дейтаграммы должно совпадать с этим не нулевым значением, иначе дейтаграмма не будет доставлена на данный сокет.

В качестве примера работы с сырым сокетом, напишем простенькое приложение, которое будет отправлять ARP запрос на получение MAC адреса устройства в сети по известному IP адресу.

Первым делом потребуется заполнить заголовок ethernet кадра. Структуру заголовка можно определить самостоятельно или воспользоваться уже определенной — struct ethhdr, расположенной в заголовочном файле if_ether.h.

char            datagram[1024]; // буфер исходящего запроса.
struct ifreq    hw_src;  // для хранения MAC-адрес исх. устройства.
struct ethhdr  *eth_hdr;  // указатель на заголовок ethernet кадра.
int             sock;

… … … 

// заполняем нулями буфера и структуры.
memset(&hw_src, 0, sizeof(hw_src));
memset(&datagram, 0, sizeof(datagram));

// запрашиваем MAC адрес сетевой карты (wlan0) на данном машине.
strcpy(hw_src.ifr_ifrn.ifrn_name, "wlan0");
sock = socket(AF_INET, SOCK_DGRAM, 0);
ioctl(sock, SIOCGIFHWADDR, &hw_src);

// Заголовок ethenet кадра помещаем в начало исходящего пакета.
eth_hdr = (struct ethhdr *) datagram;
 
// Заполняем поля заголовка ethenet кадра:
eth_hdr->h_proto = htons(0x0806);    // номер ARP протокола.
for (int i = 0; i < ETH_ALEN; i++) { // заполняем адреса отправ. и назнач. 
 eth_hdr->h_source[i] = hw_src.ifr_ifru.ifru_hwaddr.sa_data[i];
 eth_hdr->h_dest[i] = 0xFF;  // адрес назначения —  широковещ.
}

Теперь необходимо заполнить заголовок ARP, воспользуемся определенной структурой arphdr — файл if_arp.h:

struct arphdr       *arp_hdr;

… … …

// Заголовок ARP располагаем за заголовком ethenet кадра.
arp_hdr = (struct arphdr *) (datagram + sizeof(struct ethhdr));

arp_hdr->ar_hrd = htons(ARPHRD_ETHER); // используем ARP для Ethernet
arp_hdr->ar_pro = htons(0x0800);      // протокол IPv4
arp_hdr->ar_hln = 6;                // длина MAC адреса
arp_hdr->ar_pln = 4;                // длина IPv4 адреса
arp_hdr->ar_op = htons(ARPOP_REQUEST);  // тип ARP пакета — запрос.

Далее необходимо заполнить структуру ARP-запроса:

// структура ARP запроса для IPv4.
struct arp_reqv4 {
    unsigned char ar_sha[ETH_ALEN]; // MAC адрес исходящего устройства
    unsigned char ar_sip[4];  // IP адрес исходящего устройства
    unsigned char ar_dha[ETH_ALEN]; // MAC адрес устройства назначения
    unsigned char ar_dip[4];  // IP адрес устройства назначения
};

… … … 

struct arp_reqv4    *arp_req;  // указатель на ARP запрос.

… … … 

// располагаем ARP запрос за заголовком ARP.
arp_req = (struct arp_reqv4 *) (datagram + sizeof(struct ethhdr) + sizeof(struct arphdr));

// копируем MAC адрес исходящего устройства.
memcpy(&arp_req->ar_sha, hw_src.ifr_ifru.ifru_hwaddr.sa_data, 6);
memcpy(&arp_req->ar_dip, dst_addr, 4); // Копируем IP адрес назначения.

// запрашиваем IP-адрес сетевой карты (wlan0) на данной машине.
memset(&hw_src, 0, sizeof(hw_src));
strcpy(hw_src.ifr_ifrn.ifrn_name, "wlan0");
ioctl(sock, SIOCGIFADDR, &hw_src);

// и копируем IP адрес его (IP адрес исходящего устройства).
memcpy(&arp_req->ar_sip, (hw_src.ifr_ifru.ifru_addr.sa_data + 2), 4);

Запрос полностью готов, теперь необходимо создать сокет на канальном уровне и отправить наш ARP запрос в сеть:

int                 sock, enable;
const int           *ref_en;
struct sockaddr     param;

… … …

memset(&param, 0, sizeof(param));
strcpy(param.sa_data, "wlan0");

ref_en = &enable; enable = 1;
sock = socket(AF_INET, SOCK_PACKET, ETH_P_ARP);
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, ref_en, sizeof(enable));
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, ref_en, sizeof(enable));

if (sendto(sock, datagram, sizeof(ethhdr) + sizeof(arphdr) + sizeof(arp_reqv4), 0, (struct sockaddr *) &param, sizeof(param)) < 0) {
 printf("ARP request was sent unsuccessfully...\n");
}
else {
 printf ("ARP request was sent successfully...\n");
}

Запустим анализатор трафика Wireshark и убедимся в том, что запрос отправляется и что не мало важно приходит ответ - рис. 1. В ниже представленном примере осуществляется запрос MAC адреса роутера с IP адресом 192.168.0.3.

Рис. 1. Запрос MAC адреса роутера по IP.