| Изменения | |
| Июнь 2002 | первая редакция |
| Февраль 2003 | Архитектура, loClientCreate_agg(), loDF_SUBSCRIBE_RAW |
| 3 марта 2003 | loCacheTimestamp() |
Библиотека LightOPC предназначена для создания OPC серверов. Она предоставляет OPC-DA интерфейс для клиентских приложений. С другой стороны (обожаю такие двусмысленности) она имеет определенный интерфейс для драйверов усторйств, полевых сетей (fieldbus), систем сбора данных. Настоящий документ посвящён "драйверному" интерфейсу LightOPC.
Естественно, спецификация OPC определяет не только DCOM интерфейсы, но и атрибуты данных, модель взаимодействия и прочие принципиальные вещи. Соответственно, определенные данные передаются от драйвера устройства к OPC-клиенту без изменений, поэтому читатель должен иметь под рукой OPC-DA стандарт: мы здесь не будем рассматривать все аспекты индустриальной автоматизации.
Так же останутся без внимания вопросы, освященные в LOPC-FAQ. Весьма рекомендуется заглядывать в lightopc.h -- там может содержаться ценная информация, опущенная здесь в художественных целях (или, во всяком случае, более соответствующая вашей версии библиотеки). Наконец, в файле options.h содержатся сведения о настраиваемых параметрах периода компиляции библиотеки.
Редкоиспользуемые возможности 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. Мало что может с ним сравниться.
....................................... : 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 <========================================+ : :
:...............................................:.....:
|
Как можно догадаться из вышесказанного, LightOPC -- вдоль и поперёк многониточен и с этим надо считаться. В частности:
Отметим также, что некоторые callbackи вызываются не из той клиентской нитки, которая их инициировала. Поэтому на CoQueryClientBlanket() полагаться не стоит.
Многие функциии LightOPC (если не указано иное) возвращают int
код ошибки из тех, что определены в errno.h.
0 | - всё хорошо; |
|---|---|
EBADF | - недействительный loService / loClient; |
EEXIST | - имя (тега) уже существует; |
ENOENT | - нет такого имени (тега), элемента; |
-1 | - просто ошибка. |
В начале надо создать loService. Хотя бы один, но их может быть больше. Он создается функцией:
int loServiceCreate(loService **result, const loDriver *,
unsigned tagcount);В *result, естественно, будет помещен результат. Он будет использоваться при вызове большинства остальных функций.
Сруктура loDriver содержит необходимые для инициализации данные, рассматриваимые в следующей главе.
Важнейший параметр - tagcount. Он определяет количество тегов, поддерживаемых сервером. Оно может быть любым (в разумных приделах). Теги можно добавлять в любое время, в частности и в работающий сервер, и из большинства callbackов, но нельзя удалять теги или увеличить tagcount. Единственный способ сделать это -- создать новый loService.
Разумные пределы для tagcount опредляются объёмом оперативной памяти: каждый тег занимает примерно от 130 до 180 байт, не считая имени тега.
int loServiceDestroy(loService *se).
Этот вызов имеет особенность: он не уничтожает loService непосредственно, но инициирует асинхронный процесс отключения присоединенных клиентов. Поэтому с момента вызова loServiceDestroy() вы не должны делать обращений к loService (такие обращения могут быть небезопасны), но само уничтожение объекта может произойти уже после возврата из loServiceDestroy().
Способ поймать момент отключения всех клиентов будет рассмотрен вместе с loClient (см. loClientCreate()).
Структура loDriver описывает драйвер и содержит много интересных параметров. Для начала её можно инициализировать нулями.
| loMilliSec ldRefreshRate | - гранулярность клиентских UpdateRate; |
|---|---|
| loMilliSec ldRefreshRate_min | - минимальный допустимый UpdateRate. |
Эти параметры определяют интенсивность поиска изменений в кеше. По умолчанию они весьма малы и часто имеет смысл сделать их сравнимыми с реактивностью ваших технологических процессов, что снизит нагрузку на сервер.
UpdateRate является параметром OPC группы, устанавливаемым клиентом и определяющим частоту обновления кеша. Подробнее он описан в стандарте OPC-DA.
ldRefreshRate_min позволяет защитить клиента от аномально частых обновлений.
Гранулярность ldRefreshRate, отличная от 1, может привести к отличию действущих, «пересмотренных» UpdateRate от запрошенных клиентом.
Другие параметры:
void *loDriverArg(loService *);
| 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)
HRESULT loEnumStrInsert(LPENUMSTRING, const loWchar *)
Естественно, loEnumStrInsert() может быть применён только к специальному перечислителю, полученному от LightOPC.
void (*ldCurrentTime)(const loCaller *, FILETIME *)
unsigned (*ldGetErrorString)(const loCaller *, HRESULT ecode,
LCID locale, loWchar *buf, unsigned wcsize)
Функция должна возвращать 0, если она не может транслировать ecode. Во всех прочих случаях (в том числе, при нулевом buf или wcsize) должна быть возвращена полная длина сообщения в символах, не считая завершающего '\0'. Если буфер слишком короток, то сообщение должно быть возвращено в усеченном виде, а возвращенная длина должна соответствовать неусеченному сообщению.
HRESULT (*ldQueryAvailableLocaleIDs)(const loCaller *,
DWORD* pdwCount, LCID** pdwLcid)
int (*ldCheckLocale)(const loCaller *, LCID dwLcid)
| loService *ca_se | - самоочевидно; |
|---|---|
| void *ca_se_arg | - соответствующий ldDriverArg; |
| loClient *ca_cli | - клиентский объект; |
| void *ca_cli_arg | - соответствующий release_handle_arg. |
Клиенты и серверы OPC обмениваются данными посредством разделяемых переменных, которые мы будем называть тегами.
Совокупность тегов, предоставляемых сервером, называется адресным пространством (Address Space, пространством имён) этого сервера.
В адресном пространстве LightOPC серверов теги создаются функциями семейства 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*/
);
"device22/unit33/tag11",
в предположении, что разделитель ldBranchSep='/';
обратите внимание, что ведущий (перед "device22") разделитель отсутствует).
tName может быть пустой строкой "" или нулевым указателем
(0, NULL) при этом будет создан безымянный
тег, рассматриваемый в отдельной главе.
"reg[000...999]", а саму
тысячу однообразных имён скрыть. Альтернативный способ -- использование
безымянных тегов.
"reg[000...999]".
Компанию 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) можно принять следующие меры (независимые друг от друга):
Создаваемые теги, могут не иметь имён. Естественно, такие теги не будут видны клиентам через IOPCBrowseServerAddressSpace, однако они доступны для всех прочих операций.
HRESULT (*ldAskItemID)(const loCaller *,
loTagId *ti, void **acpa, /* to be returned */
const loWchar *itemid,
const loWchar *accpath,
int vartype,
int goal
);| 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.
|
| * * | новые коды могут быть определены в дальнейшем. |
Соединение с клиентом обслуживается объектом 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
);typedef struct loVendorInfo
{
WORD lviMajor, lviMinor, lviBuild, lviReserv;
char *lviInfo;
} loVendorInfo;
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
);QI::CreateInstance.
ЗАМЕЧАНИЕ: Используйте *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
);Вы, вероятно, захотите использовать release_handle_arg для сохранения указателя на свой объект, ассоциированный (или агрегированный) с loClient.
Ваша реализация дополнительных интерфейсов должна делегировать все методы IUnknown объекту loClient. Реализация *qi_chain() не должна вызывать loClient::QueryInterface(); она должна возвращать ошибку при запросе интерфейса IUnknown и любых интерфейсов, реализацию которых вы оставляете за loClient. Завершение жизни агрегированного объекта можно определить по release_handle().
Функция loClientChain() не является ниточно-безопасной (threadsafe) и не должна вызываться после того, как loClient передан клиенту.
Для того, чтобы устанавливать соединения с 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
);
| loOP_OPERATE | нормальная работа, может использоваться для возобновления работы после loOP_STOP; |
|---|---|
| loOP_STOP | приостановить обслуживание клиента. Большинство запросов клиента будут возвращать ошибку E_FAIL; но GetErrorString(), GetServerStatus() и некоторые другие функции будут работать. |
| loOP_SHUTDOWN | послать клиенту уведомление о завершении через IOPCShutdown. Клиенты, не использующие IOPCShutdown, этого уведомления, естественно, не получат; |
| loOP_DISCONNECT | вызвать CoDisconnectObject() для loClient и всех объектов, созданных для клиента. В некоторых случаях LightOPC может передавать клиенту объекты, созданные непосредственно драйвером; для таких объектов CoDisconnectObject() должен вызывать сам драйвер. |
Обычно, драйверу желательно контролировать время жизни подключенных клиентов, чтобы уметь своевременно завершиться.
Драйвер естественным образом контролирует подключение клиентов, создавая loClient в IClassFactory::CreateInstance. Однако, передав интерфейсный указатель клиенту, драйвер более не может непосредственно контролировать существование созданного loClient.
Определить момент уничтожения loClient драйвер может указав release_handle(). Таким образом, драйвер может своевременно уничтожать свои приватные данные, ассоциируемые с конкретным клиентом, а также поддерживать счётчик активных клиентов.
Отметим, что в момент вызова release_handle() соответствующий loClient уже не может функционировать корректно.
В общем случае справедливы следующие правила:
Для принудительного отключения всех или избранных клиентов драйвер может использовать loSetState(). Типичная последовательность такова:
Для решения таких проблем имеются специальные возможности:
Есть, конечно, и другие тонкости...
Предоставляются функции регистрации локального 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);
В дополнение к обычным 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
По умолчанию LightOPC работает в свободно-ниточной модели (freethreaded), но можно перевести библиотеку в режим поддержки «совместной» (both) модели.
Ниточная модель устанавливается флагами периода исполнения (run-time) loDriver::ldFlags -- глобально, или loClientCreate() / ldFlags -- на клиента:
Обычно, 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»! Тем не менее это работает, поскольку:
Суть проблемы в том, что ранние версии стандарта недостаточно внятно определяли порядок обработки клиентом пустых перечислителей. Позднее ошибку исправили, но встречаются ещё клиенты, написанные до её исправления.
Итак, если должен быть возвращён пустой перечислитель, то сервер может вернуть:
Если код завершения S_FALSE, то некоторые клиенты не отпускают пустой перечислитель, что приводит к утечкам памяти. Другие клиенты пытаются отпустить NULL, что приводит к краху.
LightOPC позволяет управлять возвращением пустых перечислителей как на этапе компиляции (параметр ENUM_EMPTY в options.h), так и на этапе исполнения (причём, независимо для разных клиентов):
Свойства (или атрибуты) тегов ItemProperties поддерживаются.
DWORD propid
loPLid
loTagId
Списки свойств, ассоцированные с тегом, упорядочены и идентифицируются номерами от 1 до loPROPLIST_MAX включительно. По умолчанию loPROPLIST_MAX = 3, это значение может быть изменено при перекомпиляции библиотеки.
Номер списка является его приоритетом: при поиске заданного свойства propid для некоторого тега сначала будет просмотрен список с номером 1. Последним просматривается подразумеваемый список из шести специальных свойств.
Список специальных свойств подразумевается у каждого тега, имеющего значение
(loAddRealTag(tdValue)), даже если он не имеет имени, помечен
как невидимый или является подсказкой.
ОСТОРОЖНО! В качестве свойства "текущее значение" возвращается
кешированное значение тега, что не соответствует стандарту. OPC-DA
требует выполнения некешированного чтения, что, по всей видимости,
объясняется недостоверностью содержимого кеша для неактивных тегов.
В LightOPC кеш вполне контролируется драйвером для всех тегов, независимо от
их активности. Если, тем не менее, некешированное чтение желательно, то
драйвер может выполнить его в рассматриваемом ниже callbackе
*ldGetItemProperties(). Следует, однако, иметь в виду, что эта
функция не подвергается принудительной сериализации,
в отличие от обычных запросов некешированного чтения.
loPLid loPropListCreate(loService *);
Будучи создан, спиок может быть уничтожен только вместе с содержащим его 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
);
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, позволяющий, в частности, возврашать некешированные значения свойств.
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);
В целом, эти функции соответствуют методам интерфейса IOPCItemProperties, однако они получают дополнительную информацию о контексте вызова и, что более важно, их выходные параметры уже содержат результат обработки запроса библиотекой LightOPC.
Работа сервера состоит, главным образом, в обеспечении обмена данными между клиентами и устройствами.
Дополнительно сервер может контролировать права доступа клиентов и вести аудит активности клиентов.
LightOPC предоставляет два способа обмена данными: запись в кеш и запросы к устройству.
Данные OPC представляют собой разделяемые переменные (теги). Помимо собственно значения, тег имеет ряд дополнительных атрибутов:
typedef struct loTagState
{
FILETIME tsTime;
HRESULT tsError;
int tsQuality;
} loTagState;
Драйвер устанавливает текущеие значения тегов, используя следующую структуру:
typedef struct loTagValue
{
VARIANT tvValue; /* значение тега */
loTagState tvState; /* атрибуты значения, см. выше */
loTagId tvTi; /* внутренний идентификатор тега */
} loTagValue;
Тип значения тега tvValue не должен иметь флага VT_BYREF.
В передаваемых драйверу клиентских запросах теги идентифицируются следующим образом:
typedef struct loTagPair
{
loTagId tpTi; /* внутренний идентификатор тега */
loRealTag tpRt; /* драйверный идентификатор тега */
void *tpAP; /* драйверный идентификатор пути доступа
может быть нулевым указателем */
} loTagPair;
Всё просто: драйвер пишет данные в кеш, а клиенты их читают.
Обычно, драйвер должен регулярно поизводить обновления кеша.
Это совершенно необходимо, поскольку кеш используется для генерации
уведомлений (OnDataChange) клиентам.
Исключения составлют те экзотические случаи, когда клиентам не требуется полноценный доступ к данным, например, когда сервер должен предоставлять лишь интерфейс просмотра адресного пространства IOPCBrowseServerAddressSpace. В таких случаях обновление кеша не является обязательным.
Процедуры обновления кеша предельно просты для драйвера. Драйвер синхронно пишет данные во вспомогательный буфер из которого они асинхронно перемещаются в основной кеш отдельной ниткой. Таким образом, вся громоздкая логика синхронизации сосредоточена в этой отдельной нитке и не вызывается драйвером непосредственно. Напротив, драйвер оказывается надёжно изолирован от клиентских обращений кешу. Запись во вспомогательный буфер не может быть блокирована клиентом и вообще никак не интерферирует с клиентскими запросами.
Драйвер волен сам устанавливать времена опроса устройств и обновления кеша никак не координируясь с клиентскими запросами, что в конечном счёте упрощает драйвер.
Примитивный драйвер может писать в кеш прямо в цикле опроса устройств, а опыт показывает, что чем примитивней, тем надёжней...
Простейший способ записать данные в кеш:
loTrid loCacheUpdate(loService *,
unsigned count,
loTagValue taglist[],
FILETIME *timestamp
);
Более гибкий способ записи в кеш предоставляют функции:
loTagValue *loCacheLock(loService *);
loTrid loCacheUnlock(loService *, FILETIME *);
Функция loCacheLock() блокирует вспомогательный буфер для исключительного доступа драйвера и возвращает указатель на этот буфер (или 0 - в случае ошибки - неверного loService). Драйвер должен, используя loTagID как индекс в этом массиве, заполнить обновляемые элементы новыми значениями.
Все теги обновлять необязательно. Драйвер помечает измененые теги, устанавливая им ненулевое значение loTagValue::tvTi.
Драйвер может обнаружить, что данные от предыдущих транзакций не успели переместиться в кеш. Об этом будет свидетельствовать ненулевые значения соответствующих 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 должен быть приведён к каноническому типу тега.
Запись в кеш не происходит непосредственно при вызоыве 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*()
явным образом.
Для управления частотой опроса устройств или использованием полосы пропускания полевой сети драйверу может потребоваться некоторая информация об интенсивности клиентских запросов.
В тоже время, рационально использовать такую информацию трудно, тем более, что часто она во многом обусловлена особенностями (или ошибками) клиентов.
Например, клиент (SCADA) может потребовать опрашивать некоторые теги с периодом 1мс, однако это вовсе не означает, что такая частота опроса необходима или возможна. Быть может, оператор просто захотел построить тренд этого параметра на своём 25" дисплее... А SCADA резонно запросила значения на каждый экранный пиксель.
Можно заметить, что адаптивная настройка сервера в таких условиях может оказаться не только трудной, но и бессмысленной. А попытки удержаться в рамках заданной полосы пропускания, поднимая частоту опроса одних тегов за счёт других, и вовсе ведут к вредной демократии.
Очевидно, планируя опрос устройств, сервер должен полагаться не на эфемерную коньюнктуру клиентских запросов, но на априорное знание свойств устройств и процессов, ими контролируемых, каковое знание должно являться частью конфигурации сервера.
Тем не менее, некоторая адаптивная подстройка сервера возможна.
Драйвер может учитывавать количество активных клиентов, естественным образом контролируя их подключёние. Смотри также параметры loDriver:: ldRefreshRate и ldQueueMax.
Используя функции loCacheLock() и loCacheUnlock(), драйвер может обнаружить, что данные не успевают перемещаться в основной кеш. В этом случае логично было бы снизить частоту опроса -- всё равно излишние данные будут перекрыты более свежими.
Однако, при снижении частоты опроса необходима некоторая инерционность:
Во-первых, всегда возможны длительные приостановки работы, вызванные случайными причинами, а обнаружить восстановление нормальной скорости прохождения танзакций обновления кеша труднее, чем её снижение.
Во-вторых, действующий механизм обновления кеша сам по себе обеспечивает некоторую адаптивность.
Итак, после вызова loCacheUpdate() или loCacheUnlock() события развиваются следующим образом:
Попытки повторного обновления кеша драйвером в течение всего этого времени не блокируются и драйвер может обнаружить, что ранее инициированная транзакция не завершена. Данные «старых» обновлений могут замещаться «новыми».
Очевидно, что если драйвер обновляет различные группы тегов в разных транзакциях, то о перегрузке сервера можно говорить только если имеет место перезапись одних и тех же недообновленных тегов.
Вообще же, ключевым вопросом производительности является количество транзакций, которые успевают осесть (и соединиться) в буфере пока блокируется основной кеш. Если оно велико, то сервер работает; если мало -- крутится вхлостую.
Драйвер может получать уведомления о тегах, активированных клиентами. Для неактивных тегов кеш можно не обновлять. Соответственно, драйвер может снижать частоту опроса неактивных тегов или не опрашивать их совсем. Не будем, впрочем, забывать и о возможных прямых обращениях к устройствам.
void (*ldSubscribe)(const loCaller *,
int count, loTagPair til[]);
Изначально все теги неактивны.
Условия вызова ldSubscribe() определяются наличием флага loDF_SUBSCRIBE_RAW в loDriver::ldFlags.
Желательно, чтобы прекращая опрос тега, драйвер установил ему качество OPC_QUALITY_LAST_KNOWN, OPC_QUALITY_OUT_OF_SERVICE или иное, отличное от OPC_QUALITY_GOOD.
Эффективной альтернативой постоянному опросу устройств является обслуживание устройств по запросам (прерываниям).
Предполагается, что устройство способно само обнаружить изменение контролируемых данных и послать соответствующий сигнал драйверу, а драйвер способен этот сигнал воспринять и загрузить обновлённые данные.
С точки зрения сервера это означает, всего лишь, что процедура обновления кеша может быть инициирована в случайный момент времени, как ей и положено.
Тем не менее, обновление кеша по прерыванию имеет две особенности:
Во-первых, драйвер может с пользой применить флаги
loTF_NOCOMP, loDF_NOCOMP.
Во-вторых, и это более существенно, драйвер должен периодически обновлять,
если и не сами данные, то хотя-бы их отметки времени.
Дело в том, что согласно стандарту OPC-DA, клиент в праве рассчитывать на получение текущих значений тегов. Причём под текущим значением понимается значение не старее объявленного UpdateRate соответствующих групп, то есть отметка времени тега не должна отставать от текущего времени более чем на UpdateRate миллисекунд, даже если значение тега давно не изменялось.
Однако, при чтении кеша клиент фактически получит время последнего изменения тега, вместо времени, близкого к текущему и соответствующего текущему значению тега.
Таким образм, драйвер должен в отсутствие изменений значений тегов регулярно обновлять их временные отметки. Отметки времени должны отставать от текущего времени на величину задержки реакции на прерывание.
Увы, для обеспечения монотонности времени LightOPC не может проставлять время сам, поскольку не знает реактивности устройства / драйвера. Он не может также запрашивать время у драйвера, поскольку на момент запроса могут иметься незавершённые транзакции обновления кеша: они могут соответствовать более раннему времени, чем момент запроса, но к клиенту эти данные попадут позже.
Итак, чтобы избежать посылки клиенту старых данных со свежим временем, необходимо направлять обновления времени по тому же пути, что и обновления данных. Есть несколько способов делать это:
loCacheTimestamp().
int loCacheTimestamp(loService *,
unsigned count,
const FILETIME ts[]
);
loCacheUpdate(), loCacheUnlock();
При увеличении count создаются новые группы; при уменьшении -
группы с индексами >= count не изменяют своих отметок времени поэтому
имеет смысл помещать частообновляемые группы в начало списка.
| 0 | - операция выполнена успешно; |
| ENOMEM | - количество групп не божет быть увеличено. |
Функция loCacheTimestamp() может вызываться только при
блокированном кеше - между вызовами
loCacheLock() и
loCacheUnlock().
В любом случае, для обеспечения монотонности времени драйвер должен обновлять времена тегов всякий раз, когда обновляет их значения. Желательно также, чтобы при обновлении значений тегов всегда использовалось более свежее время, чем при последнем обновлении их времени.
Callbackи -- необходимое зло. Например, при некешируемых обращениях к устройству.
Запросы к устройству инициируются клиентом указанием параметра OPC_DS_DEVICE и транслируются в два callbackа: *ldReadTags() для чтения и *ldWriteTags() для записи.
Для этих вызовов гарантируются:
Таким образом, драйвер может реализовывать такие странные вещи как разрушающее чтение. Помните однако, что вызовы от разных loClient не сериализуются и могут выполняться параллельно разными нитками.
Внутри большинства callbackов (если явно не указано иное) драйвер может безбоязненно вызывать любые функции библиотеки LightOPC.
int (*loDriver::ldWriteTags)(const loCaller *,
unsigned count,
loTagPair taglist[],
VARIANT values[],
HRESULT errs[],
HRESULT *master_err,
LCID
);
Драйвер может не определять функцию loDriver::*ldWriteTags(). В этом случае запись будет производиться непосредственно в основной кеш и изменённые значения будут видны всем клиентам. Идентичный результат даст реализация *ldWriteTags(), не делающая ничего, кроме возврата loDW_TOCACHE.
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
Отметим, что теги, для которых драйвер установил неуспешные коды в errs[], не должны иметь хорошего качества OPC_QUALITY_GOOD в qualities[].
Обычно ожидается, что произведя чтение устройства, драйвер произведёт и обновление кеша. LightOPC не обновляет кеш автоматически, поэтому драйвер должен делать это сам.
Обновляя кеш по запросу некешированного чтения, драйвер может не обслуживать сам этот запрос, а вернуть значение, отличное от loDR_STORED. В этом случае библиотека вернёт клиенту кешированные данные, атомарность операции при этом не может быть гарантирована, более того, гарантируется лишь, что данные будут соответствовать транзакции не более ранней, чем указанная драйвером.
Следующие действия приводят к эквивалентным результатам:
return loCacheUpdate(...);
|
loTridWait(loCacheUpdate(...));
|
Драйвер может выполнить некешированное чтение лишь для части тегов запроса. Для этого необходимо фактически прочитанные теги пометить как taglist[].tpTi = 0 и вернуть значение, отличное от loDR_STORED. Теги с необнулёнными taglist[].tpTi будут прочитаны из кеша.
ВНИМАНИЕ! Аргументы values[], qualities[], stamps[], errs[] могут быть нулевыми указателями. Это происходит, если клиент инициировал операцию Refresh(), а не Read(). Естественно, в этом случае драйвер не должен заполнять указанные массивы и не должен возвращать loDR_STORED; драйвер может только обновить кеш для запрошенных тегов. Чтобы обнаружить эту ситуацию достаточно проверять только values[].
Драйвер может не определять функцию loDriver::*ldReadTags(). В этом случае запросы чтения устройства по прежнему будут подвергаться принудительной сериализации и выполняться последовательно, но чтение всегда будет производиться из кеша.
Стандарт рекомендует для преобразования типов локализуемых данных (то есть,
имеющих специфическое национальное представление: дата, время, десятичная
точка...) использовать функцию 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() может вызываться очень часто и из под многих блокировок. Поэтому к её реализации предъявляются дополнительные требования:
Файл sample.cpp содержит пример *ldConvertTags(), реализующий национализируемый перечислитель дней недели (наблюдать его работу можно, если запросить тег «enum-localizable» как VT_BSTR и изменять LCID группы).
Другое возможное применение *ldConvertTags() -- преобразование типов массивов (VT_ARRAY|*), поскольку функция VariantChangeType() массивы преобразовывать не умеет.
Весь мир ныне помешан на безопасности и защите. Компутерный мир тоже.
Управление доступом предусматривает наличие авторизационной базы данных (ACL), содержащей списки объектов, субъектов и прав на обращение последних к первым. Однако, ведение такой базы данных определённо не является функцией OPC сервера. Кроме того, детали реализации авторизационной базы данных существенно зависят от требований конкретного применения.
Поэтому LightOPC предусматривает возможность обращений к внешним ACL, предоставляя встроенную реализацию лишь простейших средств управления доступом.
Имеется несколько способов контролировать доступ клиентов к тегам.
int loClientAccessMode(loService *, loClient *, int accmode);
| loAM_RDWR | разрешить чтение и запись (действует по умолчанию) |
| loAM_RDONLY_OP | запретить запись |
| loAM_RDONLY_ADD | сбрасывать OPC_WRITEABLE в полях dwAccessRights структур OPCITEMRESULT (IOPCGroup::AddItem() / ValidateItem()) и OPCITEMATTRIBUTES (IEnumOPCItemAttributes) |
| loAM_RDONLY_BROWSE | сбрасывать OPC_WRITEABLE для IOPCBrowseServerAddrssSpace |
| loAM_NOREAD_DEV | заменять чтение OPC_DS_DEVICE чтением OPC_DS_CACHE |
| loAM_ERREAD_DEV | генерировать ошибку при попытках чтения OPC_DS_DEVICE |
| 0 | - всё успешно; |
|---|---|
| EBADF | - недействительный loService; |
| EINVAL | - недопустимый accmode; |
| ENOENT | - не найден loClient. |
Функция может быть вызвана в любое время и, в отличие от проверки полномочий в *ldWriteTags() и *ldReadTags(), позволяет влиять на IEnumOPCItemAttributes и IOPCBrowseServerAddrssSpace.
const char *loClientName(loClient *);
Согласно стандарту, имя клиента может быть использовано в отладочных целях. Поэтому имя может быть искажено библиотекой LightOPC следующими способами:
void *loClientArg(loClient *);
void *loDriverArg(loService *);
int loGetBandwidth(loService *, loClient *cli);
Возможен возврат -1 -- в случае ошибки, или если текущий процент не может быть вычислен.
Значение текущего процента вычисляется путём усреднения отдельных мгновенных значений. Постоянная времени интеграции порядка 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);
Точность -- около 1 мкс, что заведомо лучше чем 1 с SystemTimeToVariantTime().
HRESULT lo_variant_changetype_array(VARIANT *dst, VARIANT *src,
LCID lcid,
unsigned short flags,
VARTYPE vt);
lo_variant_changetype_array()
lo_variant_to_filetime()
lo_statperiod()
Никогда не любил писать заключения...
Впрочем, и такое заключение придаёт оглавлению изрядное сходство с известным опусом.
Вы, конечно, догадались с каким?