# 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`. ```csharp 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: ```csharp 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. ```csharp 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: ```csharp 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.