---
module: agent.tcr-events
title: TCR Events — cassette + sensor + process monitoring for Teller Cash Recyclers
tab: Architect
order: 30
audience: dev
---

> **Architect · dev.** Written 2026-07-01 from the compiled `hiveops-module-tcr-events` artifact (v4.3.5); `.java` sources are absent from the working tree, so class/method flow below is reconstructed from bytecode in `target/classes`. Cross-reference [technical.agent] for the module system and event pipeline.

## Shape

A ServiceLoader `AgentModule` (`com.hiveops.tcrevents.TcrEventsModule`, registered in `META-INF/services/com.hiveops.core.module.AgentModule`) that owns one scheduler and three stateless log pollers. Package `com.hiveops.tcrevents`. Classes:

- `TcrEventsModule` — lifecycle, config, scheduler, wiring.
- `TcrAccessLogMonitor` — process/shutdown events from `ACESSLog<MMdd>.txt`.
- `CstSnsLogMonitor` — sensor/mismatch events from `CST_SNR_<MMdd>.log`.
- `TcrCassetteMonitor` — per-slot inventory from `JsonLog_<MMdd>.json`.

It uses no platform-abstraction layer (`PlatformService`) — file access is portable JVM I/O (`java.nio.file.Paths`, `RandomAccessFile` opened `"r"`), even though the target log layout is the Windows LG/ATEC TCR software.

## Lifecycle & config (`TcrEventsModule`)

`getName()` → `tcr-events`; `getVersion()` → `1.0.0`.

`initialize(ModuleContext)` runs a short guard chain, then wires the pollers:

1. **Device gate** — reads `device.type` from `ctx.getMainProperties()` (default `ATM`), `trimToEmpty` + `upperCase`; unless it equals `ATEC_TCR` or `TCR`, logs `device.type is '{}', not ATEC_TCR — module disabled` and returns not-ready.
2. **Enable gate** — `tcr.events.enabled` (default `true`); if not `equalsIgnoreCase("true")`, disabled.
3. **Log dir** — `tcr.log.dir` via `trimToNull`; if null, disabled. Resolved with `Paths.get(...)`; if the path doesn't exist it only warns (`will retry each poll`) and continues — pollers re-check existence each tick.
4. **Cadence** — `tcr.events.poll.interval.sec` via `NumberUtils.toInt(..., 30)` → `pollSec` field (default 30).
5. **Transport** — if `agent.endpoint` is set, `HttpClientHelper.loadSettingsFromProperties(props, "agent")`; otherwise prefix `"incident"`. Builds one `IncidentEventClient(settings, ctx.getAtmName(), ctx.getCountry())`, shared by all three pollers.
6. **Wire** — constructs `CstSnsLogMonitor`, `TcrAccessLogMonitor`, `TcrCassetteMonitor`, each `(Path logDir, String atmName, IncidentEventClient client)`; sets `ready = true`.

`isEnabled()` returns the `ready` flag. `start()` creates a `newSingleThreadScheduledExecutor` with a daemon thread named `tcr-events` and `scheduleAtFixedRate` at `pollSec` seconds; each tick calls `cstMonitor.poll()`, `accessMonitor.poll()`, `cassetteMonitor.poll()` inside a try/catch that logs `tcr-events: poll error: {}` (a poll failure never kills the schedule). `stop()` shuts the scheduler down.

## Poller pattern (tail → detect → emit)

All three share the same structure:

1. Compute today's filename from a `MMdd` `SimpleDateFormat` and resolve it under `tcr.log.dir`.
2. Open with `RandomAccessFile(file, "r")`, seek to the retained per-file byte offset, read newly appended lines, and advance the offset. (In-memory offset state — no persistence, so an agent restart re-reads from 0 for the current file; the vendor writes append-only within a day.)
3. Match lines and, on a hit, build a `CreateJournalEventRequest` via its fluent `builder()` and call `client.sendEvent(...)`; failures log `tcr-events: failed to send {}: {}` and are swallowed per-line.

### `TcrAccessLogMonitor`
Plain substring/`contains` matching on `ACESSLog<MMdd>.txt`:
- `"Process restarted from abnormal termination"` → `TCR_PROCESS_RESTART` (details "TCR process restarted after abnormal termination").
- `"## Shutdown ##"` / `"## Shutdown for reboot ##"` → `TCR_SHUTDOWN`; details are "TCR shutdown for reboot" if the line contains `reboot`, else "TCR operator shutdown".
- Builder fields set: `agentAtmId`, `eventType`, `eventDetails`, `eventSource=TCR_ACESS_LOG`.

### `CstSnsLogMonitor`
Regex-driven parse of `CST_SNR_<MMdd>.log`. Compiled patterns cover block delimiters (`>>>>> hh:mm:ss.SSS - <TAG> START/END`), `[N_E]` lines, generic `(\w{2,3}):\s*(\w+)` key/value pairs, and the mismatch line `** CST <x> - SNR deposit count ... mismatch ... SNR count: - N - N`. State-code → event mapping:
- `EMPT` → `CASSETTE_EMPTY` ("TCR cassette slot <n> is empty")
- `SNRF` → `TCR_SENSOR_FAULT` ("... sensor failure")
- `TMAN` → `TCR_MANUAL_REQUIRED` ("... requires manual intervention")
- `NORM` → recovery only (logs `slot {} recovered (was {})`, no event)
- deposit-count mismatch → `TCR_SENSOR_MISMATCH` ("Slot <n>: SNR mismatch — data=<> sensor=<>")
- Builder fields set: `agentAtmId`, `eventType`, `eventDetails`, `eventSource=TCR_CST_SNR`, `cassettePosition` (slot).

### `TcrCassetteMonitor`
Parses `JsonLog_<MMdd>.json`, filtering to lines whose JSON has `"Command":"RU"` (recycler-unit inventory snapshot; both `"Command":"RU"` and `"Command": "RU"` spacings are matched). For each entry in `Cassette_Info` it reads `Cassette_Type`, `Cassette_Status`, `Position`/`Sub_Position`, `Nation`(US)/currency `USD`, `Denomination`, `Denom`, `Count`, and emits one `CASSETTE_INVENTORY` event per slot ("TCR slot <pos>: <status> notes (<count>)"). Mappings:
- Status `Available` / `Low` / `Empty` / `Full` → `cassetteFillLevel`.
- Type `Recycle_Cassette`→`RECYCLE`, `Operation_Cassette`→`OPERATION`, `Reject_Cassette`→`REJECT` → `cassetteType`.
- Builder fields set: `agentAtmId`, `eventType`, `eventSource=TCR_CASSETTE`, `eventDetails`, `cassettePosition`, `cassetteType`, `cassetteCurrency`, `cassetteDenomination`, `cassetteFillLevel`, `cassetteBillCount`. Logs `sent CASSETTE_INVENTORY for {} slots`.

## How events reach the platform

`IncidentEventClient` (from `hiveops-agent-journal`, `com.hiveops.events`) serializes each `CreateJournalEventRequest` with Jackson (`@JsonInclude(NON_NULL)`) and does `POST {settings.endpoint}/api/journal-events`, accepting 200/201/202. Because the endpoint prefix is chosen from `agent.endpoint` vs `incident.endpoint`, on current fleets this targets the **agent-proxy** `/agent` path, which forwards into the incident pipeline; older configs post straight to incident. The ATM is auto-registered from `agentAtmId` — no `resolveAtmId()` call (deprecated). `agent.auth.token` is the bearer for every request.

Notable: none of the three pollers call `builder.deviceType(...)`, so `deviceType` is null on the wire and the backend applies its default (ATM) — the events are not tagged as TCR-device events even though the module only runs on TCR devices.

## Platform abstraction

None used. The module is pure JVM file-tailing + HTTP; it inherits ATM identity (`getAtmName()`, `getCountry()`) and shared properties from `ModuleContext`, and the HTTP layer from `HttpClientHelper`/`HttpClientSettings`. There is no `SystemCommands`/`PathResolver`/registry interaction, so behavior is identical across OSes given the same `tcr.log.dir` contents.
