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

> **Internal · architecture.** Written 2026-07-01 from `hiveops-module-atec-tcr-events` source. Verify against code before relying on specifics.

## Module shape

Package `com.hiveops.tcrevents`, three classes, no controllers/DB/Kafka (this is an agent module, not a microservice — see [technical.agent]):

- **`TcrEventsModule`** — the `AgentModule` SPI entry point (registered via `META-INF/services/com.hiveops.core.module.AgentModule`). Owns lifecycle (`initialize()` → `isEnabled()` → `start()` → `stop()`) and the shared `ScheduledExecutorService`.
- **`CstSnsLogMonitor`** (package-private) — parses `CST_SNR_MMDD.log`.
- **`TcrAccessLogMonitor`** (package-private) — parses `ACESSLog<MMDD>.txt` (code: `logDir.resolve("ACESSLog" + mmdd + ".txt")` — **no underscore** before the date; the class javadoc's `ACESSLog_MMDD.txt` is stale).

Both monitors are plain constructed objects (`new CstSnsLogMonitor(logDir, atmName, client)`), not their own `AgentModule`s — `TcrEventsModule` drives both from a single poll loop.

## Flow: poll → parse → detect → emit

```
TcrEventsModule.start()
  → ScheduledExecutorService (daemon thread "tcr-events", fixed-rate, every 30s — see internal.md re: config bug)
    → poll()
      → cstMonitor.poll()      // CstSnsLogMonitor
      → accessMonitor.poll()   // TcrAccessLogMonitor
```

Each monitor uses **tail-by-offset** log reading, the same pattern as the agent's journal readers:
- Tracks `currentFile` + `fileOffset` (byte position) per monitor.
- On each `poll()`, opens the file with `RandomAccessFile`, seeks to `fileOffset`, reads new lines to EOF, advances `fileOffset` to `raf.getFilePointer()`.
- Daily rotation: `rotateDailyFileIfNeeded()` compares `LocalDate.now()` to the last-seen date; on a new day it recomputes the filename (`CST_SNR_<MMDD>.log` / `ACESSLog<MMDD>.txt` — the date is `MMdd` via `DateTimeFormatter.ofPattern("MMdd")`) and resets `fileOffset` to 0.
- Truncation guard: if `raf.length() < fileOffset` (file was truncated/replaced mid-session), `CstSnsLogMonitor` resets offset + in-progress parse state; `TcrAccessLogMonitor` resets offset only (no block state to clear).

### `CstSnsLogMonitor` — block-based parsing

The CST_SNR log groups related lines into operation blocks:
```
>>>>>  HH:MM:SS.mmm - <OP> START      (OP = RP/Replenishment, TD/Teller Deposit, BC/Bill Count, ...)
...
[N_E]  AH: NORM AL: EMPT BH: NORM ...
...
>>>>>  HH:MM:SS.mmm - <OP> END
```
- `BLOCK_START` / `BLOCK_END` regexes toggle an `inBlock` flag and accumulate `blockLines`.
- On `BLOCK_END`, `parseBlock()` finds the single `[N_E]` line, tokenizes `SLOT: STATUS` pairs (`SLOT_TOKEN` regex), and diffs each slot against an in-memory `Map<String,String> slotStatus` — **only emits when status actually changes** (no dedup on the server side, so this in-agent memory is the only guard against event storms).
- Status → event mapping: `EMPT`→`CASSETTE_EMPTY`, `SNRF`→`TCR_SENSOR_FAULT`, `TMAN`→`TCR_MANUAL_REQUIRED`, `NORM`→ log-only (no event) recovery notice.
- A separate `MISMATCH` regex is checked on **every line independent of block state** (not gated by `inBlock`) — catches `** CST <slot> - SNR deposit count & 80 data deposit count mismatch! ... - <dataCount> - <snsCount>` and emits `TCR_SENSOR_MISMATCH` immediately.
- `slotStatus` map is per-`CstSnsLogMonitor` instance (i.e. per-agent-process lifetime), cleared on file truncation or day rollover — so a slot that was `EMPT` yesterday will re-emit `CASSETTE_EMPTY` today if it's still empty at the first block of the new day (this is intentional: state doesn't carry across the daily file rotation).

### `TcrAccessLogMonitor` — line-based parsing

Simpler: no blocks, just substring matches per line:
- `"Process restarted from abnormal termination"` → `TCR_PROCESS_RESTART`
- `"## Shutdown ##"` → `TCR_SHUTDOWN` (message: "TCR operator shutdown")
- `"## Shutdown for reboot ##"` → `TCR_SHUTDOWN` (message: "TCR shutdown for reboot" — same event type, different `eventDetails` text based on whether the line contains `"reboot"`)

No de-dup logic here — every matching line emits, every poll cycle it appears in the unread tail.

## How events reach the platform

Both monitors emit through the same shared `IncidentEventClient` instance (constructed once in `TcrEventsModule.initialize()`), which is the same client class used by `journal-events` and other event-reporting modules — see [technical.agent] for the full outbound API surface.

```
CstSnsLogMonitor/TcrAccessLogMonitor.emit(eventType, details, ...)
  → CreateJournalEventRequest.builder()
      .agentAtmId(atmId)          // ATM name string, not incident's numeric atmId — resolved server-side
      .eventType(eventType)       // free-form String, e.g. "TCR_SENSOR_FAULT" — not validated client-side
      .eventDetails(details)
      .cassettePosition(slot)     // CstSnsLogMonitor only; null for TcrAccessLogMonitor events
      .eventSource("TCR_CST_SNR" | "TCR_ACESS_LOG")
      .build()
  → IncidentEventClient.sendEvent(req)
      → HTTP POST {agent.endpoint or incident.endpoint}/api/journal-events
```

`eventType` on `CreateJournalEventRequest` is a plain `String` field (`hiveops-agent-journal`'s DTO) — the agent's own `EventType` enum (`com.hiveops.events.EventType`) does **not** include the five `TCR_*` values this module emits (only `CASSETTE_EMPTY` overlaps with the enum). Nothing client-side stops an unregistered type from being sent; recognition/labeling happens server-side against hiveops-incident's DB-backed `event_type` table.

## Auth / endpoint selection

`initialize()` picks the property prefix based on whether `agent.endpoint` is set:
```java
String prefix = agentEndpoint != null ? "agent" : "incident";
HttpClientSettings incidentSettings = HttpClientHelper.loadSettingsFromProperties(props, prefix);
```
`HttpClientHelper.loadSettingsFromProperties` resolves institution key with fallback order `{prefix}.institution.key` → `agent.institution.key` → `incident.institution.key`, and always reads the bearer token from `agent.auth.token` regardless of prefix. This module has no bespoke auth handling — it's 100% the shared agent HTTP settings pattern.

## Platform abstraction

None used directly. The module reads plain files via `java.nio.file` / `RandomAccessFile` — no `PlatformService`/`PathResolver` dependency, even though it's Windows-only in practice (the LG TCR software and its log format only exist on ATEC Windows kiosks). The Windows-only constraint is enforced by convention/deployment (only installed on `device.type=ATEC_TCR` machines) rather than by a runtime OS check in this module's code.

## Cross-links
- [technical.agent] — module system, lifecycle, shared HTTP client settings, full outbound API surface
- [platform.module-map]
