AB CIP PR 3 — IReadable implementation against libplctag. Introduces IAbCipTagRuntime + IAbCipTagFactory abstraction matching the Modbus transport-factory pattern (ctor optional arg, default production impl injected) so the driver's read/status-mapping logic is unit-testable without a live PLC or the native libplctag binary. LibplctagTagRuntime is the default wire-backed implementation — wraps libplctag.Tag + translates our AbCipDataType enum into GetInt8/GetUInt8/GetInt16/GetUInt16/GetInt32/GetUInt32/GetInt64/GetUInt64/GetFloat32/GetFloat64/GetString/GetBit calls covering Bool (standalone + BOOL-in-DINT via .N bit selector), SInt/USInt, Int/UInt, DInt/UDInt, LInt/ULInt, Real, LReal, String, Dt (epoch DINT), with Structure deferred to PR 6. MapPlcType bridges our libplctag attribute strings (controllogix, compactlogix, micro800) to libplctag.PlcType enum; CompactLogix rolls under ControlLogix per libplctag's family grouping which matches the wire protocol reality. AbCipDriver now implements IReadable — ReadAsync iterates fullReferences preserving order, looks up each tag definition + its device, lazily materialises the tag runtime via EnsureTagRuntimeAsync on first touch (cached thereafter for the lifetime of the device), catches OperationCanceledException to honor cancellation, maps libplctag non-zero status via AbCipStatusMapper.MapLibplctagStatus, catches any other exception as BadCommunicationError. Health surface moves to Healthy on success + Degraded with the last error message on failure. Initialize-failure path disposes the half-created runtime before rethrowing so no native handles leak. DeviceState gains a Runtimes dict alongside the existing TagHandles collection; DisposeHandles walks both so ShutdownAsync + ReinitializeAsync cleanly destroy every native tag. 12 new unit tests in AbCipDriverReadTests using FakeAbCipTag / FakeAbCipTagFactory (test fake under tests/...AbCip.Tests/FakeAbCipTag.cs) covering unknown reference → BadNodeIdUnknown, unknown device → BadNodeIdUnknown, successful DInt read with correct Good status + captured value, lazy-init on first read with reuse across subsequent reads, non-zero libplctag status mapping via AbCipStatusMapper, exception during read surfacing as BadCommunicationError with health Degraded, batched reads preserving order + per-tag status, health Healthy after success, TagCreateParams composition from device + profile (gateway / port / CIP path / libplctag attribute / tag name wiring), cancellation propagation via OperationCanceledException, ShutdownAsync disposing every runtime, Initialize-failure disposing the aborted runtime. Total AbCip unit tests now 88/88 passing. Integration test project scaffolding — tests/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests with AbServerFixture (IAsyncLifetime that starts ab_server when the binary is on PATH, otherwise marks IsAvailable=false), AbServerFact attribute (Fact-equivalent that skips when ab_server is missing), one smoke test exercising DInt read end-to-end. Project runs cleanly — the single smoke test skips on boxes without ab_server (0 failed, 0 passed, 1 skipped) + runs on boxes with it. Follow-up work captured in comments — ab_server CI fixture (download prebuilt Windows x64 binary as GitHub release asset) + per-family JSON profiles + hand-rolled CIP stub for UDT fidelity ship in the PR 6/9-12 window. Solution file updated. Full solution builds 0 errors across all 28 projects. Modbus + other existing tests untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
63
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
Normal file
63
src/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
/// <summary>
|
||||
/// Thin wire-layer abstraction over a single CIP tag. The driver holds one instance per
|
||||
/// <c>(device, tag path)</c> pair; the default implementation delegates to
|
||||
/// <see cref="LibplctagTagRuntime"/>. Tests swap in a fake via
|
||||
/// <see cref="IAbCipTagFactory"/> so the driver's read / write / status-mapping logic can
|
||||
/// be exercised without a running PLC or the native libplctag binary.
|
||||
/// </summary>
|
||||
public interface IAbCipTagRuntime : IDisposable
|
||||
{
|
||||
/// <summary>Create the underlying native tag (equivalent to libplctag's <c>plc_tag_create</c>).</summary>
|
||||
Task InitializeAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Issue a read; on completion the local buffer holds the current PLC value.</summary>
|
||||
Task ReadAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Flush the local buffer to the PLC.</summary>
|
||||
Task WriteAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Raw libplctag status code — mapped to an OPC UA StatusCode via
|
||||
/// <see cref="AbCipStatusMapper.MapLibplctagStatus"/>. Zero on success, negative on error.
|
||||
/// </summary>
|
||||
int GetStatus();
|
||||
|
||||
/// <summary>
|
||||
/// Decode the local buffer into a boxed .NET value per the tag's configured type.
|
||||
/// <paramref name="bitIndex"/> is non-null only for BOOL-within-DINT tags captured in
|
||||
/// the <c>.N</c> syntax at parse time.
|
||||
/// </summary>
|
||||
object? DecodeValue(AbCipDataType type, int? bitIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Encode <paramref name="value"/> into the local buffer per the tag's type. Callers
|
||||
/// pair this with <see cref="WriteAsync"/>.
|
||||
/// </summary>
|
||||
void EncodeValue(AbCipDataType type, int? bitIndex, object? value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for per-tag runtime handles. Instantiated once per driver, consumed per
|
||||
/// <c>(device, tag path)</c> pair at the first read/write.
|
||||
/// </summary>
|
||||
public interface IAbCipTagFactory
|
||||
{
|
||||
IAbCipTagRuntime Create(AbCipTagCreateParams createParams);
|
||||
}
|
||||
|
||||
/// <summary>Everything libplctag needs to materialise a tag handle.</summary>
|
||||
/// <param name="Gateway">Gateway IP / hostname parsed from <see cref="AbCipHostAddress.Gateway"/>.</param>
|
||||
/// <param name="Port">EtherNet/IP TCP port — default 44818.</param>
|
||||
/// <param name="CipPath">CIP route path, e.g. <c>1,0</c>. Empty for Micro800.</param>
|
||||
/// <param name="LibplctagPlcAttribute">libplctag <c>plc=...</c> attribute, per family profile.</param>
|
||||
/// <param name="TagName">Logix symbolic tag name as emitted by <see cref="AbCipTagPath.ToLibplctagName"/>.</param>
|
||||
/// <param name="Timeout">libplctag operation timeout (applies to Initialize / Read / Write).</param>
|
||||
public sealed record AbCipTagCreateParams(
|
||||
string Gateway,
|
||||
int Port,
|
||||
string CipPath,
|
||||
string LibplctagPlcAttribute,
|
||||
string TagName,
|
||||
TimeSpan Timeout);
|
||||
Reference in New Issue
Block a user