6b3da93151
A browse-committed AB CIP / TwinCAT / FOCAS tag bound with its model's DEFAULT
data type, not the one the device reported. A browsed REAL committed as
`{"tagPath":"Pump_1_Speed","dataType":"DInt"}` — and those three drivers decode
the wire bytes by that field, not by the raw row's OPC UA DataType — so the tag
read four bytes of a float through an integer decoder and published a plausible
wrong number with no error raised anywhere. FOCAS was missed when #536 was filed.
The type is CARRIED, not derived. Reverse-mapping the leaf's driver-agnostic
DriverDataType looks like the smaller fix and is wrong: SInt/Int/DInt/USInt/UInt
all collapse into Int32 on the way out of the driver, so that version would bind
a 16-bit INT as a 32-bit DINT while appearing to fix the REAL. Drivers now state
their own type name on DriverAttributeInfo.NativeDataType, which reaches the
picker as AttributeInfo.AddressFields["dataType"] and the commit mapper from
there. A leaf that states nothing keeps the model default — the pre-#536
behaviour, kept deliberately as the fallback rather than a guess.
Numeric strings are refused before parsing: Enum.TryParse accepts "9" for any
enum and Enum.IsDefined then confirms it, so an unguarded parse would bind a tag
to a type nobody named. A test caught this in the first version of the guard.
--- and the other half, which the fixture was hiding ---
Test Connect on a device with NO authored tags — every AB CIP device at the
moment an operator first presses the button — reported "CIP handshake failed"
against a healthy controller. That was written down as a fixture limitation
("ab_server doesn't implement @raw_cpu_type"). Both halves of that were wrong:
* @raw_cpu_type is not a libplctag special tag at all (only @raw, @tags and
@udt/ are), so it goes out as an ordinary symbolic name that no controller
defines, and the controller's "tag does not exist" IS the successful answer.
* ab_server answered an unresolvable tag path with CIP 0x03 where a Logix
controller answers 0x04 — ErrorBadParam vs ErrorNotFound, which the probe
classifies oppositely. Fixed in vendored patch 0002.
* With the fixture then telling the truth, the real defect surfaced:
AbCipDriverProbe.IsReachableException matched the native C status spellings
("STATUS_NOT_FOUND"), but libplctag.NET throws with the MANAGED enum name
("ErrorNotFound"). Those substrings can never match, so the reachable
classification could not fire on the exception path — which is the path every
no-authored-tags probe takes. This was failing on real hardware too, not just
against the sim. It now recovers the Status and defers to IsReachableStatus,
so there is one classification rather than two that can drift.
Rejected: switching the fallback to @tags. libplctag builds a tag-listing tag
with NO network I/O, so it reports OK against any TCP listener — measured green
against a Mosquitto broker.
Verification: full solution suite green. Falsification runs confirm exactly the
intended tests go red — 9 for the type carry, 3 (the reachable statuses only) for
the probe classification. Live on docker-dev: browse-committed TestString/
Motor_Run/TestSINT now store String/Bool/SInt, alongside last session's
Pump_1_Speed still holding the wrong DInt. TestSINT is the falsifier — its row
DataType is Int32, identical to TestDINT's, so no reverse map could have produced
"SInt".
Known consequence: pre-existing browse-committed tags keep their wrong dataType;
the fix applies to new commits only.
Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
90 lines
4.7 KiB
C#
90 lines
4.7 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
/// <summary>
|
|
/// Driver-agnostic per-attribute (tag) descriptor used by the generic node-manager
|
|
/// to build OPC UA address-space variables. Every driver maps its native attribute
|
|
/// metadata into this DTO during discovery.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Per <c>docs/v2/plan.md</c> §5a (LmxNodeManager reusability) — <c>DriverAttributeInfo</c>
|
|
/// replaces the v1 Galaxy-specific <c>GalaxyAttributeInfo</c> in the generic node-manager
|
|
/// so the same node-manager class works against every driver.
|
|
/// </remarks>
|
|
/// <param name="FullName">
|
|
/// Driver-side full reference for read/write addressing
|
|
/// (e.g. for Galaxy: <c>"DelmiaReceiver_001.DownloadPath"</c>).
|
|
/// </param>
|
|
/// <param name="DriverDataType">Driver-agnostic data type; maps to OPC UA built-in type at build time.</param>
|
|
/// <param name="IsArray">True when this attribute is a 1-D array.</param>
|
|
/// <param name="ArrayDim">Declared array length when <see cref="IsArray"/> is true; null otherwise.</param>
|
|
/// <param name="SecurityClass">Write-authorization tier for this attribute.</param>
|
|
/// <param name="IsHistorized">True when this attribute is expected to feed historian / HistoryRead.</param>
|
|
/// <param name="IsAlarm">
|
|
/// True when this attribute represents an alarm condition (Galaxy: has an
|
|
/// <c>AlarmExtension</c> primitive). The generic node-manager enriches the variable with an
|
|
/// OPC UA <c>AlarmConditionState</c> when true. Defaults to false so existing non-Galaxy
|
|
/// drivers aren't forced to flow a flag they don't produce.
|
|
/// </param>
|
|
/// <param name="WriteIdempotent">
|
|
/// True when a timed-out or failed write to this attribute is safe to replay. Per
|
|
/// <c>docs/v2/plan.md</c>, writes are NOT auto-retried by default
|
|
/// because replaying a pulse / alarm-ack / counter-increment / recipe-step advance can
|
|
/// duplicate field actions. Drivers flag only tags whose semantics make retry safe
|
|
/// (holding registers with level-set values, set-point writes to analog tags) — the
|
|
/// capability invoker respects this flag when deciding whether to apply Polly retry.
|
|
/// </param>
|
|
/// <param name="Source">
|
|
/// Discriminates which runtime subsystem owns this node's dispatch.
|
|
/// Defaults to <see cref="NodeSourceKind.Driver"/> so existing callers are unchanged.
|
|
/// </param>
|
|
/// <param name="VirtualTagId">
|
|
/// Set when <paramref name="Source"/> is <see cref="NodeSourceKind.Virtual"/> — stable
|
|
/// logical id the VirtualTagEngine addresses by. Null otherwise.
|
|
/// </param>
|
|
/// <param name="ScriptedAlarmId">
|
|
/// Set when <paramref name="Source"/> is <see cref="NodeSourceKind.ScriptedAlarm"/> —
|
|
/// stable logical id the ScriptedAlarmEngine addresses by. Null otherwise.
|
|
/// </param>
|
|
/// <param name="NativeDataType">
|
|
/// The driver's OWN type name for this attribute (e.g. <c>"Real"</c> for an
|
|
/// <c>AbCipDataType</c>, <c>"Int16"</c> for a <c>TwinCATDataType</c>) when the driver binds
|
|
/// by a driver-specific type enum in its <c>TagConfig</c>; null for a driver whose tag config
|
|
/// carries no type (OPC UA Client, Galaxy) or which takes the type from the wire (MQTT
|
|
/// Sparkplug births, MTConnect probe inference).
|
|
/// <para>
|
|
/// <b>It exists because <paramref name="DriverDataType"/> is lossy in exactly the direction a
|
|
/// browse-commit needs</b> (Gitea #536): <c>AbCipDataType.SInt</c>, <c>.Int</c> and <c>.DInt</c>
|
|
/// all map to <see cref="DriverDataType.Int32"/>, so a commit that reverse-mapped the
|
|
/// driver-agnostic type would bind a 16-bit INT as a 32-bit DINT. Nothing downstream of the
|
|
/// address-space build reads this — it is authoring metadata carried alongside the type the
|
|
/// node manager actually uses.
|
|
/// </para>
|
|
/// </param>
|
|
public sealed record DriverAttributeInfo(
|
|
string FullName,
|
|
DriverDataType DriverDataType,
|
|
bool IsArray,
|
|
uint? ArrayDim,
|
|
SecurityClassification SecurityClass,
|
|
bool IsHistorized,
|
|
bool IsAlarm = false,
|
|
bool WriteIdempotent = false,
|
|
NodeSourceKind Source = NodeSourceKind.Driver,
|
|
string? VirtualTagId = null,
|
|
string? ScriptedAlarmId = null,
|
|
string? NativeDataType = null);
|
|
|
|
/// <summary>
|
|
/// Discriminates which runtime subsystem owns this node's Read/Write/
|
|
/// Subscribe dispatch. <c>Driver</c> = a real IDriver capability surface;
|
|
/// <c>Virtual</c> = a Phase 7 <see cref="DriverAttributeInfo"/>.VirtualTagId'd tag
|
|
/// computed by the VirtualTagEngine; <c>ScriptedAlarm</c> = a scripted Part 9 alarm
|
|
/// materialized by the ScriptedAlarmEngine.
|
|
/// </summary>
|
|
public enum NodeSourceKind
|
|
{
|
|
Driver = 0,
|
|
Virtual = 1,
|
|
ScriptedAlarm = 2,
|
|
}
|