[Home: www.ipi.ac.ru/lab43/lopc-ru.html]

Руководство по программированию LightOPC

© 2002, Тимофей Бондаренко
Изменения
Июнь 2002 первая редакция
Февраль 2003 Архитектура, loClientCreate_agg(), loDF_SUBSCRIBE_RAW
3 марта 2003 loCacheTimestamp()

  1. Введение
  2. Общая модель
    1. Архитектура LightOPC
    2. Нитки
    3. Ошибки
  3. Начало
    1. loServiceCreate()
    2. Адресное пространство
      1. Теги: loAddRealTag() и компания
      2. Безымянные теги, AccessPath и *ldAskItemID()
    3. Клиенты
      1. Агрегация
      2. Запуск и loSetState()
      3. Отключение
      4. Тонкости
        1. Регистрация
        2. Ниточные модели
        3. Пустые перечислители
    4. IOPCItemProperties
  4. Работа
    1. Данные
      1. Обновление кеша
        1. Отложенная запись и loTrid
        2. Управление активностью
          1. Количество клиентов
          2. Частота обновлений кеша
          3. Мониторинг активных тегов
        3. Обновление по прерываниям
      2. Обращения к устройству
        1. *ldWriteTags()
        2. *ldReadTags()
      3. Преобразования типов и национализация
    2. Управление доступом
    3. Вспомогательные функции
  5. Заключение

1. Введение

Библиотека LightOPC предназначена для создания OPC серверов. Она предоставляет OPC-DA интерфейс для клиентских приложений. С другой стороны (обожаю такие двусмысленности) она имеет определенный интерфейс для драйверов усторйств, полевых сетей (fieldbus), систем сбора данных. Настоящий документ посвящён "драйверному" интерфейсу LightOPC.

Естественно, спецификация OPC определяет не только DCOM интерфейсы, но и атрибуты данных, модель взаимодействия и прочие принципиальные вещи. Соответственно, определенные данные передаются от драйвера устройства к OPC-клиенту без изменений, поэтому читатель должен иметь под рукой OPC-DA стандарт: мы здесь не будем рассматривать все аспекты индустриальной автоматизации.

Так же останутся без внимания вопросы, освященные в LOPC-FAQ. Весьма рекомендуется заглядывать в lightopc.h -- там может содержаться ценная информация, опущенная здесь в художественных целях (или, во всяком случае, более соответствующая вашей версии библиотеки). Наконец, в файле options.h содержатся сведения о настраиваемых параметрах периода компиляции библиотеки.

Редкоиспользуемые возможности LightOPC описываются с удручающими подробностями. Поэтому не стесняйтесь пропускать разделы, показавшиеся вам излишними.

2. Общая модель

Договоримся, что у нас есть:

OPC-клиент
- совершенно посторонняя программа, желающая работать с OPC-сервером.
LightOPC
- библиотека, обслуживающая OPC-DA интерфейс.
драйвер
- ваша программа, работающая с вашими устройствами и использующая библиотеку LightOPC.
OPC-сервер
- совокупность драйвера и библиотеки LightOPC.

Таким образом, библиотека LightOPC является посредником между вашей программой (драйвером) и OPC-клиентами.

LightOPC оперирует двумя основными объектами: loService и loClient.

loClient суть есть DCOM (OLE) сервер c OPC интерфейсом. Драйвер почти ничего не может с ним делать, поэтому он и назван клиентом -- этот объект является олицетворением отдельного клиента с точки зрения драйвера.

loClient хранит служебные данные, специфичные для отдельного клиента и предоставляет стандартные методы OPC данным технологического процесса. Однако, сами данные технологического процесса дранятся в объекте loService в единственном экземпляре, и разделяются между loClientами.

loService осуществляет передачу данных от драйвера к клиентам. Он содержит в себе двухуровневый кеш и пространство имён (Address Space). Драйвер должен регулярно «освежать» этот кеш.

Кеш двухуровневый. loService создает отдельную нитку для реплицирования данных из вспомогательного кеша (буфера, в который пишет драйвер) в основной -- из которого читают клиенты. Благодаря этому запись данных в кеш не может быть блокирована активными клиентами, то есть не занимает больше времени, чем это физически необходимо и её продолжительность не зависит от загрузки сервера.

Каждый клиент (loClient) имеет отдельную нитку для обработки асинхронных запросов и генерации OnDataChange уведомлений. Эта нитка также обслуживает синхронные запросы некешированного чтения (OPC_DS_DEVICE) и некоторые специальные функции. Такое решение обеспечивает, с одной стороны -- независимость клиентов друг от друга (например, в случае зависания клиента или обрыва связи), а с другой стороны -- гарантирует сохранение последовательности запросов к устройству (то есть все запросы записи / чтения-OPC_DS_DEVICE передаются драйверу в порядке поступления).

К сожалению, принудительная сериализация синхронных запросов чтения OPC_DS_DEVICE приводит к тому что они обслуживаются заметно медленнее, чем синхронное чтение OPC_DS_CACHE -- по сути дела синхронное чтение OPC_DS_DEVICE внутри LightOPC преобразуется в асинхронное с соответствующим ожиданием. Впрочем, внутренняя сериализация выполняется в LightOPC весьма быстро -- определенно быстрее, чем асинхронное чтение в клиентах, а зачастую быстрее, чем синхронное чтение в других OPC серверах.

Вообще же, производительность -- сильная сторона LightOPC. Мало что может с ним сравниться.

2.1 Архитектура LightOPC

Общая архитектура


.......................................
:            OPC Server               :
:                                     :
:  ______                 _________   :             ___________
: /      \               /         \  :            /           \
 /        \             / Light-OPC \   OLE-COM   /             \
<  ДРАЙВЕР >  lo-API   <   LIBRARY   >   OPC-DA  <   OPC-Client  >
 \        /  интерфейс  \           /  интерфейс  \    (SCADA)  /
: \______/               \_________/  :            \___________/
:                                     :
:.....................................:

Пути движения данных


+---------+                               ........................
|         |                               :  +-----------------+ :
| Process |---------\  loCacheUpdate() ----->|                 | :
|         |  DRIVER  >                    :  |    Secondary    | :
|  Data   |-----^---/  loCacheLock() ------->|      Cache      | :
|         |     |                         :  |                 | :
+---------+     |    +--------------+     :  +-----------------+ :
                +----| ldReadTags() |     :    |             |   :
                     | ldWriteTags()|     :    |loUpdatePipe |   :
                     +--------------+     :   \|   thread    |/  :
                           |              :    \             /   :
                           |              :     \           /    :
                     +-----^----+         :  +-----------------+ :
   /============= +--|          | /----------|     Primary     | :
  /               |  | loClient |<           |      Cache      | :
 <  OPC DA     +--|  |          | \----------|                 | :
  \            |  |  +----------+         :  +-----------------+ :
   \========== |  |          |            :                      :
               |  +----------+            :...... loService .....:
               |           |
               +-----------+

Модель асинхронной обработки


... OPC-DA ..... ...loClient::client_scheduler() thread...      .. D ..
                :                                        :      :  R  :
    +---        :                 +-----------+/--/ UpdatePipe /=  I  :
    |   <--------- Subscription --|  Primary  |\--\~~ thread ~~\=  V  :
    |   <--------- OnDataChange --|   Cache   |<===+     :......:  E  :
    |           :                 +-----^-----+    |            :  R  :
    |   <-------------------------------|-------------Async--+  :     :
    |           :                       |          |         |  :     :
    |        +---------------+        CACHE        |         |  :     :
AsyncIO ---->|   Очередь     |          |          |   +-------------+:
             |   запросов    |-- Async--+--DEVICE----->| ldReadTags  |:
             |   q_req       |                     |   | ldWriteTags |:
   DEVICE -->+---------------+-- Sync-DEVICE --------->|             |:
       |        :                                  |   +-------------+:
       |     +-----------------+                   |         |  :     :
SyncIO-+ <---| Очередь ответов |                   |         |  :     :
       |     | q_ret           |<----------------------Sync--+  :     :
       |     +-----------------+                   |            :     :
    CACHE <========================================+            :     :
                :...............................................:.....:

2.2 Нитки

Как можно догадаться из вышесказанного, LightOPC -- вдоль и поперёк многониточен и с этим надо считаться. В частности:

  1. callbackи могут вызываться одновременно разными нитками;
  2. вольшинство функций LightOPC являются threadsafe и могут вызываться когда и откуда угодно;
  3. но есть и исключения, имеющие ограничения по синхронизации, они специально указаны в lightopc.h. В частности, уничтожение loService (loServiceDestroy()).

Отметим также, что некоторые callbackи вызываются не из той клиентской нитки, которая их инициировала. Поэтому на CoQueryClientBlanket() полагаться не стоит.

2.3 Ошибки

Многие функциии LightOPC (если не указано иное) возвращают int код ошибки из тех, что определены в errno.h.

Коды ошибок в основном очевидны, за исключением:
0 - всё хорошо;
EBADF - недействительный loService / loClient;
EEXIST - имя (тега) уже существует;
ENOENT - нет такого имени (тега), элемента;
-1 - просто ошибка.

3. Начало

3.1 loServiceCreate()

В начале надо создать loService. Хотя бы один, но их может быть больше. Он создается функцией:

int loServiceCreate(loService **result, const loDriver *, unsigned tagcount);

В *result, естественно, будет помещен результат. Он будет использоваться при вызове большинства остальных функций.

Сруктура loDriver содержит необходимые для инициализации данные, рассматриваимые в следующей главе.

Важнейший параметр - tagcount. Он определяет количество тегов, поддерживаемых сервером. Оно может быть любым (в разумных приделах). Теги можно добавлять в любое время, в частности и в работающий сервер, и из большинства callbackов, но нельзя удалять теги или увеличить tagcount. Единственный способ сделать это -- создать новый loService.

Разумные пределы для tagcount опредляются объёмом оперативной памяти: каждый тег занимает примерно от 130 до 180 байт, не считая имени тега.

Натурально, loService уничтожается вызовом
int loServiceDestroy(loService *se).

Этот вызов имеет особенность: он не уничтожает loService непосредственно, но инициирует асинхронный процесс отключения присоединенных клиентов. Поэтому с момента вызова loServiceDestroy() вы не должны делать обращений к loService (такие обращения могут быть небезопасны), но само уничтожение объекта может произойти уже после возврата из loServiceDestroy().

Способ поймать момент отключения всех клиентов будет рассмотрен вместе с loClient (см. loClientCreate()).

loDriver

Структура loDriver описывает драйвер и содержит много интересных параметров. Для начала её можно инициализировать нулями.

Если вы делаете что-то хорошее, то стоит установить (в миллисекундах мс/ms):
loMilliSec ldRefreshRate- гранулярность клиентских UpdateRate;
loMilliSec ldRefreshRate_min- минимальный допустимый UpdateRate.

Эти параметры определяют интенсивность поиска изменений в кеше. По умолчанию они весьма малы и часто имеет смысл сделать их сравнимыми с реактивностью ваших технологических процессов, что снизит нагрузку на сервер.

UpdateRate является параметром OPC группы, устанавливаемым клиентом и определяющим частоту обновления кеша. Подробнее он описан в стандарте OPC-DA.

ldRefreshRate_min позволяет защитить клиента от аномально частых обновлений.

Гранулярность ldRefreshRate, отличная от 1, может привести к отличию действущих, «пересмотренных» UpdateRate от запрошенных клиентом.

Другие параметры:

int ldQueueMax
максимальное количество отложенных асинхронных запросов чтения/записи (IOPCAsyncIO[2]) на клиента. Клиент может инициировать больше одновременных асинхронных запросов, чем сервер в состоянии обработать, поэтому может оказаться целесообразнее отбрасывать избыточные запросы по поступлении, чем ставить их в очередь. На синхронные запросы это ограничение не распространяется (хотя они, при определенных обстоятельствах, могут оказаться в той же очереди).

char ldBranchSep
односимвольный разделитель компонетов имени тега. Если 0 -- то пространство имен будет плоским (OPC_NS_FLAT); противном случае -- иерархическим. Обычно используют '.'; мне привычнее -- '/'.

void *ldDriverArg
очень полезный параметр. Он никак не используется, но исправно передается в каждый callback от LightOPC. В частности, его копия передается в loCaller::ca_se_arg; он также возвращается функцией
void *loDriverArg(loService *);
Вы хотели построить свой C++ класс вокруг loService?

unsigned ldFlags
разные двоичные флаги. подробно они будут описываться по мере употребления.
В общем случае имя флага определяет область его применения:
loDF_ - loDriver/loService (см. loServiceCreate());
loDf_ - loDriver/loService или loClient (см. loClientCreate());
loTF_ - отдельный тег (loAddRealTag()).
Большинство флагов могут быть указаны в разных местах. Флаги, указанные в loDriver::ldFlags, действуют на все теги и всех loClient.

Остальные параметры суть указатели на callbackи драйвера (функции драйвера, которые могут быть вызваны библиотекой LightOPC). Естественно, все они необязательны.

Наименее интересны следующиее вызовы (другие будут рассмотрены по мере описания решаемых ими задач):

HRESULT (*ldBrowseAccessPath)(const loCaller *, const loWchar *tagname, LPENUMSTRING *es)
- прямой вызов из IOPCBrowseServerAddressSpace::BrowseAccessPath(). LightOPC прозрачно передает всё связанное с AccessPath от клиента драйверу и обратно. В этом вызове драйвер получает пустой перечислитель строк, который может быть заполнен через
HRESULT loEnumStrInsert(LPENUMSTRING, const loWchar *)
Драйвер также может вернуть в *es собственный перечеслитель, однако LightOPC не сможет включить его в список работающих объектов и, соответственно, вызвать для него CoDisconnectObject(). Драйвер может сделать это сам.

Естественно, loEnumStrInsert() может быть применён только к специальному перечислителю, полученному от LightOPC.

void (*ldCurrentTime)(const loCaller *, FILETIME *)
- определение календарного времени драйвера/сервера, если оно не совпадает с системным.

unsigned (*ldGetErrorString)(const loCaller *, HRESULT ecode, LCID locale, loWchar *buf, unsigned wcsize)
- трансляция кода ошибки в текстовое сообщение. Встроенная реализация обеспечиватет трансляцию всех OPC ошибок и некоторых OLE ошибок в англоязычные сообщения, а также трансляцию всех прочих системных кодов ошибок в сообщения на английском или системном (в зависимости от версии и локализации Windows) языке. Драйвер может определить эту функцию для трансляции нестандартных ошибок или поддержки иных языков.

wcsize
содержит длину буфера buf для возвращаемой строки (в символах -- не в байтах), включая завершающий '\0'.

Функция должна возвращать 0, если она не может транслировать ecode. Во всех прочих случаях (в том числе, при нулевом buf или wcsize) должна быть возвращена полная длина сообщения в символах, не считая завершающего '\0'. Если буфер слишком короток, то сообщение должно быть возвращено в усеченном виде, а возвращенная длина должна соответствовать неусеченному сообщению.

HRESULT (*ldQueryAvailableLocaleIDs)(const loCaller *, DWORD* pdwCount, LCID** pdwLcid)
- прямой вызов из IOPCCommon::QueryAvailableLocaleIDs(). Драйвер должен распределять возвращаемый pdwLcid через CoTaskMemAlloc(). По умолчанию LightOPC возвращает 0 (NEUTRAL/NEUTRAL) и 9 (ENGLISH/NEUTRAL).

int (*ldCheckLocale)(const loCaller *, LCID dwLcid)
- проверяет приемлемость запрошенного клиентом LCID. Драйвер должен вернуть 0 - если принимает и -1 - если не принимает LCID. По умолчанию LightOPC принимает любые LCID, что плохо согласуется со стандартом, зато хорошо -- с реальными клиентами.

loCaller

Все callbackи имеют аргумент loCaller, описывающий контекст вызова вызова:
loService *ca_se - самоочевидно;
void *ca_se_arg - соответствующий ldDriverArg;
loClient *ca_cli - клиентский объект;
void *ca_cli_arg - соответствующий release_handle_arg.
Поля ca_cli и ca_cli_arg могут содержать нули, если конкретный вызов не может быть ассоциирован с определенным клиентом.

3.2 Адресное пространство

Клиенты и серверы OPC обмениваются данными посредством разделяемых переменных, которые мы будем называть тегами.

Совокупность тегов, предоставляемых сервером, называется адресным пространством (Address Space, пространством имён) этого сервера.

В адресном пространстве LightOPC серверов теги создаются функциями семейства loAddRealTag().

3.2.1 Теги: loAddRealTag() и компания

Основной функцией создания тегов является

int loAddRealTag(loService *, /* service context */
             loTagId    *ti,      /* returned TagId */
             loRealTag   rt,
             const char *tName,
             int         tFlag,   /* loTF_XXX */
             unsigned    tRight,  /*OPC_READABLE|OPC_WRITEABLE*/
             VARIANT    *tValue,  /* Canonical VARTYPE & value */
             int         tEUtype, /* OPCEUTYPE */
             VARIANT    *tEUinfo  /*optional, depends on tdEUtype*/
                 );
Аргументы функции таковы:

loTagId *ti
возвращаемый целочисленный идентификатор тега в рамках заданного loService. Он должен будет использоваться драйвером при последующих ссылках на созданный тег, например в loTagValue::tvTi. Значение 0 соответствует «несуществующему» тегу.

loRealTag rt
«обратная» ссылка на тег. Параметр будет ассоциирован с тегом и будет передаваться во все запросы к драйверу (например, loTagPair::tpRt). loRealTag имеет тип указателя (на loRealTag_) и драйвер может передать в rt указатель на свои данные, которые он сам ассоциирует с этим тегом. Это позволит драйверу упростить обработку запросов. Желательно чтобы значение 0 соответствовало «несуществующему» тегу, поскольку в некоторых случаях LightOPC включает в запросы пустые ячейки тегов, не подлежащие обработке, такие ячейки помечаются нулевыми loTagId & loRealTag.

const char *tName
имя создаваемого тега. Имя должно быть уникально в пределах loService. По умолчанию имена чувствительны к регистру символов. Можно сделать их нечувствительными, указав флаг loDF_IGNCASE в loDriver::ldFlags / loServiceCreate() при инициализации сервера. Регистр букв (и их трансляция в UNICODE) определяются текущей категорией локализации (см. setlocale()); нежелательно изменять её после инициализации сервера. В иерархическом адресном пространстве (loDriver::ldBranchSep) имя должно быть полным (например, "device22/unit33/tag11", в предположении, что разделитель ldBranchSep='/'; обратите внимание, что ведущий (перед "device22") разделитель отсутствует). tName может быть пустой строкой "" или нулевым указателем (0, NULL) при этом будет создан безымянный тег, рассматриваемый в отдельной главе.

int tFlag
комбинация флагов loTF_???? или 0. Большинство из них имеют синонимы loDF_???? и могут быть установлены для всех тегов чарез loDriver::ldFlags / loServiceCreate().

loTF_NOCOMP, loDF_NOCOMP
не производить сравнения значений тега при определении необходимости уведомления клиента об изменении тега; считать тег изменившимся, если драйвер обновлял кеш для этого тега с момента последнего уведомления. Это облегчает жизнь серверу, если драйвер имеет эффективный способ обнаружения изменения значений тегов, например управляется прерываниями.
loTF_NOCONV, loDF_NOCONV
запретить преобразование типа тега от канонического к запрошенному клиентом.
loTF_EMPTY
сделать тег невидимым для OPCGroup / IOPCItemMgt::AddItem() / ValidateItem(). Это аналогично указанию пустого tdValue, за исключением обработки запросов к IOPCItemProperties и фильтрации типов в IOPCBrowseServerAddressSpace.
loTF_NOBROWSE
сделать тег невидимым для IOPCBrowseServerAddressSpace. Бывает, что имеется, скажем, тысяча тегов: reg000 ... reg999. Имеет сммысл показать клиенту только намёк (hint) "reg[000...999]", а саму тысячу однообразных имён скрыть. Альтернативный способ -- использование безымянных тегов.
loTF_CONVERT
вызывать драйвер *ldConvertTags() при чтении тега из кеша с конверсией к типам VT_BSTR, VT_DATE, VT_ARRAY|*. Полезно при локализованных преобразованиях типа или при преобразованиях типа массивов (системная функция VariantChangeType() не умеет преобразовывать типы массивов).
loTF_CHECKITEM, loDF_CHECKITEM
вызывать драйвер *ldAskItemID() при любом обращении клиента к именованному тегу. Позволяет запрещать отдельным клиентам читать отдельные теги.
loTF_CANONAME, loDF_CANONAME
используя *ldAskItemID(), драйвер может по какой-либо причине отобразить тег, запрошенный клиентом, на тег, имеющий каноническое имя (указанное при создании тега loAddRealTag()), отличное от запрошенного клиентом. При использовании указанного флага клиенту всегда будет возвращаться каноническое имя тега в IEnumOPCItemAttributes / OPCITEMATTRIBUTES::szItemID. По умолчанию возвращается имя, запрошенное клиентом. Использование флага позволяет сэкономить немного памяти.

unsigned tRight
комбинация OPC_READABLE и OPC_WRITEABLE, или чего-нибудь ещё. Стандарт именует этот атрибут «правами доступа», оговариваясь, что это не права, а, скорее «природа» тега. LightOPC использует этот атрибут для ограничения доступа к тегу, если определён параметр компиляции LO_CHECK_RIGHTS (см. файл options.h).

VARIANT *tValue
содержит канонический тип тега и значение, использемое для проверки допустимости преобразований типа тега. Если указан VT_EMPTY или NULL указатель, то создаётся «псевдотег», видимый в IOPCBrowseAddressSpace, но невидимый в OPCGroup / IOPCItemMgt::AddItem() / ValidateItem(). Он может быть использован как намёк (hint) "reg[000...999]".

int tEUtype; VARIANT *tEUinfo
определяет информацию, возвращаемую в IEnumOPCItemAttributes / OPCITEMATTRIBUTES::dwEUtype,vEUInfo. Если tEUtype = 0, то и tEUinfo должен быть 0. В противном случае tEUtype должен быть значением из OPCEUTYPE, а tEUinfo -- соответствующими данными. В случае OPC_ANALOG указанные границы будут использоваться при вычислении зоны нечувствительности (deadband) и проверке преобразования типов.

Компанию loAddRealTag() составляют функции:

int loAddRealTag_a(
loService *, loTagId *ti, loRealTag rt, const char *tName, int tFlag, unsigned tRight, VARIANT *tValue,
     double range_min, double range_max );

- упрощенный вариант для OPC_ANALOG.

int loAddRealTag_b(
loService *, loTagId *ti, loRealTag rt, const char *tName, int tFlag, unsigned tRight,
     loTagId tBase );

- использует tValue, tEUtype и tEUinfo от ранее созданного tBase, что позволяет сэкономить память при создании множества однотипных тегов. (Гмм... около 30% -- 50 байт на тег; 5M на 100k тегов).

Функции loAddRealTagW(), loAddRealTag_aW(), loAddRealTag_bW() аналогичны описанным, но принимают «широкие» имена тегов.

Для ускорения создания и поиска тегов (при большом количестве тегов, скажем более 100000) можно принять следующие меры (независимые друг от друга):

  1. испльзовать безымянные теги и «намёки»;
  2. первыми создавать именованные теги, а затем -- безымянные;
  3. использовать loDF_CHECKITEM и реализовать очень быстрый поиск в *ldAskItemID().

3.2.2 Безымянные теги, AccessPath и *ldAskItemID()

Создаваемые теги, могут не иметь имён. Естественно, такие теги не будут видны клиентам через IOPCBrowseServerAddressSpace, однако они доступны для всех прочих операций.

Для использования безымянных тегов драйвер должен определить callback loDriver::ldAskItemID:
HRESULT (*ldAskItemID)(const loCaller *,
                      loTagId *ti, void **acpa, /* to be returned */
                      const loWchar *itemid,
                      const loWchar *accpath,
                      int vartype,
                      int goal
                       );
Функция вызывается, если клиент запрашивает тег по имени и выполняется одно из условий:
  1. указанное клиентом имя не найдено среди именованных тегов;
  2. клиентом указан непустой AccessPath (см. OPC-DA OPCGroup / IOPCItemMgt::AddItem()) и loDF_IGNACCPATH не был указан драйвером при инициализации loDriver::ldFlags. Устанавливая loDF_IGNACCPATH, драйвер может избежать лишних вызовов *ldAskItemID() при игнорировании AccessPath;
  3. драйвер использовал loDF_CHECKITEM или loTF_CHECKITEM в loDriver::ldFlags или loAddRealTag() соответственно.
Драйвер должен транслировать запрошенный тег в существующий loTagId (или создать новый) и вернуть код ошибки (обычно, OPC_E_INVALIDITEMID, OPC_E_UNKNOWNITEMID, OPC_E_UNKNOWNPATH, E_FAIL или S_OK). Драйвер может таже отказать указанному клиенту в доступе к запрошенному тегу.
const loCaller *
описатель клиента;
loTagId *ti
возвращаемый идентификатор тега. Игнорируется при неуспехе. При успехе может быть 0 -- в этом случае LightOPC проведет повторный поиск тега, но, очевидно сможет найти только именованный тег. Драйвер может создавать новые теги, или динамически отображать одно имя на разные теги. Возвращенный тег может иметь имя и оно может не совпадать с запрошенным, см. loTF_CANONAME, loDF_CANONAME в loAddRealTag();
void **acpa
возвращаемый ключ пути доступа. Если драйвер поддерживает пути доступа (AccessPath), то он может создать компактный идентификатор пути accpath (указатель) и возвратить его в *acpa. LightOPC включает этот идентификатор в обращения к драйверу (loTagPair::tpAP), таким образом драйвер может учитывать AccessPath при выполнении операций OPC_DS_DEVICE, однако запросы к кешу (OPC_DS_CACHE) обрабатываются без участия драйвера и AccessPath для них будет игнорироваться. Драйвер также может отображать разные пути на разные реальные теги. Желательно, чтобы драйвер трактовал нулевое значение идентификатора пути loTagPair::tpAP / *acpa особым образом, поскольку оно назначается по умолчанию, если клиент не указал AccessPath или тег был найден без использования *ldAskItemID().
const loWchar *itemid
имя тега, запрошенного клиентом, его-то драйвер и должен транслировать в *ti;
const loWchar *accpath
путь доступа к тегу, запрошенный клиентом, драйвер может преобразовать его в *acpa;
int vartype
тип тега, запрошенный клиентом; может отличаться от VT_EMPTY, только если клиент запросил OPCGroup / IOPCItemMgt::AddItem() / ValidateItem();
int goal
цель вызова *ldAskItemID(), клиентский запрос, ставший причиной вызова:
loDAIG_LOINT внутренние нужды LightOPC;
loDAIG_ADDITEM OPCGroup / IOPCItemMgt::AddItems();
loDAIG_VALIDATE OPCGroup / IOPCItemMgt::ValidateItem();
loDAIG_BROWSE какой-либо метод IOPCBrowseAddressSpace::, например GetItemID();
loDAIG_IPROPERTY какой-либо метод IOPCItemProperties::;
loDAIG_IPROPQUERY * IOPCItemProperties::QueryAvailableProperties();
loDAIG_IPROPGET * IOPCItemProperties::GetItemProperties();
loDAIG_IPROPLOOKUP * IOPCItemProperties::LookupItemID();
loDAIG_IPROPRQUERY * IOPCItemProperties::QueryAvailableProperties() для тега, ссылающегося на данный тег как на свойство (property);
loDAIG_IPROPRGET * IOPCItemProperties::GetItemProperties() для тега, ссылающегося на данный тег как на свойство (property);
* loDAIG_MASK маска, используемая для подавления несущественных кодов (помеченных *): (loDAIG_MASK & loDAIG_IPROP???) ==> loDAIG_IPROPERTY.
* * новые коды могут быть определены в дальнейшем.
Параметр goal может быть использован для управления доступа к тегам и для определения необходимости создания новых тегов.

3.3 Клиенты

Соединение с клиентом обслуживается объектом loClient. Этот объект предоставляет клиенту интерфейсный указатель IOPCServer и все прочие, связанные с ним интерфейсы.

loClient создается в контексте loService. Каждый loClient обслуживает ровно одно соединение.

loClient создается функцией:

   int loClientCreate(loService *se, loClient **cli,
             int ldFlags, /* per-client loDf_XXX flags */
             const loVendorInfo *vi,
             void (*release_handle)(void *, loService *, loClient *),
             void *release_handle_arg
                   );
loService *se
корневой объект сервера, может обслуживать произвольное количество клиентов;
loClient **cli
результат, созданный loClient. Является OLE/COM интерфейсным указателем. Может быть приведён к типу IUnknown*. RefCount = 1; объект может быть уничтожен вызовом IUnknown::Release().
int ldFlags
комбинация значений loDf_*, специфичных для отдельного клиента. Они совокупляются операцией '|' с флагами loDF_*, указанными в loDriver::ldFlags, влияющими на всех клиентов заданного loService.
const loVendorInfo *vi
сведения о производителе, соответствующие поля перносятся в OPCSERVERSTATUS:
typedef struct loVendorInfo
       {
        WORD lviMajor, lviMinor, lviBuild, lviReserv;
        char *lviInfo;
       } loVendorInfo;
void (*release_handle)(void *, loService *, loClient *)
callback функция, вызываемая при уничтожении loClient. Позволяет определить момент отсоединения клиента.
void *release_handle_arg
произвольный аргумент, который будет передан в *release_handle(). Также передается во многие другие callbackи (например, в loCaller::ca_cli_arg.
Все аргументы, кроме se и cli, необязательны и могут быть нулями.

3.3.1 Агрегация

loClient поддерживает интерфейсы: IUnknown, IOPCCommon, IOPCServer, IOPCBrowseServerAddressSpace, IOPCItemProperties, IConnectionPointContainer, и, в некоторых случаях, IMarshal (FreeThreadedMarshaler). Интерфейсы IPersistFile, IOPCPublicGroup, IOPCSecurity не поддерживаются.

Вы можете добавить поддержку своих реализаций IPersistFile, IOPCSecurity и других (правда, с IOPCPublicGroup будут трудности).

loClient, поддерживающий обычную агрегацию OLE/COM создается вызовом:

   int loClientCreate_agg(loService *se, loClient **cli,
             IUnknown *outer, IUnknown **inner,
             int ldFlags, /* per-client loDf_XXX flags */
             const loVendorInfo *vi,
             void (*release_handle)(void *, loService *, loClient *),
             void *release_handle_arg
                   );
IUnknown *outer
указывает «внешний», агрегирующий объект, переданный в QI::CreateInstance.
IUnknown **inner
результат, созданный интерфейсный указатель для передачи агрегирующему объекту.
Прочие аргументы аналогичны loClientCreate().

ЗАМЕЧАНИЕ: Используйте *cli для идентификации созданного loClient, но (*inner)->Release() - для его уничтожения, поскольку все вызовы (*cli)-> будут делегированы агрегирующему объекту.

Помимо обычной агрегации OLE/COM есть другой путь, с меньшими накладными расходами:

   int loClientChain(loClient *cli,
           HRESULT (*qi_chain)(void *rha, loService *, loClient *,
                               const IID *, LPVOID *),
           void (*release_handle)(void *rha, loService *, loClient *),
           void *release_handle_arg
                  );
qi_chain
будет вызываться из loClient::QueryInterface() перед поиском встроенной реализации запрошенного интерфейса. Таким образом, вы получаете возможность не только добавлять новые, но и подменять встроенные интерфейсы loClient.
rha
передается release_handle_arg;
release_handle, release_handle_arg
заменяют, указанные при создании loClient

Вы, вероятно, захотите использовать release_handle_arg для сохранения указателя на свой объект, ассоциированный (или агрегированный) с loClient.

Ваша реализация дополнительных интерфейсов должна делегировать все методы IUnknown объекту loClient. Реализация *qi_chain() не должна вызывать loClient::QueryInterface(); она должна возвращать ошибку при запросе интерфейса IUnknown и любых интерфейсов, реализацию которых вы оставляете за loClient. Завершение жизни агрегированного объекта можно определить по release_handle().

Функция loClientChain() не является ниточно-безопасной (threadsafe) и не должна вызываться после того, как loClient передан клиенту.

3.3.2 Запуск и loSetState()

Для того, чтобы устанавливать соединения с OLE клиентами, необходимо сделать «фабрику классов» ClassFactory. Встроенной реализации LightOPC не предоставляет, однако прилагаемый пример sample.cpp содержит полноценную реализацию IClassFactory, включающую необходимые процедуры регистрации и выгрузки / завершения для in-proc и out-of-proc серверов.

IClassFactory::CreateInstance является также хорошим местом для вызова CoQueryClientBlanket() с целью идентификации клиента и последующего управления доступом.

Перед тем, как отдать свежесозданный loClient клиенту, необходимо сделать ещё одну вещь:

loSetState(my_service, (loClient*)server, loOP_OPERATE, (int)OPC_STATUS_RUNNING, "Goodbye, client");
Смысл таков:

int loSetState(loService *se, loClient *cli,
               int oper,
               int state,
               const char *reason
               );
loClient *cli
клиент, состояние которого необходимо изменить. 0 вызывает изменение состояния всех клиентов, ассоциируемых с se.
int oper
0 - не изменять состояние, или комбинация значений:
loOP_OPERATE нормальная работа, может использоваться для возобновления работы после loOP_STOP;
loOP_STOP приостановить обслуживание клиента. Большинство запросов клиента будут возвращать ошибку E_FAIL; но GetErrorString(), GetServerStatus() и некоторые другие функции будут работать.
loOP_SHUTDOWN послать клиенту уведомление о завершении через IOPCShutdown. Клиенты, не использующие IOPCShutdown, этого уведомления, естественно, не получат;
loOP_DISCONNECT вызвать CoDisconnectObject() для loClient и всех объектов, созданных для клиента. В некоторых случаях LightOPC может передавать клиенту объекты, созданные непосредственно драйвером; для таких объектов CoDisconnectObject() должен вызывать сам драйвер.

int state
0 - не изменять, или одно из значений OPCSERVERSTATE. Это значение будет передаваться клиенту в OPCSERVERSTATUS::dwServerState.

const char *reason
0 - не изменять, или текстовое сообщение о причине для IOPCShutdown::ShutdownRequest().

loSetStateW()
принимает «широкий» reason.

3.3.3 Отключение

Обычно, драйверу желательно контролировать время жизни подключенных клиентов, чтобы уметь своевременно завершиться.

Драйвер естественным образом контролирует подключение клиентов, создавая loClient в IClassFactory::CreateInstance. Однако, передав интерфейсный указатель клиенту, драйвер более не может непосредственно контролировать существование созданного loClient.

Определить момент уничтожения loClient драйвер может указав release_handle(). Таким образом, драйвер может своевременно уничтожать свои приватные данные, ассоциируемые с конкретным клиентом, а также поддерживать счётчик активных клиентов.

Отметим, что в момент вызова release_handle() соответствующий loClient уже не может функционировать корректно.

В общем случае справедливы следующие правила:

  1. После того как loClient передан клиенту от может быть уничтожен клиентом в любой момент.
  2. С момента инициации уничтожения loClient все вызовы LightOPC, ссылающиеся на этот объект будут завершаться с ошибкой (но без краха).
  3. release_handle() вызывается некоторое время спустя после инициации уничтожения клиента.
  4. Через некоторое время после возврата из release_handle() соответствущий loClient уничтожается окончательно.
  5. Функции LightOPC, имеющие аргументами loService и loClient аккуратно проверяют корректность loClient и надёжно обнаруживают несуществующие loClient, таким образом, эти функции могут использоваться независимо от того завершился соответствующий release_handle() или нет.
  6. Функции LightOPC, имеющие аргументом loClient без loService не всегда могут обнаружить ошибку использования уничтоженного loClient. Поэтому их вызов после завершения соответствующего release_handle() может привести к непредсказуемым последствиям.
  7. Гарантируется, что внутри вызываемых LightOPC callbackов можно использовать соответствующий loClient, однако не гарантируется, что его уничтожение ещё не было инициировано.

Для принудительного отключения всех или избранных клиентов драйвер может использовать loSetState(). Типичная последовательность такова:

loOP_SHUTDOWN
-- ждём разумное время и считаем release_handle();
loOP_STOP
-- если отцепились не все, то делаем намёк для совсем непонятливых и ждём ещё немного;
loOP_DISCONNECT
-- должен вызвать безусловное и непосредственное отключение всех клиентов.
loServiceDestroy() тоже делает всё это, но без ожиданий. В любом случае стоит дождаться получения всех release_handle(), на все loClient, ассоциированные с определённым loService, поскольку уничтожение loService происходит асинхронно и не может быть завершено прежде чем будут вызваны все необходимые release_handle().

3.3.4 Тонкости

Использование различных клиентов порождает разного рода проблемы совместимости.

Для решения таких проблем имеются специальные возможности:

loDf_NOFORCE
игнорировать флаг FORCE в IOPCServer::RemoveGroup().
loDf_DWG
destroy-with-groups -- при уничтожении loClient принудительно уничтожать все ассоциированные с ним клиентские группы, которые не были отпущены клиентом. Обычно клиент должен сам убить их, прежде чем отпускать сервер, но некоторые клиенты этого не делают. Стандарт оставляет вопрос уничтожения этих групп на усмотрение сервера -- вот и усматривайте...

Есть, конечно, и другие тонкости...

3.3.4.1 Регистрация

Предоставляются функции регистрации локального OLE/COM сервера:


int loServerRegister(const GUID *CLSID_Svr,
                     const char *ProgID,
                     const char *ServName,
                     const char *exPath,
                     const char *Model
                        /* 0=exe, ""=STA dll, "Both", "Free" ...*/
                     );

int loServerUnregister(const GUID *CLSID_Svr, const char *ProgID);
const GUID *CLSID_Svr
его вам надо сгенерировать с помощью guidgen.exe или uuidgen.exe;
const char *ProgID
придумайте имя своему серверу;
const char *ServName
и комментарий к нему;
const char *exPath
имя исполняемого файла, возможно с ключами командной строки;
const char *Model
ниточная модель сервера: 0=exe, ""=STA dll, "Both", "Free" ...

В дополнение к обычным OLE/COM ключам loServerRegister() регистрирует ещё и ComponentCategories OPC DA 1.0 & 2.0.

Для выполнения нелокальной регистрации вам придётся повозится с ключами

HKCR\AppID\{CLSID_Svr}\RemoteServerName=
HKCR\AppID\{CLSID_Svr}\RunAs="Interactive User"
HKCR\AppID\{CLSID_Svr}\DllSurrogate
3.3.4.2 Ниточные модели

По умолчанию LightOPC работает в свободно-ниточной модели (freethreaded), но можно перевести библиотеку в режим поддержки «совместной» (both) модели.

Ниточная модель устанавливается флагами периода исполнения (run-time) loDriver::ldFlags -- глобально, или loClientCreate() / ldFlags -- на клиента:

loDf_FREEMODEL
-- действует по умолчанию, свободная модель.
loDf_FREEMARSH
-- разрешает использовать CoCreateFreeThreadedMarshaler() для объектов OPCServer / OPCGroup.
loDf_BOTHMODEL
-- включает:
  1. межниточный маршалинг (CoMarshalInterThreadInterfaceInStream()) для клиентских callback интерфейсов;
  2. «local message loop» для некоторых внутренних функций ниточной синхронизации, что необходимо для работы межниточного маршалинга.

Обычно, loDf_FREEMARSH | loDf_BOTHMODEL используют вместе.

Обычно, ниточная модель существенна только для in-proc серверов, поскольку «совместимые» модели позволяют избажать маршалинга, а в out-of-proc серверах маршалинг неизбежен.

Однако бывает, что ваш out-of-proc сервер является лишь частью большой программы, которая уже использует модель, отличную от "free". В этом случае вызов CoInitializeEx(NULL, COINIT_MULTITHREADED) вернёт ошибку. Придётся либо использовать loDf_BOTHMODEL (подразумевая наличие message loop где-либо в программе), либо запускать сервер новой ниткой.

Обычно, однониточные клиенты быстрее работают с серверами "both" модели, а многониточные -- с "free".

Использование флагов loDf_FREEMARSH и loDf_BOTHMODEL должно быть разрешено при компиляции параметрами LO_USE_FREE_MARSHAL и LO_USE_BOTHMODEL.

Сервер должен использовать соответствующую инициализацию CoInitializeEx() -- для out-of-proc, или "ThreadingModel" -- для in-proc.

Использование свободнониточного маршалера в перечислителях определяется параметром компиляции LO_USE_FREEMARSH_ENUM. По умолчанию, все перечислители агрегируют «свободный маршалер», не являясь при этом «threadsafe»! Тем не менее это работает, поскольку:

  1. параллельное использование перечислителя разными нитками -- занятие бессмысленное в любом случае;
  2. для перечислителей объектов (таких, как IEnumUnknown) их «свободный маршалер» отключается, если хотя-бы один из перечисляемых объектов не имеет такового.
3.3.4.3 Пустые перечислители

Суть проблемы в том, что ранние версии стандарта недостаточно внятно определяли порядок обработки клиентом пустых перечислителей. Позднее ошибку исправили, но встречаются ещё клиенты, написанные до её исправления.

Итак, если должен быть возвращён пустой перечислитель, то сервер может вернуть:

итого, 4 комбинации...

Если код завершения S_FALSE, то некоторые клиенты не отпускают пустой перечислитель, что приводит к утечкам памяти. Другие клиенты пытаются отпустить NULL, что приводит к краху.

LightOPC позволяет управлять возвращением пустых перечислителей как на этапе компиляции (параметр ENUM_EMPTY в options.h), так и на этапе исполнения (причём, независимо для разных клиентов):

loDf_EE_SFALSE
(действует по умолчанию) вернуть S_FALSE и пустой перечислитель. Это соответствует текущей редакции стандарта, но может приводить к утечкам памяти на старых клиентах (Descartes OPC).
loDf_EE_NULL
вернуть S_FALSE и NULL. Утечек памяти нет, но возможен крах некоторых старых клиентов.
loDf_EE_SOK
вернуть S_OK и пустой перечислитель. Надёжно работает на любых клиентах, но не соответствует стандарту.

3.4 IOPCItemProperties

Свойства (или атрибуты) тегов ItemProperties поддерживаются.

Для экономии памяти и облегчения настройки множества тегов со сходными свойствами использована трёхуровневая схема:

Свойство, DWORD propid
Собственно, отдельное свойство тега, имеющее целочисленный идентификатор propid, описание, значение и / или имя ассоциируемого со значением тега.
Список свойств, loPLid
Список отдельных свойств, идентифицируемых propid; длина списка не ограничена, но список не может содержать более одного свойства с заданным идентификатором propid.
Тег, loTagId
Тег может быть ассоциирован с конечным (loPROPLIST_MAX) количеством списоков тегов loPLid. При этом каждый список может быть ассоциирован с произвольным количествам тегов.

Списки свойств, ассоцированные с тегом, упорядочены и идентифицируются номерами от 1 до loPROPLIST_MAX включительно. По умолчанию loPROPLIST_MAX = 3, это значение может быть изменено при перекомпиляции библиотеки.

Номер списка является его приоритетом: при поиске заданного свойства propid для некоторого тега сначала будет просмотрен список с номером 1. Последним просматривается подразумеваемый список из шести специальных свойств.

Список специальных свойств подразумевается у каждого тега, имеющего значение (loAddRealTag(tdValue)), даже если он не имеет имени, помечен как невидимый или является подсказкой.
ОСТОРОЖНО! В качестве свойства "текущее значение" возвращается кешированное значение тега, что не соответствует стандарту. OPC-DA требует выполнения некешированного чтения, что, по всей видимости, объясняется недостоверностью содержимого кеша для неактивных тегов. В LightOPC кеш вполне контролируется драйвером для всех тегов, независимо от их активности. Если, тем не менее, некешированное чтение желательно, то драйвер может выполнить его в рассматриваемом ниже callbackе *ldGetItemProperties(). Следует, однако, иметь в виду, что эта функция не подвергается принудительной сериализации, в отличие от обычных запросов некешированного чтения.

Создание нового пустого списка тегов:

loPLid loPropListCreate(loService *);

Возвращаемое значанеие loPLid
0 - при нехватке памяти или недействительном loService. Нулевой loPLid используется также, как предопределённый пустой список.

Будучи создан, спиок может быть уничтожен только вместе с содержащим его loService. Однако, состав списка может изменяться в любое время.

Добавление свойства в список тегов:

int loPropertyAdd(loService *se,
                  loPLid plid,
                  unsigned propid,
                  VARIANT *val,
                  const char *path,
                  const char *description
                  );
int loPropertyAddW(loService *se,
                   loPLid plid,
                   unsigned propid,
                   VARIANT *val,
                   const loWchar *path,
                   const loWchar *description
                   );
loService *se
контекст;
loPLid plid
идентификатор ранее созданного списка, в который добавляется свойство;
unsigned propid
идентификатор добавляемого свойства, должен быт уникален в указанном plid;
VARIANT *val
значение свойства. Может быть NULL или VT_EMPTY. В этом случае, если не определён path то создаётся «подавитель» свойств: его значение не передаётся клиенту, но будучи найден в списке свойств, он подавляет определения этого свойства в низкоприоритетных списках, включая список шести специальных свойств.

const char *path
необязательный (может быть NULL или "") путь к тегу -- значению свойства. Используется для IOPCItemProperties::LookupItemIDs(). Если path определён, а val -- нет, то при чтении свойства клиенту будет возвращаться значение соответствующего тега из кеша. В иерархическом адресном пространстве path может быть как абсолютным, так и относительным. Путь, начинающийся с loDriver::ldBranchSep считается относительным. При этом путь, начинающийся с одного ldBranchSep просто подклеивается к имени тега, для которого запрашивалось свойство; с двух -- к его родителю и т.д. Если path содержит больше ldBranchSep, чем имя тега, или не начинается с ldBranchSep, то путь считается абсолютным. При создании свойства корректность пути не проверяется, то есть, тег указываемый путём не обязан существовать.
Замечание. Имя тега, указываемое в loAddRealTag() может содержать завершающие ldBranchSep. Они будут учитываться при применении относительных путей и игнорироваться во всех остальных случаях.

const char *description
необязательное (может быть NULL) текстовое описание свойства. LightOPC знает описания шести специальных свойств, а если определён параметр компиляции LO_NONBASIC_PROPDESCR то и всех прочих свойств, перечисленных в OPC DA 2.05.
ВНИМАНИЕ! В отличие от всех прочих строковых параметров, LightOPC не копирует строку description, а сохраняет указатель на неё «как есть». Следовательно, драйвер должен гарантировать корректность description в течение всего времени жизни loService.

Удаление свойства из списка:
int loPropertyRemove(loService *se, loPLid plid, unsigned propid);

Изменение значения свойства:
int loPropertyChange(loService *se, loPLid plid, unsigned propid, VARIANT *val);

Связывание списка свойств с тегом:
int loPropListAssign(loService *se, loPLid plid, loTagId ti, int prio);

Каждый тег может иметь до loPROPLIST_MAX списков свойств. Каждый список свойств может быть назначен произвольному количеству тегов. Списки, назначенные тегу, имеют (внутри тега) номера prio 1...loPROPLIST_MAX, соответствующие их приоритетам (1 - высший). Использование plid = 0 очищает соответствующий список для тега.

Драйвер имеет альтернативный способ работать с IOPCItemProperties, позволяющий, в частности, возврашать некешированные значения свойств.

Это -- callbackи loDriver:

HRESULT (*ldQueryAvailableProperties)(const loCaller *,
        const loTagPair *tag, const LPWSTR szItemID,
        DWORD *pdwCount, DWORD **ppPropertyIDs,
        LPWSTR **ppDescriptions, VARTYPE **ppvtDataTypes);

HRESULT (*ldGetItemProperties)(const loCaller *,
        const loTagPair *tag, const LPWSTR szItemID,
        DWORD dwCount, DWORD *pdwPropertyIDs,
        VARIANT **ppvData, HRESULT **ppErrors, LCID lcid);

HRESULT (*ldLookupItemIDs)(const loCaller *,
        const loTagPair *tag, const LPWSTR szItemID,
        DWORD dwCount, DWORD *pdwPropertyIDs,
        LPWSTR **ppszNewItemIDs, HRESULT **ppErrors);
const loTagPair *tag
идентифицирует тег, если это возможно, параметрами loTagPair::tpTi и loTagPair::tpRt, loTagPair::tpAP всегда 0.

В целом, эти функции соответствуют методам интерфейса IOPCItemProperties, однако они получают дополнительную информацию о контексте вызова и, что более важно, их выходные параметры уже содержат результат обработки запроса библиотекой LightOPC.

4. Работа

Работа сервера состоит, главным образом, в обеспечении обмена данными между клиентами и устройствами.

Дополнительно сервер может контролировать права доступа клиентов и вести аудит активности клиентов.

LightOPC предоставляет два способа обмена данными: запись в кеш и запросы к устройству.

4.1 Данные

Данные OPC представляют собой разделяемые переменные (теги). Помимо собственно значения, тег имеет ряд дополнительных атрибутов:

typedef struct loTagState
   {
    FILETIME tsTime;
    HRESULT  tsError;
    int      tsQuality;
   } loTagState;
FILETIME tsTime
момент времени (абсолютное UTC - GMT время), которому соответствует значение тега;
HRESULT tsError
код ошибки для операции чтения тега из устройства или дополнительный код статуса тега, уточняющий tsQuality. Обычно, должен быть S_OK;
int tsQuality
качество значения тега, см. OPC-DA;

Драйвер устанавливает текущеие значения тегов, используя следующую структуру:

typedef struct loTagValue
   {
    VARIANT    tvValue; /* значение тега                 */
    loTagState tvState; /* атрибуты значения, см. выше   */
    loTagId    tvTi;    /* внутренний идентификатор тега */
   } loTagValue;

Тип значения тега tvValue не должен иметь флага VT_BYREF.

В передаваемых драйверу клиентских запросах теги идентифицируются следующим образом:

typedef struct loTagPair
   {
    loTagId   tpTi; /* внутренний идентификатор тега */
    loRealTag tpRt; /* драйверный идентификатор тега */
    void     *tpAP; /* драйверный идентификатор пути доступа
                       может быть нулевым указателем */
   } loTagPair;

4.1.1 Обновление кеша

Всё просто: драйвер пишет данные в кеш, а клиенты их читают.

Обычно, драйвер должен регулярно поизводить обновления кеша. Это совершенно необходимо, поскольку кеш используется для генерации уведомлений (OnDataChange) клиентам.

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

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

Драйвер волен сам устанавливать времена опроса устройств и обновления кеша никак не координируясь с клиентскими запросами, что в конечном счёте упрощает драйвер.

Примитивный драйвер может писать в кеш прямо в цикле опроса устройств, а опыт показывает, что чем примитивней, тем надёжней...

Простейший способ записать данные в кеш:

loTrid loCacheUpdate(loService *,
                     unsigned   count,
                     loTagValue taglist[],
                     FILETIME  *timestamp
                    );
unsigned count
количество элементов в массиве taglist;
loTagValue taglist[]
массив из count значений тегов; может содержать произвольный набор тегов; элементы с нулевым loTagValue::tvTi игнорируются; совершенно необходимо, чтобы типы значений loTagValue::tvValue точно соответствовали каноническим;
FILETIME *timestamp
необязательное (может быть NULL) время, используемое для замещения нулевых loTagValue::tvState.tsTime. Порядок замещения определяется параметрами компиляции LO_FILL_TIMESTAMP и LO_EV_TIMESTAMP. По умолчанию замещение производится наиболее свежим (последним) timestamp в момент удовлетворения клиентского запроса. NULL указатель не изменяет ранее установленное значение. См. также loCacheTimestamp().
loTrid возвращаемое значение
идентификатор транзакции обновления кеша.
0 - при ошибках недействительного loService или иных неправильных аргументах. Ошибки VariantCopy() при копировании данных обнаруживаются, но не влияют на результат loCacheUpdate().

Более гибкий способ записи в кеш предоставляют функции:

loTagValue *loCacheLock(loService *);
loTrid loCacheUnlock(loService *, FILETIME *);

Функция loCacheLock() блокирует вспомогательный буфер для исключительного доступа драйвера и возвращает указатель на этот буфер (или 0 - в случае ошибки - неверного loService). Драйвер должен, используя loTagID как индекс в этом массиве, заполнить обновляемые элементы новыми значениями.

Все теги обновлять необязательно. Драйвер помечает измененые теги, устанавливая им ненулевое значение loTagValue::tvTi.

При переносе данных из вспомогательного буфера в основной кеш:
  - поля loTagValue::tvTi очищаются нулями;
  - поля loTagValue::tvValue могут быть очищены или нет, в зависимости от параметра компиляции LO_KEEP_OLD_CACHE;
  - остальные поля (loTagValue::tvState.*) не изменяются.

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

Также, драйвер в любом случае должен быть готов к наличию старых данных в буфере (что особенно критично для loTagValue::tvValue, так как там могут иметься ссылки на динамически распределяемые данные).

Функция loCacheUnlock() инициирует процесс перемещения данных в основной кеш. Возвращаемое значение loTrid и аргумент FILETIME имеют тот же смысл, что и в функции loCacheUpdate(). Фактически, функция loCacheUpdate() пользуется функциями loCacheLock() и loCacheUnlock(); возможно, вам будет интересно посмотреть её реализацию (файл cacheupd.c).

Пользуясь функциями loCacheLock() и loCacheUnlock(), драйвер может устанавливать значение поля loTagValue::tvTi равным loCH_TIMESTAMP. При этом будет обновлён только маркер времени тега loTagValue::tvState.tsTime, а сам тег будет считаться неизменившимся. При использовании функции loCacheUpdate() такой возможности, естественно, нет, поскольку поле loTagValue::tvTi используется для идентификации изменяемого тега.

Имейте, однако, в виду, что loCH_TIMESTAMP нежелательно устанавливать, если loTagValue::tvTi уже содержит ненулевое значение от предыдущей транзакции (которая, очевидно, не успела завершиться).

Помните также, что loTagValue::tvValue должен быть приведён к каноническому типу тега.

4.1.1.1 Отложенная запись и loTrid

Запись в кеш не происходит непосредственно при вызоыве loCacheUpdate(), но откладывается и выполняется асинхронно. Таким образом, loCacheUpdate() не ждёт освобождения кеша читающими его клиентами и не порождает непредсказуемых задержек.

Драйвер может дождаться переноса данных в основной кеш вызовом функции

int loTridWait(loService *, loTrid);

возвращаемое значение:
-1- недопустимый аргумент
0- ожидание прервано, loServiceDestroy()
1- транзакция закончена

Функция

loTrid loTridLast(loService *);

Возвращает идентификатор текущей транзакции в первичном кеше или 0, если loService недействителен или был вызыван loServiceDestroy().

Функция

int loTridOrder(loTrid earlier, loTrid latter);

Сравнивает два идентификатора транзакций и возвращает ненулевое значение если они равны или earlier имела место раньше, чем latter. Например, вызов
    loTridOrder(my_trid, loTridLast(my_service))
возвратит 0, если транзакция my_trid ещё не добралась до первичного кеша.

Время жизни loTrid ограничено. Новый идентификатор формируется при каждой попытке записи в кеш; ранее возвращённые значения могут использоваться повторно. Гарантируется, что драйвер может одновременно использовать (сравнивать с помощью loTridWait() или loTridOrder()) по меньшей мере 2^31 последних последовательно сгенерированных loTrid.

Транзакции атомарны в том смысле, что все значения тегов, указанные в одной транзакции, могут быть помещены в кеш только вместе. Однако, последовательные транзакции могут сливаться, при этом более старые данные теряются. Во всяком случае, если драйвер всегда обновляет значения группы тегов в одной транзакции (то есть никогда не обновляет отдельные теги этой группы друг без друга) то кешированные значения этой группы тегов в любой момент времени будут соответствовать одной транзакции.

Обычно драйверу нет нужды использовать функции семейства loTrid*() явным образом.

4.1.1.2 Управление активностью

Для управления частотой опроса устройств или использованием полосы пропускания полевой сети драйверу может потребоваться некоторая информация об интенсивности клиентских запросов.

В тоже время, рационально использовать такую информацию трудно, тем более, что часто она во многом обусловлена особенностями (или ошибками) клиентов.

Например, клиент (SCADA) может потребовать опрашивать некоторые теги с периодом 1мс, однако это вовсе не означает, что такая частота опроса необходима или возможна. Быть может, оператор просто захотел построить тренд этого параметра на своём 25" дисплее... А SCADA резонно запросила значения на каждый экранный пиксель.

Можно заметить, что адаптивная настройка сервера в таких условиях может оказаться не только трудной, но и бессмысленной. А попытки удержаться в рамках заданной полосы пропускания, поднимая частоту опроса одних тегов за счёт других, и вовсе ведут к вредной демократии.

Очевидно, планируя опрос устройств, сервер должен полагаться не на эфемерную коньюнктуру клиентских запросов, но на априорное знание свойств устройств и процессов, ими контролируемых, каковое знание должно являться частью конфигурации сервера.

Тем не менее, некоторая адаптивная подстройка сервера возможна.

4.1.1.2.1 Количество клиентов

Драйвер может учитывавать количество активных клиентов, естественным образом контролируя их подключёние. Смотри также параметры loDriver:: ldRefreshRate и ldQueueMax.

4.1.1.2.2 Частота обновлений кеша

Используя функции loCacheLock() и loCacheUnlock(), драйвер может обнаружить, что данные не успевают перемещаться в основной кеш. В этом случае логично было бы снизить частоту опроса -- всё равно излишние данные будут перекрыты более свежими.

Однако, при снижении частоты опроса необходима некоторая инерционность:

Во-первых, всегда возможны длительные приостановки работы, вызванные случайными причинами, а обнаружить восстановление нормальной скорости прохождения танзакций обновления кеша труднее, чем её снижение.

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

Итак, после вызова loCacheUpdate() или loCacheUnlock() события развиваются следующим образом:

  1. Инициируется процесс перемещения данных в основной кеш.

  2. Инициируется процесс блокирования первичного кеша. Это связано с ожиданием завершения всех текущих клиентских обращений к кешу. Очевидно, длительность ожидания зависит от текущей интенсивности клиентских запросов.

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

  3. Как только основной кеш удалось заблокировать происходит блокирование вспомогательного буфера; дальнейшие попытки драйвера обновить кеш будут блокироваться.

  4. Происходит перемещение данных из вспомогательного буфера в основной кеш. Все накопленные транзакции завершаются.

  5. Разблокируется вспомогательный буфер. Теперь он чист и не содержит предыдущих незавершенных транзакций.

Очевидно, что если драйвер обновляет различные группы тегов в разных транзакциях, то о перегрузке сервера можно говорить только если имеет место перезапись одних и тех же недообновленных тегов.

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

4.1.1.2.3 Мониторинг активных тегов

Драйвер может получать уведомления о тегах, активированных клиентами. Для неактивных тегов кеш можно не обновлять. Соответственно, драйвер может снижать частоту опроса неактивных тегов или не опрашивать их совсем. Не будем, впрочем, забывать и о возможных прямых обращениях к устройствам.

Уведомления приходят в callback, определяемый в структуре loDriver:

void (*ldSubscribe)(const loCaller *, int count, loTagPair til[]);

loCaller *
описан выше;
int count
абсолютная величина определяет количество тегов в массиве til[]; если count > 0 то теги активируются, если count < 0, то деактивируются;
loTagPair til[]
массив из |count| [де]активируемых тегов; формат loTagPair описан выше; поля loTagPair::tpAP содержат 0.

Изначально все теги неактивны.

Условия вызова ldSubscribe() определяются наличием флага loDF_SUBSCRIBE_RAW в loDriver::ldFlags.

Если loDF_SUBSCRIBE_RAW не установлен
Библиотека LightOPC сама ведёт счётчики активаций, поэтому драйвер получает уведомления только о первой активации и последней деактивации каждого тега. Соответственно, игнорируя til[], драйвер может подсчитывать только общее количество активных тегов простым суммированием count.
Поля ca_cli* содержат 0 и конкретный клиент не может быть идентифицирован;
Если loDF_SUBSCRIBE_RAW установлен
Каждая [де]активация тега в каждой группе любым клиентом будет доставляться в ldSubscribe(). Драйвер должен сам обслуживать счетчики активаций. Однако, идентификация клиента возможна.

Желательно, чтобы прекращая опрос тега, драйвер установил ему качество OPC_QUALITY_LAST_KNOWN, OPC_QUALITY_OUT_OF_SERVICE или иное, отличное от OPC_QUALITY_GOOD.

4.1.1.3 Обновление по прерываниям

Эффективной альтернативой постоянному опросу устройств является обслуживание устройств по запросам (прерываниям).

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

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

Тем не менее, обновление кеша по прерыванию имеет две особенности:
Во-первых, драйвер может с пользой применить флаги loTF_NOCOMP, loDF_NOCOMP.
Во-вторых, и это более существенно, драйвер должен периодически обновлять, если и не сами данные, то хотя-бы их отметки времени.

Дело в том, что согласно стандарту OPC-DA, клиент в праве рассчитывать на получение текущих значений тегов. Причём под текущим значением понимается значение не старее объявленного UpdateRate соответствующих групп, то есть отметка времени тега не должна отставать от текущего времени более чем на UpdateRate миллисекунд, даже если значение тега давно не изменялось.

Однако, при чтении кеша клиент фактически получит время последнего изменения тега, вместо времени, близкого к текущему и соответствующего текущему значению тега.

Таким образм, драйвер должен в отсутствие изменений значений тегов регулярно обновлять их временные отметки. Отметки времени должны отставать от текущего времени на величину задержки реакции на прерывание.

Увы, для обеспечения монотонности времени LightOPC не может проставлять время сам, поскольку не знает реактивности устройства / драйвера. Он не может также запрашивать время у драйвера, поскольку на момент запроса могут иметься незавершённые транзакции обновления кеша: они могут соответствовать более раннему времени, чем момент запроса, но к клиенту эти данные попадут позже.

Итак, чтобы избежать посылки клиенту старых данных со свежим временем, необходимо направлять обновления времени по тому же пути, что и обновления данных. Есть несколько способов делать это:

  1. Можно просто периодически обновлять все теги, исправляя их loTagValue::tvState.tsTime.
  2. Используя loCacheLock() и loCH_TIMESTAMP можно устанавливать произвольные времена отдельным тегам.
  3. Можно установить отдельным тегам нулевой loTagValue::tvState.tsTime и указывать реальное значение времени для них в аргументе timestamp функций loCacheUpdate(), loCacheUnlock().
  4. Можно установить отдельным тегам нулевой loTagValue::tvState.tsTime.dwHighDateTime, а в tsTime.dwLowDateTime помещать индекс эквитемпоральной группы. Все теги одной группы имеют одну отметку времени, устанавливаемую функцией loCacheTimestamp().

int loCacheTimestamp(loService *,
                     unsigned  count,
               const FILETIME  ts[]
                    );
unsigned count
- количество элементов в массиве ts[]; может меняться от вызова к вызову.

const FILETIME ts[]
- отметки времени для групп [ 0 ... (count-1) ]. Группа 0 - та же, что подразумевается в аргументе timestamp функций loCacheUpdate(), loCacheUnlock(); При увеличении count создаются новые группы; при уменьшении - группы с индексами >= count не изменяют своих отметок времени поэтому имеет смысл помещать частообновляемые группы в начало списка.

Возвращаемое значение
0 - операция выполнена успешно;
ENOMEM - количество групп не божет быть увеличено.

Функция loCacheTimestamp() может вызываться только при блокированном кеше - между вызовами loCacheLock() и loCacheUnlock().

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

4.1.2 Обращения к устройству

Callbackи -- необходимое зло. Например, при некешируемых обращениях к устройству.

Запросы к устройству инициируются клиентом указанием параметра OPC_DS_DEVICE и транслируются в два callbackа: *ldReadTags() для чтения и *ldWriteTags() для записи.

Для этих вызовов гарантируются:

атомарность
каждый клиентский запрос представлен отдельным, неделимым вызовом.

сериализация
все запросы одного клиента -- синхронные и асинхронные -- обрабатываются последовательно, в порядке поступления.

Таким образом, драйвер может реализовывать такие странные вещи как разрушающее чтение. Помните однако, что вызовы от разных loClient не сериализуются и могут выполняться параллельно разными нитками.

Внутри большинства callbackов (если явно не указано иное) драйвер может безбоязненно вызывать любые функции библиотеки LightOPC.

4.1.2.1 *ldWriteTags()

int (*loDriver::ldWriteTags)(const loCaller *,
                             unsigned  count,
                             loTagPair taglist[],
                             VARIANT   values[],
                             HRESULT   errs[],
                             HRESULT  *master_err,
                             LCID
                            );
const loCaller *
- контекст вызова.

unsigned count
- количество элементов в массивах taglist[], values[], errs[].

loTagPair taglist[]
- описатели тегов запроса, драйвер должен игнорировать записи, имеющие tpTi == 0, или, возможно, tpRt == 0.

VARIANT values[]
- значения для записи, в том виде, как их указал клиент. Возможно, требуется преобразование типа. Драйвер может модифицировать values[] и выполнять преобразование «на месте».

HRESULT errs[]
- драйвер должен записать сюда результат записи каждого тега (кроме тех, что были проигнорированы), в соответствии с требованиями стандарта.

HRESULT *master_err
- суммарный код ошибки; драйвер должен установить его в S_FALSE, если если он записывал в errs[] значения, отличные от S_OK. В противном случае (если хорошо всё, что не проигнорировано) драйвер не должен изменять значение *master_err.

LCID
- национальный идентификатор, ассоциированный с запросом. Предполагается, что клиент может посылать (и получать) данные (values[]) в национальном (локализованном) представлении.

возвращаемое значение
- может быть одним из следующих:
loDW_TOCACHE
библиотека LightOPC должна будет выполнить запись в кеш всех значений, имеющих ненулевые taglist[].tpTi. Типы данных будут приведены к каноническим автоматически (через VariantChangeType()). Естественно, драйвер может модифицировать taglist[], values[] и errs[]. Драйвер может выполнить запись в устройство лишь отдельных тегов запроса, естественно, это разрушит атомарность запроса.
loDW_ALLDONE
библиотека LightOPC не будет выполнять запись в кеш. Вероятно, драйвер должен будет сделать это явным образом, однако это зависит от тонкостей процедуры записи в устройство.

Драйвер может не определять функцию loDriver::*ldWriteTags(). В этом случае запись будет производиться непосредственно в основной кеш и изменённые значения будут видны всем клиентам. Идентичный результат даст реализация *ldWriteTags(), не делающая ничего, кроме возврата loDW_TOCACHE.

4.1.2.2 *ldReadTags()

loTrid (*loDriver::ldReadTags)(const loCaller *,
                        unsigned  count,
                        loTagPair taglist[],
                        VARIANT   values[],
                        WORD      qualities[],
                        FILETIME  stamps[],
                        HRESULT   errs[],
                        HRESULT  *master_err,
                        HRESULT  *master_qual,
                        const VARTYPE vtype[],
                        LCID
                       );
Параметры loCaller *, count, taglist[], values[], errs[], *master_err и LCID
имеют то же назначение и смысл, что и одноимённые параметры функции *ldWriteTags(). Однако, values[] используется для возврата прочитанных значений.

WORD qualities[], FILETIME stamps[]
атрибуты тега -- качество и отметка времени, ассоциируемые со значением тега. Драйвер должен заполнить эти массивы. Они будут переданы клиенту.

HRESULT *master_qual
суммарное качество, аналогично *master_err, драйвер должен установить значение S_FALSE, если какие-либо из прочитанных тегов имеют qualities[] отличное, от OPC_QUALITY_GOOD.

Отметим, что теги, для которых драйвер установил неуспешные коды в errs[], не должны иметь хорошего качества OPC_QUALITY_GOOD в qualities[].

VARTYPE vtype[]
типы значений тегов, запрошенные клиентом. Драйвер должен преобразовывать возвращаемые values[] к указанным типам, возможно, используя параметр LCID.

возвращаемое значение
- может быть одним из следующих:
loDR_STORED
драйвер прочитал и сохранил все необходимые значения. Библиотека LightOPC должна просто отправить их клиенту.
loDR_CACHED
запрошенные значения уже находятся в кеше. Библиотека LightOPC должна выполнить чтение из кеша тегов, имеющих ненулевые taglist[].tpTi.
loTrid - идентификатор транзакции, возвращённый функциями loCacheUpdate(), loCacheUnlock().
запрошенные значения отправлены в кеш. Библиотека LightOPC должна дождаться завершения транзакции, а затем выполнить чтение, как и в случае loDR_CACHED.

Обычно ожидается, что произведя чтение устройства, драйвер произведёт и обновление кеша. LightOPC не обновляет кеш автоматически, поэтому драйвер должен делать это сам.

Обновляя кеш по запросу некешированного чтения, драйвер может не обслуживать сам этот запрос, а вернуть значение, отличное от loDR_STORED. В этом случае библиотека вернёт клиенту кешированные данные, атомарность операции при этом не может быть гарантирована, более того, гарантируется лишь, что данные будут соответствовать транзакции не более ранней, чем указанная драйвером.

Следующие действия приводят к эквивалентным результатам:
return loCacheUpdate(...); loTridWait(loCacheUpdate(...));
return loDR_CACHED;

Драйвер может выполнить некешированное чтение лишь для части тегов запроса. Для этого необходимо фактически прочитанные теги пометить как taglist[].tpTi = 0 и вернуть значение, отличное от loDR_STORED. Теги с необнулёнными taglist[].tpTi будут прочитаны из кеша.

ВНИМАНИЕ! Аргументы values[], qualities[], stamps[], errs[] могут быть нулевыми указателями. Это происходит, если клиент инициировал операцию Refresh(), а не Read(). Естественно, в этом случае драйвер не должен заполнять указанные массивы и не должен возвращать loDR_STORED; драйвер может только обновить кеш для запрошенных тегов. Чтобы обнаружить эту ситуацию достаточно проверять только values[].

Драйвер может не определять функцию loDriver::*ldReadTags(). В этом случае запросы чтения устройства по прежнему будут подвергаться принудительной сериализации и выполняться последовательно, но чтение всегда будет производиться из кеша.

4.1.3 Преобразования типов и национализация

Стандарт рекомендует для преобразования типов локализуемых данных (то есть, имеющих специфическое национальное представление: дата, время, десятичная точка...) использовать функцию VariantChangeTypeEx(). Это, однако, не всегда даёт осмысленный результат.
Во-первых, национальный идентификатор LCID не отражает локальных пользовательских настроек национальных форматов;
во-вторых, OPC-DA предусматривает передачу локализованных текстовых значений перечислимых типов (EUtype = OPC_ENUMERATED), которые, естественно вообще не могут быть преобразованы VariantChangeTypeEx().

Callbackи *ldReadTags() и *ldWriteTags() получают необходимый идентификатор LCID и могут производить локализацию.

Обычно, чтение кеша выполняется без участия драйвера и локализация тегов не выполняется. Однако, при необходимости она может быть возложена на callback:

   void (*loDriver::ldConvertTags)(const loCaller *,
                          unsigned  count,
                    const loTagPair taglist[],
                          VARIANT   values[],
                          WORD      qualities[],
                          HRESULT   errs[],
                          HRESULT  *master_err,
                          HRESULT  *master_qual,
                    const VARIANT   source[],
                    const VARTYPE   vtype[],
                          LCID);

Большинство аргументов аналогичны одноимённым в функциях *ldReadTags() и *ldWriteTags().

Значения из source[] должны быть преобразованы к типам vtype[] в соответствии с LCID и сохранены в values[]. Указатели source и values могут быть одинаковыми.

LCID всегда целевой, запрошенный клиентом; исходные source[] имеют то же представление, в каком драйвер записывал их в кеш.

При ошибках следует исправить соответствующие qualities[] и errs[], а также *master_err и *master_qual.

Элементы с нулевыми taglist[].tpTi должны игнорироваться.

Функция *ldConvertTags() вызывается библиотекой перед тем как передать прочитанные из кеша данные клиенту. Причём только для тегов, имеющих флаг loTF_CONVERT, и только если клиентом запрошен тип VT_BSTR, VT_DATE или VT_ARRAY.

Атомарность запросов для этой фынкции не гарантируется.

В отличие от прочих callbackов LightOPC функция *ldConvertTags() может вызываться очень часто и из под многих блокировок. Поэтому к её реализации предъявляются дополнительные требования:

  1. лишь немногие функции LightOPC (loClientName(), loClientArg(), loDriverArg()) могут использоваться без ограничений;
  2. функции loTridWait(), loSetState() приведут к зависанию (deadlock) и не должны вызываться;
  3. обращения к другим функциям LightOPC приведут к заметному снижению производительности сервера;
  4. *ldConvertTags() должна работать быстро и не ождать никаких событий.

Файл sample.cpp содержит пример *ldConvertTags(), реализующий национализируемый перечислитель дней недели (наблюдать его работу можно, если запросить тег «enum-localizable» как VT_BSTR и изменять LCID группы).

Другое возможное применение *ldConvertTags() -- преобразование типов массивов (VT_ARRAY|*), поскольку функция VariantChangeType() массивы преобразовывать не умеет.

4.2 Управление доступом

Весь мир ныне помешан на безопасности и защите. Компутерный мир тоже.

Управление доступом предусматривает наличие авторизационной базы данных (ACL), содержащей списки объектов, субъектов и прав на обращение последних к первым. Однако, ведение такой базы данных определённо не является функцией OPC сервера. Кроме того, детали реализации авторизационной базы данных существенно зависят от требований конкретного применения.

Поэтому LightOPC предусматривает возможность обращений к внешним ACL, предоставляя встроенную реализацию лишь простейших средств управления доступом.

Имеется несколько способов контролировать доступ клиентов к тегам.

4.3 Вспомогательные функции

const char *loClientName(loClient *);
Возвращает имя, специфицированное клиентом в IOPCCommon::SetClientName() или, при ошибках, 0.
Функция будет возвращать 0 с момента инициации уничтожения loClient и, в частности, внутри release_handle(). Функция не должна вызываться после завершения release_handle().

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

  1. укорочено до некоторого предела;
  2. преобразовано в «узкую» (байтовую) строку, возможно, с искажением отдельных символов;
  3. до того как клиент установит имя может содержать внутренний идентификатор, сформированный LightOPC;
  4. может содержать частично изменённое имя, поскольку доступ к области памяти имени никак (почти) не синхронизируется.
В любом случае гарантируется, что
  1. однажды возвращённый указатель для некоторого loClient останется корректным вплоть до полного уничтожения этого loClient (то есть до завершения соответствующего release_handle());
  2. вплоть до завершения уничтожения loClient все вызовы loClientName() для этого loClient будут возвращать тот же самый указатель или 0;
  3. содержимое строки по этому указателю может меняться в любое время (осторожно со strlen()!), но, во всяком случае, это будет правильная терминированная строка.

void *loClientArg(loClient *);
Возвращает release_handle_arg, для указанного loClient.
Как и все прочие функции LightOPC, не имеющие аргумента loService, эта функция не слишком надёжно проверяет актуальность loClient и поэтому дожна использоваться с осторожностью.

void *loDriverArg(loService *);
Возвращает значение loDriver::ldDriverArg, использованное при создании loService.
Функция совсем не проверяет актуальность loService и может быть реализована как макрос.

int loGetBandwidth(loService *, loClient *cli);
Возвращает текущий процент использования полосы пропускания (OPCSERVERSTATUS::Bandwidth) указанным клиентом. Или наивысшее значение среди всех клиентов loService, если cli = 0.

Возможен возврат -1 -- в случае ошибки, или если текущий процент не может быть вычислен.

Процент использования полосы пропускания характеризует, главным образом, способность сервера обслуживать запрошенный UpdateRate. Значение выше 50% свидетельствует о перегрузке сервера.

Значение текущего процента вычисляется путём усреднения отдельных мгновенных значений. Постоянная времени интеграции порядка 1 с и может быть изменена вызовом:

unsigned lo_statperiod(unsigned);

Новая постоянная времени указывается в миллискундах и должна лежать в пределах 16...10000 мс.

Функция изменяет период интеграции для всех loService текущего процесса и может на некоторое время нарушить корректность рапортуемых полос пропускания.


double lo_filetime_to_variant(const FILETIME *ft);
void lo_variant_to_filetime(FILETIME *ft, double vd);
Преобразуют время из формата FILETIME в VariantTime (VT_DATE) и обратно.

Точность -- около 1 мкс, что заведомо лучше чем 1 с SystemTimeToVariantTime().



HRESULT lo_variant_changetype_array(VARIANT *dst, VARIANT *src,
                                    LCID lcid,
                                    unsigned short flags,
                                    VARTYPE vt);
Преобразует типы VARIANTов, включая типы VT_ARRAY|*. В остальном аналогична VariantChangeTypeEx() (при lcid != 0), или VariantChangeType() (при lcid == 0).

ЗАМЕЧАНИЕ: Функции
lo_variant_changetype_array()
lo_variant_to_filetime()
lo_statperiod()
не являются необходимыми для работы LightOPC и могут отсутствовать в отдельных версиях библиотеки.

5. Заключение

Никогда не любил писать заключения...

Впрочем, и такое заключение придаёт оглавлению изрядное сходство с известным опусом.

Вы, конечно, догадались с каким?