diff --git a/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs b/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs
index 1933f47..fa16fcf 100644
--- a/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs
+++ b/src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs
@@ -46,6 +46,78 @@ public sealed class VariantConverterTests
Assert.Equal("VT_DATE", converted.VariantType);
}
+ /// Verifies that scalar MxValue kinds convert to the matching boxed CLR type for a COM write.
+ [Fact]
+ public void ConvertToComValue_WithInt32_ReturnsBoxedInt()
+ {
+ object? result = _converter.ConvertToComValue(new MxValue { Int32Value = 123 });
+
+ Assert.Equal(123, Assert.IsType(result));
+ }
+
+ /// Verifies that a boolean MxValue converts to a boxed bool for a COM write.
+ [Fact]
+ public void ConvertToComValue_WithBool_ReturnsBoxedBool()
+ {
+ object? result = _converter.ConvertToComValue(new MxValue { BoolValue = true });
+
+ Assert.True(Assert.IsType(result));
+ }
+
+ /// Verifies that a string MxValue converts to a string for a COM write.
+ [Fact]
+ public void ConvertToComValue_WithString_ReturnsString()
+ {
+ object? result = _converter.ConvertToComValue(new MxValue { StringValue = "abc" });
+
+ Assert.Equal("abc", Assert.IsType(result));
+ }
+
+ /// Verifies that a timestamp MxValue converts to a UTC DateTime the COM marshaler renders as VT_DATE.
+ [Fact]
+ public void ConvertToComValue_WithTimestamp_ReturnsUtcDateTime()
+ {
+ DateTime dateTime = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
+
+ object? result = _converter.ConvertToComValue(
+ new MxValue { TimestampValue = ProtobufTimestamp.FromDateTime(dateTime) });
+
+ Assert.Equal(dateTime, Assert.IsType(result));
+ }
+
+ /// Verifies that an MXAccess-null MxValue converts to a CLR null.
+ [Fact]
+ public void ConvertToComValue_WithNull_ReturnsNull()
+ {
+ object? result = _converter.ConvertToComValue(new MxValue { IsNull = true });
+
+ Assert.Null(result);
+ }
+
+ /// Verifies that an integer-array MxValue converts to an int array the COM marshaler renders as a SAFEARRAY.
+ [Fact]
+ public void ConvertToComValue_WithInt32Array_ReturnsInt32Array()
+ {
+ MxValue value = new()
+ {
+ ArrayValue = new MxArray
+ {
+ Int32Values = new Int32Array { Values = { 1, 2, 3 } },
+ },
+ };
+
+ object? result = _converter.ConvertToComValue(value);
+
+ Assert.Equal(new[] { 1, 2, 3 }, Assert.IsType(result));
+ }
+
+ /// Verifies that an MxValue with no value kind set cannot be converted for a COM write.
+ [Fact]
+ public void ConvertToComValue_WithNoKind_Throws()
+ {
+ Assert.Throws(() => _converter.ConvertToComValue(new MxValue()));
+ }
+
/// Verifies that file time values with expected time data type are converted to protobuf timestamps.
[Fact]
public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp()
diff --git a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs
index b9f512f..86a0231 100644
--- a/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs
+++ b/src/MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs
@@ -379,10 +379,10 @@ public sealed class AlarmCommandExecutorTests
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { }
public void Suspend(int serverHandle, int itemHandle) { }
public void Activate(int serverHandle, int itemHandle) { }
- public void Write(int serverHandle, int itemHandle, object value, int userId) { }
- public void Write2(int serverHandle, int itemHandle, object value, object timestampValue, int userId) { }
- public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value) { }
- public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, object timestampValue) { }
+ public void Write(int serverHandle, int itemHandle, object? value, int userId) { }
+ public void Write2(int serverHandle, int itemHandle, object? value, object? timestampValue, int userId) { }
+ public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value) { }
+ public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestampValue) { }
public int AuthenticateUser(string userName, string password) => 0;
public int ArchestrAUserToId(string userName) => 0;
}
diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
index fd121c4..faf9ec1 100644
--- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
+++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
@@ -134,5 +134,26 @@ public sealed class MxAccessComServerTests
{
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
}
+
+ public void Write(int serverHandle, int itemHandle, object? value, int userId)
+ {
+ calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}");
+ }
+
+ public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
+ {
+ calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}");
+ }
+
+ public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
+ {
+ calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}");
+ }
+
+ public void WriteSecured2(
+ int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
+ {
+ calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
+ }
}
}
diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
index 43fa4f2..b4bc61e 100644
--- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
+++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
+using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
using MxGateway.Worker.MxAccess;
using MxGateway.Worker.Sta;
@@ -617,6 +618,143 @@ public sealed class MxAccessCommandExecutorTests
Assert.Null(factory.FakeComObject.AdviseServerHandle);
}
+ /// Verifies that Write dispatches the converted value to MXAccess on the STA thread.
+ [Fact]
+ public async Task DispatchAsync_Write_CallsMxAccessOnStaWithConvertedValue()
+ {
+ FakeMxAccessComObject fakeComObject = new(registerHandle: 70);
+ FakeMxAccessComObjectFactory factory = new(fakeComObject);
+ using StaRuntime runtime = CreateRuntime();
+ using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
+ await session.StartAsync(workerProcessId: 1234);
+
+ MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand(
+ "write", serverHandle: 70, itemHandle: 700, value: 123, userId: 5));
+
+ Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
+ Assert.Equal(MxCommandKind.Write, reply.Kind);
+ Assert.Equal(70, fakeComObject.WriteServerHandle);
+ Assert.Equal(700, fakeComObject.WriteItemHandle);
+ Assert.Equal(123, fakeComObject.WriteValue);
+ Assert.Equal(5, fakeComObject.WriteUserId);
+ Assert.Equal(runtime.StaThreadId, fakeComObject.WriteThreadId);
+ }
+
+ /// Verifies that Write2 forwards the converted value and timestamp to MXAccess.
+ [Fact]
+ public async Task DispatchAsync_Write2_ForwardsValueAndTimestamp()
+ {
+ FakeMxAccessComObject fakeComObject = new(registerHandle: 71);
+ FakeMxAccessComObjectFactory factory = new(fakeComObject);
+ using StaRuntime runtime = CreateRuntime();
+ using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
+ await session.StartAsync(workerProcessId: 1234);
+ DateTime timestamp = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
+
+ MxCommandReply reply = await session.DispatchAsync(CreateWrite2Command(
+ "write2", serverHandle: 71, itemHandle: 710, value: 456, timestamp: timestamp, userId: 6));
+
+ Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
+ Assert.Equal(MxCommandKind.Write2, reply.Kind);
+ Assert.Equal(710, fakeComObject.WriteItemHandle);
+ Assert.Equal(456, fakeComObject.WriteValue);
+ Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
+ Assert.Equal(6, fakeComObject.WriteUserId);
+ }
+
+ /// Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess.
+ [Fact]
+ public async Task DispatchAsync_WriteSecured_ForwardsUserIds()
+ {
+ FakeMxAccessComObject fakeComObject = new(registerHandle: 72);
+ FakeMxAccessComObjectFactory factory = new(fakeComObject);
+ using StaRuntime runtime = CreateRuntime();
+ using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
+ await session.StartAsync(workerProcessId: 1234);
+
+ MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
+ "write-secured", serverHandle: 72, itemHandle: 720, value: 789, currentUserId: 11, verifierUserId: 22));
+
+ Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
+ Assert.Equal(MxCommandKind.WriteSecured, reply.Kind);
+ Assert.Equal(720, fakeComObject.WriteItemHandle);
+ Assert.Equal(789, fakeComObject.WriteValue);
+ Assert.Equal(11, fakeComObject.WriteCurrentUserId);
+ Assert.Equal(22, fakeComObject.WriteVerifierUserId);
+ }
+
+ /// Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess.
+ [Fact]
+ public async Task DispatchAsync_WriteSecured2_ForwardsUserIdsValueAndTimestamp()
+ {
+ FakeMxAccessComObject fakeComObject = new(registerHandle: 73);
+ FakeMxAccessComObjectFactory factory = new(fakeComObject);
+ using StaRuntime runtime = CreateRuntime();
+ using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
+ await session.StartAsync(workerProcessId: 1234);
+ DateTime timestamp = new(2026, 5, 19, 13, 30, 0, DateTimeKind.Utc);
+
+ MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2Command(
+ "write-secured2", serverHandle: 73, itemHandle: 730, value: 1011,
+ timestamp: timestamp, currentUserId: 33, verifierUserId: 44));
+
+ Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
+ Assert.Equal(MxCommandKind.WriteSecured2, reply.Kind);
+ Assert.Equal(1011, fakeComObject.WriteValue);
+ Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
+ Assert.Equal(33, fakeComObject.WriteCurrentUserId);
+ Assert.Equal(44, fakeComObject.WriteVerifierUserId);
+ }
+
+ /// Verifies that Write without a payload returns an invalid request error.
+ [Fact]
+ public async Task DispatchAsync_WriteWithoutPayload_ReturnsInvalidRequest()
+ {
+ FakeMxAccessComObject fakeComObject = new(registerHandle: 74);
+ FakeMxAccessComObjectFactory factory = new(fakeComObject);
+ using StaRuntime runtime = CreateRuntime();
+ using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
+ await session.StartAsync(workerProcessId: 1234);
+
+ MxCommandReply reply = await session.DispatchAsync(new StaCommand(
+ "session-1",
+ "missing-write-payload",
+ new MxCommand
+ {
+ Kind = MxCommandKind.Write,
+ }));
+
+ Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
+ Assert.Null(fakeComObject.WriteServerHandle);
+ }
+
+ /// Verifies that Write without a value returns an invalid request error.
+ [Fact]
+ public async Task DispatchAsync_WriteWithoutValue_ReturnsInvalidRequest()
+ {
+ FakeMxAccessComObject fakeComObject = new(registerHandle: 75);
+ FakeMxAccessComObjectFactory factory = new(fakeComObject);
+ using StaRuntime runtime = CreateRuntime();
+ using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
+ await session.StartAsync(workerProcessId: 1234);
+
+ MxCommandReply reply = await session.DispatchAsync(new StaCommand(
+ "session-1",
+ "missing-write-value",
+ new MxCommand
+ {
+ Kind = MxCommandKind.Write,
+ Write = new WriteCommand
+ {
+ ServerHandle = 75,
+ ItemHandle = 750,
+ },
+ }));
+
+ Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
+ Assert.Null(fakeComObject.WriteServerHandle);
+ }
+
private static StaCommand CreateRegisterCommand(
string correlationId,
string clientName)
@@ -729,6 +867,126 @@ public sealed class MxAccessCommandExecutorTests
});
}
+ private static MxValue CreateIntegerValue(int value)
+ {
+ return new MxValue
+ {
+ DataType = MxDataType.Integer,
+ VariantType = "VT_I4",
+ Int32Value = value,
+ };
+ }
+
+ private static MxValue CreateTimestampValue(DateTime timestamp)
+ {
+ return new MxValue
+ {
+ DataType = MxDataType.Time,
+ VariantType = "VT_DATE",
+ TimestampValue = Timestamp.FromDateTime(timestamp),
+ };
+ }
+
+ private static StaCommand CreateWriteCommand(
+ string correlationId,
+ int serverHandle,
+ int itemHandle,
+ int value,
+ int userId)
+ {
+ return new StaCommand(
+ "session-1",
+ correlationId,
+ new MxCommand
+ {
+ Kind = MxCommandKind.Write,
+ Write = new WriteCommand
+ {
+ ServerHandle = serverHandle,
+ ItemHandle = itemHandle,
+ Value = CreateIntegerValue(value),
+ UserId = userId,
+ },
+ });
+ }
+
+ private static StaCommand CreateWrite2Command(
+ string correlationId,
+ int serverHandle,
+ int itemHandle,
+ int value,
+ DateTime timestamp,
+ int userId)
+ {
+ return new StaCommand(
+ "session-1",
+ correlationId,
+ new MxCommand
+ {
+ Kind = MxCommandKind.Write2,
+ Write2 = new Write2Command
+ {
+ ServerHandle = serverHandle,
+ ItemHandle = itemHandle,
+ Value = CreateIntegerValue(value),
+ TimestampValue = CreateTimestampValue(timestamp),
+ UserId = userId,
+ },
+ });
+ }
+
+ private static StaCommand CreateWriteSecuredCommand(
+ string correlationId,
+ int serverHandle,
+ int itemHandle,
+ int value,
+ int currentUserId,
+ int verifierUserId)
+ {
+ return new StaCommand(
+ "session-1",
+ correlationId,
+ new MxCommand
+ {
+ Kind = MxCommandKind.WriteSecured,
+ WriteSecured = new WriteSecuredCommand
+ {
+ ServerHandle = serverHandle,
+ ItemHandle = itemHandle,
+ CurrentUserId = currentUserId,
+ VerifierUserId = verifierUserId,
+ Value = CreateIntegerValue(value),
+ },
+ });
+ }
+
+ private static StaCommand CreateWriteSecured2Command(
+ string correlationId,
+ int serverHandle,
+ int itemHandle,
+ int value,
+ DateTime timestamp,
+ int currentUserId,
+ int verifierUserId)
+ {
+ return new StaCommand(
+ "session-1",
+ correlationId,
+ new MxCommand
+ {
+ Kind = MxCommandKind.WriteSecured2,
+ WriteSecured2 = new WriteSecured2Command
+ {
+ ServerHandle = serverHandle,
+ ItemHandle = itemHandle,
+ CurrentUserId = currentUserId,
+ VerifierUserId = verifierUserId,
+ Value = CreateIntegerValue(value),
+ TimestampValue = CreateTimestampValue(timestamp),
+ },
+ });
+ }
+
private static StaCommand CreateUnAdviseCommand(
string correlationId,
int serverHandle,
@@ -1080,6 +1338,118 @@ public sealed class MxAccessCommandExecutorTests
throw adviseSupervisoryException;
}
}
+
+ /// Gets the server handle passed to the most recent write, if called.
+ public int? WriteServerHandle { get; private set; }
+
+ /// Gets the item handle passed to the most recent write, if called.
+ public int? WriteItemHandle { get; private set; }
+
+ /// Gets the value passed to the most recent write, if called.
+ public object? WriteValue { get; private set; }
+
+ /// Gets the timestamp passed to the most recent timestamped write, if called.
+ public object? WriteTimestamp { get; private set; }
+
+ /// Gets the user id passed to the most recent Write/Write2, if called.
+ public int? WriteUserId { get; private set; }
+
+ /// Gets the current user id passed to the most recent secured write, if called.
+ public int? WriteCurrentUserId { get; private set; }
+
+ /// Gets the verifier user id passed to the most recent secured write, if called.
+ public int? WriteVerifierUserId { get; private set; }
+
+ /// Gets the thread ID on which the most recent write was called.
+ public int? WriteThreadId { get; private set; }
+
+ /// Writes a value to an item and tracks the operation.
+ /// Server handle for the write.
+ /// Item handle to write to.
+ /// Value to write.
+ /// MXAccess user id for the write.
+ public void Write(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ int userId)
+ {
+ operationNames.Add($"Write:{serverHandle}:{itemHandle}");
+ WriteServerHandle = serverHandle;
+ WriteItemHandle = itemHandle;
+ WriteValue = value;
+ WriteUserId = userId;
+ WriteThreadId = Environment.CurrentManagedThreadId;
+ }
+
+ /// Writes a timestamped value to an item and tracks the operation.
+ /// Server handle for the write.
+ /// Item handle to write to.
+ /// Value to write.
+ /// Source timestamp for the write.
+ /// MXAccess user id for the write.
+ public void Write2(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ object? timestamp,
+ int userId)
+ {
+ operationNames.Add($"Write2:{serverHandle}:{itemHandle}");
+ WriteServerHandle = serverHandle;
+ WriteItemHandle = itemHandle;
+ WriteValue = value;
+ WriteTimestamp = timestamp;
+ WriteUserId = userId;
+ WriteThreadId = Environment.CurrentManagedThreadId;
+ }
+
+ /// Performs a secured write to an item and tracks the operation.
+ /// Server handle for the write.
+ /// Item handle to write to.
+ /// Operator user id.
+ /// Verifier user id.
+ /// Value to write.
+ public void WriteSecured(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value)
+ {
+ operationNames.Add($"WriteSecured:{serverHandle}:{itemHandle}");
+ WriteServerHandle = serverHandle;
+ WriteItemHandle = itemHandle;
+ WriteCurrentUserId = currentUserId;
+ WriteVerifierUserId = verifierUserId;
+ WriteValue = value;
+ WriteThreadId = Environment.CurrentManagedThreadId;
+ }
+
+ /// Performs a secured timestamped write to an item and tracks the operation.
+ /// Server handle for the write.
+ /// Item handle to write to.
+ /// Operator user id.
+ /// Verifier user id.
+ /// Value to write.
+ /// Source timestamp for the write.
+ public void WriteSecured2(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value,
+ object? timestamp)
+ {
+ operationNames.Add($"WriteSecured2:{serverHandle}:{itemHandle}");
+ WriteServerHandle = serverHandle;
+ WriteItemHandle = itemHandle;
+ WriteCurrentUserId = currentUserId;
+ WriteVerifierUserId = verifierUserId;
+ WriteValue = value;
+ WriteTimestamp = timestamp;
+ WriteThreadId = Environment.CurrentManagedThreadId;
+ }
}
/// Factory for creating fake MXAccess COM objects in tests.
diff --git a/src/MxGateway.Worker/Conversion/VariantConverter.cs b/src/MxGateway.Worker/Conversion/VariantConverter.cs
index c368769..757960b 100644
--- a/src/MxGateway.Worker/Conversion/VariantConverter.cs
+++ b/src/MxGateway.Worker/Conversion/VariantConverter.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Linq;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
@@ -118,6 +119,63 @@ public sealed class VariantConverter
}
}
+ ///
+ /// Converts an into a CLR object suitable for an
+ /// MXAccess COM write. The COM marshaler boxes the returned value into the
+ /// matching VARIANT, so this is the inverse of .
+ ///
+ /// Protobuf value to convert.
+ /// A COM-marshalable value, or for an MXAccess null.
+ public object? ConvertToComValue(MxValue value)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (value.IsNull)
+ {
+ return null;
+ }
+
+ return value.KindCase switch
+ {
+ MxValue.KindOneofCase.BoolValue => value.BoolValue,
+ MxValue.KindOneofCase.Int32Value => value.Int32Value,
+ MxValue.KindOneofCase.Int64Value => value.Int64Value,
+ MxValue.KindOneofCase.FloatValue => value.FloatValue,
+ MxValue.KindOneofCase.DoubleValue => value.DoubleValue,
+ MxValue.KindOneofCase.StringValue => value.StringValue,
+ // The COM marshaler renders a DateTime as VT_DATE; MXAccess accepts
+ // it as the timestamped-write time argument.
+ MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTime(),
+ MxValue.KindOneofCase.ArrayValue => ConvertToComArray(value.ArrayValue),
+ MxValue.KindOneofCase.RawValue => throw new ArgumentException(
+ "MxValue raw payloads cannot be written to MXAccess.", nameof(value)),
+ _ => throw new ArgumentException(
+ "MxValue has no value kind set; nothing to write.", nameof(value)),
+ };
+ }
+
+ private static Array ConvertToComArray(MxArray array)
+ {
+ return array.ValuesCase switch
+ {
+ MxArray.ValuesOneofCase.BoolValues => array.BoolValues.Values.ToArray(),
+ MxArray.ValuesOneofCase.Int32Values => array.Int32Values.Values.ToArray(),
+ MxArray.ValuesOneofCase.Int64Values => array.Int64Values.Values.ToArray(),
+ MxArray.ValuesOneofCase.FloatValues => array.FloatValues.Values.ToArray(),
+ MxArray.ValuesOneofCase.DoubleValues => array.DoubleValues.Values.ToArray(),
+ MxArray.ValuesOneofCase.StringValues => array.StringValues.Values.ToArray(),
+ MxArray.ValuesOneofCase.TimestampValues =>
+ array.TimestampValues.Values.Select(timestamp => timestamp.ToDateTime()).ToArray(),
+ MxArray.ValuesOneofCase.RawValues => throw new ArgumentException(
+ "MxArray raw payloads cannot be written to MXAccess.", nameof(array)),
+ _ => throw new ArgumentException(
+ "MxArray has no element values set; nothing to write.", nameof(array)),
+ };
+ }
+
private static MxValue ConvertScalar(
object value,
MxDataType expectedDataType)
diff --git a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs
index 2645e54..33b251f 100644
--- a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs
+++ b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs
@@ -56,4 +56,56 @@ public interface IMxAccessServer
void AdviseSupervisory(
int serverHandle,
int itemHandle);
+
+ /// Writes a value to an item.
+ /// Server handle identifying the registration.
+ /// Item handle to write to.
+ /// COM-marshalable value to write; writes an MXAccess null.
+ /// MXAccess user id (security classification) for the write.
+ void Write(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ int userId);
+
+ /// Writes a value with an explicit source timestamp to an item.
+ /// Server handle identifying the registration.
+ /// Item handle to write to.
+ /// COM-marshalable value to write; writes an MXAccess null.
+ /// COM-marshalable source timestamp for the write.
+ /// MXAccess user id (security classification) for the write.
+ void Write2(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ object? timestamp,
+ int userId);
+
+ /// Performs a secured/verified write to an item.
+ /// Server handle identifying the registration.
+ /// Item handle to write to.
+ /// MXAccess user id of the operator performing the write.
+ /// MXAccess user id of the verifier authorizing the write.
+ /// COM-marshalable value to write; writes an MXAccess null.
+ void WriteSecured(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value);
+
+ /// Performs a secured/verified write with an explicit source timestamp.
+ /// Server handle identifying the registration.
+ /// Item handle to write to.
+ /// MXAccess user id of the operator performing the write.
+ /// MXAccess user id of the verifier authorizing the write.
+ /// COM-marshalable value to write; writes an MXAccess null.
+ /// COM-marshalable source timestamp for the write.
+ void WriteSecured2(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value,
+ object? timestamp);
}
diff --git a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs
index 7ddd2f7..9b4e35d 100644
--- a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs
+++ b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs
@@ -140,6 +140,74 @@ public sealed class MxAccessComServer : IMxAccessServer
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
}
+ ///
+ public void Write(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ int userId)
+ {
+ if (mxAccessComObject is IMxAccessServer typedFake)
+ {
+ typedFake.Write(serverHandle, itemHandle, value, userId);
+ return;
+ }
+
+ AsProxyServer().Write(serverHandle, itemHandle, value!, userId);
+ }
+
+ ///
+ public void Write2(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ object? timestamp,
+ int userId)
+ {
+ if (mxAccessComObject is IMxAccessServer typedFake)
+ {
+ typedFake.Write2(serverHandle, itemHandle, value, timestamp, userId);
+ return;
+ }
+
+ AsProxyServer4().Write2(serverHandle, itemHandle, value!, timestamp!, userId);
+ }
+
+ ///
+ public void WriteSecured(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value)
+ {
+ if (mxAccessComObject is IMxAccessServer typedFake)
+ {
+ typedFake.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
+ return;
+ }
+
+ AsProxyServer().WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value!);
+ }
+
+ ///
+ public void WriteSecured2(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value,
+ object? timestamp)
+ {
+ if (mxAccessComObject is IMxAccessServer typedFake)
+ {
+ typedFake.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
+ return;
+ }
+
+ AsProxyServer4().WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value!, timestamp!);
+ }
+
private ILMXProxyServer AsProxyServer()
{
return mxAccessComObject as ILMXProxyServer
diff --git a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
index da49ffa..a9a8a20 100644
--- a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
+++ b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
@@ -74,6 +74,10 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
MxCommandKind.Advise => ExecuteAdvise(command),
MxCommandKind.UnAdvise => ExecuteUnAdvise(command),
MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command),
+ MxCommandKind.Write => ExecuteWrite(command),
+ MxCommandKind.Write2 => ExecuteWrite2(command),
+ MxCommandKind.WriteSecured => ExecuteWriteSecured(command),
+ MxCommandKind.WriteSecured2 => ExecuteWriteSecured2(command),
MxCommandKind.AddItemBulk => ExecuteAddItemBulk(command),
MxCommandKind.AdviseItemBulk => ExecuteAdviseItemBulk(command),
MxCommandKind.RemoveItemBulk => ExecuteRemoveItemBulk(command),
@@ -223,6 +227,108 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
return CreateOkReply(command);
}
+ private MxCommandReply ExecuteWrite(StaCommand command)
+ {
+ if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write)
+ {
+ return CreateInvalidRequestReply(command, "Write command payload is required.");
+ }
+
+ WriteCommand writeCommand = command.Command.Write;
+ if (writeCommand.Value is null)
+ {
+ return CreateInvalidRequestReply(command, "Write command value is required.");
+ }
+
+ session.Write(
+ writeCommand.ServerHandle,
+ writeCommand.ItemHandle,
+ variantConverter.ConvertToComValue(writeCommand.Value),
+ writeCommand.UserId);
+
+ return CreateOkReply(command);
+ }
+
+ private MxCommandReply ExecuteWrite2(StaCommand command)
+ {
+ if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write2)
+ {
+ return CreateInvalidRequestReply(command, "Write2 command payload is required.");
+ }
+
+ Write2Command write2Command = command.Command.Write2;
+ if (write2Command.Value is null)
+ {
+ return CreateInvalidRequestReply(command, "Write2 command value is required.");
+ }
+
+ if (write2Command.TimestampValue is null)
+ {
+ return CreateInvalidRequestReply(command, "Write2 command timestamp value is required.");
+ }
+
+ session.Write2(
+ write2Command.ServerHandle,
+ write2Command.ItemHandle,
+ variantConverter.ConvertToComValue(write2Command.Value),
+ variantConverter.ConvertToComValue(write2Command.TimestampValue),
+ write2Command.UserId);
+
+ return CreateOkReply(command);
+ }
+
+ private MxCommandReply ExecuteWriteSecured(StaCommand command)
+ {
+ if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.WriteSecured)
+ {
+ return CreateInvalidRequestReply(command, "WriteSecured command payload is required.");
+ }
+
+ WriteSecuredCommand writeSecuredCommand = command.Command.WriteSecured;
+ if (writeSecuredCommand.Value is null)
+ {
+ return CreateInvalidRequestReply(command, "WriteSecured command value is required.");
+ }
+
+ session.WriteSecured(
+ writeSecuredCommand.ServerHandle,
+ writeSecuredCommand.ItemHandle,
+ writeSecuredCommand.CurrentUserId,
+ writeSecuredCommand.VerifierUserId,
+ variantConverter.ConvertToComValue(writeSecuredCommand.Value));
+
+ return CreateOkReply(command);
+ }
+
+ private MxCommandReply ExecuteWriteSecured2(StaCommand command)
+ {
+ if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.WriteSecured2)
+ {
+ return CreateInvalidRequestReply(command, "WriteSecured2 command payload is required.");
+ }
+
+ WriteSecured2Command writeSecured2Command = command.Command.WriteSecured2;
+ if (writeSecured2Command.Value is null)
+ {
+ return CreateInvalidRequestReply(command, "WriteSecured2 command value is required.");
+ }
+
+ if (writeSecured2Command.TimestampValue is null)
+ {
+ return CreateInvalidRequestReply(command, "WriteSecured2 command timestamp value is required.");
+ }
+
+ session.WriteSecured2(
+ writeSecured2Command.ServerHandle,
+ writeSecured2Command.ItemHandle,
+ writeSecured2Command.CurrentUserId,
+ writeSecured2Command.VerifierUserId,
+ variantConverter.ConvertToComValue(writeSecured2Command.Value),
+ variantConverter.ConvertToComValue(writeSecured2Command.TimestampValue));
+
+ return CreateOkReply(command);
+ }
+
private MxCommandReply ExecuteAddItemBulk(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItemBulk)
diff --git a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs
index 965bdfb..81f0d78 100644
--- a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs
+++ b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs
@@ -227,6 +227,78 @@ public sealed class MxAccessSession : IDisposable
MxAccessAdviceKind.Supervisory);
}
+ /// Writes a value to an item.
+ /// Handle returned by the worker.
+ /// Handle returned by the worker.
+ /// COM-marshalable value to write.
+ /// MXAccess user id (security classification) for the write.
+ public void Write(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ int userId)
+ {
+ ThrowIfDisposed();
+
+ mxAccessServer.Write(serverHandle, itemHandle, value, userId);
+ }
+
+ /// Writes a value with an explicit source timestamp to an item.
+ /// Handle returned by the worker.
+ /// Handle returned by the worker.
+ /// COM-marshalable value to write.
+ /// COM-marshalable source timestamp for the write.
+ /// MXAccess user id (security classification) for the write.
+ public void Write2(
+ int serverHandle,
+ int itemHandle,
+ object? value,
+ object? timestamp,
+ int userId)
+ {
+ ThrowIfDisposed();
+
+ mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId);
+ }
+
+ /// Performs a secured/verified write to an item.
+ /// Handle returned by the worker.
+ /// Handle returned by the worker.
+ /// MXAccess user id of the operator performing the write.
+ /// MXAccess user id of the verifier authorizing the write.
+ /// COM-marshalable value to write.
+ public void WriteSecured(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value)
+ {
+ ThrowIfDisposed();
+
+ mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
+ }
+
+ /// Performs a secured/verified write with an explicit source timestamp.
+ /// Handle returned by the worker.
+ /// Handle returned by the worker.
+ /// MXAccess user id of the operator performing the write.
+ /// MXAccess user id of the verifier authorizing the write.
+ /// COM-marshalable value to write.
+ /// COM-marshalable source timestamp for the write.
+ public void WriteSecured2(
+ int serverHandle,
+ int itemHandle,
+ int currentUserId,
+ int verifierUserId,
+ object? value,
+ object? timestamp)
+ {
+ ThrowIfDisposed();
+
+ mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
+ }
+
/// Adds multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item definitions to add.