RINGby Ringwood

01 — System Overview#

Who this is for. Anyone about to work on Ring for the first time, and anyone returning to it after a while and needing the map again.

What you'll learn. What Ring is and what plant it drives; what the solution actually contains; where each kind of code lives; the exact technology stack; and which controller address means what — because confusing the bench twin with the live plant controller is the single cheapest way to do real damage here.


1.0 Vocabulary — read this if you come from only one side#

Ring sits between two worlds that rarely share a vocabulary. This section defines every term this book uses without further explanation. Skip whichever column you already know.

Controls / PLC terms, for a software engineer#

Term What it means here
PLC Programmable Logic Controller — the industrial computer that actually operates the plant. Ring never controls anything directly; it asks the PLC to, and the PLC decides
CompactLogix 1769-L36ERM The specific Allen-Bradley PLC model this plant runs. "L36ERM" and "the controller" mean the same box throughout this book
EtherNet/IP The industrial Ethernet protocol Ring speaks to the controller. Not the same thing as "an ethernet IP address"
CIP Common Industrial Protocol — the request/response layer carried over EtherNet/IP. A "lost CIP reply" is a request that reached the controller whose answer never came back
EIP session A connection the controller holds open for one client. A CompactLogix supports only about four to eight per source IP, which is why Ring staggers its pollers and why running several field scripts at once starves them
Tag A named variable inside the controller (current_step, Tanks[3].Level). Ring reads and writes tags; it has no other way to see or change anything
UDT User-Defined Type — a struct on the controller. Tanks[3] is one UDT element; .Level is a member of it
DINT / INT / SINT / REAL / BOOL Controller data types: 32-bit int, 16-bit int, 8-bit int, 32-bit float, single bit. Reading an INT with a 32-bit read fails; this bites regularly
Ladder / ST routine The program running on the controller. "Ladder logic" is the graphical form; "ST" is Structured Text. Ring does not run it, cannot change it, and must not fight it
Scan One pass of the controller's program. "Cleared within four scans" means the ladder overwrites your value almost immediately
Rung One line of ladder logic. XIC / XIO / OTE / OTU / OTL / ONS / MOV are instructions on a rung. ONS is a one-shot: it fires on a rising edge, so a bit stuck TRUE means it can never fire again
Sealed coil A rung whose output holds itself on until something specific breaks the seal. PC_Write_Integer[30].1 (Hold) drives one — which is why a stuck Hold bit cannot be resumed
Momentary pulse Set a bit TRUE, wait, set it FALSE. The controller acts on the transition. Both halves matter; leaving the bit TRUE is a fault
L5K / L5X Text exports of the controller program. scripts/RS_3000_2024_APR_22.L5K is plaintext and is the math authority; the 2026 running export is source-protected
Setpoint A target value an operator sets (a temperature, a level). Distinct from a command bit
HMI Human-Machine Interface — the operator screen. Ring is an HMI
LCP Local Control Panel — the plant PC that runs Ring
Commissioning Proving on real hardware that a change does what it is supposed to. "Held for commissioning" means the code exists and is deliberately inert
TVC The temperature-control unit family in this plant (TVC[], TVC_Ctrl[], TVC_IO[] tags). One TVC unit can serve more than one tank
Doser / use tank The tanks that dose adhesive to the corrugator, as opposed to storage tanks and the single mix (Make-Ready) tank

WPF / .NET terms, for a controls engineer#

Term What it means here
WPF Windows Presentation Foundation — the Windows UI framework Ring's screens are built with
XAML The markup language that describes a WPF screen. Each .xaml file has a matching .xaml.cs "code-behind"
Code-behind The C# file paired with a XAML file. Most Ring screens do their work here
MVVM / ViewModel A pattern where screen state lives in a plain class (the ViewModel) and the XAML binds to it. Ring uses this for its process screens and plain code-behind elsewhere — see chapter 06
Binding / DynamicResource Ways XAML pulls a value from somewhere else. A DynamicResource re-reads when the source changes, which is how a language switch repaints live
Dispatcher / UI thread The single thread allowed to touch controls. Blocking it freezes the screen
DispatcherTimer vs System.Threading.Timer The first ticks on the UI thread; the second on a background thread. Ring's PLC pollers deliberately use the second
Resource dictionary A XAML file of shared values — colours, sizes, and every translated string
DI container The object that constructs services and hands them to whoever needs them
xUnit / [Fact] The test framework and the attribute that marks one test
csproj (non-SDK) Ring's project files list every source file by hand. Adding a file without adding its row means it silently does not exist
SQLite / WAL The single-file database Ring keeps its history in, and its write-ahead log
libplctag The open-source library Ring uses to speak EtherNet/IP. Ring adds no protocol code of its own

1.1 What Ring is#

Ring is a Windows desktop HMI for a corrugator's starch-adhesive glue kitchen — the plant that cooks and doses the starch adhesive a corrugator uses to glue board. It replaces ~RS360, a Borland C++ application that ran the plant for years (README.md); the marketed product name for that lineage is RS-3000, which is why both names appear in the tree (docs/manual/sales/README.md).

Three properties shape everything else in the codebase:

  1. It is a plant application, not a records application. A bad write does not corrupt a row, it moves a valve. This is the origin of the whole gate stack in chapter 04.
  2. It ships read-only. PlcSettings.ReadOnlyMode defaults to true in C# (Ring/Infrastructure/Configuration/AppSettings.cs:326) and is true in the shipped Ring/Config/appsettings.json. The resolver PlcWriteGuard.ResolveConfiguredReadOnly is settings?.PlcSettings?.ReadOnlyMode ?? true (Ring/Services/PLC/PlcWriteGuard.cs:75-76) — a missing or garbled settings object fails closed.
  3. It is a single process with no server. One EXE, one local SQLite file, one controller. There is no service tier, no message bus, no cloud dependency.

Cutover. The repository is pointed at the 2026-09-08 write-enabled cutover — the day ReadOnlyMode is intended to be flipped to false and Ring begins driving the plant. Wherever this book says "writes are suppressed", that is a statement about the shipped configuration, not a permanent property. The go/no-go gate is docs/production-readiness/WRITE_ENABLE_READINESS_2026-07-26.md; the procedure is CUTOVER_RUNBOOK.md.


1.2 The plant#

Ring's first site runs it in English, but the plant replaced a Dutch-language legacy system (README.md) — the controller's alarm text and the imported recipe/tank vocabulary are Dutch. That heritage matters in code in at least two places worth knowing about up front:

  • Operator-facing text is localized into thirteen languages (Ring/Resources/Strings/, thirteen Strings.<code>.xaml files), and Dutch is a first-class target, not an afterthought.
  • LocalizationService deliberately never touches CurrentCulture / CurrentUICulture (Ring/Services/Display/LocalizationService.cs:23), so a Dutch UI never changes numeric parsing. That separation is why PlcTagWriter.TryParseRealInvariant (Ring/Services/PLC/PlcTagWriter.cs:393) exists: under nl-NL, a culture-sensitive parse turns "1.5" into 15.0 — a tenfold setpoint write.
Storage Tank Group screen showing several storage tanks side by side with live level and temperature readouts
The plant this section describes: storage tanks side by side, live. This is the Storage Tank Group screen, not a diagram — see chapter 06 for how the screen is built.

Tanks — read this carefully, the numbers are role-dependent#

The tank population is configurable at runtime, not compiled in. It lives in Ring/Services/Roster/TankRoster.cs, persisted as %LocalAppData%\Ring\tank_roster.json by Ring/Services/Roster/TankRosterState.cs.

Fact Value Source
Roles in the roster Storage, Doser Ring/Services/Roster/TankRole.cs
Max slots per role 6 TankRoster.MaxSlotsPerRole (TankRoster.cs:20)
Hard cap on total roster slots 12 TankRoster.MaxTotalSlots (TankRoster.cs:23)
Shipped fallback roster 4 storage (PLC 1–4) + 4 dosers (discovery-filled) TankRoster.Default() (TankRoster.cs:102-115)
The 13-tank target preset 1 mix (not in the roster) + 6 storage + 6 doser TankRoster.IndianaPreset() (TankRoster.cs:118-139)

So the widely-quoted "13 tanks (1 mix + 6 storage + 6 doser)" is the target preset, and the mix tank is deliberately outside the roster — the roster holds at most twelve slots and the mix / Make-Ready tank is modelled separately (Ring/Views/MainScreen/MakeReadyTank.xaml, Ring/ViewModels/MainScreen/MakeReadyTankViewModel.cs). A site that has never touched Setup → Tank Roster is running the legacy four-storage default. Do not assume six of anything from a screenshot.

A roster slot (Ring/Services/Roster/TankRosterSlot.cs) carries:

Member Meaning
SlotNumber Position within its role's list
Role Storage or Doser
PlcIndex The 1-based controller array index this slot reads and writes; -1 means unmapped (dosers use -1 to mean "fill me from role-byte discovery")
Enabled Disabled slots issue no reads and are never written
LegacyWindowKey Which legacy detail window this slot routes to — "StorageTank1", "StorageTank2", "StorageTank4", "Lowmidtank"

LegacyWindowKey is under a rename freeze. Its literal string values are compared StringComparison.Ordinal in RosterWriteIndex.StoragePlcIndexForLegacyWindow (Ring/Services/PLC/RosterWriteIndex.cs:235-261), whose return value is the PLC array index a storage-tank write targets. See ARCHITECTURE.md for the full freeze list; chapter 04 explains what happens when the lookup cannot answer honestly.


1.3 Controllers: which address is which#

Address Device Ring's relationship
172.22.103.10 CompactLogix 1769-L36ERM v20.12 The plant controller Ring reads and (post-cutover) writes
172.22.103.11 PanelView none
172.22.103.12 MicroLogix none
172.22.103.14 Legacy RS-360 HMI PC source of legacy data captures only
192.168.202.15 Bench L36ERM twin where write behaviour is proven first

(Table reproduced from docs/PLC_FACTS.md, which cites the 2026-06-29 field survey.)

Two consequences that catch people:

  • Bench evidence does not automatically transfer. Tag parity between the two controllers is an open question, and it has to be checked per tankenabl_agt / enabl_tvc are true for tanks 1, 2, 8 and 9 only, so a tag can exist on both boxes and be inert on one (docs/PLC_FACTS.md).
  • Nothing in Ring's configuration distinguishes bench from plant. The IP is one string. PlcWriteEndpointGuard (Ring/Services/PLC/PlcWriteEndpointGuard.cs) can tell you the endpoint is loopback or unconfigured, but it cannot tell you that you aimed a write at the wrong real controller. That is a procedural control, not a coded one.

The shipped Ring/Config/appsettings.json ships DefaultIpAddress: "" — an empty IP, not a loopback one. That matters: PlcConnectionConfig.GetPlcIp() falls back to 127.0.0.1 with a once-per-process warning rather than throwing (Ring/Infrastructure/Configuration/PlcConnectionConfig.cs:93-121), so the fall-through is the shipped state. Chapter 04 covers the endpoint gate that exists precisely because of this.


1.4 Solution layout#

Ring.sln contains exactly two projects — there is no third assembly, no shared library, no separate test-utility project:

Project Kind Target Output
Ring/Ring.csproj WPF app, classic (non-SDK) csproj v4.7.2, LangVersion 8.0 WinExe, AssemblyName Ring
Ring.Tests/Ring.Tests.csproj xUnit test library, classic csproj v4.7.2 Library, AssemblyName Ring.Tests

(Verified in Ring.sln, Ring/Ring.csproj:8-12, Ring.Tests/Ring.Tests.csproj:8-11.)

Nothing is globbed. A new .cs, .xaml, or image is invisible to the compiler until a row is hand-added to the csproj — and Ring.Tests/ProjectMembershipGuardTests.cs fails the build if you forget. See chapter 08.


1.5 Directory map#

Ring/                             the WPF application
  Views/                          XAML screens + code-behind
    App.xaml / App.xaml.cs        <ApplicationDefinition>; the real entry point
    MainWindow.xaml(.cs)          the shell: chrome, content area, nav history,
                                  PLC watchdog tick, alarm pump
    MainScreen/                   process screens (Make Ready, tank groups, TVC card…)
    BatchStart/                   the four batch-arming screens + their pure policy types
    TVCcontrol/                   the four TVC per-tank control windows + bridge
    UseTanks/                     MF1 + UseTank2 + UseTank3 windows + bridge
    Process/                      Alarm screen
    Reports/                      report screens + ReportHostView (the hosted-screen shell)
    Setup/                        supervisor/admin configuration screens and dialogs
    Shifts/                       shift control, handover, handoff, production log
    Help/                         manuals library, alarm legend, contact
    Wizard/                       first-run setup wizard and its steps
    UserControls/                 NavBar, FitHost, ScreenHeader, toasts, badges, charts
    Dashboard/                    DashboardView
  ViewModels/                     the VM-driven subset (MainScreen/, UseTanks/, chrome VMs)
  Services/                       everything non-UI (see chapter 07)
    PLC/                          pollers, snapshots, readers, writers, gates
    Alarms/ Audit/ Batch/ Configuration/ Display/ Documentation/ Email/
    Export/ GroupSetup/ Notifications/ Operators/ Predictive/ Reports/ Roster/
  Database/                       DatabaseInitializer + Interfaces/ + Repositories/
  Models/                         plain data records
  Infrastructure/                 Configuration/, DependencyInjection/, Exceptions/, Security/
  Controls/                       NumericKeypad, TankLevelControl, KeypadBehavior
  Converters/                     value converters
  Validation/                     attribute + helper validators
  Resources/                      Colors/Tokens/Components/RingTheme + Strings/ (13 locales) + data/
  Images/                         screen art and icons (every file hand-registered)
  Assets/Manuals/                 the operator manual PDF that ships inside the app
  Config/appsettings.json         the config that ships; copied into the build output
Ring.Tests/                       the xUnit suite — the only test project
docs/                             see docs/INDEX.md for what is authoritative
  manual/engineering/             this book
  manual/sales/                   the commercial book
  reference/plc/                  durable tag maps + APPSETTINGS_REFERENCE
  production-readiness/           runbooks, controls records, PLC layouts, analysis
  archive/                        historical evidence — never a current-state authority
scripts/                          PowerShell + the two L5K controller exports
tools/ab_server/                  bundled libplctag AB simulator (a fake PLC for dev)
remote-view/                      MeshCentral-based remote-viewing companion package
plc-discovery/                    EtherNet/IP discovery + legacy-extract helpers
.github/workflows/                production-gate.yml + plc-tag-audit.yml

Root-level documents are the ones an on-site team reads: CUTOVER_RUNBOOK.md, OPERATOR_TRAINING_RUNBOOK.md, HYPERCARE_PLAN.md.


1.6 Technology stack#

Concern Choice Notes
UI WPF on .NET Framework 4.7.2, C# 8.0 Ring/Ring.csproj:11-12
Look and feel MahApps.Metro base styles + Ring's own token dictionaries merge order pinned in Ring/Views/App.xaml
DI Microsoft.Extensions.DependencyInjection composition root is ServiceCollectionExtensions.AddRingServices (Ring/Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs)
Config Microsoft.Extensions.Configuration JSON provider Ring/Infrastructure/Configuration/ConfigurationService.cs
Controller I/O libplctag 1.5.2 + libplctag.NativeImport no custom EtherNet/IP stack; see chapter 03
Persistence System.Data.SQLite, hand-written ADO no ORM, no Dapper; see chapter 05
JSON Newtonsoft.Json used by the journals, roster/state files, profile export
Tests xUnit, run by xunit.console.exe dotnet test cannot run this suite
Packages packages.config + packages/ with pinned relative HintPaths a package upgrade is a csproj edit

Third-party components and their notices are enumerated in THIRD-PARTY-NOTICES.md.

Licensing is an open owner decision. There is no LICENSE file in this repository and no statement of ownership or redistribution terms (CONTRIBUTING.md). That gap is deliberate — the terms are the owner's call — and this book does not invent one.


1.7 Running it without a plant#

Three distinct mechanisms, often confused:

Mode How What it gives you
Demo Mode DemoMode.Enabled = true in <exe dir>\Config\appsettings.json (or the .local.json overlay) + restart. Config only — there is no CLI flag (docs/CONFIG_AND_STARTUP.md §6) Synthetic animated plant data from Ring/Services/PLC/DemoModeData.cs; every write short-circuits to a logged no-op
Simulated batch loop scripts/Run-SimDemo.ps1 Copies the build output to an isolated directory, writes its own loopback overlay, starts tools/ab_server plus a heartbeat pump, launches Ring with --isolated-sim-harness, drives a mock batch and asserts it persisted. scripts/Stop-SimDemo.ps1 tears it down
Synthetic history scripts/Seed-SmokeTestData.ps1 Deterministic batches, usage and alarms in SQLite so every report screen has content (scripts/SMOKE_TEST_README.md)

--isolated-sim-harness is not a general "quiet mode": it only takes effect when ReadOnlyMode is true and the configured PLC IP is a loopback address, so it cannot be used to silence warnings against a real controller (docs/CONFIG_AND_STARTUP.md §6; the check is IsIsolatedSimulationHarness in Ring/Views/App.xaml.cs).

Ring is single-instance: a second launch focuses the first and exits (Ring/Views/App.xaml.cs:21, 113). Chapter 02 explains why that mutex covers less than it looks like it does.


1.8 Two standing constraints you inherit on day one#

Both are stated in ARCHITECTURE.md, and both will catch you before you have read that far.

The rename freeze (ARCHITECTURE.md §11)#

Three identifiers are load-bearing as strings. Renaming any of them is a behaviour change wearing a refactor's clothes, and none of them will fail to compile.

Identifier Why the string matters Verified at
TankRosterSlot.LegacyWindowKey values — "StorageTank1", "StorageTank2", "StorageTank4", "Lowmidtank" Compared with StringComparison.Ordinal in RosterWriteIndex.StoragePlcIndexForLegacyWindow, whose return value is the PLC array index a storage-tank write targets. Rename one and a write goes to a different physical tank; make two collide and the lookup returns NoWrite. The same strings are hardcoded in the four TVC screens and switched on in NavBar Ring/Services/PLC/RosterWriteIndex.cs:250; Ring/Services/Roster/TankRoster.cs:104-113
The Ring.Views.BatchStart namespace MainWindow.NavigateBack() branches on t.Namespace.StartsWith("Ring.Views.BatchStart", StringComparison.Ordinal) to decide whether Back re-creates a screen or reuses the cached instance. The four Batch Start screens assume a first-time Load; rename the namespace and Back silently switches them to instance reuse Ring/Views/MainWindow.xaml.cs:640
Screen class names UiScreenResolver writes GetType().Name into the tamper-evident UI audit journal, and that value is part of the hashed record payload. Renaming a screen class splits its identity across the rename boundary: old records still verify, but they no longer match live labels Ring/Services/Audit/UiScreenResolver.cs

If you need one of these renames, it is a change with a test and a decision behind it, not a cleanup commit.

Architectural debt that is deferred on purpose (ARCHITECTURE.md §10)#

None of it is on fire, and all of it is parked past the 2026-09-08 cutover by an explicit decision: deep refactors of the PLC layer weeks before a write-enabled cutover trade a real risk for a cosmetic gain. Knowing the list stops you "fixing" something that is a known, scheduled item:

  • PLC layer — no PlcTagFactory / IPlcSession abstraction (tag construction is duplicated across many sites); write paths return bool, tuples and several bespoke enums instead of one PlcWriteResult; there is no PlcPollerBase, so the seven pollers repeat timer/backoff/publish logic and the timers outside the coordinator stay outside it; there is no single tag-name builder and no PlcConnectionConfig cache; WriteTimeout is wired but ReadTimeout is what most readers resolve.
  • UI — per-tank screen family consolidation (UseTanks → TVC → BatchStart), with three ordered prerequisites; StorageTankGroupViewModel's half-finished migration; a Dashboard hardcoded around the legacy tank count.
  • Cross-cuttingServiceLocator retirement and ambient-state → DI conversion; alarm operator text is English-only in SQLite (a schema change, not a dictionary edit); C#-composed text does not live-repaint on a language change; and a git history rewrite that is post-cutover only because it would force a rebase of every worktree days before the plant goes live.

The forward-looking list is docs/production-readiness/POST_CUTOVER_FOLLOWUPS.md.


Next: 02 — Runtime Architecture.


Verified against#

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

README.md · ARCHITECTURE.md · CLAUDE.md · CONTRIBUTING.md · Ring.sln · Ring/Ring.csproj · Ring.Tests/Ring.Tests.csproj · Ring/Infrastructure/Configuration/AppSettings.cs · Ring/Infrastructure/Configuration/PlcConnectionConfig.cs · Ring/Config/appsettings.json · Ring/Services/PLC/PlcWriteGuard.cs · Ring/Services/PLC/PlcTagWriter.cs · Ring/Services/PLC/RosterWriteIndex.cs · Ring/Services/Roster/TankRoster.cs · Ring/Services/Roster/TankRosterSlot.cs · Ring/Services/Roster/TankRole.cs · Ring/Services/Display/LocalizationService.cs · Ring/Views/MainWindow.xaml.cs · Ring/Views/App.xaml.cs · Ring/Resources/Strings/ (directory listing) · docs/PLC_FACTS.md · docs/CONFIG_AND_STARTUP.md · THIRD-PARTY-NOTICES.md · scripts/ (listing)

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.