Files
Marc Schier 724757122b Add recycling simulator and plastics/rubber companion packages (#4772)
# Description

Add a generic, company-neutral recycling simulation sample and reusable
plastics/rubber companion-model packages. One durable causal coordinator
and ten separately launched OPC UA process views provide deterministic
material balances, finite buffers, genealogy, utilities, quality
holds/disposition, authenticated controls and crash recovery. Process
views consume committed state instead of running independent
simulations.

Real companion-model equipment instances use DI/Machinery/AMB discovery,
Machine Vision, Pumps/Scales, granulate drying, extrusion, filtration
and pelletizing. Vendor/process extensions remain sample-local.
Connectors and WoT conversion are out of scope. **External
source-contract parity and complete deployment qualification are not
claimed complete.**

The sample and managed tests target **.NET 8, 9 and 10**. The
consolidated [recycling guide](samples/Recycling/README.md) explains the
actual composition, simulation, observation paths and exposed data; the
application-directory README points to that guide.

Typed client member extensions are emitted at build time through the
default-false `ModelSourceGeneratorClientAccessorsOnly` opt-in.
Extrusion uses it for all twelve parts instead of checked-in generated
C# and a Python generator. The shared client runtime retains typed
values, quality, timestamps and streaming behavior.

### Package layout

Six packages contain fourteen models in two model-producing assemblies:

| Specifications | Model | Typed client | Server runtime |
| --- | --- | --- | --- |
| OPC 40083 and OPC 40082-6 | `Opc.Ua.PlasticsRubber` |
`Opc.Ua.PlasticsRubber.Client` | `Opc.Ua.PlasticsRubber.Server` |
| OPC 40084 parts 1–12 | `Opc.Ua.Extrusion` | `Opc.Ua.Extrusion.Client`
| `Opc.Ua.Extrusion.Server` |

NuGet IDs use the `OPCFoundation.NetStandard.` prefix. All six packages
are registered in `.azurepipelines/expected-packages.txt` and the
generated preview-pack solution. Granulate-drying code lives in
`GranulateDrying` folders in the PlasticsRubber projects; extrusion uses
per-part folders. There are no standalone granulate-drying or per-part
extrusion packages. Published model identities and CLR namespaces remain
distinct and unchanged.

The common 1.03 interface is independently authored on the explicitly
MIT-licensed 1.02 implementation basis; the official 1.03 XML is not
redistributed. The other imported models retain their individual MIT
notices. All 42 pinned model, identifier and basis files retain their
manifest-backed bytes, including their staged Git blobs. No formal
companion certification is claimed; the disputed Filter profile is not
advertised.

## Related Issues

No tracking issue is currently linked.

## Checklist

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

- [ ] 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.
- [ ] 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.
- [x] 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.
- [x] I have addressed **all** PR feedback received.

Current-head CI and coverage remain pending; the full local `UA.slnx`
test matrix is not claimed complete.
2026-10-15 07:00:58 +02:00

9.3 KiB

Generic recycling deployment examples

These examples run the sample as one coordinator and ten process views. They use the same synthetic configuration and typed coordination model as the console host. They do not add a WoT connector, cloud ingestion, hardware control, or certification. Check the sample README's current limitations before using the data.

The Dockerfile publishes and runs the managed .NET 10 target; all eleven services use that one image. The repository does not publish a prebuilt image for these examples. Build your own image. All private scenarios, certificate stores, user keys, and runtime state stay outside the repository and the container build context.

Shared simulation configuration

All eleven roles must use the same effective simulation configuration and embedded sample model. The checked-in Compose and Kubernetes commands omit --scenario for every role, so all use healthy; the shared image also supplies the same compiled sample model.

To select another named scenario, add the same --scenario <name> to the coordinator and all ten process-view commands/argument lists. The option is local to each executable; configuring only the coordinator does not propagate it to the views.

For an external scenario JSON file, supply identical effective contents to every role and add --scenario <container-path> to each command. Mount the file read-only into each Compose service and each Kubernetes workload using your deployment's configuration provisioning. The existing identity-mapping mounts do not automatically supply a scenario file. Keep scenario files out of the image context and keep existing data mounts and persistent claims intact.

Views verify the complete configuration/model hash before membership commands or projection. A mismatch or missing identity produces BadConfigurationError, leaves the endpoint available for diagnosis, and stops automatic reconnect. Correct the view's scenario/build and restart it; do not delete journals, reset the run, or replace retained volumes to bypass the refusal. Matching scenario names alone is insufficient: residence, tick duration, leases, limits and all other effective simulation settings must also match. Role-specific certificates/endpoints and wall-clock pacing may remain independent.

Provision first

Use the stack's certificate manager and certificate-store guidance. Do not enable automatic acceptance of unknown certificates to make startup appear successful.

Provision a separate data directory for each role:

external-recycling-data
  identities.json
  coordinator
    pki
    state
  collection
    pki
    users
    state
  sorting
    pki
    users
    state
  ...
  bagging
    pki
    users
    state

pki is the application's normal certificate-store root. users is a directory certificate store containing only that process's user identity and private key. state is writable runtime storage; only the coordinator has a production journal. Create these directories through the appropriate storage/certificate APIs and allow the container's non-root UID/GID 1654 to access them. Protect private keys and encrypt persistent storage and backups as appropriate.

The sample's application URI is stable: urn:localhost:OPCFoundation:Recycling-line-1-<role>. Application certificates must include the configured endpoint DNS name, such as recycling-sorting, in their permitted names. The process user subject selected by these examples is CN=Recycling-line-1-<role>; provision that subject in its own users store. Application identity and user identity are separate certificates and trust checks.

Populate identities.json using the sample README's schema. Give each of the ten process identities its own ProcessView binding and process selection. Add separately provisioned observer/operator/administrator user identities as needed. Never place operator or administrator private keys in a process-view container. The mapping contains public certificate fingerprints, not private keys; public fingerprints alone do not authenticate a caller.

Exchange the required application public certificates into peer trust and the required user public certificates into user trust. Preserve the stack's actual directory-store layout and filenames; do not manufacture private-key filenames or store keys in JSON.

Docker Compose

From the repository root:

docker build -f samples\Recycling\RecyclingServer\Dockerfile -t recycling-server:local .
$env:RECYCLING_DATA_DIR = 'D:\recycling-data'
docker compose -f samples\Recycling\RecyclingServer\compose.yaml config --quiet
docker compose -f samples\Recycling\RecyclingServer\compose.yaml up -d

Use an absolute, provisioned RECYCLING_DATA_DIR outside the repository; the path above is an example, not a folder the sample creates. Each container mounts only its own role directory and the shared read-only fingerprint mapping. Its application filesystem is read-only, with a temporary filesystem for runtime scratch work. Change RECYCLING_IMAGE to use a different locally built image.

Coordinator port 62570 and view ports 62571 through 62580 are published on the host's loopback interface only. Inside the Compose network every role listens on port 62570 and advertises its own recycling-<role> DNS endpoint. An external client must be able to resolve the advertised DNS names and trust matching certificates; publishing a port does not rewrite OPC UA discovery URLs or certificate names. Configure deployment-appropriate endpoints rather than disabling domain validation.

Views can start before the coordinator is usable and retry without inventing local production. The new run starts only after all ten roles authenticate, register, and initialize. Inspect the typed Coordinator.State and view availability with an authenticated OPC UA client; an existing container or an open TCP port does not prove the material flow is running.

docker compose -f samples\Recycling\RecyclingServer\compose.yaml logs --tail 100
docker compose -f samples\Recycling\RecyclingServer\compose.yaml down

Stopping containers does not delete the externally mounted state. Do not delete journals or reset the run as a substitute for recovery.

Kubernetes

recycling.yaml contains one namespace, eleven headless Services, and eleven single-replica StatefulSets. Each role has its own persistent volume claim; the coordinator is not an HA replica set. The workload has no Kubernetes API permissions or mounted service-account token. Pods run without root privileges, drop capabilities, and mount a read-only application filesystem.

Choose an explicit disposable/test context. Load the locally built image into that cluster or change the image reference to a registry you control; recycling-server:local is not a published registry image.

kubectl kustomize samples\Recycling\RecyclingServer\deploy
kubectl --context <test-context> create namespace recycling-sample
kubectl --context <test-context> -n recycling-sample create configmap recycling-identities `
    --from-file=identities.json=<external-identities-file>

Provision each role's claim through your cluster's approved storage/certificate tooling before starting it. Claim names follow data-recycling-<role>-0; their root contains the same pki, users (views), and state directories as the console/Compose layout. The manifests use the cluster's default storage class and request 1 GiB per role. Adjust the storage class, encryption, capacity, resources, and network policy to your environment. For staged provisioning, pre-create and populate the claims with those exact names before enabling the roles. A StatefulSet with zero replicas does not itself create its claims.

kubectl --context <test-context> apply --dry-run=server `
    -k samples\Recycling\RecyclingServer\deploy
kubectl --context <test-context> apply -k samples\Recycling\RecyclingServer\deploy
kubectl --context <test-context> -n recycling-sample get pods,pvc,services

Do not run these commands against an implicit current context. Do not force-delete a coordinator pod or scale it above one while its prior writer may still own the volume. ReadWriteOnce alone is not a distributed writer lock, and a StatefulSet is not a consensus protocol. The coordinator also enforces exclusive writer ownership in its store.

TCP startup/readiness/liveness probes check transport availability only. They deliberately do not wait for all participants, which would hide the endpoints needed to satisfy that barrier. Use the typed run state, leases, snapshot revisions, and unavailable statuses for production readiness and recovery checks. A paused run is not a liveness failure.

Operational checks

With provisioned test identities, verify a new run's participant barrier, material progress, hold/disposition behavior, one view's loss/reconnection, and a coordinator restart with its volume preserved. Confirm the run ID, committed counters, source contributions, and command outcomes are recovered without duplicate transfers. Leave paused runs paused after recovery.

Do not interpret static manifest parsing, a listening socket, or successful process startup as evidence of full companion conformance or source-data parity. Native/container startup and cluster checks require the relevant runtime and an explicitly provisioned test environment.