locka99 / opcua

A client and server implementation of the OPC UA specification written in Rust
Mozilla Public License 2.0
504 stars 135 forks source link

Correct way of using the server's AddressSpace in method callbacks #386

Open DocDriven opened 3 weeks ago

DocDriven commented 3 weeks ago

I have a usecase where I want to update the OPC UA servers variable when a method is called. The callback function of this method retrieves values from a database and uses them for the update. For this reason, I need the server's address space inside the callback to access the server variables. However, I am not getting it to work properly as my clients get a timeout.

This is a minimal working example without the database-related code.

use opcua::server::{
    address_space::method::MethodBuilder, callbacks, prelude::*, session::SessionManager,
};
use opcua::sync::{Mutex, RwLock};
use std::path::PathBuf;
use std::sync::Arc;

struct GetTankSystemParamsMethod {
    fill_pct_node: NodeId,
    address_space: Arc<RwLock<AddressSpace>>,
}

impl callbacks::Method for GetTankSystemParamsMethod {
    fn call(
        &mut self,
        _session_id: &NodeId,
        _session_map: Arc<RwLock<SessionManager>>,
        _request: &CallMethodRequest,
    ) -> Result<CallMethodResult, StatusCode> {
        let fill_pct: f64 = 50.0; // value from database

        // Write retrieved values to the server
        let now = DateTime::now();
        let mut address_space = self.address_space.write();
        address_space.set_variable_value(self.fill_pct_node.clone(), fill_pct, &now, &now);

        // Return the result in the answer
        Ok(CallMethodResult {
            status_code: StatusCode::Good,
            input_argument_results: None,
            input_argument_diagnostic_infos: None,
            output_arguments: Some(vec![fill_pct.into()]),
        })
    }
}

fn main() {
    let server_config = ServerConfig::load(&PathBuf::from("server.conf")).unwrap();
    let server = Server::new(server_config);
    let address_space = server.address_space();

    let tank_system_type_ident = NodeId::next_numeric(1);
    {
        let mut address_space = address_space.write();

        // Create the custom datatype
        ObjectTypeBuilder::new(
            &tank_system_type_ident,
            QualifiedName::new(1, "tankSystemType"),
            "tankSystemType",
        )
        .is_abstract(false)
        .subtype_of(ObjectTypeId::BaseObjectType)
        .insert(&mut address_space);

        let fill_pct_attr_ident = NodeId::next_numeric(1);
        VariableBuilder::new(
            &fill_pct_attr_ident,
            QualifiedName::new(1, "FillPercentage"),
            "FillPercentage",
        )
        .data_type(&DataTypeId::Double)
        .property_of(tank_system_type_ident.clone())
        .has_type_definition(VariableTypeId::PropertyType)
        .has_modelling_rule(ObjectId::ModellingRule_Mandatory)
        .value_rank(-1)
        .insert(&mut address_space);

        // instantiate the custom data type
        let tank_system_1_ident = NodeId::next_numeric(1);
        ObjectBuilder::new(
            &tank_system_1_ident,
            QualifiedName::new(1, "tankSystem1"),
            "tankSystem1",
        )
        .organized_by(address_space.objects_folder().node_id())
        .has_type_definition(tank_system_type_ident.clone())
        .insert(&mut address_space);

        let fill_pct_ident = NodeId::next_numeric(1);
        VariableBuilder::new(
            &fill_pct_ident,
            QualifiedName::new(1, "FillPercentage"),
            "FillPercentage",
        )
        .data_type(&DataTypeId::Double)
        .property_of(tank_system_1_ident.clone())
        .has_type_definition(VariableTypeId::PropertyType)
        .value(Variant::Double(0.0))
        .insert(&mut address_space);

        /*
         * Add methods to server
         */
        let get_tank_system_params_method_ident = NodeId::next_numeric(1);
        MethodBuilder::new(
            &get_tank_system_params_method_ident,
            QualifiedName::new(1, "getTankSystemParams"),
            "getTankSystemParams",
        )
        .component_of(tank_system_1_ident.clone())
        .output_args(
            &mut address_space,
            &[
                ("FillPercentage", DataTypeId::Double).into(),
            ],
        )
        .callback(Box::new(GetTankSystemParamsMethod {
            fill_pct_node: fill_pct_ident.clone(),
            address_space: server.address_space(),
        }))
        .insert(&mut address_space);
    }
    server.run();
}

I do not get an error, but the execution seems to stop inside the callback function with the line

    let mut address_space = self.address_space.write();

I would be grateful for some help regarding this issue, maybe there is a more elegant way of accessing the address_space that I am not aware of.

einarmo commented 1 day ago

This is unfortunately a flaw with the current server implementation. If you see here, the server locks the address space before calling methods, meaning that if you try to lock it again you get a deadlock.

I believe this is not the case on the async-server branch, but that's also a very major rewrite of the server SDK, so your existing code would need to change.