/* ======================================================================== * 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 System.Threading; using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Server.Fluent; namespace Quickstarts.ReferenceServer { /// /// Fluent wiring for the CTT reference server methods. /// /// /// The method nodes and their input/output argument declarations are /// baked into the NodeSet2 model via <Name>MethodType /// declaration nodes that the instance methods point at through /// MethodDeclarationId. The source generator therefore emits a /// typed OnCall overload for every method, so the call handlers /// can be expressed as plain lambdas instead of hand-written /// GenericMethodCalledEventHandler callbacks that unpack and /// repack Variant arguments by index. /// 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((context, node, ref 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(RaiseTriggerEventAsync); builder.CTT.NodeIds.NodeIds_Events.NodeIds_Events_TriggerNode02 .OnWrite(RaiseTriggerEventAsync); async ValueTask RaiseTriggerEventAsync( ISystemContext context, NodeState node, Variant value, CancellationToken cancellationToken) { _ = value; var e = new BaseEventState(null); e.Initialize( context, node, EventSeverity.Medium, new LocalizedText($"Trigger event from '{node.DisplayName.Text}'")); BaseObjectState notifier = m_historicalEventNotifier ?? throw new ServiceResultException( StatusCodes.BadConfigurationError, "The historical CTT event notifier is not configured."); await notifier.ReportEventAsync( context, e, cancellationToken).ConfigureAwait(false); return new AttributeWriteResult(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. // The bits are added to the AccessLevel loaded from the model (and // the StatusWrite / TimestampWrite bits added after loading), so the // low byte stays consistent with AccessLevel and UserAccessLevel. builder.CTT.Scalar.Scalar_Static .Scalar_Static_NonatomicReadWrite.Node.AccessLevelEx |= AccessLevels.CurrentReadOrWrite | (uint)AccessLevelExType.NonatomicRead | (uint)AccessLevelExType.NonatomicWrite; // The CTT (Address Space Atomicity 001.js) looks for a non-atomic variable in the // first 10000 variables of its cache, sorted by NodeId string. The 5000 mass // variables sort before Scalar_Static_*, so a second non-atomic variable lives in // the AccessRights folder, which sorts first. builder.CTT.AccessRights.AccessRights_AccessAll .AccessRights_AccessAll_NonatomicReadWrite.Node.AccessLevelEx |= AccessLevels.CurrentReadOrWrite | (uint)AccessLevelExType.NonatomicRead | (uint)AccessLevelExType.NonatomicWrite; } /// /// Registers the periodic value simulation on the fluent builder. The /// dynamic nodes are collected imperatively in /// (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 /// Simulation().OnTick(...). The loop fires at a short fixed /// resolution and the tick handler applies the writable /// Scalar_Simulation_Interval value. /// partial void Configure(INodeManagerBuilder builder) { builder .Simulation(s_simulationTickInterval) .OnTick((_, elapsed, cancellationToken) => RunSimulationStepAsync(elapsed, cancellationToken)); } } }