RF-Link

RF-Link это комплекс программ и компонентов для приема извещений от контрольно-охранных приборов. Разработан компанией Новатех. Извещения принимаются драйверами канала (радио, Ethernet, GSM, итд..), расшифровываются драйверами приборов, сохраняются в базе данных, передаются на рабочее место дежурного оператора центрального поста охраны.

Первой задачей было устранение потерь входящих сетевых пакетов при пиковых нагрузках, намного превышающих расчетные. Обычно это происходило при сбоях и изменениях конфигурации сети у оператора связи. Пакеты от сотен приборов где-то накапливались и в один момент одновременно поступали в драйвер в огромных количествах и не успевали обрабатываться. Для начала я сделал очередь пакетов и разделил прием и обработку пакетов по разным процессам. Тысячи пакетов сразу попадали в очередь и последовательно обрабатывались с задержкой в несколько секунд, но не терялись.

Экспорт отчетов

Следующей задачей было устранение зависания при формировании отчетов по событиям за большой период. Это более миллиона строк в одной таблице Excel. Таблица формировалась с использованием механизма работы с Excel через COM (OLE), довольно медленно. Вместо этого был использован компонент экспорта сразу в файл Excel, он работал на порядки быстрее. Возникла другая проблема. База данных находилась на одном компьютере (на сервере), а отчет формировался в программе оператора (на клиенте). Нужно было как-то передать запрос по сети на сервер и получить миллион строк. Напрямую подключиться к серверу базы данных нельзя. В программе был использован механизм DataSnap, но он требовал отдельного сетевого соединения, был довольно сложен и тормозил. В нем происходило преобразование данных в XML и обратно.

Я сделал проще. Был написан простой механизм сериализации данных. Целые числа передавались как 4 байта. Остальные данные как целое число длины + необходимое число байт данных. Сначала передавался блок метаданных, который определял формат данных. Например, количество параметров SQL запроса и их описание (номер, название, тип, чтение-запись). Затем шел поток данных, который мог иметь как фиксированный размер (таблица на 10 строк, например), так и неопределенный, до получения маркера конца. Поскольку формат был бинарный без каких-либо преобразований, то его скорость была равна скорости простого чтения-записи блоков памяти. А нефиксированный размер позволял сериализовать данные частями, по мере чтения результатов SQL-запроса и отправки данных в сетевое соединение. Выяснилось, что выделение памяти работает странно - если выделять память по мере надобности, то оно очень медленно работает. А если выделить сразу гигабайт, то индикатор объема выделенной памяти не изменяется, только по мере фактической записи в этот объем. И не тормозит!

Возникла новая проблема - стандартные компоненты не могли быстро заполнять датасет (таблицу) в памяти. до 100к строк все работало быстро, а дальше начинало тормозить в геометрической прогрессии. Хотя это простейшая таблица без индексов, инкрементов и вычисляемых полей. Перепробовал разные датасеты, списки, коллекции - результат тот же. Потом нашел виртуальный датасет (http://digilander.libero.it/snapobject/), который не хранит данные, а вызывает разные функции при обращении к данным. Данные у меня уже есть, в виде бинарного блока определенного формата, откуда данные легко читаются, но только последовательно с самого начала. Поскольку чтение таблиц обычно происходит последовательно с самого начала, то не составило большой проблемы написать свой виртуальный датасет с привязкой к блоку данных, где данные читаются сразу строками и запоминается позиция последней прочитанной строки, то есть не требуется каждый раз перечитывать все с самого начала. Эксперименты показали, что таблица с двумя миллионами строк (порядка 500 мегов в памяти) отображается моментально с начала и с незначительной задержкой при переходе на произвольную строку в конце. Итого на рабочей базе запрос на более 900к строк с передачей данных по сети и экспортом в Excel занимал порядка 5 секунд.

Оптимизация сервера

Очередная проблема - запрос может выполняться долго, если сервер сильно нагружен. И во время ожидания ответа другие данные не обрабатываются. Нужна асинхронная обработка команд между клиентом и сервером, чтобы во время выполнения одной команды могли обрабатываться другие команды и данные. Для этого пришлось переделать механизм обработки входящих пакетов. Полностью асинхронную обработку данных делать пока не стал, слишком много требовалось переделать. Я сделал компромиссно-гибридный вариант - отправка модального запроса или команды требующей ответа ставит семафор блокировки. Пока стоит семафор, другие команды не отправляются, а все входящие пакеты кроме ответа попадают в очередь. После получения ответа или таймаута обрабатываются накопившиеся в очереди пакеты. Для SQL-запросов семафор не ставится, то есть они полностью асинхронны. После отправки SQL-запроса программа продолжает работать как обычно, а при получении ответа результат передается объекту-отправителю запроса. То есть, по сути SQL-запросы отправляются, выполняются и обрабатываются в фоне, и потом сразу отображается результат.

Следующей проблемой был долгий старт сервера. При старте сервер читал из базы данных данные по всем объектам и приборам, а также по сотне последних событий для каждого объекта по определенным условиям. Если объектов тысяча, то событий было 100 тысяч. Причем, одним группирующим запросом это сделать не получалось, он выполнялся дольше, чем тысяча таких запросов по каждому прибору. Вообщем-то это не такая уж большая проблема, но обнаружился огромным разброс скорости выполнения одинаковых запросов. Долгое время грешил на несовершенство используемой базы данных, но в синтетических тестах все работало быстро и гладко. Обнаружилось, что даже при отсутствии событий, сервер в фоне постоянно (десятки раз в секунду) обновляет в БД таблицы состояний драйверов, даже если состояние не менялось. А программа оператора через DataSnap 10 раз в секунду запрашивает последнее состояние для отображения значка прибора. Вместо этого, я сделал новый вид сетевой команды - запрос статуса драйверов. В ответ на него сервер присылает список драйверов, где для каждого драйвера указано состояние и статистика работы (получено/отправлено/ошибок), что позволило сделать анимацию иконки состояния в зависимости от активности драйвера. А на сервера состояние драйвера в БД обновляется только при изменении или каждые 30 сек. На всякий случай, для отчетов. Это резко снизило нагрузку на базу данных. Кроме того, при чтении объектов из БД происходили проверки уникальности порядкового номера. Поскольку такая проверка уже есть в самой БД, то сделал отключение проверки номеров при чтении из БД. Это заодно ускорило загрузку объектов из сети при старте программы оператора.

УС-А

Далее, я начал доработку драйвера канала УС-А (Устройство Согласования Автономное). Это такой хитрый автономный коммутатор (фактический отдельный сервер с резервным питанием и кучей запасных GSM-приемников), который самостоятельно поддерживает связь с 600 приборами по каналу GSM. К серверу подключается через шину CAN и преобразователь CAN-USB, который называется МПСИ (Модуль Преобразования Системного Интерфейса). На одной шине CAN может быть множество таких коммутаторов, на тысячи приборов. УС-А посылает данные как по запросу, так и самостоятельно и имеет довольно сложный протокол.

Для начала я поднял локальный веб-сервер с wiki и подробно задокументировал все доступные данные по работе прибора, протоколу связи, детали реализации в драйвере. Сразу стали видны закономерности и аномалии, недочеты в проектировании драйвера. Драйвер делали на основе драйвера другого прибора связи, гораздо более простого. Из последовательного порта читаются CAN-пакеты (субфреймы) длиной до 8 байт, которые могут объединяться в посылку (фрейм) до 256 байт. Содержимое посылки расшифровывалось и обрабатывалось драйвером УС-А, потом данные передавались дальше в сервер. Получалась очень длинная цепочка вызовов от чтения данных из порта до записи события в БД и отправки в программу оператора. Я сделал несколько цепочек - отдельно чтение и расшифровка фреймов из порта и помещение их в очередь, отдельно обработка фреймов из очереди по протоколу УС-А, отдельно отправка извещений в сервер.

Чтение пакетов CAN из порта сделал отдельным компонентом. Внутри адаптера CAN-USB был чип FTDI, драйверы для которого уже есть почти во всех операционных системах. Только он использовался не как обычный UART (COM-порт), а через библиотеку ftd2xx - она позволяла читать модель и серийный номер устройства, что очень облегчало настройку и привязку к нужному устройству FTDI. Шина CAN очень хорошая штука - в ней данные передаются не байтами, а сразу пакетами. Согласованием параметров и арбитражом шины, контролем и коррекцией ошибок, фильтрацией по заголовкам занимается чип, а на входе и выходе очередь готовых пакетов. Электрически CAN похожа на RS-485, а логически на UDP. Все это я выделил в отдельный независимый компонент, который автономно читает пакеты, собирает из них фреймы, расшифровывает, проверяет и раскладывает их очереди по разным логическим каналам (адресам). А также входящие фреймы шифрует, разбивает на CAN-пакеты и отправляет в порт.

Драйвер прибора связи УС-А был полностью переработан. Было произведено разделение на модель (данные+логика) и отображение (визуальные формы, списки, кнопки, надписи, и прочий GUI). Модель работает сама по себе, а отображение создается при необходимости (когда пользователь открывает окно свойств драйвера) и привязано к модели, показывает ее текущее состояние и позволяет пользователю управлять ей. Это намного удобнее и эффективнее, чем хранить данные в визуальных формах, постоянно синхронизировать с другими данными. Нужно лишь использовать виртуальные контролы, которые перед отображением или изменением данных запрашивают из при помощи событий (callback-ов). Например, в УС-А есть 600 формуляров (карточек с настройками и состоянием охранных приборов), и они постоянно обновляются. Виртуальный список при перерисовке обращается только к данным десятка формуляров, которые видны на экране. Достаточно перерисовывать его 2 раза в секунду по таймеру, чем 20 раз в секунду после каждого события. То же самое для отладочного журнала сообытий. А если контрол или список не виден, то его не нужно перерисовывать и он не потребляет ресурсы при изменении содержимого.

Была также реализована асинхронная модель управления УС-А, которая по таймеру выполняла разные операции:
- брала фрейм из очереди канала и обрабатывала его;
- проверяла состояние формуляров и отправляла запрос недостающих данных;
- обновляла статистику, проверяла таймауты, отправляла периодические запросы;
- обрабатывала команды сервера и пользователя;
Почему по таймеру, а не сразу? Это позволяет сократить длину цепочек вызовов функций, избежать зацикливаний, управлять нагрузкой на систему. Например, при начальном чтении всех формуляров (после старта драйвера или подключении устройства) идет запрос диапазона формуляров, в ответ на который приходит несколько пакетов. После чтения последнего формуляра из диапазона нужно отправить следующий запрос. В это время могут приходить и другие события, которые тоже должны обрабатываться. В асинхронной модели с этим никаких проблем - обработчик ничего не ждет, не блокирует. Есть данные или команда в очереди - обрабатывает и выходит до следующего тика таймера. Изменяя задержку таймера можно регулировать нагрузку. Когда много данных в очереди или нужно быстро отправить следующий запрос, задержка минимальна. Когда нет данных - задержка в сотни миллисекунд.

Была обнаружена странность в работе адаптера CAN-USB. Пакеты CAN приходили не по порядку отправки, а перетасованные в случайном порядке! То есть 1,2, 7, 4, 5, 6, 3, 8.. Как оказалось, в чипе два буфера для выходящих пакетов, у каждого свой фильтр. Они использовались как один общий буфер, при заполнении первого буфера пакет помещался во второй. А забирались сначала из первого, потом из второго. И получалась перетасовка порядка пакетов. Прошивку исправили, но для корректной работы уже используемых устройств пришлось делать хитрый сортировщик и валидатор пакетов. Пришлось читать много бинарных дампов (оказывается, их вполне можно читать, как иероглифы!), добавить множество проверок, и вообще переписать весь компонент адаптера CAN. Он стал сложнее, в нем появились новые настройки по каждому каналу, определение версии прошивки, самонастройка на оптимальный режим работы, самовосстановление при ошибках, неисправностях железа (отключение, обрыв, замыкание, перепутаны провода). Адаптер и устройства на шине можно подключать-отключать-ломать-чинить прямо на ходу - все корректно обрабатывается в реальном времени.

Вместе с драйвером УС-А был переписан драйвер прибора связи БРП (Блок Радио-Приемников) - два цифровых радиоприемника на шине CAN, с фильтрацией и буферизацией принимаемых сообщений. В нем нет формуляров, он просто принимает сообщения от охранных приборов на конкретном радиоканале. Работа с CAN была унифицирована под компонент адаптера CAN, а обработчик пакетов переделан в асинхронную модель. Был риск ухудшить или совсем поломать работу драйвера, который считался самым лучшим в системе. Но в итоге драйвер стал работать еще лучше - моментально восстанавливался при сбоях и спокойно справлялся с нагрузками, на порядки превышающие максимальные.

Отладка сервера

Сервер работал хоть и не особо быстро, но довольно стабильно. Но иногда возникали непонятные ошибки при изменении настроек или просто во время работы. И обязательно были ошибки при зыкрытии сервера. Ошибки при изменении настроек были связаны с записью в реестр без прав администратора. Я унифицировал все обращения к реестру, а затем и работу с конфигами в один класс конфига. Программа больше не обращается к реестру или файлам настроек напрямую. Вместо этого идет обращение к объекту конфига. Это позволило кешировать настройки (в некоторых случаях они читались при каждом событии), сохранять настройки одновременно в реестр и в файл, объединить много мелких конфигов в один с множеством разделов, исключить обращение к системным веткам реестра без прав администратора.

Причину ошибок обращения за пределы выделенной памяти (Access Violation) при работе и закрытии программы очень сложно найти. Некорректное освобождение памяти невозможно обнаружить, но можно поймать последующее обращение к ранее освобожденной памяти и выход за пределы диапазона. Эти проверки были отключены в настройках компилятора, а также в коде были многочисленные "заглушки" исключений с комментариями типа "борьба с AV" и "глюк Delphi". Я сделал новую ветку проекта, включил все проверки и предупреждения и заменил заглушки предупреждениями с записью в лог. Систему логирования я перенес с проекта ClassCard - она хорошо себя проявила, а монитор логов очень помог в отладке и оптимизации большого потока событий.

Поначалу огромное количество времени ушло только на то, чтобы компилятор мог собрать проект без ошибок и предупреждений. Мелкие, но болезненные ошибки были везде - в конструкторах и деструкторах классов, в порядке вызова наследуемых методов, в инициализации модулей, создании и удалении объектов. Во многих случаях исправление ошибки приводило к проблемам в других местах, поскольку там были "костыли" для обхода этой ошибки. Иной раз костыли были многоуровневые, устраняли проблемы, созданные другими костылями. Например, есть несколько форматов (протоколов) извещений от приборов, и есть несколько типов каналов связи. В заголовке извещения есть поле вида извещения, в котором есть тип прибора, протокол извещения и тип канала связи. Но почему-то в некоторых случаях используется только один тип (либо тип прибора либо тип канала связи), а недостающие типы определяются по названию прибора. А поскольку есть приборы, поддерживающие разные протоколы и каналы связи, то для них анализируется номер прибора и номер телефона - там может оказаться что угодно - модель прибора, тип канала связи, серийный номер, MAC, IMEI, IP, номер коммутатора.. И тогда извещение обрабатывается не драйвером прибора, а отдельными ветками условий внутри сервера. По идее, сервер не должен ничего знать об особенностях работы приборов и каналов - только состояние прибора и шлейфов, а также разные виды событий, тревог, неисправностей. И если прибор поддерживает несколько каналов связи одновременно, то проще один раз добавить поддержку состояния для разных каналов, чем имитировать это на одном канале каждый раз, когда это нужно.

К сожалению, сразу переделать как правильно очень рискованно - очень большой объем изменений, некоторые драйвера устаревших приборов невозможно протестировать и отладить, потому что они сложные, к ним нет ни документации, ни тестовых приборов. Но они где-то еще используются, поэтому их лучше не трогать. Поэтому я взялся за исправление более простых вещей. Сервер не справлялся с большим потоком извещений, возникали неуловимые ошибки в разных местах. Я постарался уменьшить длину цепочек обработки извещений от драйвера до отправки в сеть. Выделил работу с сетевыми соединениями и пакетами в отдельный компонент. Унифицировал все виды сетевых пакетов в один, но с разными методами чтения блока данных. Вся работа с сетью была сведена к простому принципу - есть две потоконезависимые очереди пакетов (входящая и исходящая). Компонент сокета в фоновом процессе читает и расшифровывает входящие пакеты, кладет их в очередь входящих. А также шифрует и отправляет в сеть пакеты из очереди исходящих.  Есть всего два события - после подключения и при отключении. Ошибки вызывают событие отключения с текстом подробностей ошибки. Со стороны сервера для каждого сетевого клиента создается обработчик соединения - он обрабатывает пакеты из очереди входящих, выполняет команды и запросы клиентов, передает новые события, извещения и статусы приборов. Если возникла проблема с сетью, это никак не влияет на работу ядра сервера, каждое сетевое соединение полностью автономно. И скорость обработки пакетов а ядре не ограничена скоростью отправки в сеть и задержками при ожидании ответа или готовности. То же самое было сделано со стороны клиента, и заодно все команды были сделаны полностью асинхронными. После отправки команды обрабатывались любые входящие сообщения, а при получении ответа срабатывало событие результата. Как оказалось, большинство команд ожидало простой результат - либо ОК, либо текст ошибки для показа пользователю. Для однотипных команд сделано ограничение на повторную отправку команды во время ожидания ответа на предыдущую команду.

Далее были переработаны иерархии подчиненности объектов. Каждый объект или визуальная форма должны иметь создателя-владельца, создаваться и уничтожаться в одном блоке (в обертке try .. finally) или в соседних блоках, обязательно с обнулением указателя. Последовательность создания и удаления должны зеркально соответствовать друг другу. Было исправлено много случаев, когда форма или объект создавались в одном модуле, а уничтожались в другом, иногда не уничтожались после использования (утечка памяти), уничтожались повторно или использовались после уничтожения. Массивы и записи заменены на коллекции (TCollection), которые позволяют отслеживать изменения в элементах. Для ускорения поиска в больших списках использованы строковые хеши (TStringHash), позволяющие к строковому значению привязать числовое (позицию в списке) и быстро его получать. Фоновые процессы (TThread) были сделаны атомарными - все внутренние объекты создаются и удаляются в Execute(), а не в конструкторе-деструкторе и покрыты try..finally). В секциях инициализации драйверов создавать и удалять потоки нельзя, поэтому пришлось переделать некоторые драйвера, использующие многопоточность. Были добавлены проверки правильности переданных параметров, ключевые функции обернуты в try..except с логированием подробностей ошибки. Код был отформатирован по рекомендациям Embarcadero. Параметры функций и глобальные идентификаторы получили префиксы, позволяющие однозначно отличить их от похожих локальных переменных. Приватные поля классов отделены от одноименных публичных свойств, названия некоторых полей стали более "говорящими". Для многих методов и функций добавлены комментарии с подробным описанием, зачем так сделано, когда и как это использовать. В общей сложности было переписано больше сотни тысяч строк кода.

В результате удалось устранить утечки памяти, сбои в работе и при завершении работы. Сервер стал работать стабильно под нагрузкой, на порядок выше максимальной даже в отладочном режиме, без оптимизаций, но с огромным количеством проверок. Коллега создал тестовые прошивки для устройств связи, которые генерируют сплошной поток извещений приборов. Многодневный прогон показал, что сервер стабильно отрабатывает все извещения и даже остается еще большой запас мощности, ошибок и утечек не обнаружено. Но слабым звеном теперь является клиентская программа - рабочее место дежурного оператора.

Отладка программы "Дежурный Оператор"

Была произведена замена одинаковых участков кода на вызов функций или методов. Это сразу значительно упростило код и позволило исправить некоторые ошибки, связанные с логикой работы. В некоторых случаях, функционал дублировался в разных местах с некоторыми отличиями, что создавало неоднозначное поведение программы в разных ситуациях. Например, во многих местах при обработке событий были проверки, что событие имеет признаки тревоги. Например, преждевременное снятие с охраны или отключение охраны без ключа. Эти проверки были перенесены в сервер, и в состоянии охраняемого объекта возникает тревога даже если событие не является тревожным с точки зрения прибора. И если в охраняемом объекте включилась тревога, то отключить (отработать) ее может только оператор. Отключение тревоги на самом приборе отключает только тревогу прибора, а у дежурного оператора тревога остается. И наоборот, если оператор отключил тревогу, но тревога остается на приборе, то через короткое время тревога повторится. То есть, для снятия тревоги ее нужно отключить и на приборе, и у дежурного оператора. А поскольку состояние тревоги хранится и меняется на сервере, то невозможно сбросить тревогу просто закрыв программу оператора.

Еще была проблема, что операторы ставили 16-й размер шрифта, из-за чего в карточке объекта текст не влезал в отведенные поля ни по длине, ни по высоте. Просто изменить высоту поля текста недостаточно, нужно менять положение полей так, чтобы между полями оставались одинаковые промежутки и чтобы общий макет формы не нарушался, и чтобы элементы не уходили за пределы экрана. Был сделан каскадный выравниватель полей, который начиная с верхнего ряда полей устанавливал нужную высоту в зависимости от размера шрифта, затем подгонял положение и размер следующего ряда, и так далее. С учетом многорядных элементов (многострочных полей, блоков) и фиксированных элементов (кнопки, картинки). Для некоторых полей и таблиц добавлена регулировка размеров пользователем с сохранением-восстановлением размера при закрытии-открытии формы. Теперь форма нормально отображается с размерами шрифта от 8 до 20, ее можно подогнать под любой размер и формат экрана.

Еще была проблема с отображением актуального состояния объектов и приборов в визуальных элементах (таблицы, поля). Состояние хранилось в 4-х местах:
  • в объектах RF-Link, которые хранятся на сервере и передаются оператору по сети
  • в таблице сведений, поля которой соответствуют отображаемым полям визуальных таблиц и форм, и вычисляются из полей объектов RF-Link
  • визуальная таблица содержит копии строк таблицы сведений, которые обновляются при каждом обновлении визуальной таблицы
  • визуальные поля
Каждый раз при получении нового состояния объекта происходило принудительное обновление таблицы сведений и визуальных полей. Я сделал иначе:
  • таблицу сведений заменил на коллекцию инфообъектов, которые содержат ссылку на объект RF-Link, вычисляемые поля (текст статуса, иконки состояния и уровня сигнала) и некоторые свойства визуального списка (отмечен, развернут, режим сортировки). 
  • в визуальных таблицах ссылки на инфообъекты, списки и поля виртуальны, при отображении берут текст и значки из инфообъектов
  • в формах ссылки на объекты RF-Link
В результате единственным местом хранения состояния является объект RF-Link, а остальное на него ссылается. При изменении состояния объекта нет нужды обновлять информацию в других местах - все обновится при перерисовке. Саму перерисовку можно делать не сразу, а устанавливать признак необходимости перерисовки и выполнять по таймеру, не чаще 5  раз в секунду. Тогда при получении 1000 событий в секунду будет не 1000 перерисовок, а только 5. И отображаемая информация всегда соответствует хранимой. Заодно значительно уменьшился объем кода и быстродействие, поскольку при обработке событий визуальных элементов сразу доступна ссылка на объект RF-Link и его не нужно искать в списке по коду.

Доработка программы "Конфигуратор"

Конфигуратор был хорошо спроектирован, все редактируемые объекты имели форму списка и форму редактирования. Все действия над объектами (создание, изменение, удаление) сохраняются в стек действий. Действия могут быть вложенными - например, после создания охраняемого объекта внутри него создаются ключи и шлейфы. При отмене действия создания, удаляется создаваемый объект и все дочерние действия и объекты. Но есть нюансы. Ключи и шлейфы привязаны не только к охраняемому объекту, но и к прибору. А ответственные лица могут быть общими у разных охраняемых объектов. Часть нюансов появилась уже после проектирования и их поведение прописывалось для каждого случая как исключение из общей системы. Чтобы нюансы сделать частью системы, пришлось изменить некоторые принципы работы стека действий. 

Одной из главных проблем было удаление объектов, на которые есть ссылки (указатели). После удаления объекта эти ссылки становились недействительными, переход по ним приводил к ошибкам Access Violation. В качестве быстрого решения я заменил многие указатели на "слабые ссылки" - маленькие вспомогательные объекты, которые содержат указатель на объект RF-Link и счетчик использований (Reference Counter). У каждого объекта RF-Link можно получить "слабую ссылку", которая всегда достоверно знает, что объект существует. Когда "слабая ссылка" не используется (счетчик обращений равен нулю), то она самоудаляется. Это позволило обнаружить и исправить очень много неприятных ошибок.

Очередной проблемой было удаление подчиненных элементов при удалении элемента-владельца. В архитектуре программы не было предусмотрено четкой иерархии подчинения, элементы ссылались друг на друга произвольным образом. Но есть четкое разделение глобальных (где все элементы) и подчиненных списков. В глобальные списки был добавлен список ссылок на подчиненные списки. При создании или удалении подчиненного списка он ссылку на себя сообщает глобальному списку. В результате, при удалении элемента в глобальном списке, элемент автоматически удалялся из всех подчиненных списков.

Распределение нагрузки

Проведенные доработки не только увеличили надежность, но и значительно снизили потребление ресурсов компьютера, что позволило во много раз увеличить количество охранных приборов и объектов. Слабым местом стал сам дежурный оператор (живой человек), которому приходилось вручную отрабатывать ситуации, происходящие на тысячах объектов. Потребовалось автоматически и полуавтоматически распределять объекты по нескольким операторам так, чтобы при отключении оператора его объекты переходили к другому оператору. Главным требованием было не терять ключевые события при смене операторов. Для этого все состояния объектов хранились и изменялись на сервере, а все действия операторов по изменению состояний передавались на сервер. Оператор не мог локально изменить состояние - он отправлял команду на сервер, сервер эту команду обрабатывал и возвращал новое состояние или сообщение об ошибке. В этом случае, если оператор "Альфа" не будет отрабатывать возникшую ситуацию и закроет программу, оператор "Бета" принудительно увидит все объекты и ситуации оператора "Альфа" и сможет их отработать. И наоборот, если оператор "Альфа" открыл программу, то часть объектов и ситуаций оператора "Бета" перейдет к нему.

Унификация

Внутри программы есть много похожих функций и данных. У некоторых функций внутри много вариаций для разных каналов связи и приборов. И это помимо того, что у каждой модели прибора и канала связи есть свой драйвер в виде динамически подключаемой библиотеки. Чтобы не путаться в похожих вещах, все похожее было объединено. Вместо разных видов номеров (приборов, станций, формуляров, телефонов, MAC, IMEI, каналов) были сделаны текстовые адреса, похожие на URL. Адрес состоит из префикса, номера станции, номера прибора и доп.номера. Части адреса кроме префикса могут быть пропущены, где-то достаточно только номера станции, где-то только номера прибора. Префикс определяет тип адреса, как его расшифровывать. По адресу можно однозначно идентифицировать прибор. У прибора может быть несколько адресов для разных каналов связи.

Подключаемые библиотеки драйверов приборов были доработаны так, чтобы в одном драйвере могло быть несколько моделей приборов. Состав свойств моделей был расширен. У моделей приборов появились простые четырехбуквенные обозначения вместо привязки к строке базы данных с именем файла драйвера. Был добавлен встроенный универсальный обработчик "по умолчанию" для самого ходового протокола, который работает при отсутствии внешнего драйвера. Также предусмотрен протокол для сообщений не требующих расшифровки и обработки - обычно это сообщения самих драйверов связи. Предусмотрены две системы команд управления драйверами - простая с фиксированным набором кодов-команд и расширенная с любыми текстовыми командами. Пакеты извещений разделены на метаданные и данные, данные больше не передаются в не предназначенных для них полях. API и код драйверов стал значительно меньше и проще. Убраны обратные вызовы из драйвера в сервер. При необходимости вернуть из драйвера несколько результатов обработки одного извещения, или запросить дополнительные данные, для результата просто устанавливается признак "требуется повтор" или "требуются данные". Сервер еще раз вызовет функцию драйвера с теми же параметрами или передаст в нее запрашиваемые данные. Это также позволило делать отладку средствами среды разработки на всем пути обработки событий.

Введение универсальной адресации приборов и доработка API драйверов сделала возможным удаленное управление драйверами связи с рабочего места оператора или конфигуратора и упростило настройку прибора в системе. Больше не нужно настраивать формуляр прибора в двух разных программах. Формуляр и состояние прибора, которые раньше хранились в драйвере связи, в драйвере прибора, в сервере и в клиенте и постоянно синхронизировались по общему номеру прибора, теперь хранятся только в сервере. Драйвера могут хранить формуляр прибора, но в этом нет надобности, поскольку вся необходимая для обращения к прибору информация есть в строке адреса. Драйверу нужно только извлечь из нее нужную часть.

Многоканальность

Очередным требованием к системе стала поддержка приема сообщений по нескольким каналам связи одновременно и независимо. То есть, если прибор подключен через модем и по радио, то прослушивать оба канала одновременно и отбрасывать дубликаты сообщений. Благодаря новой адресации приборов и метаданным можно легко определить, по какому каналу и протоколу пришло извещение и куда можно отправить команду или подтверждение (если в этом есть необходимость). Для тестирования и нагрузочных испытаний многоканальности, программный эмулятор приборов был доработан, в него добавлена эмуляция блока радиоприемников, подключаемого к серверу по IP. Получается, что извещения от приборов идут как напрямую в сервер по UDP, так и через эмулятор радиоприемника в драйвер радиоканала сервера.

Переход на базу данных SQLite

Чтобы преодолеть ограничения размера файла БД Access в 2 Гб и несовместимость с другими платформами, была использована портативная кроссплатформенная БД SQLite. Написан модуль данных, в котором создается новая база с нуля, заполняется значениями по умолчанию. Все функции чтения-записи элементов в базу данных Access продублированы для базы SQLite. Тесты показали, что SQLite работает в среднем в 2 раза быстрее, чем Access. Написан модуль переноса данных из базы Access в базу SQLite. Были некоторые проблемы с несоответствием типов данных, в SQLite нет типов DateTime и Boolean. В языке запросов тоже есть отличия, некоторые запросы в отчетах пришлось доработать.

GPRS / УС-А 2.0

Устранение узких мест в драйверах связи позволило перенести из УС-А в драйвер функционал по организации сеансов связи с удаленными приборами. УС-А при этом работает как набор модемов GPRS/3G. Также можно подключаться к сети оператора через любой системный сетевой интерфейс - Ethernet, WiFi, USB-модемы. Чтобы проблемы сетевого интерфейса не влияли на работу драйвера и сервера, внутри драйвера сделано несколько независимых модулей - "отсеков", обмен данными между которыми происходит через очереди команд и сообщений. Каждый модуль работает по "тикам" таймера, за один тик выполняется проверка состояния, прием-обработка-передача порции сообщений из очереди, опрос портов, и прочие короткие операции. Изменяя промежуток между тиками можно изменять интенсивность работы модуля, при большом количестве сообщений в очереди можно уменьшить промежутки для ускорения обработки, а в случае перегрузки системы увеличить промежутки для снижения нагрузки. Кроме того, тики можно запускать в отдельном потоке, тем самым распаралеливая обработку событий. Время старта тика фиксируется, и если тик выполняется слишком долго, то его можно принудительно сбросить и запустить заново. Так же можно принудительно остановить и выгрузить из памяти весь драйвер, не останавливая сервер. Все происходящее в драйвере отражается в графическом интерфейсе, обновляясь два раза в секунду. Нагрузка на процессор при этом ничтожна.


Comments