fix(twincat): #645 live gate — sum-read never worked on hardware; TC2 diagnostics fallback
Read-only live gate on Elma presses Z35474 (TC3 3.1.4024) and Z34834 (TC2 2.11): - Sum-read: every chunk came back BadCommunicationError. Beckhoff's SumRead(IList<IgIoSumReadEntity>) ctor casts to IList<SumDataEntity>, which throws InvalidCastException for a List<> (only an array survives via covariance). The fake-client tests never built a real SumRead. New AdsTwinCATClient.CreateSumRead passes an array; TwinCATSumReadConstructionTests pins it (red on the List form) plus a falsifiability control on the library. After the fix: 59 tags, status identical everywhere, values identical on every non-moving tag, ~0.35-0.66 s vs ~2.7 s per poll; TC2 CE 2.11 answers sum reads. - An unexpected sum chunk failure now logs one Warning per device (it was invisible: the health message was overwritten by the next good read). - Diagnostics: TC2 has no TwinCAT_SystemInfoVarList; fall back to .SYSTEMINFO / .SYSTEMTASKINFOARR[1] once the TC3 name answers not-found (live: all six keys). - #612 browse: Flat verified on TC3 (10,770 leaves; VirtualTree 1,459 strict subset) — comment and docs updated, no load-mode option needed. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -96,6 +96,10 @@ public sealed class TwinCATDiagnosticsTests
|
||||
client.ReadStatuses[TwinCATDriver.SymCycleTime] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
client.ReadStatuses[TwinCATDriver.SymLastExecTime] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
client.ReadStatuses[TwinCATDriver.SymOnlineChangeCnt] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
// ...and the TwinCAT 2 equivalents are absent too.
|
||||
client.ReadStatuses[TwinCATDriver.SymTc2CycleTime] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
client.ReadStatuses[TwinCATDriver.SymTc2LastExecTime] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
client.ReadStatuses[TwinCATDriver.SymTc2OnlineChangeCount] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
|
||||
await Sample(drv, client, Host1);
|
||||
var readsAfterFirst = client.SingleReadCount;
|
||||
@@ -109,6 +113,49 @@ public sealed class TwinCATDiagnosticsTests
|
||||
client.SingleReadCount.ShouldBe(readsAfterFirst + 1); // only AppName is read again
|
||||
}
|
||||
|
||||
/// <summary>hw2 live gate (TC2 2.11 Elma Z34834): a TwinCAT 2 runtime has no TwinCAT_SystemInfoVarList but
|
||||
/// serves the same facts as `.SYSTEMINFO` / `.SYSTEMTASKINFOARR[1]`; the bag must be filled from those, and
|
||||
/// after the first tick only the TC2 names are read (4 reads, not 8).</summary>
|
||||
[Fact]
|
||||
public async Task TwinCAT2_system_globals_fill_the_bag_when_the_TC3_names_are_absent()
|
||||
{
|
||||
var drv = NewDriver(new FakeTwinCATClientFactory(), false, true, (Host1, null));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var client = new FakeTwinCATClient { AdsStateText = "Run" };
|
||||
foreach (var tc3 in new[] { TwinCATDriver.SymAppName, TwinCATDriver.SymOnlineChangeCnt, TwinCATDriver.SymCycleTime, TwinCATDriver.SymLastExecTime })
|
||||
client.ReadStatuses[tc3] = TwinCATStatusMapper.BadNodeIdUnknown;
|
||||
client.Values[TwinCATDriver.SymTc2ProjectName] = "ELMA_5STAGE";
|
||||
client.Values[TwinCATDriver.SymTc2OnlineChangeCount] = (ushort)3;
|
||||
client.Values[TwinCATDriver.SymTc2CycleTime] = 100_000u; // 10.0 ms
|
||||
client.Values[TwinCATDriver.SymTc2LastExecTime] = 40_000u; // 4.0 ms
|
||||
|
||||
await Sample(drv, client, Host1);
|
||||
var readsAfterFirst = client.SingleReadCount;
|
||||
await Sample(drv, client, Host1);
|
||||
|
||||
var bag = ((IDriverDiagnostics)drv).GetDiagnostics();
|
||||
bag["TwinCAT.AppName"].ShouldBe("ELMA_5STAGE");
|
||||
bag["TwinCAT.OnlineChangeCnt"].ShouldBe("3");
|
||||
bag["TwinCAT.CycleTimeMs"].ShouldBe("10.0");
|
||||
bag["TwinCAT.LastExecTimeMs"].ShouldBe("4.0");
|
||||
bag["TwinCAT.HeadroomMs"].ShouldBe("6.0");
|
||||
readsAfterFirst.ShouldBe(8);
|
||||
client.SingleReadCount.ShouldBe(readsAfterFirst + 4);
|
||||
}
|
||||
|
||||
/// <summary>A TC3 target that serves TwinCAT_SystemInfoVarList is never asked for the TC2 names.</summary>
|
||||
[Fact]
|
||||
public async Task A_TC3_target_never_reads_the_TwinCAT2_names()
|
||||
{
|
||||
var drv = NewDriver(new FakeTwinCATClientFactory(), false, true, (Host1, null));
|
||||
await drv.InitializeAsync("{}", CancellationToken.None);
|
||||
var client = PlcClient();
|
||||
|
||||
await Sample(drv, client, Host1);
|
||||
|
||||
client.SingleReadCount.ShouldBe(4);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a failed sample keeps the previous snapshot rather than blanking the bag.</summary>
|
||||
[Fact]
|
||||
public async Task Failed_sample_keeps_the_last_values()
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using Shouldly;
|
||||
using TwinCAT.Ads;
|
||||
using TwinCAT.Ads.SumCommand;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// hw2 live gate (2026-10-08, TC3 Z35474): every ADS sum-read chunk failed with
|
||||
/// <see cref="InvalidCastException"/> thrown by Beckhoff's own <see cref="SumRead"/> constructor, because the
|
||||
/// <c>IList<IgIoSumReadEntity></c> overload casts its argument to <c>IList<SumDataEntity></c> — which
|
||||
/// works for an array (covariance) and not for a <c>List<IgIoSumReadEntity></c>. The driver turned it into
|
||||
/// <c>BadCommunicationError</c> for every batched tag on every poll. The fake client never builds a real
|
||||
/// <see cref="SumRead"/>, so these tests construct one directly; the constructor does no I/O, so an unconnected
|
||||
/// <see cref="AdsClient"/> is enough.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class TwinCATSumReadConstructionTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateSumRead_builds_a_real_Beckhoff_sum_command_without_a_cast_failure()
|
||||
{
|
||||
using var client = new AdsClient();
|
||||
|
||||
var sum = AdsTwinCATClient.CreateSumRead(client, [(0x4040u, 0x10u, 2), (0x4040u, 0x20u, 4), (0x4020u, 0x0u, 81)]);
|
||||
|
||||
sum.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_Beckhoff_list_overload_really_throws_for_a_List_which_is_why_CreateSumRead_passes_an_array()
|
||||
{
|
||||
// Falsifiability control: if a future Beckhoff release fixes the cast, this test goes red and the remark
|
||||
// on CreateSumRead can be retired — but the array form keeps working either way.
|
||||
using var client = new AdsClient();
|
||||
var list = new List<IgIoSumReadEntity> { new(0x4040, 0x10, 2), new(0x4040, 0x20, 4) };
|
||||
|
||||
Should.Throw<InvalidCastException>(() => new SumRead(client, list, SumCommandErrorStrategy.Relaxed));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user