locka99/opcua

repository·master·Indexed 20 days ago

https://github.com/locka99/opcua

A suite of NodeJS-based tooling and Rust implementations for OPC UA. It provides tools to automate the translation of OPC UA schema definitions (XML Nodesets, BSD files, CSVs) into type-safe Rust modules, along with various samples including a demo-server, a chess-server, and an MQTT bridging client.

Tokens
28.3K
Snippets
91
Records
149
Agent score
69%

What's inside locka99-opcua

  1. Understand OPC UA Feature Support and Limitations

    master

    This implementation focuses on the opc.tcp:// binary protocol. It does not support binary over https:// or XML-based transport.

    Server Capabilities

    The server implements the Base Server and Embedded UA profiles. Supported services include:

    • Discovery: GetEndpoints (Note: FindServers and RegisterServer are currently stubs returning BadNotSupported).
    • Attribute: Read, Write, History Read, and History Update (History services require implementing server-side callbacks).
    • Session: CreateSession, ActivateSession, CloseSession, and Cancel (stub).
    • Node Management: AddNodes, AddReferences, DeleteNodes, DeleteReferences.
    • View: Browse, BrowseNext, TranslateBrowsePathsToNodeIds.
    • MonitoredItem: CreateMonitoredItems (supports data change filters with dead band and event filters), ModifyMonitoredItems, SetMonitoringMode, SetTriggering, DeleteMonitoredItems.
    • Subscription: CreateSubscription, ModifySubscription, DeleteSubscriptions, Publish, Republish, SetPublishingMode (Note: TransferSubscriptions is a stub that fails).
    • Method: Call.

    Client Capabilities

    The client API is synchronous (calls return when a response is received or a timeout occurs). It supports all server services listed above, plus:

    • FindServers: To find other servers when connected to a discovery server.
    • RegisterServer: To register a server when connected to a discovery server.

    Current Limitations

    • No Diagnostics: Diagnostic information is not supplied for requests.
    • No Session Resumption: Disconnections cause all session information to be discarded.
    • Static Nodeset: The default nodeset is mostly static; server info fields use default values unless explicitly set.
    • Limited Access Control: Permissions are applied to all sessions for specific nodes.
    • Single Session per Transport: Multiple sessions cannot be created within a single transport.
  2. Project structure and crate organization

    master

    The project is organized into several specialized Rust crates, most of which are available on crates.io:

    • opcua-types: Machine-generated and handwritten OPC UA types.
    • opcua-core: Common functionality for both client and server (e.g., encoding/decoding chunks).
    • opcua-crypto: Encryption functionality.
    • opcua-client: Client-side API.
    • opcua-server: Server-side API (can optionally use opcua-client for discovery registration).
    • opcua-certificate-creator: CLI tool for creating OPC UA compatible public certificates and private keys.

    Other workspace components include:

    • samples/: Client and server examples.
    • tools/: Scripts for machine-generating status codes, structs, and NodeIds.
    • integration/: Integration tests.
  3. Manage certificates and keys with CertificateStore

    master

    The CertificateStore manages the PKI (Public Key Infrastructure) for the server. It handles the storage and validation of certificates and private keys.

    • Storage Format: Certificates and private keys are stored on disk as PEM-encoded files.
    • Organization: The store uses different directories to distinguish between accepted and rejected certificates.
    • Implementation: While the store logic is written in Rust, it utilizes OpenSSL to perform PEM reading/writing and to validate certificate contents.
  4. Mapping OPC UA primitives to Rust types

    master

    The implementation maps OPC UA specification types to their idiomatic Rust equivalents:

    OPC UA TypeRust Type
    Booleanbool
    SBytei8
    Byteu8
    Int16i16
    UInt16u16
    Int32i32
    UInt32u32
    Int64i64
    UInt64u64
    Floatf32
    Doublef64

    Note on Strings: The OPC UA String type is mapped to UAString in Rust. This is because OPC UA distinguishes between a null value and an empty string. UAString is a struct holding an Option<String>, where None represents a null value.

  5. Understand the opcua-crypto crate and its dependencies

    master

    The opcua-crypto crate provides the cryptographic functionality used by both the OPC UA server and client. It acts as a wrapper around the openssl crate to provide necessary functions while attempting to hide OpenSSL's internal complexity from the rest of the codebase.

    Note on Dependencies: Because it relies on OpenSSL (a C library), users may encounter configuration challenges during setup. The project aims to eventually move toward pure-Rust implementations using crates like ring, webpki, or rcgen, but currently, OpenSSL is the primary provider for X509 and complex cryptographic operations.

  6. Understand the Synchronous Client API behavior

    master

    The current client-side API follows a synchronous external / asynchronous internal pattern.

    When you call a client function, the call is synchronous from your perspective: the function waits for the operation to execute or fail before returning control to your code. Internally, however, the library performs these operations asynchronously using Tokio.

    Note for developers: This bridge is implemented using Arc<RwLock<Session>> to manage state between the synchronous caller and the asynchronous background tasks.

  7. Managing Security and Sessions

    master

    Security is established during the connection to an endpoint. For secure endpoints, the client and server exchange X509 certificates to establish trust. Once trust is established, they create an encrypted channel using the specified security policy's algorithms. They may also choose to sign/verify packets.

    Activating a Session: After connecting to an endpoint, the client must present an identity token to activate a session. The server uses this identity to determine authorization (e.g., allowing anonymous users to read but requiring credentials to write).

    Supported Identity Tokens:

    • Anonymous: No credentials provided.
    • User/pass: Username and password.
    • X509: A certificate associated with the user.
  8. Understand client PKI and certificate handling

    master

    The client manages its own Public Key Infrastructure (PKI) for secure connections:

    • Folder Creation: At startup, the client checks for a pki/ folder and creates it if it is missing.
    • Certificate Generation: If a client certificate does not already exist, the client will generate one automatically.
    • Secure Connections: When connecting via signed or signed/encrypted channels, the client presents its certificate to the server.
    • Server Trust: Servers may reject unrecognized certificates. Depending on your server's configuration, you may need to manually add the client's certificate to the server's trusted list.
    • Server Trust (Client Side): The client's client.conf is pre-configured to automatically trust the server's certificate, so no manual client-side trust configuration is required for the server's certificate.
  9. How the OPC UA server lifecycle works

    master

    The lifecycle of an OPC UA server follows these steps:

    1. Configuration: Create or load a configuration defining TCP addresses, ports, endpoints, and user identities.
    2. Instantiation: Create the Server instance from the configuration.
    3. Address Space Setup: Populate the address space with nodes (objects, variables, etc.) and register callbacks or timers.
    4. Execution: Run the server (e.g., via server.run()).
    5. Operation: The server runs indefinitely, listening for client connections.
  10. How asynchronous I/O and sessions work

    master

    The project uses tokio for asynchronous I/O and timers. Sessions are modeled as state machines that transition through several stages:

    • New: Initial state.
    • WaitingHello: Waiting for a client to send a HEL message.
    • ProcessMessages: The main processing state.
    • Finished(StatusCode): The session has ended. The StatusCode indicates if the termination was Good or an OPC UA error.

    Server Task Lifecycle

    When a server accepts a socket, it spawns several concurrent tasks to manage the session:

    1. Hello timeout task: Monitors the connection for a HELLO message; sets the state to Finished on timeout.
    2. Reading task: Waits for complete messages to arrive via an mpsc sender.
    3. Writing task: Waits for messages to write via an mpsc receiver.
    4. Finished monitor task: Checks for the Finished state to trigger cleanup.

    Any task (timeout, encoding error, etc.) can set the session to Finished. When this happens, the socket is closed and all associated tasks terminate.

  11. Manage variable values via manual updates, getters, or timers

    master

    There are three primary ways to handle variable values in the address space:

    1. Manual Updates (Push): Set the value directly in your code (e.g., from a thread or event) using address_space.set_variable_value. This is useful for asynchronous updates.
    2. Getters (Pull): Register an AttrFnGetter on a variable. The server will invoke this function whenever a client requests the value. The getter receives parameters like &NodeId, TimestampsToReturn, AttributeId, NumericRange, and &QualifiedName to allow conditional responses.
    3. Timers: Use the built-in timer mechanism to invoke a callback at regular intervals to update values.
    // Manual update example
    let now = DateTime::now();
    let value = 123.456f;
    let node_id = NodeId::new(2, "myvalue");
    let _ = address_space.set_variable_value(node_id, value, &now, &now);
    
    // Getter example
    let node_id = NodeId::new(2, "myvalue");
    if let Some(ref mut v) = address_space.find_variable_mut(node_id.clone()) {
        let getter = AttrFnGetter::new(
            move |_, _, _, _, _, _| -> Result<Option<DataValue>, StatusCode> {
                Ok(Some(DataValue::new_now(123.456f)))
            },
        );
        v.set_value_getter(Arc::new(Mutex::new(getter)));
    }