doc:jroboplc:modules:arctrm

This is an old revision of the document!


= Arctrm Module =

1. Purpose

The `arctrm` plugin provides the `ArctrmModule`, a temperature value archiver for JRoboPLC. Its responsibility is to read temperature values from configured termo device tags, write periodic snapshots into a Firebird database, and record device connection status changes in an event log.

The module does not implement the hardware protocol itself. It reads values through `Ref` objects that point to tags exposed by other modules. In the inspected code, configuration is read from the `promauto.termo5` section, and the referenced hardware type `promauto.termo5` is created by the peripheral plugin as `PaTermo5Module`.

The plugin description returned by `ArctrmPlugin.getPluginDescription()` is `termo value archiver`.

2. High-level Architecture

The module is split into a small runtime layer, an object hierarchy for configured devices, and a database service.

`ArctrmPlugin` registers the plugin name `arctrm` and creates `ArctrmModule` instances. It delegates module loading to `ArctrmModule.load(conf)`.

`ArctrmModule` owns the module lifecycle. It loads configuration, creates the `connected` tag, prepares the database service and device references, initializes database structures, runs each execution cycle, writes archive records, writes status events, and handles reload/closedown.

`ArctrmDataService` extends `DatabaseProtoServiceImpl` and contains all direct database interaction. It resolves the configured database module, verifies that it is a `FirebirdDatabaseModule`, loads and executes the Arctrm database script, builds schema-qualified table names, creates missing sensor columns, synchronizes podves metadata, writes temperature rows and event rows, deletes old archive rows, and tracks the current archive record count.

`ArctrmDevice` represents one configured termo device. It owns a list of `Podves` objects and a reference to the device tag `SYSTEM.ErrorFlag`. It delegates value collection to its podves list and writes an event when its connection status changes.

`Podves` represents one podves channel group under a device. It owns a list of `Sensor` objects, synchronizes itself with the `PODVES` table, and writes one `TEMPER` archive row for all its sensor values.

`Sensor` represents one temperature tag reference. It links to a tag such as `Pdv0.T0`, reads the integer value, and normalizes missing or out-of-range values to module constants.

`Duration` parses period strings and aligns timestamps to a configured period boundary.

`Constants` defines event codes and special sensor values.

`Defaults` defines default configuration values.

`ArctrmException` is a checked exception used for module-specific preparation and initialization failures.

`Context` currently contains no fields or methods and is not used by the implementation.

3. Object Model

The runtime object hierarchy is:

ArctrmModule
+-- ArctrmDevice
    +-- Podves
        +-- Sensor

`ArctrmModule` owns all configured devices in the `devices` list. It creates this list during construction and fills it in `loadModule()` by calling `ArctrmDevice.load(…)`.

`ArctrmDevice` is created from one entry in the `promauto.termo5` configuration map. The entry key becomes the device name. The device creates one `Ref` to `<deviceName>:SYSTEM.ErrorFlag` and one `Podves` object for each configured podves entry.

`Podves` names are built as `<deviceName>.<podvesNum>`. The numeric `podvesNum` is parsed from the podves configuration key by taking `substring(1)` and parsing it as an integer. For example, a key such as `p3` becomes podves number `3`. The configuration value becomes the podves description.

`Sensor` instances are created inside each podves. For each sensor index from `0` to `sensorCount - 1`, `Podves` creates a tag name `Pdv<num>.T<index>` and the sensor links to `<deviceName>:<tagName>`.

The database model mirrors part of this object model:

PODVES
+-- TEMPER

Each `Podves` stores its database identifier in `Podves.id` after `ArctrmDataService.syncPodves(…)` returns the `PODVES.ID` value. Temperature archive rows in `TEMPER` refer to this identifier through `TEMPER.PODVES_ID`.

4. Execution Flow

Loading

`ArctrmModule.loadModule(conf)` reads module-specific configuration:

- `database` into `svc.databaseModuleName`; - `schema` into `svc.schema`; - `size` into `svc.maxRecordCount`; - `period`, parsed by `Duration.parseMillis(…)` into `periodMs`; - `sensorCount` into `sensorCount`; - `promauto.termo5`, passed to `ArctrmDevice.load(…)`.

`size` must be greater than zero. `period` must parse successfully and must not result in `0`. `sensorCount` must be greater than zero. Any exception while parsing the period or loading devices is logged through `env.printError(…)`, and loading returns `false`.

After successful configuration loading, the module creates a boolean tag named `connected` with initial value `false`.

The inherited `AbstractModule.load(…)` also reads common module settings such as `enable`, `debug.logging`, flags, function tags, initial tag values, and tag flags. The Arctrm code directly relies on `enable` in lifecycle handling and in `getInfo()`.

Preparation

`prepareModule()` calls `svc.prepare(sensorCount)`. The service resolves the configured database module, requires it to be a Firebird database module, loads `dbscr/dbscr.arctrm.yml`, builds table names for `EVENTLOG`, `PODVES`, and `TEMPER`, and builds the parameterized insert SQL for temperature rows.

After the service is prepared, each `ArctrmDevice` prepares its podves and its `SYSTEM.ErrorFlag` reference. Each `Podves` prepares all of its sensor references. `prepareModule()` then sets `needInit = true` and resets `lastDt` to `null`.

If `svc.prepare(…)` throws `ArctrmException`, the module logs the message and preparation returns `false`.

Initialization

`executeModule()` calls `init()` at the beginning of a connected execution cycle. `init()` returns immediately if `needInit` is `false`.

When initialization is required, `init()`:

1. Executes the database initialization script through `svc.init(sensorCount)`. 2. Synchronizes all podves through `device.init(svc)`. 3. Loads the last saved timestamp from `TEMPER` into `lastDt` through `svc.getLastDt()`. 4. Writes an initialization event with `Constants.EVENT_ARCTRM_INIT` and an empty message. 5. Commits the database transaction. 6. Sets `needInit = false`.

If no row exists in `TEMPER`, `svc.getLastDt()` returns `LocalDateTime.MIN`.

Execute Cycle

Each `executeModule()` cycle starts by checking `svc.isConnected()`. If the database is not connected, the module sets the `connected` tag off, sets `needInit = true`, and returns `false`.

When the database is connected, the module:

1. Runs `init()` if needed. 2. Sets the `connected` tag on. 3. Gets the database server datetime from `svc.now()`. 4. Floors that timestamp to the configured period with `Duration.floorToPeriod(…)`. 5. Updates every configured device, podves, and sensor. 6. If the floored timestamp differs from `lastDt`, writes one temperature row per podves and runs archive cleanup. 7. Writes device status events for devices whose status changed. 8. Commits the transaction. 9. Assigns the floored timestamp to `lastDt`.

All device and sensor communication in this flow is tag based. `Sensor.update()` calls `ref.linkIfNotValid()` and `ref.getInt()`; it does not communicate with serial ports or hardware protocols directly.

Exceptions During Execution

Any exception thrown inside the connected execution block is caught by `executeModule()`. The module logs the exception, rolls back the data service, turns the `connected` tag off, sets `needInit = true`, and then returns `true`.

Closedown

`closedownModule()` returns immediately when `enable` is `false`. Otherwise it sets the `connected` tag off and returns `true`. It does not close the database connection directly.

Reload

`reload()` creates a temporary `ArctrmModule`, loads it, calls `closedown()` on the current instance, copies inherited settings from the temporary module, replaces `svc`, `devices`, `periodMs`, and `sensorCount`, then calls `prepare()`.

The implementation does not copy old runtime values from the previous `connected` tag. It also does not explicitly move tags between tag tables during reload.

5. Database Schema

The schema is defined in `src/main/resources/dbscr/dbscr.arctrm.yml` and executed as script `arctrm.init1` with the configured `schema` parameter.

EVENTLOG

Purpose: stores module and device status events.

Fields:

- `ID INTEGER GENERATED BY DEFAULT AS IDENTITY`: primary key. - `DT TIMESTAMP`: event timestamp. - `EVENT_CODE SMALLINT NOT NULL`: event code from `Constants`. - `MESSAGE VARCHAR(128)`: event message.

Indexes:

- `IX_EVENTLOG_DT` on `DT`.

Writes:

- `ArctrmModule.init()` writes `EVENT_ARCTRM_INIT` with an empty message. - `ArctrmDevice.saveStatus()` writes `EVENT_CONNECTED`, `EVENT_DISCONNECTED`, or `EVENT_NOLINK` with the device name as the message when device status changes.

The code does not implement retention cleanup for `EVENTLOG`.

PODVES

Purpose: stores the configured podves names and descriptions.

Fields:

- `ID INTEGER GENERATED BY DEFAULT AS IDENTITY`: primary key. - `NAME VARCHAR(32) NOT NULL`: unique podves name, built as `<deviceName>.<podvesNum>`. - `DESCR VARCHAR(64)`: description from configuration.

Constraints:

- Unique constraint on `NAME`.

Writes:

- `ArctrmDataService.syncPodves(…)` uses Firebird `update or insert … matching (name) returning id` to insert or update the row and return `ID`.

The code does not delete `PODVES` rows that disappear from configuration.

TEMPER

Purpose: stores periodic temperature snapshots.

Fields from the database script:

- `ID BIGINT GENERATED BY DEFAULT AS IDENTITY`: primary key. - `PODVES_ID INTEGER`: foreign key to `PODVES`. - `DT TIMESTAMP`: archive timestamp.

Dynamic fields:

- `T0`, `T1`, …, `T<sensorCount - 1>` are added by `ArctrmDataService.ensureSensorColumns(sensorCount)` if they do not already exist. - Each dynamic field is created as `INTEGER`.

Relationships:

- `TEMPER.PODVES_ID` references `PODVES.ID` with `ON DELETE CASCADE ON UPDATE CASCADE`.

Indexes:

- `IX_TEMPER_DT` on `DT`.

Writes:

- `ArctrmDataService.saveTemper(…)` inserts one row per podves with `PODVES_ID`, `DT`, and all sensor values.

Cleanup:

- `ArctrmDataService.deleteOldRecords()` deletes oldest rows by `ID` when the cached record count plus pending inserts exceeds `maxRecordCount`.

6. Archive Algorithm

Sensor Collection

Each execution cycle calls `device.update()`, which calls `podves.update()`, which calls `sensor.update()` for every sensor.

`Sensor.update()` behavior:

- If the referenced tag cannot be linked, `value` becomes `Constants.VALUE_NOLINK` (`3000`). - If the referenced integer value is below `Sensor.VALUE_LIMIT_LOW` (`-1000`), `value` becomes `Constants.VALUE_BROKEN` (`3010`). - If the referenced integer value is above `Sensor.VALUE_LIMIT_HIGH` (`2000`), `value` becomes `Constants.VALUE_SHORTAGE` (`3011`). - Otherwise, the referenced integer value is stored unchanged.

The implementation does not preserve the raw sensor value when it is outside the allowed range.

Timestamp Generation

The module uses the database server time, not JVM local time, for archive timestamps. `ArctrmDataService.now()` returns `db.getServerDatetime()`.

The timestamp is aligned by `Duration.floorToPeriod(dateTime, periodMs)`. The alignment epoch is `1970-01-01T00:00`, and the method floors milliseconds from that epoch using `Math.floorDiv`.

Supported period suffixes are:

- `ms`: milliseconds; - `s`: seconds; - `m`: minutes; - `h`: hours; - `d`: days; - no suffix: milliseconds.

Negative periods, empty periods, null periods, invalid numeric values, and arithmetic overflow are rejected by `Duration.parseMillis(…)`. `ArctrmModule.loadModule(…)` additionally rejects a parsed period of `0`.

Write Timing

Temperature rows are written only when the current floored timestamp differs from `lastDt`.

During initialization, `lastDt` is loaded as the `DT` from the latest `TEMPER` row by descending `ID`. If there are no rows, `lastDt` is `LocalDateTime.MIN`. After a successful execution commit, `lastDt` is set to the current floored timestamp.

When a write is due, each device writes all of its podves. Each podves writes exactly one row containing all current sensor values.

Archive Maintenance

`ArctrmDataService.init(…)` initializes `recordCount` with `select count(*) from <TEMPER>`.

Every temperature insert increments `uncommitedCount`. Before commit, `deleteOldRecords()` computes:

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

If `deleteCount` is greater than zero, the service executes:

delete from <TEMPER> order by id rows <deleteCount>

After commit, the cached count is updated as:

recordCount = recordCount + uncommitedCount - deleteCount

Then `uncommitedCount` and `deleteCount` are reset to zero. Rollback also resets `uncommitedCount` and `deleteCount`, but it does not recompute `recordCount`.

7. Runtime Information

`ArctrmModule.getInfo()` returns runtime summary text.

If the module is disabled, it returns:

disabled

For enabled modules, the format is:

<optional NOT CONNECTED prefix>devices=<n> podves=<n> sensors=<n><optional nolinkSensors> cursize=<n> last=<timestamp-or-never>

Fields:

- `NOT CONNECTED!`: prepended with `ANSI.redBold(…)` when `tagConnected.getBool()` is `false`. - `devices`: current size of the `devices` list. - `podves`: sum of `device.podvess.size()` across all devices. - `sensors`: sum of `podves.sensors.size()` across all podves. - `nolinkSensors`: shown only when at least one sensor has `Sensor.getValue() == Constants.VALUE_NOLINK`; the whole ` nolinkSensors=<n>` fragment is wrapped in `ANSI.redBold(…)`. - `cursize`: current cached `TEMPER` record count from `svc.getRecordCount()`. - `last`: `never` when `lastDt` is `null` or `LocalDateTime.MIN`; otherwise `lastDt` formatted as `yyyy-MM-dd HH:mm:ss`.

The current implementation does not include the database module name in `getInfo()`.

8. Configuration

The module reads the following Arctrm-specific parameters in `ArctrmModule.loadModule(…)`.

database

Database module name.

Default: `db`.

Stored in `svc.databaseModuleName`. During preparation it must resolve to a module implementing `Database`, and it must be an instance of `FirebirdDatabaseModule`.

schema

Database schema prefix used by the database script and by table-name generation.

Default: `AT`.

Stored in `svc.schema` and passed to the database script as `schema=<value>`.

size

Maximum number of records retained in `TEMPER`.

Default: `1_000_000`.

The value must be greater than zero. It is stored in `svc.maxRecordCount`.

period

Archive period.

Default: `1h`.

The value is parsed by `Duration.parseMillis(…)`. Supported suffixes are `ms`, `s`, `m`, `h`, and `d`; values without suffix are interpreted as milliseconds. The parsed value must be greater than zero.

sensorCount

Number of temperature sensors created for each podves.

Default: `6`.

The value must be greater than zero. It controls how many `Sensor` objects are created for every podves and how many dynamic `T*` columns are required in `TEMPER`.

promauto.termo5

Device configuration map consumed by `ArctrmDevice.load(…)`.

Expected shape from implementation:

promauto.termo5:
  <deviceModuleName>:
    p0: <description>
    p1: <description>

The device map key is used as the referenced module name. Each podves key must have at least two characters and must contain a numeric suffix after the first character, because the code parses `entry.getKey().substring(1)` as an integer. The code does not require the first character to be `p`; it only ignores the first character and parses the rest.

The repository does not contain an Arctrm-specific sample configuration, so the exact production configuration shape beyond this parsing behavior is unknown.

enable

This is inherited from `AbstractModule`, not parsed in `ArctrmModule.loadModule(…)`.

Default: `true`.

When `enable` is false, inherited lifecycle methods skip preparation, execution, and closedown, and `getInfo()` returns `disabled`.

9. Error Handling

Database Connection Failures

During preparation:

- missing configured database module causes `ArctrmException(“Database module is not found: …”)`; - non-Firebird database causes `ArctrmException(“Database type must be Firebird: …”)`; - database script load failure causes `ArctrmException(“Failed to load database script”)`.

`prepareModule()` catches `ArctrmException`, logs the message, and returns `false`.

During execution:

- if `svc.isConnected()` is false, the module turns the `connected` tag off, sets `needInit = true`, and returns `false`; - `getInfo()` shows `NOT CONNECTED!` when the module's `connected` tag is false.

Communication Failures

Arctrm detects device communication state through the referenced `SYSTEM.ErrorFlag` tag:

- if the reference cannot be linked, device status is `EVENT_NOLINK`; - if the tag is linked and `refErrorFlag.getBool()` is true, status is `EVENT_DISCONNECTED`; - otherwise status is `EVENT_CONNECTED`.

`ArctrmDevice.saveStatus(…)` writes an event only when this status differs from `lastStatus`.

The module does not communicate with hardware directly and does not inspect serial-port errors. Those details belong to the referenced peripheral modules.

Invalid Sensor Values

`Sensor.update()` treats values below `-1000` as `VALUE_BROKEN` (`3010`) and values above `2000` as `VALUE_SHORTAGE` (`3011`). These constants are archived in the same `TEMPER.T*` fields as normal sensor values.

The implementation does not log invalid sensor values and does not write separate events for them.

If a sensor reference cannot be linked, `Sensor.update()` stores `VALUE_NOLINK` (`3000`). `getInfo()` counts sensors whose current value equals this constant and displays `nolinkSensors=<n>` only when the count is greater than zero.

Unexpected Exceptions

Unexpected exceptions during the connected execution block are caught by `executeModule()`. The module logs the exception, rolls back through `svc.rollback()`, turns the `connected` tag off, and marks `needInit = true` so the next connected cycle reinitializes.

10. Performance Considerations

The implementation contains several simple optimizations:

- The temperature insert SQL is generated once in `ArctrmDataService.prepare(…)` based on `sensorCount`. - `saveTemper(…)` uses a prepared statement through `openPreparedStatement(…)`; the inherited service reuses it until commit or rollback closes it. - Archive rows are written only when the period-aligned timestamp changes, not on every module execution cycle. - `recordCount` is cached after initialization and updated on commit instead of running `count(*)` after every insert. - Old records are deleted only when the cached count plus pending inserts exceeds `maxRecordCount`. - Existing `TEMPER` column names are loaded once during initialization before missing sensor columns are added.

The implementation does not use JDBC batch inserts. It executes one insert per podves when an archive write is due.

11. Known Limitations

- Only Firebird databases are supported; `ArctrmDataService.prepare(…)` rejects any database module that is not a `FirebirdDatabaseModule`. - `sensorCount` is global for the whole module. Different podves cannot have different sensor counts. - `PODVES` rows are inserted or updated, but rows removed from configuration are not deleted by Arctrm. - `EVENTLOG` has no cleanup logic. - `TEMPER` cleanup is based on ascending `ID`, not on `DT`. - The last saved timestamp is loaded from the row with the highest `ID`, not from the maximum `DT`. - The cached `recordCount` can become stale if another process modifies `TEMPER` after initialization. - `Sensor.value` defaults to Java's default integer value `0` until the first `Sensor.update()` call. - `ArctrmDevice.lastStatus` defaults to `0`, which is also `EVENT_ARCTRM_INIT`; therefore the first device status event is written unless the current status also equals `0`. Current device statuses are `1`, `2`, or `3`. - The module creates a `connected` tag with `tagtable.createBool(…)`; behavior when reloading and recreating this tag is governed by the tag table implementation, which is outside this module. - No Arctrm-specific automated tests or sample Arctrm module configuration were found in the inspected source tree. - The business meaning of event codes is limited to the constant names in `Constants`; no additional event-code documentation is present in the implementation.

12. Possible Improvements

- Add unit tests for `Duration`, `Sensor.update()`, `getInfo()`, device status transitions, and archive cleanup count updates. - Add an Arctrm sample configuration that shows the expected `promauto.termo5` structure. - Use JDBC batch inserts for archive writes when many podves are configured. - Add explicit quality/status columns or a separate status table instead of encoding sensor states as special integer values in `T*` fields. - Add retention or cleanup for `EVENTLOG`. - Add synchronization cleanup for `PODVES` rows no longer present in configuration, if that matches operational requirements. - Load the last archive timestamp by maximum `DT` if archive continuity should be timestamp-based rather than insertion-order based. - Periodically reconcile cached `recordCount` with the database if external writers or manual maintenance are expected. - Expose more runtime diagnostics, such as database module name, period, maximum archive size, last exception text, or event write counts.

Source Coverage Review

The documentation above was checked against the current implementation of:

- `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` - directly referenced behavior in `DatabaseProtoServiceImpl`, `Database`, `Ref`, and peripheral termo tag creation.

Sections with intentionally stated unknowns or limited evidence:

- Configuration: no Arctrm-specific sample configuration was found, so only the structure implied by `loadModule()` and `ArctrmDevice.load()` is documented. - Purpose: the code describes the plugin as `termo value archiver`; any broader product or operator-facing purpose is not documented in source. - Database Schema: field business semantics beyond names, types, constraints, and write paths are not documented in source. - Error Handling: hardware/protocol-level failures are not implemented by Arctrm and are only visible through referenced tags. - Known Limitations: operational impact is inferred only from implementation behavior; deployment-specific requirements are unknown.

  • doc/jroboplc/modules/arctrm.1784742991.txt.gz
  • Last modified: 2026/07/22 20:56
  • by denis