ddd7678269
A full RebuildAddressSpace recreated every native Part 9 condition inactive + acknowledged, and when the alarm-tag set was unchanged (e.g. one UnsTagReference added to an alarm-bearing raw tag) nothing re-told the new node its state: the host does not re-subscribe for an unchanged set, and Galaxy / OpcUaClient have no snapshot to replay anyway. Active alarms read normal until their next device transition. OtOpcUaNodeManager now captures each NATIVE condition's live state (from the node, so a client-applied ack is kept) before the rebuild clears, restores it when the same id is re-materialised native in the same apply, and reports ONE condition event per restored off-normal condition at the new IOpcUaAddressSpaceSink.CompleteAddressSpaceApply (after notifier wiring). Unclaimed captures are discarded there, so a later re-add is fresh. Quality is carried silently for normal conditions too (it is only re-annotated on a connectivity edge). Scripted conditions are untouched (#487 re-asserts them). OpcUaPublishActor calls CompleteAddressSpaceApply at the end of every apply. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
305 lines
23 KiB
C#
305 lines
23 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
|
|
|
/// <summary>
|
|
/// Abstraction over the OPC UA SDK's address space. <c>OpcUaPublishActor</c> consumes this
|
|
/// so the Runtime project doesn't reference <c>Opc.Ua.Server</c> directly — production
|
|
/// binds a real SDK-backed sink in the fused Host's wiring, dev/Mac binds the
|
|
/// <see cref="NullOpcUaAddressSpaceSink"/> no-op.
|
|
/// </summary>
|
|
public interface IOpcUaAddressSpaceSink
|
|
{
|
|
/// <summary>Write a Variable node's current value + status code + source timestamp.</summary>
|
|
/// <param name="nodeId">The OPC UA node ID of the variable.</param>
|
|
/// <param name="value">The value to write.</param>
|
|
/// <param name="statusCode">The full OPC UA status code, sub-code included. Passed to the SDK verbatim
|
|
/// (#519): a driver's <see cref="OpcUaStatus.BadNoCommunication"/> reaches the client as
|
|
/// <c>BadNoCommunication</c>, not as a generic <see cref="OpcUaStatus.Bad"/>. Use
|
|
/// <see cref="OpcUaStatus"/>' named constants rather than a literal.</param>
|
|
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
|
/// <param name="realm">Which of the two v3 namespaces (<see cref="AddressSpaceRealm.Raw"/> device tree or
|
|
/// <see cref="AddressSpaceRealm.Uns"/> equipment tree) the <paramref name="nodeId"/> lives in — the sink
|
|
/// resolves the namespace index from the realm rather than parsing it out of the id string. WP3's fan-out
|
|
/// calls this once per NodeId (raw + every referencing UNS node) with the matching realm.</param>
|
|
void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm);
|
|
|
|
/// <summary>Write a batch of Variable values. Semantically identical to calling
|
|
/// <see cref="WriteValue"/> per item; an SDK-backed sink overrides this to take its internal
|
|
/// lock ONCE per batch instead of once per value (the hot-path win). The default implementation
|
|
/// loops <see cref="WriteValue"/> so existing sinks and fakes behave unchanged.
|
|
/// <para>A batching implementation MUST isolate per item — one bad item may not drop the tail of the
|
|
/// batch — and report the aggregate as a <see cref="ValueWriteBatchException"/>; see that type for why.
|
|
/// The default loop above predates the contract and is only used by fakes / the no-op sink.</para></summary>
|
|
/// <param name="values">The value writes to apply, in order.</param>
|
|
/// <exception cref="ValueWriteBatchException">A batching implementation throws this when some — but not
|
|
/// necessarily all — items failed; every other item has been applied.</exception>
|
|
void WriteValues(IReadOnlyList<ValueWrite> values)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(values);
|
|
for (var i = 0; i < values.Count; i++)
|
|
{
|
|
var v = values[i];
|
|
WriteValue(v.NodeId, v.Value, v.StatusCode, v.SourceTimestampUtc, v.Realm);
|
|
}
|
|
}
|
|
|
|
/// <summary>Batched <b>status-only</b> write: each node keeps its CURRENT value and only its
|
|
/// StatusCode + SourceTimestamp change. This is the comms-loss annotation for value variables
|
|
/// (archreview 03/S15) — a driver that has stopped communicating leaves the node holding its last
|
|
/// known value (the Part 8 convention SCADA clients expect) under a
|
|
/// <see cref="OpcUaStatus.BadNoCommunication"/> status, rather than nulling it.
|
|
/// <para>A sink that HOLDS the node's value must retain it — the SDK-backed sink does, because the
|
|
/// node itself is where the value lives. This DEFAULT implementation cannot: the host holds no value
|
|
/// cache (v3's single-source design), so it degrades to <see cref="WriteValue"/> with a null value,
|
|
/// which is the honest statement for a value-less sink and keeps every fake compiling unchanged.</para>
|
|
/// <para>Same per-item isolation contract as <see cref="WriteValues"/>. An unknown or
|
|
/// unmaterialised NodeId is a <b>no-op, never a create</b>: a status carries nothing to create a node
|
|
/// from, and the caller's index can legitimately name a node a surgical remove has just dropped.</para></summary>
|
|
/// <param name="statuses">The status writes to apply, in order.</param>
|
|
/// <exception cref="ValueWriteBatchException">A batching implementation throws this when some — but not
|
|
/// necessarily all — items failed; every other item has been applied.</exception>
|
|
void WriteStatuses(IReadOnlyList<StatusWrite> statuses)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(statuses);
|
|
for (var i = 0; i < statuses.Count; i++)
|
|
{
|
|
var s = statuses[i];
|
|
WriteValue(s.NodeId, null, s.StatusCode, s.SourceTimestampUtc, s.Realm);
|
|
}
|
|
}
|
|
|
|
/// <summary>Write an alarm-condition's full Part 9 state. When a real condition node has been
|
|
/// materialised for <paramref name="alarmNodeId"/> via <see cref="MaterialiseAlarmCondition"/>,
|
|
/// this projects the whole <see cref="AlarmConditionSnapshot"/>
|
|
/// (Enabled/Active/Acked/Confirmed/Shelving/Severity/Message) onto it and recomputes Retain;
|
|
/// otherwise it falls back to the legacy two-element <c>[Active, Acknowledged]</c> placeholder
|
|
/// variable. A materialised condition also fires a Part 9 condition event on each transition (T16)
|
|
/// so subscribed clients receive the alarm event, not just the changed attributes.</summary>
|
|
/// <param name="alarmNodeId">The OPC UA node ID of the alarm (== ScriptedAlarmId for materialised conditions).</param>
|
|
/// <param name="state">The full condition state to project onto the node.</param>
|
|
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
|
/// <param name="realm">The namespace realm <paramref name="alarmNodeId"/> lives in (must match the realm
|
|
/// the condition was materialised under).</param>
|
|
void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm);
|
|
|
|
/// <summary>#477 — annotate a materialised condition's source-data quality OUT OF BAND from any alarm
|
|
/// transition (used by the driver-connectivity path: comms lost → <see cref="OpcUaStatus.BadNoCommunication"/>,
|
|
/// restored → <see cref="OpcUaStatus.Good"/>). Sets ONLY the condition's Quality — never
|
|
/// Active/Acked/Severity/Retain (a comms-lost active alarm stays active) — and fires one Part 9 event
|
|
/// only on a change to the status code itself — since #519 that includes a move between two Bad
|
|
/// sub-codes, which genuinely changes what a subscriber reads. A no-op for an unmaterialised /
|
|
/// non-condition node.</summary>
|
|
/// <param name="alarmNodeId">The condition node id (RawPath for a native alarm).</param>
|
|
/// <param name="statusCode">The source-data status code to annotate, sub-code included.</param>
|
|
/// <param name="sourceTimestampUtc">The connectivity transition timestamp in UTC.</param>
|
|
/// <param name="realm">The namespace realm the condition was materialised under.</param>
|
|
void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm);
|
|
|
|
/// <summary>
|
|
/// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
|
|
/// can browse it as a proper condition (with basic Active/Ack state). The node id equals the
|
|
/// alarm node id (the ScriptedAlarmId) so subsequent <see cref="WriteAlarmCondition"/> calls update
|
|
/// it. Used by <c>AddressSpaceApplier.MaterialiseScriptedAlarms</c>. Idempotent.
|
|
/// </summary>
|
|
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId); becomes the condition's NodeId.</param>
|
|
/// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param>
|
|
/// <param name="displayName">Human-readable condition name (BrowseName / DisplayName / Message).</param>
|
|
/// <param name="alarmType">Domain alarm type — mapped to the SDK condition subtype by the sink.</param>
|
|
/// <param name="severity">Domain severity (OPC UA 1..1000 scale); mapped to the SDK severity buckets.</param>
|
|
/// <param name="isNative">When true this is a driver-fed (native, e.g. Galaxy) equipment-tag alarm; when
|
|
/// false (default) it is a scripted alarm. The node manager tracks native condition node ids so a later
|
|
/// task can route a native condition's inbound Acknowledge to the driver instead of the scripted engine.</param>
|
|
/// <param name="realm">The namespace realm the condition (and its parent folder <paramref name="equipmentNodeId"/>)
|
|
/// live in.</param>
|
|
void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false);
|
|
|
|
/// <summary>
|
|
/// v3 Batch 4 (multi-notifier native alarms) — wire an already-materialised native alarm condition
|
|
/// (<paramref name="alarmNodeId"/>, materialised at the raw tag via <see cref="MaterialiseAlarmCondition"/>
|
|
/// with ConditionId = the RawPath) as an event notifier of EACH referencing equipment's UNS folder, so
|
|
/// the condition's <b>single</b> <c>ReportEvent</c> fans one event to every referencing equipment root
|
|
/// WITHOUT re-reporting per root (distinct EventIds would break Server-object dedup + Part 9 ack
|
|
/// correlation). Per folder the sink wires the SDK's bidirectional notifier pattern
|
|
/// (<c>alarm.AddNotifier(isInverse:true, folder)</c> + <c>folder.AddNotifier(isInverse:false, alarm)</c>)
|
|
/// and promotes the folder to an event notifier. <b>Idempotent</b> (a re-wire of the same pair updates,
|
|
/// never duplicates); a <b>missing endpoint is a no-op</b> (logged, never thrown) so a mid-rebuild race
|
|
/// can't fault a deploy. The sink tracks each wired pair so a later rebuild / condition-removal /
|
|
/// equipment-subtree-removal tears the notifier down bidirectionally (no inverse-notifier entry leaks
|
|
/// across redeploys).
|
|
/// </summary>
|
|
/// <param name="alarmNodeId">The native alarm condition's node id (== the backing tag's RawPath).</param>
|
|
/// <param name="alarmRealm">The namespace realm the condition lives in (Raw for native alarms).</param>
|
|
/// <param name="notifierFolderNodeIds">The equipment folder node ids (their <c>s=</c> ids) to wire as
|
|
/// extra event-notifier roots for this condition. An unknown / not-yet-materialised folder id is skipped.</param>
|
|
/// <param name="notifierFolderRealm">The namespace realm the notifier folders live in (Uns for equipment folders).</param>
|
|
void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm,
|
|
IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm);
|
|
|
|
/// <summary>
|
|
/// Ensure a folder node exists under the given parent. Used by <c>AddressSpaceApplier</c> to
|
|
/// materialise the UNS Area/Line/Equipment hierarchy in the address space. When
|
|
/// <paramref name="parentNodeId"/> is null the folder is parented under the namespace
|
|
/// root. Idempotent: calling twice with the same id is safe.
|
|
/// </summary>
|
|
/// <param name="folderNodeId">The OPC UA node ID for the folder.</param>
|
|
/// <param name="parentNodeId">The parent folder node ID, or null for namespace root.</param>
|
|
/// <param name="displayName">The display name for the folder.</param>
|
|
/// <param name="realm">The namespace realm the folder (and its <paramref name="parentNodeId"/>) live in.</param>
|
|
void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm);
|
|
|
|
/// <summary>
|
|
/// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
|
|
/// <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with
|
|
/// Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both.
|
|
/// Used by <c>AddressSpaceApplier</c> to materialise equipment-namespace tags ahead of any
|
|
/// driver-side subscribe so OPC UA clients can browse them. Idempotent.
|
|
/// </summary>
|
|
/// <param name="variableNodeId">The OPC UA node ID for the variable.</param>
|
|
/// <param name="parentFolderNodeId">The parent folder node ID, or null for namespace root.</param>
|
|
/// <param name="displayName">The display name for the variable.</param>
|
|
/// <param name="dataType">OPC UA built-in type name ("Boolean" / "Int32" / "Float" / etc.).</param>
|
|
/// <param name="writable">When true the node is created <c>CurrentReadWrite</c> (an authored
|
|
/// ReadWrite equipment tag); when false it stays <c>CurrentRead</c> (read-only). Non-equipment-tag
|
|
/// variables (folders' children, alarm placeholders) always pass <c>false</c>.</param>
|
|
/// <param name="historianTagname">null ⇒ the variable is not historized; non-null ⇒ create it
|
|
/// Historizing with the HistoryRead access bit and register the (already default-resolved)
|
|
/// historian tagname.</param>
|
|
/// <param name="isArray">When true the node is created as a 1-D array (<c>ValueRank=OneDimension</c>
|
|
/// with <c>ArrayDimensions=[arrayLength]</c>); when false (default) it stays scalar
|
|
/// (<c>ValueRank=Scalar</c>). Array elements share the scalar's base <paramref name="dataType"/> —
|
|
/// rank + dimensions carry the array-ness.</param>
|
|
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is
|
|
/// true; ignored for scalars. Null ⇒ length 0 (unbounded).</param>
|
|
/// <param name="realm">The namespace realm the variable (and its <paramref name="parentFolderNodeId"/>) live
|
|
/// in. A historized UNS reference node registers the SAME <paramref name="historianTagname"/> as its backing
|
|
/// raw node — both NodeIds map to one tagname — so this call carries the shared tagname per realm.</param>
|
|
/// <param name="refuseArrayOnScalar">Gitea #610 review (R6): when true and the node is SCALAR, a client write
|
|
/// of an array value is refused inline (<c>BadTypeMismatch</c>, nothing dispatched) because the owning
|
|
/// driver's scalar encoder cannot take one (Modbus / S7). False (default) keeps the pre-#610 behaviour — the
|
|
/// write is dispatched and the driver decides (OpcUaClient forwards it to the upstream server, Galaxy encodes
|
|
/// it). Fixed at creation; an existing node keeps the value it was created with.</param>
|
|
void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, bool refuseArrayOnScalar = false);
|
|
|
|
/// <summary>
|
|
/// Tear down + repopulate the address space. Called by <c>OpcUaPublishActor</c> after a
|
|
/// successful deployment apply so the node manager reflects the new config. Idempotent.
|
|
/// </summary>
|
|
void RebuildAddressSpace();
|
|
|
|
/// <summary>
|
|
/// Gitea #641 — mark the END of one address-space apply pass (after <see cref="RebuildAddressSpace"/>, if
|
|
/// one ran, and every materialise / notifier-wiring call that followed it). An SDK-backed sink carries each
|
|
/// native condition's live Part 9 state across a rebuild and restores it when the same condition is
|
|
/// re-materialised; this is where it reports the ONE condition event per restored condition (now that
|
|
/// every notifier folder is wired) and DISCARDS any captured state no materialise claimed, so a later
|
|
/// deploy that re-adds the same id gets a fresh node rather than a resurrection. <c>OpcUaPublishActor</c>
|
|
/// calls it once at the end of every apply, rebuild or not. Idempotent; the default is a no-op.
|
|
/// </summary>
|
|
void CompleteAddressSpaceApply()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Announce that nodes were added at runtime (discovered-node injection) under
|
|
/// <paramref name="affectedNodeId"/> so subscribed clients refresh their browse
|
|
/// (Part 3 GeneralModelChangeEvent, verb NodeAdded).
|
|
/// </summary>
|
|
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
|
/// <param name="realm">The namespace realm <paramref name="affectedNodeId"/> lives in.</param>
|
|
void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm);
|
|
|
|
/// <summary>
|
|
/// Add an OPC UA reference from an already-materialised source node to an already-materialised
|
|
/// target node, both realm-qualified. Used by <c>AddressSpaceApplier</c> to link each UNS reference
|
|
/// Variable (source, <see cref="AddressSpaceRealm.Uns"/>) to its backing Raw node (target,
|
|
/// <see cref="AddressSpaceRealm.Raw"/>) with an <c>Organizes</c> edge, so the UNS variable
|
|
/// browse-resolves to its raw source (and the raw node back via the inverse). The edge is wired
|
|
/// bidirectionally (forward on the source, inverse on the target) so browse resolves both ways.
|
|
/// <b>Idempotent</b> (an existing edge is not duplicated); a <b>missing endpoint is a no-op</b>
|
|
/// (logged, never thrown) so a mid-rebuild race can't fault a deploy.
|
|
/// </summary>
|
|
/// <param name="sourceNodeId">The source node's <c>s=</c> id (the referencing node — e.g. the UNS variable).</param>
|
|
/// <param name="sourceRealm">The namespace realm <paramref name="sourceNodeId"/> lives in.</param>
|
|
/// <param name="targetNodeId">The target node's <c>s=</c> id (the referenced node — e.g. the backing Raw node).</param>
|
|
/// <param name="targetRealm">The namespace realm <paramref name="targetNodeId"/> lives in.</param>
|
|
/// <param name="referenceType">The hierarchical reference type name — <c>Organizes</c> (default),
|
|
/// <c>HasComponent</c>, or <c>HasProperty</c>; unknown names fall back to <c>Organizes</c>.</param>
|
|
void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm,
|
|
string targetNodeId, AddressSpaceRealm targetRealm,
|
|
string referenceType = "Organizes");
|
|
}
|
|
|
|
/// <summary>One value write within a <see cref="IOpcUaAddressSpaceSink.WriteValues"/> batch.
|
|
/// Same semantics per field as <see cref="IOpcUaAddressSpaceSink.WriteValue"/> (#519: status verbatim).</summary>
|
|
public readonly record struct ValueWrite(
|
|
string NodeId, object? Value, uint StatusCode, DateTime SourceTimestampUtc, AddressSpaceRealm Realm);
|
|
|
|
/// <summary>One status-only write within a <see cref="IOpcUaAddressSpaceSink.WriteStatuses"/> batch: the
|
|
/// node's current VALUE is retained and only <paramref name="StatusCode"/> +
|
|
/// <paramref name="SourceTimestampUtc"/> change.</summary>
|
|
/// <param name="NodeId">The OPC UA node id of the variable, within <paramref name="Realm"/>.</param>
|
|
/// <param name="StatusCode">The full status code, sub-code included, passed to the SDK verbatim (#519).</param>
|
|
/// <param name="SourceTimestampUtc">The source timestamp in UTC.</param>
|
|
/// <param name="Realm">Which of the two v3 namespaces <paramref name="NodeId"/> lives in.</param>
|
|
public readonly record struct StatusWrite(
|
|
string NodeId, uint StatusCode, DateTime SourceTimestampUtc, AddressSpaceRealm Realm);
|
|
|
|
/// <summary>Aggregate outcome of a partially-failed <see cref="IOpcUaAddressSpaceSink.WriteValues"/> batch:
|
|
/// every non-failing item HAS been applied; this carries what the caller needs to log and meter honestly.</summary>
|
|
/// <remarks>A batched sink must NOT abandon the tail of a batch when one item throws: the poll engine
|
|
/// updates its last-value cache BEFORE raising, so a dropped item is never re-raised until that tag's value
|
|
/// changes again — the node would serve a stale value indefinitely. Hence the sink isolates per item and
|
|
/// reports the aggregate here rather than letting the first failure escape.</remarks>
|
|
/// <param name="firstFailedNodeId">The NodeId of the first item that failed (empty when that item's own
|
|
/// NodeId was null/empty — the validation failure this type most often reports).</param>
|
|
/// <param name="failedCount">How many items of the batch failed.</param>
|
|
/// <param name="succeededCount">How many items of the batch were applied.</param>
|
|
/// <param name="firstFailure">The exception the first failing item produced; the inner exception.</param>
|
|
public sealed class ValueWriteBatchException(string firstFailedNodeId, int failedCount, int succeededCount, Exception firstFailure)
|
|
: Exception($"WriteValues batch: {failedCount} of {failedCount + succeededCount} items failed (first: '{firstFailedNodeId}')", firstFailure)
|
|
{
|
|
/// <summary>Gets the NodeId of the first item that failed.</summary>
|
|
public string FirstFailedNodeId { get; } = firstFailedNodeId;
|
|
|
|
/// <summary>Gets the number of items of the batch that failed.</summary>
|
|
public int FailedCount { get; } = failedCount;
|
|
|
|
/// <summary>Gets the number of items of the batch that were successfully applied.</summary>
|
|
public int SucceededCount { get; } = succeededCount;
|
|
}
|
|
|
|
/// <summary>No-op sink. Bound by default so the actors are safe to run in dev / Mac /
|
|
/// integration tests without a real SDK behind them.</summary>
|
|
public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink
|
|
{
|
|
public static readonly NullOpcUaAddressSpaceSink Instance = new();
|
|
private NullOpcUaAddressSpaceSink() { }
|
|
|
|
/// <inheritdoc />
|
|
public void WriteValue(string nodeId, object? value, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
|
|
|
/// <inheritdoc />
|
|
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
|
public void WriteAlarmQuality(string alarmNodeId, uint statusCode, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }
|
|
|
|
/// <inheritdoc />
|
|
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false) { }
|
|
|
|
/// <inheritdoc />
|
|
public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) { }
|
|
|
|
/// <inheritdoc />
|
|
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm) { }
|
|
|
|
/// <inheritdoc />
|
|
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, bool refuseArrayOnScalar = false) { }
|
|
|
|
/// <inheritdoc />
|
|
public void RebuildAddressSpace() { }
|
|
|
|
/// <inheritdoc />
|
|
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) { }
|
|
|
|
/// <inheritdoc />
|
|
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
|
}
|