[M5] mxaccess-asb: F25 step 8 — subscription operations

CreateSubscription / AddMonitoredItems / Publish / DeleteSubscription.
Completes the IASBIDataV2 read-and-subscribe path; remaining ops
(Write/PublishWriteComplete/DeleteMonitoredItems) are mechanical
extensions of the same pattern.

Contracts:
* `MonitoredItemValue` codec (IAsbCustomSerializableType binary
  fast-path: ItemIdentity + RuntimeValue + AsbVariant per
  `AsbContracts.cs:1064-1068`) with array codec (4-byte int32
  count + per-element body, mirrors `WriteArrayToStream` at
  `cs:1095-1103`).

Request builders:
* `build_create_subscription_request_body(max_queue_size,
  sample_interval)` — primitive fields per `cs:215-223`.
* `build_delete_subscription_request_body(subscription_id)` —
  primitive field per `cs:232-237`.
* `build_publish_request_body(subscription_id)` — primitive field
  per `cs:287-292`.
* `build_add_monitored_items_request_body(subscription_id, items,
  require_id)` — minimal MonitoredItem shape (Item +
  SampleInterval + Buffered). Full optional-field set
  (Active/TimeDeadband/ValueDeadband/UserData) deferred to a later
  iteration once a live capture confirms the WCF DataContract
  XML wire form.

Response decoders:
* `decode_create_subscription_response` — single int64
  SubscriptionId field. Decoder accepts Int64Text, Int32Text,
  Zero/One, or numeric-string Chars (covers all WCF binary
  numeric encodings).
* `decode_add_monitored_items_response` — Status array +
  ItemCapabilities-presence flag (mirrors RegisterItemsResponse).
* `decode_publish_response` — Status array + Values
  (MonitoredItemValue) array.

`BodyField::Int64Element` variant added for the primitive
SubscriptionId / MaxQueueSize / SampleInterval fields. `uint64`
helper casts to i64 (covers proven value range; if ulong > i64::MAX
ever appears we'll add UInt64Text to F21's NbfxText enum).

Client wrappers (4 new methods on AsbClient):
* `create_subscription(max_queue_size, sample_interval)`
* `add_monitored_items(subscription_id, items, require_id)`
* `publish(subscription_id)`
* `delete_subscription(subscription_id)`

11 new tests cover:
* MonitoredItemValue round-trip + array round-trip.
* CreateSubscription request body shape (Int64 payloads).
* CreateSubscription response decoder via Int64Text.
* CreateSubscription response decoder via Chars text fallback.
* CreateSubscription response missing-field error.
* AddMonitoredItems body carries SubscriptionId + MonitoredItem
  elements.
* AddMonitoredItems response Status round-trip.
* DeleteSubscription body carries SubscriptionId.
* Publish request body shape.
* Publish response Status + Values round-trip.

Workspace: 691 tests pass (was 680, +11). The asb-subscribe example
can now do create_subscription → add_monitored_items → publish-loop
→ delete_subscription once wire-byte reconciliation against a live
capture confirms the MonitoredItem XML shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 12:57:59 -04:00
parent c6570dcd06
commit b543eb1f84
5 changed files with 774 additions and 13 deletions
+71 -4
View File
@@ -55,10 +55,16 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::contracts::{ItemIdentity, ItemStatus};
use crate::envelope::{ConnectionValidator, EnvelopeError, SoapEnvelope};
use crate::operations::{
ConnectResponse, OperationError, ReadResponse, RegisterItemsResponse, UnregisterItemsResponse,
build_authenticate_me_request_body, build_connect_request_body, build_disconnect_request_body,
build_keep_alive_request_body, build_read_request_body, build_register_items_request_body,
build_unregister_items_request_body, decode_connect_response, decode_read_response,
AddMonitoredItemsResponse, ConnectResponse, CreateSubscriptionResponse,
DeleteSubscriptionResponse, MinimalMonitoredItem, OperationError, PublishResponse,
ReadResponse, RegisterItemsResponse, UnregisterItemsResponse,
build_add_monitored_items_request_body, build_authenticate_me_request_body,
build_connect_request_body, build_create_subscription_request_body,
build_delete_subscription_request_body, build_disconnect_request_body,
build_keep_alive_request_body, build_publish_request_body, build_read_request_body,
build_register_items_request_body, build_unregister_items_request_body,
decode_add_monitored_items_response, decode_connect_response,
decode_create_subscription_response, decode_publish_response, decode_read_response,
decode_register_items_response, decode_unregister_items_response,
};
use crate::{actions, decode_envelope, encode_envelope};
@@ -335,6 +341,67 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> AsbClient<T> {
Ok(decode_read_response(&response.body_tokens)?)
}
/// `CreateSubscription` operation — allocates a server-side
/// subscription and returns its ID. Caller threads the ID through
/// subsequent `add_monitored_items` / `publish` /
/// `delete_subscription` calls.
pub async fn create_subscription(
&mut self,
max_queue_size: i64,
sample_interval: u64,
) -> Result<CreateSubscriptionResponse, ClientError> {
let body = build_create_subscription_request_body(max_queue_size, sample_interval);
let response = self
.send_signed_envelope(actions::CREATE_SUBSCRIPTION, body, false)
.await?;
Ok(decode_create_subscription_response(
&response.body_tokens,
&self.read_dictionary,
)?)
}
/// `AddMonitoredItems` operation — adds items to an existing
/// subscription. Uses [`MinimalMonitoredItem`] (Item +
/// SampleInterval + Buffered); optional fields are deferred to a
/// later F25 iteration.
pub async fn add_monitored_items(
&mut self,
subscription_id: i64,
items: &[MinimalMonitoredItem],
require_id: bool,
) -> Result<AddMonitoredItemsResponse, ClientError> {
let body = build_add_monitored_items_request_body(subscription_id, items, require_id);
let response = self
.send_signed_envelope(actions::ADD_MONITORED_ITEMS, body, false)
.await?;
Ok(decode_add_monitored_items_response(&response.body_tokens)?)
}
/// `Publish` operation — long-polls the subscription queue for
/// available samples. Typical pattern is to call this in a loop
/// with a small `tokio::time::timeout` per call.
pub async fn publish(&mut self, subscription_id: i64) -> Result<PublishResponse, ClientError> {
let body = build_publish_request_body(subscription_id);
let response = self
.send_signed_envelope(actions::PUBLISH, body, false)
.await?;
Ok(decode_publish_response(&response.body_tokens)?)
}
/// `DeleteSubscription` operation — releases a server-side
/// subscription. The response body is empty per
/// `AsbContracts.cs:239-240`.
pub async fn delete_subscription(
&mut self,
subscription_id: i64,
) -> Result<DeleteSubscriptionResponse, ClientError> {
let body = build_delete_subscription_request_body(subscription_id);
let _ = self
.send_signed_envelope(actions::DELETE_SUBSCRIPTION, body, false)
.await?;
Ok(DeleteSubscriptionResponse)
}
/// `RegisterItems` operation — sends a signed `RegisterItemsIn`
/// SOAP envelope and decodes the `RegisterItemsResponse`.
pub async fn register_items(