RINGby Ringwood

07 — Services Catalog#

Who this is for. Anyone looking for "where does X live?" or "is there already something that does this?" before writing a new service.

What you'll learn. A guided index to everything under Ring/Services/ — each entry a short, verified statement of responsibility and its key collaborators, with its file path. Entries are grouped by concern rather than by directory where that reads better; the directory is always given.

How to read this. Each summary was taken from the type's own doc comment in the cited file. Where a service's own comment states a limit, a rationale or a warning, this catalog carries it — those are the parts that stop you reintroducing a fixed bug. Where a service is large enough to need its own explanation, this chapter points at the chapter that gives it.

Coverage. Every .cs file directly under Ring/Services/ and in each of its fifteen subdirectories is named somewhere below. Interfaces and their default implementations are grouped together; small companion types (enums, records, option objects) are named with the service that owns them rather than given their own row. Nothing under Ring/Services/ is silently omitted — if you cannot find a file here, that is a defect in this chapter. This catalog is Services-only; the equivalent catalog for Ring/ViewModels/ is chapter 06 §6.1a.

The PLC services are catalogued here only briefly; chapters 03 and 04 are their real documentation.

Insights report screen with multiple analytics cards: yield, spend, alarm load and stock forecast
One screen, a dozen services from this catalog: the Insights screen's cards are the dashboard/insights ViewModels of chapter 06 §6.1a, each backed by a query service or calculator named below.

7.1 PLC — Ring/Services/PLC/#

The largest subfolder and essentially flat. Grouped by role:

Gates and write infrastructure#

File Responsibility
PlcWriteGuard.cs The global read-only gate and its one-way session latch. §4.1
PlcWriteEndpointGuard.cs Refuses a write whose target cannot be the plant controller. §4.4
PlcHeartbeatConnectionTracker.cs Link state machine; AllowPlcWrites() / AllowHeavyPlcPolling(). §3.6
PlcConnectionState.cs The six-state enum, with Starved documented as distinct from Disconnected
MainTvcWriteAuthorization.cs Hard commissioning hold — IsAuthorized => false
TankTempModeWriteAuthorization.cs Hard commissioning hold — empty authorized-tank list; also owns the 0/1/2 value vocabulary
TvcCoolingWriteAuthorization.cs Per-tank cooling authorization {1,2,4}; can only ever refuse the cooling selection
TankInstalledWriteGate.cs Fail-closed commissioning gate from Tanks_c[N] install bits; UI fails open on Unknown, the wire fails closed
RosterWriteIndex.cs Single owner of "which physical tank does this UI slot write to?"; three sticky fail-closed locks
FormulaBankWriteInterlock.cs Active-batch interlock for the destructive bank rewrite, plus the FormulaBankWriteOutcome vocabulary
MomentaryPulse.cs The shared de-assert half of every momentary pulse: spaced retries, STUCK TRUE, opt-in background re-clear
PlcSetpointWriteQueue.cs Per-tag write serializer with per-control coalescing. Pure dispatch; holds no safety gate
PlcWriteTagKeys.cs Canonical serialization keys so two paths writing one physical tag share one queue slot
PlcWriteResult.cs The unambiguous PlcWriteStatus outcome vocabulary (blocked ≠ simulated ≠ success)
PlcWriteEvidenceJournal.cs Append-only JSONL evidence for queued writes. Evidence only — no replay API
SetpointEchoGuard.cs "Is this a genuine operator change?" — no read-back baseline ⇒ no write
SetpointSeedContext.cs Per-window state for the seed-before-writable contract. Pure, no WPF types
SetpointRefreshCoordinator.cs Turns a settled write into a re-read, off the queue's events
TankControlSetpointReader.cs / TankControlSetpoints.cs The one-shot read-back seam that fills those baselines

Writers#

File Writes
PlcTagWriter.cs The shared write funnel; Write / WriteClear; four gates inline
BatchStartTankPlcWriter.cs + BatchStartPlcWriteHelper.cs Tanks[N] batch-start quintet (Formula_number, Request_level, Splitting_A/B, Start_Type last); the helper is the ordering seam
TVCControlPlcService.cs (+ ITVCControlPlcService.cs) Tanks[N].Preset_Temp, TVC[N].Preset_Temp / Temp_mode, TVC_Ctrl[N].enabled
UseTankControlPlcWriter.cs Tanks[N].Agitator_mode, Agt_on_pre, Agt_off_pre, Liq_add_1..3_mode
UseTankFormulaPlcWriter.cs Tanks[slot].Formula_number from the Use Tank windows — gated groundwork
TankAgitatorPlcWriter.cs Tank_IO[N].ZZZZZZZZZZTank_I_O9 bit 1 (O_Agitat), read-modify-write
BatchFormulaPresetPlcWriter.cs + FormulaBankCommitCoordinator.cs + FormulaPresetCodec.cs + FormulaBankPresetPlcReader.cs The Formula_01..06_Preset_{Operation,Amount,Mix}[0..30] bank; the coordinator drives the commit through an IFormulaBankPlcCommitTarget seam; the codec is the pure array shaping
InventoryAmountPlcWriter.cs Inventory_Amount[0..13] REAL, one bulk array write
PcWriteInteger30BatchControlPlcService.cs [30].0 Resume / .1 Hold / .2 Reset / .3 Silence, under a shared word-30 lock
ShiftControlPlcService.cs PC_Write_Integer[44/45/48/49].(shift-1) momentary pulses
ProcessorTimeDatePlcService.cs Controller wall-clock words [34..39] + the [30].10 commit pulse
InventoryEventCaptureService.cs The PC_Write_Integer[27].4 acknowledge — and the capture that precedes it
TankNamePlcWriter.cs / TankNamePlcSyncService.cs / TankNamePlcCodec.cs / TankNamePlcReader.cs / TankNamePlcNameHydrator.cs / TankNamePlcProbeState.cs / TankNameMergeRules.cs The provisional HMI_Tank_Name / HMI_Group_Name STRING family. No such tags exist on the plant today; inert on three independent fail-safes

Pollers, snapshots and caches#

PlcPollingCoordinator.cs owns seven pollers, each publishing an immutable snapshot into a static holder or cache:

Poller Publishes into
PlcSnapshotPoller PcReadFloatSnapshot, MakeReadyTankPlcData, PcDisplayOutputsCache
PcReadIntegerPoller PcReadIntegerCache — a heartbeat-word tick and a full [0..479] block tick
BatchStepsPoller BatchStepsSnapshotHolder (BatchStepsSnapshot)
StorageTankGroupPoller StorageTanksSnapshotHolder (StorageTanksSnapshot)
UseTankGroupPoller UseTanksSnapshotHolder (UseTanksSnapshot)
TVCPoller TVCSnapshotHolder (TVCSnapshot)
BatchStartPoller BatchStartSnapshotHolder (BatchStartSnapshot)

RosterPollPlan.cs is the pure roster→read-target translation. Cadences and gotchas: §3.2.

Readers and decoding#

PlcTagReader.cs (+ IPlcTagReader.cs, PlcDataType.cs) is the per-tag reader; UdtTankReader.cs + UdtTankLayout.cs are the byte-offset UDT path. Per-subsystem readers: StorageTankTagReaderService, UseTankTagReaderService, TVCTagReaderService, BatchTagReaderService, InventoryStampsPlcReader. Index maps: PcReadIndexes.cs, PcWriteIndexes.cs. Decoders: SystemStatusWord.cs (the PC_Read_Integer[9] hold/running word and the Process-menu command policy), BatchOperationMapper.cs (operation code → description), IngredientFamilyMapper.cs (operation codes → coarse ingredient families), BatchStamps.cs (element indexes inside Batch_Current_Stamps), AgitatorPresetBands.cs (the single definition of the agitator preset minute bands).

Diagnostics and demo#

File Responsibility
PlcCommunicationLogService.cs RCS-128: in-memory TX/RX ring buffer + optional per-day CSV, feeding the live DisplayCommunicationForm tail. §7.13
PlcCommEventLogService.cs + PlcCommEventType.cs Not the row above, despite the near-identical name: persists PLC connection-lifecycle events (Connected/Disconnected/HeartbeatStalled/reprobe/session/write outcomes) to the SQL PlcCommunicationLog table (schema v32) for after-the-fact auditability. Bounded queue, single batched writer, CommLogRetentionDays default 90 d. ch. 03 §3.7
PlcClockSkewMonitor.cs Read-only DST/skew sentinel
PcWriteInteger30StrandedBitMonitor.cs Read-only sentinel for a stranded command bit in PC_Write_Integer[30]
TagManifestService.cs Loads scripts/live-tags.json; its Live/Dead verdicts are 2024-sourced and non-authoritative
TagSuggestionCatalog.cs Tag Inspector autocomplete
TrendingDataService.cs 2 s Trending screen feed — builds its own readers
ViscometerLiveRecorder.cs 5 s; reads snapshots only, writes SQLite
ProcessHoldState.cs Live mixer HOLD from the polled status word
DemoModeData.cs Demo-mode substitute for pollers that bypass PlcTagReader
DemoPlcTagReader.cs Demo-mode substitute seam inside PlcTagReader
DemoPlcTagWriter.cs Demo-mode substitute seam inside PlcTagWriter

7.2 Alarms — Ring/Services/Alarms/#

An alarm, controller → operator#

The pieces below are each independently correct but never sequenced elsewhere in this book. This is the path a single alarm actually takes, verified against the sources:

  1. Acquisition. Two independent 2 s timers reach the same call — MainWindow's _plcUpdateTimer DispatcherTimer and a background System.Threading.Timer alarm pump (PlcAlarmBackgroundPumpInterval, MainWindow.xaml.cs:170) — both routed through RequestPlcAlarmPollAndPopups(), which throttles the actual PLC read to no more than once per PlcAlarmPollMinGap = 8 s (MainWindow.xaml.cs:164) and busy-gates the two callers so they never race. That reaches AlarmAlarmNumberPlcService.PollAlarmsForPopupsAndCache() (:578-629), which bulk-reads Alarm_Alarm_Number as INT[40] in one PLC read (:162-171), falling back to 40 individual indexed reads only if the bulk decode fails (:211-244) — the point being to avoid a 40× timeout storm on the common path.
  2. Decode. AlarmNumberDecoder.DecodeBaseAlarmNumber (:101-112) strips the band offset to get the catalog number; DecodeState (:114-128) classifies the raw value as Active (1–999), a silenced fault still present (1000–1999) or a resolved, cleared fault (2000–2999).
  3. Describe. AlarmDescriptionBuilder.BuildDescriptionOrError (:49-63) looks the catalog number up in the AlarmDefinitions table, substituting a computed tank number where the definition's HasTankIndex says to; an uncatalogued number gets a localizable "Alarm {N}" placeholder rather than a blank (:71-75).
  4. Timestamp, where legacy PLC date parts are used. AlarmLegacyDateTime.FromPlcParts (:27-65) reconstructs a full DateTime from the PLC's month/day/hour/minute/second (it supplies no year, so the year is inferred from the PC clock with a rollover rule); AlarmLifecycleTimeParser.TryParseDisplayToUtcIso (:11-44) is the inverse, parsing a grid-displayed string back to UTC ISO for storage.
  5. Persist and time-track. AlarmAlarmNumberPlcService.RecordPlcAlarmEventsIfEnabled (:768-938) reconciles the whole snapshot into the lifecycle repository and inserts one PlcAlarmEvents row per identity transition. PlcAlarmUiTimingStore.ApplySnapshot (:129-200) separately tracks each slot's raised/acknowledged/resolved times in memory — carrying a base alarm's original raise time across the PLC's own shift-insert queue moving it to a different slot — and rehydrates from persisted events at startup (HydrateFromEvents, :212-258) so a restart cannot fake-reset an alarm's displayed age.
  6. Present. AlarmSummaryComposer.Compose (:44-74) splits rows into shelved (suppressed) versus visible and sorts both by severity then recency for the ISA-101 Alarm Summary screen — display-only, and never a factor in detection, logging or escalation (:9-16). AlarmScreenLinkResolver.Resolve (:106-130) maps the catalog number to a deep link into the relevant equipment screen; a plant-wide alarm deliberately resolves to None because no single screen owns it. PlaybookService.GetByAlarmNumber (:105-147) returns the first-response Meaning/FirstCheck/WhoToCall entry, optionally overridden per alarm from the database.
  7. Escalate. A newly-raised alarm fires IAlarmEscalator.NotifyAsync (AlarmAlarmNumberPlcService.cs:1090-1125). The default is NoopAlarmEscalator, which only logs a "would notify" line — active until AlarmEscalation.Mode is switched to "Smtp" or "Webhook". Independently of escalation, rule-driven alarm emails are gated per rule by AlarmEmailStormControl.Register (:55-88), which returns SendSingle, SuppressedByCooldown or Digest so a flood collapses into one email rather than one per event; a severity-aware check against SmtpAlarmEscalator.WouldSend stops the rule engine and the SMTP escalator from both emailing the same raised alarm.
  8. Silence — the one write in this pipeline. Everything above is reads and local state. Silencing an alarm is an operator-initiated write, covered on the write path in chapter 04 §4.7, not repeated here.

The catalog, by role#

Acquisition and decoding.

File Responsibility
AlarmAlarmNumberPlcService.cs Reads Alarm_Alarm_Number as INT[40] in one PLC read, no per-element sequential fallback on the common path
AlarmNumberDecoder.cs Splits a raw value into base catalog number + band offset (Active/Silenced/Resolved)
AlarmDescriptionBuilder.cs Looks the base number up in the AlarmDefinitions table; unknown numbers get a placeholder, never a blank
AlarmLegacyDateTime.cs Reconstructs alarm/ack timestamps from the PLC's month/day/hour/minute/second fields (no year supplied)
AlarmLifecycleTimeParser.cs Parses grid display strings back to UTC ISO for storage
PlcAlarmUiTimingStore.cs Tracks raised/ack/silence times per slot; survives the PLC's own shift-insert queue and a Ring restart

The silence write.

File Responsibility
AlarmSilencePlcService.cs Writes the silence latch — PC_Write_Integer[30].3 by default, with a configured legacy level tag behind EnableCustomSilenceLatchTag. Its outcome type distinguishes a real write from a suppression. §4.7

Presentation and triage.

File Responsibility
AlarmStatusDisplay.cs Operator-facing rendering of a lifecycle status pair
AlarmSummaryComposer.cs Pure shelf-filter + priority sort for the ISA-101 summary — display-only
AlarmScreenLinkResolver.cs Alarm → screen deep link; plant-wide alarms deliberately resolve to None
PlaybookService.cs + AlarmPlaybookSeedService.cs Operator first-response playbooks, keyed by catalog number, DB-overridable
AlarmLegendService.cs + AlarmLegendReportDocument.cs The bilingual wall chart from Resources/data/alarm-legend.csv
AlarmHistoryReportDocument.cs The printable/exportable alarm history report

Notification and escalation.

File Responsibility
IAlarmNotifier / AlarmNotifier.cs In-cab sound + flash
IAlarmEscalator The off-site escalation interface (NotifyAsync)
NoopAlarmEscalator.cs The default. Logs a "would notify" line; no network call
SmtpAlarmEscalator.cs Sends the alarm email; WouldSend(severity) is the single authority for whether it will actually send
WebhookAlarmEscalator.cs + WebhookCardFormatter.cs Posts an alarm card to a configured webhook (Teams/Slack auto-detected); SSRF-checked via WebhookUrlValidator
AlarmEmailService.cs + AlarmEmailComposer.cs + AlarmContextProvider.cs Composes the rule-driven alarm email; AlarmContextProvider stamps the running-batch context onto it
AlarmEmailStormControl.cs Per-rule storm suppression — one instance per rule Id, defensively locked because the alarm hook fires from a background path

Analytics.

File Responsibility
AlarmAnalyticsCalculator.cs Pure, over a window of lifecycle rows
AlarmAnalyticsQueryService.cs The thin read-only wrapper that supplies it

7.3 Audit — Ring/Services/Audit/#

File Responsibility
UiAuditJournal.cs The hash-chained, append-only operator trail plus its tail anchor and VerifyChain. Tamper-evident, not tamper-proof (unkeyed SHA-256, stated in its own doc). §5.8
UiActionAuditor.cs App-wide, observe-only capture of operator interactions into the journal
UiActionRecord.cs The record shape and the UiActionKinds vocabulary
UiAuditQueryService.cs Pure projection over the journal: parse lines, keep the readable ones, apply the operator's filters. No WPF, no clock, no I/O beyond the files it is handed — so the screen's filter semantics are provable in a unit test
UiScreenResolver.cs Answers "which screen was the operator looking at?" — writes GetType().Name into the hashed payload, hence the rename freeze
IncidentExportBuilder.cs Reads both JSONL journals back for an incident export

7.4 Batch — Ring/Services/Batch/#

File Responsibility
BatchLifecycleRecorder.cs The recorder driven by BatchStepsPoller edges; on batch end calls BatchRepository.CompleteWithUsage so completion and usage commit in one transaction
BatchProgressMath.cs Pure progress math over the step snapshot
BatchStampsBackfillService.cs Startup catch-up for controller verdicts missed while Ring was down — bulk-only by design
BatchStampsMatcher.cs Decides which controller stamps slot belongs to a given Ring batch
BatchHistoryBackfillService.cs Preview-first importer for the legacy six-month batch history
BadBatchEarlyWarningService.cs Pure warning engine for live bad-batch indicators. "It never owns plant assumptions: every warning requires caller-supplied baselines and thresholds"
BatchCompletionCelebrationService.cs + BatchCompletionScorecardComposer.cs + ICompletionChimePlayer / CompletionChimePlayer.cs The "batch klaar" moment: a chime deliberately different from the alarm sound, plus a composed toast line

Batch policy types (eligibility, preconditions, split mapping) live with the screens in Ring/Views/BatchStart/ — see chapter 06 §6.3.


7.5 Display — Ring/Services/Display/#

File Responsibility
LocalizationService.cs Thirteen-locale dictionary swapping; never touches CurrentCulture/CurrentUICulture. §6.5
DisplayUnitService.cs Owns the station's chosen display units. Display only
UnitSystem.cs / PerQuantityUnits.cs / UnitDisplay.cs The unit family, the per-quantity volume unit, and the unit-aware layer around the formatter — all explicitly display only
PlcDisplayFormatter.cs Single source of truth for PLC-value display formatting
LocalizedInput.cs / OperatorNumberInput.cs Locale-safe and locale-tolerant parsing of operator-typed numbers. The distinction matters: operator input accepts both conventions; wire values never do (see PlcTagWriter.TryParseRealInvariant)
DataFreshnessClassifier.cs How old is this PLC-sourced value, and may it be shown? Feeds the freshness converters

Ring/Services/PlantNativeUnits.cs (top level) is the single declaration of this site's native recipe/batch weight unit and the display seam every consumer goes through — read it together with docs/production-readiness/RECIPE_UNITS_VERDICT_2026-07-31.md, which settled the question: pounds.


7.6 Reports — Ring/Services/Reports/#

The largest non-PLC subfolder. Three layers, and it is worth keeping them separate when you add one.

Pure calculators (no I/O, no PLC, no UI, fully testable):

File Responsibility
BatchCostMath.cs Per-batch cost math — ingredient cost and leak detection
BatchCompletionEtaCalculator.cs Batch-completion ETA math
DowntimeOeeCalculator.cs Downtime/OEE rollup over operator-captured downtime events plus caller-supplied economics
DrySolidsMath.cs Converts a delivered batch volume (US gal) to dry-starch pounds; feeds glue-usage, dry-lbs/sqft and giveaway-$ analytics
DryLbsPerSqFtCalculator.cs Dry-lbs-per-square-foot ratio; an explicit no-data outcome distinct from zero when solids or completed batches are missing
GiveawayCostCalculator.cs Prices giveaway volume; returns NotConfigured rather than $0 when no marginal starch rate is set
YieldScorecardCalculator.cs End-of-batch yield and quality scorecard math
YieldScorecardAggregator.cs Windowed yield/giveaway aggregation — distribution counts and a signed mean giveaway
ReportScheduleCalculator.cs Pure cadence due-math for scheduled report emails (next-due, monthly rules)
ReportDateRange.cs Canonical half-open [StartInclusive, EndExclusive) date range shared by the report screens
ReportPageNumbering.cs The page-count stamp in a report's page header
ProcessHistoryChartProjection.cs Turns queried ProcessHistorySample rows into summary statistics and a gap-aware chart series for the Tank History report
ShiftConsumptionBreakdown.cs Per-operation ingredient breakdown for one Shift Consumption occurrence
InventoryInitiatorMapper.cs Decodes Inventory_Stamps[0] ("Update Initiator") to display text, mirroring the legacy switch

Read-only query services (assemble a payload from persisted data only):

File Responsibility
CostsQueryService.cs Assembles the Costs report / dashboard "Spend" payload from Batch + IngredientUsage + FormulaSteps + effective-dated IngredientCost
GlueUsageQueryService.cs Sums delivered glue volume and derived dry-starch pounds over a date range
YieldGiveawayQueryService.cs Yield/Giveaway Tracker query, using only persisted RequestLevel/ActualVolume batch fields
ShiftConsumptionQueryService.cs Joins finalized batch/ingredient-usage rows to operator-defined shift windows
ShiftHandoverQueryService.cs "What happened on my shift" aggregation for one shift occurrence
DryLbsPerSqFtQueryService.cs The join that pairs each shift occurrence with DryLbsPerSqFtCalculator's pure ratio
BatchEtaQueryService.cs Thin read-only layer feeding BatchCompletionEtaCalculator
AlarmBatchCorrelationService.cs Correlates alarms with the batch(es) running when each alarm went active
DowntimeDollarCounterService.cs Aggregates alarm lifecycle durations by alarm number/description and applies caller-supplied economics
FormulaCycleBaselineService.cs Per-formula, per-step median cycle-time baselines from observed completed batch-step timings

Document builders (FlowDocument for on-screen preview and PrintDialog output — each pairs with the report screen of the same name in chapter 06 §6.3): BatchReportDocument, BatchHistoryReportDocument, BatchHistoryUsageReportDocument, UsageReportDocument, CostsReportDocument, FormulaReportDocument, GlueUsageReportDocument, InventorySnapshotReportDocument, ShiftHandoverReportDocument, YieldGiveawayReportDocument, DataEntryReportDocument. PdfExportService renders any of them to PDF.

Scheduling: ReportSchedulerService (1-minute timer over subscriptions) + ScheduledReportRenderer.

And one small type that carries a contract: ReportDataUnavailableNotice.cs — the single wording used everywhere a report surface has to say "this is not a zero, the data could not be read". It is the user-facing half of RepositoryReadScope (§5.2).


7.7 Email and notifications#

Ring/Services/Email/IEmailDeliveryService / EmailDeliveryService.cs is the shared SMTP transport for the whole app: Enqueue drops the message on an in-memory BlockingCollection and returns immediately, so a dead or slow SMTP server can never block the UI thread; a background worker sends with a 3-attempt retry (1s/2s/4s backoff).

Durable-by-construction queue. Enqueue writes a Status='Pending' EmailLog row (IEmailLogRepository) synchronously, before the message ever touches the in-memory queue. The worker updates that same row to Sent or Failed when the send finishes, with the full exception text on failure. This means a dead SMTP server degrades to visible log rows, never a frozen HMI or a silently vanished message.

Crash / power-cut recovery — and why it never auto-resends. A process death after the pending row is written but before the worker finishes leaves that row stuck at Status='Pending'. On the next startup, RecoverPendingFromPriorRun sweeps every such row and marks it Failed with reason "recovered after restart…" — it is deliberately never resent, because this service cannot tell whether the original attempt actually left the wire, and a duplicate alarm/report email is judged a worse failure mode than a documented gap the operator can act on. Documented residual: the one window this doesn't cover is a crash during the few seconds DatabaseWriterQuiesce/SuspendDelivery holds the database file for a restore or factory reset — a message queued in that narrow window has no durable row to recover, so it degrades to in-memory-only for that one message. Outside a crash, SuspendDelivery/ResumeDelivery itself does not drop mail: queued messages stay in the in-memory queue and drain on resume, and their log writes are buffered and replayed, not lost.

EmailAddressValidator.cs is the single "is this a usable address" check for the UI; EmailSeedService.cs is the idempotent, self-trapping seeder for out-of-the-box defaults; BatchCompletionEmailService.cs composes the completion mail.

Ring/Services/Notifications/ToastService.cs (app-wide non-blocking toasts; singleton because a single ToastHost overlay is hosted once), ToastItem.cs, ToastSeverity.cs, ToastBrushPalette.cs (severity → semantic design-system accent brush).


7.8 Predictive — Ring/Services/Predictive/#

All five are pure or read-only.

File Responsibility
FeedRateMonitorService.cs Pure, fully-testable predictive-failure math. No DB calls
EquipmentHealthService.cs Evaluates drift rows plus the two facts that qualify them
TankTemperatureStabilityCalculator.cs One tank's cook-temperature stability over a window: mean and variability
GlueLineRunDryCalculator.cs Run-dry projection for the mix tank feeding the corrugating glue line
IPredictiveEventSource.cs The hand-off point from the predictive monitor to the alarm-email rule engine

Related top-level services in the same spirit: GoldenBatchDeviationCalculator.cs (temperature only — not viscosity, and the file says so), IngredientStockForecastService.cs (pure stock-runway math), MaintenanceDueCalculator.cs / MaintenanceReminderEvaluator.cs (which return Unknown with a null percent and an em-dash rather than inventing a number when there is no usable interval), ScaleCalibrationOverdueEvaluator.cs, TankCipDueCalculator.cs, ReceivingVarianceMath.cs (whose honesty rule is that with no configured tolerance an event is never flagged short).


7.9 Roster and group setup#

Ring/Services/Roster/TankRoster.cs (the ordered site-configurable slot list, Default() and IndianaPreset()), TankRosterSlot.cs, TankRole.cs, TankRosterState.cs (the process-wide holder over %LocalAppData%\Ring\tank_roster.json, with Changed, RestartRequired, LoadFailed and CaptureSessionBaseline), StorageTankScope.cs (the storage tank numbers an analytics surface must cover, taken from the roster).

Everything downstream of the roster is safety-relevant: see chapter 01 §1.2 and chapter 04 §4.3.

Ring/Services/GroupSetup/GroupHardwareSetupState.cs, the legacy Setup → Groups hardware options (tank count, display style, screen tank mapping, cooling/discharge) over %LocalAppData%\Ring\group_hardware_setup.json.


7.10 Operators, configuration, documentation, export#

Directory / file Responsibility
Operators/IOperatorSessionService.cs, OperatorSessionService.cs "Who is operating right now" — identity only, not authentication; best-effort persisted last choice in %LocalAppData%\Ring\operator-session.json
Configuration/IConfigChangeNotifier.cs, ConfigChangeNotifier.cs Notifies consumer screens when a supervisor edits a category of configuration
Documentation/ManualPageLauncher.cs Opens the shipped all-in-one RS-3000 operators manual at a specific page
Documentation/ScreenHelpService.cs One per-screen deep link into the manuals library
Documentation/MarkdownToHtml.cs Minimal dependency-free Markdown → HTML for the operator-facing docs
Export/DatabaseExportSchema.cs The whitelist of exportable tables and columns
Export/DatabaseExportService.cs Streams a whitelisted table to CSV

Authentication proper lives outside Services/, in Ring/Infrastructure/Security/AdminCredentialGate.cs, CredentialRotationService.cs (which writes appsettings.local.json), LoginAttemptTracker.cs.

Two more small, easily-missed files sit beside the settings writers in Ring/Infrastructure/Configuration/, not in Security/: SmtpPasswordProtector.cs (DPAPI-protects the SMTP password stored in configuration) and WebhookUrlValidator.cs (the SSRF guard consulted by WebhookAlarmEscalator before it POSTs to an operator-configured URL).


7.11 Top-level services — Ring/Services/*.cs#

Logging, crash and health#

File Responsibility
ILogger.cs / Logger.cs The logging abstraction and its file/ring-buffer implementation (%ProgramData%\Ring\logs\application.log)
CrashLogger.cs Global last-chance crash logger, wiring three escape paths to one file-per-crash under <exe dir>\logs\crashes\
UiHangDetector.cs Classifies the UI thread's state from a single sentinel tick (paired with MainWindow's 5 s ping/pong)
UnbiasedClock.cs A monotonic millisecond clock that does not advance while the system is asleep
DiagnosticBundleService.cs Builds a one-click support zip

Database lifetime#

DatabaseBackupService.cs, DatabaseHealthService.cs, DatabaseWriterQuiesce.cs, DatabaseMaintenanceLatch.cs — all covered in chapter 05.

Startup and process#

File Responsibility
StartupDegradedState.cs Records the session-degrading condition (failed DB init) and forces PlcWriteGuard read-only for the session
SecondLaunchAdvice.cs Decides what to tell a second launch. Two independent brakes: an explicit boot marker (authoritative — the other process says it is still starting) and a sustained re-probe for the post-boot case
NameTablesStartupHydrator.cs Hydrates the three name tables at startup
NavHistoryDisposer.cs Pure decision logic for back-navigation history: when a retained screen may be disposed

Ambient name state#

MakeReadyTankSupervisorNamesState.cs and StorageTankGroupSupervisorNamesState.cs — in-memory supervisor-entered names (and, for the former, derived inventory line titles), updated on Apply from the matching Setup screen, with a NamesChanged event the UI subscribes to. Both are examples of the ambient-static pattern that is being retired (chapter 02 §2.6).

Formula and recipe#

File Responsibility
FormulaNameResolver.cs App-wide display-name lookups for formula numbers and storage tank numbers
FormulaSolidsMath.cs Dry-solids percentage from recipe rows. Display/report only — never touches a PLC. Its two oracles (the plant's own decoded recipe CSV and the legacy step-seed variant) are pinned forever by golden parity tests
FormulaIngredientAmountLimits.cs Min/max/default amount (lb) per formula operation, aligned with the legacy panel limits
PlantNativeUnits.cs The single declaration of the site's native weight unit

Maintenance and calibration#

MaintenanceTaskSeeder.cs (seeds sensible defaults on first run), MaintenancePmSeedData.cs (the one replaceable file holding the provisional PM register), MaintenancePlantPmSeedData.cs (the real plant PM register, seeded from the site's own PM forms), MaintenanceDueCalculator.cs, MaintenanceReminderEvaluator.cs, ScaleCalibrationOverdueEvaluator.cs, TankCipDueCalculator.cs.

Analytics and history#

ProcessHistorianService.cs, RuntimeAccumulatorService.cs, TankLevelTrendService.cs, ShiftDryLbsService.cs (one shift's derived dry-lbs total together with the population it was summed over — the pairing is the point), IngredientStockForecastService.cs, GoldenBatchDeviationCalculator.cs, ReceivingVarianceMath.cs, ReceivingVerificationService.cs (manual-first bulk-receiving verification).

Commissioning and legacy#

PlantProfileService.cs (builds a versioned PlantProfile JSON commissioning export), LegacyRs360ImportService.cs (imports legacy tank names, make-ready ingredient names and custom data), DashboardService.cs (coordinates dashboard data from the existing ViewModels — and is on the write-surface register as Infrastructure because it names PlcWriteGuard).


7.12 A note on where not to put things#

Two conventions this catalog makes visible and that are worth preserving:

  • Pure math lives in its own type, WPF-free and I/O-free. Almost every calculator above says so in its own doc comment, and the reason is stated in CONTRIBUTING.md: tests prefer WPF-free logic objects over constructing UserControls. A calculation embedded in a code-behind is a calculation with no test.
  • "Honesty" types are a pattern here, not an accident. ReportDataUnavailableNotice, RepositoryReadScope, MaintenanceStatus.Unknown, GiveawayCostState.NotConfigured, TankCapability.Unknown, PlcWriteStatus.WrittenUnverified, HasAnyData on the snapshots — each exists so a missing input renders as missing, not as a confident zero. Follow the pattern rather than defaulting.

7.13 Where the evidence lives#

Every subsystem above is described mechanically somewhere in this book, but nothing collects "here is the file you open" in one place. This table does.

Artefact Path Written by Rotation
Application log %ProgramData%\Ring\logs\application.log Ring/Services/Logger.cs Size-rotated, MaxLogFileBytes = 10 MB (:35), MaxBackupCount = 5 (:36) → .log.1...log.5
PLC comm log (in-memory) n/a — a 500-event ring buffer PlcCommunicationLogService.cs (MaxBufferedEvents = 500, :32) Overwrites oldest on overflow
PLC comm log (optional per-day CSV) %ProgramData%\Ring\logs\plc-comm-YYYY-MM-DD.csv (PlcCommunicationLogService.cs:74) — the same folder as application.log, not the install directory Same service, gated by its own DiskLoggingEnabled runtime property (:62) — a UI toggle in DisplayCommunicationForm / the NavBar comm-log button, not an AppSettings key RetentionDays = 30 (:68), also a runtime property
PLC comm log (SQL table) PlcCommunicationLog table inside the database (schema v32), queryable via Setup → Database Export PlcCommEventLogService.cs — see ch. 03 §3.7, not the CSV row above PlcSettings.CommLogRetentionDays, default 90 d, pruned on startup + daily
Write evidence journal %ProgramData%\Ringwood\Ring\plc-write-evidence.jsonl PlcWriteEvidenceJournal.cs:75-79 Size-rotated at 10 MB, 5 retained archives (ch. 04 §4.9)
Operator audit journal %ProgramData%\Ringwood\Ring\ui-audit\ui-audit.jsonl UiAuditJournal.cs:1040-1041 Append-only, hash-chained (ch. 05 §5.8)
Production gate summary artifacts/production-gate/production-gate-summary.json scripts/Invoke-ProductionGate.ps1 (:190) Generated, not committed — regenerated every gate run
The database <exe dir>\RingwoodDatabase.db (+ -wal/-shm) Repositories, via RingwoodDbAccess See ch. 05 §5.1

Note that the two %ProgramData% roots are not the same directoryRing\logs\ for the application log versus Ringwood\Ring\ for both JSONL journals. This is a naming accident worth knowing before you go looking for one and find the other empty.

A gate-blocked write, as it appears in the log. PlcWriteGuard.LogBlocked (PlcWriteGuard.cs:97) emits one line per suppressed write:

[WARN] [ReadOnly] PlcTagWriter.Write: PLC write to 'Tanks[3].Preset_Temp'=75 SUPPRESSED (ReadOnlyMode ON — nothing sent to the controller).
[WARN] [ReadOnly] TVCControlPlcService.SetAgitatorModeAsync: PLC write to 'Tanks[2].Agitator_mode'=Auto SUPPRESSED (ReadOnlyMode ON — nothing sent to the controller).

The source half of the message (PlcTagWriter.Write, TVCControlPlcService.SetAgitatorModeAsync) is whatever string the caller passed — it names the method, not a fixed vocabulary, so grepping for SUPPRESSED is more reliable than grepping for one source name.

A healthy boot, roughly in order. Reconstructed from the call order in App.xaml.csConfigurationService.csDatabaseInitializer.csDatabaseHealthService.csMainWindow.xaml.cs; treat this as high-confidence, not an exhaustively traced guarantee — a real boot has more conditional branches than this list:

  1. "Configuration loaded successfully from: {path}" (ConfigurationService.cs)
  2. "[DatabaseInitializer] Boot sample: Path={path}, FileExistedAtBoot={bool}"
  3. "[DatabaseInitializer] Starting. Path={path}, FileExistedAtStartup={bool}"
  4. "Database initialized (first run) at {path}" or "Database OK at {path}"
  5. "[DatabaseHealthService] PRAGMA integrity_check = ok ({path})"
  6. "[DatabaseHealthService] Schema check OK ({path})"
  7. "[DatabaseHealthService] Online backup OK → {backupPath} ({bytes} bytes)"
  8. Background services (TankNamePlcNameHydrator, PcWriteInteger30StrandedBitMonitor, ReportSchedulerService, ProcessHistorianService, RuntimeAccumulatorService) are silent on success — they only log on failure, so their absence from the log is itself the healthy signal.
  9. MainWindow's background reachability probe: "[PlcMonitoring] TryStartPlcMonitoringIfReachable: resolved PLC IP = {ip}", then "[PlcMonitoring] PLC reachable at {ip}:44818 on attempt {n}/3 — starting monitoring.", then the StartPlcMonitoring sequence ending in "[PlcMonitoring] StartPlcMonitoring: started OK (background poll 500ms, UI 1000ms)." (Demo mode replaces the probe with "[PlcMonitoring] Demo mode enabled — skipping TCP reachability probe, starting synthetic pollers directly.")

A caution the log itself creates. At the shipped Debug log level the application log has been observed to grow at roughly 32 MB/h; with the 10 MB × 5 rotation ring that is only about 1.6 hours of retained history (docs/production-readiness/WRITE_ENABLE_READINESS_2026-07-26.md:441-444, which recommends shipping Information for production or enlarging the ring). If you are trying to find what happened more than two hours ago on a Debug-level station, the log may simply no longer have it.


Next: 08 — Testing and Quality.


Verified against#

Every claim in this chapter was read out of these files on 2026-09-01:

Every .cs file under Ring/Services/ and its fifteen subdirectories (Alarms, Audit, Batch, Configuration, Display, Documentation, Email, Export, GroupSetup, Notifications, Operators, PLC, Predictive, Reports, Roster) — summaries taken from each type's own doc comment · Ring/Infrastructure/Security/ (listing) · Ring/Infrastructure/Configuration/AlarmEscalationConfigWriter.cs, AnalyticsSettingsConfigWriter.cs, CostSettingsConfigWriter.cs, AppSettingsSectionWriter.cs, SmtpPasswordProtector.cs, WebhookUrlValidator.cs · Ring/Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs · Ring/Services/PLC/PlcWriteGuard.cs, PlcCommunicationLogService.cs, PlcCommEventLogService.cs, PlcCommEventType.cs · Ring/Services/Email/EmailDeliveryService.cs (RecoverPendingFromPriorRun, RecoveredAfterRestartError, SuspendDelivery/ResumeDelivery, CompletePendingSafe) · Ring/Database/Interfaces/IEmailLogRepository.cs, Ring/Database/Repositories/EmailLogRepository.cs · Ring/Database/Repositories/PlcCommunicationLogRepository.cs · Ring/Services/PLC/UdtTankReader.cs · scripts/Invoke-ProductionGate.ps1 · docs/production-readiness/WRITE_ENABLE_READINESS_2026-07-26.md · CONTRIBUTING.md

Generated from the docs/manual/engineering book in the Ring repository — the markdown there is the source of truth. Paths shown in code like this point into the Ring source repository, which is private to Ringwood — they are not links.