Files
lmxopcua/docs/ReadWriteOperations.md
Joseph Doherty 965e430f48 Add component-level documentation for all 14 server subsystems
Provides technical documentation covering OPC UA server, address space,
Galaxy repository, MXAccess bridge, data types, read/write, subscriptions,
alarms, historian, incremental sync, configuration, dashboard, service
hosting, and CLI tool. Updates README with component documentation table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:47:59 -04:00

5.4 KiB

Read/Write Operations

LmxNodeManager overrides the OPC UA Read and Write methods to translate client requests into MXAccess runtime calls. Each override resolves the OPC UA NodeId to a Galaxy tag reference, performs the I/O through IMxAccessClient, and returns the result with appropriate status codes.

Read Override

The Read override in LmxNodeManager intercepts value attribute reads for nodes in the Galaxy namespace.

Resolution flow

  1. The base class Read runs first, handling non-value attributes (DisplayName, DataType, etc.) through the standard node manager.
  2. For each ReadValueId where AttributeId == Attributes.Value, the override checks whether the node belongs to this namespace (NamespaceIndex match).
  3. The string-typed NodeId.Identifier is looked up in _nodeIdToTagReference to find the corresponding FullTagReference (e.g., DelmiaReceiver_001.DownloadPath).
  4. _mxAccessClient.ReadAsync(tagRef) retrieves the current value, timestamp, and quality from MXAccess. The async call is synchronously awaited because the OPC UA SDK Read override is synchronous.
  5. The returned Vtq is converted to a DataValue via CreatePublishedDataValue, which normalizes array values through NormalizePublishedValue (substituting a default typed array when the value is null for array nodes).
  6. On success, errors[i] is set to ServiceResult.Good. On exception, the error is set to BadInternalError.
if (_nodeIdToTagReference.TryGetValue(nodeIdStr, out var tagRef))
{
    var vtq = _mxAccessClient.ReadAsync(tagRef).GetAwaiter().GetResult();
    results[i] = CreatePublishedDataValue(tagRef, vtq);
    errors[i] = ServiceResult.Good;
}

Write Override

The Write override follows a similar pattern but includes access-level enforcement and array element write support.

Access level check

The base class Write runs first and sets BadNotWritable for nodes whose AccessLevel does not include CurrentWrite. The override skips these nodes:

if (errors[i] != null && errors[i].StatusCode == StatusCodes.BadNotWritable)
    continue;

The AccessLevel is set during node creation based on SecurityClassificationMapper.IsWritable(attr.SecurityClassification). Read-only Galaxy attributes (e.g., security classification FreeRead) get AccessLevels.CurrentRead only.

Write flow

  1. The NodeId is resolved to a tag reference via _nodeIdToTagReference.
  2. The raw value is extracted from writeValue.Value.WrappedValue.Value.
  3. If the write includes an IndexRange (array element write), TryApplyArrayElementWrite handles the merge before sending the full array to MXAccess.
  4. _mxAccessClient.WriteAsync(tagRef, value) sends the value to the Galaxy runtime.
  5. On success, PublishLocalWrite updates the in-memory node immediately so subscribed clients see the change without waiting for the next MXAccess data change callback.

Array element writes via IndexRange

TryApplyArrayElementWrite supports writing individual elements of an array attribute. MXAccess does not support element-level writes, so the method performs a read-modify-write:

  1. Parse the IndexRange string as a zero-based integer index. Return BadIndexRangeInvalid if parsing fails or the index is negative.
  2. Read the current array value from MXAccess via ReadAsync.
  3. Clone the array and set the element at the target index.
  4. NormalizeIndexedWriteValue unwraps single-element arrays (OPC UA clients sometimes wrap a scalar in a one-element array).
  5. ConvertArrayElementValue coerces the value to the array's element type using Convert.ChangeType, handling null values by substituting the type's default.
  6. The full modified array is written back to MXAccess as a single WriteAsync call.
var nextArray = (Array)currentArray.Clone();
nextArray.SetValue(ConvertArrayElementValue(normalizedValue, elementType), index);
updatedArray = nextArray;

Value Type Conversion

CreatePublishedDataValue wraps the conversion pipeline. NormalizePublishedValue checks whether the tag is an array type with a declared ArrayDimension and substitutes a default typed array (via CreateDefaultArrayValue) when the raw value is null. This prevents OPC UA clients from receiving a null variant for array nodes, which violates the specification for nodes declared with ValueRank.OneDimension.

CreateDefaultArrayValue uses MxDataTypeMapper.MapToClrType to determine the CLR element type, then creates an Array.CreateInstance of the declared length. String arrays are initialized with string.Empty elements rather than null.

PublishLocalWrite

After a successful write, PublishLocalWrite updates the variable node in memory without waiting for the MXAccess OnDataChange callback to arrive:

private void PublishLocalWrite(string tagRef, object? value)
{
    var dataValue = CreatePublishedDataValue(tagRef, Vtq.Good(value));
    variable.Value = dataValue.Value;
    variable.StatusCode = dataValue.StatusCode;
    variable.Timestamp = dataValue.SourceTimestamp;
    variable.ClearChangeMasks(SystemContext, false);
}

ClearChangeMasks notifies the OPC UA framework that the node value has changed, which triggers data change notifications to any active monitored items. Without this call, subscribed clients would only see the update when the next MXAccess data change event arrives, which could be delayed depending on the subscription interval.