probe: GetStatistics polling viable, Galaxy has no active alarms today
Extended AlarmClientWmProbeTests.ProbeAlarmClientWmMessages to also call GetStatistics every ~2s during the pump window. Re-ran on the dev rig 2026-05-01: - GetStatistics is safely callable from the same thread that did RegisterConsumer + Subscribe. Every poll (9 calls / 20s window) returned rc=0, no exceptions. - Galaxy currently has zero active alarms. total=0 active=0 suppressed=0 newAlarms=0 across every poll. positions[] and handles[] arrays were empty. - changes=1 codes=[7] was constant across all polls, matching the constant 1 Hz WM 0xC275 cadence — same heartbeat semantics exposed through both the WM path and the pull API. Confirms the polling design is mechanically viable: GetStatistics threading-affinity is fine and the call is cheap. The remaining unknown is whether GetStatistics populates positions[] / handles[] with real entries when an alarm actually fires. Proving that requires triggering an alarm — next probe is an MxAccess write to a $Alarm-extended boolean tag (reference pending). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,20 +157,48 @@ AVEVA's own message-pump thread — confirmable by extending the
|
|||||||
probe to fire `GetStatistics` on its own thread and check the
|
probe to fire `GetStatistics` on its own thread and check the
|
||||||
result.
|
result.
|
||||||
|
|
||||||
|
## GetStatistics polling — second probe run, 2026-05-01
|
||||||
|
|
||||||
|
Extended the probe to call `GetStatistics` every ~2s alongside the
|
||||||
|
WM logger. Key findings:
|
||||||
|
|
||||||
|
- **`GetStatistics` is safely callable from the same thread that
|
||||||
|
did `RegisterConsumer` + `Subscribe`.** Every poll returned rc=0
|
||||||
|
with no exceptions over 9 polls / 20s window.
|
||||||
|
- **The deployed Galaxy currently has zero active alarms.** Every
|
||||||
|
poll reported `total=0 active=0 suppressed=0 newAlarms=0`. The
|
||||||
|
`positions[]` and `handles[]` arrays were empty.
|
||||||
|
- **`changes=1 codes=[7]` was constant across all polls**, matching
|
||||||
|
the constant 1 Hz WM 0xC275 cadence. Code 7 is consistent with a
|
||||||
|
"heartbeat / subscription healthy" sentinel — same semantics as
|
||||||
|
the WM but reported through the pull-side API.
|
||||||
|
- `percent=100` (query-complete percentage) was constant — the
|
||||||
|
subscription is steady-state.
|
||||||
|
|
||||||
|
This confirms the polling design (option 1 in the previous section)
|
||||||
|
is mechanically viable. The remaining open question is whether
|
||||||
|
`GetStatistics` populates `positions[] / handles[]` with real
|
||||||
|
entries when an alarm transition actually fires — proving that
|
||||||
|
requires firing an alarm.
|
||||||
|
|
||||||
## Open follow-up probes
|
## Open follow-up probes
|
||||||
|
|
||||||
Each can be added to `AlarmClientWmProbeTests` as a separate
|
Each can be added to `AlarmClientWmProbeTests` as a separate
|
||||||
Skip-gated test:
|
Skip-gated test:
|
||||||
|
|
||||||
1. **Fire a real Galaxy alarm during the pump window.** Confirms
|
1. **Fire a real Galaxy alarm during the pump window.** The cleanest
|
||||||
whether the WM 0xC275 cadence changes (becomes per-change rather
|
programmatic trigger is an MxAccess write that flips a
|
||||||
than periodic) and whether `GetStatistics` returns a non-empty
|
`$Alarm`-extended boolean to true (alarm in) and back to false
|
||||||
`ChangeCodes / ChangePos / hAlarm` triple.
|
(alarm out). Pinning the exact tag reference is pending — needs
|
||||||
2. **Call `GetStatistics` on a different thread from the
|
either a documented test-fixture tag or an interactive selection
|
||||||
`RegisterConsumer` thread** to test threading affinity.
|
in System Platform IDE. Once the trigger fires, this resolves
|
||||||
3. **Hook AVEVA's internal window** to log what WMs it actually
|
whether AVEVA's pulled change set arrives via `GetStatistics`
|
||||||
processes (would resolve option 2 above).
|
`positions[] / handles[]` (per-change polling works) or only via
|
||||||
4. **Decompile `aaAlarmManagedClient.dll`'s IL** for the
|
the AVEVA-internal window (callback path needed).
|
||||||
|
2. **Hook AVEVA's internal window** to log what WMs it actually
|
||||||
|
processes — only relevant if probe 1 shows `GetStatistics` does
|
||||||
|
NOT report per-change activity.
|
||||||
|
3. **Decompile `aaAlarmManagedClient.dll`'s IL** for the
|
||||||
`RegisterConsumer` method to find what `RegisterWindowMessage`
|
`RegisterConsumer` method to find what `RegisterWindowMessage`
|
||||||
string is used and whether there's a callback-registration
|
string is used and whether there's a callback-registration
|
||||||
surface on `WNAL_Register` that the managed client wraps. The
|
surface on `WNAL_Register` that the managed client wraps. The
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
|||||||
this.output = output;
|
this.output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs")]
|
[Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs + GetStatistics polling results")]
|
||||||
public void ProbeAlarmClientWmMessages()
|
public void ProbeAlarmClientWmMessages()
|
||||||
{
|
{
|
||||||
// 1. Pre-resolve a few candidate RegisterWindowMessage strings so any
|
// 1. Pre-resolve a few candidate RegisterWindowMessage strings so any
|
||||||
@@ -241,8 +241,13 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
|||||||
|
|
||||||
// 3c. Pump for the configured duration. Log every message we see
|
// 3c. Pump for the configured duration. Log every message we see
|
||||||
// (filtered light to avoid noise from WM_PAINT / WM_TIMER /
|
// (filtered light to avoid noise from WM_PAINT / WM_TIMER /
|
||||||
// WM_GETICON spam from typical pumps).
|
// WM_GETICON spam from typical pumps). Every ~2s also call
|
||||||
|
// GetStatistics and snapshot up to N records, to test the
|
||||||
|
// polling design — if Galaxy has any active alarms or any
|
||||||
|
// have changed since Subscribe, we'll see them here.
|
||||||
DateTime deadline = DateTime.UtcNow + PumpDuration;
|
DateTime deadline = DateTime.UtcNow + PumpDuration;
|
||||||
|
DateTime nextPoll = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||||
|
int pollCount = 0;
|
||||||
while (DateTime.UtcNow < deadline)
|
while (DateTime.UtcNow < deadline)
|
||||||
{
|
{
|
||||||
while (PeekMessage(out MSG msg, IntPtr.Zero, 0, 0, PM_REMOVE))
|
while (PeekMessage(out MSG msg, IntPtr.Zero, 0, 0, PM_REMOVE))
|
||||||
@@ -251,6 +256,11 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
|||||||
TranslateMessage(ref msg);
|
TranslateMessage(ref msg);
|
||||||
DispatchMessage(ref msg);
|
DispatchMessage(ref msg);
|
||||||
}
|
}
|
||||||
|
if (DateTime.UtcNow >= nextPoll)
|
||||||
|
{
|
||||||
|
PollGetStatistics(client, ++pollCount);
|
||||||
|
nextPoll = DateTime.UtcNow + TimeSpan.FromSeconds(2);
|
||||||
|
}
|
||||||
Thread.Sleep(10);
|
Thread.Sleep(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +284,73 @@ public sealed class AlarmClientWmProbeTests : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PollGetStatistics(AlarmClient client, int seq)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int percent = 0, total = 0, active = 0, suppressed = 0;
|
||||||
|
int suppressedFilters = 0, newAlarms = 0, changes = 0;
|
||||||
|
int[] codes = Array.Empty<int>();
|
||||||
|
int[] positions = Array.Empty<int>();
|
||||||
|
int[] handles = Array.Empty<int>();
|
||||||
|
int rc = client.GetStatistics(
|
||||||
|
ref percent, ref total, ref active, ref suppressed,
|
||||||
|
ref suppressedFilters, ref newAlarms, ref changes,
|
||||||
|
ref codes, ref positions, ref handles);
|
||||||
|
string codesStr = codes != null ? string.Join(",", codes) : "<null>";
|
||||||
|
string posStr = positions != null ? string.Join(",", positions) : "<null>";
|
||||||
|
string handlesStr = handles != null ? string.Join(",", handles) : "<null>";
|
||||||
|
int posLen = positions?.Length ?? 0;
|
||||||
|
Log($"GetStatistics #{seq} rc={rc} pct={percent} total={total} active={active} " +
|
||||||
|
$"suppressed={suppressed} suppressedFilters={suppressedFilters} new={newAlarms} changes={changes} " +
|
||||||
|
$"codes=[{codesStr}] positions=[{posStr}] handles=[{handlesStr}]");
|
||||||
|
|
||||||
|
// If positions has entries, fetch one record so we see the
|
||||||
|
// record-shape AVEVA exposes for a real alarm.
|
||||||
|
if (posLen > 0 && positions != null)
|
||||||
|
{
|
||||||
|
int idx = positions[0];
|
||||||
|
AlarmRecord rec = new AlarmRecord();
|
||||||
|
int recRc = client.GetAlarmExtendedRec(idx, ref rec);
|
||||||
|
Log($" GetAlarmExtendedRec(idx={idx}) rc={recRc} -> " +
|
||||||
|
DescribeAlarmRecord(rec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"GetStatistics #{seq} threw: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DescribeAlarmRecord(AlarmRecord rec)
|
||||||
|
{
|
||||||
|
// Reflect over the record's public properties so we don't have to
|
||||||
|
// guess the field shape — the discovery probe already showed it has
|
||||||
|
// ar_AlarmName / ar_Provider / ar_Group / ar_AlmTransition / etc.
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.Append("{ ");
|
||||||
|
bool first = true;
|
||||||
|
foreach (var prop in rec.GetType().GetProperties(
|
||||||
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object? v = prop.GetValue(rec);
|
||||||
|
string vs = v?.ToString() ?? "<null>";
|
||||||
|
if (vs.Length > 50) vs = vs.Substring(0, 47) + "...";
|
||||||
|
if (!first) sb.Append(", ");
|
||||||
|
sb.Append($"{prop.Name}={vs}");
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// skip failing accessors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.Append(" }");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private void LogIfInteresting(MSG m)
|
private void LogIfInteresting(MSG m)
|
||||||
{
|
{
|
||||||
// Filter out the highest-volume noise (timer ticks, paint, mouse moves
|
// Filter out the highest-volume noise (timer ticks, paint, mouse moves
|
||||||
|
|||||||
Reference in New Issue
Block a user