среда, 28 февраля 2018 г.

Модель описания протокола, часть 2

Потратил около недели чтобы реализовать один конечный автомат из стандарта BACnet/MSTP. Хочу поделиться идеями реализации, пожаловаться на сложности. Во первых, надо сказать я его раза три написал. При том что описание получается от 300+ до 600+ строк. Это такой громадный switch() со встроенными проверками и switch() по командам протокола и ветвлением по разным признакам.



Сначала описал этого монстра близко к документации. Получил некоторую структуру. Из которой стало видно... Ничерта из нее не видно! Я понял одно - по описанию ее никак не отладить, хотя описание протокола очень подробно описывает все действия в каждом состоянии. И с учетом отдельного документа Errata к стандарту, получаем достаточно точное и описание, которое можно заставить работать, если бы оно могло заработать с первого раза.
Итак о чем собственно речь.
Это протокол для RS-485 (последовательного порта) полудуплексный с выбором мастера в сети методом передачи "токена" по цепочки мастеров.

Чтобы определить, кто в сети может работать мастером, каждая "нода" - устройство должно опрашивать адреса от себя (своего адреса) последовательно до следующего мастера. В протоколе предусмотрена для этого процедура поиска мастера Poll For Master/ Reply to Poll For Master/ Token Pass. Вся эта процедура должна быть оптимизирована, чтобы не сильно загружать сеть служебными запросами. В результате каждая нода, которая желает быть мастером должна получить "токен" и воспользоваться правом мастера на передачу данных.


Отдельно можно рассмотреть вариант, когда нода не желает быть мастером. Все упрощается до двух состояний: ждем вопроса/ отвечаем. Написал реализацию "ведомой ноды", понял что логика работы "ведомой ноды" содержится в каком-то виде внутри конечного автомата "мастеровой ноды", с той разницей, что у мастера есть необходимость ловить таймаут "Нет токена, подозрительно тихо на линии". Такое происходит, когда срывается обмен, например, при ошибках на линии или в результате отсоединения или присоединения нового устройства. Событие называется "Токен потерялся". После этого для каждого мастера есть временное окно в котором мастер может захватить линию и проявить свою активность не создавая коллизию. Все бы хорошо, но для этих операций нужна точность порядка 5мс и выше. Временное окно в которое нужно вписаться 10мс. Т.е по сути кроме разных протокольных реверансов мы получаем настройку таймеров в каждом состоянии.

Выделил несколько групп состояний и сделал вторую реализацию протокола, три отдельные функции: 1) ведомое устройство, 2) мастер, 3) поиск мастера и передача "токена".



Даже придумал такую системную хитрость -- фазовую функцию, которая подменяет сервис и его контекст. От ведомого устройства в режим мастера мы попадаем по "токену" или по задержке "потеряли токен". С токенами играемся (3), когда нечего отсылать. В режим мастера попадаем, когда наигрались с токеном. Когда нечего отсылать передаем токен и переходим в ведомое состояние (1).
Ничего этого не пригодилось, потому что все переделал еще раз.

Вот что не понравилось. Куски кода очень похожи и повторяются много раз. Некоторые состояния не завершаются действием. Тут проблема. Проблема в том, что если мы завершили операцию действием, отослали данные, то мы ждем событие от драйвера. А если мы не завершили действие, то не понятно, как и по какой причине мы возвращаемся в обработчик протокола. Кроме того возникает некоторая кривизна с таймерами и состояниями. Если мы отсылаем данные, как рассчитать таймаут: надо прибавить время отсылки данных. Вот и нет, пришлось ввести неучтенное стандартом событие -- DL-UNITDATA.confirm - подтверждение отправки данных, для того чтобы от него отмерять таймауты.

Что еще не так. В стандарте есть промежуточные состояния, после которых не понятно как ловить событие. К таким состояниям относятся: DONE_WITH_TOKEN или USE_TOKEN. Некоторые переходы описанные в стандарте в состояние IDLE тоже обладают этим свойством, они именно переходы, не завершаются действием. Языковая конструкция получается кривая.
Представим, что структура была:
switch() {
...
case USE_TOKEN:....
case IDLE: if (Таймаут) {} else if (доставка пакета) {} else if (битый пакет) {} else if (подтверждение отправки) {} ...

Вот в эту конструкцию надо вписать переход из состояния PASS_TOKEN в IDLE, по условию, что токен передан, а если не передан и активность на линии не началась, то переходить в другие состояния. Активность начинается -- это флаг состояния DL-UNITDATA.indication, т.е. мне нужно выйти из структуры switch() и попасть внутрь структуры case IDLE:--> if(DL_UNITDATA.indication). Первое что леизит в голову - goto внутрь, в самую глубь структуры. ТАК НЕЛЬЗЯ, так программы не пишут - это грех! Как туда попасть чтобы не нарушать правила Вирта?
Я придумал структуру :
while(1) {
switch(state){
case PASS_TOKEN: ... if(токен передан) {state = IDLE; continue; } ...;
}
break;
}
Использование оператора continue возвращает в начало цикла. И мы второй раз заходим в структур switch() но уже с новым состоянием. Благодаря этому трюку удалось несколько сократить объем кода, выкинуть повторы в структуре.

Стоп! Тут всем стало ясно что я делаю собственную реализацию по мотивам стандарта, но не точно, потому что мой анализ стал отличаться от логики описанной в стандарте. И тут я снова вылетаю из окна!  Я все стер, все полторы тысячи строк кода и начал работу заново. Я начал работу с создания виртуальной сети. Написал сеть в форме очереди сообщений по таймеру. Пробовал для этого приспособить микрософтовый API TimerQueueTimer - таймерную очередь таймеров, получил поток ошибок и глюков, с не вполне освободившимися от событий структурами, и отказался от идеи. Потом сделал уникальную вешь, сделал свой планировщик на основе асинхронной очереди, который вполне можно приживить внутрь контроллера. Как вам идея - виртуализация сети в контроллере?
Виртуальная сеть нужна чтобы отладить работу протокола. Но протокол весь строится на таймаутах. Если я наезжаю отладчиком на мультипотоковое нечто состоящее из пяти асинхронных процессов, отладка не получается. Одно ясно, все описано без ошибок: виртуальная сеть и служба "мастеровой ноды", то должен получится хороший хоровод. Можно время замедлять, но все равно, мы ходим через события "потеря токена" всякий раз когда пытаемся отлаживать. Я хочу сказать что отладить не очень то получается, хотя можно проследить в каких состояниях и по каким причинам процесс повисает или идет не по плану. В виртуальной сети можно сделать лог форме pcap- формате данных для Wireshark тогда, можно очень наглядно изучать порядок работы узлов сети и анализировать эффективность обмена. Такой подход позволил примерно за сутки отладить всю структуру.
Я напомню, моя структура отличается тем, что я пытаюсь отладить работу не существующего сетевого драйвера, который общается через события Indication,Confirm,Request,... с учетом Timeout.

По поводу эффективности опроса узлов сети. Есть такая идея: нечего слать - надо опрашивать узлы сети. Опрашивать ноды, которых из раза в раз в сети нет и не появится, --  бессмысленно и раздирает пропускную способность сети на таймауты. Эффективность повышается если на N нормальных опросов делать один запрос на поиск новой ноды в сети. Это не все, чем меньше свободных адресов от меня до следующей ноды, тем реже их можно искать. Вот такие идеи ведут к некоторой перегрузке логики работы службы, но они необходимы для нормальной работы. Поди разгляди эту логику в стандарте. Она размазана сразу по всем состояниям, в одном состоянии переменная инициализируется, в другом параметр плюсуется, в третьем проверяется. Проследить эту логику практически не возможно, сразу не очевидно через какие состояния и проверки проходит процесс, а через какие не проходит. Все это кажется невероятно сложно. Просто бывает, если есть одно место, где надо плюсовать, например if (парам меньше макс){парам++} else {парам=0} -- это понятно, но вот когда тоже самое происходит в десяти разных состояниях, в двух инициализруется, в трех других состояниях проверяется, а в четвертом плюсуется, тут голова и вскипает. Ну собственно поэтому я все стер и заново написал именно в таком виде, чтобы инициализация и плюсование происходили в одном состоянии в одной проверке. Получилось, но это уже совсем другая реализация. Исходно стандарт в результате опроса узлов сети распознает состояние "единственный мастер в сети" и от этого логика меняется. В случае если передача токена не случилась, переспрашивает Nretry раз, и повышает эффективность опроса путем регламентирования числа пакетов до передачи токена и числа опросов узлов. В стандарте не учтена замечательная возможность не передавать токен дальше, а передавать назад роутеру, чтобы сократить время на ответ. Это то что я хочу добавить, если из анализа будет видно что эта функция себя оправдывает. Хочу сделать полумастера, который не сильно думает над поиском мастера в сети, а тут же отдает токен вызывающей стороне. Сейчас, мастеровая нода сканирует адреса от себя TS (this station) до NS(next successor). После завершения цикла продолжат сканировать с номера TS+1, в случае возврата, надо чтобы мастеровая нода продолжала сканировать от NS+1 дальше. Мне почему-то не хватает в концепции "подмастерья" "полумастера", чтобы он поддерживал все варианты общения: сегментацию пакетов, нотификацию и уведомления, но не поддерживал эти самые рутерия и выборы в сети. Получил "токен", повертел, отдал обратно. Токен - это не число, это вообще не объект - это инициатива.

К концу дня начал писать реализацию роутера сети BACnet. Устройство, у которого два порта - уже роутер. Надо теперь научить эти устройва, которые друг друга бесят выбирая мастера в сети, выбирать кто из них будет работать роутером для какой виртуальной сети и с какой надежностью. Роутеров может быть много, вытаскивание из сети устройства не должно приводить к потере связи с остальными устройствами.  Who_Is_Router_to_Network? I_Am_Router_to_Network... около десятка разнообразных сообщений подобного рода. Начал выражать словами, что такое таблица рутения и как она вытесняется по надежности/достоверности/эффективности/популярности маршрутов.

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

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

Пожаловался на судьбу. 28.02.2018

Комментариев нет:

Отправить комментарий