Extending HMI with X calls in Python
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
Here is the service code, guided with comments in “X” call processing section. For the general information about services structure in Python, read A simple service in Python:
#!/opt/eva4/venv/bin/python
__version__ = '0.0.1'
import evaics.sdk as sdk
import busrt
from types import SimpleNamespace
from evaics.sdk import pack, unpack, OID, RAW_STATE_TOPIC, XCall
# define a global namespace
_d = SimpleNamespace(service=None)
# RPC calls handler
def handle_rpc(event):
# handle X calls from HMI
if event.method == b'x':
try:
xp = XCall(unpack(event.get_payload()))
if xp.method == 'set':
oid = OID(xp.params['i'])
status = xp.params['status']
# check if the item is a sensor
if oid.kind != 'sensor':
raise busrt.rpc.RpcException(
'can set state for sensors only',
sdk.ERR_CODE_ACCESS_DENIED)
# check if the caller's session is not a read-only one
xp.require_writable()
# check if the caller's ACL has write access to the provided OID
xp.require_item_write(oid)
# set the sensor state
# in this example the service does not check does the sensor
# really exist in the core or not
event = dict(status=status)
if 'value' in xp.params:
event['value'] = xp.params['value']
topic = f'{RAW_STATE_TOPIC}{oid.to_path()}'
_d.service.bus.send(
topic,
busrt.client.Frame(pack(event), tp=busrt.client.OP_PUBLISH))
return
else:
sdk.no_rpc_method()
except busrt.rpc.RpcException as e:
raise e
except Exception as e:
raise busrt.rpc.RpcException(str(e), sdk.ERR_CODE_FUNC_FAILED)
else:
sdk.no_rpc_method()
def run():
info = sdk.ServiceInfo(author='Bohemia Automation',
description='Sensor state manipulations',
version=__version__)
service = sdk.Service()
_d.service = service
service.init(info, on_rpc_call=handle_rpc)
service.block()
run()
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 }
);