mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 22:55:49 +08:00
# Description The CTT/compliance `ReferenceNodeManager` built its ~6,000 node address space entirely in imperative C#. This PR migrates it to a source-generated `[NodeManager]` partial driven by an exported NodeSet2 model (`ReferenceServer.NodeSet2.xml`), mirroring the `MinimalBoilerServer` sample and completing the goal of issue #4204. The address space is now materialized from the model by the source generator. The large orchestration body was kept but converted from create-based to **find-instead-of-create**: each `Create*` primitive now looks up the model-materialized node via `FindPredefinedNode<T>` and re-applies runtime values, DataAccess metadata, and write handlers, rather than constructing nodes from scratch. This keeps the deterministic value seeding and behavior wiring intact while the structure comes from the model. ## Notable details for reviewers - **Idempotent reference wiring.** The mesh references in the `References` folder are already present in the model, so the historic manual `AddReference` calls would throw on duplicates. Added a small `AddReferenceIfMissing` helper so those calls are safe. - **Non-string NodeIds.** The `NodeIds` folder assigns numeric / GUID / opaque NodeIds. The model exports those nodes with their final ids, so find-by-string-path no longer works; added `CreateVariableById(NodeId)` to locate them by their real id. - **Generator bug fix (worth a careful look).** `ModelDesignExtensions.GetValueRankAsCode` reconstructed `ValueRank` as `dimensions.Length + 1`, so a node with `ArrayDimensions="3,3"` was emitted as `ValueRank = 3`. This affected **any** NodeSet2 with a fixed multi-dimensional array (rank-3 matrices, `BadTypeMismatch` on whole-matrix writes, wrong decoded dimensions). Corrected to `dimensions.Length` and updated the unit tests that had codified the off-by-one. ## Validation - `ReferenceServerTest`: 44/44 on net10.0 and net48 - `Opc.Ua.SourceGeneration.Core.Tests`: pass on net8.0/net9.0/net10.0/net48 - Integration on net10.0: ComplexTypes, Client, History, Sessions, Subscriptions all pass - Clean build, no new analyzer warnings ## Related Issues - Fixes #4204 ## 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. - [ ] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] 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. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
157 lines
7.0 KiB
C#
157 lines
7.0 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 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 Opc.Ua;
|
|
using Opc.Ua.Server.Fluent;
|
|
|
|
namespace Quickstarts.ReferenceServer
|
|
{
|
|
/// <summary>
|
|
/// Fluent wiring for the CTT reference server methods.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The method nodes and their input/output argument declarations are
|
|
/// baked into the NodeSet2 model via <c><Name>MethodType</c>
|
|
/// declaration nodes that the instance methods point at through
|
|
/// <c>MethodDeclarationId</c>. The source generator therefore emits a
|
|
/// typed <c>OnCall</c> overload for every method, so the call handlers
|
|
/// can be expressed as plain lambdas instead of hand-written
|
|
/// <c>GenericMethodCalledEventHandler</c> callbacks that unpack and
|
|
/// repack <see cref="Opc.Ua.Variant"/> arguments by index.
|
|
/// </remarks>
|
|
public partial class ReferenceNodeManager
|
|
{
|
|
partial void Configure(IReferenceNodeManagerBuilder builder)
|
|
{
|
|
// No inputs, no outputs.
|
|
builder.CTT.Methods.Methods_Void
|
|
.OnCall(() => { });
|
|
|
|
// float + uint -> float.
|
|
builder.CTT.Methods.Methods_Add
|
|
.OnCall((value, count) => value + count);
|
|
|
|
// short * ushort -> int.
|
|
builder.CTT.Methods.Methods_Multiply
|
|
.OnCall((op1, op2) => op1 * op2);
|
|
|
|
// int / ushort -> float.
|
|
builder.CTT.Methods.Methods_Divide
|
|
.OnCall((op1, op2) => op1 / (float)op2);
|
|
|
|
// short - byte -> short.
|
|
builder.CTT.Methods.Methods_Substract
|
|
.OnCall((op1, op2) => (short)(op1 - op2));
|
|
|
|
// string -> string.
|
|
builder.CTT.Methods.Methods_Hello
|
|
.OnCall(value => "hello " + value);
|
|
|
|
// string -> void.
|
|
builder.CTT.Methods.Methods_Input
|
|
.OnCall(_ => { });
|
|
|
|
// void -> string.
|
|
builder.CTT.Methods.Methods_Output
|
|
.OnCall(() => "Output");
|
|
|
|
// Value-write handler for the simulation Enabled control variable.
|
|
// The node and its value are baked into the NodeSet2 model; only the
|
|
// write behaviour is wired here (Prio 2).
|
|
builder.CTT.Scalar.Scalar_Simulation.Scalar_Simulation_Enabled
|
|
.OnWrite((ISystemContext context, NodeState node, ref Variant value) =>
|
|
{
|
|
try
|
|
{
|
|
m_simulationEnabled = (bool)value;
|
|
return ServiceResult.Good;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.ErrorWritingEnabledVariable(e);
|
|
return ServiceResult.Create(e, StatusCodes.Bad, "Error writing Enabled variable.");
|
|
}
|
|
});
|
|
builder.CTT.Scalar.Scalar_Simulation.Scalar_Simulation_Interval
|
|
.OnWrite(OnWriteInterval);
|
|
|
|
// The two event-trigger variables raise a simple event when written.
|
|
builder.CTT.NodeIds.NodeIds_Events.NodeIds_Events_TriggerNode01
|
|
.OnWrite((ISystemContext context, NodeState node, ref Variant value)
|
|
=> RaiseTriggerEvent(context, node));
|
|
builder.CTT.NodeIds.NodeIds_Events.NodeIds_Events_TriggerNode02
|
|
.OnWrite((ISystemContext context, NodeState node, ref Variant value)
|
|
=> RaiseTriggerEvent(context, node));
|
|
|
|
ServiceResult RaiseTriggerEvent(ISystemContext context, NodeState node)
|
|
{
|
|
var e = new BaseEventState(null);
|
|
e.Initialize(
|
|
context,
|
|
node,
|
|
EventSeverity.Medium,
|
|
new LocalizedText($"Trigger event from '{node.DisplayName.Text}'"));
|
|
Server.ReportEvent(context, e);
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
// AccessLevelEx advertises non-atomic read/write on the read/write
|
|
// static scalar. It cannot be expressed in the NodeSet2 model (the
|
|
// UANodeSet schema has no AccessLevelEx attribute), so it is applied
|
|
// here through the fluent .Node escape hatch (Prio 2). The divergent
|
|
// UserAccessLevel values, in contrast, are now baked directly into
|
|
// the NodeSet2 model (Prio 1) and loaded by the source generator.
|
|
((BaseVariableState)builder.CTT.Scalar.Scalar_Static
|
|
.Scalar_Static_NonatomicReadWrite.Node).AccessLevelEx =
|
|
AccessLevels.CurrentReadOrWrite
|
|
| (uint)AccessLevelExType.NonatomicRead
|
|
| (uint)AccessLevelExType.NonatomicWrite;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the periodic value simulation on the fluent builder. The
|
|
/// dynamic nodes are collected imperatively in
|
|
/// <see cref="RegisterSimulationVariables"/> (they are baked into the
|
|
/// NodeSet2 model, but the fluent surface has no per-variable
|
|
/// random-value model), while the periodic loop that pushes fresh
|
|
/// random values to them is expressed here via
|
|
/// <c>Simulation().OnTick(...)</c>. The loop fires at a short fixed
|
|
/// resolution and the tick handler applies the writable
|
|
/// <c>Scalar_Simulation_Interval</c> value.
|
|
/// </summary>
|
|
partial void Configure(INodeManagerBuilder builder)
|
|
{
|
|
builder
|
|
.Simulation(s_simulationTickInterval)
|
|
.OnTick((_, elapsed, cancellationToken) => RunSimulationStepAsync(elapsed, cancellationToken));
|
|
}
|
|
}
|
|
}
|