mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
ae241b6625
**Stack 5/5** — base #4126. Part of the #4096 split; see #4096 for the overall picture. Closes the API work for #3827. > Review after #4123, #4124, #4125 and #4126. This branch is content-identical to the original #4096 branch, so merging the stack lands exactly what #4096 contained. ## The problem this solves OPC 40010 1.02 defines a rich robot **topology** but **no motion verbs at all** — its actuation surface is the SystemOperation and TaskControl state machines plus a Controller `Programs` directory. Meanwhile [URML](https://github.com/URML-MARS/URML) names OPC UA Robotics as its canonical non-ROS substrate and asks for `MoveTo` / `Grasp` / `Release` methods the companion spec does not define, so today it needs a hand-maintained per-deployment NodeId mapping file. This closes that gap in two clearly separated tiers, and never presents the second as standard. ## Contents **Server** — stock `RoboticsNodeManager`, ordered `IRoboticsModelProvider` composition, `IRoboticsConfigurator` code-behind, and validated fluent topology builders for systems, controllers, motion devices, axes, power trains, motors, gears, drives, safety states, software, task controls and task modules, with the correct OPC 40010 semantic references. **Standard operations** — `AddSystemOperation` and `AddTaskControlOperation` drive the Part 16 state machines. An illegal cause is rejected with `BadInvalidState` before the handler runs; a handler returning a bad `ServiceResult` does not move the machine; and a transition already in flight rejects a concurrent one rather than letting both commit. **Programs** — binds the optional `FileDirectoryType` through the reusable `IFileDirectoryBinder` added in 1/5. **Operation conventions** — opt-in and explicitly non-normative `MoveTo` / `MoveJ` / `MoveL` / `Grasp` / `Release` / `PickFrom` / `PlaceAt` / `SwapTool` / `SetOutput` / `CallProgram`, created **only** in an application-owned namespace — the OPC UA, DI, IA and Robotics namespaces are rejected — plus a generic `AddOperation<TRequest, TResponse>` extension point. **Client** — `RoboticsClient` extends the DI client, is subtype aware throughout, and provides typed topology snapshots, the standard operation and task-control clients, `ProgramsAsync`, streaming observation and a DI factory. Verbs resolve by BrowseName, so no NodeId mapping file is needed. **Sample** — `MinimalRobotServer` composes five models (Robotics, IA, DI, OpenUSD, RSL/GPOS) purely through `IRoboticsModelProvider` contributions, which is the real proof that the stock hosting is sufficient. It follows the published KUKA KR 16-2 kinematics and axis limits with an eased pick-and-place cycle. Also restores the vendor-subtype scanner test held back from 2/5, which uses the Robotics NodeSet as its fixture. ## Defects found and fixed along the way - **`ControllerType.CurrentUser`** is Mandatory in OPC 40010 but was neither built nor validated. Now has a builder and a validation error. - **`MotionDeviceType.TaskControlReference`** was absent because it must target a `TaskControlOperationType` instance that did not exist. Now wired. ## Validation `dotnet build UA.slnx` — 0 errors, 0 warnings. | Suite | Result | | --- | --- | | Opc.Ua.Robotics.Tests | 103 / 0 | | Opc.Ua.OpenUsd.Tests | 635 / 0 | | Opc.Ua.Di.Tests | 342 / 0 | | Opc.Ua.SourceGeneration.Tests | 94 / 0 | | Opc.Ua.Aot.Tests (NativeAOT) | 120 / 0 | --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
195 lines
8.7 KiB
C#
195 lines
8.7 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Robotics;
|
|
|
|
namespace Opc.Ua.Robotics.Client.Tests
|
|
{
|
|
/// <summary>
|
|
/// Verifies the public method-state APIs generated from the Robotics NodeSet.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Robotics")]
|
|
public sealed class GeneratedMethodSignatureTests
|
|
{
|
|
[Test]
|
|
public void LoadByNameCarriesNameAndStatus()
|
|
{
|
|
MethodInfo syncInvoke = GetInvoke("LoadByNameMethodStateMethodCallHandler");
|
|
MethodInfo asyncInvoke = GetInvoke("LoadByNameMethodStateMethodAsyncCallHandler");
|
|
Type resultType = GetRoboticsType("LoadByNameMethodStateResult");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(syncInvoke.GetParameters(), Has.Length.EqualTo(5));
|
|
Assert.That(syncInvoke.GetParameters()[3].ParameterType, Is.EqualTo(typeof(string)));
|
|
Assert.That(syncInvoke.GetParameters()[3].Name, Is.EqualTo("name"));
|
|
Assert.That(
|
|
syncInvoke.GetParameters()[4].ParameterType,
|
|
Is.EqualTo(typeof(int).MakeByRefType()));
|
|
Assert.That(asyncInvoke.GetParameters(), Has.Length.EqualTo(5));
|
|
Assert.That(asyncInvoke.GetParameters()[3].ParameterType, Is.EqualTo(typeof(string)));
|
|
Assert.That(
|
|
asyncInvoke.GetParameters()[4].ParameterType,
|
|
Is.EqualTo(typeof(CancellationToken)));
|
|
AssertAsyncResult(asyncInvoke.ReturnType, resultType);
|
|
Assert.That(resultType.GetProperty("ServiceResult"), Is.Not.Null);
|
|
Assert.That(resultType.GetProperty("Status")?.PropertyType, Is.EqualTo(typeof(int)));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void StopCarriesModeAndStatus()
|
|
{
|
|
MethodInfo syncInvoke = GetInvoke("StopMethodStateMethodCallHandler");
|
|
MethodInfo asyncInvoke = GetInvoke("StopMethodStateMethodAsyncCallHandler");
|
|
Type resultType = GetRoboticsType("StopMethodStateResult");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(syncInvoke.GetParameters(), Has.Length.EqualTo(5));
|
|
Assert.That(syncInvoke.GetParameters()[3].ParameterType, Is.EqualTo(typeof(long)));
|
|
Assert.That(syncInvoke.GetParameters()[3].Name, Is.EqualTo("stopMode"));
|
|
Assert.That(
|
|
syncInvoke.GetParameters()[4].ParameterType,
|
|
Is.EqualTo(typeof(int).MakeByRefType()));
|
|
Assert.That(asyncInvoke.GetParameters(), Has.Length.EqualTo(5));
|
|
Assert.That(asyncInvoke.GetParameters()[3].ParameterType, Is.EqualTo(typeof(long)));
|
|
Assert.That(
|
|
asyncInvoke.GetParameters()[4].ParameterType,
|
|
Is.EqualTo(typeof(CancellationToken)));
|
|
AssertAsyncResult(asyncInvoke.ReturnType, resultType);
|
|
Assert.That(resultType.GetProperty("Status")?.PropertyType, Is.EqualTo(typeof(int)));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void GetReadyCarriesStatus()
|
|
{
|
|
MethodInfo syncInvoke = GetInvoke("GetReadyMethodStateMethodCallHandler");
|
|
MethodInfo asyncInvoke = GetInvoke("GetReadyMethodStateMethodAsyncCallHandler");
|
|
Type resultType = GetRoboticsType("GetReadyMethodStateResult");
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(syncInvoke.GetParameters(), Has.Length.EqualTo(4));
|
|
Assert.That(
|
|
syncInvoke.GetParameters()[3].ParameterType,
|
|
Is.EqualTo(typeof(int).MakeByRefType()));
|
|
Assert.That(asyncInvoke.GetParameters(), Has.Length.EqualTo(4));
|
|
Assert.That(
|
|
asyncInvoke.GetParameters()[3].ParameterType,
|
|
Is.EqualTo(typeof(CancellationToken)));
|
|
AssertAsyncResult(asyncInvoke.ReturnType, resultType);
|
|
Assert.That(resultType.GetProperty("Status")?.PropertyType, Is.EqualTo(typeof(int)));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public void DiGetUpdateBehaviorOverloadsRemainDistinct()
|
|
{
|
|
MethodInfo cachedInvoke = GetDiInvoke(
|
|
"GetUpdateBehaviorCachedLoadingMethodStateMethodCallHandler");
|
|
MethodInfo fileSystemInvoke = GetDiInvoke(
|
|
"GetUpdateBehaviorFileSystemMethodStateMethodCallHandler");
|
|
Type cachedState = GetDiType("CachedLoadingState");
|
|
Type fileSystemState = GetDiType("FileSystemLoadingState");
|
|
|
|
ParameterInfo[] cachedParameters = cachedInvoke.GetParameters();
|
|
ParameterInfo[] fileSystemParameters = fileSystemInvoke.GetParameters();
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(cachedParameters, Has.Length.EqualTo(7));
|
|
Assert.That(cachedParameters[3].ParameterType, Is.EqualTo(typeof(string)));
|
|
Assert.That(cachedParameters[4].ParameterType, Is.EqualTo(typeof(string)));
|
|
Assert.That(
|
|
cachedParameters[5].ParameterType.GetGenericArguments()[0],
|
|
Is.EqualTo(typeof(string)));
|
|
Assert.That(
|
|
cachedParameters[6].ParameterType,
|
|
Is.EqualTo(typeof(uint).MakeByRefType()));
|
|
Assert.That(fileSystemParameters, Has.Length.EqualTo(5));
|
|
Assert.That(
|
|
fileSystemParameters[3].ParameterType.GetGenericArguments()[0].FullName,
|
|
Is.EqualTo("Opc.Ua.NodeId"));
|
|
Assert.That(
|
|
fileSystemParameters[4].ParameterType,
|
|
Is.EqualTo(typeof(uint).MakeByRefType()));
|
|
Assert.That(
|
|
cachedState.GetProperty("GetUpdateBehavior")?.PropertyType.Name,
|
|
Is.EqualTo("GetUpdateBehaviorCachedLoadingMethodState"));
|
|
Assert.That(
|
|
fileSystemState.GetProperty("GetUpdateBehavior")?.PropertyType.Name,
|
|
Is.EqualTo("GetUpdateBehaviorFileSystemMethodState"));
|
|
});
|
|
}
|
|
|
|
private static Type GetRoboticsType(string name)
|
|
{
|
|
return typeof(RoboticsModel).Assembly.GetType(
|
|
"Opc.Ua.Robotics." + name,
|
|
throwOnError: true)!;
|
|
}
|
|
|
|
private static Type GetDiType(string name)
|
|
{
|
|
return typeof(Opc.Ua.Di.Namespaces).Assembly.GetType(
|
|
"Opc.Ua.Di." + name,
|
|
throwOnError: true)!;
|
|
}
|
|
|
|
private static MethodInfo GetInvoke(string delegateName)
|
|
{
|
|
return GetRoboticsType(delegateName).GetMethod("Invoke")!;
|
|
}
|
|
|
|
private static MethodInfo GetDiInvoke(string delegateName)
|
|
{
|
|
return GetDiType(delegateName).GetMethod("Invoke")!;
|
|
}
|
|
|
|
private static void AssertAsyncResult(Type returnType, Type resultType)
|
|
{
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(returnType.IsGenericType, Is.True);
|
|
Assert.That(
|
|
returnType.GetGenericTypeDefinition(),
|
|
Is.EqualTo(typeof(ValueTask<>)));
|
|
Assert.That(returnType.GetGenericArguments()[0], Is.EqualTo(resultType));
|
|
});
|
|
}
|
|
}
|
|
}
|