200 lines
7.9 KiB
C#
200 lines
7.9 KiB
C#
using MessagePack;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Ipc;
|
|
|
|
/// <summary>
|
|
/// <see cref="IFocasClient"/> implementation that forwards every operation over a
|
|
/// <see cref="FocasIpcClient"/> to a <c>Driver.FOCAS.Host</c> process. Keeps the
|
|
/// <c>Fwlib32.dll</c> P/Invoke out of the main server process so a native crash
|
|
/// blast-radius stops at the Host boundary.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Session lifecycle: <see cref="ConnectAsync"/> sends <c>OpenSessionRequest</c> and
|
|
/// caches the returned <c>SessionId</c>. Subsequent <see cref="ReadAsync"/> /
|
|
/// <see cref="WriteAsync"/> / <see cref="ProbeAsync"/> calls thread that session id
|
|
/// onto each request DTO. <see cref="Dispose"/> sends <c>CloseSessionRequest</c> +
|
|
/// disposes the underlying pipe.
|
|
/// </remarks>
|
|
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<OpenSessionRequest, OpenSessionResponse>(
|
|
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<ReadRequest, ReadResponse>(
|
|
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<uint> 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<PmcBitWriteRequest, PmcBitWriteResponse>(
|
|
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<WriteRequest, WriteResponse>(
|
|
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<bool> ProbeAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return false;
|
|
try
|
|
{
|
|
var resp = await _ipc.CallAsync<ProbeRequest, ProbeResponse>(
|
|
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<bool>(bytes),
|
|
FocasDataTypeCode.Byte => MessagePackSerializer.Deserialize<byte>(bytes),
|
|
FocasDataTypeCode.Int16 => MessagePackSerializer.Deserialize<short>(bytes),
|
|
FocasDataTypeCode.Int32 => MessagePackSerializer.Deserialize<int>(bytes),
|
|
FocasDataTypeCode.Float32 => MessagePackSerializer.Deserialize<float>(bytes),
|
|
FocasDataTypeCode.Float64 => MessagePackSerializer.Deserialize<double>(bytes),
|
|
FocasDataTypeCode.String => MessagePackSerializer.Deserialize<string>(bytes),
|
|
_ => MessagePackSerializer.Deserialize<int>(bytes),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory producing <see cref="IpcFocasClient"/>s. One pipe connection per
|
|
/// <c>IFocasClient</c> — matches the driver's one-client-per-device invariant. The
|
|
/// deployment wires this into the DI container in place of
|
|
/// <see cref="UnimplementedFocasClientFactory"/>.
|
|
/// </summary>
|
|
public sealed class IpcFocasClientFactory(Func<FocasIpcClient> ipcClientFactory, FocasCncSeries series = FocasCncSeries.Unknown)
|
|
: IFocasClientFactory
|
|
{
|
|
public IFocasClient Create() => new IpcFocasClient(ipcClientFactory(), series);
|
|
}
|