понедельник, 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.