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). Сервис:
database.FirebirdDatabaseModule.dbscr/dbscr.arctrm.yml.EVENTLOG, PODVES, TEMPER с учетом схемы.TEMPER с колонками T0..T<n-1>.
После подготовки сервиса модуль вызывает prepare() у каждого устройства. Устройства подготавливают ссылки на SYSTEM.ErrorFlag, а подвесы - ссылки всех датчиков и Pdv<num>.Time.
В конце подготовки выставляется needInit = true, а lastDt сбрасывается в null.
Инициализация выполняется в начале рабочего цикла при наличии подключения к базе и только если needInit == true.
init() выполняет следующие шаги:
svc.init(sensorCount) выполняет скрипт arctrm.init1, добавляет отсутствующие колонки датчиков в TEMPER, загружает recordCount и сбрасывает счетчики неподтвержденных операций.device.init(svc), а каждый подвес синхронизирует себя с таблицей PODVES.lastDt загружается запросом последней строки TEMPER по убыванию ID.EVENTLOG пишется событие EVENT_ARCTRM_INIT с пустым сообщением.svc.commit().needInit сбрасывается в false.
Если в TEMPER нет строк, getLastDt() возвращает LocalDateTime.MIN.
Каждый вызов executeModule() начинается с проверки svc.isConnected(). Если база недоступна, модуль выключает тег connected, ставит needInit = true и возвращает false.
При доступной базе цикл такой:
connected устанавливается в true.svc.now().Duration.floorToPeriod(…).Pdv<num>.Time.dt отличается от lastDt, сохраняется архивный срез и выполняется очистка старых строк.EVENTLOG.svc.commit().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>.T0 … Pdv<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(). В этом случае модуль:
connected;needInit = true;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). Модуль:
env.printError(…);svc.rollback();connected;needInit = true;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.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.Места, где код не дает полной информации:
Pdv<num>.Time и SYSTEM.ErrorFlag.EVENTLOG, удалению старых PODVES и реакции на внешние изменения базы в коде не определены.