asb: SampleInterval unit fix + F34 followup for Publish-decoder gap
rust / build / test / clippy / fmt (push) Has been cancelled

Investigation triggered by "Publish returns 0 values where .NET sees real
values" against the local AVEVA install.

Three findings:

1. SampleInterval unit: the wire field is **milliseconds**, not 100-ns
   ticks. The .NET reference (MxAsbDataClient.cs:441) defaults to
   `ulong sampleInterval = 1000` and the probe passes `subscribeSampleMs`
   directly through that surface. Sending 10_000_000 (1s in 100-ns ticks)
   makes MxDataProvider schedule the next sample ~2.8 hours out; Publish
   polls always come back empty until the misinterpreted timer expires.
   Fixed in `examples/asb-subscribe.rs` (sample_interval_ticks →
   sample_interval_ms = 1000) and clarified in
   `MinimalMonitoredItem.sample_interval`'s doc comment with the live-2026-05-06
   evidence.

2. result_code=32 is `AsbErrorCode.PublishComplete`
   (`AsbResultMapping.cs:37`) — informational, not a fatal error. .NET's
   `ToResult` (cs:122-129) explicitly treats it like Success.
   `ArchestrAResult.ErrorCode` and `ResultCode` are aliases for the same
   `resultCodeField` (cs:424-434), so `publish[i]_error=0x00000020` in
   the .NET probe trace = `result_code=Some(32)` in our trace = the same
   thing. Already handled correctly via the F26 narrower-bail fix
   (commit 983f029) — no code change needed.

3. **F34 filed** for the residual gap: with both sides seeing
   result_code=32 + success=false, .NET extracts a value but we extract
   zero. Three open hypotheses (wire-shape mismatch / payload-locator
   bug / MonitoredItemValue byte-layout bug); all need a middleman
   asb-relay.rs trace between the .NET probe and MxDataProvider to
   confirm. Adjacent symptom: AddMonitoredItemsResponse Status reads as
   0 items where .NET sees 1 — likely the same root cause; one fix
   should close both.

Live re-runs to validate the new sample-interval unit were blocked by
the documented F32 InvalidConnectionId transient (the
pending-connection table on MxDataProvider fills up after many
back-to-back test cycles; clears after a 30s+ cool-down).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-06 02:28:44 -04:00
parent 983f02921c
commit 0771664092
3 changed files with 56 additions and 3 deletions
@@ -855,9 +855,19 @@ pub fn build_add_monitored_items_request_body(
/// also has optional Active, TimeDeadband, ValueDeadband, and UserData
/// fields. Those are deferred to a later F25 iteration once a live
/// capture confirms the wire-byte form.
///
/// **`sample_interval` unit is milliseconds**, NOT 100-ns ticks. The
/// .NET reference's `MxAsbDataClient.AddMonitoredItems` defaults to
/// `ulong sampleInterval = 1000` (= 1 second), passed straight to the
/// wire (`MxAsbDataClient.cs:441`). Sending tick-units (e.g.
/// `10_000_000` for "1 second in 100-ns ticks") makes MxDataProvider
/// schedule the next sample ~2.8 hours out — `Publish` polls then
/// always come back empty until the misinterpreted timer expires.
/// Verified live 2026-05-06.
#[derive(Debug, Clone, PartialEq)]
pub struct MinimalMonitoredItem {
pub item: ItemIdentity,
/// Sample interval in **milliseconds** (matches the .NET wire form).
pub sample_interval: u64,
pub buffered: bool,
}
@@ -132,9 +132,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// -- Subscribe-flow ----------------------------------------------------
if env.run_subscribe {
eprintln!("creating subscription [canonical XML CreateSubscription] (max_queue=100, sample=1s)");
let sample_interval_ticks: u64 = 10_000_000; // 1 second
// SampleInterval is in **milliseconds** on the wire — the .NET
// reference's `MxAsbDataClient.CreateSubscription` /
// `AddMonitoredItems` default is `ulong sampleInterval = 1000`
// (`MxAsbDataClient.cs:396,441`). Sending 10_000_000 here would
// be interpreted as ~2.8 hours between samples and the publish
// poll would always come back empty.
let sample_interval_ms: u64 = 1000;
let max_queue_size: i64 = 100;
let sub_response = match client.create_subscription(max_queue_size, sample_interval_ticks).await {
let sub_response = match client.create_subscription(max_queue_size, sample_interval_ms).await {
Ok(r) => r,
Err(e) => {
eprintln!(" create_subscription failed: {e}");
@@ -151,7 +157,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let monitored = vec![MinimalMonitoredItem::new(
ItemIdentity::absolute_by_name(&env.tag),
sample_interval_ticks,
sample_interval_ms,
)];
eprintln!("adding monitored items [canonical XML AddMonitoredItems]");