using MessagePack; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Ipc; /// /// implementation that forwards every operation over a /// to a Driver.FOCAS.Host process. Keeps the /// Fwlib32.dll P/Invoke out of the main server process so a native crash /// blast-radius stops at the Host boundary. /// /// /// Session lifecycle: sends OpenSessionRequest and /// caches the returned SessionId. Subsequent / /// / calls thread that session id /// onto each request DTO. sends CloseSessionRequest + /// disposes the underlying pipe. /// public sealed class IpcFocasClient : IFocasClient { private readonly FocasIpcClient _ipc; private readonly FocasCncSeries _series; private long _sessionId; private bool _connected; public IpcFocasClient(FocasIpcClient ipc, FocasCncSeries series = FocasCncSeries.Unknown) { _ipc = ipc ?? throw new ArgumentNullException(nameof(ipc)); _series = series; } public bool IsConnected => _connected; public async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken) { if (_connected) return; var resp = await _ipc.CallAsync( FocasMessageKind.OpenSessionRequest, new OpenSessionRequest { HostAddress = $"{address.Host}:{address.Port}", TimeoutMs = (int)Math.Max(1, timeout.TotalMilliseconds), CncSeries = (int)_series, }, FocasMessageKind.OpenSessionResponse, cancellationToken).ConfigureAwait(false); if (!resp.Success) throw new InvalidOperationException( $"FOCAS Host rejected OpenSession for {address}: {resp.ErrorCode ?? "?"} — {resp.Error}"); _sessionId = resp.SessionId; _connected = true; } public async Task<(object? value, uint status)> ReadAsync( FocasAddress address, FocasDataType type, CancellationToken cancellationToken) { if (!_connected) return (null, FocasStatusMapper.BadCommunicationError); var resp = await _ipc.CallAsync( FocasMessageKind.ReadRequest, new ReadRequest { SessionId = _sessionId, Address = ToDto(address), DataType = (int)type, }, FocasMessageKind.ReadResponse, cancellationToken).ConfigureAwait(false); if (!resp.Success) return (null, resp.StatusCode); var value = DecodeValue(resp.ValueBytes, resp.ValueTypeCode); return (value, resp.StatusCode); } public async Task WriteAsync( FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken) { if (!_connected) return FocasStatusMapper.BadCommunicationError; // PMC bit writes get the first-class RMW frame so the critical section stays on the Host. if (address.Kind == FocasAreaKind.Pmc && type == FocasDataType.Bit && address.BitIndex is int bit) { var bitResp = await _ipc.CallAsync( FocasMessageKind.PmcBitWriteRequest, new PmcBitWriteRequest { SessionId = _sessionId, Address = ToDto(address), BitIndex = bit, Value = Convert.ToBoolean(value), }, FocasMessageKind.PmcBitWriteResponse, cancellationToken).ConfigureAwait(false); return bitResp.StatusCode; } var resp = await _ipc.CallAsync( FocasMessageKind.WriteRequest, new WriteRequest { SessionId = _sessionId, Address = ToDto(address), DataType = (int)type, ValueTypeCode = (int)type, ValueBytes = EncodeValue(value, type), }, FocasMessageKind.WriteResponse, cancellationToken).ConfigureAwait(false); return resp.StatusCode; } public async Task ProbeAsync(CancellationToken cancellationToken) { if (!_connected) return false; try { var resp = await _ipc.CallAsync( FocasMessageKind.ProbeRequest, new ProbeRequest { SessionId = _sessionId }, FocasMessageKind.ProbeResponse, cancellationToken).ConfigureAwait(false); return resp.Healthy; } catch { return false; } } public void Dispose() { if (_connected) { try { _ipc.SendOneWayAsync(FocasMessageKind.CloseSessionRequest, new CloseSessionRequest { SessionId = _sessionId }, CancellationToken.None) .GetAwaiter().GetResult(); } catch { /* best effort */ } _connected = false; } _ipc.DisposeAsync().AsTask().GetAwaiter().GetResult(); } private static FocasAddressDto ToDto(FocasAddress addr) => new() { Kind = (int)addr.Kind, PmcLetter = addr.PmcLetter, Number = addr.Number, BitIndex = addr.BitIndex, }; private static byte[]? EncodeValue(object? value, FocasDataType type) { if (value is null) return null; return type switch { FocasDataType.Bit => MessagePackSerializer.Serialize(Convert.ToBoolean(value)), FocasDataType.Byte => MessagePackSerializer.Serialize(Convert.ToByte(value)), FocasDataType.Int16 => MessagePackSerializer.Serialize(Convert.ToInt16(value)), FocasDataType.Int32 => MessagePackSerializer.Serialize(Convert.ToInt32(value)), FocasDataType.Float32 => MessagePackSerializer.Serialize(Convert.ToSingle(value)), FocasDataType.Float64 => MessagePackSerializer.Serialize(Convert.ToDouble(value)), FocasDataType.String => MessagePackSerializer.Serialize(Convert.ToString(value) ?? string.Empty), _ => MessagePackSerializer.Serialize(Convert.ToInt32(value)), }; } private static object? DecodeValue(byte[]? bytes, int typeCode) { if (bytes is null) return null; return typeCode switch { FocasDataTypeCode.Bit => MessagePackSerializer.Deserialize(bytes), FocasDataTypeCode.Byte => MessagePackSerializer.Deserialize(bytes), FocasDataTypeCode.Int16 => MessagePackSerializer.Deserialize(bytes), FocasDataTypeCode.Int32 => MessagePackSerializer.Deserialize(bytes), FocasDataTypeCode.Float32 => MessagePackSerializer.Deserialize(bytes), FocasDataTypeCode.Float64 => MessagePackSerializer.Deserialize(bytes), FocasDataTypeCode.String => MessagePackSerializer.Deserialize(bytes), _ => MessagePackSerializer.Deserialize(bytes), }; } } /// /// Factory producing s. One pipe connection per /// IFocasClient — matches the driver's one-client-per-device invariant. The /// deployment wires this into the DI container in place of /// . /// public sealed class IpcFocasClientFactory(Func ipcClientFactory, FocasCncSeries series = FocasCncSeries.Unknown) : IFocasClientFactory { public IFocasClient Create() => new IpcFocasClient(ipcClientFactory(), series); }