Як написати opc da сервер

Як написати opc da сервер

OPC - інтерфейс інтеграції різнорідних систем і пристроїв з різними протоколами обміну. В даний час механізм і специфікація OPC є основним інструментом для обміну даними в системах автоматіцаціі і обліку. OPC DA в цьому відношенні є вже дещо застарілим, але поки що найпоширенішим стандартом. На зміну йому вже кілька років настає новий, об'єднаний, мультиплатформенний стандарт OPC UA. Крім цього існують ще дві менш поширених специфікації OPC HDA (для запиту архівних даних, тобто коли один тег має ще один вимір - час) і OPC AE (специфічний стандарт для передачі тривог і подій). Я не писав серверів OPC HDA і AE, тому нічого корисного розповісти вам не зможу.

Ви скажете, а як же базове завдання отримання даних з пристроїв-обчислювачів, які накопичують і зберігають дані по каналах в часі, їх адже неможливо передати через OPC DA? Ви матимете рацію - дійсно неможливо, але я в цьому випадку трохи хитрував і передавав тимчасової зріз послідовно всередині одного тега або кодувати в одному, а в такому випадку завдання розпакування даних лягала на OPC клієнта, що в багатьох випадках дозволяло реалізовувати обробку даних усередині них.

Повернемося до написання простуйшего OPC DA сервера для типового пристрою. До речі це зовсім необов'язково, що сервер пишеться для інтеграції саме пристроїв. Ні, на іншій стороні може бути все, що завгодно від бази даних, до текстових файлів або навіть людини, який ці дані вводить через будь-який інтерфейс. У загальному випадку нам потрібно знати лише апаратний інтерфейс взаємодії з віддаленим об'єктом і протокол обміну з ним.

Отже ми маємо:
якийсь пристрій з послідовним інтерфейсом
протокол обміну відбувається в режимі запит-відповідь
майстром є пристрій, клієнт-сервер підключається і запитує дані

Як написати opc da сервер

Природно для інших інтерфейсів в хід підуть інші програми, наприклад для ethernet сніфери. Для протоколу modbus корисно відразу встановити кілька утиліт, які дозволять опитувати всі регістри, що б перевірити власні посилки на коректність.

Це буде необхідно якщо виникнуть проблеми при налагодженні сервера і:
  • порівняння посилок, що генеруються власним сервером і сервісним ПО
  • порівняння регістрів обміну, таймаутів між посилками
  • щоб упевнитися, що справа не в коді

Сервер буду писати на базі звичної для мене бібліотеки lighopc (www.ipi.ac.ru/lab43/lopc-ru.html). Существет чимало інших бібліотек і каркасів серверів і клієнтів, які можна знайти в інтернеті, з багатьма я в сові час попрацював, але зупинився саме на lightopc через його простоти. Цей проект давно вже не розвивається і не підтримує інші специфікації крім OPC DA.

Тепер приступаємо безпосередньо до написання сервера. Запускаємо VS, вибираємо тип проекту - консольний додаток, а сам проект залишаємо порожнім. Я використовую консольний додаток, поскільки в старе добру консоль зручно виводити то, ніж в даний момент займається сервер, включаючи поточні значення параметрів, запити / відповіді від устойств або його власні помилки.

Як написати opc da сервер

Створюємо вихідний порожній файл сервера. Далі я не буду заглиблюватися в проектування і написання класів, розподіл їх по окремих файлів і модулів, так як моя задача лише показати саму суть, тобто створити максимально простий зразок сервера. Отже, мінімальний проект OPC сервера буде складатися з вихідного файлу на мові C, заголовки, файлу інтерфейсів і декількох бібліотек.

Створюємо і додаємо в проект файли:
- device.h
- device.cpp

Необхідно завантажити з сайту lighopc (www.ipi.ac.ru/lab43/lopc-ru.html) зібрані бібліотеки lightopc-0.888-0313bin. Всередині архіву будуть файли lightopc.dll, lightopc.lib, їх потрібно додати в проект.
Звідти ж викачуємо бібліотеку для ведення лог-файлів сервера unilog-0.55-1227. Усередині будуть файли unilog.dll і unilog.lib, їх також додаємо в проект.
Далі нам знадобитися бібліотека opcda з SDK (opcfoundation.org), її заголовний суем туди ж і лінкуем з програми.

Як написати opc da сервер

#include "opcda.h"
Програмний модуль послідовного інтерфейсу можна використовувати будь-який з безлічі представлених в інеті або написати свій клас. Я буду використовувати невеликий клас з відповідною назвою serialport і типовими функціями відкриття, закриття порту, записи і читання з порту. Вихідний коди можна подивитися в архіві, який додається до цієї статті.

Це найбільш правильне рішення, хоча ще гнучкіше буде створити прошарок у вигляді модуля, який в залежності від типу інтерфейсу буде використовувати ті чи інші функції для роботи з ним, а зовнішні виклики з основної програми залишаться однаковими. Це дозволить мати один вихідний код і легко перемикатися між типами інтерфейсів, або навіть поєднувати при роботі одного сервера.
Якщо буде використовуватися конвертер інтерфейсів (RS-CAN, RS-Ethernet), то до проекту буде потрібно підключити відповідну бібліотеку з API.


Що точно буде потрібно визначити.
#define _WIN32_DCOM // Дозволяє розширення DCOM
#define INITGUID // Ініціалізує OLE константи
#define ECL_SID "opc.device" // ідентифікатор OPC сервера

Точно будуть потрібні наступні бібліотеки.
#include // стандартне введення-виведення
#include // математичні функції
#include "server.h" // заголовки цього сервера
#include "unilog.h" // бібліотека для лог-файлів
#include "opcda.h" // базові функції OPC: DA
#include "lightopc.h" // заголовлочний файл light OPC

Визначаємо змінні для lightopc.
static const loVendorInfo vendor =; // версія сервера (Major / Minor / Build / Reserv)
static int OPCstatus = OPC_STATUS_RUNNING; // статус OPC сервера
loService * my_service; // екземпляр lightOPC сервера

Пам'ять під змінні - теги сервера можна виділяти динамічно, а можна взяти з запасом статично, TAGS_NUM_MAX - максісмальное кількість тегів.
static CHAR * tn [TAGS_NUM_MAX]; // Назви тегів
static loTagValue tv [TAGS_NUM_MAX]; // Значення тегів
static loTagId ti [TAGS_NUM_MAX]; // Ідентифікатори тегів

#include "tinyxml / tinyxml.h" // XML parser lib

Як написати opc da сервер

Базовий клас myClassFactory: public IClassFactory і його тіпвие функції я зазвичай виділяю в окремий файлик, щоб він не муляв очі. Він незмінний і його код вам на перших порах розбирати не буде потрібно. Просто завантажуємо і вставляємо перед основним кодом.

Всі можливі входи в програму ведуть до нашої функції mymain.

Далі розбираємо вміст функції main, прямо по пунктах.
INT mymain (HINSTANCE hInstance, INT argc, CHAR * argv []);

1) Відразу створюємо лог-файл, щоб записувати туди всі дії сервера. На налагодження у кожного з нас є свої погляди. Я, наприклад, люблю писати всі дії програми і всі проміжні значення змінних в логи, щоб потім апалізіровать поведінку програми крок за кроком і тільки вже якщо виникає якась колізія, тоді роблю покрокову налагодження в інтегрованому дебаггера.

Отже, створюю файл, тут LOG_FNAME - ім'я файлу.
logg = unilog_Create (ECL_SID, LOG_FNAME, NULL, 0, ll_DEBUG); // level [ll_FATAL. ll_DEBUG]
UL_INFO ((LOGID, "Unknown device OPC server start"));>

2) Що повинен робити наш сервер будучи запущеним? Перш за все працювати, а працює сервер в зв'язці з OPC-клієнтом (немає, він звичайно може молотити і вхолосту, але без підключених клієнтів це не завжди буде мати сенс). Хоча я залишу цей момент на ваш розсуд. Зрештою ніхто не забороняє вам написати сервер, який буде працювати завжди і навіть без підключених клієнтів і займатися чимось не менше корисним, ніж надання даних. Наприклад, я інтегрував в сервер функцію запису даних в текстові файли і базу даних, а клієнтам передавав ці дані в міру їх підключення. Це не зовсім коректно з точки зору іделолгіі, але досить практично.
Для того, щоб клієнти могли підключитися до сервера їм необхідно знати чи є потрібний сервер в списку зареєстрованих серверів на локальній або віддаленій машині. Для цього нашому пристрій потребує зерегістріроваться в системі.
За це відповідає функція loServerRegister (GID_ECLOPCserverExe, eProgID, eClsidName, argv0, 0)
GID_ECLOPCserverExe - згенерований раніше UID
eProgID, eClsidName - ідентифікатор, ім'я сервера - const char eProgID [] = ECL_SID; const char eClsidName [] = ECL_SID;
argv0 - командний рядок (власне цей шлях і зареєструє операційна система в реєстрі)
Запуск цієї функції пропоную здійснювати традиційним способом - шляхом передачі ключа в командному рядку.
Наприклад, ключ / r або / register, буде давати зрозуміти серверу, що потрібно зареєструвати сервер в системі.
Ключ / u або / unregister, буде давати зрозуміти серверу, що потрібно видалити сервер з системи.

За видалення сервера з системи відповідає функція loServerUnregister (GID_ECLOPCserverExe, eClsidName).
Не забуваємо видалити за собою створений екземпляр балки
unilog_Delete (logg); logg = NULL;
В інших випадках сервер повинен працювати в штатному режимі.

3) Перед початком роботи инициализируем ключову бібліотеку COM об'єктів функцією CoInitializeEx (NULL, COINIT_MULTITHREADED).

4) Далі знову ж існує два конкретних варіанти розвитку подій. Ми або намагаємося форматувати інтерфейс, спробувати знайти пристрій і в разі успіху рухатися далі, а в разі невдачі закінчити роботу сервера. Або ми в будь-якому випадку реєструємо екземпляр сервера, не чекаючи готовності інтерфейсу і відповіді від пристрою і працюємо вхолосту, чекаючи його підключення. У нашому випадку не сильно принципово, тому для початку запускаю функцію ініціалізації інтерфейсу InitDriver (), опис якої наведу пізніше. Якщо ініціалізація провалюється, пам'ятаю крім видалення екзепляри модуля логів ище і провести деініціалізацію COM CoUninitialize (). Також видаляємо і сам loService - loServiceDestroy (my_service).

5) Створення і заповнення структури драйвера опитування логічно виділено в функцію InitDriver (), разом з ініціалізацією інтерфейсу.
Виробляти парсинг конфігураційного файлу, відкривати і налаштовувати послідовний порт я не буду. Зрештою ми ж не з реальним пристроєм будемо працювати і захаращувати програму поки не буду. Якщо буде потрібно, то опишу цей розділ пізніше.

6) ініціалізувавши теги. Створю два фіктивних пристрою по 3 тега на кожен. Перший тег буде цілочисельним, а решта два з плаваючою комою.
Тут tag_add - лічильник додаються тегів.

8) При підключенні нового клієнта лічильник збільшується на одиницю, що і робить функція класу myClassFactory -> AddRef (), my_CF.Release () - навпаки служить зворотного цілі і знищує одного клієнта, зменшуючи лічильник на одиницю. Поки кількість, підключених клієнтів, більше нуля - функція in_use повертає одиницю.

9) Все. Тепер ми можемо повноцінно працювати. Наприклад, опитувати пристрій в циклі.
while (my_CF.in_use ()) if (WorkEnable) poll_device (); Тобто поки хоча б один клієнт підключений до сервера і власний прапор сервера встановлений ми в циклі виробляємо опитування пристрою. Власний прапор WorkEnable я ввожу, щоб мати можливість в будь-який момент припинити роботу сервера не чекаючи відключення всіх клієнтів.

10) Функція опитування даних poll_device () - основна функція сервера, в якій смакота опитування пристрою і оновлення значень тегів. Якщо ви будете використовувати кілька примірників одного інтерфейсу (та навіть якщо і не плануєте робити це зараз), то буде потрібно виділити кожному окремий потік, інакше формування / обробка запитів / відповідей буде проводитися послідовно в одному, що вкрай негативно позначиться на швидкості роботи всього сервера. Простіше кажучи кожному COM-порту, кожному сокету і кожного клієнта ми виділяємо окремий потік, який буде займатися обміном даними по цьому інтерфейсу.
Ось, приклад організації такої роботи:

Як написати opc da сервер

Як написати opc da сервер

Приклад лог-файлу правильно працюючого сервера. завантажити проект

Схожі статті