This is an old revision of the document!
Описание модуля ''arctrm'' для разработчика
1. Назначение
ArctrmModule - модуль архивации температурных значений. Он читает значения датчиков из тегов уже настроенных модулей оборудования, сохраняет периодические срезы в базу Firebird и пишет события изменения состояния связи с устройствами.
Сам модуль не реализует протокол обмена с оборудованием. Доступ к данным выполняется через Ref: каждый Sensor ссылается на тег вида <deviceName>:Pdv<num>.T<sensorIndex>, а 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 | Описывает один подвес устройства, содержит список Sensor, синхронизирует запись в PODVES, сохраняет одну строку архива со всеми своими датчиками. |
Sensor | Ссылается на температурный тег, читает целочисленное значение и заменяет отсутствующие или выходящие за диапазон значения специальными кодами. |
Duration | Разбирает строковый период и выравнивает дату-время на границу периода. |
Constants | Содержит коды событий и специальные значения датчиков. |
Defaults | Содержит значения конфигурации по умолчанию. |
ArctrmException | Проверяемое исключение для ошибок подготовки и инициализации Arctrm. |
Context | Пустой класс; в текущей реализации не используется. |
Прямое подключение к базе выполняет только ArctrmDataService. Остальная часть модуля работает с объектами предметной модели и теговыми ссылками.
3. Структура объектов
Иерархия объектов во время работы:
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.
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). Сервис:
- Ищет модуль базы данных по имени
database. - Проверяет, что найденный модуль является
FirebirdDatabaseModule. - Загружает ресурс
dbscr/dbscr.arctrm.yml. - Формирует имена таблиц
EVENTLOG,PODVES,TEMPERс учетом схемы. - Генерирует SQL вставки в
TEMPERс колонкамиT0..T<n-1>.
После подготовки сервиса модуль вызывает prepare() у каждого устройства. Устройства подготавливают ссылки на SYSTEM.ErrorFlag, а подвесы - ссылки всех датчиков.
В конце подготовки выставляется 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(…). - Все устройства, подвесы и датчики обновляют текущие значения.
- Если рассчитанный
dtотличается отlastDt, сохраняется архивный срез и выполняется очистка старых строк. - Для каждого устройства проверяется состояние связи и при изменении пишется событие в
EVENTLOG. - Выполняется
svc.commit(). lastDtобновляется текущим выровненным временем.
Сохранение данных
При наступлении нового периода каждое устройство вызывает savePodves(…), каждый подвес собирает значения своих датчиков и передает их в svc.saveTemper(dt, id, values). В базу пишется одна строка TEMPER на один подвес.
События состояния устройства сохраняются отдельно через svc.saveEvent(…). Событие пишется только при изменении статуса относительно lastStatus.
Останов и 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 | 0 | Записывается при инициализации Arctrm. |
EVENT_CONNECTED | 1 | Записывается при переходе устройства в состояние связи. |
EVENT_DISCONNECTED | 2 | Записывается, если SYSTEM.ErrorFlag == true. |
EVENT_NOLINK | 3 | Записывается, если ссылка на SYSTEM.ErrorFlag не найдена. |
Очистка 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()
-> 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 из базы.
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>.T0 … Pdv<num>.T5.
Комментарий #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 в случаях:
- модуль базы данных не найден;
- найденный модуль не является
FirebirdDatabaseModule; - ресурс
dbscr/dbscr.arctrm.ymlне загружен.
prepareModule() перехватывает ArctrmException, пишет ошибку через env.printError(…) и возвращает false.
На этапе выполнения отсутствие подключения определяется через svc.isConnected(). В этом случае модуль:
- выключает
connected; - ставит
needInit = true; - возвращает
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). Модуль:
- пишет ошибку через
env.printError(…); - вызывает
svc.rollback(); - выключает
connected; - устанавливает
needInit = true; - возвращает
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. Возможные улучшения
- Добавить тесты для
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и реакции на внешние изменения базы в коде не определены.