Table of Contents

arctrm для разработчика

1. Назначение

ArctrmModule - модуль архивации температурных значений. Он читает значения датчиков из тегов уже настроенных модулей оборудования, сохраняет периодические срезы в базу Firebird и пишет события изменения состояния связи с устройствами и потери актуальности данных подвесов.

Сам модуль не реализует протокол обмена с оборудованием. Доступ к данным выполняется через Ref: каждый Sensor ссылается на тег вида <deviceName>:Pdv<num>.T<sensorIndex>, Podves проверяет актуальность данных по <deviceName>:Pdv<num>.Time, а ArctrmDevice контролирует состояние устройства через <deviceName>:SYSTEM.ErrorFlag. По коду видно, что конфигурация устройств берется из секции promauto.termo5; тип promauto.termo5 создается периферийным плагином как PaTermo5Module.

Описание плагина в ArctrmPlugin.getPluginDescription() - termo value archiver.

2. Архитектура

Модуль состоит из жизненного цикла JRoboPLC, объектной модели устройств и сервиса доступа к базе данных.

Класс Ответственность
ArctrmPlugin Регистрирует плагин с именем arctrm, создает ArctrmModule и вызывает загрузку конфигурации.
ArctrmModule Управляет загрузкой, подготовкой, инициализацией, циклом выполнения, записью архива, записью событий, connected-тегом, остановом и reload.
ArctrmDataService Инкапсулирует работу с БД: поиск модуля базы, проверку Firebird, загрузку и выполнение скрипта, синхронизацию PODVES, запись TEMPER и EVENTLOG, удаление старых архивных строк и учет текущего размера архива.
ArctrmDevice Описывает одно настроенное устройство, содержит список Podves, читает SYSTEM.ErrorFlag и пишет события изменения статуса устройства.
Podves Описывает один подвес устройства, проверяет актуальность данных по тегу Pdv<num>.Time, содержит список Sensor, синхронизирует запись в PODVES, сохраняет строку архива с актуальными данными и формирует событие потери актуальности.
Sensor Ссылается на температурный тег, читает целочисленное значение и заменяет отсутствующие или выходящие за диапазон значения специальными кодами.
Duration Разбирает строковый период и выравнивает дату-время на границу периода.
Constants Содержит коды событий, специальные значения датчиков и предел актуальности данных подвеса.
Defaults Содержит значения конфигурации по умолчанию.
Params Пустой класс; в текущей реализации не используется.
ArctrmException Проверяемое исключение для ошибок конфигурации, подготовки и инициализации Arctrm.

Прямое подключение к базе выполняет только ArctrmDataService. Остальная часть модуля работает с объектами предметной модели и теговыми ссылками.

3. Структура объектов

Иерархия объектов во время работы:

ArctrmModule
└── ArctrmDevice
    └── Podves
        └── Sensor

ArctrmModule хранит все устройства в списке devices. Список создается в конструкторе и заполняется в loadModule() вызовом ArctrmDevice.load(…).

ArctrmDevice создается по одной записи из карты promauto.termo5. Ключ записи становится именем устройства и используется как имя модуля, из которого читаются теги. Устройство создает:

Podves создается по записи внутри конфигурации устройства. Номер подвеса получается из ключа конфигурации через entry.getKey().substring(1) и Integer.parseInt(…). Например, ключ p3 дает номер 3. Имя подвеса формируется как <deviceName>.<podvesNum>, а значение конфигурации сохраняется как описание descr. Кроме списка датчиков подвес создает ссылку refTime на тег <deviceName>:Pdv<num>.Time и хранит признаки dataValid и needSaveEvent.

Sensor создается для каждого индекса от 0 до sensorCount - 1. Имя тега строится так:

String tagname = String.format("Pdv%d.T%d", num, i);

Ссылка датчика указывает на <deviceName>:Pdv<num>.T<i>.

Связь с базой данных строится через PODVES.ID: после syncPodves(…) объект Podves сохраняет идентификатор в поле id, а строки TEMPER ссылаются на него через PODVES_ID.

4. Принцип работы

Загрузка конфигурации

ArctrmModule.loadModule(conf) читает параметры модуля, проверяет базовую корректность значений и создает тег connected.

Параметр Куда сохраняется Проверка
database svc.databaseModuleName Проверяется позже в ArctrmDataService.prepare(…).
schema svc.schema Специальной проверки в loadModule() нет.
size svc.maxRecordCount Должен быть больше 0.
period periodMs Разбирается через Duration.parseMillis(…); результат не должен быть 0.
sensorCount sensorCount Должен быть больше 0.
promauto.termo5 devices Передается в ArctrmDevice.load(…); ошибки конфигурации приводят к false.

Общие параметры enable, debug.logging, func.tags, tag.values, tag.flags и флаги модуля обрабатываются в AbstractModule.load(…). В самом Arctrm явно используется enable: при выключенном модуле getInfo() возвращает disabled, а унаследованный жизненный цикл пропускает подготовку и выполнение.

Подготовка базы и ссылок

prepareModule() вызывает svc.prepare(sensorCount). Сервис:

  1. Ищет модуль базы данных по имени database.
  2. Проверяет, что найденный модуль является FirebirdDatabaseModule.
  3. Загружает ресурс dbscr/dbscr.arctrm.yml.
  4. Формирует имена таблиц EVENTLOG, PODVES, TEMPER с учетом схемы.
  5. Генерирует SQL вставки в TEMPER с колонками T0..T<n-1>.

После подготовки сервиса модуль вызывает prepare() у каждого устройства. Устройства подготавливают ссылки на SYSTEM.ErrorFlag, а подвесы - ссылки всех датчиков и Pdv<num>.Time.

В конце подготовки выставляется needInit = true, а lastDt сбрасывается в null.

Инициализация

Инициализация выполняется в начале рабочего цикла при наличии подключения к базе и только если needInit == true.

init() выполняет следующие шаги:

  1. svc.init(sensorCount) выполняет скрипт arctrm.init1, добавляет отсутствующие колонки датчиков в TEMPER, загружает recordCount и сбрасывает счетчики неподтвержденных операций.
  2. Каждое устройство вызывает device.init(svc), а каждый подвес синхронизирует себя с таблицей PODVES.
  3. lastDt загружается запросом последней строки TEMPER по убыванию ID.
  4. В EVENTLOG пишется событие EVENT_ARCTRM_INIT с пустым сообщением.
  5. Выполняется svc.commit().
  6. needInit сбрасывается в false.

Если в TEMPER нет строк, getLastDt() возвращает LocalDateTime.MIN.

Цикл выполнения

Каждый вызов executeModule() начинается с проверки svc.isConnected(). Если база недоступна, модуль выключает тег connected, ставит needInit = true и возвращает false.

При доступной базе цикл такой:

  1. Выполняется отложенная инициализация.
  2. Тег connected устанавливается в true.
  3. Берется серверное время базы через svc.now().
  4. Время округляется вниз до границы периода через Duration.floorToPeriod(…).
  5. Все устройства и подвесы обновляют состояние; датчики опрашиваются только у подвесов с актуальным значением Pdv<num>.Time.
  6. Если рассчитанный dt отличается от lastDt, сохраняется архивный срез и выполняется очистка старых строк.
  7. Для каждого устройства проверяется состояние связи и при изменении пишется событие в EVENTLOG.
  8. Выполняется svc.commit().
  9. lastDt обновляется текущим выровненным временем.

Сохранение данных

При наступлении нового периода каждое устройство вызывает savePodves(…). Подвес с актуальными данными собирает значения своих датчиков и передает их в svc.saveTemper(dt, id, values); в базу пишется одна строка TEMPER. Если ссылка на Pdv<num>.Time не найдена или значение находится вне диапазона 0..600, строка для этого подвеса не создается.

События состояния устройства сохраняются отдельно через svc.saveEvent(…). Событие пишется только при изменении статуса относительно lastStatus. Событие EVENT_PODVES_UPDATE_STALE формируется при первой потере актуальности данных подвеса и сохраняется вызовом Podves.save(…) при наступлении следующего архивного периода.

Останов и reload

closedownModule() при включенном модуле устанавливает connected в false. Закрытие соединения с базой в этом методе не выполняется.

reload() создает временный ArctrmModule, загружает новую конфигурацию, вызывает closedown() у текущего экземпляра, копирует унаследованные настройки, заменяет svc, devices, periodMs, sensorCount и снова вызывает prepare(). Явного переноса старых значений тегов в этом методе нет.

5. Структура базы данных

Схема создается скриптом src/main/resources/dbscr/dbscr.arctrm.yml, который загружается как ресурс и выполняется под именем arctrm.init1.

Таблица ''EVENTLOG''

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

CREATE TABLE {schema}EVENTLOG (
    ID          INTEGER GENERATED BY DEFAULT AS IDENTITY CONSTRAINT {schema}PK_EVENTLOG PRIMARY KEY,
    DT          TIMESTAMP,
    EVENT_CODE  SMALLINT NOT NULL,
    MESSAGE     VARCHAR(128)
)
Поле Назначение
ID Первичный ключ.
DT Время события; при записи берется db.getServerDatetime().
EVENT_CODE Код события из Constants.
MESSAGE Сообщение; для статуса устройства используется имя устройства, для потери актуальности - имя подвеса, для инициализации - пустая строка.

Индекс: IX_EVENTLOG_DT по полю DT.

Коды событий, видимые из кода:

Константа Значение Где используется
EVENT_ARCTRM_INIT 100 Записывается при инициализации Arctrm.
EVENT_DEVICE_CONNECTED 99 Записывается при переходе устройства в состояние связи.
EVENT_DEVICE_DISCONNECTED 1 Записывается, если SYSTEM.ErrorFlag == true.
EVENT_DEVICE_NOLINK 0 Записывается, если ссылка на SYSTEM.ErrorFlag не найдена и предыдущий статус имел другой код.
EVENT_PODVES_UPDATE_STALE 3 Записывается при первой потере актуальности данных подвеса; в MESSAGE передается имя <deviceName>.<podvesNum>.

Очистка EVENTLOG в коде не реализована.

Таблица ''PODVES''

Назначение: справочник настроенных подвесов.

CREATE TABLE {schema}PODVES (
    ID          INTEGER GENERATED BY DEFAULT AS IDENTITY CONSTRAINT {schema}PK_PODVES PRIMARY KEY,
    NAME        VARCHAR(32) NOT NULL CONSTRAINT {schema}UQ_PODVES_NAME UNIQUE,
    DESCR       VARCHAR(64)
)
Поле Назначение
ID Первичный ключ, сохраняется в Podves.id.
NAME Уникальное имя подвеса в формате <deviceName>.<podvesNum>.
DESCR Описание из конфигурации.

Синхронизация выполняется через Firebird-запрос:

UPDATE OR INSERT INTO <PODVES> (name, descr)
VALUES (?, ?)
matching (name)
returning id

Удаление строк PODVES, исчезнувших из конфигурации, не реализовано.

Таблица ''TEMPER''

Назначение: архив температурных срезов.

CREATE TABLE {schema}TEMPER (
    ID          BIGINT GENERATED BY DEFAULT AS IDENTITY CONSTRAINT {schema}PK_TEMPER PRIMARY KEY,
    PODVES_ID   INTEGER CONSTRAINT {schema}FK_TEMPER_PODVES REFERENCES {schema}PODVES ON DELETE CASCADE ON UPDATE CASCADE,
    DT          TIMESTAMP
)
Поле Назначение
ID Первичный ключ строки архива.
PODVES_ID Ссылка на PODVES.ID.
DT Выровненная метка времени архивного периода.
T0..T<n-1> Динамические целочисленные колонки значений датчиков.

Индекс: IX_TEMPER_DT по полю DT.

Колонки датчиков добавляются в ArctrmDataService.ensureSensorColumns(sensorCount):

ALTER TABLE <TEMPER> ADD T<i> INTEGER

Если колонка уже есть, она не создается повторно.

6. Алгоритм архивирования

Опрос датчиков

На каждом цикле выполнения вызывается цепочка:

ArctrmModule.executeModule()
  -> ArctrmDevice.update()
     -> Podves.update()
        -> Podves.checkTimeValid()
        -> Sensor.update() при актуальных данных

Сначала Podves.update() проверяет ссылку Pdv<num>.Time. Данные считаются актуальными, если ссылка существует, а значение тега находится в диапазоне от 0 до PODVES_UPDATE_TIME_LIMIT = 600 включительно. При неактуальных данных Sensor.update() не вызывается и значения датчиков остаются прежними.

Для подвеса с актуальными данными Sensor.update() работает с теговой ссылкой:

Условие Записываемое значение
Тег не найден через ref.linkIfNotValid() VALUE_NOLINK = 3013
Значение меньше -1000 VALUE_SHORTAGE = 3010
Значение больше 2000 VALUE_BROKEN = 3011
Значение в диапазоне Исходное целое значение тега

Исходное значение, вышедшее за допустимый диапазон, отдельно не сохраняется.

Формирование времени

Время архива берется от базы данных:

LocalDateTime dt = Duration.floorToPeriod(svc.now(), periodMs);

svc.now() возвращает db.getServerDatetime(). JVM-время для архивной метки не используется.

Duration.floorToPeriod(…) считает миллисекунды от 1970-01-01T00:00 и округляет вниз до границы периода.

Период архива

Строковый период разбирается в Duration.parseMillis(…).

Суффикс Единица
ms миллисекунды
s секунды
m минуты
h часы
d дни
нет суффикса миллисекунды

null, пустая строка, отрицательное значение, неверное число и переполнение приводят к IllegalArgumentException. Значение 0 дополнительно запрещено в ArctrmModule.loadModule().

Запись архива

Архивные строки пишутся только если рассчитанный dt отличается от lastDt. При записи:

Формат подготовленного SQL строится один раз при подготовке:

INSERT INTO <TEMPER> (podves_id, dt, T0, T1, ...)
VALUES (?, ?, ?, ?, ...)

Циклическое обслуживание архива

При инициализации recordCount загружается запросом select count(*) from <TEMPER>. Каждая вставка увеличивает uncommitedCount.

Перед фиксацией нового архивного среза рассчитывается число удаляемых строк:

deleteCount = max(0, recordCount + uncommitedCount - maxRecordCount)

Если deleteCount > 0, выполняется удаление старейших строк по ID:

DELETE FROM <TEMPER> ORDER BY id ROWS <deleteCount>

После успешного commit() счетчик обновляется:

recordCount = recordCount + uncommitedCount - deleteCount

rollback() сбрасывает uncommitedCount и deleteCount, но не пересчитывает recordCount из базы.

7. Конфигурация

Параметры Arctrm, читаемые из loadModule():

Параметр Значение по умолчанию Назначение Проверка
database db Имя модуля базы данных. В prepare() модуль должен существовать и быть FirebirdDatabaseModule.
schema AT Схема/префикс объектов БД. Отдельной проверки нет.
size 1_000_000 Максимальное количество строк в TEMPER. Должно быть больше 0.
period 1h Период архивирования. Должен корректно разобраться и быть больше 0.
sensorCount 6 Количество датчиков на каждый подвес. Должно быть больше 0.
promauto.termo5 нет в Defaults Карта устройств и подвесов. Ошибки разбора приводят к отказу загрузки.
enable true в AbstractModule Общий флаг включения модуля. Обрабатывается базовым классом.

Пример конфигурации

plugin.arctrm:
  module.arctrm:
    database:   db
    period:     5s
    size:       30 #604800
    schema:        AT
    sensorCount:   6

    promauto.termo5:
      mytrm1:
        p0: 231 силос, корпус 2 (гос.резерв)
        p5: 232 силос, корпус 2.

      mytrm2:
        p1: 401 силос, корпус 4.

В этом примере plugin.arctrm и module.arctrm соответствуют схеме загрузки конфигурации: ConfigurationYaml.getModuleConf(…) ищет plugin.<pluginName> и внутри него module.<moduleName>. Значения database, period, size, schema и sensorCount читаются в ArctrmModule.loadModule().

Секция promauto.termo5 передается в ArctrmDevice.load(…). Ключи mytrm1 и mytrm2 становятся именами модулей-источников тегов. Ключи p0, p5 и p1 дают номера подвесов 0, 5 и 1, потому что код берет часть ключа после первого символа и разбирает ее как число. При sensorCount: 6 для каждого подвеса создаются ссылки на теги Pdv<num>.T0Pdv<num>.T5, а также ссылка на Pdv<num>.Time.

Комментарий #604800 в строке size является YAML-комментарием и не участвует в значении параметра. Активное значение в примере - 30.

Пример выше предоставлен как документационный пример и соответствует фактическому разбору конфигурации в коде. Готовый Arctrm-конфиг в просмотренных файлах репозитория не найден.

8. Диагностика

getInfo() возвращает краткую строку состояния.

Если модуль выключен:

disabled

Для включенного модуля формат такой:

<NOT CONNECTED! >devices=<n> podves=<n> sensors=<n>< nolinkSensors=<n>> cursize=<n> last=<time|never>
Поле Значение
NOT CONNECTED! Добавляется через ANSI.redBold(…), если tagConnected.getBool() == false.
devices Количество объектов в devices.
podves Сумма device.podvess.size() по всем устройствам.
sensors Сумма podves.sensors.size() по всем подвесам.
nolinkSensors Показывается только если есть датчики со значением VALUE_NOLINK; фрагмент подсвечивается ANSI.redBold(…).
cursize Текущий кэшированный размер TEMPER, возвращаемый svc.getRecordCount().
last never, если lastDt == null или LocalDateTime.MIN; иначе дата в формате yyyy-MM-dd HH:mm:ss.

Имя модуля базы данных, период архива и максимальный размер архива в текущем getInfo() не выводятся.

9. Обработка ошибок

Ошибки базы данных

На этапе подготовки ArctrmDataService.prepare(…) выбрасывает ArctrmException в случаях:

prepareModule() перехватывает ArctrmException, пишет ошибку через env.printError(…) и возвращает false.

На этапе выполнения отсутствие подключения определяется через svc.isConnected(). В этом случае модуль:

  1. выключает connected;
  2. ставит needInit = true;
  3. возвращает false.

Ошибки связи с устройствами

Состояние устройства определяется по SYSTEM.ErrorFlag:

Условие Код события
Ссылка на SYSTEM.ErrorFlag не найдена EVENT_DEVICE_NOLINK = 0
Ссылка есть, ErrorFlag == true EVENT_DEVICE_DISCONNECTED = 1
Ссылка есть, ErrorFlag == false EVENT_DEVICE_CONNECTED = 99

Событие пишется только при изменении состояния относительно lastStatus. Поле lastStatus изначально равно 0, поэтому начальное состояние EVENT_DEVICE_NOLINK не записывается, а начальные состояния EVENT_DEVICE_DISCONNECTED и EVENT_DEVICE_CONNECTED записываются. Низкоуровневые ошибки обмена с оборудованием Arctrm не анализирует; они видны только через теги внешних модулей.

Неактуальные данные подвеса

Ссылка на Pdv<num>.Time считается неактуальной, если она не найдена либо содержит значение меньше 0 или больше 600. При переходе dataValid из null или true в false устанавливается needSaveEvent. До восстановления актуальности датчики не опрашиваются, а строка TEMPER для подвеса не записывается.

При наступлении следующего архивного периода Podves.save(…) записывает EVENT_PODVES_UPDATE_STALE = 3 с именем подвеса в MESSAGE и сбрасывает needSaveEvent. Повторное событие возможно только после восстановления актуальности и ее новой потери; отдельное событие восстановления не предусмотрено.

Некорректные значения датчиков

Значения ниже -1000 заменяются на VALUE_SHORTAGE = 3010, значения выше 2000 - на VALUE_BROKEN = 3011. Отдельные события и отдельное логирование для таких значений не реализованы.

Если тег датчика не найден, значение становится VALUE_NOLINK = 3013. Такие датчики дополнительно подсчитываются в getInfo().

Неожиданные исключения

Все исключения внутри основной части executeModule() перехватываются общим catch (Exception e). Модуль:

  1. пишет ошибку через env.printError(…);
  2. вызывает svc.rollback();
  3. выключает connected;
  4. устанавливает needInit = true;
  5. возвращает true.

10. Производительность

Реализация использует несколько простых оптимизаций.

Механизм Эффект
Генерация sqlTemperInsert один раз в prepare() Не строит SQL вставки на каждом архивном цикле.
openPreparedStatement(…) в saveTemper() Подготовленный PreparedStatement переиспользуется до commit() или rollback().
Запись только при смене периода Обычные циклы выполнения без нового периода не пишут строки TEMPER.
Кэш recordCount Не выполняет count(*) после каждой вставки.
Очистка только при превышении size Удаление старых строк запускается только при необходимости.
Однократная загрузка списка колонок TEMPER при init Позволяет добавить только отсутствующие T*-колонки.

JDBC batch-вставки не используются: при наступлении периода выполняется одна вставка на каждый подвес с актуальными данными.

11. Ограничения

Ограничение Основание в реализации
Поддерживается только Firebird. ArctrmDataService.prepare(…) требует FirebirdDatabaseModule.
sensorCount общий для всего модуля. Один параметр используется для всех Podves.
Удаление отсутствующих в конфигурации подвесов не выполняется. Есть update or insert, но нет удаления из PODVES.
EVENTLOG не очищается. В коде нет метода очистки этой таблицы.
Очистка TEMPER идет по ID, а не по DT. SQL: delete … order by id rows ….
Последняя дата архива ищется по максимальному ID, не по максимальному DT. SQL: select first 1 dt … order by id desc.
recordCount может устареть при внешних изменениях TEMPER. После init счетчик обновляется только внутренними commit/rollback.
Значение Sensor.value до первого опроса равно Java-значению по умолчанию 0. Поле int value не инициализируется явно.
Нет отдельного признака качества значения в TEMPER. Спецсостояния записываются числами в T*.
Штатный Arctrm-конфиг в просмотренных файлах не найден. Пример в этом документе является документационным и проверен по коду загрузки.
Назначение кодов событий описано только именами констант. Дополнительной документации в коде нет.

12. Возможные улучшения

Проверка по исходникам

Документ сверялся с текущей реализацией:

Места, где код не дает полной информации: