Commit Graph

3 Commits

Author SHA1 Message Date
Klaus Landsdorf dbee7b9988 Robotics client: fill the engineering units and ranges of the axis values and the load mass, and the gear ratio (#4661)
# Description

Follow-up to #4619. It was prepared stacked on #4619 and is now one
commit on top of master, with #4619's review changes in place.

`AxisSnapshot.Engineering`, `LoadSnapshot.MassEngineering` and
`GearSnapshot.GearRatio` are part of the snapshot contracts and
documented in `docs/Robotics.md` (the URML capability manifest takes the
joint limits from them), but `RoboticsClient` never set them: every axis
came back without units or limits, every load without the unit of its
mass and every gear without its ratio.

OPC 40010-1 declares `ActualPosition` (mandatory), `ActualSpeed` and
`ActualAcceleration` (optional) below the axis's DI `ParameterSet`, and
the load's `Mass`, as `AnalogUnitType` variables with a mandatory
`EngineeringUnits` and an optional `EURange` property, and `GearRatio`
as a mandatory `RationalNumber` variable of the gear. The client now
reads them:

- **Axis:** `ReadAxisAsync`, and with it `ReadSystemAsync`, resolves the
`EngineeringUnits` and `EURange` of the three axis values with one
TranslateBrowsePathsToNodeIds and reads them with one Read per axis
(none when nothing resolves) into `PositionUnit`, `SpeedUnit`,
`AccelerationUnit` and `Limits.Position/Speed/Acceleration`.
- **Load:** `Mass` is resolved and read together with its
`EngineeringUnits` and `EURange` (`MassEngineering`), replacing the
separate resolve and read.
- **Gear:** `GearRatio` is read in the same request as `Pitch`.
- Loads and gears cost no extra request.
- A property the server does not publish, whose read fails or whose
value has a bad status stays `null`. The structures go through the
existing `ToStructure<T>`, which checks `StatusCode.IsGood` as for
`MotorTemperatureEngineering`. The rest of the snapshot is still
returned.
- Because `Mass` and `Pitch` are now part of that request, a failed
`Mass` or `Pitch` read keeps the server's status in the `DataValue`,
like the axis values and `MotorTemperature`. Before,
`Session.ReadValueAsync` threw and failed the whole snapshot read.

The public API does not change.

## Changes

- `src/Opc.Ua.Robotics.Client/RoboticsClient.Topology.cs`:
  - new `ReadAxisEngineeringAsync`, called from `ReadAxisAsync`;
- `ReadLoadAsync` and `ReadGearAsync` use the existing
`ReadChildrenAsync`.
- `docs/Robotics.md`:
  - describes the members and their null cases;
  - adds them to the values that carry no NodeId;
- names `AxisSnapshot.Engineering` instead of the non-existent
`AxisSnapshot.Limits` in the URML section.

## Tests

- **New, unit** (`Opc.Ua.Robotics.Tests`, `RoboticsClientFacadeTests`):
- `EngineeringValuesAndTheGearRatioComeFromTheVariablesTheyDescribe`:
the values are checked against same-named decoys below the axis and a
`GearRatio` in namespace 0.
- `AbsentOrUnreadableEngineeringValuesAndGearRatiosStayNull`: a missing
`EURange`, a load without engineering properties, a failing unit read
and a failing ratio read.
- `BadQualityEngineeringValuesAndGearRatiosReadAsNoStructure`: a range,
a unit and a ratio with a bad status read as `null`.
  - `AFailedMassOrPitchReadKeepsTheStatusAndTheSnapshot`.
  - `AnAxisWithoutAParameterSetHasNoEngineeringValues`.
- **New, end-to-end**
(`RoboticsClientServerE2eTests.EngineeringValuesAndGearRatiosAreReadAsync`):
- the test cell now publishes a unit and range for `ActualSpeed`, a unit
without a range for `ActualAcceleration`, a range for the flange load's
mass and a tool load with a mass only;
- the test reads the units, ranges and gear ratios back through
`ReadSystemAsync` and `ReadAxisAsync`.
- **Fail without the fix:** with `src/Opc.Ua.Robotics.Client` from
master, five of the six new tests fail. Four get `null` units, ranges or
ratios. `AFailedMassOrPitchReadKeepsTheStatusAndTheSnapshot` fails with
`ServiceResultException` `Bad_NodeIdUnknown`.
`AnAxisWithoutAParameterSetHasNoEngineeringValues` passes there by
design: it pins that an axis without a `ParameterSet` still reads
without error.
- `Opc.Ua.Robotics.Tests` passed 661/661 on net10.0 and 601/601 on
net8.0 (macOS).
- `Opc.Ua.Robotics.Client` builds for net48, net8.0, net9.0 and net10.0
with 0 warnings.
- `dotnet format` (whitespace, style, analyzers) on the changed files:
nothing on the changed lines beyond the `IDE0002` info that the existing
`RoboticsSessionHarness.Ua(Opc.Ua.BrowseNames.…)` calls in the same test
class also get.
- Changed lines covered: 52/52 (100 %).
- Fork CI (full matrix incl. macOS) and CodeQL: [CI run
37147764118](https://github.com/biancode/UA-.NETStandard/actions/runs/37147764118)
and [CodeQL run
37147766527](https://github.com/biancode/UA-.NETStandard/actions/runs/37147766527).
CodeQL is green; CI is green except `Opc.Ua.Interop.Tests`
(`LegacyClientExtendedCheckAsync("ComplexTypes")` /
`LoadAndDecodeLegacyComplexTypesAsync`), which is flaky on master since
#4630 (details in #4617), and except one Windows run of
`StartIsIdempotentAndDisposeStopsListenerAsync` in
`Opc.Ua.Redundancy.Kubernetes.Tests` (HttpListener prefix already
registered), which this change does not touch either.

## Notes for review

- `ReadAxisAsync` costs one TranslateBrowsePathsToNodeIds and at most
one Read more per axis. The other snapshots keep their request count.
- `AxisSnapshot.Engineering`, `LoadSnapshot.MassEngineering` and
`GearSnapshot.GearRatio` are read once and carry no NodeId, like
`MotorTemperatureEngineering`.

## Related Issues

- Fixes #4660
- Follows up #4619

## Checklist

- [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.
- [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. (Affected
suite on net10.0 and net8.0 locally on macOS; .NET Framework is covered
by the fork CI's Windows net48 legs.)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-05 06:27:12 +02:00
Klaus Landsdorf c801ac1d64 Robotics client: expose value NodeIds, operation states and the missing identification and motor fields (#4619)
# Description

A Robotics snapshot returns the values it read, but not the variables
they came from. A client that shows a snapshot and wants live updates
has to resolve every value again before it can subscribe. This PR adds
that information to the snapshots, along with the operation state and
the DI and motor fields the contracts did not cover. Every addition is
an init property with a default; no existing member changes.

- **Value NodeIds:** every snapshot value comes with the NodeId of its
variable, or `NodeId.Null` when the server does not publish it:
-
`AxisStateSnapshot.ActualPositionId`/`ActualSpeedId`/`ActualAccelerationId`
- `MotionDeviceSnapshot.SpeedOverrideId`, `LoadSnapshot.MassId`,
`GearSnapshot.PitchId`, `TaskModuleSnapshot.IsReferencedId`
-
`SafetyStateSnapshot.EmergencyStopId`/`OperationalModeId`/`ProtectiveStopId`,
`SafetyFunctionSnapshot.ActiveId`/`EnabledId`
-
`TaskControlSnapshot.ExecutionModeId`/`TaskProgramLoadedId`/`TaskProgramNameId`

Each id comes from the path resolution the value read already does, so
it costs no extra request.
- **Operation state:** `ControllerSnapshot.SystemOperationId`.
`ControllerSnapshot` and `TaskControlSnapshot` add `CurrentStateId`, the
`CurrentState` variable of the SystemOperationStateMachine or
TaskControlStateMachine, and `CurrentState`, the
`RoboticsOperationState` it named when read. `CurrentState` is `null`
when the state machine is absent, its read fails, or it names a state
other than Idle, Ready or Executing; the rest of the snapshot is still
returned.
- **Identification:**
`RoboticsComponentIdentification.HardwareRevision`, `SoftwareRevision`,
`ManufacturerUri`, `ProductInstanceUri` (DI properties).
- **Motor:** `MotorSnapshot.MotorTemperature` with
`MotorTemperatureEngineering` (EngineeringUnits, EURange),
`BrakeReleased` and `EffectiveLoadRate`, each with its NodeId. They are
read from the motor's DI `ParameterSet` with one
TranslateBrowsePathsToNodeIds and one Read per motor.
- The reads use the path helpers from #4609 and the generated proxies;
the per-node structure is unchanged. `docs/Robotics.md` describes the
new members with an example.

Tests:

- **`RoboticsClientServerE2eTests`** (hosted server over opc.tcp). The
fixture now publishes the identification revisions and URIs, the motor's
unit, range and load rate, the gear pitch, and a SystemOperation that
starts in Ready. The new tests:
- compare the ids with the server's own nodes, and check that reading
each id returns the snapshot value;
  - subscribe to an `ActualPositionId` and receive changing values;
- follow the task control's `CurrentState` through LoadByName and
UnloadProgram;
  - read the identification and motor fields.
- **`RoboticsClientFacadeTests`:** check the ids against the
ParameterSet children next to same-named decoys, read `CurrentState`
alongside its `Id` child, and cover an unknown state, a failing
CurrentState read, and a controller and task control without operations.
- On the base branch, the new tests do not compile because the members
are missing.

Local results:
- `UA.slnx` Release builds on all target frameworks with no new
warnings.
- `Opc.Ua.Robotics.Tests`: 650/650 on net10.0 and 590/590 on net8.0.
- Changed-line coverage against #4609: 100 % (168/168,
`check-coverage.ps1`).
- Fork CI (full matrix incl. macOS) and CodeQL on the current head
(`3dbb91392`, with master merged in): green — [CI run
36976259619](https://github.com/biancode/UA-.NETStandard/actions/runs/36976259619)
(189 jobs), [CodeQL run
36976262134](https://github.com/biancode/UA-.NETStandard/actions/runs/36976262134).

## Related Issues

- Fixes #4618

## Checklist

- [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.
- [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. (Affected
suites on net10.0 and net8.0 locally on macOS; .NET Framework via the
fork CI's Windows legs.)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-03 18:14:23 +02:00
Klaus Landsdorf f20d94fcac Robotics: resolve client reads with namespaces and ParameterSet; call ResetToProgramStart on the ReadySubstateMachine (#4609)
# Description

`Opc.Ua.Robotics.Client` read almost nothing from a real OPC 40010-1
server. Every member was looked up with a namespace-0 browse name, and
process values were looked for directly below the component instead of
below the DI `ParameterSet`. #4569 made motors, gears and drives
resolve; the values, identification and enumerations of every snapshot
still came back empty, and the Observe methods tried to monitor
`NodeId.Null`. Separately, `ResetToProgramStartAsync` called the method
on the `Ready` state, which has no such method.

This is the minimal fix on the current structure; the generated proxies,
the per-node reads, the motor and gear lookup by type and the IsDrivenBy
drive lookup with its de-duplication stay as they are. The public API is
unchanged.

- **Reads:** the resolve helpers take a `QualifiedName[]` path built
from the session's namespace table — DI browse names for identification
properties, Robotics browse names for members of the component, and
`DI:ParameterSet/<Robotics name>` for process values
(ActualPosition/Speed/Acceleration, SpeedOverride, EmergencyStop,
OperationalMode, ProtectiveStop, ExecutionMode, TaskProgramLoaded,
TaskProgramName). A namespace the server does not publish makes the path
not resolve instead of matching namespace 0.
- **Observe methods:** `ObserveAxisAsync` and `ObserveSafetyAsync`
subscribe only to values that resolve, and end when none do.
- **ResetToProgramStart:** called on the TaskControlStateMachine's
`ReadySubstateMachine`; if it is missing, the call fails with
Bad_NotFound.
- `docs/Robotics.md` describes how the snapshot reads map to the OPC
40010-1 layout.

Tests:

- `RoboticsClientServerE2eTests` (new, all target frameworks): a
six-axis cell built with the Robotics builders and hosted over opc.tcp.
It checks the `ReadSystemAsync` values, that `ObserveAxisAsync` delivers
changing positions, and that the server's `ResetToProgramStart` handler
runs. All three fail on master.
- `RoboticsClientFacadeTests`: the scripted session now matches browse
paths by the full QualifiedName, lays nodes out as the NodeSet does, and
adds namespace-0 and wrong-level decoys; two new tests check the object
ResetToProgramStart is called on and the Bad_NotFound case.

Local results on the branch: `UA.slnx` Release builds on all target
frameworks with no new warnings; `Opc.Ua.Robotics.Tests` 641/641 on
net10.0 and 581/581 on net8.0; changed-line coverage 97.9 %
(`check-coverage.ps1`). Fork CI (full matrix incl. macOS) and CodeQL:
green — [CI run
36755707494](https://github.com/biancode/UA-.NETStandard/actions/runs/36755707494)
(189 jobs incl. macOS), [CodeQL run
36755711056](https://github.com/biancode/UA-.NETStandard/actions/runs/36755711056).

Possible follow-ups, not part of this PR: reading the topology level by
level in batches (one TranslateBrowsePathsToNodeIds, Browse and Read per
level) and filling the axis engineering units and limits, the load mass
unit and range, and `GearRatio`, which the snapshot contracts define but
the client does not read yet.

## Related Issues

- Fixes #4608

## Checklist

- [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.
- [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. (Affected
suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered
by the fork CI's Windows net48 legs.)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-10-01 13:30:06 +00:00