doc:jroboplc:modules:arctrm_dev

This is an old revision of the document!


Модуль Arctrm

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

Сам модуль не реализует протокол обмена с оборудованием. Доступ к данным выполняется через Ref: каждый Sensor ссылается на тег вида <deviceName>:Pdv<num>.T<sensorIndex>, а 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 Описывает один подвес устройства, содержит список Sensor, синхронизирует запись в PODVES, сохраняет одну строку архива со всеми своими датчиками.
Sensor Ссылается на температурный тег, читает целочисленное значение и заменяет отсутствующие или выходящие за диапазон значения специальными кодами.
Duration Разбирает строковый период и выравнивает дату-время на границу периода.
Constants Содержит коды событий и специальные значения датчиков.
Defaults Содержит значения конфигурации по умолчанию.
ArctrmException Проверяемое исключение для ошибок подготовки и инициализации Arctrm.
Context Пустой класс; в текущей реализации не используется.

Прямое подключение к базе выполняет только 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.

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, а подвесы - ссылки всех датчиков.

В конце подготовки выставляется 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. Все устройства, подвесы и датчики обновляют текущие значения.
  6. Если рассчитанный dt отличается от lastDt, сохраняется архивный срез и выполняется очистка старых строк.
  7. Для каждого устройства проверяется состояние связи и при изменении пишется событие в EVENTLOG.
  8. Выполняется svc.commit().
  9. lastDt обновляется текущим выровненным временем.

При наступлении нового периода каждое устройство вызывает savePodves(…), каждый подвес собирает значения своих датчиков и передает их в svc.saveTemper(dt, id, values). В базу пишется одна строка TEMPER на один подвес.

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

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 0 Записывается при инициализации Arctrm.
EVENT_CONNECTED 1 Записывается при переходе устройства в состояние связи.
EVENT_DISCONNECTED 2 Записывается, если SYSTEM.ErrorFlag == true.
EVENT_NOLINK 3 Записывается, если ссылка на SYSTEM.ErrorFlag не найдена.

Очистка 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()
        -> Sensor.update()

Sensor.update() работает только с теговой ссылкой:

Условие Записываемое значение
Тег не найден через ref.linkIfNotValid() VALUE_NOLINK = 3000
Значение меньше -1000 VALUE_BROKEN = 3010
Значение больше 2000 VALUE_SHORTAGE = 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. При записи:

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

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

Комментарий #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_NOLINK
Ссылка есть, ErrorFlag == true EVENT_DISCONNECTED
Ссылка есть, ErrorFlag == false EVENT_CONNECTED

Событие пишется только при изменении состояния относительно lastStatus. Низкоуровневые ошибки обмена с оборудованием Arctrm не анализирует; они видны только через теги внешних модулей.

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

Если тег датчика не найден, значение становится VALUE_NOLINK. Такие датчики дополнительно подсчитываются в 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;
  • Context.java;
  • src/main/resources/dbscr/dbscr.arctrm.yml;
  • DatabaseProtoServiceImpl.java;
  • AbstractModule.java;
  • Ref.java;
  • PeripherialPlugin.java;
  • PaTermo5Module.java.

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

  • Пример конфигурации предоставлен как документационный; штатный Arctrm-конфиг в просмотренных файлах репозитория не найден.
  • Бизнес-смысл событий известен только по именам констант.
  • Низкоуровневое поведение оборудования находится вне Arctrm; модуль видит только теги и SYSTEM.ErrorFlag.
  • Требования к эксплуатационной очистке EVENTLOG, удалению старых PODVES и реакции на внешние изменения базы в коде не определены.
  • doc/jroboplc/modules/arctrm_dev.1784745624.txt.gz
  • Last modified: 2026/07/22 21:40
  • by denis