Server API for plugins

SignalK server provides an interface to allow plugins to access / update the full data model, operations and send / receive deltas (updates).

These functions are available via the app passed to the plugin when it is invoked.


Accessing the Data Model

app.getPath(path)

Returns the entry for the provided path starting from the root of the full data model.

Example:

let baseStations = app.getPath('shore.basestations');

// baseStations:
{
  'urn:mrn:imo:mmsi:2766140': {
    url: 'basestations',
    navigation: { position: {latitude: 45.2, longitude: 76.4} },
    mmsi: '2766140'
  },
  'urn:mrn:imo:mmsi:2766160': {
    url: 'basestations',
    navigation: { position: {latitude: 46.9, longitude: 72.22} },
    mmsi: '2766160'
  }
}

app.getSelfPath(path)

Returns the entry for the provided path starting from vessels.self in the full data model.

Example:

let uuid = app.getSelfPath('uuid');
// Note: This is synonymous with app.getPath('vessels.self.uuid')

app.debug(uuid); 
// urn:mrn:signalk:uuid:a9d2c3b1-611b-4b00-8628-0b89d014ed60

app.registerPutHandler(context, path, callback, source)

Register a handler to action PUT requests for a specific path.

The action handler can handle the request synchronously or asynchronously.

The callback parameter should be a function which accepts the following arguments:

  • context
  • path
  • value
  • callback

For synchronous actions, the handler must return a value describing the response of the request:

{
  state: 'COMPLETED',
  statusCode: 200
}

or

{
 state:'COMPLETED',
 statusCode: 400,
 message:'Some Error Message'
}

The statusCode value can be any valid HTTP response code.

For asynchronous actions, that may take considerable time to complete and the requester should not be kept waiting for the result, the handler must return:

{ state: 'PENDING' }

When the action has completed the handler should call the callback function with the result:

callback({ state: 'COMPLETED', statusCode: 200 })

or

callback({
  state:'COMPLETED',
  statusCode: 400,
  message:'Some Error Message'
})

Example: Synchronous response:

function myActionHandler(context, path, value, callback) {
  if(doSomething(context, path, value)){
    return { state: 'COMPLETED', statusCode: 200 };
  } else {
    return { state: 'COMPLETED', statusCode: 400 };
  }
}

plugin.start = (options) => {
  app.registerPutHandler('vessels.self', 'some.path', myActionHandler, 'somesource.1');
}

Example: Asynchronous response:

function myActionHandler(context, path, value, callback) {

  doSomethingAsync(context, path, value, (result) =>{
    if(result) {
      callback({ state: 'COMPLETED', result: 200 })
    } else {
      callback({ state: 'COMPLETED', result: 400 })
    }
  });

  return { state: 'PENDING' };
}

plugin.start = (options) => {
  app.registerPutHandler('vessels.self', 'some.path', myActionHandler);
}

Working with Deltas

app.handleMessage(pluginId, delta, skVersion = 'v1')

Emit a delta message.

Note: These deltas are handled by the server in the same way as any other incoming deltas.

Example:

app.handleMessage('my-signalk-plugin', {
  updates: [
    {
      values: [
        {
          path: 'navigation.courseOverGroundTrue',
          value: 1.0476934
        }
      ]
    }
  ]
});

Plugins emitting deltas that use Signal K v2 paths (like the Course API paths) should call handleMessage with the optional skVersion parameter set to v2. This prevents v2 API data getting mixed in v1 paths' data in full data model & the v1 http API.

Omitting the skVersion parameter will cause the delta to be sent as v1.

app.streambundle.getBus(path)

Get a Bacon JS stream for a Signal K path that will stream values from any context.

The path parameter is optional. If it is not provided the returned stream produces values for all paths.

Stream values are objects with the following structure:

  {
    path: ...,
    value: ...,
    context: ...,
    source: ...,
    $source: ...,
    timestamp: ...
  }

Example:

app.streambundle
  .getBus('navigation.position')
  .forEach(pos => app.debug(pos));
  
// output
{
  path: 'navigation.position',
  value: { longitude: 24.7366117, latitude: 59.72493 },
  context: 'vessels.urn:mrn:imo:mmsi:2766160',
  source: {
    label: 'n2k-sample-data',
    type: 'NMEA2000',
    pgn: 129039,
    src: '43'
  },
  '$source': 'n2k-sample-data.43',
  timestamp: '2014-08-15T19:00:02.392Z'
}
{
  path: 'navigation.position',
  value: { longitude: 24.82365, latitude: 58.159598 },
  context: 'vessels.urn:mrn:imo:mmsi:2766140',
  source: {
    label: 'n2k-sample-data',
    type: 'NMEA2000',
    pgn: 129025,
    src: '160'
  },
  '$source': 'n2k-sample-data.160',
  timestamp: '2014-08-15T19:00:02.544Z'
}

app.streambundle.getSelfBus(path)

Get a Bacon JS stream for path from the vessels.self context.

The path parameter is optional. If it is not provided the returned stream contains values for all paths.

Example:

app.streambundle
  .getSelfBus('navigation.position')
  .forEach(pos => app.debug(pos));

// output
{
  path: 'navigation.position',
  value: { longitude: 24.7366117, latitude: 59.72493 },
  context: 'vessels.urn:mrn:signalk:uuid:a9d2c3b1-611b-4b00-8628-0b89d014ed60',
  source: {
    label: 'n2k-sample-data',
    type: 'NMEA2000',
    pgn: 129039,
    src: '43'
  },
  '$source': 'n2k-sample-data.43',
  timestamp: '2014-08-15T19:00:02.392Z'
}
{
  path: 'navigation.position',
  value: { longitude: 24.7366208, latitude: 59.7249198 },
  context: 'vessels.urn:mrn:signalk:uuid:a9d2c3b1-611b-4b00-8628-0b89d014ed60',
  source: {
    label: 'n2k-sample-data',
    type: 'NMEA2000',
    pgn: 129025,
    src: '160'
  },
  '$source': 'n2k-sample-data.160',
  timestamp: '2014-08-15T19:00:02.544Z'
}

app.streambundle.getSelfStream(path)

Get a Bacon JS stream for a path in the vessels.self context.

The path argument is optional. If it is not provided the returned stream produces values for all paths.

Note: This is similar to app.streambundle.getSelfBus(path), except that the stream values contain only the value property from the incoming deltas.

Example:

app.streambundle
  .getSelfStream('navigation.position')
  .forEach(pos => app.debug(pos));

// output

  my-signalk-plugin { longitude: 24.736677, latitude: 59.7250108 } +600ms
  my-signalk-plugin { longitude: 24.736645, latitude: 59.7249883 } +321ms
  my-signalk-plugin { longitude: 24.7366563, latitude: 59.7249807 } +174ms
  my-signalk-plugin { longitude: 24.7366563, latitude: 59.724980699999996 } +503ms

app.streambundle.getAvailablePaths()

Get a list of available full data model paths maintained by the server.

Example:

app.streambundle.getAvailablePaths();

// returns
[
  "navigation.speedOverGround",
  "navigation.courseOverGroundTrue",
  "navigation.courseGreatCircle.nextPoint.position",
  "navigation.position",
  "navigation.gnss.antennaAltitude",
  "navigation.gnss.satellites",
  "navigation.gnss.horizontalDilution",
  "navigation.gnss.positionDilution",
  "navigation.gnss.geoidalSeparation",
  "navigation.gnss.type","navigation.gnss.methodQuality",
  "navigation.gnss.integrity",
  "navigation.magneticVariation",
]

app.registerDeltaInputHandler ((delta, next) => {} )

Register a function to intercept all delta messages before they are processed by the server.

The callback function should call next(delta) with either:

  • A modified delta (if it wants to alter the incoming delta)
  • With the original delta to process it normally.

Note: Not calling next(delta) will cause the incoming delta to be dropped and will only show in delta statistics.

Other, non-delta messages produced by provider pipe elements are emitted normally.

app.registerDeltaInputHandler((delta, next) => {
  delta.updates.forEach(update => {
    update.values.forEach(pathValue => {
      if(pathValue.startsWith("foo")) {
        pathValue.path = "bar"
      }
    })
  })
  next(delta)
});

Configuration

app.savePluginOptions(options, callback)

Save changes to the plugin's configuration options.

Example:

let options = {
  myConfigValue = 'Something the plugin calculated'
};

app.savePluginOptions(options, () => {app.debug('Plugin options saved')});

app.readPluginOptions()

Read the stored plugin configuration options.

Example:

let options = app.readPluginOptions();

app.getDataDirPath()

Returns the full path of the directory where the plugin can persist its internal data, e.g. data files, etc.

Example:

let myDataFile = require('path').join( app.getDataDirPath(), 'somedatafile.ext')

Messages and Debugging

app.setPluginStatus(msg)

Set the current status of the plugin that is displayed in the plugin configuration UI and the Dashboard.

The msg parameter should be a short text message describing the current status of the plugin.

Example:

app.setPluginStatus('Initializing');
// Do something
app.setPluginStatus('Done initializing');

Note: Replaces deprecated setProviderStatus()

app.setPluginError(msg)

Set the current error status of the plugin that is displayed in the plugin configuration UI and the Dashboard.

The msg parameter should be a short text message describing the current status of the plugin.

Example:

app.setPluginError('Error connecting to database');

Note: Replaces deprecated setProviderError()

app.debug(...)

Log debug messages.

This function exposes the debug method from the debug module. The npm module name is used as the debug name.

app.debug() can take any type and will serialize it before outputting.

_Note: Do not use debug from the debug module directly! Using app.debug()provided by the server ensures that the plugin taps into the server's debug logging system, including the helper switches in Admin UI's Server Log page.

app.error(message)

Report errors in a human-oriented message. Currently just logs the message, but in the future error messages hopefully will show up in the admin UI.

reportOutputMessages(count)

Report to the server that the plugin has sent data to other hosts so it can be displayed on the Dashboard.

Note: This function is for use when the plugin is sending data to hosts other than the Signal K server (e.g. network packets, http requests or messages sent to a broker).

This function should NOT be used for deltas that the plugin sends with handleMessage()!

The count parameter is optional and represents the number of messages sent between this call the previous call. If omitted the call will count as one output message.

Example:

app.reportOutputMessages(54);

Serial Port

app.getSerialPorts() => Promise<Ports>

This returns a Promise which will resolve to a Ports object which contains information about the serial ports available on the machine.


Resources API Interface

app.registerResourceProvider(ResourceProvider)

Used by Resource Provider plugins to register each resource type it handles.

See Resource Provider Plugins for details.

app.resourcesApi.setResource(resource_type, resource_id, resource_data, provider_id?)

Create / update value of the resource with the supplied SignalK resource_type and resource_id.

Note: Requires a registered Resource Provider for the supplied resource_type.

  • resource_type: Any Signal K (i.e. routes,waypoints, notes, regions & charts) or user defined resource types.

  • resource_id: The resource identifier. (e.g. ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a)

  • resource_data: A complete and valid resource record.

  • provider_id (optional): The id of the Resource Provider plugin to use to complete the request. Most commonly used for creating a new resource entry when more than one provider is registered for the specified resource type.

  • returns: Promise<void>

Example:

app.resourcesApi.setResource(
  'waypoints',
  'ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a',
  {
    "position": {"longitude": 138.5, "latitude": -38.6}, 
    "feature": {
      "type":"Feature", 
      "geometry": {
        "type": "Point", 
        "coordinates": [138.5, -38.6] 
      }, 
      "properties":{} 
    }
  }
).then ( () => {
  // success
  ...
}).catch (error) { 
  // handle error
  console.log(error.message);
  ...
}

app.resourcesApi.deleteResource(resource_type, resource_id, provider_id?)

Delete the resource with the supplied SignalK resource_type and resource_id.

Note: Requires a registered Resource Provider for the supplied resource_type.

  • resource_type: Any Signal K (i.e. routes,waypoints, notes, regions & charts) or user defined resource types.

  • resource_id: The resource identifier. (e.g. ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a)

  • provider_id (optional): The id of the Resource Provider plugin to use to complete the request.

  • returns: Promise<void>

Example:

app.resourcesApi.deleteResource(
  'notes', 
  'ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a'
).then ( () => {
  // success
  ...
}).catch (error) { 
  // handle error
  console.log(error.message);
  ...
}

app.resourcesApi.listResources(resource_type, params, provider_id?)

Retrieve collection of resource entries of the supplied resource_type matching the provided criteria.

Note: Requires a registered Resource Provider for the supplied resource_type.

  • resource_type: Any Signal K (i.e. routes,waypoints, notes, regions & charts) or user defined resource types.

  • params: Object contining key | value pairs repesenting the crteria by which to filter the returned entries.

  • provider_id (optional): The id of the Resource Provider plugin to use to complete the request.

Note: The registered Resource Provider must support the supplied parameters for results to be filtered.

  • returns: Promise<{[key: string]: any}>

Example:

app.resourcesApi.listResources(
  'waypoints', 
  {region: 'fishing_zone'}
).then (data => {
  // success
  console.log(data);
  ...
}).catch (error) { 
  // handle error
  console.log(error.message);
  ...
}

Course API Interface

The Course API provides the following functions for use by plugins.

app.getCourse()

Retrieves the current course information.

  • returns: Resolved Promise on success containing the same course information returned by the /course API endpoint.

app.clearDestination()

Cancels navigation to the current point or route being followed.

  • returns: Resolved Promise on success.

app.setDestination(dest)

Set course to a specified position / waypoint.

  • dest: Object containing destination position information as per /course/destination.

  • returns: Resolved Promise on success.

app.activateRoute(rte)

Follow a route. in the specified direction and starting at the specified point.

  • rte: Object containing route information as per /course/activeRoute.

  • returns: Resolved Promise on success.


Notifications API (proposed)

app.notify(path, value, pluginId)

Notifications API interface method for raising, updating and clearing notifications.

  • path: Signal K path of the notification

  • value: A valid Notification object or null if clearing a notification.

  • pluginId The plugin identifier.

To raise or update a for a specified path, call the method with a valid Notification object as the value.

  • returns: string value containing the id of the new / updated notification.

Example:

const alarmId = app.notify(
  'myalarm', 
  {
	message: 'My cutom alarm text',
	state: 'alert'
  },
  'myAlarmPlugin'
)

// alarmId = "ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a"

To clear (cancel) a notification call the method with null as the value.

  • returns: void.

Example: Clear notification

const alarmId = app.notify(
  'myalarm', 
  null,
  'myAlarmPlugin'
)

PropertyValues

The PropertyValues mechanism provides a means for passing configuration type values between different components running in the server process such as plugins and input connections.

A plugin can both emit values and listen for values emitted by others.

The difference between the PropertyValues mechanism and Event Emitters in NodeJs is that when onPropertyValues is called, the callback() function will be invoked and passed an array containing all of the previous values for that property name, starting with the initial value of undefined. If no values have been emitted for that property name the callback will be invoked with a value of undefined.

app.emitPropertyValue: (name: string, value: any) => void

onPropertyValues: (
  name: string,
  callback: (propValuesHistory: PropertyValue[]) => void
) => Unsubscribe

PropertyValue has the following structure:

interface PropertyValue {
  timestamp: number // millis
  setter: string // plugin id, server, provider id
  name: string
  value: any
}

Note that the value can be also a function.

This mechanism allows plugins to offer extensions via "Well Known Properties", for example

Code handling incoming PropertyValues should be fully reactive due to:

  • Plugins being able to emit PropertyValues when they activated and / or started
  • There being no defined load / startup order for plugins / connections.

So even if all plugins / connections emit during their startup, you cannot depend on a specific PropertyValue being available. It may be present when your code starts or it may arrive after your code has started.

Note: The PropertyValues mechanism is not intended to be used for data passing on a regular basis, as the total history makes it a potential memory leak.

To safeguard against a component accidentally emitting regularly, via a fixed upper bound is enforced for the value array per property name. New values will be ignored if the upper bound is reached and are logged as errors.


Exposing custom HTTP paths & OpenApi

Plugins are able to provide an API via a function called registerWithRouter(router), which like the plugin's start and stop functions, will be called during plugin startup with an Express router as the parameter.

The router will be mounted at /plugins/<pluginId> and you can use standard Express (.get() .post() .use(), etc) methods to add HTTP path handlers.

Note: GET /plugins/<pluginid> and POST /plugins/<pluginid>/configure are reserved by server (see below).

It should be noted that Express does not have a public API for deregistering subrouters, so stop does not do anything to the router.

If a plugin does provide an API, it is strongly recommended that it provide an OpenApi description to document its operation.

Doing so promotes interoperability with other plugins / webapps by making it easy to find and use the functionality built into plugins. It is also a means to avoid duplication, promote reuse and the possibility of including them in the Signal K specification.

See Server Plugins for details.


Plugin configuration HTTP API

GET /plugins/

Get a list of installed plugins and their configuration data.

GET /plugins/<pluginid>

Get information from an installed plugin.

Example result:

{
  "enabled": false,
  "id": "marinetrafficreporter",
  "name": "Marine Traffic Reporter"
}

POST /plugins/<pluginid>/configure

Save configuration data for a plugin. Stops and starts the plugin as a side effect.