UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

838 lines (783 loc) 47.2 kB
<!DOCTYPE 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) =&gt; { 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) =&gt;{ if(result) { callback({ state: 'COMPLETED', result: 200 }) } else { callback({ state: 'COMPLETED', result: 400 }) } }); return { state: 'PENDING' }; } plugin.start = (options) =&gt; { 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 &amp; 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 =&gt; 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 =&gt; 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 =&gt; 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) =&gt; {} )</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) =&gt; { delta.updates.forEach(update =&gt; { update.values.forEach(pathValue =&gt; { 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, () =&gt; {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() =&gt; Promise&lt;Ports&gt;</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> &amp; <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&lt;any&gt;</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 ( () =&gt; { // 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> &amp; <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&lt;void&gt;</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 ( () =&gt; { // 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> &amp; <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&lt;void&gt;</code></p> </li> </ul> <p><em>Example:</em></p> <pre><code class="language-javascript">app.resourcesApi.deleteResource( 'notes', 'ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a' ).then ( () =&gt; { // 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> &amp; <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&lt;{[key: string]: any}&gt;</code></li> </ul> <p><em>Example:</em></p> <pre><code class="language-javascript">app.resourcesApi.listResources( 'waypoints', {region: 'fishing_zone'} ).then (data =&gt; { // 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) =&gt; void onPropertyValues: ( name: string, callback: (propValuesHistory: PropertyValue[]) =&gt; void ) =&gt; 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 &amp; 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/&lt;pluginId&gt;</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/&lt;pluginid&gt;</code> and <code>POST /plugins/&lt;pluginid&gt;/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/&lt;pluginid&gt;</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/&lt;pluginid&gt;/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>