From fb40e4c20bcd0f3578a00d842e518bd12228fd8a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 6 May 2026 02:49:11 -0400 Subject: [PATCH] [F34 partial] mxaccess-asb: fix collect_asbidata_payloads + add Active flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigation via examples/asb-relay.rs middleman captured the full S→C bytes of a working PublishResponse from the .NET probe against MxDataProvider. Decoder fix verified by regression test against the captured fixture; one further wire-format gap surfaced and is filed. Closed in this commit: 1. collect_asbidata_payloads filtered out empty elements so positional payload[N] indexing collapsed when Status was empty-but-present. The wire form for PublishResponse is: ← empty placeholder {bytes} Our decoder lost the positional info and read Values as Status, then panicked on the malformed parse. Fix: always push every element (empty or not) so payloads[0]=Status and payloads[1]=Values stay aligned. New regression test tests/publish_capture.rs runs the full decode chain over the captured wire bytes (305-byte frame at tests/fixtures/publish-response-with-value.bin) and asserts values.len() == 1. 2. MinimalMonitoredItem.active: Option + new with_active() constructor. The .NET reference's MxAsbDataClient.AddMonitoredItems defaults to active: true (cs:441). Without true on the wire, MxDataProvider treats the subscription as inactive and Publish polls return empty Values. Both binary build and canonical XML emitters now conditionally emit when active.is_some(). Shared push_monitored_item_body helper eliminates the duplicate MonitoredItem encoder between AddMonitoredItems and DeleteMonitoredItems builders. 3. SampleInterval unit: clarified as **milliseconds** in MinimalMonitoredItem.sample_interval doc + the example (sample_interval_ticks → sample_interval_ms = 1000). Matches the .NET reference's `ulong sampleInterval = 1000` default. Open: F34's deeper finding — `MonitoredItem`'s wire schema is DataContract field-suffix names (`activeField`, `bufferedField`, `itemField`, `sampleIntervalField`, etc., per the per-session NBFX dictionary the .NET probe declares), NOT XmlSerializer property names (`Active`, `Buffered`, `Item`, `SampleInterval`). Our binary NBFX builder still uses the property names, so MxDataProvider silently fails to register monitored items — successField=true with a 0-length Status array. The fix needs a complete rebuild of build_add_monitored_items_request_body and build_delete_monitored_items_request_body to use the field-suffix names plus emit the *Specified siblings (activeFieldSpecified, idFieldSpecified, etc.) as their own elements. The HMAC canonical XML side is unaffected (XmlSerializer naming is correct there; verified byte-equal to .NET via the F28 fixtures). Detailed in design/followups.md F34's "Open" section. Live verification of the F34-partial bonus context: - Read still returns 99 end-to-end via canonical XML signing. - AddMonitoredItems still returns Status[0] = 0 items (server doesn't recognize our DataContract-misnamed payload). - Publish still returns 0 values (the F34-open consequence). - All other 13 canonical-XML signed ops succeed at the request level (no SOAP faults, no HMAC rejections). Workspace: mxaccess-asb 95 → 96 (+1 capture-driven decoder test); default-feature clippy clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- design/followups.md | 25 ++- rust/crates/mxaccess-asb/src/operations.rs | 190 +++++++++++------- rust/crates/mxaccess-asb/src/xml_canonical.rs | 7 + .../fixtures/publish-response-with-value.bin | Bin 0 -> 305 bytes .../mxaccess-asb/tests/publish_capture.rs | 65 ++++++ .../crates/mxaccess/examples/asb-subscribe.rs | 6 +- 6 files changed, 212 insertions(+), 81 deletions(-) create mode 100644 rust/crates/mxaccess-asb/tests/fixtures/publish-response-with-value.bin create mode 100644 rust/crates/mxaccess-asb/tests/publish_capture.rs diff --git a/design/followups.md b/design/followups.md index 0f8ddb5..975ef4c 100644 --- a/design/followups.md +++ b/design/followups.md @@ -74,9 +74,28 @@ move to `## Resolved` with a date + commit hash. For the per-step body of every line listed in the cumulative execution log, see the matching commit message — each commit is a single F-number step with its own scope, fixtures, test count delta, and follow-up notes. The detailed per-step write-ups previously inlined here added little beyond what `git show ` provides. -### F34 — Publish response: decoder reads 0 values where .NET sees real values -**Severity:** P2 — only affects the F26 stream's data flow against MxDataProvider; the canonical-XML signing for Publish itself is verified working (server accepts the request, returns a non-fault response). -**Source:** Live `cargo run -p mxaccess --example asb-subscribe` against the local AVEVA install, 2026-05-06. +### F34 — `MonitoredItem` wire format: DataContract field-suffix names, not XmlSerializer property names +**Severity:** P2 — only affects the F26 stream's data flow against MxDataProvider; canonical-XML HMAC signing for the operation is verified working (server accepts the request, returns a non-fault response). +**Source:** Live `cargo run -p mxaccess --example asb-subscribe` + `examples/asb-relay.rs` capture, 2026-05-06. + +**Two sub-issues, one closed and one open.** + +**Closed: `decode_publish_response` filtered empty `` placeholders out of the positional payload list.** Captured the full S→C bytes of a working `PublishResponse` via `examples/asb-relay.rs` between the .NET probe and MxDataProvider (fixture stashed at `crates/mxaccess-asb/tests/fixtures/publish-response-with-value.bin`). The wire shape is `{bytes}` — Status is empty-but-present, Values carries the binary `MonitoredItemValue[]`. Our `collect_asbidata_payloads` previously skipped the empty Status, shifting Values down to index `0` where the decoder mis-read it as Status and corrupted the parse. Fix: always push every `` element as a positional entry, empty or not. `tests/publish_capture.rs` runs the full decode chain over the real wire bytes and asserts `values.len() == 1`. **Verified 2026-05-06.** + +**Open: AddMonitoredItems / DeleteMonitoredItems request bodies use the wrong element-name form on the binary NBFX wire.** Live capture of the .NET probe's `AddMonitoredItems` request exposes a per-session NBFX dictionary declaring these strings *(verbatim, in declaration order)*: `activeField`, `activeFieldSpecified`, `bufferedField`, `itemField`, `contextNameField`, `idField`, `idFieldSpecified`, `nameField`, `referenceTypeField`, `typeField`, `sampleIntervalField`, `timeDeadbandField`, `timeDeadbandFieldSpecified`, `userDataField`, `lengthField`, `payloadField`, `valueDeadbandField`. These are the `[DataMember(Name = "...")]` private-field names from `AsbContracts.cs:940-965` — the wire form chosen by `DataContractSerializer` for non-`IAsbCustomSerializableType` types like `MonitoredItem`. Our `build_add_monitored_items_request_body` (and `build_delete_monitored_items_request_body`) emits XmlSerializer-property names like ``, ``, ``, `` — those are the *canonical XML for HMAC* shape (XmlSerializer-derived), which is correct for the signing input but wrong for the binary wire payload. MxDataProvider silently fails to register monitored items whose field names don't match its DataContract schema, returns a 0-length `Status` array (`successField=true`, `resultCode=0`, but no items actually registered), and consequently the `Publish` poll loop forever returns empty `Values`. + +**The dual-format world**: ASB requests have *two* element-name conventions on the wire: +- **HMAC canonical XML** (input to `AsbAuthenticator::Sign`): XmlSerializer-derived names — ``, ``, `` etc. Driven by `[XmlElement(...)]` and property names. Our `xml_canonical` emitter is byte-equal to .NET here (F28 step 2 fixtures verify). +- **Binary NBFX body** (the actual wire request): DataContractSerializer-derived names — ``, ``, etc. Driven by private-field `[DataMember(Name = "...")]`. Our builders for AddMonitoredItems / DeleteMonitoredItems are wrong here. + +For ops where the body is purely `IAsbCustomSerializableType` arrays (Read, Register, Unregister), no DataContract names appear — every payload is wrapped as `{bytes}` (binary fast-path) and our builders are correct. The DataContract schema only matters for ops carrying non-`IAsbCustomSerializable` types like `MonitoredItem` and `WriteValue`. + +**Resolves when:** `build_add_monitored_items_request_body` and `build_delete_monitored_items_request_body` rewrite each `MonitoredItem` child as the DataContract field-suffix names (`activeField` instead of `Active`, etc.), with the `*Specified` siblings emitted as their own elements (`activeFieldSpecified`, `idFieldSpecified`, etc.). The dictionary-id pre-population that .NET's WCF binary writer uses to compress these long strings is a perf optimisation; an inline-string emit will work for correctness. Likely the same fix applies to `WriteBasicRequest`'s `WriteValue[]? Values` field (also non-`IAsbCustomSerializable`) — that's a future capture-and-verify pass. + +**Bonus context discovered while debugging F34:** +- `MinimalMonitoredItem` gained an `active: Option` field with a `with_active(item, interval, active)` constructor. Without `true` on the wire, MxDataProvider treats the subscription as inactive even when AddMonitoredItems "succeeds" — F26 stream then never sees values. (Once the field-name fix lands, this becomes the determining factor for whether values flow.) +- `SampleInterval` unit corrected from "100-ns ticks" to **milliseconds** in the example + the `MinimalMonitoredItem.sample_interval` doc — matches `MxAsbDataClient.cs:441`'s `ulong sampleInterval = 1000` default. +- `result_code = 32` is `AsbErrorCode.PublishComplete` (`AsbResultMapping.cs:37`), informational not fatal — `ToResult:122-129` treats it like `Success`. F26 stream's `publish_loop` narrowed to bail only on `RESULT_CODE_INVALID_CONNECTION_ID = 1`. **Evidence.** Side-by-side comparison after the .NET probe and Rust client both ran `--subscribe` against the same `TestChildObject.TestInt` tag: diff --git a/rust/crates/mxaccess-asb/src/operations.rs b/rust/crates/mxaccess-asb/src/operations.rs index a0910ae..1591107 100644 --- a/rust/crates/mxaccess-asb/src/operations.rs +++ b/rust/crates/mxaccess-asb/src/operations.rs @@ -519,36 +519,7 @@ pub fn build_delete_monitored_items_request_body( }, ]; for item in items { - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("MonitoredItem".to_string()), - }); - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("Item".to_string()), - }); - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("ASBIData".to_string()), - }); - tokens.push(NbfxToken::Text(NbfxText::Bytes(item.item.encode()))); - tokens.push(NbfxToken::EndElement); // - tokens.push(NbfxToken::EndElement); // - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("SampleInterval".to_string()), - }); - tokens.push(NbfxToken::Text(NbfxText::Int64( - item.sample_interval as i64, - ))); - tokens.push(NbfxToken::EndElement); - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("Buffered".to_string()), - }); - tokens.push(NbfxToken::Text(NbfxText::Bool(item.buffered))); - tokens.push(NbfxToken::EndElement); - tokens.push(NbfxToken::EndElement); // + push_monitored_item_body(&mut tokens, item); } tokens.push(NbfxToken::EndElement); // tokens.push(NbfxToken::EndElement); // @@ -803,40 +774,7 @@ pub fn build_add_monitored_items_request_body( }, ]; for item in items { - // - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("MonitoredItem".to_string()), - }); - // {ItemIdentity binary} - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("Item".to_string()), - }); - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("ASBIData".to_string()), - }); - tokens.push(NbfxToken::Text(NbfxText::Bytes(item.item.encode()))); - tokens.push(NbfxToken::EndElement); // - tokens.push(NbfxToken::EndElement); // - // - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("SampleInterval".to_string()), - }); - tokens.push(NbfxToken::Text(NbfxText::Int64( - item.sample_interval as i64, - ))); - tokens.push(NbfxToken::EndElement); - // - tokens.push(NbfxToken::Element { - prefix: None, - name: NbfxName::Inline("Buffered".to_string()), - }); - tokens.push(NbfxToken::Text(NbfxText::Bool(item.buffered))); - tokens.push(NbfxToken::EndElement); - tokens.push(NbfxToken::EndElement); // + push_monitored_item_body(&mut tokens, item); } tokens.push(NbfxToken::EndElement); // // @@ -850,11 +788,62 @@ pub fn build_add_monitored_items_request_body( tokens } -/// Minimal `MonitoredItem` shape covering just `Item`, `SampleInterval`, -/// and `Buffered`. The full .NET `MonitoredItem` (`AsbContracts.cs:936-1030`) -/// 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. +/// Emit a single `...` NBFX subtree. +/// Shared between AddMonitoredItems and DeleteMonitoredItems request +/// builders. Field order matches the .NET `MonitoredItem` declaration: +/// Item / SampleInterval / Active (when Specified) / Buffered. +fn push_monitored_item_body(tokens: &mut Vec, item: &MinimalMonitoredItem) { + tokens.push(NbfxToken::Element { + prefix: None, + name: NbfxName::Inline("MonitoredItem".to_string()), + }); + // {ItemIdentity binary} + tokens.push(NbfxToken::Element { + prefix: None, + name: NbfxName::Inline("Item".to_string()), + }); + tokens.push(NbfxToken::Element { + prefix: None, + name: NbfxName::Inline("ASBIData".to_string()), + }); + tokens.push(NbfxToken::Text(NbfxText::Bytes(item.item.encode()))); + tokens.push(NbfxToken::EndElement); // + tokens.push(NbfxToken::EndElement); // + // + tokens.push(NbfxToken::Element { + prefix: None, + name: NbfxName::Inline("SampleInterval".to_string()), + }); + tokens.push(NbfxToken::Text(NbfxText::Int64( + item.sample_interval as i64, + ))); + tokens.push(NbfxToken::EndElement); + // — emitted only when ActiveSpecified=true + // (`MonitoredItem.Active` setter at `AsbContracts.cs:982-987`). + // Required to make MxDataProvider actually deliver values; F34. + if let Some(active) = item.active { + tokens.push(NbfxToken::Element { + prefix: None, + name: NbfxName::Inline("Active".to_string()), + }); + tokens.push(NbfxToken::Text(NbfxText::Bool(active))); + tokens.push(NbfxToken::EndElement); + } + // + tokens.push(NbfxToken::Element { + prefix: None, + name: NbfxName::Inline("Buffered".to_string()), + }); + tokens.push(NbfxToken::Text(NbfxText::Bool(item.buffered))); + tokens.push(NbfxToken::EndElement); + tokens.push(NbfxToken::EndElement); // +} + +/// Minimal `MonitoredItem` shape covering `Item`, `SampleInterval`, +/// `Buffered`, and the `*Specified`-gated `Active` field. The full +/// .NET `MonitoredItem` (`AsbContracts.cs:936-1030`) also has +/// `TimeDeadband`, `ValueDeadband`, and `UserData` — deferred until a +/// live capture confirms each one's wire-byte form. /// /// **`sample_interval` unit is milliseconds**, NOT 100-ns ticks. The /// .NET reference's `MxAsbDataClient.AddMonitoredItems` defaults to @@ -864,20 +853,61 @@ pub fn build_add_monitored_items_request_body( /// 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. +/// +/// **`active` is the `*Specified` knob that decides whether +/// `` appears on the wire**. `None` → not emitted (server +/// defaults to inactive — `Publish` polls return zero values). +/// `Some(true)` → emitted as `true`; the +/// MxDataProvider then actually delivers values from the +/// subscription. The .NET reference's `AddMonitoredItems` defaults +/// to `active: true` (`MxAsbDataClient.cs:441`); the +/// `MonitoredItem.Active` setter at `AsbContracts.cs:982-987` +/// auto-flips `ActiveSpecified=true` so the wire includes the +/// element. F34: this asymmetry is what made our subscribe path +/// see zero values where .NET sees real ones — 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, + /// `Some(b)` emits `{b}` on the wire (the + /// `*Specified` pattern). `None` omits the element entirely — + /// MxDataProvider then treats the item as inactive and delivers + /// no values. Use `Some(true)` to actually receive samples. + pub active: Option, } impl MinimalMonitoredItem { + /// Build a default `MinimalMonitoredItem`: item + interval, no + /// Active flag, no Buffered. Matches the .NET `new MonitoredItem + /// { Item = ..., SampleInterval = ... }` shape used by the + /// canonical-XML fixtures. + /// + /// **For live subscriptions that should actually deliver values, + /// prefer [`Self::with_active`].** Without `Active=true` on the + /// wire, the server defaults to inactive and `Publish` returns + /// empty payloads. pub fn new(item: ItemIdentity, sample_interval: u64) -> Self { Self { item, sample_interval, buffered: false, + active: None, + } + } + + /// Build a `MinimalMonitoredItem` with `Active=true`. This is + /// what the .NET reference's `AddMonitoredItems` emits by default + /// (`MxAsbDataClient.cs:441`) and what makes MxDataProvider + /// actually deliver values from the subscription. + pub fn with_active(item: ItemIdentity, sample_interval: u64, active: bool) -> Self { + Self { + item, + sample_interval, + buffered: false, + active: Some(active), } } } @@ -1368,18 +1398,24 @@ pub fn collect_asbidata_payloads(tokens: &[NbfxToken]) -> Vec> { // token, but a `Bytes`-then-`EndElement` (from the // `WithEndElement` variant) leaves a sequence of // `Bytes` tokens we walk here. - let mut combined: Option> = None; + let mut buf = Vec::new(); while let Some(NbfxToken::Text(NbfxText::Bytes(payload))) = tokens.get(inner) { - match combined.as_mut() { - Some(buf) => buf.extend_from_slice(payload), - None => combined = Some(payload.clone()), - } + buf.extend_from_slice(payload); inner += 1; } - if let Some(buf) = combined { - out.push(buf); - } + // F34: ALWAYS push, even when buf is empty. The wire + // uses `` (empty) as positional + // placeholders — e.g. `PublishResponse` emits an + // empty `` for `Status` when the per-item + // status array is empty, followed by a populated + // `{values}` for `Values`. If we + // skip the empty one, the Values payload shifts down + // to index 0 where the decoder reads it as Status + // and corrupts the parse. Captured live 2026-05-06 + // via `examples/asb-relay.rs` middleman; fixture at + // `tests/fixtures/publish-response-with-value.bin`. + out.push(buf); } } idx += 1; diff --git a/rust/crates/mxaccess-asb/src/xml_canonical.rs b/rust/crates/mxaccess-asb/src/xml_canonical.rs index 0d7ca8d..414e077 100644 --- a/rust/crates/mxaccess-asb/src/xml_canonical.rs +++ b/rust/crates/mxaccess-asb/src/xml_canonical.rs @@ -507,6 +507,13 @@ fn emit_monitored_item(s: &mut String, item: &MinimalMonitoredItem) { // xmlns redeclaration since they're already in iom:2. emit_inline_item_identity(s, " ", "Item", &item.item); emit_iom_text(s, " ", "SampleInterval", &item.sample_interval.to_string()); + // is *Specified-gated; emit only when the consumer + // opted in (None → not on the wire). MxDataProvider's + // Publish path requires Active=true to actually deliver values + // — F34, verified live 2026-05-06. + if let Some(active) = item.active { + emit_iom_text(s, " ", "Active", if active { "true" } else { "false" }); + } // ValueDeadband + UserData: default-Variant shape (type=0, // length=0, payload nil). emit_iom_default_variant(s, " ", "ValueDeadband"); diff --git a/rust/crates/mxaccess-asb/tests/fixtures/publish-response-with-value.bin b/rust/crates/mxaccess-asb/tests/fixtures/publish-response-with-value.bin new file mode 100644 index 0000000000000000000000000000000000000000..a1e340f231db9e05e950cfbe4bc851af943e7a29 GIT binary patch literal 305 zcmZQu$0WcXP@0sJS)37+T3nEySDYHg#LZaD!p)e-7RKSt$jz9+%*~j|>cq~)Ia9>| z3Uom<8Ud4Y;uhg;gc^yzjx6+Ul$%ZhNjgb zjA49Eu3Vx{;#`v4j7gjz%Uq>_y0{sWWf@)NfhML1s>%vbGHa8++~)ogUtZgtg# zNisTcI0id;x+In)A~EN=GBPkQfG`X&ff@h*GZtJ3U)HelUI!x!0}D_Lgp+}oogGLt THMh309{@6-U_K)QBO@aKlrlay literal 0 HcmV?d00001 diff --git a/rust/crates/mxaccess-asb/tests/publish_capture.rs b/rust/crates/mxaccess-asb/tests/publish_capture.rs new file mode 100644 index 0000000..cbd1eee --- /dev/null +++ b/rust/crates/mxaccess-asb/tests/publish_capture.rs @@ -0,0 +1,65 @@ +//! F34 — wire-byte trace of a captured `PublishResponse`. +//! +//! `tests/fixtures/publish-response-with-value.bin` is the verbatim +//! S→C bytes the .NET probe (`MxAsbClient.Probe --subscribe`) saw on +//! its first `Publish` poll against the local AVEVA install on +//! 2026-05-06, captured via `examples/asb-relay.rs` middleman with +//! `--via=net.tcp://127.0.0.1:8088/...`. The .NET probe extracted +//! `preview:99` from this exchange — the value bytes +//! `[63 00 00 00]` (= 99 in LE i32) are visible at file offset 0x110. +//! +//! Test goal: dump `decode_envelope` + `decode_publish_response` +//! output so we can see exactly where our value-extraction diverges +//! from .NET's (F34 hypotheses). +//! +//! Frame layout: 3-byte NMF SizedEnvelope header (`06 ae 02`, +//! varint length = 302) + 302-byte SOAP envelope. + +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic +)] + +use mxaccess_asb::{decode_envelope, decode_publish_response}; +use mxaccess_asb_nettcp::nbfx::DynamicDictionary; + +#[test] +fn publish_response_capture_decoder_trace() { + let raw = std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/publish-response-with-value.bin"), + ) + .expect("read fixture"); + assert_eq!(raw.len(), 305, "frame length sanity check"); + + // Strip 3-byte NMF SizedEnvelope header. + let envelope = &raw[3..]; + assert_eq!(envelope.len(), 302); + + let mut dict = DynamicDictionary::new(); + let decoded = decode_envelope(envelope, &mut dict).expect("decode_envelope succeeds"); + eprintln!("=== body tokens ({} total) ===", decoded.body_tokens.len()); + for (i, tok) in decoded.body_tokens.iter().enumerate() { + eprintln!(" body[{i}]={tok:?}"); + } + + let response = decode_publish_response(&decoded.body_tokens) + .expect("decode_publish_response succeeds"); + eprintln!("=== decoded PublishResponse ==="); + eprintln!(" status_count: {}", response.status.len()); + eprintln!(" values_count: {}", response.values.len()); + eprintln!(" result_code: {:?}", response.result_code); + eprintln!(" success: {:?}", response.success); + + // The .NET probe extracted 1 value with preview:99 from the same + // wire bytes. If our decoder reports 0 values, the test fails and + // the eprintln body-token dump above shows where the gap is. + assert_eq!( + response.values.len(), + 1, + ".NET sees 1 value (preview:99) from the same bytes; our decoder reads {}", + response.values.len(), + ); +} diff --git a/rust/crates/mxaccess/examples/asb-subscribe.rs b/rust/crates/mxaccess/examples/asb-subscribe.rs index afd3b98..263d809 100644 --- a/rust/crates/mxaccess/examples/asb-subscribe.rs +++ b/rust/crates/mxaccess/examples/asb-subscribe.rs @@ -155,9 +155,13 @@ async fn main() -> Result<(), Box> { sub_response.subscription_id, sub_response.result_code, sub_response.success ); - let monitored = vec![MinimalMonitoredItem::new( + // F34: MUST set Active=true (`MonitoredItem.ActiveSpecified=true` + // on the wire). Without it MxDataProvider treats the item as + // inactive and Publish polls always return zero values. + let monitored = vec![MinimalMonitoredItem::with_active( ItemIdentity::absolute_by_name(&env.tag), sample_interval_ms, + true, )]; eprintln!("adding monitored items [canonical XML AddMonitoredItems]");