fix(abcip): browse program-scoped tags and surface symbol array dimensions
Live read-only check against production CompactLogix 5069-L320ERS2 presses: - The controller @tags listing names each program (Program:UN01_Press, a system symbol) but none of its tags; those are listed only by Program:<name>.@tags. The enumerator read @tags alone, so every program-scoped tag was unbrowseable — including the PackML state (Program:UN01_Press.Out_eState) and all 51 alarm bits the Ignition project reads. The enumerator now reads each program's listing and decodes it with that program as scope; an unreadable program listing is skipped, not fatal. - The Symbol Object decoder ignored the dimension bits + dim words, so every discovered array (DINT[32], REAL[32], STRING[50], UDT[23] on the two controllers checked) browsed as a scalar. IsArray/ElementCount are now populated (multi-dim flattened). A 1-D single element entry stays scalar: ab_server reports every scalar that way, a real controller sets no dimension bits on scalars. Pinned by entries captured off the controllers (CipSymbolObjectRealControllerCaptureTests, AbCipProgramScopeEnumerationTests).
This commit is contained in:
+85
@@ -0,0 +1,85 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Symbol Object (<c>@tags</c>) entries captured byte for byte off two real CompactLogix
|
||||
/// controllers on 2026-10-07 (read-only, one listing read each): a 5069-L320ERS2 rev 34.11 press
|
||||
/// and a second CompactLogix. Unlike the <c>ab_server</c> capture in
|
||||
/// <see cref="CipSymbolObjectWireCaptureTests"/> — which reports every scalar as a 1-element
|
||||
/// array — a real controller sets the dimension bits only on genuine arrays.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class CipSymbolObjectRealControllerCaptureTests
|
||||
{
|
||||
// Controller scope, press: WW_InCycle (BOOL), Inf_LookupFwkB (UDT[23], template 3386),
|
||||
// Program:UN01_Press (the program entry itself, type 0x1068).
|
||||
private const string WwInCycle = "AF010000C10001000000000000000000000000000A0057575F496E4379636C65";
|
||||
private const string InfLookupFwkB = "6D0000003AAD60001700000000000000000000000E00496E665F4C6F6F6B757046776B42";
|
||||
private const string ProgramUn01 = "7900000068100000000000000000000000000000120050726F6772616D3A554E30315F5072657373";
|
||||
|
||||
// Controller scope, second controller: arrSysTime (DINT[7]), arrMessages (STRING[50]).
|
||||
private const string ArrSysTime = "11000000C42004000700000000000000000000000A0061727253797354696D65";
|
||||
private const string ArrMessages = "61000000CEAF58003200000000000000000000000B006172724D65737361676573";
|
||||
|
||||
// Program:UN01_Press.@tags: names arrive WITHOUT the Program: prefix.
|
||||
private const string OutEState = "1A000000C40004000000000000000000000000000A004F75745F655374617465";
|
||||
private const string DisableStates = "4C000000C42004002000000000000000000000000E005F44697361626C65537461746573";
|
||||
|
||||
private static List<AbCipDiscoveredTag> Decode(params string[] entries) =>
|
||||
CipSymbolObjectDecoder.Decode(Convert.FromHexString(string.Concat(entries))).ToList();
|
||||
|
||||
/// <summary>Array dimensions in the entry surface as IsArray + ElementCount; scalars stay scalar.</summary>
|
||||
[Fact]
|
||||
public void Real_controller_arrays_surface_their_element_count()
|
||||
{
|
||||
var tags = Decode(WwInCycle, InfLookupFwkB, ArrSysTime, ArrMessages);
|
||||
|
||||
var inCycle = tags.Single(t => t.Name == "WW_InCycle");
|
||||
inCycle.IsArray.ShouldBeFalse();
|
||||
inCycle.ElementCount.ShouldBe(1);
|
||||
|
||||
var sysTime = tags.Single(t => t.Name == "arrSysTime");
|
||||
sysTime.DataType.ShouldBe(AbCipDataType.DInt);
|
||||
sysTime.IsArray.ShouldBeTrue();
|
||||
sysTime.ElementCount.ShouldBe(7);
|
||||
|
||||
var lookup = tags.Single(t => t.Name == "Inf_LookupFwkB");
|
||||
lookup.DataType.ShouldBe(AbCipDataType.Structure);
|
||||
lookup.TemplateInstanceId.ShouldBe(3386u);
|
||||
lookup.IsArray.ShouldBeTrue();
|
||||
lookup.ElementCount.ShouldBe(23);
|
||||
|
||||
var messages = tags.Single(t => t.Name == "arrMessages");
|
||||
messages.TemplateInstanceId.ShouldBe(0x0FCEu);
|
||||
messages.IsArray.ShouldBeTrue();
|
||||
messages.ElementCount.ShouldBe(50);
|
||||
}
|
||||
|
||||
/// <summary>A program entry is a system tag whose program name the enumerator can walk.</summary>
|
||||
[Fact]
|
||||
public void Program_entry_is_system_and_names_its_program()
|
||||
{
|
||||
var program = Decode(ProgramUn01).ShouldHaveSingleItem();
|
||||
|
||||
program.IsSystemTag.ShouldBeTrue();
|
||||
CipSymbolObjectDecoder.ProgramNameOf(program).ShouldBe("UN01_Press");
|
||||
CipSymbolObjectDecoder.ProgramNameOf(Decode(WwInCycle).Single()).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>A program listing decodes with the program as each tag's scope.</summary>
|
||||
[Fact]
|
||||
public void Program_listing_decodes_with_program_scope()
|
||||
{
|
||||
var tags = CipSymbolObjectDecoder.Decode(
|
||||
Convert.FromHexString(OutEState + DisableStates), programScope: "UN01_Press").ToList();
|
||||
|
||||
tags.Select(t => t.Name).ShouldBe(["Out_eState", "_DisableStates"]);
|
||||
tags.ShouldAllBe(t => t.ProgramScope == "UN01_Press");
|
||||
tags[0].DataType.ShouldBe(AbCipDataType.DInt);
|
||||
tags[1].IsArray.ShouldBeTrue();
|
||||
tags[1].ElementCount.ShouldBe(32);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user