Files
UA-.NETStandard/version.targets
Marc Schier 1bb4bdca1d Add Machine Vision Parts 1 and 2 (#4733)
# Description

Implement the released [OPC
40100-1](https://reference.opcfoundation.org/specs/OPC-40100-1) and [OPC
40100-2](https://reference.opcfoundation.org/specs/OPC-40100-2)
specifications with generated models, provider-backed runtimes, typed
clients, MCP workflows, and DI/Machinery integration.

- **Part 1 stays in `Opc.Ua.Vision`** and its client/server packages
alongside the distinct Perception model. Provide control/acquisition
state machines, jobs and synchronized steps,
recipe/product/configuration/result management, file transfers,
diagnostics, events, and conditions.
- **Part 2 is in `Opc.Ua.MachineVision`, `.Client`, and `.Server`.**
Provide asset categories, identification, image-handling aspects,
health/maintenance data, topology, and transactional updates. The base
Vision assemblies do not reference these optional packages; the Part 2
wire model does not depend on Part 1.
- Preserve the release-pinned model bytes, wire namespaces and NodeIds.
Perception types are in `Opc.Ua.Vision.Perception`; the AMCM generated
namespace remains `Opc.Ua.Vision.MachineVision.AMCM`, and asset
client/server extension namespaces remain `Opc.Ua.Vision.Client` and
`Opc.Ua.Vision.Server`.
- Use `AddOpcUaMachineVision` and `AddOpcUaMachineVisionAMCM` for model
loading and factory registration. Each has node-collection,
encodeable-factory-builder and node-state-factory-builder overloads.
These short entry points delegate to the generated implementations;
existing namespace-derived generated names remain available for
compatibility.
- Support multiple independent named systems/assets under one
namespace-owning manager. Reuse injectable providers, file binding,
bounded memory stores, optional transactional `IFileSystemProvider`
persistence, state machines, telemetry and conformance infrastructure.
- Keep one `docs/Vision.md` guide, titled `Machine Vision (OPC 40100-1
and OPC 40100-2)`, and one `Opc.Ua.Mcp.Vision` package. Include
deterministic server/client samples.
- Expose 17 Machine Vision MCP tools, including capabilities/status,
bounded relationships and observation, complete small-file
uploads/downloads, management, explicit control, conditions, and chunked
transfers. Catalogue counts: 39 Vision-specific, 43 with connection
tools, 81 Vision+Robotics, 155 Full. Every Machine Vision wire parameter
has a schema description.

**Compatibility and limits:** `AddVision()` remains Perception-only
unless released parts are selected. Standalone Part 2 uses
`AddVision().AddMachineVision()`; Machinery composition uses
`AddVisionToMachinery()`. Existing tool defaults and command signatures
are preserved. Observation owns and cleans up its subscription: 5
seconds/50 notifications by default, capped at 60 seconds/100
notifications. Complete-file tools are capped at 1 MiB; larger files use
chunked transfers. Domain Error values remain distinct from UA service
failures.

**Versioning:** the seven Vision/MachineVision/MCP packages and their
Debug variants follow the repository root version instead of being
forced to preview. Vision.OpenUsd and its descendants remain
preview-only. Development builds still inherit the root prerelease
version. No release version, tag, release branch, publication policy, or
feed promotion is changed.

**Review actions:** all fourteen review threads have action/evidence
replies and are resolved. Changes include the unified guide, Perception
namespace, separate Part 2 packages and final naming, multi-system
ownership explanation, interrupted-write cleanup, observer failure
isolation, complete MCP parameter descriptions, and explicit package
inventory. The preview-layout migration topic and the requested
ReleaseProcess caveat were removed. The test Publish collector now
retries recognized transient failures within one cancellation budget and
propagates other exceptions. Commit 69b1f5db9 applies the requested
short loader names and renames standalone hosting to `AddMachineVision`,
with callers, tests and documentation updated. The intervening master
merge is preserved.

### Verification

**Latest API naming changes, verified locally:**

| Scope | Result |
| --- | --- |
| Full Vision suite, net10 | 572 passed, 1 existing skip, 0 failed |
| Full Vision suite, net48 | 515 passed, 0 skipped, 0 failed |
| Final loader/hosting cases after cross-TFM null-guard adjustment | 9
passed, 0 failed, 0 skipped |
| Machine Vision MCP and profile cases | 198 passed, 0 failed, 0 skipped
|
| Main and MCP AOT test-project compilation | Both built with 0
warnings/errors |
| MCP AOT companion, managed execution | 5 passed, 0 failed, 0 skipped;
not claimed as native execution |

`ModelLoaderNamesRegisterTypedFactories`,
`ModelLoaderNamesRejectNullArguments`, and
`GeneratedModelsLoadTogetherAndCreateTypedInstances` cover all six
loader overloads, fluent return identities, typed instances, namespace
coexistence and null guards. Existing standalone Part 2 tests verify
idempotent registration, one-owner enforcement and null handling through
`AddMachineVision`. No source-generator implementation, CLR namespace,
wire identity or model payload was changed for these naming updates.

The current 69b1f5db9 revision is running
[CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826937),
[package
validation](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165823359),
[MCP
usability](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826874),
[CodeQL](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826906),
and [Images
CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826884).
These runs are not claimed as completed results.

**Earlier completed verification:** all seven default-TFM package
archives were inspected after the package rename: exact IDs, 27 clean
DLLs, correct dependencies/readmes/licenses, and unchanged pinned model
hashes. [CI on
c75ec4fa6](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38142519760)
passed, including the formerly failing Linux batch and required summary.
Its unchanged coverage gate passed at **88.56% lines, 78.96% branches,
84.28% patch** against the 75% patch floor. The later
bounded-Publish-helper regressions reproduced 13 failures before the fix
and passed afterward; the full mainline triggering fixture passed 67
tests on each of net10/net48. These are historical results, not current
full-matrix results.

<details>
<summary>Executed API naming validation commands and outcomes</summary>

Run from the worktree root using the established artifact trees.
Dependency builds were performed before the no-build full net10 run; the
focused loader/hosting selection was rebuilt and rerun after the final
null-guard adjustment.

```powershell
$files = 'C:\Users\mschier\.copilot\session-state\deb2ba10-b0e9-4a44-8783-8331bc850814\files'
$env:DOTNET_CLI_USE_MSBUILD_SERVER = '0'
$env:TEMP = 'C:\Temp\mv-deb2\followup'
$env:TMP = $env:TEMP

# Passed: 572; Failed: 0; Skipped: 1.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-build --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net10" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

# Passed: 9; Failed: 0; Skipped: 0, on the final source.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~MachineVisionModelTests|FullyQualifiedName~StandalonePartTwoRegistrationUsesOneOwnerAndOneModelProvider|FullyQualifiedName~StandalonePartTwoRequiresRegistrationAndRejectsCompetingOwnership' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-model" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

# Passed: 198; Failed: 0; Skipped: 0.
dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~VisionMachineToolsTests|FullyQualifiedName~McpHostBuilderTests|FullyQualifiedName~McpToolProfileCompositionTests' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-mcp" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup'

$env:TEMP = 'C:\Temp\mv-deb2\followup48'
$env:TMP = $env:TEMP

# Passed: 515; Failed: 0; Skipped: 0.
dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Debug -f net48 --no-restore --artifacts-path "$files\solution-net48-artifacts" -p:CustomTestTarget=net48 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net48-final" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup48'
```

Opt-in long-running/stress/durable tiers have not been rerun locally;
the unqualified **all tests** checklist item remains open. Original
implementation matrix counts are not presented as current results.

</details>

## Related Issues

- Fixes #4732.

The requester reviewed and approved the implementation plan and
subsequent packaging/API decisions in the development session. The
tracking issue records the design; maintainer architectural
discussion/sign-off there is still pending. This PR remains a draft.

## 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.
- [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.
- [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.
2026-10-12 07:01:27 +00:00

107 lines
8.2 KiB
XML

<Project>
<PropertyGroup>
<!--
A Debug-configuration pack renames every package to "<id>.Debug" (see
the per-project PackageId overrides), so classify on the base ID: the
Debug variant of a preview family must be pinned exactly like its
Release counterpart. Test-PreviewPackageId in
.azurepipelines/package-version-policy.ps1 strips the same suffix, and
validate-nuget-package-set.ps1 fails the build if the two classifiers
ever disagree.
-->
<_PreviewPackageBaseId>$([System.Text.RegularExpressions.Regex]::Replace('$(PackageId)', '\.Debug$', ''))</_PreviewPackageBaseId>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.XRegistry' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.XRegistry.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.WotCon' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.WotCon.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.Vision.OpenUsd' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.Vision.OpenUsd.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.Robotics' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.Robotics.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.Redundancy' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.Redundancy.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.Positioning' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.Positioning.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.OpenUsd' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.OpenUsd.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.AI' OR $([System.String]::Copy('$(_PreviewPackageBaseId)').StartsWith('$(PackagePrefix).Opc.Ua.AI.'))">true</_IsPreviewPackage>
<_IsPreviewPackage Condition="'$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.Mcp.Robotics' OR '$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.OpenUsd.Connector' OR '$(_PreviewPackageBaseId)' == '$(PackagePrefix).Opc.Ua.OpenUsd.Connector.Viewer'">true</_IsPreviewPackage>
<!--
On a canonical release/M.m branch build (NBGV_PublicRelease == 'True'),
common.props/version.props deliberately do NOT reference the
Nerdbank.GitVersioning package (Version/PackageVersion instead come from
NBGV_* environment variables set once per pipeline run by
.azurepipelines/set-version.ps1), so the GetBuildVersion target this
target would otherwise depend on does not exist in that build and a
literal "GetBuildVersion" DependsOnTargets would fail every release
build with MSB4057. Depend on it only in the local/dev path, where the
package IS referenced and Version/PackageVersion are computed
dynamically at build time rather than already resolved at evaluation
time.
-->
<_PreviewVersionDependsOn Condition="'$(NBGV_PublicRelease)' != 'True'">GetBuildVersion</_PreviewVersionDependsOn>
<GetPackageVersionDependsOn>
ApplyPreviewPackageVersion;
$(GetPackageVersionDependsOn)
</GetPackageVersionDependsOn>
<GenerateNuspecDependsOn>
ApplyPreviewPackageVersion;
$(GenerateNuspecDependsOn)
</GenerateNuspecDependsOn>
</PropertyGroup>
<!--
Keeps the package families listed above ("_IsPreviewPackage") on the
preview channel even once the root version.json commit is an exact
stable release such as "2.0.0" on release/2.0. See
docs/ReleaseProcess.md for the release-branch/version model this
implements and preview-version.props for how the numbered suffix below
is chosen.
-->
<Target Name="ApplyPreviewPackageVersion"
DependsOnTargets="$(_PreviewVersionDependsOn)"
Condition="'$(_IsPreviewPackage)' == 'true' AND '$(IsPackable)' == 'true'">
<PropertyGroup>
<_PreviewSourceVersion Condition="'$(PackageVersion)' != ''">$(PackageVersion)</_PreviewSourceVersion>
<_PreviewSourceVersion Condition="'$(_PreviewSourceVersion)' == '' AND '$(NuGetPackageVersion)' != ''">$(NuGetPackageVersion)</_PreviewSourceVersion>
<_PreviewSourceVersion Condition="'$(_PreviewSourceVersion)' == ''">$(Version)</_PreviewSourceVersion>
<!-- Case 1: the root version is already an in-development preview
(e.g. "2.0.0-preview.42" or "2.0.1-preview.7+gabcdef" while
master or a release branch is not yet at a stable commit) - every
package, selected or not, already carries that same tag. Nothing
to do. -->
<_PreviewPackageVersion
Condition="$([System.String]::Copy('$(_PreviewSourceVersion)').Contains('-preview'))">$(_PreviewSourceVersion)</_PreviewPackageVersion>
<!-- Case 2a: the root version's only prerelease identifier is a commit
id (e.g. "2.0.0-gabcdef0123"). Nerdbank.GitVersioning produces
this for a stable version.json built from a non-public-release
ref, such as master briefly carrying "2.0.0". Case 2 would turn it
into "2.0.0-preview.gabcdef0123", and SemVer 2 ranks the
alphanumeric "g..." identifier above every numeric one, so it
would outrank every "-preview.N" ever released from that base -
permanently, because published versions are immutable. Number it
0 instead so it sorts below any committed
PreviewPackageBuildNumber. ConvertTo-PreviewPackageVersion in
.azurepipelines/package-version-policy.ps1 applies the same rule. -->
<_PreviewPackageVersion
Condition="'$(_PreviewPackageVersion)' == '' AND $([System.Text.RegularExpressions.Regex]::IsMatch('$(_PreviewSourceVersion)', '^[^-+]+-g[0-9a-f]+(\+.*)?$'))">$([System.Text.RegularExpressions.Regex]::Replace('$(_PreviewSourceVersion)', '^([^-+]+)-(g[0-9a-f]+)', '$1-preview.0.$2'))</_PreviewPackageVersion>
<!-- Case 2: the root version carries some other prerelease label
(for example a hypothetical "-rc.1"). Preserve that label but
mark the selected families as preview under it too. -->
<_PreviewPackageVersion
Condition="'$(_PreviewPackageVersion)' == '' AND $([System.String]::Copy('$(_PreviewSourceVersion)').Contains('-'))">$([System.String]::Copy('$(_PreviewSourceVersion)').Replace('-', '-preview.'))</_PreviewPackageVersion>
<!-- Case 3: the root version has build metadata but no prerelease
label at all (e.g. "2.0.0+gabcdef"). SemVer requires build
metadata to stay last, so insert the tag before the "+" rather
than appending after it. -->
<_PreviewPackageVersion
Condition="'$(_PreviewPackageVersion)' == '' AND $([System.String]::Copy('$(_PreviewSourceVersion)').Contains('+'))">$([System.String]::Copy('$(_PreviewSourceVersion)').Replace('+', '-preview.$(PreviewPackageBuildNumber)+'))</_PreviewPackageVersion>
<!-- Case 4: the root version is an exact stable release (no
prerelease label or build metadata at all, e.g. "2.0.0" once
release/2.0 has been stabilized) - synthesize the numbered
preview suffix explicitly, since there is no git-height
placeholder left in the string to reuse. See
preview-version.props for how the number is chosen. -->
<_PreviewPackageVersion
Condition="'$(_PreviewPackageVersion)' == ''">$(_PreviewSourceVersion)-preview.$(PreviewPackageBuildNumber)</_PreviewPackageVersion>
<Version>$(_PreviewPackageVersion)</Version>
<PackageVersion>$(_PreviewPackageVersion)</PackageVersion>
<NuGetPackageVersion>$(_PreviewPackageVersion)</NuGetPackageVersion>
<NBGV_NuGetPackageVersion>$(_PreviewPackageVersion)</NBGV_NuGetPackageVersion>
</PropertyGroup>
</Target>
</Project>