signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
838 lines (783 loc) • 47.2 kB
HTML
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Server API - Signal K Server Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="A Guide for users and developers.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="../../favicon.png">
<link rel="stylesheet" href="../../css/variables.css">
<link rel="stylesheet" href="../../css/general.css">
<link rel="stylesheet" href="../../css/chrome.css">
<link rel="stylesheet" href="../../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../../highlight.css">
<link rel="stylesheet" href="../../tomorrow-night.css">
<link rel="stylesheet" href="../../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var html = document.querySelector('html');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<a href="/" style="width:155px;height:30px;background-image:url(/signal-k-logo-image-text.svg);display:inline-block;background-repeat:no-repeat;background-position:center center;background-size:150px auto;"></a>
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../../index.html">Introduction</a></li><li class="chapter-item expanded affix "><li class="part-title">Getting Started</li><li class="chapter-item expanded "><a href="../../installation/install.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../installation/raspberry_pi_installation.html"><strong aria-hidden="true">1.1.</strong> Installing on Raspberry Pi</a></li><li class="chapter-item expanded "><a href="../../installation/updating.html"><strong aria-hidden="true">1.2.</strong> Updating your installation</a></li><li class="chapter-item expanded "><a href="../../installation/command_line.html"><strong aria-hidden="true">1.3.</strong> Runtime environment & options</a></li></ol></li><li class="chapter-item expanded "><a href="../../security.html"><strong aria-hidden="true">2.</strong> Security</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../setup/generating_tokens.html"><strong aria-hidden="true">2.1.</strong> Generating tokens</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">Setup</li><li class="chapter-item expanded "><a href="../../setup/configuration.html"><strong aria-hidden="true">3.</strong> Configuration</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../setup/seatalk/seatalk.html"><strong aria-hidden="true">3.1.</strong> Seatalk Connections</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">Feature How Tos</li><li class="chapter-item expanded "><a href="../../features/anchoralarm/anchoralarm.html"><strong aria-hidden="true">4.</strong> Anchor Alarm</a></li><li class="chapter-item expanded "><a href="../../features/navdataserver/navdataserver.html"><strong aria-hidden="true">5.</strong> NMEA0183 Server</a></li><li class="chapter-item expanded "><a href="../../features/datalogging/datalogging.html"><strong aria-hidden="true">6.</strong> Data Logging</a></li><li class="chapter-item expanded affix "><li class="part-title">Support</li><li class="chapter-item expanded "><a href="../../support/help.html"><strong aria-hidden="true">7.</strong> Help & Support</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../support/faq.html"><strong aria-hidden="true">7.1.</strong> FAQs</a></li></ol></li><li class="chapter-item expanded "><a href="../../support/sponsor.html"><strong aria-hidden="true">8.</strong> Sponsor</a></li><li class="chapter-item expanded affix "><li class="part-title">Develop</li><li class="chapter-item expanded "><a href="../../develop/developer_notes.html"><strong aria-hidden="true">9.</strong> Notes for Developers</a></li><li class="chapter-item expanded "><a href="../../whats_new.html"><strong aria-hidden="true">10.</strong> What's New in V2</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../breaking_changes.html"><strong aria-hidden="true">10.1.</strong> Changes & Deprecations</a></li></ol></li><li class="chapter-item expanded "><a href="../../develop/webapps.html"><strong aria-hidden="true">11.</strong> WebApps</a></li><li class="chapter-item expanded "><a href="../../develop/plugins/server_plugin.html"><strong aria-hidden="true">12.</strong> Plugins</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../develop/plugins/deltas.html"><strong aria-hidden="true">12.1.</strong> Processing Data</a></li><li class="chapter-item expanded "><a href="../../develop/plugins/server_plugin_api.html" class="active"><strong aria-hidden="true">12.2.</strong> Server API</a></li><li class="chapter-item expanded "><a href="../../develop/plugins/resource_provider_plugins.html"><strong aria-hidden="true">12.3.</strong> Resource Providers</a></li><li class="chapter-item expanded "><a href="../../develop/rest-api/course_calculations.html"><strong aria-hidden="true">12.4.</strong> Course Providers</a></li><li class="chapter-item expanded "><a href="../../develop/plugins/autopilot_provider_plugins.html"><strong aria-hidden="true">12.5.</strong> Autopilot Providers</a></li></ol></li><li class="chapter-item expanded "><a href="../../develop/plugins/publishing.html"><strong aria-hidden="true">13.</strong> Publishing to the AppStore</a></li><li class="chapter-item expanded "><a href="../../develop/rest-api/open_api.html"><strong aria-hidden="true">14.</strong> REST APIs</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../develop/rest-api/course_api.html"><strong aria-hidden="true">14.1.</strong> Course API</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="../../develop/rest-api/course_calculations.html"><strong aria-hidden="true">14.1.1.</strong> Course Calculations</a></li></ol></li><li class="chapter-item expanded "><a href="../../develop/rest-api/resources_api.html"><strong aria-hidden="true">14.2.</strong> Resources API</a></li><li class="chapter-item expanded "><a href="../../develop/rest-api/notifications_api.html"><strong aria-hidden="true">14.3.</strong> Notifications API</a></li><li class="chapter-item expanded "><a href="../../develop/rest-api/autopilot_api.html"><strong aria-hidden="true">14.4.</strong> Autopilot API</a></li><li class="chapter-item expanded "><a href="../../develop/rest-api/anchor_api.html"><strong aria-hidden="true">14.5.</strong> Anchor API</a></li></ol></li><li class="chapter-item expanded "><a href="../../develop/contributing.html"><strong aria-hidden="true">15.</strong> Contribute</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Signal K Server Documentation</h1>
<div class="right-buttons">
<a href="../../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://github.com/SignalK/signalk-server/tree/master/docs/src" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="server-api-for-plugins"><a class="header" href="#server-api-for-plugins">Server API for plugins</a></h1>
<p>SignalK server provides an interface to allow plugins to:</p>
<ul>
<li>Discover Features.</li>
<li>Access / update the full data model</li>
<li>send / receive deltas (updates)</li>
<li>Interact with APIs</li>
<li>Expose HTTP endpoints</li>
</ul>
<p>These functions are available via the <code>app</code> object passed to the plugin when it is invoked.</p>
<hr />
<h3 id="discover-features"><a class="header" href="#discover-features">Discover Features</a></h3>
<h4 id="appgetfeaturesenabed"><a class="header" href="#appgetfeaturesenabed"><code>app.getFeatures(enabed)</code></a></h4>
<p>Returns an object detailing the available APIs and Plugins.</p>
<p>The <code>enabled</code> parameter is optional and has the following values:</p>
<ul>
<li><code>undefined</code> (not provided): list all features</li>
<li><code>true</code>: list only enabled features</li>
<li><code>false</code>: list only disabled features</li>
</ul>
<p><em>Example:</em></p>
<pre><code class="language-javascript">let features = app.getFeatures();
{
"apis": [
"resources","course"
],
"plugins": [
{
"id": "anchoralarm",
"name": "Anchor Alarm",
"version": "1.13.0",
"enabled": true
},
{
"id": "autopilot",
"name": "Autopilot Control",
"version": "1.4.0",
"enabled": false
},
{
"id": "sk-to-nmea2000",
"name": "Signal K to NMEA 2000",
"version": "2.17.0",
"enabled": false
},
{
"id": "udp-nmea-sender",
"name": "UDP NMEA0183 Sender",
"version": "2.0.0",
"enabled": false
}
]
}
</code></pre>
<h3 id="accessing-the-data-model"><a class="header" href="#accessing-the-data-model">Accessing the Data Model</a></h3>
<h4 id="appgetpathpath"><a class="header" href="#appgetpathpath"><code>app.getPath(path)</code></a></h4>
<p>Returns the entry for the provided path starting from the <code>root</code> of the full data model.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">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'
}
}
</code></pre>
<h4 id="appgetselfpathpath"><a class="header" href="#appgetselfpathpath"><code>app.getSelfPath(path)</code></a></h4>
<p>Returns the entry for the provided path starting from <code>vessels.self</code> in the full data model.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">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
</code></pre>
<h4 id="appregisterputhandlercontext-path-callback-source"><a class="header" href="#appregisterputhandlercontext-path-callback-source"><code>app.registerPutHandler(context, path, callback, source)</code></a></h4>
<p>Register a handler to action <a href="http://signalk.org/specification/1.3.0/doc/put.html"><code>PUT</code></a> requests for a specific path.</p>
<p>The action handler can handle the request synchronously or asynchronously.</p>
<p>The <code>callback</code> parameter should be a function which accepts the following arguments:</p>
<ul>
<li><code>context</code></li>
<li><code>path</code></li>
<li><code>value</code></li>
<li><code>callback</code></li>
</ul>
<p>For synchronous actions, the handler must return a value describing the response of the request:</p>
<pre><code class="language-javascript">{
state: 'COMPLETED',
statusCode: 200
}
</code></pre>
<p>or</p>
<pre><code class="language-javascript">{
state:'COMPLETED',
statusCode: 400,
message:'Some Error Message'
}
</code></pre>
<p>The <code>statusCode</code> value can be any valid HTTP response code.</p>
<p>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:</p>
<pre><code class="language-javascript">{ state: 'PENDING' }
</code></pre>
<p>When the action has completed the handler should call the <code>callback</code> function with the result:</p>
<pre><code class="language-javascript">callback({ state: 'COMPLETED', statusCode: 200 })
</code></pre>
<p>or</p>
<pre><code class="language-javascript">callback({
state:'COMPLETED',
statusCode: 400,
message:'Some Error Message'
})
</code></pre>
<p><em>Example: Synchronous response:</em></p>
<pre><code class="language-javascript">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');
}
</code></pre>
<p><em>Example: Asynchronous response:</em></p>
<pre><code class="language-javascript">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);
}
</code></pre>
<hr />
<h3 id="working-with-deltas"><a class="header" href="#working-with-deltas">Working with Deltas</a></h3>
<h4 id="apphandlemessagepluginid-delta-skversion--v1"><a class="header" href="#apphandlemessagepluginid-delta-skversion--v1"><code>app.handleMessage(pluginId, delta, skVersion = 'v1')</code></a></h4>
<p>Emit a delta message.</p>
<p><em>Note: These deltas are handled by the server in the same way as any other incoming deltas.</em></p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.handleMessage('my-signalk-plugin', {
updates: [
{
values: [
{
path: 'navigation.courseOverGroundTrue',
value: 1.0476934
}
]
}
]
});
</code></pre>
<p>Plugins emitting deltas that use Signal K v2 paths (like the <a href="http://localhost:3000/admin/openapi/?urls.primaryName=course">Course API</a> paths) should call <code>handleMessage</code> with the optional <code>skVersion</code> parameter set to <code>v2</code>. This prevents v2 API data getting mixed in v1 paths' data in full data model & the v1 http API.</p>
<p>Omitting the <code>skVersion</code> parameter will cause the delta to be sent as <code>v1</code>.</p>
<h4 id="appstreambundlegetbuspath"><a class="header" href="#appstreambundlegetbuspath"><code>app.streambundle.getBus(path)</code></a></h4>
<p>Get a <a href="https://baconjs.github.io/">Bacon JS</a> stream for a Signal K path that will stream values from any context.</p>
<p>The <code>path</code> parameter is optional. If it is not provided the returned stream produces values for all paths.</p>
<p>Stream values are objects with the following structure:</p>
<pre><code class="language-javascript"> {
path: ...,
value: ...,
context: ...,
source: ...,
$source: ...,
timestamp: ...
}
</code></pre>
<p><em>Example:</em></p>
<pre><code class="language-javascript">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'
}
</code></pre>
<h4 id="appstreambundlegetselfbuspath"><a class="header" href="#appstreambundlegetselfbuspath"><code>app.streambundle.getSelfBus(path)</code></a></h4>
<p>Get a <a href="https://baconjs.github.io/">Bacon JS</a> stream for path from the <code>vessels.self</code> context.</p>
<p>The <code>path</code> parameter is optional. If it is not provided the returned stream contains values for all paths.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">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'
}
</code></pre>
<h4 id="appstreambundlegetselfstreampath"><a class="header" href="#appstreambundlegetselfstreampath"><code>app.streambundle.getSelfStream(path)</code></a></h4>
<p>Get a <a href="https://baconjs.github.io/">Bacon JS</a> stream for a path in the <code>vessels.self</code> context.</p>
<p>The <code>path</code> argument is optional. If it is not provided the returned stream produces values for all paths.</p>
<p><em>Note: This is similar to <code>app.streambundle.getSelfBus(path)</code>, except that the stream values contain only the <code>value</code> property from the incoming deltas.</em></p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">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
</code></pre>
<h4 id="appstreambundlegetavailablepaths"><a class="header" href="#appstreambundlegetavailablepaths"><code>app.streambundle.getAvailablePaths()</code></a></h4>
<p>Get a list of available full data model paths maintained by the server.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">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",
]
</code></pre>
<h4 id="appregisterdeltainputhandler-delta-next---"><a class="header" href="#appregisterdeltainputhandler-delta-next---"><code>app.registerDeltaInputHandler ((delta, next) => {} )</code></a></h4>
<p>Register a function to intercept all delta messages <em>before</em> they are processed by the server.</p>
<p>The callback function should call <code>next(delta)</code> with either:</p>
<ul>
<li>A modified delta (if it wants to alter the incoming delta)</li>
<li>With the original delta to process it normally.</li>
</ul>
<p><em>Note: Not calling <code>next(delta)</code> will cause the incoming delta to be dropped and will only show in delta statistics.</em></p>
<p>Other, non-delta messages produced by provider pipe elements are emitted normally.</p>
<pre><code class="language-javascript">app.registerDeltaInputHandler((delta, next) => {
delta.updates.forEach(update => {
update.values.forEach(pathValue => {
if(pathValue.startsWith("foo")) {
pathValue.path = "bar"
}
})
})
next(delta)
});
</code></pre>
<hr />
<h3 id="configuration"><a class="header" href="#configuration">Configuration</a></h3>
<h4 id="appsavepluginoptionsoptions-callback"><a class="header" href="#appsavepluginoptionsoptions-callback"><code>app.savePluginOptions(options, callback)</code></a></h4>
<p>Save changes to the plugin's configuration options.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">let options = {
myConfigValue = 'Something the plugin calculated'
};
app.savePluginOptions(options, () => {app.debug('Plugin options saved')});
</code></pre>
<h4 id="appreadpluginoptions"><a class="header" href="#appreadpluginoptions"><code>app.readPluginOptions()</code></a></h4>
<p>Read the stored plugin configuration options.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">let options = app.readPluginOptions();
</code></pre>
<h4 id="appgetdatadirpath"><a class="header" href="#appgetdatadirpath"><code>app.getDataDirPath()</code></a></h4>
<p>Returns the full path of the directory where the plugin can persist its internal data, e.g. data files, etc.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">let myDataFile = require('path').join( app.getDataDirPath(), 'somedatafile.ext')
</code></pre>
<hr />
<h3 id="messages-and-debugging"><a class="header" href="#messages-and-debugging">Messages and Debugging</a></h3>
<h4 id="appsetpluginstatusmsg"><a class="header" href="#appsetpluginstatusmsg"><code>app.setPluginStatus(msg)</code></a></h4>
<p>Set the current status of the plugin that is displayed in the plugin configuration UI and the Dashboard.</p>
<p>The <code>msg</code> parameter should be a short text message describing the current status of the plugin.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.setPluginStatus('Initializing');
// Do something
app.setPluginStatus('Done initializing');
</code></pre>
<p><em>Note: Replaces deprecated <code>setProviderStatus()</code></em></p>
<h4 id="appsetpluginerrormsg"><a class="header" href="#appsetpluginerrormsg"><code>app.setPluginError(msg)</code></a></h4>
<p>Set the current error status of the plugin that is displayed in the plugin configuration UI and the Dashboard.</p>
<p>The <code>msg</code> parameter should be a short text message describing the current status of the plugin.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.setPluginError('Error connecting to database');
</code></pre>
<p><em>Note: Replaces deprecated <code>setProviderError()</code></em></p>
<h4 id="appdebug"><a class="header" href="#appdebug"><code>app.debug(...)</code></a></h4>
<p>Log debug messages.</p>
<p>This function exposes the <code>debug</code> method from the <a href="https://www.npmjs.com/package/debug">debug module</a>.
The npm module name is used as the debug name.</p>
<p><code>app.debug()</code> can take any type and will serialize it before outputting.</p>
<p>_Note: Do not use <code>debug</code> from the debug module directly! Using <code>app.debug()</code>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.</p>
<h4 id="apperrormessage"><a class="header" href="#apperrormessage"><code>app.error(message)</code></a></h4>
<p>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.</p>
<h4 id="reportoutputmessagescount"><a class="header" href="#reportoutputmessagescount"><code>reportOutputMessages(count)</code></a></h4>
<p>Report to the server that the plugin has sent data to other hosts so it can be displayed on the Dashboard.</p>
<p><em>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).</em></p>
<p><em><strong>This function should NOT be used for deltas that the plugin sends with <code>handleMessage()</code>!</strong></em></p>
<p>The <code>count</code> parameter is <em>optional</em> and represents the number of messages sent between this call the previous
call. If omitted the call will count as one output message.</p>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.reportOutputMessages(54);
</code></pre>
<hr />
<h3 id="serial-port"><a class="header" href="#serial-port">Serial Port</a></h3>
<h4 id="appgetserialports--promiseports"><a class="header" href="#appgetserialports--promiseports"><code>app.getSerialPorts() => Promise<Ports></code></a></h4>
<p>This returns a Promise which will resolve to a Ports object which contains information about the serial ports available on the machine.</p>
<hr />
<h3 id="resources-api-interface"><a class="header" href="#resources-api-interface">Resources API Interface</a></h3>
<h4 id="appregisterresourceproviderresourceprovider"><a class="header" href="#appregisterresourceproviderresourceprovider"><code>app.registerResourceProvider(ResourceProvider)</code></a></h4>
<p>Used by <em>Resource Provider plugins</em> to register each resource type it handles.</p>
<p>See <a href="../plugins/resource_provider_plugins.html#registering-as-a-resource-provider"><code>Resource Provider Plugins</code></a> for details.</p>
<h4 id="appresourcesapigetresourceresource_type-resource_id-provider_id"><a class="header" href="#appresourcesapigetresourceresource_type-resource_id-provider_id"><code>app.resourcesApi.getResource(resource_type, resource_id, provider_id?)</code></a></h4>
<p>Retrieve the resource with the supplied SignalK resource_type and resource_id.</p>
<p><em>Note: Requires a registered Resource Provider for the supplied <code>resource_type</code>.</em></p>
<ul>
<li>
<p><code>resource_type</code>: Any Signal K <em>(i.e. <code>routes</code>,<code>waypoints</code>, <code>notes</code>, <code>regions</code> & <code>charts</code>)</em>
or user defined resource types.</p>
</li>
<li>
<p><code>resource_id</code>: The resource identifier. <em>(e.g. <code>ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a</code>)</em></p>
</li>
<li>
<p><code>provider_id</code> (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.</p>
</li>
<li>
<p>returns: <code>Promise<any></code> Promise for the resource.</p>
</li>
</ul>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.resourcesApi.getResource(
'waypoints',
'ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a'
}
).then ( () => {
// success
...
}).catch (error) {
// handle error
console.log(error.message);
...
}
</code></pre>
<h4 id="appresourcesapisetresourceresource_type-resource_id-resource_data-provider_id"><a class="header" href="#appresourcesapisetresourceresource_type-resource_id-resource_data-provider_id"><code>app.resourcesApi.setResource(resource_type, resource_id, resource_data, provider_id?)</code></a></h4>
<p>Create / update value of the resource with the supplied SignalK resource_type and resource_id.</p>
<p><em>Note: Requires a registered Resource Provider for the supplied <code>resource_type</code>.</em></p>
<ul>
<li>
<p><code>resource_type</code>: Any Signal K <em>(i.e. <code>routes</code>,<code>waypoints</code>, <code>notes</code>, <code>regions</code> & <code>charts</code>)</em>
or user defined resource types.</p>
</li>
<li>
<p><code>resource_id</code>: The resource identifier. <em>(e.g. <code>ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a</code>)</em></p>
</li>
<li>
<p><code>resource_data</code>: A complete and valid resource record.</p>
</li>
<li>
<p><code>provider_id</code> (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.</p>
</li>
<li>
<p>returns: <code>Promise<void></code></p>
</li>
</ul>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.resourcesApi.setResource(
'waypoints',
'ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a',
{
"name": "My Waypoint",
"feature": {
"type":"Feature",
"geometry": {
"type": "Point",
"coordinates": [138.5, -38.6]
},
"properties":{}
}
}
).then ( () => {
// success
...
}).catch (error) {
// handle error
console.log(error.message);
...
}
</code></pre>
<h4 id="appresourcesapideleteresourceresource_type-resource_id-provider_id"><a class="header" href="#appresourcesapideleteresourceresource_type-resource_id-provider_id"><code>app.resourcesApi.deleteResource(resource_type, resource_id, provider_id?)</code></a></h4>
<p>Delete the resource with the supplied SignalK resource_type and resource_id.</p>
<p><em>Note: Requires a registered Resource Provider for the supplied <code>resource_type</code>.</em></p>
<ul>
<li>
<p><code>resource_type</code>: Any Signal K <em>(i.e. <code>routes</code>,<code>waypoints</code>, <code>notes</code>, <code>regions</code> & <code>charts</code>)</em>
or user defined resource types.</p>
</li>
<li>
<p><code>resource_id</code>: The resource identifier. <em>(e.g. <code>ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a</code>)</em></p>
</li>
<li>
<p><code>provider_id</code> (optional): The id of the Resource Provider plugin to use to complete the request.</p>
</li>
<li>
<p>returns: <code>Promise<void></code></p>
</li>
</ul>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.resourcesApi.deleteResource(
'notes',
'ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a'
).then ( () => {
// success
...
}).catch (error) {
// handle error
console.log(error.message);
...
}
</code></pre>
<h4 id="appresourcesapilistresourcesresource_type-params-provider_id"><a class="header" href="#appresourcesapilistresourcesresource_type-params-provider_id"><code>app.resourcesApi.listResources(resource_type, params, provider_id?)</code></a></h4>
<p>Retrieve collection of resource entries of the supplied resource_type matching the provided criteria.</p>
<p><em>Note: Requires a registered Resource Provider for the supplied <code>resource_type</code>.</em></p>
<ul>
<li>
<p><code>resource_type</code>: Any Signal K <em>(i.e. <code>routes</code>,<code>waypoints</code>, <code>notes</code>, <code>regions</code> & <code>charts</code>)</em>
or user defined resource types.</p>
</li>
<li>
<p><code>params</code>: Object contining <code>key | value</code> pairs repesenting the crteria by which to filter the returned entries.</p>
</li>
<li>
<p><code>provider_id</code> (optional): The id of the Resource Provider plugin to use to complete the request.</p>
</li>
</ul>
<p><strong>Note: The registered Resource Provider must support the supplied parameters for results to be filtered.</strong></p>
<ul>
<li>returns: <code>Promise<{[key: string]: any}></code></li>
</ul>
<p><em>Example:</em></p>
<pre><code class="language-javascript">app.resourcesApi.listResources(
'waypoints',
{region: 'fishing_zone'}
).then (data => {
// success
console.log(data);
...
}).catch (error) {
// handle error
console.log(error.message);
...
}
</code></pre>
<hr />
<h3 id="course-api-interface"><a class="header" href="#course-api-interface">Course API Interface</a></h3>
<p>The <a href="../rest-api/course_api.html"><code>Course API</code></a> provides the following functions for use by plugins.</p>
<h4 id="appgetcourse"><a class="header" href="#appgetcourse"><code>app.getCourse()</code></a></h4>
<p>Retrieves the current course information.</p>
<ul>
<li>returns: Resolved Promise on success containing the same course information returned by the <a href="/doc/openapi/?urls.primaryName=course#/course/get_course"><code>/course</code></a> API endpoint.</li>
</ul>
<h4 id="appcleardestination"><a class="header" href="#appcleardestination"><code>app.clearDestination()</code></a></h4>
<p>Cancels navigation to the current point or route being followed.</p>
<ul>
<li>returns: Resolved Promise on success.</li>
</ul>
<h4 id="appsetdestinationdest"><a class="header" href="#appsetdestinationdest"><code>app.setDestination(dest)</code></a></h4>
<p>Set course to a specified position / waypoint.</p>
<ul>
<li>
<p><code>dest</code>: Object containing destination position information as per <a href="/doc/openapi/?urls.primaryName=course#/destination/put_course_destination"><code>/course/destination</code></a>.</p>
</li>
<li>
<p>returns: Resolved Promise on success.</p>
</li>
</ul>
<h4 id="appactivaterouterte"><a class="header" href="#appactivaterouterte"><code>app.activateRoute(rte)</code></a></h4>
<p>Follow a route.
in the specified direction and starting at the specified point.</p>
<ul>
<li>
<p><code>rte</code>: Object containing route information as per <a href="/doc/openapi/?urls.primaryName=course#/activeRoute/put_course_activeRoute"><code>/course/activeRoute</code></a>.</p>
</li>
<li>
<p>returns: Resolved Promise on success.</p>
</li>
</ul>
<hr />
<h3 id="propertyvalues"><a class="header" href="#propertyvalues">PropertyValues</a></h3>
<p>The <em>PropertyValues</em> mechanism provides a means for passing configuration type values between different components running in the server process such as plugins and input connections.</p>
<p>A plugin can both <em>emit</em> values and <em>listen</em> for values emitted by others.</p>
<p>The difference between the <em>PropertyValues</em> mechanism and <em>Event Emitters</em> in NodeJs is that when <code>onPropertyValues</code> is called, the <code>callback()</code> function will be invoked and passed an array containing all of the previous values for that <em>property name</em>, starting with the initial value of <code>undefined</code>. If no values have been emitted for that <em>property name</em> the callback will be invoked with a value of <code>undefined</code>.</p>
<pre><code class="language-typescript">app.emitPropertyValue: (name: string, value: any) => void
onPropertyValues: (
name: string,
callback: (propValuesHistory: PropertyValue[]) => void
) => Unsubscribe
</code></pre>
<p><strong>PropertyValue</strong> has the following structure:</p>
<pre><code class="language-typescript">interface PropertyValue {
timestamp: number // millis
setter: string // plugin id, server, provider id
name: string
value: any
}
</code></pre>
<p><em>Note that the value can be also a function.</em></p>
<p>This mechanism allows plugins to <em>offer</em> extensions via <em>"Well Known Properties"</em>, for example</p>
<ul>
<li>additional <a href="https://github.com/SignalK/nmea0183-signalk/pull/193">NMEA0183 sentence parsers for custom sentences</a> via <code>nmea0183sentenceParser</code></li>
<li>additional PGN definitions for propietary or custom PGNs</li>
</ul>
<p>Code handling incoming <em>PropertyValues</em> should be fully reactive due to:</p>
<ul>
<li>Plugins being able to emit <em>PropertyValues</em> when they activated and / or started</li>
<li>There being no defined load / startup order for plugins / connections.</li>
</ul>
<p>So even if all plugins / connections emit during their startup, you cannot depend on a specific <em>PropertyValue</em> being available. It may be present when your code starts or it may arrive after your code has started.</p>
<p><strong>Note: The <em>PropertyValues</em> mechanism is not intended to be used for data passing on a regular basis, as the total history makes it a potential memory leak.</strong></p>
<p>To safeguard against a component accidentally emitting regularly, via a fixed upper bound is enforced for the value array per <em>property name</em>. New values will be ignored if the upper bound is reached and are logged as errors.</p>
<hr />
<h3 id="exposing-custom-http-paths--openapi"><a class="header" href="#exposing-custom-http-paths--openapi">Exposing custom HTTP paths & OpenApi</a></h3>
<p>Plugins are able to provide an API via a function called <code>registerWithRouter(router)</code>, which like the plugin's <code>start</code> and <code>stop</code> functions, will be called during plugin startup with an <em>Express</em> router as the parameter.</p>
<p>The router will be mounted at <code>/plugins/<pluginId></code> and you can use standard <em>Express</em> <em>(<code>.get()</code> <code>.post()</code> <code>.use()</code>, etc)</em> methods to add HTTP path handlers.</p>
<p><em>Note: <code>GET /plugins/<pluginid></code> and <code>POST /plugins/<pluginid>/configure</code> are reserved by server (see below).</em></p>
<p>It should be noted that <em>Express</em> does not have a public API for deregistering subrouters, so <code>stop</code> does not do anything to the router.</p>
<p>If a plugin does provide an API, it is strongly recommended that it provide an <strong>OpenApi description</strong> to document its operation.</p>
<p>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.</p>
<p>See <a href="server_plugin.html#add-an-openapi-definition">Server Plugins</a> for details.</p>
<hr />
<h3 id="plugin-configuration-http-api"><a class="header" href="#plugin-configuration-http-api">Plugin configuration HTTP API</a></h3>
<h4 id="get-plugins"><a class="header" href="#get-plugins"><code>GET /plugins/</code></a></h4>
<p>Get a list of installed plugins and their configuration data.</p>
<h4 id="get-pluginspluginid"><a class="header" href="#get-pluginspluginid"><code>GET /plugins/<pluginid></code></a></h4>
<p>Get information from an installed plugin.</p>
<p>Example result:</p>
<pre><code class="language-json">{
"enabled": false,
"id": "marinetrafficreporter",
"name": "Marine Traffic Reporter"
}
</code></pre>
<h4 id="post-pluginspluginidconfigure"><a class="header" href="#post-pluginspluginidconfigure"><code>POST /plugins/<pluginid>/configure</code></a></h4>
<p>Save configuration data for a plugin. Stops and starts the plugin as a side effect.</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../../develop/plugins/deltas.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../../develop/plugins/resource_provider_plugins.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../../develop/plugins/deltas.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next" href="../../develop/plugins/resource_provider_plugins.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../../elasticlunr.min.js"></script>
<script src="../../mark.min.js"></script>
<script src="../../searcher.js"></script>
<script src="../../clipboard.min.js"></script>
<script src="../../highlight.js"></script>
<script src="../../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>