doc:jroboplc:modules:arctrm_dev

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

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.

Модуль состоит из жизненного цикла 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. Остальная часть модуля работает с объектами предметной модели и теговыми ссылками.

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

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

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

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

  • список podvess;
  • ссылку Ref на тег SYSTEM.ErrorFlag;
  • состояние lastStatus, по которому определяется необходимость записи события.

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.

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(…) при наступлении следующего архивного периода.

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

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

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

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

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 в коде не реализована.

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

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, исчезнувших из конфигурации, не реализовано.

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

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

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

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

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. При записи:

  • каждое устройство вызывает save(…) для всех своих подвесов;
  • каждый подвес с актуальными данными формирует список текущих значений датчиков;
  • ArctrmDataService.saveTemper(…) вставляет одну строку TEMPER для каждого подвеса с актуальными данными;
  • подвес с неактуальными данными не создает строку TEMPER, но при установленном needSaveEvent записывает событие EVENT_PODVES_UPDATE_STALE.

Формат подготовленного 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 из базы.

Параметры 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-конфиг в просмотренных файлах репозитория не найден.

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() не выводятся.

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

  • модуль базы данных не найден;
  • найденный модуль не является FirebirdDatabaseModule;
  • ресурс dbscr/dbscr.arctrm.yml не загружен.

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.

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

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

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

Ограничение Основание в реализации
Поддерживается только 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-конфиг в просмотренных файлах не найден. Пример в этом документе является документационным и проверен по коду загрузки.
Назначение кодов событий описано только именами констант. Дополнительной документации в коде нет.
  • Добавить тесты для Duration, Sensor.update(), getInfo(), смены статусов устройств и расчета recordCount.
  • Использовать batch-вставки для TEMPER, если число подвесов большое.
  • Добавить явные поля качества/статуса вместо кодирования состояний специальными числами в T*.
  • Реализовать политику очистки EVENTLOG.
  • Добавить синхронизацию удаления PODVES, если удаленные из конфигурации подвесы должны исчезать из справочника.
  • Загружать последнюю архивную метку по max(DT), если важна именно временная непрерывность, а не порядок вставки.
  • Периодически сверять recordCount с реальным количеством строк при возможных внешних изменениях базы.
  • Расширить getInfo(): показывать имя БД, период, лимит архива, последнюю ошибку или время последней успешной записи.

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

  • ArctrmModule.java;
  • ArctrmDataService.java;
  • ArctrmDevice.java;
  • Podves.java;
  • Sensor.java;
  • Constants.java;
  • Defaults.java;
  • Duration.java;
  • ArctrmPlugin.java;
  • ArctrmException.java;
  • Params.java;
  • src/main/resources/dbscr/dbscr.arctrm.yml;
  • DatabaseProtoServiceImpl.java;
  • AbstractModule.java;
  • Ref.java;
  • PeripherialPlugin.java;
  • PaTermo5Module.java.

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

  • Пример конфигурации предоставлен как документационный; штатный Arctrm-конфиг в просмотренных файлах репозитория не найден.
  • Бизнес-смысл событий известен только по именам констант.
  • Низкоуровневое поведение оборудования находится вне Arctrm; модуль видит только теги датчиков, Pdv<num>.Time и SYSTEM.ErrorFlag.
  • Требования к эксплуатационной очистке EVENTLOG, удалению старых PODVES и реакции на внешние изменения базы в коде не определены.
  • doc/jroboplc/modules/arctrm_dev.txt
  • Last modified: 2026/08/01 18:06
  • by denis