Extending HMI with X calls in Rust

X call basics

HMI service can be extended with additional calls, called “X” calls.

When a client calls method x::svc_id::method, the following happens:

  • the target service gets EAPI calls to the method “x”

  • the call contains the following payload:

field

type

description

method

String

the target x-method of the service

params

Any

the target x-method params

aci

Map

ACI (API Call Info) data

acl

Map

the session ACL

  • the implemented method must check access control to resources manually if required

  • the result (or error) is returned to the client as-is

Service example

Let us create virtual sensors and allow clients to set item data using HTTP RPC calls to HMI, but for sensors only.

Create a virtual sensor, not assigned to real equipment:

eva item create sensor:tests/sensor1

Cargo.toml file:

[package]
name = "eva-svc-example-sensor-set"
version = "4.0.2"
edition = "2021"

[dependencies]
eva-common = { version = "0.3.3", features = ["events", "common-payloads", "payload", "acl"] }
eva-sdk = { version = "0.3.1", features = ["hmi"] }
tokio = { version = "1.20.1", features = ["full"] }
async-trait = { version = "0.1.51" }
serde = { version = "1.0.133", features = ["derive", "rc"] }
log = "0.4.14"
jemallocator = { version = "0.5.0" }
once_cell = "1.13.1"

[features]
std-alloc = []

Here is the service code, guided with comments in “X” call processing section. For the general information about services structure in Rust, read A simple service in Rust:

use eva_common::err_logger;
use eva_common::events::{RawStateEventOwned, RAW_STATE_TOPIC};
use eva_common::payload::{pack, unpack};
use eva_common::prelude::*;
use eva_sdk::hmi::XParamsOwned;
use eva_sdk::prelude::*;
use once_cell::sync::OnceCell;
use serde::Deserialize;
use std::sync::Arc;

err_logger!();

const AUTHOR: &str = "Bohemia Automation";
const VERSION: &str = env!("CARGO_PKG_VERSION");
const DESCRIPTION: &str = "Sensor state manipulations";

static RPC: OnceCell<Arc<RpcClient>> = OnceCell::new();

#[cfg(not(feature = "std-alloc"))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

// BUS/RT RPC handlers
struct Handlers {
    info: ServiceInfo,
}

#[async_trait::async_trait]
impl RpcHandlers for Handlers {
    async fn handle_call(&self, event: RpcEvent) -> RpcResult {
        svc_rpc_need_ready!();
        let method = event.parse_method()?;
        let payload = event.payload();
        match method {
            // handle x-calls from HMI
            "x" => {
                if payload.is_empty() {
                    Err(RpcError::params(None))
                } else {
                    let xp: XParamsOwned = unpack(payload)?;
                    match xp.method() {
                        "set" => {
                            #[derive(Deserialize)]
                            struct Params {
                                i: OID,
                                status: ItemStatus,
                                #[serde(default)]
                                value: ValueOptionOwned,
                            }
                            // deserialize params
                            let params = Params::deserialize(xp.params)?;
                            // check if the item is a sensor
                            if params.i.kind() != ItemKind::Sensor {
                                return Err(Error::access("can set states for sensors only").into());
                            }
                            // check if the caller's session is not a read-only one
                            xp.aci.check_write()?;
                            // check if the caller's ACL has write access to the provided OID
                            xp.acl.require_item_write(&params.i)?;
                            // set the sensor state
                            // in this example the service does not check does the sensor really
                            // exist in the core or not
                            let mut event = RawStateEventOwned::new0(params.status);
                            event.value = params.value;
                            let topic = format!("{}{}", RAW_STATE_TOPIC, params.i.as_path());
                            RPC.get()
                                .unwrap()
                                .client()
                                .lock()
                                .await
                                .publish(&topic, pack(&event)?.into(), QoS::Processed)
                                .await?;
                            Ok(None)
                        }
                        _ => Err(RpcError::method(None)),
                    }
                }
            }
            _ => svc_handle_default_rpc(method, &self.info),
        }
    }
    async fn handle_notification(&self, _event: RpcEvent) {}
    async fn handle_frame(&self, _frame_: Frame) {}
}

// The service configuration must be empty
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {}

#[svc_main]
async fn main(mut initial: Initial) -> EResult<()> {
    let _config: Config = Config::deserialize(
        initial
            .take_config()
            .ok_or_else(|| Error::invalid_data("config not specified"))?,
    )?;
    let info = ServiceInfo::new(AUTHOR, VERSION, DESCRIPTION);
    let rpc = initial.init_rpc(Handlers { info }).await?;
    RPC.set(rpc.clone())
        .map_err(|_| Error::core("Unable to set RPC"))?;
    initial.drop_privileges()?;
    let client = rpc.client().clone();
    svc_init_logs(&initial, client.clone())?;
    svc_start_signal_handlers();
    svc_mark_ready(&client).await?;
    info!("{} started ({})", DESCRIPTION, initial.id());
    svc_block(&rpc).await;
    svc_mark_terminating(&client).await?;
    Ok(())
}

Service template

The following template can be used to quickly create a service instance with eva-shell:

eva svc create my.svc.sensor_set svc-tpl.yml
command: path/to/eva-svc-example-sensor-set
bus:
  path: var/bus.ipc
config: {}
user: nobody
workers: 1

API call example

The service responds to the following API calls (httpie call example):

(
cat <<EOF
{
    "jsonrpc": "2.0",
    "id": 1, "method":
    "x::my.svc.sensor_set::set",
    "params": {
        "k": "mykey",
        "i": "sensor:tests/sensor1",
        "status": 1,
        "value": 25
        }
}
EOF
) | http :7727

If using EVA ICS WebEngine, the call can be made as:

eva.call(
    'x::my.svc.sensor_set::set',
    'sensor:tests/sensor1',
    { status: 1, value: 25 }
);