probe: aaAlarmManagedClient receives no alarm data — full consumer chain verified
Sixth probe iteration with every consumer-side knob exhausted: - Subscriptions tried (all rc=0): \Galaxy!, \Galaxy!*, \Galaxy!, \Galaxy!TestArea, \.\Galaxy!. - Read channels polled at 500ms: GetStatistics, GetHighPriAlarm, SFCreateSnapshot + SFGetStatistics. - Filters: priority 0..32767, qtSummary + qtHistory both tried, asAlarmActiveNow. - AlarmRecord pre-init to FILETIME epoch to dodge marshaler bug on default(DateTime). Result: every read API returns empty for the entire 60s window even with TestMachine_001.TestAlarm001 firing every 10s and aaObjectViewer confirming InAlarm transitions. The aaAlarmManagedClient.AlarmClient is not the receive surface AVEVA's alarm pipeline routes to in this Galaxy configuration. The consumer chain is verified working end-to-end: Initialize + Register + Subscribe all succeed, GetProviders finds the provider, the WM 0xC275 heartbeat fires at 1Hz to AVEVA's internal hwnd. There is simply no alarm data flowing through this consumer surface. Next investigation is not consumer-side: either find the SDK aaObjectViewer's alarm panel uses, or query the historian event storage directly. If alarms only flow via the historian path on this customer's Galaxy, the worker's PR A.5 architecture is a dead-end and A.2 needs a different transport. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,17 @@ namespace MxGateway.Worker.Tests;
|
||||
public sealed class AlarmClientWmProbeTests : IDisposable
|
||||
{
|
||||
// Probe configuration. Override in the constructor below if needed.
|
||||
// Try multiple subscription expressions sequentially (each Subscribe call
|
||||
// adds to the consumer's scope). The "everything" form varies by AVEVA
|
||||
// version — we shotgun common forms.
|
||||
private static readonly string[] SubscriptionExpressions =
|
||||
{
|
||||
@"\Galaxy!", // documented "all groups under Galaxy provider"
|
||||
@"\Galaxy!*", // wildcard variant
|
||||
@"\\Galaxy!", // double-backslash UNC-style
|
||||
@"\Galaxy!TestArea", // explicit area where TestMachine_001 lives
|
||||
@"\\.\Galaxy!", // local-host prefix
|
||||
};
|
||||
private const string SubscriptionExpression = @"\Galaxy!";
|
||||
private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(60);
|
||||
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500);
|
||||
@@ -281,16 +292,28 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
||||
// literally mean "match alarms in state 'none'" (i.e., nothing),
|
||||
// since the eAlarmFilterState enum is 0/1/2/3 single-states not
|
||||
// flag bits. Try ActiveNow explicitly.
|
||||
int subscribe = client.Subscribe(
|
||||
szSubscription: SubscriptionExpression,
|
||||
wFromPri: 0, wToPri: short.MaxValue,
|
||||
QueryType: eQueryType.qtHistory,
|
||||
SortFlags: eSortFlags.sfReturnNewestFirst,
|
||||
FilterMask: eAlarmFilterState.asAlarmActiveNow,
|
||||
FilterSpecification: eAlarmFilterState.asAlarmActiveNow);
|
||||
Log($"Subscribe('{SubscriptionExpression}', qtHistory, state=ActiveNow, pri=[0..32767]) -> {subscribe}");
|
||||
// Subscribe to every candidate expression — AVEVA accepts multiple
|
||||
// overlapping subscriptions; whichever matches the producer wins.
|
||||
foreach (string expr in SubscriptionExpressions)
|
||||
{
|
||||
try
|
||||
{
|
||||
int subscribe = client.Subscribe(
|
||||
szSubscription: expr,
|
||||
wFromPri: 0, wToPri: short.MaxValue,
|
||||
QueryType: eQueryType.qtSummary,
|
||||
SortFlags: eSortFlags.sfReturnNewestFirst,
|
||||
FilterMask: eAlarmFilterState.asAlarmActiveNow,
|
||||
FilterSpecification: eAlarmFilterState.asAlarmActiveNow);
|
||||
Log($"Subscribe('{expr}') -> {subscribe}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Subscribe('{expr}') threw: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
LogProviders(client, "after Subscribe");
|
||||
LogProviders(client, "after Subscribe-multi");
|
||||
|
||||
// 3c. Pump for the configured duration. Log every message we see
|
||||
// (filtered light to avoid noise from WM_PAINT / WM_TIMER /
|
||||
@@ -322,6 +345,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
||||
{
|
||||
PollGetStatistics(client, ++pollCount);
|
||||
LogProviders(client, $"poll #{pollCount}");
|
||||
PollAllChannels(client, pollCount);
|
||||
nextPoll = DateTime.UtcNow + PollInterval;
|
||||
}
|
||||
Thread.Sleep(10);
|
||||
@@ -349,6 +373,122 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
||||
|
||||
private string lastStatsSummary = string.Empty;
|
||||
private string lastProvidersSummary = string.Empty;
|
||||
private string lastHighPriSummary = string.Empty;
|
||||
private string lastSfStatsSummary = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Try every read API the AlarmClient exposes and log when its
|
||||
/// output changes. AlarmClient has at least three distinct read
|
||||
/// surfaces — GetStatistics (current-change array), GetHighPriAlarm
|
||||
/// (single-record peek), and the SF (stored filter) family — and any
|
||||
/// of them might be the populated one.
|
||||
/// </summary>
|
||||
private static AlarmRecord NewAlarmRecord()
|
||||
{
|
||||
// The interop's auto-marshal flips DateTime fields to FILETIME on
|
||||
// the way IN as well as OUT. default(DateTime) (year 1) is outside
|
||||
// FILETIME's representable range, so initialize all DateTime fields
|
||||
// to the FILETIME epoch (1601-01-01 UTC) to satisfy the marshaler.
|
||||
AlarmRecord rec = new AlarmRecord();
|
||||
DateTime epoch = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
foreach (var f in typeof(AlarmRecord).GetFields(
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic))
|
||||
{
|
||||
if (f.FieldType == typeof(DateTime))
|
||||
{
|
||||
object boxed = rec;
|
||||
f.SetValue(boxed, epoch);
|
||||
rec = (AlarmRecord)boxed;
|
||||
}
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
private void PollAllChannels(AlarmClient client, int seq)
|
||||
{
|
||||
// Channel A: GetHighPriAlarm — direct peek of highest-priority alarm.
|
||||
try
|
||||
{
|
||||
AlarmRecord rec = NewAlarmRecord();
|
||||
int rc = client.GetHighPriAlarm(ref rec);
|
||||
string desc = rc == 0 ? DescribeAlarmRecord(rec) : "<no record>";
|
||||
string summary = $"rc={rc} {desc}";
|
||||
if (summary != lastHighPriSummary)
|
||||
{
|
||||
Log($"GetHighPriAlarm #{seq}: {summary} (changed)");
|
||||
lastHighPriSummary = summary;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string es = $"{ex.GetType().Name}: {ex.Message}";
|
||||
if (es != lastHighPriSummary)
|
||||
{
|
||||
Log($"GetHighPriAlarm #{seq}: threw {es}");
|
||||
lastHighPriSummary = es;
|
||||
}
|
||||
}
|
||||
|
||||
// Channel C: GetAlarmExtendedRec by index. Try indices 0..3 directly;
|
||||
// populated alarms (if any) appear at low indices.
|
||||
for (int idx = 0; idx <= 2; idx++)
|
||||
{
|
||||
try
|
||||
{
|
||||
AlarmRecord rec = NewAlarmRecord();
|
||||
int rc = client.GetAlarmExtendedRec(idx, ref rec);
|
||||
if (rc == 0)
|
||||
{
|
||||
string desc = DescribeAlarmRecord(rec);
|
||||
Log($"GetAlarmExtendedRec(idx={idx}) #{seq}: rc=0 -> {desc}");
|
||||
break; // log first present record only
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (idx == 0)
|
||||
{
|
||||
Log($"GetAlarmExtendedRec(idx=0) #{seq}: threw {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Channel B: SF — snapshot + GetStatistics + iterate.
|
||||
try
|
||||
{
|
||||
uint numAlarms = 0;
|
||||
int sfCreate = client.SFCreateSnapshot(0, ref numAlarms);
|
||||
int unackRet = 0, unackAlm = 0, ackAlm = 0, others = 0, events = 0, idxNewest = 0;
|
||||
int sfStats = client.SFGetStatistics(
|
||||
ref unackRet, ref unackAlm, ref ackAlm,
|
||||
ref others, ref events, ref idxNewest);
|
||||
string summary = $"SFCreate={sfCreate} numAlarms={numAlarms} " +
|
||||
$"SFStats={sfStats} unackRet={unackRet} unackAlm={unackAlm} " +
|
||||
$"ackAlm={ackAlm} others={others} events={events} idxNewest={idxNewest}";
|
||||
if (summary != lastSfStatsSummary)
|
||||
{
|
||||
Log($"SF channel #{seq}: {summary} (changed)");
|
||||
lastSfStatsSummary = summary;
|
||||
|
||||
// If non-zero, fetch the first record by index via the
|
||||
// standard GetAlarmExtendedRec — after SFCreateSnapshot the
|
||||
// indices reference the snapshot.
|
||||
if (numAlarms > 0)
|
||||
{
|
||||
AlarmRecord rec = new AlarmRecord();
|
||||
int recRc = client.GetAlarmExtendedRec(0, ref rec);
|
||||
Log($" GetAlarmExtendedRec(0) [post-snapshot] rc={recRc} -> {DescribeAlarmRecord(rec)}");
|
||||
}
|
||||
}
|
||||
client.SFDeleteSnapshot();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"SF channel #{seq}: threw {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void LogProviders(AlarmClient client, string when)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user