270 lines
11 KiB
C#
270 lines
11 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
|
|
|
/// <summary>
|
|
/// <see cref="IFocasClient"/> implementation backed by Fanuc's licensed
|
|
/// <c>Fwlib32.dll</c> via <see cref="FwlibNative"/> P/Invoke. The DLL is NOT shipped with
|
|
/// OtOpcUa; the deployment places it next to the server executable or on <c>PATH</c>
|
|
/// (per Fanuc licensing — see <c>docs/v2/focas-deployment.md</c>).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Construction is licence-safe — .NET P/Invoke is lazy, so instantiating this class
|
|
/// does NOT load <c>Fwlib32.dll</c>. The DLL only loads on the first wire call (Connect /
|
|
/// Read / Write / Probe). When missing, those calls throw <see cref="DllNotFoundException"/>
|
|
/// which the driver surfaces as <c>BadCommunicationError</c> through the normal exception
|
|
/// mapping.</para>
|
|
///
|
|
/// <para>Session-scoped handle — <c>cnc_allclibhndl3</c> opens one FWLIB handle per CNC;
|
|
/// all PMC / parameter / macro reads on that device go through the same handle. Dispose
|
|
/// calls <c>cnc_freelibhndl</c>.</para>
|
|
/// </remarks>
|
|
internal sealed class FwlibFocasClient : IFocasClient
|
|
{
|
|
private ushort _handle;
|
|
private bool _connected;
|
|
|
|
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 Task<uint> WriteAsync(
|
|
FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult(FocasStatusMapper.BadCommunicationError);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
return address.Kind switch
|
|
{
|
|
FocasAreaKind.Pmc => Task.FromResult(WritePmc(address, type, value)),
|
|
FocasAreaKind.Parameter => Task.FromResult(WriteParameter(address, type, value)),
|
|
FocasAreaKind.Macro => Task.FromResult(WriteMacro(address, value)),
|
|
_ => Task.FromResult(FocasStatusMapper.BadNotSupported),
|
|
};
|
|
}
|
|
|
|
public Task<bool> 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:
|
|
// Bit-in-byte write is a read-modify-write at the PMC level — the underlying
|
|
// pmc_wrpmcrng takes a byte payload, so caller must set the bit on a byte they
|
|
// just read. This path is flagged for the follow-up RMW work in task #181.
|
|
throw new NotSupportedException(
|
|
"FOCAS Bit writes require read-modify-write; tracked in task #181.");
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Default <see cref="IFocasClientFactory"/> — produces a fresh <see cref="FwlibFocasClient"/> per device.</summary>
|
|
public sealed class FwlibFocasClientFactory : IFocasClientFactory
|
|
{
|
|
public IFocasClient Create() => new FwlibFocasClient();
|
|
}
|