Expand description
§bytehound-opc-da-client
Backend-agnostic OPC DA client library for Rust — async, trait-based, with transparent COM management.
§Features
- Async/Await API: Built for modern asynchronous Rust using
tokioandasync-trait. - Trait-Based Abstraction: The
OpcProvidertrait allows for easy mocking and backend swapping. - Transparent COM Management: Handles COM initialization (
CoInitializeEx) and apartment thread affinity automatically in the background. - Read & Write Support: Read tag values and write typed values (
Int,Float,Bool,String) to OPC tags. - Scalable Native Browsing: Open isolated sessions and request bounded, one-level pages through OPC DA 3.0 with an automatic OPC DA 2.x fallback.
- Bounded Namespace Inventory: Stream exact ItemIDs with breadcrumb labels through a cancellable, bounded DA 3.0/2.x traversal.
- Windows COM/DCOM Support: Native OPC DA backend via
windows-rs— no external OPC crates needed. - Robust Error Handling: Leverages
thiserrorfor theOpcErrordomain type andfriendly_com_hint()for human-readable HRESULT explanations. - Test-Friendly: Built-in
MockOpcProvidervia thetest-supportfeature.
§Installation
Add this to your Cargo.toml:
[dependencies]
opc-da-client = { package = "bytehound-opc-da-client", version = "0.2.2" }§Prerequisites
- Operating System: Windows (COM/DCOM is a Windows-only technology).
- Rust: 1.88 or newer.
- OPC DA Core Components: Ensure the OPC DA Core Components are installed and registered on your system.
- DCOM Configuration: If connecting to remote servers, appropriate DCOM permissions must be configured.
§Usage Examples
§Connecting & Listing Servers
Enumerate available OPC DA servers on a local or remote host.
use opc_da_client::{OpcDaClient, OpcProvider};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpcDaClient::default();
let servers = client.list_servers("localhost").await?;
println!("Available Servers:");
for server in servers {
println!(" - {}", server);
}
Ok(())
}§Reading Tags
Connect to a specific server and read current values for a set of tags.
use opc_da_client::{OpcDaClient, OpcProvider};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpcDaClient::default();
let server_progid = "Matrikon.OPC.Simulation.1";
let tags = vec![
"Random.Int4".to_string(),
"Random.Real8".to_string(),
];
let values = client.read_tag_values(server_progid, tags).await?;
for v in values {
println!("Tag: {}, Value: {}, Quality: {}, Time: {}",
v.tag_id, v.value, v.quality, v.timestamp);
}
Ok(())
}read_tag_values is the machine-facing read API. For VT_BSTR values, TagValue::value
contains the exact COM string contents: no quote characters are added or removed. Consumers
that intentionally want the historical quoted string presentation can call
read_tag_values_for_display; its default trait implementation falls back to
read_tag_values for third-party providers.
§Writing a Value
Write a typed value to a single OPC tag.
use opc_da_client::{OpcDaClient, OpcProvider, OpcValue};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpcDaClient::default();
let server = "Matrikon.OPC.Simulation.1";
let result = client
.write_tag_value(server, "Bucket Brigade.Int4", OpcValue::Int(42))
.await?;
if result.success {
println!("✓ Write succeeded");
} else {
println!("✗ Write failed: {}", result.error.as_deref().unwrap_or("Unknown error"));
}
Ok(())
}§Browsing the Address Space
Recursively discover available tags on an OPC server.
use opc_da_client::{OpcDaClient, OpcProvider};
use std::sync::{Arc, Mutex, atomic::AtomicUsize};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpcDaClient::default();
let server_progid = "Matrikon.OPC.Simulation.1";
let sink = Arc::new(Mutex::new(Vec::new()));
let progress = Arc::new(AtomicUsize::new(0));
// Clone these Arcs before passing if you need to monitor progress
// or harvest partial results from another task on timeout.
let discovered_tags = client.browse_tags(
server_progid,
100, // Max tags to discover
progress,
sink
).await?;
println!("Found {} tags", discovered_tags.len());
Ok(())
}For large namespaces, use the bounded native browse API instead of recursive discovery:
use opc_da_client::{
BrowseNodeFilter, BrowsePageRequest, OpcDaClient, OpcProvider,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpcDaClient::default();
let server = "Matrikon.OPC.Simulation.1";
let capabilities = client.browse_capabilities(server).await?;
let session = client.open_browse_session(server).await?;
let page = client
.browse_page(
&session,
BrowsePageRequest {
parent: None,
filter: BrowseNodeFilter::All,
max_elements: capabilities.max_page_size.min(100),
continuation: None,
},
)
.await?;
for node in page.nodes {
println!("{}: {:?}", node.name, node.kind);
}
client.close_browse_session(&session).await?;
Ok(())
}Session, node, and continuation tokens are opaque UUIDs. Native browse sessions
own dedicated server connections, expire after five minutes of inactivity, and
never expose COM pointers or OPC DA continuation strings. Transport adapters can
encode tokens with to_string() and restore them with each token type’s
parse() method.
The DA 2.x fallback merges a same-named branch and leaf into one
BrowseNodeKind::BranchAndItem node and resolves its exact item ID through
GetItemID.
For large namespaces, start_inventory streams a bounded inventory without
persisting browse-session or continuation tokens:
use opc_da_client::{
InventoryEvent, InventoryOptions, OpcDaClient, OpcProvider,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = OpcDaClient::default();
let mut inventory = client
.start_inventory(
"Matrikon.OPC.Simulation.1",
InventoryOptions {
batch_size: 100,
max_entries: None,
},
)
.await?;
while let Some(event) = inventory.message().await {
match event? {
InventoryEvent::Entry(entry) => println!("{}: {}", entry.display_name, entry.item_id),
InventoryEvent::Progress(progress) => {
println!("{} items discovered", progress.unique_items);
}
InventoryEvent::Completed(result) => {
println!("complete: {}", result.complete);
break;
}
}
}
Ok(())
}The returned InventoryStream exposes pause, resume, and cancellation controls.
Each native browse call is bounded by InventoryOptions::batch_size, and
max_entries can cap a deliberately limited inventory.
§Architecture
The library is split into a core trait layer and concrete implementations:
OpcProvider: The primary async trait defining server discovery, recursive tag browsing, native paged browsing, reads, and writes.OpcDaClient: The default implementation using nativewindows-rsCOM calls. Generic overServerConnectorfor testability; defaults toComConnector.
See architecture.md for in-depth design details and spec.md for behavioral contracts.
§COM Threading Model
OPC DA relies on Windows COM, which requires per-thread initialization and strict thread affinity. The opc-da-client dependency alias handles this transparently:
- Dedicated Worker Thread: All COM operations are executed on a dedicated background worker thread initialized in Multi-Threaded Apartment (MTA) mode.
- No Manual Init: You do not need to call
CoInitializeor manage COM lifecycles in your calling application. - Host Thread Initialization: Applications that also perform COM work on their own thread can hold a public
ComGuard::new()guard for that thread’s lifetime.
§License
This project is licensed under the MIT License.
§opc-da-client
Backend-agnostic OPC DA client library for Rust — async, trait-based, with transparent COM management.
§Quick Start
use opc_da_client::{OpcDaClient, OpcProvider};
let client = OpcDaClient::default();
let servers = client.list_servers("localhost").await?;§Feature Flags
| Flag | Default | Effect |
|---|---|---|
opc-da-backend | ✅ | Native OPC DA backend via windows-rs |
test-support | ❌ | Enables MockOpcProvider via mockall |
§Platform
Windows only — OPC DA is built on COM/DCOM.
Modules§
Macros§
- try_
from_ native - Helper macro for instantiating native COM structs from safe types.
Structs§
- Browse
Capabilities - Native browse features available from an OPC DA server.
- Browse
Node - One address-space node returned by
OpcProvider::browse_page. - Browse
Node Token - Opaque identifier for a node returned by a browse session.
- Browse
Page - One bounded page of immediate address-space children.
- Browse
Page Request - Parameters for one bounded, non-recursive browse operation.
- Browse
Page Token - Opaque continuation token for the next bounded browse page.
- Browse
Session Token - Opaque identifier for a browse session owned by the COM worker.
- ComConnector
- Real COM-backed server connector implementation.
- ComGuard
- Drop guard for COM thread initialization.
- Group
Handle - Opaque handle for an OPC group.
- Inventory
Completed - Terminal result for one inventory operation.
- Inventory
Control - Control handle for a running inventory.
- Inventory
Entry - One selectable OPC DA item discovered during inventory.
- Inventory
Options - Options controlling one bounded namespace inventory.
- Inventory
Progress - Progress emitted between bounded inventory operations.
- Inventory
Stream - Cancellable stream of bounded inventory events.
- Item
Handle - Opaque handle for an OPC item.
- Mock
OpcProvider - Async trait for OPC DA operations.
- OpcDa
Client - Concrete
OpcProviderimplementation for Windows OPC DA. - TagValue
- A single tag’s read result.
- Write
Result - Result of a single write operation.
Enums§
- Browse
Namespace - OPC DA address-space organization reported by a server.
- Browse
Node Filter - Node-kind filter for a one-level browse request.
- Browse
Node Kind - Kinds of nodes returned by a native browse.
- Inventory
Event - Event emitted by
InventoryStream. - OpcError
- Centralized error enum for the OPC DA client.
- OpcValue
- Typed value to write to an OPC DA tag.
Traits§
- OpcProvider
- Async trait for OPC DA operations.
Functions§
- format_
hresult - Helper to format HRESULT with friendly hints.
- friendly_
com_ hint - Maps an
OpcErrorto a friendly COM hint if it is a COM error. - log_
opc_ error - Emits a structured
tracing::error!event with machine-parseable fields.
Type Aliases§
- OpcResult
- Result type alias for OPC DA operations.