FOCAS PR 2 — IReadable + IWritable + real FwlibFocasClient P/Invoke. Closes task #193 early now that strangesast/fwlib provides the licensed DLL references. Skips shipping with the Unimplemented stub as the default — FwlibFocasClientFactory is now the production default, UnimplementedFocasClientFactory stays as an opt-in for tests/deployments without FWLIB access. FwlibNative — narrow P/Invoke surface for the 7 calls the driver actually makes: cnc_allclibhndl3 (open Ethernet handle), cnc_freelibhndl (close), pmc_rdpmcrng + pmc_wrpmcrng (PMC range I/O), cnc_rdparam + cnc_wrparam (CNC parameters), cnc_rdmacro + cnc_wrmacro (macro variables), cnc_statinfo (probe). DllImport targets Fwlib32.dll; deployment places it next to the executable or on PATH. IODBPMC/IODBPSD/ODBM/ODBST marshaled with LayoutKind.Sequential + Pack=1 + fixed byte-array unions (avoids LayoutKind.Explicit complexity; managed-side BitConverter extracts typed values from the byte buffer). Internal helpers FocasPmcAddrType.FromLetter (G=0/F=1/Y=2/X=3/A=4/R=5/T=6/K=7/C=8/D=9/E=10 per Fanuc FOCAS/2 spec) + FocasPmcDataType.FromFocasDataType (Byte=0 / Word=1 / Long=2 / Float=4 / Double=5) exposed for testing without the DLL loaded. FwlibFocasClient is the concrete IFocasClient backed by P/Invoke. Construction is licence-safe — .NET P/Invoke is lazy so instantiating the class does NOT load Fwlib32.dll; DLL loads on first wire call (Connect/Read/Write/Probe). When missing, calls throw DllNotFoundException which the driver surfaces as BadCommunicationError via the normal exception path. Session-scoped handle from cnc_allclibhndl3; Dispose calls cnc_freelibhndl. Dispatch on FocasAreaKind — Pmc reads use pmc_rdpmcrng with the right ADR_* + data-type codes + parses the union via BinaryPrimitives LittleEndian, Parameter reads use cnc_rdparam + IODBPSD, Macro reads use cnc_rdmacro + compute scaled double as McrVal / 10^DecVal. Write paths mirror reads. PMC Bit writes throw NotSupportedException pointing at task #181 (read-modify-write gap — same as Modbus / AbCip / AbLegacy / TwinCAT). Macro writes accept int + pass decimal-point count 0 (decimal precision writes are a future enhancement). Probe calls cnc_statinfo with ODBST result. Driver wiring — FocasDriver now IDriver + IReadable + IWritable. Per-device connection caching via EnsureConnectedAsync + DeviceState.Client. ReadAsync/WriteAsync dispatch through the injected IFocasClient — ordered snapshots preserve per-tag status, OperationCanceledException rethrows, FormatException/InvalidCastException → BadTypeMismatch, OverflowException → BadOutOfRange, NotSupportedException → BadNotSupported, anything else → BadCommunicationError + Degraded health. Connect-failure disposes the half-open client. ShutdownAsync disposes every cached client. Default factory switched — constructor now defaults to FwlibFocasClientFactory (backed by real Fwlib32.dll) rather than UnimplementedFocasClientFactory. UnimplementedFocasClientFactory stays as an opt-in. 41 new tests — 14 in FocasReadWriteTests (ordered unknown-ref handling, successful PMC/Parameter/Macro reads routing through correct FocasAreaKind, repeat-read reuses connection, FOCAS error mapping, exception paths, batched order across areas, non-writable rejection, successful write logging, status mapping, batch ordering, cancellation, shutdown disposes), 27 in FwlibNativeHelperTests (12 letter-mapping cases + 3 unknown rejections + 6 data-type mapping + 4 encode helpers + Bit-write NotSupported). Total FOCAS unit tests now 106/106 passing (+41 from PR 1's 65); full solution builds 0 errors; Modbus / AbCip / AbLegacy / TwinCAT / other drivers untouched. FOCAS driver is real-wire-capable from day one — deployment drops Fwlib32.dll beside the server + driver talks to live FS 0i/16i/18i/21i/30i/31i/32i controllers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the managed helpers inside FwlibNative + FwlibFocasClient that don't require the
|
||||
/// licensed Fwlib32.dll — letter→ADR_* mapping, FocasDataType→data-type mapping, byte encoding.
|
||||
/// The actual P/Invoke calls can only run where the DLL is present; field testing covers those.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class FwlibNativeHelperTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("G", 0)]
|
||||
[InlineData("F", 1)]
|
||||
[InlineData("Y", 2)]
|
||||
[InlineData("X", 3)]
|
||||
[InlineData("A", 4)]
|
||||
[InlineData("R", 5)]
|
||||
[InlineData("T", 6)]
|
||||
[InlineData("K", 7)]
|
||||
[InlineData("C", 8)]
|
||||
[InlineData("D", 9)]
|
||||
[InlineData("E", 10)]
|
||||
[InlineData("g", 0)] // case-insensitive
|
||||
public void PmcAddrType_maps_every_valid_letter(string letter, short expected)
|
||||
{
|
||||
FocasPmcAddrType.FromLetter(letter).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Z")]
|
||||
[InlineData("")]
|
||||
[InlineData("XX")]
|
||||
public void PmcAddrType_rejects_unknown_letters(string letter)
|
||||
{
|
||||
FocasPmcAddrType.FromLetter(letter).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(FocasDataType.Bit, 0)] // byte
|
||||
[InlineData(FocasDataType.Byte, 0)]
|
||||
[InlineData(FocasDataType.Int16, 1)] // word
|
||||
[InlineData(FocasDataType.Int32, 2)] // long
|
||||
[InlineData(FocasDataType.Float32, 4)]
|
||||
[InlineData(FocasDataType.Float64, 5)]
|
||||
public void PmcDataType_maps_FocasDataType_to_FOCAS_code(FocasDataType input, short expected)
|
||||
{
|
||||
FocasPmcDataType.FromFocasDataType(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodePmcValue_Byte_writes_signed_byte_at_offset_0()
|
||||
{
|
||||
var buf = new byte[40];
|
||||
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Byte, (sbyte)-5, bitIndex: null);
|
||||
((sbyte)buf[0]).ShouldBe((sbyte)-5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodePmcValue_Int16_writes_little_endian()
|
||||
{
|
||||
var buf = new byte[40];
|
||||
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Int16, (short)0x1234, bitIndex: null);
|
||||
buf[0].ShouldBe((byte)0x34);
|
||||
buf[1].ShouldBe((byte)0x12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodePmcValue_Int32_writes_little_endian()
|
||||
{
|
||||
var buf = new byte[40];
|
||||
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Int32, 0x12345678, bitIndex: null);
|
||||
buf[0].ShouldBe((byte)0x78);
|
||||
buf[1].ShouldBe((byte)0x56);
|
||||
buf[2].ShouldBe((byte)0x34);
|
||||
buf[3].ShouldBe((byte)0x12);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodePmcValue_Bit_throws_NotSupported_for_RMW_gap()
|
||||
{
|
||||
var buf = new byte[40];
|
||||
Should.Throw<NotSupportedException>(() =>
|
||||
FwlibFocasClient.EncodePmcValue(buf, FocasDataType.Bit, true, bitIndex: 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeParamValue_Int32_writes_little_endian()
|
||||
{
|
||||
var buf = new byte[32];
|
||||
FwlibFocasClient.EncodeParamValue(buf, FocasDataType.Int32, 0x0A0B0C0D);
|
||||
buf[0].ShouldBe((byte)0x0D);
|
||||
buf[1].ShouldBe((byte)0x0C);
|
||||
buf[2].ShouldBe((byte)0x0B);
|
||||
buf[3].ShouldBe((byte)0x0A);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user