Extending HMI with X calls in Rust
Contents
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 = "0.0.1"
edition = "2021"
publish = false
[dependencies]
eva-common = { version = "0.3", features = ["events", "common-payloads", "payload", "acl"] }
eva-sdk = { version = "0.3", features = ["hmi"] }
tokio = { version = "1.36.0", features = ["full"] }
async-trait = { version = "0.1.77" }
serde = { version = "1.0.196", features = ["derive", "rc"] }
log = "0.4.20"
jemallocator = { version = "0.5.4" }
[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 serde::Deserialize;
err_logger!();
const AUTHOR: &str = "Bohemia Automation";
const VERSION: &str = env!("CARGO_PKG_VERSION");
const DESCRIPTION: &str = "Sensor state manipulations";
#[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 extra calls from HMI
"x" => {
if payload.is_empty() {
Err(RpcError::params(None))
} else {
let xp: XParamsOwned = unpack(payload)?;
match xp.method() {
"set" => {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
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());
eapi_bus::publish(&topic, pack(&event)?.into()).await?;
Ok(None)
}
_ => Err(RpcError::method(None)),
}
}
}
_ => svc_handle_default_rpc(method, &self.info),
}
}
}
// 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);
eapi_bus::init(&initial, Handlers { info }).await?;
initial.drop_privileges()?;
eapi_bus::init_logs(&initial)?;
svc_start_signal_handlers();
eapi_bus::mark_ready().await?;
info!("{} started ({})", DESCRIPTION, initial.id());
eapi_bus::block().await;
eapi_bus::mark_terminating().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
HTTP 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 }
);