Home Manual Reference Source Test

src/api.js

import { NodeClass } from 'node-opcua/lib/datamodel/nodeclass';
import { DataType, VariantArrayType } from 'node-opcua/lib/datamodel/variant';
import { StatusCodes } from 'node-opcua/lib/datamodel/opcua_status_code';
import Session from './lib/server/Session';
import NodeId from './lib/model/opcua/NodeId';

// Helpers
/**
 * Creates a callback that calls `resolve` on success and `reject` on error.
 * @param {function(result: any): void} resolve The resolve callback.
 * @param {function(error: Error): void} reject The reject callback.
 * @example
 * // `aCallbackFn` is a function that accepts a node-style callback as the last argument
 * const promise = new Promise(
 *   (resolve, reject) => aCallbackFn('other', 'args', promisifiedCallback(resolve, reject)
 * );
 */
function promisifiedCallback(resolve, reject) {
  return (err, result) => {
    if (err) {
      return reject(err);
    }
    return resolve(result);
  };
}

/**
 * Promisifies a async function that would otherwise require a callback.
 * @param {function(cb: function(error: Error, result: any)):Promise<any>} call A function that
 * accepts a callback and performs the async action to wrap.
 */
function promisified(call) {
  return new Promise((resolve, reject) => call(promisifiedCallback(resolve, reject)));
}

/**
 * Creates a session, runs `action` and closes the session.
 * @param {function(session: Session): Promise<any>} action The action to run a session.
 */
async function withSession(action) {
  const session = await Session.create();
  let result = null;
  let error = null;
  try {
    result = await action(session);
  } catch (e) {
    error = e;
  }

  await Session.close(session);

  if (error) {
    throw error;
  }

  return result;
}

// Reading/Writing

/**
 * Reads a single node's value.
 * @param {NodeId} nodeId The node to read.
 * @return {Promise<any>} The read value.
 */
export async function readNode(nodeId) {
  return withSession((session) => promisified((cb) => session.readVariableValue(nodeId, cb))).then(
    ({ value, statusCode }) => {
      if (statusCode !== StatusCodes.Good) {
        throw Object.assign(new Error(statusCode.description), { nodeId, statusCode });
      }

      return value;
    }
  );
}

/**
 * Writes a single node's value.
 * @param {NodeId} nodeId The node to write.
 * @param {Variant} value The value to write.
 * @return {Promise<node-opcua~StatusCodes} The operation status result.
 */
export function writeNode(nodeId, value) {
  return withSession((session) =>
    promisified((cb) => session.writeSingleNode(nodeId, value, cb))
  ).then((statusCode) => {
    if (statusCode !== StatusCodes.Good) {
      throw Object.assign(new Error(statusCode.description), { nodeId, statusCode });
    }

    return statusCode;
  });
}

// Methods / Scripts

/**
 * Calls an OPC-UA method on the server.
 * @param {NodeId} methodId The method's id.
 * @param {Array<Variant>} args The arguments to pass.
 */
export function callMethod(methodId, args = []) {
  return withSession((session) =>
    promisified((cb) =>
      session.call(
        [
          {
            objectId: methodId.parent,
            methodId,
            inputArguments: args,
          },
        ],
        cb
      )
    )
  ).then(([result] = []) => {
    if (result.statusCode.value) {
      throw Object.assign(new Error(result.statusCode.description), {
        methodId,
        inputArguments: args,
      });
    }

    return result;
  });
}

/**
 * Calls a server script on the server.
 * @param {NodeId} scriptId The script's id.
 * @param {Object} parameters The parameters to pass, given as a map of Variants, like
 * `{ name: { ... } }`.
 */
export function callScript(scriptId, parameters = {}) {
  return callMethod(new NodeId('AGENT.SCRIPT.METHODS.callScript'), [
    {
      dataType: DataType.NodeId,
      value: scriptId,
    },
    {
      dataType: DataType.NodeId,
      value: scriptId.parent,
    },
    {
      dataType: DataType.String,
      arrayType: VariantArrayType.Array,
      value: Object.keys(parameters),
    },
    {
      dataType: DataType.Variant,
      arrayType: VariantArrayType.Array,
      value: Object.values(parameters),
    },
  ]).then((result) => {
    const statusCode = result.outputArguments[0].value;

    if (statusCode.value) {
      throw Object.assign(
        new Error(`Script failed: ${statusCode.description}
${result.outputArguments[1].value}`),
        {
          scriptId,
          parameters,
        }
      );
    }

    return result;
  });
}

/**
 * Creates a new Node on the server.
 * @param {NodeId} nodeId The new node's id.
 * @param {Object} options The options to use.
 * @param {string} options.name The node's name.
 * @param {NodeId} [options.parentNodeId] The node's parent, defaults to the calculated parent
 * (`Test` for `Test.Child`).
 * @param {node-opcua~NodeClass} [options.nodeClass] The node's class, defaults so
 * `node-opcua~NodeClass.Variable`.
 * @param {NodeId} [options.typeDefinition] The node's type definition, must be provided for
 * non-variable nodes.
 * @param {NodeId} [options.modellingRule] The node's modelling rule.
 * @param {string} [options.reference] Name of the type of the node's reference to it's parent.
 * @param {node-opcua~Variant} [options.value] The node's value, required for all variable nodes.
 */
export function createNode(
  nodeId,
  {
    name,
    parentNodeId = nodeId.parent,
    nodeClass = NodeClass.Variable,
    typeDefinition = new NodeId('ns=0;i=62'),
    modellingRule,
    reference,
    value,
  }
) {
  const variableOptions =
    nodeClass.value === NodeClass.Variable.value
      ? {
          dataType: value.dataType.value,
          valueRank: value.arrayType ? value.arrayType.value : VariantArrayType.Scalar.value,
          value:
            value.arrayType && value.arrayType.value !== VariantArrayType.Scalar.value
              ? Array.from(value.value)
              : value.value,
        }
      : {};

  const is64Bit = value.dataType === DataType.Int64 || value.dataType === DataType.UInt64;
  if (is64Bit) {
    variableOptions.value = 0;
  }

  return callScript(new NodeId('SYSTEM.LIBRARY.ATVISE.SERVERSCRIPTS.atscm.CreateNode'), {
    paramObjString: {
      dataType: DataType.String,
      value: JSON.stringify(
        Object.assign(
          {
            nodeId,
            browseName: name,
            parentNodeId: parentNodeId || nodeId.parent,
            nodeClass: nodeClass.value,
            typeDefinition,
            modellingRule,
            reference,
          },
          variableOptions
        )
      ),
    },
  }).then(async (result) => {
    const [{ value: createdNode }] = result.outputArguments[3].value;

    if (createdNode && is64Bit) {
      await writeNode(nodeId, value);
    }

    return result;
  });
}

/**
 * Adds references to a node.
 * @param {NodeId} nodeId The node to add the references to.
 * @param {Object} references The references to add.
 * @return {Promise} Resolved once the references were added.
 * @example <caption>Add a simple reference</caption>
 * import { ReferenceTypeIds } from 'node-opcua/lib/opcua_node_ids';
 *
 * addReferences('AGENT.DISPLAYS.Main', {
 *   [47]: ['VariableTypes.ATVISE.Display'],
 *   // equals:
 *   [ReferenceTypeIds.HasTypeDefinition]: ['VariableTypes.ATVISE.Display'],
 * })
 *   .then(() => console.log('Done!'))
 *   .catch(console.error);
 */
export function addReferences(nodeId, references) {
  return callScript(new NodeId('SYSTEM.LIBRARY.ATVISE.SERVERSCRIPTS.atscm.AddReferences'), {
    paramObjString: {
      dataType: DataType.String,
      value: JSON.stringify({
        nodeId,
        references: Object.entries(references).map(([type, items]) => ({
          referenceIdValue: parseInt(type, 10),
          items,
        })),
      }),
    },
  });
}