675 lines
31 KiB
C#
675 lines
31 KiB
C#
using System.Buffers.Binary;
|
|
using System.Collections.Concurrent;
|
|
|
|
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;
|
|
|
|
// 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<string, SemaphoreSlim> _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)),
|
|
FocasAreaKind.Diagnostic => Task.FromResult(
|
|
ReadDiagnostic(address.Number, address.BitIndex ?? 0, type)),
|
|
_ => Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadNotSupported)),
|
|
};
|
|
}
|
|
|
|
public Task<(object? value, uint status)> ReadDiagnosticAsync(
|
|
int diagNumber, int axisOrZero, FocasDataType type, CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<(object?, uint)>((null, FocasStatusMapper.BadCommunicationError));
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
return Task.FromResult(ReadDiagnostic(diagNumber, axisOrZero, type));
|
|
}
|
|
|
|
public async Task<uint> 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,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private async Task<uint> 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<int> GetPathCountAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult(1);
|
|
var buf = new FwlibNative.ODBPATH();
|
|
var ret = FwlibNative.RdPathNum(_handle, ref buf);
|
|
// EW_FUNC / EW_NOOPT on single-path controllers — fall back to 1 rather than failing.
|
|
if (ret != 0 || buf.MaxPath < 1) return Task.FromResult(1);
|
|
return Task.FromResult((int)buf.MaxPath);
|
|
}
|
|
|
|
public Task SetPathAsync(int pathId, CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.CompletedTask;
|
|
var ret = FwlibNative.SetPath(_handle, (short)pathId);
|
|
if (ret != 0)
|
|
throw new InvalidOperationException(
|
|
$"FWLIB cnc_setpath failed with EW_{ret} switching to path {pathId}.");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
public Task<FocasStatusInfo?> GetStatusAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasStatusInfo?>(null);
|
|
var buf = new FwlibNative.ODBST();
|
|
var ret = FwlibNative.StatInfo(_handle, ref buf);
|
|
if (ret != 0) return Task.FromResult<FocasStatusInfo?>(null);
|
|
return Task.FromResult<FocasStatusInfo?>(new FocasStatusInfo(
|
|
Dummy: buf.Dummy,
|
|
Tmmode: buf.TmMode,
|
|
Aut: buf.Aut,
|
|
Run: buf.Run,
|
|
Motion: buf.Motion,
|
|
Mstb: buf.Mstb,
|
|
EmergencyStop: buf.Emergency,
|
|
Alarm: buf.Alarm,
|
|
Edit: buf.Edit));
|
|
}
|
|
|
|
public Task<FocasProductionInfo?> GetProductionAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasProductionInfo?>(null);
|
|
if (!TryReadInt32Param(6711, out var produced) ||
|
|
!TryReadInt32Param(6712, out var required) ||
|
|
!TryReadInt32Param(6713, out var total))
|
|
{
|
|
return Task.FromResult<FocasProductionInfo?>(null);
|
|
}
|
|
// Cycle-time timer (type=2). Total seconds = minute*60 + msec/1000. Best-effort:
|
|
// a non-zero return leaves cycle-time at 0 rather than failing the whole snapshot
|
|
// — the parts counters are still useful even when cycle-time isn't supported.
|
|
var cycleSeconds = 0;
|
|
var tmrBuf = new FwlibNative.IODBTMR();
|
|
if (FwlibNative.RdTimer(_handle, type: 2, ref tmrBuf) == 0)
|
|
cycleSeconds = checked(tmrBuf.Minute * 60 + tmrBuf.Msec / 1000);
|
|
return Task.FromResult<FocasProductionInfo?>(new FocasProductionInfo(
|
|
PartsProduced: produced,
|
|
PartsRequired: required,
|
|
PartsTotal: total,
|
|
CycleTimeSeconds: cycleSeconds));
|
|
}
|
|
|
|
private bool TryReadInt32Param(ushort number, out int value)
|
|
{
|
|
var buf = new FwlibNative.IODBPSD { Data = new byte[32] };
|
|
var ret = FwlibNative.RdParam(_handle, number, axis: 0, length: 4 + 4, ref buf);
|
|
if (ret != 0) { value = 0; return false; }
|
|
value = BinaryPrimitives.ReadInt32LittleEndian(buf.Data);
|
|
return true;
|
|
}
|
|
|
|
private bool TryReadInt16Param(ushort number, out short value)
|
|
{
|
|
var buf = new FwlibNative.IODBPSD { Data = new byte[32] };
|
|
var ret = FwlibNative.RdParam(_handle, number, axis: 0, length: 4 + 2, ref buf);
|
|
if (ret != 0) { value = 0; return false; }
|
|
value = BinaryPrimitives.ReadInt16LittleEndian(buf.Data);
|
|
return true;
|
|
}
|
|
|
|
public Task<FocasModalInfo?> GetModalAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasModalInfo?>(null);
|
|
// type 100/101/102/103 = M/S/T/B (single auxiliary code, active modal block 0).
|
|
// Best-effort — if any single read fails we still surface the others as 0; the
|
|
// probe loop only updates the cache on a non-null return so a partial snapshot
|
|
// is preferable to throwing away every successful field.
|
|
return Task.FromResult<FocasModalInfo?>(new FocasModalInfo(
|
|
MCode: ReadModalAux(type: 100),
|
|
SCode: ReadModalAux(type: 101),
|
|
TCode: ReadModalAux(type: 102),
|
|
BCode: ReadModalAux(type: 103)));
|
|
}
|
|
|
|
private short ReadModalAux(short type)
|
|
{
|
|
var buf = new FwlibNative.ODBMDL { Data = new byte[8] };
|
|
var ret = FwlibNative.Modal(_handle, type, block: 0, ref buf);
|
|
if (ret != 0) return 0;
|
|
// For aux types (100..103) the union holds the code at offset 0 as a 2-byte
|
|
// value (<c>aux_data</c>). Reading as Int16 keeps the surface identical to the
|
|
// record contract; oversized values would have been truncated by FWLIB anyway.
|
|
return BinaryPrimitives.ReadInt16LittleEndian(buf.Data);
|
|
}
|
|
|
|
public Task<FocasOverrideInfo?> GetOverrideAsync(
|
|
FocasOverrideParameters parameters, CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasOverrideInfo?>(null);
|
|
// Each parameter is independently nullable — a null parameter number keeps the
|
|
// corresponding field at null + skips the wire call. A successful read on at
|
|
// least one parameter is enough to publish a snapshot; this matches the
|
|
// best-effort policy used by GetProductionAsync (issue #259).
|
|
var feed = TryReadOverride(parameters.FeedParam);
|
|
var rapid = TryReadOverride(parameters.RapidParam);
|
|
var spindle = TryReadOverride(parameters.SpindleParam);
|
|
var jog = TryReadOverride(parameters.JogParam);
|
|
return Task.FromResult<FocasOverrideInfo?>(new FocasOverrideInfo(feed, rapid, spindle, jog));
|
|
}
|
|
|
|
private short? TryReadOverride(ushort? param)
|
|
{
|
|
if (param is null) return null;
|
|
return TryReadInt16Param(param.Value, out var v) ? v : null;
|
|
}
|
|
|
|
public Task<FocasToolingInfo?> GetToolingAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasToolingInfo?>(null);
|
|
var buf = new FwlibNative.IODBTNUM();
|
|
var ret = FwlibNative.RdToolNumber(_handle, ref buf);
|
|
if (ret != 0) return Task.FromResult<FocasToolingInfo?>(null);
|
|
// FWLIB returns long; clamp to short for the surfaced Int16 (T-codes
|
|
// overflowing 32767 are vanishingly rare on Fanuc tool tables).
|
|
var t = buf.Data;
|
|
if (t > short.MaxValue) t = short.MaxValue;
|
|
else if (t < short.MinValue) t = short.MinValue;
|
|
return Task.FromResult<FocasToolingInfo?>(new FocasToolingInfo((short)t));
|
|
}
|
|
|
|
public Task<FocasWorkOffsetsInfo?> GetWorkOffsetsAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasWorkOffsetsInfo?>(null);
|
|
|
|
// 1..6 = G54..G59. Extended G54.1 P1..P48 use cnc_rdzofsr and are deferred.
|
|
// Pass axis=-1 so FWLIB fills every axis it has; we read the first 3 (X/Y/Z).
|
|
// Length = 4-byte header + 3 axes * 10-byte OFSB = 34. We request 4 + 8*10 = 84
|
|
// (the buffer ceiling) so a CNC with more axes still completes the call.
|
|
var slots = new List<FocasWorkOffset>(6);
|
|
string[] names = ["G54", "G55", "G56", "G57", "G58", "G59"];
|
|
for (short n = 1; n <= 6; n++)
|
|
{
|
|
var buf = new FwlibNative.IODBZOFS { Data = new byte[80] };
|
|
var ret = FwlibNative.RdWorkOffset(_handle, n, axis: -1, length: 4 + 8 * 10, ref buf);
|
|
if (ret != 0)
|
|
{
|
|
// Best-effort — a single-slot failure leaves the slot at 0.0; the cache
|
|
// still publishes so reads on the other offsets serve Good. The probe
|
|
// loop will retry on the next tick.
|
|
slots.Add(new FocasWorkOffset(names[n - 1], 0, 0, 0));
|
|
continue;
|
|
}
|
|
slots.Add(new FocasWorkOffset(
|
|
Name: names[n - 1],
|
|
X: DecodeOfsbAxis(buf.Data, axisIndex: 0),
|
|
Y: DecodeOfsbAxis(buf.Data, axisIndex: 1),
|
|
Z: DecodeOfsbAxis(buf.Data, axisIndex: 2)));
|
|
}
|
|
return Task.FromResult<FocasWorkOffsetsInfo?>(new FocasWorkOffsetsInfo(slots));
|
|
}
|
|
|
|
public Task<FocasOperatorMessagesInfo?> GetOperatorMessagesAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasOperatorMessagesInfo?>(null);
|
|
// type 0..3 = OPMSG / MACRO / EXTERN / REJ-EXT (issue #261). Single-slot read
|
|
// (length 4 + 256 = 260) returns the most-recent message in each class — best-
|
|
// effort: a single-class failure leaves that class out of the snapshot rather
|
|
// than failing the whole call, mirroring GetProductionAsync's policy.
|
|
var list = new List<FocasOperatorMessage>(4);
|
|
string[] classNames = ["OPMSG", "MACRO", "EXTERN", "REJ-EXT"];
|
|
for (short t = 0; t < 4; t++)
|
|
{
|
|
var buf = new FwlibNative.OPMSG3 { Data = new byte[256] };
|
|
var ret = FwlibNative.RdOpMsg3(_handle, t, length: 4 + 256, ref buf);
|
|
if (ret != 0) continue;
|
|
var text = TrimAnsiPadding(buf.Data);
|
|
if (string.IsNullOrEmpty(text)) continue;
|
|
list.Add(new FocasOperatorMessage(buf.Datano, classNames[t], text));
|
|
}
|
|
return Task.FromResult<FocasOperatorMessagesInfo?>(new FocasOperatorMessagesInfo(list));
|
|
}
|
|
|
|
public Task<FocasCurrentBlockInfo?> GetCurrentBlockAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<FocasCurrentBlockInfo?>(null);
|
|
var buf = new FwlibNative.ODBACTPT { Data = new byte[256] };
|
|
var ret = FwlibNative.RdActPt(_handle, ref buf);
|
|
if (ret != 0) return Task.FromResult<FocasCurrentBlockInfo?>(null);
|
|
return Task.FromResult<FocasCurrentBlockInfo?>(
|
|
new FocasCurrentBlockInfo(TrimAnsiPadding(buf.Data)));
|
|
}
|
|
|
|
public Task<IReadOnlyDictionary<string, int>?> GetFigureScalingAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<IReadOnlyDictionary<string, int>?>(null);
|
|
// kind=0 → position figures (absolute/relative/machine/distance share the same
|
|
// increment system per axis). cnc_rdaxisname is deferred — the wire impl keys
|
|
// by fallback "axis{n}" (1-based), the driver re-keys when it gains axis-name
|
|
// discovery in a follow-up. Issue #262, plan PR F1-f.
|
|
short count = 0;
|
|
var buf = new FwlibNative.IODBAXIS { Data = new byte[FwlibNative.MAX_AXIS * 8] };
|
|
var ret = FwlibNative.GetFigure(_handle, kind: 0, ref count, ref buf);
|
|
if (ret != 0) return Task.FromResult<IReadOnlyDictionary<string, int>?>(null);
|
|
return Task.FromResult<IReadOnlyDictionary<string, int>?>(DecodeFigureScaling(buf.Data, count));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode the per-axis decimal-place counts from a <c>cnc_getfigure</c> reply
|
|
/// buffer. Each axis entry per <c>fwlib32.h</c> is 8 bytes laid out as
|
|
/// <c>short dec</c> + <c>short unit</c> + 4 reserved bytes; we read only
|
|
/// <c>dec</c>. Keys are 1-based <c>"axis{n}"</c> placeholders — a follow-up
|
|
/// PR can rewire to <c>cnc_rdaxisname</c> once that surface lands without
|
|
/// changing the cache contract (issue #262).
|
|
/// </summary>
|
|
internal static IReadOnlyDictionary<string, int> DecodeFigureScaling(byte[] data, short count)
|
|
{
|
|
var clamped = Math.Max((short)0, Math.Min(count, (short)FwlibNative.MAX_AXIS));
|
|
var result = new Dictionary<string, int>(clamped, StringComparer.OrdinalIgnoreCase);
|
|
for (var i = 0; i < clamped; i++)
|
|
{
|
|
var offset = i * 8;
|
|
if (offset + 2 > data.Length) break;
|
|
var dec = BinaryPrimitives.ReadInt16LittleEndian(data.AsSpan(offset, 2));
|
|
if (dec < 0 || dec > 9) dec = 0;
|
|
result[$"axis{i + 1}"] = dec;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode + trim a Fanuc ANSI byte buffer. The CNC right-pads block text + opmsg
|
|
/// bodies with nulls or spaces; trim them so the round-trip through the OPC UA
|
|
/// address space stays stable (issue #261). Stops at the first NUL so any wire
|
|
/// buffer that gets reused doesn't leak old bytes.
|
|
/// </summary>
|
|
internal static string TrimAnsiPadding(byte[] data)
|
|
{
|
|
if (data is null) return string.Empty;
|
|
var len = 0;
|
|
for (; len < data.Length; len++)
|
|
if (data[len] == 0) break;
|
|
return System.Text.Encoding.ASCII.GetString(data, 0, len).TrimEnd(' ', '\0');
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode one OFSB axis block from a <c>cnc_rdzofs</c> data buffer. Each axis
|
|
/// occupies 10 bytes per <c>fwlib32.h</c>: <c>int data</c> + <c>short dec</c> +
|
|
/// <c>short unit</c> + <c>short disp</c>. The user-facing offset is
|
|
/// <c>data / 10^dec</c> — same convention as <c>cnc_rdmacro</c>.
|
|
/// </summary>
|
|
internal static double DecodeOfsbAxis(byte[] data, int axisIndex)
|
|
{
|
|
const int blockSize = 10;
|
|
var offset = axisIndex * blockSize;
|
|
if (offset + blockSize > data.Length) return 0;
|
|
var raw = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(offset, 4));
|
|
var dec = BinaryPrimitives.ReadInt16LittleEndian(data.AsSpan(offset + 4, 2));
|
|
if (dec < 0 || dec > 9) dec = 0;
|
|
return raw / Math.Pow(10.0, dec);
|
|
}
|
|
|
|
// ---- 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Range read for the PMC coalescer (issue #266). FWLIB's <c>pmc_rdpmcrng</c>
|
|
/// payload is capped at 40 bytes (the IODBPMC.Data union width), so requested
|
|
/// ranges larger than that are chunked into 32-byte sub-calls internally —
|
|
/// callers still see one logical range, which matches the
|
|
/// <see cref="Wire.FocasPmcCoalescer"/>'s "one wire call per group" semantics.
|
|
/// </summary>
|
|
public Task<(byte[]? buffer, uint status)> ReadPmcRangeAsync(
|
|
string letter, int pathId, int startByte, int byteCount, CancellationToken cancellationToken)
|
|
{
|
|
if (!_connected) return Task.FromResult<(byte[]?, uint)>((null, FocasStatusMapper.BadCommunicationError));
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
if (byteCount <= 0) return Task.FromResult<(byte[]?, uint)>((Array.Empty<byte>(), FocasStatusMapper.Good));
|
|
|
|
var addrType = FocasPmcAddrType.FromLetter(letter)
|
|
?? throw new InvalidOperationException($"Unknown PMC letter '{letter}'.");
|
|
var result = new byte[byteCount];
|
|
const int chunkBytes = 32;
|
|
var offset = 0;
|
|
while (offset < byteCount)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
var thisChunk = Math.Min(chunkBytes, byteCount - offset);
|
|
var buf = new FwlibNative.IODBPMC { Data = new byte[40] };
|
|
var ret = FwlibNative.PmcRdPmcRng(
|
|
_handle, addrType, FocasPmcDataType.Byte,
|
|
(ushort)(startByte + offset),
|
|
(ushort)(startByte + offset + thisChunk - 1),
|
|
(ushort)(8 + thisChunk), ref buf);
|
|
if (ret != 0) return Task.FromResult<(byte[]?, uint)>((null, FocasStatusMapper.MapFocasReturn(ret)));
|
|
Array.Copy(buf.Data, 0, result, offset, thisChunk);
|
|
offset += thisChunk;
|
|
}
|
|
return Task.FromResult<(byte[]?, uint)>((result, 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) ReadDiagnostic(int diagNumber, int axisOrZero, FocasDataType type)
|
|
{
|
|
var buf = new FwlibNative.IODBPSD { Data = new byte[32] };
|
|
var length = DiagnosticReadLength(type);
|
|
var ret = FwlibNative.RdDiag(_handle, (ushort)diagNumber, (short)axisOrZero, (short)length, ref buf);
|
|
if (ret != 0) return (null, FocasStatusMapper.MapFocasReturn(ret));
|
|
|
|
var value = type switch
|
|
{
|
|
FocasDataType.Bit => (object)ExtractBit(buf.Data[0], 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)BinaryPrimitives.ReadInt32LittleEndian(buf.Data),
|
|
};
|
|
return (value, FocasStatusMapper.Good);
|
|
}
|
|
|
|
private static int DiagnosticReadLength(FocasDataType type) => type switch
|
|
{
|
|
FocasDataType.Bit or FocasDataType.Byte => 4 + 1,
|
|
FocasDataType.Int16 => 4 + 2,
|
|
FocasDataType.Int32 => 4 + 4,
|
|
FocasDataType.Float32 => 4 + 4,
|
|
FocasDataType.Float64 => 4 + 8,
|
|
_ => 4 + 4,
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Default <see cref="IFocasClientFactory"/> — produces a fresh <see cref="FwlibFocasClient"/> per device.</summary>
|
|
public sealed class FwlibFocasClientFactory : IFocasClientFactory
|
|
{
|
|
public IFocasClient Create() => new FwlibFocasClient();
|
|
}
|