# Device Integration (DI) — Software Update Facet > **Glossary:** "DI" throughout this document means OPC UA > [Device Integration (OPC 10000-100)](https://reference.opcfoundation.org/specs/OPC-10000-100), > *not* .NET Dependency Injection. This document explains how to expose the OPC 10000-100 §10.3 software-update facet on a Device Integration (DI) device, what address-space surface it creates, and how clients drive it. ## Address-space layout `WithSoftwareUpdate(...)` materialises one `SoftwareUpdateType` instance per device with the following children: ``` {device} (ComponentState / DeviceState) └─ SoftwareUpdate SoftwareUpdateType (OPC-DI i=1) ├─ Loading {Package|Direct|Cached}LoadingType │ (default: PackageLoadingType i=137) ├─ PrepareForUpdate PrepareForUpdateStateMachineType (i=213) │ ├─ CurrentState │ ├─ Prepare (Method) │ ├─ Abort (Method) │ └─ Resume (Method) ├─ Installation InstallationStateMachineType (i=249) │ ├─ CurrentState │ ├─ InstallSoftwarePackage (InstallSoftwarePackageMethodType i=389) │ ├─ InstallFiles (InstallFilesMethodType) │ ├─ Uninstall (Method) │ └─ Resume (Method) ├─ PowerCycle PowerCycleStateMachineType (i=285) └─ Confirmation ConfirmationStateMachineType (i=307) └─ Confirm (Method) ``` The Loading subtype is configurable: `UsePackageLoading()` (default, file transfer + `CloseAndCommit`), `UseDirectLoading()`, `UseCachedLoading()`. The full structure is added to the `AsyncCustomNodeManager`'s `PredefinedNodes` via `AddPredefinedNodeAsync`, so direct NodeId lookup, browse, subscription wiring, and method calls all work out of the box. ## Server side — fluent surface ```csharp using Opc.Ua.Di.Server.Builders; using Opc.Ua.Di.Server.SoftwareUpdate; // 1) Register a server-wide package store (commonly via // .NET Microsoft.Extensions.DependencyInjection). services.AddSingleton(); // 2) Inside ConfigureDevicesFor: device.WithSoftwareUpdate( ctx.GetRequiredService(), su => su.UsePackageLoading()); ``` `WithSoftwareUpdate` auto-registers a per-device `MemorySoftwareFolder` keyed by the device's NodeId. Override with `.WithSoftwareFolder(folder)` if you need a custom folder implementation (e.g. `FileSystemSoftwareFolder` for persistence between server restarts). ### Method-handler hooks The library ships "succeed immediately" defaults for every method so the sample is fully exercisable out of the box. Override individual hooks to drive the real firmware-flashing logic: ```csharp device.WithSoftwareUpdate(packageStore, su => su .OnPrepare(async (ctx, ct) => { // Validate the staged package, lock the device, etc. await myDevice.StartPreparationAsync(ct); }) .OnInstall(async (ctx, package, ct) => { // Flash the firmware. Throw to fail the state machine. await myDevice.FlashAsync(package, ct); }) .OnConfirm(async (ctx, success, ct) => { await myDevice.CommitOrRollbackAsync(success, ct); }) .OnUninstall(async (ctx, ct) => { await myDevice.UninstallAsync(ct); })); ``` `ISoftwareUpdateContext` exposes the device NodeId, the server's system context, the package store, and the per-device software folder so handlers can persist version metadata without having to re-resolve any of these. ### Server-side state reporting The four state machines (`PrepareForUpdate`, `Installation`, `PowerCycle`, `Confirmation`) are pre-seeded to their initial state (`Idle` / `NotWaitingForConfirm` / `NotWaitingForPowerCycle`) when the SU facet attaches. Every successful method invocation walks the FSM through the standard Part 16 transitions and publishes `CurrentState` / `LastTransition` updates that subscribers observe in real time. Failures route to the Error state (Installation) or back to Idle (PrepareForUpdate), again with proper `LastTransition` metadata. The Installation FSM also exposes a per-operation `PercentComplete` byte that walks 0 → 100 on success. To plug application instrumentation into the same lifecycle without having to subscribe through the address space, register the state-changed hooks: ```csharp device.WithSoftwareUpdate(packageStore, su => su .OnInstall(async (ctx, package, ct) => await myDevice.FlashAsync(package, ct)) .OnInstallationStateChanged((ctx, change) => { _logger.LogInformation( "Install on device {Device}: phase={Phase} progress={Pct}% message={Msg}", ctx.DeviceId, change.Phase, change.ProgressPercent, change.Message); return default; })); ``` Each hook fires twice per method call — `Started` before the application callback runs, then `Completed` on success or `Failed` (with the exception message) on failure. The hook is invoked from the service call's async context; exceptions thrown by the hook are logged and swallowed so instrumentation faults never abort the underlying SU operation. Domain-keyed `SoftwareUpdatePhase` (rather than raw Part 16 cause / transition ids) keeps application code independent of dispatcher internals. > **Note** — the source-generated DI FSMs ship without Part 16 > `StateTable` / `TransitionTable` / `CauseMappings` overrides, so > `FiniteStateMachineState.SetState(...)` (and by extension > `StateMachineBuilder.For(...).WithCause(...)`) is a no-op against > them today. The SU wiring works around this by writing > `CurrentState` / `LastTransition` directly via the internal > `SoftwareUpdateStateMachineDispatcher` helper. Once the generator > emits the Part 16 tables (tracked as a separate generator > enhancement) the dispatcher can collapse into a thin > `StateMachineBuilder.For(...)` adapter without touching the public > hook surface. ## Storage abstractions Two independent storage abstractions live under `Opc.Ua.Di.Server.SoftwareUpdate`: | Type | Role | |---|---| | `ISoftwarePackageStore` | Server-wide repository: Add/Get/List/Delete by id + binary payload. Backs the Loading file-transfer pipeline. | | `ISoftwareFolder` | Per-device multi-version archive (Current / Previous / Future). Updated by the default Install handler. | Default implementations: | Implementation | Use case | |---|---| | `MemoryPackageStore` | In-process tests + samples. | | `FileSystemPackageStore` | Disk-backed, composed over `IFileSystemProvider`. | | `MemorySoftwareFolder` | Default for `WithSoftwareUpdate`. | | `FileSystemSoftwareFolder` | Persistence across server restarts. | ## File-transfer pipeline When `UsePackageLoading()` is selected (the default), the SU facet materialises a `TemporaryFileTransferType` instance under `SoftwareUpdate.Loading.FileTransfer`. The internal `SoftwareUpdateFileTransferManager` implements the OPC 10000-5 §11.4 two-tier protocol: 1. Client calls `Loading.FileTransfer.GenerateFileForWrite(generateOptions)`. The server allocates a fresh handle, creates a transient `FileType` child (named `UploadFile_`) backed by an in-memory buffer, and returns the file NodeId + handle. The `generateOptions` argument is treated as a vendor-defined hint; the default manager interprets a single `String` value as the suggested package id. 2. Client opens the returned FileState in `Write|EraseExisting` mode (mode 6), streams chunks via the standard `Write` method, and closes it. 3. Client calls `Loading.FileTransfer.CloseAndCommit(fileHandle)`. The server reads the buffered payload, packages it into a `SoftwarePackage` (id from the `generateOptions` hint or a timestamped fallback), hands it to `ISoftwarePackageStore.AddAsync(metadata, stream)`, and removes the transient FileState from the address space. Operational caps (defaults, configurable via constants on `SoftwareUpdateFileTransferManager`): | Cap | Value | |---|---| | Concurrent upload handles per FileTransfer | 8 | | Max buffered upload size | 64 MiB | | Supported open mode | `Write \| EraseExisting` (6); other modes are rejected with `BadNotSupported` | | Read on a transient upload file | rejected with `BadNotSupported` | Handles are owned by the session that allocated them; cross-session access is rejected with `BadUserAccessDenied`. > **Note** — `DirectLoadingType` and `CachedLoadingType` inherit > `PackageLoadingType` and therefore expose the same FileTransfer > slot. The wiring is shared because the upload semantics are > identical; the difference between Package / Direct / Cached lives > in the application's `OnInstall` handler (which decides whether to > deploy immediately, atomically swap, or stage as fallback). ## Client side The minimal v1 `SoftwareUpdateClient` reads the device's `SoftwareVersion` property: ```csharp var client = new SoftwareUpdateClient( session, softwareUpdateNodeId, telemetry); string version = await client.ReadSoftwareVersionAsync(); ``` ### Uploading a package `SoftwareUpdateClient.UploadPackageAsync` drives the full GenerateForWrite → Open → Write* → Close → CloseAndCommit flow, streaming the payload in `chunkSizeBytes`-sized chunks (default 8 KiB): ```csharp var client = new SoftwareUpdateClient(session, suNodeId, telemetry); // byte-array convenience overload long bytesUploaded = await client.UploadPackageAsync( payload: firmwareBytes, suggestedPackageId: "acme-firmware-2.0.0", ct: ct); // streaming overload (does not seek the stream) using FileStream fs = File.OpenRead("firmware.bin"); await client.UploadPackageAsync( payload: fs, suggestedPackageId: "acme-firmware-2.0.0", chunkSizeBytes: 16 * 1024, ct: ct); ``` After commit the package is queryable via the server's `ISoftwarePackageStore.GetAsync(suggestedPackageId)`. The recommended deployment flow is: upload via `UploadPackageAsync(...)` → drive the state machines via `PrepareAsync` / `InstallSoftwarePackageAsync` / `ConfirmAsync` as described below. ### Typed Part 16 state-machine surface `SoftwareUpdateClient` exposes typed accessors for the four child state machines (`PrepareForUpdate`, `Installation`, `Confirmation`, `PowerCycle`), each composed over the source-generated `*StateMachineTypeClient` proxies and the generic Part 16 extensions (`GetCurrentFiniteStateAsync`, `ObserveFiniteTransitionsAsync`, `WaitForStateAsync`). The proxies are resolved lazily on first use via `SoftwareUpdateTypeClient.GetXxxAsync(telemetry, ct)` and cached for the lifetime of the client so the browse-path-translate round-trip happens at most once per state machine. ```csharp var client = new SoftwareUpdateClient(session, suNodeId, telemetry); // Snapshot the current state of any of the four FSMs. FiniteStateSnapshot? install = await client.GetInstallationStateAsync(ct); FiniteStateSnapshot? prep = await client.GetPrepareForUpdateStateAsync(ct); // Drive cause methods (resolved against the typed proxy — no manual // TranslateBrowsePath / method-NodeId look-up required). await client.PrepareAsync(ct); await client.InstallSoftwarePackageAsync( manufacturerUri: "urn:acme", softwareRevision: "2.0.0", patchIdentifiers: ArrayOf.Empty(), hash: default, ct: ct); await client.ConfirmAsync(ct); // Stream transitions via a streaming subscription. await foreach (FiniteStateSnapshot snap in client .ObserveInstallationTransitionsAsync(streaming, options: null, ct)) { _logger.LogInformation("Installation moved to {State}", snap.CurrentState); } ``` When the server does not expose the optional child the `Get*StateAsync` overloads return `null`; the cause-method wrappers (`PrepareAsync`, `InstallSoftwarePackageAsync`, `ConfirmAsync`, etc.) throw `ServiceResultException(BadNotFound)` so calling code never needs to redo browse-path resolution. See [`StateMachines.md`](StateMachines.md) for the generic state-machine API the typed wrappers build on. ## Walkthrough — `Applications/PumpDeviceIntegrationServer` The pump server doubles as the software-update walkthrough: a second declarative pump (`Pump #2`) created via `ConfigureDevicesFor` has the SU facet attached on top of the standard DI identification properties. ```csharp services.AddSingleton(); services .AddOpcUa() .AddServer(o => { o.ApplicationName = "PumpDeviceIntegrationServer"; o.EndpointUrls.Add($"opc.tcp://localhost:{port}/PumpDeviceIntegrationServer"); }) .AddNodeManager() .ConfigureDevicesFor(async ctx => { var pump = await ctx.CreateDeviceAsync( new QualifiedName("Pump #2", ctx.Manager.DiNamespaceIndex)); pump.WithIdentification(id => { id.Manufacturer = new LocalizedText("Acme Pumps Inc."); id.Model = new LocalizedText("PumpX-2000 (declarative)"); id.SerialNumber = "SN-DI-2"; id.SoftwareRevision = "2.5.3"; }); var packageStore = ctx.GetRequiredService(); await SoftwarePackageSeeder.SeedAsync(packageStore); pump.WithSoftwareUpdate(packageStore, su => su.UsePackageLoading()); }); await builder.Build().RunAsync(); ``` A client connecting to the running server will discover the SU subtree under `Pump #2.SoftwareUpdate`, can browse the state machines, and invoke `InstallSoftwarePackage`, `PrepareForUpdate.Prepare`, `Confirmation.Confirm`, etc. The library-supplied default handlers record the installed version in the per-device `MemorySoftwareFolder`, so a subsequent read of the device's `SoftwareVersion` reflects the new version. ## Implementation pointers - Fluent surface: `Libraries/Opc.Ua.Di.Server/Builders/ISoftwareUpdateBuilder.cs` and `DeviceBuilderSoftwareUpdateExtensions.cs`. - Address-space wiring: `SoftwareUpdateFacetWiring.cs` (internal). - State-machine dispatcher: `SoftwareUpdateStateMachineDispatcher.cs` (internal — direct `CurrentState` / `LastTransition` writes until the model source generator emits Part 16 tables for `*StateMachineState`). - File-transfer pipeline: `Libraries/Opc.Ua.Di.Server/Builders/SoftwareUpdateFileTransferManager.cs` (internal — `GenerateFileForWrite` / `CloseAndCommit` + per-handle transient `FileType` materialisation). - Storage abstractions: `Libraries/Opc.Ua.Di.Server/SoftwareUpdate/ISoftwarePackageStore.cs` and `ISoftwareFolder.cs`. - Client helpers: `Libraries/Opc.Ua.Di.Client/SoftwareUpdateClient.cs` (read-only discovery), `Libraries/Opc.Ua.Di.Client/SoftwareUpdateClient.StateMachine.cs` (typed Part 16 partial), and `Libraries/Opc.Ua.Di.Client/SoftwareUpdateClient.Upload.cs` (`UploadPackageAsync` driving the FileTransfer pipeline). - Tests: `Tests/Opc.Ua.Di.Tests/DeviceBuilderSoftwareUpdateTests.cs`, `DeviceBuilderSoftwareUpdateStateChangeTests.cs`, `SoftwareUpdateFileTransferTests.cs` (server side), `SoftwareUpdateClientStateMachineTests.cs`, `SoftwareUpdateClientUploadTests.cs`, `SoftwareUpdateClientUploadIntegrationTests.cs` (client + bridge end-to-end against `DiInProcessSessionBridge`), `MemorySoftwareFolderTests.cs`, `PackageStoreTests.cs`, `SoftwareUpdateClientTests.cs`. - Sample: `Applications/PumpDeviceIntegrationServer/Program.cs` (the `Pump #2` declarative-device block attaches the SU facet). ## Spec references - OPC 10000-100 §10.3 — Software Update (companion specification). - OPC 10000-5 §11.4 — `TemporaryFileTransferType` and the `GenerateFileForRead` / `GenerateFileForWrite` / `CloseAndCommit` method triplet.