UniSet  2.9.2
Генератор кода uniset-codegen

Общее описание утилиты uniset-codegen

Утилита uniset-codegen предназначена для генерирования "скелета" процесса управления на основе заданного xml-описания. В скелет процесса включается "рутинный" код по получению и проверке идентификаторов, переменных, обработке сообщений и ряд других вспомогательных функций. Помимо этого определяются базовые виртуальные функции, участвующие в процессе.

Процесс можно сгенерировать по нескольким шаблонам

Заметки
Генерирование на основе отдельного xml-файла удобно если вам необходимо создавать несколько объектов вашего класса, отличающихся только набором датчиков.
Генерирование на основе кофигурационного файла проекта (шаблон 'Alone') удобно использовать когда у вас планируется один единственный объект вашего класса. Т.к. в данном случае привязка производиться непосредственно к конкретным датчикам в конфигурационном файле.

Генерируемый процесс функционирует по следующему обобщённому алгоритму:

  • Обновление входов (в случае работы на основе заказа датчиков, этот шаг отсутствует)
  • проверка срабатывания внутренних таймеров
  • Обработка сообщений в очереди сообщений
  • Выполнение шага алгоритма (функция step)
  • обновление датчика "сердцебиения" /см. Слежение за "живостью" объектов ("сердцебиение") /
  • Обновление выходов
Предупреждения
По умолчанию, генерируется скелет для "пассивного процесса" (см. Типы процессов (активный и пассивный)), в котором "ВСЕ "выходы"(out_xxx) перезаписывают свои значения в SharedMemory(SM) только по изменению. Поэтому если в вашем процессе вы не меняли переменную out_xxx, а какой-то другой процесс обновил значение в SM, то значение в SM так и останется и не будет перезаписано Вашим процессом, пока он не обновит у себя значение out_xxx. Т.е. ПО УМОЛЧАНИЮ СЧИТАЕТСЯ (в целях оптимизации и уменьшения количества "удалённых вызовов"), что каждый процесс является ЕДИНОЛИЧНЫМ ВЛАДЕЛЬЦЕМ своих ВЫХОДОВ и никто кроме него не может их "обновлять" в SM. Для включения перезаписывания out_xxx на каждом шаге, можно воспользоваться ключом forceOut="1" в настроечной секции процесса. Или указать force="1" для конкретного выхода в src.xml (см. Секция <smap> ).

Помимо этого обрабатывается специальный режим: Специальный режим "тест" (TestMode)

Сам сгенерированный код представляет из себя класс ("_SK" - skeleton), который необходимо использовать как базовый класс для своего процесса. Переопределяя виртуальные функции, и реализуя в них необходимую функциональность.

Файл описания для генерирования базового класса

Исходный xml-файл описания необходимый для генерирования выглядит следующим образом.

<?xml version="1.0" encoding="utf-8"?>
<!--
name - название класса
msgcount - сколько сообщений обрабатывается за один раз
sleep_msec - пауза между итерациями в работе процесса
type
====
in - входные регистры (только для чтения)
out - выходные регистры (запись)
-->
<Test>
<settings>
<set name="class-name" val="TestGen"/>
<set name="msg-count" val="20"/>
<set name="sleep-msec" val="150"/>
<set name="logrotate" val="append"/>
</settings>
<variables>
<!-- type = [int,str,bool,float]
int: max,min,no_range_exception=[0,1]
str:
float: max,min,no_range_exception=[0,1]
bool:
min - минимальное значение (может быть не задано)
max - максимальное значение (может быть не задано)
default - значение по умолчанию (может быть не задано)
no_range_exception=1 - при выходе за границы min или max только писать unideb[WARN].
-->
<item name="startTimeout" type="int" min="0" comment="test int variable"/>
<item name="stopTimeout" type="int" max="100" default="110" no_range_exception="1"/>
<item name="test_float" type="float" max="100.0" default="50.0" public="1" const="1" />
<item name="test_bool" type="bool" />
<item name="test_str" type="str" default="ddd"/>
</variables>
<smap>
<!-- name - название переменной в конф. файле -->
<item name="input1_s" vartype="in" iotype="DI" comment="comment for input1"/>
<item name="input2_s" vartype="in" iotype="DI" comment="comment for input2" />
<item name="output1_c" vartype="out" iotype="DO" omment="comment for output1" no_check_id="1"/>
<item name="output2_c" vartype="out" iotype="DO" omment="comment for output2" force="1"/>
</smap>
<msgmap>
<item name="msg1" comment="comment for Message 1" />
</msgmap>
</Test>

Секция <settings>

В секции <settings> описываются следующие параметры:

  • class-name - Имя генерируемого класса. В итоге будет сгенерирован класс с названием "ClassName_SK" ("_SK" - сокращение от "skeleton").
  • msg-count - Количество обрабатываемых за один раз(шаг) сообщений.
  • sleep-msec - пауза между шагами основного цикла процесса
  • base-class - Имя базового класса. По умолчанию: UniSetObject

Секция <smap>

В секции <smap> описываются "входы" и "выходы" процесса связанные с датчиками. При генерировании процесса, для каждого входа или выхода генерируется ряд свойств:

  • name - идентификатор датчика связанного с этим входом или выходом (совпадает с именем указанной переменной)
  • node_name - идентификатор узла датчика связанного с этим входом или выходом (по умолчанию локальный узел)
  • [in |out |io]_name - переменная хранящая текущее состояние датчика (генерируется с префиксом в зависимости от vartype)
  • prev_[in |out |io]_name - переменная хранящая состояние датчика на предыдущем шаге (генерируется с префиксом в зависимости от vartype)
  • no_check_id - no_check_id="1" означает игнорировать (не генерировать исключение) при запуске процесса, если идентификатор датчика не найден.
  • force="1" - принудительно перезаписывать значение в SharedMemory на каждом шаге. Действует только для vartype="out"
  • loglevel="1" - признак того, что данный датчик управляет логами для этого процесса. См. Управление логами объекта (процесса) через специальный датчик.
  • initFromSM="1" - инициализировать значение из SM при старте (точнее при получении StartUp). Действует и на "in" и на "out".
Заметки
Правильнее указывать и \i необязательное поле iotype, которое должно совпадать с типом датчика к которому будет привязана данная переменная. Это позволит утилите uniset-linkeditor отслеживать правильность типов датчиков при "привязке".

Секция <msgmap>

В секции <msgmap> описываются поля связанные с идентификаторами сообщений. По сути, сообщения это тоже датчики, только используемые специальным образом. Для посылки сообщения датчик выставляется в "1" и через некоторое время должен быть сброшен. (чтобы можно было опять послать тоже самое сообщение). Т.е. само событие "сообщение" это переход датчика "0" --> "1".

Предупреждения
В сгенерированном коде реализован "автоматический" сброс сообщения через resetMsgTime миллисекунд. resetMsgTime настраивается через конфигурационную секцию (см. Конфигурирование ). Следует иметь ввиду, что это время должно быть достаточным чтобы датчик (изменение "0"-->"1") успел быть переданным по сети на другие узлы (зависит от используемого протокола передачи). Либо можно переопределить значение аргументом командной строки –argprefix-resetMsgTime msec, либо задать в файле описания в секции <settings>
<settings>
...
<set name="resetMsgTime" val="500"/>
</settings>
Для сообщений генерируется такой же набор "переменных" как и для полей указанных в <smap> (см. Секция <smap>). За исключением того, что генерируется имя с префиксом mid_. И "привязка" идентификаторов не является обязательной.

Для работы с сообщениями существует ряд правил:

  • сообщения должны посылаться при помощи специальной (сгенерированной) функции setMsg( UniSetTypes::ObjectId code, bool state ). Для передачи сообщения необходим вызов c параметром state=true.
  • Сообщения "автоматически" сбрасываются в "0" через resetMsgTime (настраиваемое в конф. секции), поэтому вызывать функции с state=false нет смысла. Если указать resetMsgTime <=0 - автоматический сброс происходить не будет, и разработчик должен самостоятельно заботиться об этом.

Секция <variables>

В данной секции можно перечислить переменные разных типов, для которых будет сгенерирован код по их "инициализации" и проверке "диапазона"(если указаны поля min или max). На данный момент поддерживаются переменные следующих типов:

  • int - int
  • long - long
  • float - float
  • double - double
  • bool - bool
  • str - std::string
  • sensor - sensor // ObjectId инициализируемый как conf->getSensorID(name)
  • object - object // ObjectId инициализируемый как conf->getObjectID(name)

Так же доступны следующие необязательные вспомогательные поля:

  • min - минимальное разрешенное значение
  • max - максимальное разрешенное значение
  • default - значение по умолчанию (при инициализации)
  • no_range_exception - не генерировать исключение в случае выхода переменной за указанный диапазон (min или max).
  • const - const="1" - сгенерировать как константу.
  • private | public | protected - Область видимости. По умолчанию: protected.
  • no_vmonit="1" - отключение "удалённого контроля за переменной". См. Мониторинг состояния внутренних переменных.

В генерируемом коде для каждой переменной происходит её инициализация по следующему шаблону (псевдокод):

varname = conf->getArgParam("--'arg_prefix'varname'",it.getProp("'varname'"));
if( varname.empty() )
varname = 'default'

Где it.getProp() - получение значения из соответствующей настроечной секции в конфигурационном файле (см. Конфигурирование). Из кода можно видеть, что приоритетным является аргумент командной строки, потом значение из конф. файла и только потом default.

Если указаны поля min или max происходит проверка значения (после инициализации) на соответствие указанному диапазону. По умолчанию, при выходе за диапазон, генерируется исключение. Но если указано no_range_exception="1", то просто выдаётся warning в unideb[Debug::WARN].

По умолчанию эти поля генерируются как protected. Но если есть необходимость, то можно указать свойство public="1" или private="1" и тогда они будут иметь соответствующую область видимости.

Аргументы командной строки

Для возможности переопределять различные параметры при помощи аргументов командной строки, сделан следующий механизм. В сгенерированном конструкторе (файл xx_SK.h) можно задать последний аргумент argprefix. Если он равен "", то в качестве префикса используется ObjectName. Все переменные из секции <variables> можно переопределять аргументом –prefix-varname val. В общем случае это будет: –ObjectName-varname val.

Помимо этого argprefix можно задать в секции <settings>

<settings>
...
<set name="arg-prefix" val="test-"/>
...
</settings>

Тогда вместо ObjectName будет использоваться указанный префикс –test- (обратите внимание на то, что префикс задаётся с - в конце).

Значения по умолчанию для входов (in_xxx) и выходов (out_xx) можно также определять через аргументы командной строки:

  • –argprefix-xxxname-default val

Дополнительно при помощи командной строки можно переопределять следующие свойства:

  • –argprefix-force-out 1/0
  • –argprefix-heartbeat-id sensorID
  • –argprefix-heartbeat-time msec
  • –argprefix-heartbeat-max num
  • –argprefix-sleep-msec msec
  • –argprefix-resetMsgTime msec
  • –argprefix-sm-test-id sensorID
  • –argprefix-activate-timeout msec
  • –argprefix-startup-timeout msec
  • –argprefix-log-[add|del]-levels xxx,xxx,xxx

Конфигурирование

Для режима генерирования на основе отдельного xml-файла (Файл описания для генерирования базового класса) необходимо дополнительно производить конфигурирование. Конфигурирование - это привязка конкретных имён датчиков к указанным полям класса. Для этого в конфигурационном файле проекта (обычно в секции <settings>) создаётся настроечная секция для вашего объекта. А в самом классе генерируется специальный конструктор, позволяющий указать настроечный xml-узел:

ClassName( UniSetTypes::ObjectId id, xmlNode* node=UniSetTypes::conf->getNode("ClassName") );

Ниже приведён пример настроечной секции, для объекта сгенерированного на основе xml-файла указанного в Файл описания для генерирования базового класса

...
<TestGen name="TestGen" startTimeout="4000" stopTimeout="2000"
input1_s="Input1_S" node_input1_s="Node2"
input2_s="DumpSensor1_S"
output1_c="DO_C"
msg1="Message1"
/>
...

Обычно для каждого объекта класса создаётся своя настроечная секция.

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

  • sleep_msec = conf->getArgPInt("--sleep-msec","150", 150); - пауза между шагами основного цикла процесса
  • resetMsgTime = conf->getPIntProp(cnode,"resetMsgTime", 2000); - время до автоматического сброса датчиков-сообщений в "0".
  • smReadyTimeout = conf->getArgInt("--sm-ready-timeout",""); - время ожидания готовности SharedMemory к работе
  • activateTimeout = conf->getArgPInt("--activate-timeout", 20000); - время отведённое на инициализацию процесса
  • msec = conf->getArgPInt("--startup-timeout", 10000); - пауза, в течение которой игнорируется сообщение SystemMessage::WatchDog. В случае если они приходят подряд.

Специальный режим "тест" (TestMode)

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

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

  • TestMode_S - глобальный датчик перехода в тестовый режим. Обязан быть в конфигурационном файле.
  • LocalTestMode_S - датчик перевода в тестовый режим для данного процесса. Задаётся в настроечной секции данного процесса.

Переход в тестовый режим осуществляется, только если ОБА датчика станут равными "1".

Заметки
Два датчика сделаны для защиты от "случайного" перехода.

Типы процессов (активный и пассивный)

uniset-codegen поддерживает генерирование двух видов процессов:

  • пассивный процесс - (по умолчанию). Основан на "заказе датчиков".
  • активный процесс - на каждом шаге опрашивает свои "входы".

    По умолчанию используется шаблон для "пассивного процесса". Т.е. все "in" датчики будут "заказаны" при старте процесса и далее работа будет вестись по сообщениям об изменении (UniSetTypes::SensorMessage).

Для генерирования активного процесса необходимо использовать параметр –no-ask. В таком процессе происходит активная работа с датчиками. Т.е. на каждом шаге основного цикла, происходит "принудительное" обновление значений всех "входов" (getValue) независимо от того, менялись ли они.

Предупреждения
Следует иметь ввиду, что при работе не на основе "заказа датчиков", существует вероятность пропустить(потерять) "изменение" состояния датчика, в случае если он поменяется и восстановится обратно в течение времени меньше чем sleep-time у данного процесса. А также такой процесс потребляет больше процессорного времени, т.к. постоянно опрашивает "входы" независимо от того, меняли ли они своё состояние.

Настройка ротации логов

В разделе '<settings>' можно задавать параметр logrotate=[truncate|append]. Он определяет как будет обработана команда SystemMessage::Logrotate, по которой происходит переоткрытие логфайла (если включено ведение логфайла).

  • truncate - при переоткрытии "обрезать" файл
  • append - при переоткрытии продолжить писать логи в конец файла

Режим append используется по умолчанию. Он удобен для использования совместно с внешней системой ротации логов, например logrotate. Которая после проведения ротации может обрезать файл и посылать ВСЕМ процессам команду LogRotate по которой файл логов будет переоткрыт, если он был заархивирован. При этом те процессы чей файл не был заархивирован, продолжат писать дальше в конец логфайла.

Шаблон 'Alone'

Шаблон "Alone" предназначен для генерирования без использования специального xml-файла с описанием переменных. Генерирование происходит непосредственно по конфигурационному файлу проекта. Для этого всё-равно необходимо создать соответствующую настроечную секцию, в которой будут прописаны параметры необходимые для генерирования "SK"-класса. При этом используемые "входы" и "выходы" записываются непосредственно у каждого используемого датчика в секции <consumers>. Ниже приведён пример конфигурирования процесса файле проекта:

...
<settings>
...
<TestGenAlone name="TestGenAlone">
<set name="ID" val="TestGenAlone"/>
<set name="class-name" val="TestGenAlone"/>
<set name="msg-count" val="20"/>
<set name="sleep-msec" val="150"/>
...
</settings>
...
<sensors>
<item id="1" name="input1_s" iotype="DI" textname="xxx">
<consumers>
<consumer name="TestGenAlone" vartype="in" type="objects"/>
</consumers>
</item>
<item id="23" name="input2_s" iotype="DI" textname="xxx">
<consumers>
<consumer name="TestGenAlone" vartype="in" type="objects"/>
</consumers>
</item>
<item id="31" name="output1_c" iotype="DO" textname="xxx" node="Test1Node">
<consumers>
<consumer name="TestGenAlone" vartype="out" type="objects"/>
</consumers>
</item>
</sensors>
<objects>
<item id="2000" name="TestGenAlone" />
</objects>

Как видно из примера, vartype переменных записывается непосредственно в свойствах <consumer>.

Предупреждения
* Следует иметь ввиду, что при изменении конфигурационного файла, необходимо перегенерировать код. И в свою очередь, если поставить в Makefile зависимость на конфигурационный файл, то каждый раз при его изменении (независимо от того, что менялось), код будет перегенерироваться.
* Шаблон 'alone' не рассчитан на создание "многих" объектов сгенерированного класса, т.к. они будут работать, с одним и тем же набором датчиков. Сложно представить себе пример, где бы это могло потребоваться. Более того, следует иметь ввиду, что создание нескольких объектов класса приведёт к конфликту по выставлению "out"-переменных.

Параметры генерирования кода

Типичное правило для генерирования в Makefile.am выглядит следующим образом:

..._SOURCES= MyClass_SK.cc ...
MyClass_SK.cc: myclass.src.xml
@UNISET_CODEGEN@ -n MyClass --no-main myclass.src.xml

В этом примере

  • myclass.src.xml - это файл с описанием переменных
  • –no-main - отключает генерирование "запускающего" файла (функция main)
  • -n - определяет название файлов для сгенерированного класса. В данном случае будут сгенерированы MyClass_SK.h и MyClass_SK.cc

Логи

Для логирования событий каждый класс содержит объект mylog (класса DebugStream) и несколько предопределённых макросов для удобства использования.

  • myinfo - info
  • mywarn - warn
  • mycrit - crit
  • mylog1..mylog9 - вывод в уровни level1....level9
  • mylogany - any

Так же можно использовать функции напрямую:

mylog->info() << "....test..." << endl;
mylog->log1() << "....test..." << endl;

Помимо объекта mylog класс содержит ряд вспомогательных функций:

  • string dumpIO() - вывод состояния всех входов и выходов в строку, в виде:
    ObjectName:
    in_input1_s(Sensor1_S)=1
    in_output2_c(Output1_C)=0
    ...
  • string str(ObjectId,showLinkName) - вывод названия указанного входа или выхода в формате "in_input1_s(Sensor1_S)". Если showLinkName=false, то будет сформирована строка "in_input1_s". Пример использования:
    ..
    myinfo << str(input1_s) << endl;
  • string strval(ObjectId,showLinkName) - вывод названия и текущего значения указанного входа или выхода в формате "in_input1_s(Sensor1_S)=1". Если showLinkName=false, то будет сформирована строка "in_input1_s=1". Пример использования:
    ..
    myinfo << strval(input1_s) << endl;

Управление логами объекта (процесса) через специальный датчик

Для динамического управления логами процесса предусмотрен механизм, который позволяет указать аналоговый датчик (AI), значение которого будет использовано как указание уровня вывода логов (см. Debug::type). Чтобы назначить специальный "вход" (vartype="in") для управления логами, достаточно в секции <smap> ТОЛЬКО ДЛЯ ОДНОГО входа указать loglevel="1". Пример:

<smap>
...
<item name="loglevel_s" vartype="in" comment="log level control" loglevel="1"/>
</smap>

Данный механизм позволяет посредством этого датчика управлять уровнем вывода логов (mylog) во время работы процесса. Для удобства предусмотрена утилита uniset2-log2val позволяющая преобразовать текстовые названия уровней в число, которое необходимо выставить датчику. Пример:

uniset2-log2val info,level1,warn,level9
4117

Т.е. для того, чтобы включить указанные логи в датчик нужно записать число 4117

Удалённое управление логами

Для возможности удалённого управления логами (включением, отключением, просмотром, и т.п.) в каждом объекте предусмотрена возможность запуска LogServer, который запускается на порту равном ID-объекта. По умолчанию LogServer не запускается (выключен). Для его запуска необходимо указать:

--argprefix-run-logserver

Дополнительно доступны следующие аргументы:

--argprefix-logserver-host - по умолчанию localhost
--argprefix-logserver-port - по умолчанию ID-объекта

Непосредственно управление логами производиться при помощи утилиты uniset2-log

-h, --help - this message
-v, --verbose - Print all messages to stdout
[-i|--iaddr] addr - LogServer ip or hostname.
[-p|--port] port - LogServer port.
[-c|--command-only] - Send command and break. (No read logs).
[-w|--timeout] msec - Timeout for wait data. Default: 0 - endless waiting
[-x|--reconnect-delay] msec - Pause for repeat connect to LogServer. Default: 5000 msec.
Commands:
[--add | -a] info,warn,crit,... [logfilter] - Add log levels.
[--del | -d] info,warn,crit,... [logfilter] - Delete log levels.
[--set | -s] info,warn,crit,... [logfilter] - Set log levels.
--off, -o [logfilter] - Off the write log file (if enabled).
--on, -e [logfilter] - On(enable) the write log file (if before disabled).
--rotate, -r [logfilter] - rotate log file.
--list, -l [logfilter] - List of managed logs.
--filter, -f logfilter - ('filter mode'). View log only from 'logfilter'(regexp)
Note: 'logfilter' - regexp for name of log. Default: ALL logs.

Мониторинг состояния внутренних переменных

В генерируемом коде встроена поддержка удалённого просмотра (мониторинга) внутреннего состояния ВСЕХ in_xx и out_xxx переменных, а также переменных определённых пользователем. Для этого достаточно использовать утилиту uniset2-vmonit

Usage: uniset2-vmonit [-s watch_sec] OBJECT_ID[@node] [uniset-admin-options]

Где можно указать (-s sec) как часто обновлять информацию и собственно указать ID (или name) объекта (процесса).

Утилита требует наличия в системе утилиты watch и на самом деле является обёрткой над утилитой uniset2-admin.

Например для отслеживания процесса 12000 утилита запускается так:

uniset2-vmonit 12000
Заметки
При локальной наладке обычно запуск производиться так: uniset2-start.sh -f uniset2-vmonit ObjectID

Добавление своих переменных в "мониторинг".

Для добавления своей переменной в "мониторинг" достаточно один раз (например в конструкторе своего класса) вызвать функцию (на самом деле макрос) vmonit(var); Пример:

class MyClass:
public MyClass_SK:
{
public:
MyClass(...)
{
vmonit(my_bool);
vmonit(my_int);
vmonit(my_long);
vmonit(my_double);
vmonit(my_float);
}
private:
bool my_bool;
int my_int;
long my_long;
double my_double;
float my_float;
...
}

После этого они появятся в выводе утилиты uniset2-vmonit

Предупреждения
На данный момент поддерживаются только простые типы переменных (bool,short,int,long,double,float см. VMonitor)

Для пользовательской информации введена виртуальная функция std::string getMonitInfo(), переопределив которую, можно сформировать свою информацию, которую можно будет удалённо читать.

HTTP API

Все обращения к api должны быть по такому пути /api/VERSION/command?params..

/help - Получение списка доступных команд
/ - Получение стандартной информации
/params/get?param1&param2 - Получить текущее значение указанных параметров. Если параметры не указаны, будут возвращены все доступные.
/params/set?param1=val1&param2=val2&.. - Установить значения для указанных параметров.
TestGenAlone
Definition: TestGenAlone.h:7
uniset
Definition: CommonEventLoop.h:14
TestGen
Definition: TestGen.h:7