using System.Buffers.Binary;
using System.Collections.Concurrent;
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
///
/// implementation backed by Fanuc's licensed
/// Fwlib32.dll via P/Invoke. The DLL is NOT shipped with
/// OtOpcUa; the deployment places it next to the server executable or on PATH
/// (per Fanuc licensing — see docs/v2/focas-deployment.md).
///
///
/// Construction is licence-safe — .NET P/Invoke is lazy, so instantiating this class
/// does NOT load Fwlib32.dll. The DLL only loads on the first wire call (Connect /
/// Read / Write / Probe). When missing, those calls throw
/// which the driver surfaces as BadCommunicationError through the normal exception
/// mapping.
///
/// Session-scoped handle — cnc_allclibhndl3 opens one FWLIB handle per CNC;
/// all PMC / parameter / macro reads on that device go through the same handle. Dispose
/// calls cnc_freelibhndl.
///
internal sealed class FwlibFocasClient : IFocasClient
{
private ushort _handle;
private bool _connected;
// Per-PMC-byte RMW lock registry. Bit writes to the same byte get serialised so two
// concurrent bit updates don't lose one another's modification. Key = "{addrType}:{byteAddr}".
private readonly ConcurrentDictionary _rmwLocks = new();
private SemaphoreSlim GetRmwLock(short addrType, int byteAddr) =>
_rmwLocks.GetOrAdd($"{addrType}:{byteAddr}", _ => new SemaphoreSlim(1, 1));
public bool IsConnected => _connected;
public Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken)
{
if (_connected) return Task.CompletedTask;
var timeoutMs = (int)Math.Max(1, timeout.TotalMilliseconds);
var ret = FwlibNative.AllcLibHndl3(address.Host, (ushort)address.Port, timeoutMs, out var handle);
if (ret != 0)
throw new InvalidOperationException(
$"FWLIB cnc_allclibhndl3 failed with EW_{ret} connecting to {address}.");
_handle = handle;
_connected = true;
return Task.CompletedTask;
}
public Task<(object? value, uint status)> ReadAsync(
FocasAddress address, FocasDataType type, CancellationToken cancellationToken)
{
if (!_connected) return Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadCommunicationError));
cancellationToken.ThrowIfCancellationRequested();
return address.Kind switch
{
FocasAreaKind.Pmc => Task.FromResult(ReadPmc(address, type)),
FocasAreaKind.Parameter => Task.FromResult(ReadParameter(address, type)),
FocasAreaKind.Macro => Task.FromResult(ReadMacro(address)),
_ => Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadNotSupported)),
};
}
public async Task WriteAsync(
FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken)
{
if (!_connected) return FocasStatusMapper.BadCommunicationError;
cancellationToken.ThrowIfCancellationRequested();
return address.Kind switch
{
FocasAreaKind.Pmc when type == FocasDataType.Bit && address.BitIndex is int =>
await WritePmcBitAsync(address, Convert.ToBoolean(value), cancellationToken).ConfigureAwait(false),
FocasAreaKind.Pmc => WritePmc(address, type, value),
FocasAreaKind.Parameter => WriteParameter(address, type, value),
FocasAreaKind.Macro => WriteMacro(address, value),
_ => FocasStatusMapper.BadNotSupported,
};
}
///
/// Read-modify-write one bit within a PMC byte. Acquires a per-byte semaphore so
/// concurrent bit writes against the same byte serialise and neither loses its update.
///
private async Task WritePmcBitAsync(
FocasAddress address, bool newValue, CancellationToken cancellationToken)
{
var addrType = FocasPmcAddrType.FromLetter(address.PmcLetter ?? "") ?? (short)0;
var bit = address.BitIndex ?? 0;
if (bit is < 0 or > 7)
throw new InvalidOperationException(
$"PMC bit index {bit} out of range (0-7) for {address.Canonical}.");
var rmwLock = GetRmwLock(addrType, address.Number);
await rmwLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// Read the parent byte.
var readBuf = new FwlibNative.IODBPMC { Data = new byte[40] };
var readRet = FwlibNative.PmcRdPmcRng(
_handle, addrType, FocasPmcDataType.Byte,
(ushort)address.Number, (ushort)address.Number, 8 + 1, ref readBuf);
if (readRet != 0) return FocasStatusMapper.MapFocasReturn(readRet);
var current = readBuf.Data[0];
var updated = newValue
? (byte)(current | (1 << bit))
: (byte)(current & ~(1 << bit));
// Write the updated byte.
var writeBuf = new FwlibNative.IODBPMC
{
TypeA = addrType,
TypeD = FocasPmcDataType.Byte,
DatanoS = (ushort)address.Number,
DatanoE = (ushort)address.Number,
Data = new byte[40],
};
writeBuf.Data[0] = updated;
var writeRet = FwlibNative.PmcWrPmcRng(_handle, 8 + 1, ref writeBuf);
return writeRet == 0 ? FocasStatusMapper.Good : FocasStatusMapper.MapFocasReturn(writeRet);
}
finally
{
rmwLock.Release();
}
}
public Task ProbeAsync(CancellationToken cancellationToken)
{
if (!_connected) return Task.FromResult(false);
var buf = new FwlibNative.ODBST();
var ret = FwlibNative.StatInfo(_handle, ref buf);
return Task.FromResult(ret == 0);
}
// ---- PMC ----
private (object? value, uint status) ReadPmc(FocasAddress address, FocasDataType type)
{
var addrType = FocasPmcAddrType.FromLetter(address.PmcLetter ?? "")
?? throw new InvalidOperationException($"Unknown PMC letter '{address.PmcLetter}'.");
var dataType = FocasPmcDataType.FromFocasDataType(type);
var length = PmcReadLength(type);
var buf = new FwlibNative.IODBPMC { Data = new byte[40] };
var ret = FwlibNative.PmcRdPmcRng(
_handle, addrType, dataType,
(ushort)address.Number, (ushort)address.Number, (ushort)length, ref buf);
if (ret != 0) return (null, FocasStatusMapper.MapFocasReturn(ret));
var value = type switch
{
FocasDataType.Bit => ExtractBit(buf.Data[0], address.BitIndex ?? 0),
FocasDataType.Byte => (object)(sbyte)buf.Data[0],
FocasDataType.Int16 => (object)BinaryPrimitives.ReadInt16LittleEndian(buf.Data),
FocasDataType.Int32 => (object)BinaryPrimitives.ReadInt32LittleEndian(buf.Data),
FocasDataType.Float32 => (object)BinaryPrimitives.ReadSingleLittleEndian(buf.Data),
FocasDataType.Float64 => (object)BinaryPrimitives.ReadDoubleLittleEndian(buf.Data),
_ => (object)buf.Data[0],
};
return (value, FocasStatusMapper.Good);
}
private uint WritePmc(FocasAddress address, FocasDataType type, object? value)
{
var addrType = FocasPmcAddrType.FromLetter(address.PmcLetter ?? "") ?? (short)0;
var dataType = FocasPmcDataType.FromFocasDataType(type);
var length = PmcWriteLength(type);
var buf = new FwlibNative.IODBPMC
{
TypeA = addrType,
TypeD = dataType,
DatanoS = (ushort)address.Number,
DatanoE = (ushort)address.Number,
Data = new byte[40],
};
EncodePmcValue(buf.Data, type, value, address.BitIndex);
var ret = FwlibNative.PmcWrPmcRng(_handle, (ushort)length, ref buf);
return ret == 0 ? FocasStatusMapper.Good : FocasStatusMapper.MapFocasReturn(ret);
}
private (object? value, uint status) ReadParameter(FocasAddress address, FocasDataType type)
{
var buf = new FwlibNative.IODBPSD { Data = new byte[32] };
var length = ParamReadLength(type);
var ret = FwlibNative.RdParam(_handle, (ushort)address.Number, axis: 0, (short)length, ref buf);
if (ret != 0) return (null, FocasStatusMapper.MapFocasReturn(ret));
var value = type switch
{
FocasDataType.Bit when address.BitIndex is int bit => ExtractBit(buf.Data[0], bit),
FocasDataType.Byte => (object)(sbyte)buf.Data[0],
FocasDataType.Int16 => (object)BinaryPrimitives.ReadInt16LittleEndian(buf.Data),
FocasDataType.Int32 => (object)BinaryPrimitives.ReadInt32LittleEndian(buf.Data),
_ => (object)BinaryPrimitives.ReadInt32LittleEndian(buf.Data),
};
return (value, FocasStatusMapper.Good);
}
private uint WriteParameter(FocasAddress address, FocasDataType type, object? value)
{
var buf = new FwlibNative.IODBPSD
{
Datano = (short)address.Number,
Type = 0,
Data = new byte[32],
};
var length = ParamReadLength(type);
EncodeParamValue(buf.Data, type, value);
var ret = FwlibNative.WrParam(_handle, (short)length, ref buf);
return ret == 0 ? FocasStatusMapper.Good : FocasStatusMapper.MapFocasReturn(ret);
}
private (object? value, uint status) ReadMacro(FocasAddress address)
{
var buf = new FwlibNative.ODBM();
var ret = FwlibNative.RdMacro(_handle, (short)address.Number, length: 8, ref buf);
if (ret != 0) return (null, FocasStatusMapper.MapFocasReturn(ret));
// Macro value = mcr_val / 10^dec_val. Convert to double so callers get the correct
// scaled value regardless of the decimal-point count the CNC reports.
var scaled = buf.McrVal / Math.Pow(10.0, buf.DecVal);
return (scaled, FocasStatusMapper.Good);
}
private uint WriteMacro(FocasAddress address, object? value)
{
// Write as integer + 0 decimal places — callers that need decimal precision can extend
// this via a future WriteMacroScaled overload. Consistent with what most HMIs do today.
var intValue = Convert.ToInt32(value);
var ret = FwlibNative.WrMacro(_handle, (short)address.Number, length: 8, intValue, decimalPointCount: 0);
return ret == 0 ? FocasStatusMapper.Good : FocasStatusMapper.MapFocasReturn(ret);
}
public void Dispose()
{
if (_connected)
{
try { FwlibNative.FreeLibHndl(_handle); } catch { }
_connected = false;
}
}
// ---- helpers ----
private static int PmcReadLength(FocasDataType type) => type switch
{
FocasDataType.Bit or FocasDataType.Byte => 8 + 1, // 8-byte header + 1 byte payload
FocasDataType.Int16 => 8 + 2,
FocasDataType.Int32 => 8 + 4,
FocasDataType.Float32 => 8 + 4,
FocasDataType.Float64 => 8 + 8,
_ => 8 + 1,
};
private static int PmcWriteLength(FocasDataType type) => PmcReadLength(type);
private static int ParamReadLength(FocasDataType type) => type switch
{
FocasDataType.Bit or FocasDataType.Byte => 4 + 1,
FocasDataType.Int16 => 4 + 2,
FocasDataType.Int32 => 4 + 4,
_ => 4 + 4,
};
private static bool ExtractBit(byte word, int bit) => (word & (1 << bit)) != 0;
internal static void EncodePmcValue(byte[] data, FocasDataType type, object? value, int? bitIndex)
{
switch (type)
{
case FocasDataType.Bit:
// PMC Bit writes with a non-null bitIndex go through WritePmcBitAsync's RMW path
// upstream. This branch only fires when a caller passes Bit with no bitIndex —
// treat the value as a whole-byte boolean (non-zero / zero).
data[0] = Convert.ToBoolean(value) ? (byte)1 : (byte)0;
break;
case FocasDataType.Byte:
data[0] = (byte)(sbyte)Convert.ToSByte(value);
break;
case FocasDataType.Int16:
BinaryPrimitives.WriteInt16LittleEndian(data, Convert.ToInt16(value));
break;
case FocasDataType.Int32:
BinaryPrimitives.WriteInt32LittleEndian(data, Convert.ToInt32(value));
break;
case FocasDataType.Float32:
BinaryPrimitives.WriteSingleLittleEndian(data, Convert.ToSingle(value));
break;
case FocasDataType.Float64:
BinaryPrimitives.WriteDoubleLittleEndian(data, Convert.ToDouble(value));
break;
default:
throw new NotSupportedException($"FocasDataType {type} not writable via PMC.");
}
_ = bitIndex; // bit-in-byte handled above
}
internal static void EncodeParamValue(byte[] data, FocasDataType type, object? value)
{
switch (type)
{
case FocasDataType.Byte:
data[0] = (byte)(sbyte)Convert.ToSByte(value);
break;
case FocasDataType.Int16:
BinaryPrimitives.WriteInt16LittleEndian(data, Convert.ToInt16(value));
break;
case FocasDataType.Int32:
BinaryPrimitives.WriteInt32LittleEndian(data, Convert.ToInt32(value));
break;
default:
BinaryPrimitives.WriteInt32LittleEndian(data, Convert.ToInt32(value));
break;
}
}
}
/// Default — produces a fresh per device.
public sealed class FwlibFocasClientFactory : IFocasClientFactory
{
public IFocasClient Create() => new FwlibFocasClient();
}