feat(abcip): #611 discovered UDT member writes + whole-UDT refusal by decision
Item 2 — controller-discovered UDT members are writable on both transports once browse-committed and authored ReadWrite (writability is the operator's: the commit lands AccessLevel Read with no writable key). The write names the member (one CIP symbolic segment per member), its own type, its own bytes. BIT members are supported: Z-35162 (read-only probe) exposes a UDT BIT member as a 1-byte 0xC1 BOOL, so the controller applies the bit and the hidden host's other bits are never in the request. - AbCipDriver: pre-write width check on BOTH transports (libplctag patched part of its read buffer and sent it all): BadTypeMismatch, Bool over a wider value BadNotSupported; nothing sent, handle kept, health untouched, warn once. - A runtime NotSupportedException is a refusal: no longer latches Degraded. Item 3 — whole-UDT writes refused BadNotSupported before any runtime exists, by decision (a structure write overwrites hidden hosts + padding), documented. ab_server patch 0003: serve member paths from dotted flat tags (UdtSeg.*), and accept libplctag's pad byte on 1-byte writes (stock ab_server refused every libplctag BOOL/SINT write with 0x03). New AbCipUdtMemberWriteTests (both transports), AbCipDiscoveredMemberWriteTests, native request-byte theory, and a browse-commit mapper pin. Docs: AbCip.md Member writes / Whole-UDT writes / Known gaps, fixture docs, CLAUDE.md AB section. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -985,4 +985,41 @@ public sealed class RawBrowseCommitMapperTests
|
||||
refused.ShouldBe(expectRefused);
|
||||
blobRejected.ShouldBe(expectRefused);
|
||||
}
|
||||
|
||||
// ---- AB CIP discovered UDT members (Gitea #611) ----------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #611 — a controller-discovered UDT member leaf commits as an ordinary raw tag: its member path
|
||||
/// is the <c>tagPath</c>, the template's atomic type is the <c>dataType</c>, and it lands READ-ONLY with
|
||||
/// no <c>writable</c> key. That is the whole writability rule for discovered members: the driver writes a
|
||||
/// member only once the operator authors the raw tag <c>ReadWrite</c> (the node-manager gate), and the
|
||||
/// commit never pre-authors that — the browse's <c>SecurityClass</c> is not consulted.
|
||||
/// </summary>
|
||||
/// <param name="memberPath">The discovered member's full name (its Logix member path).</param>
|
||||
/// <param name="browseName">The member's browse name.</param>
|
||||
/// <param name="nativeType">The atomic type the template stated.</param>
|
||||
[Theory]
|
||||
[InlineData("_EmptyTemperatureSegment.RampTime", "RampTime", "Real")]
|
||||
[InlineData("_EmptyTemperatureSegment.Enable", "Enable", "Bool")]
|
||||
[InlineData("Program:Main.Motor1.Status.Code", "Code", "Int")]
|
||||
public void A_discovered_AbCip_UDT_member_commits_read_only_with_its_member_path_and_type(
|
||||
string memberPath, string browseName, string nativeType)
|
||||
{
|
||||
var row = RawBrowseCommitMapper.MapLeaf(
|
||||
driverType: DriverTypeNames.AbCip,
|
||||
fullName: memberPath,
|
||||
browseName: browseName,
|
||||
driverDataType: null,
|
||||
defaultDataType: "Int32",
|
||||
groupPrefix: null,
|
||||
folderPath: null,
|
||||
createGroups: false,
|
||||
addressFields: NativeTypeFields(nativeType));
|
||||
|
||||
row.Tag.AccessLevel.ShouldBe(TagAccessLevel.Read);
|
||||
var o = JsonNode.Parse(row.Tag.TagConfig)!.AsObject();
|
||||
o["tagPath"]!.GetValue<string>().ShouldBe(memberPath);
|
||||
o["dataType"]!.GetValue<string>().ShouldBe(nativeType);
|
||||
o.ContainsKey("writable").ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user