Files
Alexander Köpke 4d00467f7e Add missing component to PumpType instances (#4867)
This pull request introduces improvements to how components and their
references are managed and exposed in both the Machine Vision and
Recycling sample domains. The main focus is on ensuring that components
use the correct `HasComponent` reference type, and that pumps expose
mandatory two-state status labels with appropriate properties and
descriptions. Additional tests are included to verify these behaviors.

**Component Reference Improvements:**

* Updated the `WithComponents` method in the Vision documentation and
implementation to use `HasComponent` references instead of `Organizes`,
ensuring that components are referenced in compliance with OPC UA
modeling best practices.
[[1]](diffhunk://#diff-c885bd41583e603a034705800d1cd03213b4f3c8b1e1611b31121652e6548c5aL340-R341)
[[2]](diffhunk://#diff-89611f4e76aef466943f80ed4079b1275d4ee2016610ad2407f7c8932f2d07e0L252-R252)
* Modified the Machine Vision asset builder and related tests to verify
that components are correctly referenced with `HasComponent`, and that
shared components are properly managed without reparenting or replacing.
[[1]](diffhunk://#diff-c265154f7bd0bbcbaa003aa050270e193e47ba941b41f9158a49f799efbcdf14L506-R517)
[[2]](diffhunk://#diff-c265154f7bd0bbcbaa003aa050270e193e47ba941b41f9158a49f799efbcdf14R528-R535)

**Pump Status Label Enhancements:**

* Refactored pump status variable initialization in the Recycling sample
to ensure that the mandatory two-state properties (`FalseState` and
`TrueState`) are set with the labels "Stopped" and "Running", and that a
descriptive text is provided.
* Updated the Recycling sample documentation to clarify that each pump's
status exposes the mandatory `FalseState` and `TrueState` properties as
"Stopped" and "Running", reflecting the synthetic pump state.

**Testing:**

* Added a comprehensive test to verify that every circulation and drain
pump exposes readable mandatory two-state status labels (`FalseState`
and `TrueState`) with the correct values and reference types.#
Description

_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- Fixes #github-issue-number, ...

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-10-21 13:58:08 +00:00

52 KiB
Raw Permalink Blame History

Recycling line simulation

This sample models a recycling line with one durable coordinator and ten OPC UA process servers. The same executable starts either the coordinator or one selected process. It targets .NET 8, 9 and 10 and exposes typed process data, companion-specification equipment and material genealogy over OPC UA. It is a synthetic software simulation, not a controller for physical equipment.

The optional .NET 10 operator console adds a local browser schematic and role-limited controls.

Fast animated walkthrough of the recycling operator console.

This 12-second animation loops through the console views and commands. Use the step-by-step walkthrough for the full-size screenshots.

Contents

How the sample fits together

Program.cs loads the role, scenario and certificate mappings. RecyclingHost.Build assembles the Generic Host, secure endpoint and dependency-injected runtime. The coordinator alone owns RecyclingEngine, MaterialLedger and RecyclingJournal. Each process server uses CompanionProcessViewAdapter to project committed snapshots onto its equipment: it has no material clock, independent random signals or production counters.

The shared sample model supplies generated structures, methods, clients and DI-derived process types in urn:opcfoundation.org:sample:recycling. Native companion instances and Machinery state AddIns share one DI/Machinery owner; AMB registers the actual assets. The implementation is divided into:

Directory Responsibility
Recycling.Client Shared generated model, immutable contracts, mapping and typed coordinator client; no server executable dependency.
Coordination Authenticated membership, serialized commands and committed snapshots.
Simulation Deterministic transformations, finite buffers, utilities, genealogy and holds.
Persistence Checksummed journal/checkpoint storage through the existing file-system provider.
Hosting Role startup, authorization, companion assembly and read-only equipment projections.

Material, time and faults

flowchart LR
    collection["collection"] --> sorting["sorting"] --> shredding["shredding"] --> washing["washing"]
    washing --> density["density separation"] --> drying["drying"] --> extrusion["extrusion"]
    extrusion --> filtration["melt filtration"] --> pelletizing["pelletizing"] --> bagging["bagging"]

These ten selections contain 21 separately queued substeps. For example, washing contains prewash, friction wash, hotwash and rinse; drying separates mechanical dewatering, thermal drying and buffering.

A fresh run waits for all ten distinct authenticated roles to register and acknowledge an initial snapshot. The coordinator then advances one logical tick per pacing interval:

  1. Downstream operations use previously committed inventory first. Newly received material cannot traverse another substep in that tick.
  2. Residence time, throughput, available buffer capacity and electrical/thermal supply determine which transformations can run. Disabled or unavailable equipment, jams, strand breaks and utility constraints freeze the affected residence progress.
  3. Transformations split or merge component-wise source contributions. The complete result is journaled before it becomes visible to clients or process views.

Defaults are a fixed seed of 104729, 30 logical seconds per tick, one wall-clock second between automatic ticks, 24 source batches, 60 kg buffers, 6 kg/tick throughput, 4 kg complete packages, and one to four residence ticks. Each source batch contains 5 kg dry material plus 0.25 kg water. These settings are in RecyclingConfiguration.

Mass is counted in integer milligrams, separately for natural polymer, colored polymer, dense polymer, labels, dirt and water. Every transition checks the following balance globally, per source, per substep and per split/merge:

source inputs + utility additions
  = active inventory + complete packages + rejects/wastewater + evaporation

Optical sorting removes contaminants; washes add tracked water and remove dirt/labels. Density separation rejects dense polymer while retaining natural and colored polymer equally. Mechanical drying drains water; thermal drying and extrusion evaporate it. Filtration removes particles, not dissolved color. Screening removes proportional fines. Bagging merges contributions into exact full packages and retains any incomplete package as inventory.

The named scenarios are healthy, sorter-drift, utility-disturbance, equipment-faults, combined and recovery. Fault windows use logical ticks. Jam/strand-break counts are absolute onset counters, not counters incremented by reads. Source zones, synthetic hotwash pH and package RGB also belong to committed coordinator state.

Holds and recovery

Equipment execution, data availability and material disposition are separate. Color/moisture holds follow descendants and merged packages; recovering equipment does not release material. An authorized operator or administrator explicitly releases or rejects a hold. Produced-package count can therefore differ from released-product count.

The coordinator writes complete state and accepted command outcomes before acknowledging them. Requests carry line/run identity, expected revision and a caller-owned request ID. Identical control-command retries return the original outcome, including after restart; conflicting reuse or operationally stale revisions are rejected. Membership-only metadata changes do not invalidate an otherwise current control precondition; material ticks, other controls, lifecycle changes and coordinator recovery do. Membership requests separately fence changes to their own role and its lease, without conflicting on material ticks, controls or unrelated roles' metadata. Their observed revision records the image actually applied by the view. Authenticated session ownership is unchanged. Equipment and hold actions separately fence explicit control changes, so continuous material ticks cannot starve a confirmed enable/disable or hold-disposition action. The console forwards the original expected revision unchanged; the coordinator rejects future revisions and mismatched runs. Request IDs and submitted payloads are never rewritten to obtain this compatibility. Reconnection and repeated projections do not duplicate material.

The journal uses length-delimited SHA-256 frames, atomic same-directory publication and an exclusive writer.lock. Checkpoints precede removal of superseded generations. Recovery checks the chain, configuration and embedded model, and refuses corrupt/incompatible state rather than starting an empty replacement run. A valid commit can recover past an interrupted .pending write. Storage is single-writer process-crash recovery, not distributed consensus or a universal power-loss guarantee. The retained schema is version 2; schema-1 stores are rejected.

A paused run stays paused after recovery. Pausing freezes material time but not membership, administration or disposition. Old session leases are invalidated on restart; participants re-register without resetting production. The default lease lasts 30 wall-clock seconds, and loss of a process eventually blocks its flow while upstream buffers remain finite.

Default bounds are 256 active parcels, 4,096 retained records per material/history category, 4,096 control receipts, 64 admitted writer calls, 32 local observers and 10 administrative steps per command. Heartbeats retain only their latest durable outcome per process, separately from control receipts. Held material still consumes retention capacity. Exhausted material retention enters ResourceLimited; a full writer queue refuses the call and automatic pacing retries on the next interval. Frames are capped at 64 MiB; the connection-health guidance describes the UA transport limits.

What the servers expose

Coordinator.State is a structured SnapshotDataType, not JSON in a UA string. It contains run/lifecycle identity, revision, logical tick/time, stages, parcels, complete packages, sinks, source contributions, holds, the latest execution per stage and source admission, participant leases and electrical/thermal energy. ProcessView.State exposes the same live projection alongside Process, LineId, Available and CompanionBindingsComplete. Live values are coalescible current state, not a genealogy archive: repeating all historical executions in every monitored notification causes serialization, cloning and delivery backpressure as a run grows. Coordinator.ReadSnapshot still returns the complete retained snapshot; Coordinator.ReadGenealogy pages every retained execution. No journal records or authoritative material history are discarded. Servers retain at most two retransmission messages per subscription.

Every sample stage also exposes RunState, Available, Timestamp, MachineId, Process, RunId, Revision, Tick, Stage, Material, Contributions, SourceIds, SourceBinId, SourceZone, LotId, Executions and ThroughputKgHr. Mixed lots retain every contribution; SourceZone reports mixed while the contribution records preserve individual zone labels.

Companion assets and process data

Each stage is discoverable from ProcessView, DI DeviceSet and Machinery Machines. AMB's AssetsByProductInstanceUri aliases identify the actual equipment. Machinery identification uses urn:recycling-sample:<line-id>:<asset-name>. Instance NodeIds are stable URI-qualified containment paths, independent of run IDs and numeric namespace-table positions.

The table lists the process selections, default endpoint ports and principal live bindings. The sample namespace carries process-specific measurements alongside the native companion objects.

Selection / port Equipment and substeps Exposed measurements and state
collection / 62571 Collection bin and transport; native VehicleScaleType. Inventory, source contributions/zones and vehicle weight in kg.
sorting / 62572 Inbound identification, feed conveyor, two optical stations; SimpleScaleType, Part 1 VisionSystemType, Part 2 VisionSystemAssetType and identified cameras. BeltSpeedMps, ThroughputTph, JamAlarm/JamEvents, ClassificationAccuracyPct, ContaminationPpm, DriftScore, CameraHealth and native vision results.
shredding / 62573 Primary reduction and secondary granulation with DI/Machinery state. MotorLoadPct, FeedRateKgMin, FlakeSizeAvgMm, ThroughputKgHr and jam flags/counters.
washing / 62574 Prewash, friction wash, hotwash and rinse; native circulation/drain PumpType instruments. Pump status, TurbidityNtu, WaterTempC, FaultState, WashEffectivenessPct, MotorLoadPct, BathTempC, Ph, AdhesiveRemovalPct.
density-separation / 62575 Float/sink process and native circulation pump. CirculationPumpStatus, SeparationPurityPct, HdpeRecoveryPct (a target-polymer recovery proxy).
drying / 62576 Mechanical dewatering, native OPC 40082-6 GranulateDryingDeviceType with bin/desiccant bed, then dry-material buffer. OutletMoisturePct, throughput, native temperature, airflow, dewpoint, residence durations, power and energy.
extrusion / 62577 Native OPC 40084 ExtrusionLine_InterfaceType, Extruder_InterfaceType and temperature zones. Melt temperature/pressure, zone actual/set values, throughput and sample ExtrusionStabilityScore.
melt-filtration / 62578 Native OPC 40084-6 Filter_InterfaceType, FilterPackageType, ScreenPackageType and temperature zones. Mesh fineness, filter status; sample AlarmState, DiffPressureBar, CloggingPct, MeltCleanlinessIndex.
pelletizing / 62579 Native OPC 40084-8 Pelletizer_InterfaceType, drive, knives and die plate; pellet screening. Native drive speed, sample PelletSizeAvgMm, throughput, FaultState, StrandBreakAlarm/StrandBreakEvents.
bagging / 62580 BaggingStationType, native AutomaticFillingScaleType and PackML state. BagWeightKg, PackageCount, BagsPerHr, LotId, ColourR, ColourG, ColourB, QualityClassification and absolute weighing statistics.

Vision's latest result contains typed ExecutionDataType content, a run/execution JobId and IsSimulated=true; reprojecting it does not publish a new inspection. Mechanical dewatering remains a separate process, not a desiccant dryer. Filter differential pressure is a sample measurement because the standard screen-package type has no such member.

Each pump's Operational/PumpActuation/Status exposes the mandatory FalseState and TrueState properties as Stopped and Running. Status reflects committed electrical demand; the labels describe the synthetic pump state, not a local command.

The package models are described in Machinery, Vision, Pumps, Scales, AMB, Extrusion and Plastics/rubber, including granulate drying.

Units, timestamps and quality

  • Numeric sample signals use SyntheticMeasurementType (AnalogUnitRangeType) with EngineeringUnits, EURange, SemanticField and Formula. Weights are kg; native extrusion throughput is kg/h, pressure bar, mesh fineness micrometres and pelletizer speed rpm. Dryer durations are milliseconds, electrical energy kWh, bin airflow m³/h and bed airflow l/s.
  • Rates are run-to-date averages, not instantaneous flow. Removal, purity, recovery and moisture derive from cumulative committed material. Energy exposes separate electrical and thermal joule totals divided by packaged dry mass, including held packages; a zero denominator sets IsApplicable=false.
  • pH defaults to a committed synthetic 8.5 set-point observation. RGB is the floored polymer-mass-weighted mix of configured natural/colored anchors, not calibrated colorimetry. Their individual source timestamps survive subsequent commits and reconnects. Other display coefficients are in RecyclingSignalDefaults and RecyclingNativeProcessOptions.
  • Views publish atomic images with coordinator-owned values and source times. On connection loss, Available=false; present equipment readings retain their last values/time with UncertainLastUsableValue, while the root snapshot reports unavailable status. Missing observations or undefined ratios report BadNoData, not fabricated zero readings.
  • A Good DataValue means usable data, not accepted product. CompanionBindingsComplete=true identifies this selected read-only equipment binding, not optional remote controls or formal conformance. Signals and weighing are synthetic, not calibrated/legal-for-trade measurements or a reproduction of an external producer schema.
  • The console sends 64-bit revisions and identifiers to the browser as JSON strings. Exact mass arithmetic stays in C#. It does not infer capacity, OEE, ETA or utility-supply percentages.

Run the sample

Secure identities

The host uses encrypted/signed OPC UA sessions, RSA-SHA256 application certificates and X.509 user authentication. Anonymous access and automatic trust of unknown peers are disabled. Each role has a separate application PKI root; each process view also has a distinct user certificate and private key. Certificate provisioning uses the stack's certificate manager and certificate stores.

Application public certificates go into peer trust; user public certificates go into user trust. The external role map authorizes already authenticated user certificates:

{
  "identities": [
    {
      "certificateSha256": "<64 hexadecimal SHA-256 digits of the DER user certificate>",
      "identity": "collection-view",
      "access": "ProcessView",
      "process": "Collection"
    }
  ]
}

Replace the placeholder and include one distinct ProcessView identity per process, plus any Observer, Operator and SimulatorAdministrator users. The mapping contains fingerprints, not keys. The application URI is urn:localhost:OPCFoundation:Recycling-<line-id>-<role>; the certificate's names must match the advertised --endpoint.

One shared scenario

All eleven roles use the same effective configuration and embedded sample model. Each executable independently defaults to healthy, so another scenario must be passed to both the coordinator and all ten views. For --scenario <file.json>, every role needs a readable copy with identical effective settings; container copies are mounted read-only.

JSON scenario files override the documented top-level defaults: omitted fields retain their defaults, while explicit zero values, nulls and unknown members still undergo normal validation. The checked-in scenario files have the same effective settings as their corresponding named scenarios.

Views compare the full configuration/model hash before membership or projection. It includes residence, seconds per tick, source settings, fault schedules, leases and bounds—not just the scenario name. A mismatch or missing identity reports BadConfigurationError and stops automatic reconnect, retaining the last compatible image as unavailable. Correcting the view's inputs/build and restarting it preserves the coordinator's retained state. Host endpoints, certificate stores, --pace-ms, and --view-poll-ms may differ; they are outside the simulation identity.

Build and start the roles

Build with the repository's .NET 10 SDK. For managed builds and runs, select net8.0, net9.0 or net10.0 and install the corresponding runtime. The examples select net10.0; substitute your choice in both -f and CustomTestTarget for the managed commands.

From the repository root:

dotnet build samples\Recycling\RecyclingServer\RecyclingServer.csproj -f net10.0 -p:CustomTestTarget=net10.0

Start each role in a separate terminal from samples\Recycling\RecyclingServer, where the default .recycling directory is ignored by Git. The following assumes provisioned roles.json, application PKI stores and user stores; explicit --pki-root and --state-root can select external locations.

dotnet run -f net10.0 --no-build -- --role coordinator --line-id line-1 --scenario healthy --identities .\roles.json

The coordinator listens on opc.tcp://localhost:62570/Recycling. Start a view with its own user store and subject, then repeat for every selection/port in the equipment table:

dotnet run -f net10.0 --no-build -- --process collection --line-id line-1 --scenario healthy `
    --coordinator opc.tcp://localhost:62570/Recycling --identities .\roles.json `
    --identity-store .\user-stores\collection --identity-subject CN=CollectionView

--pace-ms changes wall-clock pacing, not logical seconds per tick. Views may start before the coordinator; production starts only after all ten initialize. --help lists the flags. Docker Compose and Kubernetes examples run the same eleven roles with isolated storage; their single server Docker image uses the managed .NET 10 target.

For larger retained snapshots, --view-poll-ms 10000 reduces each view's durable lease-refresh and fallback-read traffic with the default 30-second lease. The default period is 2 seconds; it must remain below half the lease duration. This setting does not slow the separate UA subscription. Views retry BadTimeout and BadRequestTimeout as temporary communication failures; authorization/configuration rejections remain terminal.

Operator console

The console is a local ASP.NET Core 10 host serving HTML, SVG, CSS and JavaScript. It connects directly to the coordinator and configured process servers over encrypted OPC UA; the browser uses HTTP and server-sent events (SSE), not OPC UA, MCP or MQTT. Recycling.Client is shared with the servers as a sample-local, non-packable library.

Build and configure

Build from the repository root with the .NET 10 SDK:

dotnet build samples\Recycling\RecyclingOperatorConsole\RecyclingOperatorConsole.csproj `
    -f net10.0 -p:CustomTestTarget=net10.0

Provision the workstation's application identity, a dedicated UA user certificate and an unpredictable bootstrap key outside the repository and container build context. Use the certificate manager and certificate stores. Do not copy a process-view private key into the workstation, disable hostname validation or auto-accept unknown certificates. Trust the workstation application on each server it accesses, put server application certificates in workstation peer trust, and bind the UA user's public fingerprint to its role in the coordinator's external identity mapping and user trust.

One console host uses one UA user identity, not a separate identity per browser tab. Its configured role is a permission ceiling: the default Observer is read-only, and a local setting cannot grant permissions denied by the server.

The host reads standard .NET configuration environment variables:

Key prefix RecyclingConsole__ Meaning
Endpoint Explicit coordinator opc.tcp endpoint.
LineId Expected line identity; must match the selected coordinator.
Role Permission ceiling: Observer (default), Operator, or SimulatorAdministrator.
BootstrapKeyFile Required external text file containing a random 32-byte key encoded as hexadecimal.
UserCertificatePath External directory certificate store for the single configured UA user identity.
UserCertificateSubject Subject selector within that store, for example CN=Recycling-line-1-observer.
UserCertificatePasswordFile External text file holding the user store's private-key password; not the password itself.
ApplicationCertificateStore Directory store for the host's separate application identity.
TrustedPeerStore, TrustedIssuerStore, RejectedCertificateStore Application trust, issuer and rejected-certificate directory stores.
AllowedHost Exact browser host/port, normally localhost:8080.
ContainerBind false by default (loopback); true listens on container interfaces using the AllowedHost port.
ProcessEndpoints__<process> Explicit mappings using names such as Collection, DensitySeparation, MeltFiltration and Bagging.

For example, after provisioning the files and peer/user trust:

$env:RecyclingConsole__Endpoint = 'opc.tcp://localhost:62570/Recycling'
$env:RecyclingConsole__LineId = 'line-1'
$env:RecyclingConsole__Role = 'Observer'
$env:RecyclingConsole__BootstrapKeyFile = 'D:\recycling-client\secrets\bootstrap-key'
$env:RecyclingConsole__UserCertificatePath = 'D:\recycling-client\secrets\users'
$env:RecyclingConsole__UserCertificateSubject = 'CN=Recycling-line-1-observer'
$env:RecyclingConsole__UserCertificatePasswordFile = 'D:\recycling-client\secrets\user-password'
$env:RecyclingConsole__ApplicationCertificateStore = 'D:\recycling-client\pki\own'
$env:RecyclingConsole__TrustedPeerStore = 'D:\recycling-client\pki\trusted'
$env:RecyclingConsole__TrustedIssuerStore = 'D:\recycling-client\pki\issuer'
$env:RecyclingConsole__RejectedCertificateStore = 'D:\recycling-client\pki\rejected'
dotnet run --project samples\Recycling\RecyclingOperatorConsole\RecyclingOperatorConsole.csproj `
    -f net10.0 -p:CustomTestTarget=net10.0 --no-launch-profile

Use advertised endpoints with matching certificate DNS names; localhost does not rewrite discovery URLs. Set each native process mapping explicitly. The optional Compose operator-console profile supplies those mappings and isolated client mounts; see the deployment guide.

Open http://localhost:8080/bootstrap?token=<contents-of-bootstrap-key-file> locally, or use the authentication form in the walkthrough below. The launch credential is exchanged for a short-lived HttpOnly, SameSite=Strict session cookie and the browser is redirected to /. Treat that URL as a credential: keep it out of shared logs, screenshots and committed scripts, and protect browser history and the key file. Commands require the browser session and a same-origin CSRF token. Keep the allowed host explicit; do not add broad CORS or expose this sample as a remote multiuser service. Rotate the bootstrap key and restart to revoke launch access when needed.

Operator console walkthrough

Start the coordinator, all ten process servers and the console using the deployment guide, or configure the workstation host above. The browser has no Start button.

The screenshots below show an isolated synthetic line using the checked-in combined scenario. Its seed, finite source plan, fault windows and material balances are server-owned; the browser does not generate telemetry. The walkthrough uses a SimulatorAdministrator identity to show every action.

Configured role Available operations
Observer / unknown Plant, Inspector, Trends, Material and Holds inspection; no mutations.
Operator Observer operations, process enable/disable and explicit hold release/rejection.
SimulatorAdministrator Operator operations plus Pause, Resume, Step one tick, Stop, Reset and Checkpoint.

Connect and read the plant

  1. Open the configured local address, normally http://localhost:8080.
  2. On Connect the operator console, paste the contents of the external bootstrap-key file into Bootstrap token, then select Authenticate locally. Alternatively, use the local bootstrap URL described above.
  3. Check the displayed role, Connected state, lifecycle and Quality before acting. If the browser session expires, authenticate locally again; this does not create a new run.

Local authentication form with an empty bootstrap-token field.

In Plant, follow the arrows from collection through bagging. Each equipment tile represents one process; selecting it opens its substeps in the Inspector. The diagram shows process order, not the plant's physical layout. The process list below the schematic provides the same selection using keyboard focus and Enter. On narrower windows, use Inspector in the navigation rail to open the inspection drawer. Live updates patch existing SVG symbols and buttons in place to preserve pointer gestures, keyboard focus and open inspector sections.

The top bar shows admitted Input, Work in progress, Packaged mass, complete Packages, and electrical/thermal energy. These are committed cumulative counters, not instantaneous rates or estimates. Packaged material can still be held. Arrows pulse only when an observed committed transfer changes; they are not an independent animation of production. The time beside the role is the browser session's expiry, not the simulation clock. Process Transferred totals sum substep transfers: the same material can be counted at several substeps, so do not add these totals to the plant mass balance.

Combined-scenario plant showing a paused fault window, process states, quality and exception dock.

The footer separates Active faults / constraints, Quality holds and Recent command outcomes. Select a fault to inspect the affected process. A jam or strand break freezes the affected residence progress; downstream equipment can finish material it already holds, while upstream finite buffers may fill. A utility shortage constrains work rather than creating material or skipping a substep. In combined, the first fault/utility window is ticks 8–24, with a conveyor jam at ticks 12–16; later windows affect shredding and pelletizing. Pause preserves the current tick, so a fault paused inside its window remains visible. Equipment badges retain the last committed substep activity: an Executing badge in a paused snapshot does not mean material is advancing while logical time is frozen.

Inspect equipment and companion readings

Select Sorting, then scroll within the Inspector. Native companion source reports whether a process endpoint is configured and currently available, plus its run/configuration. Measurements with provenance lists values, units, value/unit quality, original source time, run, tick and projection lag. Substeps shows the separately queued equipment states and enabled/fault flags. For example, Washing has four substeps, not one instantaneous wash. An identifier or Boolean can have a Good value and BadNotSupported unit quality because it has no engineering-unit metadata; inspect value quality separately from unit metadata quality.

Sorting inspector scrolled to native companion measurements and their provenance.

Try the other processes: Drying exposes its native drying-device measurements, Extrusion its temperature/throughput bindings, and Bagging its scale/PackML projection. These are independent reads from the configured process servers, not coordinator values relabeled as native data. Missing endpoints or mismatched runs/configurations remain explicitly unavailable. The equipment table lists each process's bindings.

Later production state with accumulated packages and the Bagging inspector.

For the transformation rules, residence times and component-wise balances behind these readings, see Material, time and faults.

Disable and enable a process

  1. Select Washing, scroll to Authorized commands, then select Disable process.
  2. Review the confirmation's command, selected process, run, revision and request ID. Cancel or Escape dismisses it without submitting. Select Submit exact payload only when the target is correct.
  3. Wait for an Accepted outcome and the authoritative disabled substeps. The console does not optimistically change plant state after an HTTP response.
  4. Select Enable process to re-enable those substeps. Enable is intentionally disabled while every substep is already enabled; it becomes available after Disable is observed.

Disable-process confirmation showing the exact proposed command envelope.

Authoritative disabled Washing process with Enable process available.

Disabling freezes that process's material progress; it does not disconnect its server, stop the whole line or dispose its holds. Enabling allows progress when membership, utilities, fault conditions and downstream capacity also permit it. A process can therefore be enabled without executing.

A new confirmation follows live revisions until submission, but never retargets the process, hold or run. Submission freezes the envelope. Rejected means the request was refused: review the reason and refreshed state before choosing a new action. Pending or Unknown is not proof of success or rejection: a timeout or failed response can follow a durable commit. Retry exact envelope explicitly resubmits the original request ID and payload to retrieve the durable outcome. Do not use a new request ID to retry an unknown outcome. Run changes or loss of live delivery cancel pending confirmations. Rejections remain visible across subsequent live updates; the browser never automatically resubmits a conflicting command. Recent outcomes are bounded workstation-session observations, not a full audit log. A console restart can lose in-memory pending actions; the coordinator's durable receipts remain authoritative.

Review and dispose quality holds

Open Holds rather than relying on the footer's short preview. Outstanding shows held records; All dispositions, Released and Rejected include retained decisions. Use Next page, Previous page and Refresh page to navigate the complete retained set. The page's run/revision identifies its observation; refreshing is not a disposition. The read-only /api/holds endpoint accepts runId, an after hold ID, limit (1..100), and an optional disposition.

Outstanding quality holds with disposition filter and retained-history page controls.

An Operator or SimulatorAdministrator can select Release hold or Reject hold for a held record, review the confirmation, and submit. The decision changes that hold's disposition. It does not repair equipment, add a package or remove the historical record. Inspect the resulting disposition using All dispositions. Descendants and mixed packages retain their quality history; one release does not clear other outstanding holds affecting them.

Retained hold records after explicit release and rejection, with command outcomes in the footer.

Quality: Good describes usable data. See Holds and recovery for how product disposition remains separate from equipment recovery.

Open Material, select Load material page, then Load history page. Material records show retained parcel/package/residue IDs, tick, substep, material form and total mass; execution records identify the tick, substep and input/output IDs of a transformation. The buttons request subsequent bounded pages; Next page text reports the cursor or none. Use these IDs to trace parcels and packages through splits and merges back to their sources.

Material page showing retained residue identifiers, substeps, material form and total mass.

Scroll down within the Material panel to reach Execution history page:

Execution history showing transformation IDs, ticks, substeps and input/output material IDs.

The browser shows a compact trace, not every component of the server's material ledger. For exact source contributions and component-wise balances, inspect the typed ReadSnapshot/ReadGenealogy data described below.

Live UA notifications retain only each stage's latest execution and the latest source admission. Load history page queries the complete retained genealogy separately; it is not limited to what happened while this browser was open. Material/history requests remain run-scoped. Direct snapshot reads before and after the genealogy call reject reset races with HTTP 409, even if the subscription still exposes the old run.

Open Trends to compare work-in-progress and packaged mass over observed logical ticks. The chart and recent rows start with this console's connection, are bounded in memory and show observation gaps/reset boundaries. They are not a persisted historian: reconnecting or opening another console does not reconstruct all earlier trend points.

Session-local material trends with logical-tick observations and recent rows.

Administer simulation time and persistence

With a SimulatorAdministrator identity, select a process, scroll to Authorized commands and expand Simulator administration. These actions address the coordinator, not the selected process's native companion controls.

Expanded simulator administration with paused-state controls and recent accepted outcomes.

Action Preconditions and observable effect
Pause Freezes automatic material ticks; membership/lease refresh, inspection and authorized controls continue.
Resume Continues a paused or stopped run without replacing its identity or retained material.
Step one tick Available while paused; advances exactly one logical material tick, then remains paused.
Stop Requires confirmation. Stops automatic material progression, preserving the run for Resume or Reset. This is not a physical emergency stop.
Checkpoint Persists/compacts the coordinator's retained state without restarting the run or clearing holds.
Reset Requires a stopped run and confirmation. Creates a new run with fresh production state; it is not reconnect/recovery or a way to release a hold.

To check stepping, Pause, record the tick, select Step one tick, and check that the tick increases by exactly one while the lifecycle stays Paused. Revision can increase again because membership or control metadata changes. Checkpoint provides an explicit persisted boundary for a restart exercise; ordinary commits are already durable and do not require a manual checkpoint. Use Stop followed by Resume to continue the same run; use Stop followed by Reset only when deliberately starting a replacement run.

Reset confirmation on a stopped run, before creating a new production run.

After Reset, inspect the new run ID and cleared run-scoped production views. Process roles must initialize for the new run before production proceeds. In contrast, restarting the coordinator on its compatible journal recovers the existing run; a paused run stays paused, and process roles re-register without duplicating material. Closing the browser or console is neither Stop nor Reset and does not stop the eleven servers.

Recognize idle, constrained and unavailable states

Use lifecycle, tick, quality, material counters and fault details together:

Observation Interpretation and next check
WaitingForParticipants The ten-role initialization barrier is incomplete; check missing process servers and matching configuration/identities outside the browser.
Running, tick advancing, no new input The finite source plan may be complete. Downstream work can drain, then enabled equipment becomes idle. Defaults admit 24 source batches.
Idle with a small remaining inventory A residence/throughput boundary or incomplete final package can leave material waiting; bagging does not invent mass to fill a bag.
Paused or Stopped, unchanged tick Intentional lifecycle state. Resume, or use Step while paused; do not diagnose it from timestamp age alone.
Disabled, jammed or utility-constrained substeps Production is blocked locally. Inspect the process and scenario window; enabled upstream equipment can become buffer-limited.
ResourceLimited A configured retention/capacity bound was reached. Enable, page refresh and reconnect cannot remove it. Preserve the journal and review the configured limit before starting a new run; the browser cannot edit recipes.
Unavailable/reconnecting or non-Good quality Retained values are not fresh measurements. Mutations are disabled until a new usable live observation arrives; inspect server/transport/trust/configuration health.

Unavailable coordinator showing retained stale observations and disabled controls rather than fabricated zero values.

Source is the original observation time; Received is workstation receipt time. An unchanged source time, pH or RGB is not alone a transport failure. Conversely, a healthy HTTP page does not prove UA subscription health. The console distinguishes browser event delivery, coordinator connectivity and native process availability, and does not promote retained data to fresh data merely because a connection returns.

Channel/keep-alive failures and persistent Publish failures invalidate retained data even when the managed session keeps its ID. Returning to Ready is insufficient: controls require a fresh usable observation on the browser's live stream. SSE refusals, including the live-consumer limit, trigger retries with bounded backoff. A one-off HTTP refresh cannot re-enable controls; browsers without SSE remain read-only.

Clients and servers use matching 16 MiB message/byte-string quotas and 100,000-element array quotas. Discovered endpoints inherit the application quotas. Larger recipes must fit them; oversized responses fail delivery without changing certificate validation or authorization.

Recipe selection, pacing, fault injection, server startup/shutdown and certificate/role provisioning remain command-line/deployment operations. The console does not edit recipes, acknowledge/shelve synthetic alarm flags, write native companion production controls, or provide physical safety functions.

Observe and control the line

  1. Connect an OPC UA client with a trusted application certificate and mapped Observer user certificate. Browse namespace URIs rather than assuming numeric namespace indexes.
  2. Subscribe to Coordinator.State, ProcessView.State or individual equipment measurements. Compare RunId, Revision, Tick, Available and source timestamps: heartbeat/control commits can advance revision without advancing material time.
  3. Follow Parcels, Packages[].Material.Contributions, Holds and History to trace material. ReadGenealogy(afterExecutionId, maximumResults) returns pages of up to 128 executions; ReadSnapshot() returns the current typed image.
  4. Observe sorter-drift through classification, contamination and inherited holds; utility-disturbance through constrained stages/residence queues; and equipment-faults through jam/strand-break state and absolute counters. The recovery scenario restores equipment conditions without implicitly releasing product holds.

The generated clients are RecyclingCoordinatorTypeClient and RecyclingProcessViewTypeClient. RecyclingCoordinatorClient wraps a managed session with typed calls and subscriptions. For an already authenticated observer session:

await using var client = new RecyclingCoordinatorClient(session, telemetry);
await foreach (CoordinatorNotification update in client.WatchAsync(cancellationToken))
{
    CommittedSnapshot state = update.Snapshot;
    Console.WriteLine(
        $"Available={update.IsAvailable}, tick={state.Tick}, packages={state.Packages.Count}");
}

The coordinator's typed Execute method accepts operator/admin enable/disable and hold-disposition commands, plus administrator pause/resume/stop/reset/step/checkpoint commands. Stepping is paused-only; reset requires an explicit stop and is not recovery. Retries preserve the original request ID and payload. Native production writes/methods are refused; vision result retrieval remains available. ProcessView.ForwardCommand is limited to its own process's enable/disable commands and defaults to BadNotSupported; the optional forwarder preserves the originating end-user certificate, never elevating the process service identity.

Container images

Images CI registers recyclingserver and recyclingclient under its existing GHCR naming, branch/version tags, PR no-publish policy and platform matrix. Published tags require a successful publishing run. Build locally from the repository root:

docker build -f samples\Recycling\RecyclingServer\Dockerfile -t recycling-server:local .
docker build -f samples\Recycling\RecyclingOperatorConsole\Dockerfile -t recycling-client:local .

Both Dockerfiles compile portable framework-dependent .NET 10 IL on $BUILDPLATFORM, without a runtime identifier, app host or NativeAOT. The server image uses the .NET runtime; the console uses the ASP.NET runtime and includes wwwroot. Both run as the non-root .NET app user. Their build stages pin an SDK version independently of global.json; the image SDK must meet the repository's minimum stable SDK version.

CI publishes once natively, then reuses the prebuilt output for linux/amd64 and linux/arm64/v8 without compiling an SDK under QEMU. To reproduce the console path:

dotnet publish samples\Recycling\RecyclingOperatorConsole\RecyclingOperatorConsole.csproj `
    -c Release -f net10.0 -p:CustomTestTarget=net10.0 `
    -p:PublishAot=false -p:UseAppHost=false -p:SelfContained=false `
    -o artifacts\docker\publish\recyclingclient
docker build -f samples\Recycling\RecyclingOperatorConsole\Dockerfile `
    --build-arg PUBLISH_SOURCE=prebuilt `
    --build-arg PUBLISH_DIR=artifacts/docker/publish/recyclingclient `
    -t recycling-client:prebuilt .

For the server, substitute its project, Dockerfile and recyclingserver publish directory. Keep secrets, private scenarios and runtime journals outside either publish directory. NativeAOT publication is a separate target.

Podman can use the same Dockerfiles and topology through its Compose provider. Use engine-accessible data paths or separately provisioned role-isolated volumes; Windows host paths are not automatically Linux engine paths. Preserve the read-only secret mounts, writable per-role stores and UID/GID 1654 permissions. On Windows, verify forwarding from workstation loopback to the machine separately from the container port mapping. Any temporary forwarder must bind only to loopback, target the intended port, and be stopped after use. Do not widen HTTP bindings, relax Host/Origin checks or bypass UA certificate validation.

The deployment guide covers the optional Compose profile. It enables container binding but publishes HTTP on host loopback only. The Kubernetes example remains server-only.

Validation

Image contracts and live qualification

Image contract tests are in RecyclingImageContractTests in Opc.Ua.Recycling.Operator.Tests:

dotnet test tests\Opc.Ua.Recycling.Operator.Tests\Opc.Ua.Recycling.Operator.Tests.csproj -f net10.0 `
    -p:CustomTestTarget=net10.0 --filter FullyQualifiedName~RecyclingImageContractTests

These are static checks. Separately build both Dockerfiles from source and prepublished output, inspect both architectures and static assets, and run an isolated Compose line with strict trust and a provisioned identity. Check native inspection, unavailable endpoints, reconnect, pause, stale/unauthorized commands and exact retries on a host with a working container engine.

Browser interaction profile

The .NET 10 operator test project contains the NUnit Explicit class OperatorBrowserTests. Build first to generate Playwright's installation script, install Chromium once for the current user, then select the class:

dotnet build tests\Opc.Ua.Recycling.Operator.Tests\Opc.Ua.Recycling.Operator.Tests.csproj `
    -c Release -f net10.0 -p:CustomTestTarget=net10.0
pwsh tests\Opc.Ua.Recycling.Operator.Tests\bin\Release\net10.0\playwright.ps1 install chromium
dotnet test tests\Opc.Ua.Recycling.Operator.Tests\Opc.Ua.Recycling.Operator.Tests.csproj `
    -c Release -f net10.0 -p:CustomTestTarget=net10.0 --no-build `
    --filter FullyQualifiedName~OperatorBrowserTests

Chromium is a test prerequisite, not an application dependency or an npm build. Unfiltered runs do not execute this explicit profile. It serves the actual static UI to Chromium with deterministic test-only HTTP responses and exercises viewport, keyboard, reduced-motion and confirmation behavior. Live UA connectivity, certificate trust, production HTTP authentication and container startup need the separate live qualification above.

NativeAOT publication

Use a stable .NET SDK allowed by global.json and the platform's native toolchain. Windows needs Visual Studio C++ Build Tools with the Desktop development with C++ workload and a supported Windows SDK. Linux needs the NativeAOT prerequisites, including Clang and development libraries.

From the repository root in a Visual Studio Developer PowerShell with the linker and Windows SDK environment initialized, publish Windows x64:

dotnet publish samples\Recycling\RecyclingOperatorConsole\RecyclingOperatorConsole.csproj `
    -c Release -f net10.0 -r win-x64 -p:CustomTestTarget=net10.0 `
    -o artifacts\recycling-console-native\published
.\artifacts\recycling-console-native\published\RecyclingOperatorConsole.exe

A runtime identifier enables AOT in the console project. Do not set PublishAot=true globally: it would also apply to the .NET Standard source-generator projects. Provision and export the external certificate, bootstrap and endpoint settings from Build and configure before launching. The runtime identifier must match the native build host.

The executable profile provisions isolated test certificates and a real coordinator:

$env:OPCUA_RECYCLING_OPERATOR_NATIVE_EXE = (
    Resolve-Path artifacts\recycling-console-native\published\RecyclingOperatorConsole.exe).Path
dotnet test tests\Opc.Ua.Recycling.Operator.Tests\Opc.Ua.Recycling.Operator.Tests.csproj `
    -c Release -f net10.0 -p:CustomTestTarget=net10.0 `
    --filter FullyQualifiedName~NativeConsoleAuthenticatesStreamsAndControlsTheRealCoordinatorAsync

Also run the explicit graceful-shutdown profile against that executable:

dotnet test tests\Opc.Ua.Recycling.Operator.Tests\Opc.Ua.Recycling.Operator.Tests.csproj `
    -c Release -f net10.0 -p:CustomTestTarget=net10.0 `
    --filter FullyQualifiedName~NativeConsoleGracefullyStopsWithoutStoppingTheCoordinatorAsync

It holds an SSE request open while sending an operating-system shutdown signal, requires a clean native exit, then reads and resumes the independent coordinator through encrypted UA and verifies continued ticks in the same run. Windows uses an isolated console and a built-in Windows PowerShell helper that refuses to signal a console containing an unrelated process. Other platforms use a PID-targeted SIGTERM. Forced termination is failure cleanup only.

SSE requests observe application shutdown, browser cancellation and session expiry so an open browser does not consume the host's shutdown budget before UA services stop. Native qualification must cover authentication, SSE, typed reads, authorized commands, static assets and graceful shutdown; publishing alone does not verify them.

See the developer guide for solution-level PR gates.