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(¶ms.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 }
);