В предыдущих частях (часть 1, часть 2, часть 3) было описано создание двух процессов: имитатора и процесса управления… Теперь же настало время наладки.
Итак, на текущий момент у нас уже реализованы и запускаются следующие процессы:
можно начинать наладку…
Пришло время небольшой поясняющей картинки, чтобы понять что у нас тут происходит…
На самом деле всё конечно немного сложнее, но рисунок призван помочь понять, как у нас устроены «информационные потоки».
Итак..
Раз всё взаимодействие происходит через датчики, то наладка, в целом, это «выставление датчиков» и «отслеживание текущего состояния датчиков». Для этих целей в libuniset2-utils входит несколько утилит:
Все эти утилиты активно используются в наладке, но это инструменты отслеживания «внешней» жизни процессов. А есть ещё два дополнительных механизма, которые позволяют наблюдать «жизнь процессов» изнутри:
Конечно можно было бы начать наладку, запустив сразу два процесса, и смотреть, что там происходит. Но правильнее, если это возможно, проводить наладку процессов отдельно. Давайте начнём с имитатора.
Проверяем, что объекты доступны. Заходим в src/Services/Administrator/ и запускаем ./exist
Должны увидеть следующее
Теперь возвращаемся к началу и вспоминаем, что же должен делать имитатор.
Имитатор должен по приходу команды cmdLoad_C=1 начать имитировать наполнение цистерны (рост датчика Level_AS), а по приходу команды cmdUnload_C=1 — имитировать опустошение цистерны (уменьшать датчик Level_AS).
А значит мы должны
Давайте посмотрим вообще текущее состояние датчиков. Воспользуемся утилитой uniset2-smviewer.
Входим в каталог src/Services/SMViewer и запускаем ./start_fg.sh
Видим это…
Как видно, всё по нулям… Конечно же в реальном проекте датчиков будет ОЧЕНЬ много, и поэтому можно (и нужно) пользоваться uniset2-smviewer вместе с grep если хочется как-то фильтровать вывод…
Вторая утилита, которая нам понадобится — это uniset2-smonit, чтобы посмотреть, как датчик уровня будет меняться. Давайте запустим её. Заходим в src/Services/SMonit/ и…
Для этой утилиты нужно указать, за каким датчиками мы хотим следить, поэтому у неё есть ключик --sid
(для простоты он вписан сразу в start_fg.sh). В качестве параметра --sid можно указать идентификатор, а можно указать имя датчика. Мы укажем имя Level_AS.
smonit запускается и висит ждёт изменений. При этом в начале выводится текущее состояние датчика. Из вывода можно увидеть название, время последнего изменения (включая микросекунды), идентификатор процесса, который сохранил этот датчик в SM (в данном случае это Imitator1), и текущее значение value (и в виде float — fvalue).
Всё вроде бы готово, выставляем датчик cmdLoad_C=1 и смотрим, как побежал меняться датчик Level_AS.
Для выставления как раз воспользуемся admin-ом.
и переключившись в консоль, где у нас запущен smonit, смотрим как побежал датчик (от 0 до 100).
Значит, увеличение работает. Уменьшение проверяется аналогично… предварительно надо не забыть сбросить предыдущую команду, и мы сделаем это «одним махом»
smonit побежал в обратную сторону (от 100 до 0)
Теперь я опишу механизм, который позволяет посмотреть внутренние переменные объекта. Вообще, всё очень просто. Зная идентификатор или имя объекта, можно просто запросить у него информацию.
Итак, у нас всё запущено и работает. Давайте посмотрим, что объект Imitator1 нам покажет.
Заходим в src/Services/Administrator и запускаем команду ./oinfo Imitator1
Как видно из вывода, команда oinfo позволяет увидеть
О пользовательской информации скажу немного подробнее…
У каждого объекта (точнее у скелета класса) есть специальная функция
переопределив которую, можно выводить свою информацию, в виде текста (строки). В данном случае имитатор, например, пишет «Текущий режим работы: наполняем» (или «опустошаем»). Можно писать и что-то более сложное.
Конечно, когда Вы пишете свой процесс управления, скорее всего у Вас будут, помимо переменных объявленных в xml-файле, ещё какие-то свои поля класса. И конечно захочется так же следить (выводить информацию) о текущем состоянии и своих переменных. Нет ничего проще. Допустим в имитатор мы добавим два счётчика команд.
Тогда, если вы хотите увидеть их в выводе oinfo, просто в конструкторе сделаем два волшебных вызова:
Т.е. просто обернули свои переменные в vmonit(xxx). Тут конечно не обошлось без макросной магии, но наверно это не сильно напрягает…
В итоге на экране мы уже увидим и наши переменные (затесавшиеся среди прочих).
ВАЖНО: поддерживаются пока только стандартные простые типы: bool,int,long и т.п., для всего остального есть универсальная функция getMonitInfo()
Как известно, сколько бы механизмов отладки ни существовало, а любимый cout(или же не любимый printf) всё равно будет использован. Ну что ж, libuniset предоставляет и этот способ. На самом деле, тема очень обширная, если раскрывать все возможности и детали, то это тема для отдельной статьи. Поэтому я покажу применение и расскажу немного деталей…
В сгенерированном скелете класса, есть специальный объект для логов — log. Он по сути имеет интерфейс как cout, только это shared_ptr, поэтому пользоваться им нужно как указателем. Например
или
У лога есть 15 уровней, включать их можно «параллельно» (т.е. например info,warn,crit), ему можно указать файл, куда писать логи, можно включать и отключать вывод даты и времени в начале каждой строки и т.п. Вообщем много стандартных возможностей. Для каждого объекта можно включать или отключать логи просто указав при запуске аргумент командной строки
Но всё это было бы не так интересно, если бы не наличие такого механизма, как LogServer. В каждом объекте есть встроенный LogServer, который по умолчанию не запускается, соответственно ресурсов не потребляет и вообще не виден никак. Но простым аргументом командной строки
мы можем его активировать. По умолчанию он запускается на localhost, а в качестве порта использует идентификатор объекта. Но можно и принудительно указать host и port запуска.
После того как у объекта запущен LogServer, мы можем читать его логи, причём удалённо. Просто подключившись по указанному хосту и порту. Для чтения логов существует специальная утилита uniset2-log. При помощи неё можно помимо чтения логов, так же и управлять уровнем вывода логов, запись в файл и т.п., т.е. осуществлять полный контроль над логами объекта. Это очень удобный механизм, т.к. позволяет включать и отключать логи без перезапуска программы (а ведь часто нельзя остановить процесс, но очень нужно посмотреть, что там внутри происходит).
… давайте просто я покажу...
Итак, у нас всё запущено, причём мы добавили в start_fg.sh имитатора строчку
Кстати в выводе ./oinfo, если кто не заметил, выводится информация о том, запущен ли LogServer. Но давайте я покажу ещё раз (зайдём в каталог src/Services/Administator/ и запустим команду ./oinfo Imitator1).
Итак logserver запущен на localhost и порт 20001. Но по умолчанию (если, конечно, разработчик их принудительно не включит), логи отключены. Соответственно, мы не просто подключимся, а сразу ещё и включим все(any) логи, чтобы сразу начать их видеть. Давайте подключимся (добавим ключик -v, чтобы увидеть отладочную информацию о том, к кому мы подключаемся)
Я добавил в функцию таймера лог (уровень 3), для вывода, чтобы продемонстрировать работу.
Тогда подключаемся (как указано выше) и в другой консоли (заходим в src/Services/Administrator) выставляем команду
А вот что мы увидим в логах (удалённо читаемых)
Пользуясь возможностью удалённо читать логи, важно не забывать, что если вы их включили, то неплохо бы и уходя, отключать, потому что в реальной системе логи не должны работать, ввод/вывод — дорогое удовольствие. И ещё, я немного укажу возможности, которые не раскрыл ранее
В реальном применении LogServer конечно запускается один на все объекты (в рамках одного запускаемого файла), которые при этом выстраиваются в иерархию при помощи LogAgregator и можно ими гибко управлять.
Каждый раз рассказывая о каких-то механизмах, я пытаюсь соблюсти баланс между кучей подробностей внутреннего функционирования и простотой внешнего применения. Т.е. «вот готовые команды, берите и пользуйтесь — они работают из коробки». Поэтому я многое не рассказал, и может что-то осталось не очевидным… Постараюсь ответить на ваши вопросы.
В целом, если не считать, что нормальное тестирование — это гораздо больше всяких «тестов» (граничные случаи, задание max, min, одновременное выставление команд и т.п.), то с наладкой имитатора мы закончили. Главное было продемонстрировать инструменты для наладки, входящие в libuniset2:
Примерно таким же способом можно наладить и работу нашего процесса управления, но лучше я покажу наладку алгоритма управления, на примере использования более продвинутого способа наладки — написания функциональных тестов с использованием uniset2-testsuite. Об этом в следующей части...
Ну и в конце как обычно ссылочки:
Итак, на текущий момент у нас уже реализованы и запускаются следующие процессы:
можно начинать наладку…
Наладка. Вводная часть.
Пришло время небольшой поясняющей картинки, чтобы понять что у нас тут происходит…
На самом деле всё конечно немного сложнее, но рисунок призван помочь понять, как у нас устроены «информационные потоки».
Итак..
- Всё взаимодействие идёт через SharedMemory
- Процесс управления получает сохраняет в SM команды, а от SM получает уведомления об изменении датчика уровня(Level_s)
- Имитатор управления получает от SM уведомления об изменении команд, а в SM сохраняет имитируемое состояние уровня
- Всё взаимодействие идёт через датчики
Раз всё взаимодействие происходит через датчики, то наладка, в целом, это «выставление датчиков» и «отслеживание текущего состояния датчиков». Для этих целей в libuniset2-utils входит несколько утилит:
- uniset2-admin — это многофункциональная утилита, но в данном случае позволяет ещё и выставлять (setValue) и смотреть текущее состояние датчиков (getValue)
- uniset2-smviewer — утилита, позволяющая посмотреть сразу состояние всех датчиков, зарегистрированных в SM
- uniset2-smonit — утилита, «следящая» (мониторинг) за изменением указанных датчиков
Все эти утилиты активно используются в наладке, но это инструменты отслеживания «внешней» жизни процессов. А есть ещё два дополнительных механизма, которые позволяют наблюдать «жизнь процессов» изнутри:
- vmonit — мониторинг внутренних переменных объекта
- LogServer — удалённое чтение логов процесса
Конечно можно было бы начать наладку, запустив сразу два процесса, и смотреть, что там происходит. Но правильнее, если это возможно, проводить наладку процессов отдельно. Давайте начнём с имитатора.
Отладка работы имитатора
- Запускаем SM — Входим в каталог src/Services/SharedMemory/. Запускаем скрипт start_fg.sh
- Запускаем имитатор — Входим в каталог src/Algorithms/Imitator/. Запускаем скрипт start_fg.sh
Проверяем, что объекты доступны. Заходим в src/Services/Administrator/ и запускаем ./exist
Должны увидеть следующее
Вывод на экране
[pv@pvbook Administrator]$ ./exist
||=======******** UNISET-EXAMPLE/Services ********=========||
пусто!!!!!!
||=======******** UNISET-EXAMPLE/Controllers ********=========||
(22000 )SharedMemory1 <--- exist ok
||=======******** UNISET-EXAMPLE/Objects ********=========||
(20001 )Imitator1 <--- exist ok
[pv@pvbook Administrator]$
Теперь возвращаемся к началу и вспоминаем, что же должен делать имитатор.
Имитатор должен по приходу команды cmdLoad_C=1 начать имитировать наполнение цистерны (рост датчика Level_AS), а по приходу команды cmdUnload_C=1 — имитировать опустошение цистерны (уменьшать датчик Level_AS).
А значит мы должны
- выставить датчик cmdLoad_C=1 и увидеть нарастание Level_AS
- выставить датчик cmdUnload_C=1 и увидеть уменьшение Level_AS
Давайте посмотрим вообще текущее состояние датчиков. Воспользуемся утилитой uniset2-smviewer.
Входим в каталог src/Services/SMViewer и запускаем ./start_fg.sh
Видим это…
Вывод на экране
[pv@pvbook SMViewer]$ ./start_fg.sh
======================================================
SharedMemory1 Датчики
------------------------------------------------------
( 101) | AI | Level_AS | 0
( 100) | DI | OnControl_S | 0
------------------------------------------------------
======================================================
SharedMemory1 Выходы
------------------------------------------------------
( 103) | DO | CmdUnload_C | 0
( 102) | DO | CmdLoad_C | 0
------------------------------------------------------
======================================================
SharedMemory1 Пороговые датчики
------------------------------------------------------
Как видно, всё по нулям… Конечно же в реальном проекте датчиков будет ОЧЕНЬ много, и поэтому можно (и нужно) пользоваться uniset2-smviewer вместе с grep если хочется как-то фильтровать вывод…
Вторая утилита, которая нам понадобится — это uniset2-smonit, чтобы посмотреть, как датчик уровня будет меняться. Давайте запустим её. Заходим в src/Services/SMonit/ и…
Небольшая тонкость при использовании smonit
Т.к. uniset2-smonit запускается и отслеживает изменение указанных датчиков, он должен иметь «обратный адрес». Для uniset-системы, таким адресом является идентификатор. По умолчанию, uniset2-smonit пытается запускаться с именем TestProc. Т.е. подразумевается, что в configure.xml в секции objects объявлен объект с name=«TestProc». Но если по каким-то причинам в вашем проекте не хочется иметь такой объект, то uniset2-smonit можно запустить с параметром --name XXX и указать любое имя из существующих в проекте объектов (не задействованных в текущий момент).
Для этой утилиты нужно указать, за каким датчиками мы хотим следить, поэтому у неё есть ключик --sid
(для простоты он вписан сразу в start_fg.sh). В качестве параметра --sid можно указать идентификатор, а можно указать имя датчика. Мы укажем имя Level_AS.
./start_fg.sh Level_AS
Вывод команды
smonit запускается и висит ждёт изменений. При этом в начале выводится текущее состояние датчика. Из вывода можно увидеть название, время последнего изменения (включая микросекунды), идентификатор процесса, который сохранил этот датчик в SM (в данном случае это Imitator1), и текущее значение value (и в виде float — fvalue).
Всё вроде бы готово, выставляем датчик cmdLoad_C=1 и смотрим, как побежал меняться датчик Level_AS.
Для выставления как раз воспользуемся admin-ом.
[pv@pvbook Administrator]$ ./setValue CmdLoad_C=1
и переключившись в консоль, где у нас запущен smonit, смотрим как побежал датчик (от 0 до 100).
Вывод на экран smonit
Значит, увеличение работает. Уменьшение проверяется аналогично… предварительно надо не забыть сбросить предыдущую команду, и мы сделаем это «одним махом»
[pv@pvbook Administrator]$ ./setValue CmdLoad_C=0,CmdUnload_C=1
smonit побежал в обратную сторону (от 100 до 0)
Вывод на экране
Мониторинг внутренних переменных объекта (vmonit)
Теперь я опишу механизм, который позволяет посмотреть внутренние переменные объекта. Вообще, всё очень просто. Зная идентификатор или имя объекта, можно просто запросить у него информацию.
Итак, у нас всё запущено и работает. Давайте посмотрим, что объект Imitator1 нам покажет.
Заходим в src/Services/Administrator и запускаем команду ./oinfo Imitator1
Вывод на экране
Как видно из вывода, команда oinfo позволяет увидеть
- Состояние всех входов и выходов объекта с привязками к датчикам (внутренних in_, out_ переменных)
- Текущий список работающих таймеров, с оставшимся временем работы и т.п.
- Значения всех переменных, с которыми запущен процесс (объявленных в src.xml)
- Внутреннюю информацию по объекту (размер очереди сообщений, какой был максимум, были ли переполнения)
- А также пользовательская информация
О пользовательской информации скажу немного подробнее…
У каждого объекта (точнее у скелета класса) есть специальная функция
virtual std::string getMonitInfo() override;
переопределив которую, можно выводить свою информацию, в виде текста (строки). В данном случае имитатор, например, пишет «Текущий режим работы: наполняем» (или «опустошаем»). Можно писать и что-то более сложное.
Пример реализации функции в имитаторе
string Imitator::getMonitInfo()
{
ostringstream s;
s << "Текущий режим работы: " ;
if( in_cmdLoad_c )
s << " наполяем.." << endl;
else if( in_cmdUnload_c )
s << " опустошаем.." << endl;
return std::move(s.str());
}
Добавление в информационный вывод своих переменных
Конечно, когда Вы пишете свой процесс управления, скорее всего у Вас будут, помимо переменных объявленных в xml-файле, ещё какие-то свои поля класса. И конечно захочется так же следить (выводить информацию) о текущем состоянии и своих переменных. Нет ничего проще. Допустим в имитатор мы добавим два счётчика команд.
Добавление в Imitator.h
...
private:
unsigned int numCmdLoad = { 0 };
unsigned int numCmdUnload = { 0 };
Тогда, если вы хотите увидеть их в выводе oinfo, просто в конструкторе сделаем два волшебных вызова:
Добавление в Imitator.cc
Imitator::Imitator( UniSetTypes::ObjectId id, xmlNode* cnode, const string& prefix ):
Imitator_SK(id, cnode, prefix)
{
...
vmonit(numCmdLoad);
vmonit(numCmdUnload);
}
Т.е. просто обернули свои переменные в vmonit(xxx). Тут конечно не обошлось без макросной магии, но наверно это не сильно напрягает…
В итоге на экране мы уже увидим и наши переменные (затесавшиеся среди прочих).
Вывод на экран (повторный вызов ./oinfo)
ВАЖНО: поддерживаются пока только стандартные простые типы: bool,int,long и т.п., для всего остального есть универсальная функция getMonitInfo()
Удалённое чтение логов (встроенный LogServer)
Как известно, сколько бы механизмов отладки ни существовало, а любимый cout(
В сгенерированном скелете класса, есть специальный объект для логов — log. Он по сути имеет интерфейс как cout, только это shared_ptr, поэтому пользоваться им нужно как указателем. Например
log->info() << "......information.." << endl;
или
log->crit() << "......critical" << endl;
У лога есть 15 уровней, включать их можно «параллельно» (т.е. например info,warn,crit), ему можно указать файл, куда писать логи, можно включать и отключать вывод даты и времени в начале каждой строки и т.п. Вообщем много стандартных возможностей. Для каждого объекта можно включать или отключать логи просто указав при запуске аргумент командной строки
Управление логами через аргументы командной строки
--ObjectName-log-add-levels info,warn,crit,level1,... - это добавление логов (к уже включённым)
--ObjectName-log-del-levels info,warn,crit,level1,... - это удаление логов (из включённых)
--ObjectName-log-set-levels info,warn,crit,level1,... - это установка логов (взамен текущих)
Но всё это было бы не так интересно, если бы не наличие такого механизма, как LogServer. В каждом объекте есть встроенный LogServer, который по умолчанию не запускается, соответственно ресурсов не потребляет и вообще не виден никак. Но простым аргументом командной строки
--ObjectName-run-logserver
мы можем его активировать. По умолчанию он запускается на localhost, а в качестве порта использует идентификатор объекта. Но можно и принудительно указать host и port запуска.
Команды для переопределения host и port
--ObjectName-logserver-host xxx
--ObjectName-logserver-port zzz
После того как у объекта запущен LogServer, мы можем читать его логи, причём удалённо. Просто подключившись по указанному хосту и порту. Для чтения логов существует специальная утилита uniset2-log. При помощи неё можно помимо чтения логов, так же и управлять уровнем вывода логов, запись в файл и т.п., т.е. осуществлять полный контроль над логами объекта. Это очень удобный механизм, т.к. позволяет включать и отключать логи без перезапуска программы (а ведь часто нельзя остановить процесс, но очень нужно посмотреть, что там внутри происходит).
… давайте просто я покажу...
Итак, у нас всё запущено, причём мы добавили в start_fg.sh имитатора строчку
--Imitator1-run-logserver
Кстати в выводе ./oinfo, если кто не заметил, выводится информация о том, запущен ли LogServer. Но давайте я покажу ещё раз (зайдём в каталог src/Services/Administator/ и запустим команду ./oinfo Imitator1).
Вывод информации об объекте (обратите внимание на LogServer)
Итак logserver запущен на localhost и порт 20001. Но по умолчанию (если, конечно, разработчик их принудительно не включит), логи отключены. Соответственно, мы не просто подключимся, а сразу ещё и включим все(any) логи, чтобы сразу начать их видеть. Давайте подключимся (добавим ключик -v, чтобы увидеть отладочную информацию о том, к кому мы подключаемся)
uniset2-log -i localhost -p 20001 -v -a any
Я добавил в функцию таймера лог (уровень 3), для вывода, чтобы продемонстрировать работу.
Добавка в Imitator.cc (mylog3)
void Imitator::timerInfo( const UniSetTypes::TimerMessage* tm )
{
if( tm->id == tmStep )
{
if( in_cmdLoad_c ) // значит наполняем..
{
mylog3 << myname << "(timerInfo): таймер(" << tmStep << ").. наполняем" << endl;
out_Level_s += stepVal;
if( out_Level_s >= maxLevel )
{
out_Level_s = maxLevel;
askTimer(tmStep,0); // останавливаем таймер (и работу)
}
return;
}
if( in_cmdUnload_c ) // значит опустошаем
{
mylog3 << myname << "(timerInfo): таймер(" << tmStep << ")... опустошаем" << endl;
out_Level_s -= stepVal;
if( out_Level_s <= minLevel )
{
out_Level_s = minLevel;
askTimer(tmStep,0); // останавливаем таймер (и работу)
}
return;
}
}
}
Тогда подключаемся (как указано выше) и в другой консоли (заходим в src/Services/Administrator) выставляем команду
./setValue CmdLoad_C=1,CmdUnload_C=0
… а через некоторое время наоборот ./setValue CmdLoad_C=0,CmdUnload_C=1
А вот что мы увидим в логах (удалённо читаемых)
Чтение логов
Пользуясь возможностью удалённо читать логи, важно не забывать, что если вы их включили, то неплохо бы и уходя, отключать, потому что в реальной системе логи не должны работать, ввод/вывод — дорогое удовольствие. И ещё, я немного укажу возможности, которые не раскрыл ранее
- LogAgregator — объект, позволяющий агрегировать в себе логи от нескольких объектов и управлять ими «централизованно»
- Поддержка в LogAgregatore регулярных выражений (C++11), позволяющих более гибко выбирать какие логи (от каких объектов) мы хотим читать
- Возможность в uniset2-log указать сразу несколько команд для включения одних логов, отключения других и, например, чтения третьих. Всё это одной командой.
В реальном применении LogServer конечно запускается один на все объекты (в рамках одного запускаемого файла), которые при этом выстраиваются в иерархию при помощи LogAgregator и можно ими гибко управлять.
Небольшой итог
Каждый раз рассказывая о каких-то механизмах, я пытаюсь соблюсти баланс между кучей подробностей внутреннего функционирования и простотой внешнего применения. Т.е. «вот готовые команды, берите и пользуйтесь — они работают из коробки». Поэтому я многое не рассказал, и может что-то осталось не очевидным… Постараюсь ответить на ваши вопросы.
В целом, если не считать, что нормальное тестирование — это гораздо больше всяких «тестов» (граничные случаи, задание max, min, одновременное выставление команд и т.п.), то с наладкой имитатора мы закончили. Главное было продемонстрировать инструменты для наладки, входящие в libuniset2:
- утилиты для работы с датчиками и мониторинга их состояния
- механизм для удалённого просмотра состояния внутренних переменных объекта
- механизм удалённого чтения и управления логированием (у каждого объекта)
Примерно таким же способом можно наладить и работу нашего процесса управления, но лучше я покажу наладку алгоритма управления, на примере использования более продвинутого способа наладки — написания функциональных тестов с использованием uniset2-testsuite. Об этом в следующей части...
Ну и в конце как обычно ссылочки: