libuniset2 — библиотека для создания АСУ. Лучше один раз увидеть…Часть 4 (Наладка)

от автора

В предыдущих частях (часть 1, часть 2, часть 3) было описано создание двух процессов: имитатора и процесса управления… Теперь же настало время наладки.

Итак, на текущий момент у нас уже реализованы и запускаются следующие процессы:

можно начинать наладку…

Наладка. Вводная часть.

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

На самом деле всё конечно немного сложнее, но рисунок призван помочь понять, как у нас устроены «информационные потоки».
Итак..

  • Всё взаимодействие идёт через 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/ и…
Для этой утилиты нужно указать, за каким датчиками мы хотим следить, поэтому у неё есть ключик —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(или же не любимый printf) всё равно будет использован. Ну что ж, libuniset предоставляет и этот способ. На самом деле, тема очень обширная, если раскрывать все возможности и детали, то это тема для отдельной статьи. Поэтому я покажу применение и расскажу немного деталей…
В сгенерированном скелете класса, есть специальный объект для логов — 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. Об этом в следующей части…

Ну и в конце как обычно ссылочки:

ссылка на оригинал статьи https://habrahabr.ru/post/278589/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *