JavaScript/TypeScript SDK

This article provides an all-in-one example for JavaScript/TypeScript EVA ICS service.

JavaScript runtime

Note

EVA ICS has no built-in tools to install Node.js or other JavaScript server runtime. It must be installed manually.

JavaScript server runtime engines supported:

Other compatible engines can be used as well.

SDK module

The module can be found at: https://www.npmjs.com/package/@eva-ics/sdk and added to a project as:

npm i --save @eva-ics/sdk

API reference: https://pub.bma.ai/dev/docs/eva4-js-sdk/

See also: BUS/RT JavaScript/TypeScript client

Service example

The full example can be found at https://github.com/eva-ics/eva4/tree/main/bindings/js/example

Service code (TypeScript)

import {
  createService,
  ServiceInfo,
  ServiceMethod,
  Service,
  OID,
  pack,
  unpack,
  EventKind,
  Action,
  XCall,
  XCallData,
  EapiTopic,
  noRpcMethod,
  RpcEvent,
  Logger,
  sleep,
  exit
} from "@eva-ics/sdk";

let service: Service;
/** The service can log messages to stdout (info) and stderr (error) via
 * console.log methods but it is highly recommended to use the bus logger which
 * is available automatically after the service bus has been initialized */
let log: Logger;

/** Bus frame handler */
const onFrame = async (e: RpcEvent): Promise<void> => {
  if (!service.isActive()) {
    return;
  }
  const payload = unpack(e.frame.getPayload());
  // payloads may contain data, unserializable by the default JSON, so better
  // log them to stdout
  console.log(
    "sender:",
    e.frame.primary_sender,
    "topic:",
    e.frame.topic,
    "payload:",
    payload
  );
  if (e.frame.topic?.startsWith(EapiTopic.LocalStateTopic)) {
    const oid = new OID(
      e.frame.topic.slice(EapiTopic.LocalStateTopic.length),
      true
    );
    log.info(
      "received item state for",
      oid.asString(),
      `status: ${payload.status}`,
      `value: ${payload.value}`
    );
  }
};

/** RPC calls handler */
const onRpcCall = async (e: RpcEvent): Promise<Buffer | undefined> => {
  service.needReady();
  const method = e.method?.toString();
  const payload = unpack(e.getPayload());
  switch (method) {
    // process HTTP x-calls
    case "x":
      const xcall = new XCall(payload as XCallData);
      xcall.acl.requireAdmin();
      console.log(xcall);
      return pack({ ok: true });
    // process lmacro executions
    case "run":
      const lAction = new Action(payload);
      console.log(lAction);
      // mark action running
      await service.controller.eventRunning(lAction);
      // mark action failed
      await service.controller.eventFailed(
        lAction,
        "lmacro execution",
        "execution failed: not supported",
        -15
      );
      return;
    // process unit actions
    case "action":
      const uAction = new Action(payload);
      console.log(uAction);
      // mark action running
      await service.controller.eventRunning(uAction);
      // mark action completed
      await service.controller.eventCompleted(uAction, "all fine");
      // announce new unit state
      const path = uAction.oid.asPath();
      await service.bus.publish(
        `${EapiTopic.RawStateTopic}${path}`,
        pack({ status: 1, value: uAction.params?.value })
      );
      return;
    // example RPC method which deals with the params payload
    case "hello":
      return payload?.name ? pack(`hello ${payload.name}`) : pack("hi there");
    // get service initial payload
    case "config":
      return pack(service.initial);
    default:
      noRpcMethod();
  }
};

const main = async () => {
  // service info and RPC help
  const info = new ServiceInfo({
    author: "Bohemia Automation",
    description: "Example JS service",
    version: "0.0.1"
  })
    .addMethod(new ServiceMethod("hello").optional("name"))
    .addMethod(new ServiceMethod("config"));
  // create a service
  service = await createService({
    info,
    onFrame,
    onRpcCall
  });
  // set the global logger
  log = service.logger;
  // subscribe to item events
  await service.subscribeOIDs(["#"], EventKind.Any);
  // block the service while active
  log.warn("ready");
  await service.block();
  log.warn("exiting");
  // wait a bit to let tasks finish
  await sleep(500);
  // terminate the process with no error
  exit();
};

main();

package.json

{
  "name": "eva-ics-sdk-test",
  "version": "0.0.1",
  "main": "svc-example.ts",
  "scripts": {
    "build": "mkdir -p dist && npx esbuild svc-example.ts --bundle --platform=node --outfile=dist/out.js",
    "build-release": "mkdir -p dist && npx esbuild svc-example.ts --bundle --platform=node --minify --outfile=dist/out-release.js"
  },
  "license": "MIT",
  "dependencies": {
    "@eva-ics/sdk": "^0.0.8"
  },
  "devDependencies": {
    "@types/node": "^20.9.0",
    "esbuild": "^0.19.5"
  }
}

The service can be built as:

npm i
npm run build

Service template

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

eva svc create my.svc.js_test svc-tpl.yml
bus:
  path: var/bus.ipc
command: node /opt/eva4/bindings/js/example/dist/out.js
config:
  a: 2
  b: 3
user: nobody

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.js_test::some.method",
    "params": {
        "k": "mykey",
        "name": "value"
        }
}
EOF
) | http :7727

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

eva.call(
    'x::my.svc.js_test::some.method',
    { param: "value" }
);

Running with Deno

Installation

Deno can be installed directly into /opt/eva4 folder with the following command:

curl -fsSL https://deno.land/x/install/install.sh | DENO_INSTALL=/opt/eva4/deno sh

Deno caches all modules in $HOME/.cache/deno folder. The cached modules can be purged if required, e.g. when migrating to a newer SDK version.

SDK imports

To run services, built with EVA ICS JS SDK, with, it is possible to modify imports as the following:

import {
 // required imports
} from "npm:@eva-ics/sdk";

Service command

Run TypeScript service files directly with the following command field:

deno/bin/deno run --allow-read --allow-write --allow-ffi --allow-env --allow-net --unstable /path/to/svc.ts

It is recommended to start the service for the first time manually to let the runtime download all required modules or increase service startup timeout.