diff --git a/docs/src/develop/plugins/autopilot_provider_plugins.md b/docs/src/develop/plugins/autopilot_provider_plugins.md
index b8692ab4e..01ea757d1 100644
--- a/docs/src/develop/plugins/autopilot_provider_plugins.md
+++ b/docs/src/develop/plugins/autopilot_provider_plugins.md
@@ -1,12 +1,451 @@
-# Autopilot Provider Plugins
+# Autopilot Provider plugins
-#### (Under Development)
+_This document should be read in conjunction with the [SERVER PLUGINS](./server_plugin.md) document as it contains additional information regarding the development of plugins that implement the Signal K Autopilot API._
-_Note: This API is currently under development and the information provided here is likely to change._
+---
+## Overview
-The Signal K server [Autopilot API](../rest-api/autopilot_api.md) will provide a common set of operations for interacting with autopilot devices and (like the Resources API) will rely on a "provider plugin" to facilitate communication with the autopilot device.
+The Signal K Autopilot API defines endpoints under the path `/signalk/v2/api/vessels/self/steering/autopilots` providing a way for all Signal K clients to perform common autopilot operations independent of the autopilot device in use. The API is defined in an [OpenAPI](/doc/openapi/?urls.primaryName=autopilot) document.
-By de-coupling the operation requests from device communication provides the ability to support a wide variety of devices.
+Requests made to the Autopilot API are received by the Signal K Server, where they are validated and an authorisation check performed, before being passed on to a **provider plugin** to action the request on the autopilot device.
-[View the PR](https://github.com/SignalK/signalk-server/pull/1596) for more details.
+This de-coupling of request handling and autopilot communication provides the flexibility to support a variety of autopilot devices and ensures interoperability and reliabilty.
+
+Autopilot API requests are passed to a **provider plugin** which will process and action the request facilitating communication with the autopilot device.
+
+The following diagram provides an overview of the Autopilot API architectue.
+
+
+
+_Autopilot API architecture_
+
+
+## Provider Plugins:
+
+An autopilot provider plugin is a Signal K server plugin that implements the **Autopilot Provider Interface** which:
+- Tells server the autopilot devices provided for by the plugin
+- Registers the methods used to action requests passed from the server to perform autopilot operations.
+
+Note: multiple providers can be registered and each provider can manage one or more autopilot devices.
+
+The `AutopilotProvider` interface is defined as follows in _`@signalk/server-api`_:
+
+```typescript
+interface AutopilotProvider {
+ getData(deviceId: string): Promise
+ getState(deviceId: string): Promise
+ setState(state: string, deviceId: string): Promise
+ getMode(deviceId: string): Promise
+ setMode(mode: string, deviceId: string): Promise
+ getTarget(deviceId: string): Promise
+ setTarget(value: number, deviceId: string): Promise
+ adjustTarget(value: number, deviceId: string): Promise
+ engage(deviceId: string): Promise
+ disengage(deviceId: string): Promise
+ tack(direction: TackGybeDirection, deviceId: string): Promise
+ gybe(direction: TackGybeDirection, deviceId: string): Promise
+ dodge(direction: TackGybeDirection, deviceId: string): Promise
+}
+```
+
+**Note: An Autopilot Provider plugin MUST:**
+- Implement all Autopilot API interface methods.
+- Facilitate communication on the target autopilot device to send commands and retrieve both status and configuration information
+- Ensure the `engaged` path attribute value is maintained to reflect the operational status of the autopilot.
+- Map the `engage` and `disengage` operations to an appropriate autopilot device `state`.
+- Set the state as `off-line` if the autopilot device is not connected or unreachable.
+
+
+### Registering as an Autopilot Provider
+
+A provider plugin must register itself with the Autopilot API during start up by calling the `registerAutopilotProvider`.
+
+The function has the following signature:
+
+```typescript
+app.registerAutopilotProvider(provider: AutopilotProvider, devices: string[])
+```
+where:
+
+- `provider`: is a valid **AutopilotProvider** object
+- `devices`: is an array of identifiers for the autopilot devices managed by the plugin.
+
+_Example: Plugin registering as a routes & waypoints provider._
+```javascript
+import { AutopilotProvider } from '@signalk/server-api'
+
+module.exports = function (app) {
+
+ const plugin = {
+ id: 'mypluginid',
+ name: 'My autopilot Provider plugin'
+ }
+
+ const autopilotProvider: AutopilotProvider = {
+ getData: (deviceId: string) => { return ... },
+ getState: (deviceId: string) => { return ... },
+ setState: (state: string, deviceId: string) => { return true },
+ getMode: (deviceId: string) => { return ... },
+ setMode: (mode: string, deviceId: string) => { ... },
+ getTarget: (deviceId: string) => { return ... },
+ setTarget(value: number, deviceId: string) => { ... },
+ adjustTarget(value: number, deviceId: string) => { ... },
+ engage: (deviceId: string) => { ... },
+ disengage: (deviceId: string) => { ... },
+ tack:(direction: TackGybeDirection, deviceId: string) => { ... },
+ gybe:(direction: TackGybeDirection, deviceId: string) => { ... }
+ }
+
+ const pilots = ['pilot1', 'pilot2']
+
+ plugin.start = function(options) {
+ ...
+ try {
+ app.registerAutopilotProvider(autopilotProvider, pilots)
+ }
+ catch (error) {
+ // handle error
+ }
+ }
+
+ return plugin
+}
+```
+
+### Updates from Autopilot device
+
+Updates from an autopilot device are sent to the Autopillot API via the `autopilotUpdate` interface method.
+
+Typically an autopilot provider plugin will call `autopilotUpdate` when receiving data from the autopilot device.
+
+_Note: All updates originating from the autopilot device, regardless of the communications protocol (NMEA2000, etc) should be sent to the Autopilot API using `autopilotUpdate`._
+
+The function has the following signature:
+
+```typescript
+app.autopilotUpdate(deviceID: string, attrib: AutopilotUpdateAttrib, value: Value)
+```
+where:
+
+- `deviceId`: is the autopilot device identifier
+- `attrib`: is the attribute / path being updated
+- `value`: the new value.
+
+_Example:_
+```javascript
+app.autopilotUpdate('my-pilot', 'target', 1.52789)
+app.autopilotUpdate('my-pilot', 'mode', 'compass')
+```
+
+### Alarms from Autopilot device
+
+Alarms from an autopilot device are sent to the Autopillot API via the `autopilotAlarm` interface method.
+
+An autopilot provider plugin will call `autopilotAlarm` when the data received data from the autopilot device is an alarm.
+
+_Note: A set of normalised alarm names are defined and alarm messages from the autopilot device should be mapped to one of the following:_
+
+- `waypointAdvance`
+- `waypointArrival`
+- `routeComplete`
+- `xte`
+- `heading`
+- `wind`
+
+The function has the following signature:
+
+```typescript
+app.autopilotAlarm(deviceID: string, alarmName: AutopilotAlarm, value: Notification)
+```
+where:
+
+- `deviceId`: is the autopilot device identifier
+- `alarmName`: string containing a normalised alarm name.
+- `value`: is a Signal K Notification object.
+
+_Example:_
+```javascript
+app.autopilotAlarm('my-pilot', 'waypointAdvance', {
+ state: 'alert'
+ method: ['sound']
+ message: 'Waypoint Advance'
+})
+```
+
+
+### Provider Methods:
+
+**`getData(deviceId)`**: This method returns an AutopilotInfo object containing the current data values and valid options for the supplied autopilot device identifier.
+
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{AutopilotInfo}>`
+
+_Note: It is the responsibility of the autopilot provider plugin to map the value of `engaged` to the current `state`._
+
+
+_Example:_
+```
+GET signalk/v2/api/vessels/self/steering/autopilots/mypilot1
+```
+_AutopilotProvider method invocation:_
+```javascript
+getData('mypilot1');
+```
+
+_Returns:_
+```javascript
+{
+ options: {
+ states: [
+ {
+ name: 'auto' // autopilot state name
+ engaged: true // actively steering
+ },
+ {
+ name: 'standby' // autopilot state name
+ engaged: false // not actively steering
+ }
+ ]
+ modes: ['compass', 'gps', 'wind']
+},
+ target: 0.326
+ mode: 'compass'
+ state: 'auto'
+ engaged: true
+}
+```
+
+---
+**`getState(deviceId)`**: This method returns the current state of the supplied autopilot device identifier. If the autopilot device is not connected or unreachable then `off-line` should be returned.
+
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{string}>`
+
+_Example:_
+```
+GET signalk/v2/api/vessels/self/steering/autopilots/mypilot1/state
+```
+_AutopilotProvider method invocation:_
+```javascript
+getState('mypilot1');
+```
+
+_Returns:_
+```javascript
+'auto'
+```
+
+---
+**`setState(state, deviceI?)`**: This method sets the autopilot device with the supplied identifier to the supplied state value.
+
+- `state:` state value to set. Must be a valid state value.
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{boolean}>` indicating the new value of `engaged`.
+
+throws on error or if supplied state value is invalid.
+
+_Example:_
+```javascript
+PUT signalk/v2/api/vessels/self/steering/autopilots/mypilot1/state {value: "standby"}
+```
+_AutopilotProvider method invocation:_
+```javascript
+setState('standby', 'mypilot1');
+```
+
+_Returns:_
+```javascript
+false
+```
+
+---
+**`getMode(deviceId)`**: This method returns the current mode of the supplied autopilot device identifier.
+
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{string}>`
+
+_Example:_
+```
+GET signalk/v2/api/vessels/self/steering/autopilots/mypilot1/mode
+```
+_AutopilotProvider method invocation:_
+```javascript
+getMode('mypilot1');
+```
+
+_Returns:_
+```javascript
+'compass'
+```
+
+---
+**`setMode(mode, deviceId)`**: This method sets the autopilot device with the supplied identifier to the supplied mode value.
+
+- `mode:` mode value to set. Must be a valid mode value.
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error or if supplied mode value is invalid.
+
+_Example:_
+```javascript
+PUT signalk/v2/api/vessels/self/steering/autopilots/mypilot1/mode {value: "gps"}
+```
+_AutopilotProvider method invocation:_
+```javascript
+setMode('gps', 'mypilot1');
+```
+
+---
+**`setTarget(value, deviceId)`**: This method sets target for the autopilot device with the supplied identifier to the supplied value.
+
+- `value:` target value in radians.
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error or if supplied target value is outside the valid range.
+
+_Example:_
+```javascript
+PUT signalk/v2/api/vessels/self/steering/autopilots/mypilot1/target {value: 0.361}
+```
+_AutopilotProvider method invocation:_
+```javascript
+setTarget(0.361, 'mypilot1');
+```
+
+---
+**`adjustTarget(value, deviceId)`**: This method adjusts target for the autopilot device with the supplied identifier by the supplied value.
+
+- `value:` value in radians to add to current target value.
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error or if supplied target value is outside the valid range.
+
+_Example:_
+```javascript
+PUT signalk/v2/api/vessels/self/steering/autopilots/mypilot1/target {value: 0.361}
+```
+_AutopilotProvider method invocation:_
+```javascript
+adjustTarget(0.0276, 'mypilot1');
+```
+
+---
+**`engage(deviceId)`**: This method sets the state of the autopilot device with the supplied identifier to a state that is actively steering the vessel.
+
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error.
+
+_Example:_
+```javascript
+POST signalk/v2/api/vessels/self/steering/autopilots/mypilot1/engage
+```
+_AutopilotProvider method invocation:_
+```javascript
+engage('mypilot1');
+```
+
+---
+**`disengage(deviceId)`**: This method sets the state of the autopilot device with the supplied identifier to a state that is NOT actively steering the vessel.
+
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error.
+
+_Example:_
+```javascript
+POST signalk/v2/api/vessels/self/steering/autopilots/mypilot1/disengage
+```
+_AutopilotProvider method invocation:_
+```javascript
+disengage('mypilot1');
+```
+
+---
+**`tack(direction, deviceId)`**: This method instructs the autopilot device with the supplied identifier to perform a tack in the supplied direction.
+
+- `direction`: 'port' or 'starboard'
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error.
+
+_Example:_
+```javascript
+POST signalk/v2/api/vessels/self/steering/autopilots/mypilot1/tack/port
+```
+_AutopilotProvider method invocation:_
+```javascript
+tack('port', 'mypilot1');
+```
+
+---
+**`gybe(direction, deviceId)`**: This method instructs the autopilot device with the supplied identifier to perform a gybe in the supplied direction.
+
+- `direction`: 'port' or 'starboard'
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error.
+
+_Example:_
+```javascript
+POST signalk/v2/api/vessels/self/steering/autopilots/mypilot1/gybe/starboard
+```
+_AutopilotProvider method invocation:_
+```javascript
+gybe('starboard', 'mypilot1');
+```
+
+---
+**`dodge(direction, deviceId)`**: This method instructs the autopilot device with the supplied identifier to manually override the rudder position by two (2) degrees in the supplied direction.
+
+- `direction`: 'port' or 'starboard'
+- `deviceId:` identifier of the autopilot device to query.
+
+returns: `Promise<{void}>`
+
+throws on error.
+
+_Example:_
+```javascript
+POST signalk/v2/api/vessels/self/steering/autopilots/mypilot1/dodge/starboard
+```
+_AutopilotProvider method invocation:_
+```javascript
+dodge('starboard', 'mypilot1');
+```
+
+
+### Unhandled Operations
+
+A provider plugin **MUST** implement **ALL** Autopilot API interface methods, regardless of whether the operation is supported or not.
+
+For an operation that is not supported by the autopilot device, then the plugin should `throw` an exception.
+
+_Example:_
+```typescript
+{
+ // unsupported operation method definition
+ gybe: async (d, id) => {
+ throw new Error('Unsupprted operation!)
+ }
+}
+```
diff --git a/docs/src/develop/rest-api/autopilot_api.md b/docs/src/develop/rest-api/autopilot_api.md
index 21e5229a7..eea6923e2 100644
--- a/docs/src/develop/rest-api/autopilot_api.md
+++ b/docs/src/develop/rest-api/autopilot_api.md
@@ -1,38 +1,334 @@
-# Autopilot API
+# Working with the Autopilot API
-#### (Under Development)
-_Note: This API is currently under development and the information provided here is likely to change._
+## Overview
-The Signal K server Autopilot API will provide a common set of operations for interacting with autopilot devices and (like the Resources API) will rely on a "provider plugin" to facilitate communication with the autopilot device.
+The Autopilot API defines the `autopilots` path under the `steering` schema group _(e.g. `/signalk/v2/api/vessels/self/steering/autopilots`)_ for representing information from one or more autopilot devices.
-The Autopilot API will handle requests to `/steering/autopilot` paths and pass them to an Autopilot Provider plugin which will send the commands to the autopilot device.
+The Autopilot API provides a mechanism for applications to issue requests to autopilot devices to perform common operations. Additionally, when multiple autopilot devices are present, each autopilot device is individually addressable.
-The following operations are an example of the operations identified for implementation via HTTP `GET` and `PUT` requests:
+ _Note: Autopilot provider plugins are required to enable the API operation and provide communication with autopilot devices. See [Autopilot Provider Plugins](../plugins/autopilot_provider_plugins.md) for details._
-PUT `/steering/autopilot/engage` (engage / activate the autopilot)
+## The `Default` Autopilot
-PUT `/steering/autopilot/disengage` (disengage / deactivate the autopilot)
+To ensure a consistent API calling profile and to simplify client operations, the Autopilot API will assign a `default` autopilot device which is accessible using the path `./steering/autopilots/default`.
-GET `/steering/autopilot/state` (retrieve the current autopilot state)
+- When only one autopilot is present, it will be assigned as the `default`.
-PUT `/steering/autopilot/state` (set the autopilot state)
+- When multiple autopilots are present, and a `default` is yet to be assigned, one will be assigned when:
+ - An update is received from a provider plugin, the autopilot which is the source of the update will be assigned as the `default`.
+ - An API request is received, the first autopilot device registered, is assigned as the `default`.
+ - A request is sent to the `defaultPilot` API endpoint _(see [Setting the Default Autopilot](#setting-the-default-provider))_.
-GET `/steering/autopilot/mode` (retrieve the current autopilot mode)
+### Setting the Default Autopilot
-PUT `/steering/autopilot/mode` (set autopilot mode)
+To set / change the `default` autopilot, submit an HTTP `POST` request to `/signalk/v2/api/vessels/self/steering/autopilots/defaultPilot/{id}` where `{id}` is the identifier of the autopilot to use as the default.
-GET `/steering/autopilot/target` (get currrent target value)
+_Example:_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/defaultPilot/raymarine-id"
+```
-PUT `/steering/autopilot/target` (set the target value based on the selected mode)
+The autopilot with the supplied id will now be the target of requests made to `./steering/autopilots/default/*`.
-PUT `/steering/autopilot/target/adjust` (increment / decrement target value)
-PUT `/steering/autopilot/tack/port` (perform a port tack)
+### Getting the Default Autopilot
-PUT `/steering/autopilot/tack/starboard` (perform a starboard tack)
+To get the id of the `default` autopilot, submit an HTTP `GET` request to `/signalk/v2/api/vessels/self/steering/autopilots/defaultPilot`.
+_Example:_
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/defaultPilot"
+```
+_Response:_
+```JSON
+{
+ "id":"raymarine-id"
+}
+```
-[View the PR](https://github.com/SignalK/signalk-server/pull/1596) for more details.
+## Listing the available Autopilots
+To retrieve a list of installed autopilot devices, submit an HTTP `GET` request to `/signalk/v2/api/vessels/self/steering/autopilots`.
+
+The response will be an object containing all the registered autopilot devices, keyed by their identifier, detailing the `provider` it is registered by and whether it is assigned as the `default`.
+
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots"
+```
+_Example response: list of registered autopilots showing that `pypilot-id` is assigned as the `default`._
+
+```JSON
+{
+ "pypilot-id": {
+ "provider":"pypilot-provider",
+ "isDefault": true
+ },
+ "raymarine-id": {
+ "provider":"raymarine-provider",
+ "isDefault": false
+ }
+}
+```
+
+## Autopilot Deltas
+
+Deltas emitted by the Autopilot API will have the base path `steering.autopilot` with the `$source` containing the autopilot device identifier.
+
+_Example: Deltas for `autopilot.engaged` from two autopilots (`raymarine-id`)._
+```JSON
+{
+ "context":"vessels.self",
+ "updates":[
+ {
+ "$source":"pypilot-id",
+ "timestamp":"2023-11-19T06:12:47.820Z",
+ "values":[
+ {"path":"steering.autopilot.engaged","value":false}
+ ]
+ },
+ {
+ "$source":"raymarine-id",
+ "timestamp":"2023-11-19T06:12:47.820Z",
+ "values":[
+ {"path":"steering.autopilot.engaged","value":true}
+ ]
+ }
+ ]
+}
+```
+
+
+## Autopilot Notifications
+
+The Autopilot API will provide notifications under the path `notifications.steering.autopilot` with the `$source` containing the autopilot device identifier.
+
+A set of normalised notification paths are defined to provide a consistant way for client apps to receive and process alarm messages.
+
+- `waypointAdvance`
+- `waypointArrival`
+- `routeComplete`
+- `xte`
+- `heading`
+- `wind`
+
+_Example:_
+```JSON
+{
+ "context":"vessels.self",
+ "updates":[
+ {
+ "$source":"pypilot-id",
+ "timestamp":"2023-11-19T06:12:47.820Z",
+ "values":[
+ {
+ "path": "notifications.steering.autopilot.waypointAdvance",
+ "value": {
+ "state": "alert",
+ "method": ["sound"],
+ "message": "Waypoint Advance"
+ }
+ }
+ ]
+ }
+ ]
+}
+
+```
+
+## Autopilot offline / unreachable
+
+If an autopilot device is not connected or unreachable, the provider for that autopilot device will set the `state` of the device to `off-line`.
+
+
+## Autopilot Operations
+
+All API operations are invoked by issuing a request to:
+1. `/signalk/v2/api/vessels/self/steering/autopilots/default`
+2. `/signalk/v2/api/vessels/self/steering/autopilots/{id}`
+
+_Example:_
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/default/state"
+
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/pypilot-id/mode"
+```
+
+### Retrieving Autopilot Status
+
+To retrieve the current autopilot configuration as well as a list of available options for `state` and `mode` selections, submit an HTTP `GET` request to `/signalk/v2/api/vessels/self/steering/autopilots/{id}`.
+
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/{id}"
+```
+_Response:_
+
+```JSON
+{
+ "options":{
+ "state":["enabled","disabled"],
+ "mode":["gps","compass","wind"]
+ },
+ "state":"disabled",
+ "mode":"gps",
+ "target": 0,
+ "engaged": false
+}
+```
+
+Where:
+- `options` contains arrays of valid `state` and `mode` selection options
+- `state` represents the current state of the device
+- `mode` represents the current mode of the device
+- `target` represents the current target value with respect to the selected `mode`
+- `engaged` will be true when the autopilot is actively steering the vessel.
+
+
+### Setting the Autopilot State
+
+Autopilot state can be set by submitting an HTTP `PUT` request to the `state` endpoint containing a value from the list of available states.
+
+```typescript
+HTTP PUT "/signalk/v2/api/vessels/self/steering/autopilots/{id}/state" {"value": "disabled"}
+```
+
+### Getting the Autopilot State
+
+The current autopilot state can be retrieved by submitting an HTTP `GET` request to the `state` endpoint.
+
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/{id}/state"
+```
+
+_Response:_
+
+```JSON
+{
+ "value":"enabled",
+}
+```
+
+### Setting the Autopilot Mode
+
+Autopilot mode can be set by submitting an HTTP `PUT` request to the `mode` endpoint containing a value from the list of available modes.
+
+```typescript
+HTTP PUT "/signalk/v2/api/vessels/self/steering/autopilots/{id}/mode" {"value": "gps"}
+```
+
+### Getting the Autopilot Mode
+
+The current autopilot mode can be retrieved by submitting an HTTP `GET` request to the `mode` endpoint.
+
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/{id}/mode"
+```
+
+_Response:_
+
+```JSON
+{
+ "value":"gps",
+}
+```
+
+### Setting the Target value
+
+Autopilot target value can be set by submitting an HTTP `PUT` request to the `target` endpoint containing the desired value in radians.
+
+_Note: The value supplied should be a number within the valid range for the selected `mode`._
+
+```typescript
+HTTP PUT "signalk/v2/api/vessels/self/steering/autopilots/{id}/target" {"value": 1.1412}
+```
+
+The target value can be adjusted a +/- value by submitting an HTTP `PUT` request to the `target/adjust` endpoint with the value to add to the current `target` value in radians.
+
+```typescript
+HTTP PUT "signalk/v2/api/vessels/self/steering/autopilots/{id}/target/adjust" {"value": -0.1412}
+```
+
+### Getting the current Target value
+
+The current autopilot target value _(in radians)_ can be retrieved by submitting an HTTP `GET` request to the `target` endpoint.
+
+```typescript
+HTTP GET "/signalk/v2/api/vessels/self/steering/autopilots/{id}/target"
+```
+
+_Response:_
+
+```JSON
+{
+ "value":"3.14",
+}
+```
+
+### Engaging / Disengaging the Autopilot
+
+#### Engaging the autopilot
+
+An autopilot can be engaged by [setting it to a speciifc `state`](#setting-the-state) but it can also be engaged more generically by submitting an HTTP `POST` request to the `engage` endpoint.
+
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/engage"
+```
+
+_Note: The resultant `state` into which the autopilot is placed will be determined by the **provider plugin** and the autopilot device it is communicating with._
+
+#### Disengaging the autopilot
+
+An autopilot can be disengaged by [setting it to a speciifc `state`](#setting-the-state) but it can also be disengaged more generically by submitting an HTTP `POST` request to the `disengage` endpoint.
+
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/disengage"
+```
+
+_Note: The resultant `state` into which the autopilot is placed will be determined by the **provider plugin** and the autopilot device it is communicating with._
+
+### Perform a Tack
+
+To send a command to the autopilot to perform a tack in the required direction, submit an HTTP `POST` request to `/tack/{direction}` where _direction_ is either `port` or `starboard`.
+
+_Example: Tack to Port_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/tack/port"
+```
+
+_Example: Tack to Starboard_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/tack/starboard"
+```
+
+
+### Perform a Gybe
+
+To send a command to the autopilot to perform a gybe in the required direction, submit an HTTP `POST` request to `/gybe/{direction}` where _direction_ is either `port` or `starboard`.
+
+_Example: Gybe to Port_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/gybe/port"
+```
+
+_Example: Gybe to Starboard_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/gybe/starboard"
+```
+
+
+
+### Dodging / overriding
+
+To send a command to the autopilot to manually override the rudder position two (2) degrees in the requested direction in order to avoid an obstacle,
+submit an HTTP `POST` request to `/dodge/{direction}` where _direction_ is either `port` or `starboard`.
+
+_Example: Dodge to Port_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/dodge/port"
+```
+
+_Example: Gybe to Starboard_
+```typescript
+HTTP POST "/signalk/v2/api/vessels/self/steering/autopilots/{id}/dodge/starboard"
+```
\ No newline at end of file
diff --git a/docs/src/img/autopilot_provider.dia b/docs/src/img/autopilot_provider.dia
new file mode 100644
index 000000000..7518cc3a1
Binary files /dev/null and b/docs/src/img/autopilot_provider.dia differ
diff --git a/docs/src/img/autopilot_provider.svg b/docs/src/img/autopilot_provider.svg
new file mode 100644
index 000000000..cdf601e15
--- /dev/null
+++ b/docs/src/img/autopilot_provider.svg
@@ -0,0 +1,705 @@
+
+
diff --git a/packages/server-api/.gitignore b/packages/server-api/.gitignore
index 1521c8b76..a6c9ee411 100644
--- a/packages/server-api/.gitignore
+++ b/packages/server-api/.gitignore
@@ -1 +1,2 @@
dist
+src/autopilotapi.guard.ts
diff --git a/packages/server-api/package.json b/packages/server-api/package.json
index f54d487eb..ca18fc07d 100644
--- a/packages/server-api/package.json
+++ b/packages/server-api/package.json
@@ -5,7 +5,8 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
- "build": "tsc --declaration",
+ "generate": "ts-auto-guard src/autopilotapi.ts 2>/dev/null",
+ "build": "npm run generate && tsc --declaration",
"watch": "tsc --declaration --watch",
"prepublishOnly": "npm run build",
"typedoc": "typedoc --out docs src",
@@ -33,6 +34,7 @@
"express": "^4.10.4",
"mocha": "^10.2.0",
"prettier": "^2.8.4",
+ "ts-auto-guard": "^4.1.4",
"ts-node": "^10.9.1",
"typedoc": "^0.23.23",
"typescript": "^4.1.5"
diff --git a/packages/server-api/src/autopilotapi.ts b/packages/server-api/src/autopilotapi.ts
index 8c9bf54ac..cf8c28d8a 100644
--- a/packages/server-api/src/autopilotapi.ts
+++ b/packages/server-api/src/autopilotapi.ts
@@ -1,27 +1,112 @@
+import { Notification, Value } from './deltas'
+
+export type AutopilotUpdateAttrib =
+ | 'mode'
+ | 'state'
+ | 'target'
+ | 'engaged'
+ | 'options'
+ | 'alarm'
+
+const AUTOPILOTUPDATEATTRIBS: AutopilotUpdateAttrib[] = [
+ 'mode',
+ 'state',
+ 'target',
+ 'engaged',
+ 'options',
+ 'alarm'
+]
+
+export const isAutopilotUpdateAttrib = (s: string) =>
+ AUTOPILOTUPDATEATTRIBS.includes(s as AutopilotUpdateAttrib)
+
+export type AutopilotAlarm =
+ | 'waypointAdvance'
+ | 'waypointArrival'
+ | 'routeComplete'
+ | 'xte'
+ | 'heading'
+ | 'wind'
+
+const AUTOPILOTALARMS: AutopilotAlarm[] = [
+ 'waypointAdvance',
+ 'waypointArrival',
+ 'routeComplete',
+ 'xte',
+ 'heading',
+ 'wind'
+]
+
+export const isAutopilotAlarm = (s: string) =>
+ AUTOPILOTALARMS.includes(s as AutopilotAlarm)
+
+export type TackGybeDirection = 'port' | 'starboard'
+
export interface AutopilotApi {
- register: (pluginId: string, provider: AutopilotProvider) => void
- unRegister: (pluginId: string) => void
+ register(pluginId: string, provider: AutopilotProvider): void
+ unRegister(pluginId: string): void
+ apUpdate(
+ pluginId: string,
+ deviceId: string,
+ attrib: AutopilotUpdateAttrib,
+ value: Value
+ ): void
+ apAlarm(
+ pluginId: string,
+ deviceId: string,
+ alarmName: AutopilotAlarm,
+ value: Notification
+ ): void
}
+/** @see {isAutopilotProvider} ts-auto-guard:type-guard */
export interface AutopilotProvider {
- pilotType: string
- methods: AutopilotProviderMethods
+ getData(deviceId: string): Promise
+ getState(deviceId: string): Promise
+ setState(state: string, deviceId: string): Promise
+ getMode(deviceId: string): Promise
+ setMode(mode: string, deviceId: string): Promise
+ getTarget(deviceId: string): Promise
+ setTarget(value: number, deviceId: string): Promise
+ adjustTarget(value: number, deviceId: string): Promise
+ engage(deviceId: string): Promise
+ disengage(deviceId: string): Promise
+ tack(direction: TackGybeDirection, deviceId: string): Promise
+ gybe(direction: TackGybeDirection, deviceId: string): Promise
+ dodge(direction: TackGybeDirection, deviceId: string): Promise
+}
+
+export interface AutopilotStateDef {
+ name: string // autopilot state
+ engaged: boolean // true if state indicates actively steering
+}
+
+export interface AutopilotOptions {
+ states: AutopilotStateDef[]
+ modes: string[]
}
-export interface AutopilotProviderMethods {
- pluginId?: string
- engage: (enable: boolean) => Promise
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- getConfig: () => Promise<{ [key: string]: any }>
- getState: () => Promise
- setState: (state: string) => Promise
- getMode: () => Promise
- setMode: (mode: string) => Promise
- setTarget: (value: number) => Promise
- adjustTarget: (value: number) => Promise
- tack: (port: boolean) => Promise
+export interface AutopilotInfo {
+ options: AutopilotOptions
+ target: number | null
+ mode: string | null
+ state: string | null
+ engaged: boolean
}
export interface AutopilotProviderRegistry {
- registerAutopilotProvider: (provider: AutopilotProvider) => void
+ registerAutopilotProvider(
+ provider: AutopilotProvider,
+ devices: string[]
+ ): void
+ autopilotUpdate(
+ deviceId: string,
+ attrib: AutopilotUpdateAttrib,
+ value: Value
+ ): void
+ autopilotAlarm(
+ deviceId: string,
+ alarmName: AutopilotAlarm,
+ value: Notification
+ ): void
}
diff --git a/packages/server-api/src/index.ts b/packages/server-api/src/index.ts
index fc7a6c532..87930f2be 100644
--- a/packages/server-api/src/index.ts
+++ b/packages/server-api/src/index.ts
@@ -27,9 +27,11 @@ export * from './resourcetypes'
export * from './resourcesapi'
export { ResourceProviderRegistry } from './resourcesapi'
import { ResourceProviderRegistry } from './resourcesapi'
-import { PointDestination, RouteDestination, CourseInfo } from './coursetypes'
-
export * from './autopilotapi'
+import { AutopilotProviderRegistry } from './autopilotapi'
+export { AutopilotProviderRegistry } from './autopilotapi'
+export * from './autopilotapi.guard'
+import { PointDestination, RouteDestination, CourseInfo } from './coursetypes'
export type SignalKApiId =
| 'resources'
@@ -63,7 +65,8 @@ export interface PropertyValuesEmitter {
export interface PluginServerApp
extends PropertyValuesEmitter,
- ResourceProviderRegistry {}
+ ResourceProviderRegistry,
+ AutopilotProviderRegistry {}
/**
* This is the API that a [server plugin](https://github.com/SignalK/signalk-server/blob/master/SERVERPLUGINS.md) must implement.
diff --git a/src/api/autopilot/index.ts b/src/api/autopilot/index.ts
new file mode 100644
index 000000000..8c6649459
--- /dev/null
+++ b/src/api/autopilot/index.ts
@@ -0,0 +1,695 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { createDebug } from '../../debug'
+const debug = createDebug('signalk-server:api:autopilot')
+
+import { IRouter, NextFunction, Request, Response } from 'express'
+import { WithSecurityStrategy } from '../../security'
+
+import { Responses } from '../'
+import { SignalKMessageHub } from '../../app'
+
+import {
+ AutopilotProvider,
+ AutopilotInfo,
+ SKVersion,
+ Path,
+ Value,
+ Notification,
+ Delta,
+ isAutopilotProvider,
+ AutopilotUpdateAttrib,
+ isAutopilotUpdateAttrib,
+ AutopilotAlarm,
+ isAutopilotAlarm
+} from '@signalk/server-api'
+
+const AUTOPILOT_API_PATH = `/signalk/v2/api/vessels/self/steering/autopilots`
+const DEFAULTIDPATH = 'default'
+
+interface AutopilotApplication
+ extends WithSecurityStrategy,
+ SignalKMessageHub,
+ IRouter {}
+
+interface AutopilotList {
+ [id: string]: { provider: string; isDefault: boolean }
+}
+
+export class AutopilotApi {
+ private autopilotProviders: Map = new Map()
+
+ private defaultProviderId?: string
+ private defaultDeviceId?: string
+ private deviceToProvider: Map = new Map()
+
+ constructor(private server: AutopilotApplication) {}
+
+ async start() {
+ this.initApiEndpoints()
+ return Promise.resolve()
+ }
+
+ // ***** Plugin Interface methods *****
+
+ // Register plugin as provider.
+ register(pluginId: string, provider: AutopilotProvider, devices: string[]) {
+ debug(`** Registering provider(s)....${pluginId} ${provider}`)
+
+ if (!provider) {
+ throw new Error(`Error registering provider ${pluginId}!`)
+ }
+ if (!devices) {
+ throw new Error(`${pluginId} has not supplied a device list!`)
+ }
+ if (!isAutopilotProvider(provider)) {
+ throw new Error(
+ `${pluginId} is missing AutopilotProvider properties/methods!`
+ )
+ } else {
+ if (!this.autopilotProviders.has(pluginId)) {
+ this.autopilotProviders.set(pluginId, provider)
+ }
+ devices.forEach((id: string) => {
+ if (!this.deviceToProvider.has(id)) {
+ this.deviceToProvider.set(id, pluginId)
+ }
+ })
+ }
+ debug(
+ `No. of AutoPilotProviders registered =`,
+ this.autopilotProviders.size
+ )
+ }
+
+ // Unregister plugin as provider.
+ unRegister(pluginId: string) {
+ if (!pluginId) {
+ return
+ }
+ debug(`** Request to un-register plugin.....${pluginId}`)
+
+ if (!this.autopilotProviders.has(pluginId)) {
+ debug(`** NOT FOUND....${pluginId}... cannot un-register!`)
+ return
+ }
+
+ debug(`** Un-registering autopilot provider....${pluginId}`)
+ this.autopilotProviders.delete(pluginId)
+
+ debug(`** Update deviceToProvider Map .....${pluginId}`)
+ this.deviceToProvider.forEach((v: string, k: string) => {
+ debug('k', k, 'v', v)
+ if (v === pluginId) {
+ this.deviceToProvider.delete(k)
+ }
+ })
+
+ // update default if required
+ if (pluginId === this.defaultProviderId) {
+ debug(`** Resetting defaults .....`)
+ this.defaultDeviceId = undefined
+ this.defaultProviderId = undefined
+ this.emitDeltaMsg('defaultPilot', this.defaultDeviceId, 'autopilotApi')
+ }
+
+ debug(
+ `Remaining number of AutoPilot Providers registered =`,
+ this.autopilotProviders.size,
+ 'defaultProvider =',
+ this.defaultProviderId
+ )
+ }
+
+ // Pass changed attribute / value from autopilot.
+ apUpdate(
+ pluginId: string,
+ deviceId: string = pluginId + '.default',
+ attrib: AutopilotUpdateAttrib,
+ value: Value
+ ) {
+ if (deviceId && !this.deviceToProvider.has(deviceId)) {
+ this.deviceToProvider.set(deviceId, pluginId)
+ }
+ if (isAutopilotUpdateAttrib(attrib)) {
+ try {
+ if (!this.defaultDeviceId) {
+ this.initDefaults(deviceId)
+ }
+ this.emitDeltaMsg(attrib, value, deviceId)
+ } catch (err) {
+ debug(`ERROR apUpdate(): ${pluginId}->${deviceId}`, err)
+ }
+ } else {
+ debug(
+ `ERROR apUpdate(): ${pluginId}->${deviceId}`,
+ `${attrib} is NOT an AutopilotUpdateAttrib!`
+ )
+ }
+ }
+
+ // Pass alarm / notification from autopilot.
+ apAlarm(
+ pluginId: string,
+ deviceId: string = pluginId + '.default',
+ alarmName: AutopilotAlarm,
+ value: Notification
+ ) {
+ if (isAutopilotAlarm(alarmName)) {
+ debug(`Alarm -> ${deviceId}:`, value)
+ this.server.handleMessage(deviceId, {
+ updates: [
+ {
+ values: [
+ {
+ path: `notifications.steering.autopilot.${alarmName}` as Path,
+ value: value
+ }
+ ]
+ }
+ ]
+ })
+ }
+ }
+
+ // ***** /Plugin Interface methods *****
+
+ private updateAllowed(request: Request): boolean {
+ return this.server.securityStrategy.shouldAllowPut(
+ request,
+ 'vessels.self',
+ null,
+ 'steering.autopilot'
+ )
+ }
+
+ private initApiEndpoints() {
+ debug(`** Initialise ${AUTOPILOT_API_PATH} endpoints. **`)
+
+ this.server.use(
+ `${AUTOPILOT_API_PATH}/*`,
+ (req: Request, res: Response, next: NextFunction) => {
+ debug(`Autopilot path`, req.method, req.params)
+ try {
+ if (['PUT', 'POST'].includes(req.method)) {
+ debug(`Autopilot`, req.method, req.path, req.body)
+ if (!this.updateAllowed(req)) {
+ res.status(403).json(Responses.unauthorised)
+ } else {
+ next()
+ }
+ } else {
+ debug(`Autopilot`, req.method, req.path, req.query, req.body)
+ next()
+ }
+ } catch (err: any) {
+ res.status(500).json({
+ state: 'FAILED',
+ statusCode: 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ }
+ }
+ )
+
+ // get autopilot provider information
+ this.server.get(`${AUTOPILOT_API_PATH}`, (req: Request, res: Response) => {
+ res.status(200).json(this.getDevices())
+ })
+
+ // get default autopilot device
+ this.server.get(
+ `${AUTOPILOT_API_PATH}/defaultPilot`,
+ (req: Request, res: Response) => {
+ debug(`params:`, req.params)
+ res.status(Responses.ok.statusCode).json({ id: this.defaultDeviceId })
+ }
+ )
+
+ // set default autopilot device
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/defaultPilot/:id`,
+ (req: Request, res: Response) => {
+ debug(`params:`, req.params)
+ if (!this.deviceToProvider.has(req.params.id)) {
+ debug('** Invalid device id supplied...')
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ return
+ }
+ this.initDefaults(req.params.id)
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ }
+ )
+
+ // get default autopilot status & options
+ this.server.get(
+ `${AUTOPILOT_API_PATH}/:id`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .getData(req.params.id)
+ .then((data: AutopilotInfo) => {
+ res.json(data)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // get autopilot options
+ this.server.get(
+ `${AUTOPILOT_API_PATH}/:id/options`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .getData(req.params.id)
+ .then((r: AutopilotInfo) => {
+ res.json(r.options)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // engage / enable the autopilot
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/engage`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .engage(req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // disengage / disable the autopilot
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/disengage`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .disengage(req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // get state
+ this.server.get(
+ `${AUTOPILOT_API_PATH}/:id/state`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .getState(req.params.id)
+ .then((r: string) => {
+ res.json({ value: r })
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // set state
+ this.server.put(
+ `${AUTOPILOT_API_PATH}/:id/state`,
+ (req: Request, res: Response) => {
+ if (typeof req.body.value === 'undefined') {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ return
+ }
+ this.useProvider(req)
+ .setState(req.body.value, req.params.id)
+ .then((r: boolean) => {
+ debug('engaged =', r)
+ this.emitDeltaMsg('engaged', r, req.params.id)
+ if (req.params.id === this.defaultDeviceId) {
+ this.emitDeltaMsg('engaged', r, DEFAULTIDPATH)
+ }
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch(() => {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ })
+ }
+ )
+
+ // get mode
+ this.server.get(
+ `${AUTOPILOT_API_PATH}/:id/mode`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .getMode(req.params.id)
+ .then((r: string) => {
+ res.json({ value: r })
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // set mode
+ this.server.put(
+ `${AUTOPILOT_API_PATH}/:id/mode`,
+ (req: Request, res: Response) => {
+ if (typeof req.body.value === 'undefined') {
+ res.status(400).json(Responses.invalid)
+ return
+ }
+ this.useProvider(req)
+ .setMode(req.body.value, req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch(() => {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ })
+ }
+ )
+
+ // get target
+ this.server.get(
+ `${AUTOPILOT_API_PATH}/:id/target`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .getTarget(req.params.id)
+ .then((r: number) => {
+ res.json({ value: r })
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // set target
+ this.server.put(
+ `${AUTOPILOT_API_PATH}/:id/target`,
+ (req: Request, res: Response) => {
+ if (typeof req.body.value !== 'number') {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ return
+ }
+ if (req.body.value < 0 - Math.PI || req.body.value > 2 * Math.PI) {
+ res.status(400).json({
+ state: 'FAILED',
+ statusCode: 400,
+ message: `Error: Value supplied is outside of the valid range (-PI < value < 2*PI radians).`
+ })
+ return
+ }
+ this.useProvider(req)
+ .setTarget(req.body.value, req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch(() => {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ })
+ }
+ )
+
+ // adjust target
+ this.server.put(
+ `${AUTOPILOT_API_PATH}/:id/target/adjust`,
+ (req: Request, res: Response) => {
+ if (typeof req.body.value !== 'number') {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ return
+ }
+ this.useProvider(req)
+ .adjustTarget(req.body.value, req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch(() => {
+ res.status(Responses.invalid.statusCode).json(Responses.invalid)
+ })
+ }
+ )
+
+ // port tack
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/tack/port`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .tack('port', req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // starboard tack
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/tack/starboard`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .tack('starboard', req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // port gybe
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/gybe/port`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .gybe('port', req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // starboard gybe
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/gybe/starboard`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .gybe('starboard', req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // dodge to port
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/dodge/port`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .dodge('port', req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // dodge to starboard
+ this.server.post(
+ `${AUTOPILOT_API_PATH}/:id/dodge/starboard`,
+ (req: Request, res: Response) => {
+ this.useProvider(req)
+ .dodge('starboard', req.params.id)
+ .then(() => {
+ res.status(Responses.ok.statusCode).json(Responses.ok)
+ })
+ .catch((err) => {
+ res.status(err.statusCode ?? 500).json({
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ })
+ })
+ }
+ )
+
+ // error response
+ this.server.use(
+ `${AUTOPILOT_API_PATH}/*`,
+ (err: any, req: Request, res: Response, next: NextFunction) => {
+ const msg = {
+ state: err.state ?? 'FAILED',
+ statusCode: err.statusCode ?? 500,
+ message: err.message ?? 'No autopilots available!'
+ }
+ if (res.headersSent) {
+ console.log('EXCEPTION: headersSent')
+ return next(msg)
+ }
+ res.status(500).json(msg)
+ }
+ )
+ }
+
+ // returns provider to use.
+ private useProvider(req: Request): AutopilotProvider {
+ debug(`useProvider(${req.params.id})`)
+
+ if (req.params.id === DEFAULTIDPATH) {
+ if (!this.defaultDeviceId) {
+ this.initDefaults()
+ }
+ if (
+ this.defaultProviderId &&
+ this.autopilotProviders.has(this.defaultProviderId)
+ ) {
+ debug(`Using default device provider...`)
+ return this.autopilotProviders.get(
+ this.defaultProviderId
+ ) as AutopilotProvider
+ } else {
+ debug(`No default device provider...`)
+ throw Responses.invalid
+ }
+ } else {
+ const pid = this.deviceToProvider.get(req.params.id) as string
+ if (this.autopilotProviders.has(pid)) {
+ debug(`Found provider...using ${pid}`)
+ return this.autopilotProviders.get(pid) as AutopilotProvider
+ } else {
+ debug('Cannot get Provider!')
+ throw Responses.invalid
+ }
+ }
+ }
+
+ // Returns an array of provider info
+ private getDevices(): AutopilotList {
+ const pilots: AutopilotList = {}
+ this.deviceToProvider.forEach((providerId: string, deviceId: string) => {
+ pilots[deviceId] = {
+ provider: providerId,
+ isDefault: deviceId === this.defaultDeviceId
+ }
+ })
+ return pilots
+ }
+
+ /** Initialises the value of default device / provider.
+ * If id is not supplied sets first registered device as the default.
+ **/
+ private initDefaults(deviceId?: string) {
+ debug(`initDefaults()...${deviceId}`)
+
+ // set to supplied deviceId
+ if (deviceId && this.deviceToProvider.has(deviceId)) {
+ this.defaultDeviceId = deviceId
+ this.defaultProviderId = this.deviceToProvider.get(
+ this.defaultDeviceId
+ ) as string
+ this.emitDeltaMsg('defaultPilot', this.defaultDeviceId, 'autopilotApi')
+ debug(`Default Device = ${this.defaultDeviceId}`)
+ debug(`Default Provider = ${this.defaultProviderId}`)
+ return
+ }
+
+ // else set to first AP device registered
+ if (this.deviceToProvider.size !== 0) {
+ const k = this.deviceToProvider.keys()
+ this.defaultDeviceId = k.next().value as string
+ this.defaultProviderId = this.deviceToProvider.get(
+ this.defaultDeviceId
+ ) as string
+ this.emitDeltaMsg('defaultPilot', this.defaultDeviceId, 'autopilotApi')
+ debug(`Default Device = ${this.defaultDeviceId}`)
+ debug(`Default Provider = ${this.defaultProviderId}`)
+ return
+ } else {
+ throw new Error(
+ 'Cannot set defaultDevice....No autopilot devices registered!'
+ )
+ }
+ }
+
+ // emit delta updates on operation success
+ private emitDeltaMsg(path: string, value: any, source: string) {
+ const msg: Delta = {
+ updates: [
+ {
+ values: [
+ {
+ path: `steering.autopilot${path ? '.' + path : ''}` as Path,
+ value: value
+ }
+ ]
+ }
+ ]
+ }
+ debug(`delta -> ${source}:`, msg.updates[0])
+ this.server.handleMessage(source, msg, SKVersion.v2)
+ this.server.handleMessage(source, msg, SKVersion.v1)
+ }
+}
diff --git a/src/api/autopilot/openApi.json b/src/api/autopilot/openApi.json
new file mode 100644
index 000000000..663a18881
--- /dev/null
+++ b/src/api/autopilot/openApi.json
@@ -0,0 +1,716 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "version": "2.0.0",
+ "title": "Signal K Autopilot API",
+ "termsOfService": "http://signalk.org/terms/",
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ }
+ },
+ "externalDocs": {
+ "url": "http://signalk.org/specification/",
+ "description": "Signal K specification."
+ },
+ "servers": [
+ {
+ "url": "/signalk/v2/api/vessels/self/steering/autopilots"
+ }
+ ],
+ "tags": [
+ {
+ "name": "autopilot",
+ "description": "Signal K Autopilot API"
+ }
+ ],
+ "components": {
+ "schemas": {
+ "autopilotStateOption": {
+ "type": "object",
+ "title": "Autopilot state option definition",
+ "description": "Autopilot `state` option and indication whether pilot is actively steering.",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "State name / label",
+ "example": "enabled"
+ },
+ "engaged": {
+ "type": "boolean",
+ "description": "Set `true` if pilot is actively steering when in this `state`.",
+ "example": true
+ }
+ },
+ "example": [
+ { "name": "auto", "engaged": true },
+ { "name": "standby", "engaged": false }
+ ]
+ },
+ "autopilotOptions": {
+ "type": "object",
+ "title": "Autopilot configuration options",
+ "description": "A collection of configuration options and their valid values",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "properties": {
+ "states": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/autopilotStateOption"
+ },
+ "description": "List of valid autopilot states."
+ },
+ "modes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of valid Mode values.",
+ "example": ["compass", "gps"]
+ }
+ }
+ }
+ },
+ "responses": {
+ "200ActionResponse": {
+ "description": "PUT OK response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "state": {
+ "type": "string",
+ "enum": ["COMPLETED"]
+ },
+ "statusCode": {
+ "type": "number",
+ "enum": [200]
+ }
+ },
+ "required": ["statusCode", "state"]
+ }
+ }
+ }
+ },
+ "ErrorResponse": {
+ "description": "Failed operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "description": "Request error response",
+ "properties": {
+ "state": {
+ "type": "string",
+ "enum": ["FAILED"]
+ },
+ "statusCode": {
+ "type": "number",
+ "enum": [400, 404]
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["state", "statusCode", "message"]
+ }
+ }
+ }
+ }
+ },
+ "parameters": {
+ "AutopilotIdParam": {
+ "name": "id",
+ "in": "path",
+ "description": "autopilot id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ },
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ },
+ "cookieAuth": {
+ "type": "apiKey",
+ "in": "cookie",
+ "name": "JAUTHENTICATION"
+ }
+ }
+ },
+ "security": [{ "cookieAuth": [] }, { "bearerAuth": [] }],
+ "paths": {
+ "/": {
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Retrieve list of autopilots.",
+ "description": "Returns a list of autopilots indexed by their identifier.",
+ "responses": {
+ "default": {
+ "description": "Autopilot device list response.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "required": ["provider", "isDefault"],
+ "properties": {
+ "provider": {
+ "type": "string",
+ "description": "Provider plugin managing the autopilot device.",
+ "example": "my-pilot-provider"
+ },
+ "isDefault": {
+ "type": "boolean",
+ "description": "Set to true when the autopilot is currently set as the default.",
+ "example": "false"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "error": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/defaultPilot/{id}": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Get the default autopilot device id.",
+ "description": "Returns the device id of the autopilot assigned as the default.",
+ "responses": {
+ "default": {
+ "description": "Autopilot configuration response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["id"],
+ "properties": {
+ "id": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Set the default autopilot device.",
+ "description": "Sets the autopilot with the supplied `id` as the default.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Retrieve autopilot information.",
+ "description": "Returns the current state autopilot information including the available options for `state` and `mode`.",
+ "responses": {
+ "default": {
+ "description": "Autopilot configuration response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["state", "mode", "target", "engaged"],
+ "properties": {
+ "engaged": {
+ "type": "boolean",
+ "description": "Autopilot is engaged and actively steering the vessel",
+ "example": "true"
+ },
+ "state": {
+ "type": "string",
+ "description": "Autopilot state",
+ "example": "auto"
+ },
+ "mode": {
+ "type": "string",
+ "description": "Autopilot operational mode",
+ "example": "compass"
+ },
+ "target": {
+ "description": "Current target value (radians)",
+ "type": "number",
+ "example": 1.25643
+ },
+ "options": {
+ "$ref": "#/components/schemas/autopilotOptions"
+ }
+ }
+ }
+ }
+ }
+ },
+ "error": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/options": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Retrieve autopilot options.",
+ "description": "Returns the selectable options and the values that can be applied (e.g. for`state` and `mode`).",
+ "responses": {
+ "default": {
+ "description": "Autopilot configuration response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/autopilotOptions"
+ }
+ }
+ }
+ },
+ "error": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/engage": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Engage autopilot to steer vessel",
+ "description": "Provider plugin will set the autopilot to a `state` where it is actively steering the vessel. `state` selected is determined by the provider plugin.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/disengage": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Disengage autopilot from steering vessel.",
+ "description": "Provider plugin will set the autopilot to a `state` where it is NOT actively steering the vessel. `state` selected is determined by the provider plugin.",
+
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/state": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Retrieve the current state.",
+ "description": "Returns the current `state` value from the autopilot.",
+ "responses": {
+ "default": {
+ "description": "Autopilot value response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["value"],
+ "properties": {
+ "value": {
+ "type": "string",
+ "example": "standby"
+ }
+ }
+ }
+ }
+ }
+ },
+ "error": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ },
+ "put": {
+ "tags": ["autopilot"],
+ "summary": "Set autopilot state.",
+ "description": "Set the autopilot to the supplied valid `state` value.",
+ "requestBody": {
+ "description": "Supply valid `state` value (as per response from autopilot information request).",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value representing the `state` the autopilot is to enter.",
+ "example": "enabled"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/mode": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Retrieve the current mode.",
+ "description": "Returns the current `mode` value from the autopilot.",
+ "responses": {
+ "default": {
+ "description": "Autopilot value response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["value"],
+ "properties": {
+ "value": {
+ "type": "string",
+ "example": "compass"
+ }
+ }
+ }
+ }
+ }
+ },
+ "error": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ },
+ "put": {
+ "tags": ["autopilot"],
+ "summary": "Set autopilot mode",
+ "description": "Set the autopilot to the supplied valid `mode` value.",
+ "requestBody": {
+ "description": "Supply valid `mode` value (as per response from autopilot information request).",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value representing the `mode` the autopilot is to enter.",
+ "example": "compass"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/target": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "get": {
+ "tags": ["autopilot"],
+ "summary": "Retrieve the current target value (radians).",
+ "description": "Value in radians of the current target value.",
+ "responses": {
+ "default": {
+ "description": "Autopilot value response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["value"],
+ "properties": {
+ "value": {
+ "type": "number",
+ "description": "Value in radians",
+ "min": -3.141592653589793,
+ "max": 6.283185307179586,
+ "example": -1.23789
+ }
+ }
+ }
+ }
+ }
+ },
+ "error": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ },
+ "put": {
+ "tags": ["autopilot"],
+ "summary": "Set autopilot `target` value (radians).",
+ "description": "Value supplied must be in the valid range between -PI & 2*PI.",
+ "requestBody": {
+ "description": "Value in radians within the bounds of the current autopilot mode. (i.e. compass: 0 to 2*Pi, wind angle: -Pi to Pi)",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "number",
+ "description": "Value in radians",
+ "min": -3.141592653589793,
+ "max": 6.283185307179586,
+ "example": -1.23789
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/target/adjust": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "put": {
+ "tags": ["autopilot"],
+ "summary": "Adjust autopilot target value by +/- radians.",
+ "description": "Value supplied will be added to the current target value. The result must be in the valid range between -PI & 2*PI.",
+ "requestBody": {
+ "description": "A value in radians to add to the current `target` value.",
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "number",
+ "description": "Value in radians.",
+ "min": -0.1745,
+ "max": 0.1745,
+ "example": -0.23789
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/tack/port": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Tack to port.",
+ "description": "Execute a port tack.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/tack/starboard": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Tack to starboard.",
+ "description": "Execute a starboard tack.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/gybe/port": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Gybe to port.",
+ "description": "Execute a gybe to port.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/gybe/starboard": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Gybe to starboard.",
+ "description": "Execute a gybe to starboard.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/dodge/port": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Turn to port.",
+ "description": "Manual override turn to port.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ },
+ "/{id}/dodge/starboard": {
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/AutopilotIdParam"
+ }
+ ],
+ "post": {
+ "tags": ["autopilot"],
+ "summary": "Turn to starboard.",
+ "description": "Manual override turn to starboard.",
+ "responses": {
+ "200ActionResponse": {
+ "$ref": "#/components/responses/200ActionResponse"
+ },
+ "default": {
+ "$ref": "#/components/responses/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/api/autopilot/openApi.ts b/src/api/autopilot/openApi.ts
new file mode 100644
index 000000000..e8425c96d
--- /dev/null
+++ b/src/api/autopilot/openApi.ts
@@ -0,0 +1,8 @@
+import { OpenApiDescription } from '../swagger'
+import autopilotApiDoc from './openApi.json'
+
+export const autopilotApiRecord = {
+ name: 'autopilot',
+ path: '/signalk/v2/api/vessels/self/steering/autopilot',
+ apiDoc: autopilotApiDoc as unknown as OpenApiDescription
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index a3ccff4ea..c629ccb39 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -4,6 +4,7 @@ import { WithSecurityStrategy } from '../security'
import { CourseApi } from './course'
import { FeaturesApi } from './discovery'
import { ResourcesApi } from './resources'
+import { AutopilotApi } from './autopilot'
import { SignalKApiId } from '@signalk/server-api'
export interface ApiResponse {
@@ -56,8 +57,18 @@ export const startApis = (
;(app as any).courseApi = courseApi
apiList.push('course')
+ const autopilotApi = new AutopilotApi(app)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ;(app as any).autopilotApi = autopilotApi
+ apiList.push('autopilot')
+
const featuresApi = new FeaturesApi(app)
- Promise.all([resourcesApi.start(), courseApi.start(), featuresApi.start()])
+ Promise.all([
+ resourcesApi.start(),
+ courseApi.start(),
+ featuresApi.start(),
+ autopilotApi.start()
+ ])
return apiList
}
diff --git a/src/api/swagger.ts b/src/api/swagger.ts
index 028b8719e..ed2842545 100644
--- a/src/api/swagger.ts
+++ b/src/api/swagger.ts
@@ -5,6 +5,7 @@ import { SERVERROUTESPREFIX } from '../constants'
import { courseApiRecord } from './course/openApi'
import { notificationsApiRecord } from './notifications/openApi'
import { resourcesApiRecord } from './resources/openApi'
+import { autopilotApiRecord } from './autopilot/openApi'
import { securityApiRecord } from './security/openApi'
import { discoveryApiRecord } from './discovery/openApi'
import { appsApiRecord } from './apps/openApi'
@@ -26,10 +27,11 @@ interface ApiRecords {
const apiDocs = [
discoveryApiRecord,
appsApiRecord,
- securityApiRecord,
+ autopilotApiRecord,
courseApiRecord,
notificationsApiRecord,
- resourcesApiRecord
+ resourcesApiRecord,
+ securityApiRecord
].reduce((acc, apiRecord: OpenApiRecord) => {
acc[apiRecord.name] = apiRecord
return acc
diff --git a/src/interfaces/plugins.ts b/src/interfaces/plugins.ts
index 71ab1f9bf..772c3cf29 100644
--- a/src/interfaces/plugins.ts
+++ b/src/interfaces/plugins.ts
@@ -21,8 +21,13 @@ import {
PropertyValues,
PropertyValuesCallback,
ResourceProvider,
+ AutopilotProvider,
ServerAPI,
RouteDestination,
+ AutopilotUpdateAttrib,
+ AutopilotAlarm,
+ Value,
+ Notification,
SignalKApiId
} from '@signalk/server-api'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -32,6 +37,7 @@ import express, { Request, Response } from 'express'
import fs from 'fs'
import _ from 'lodash'
import path from 'path'
+import { AutopilotApi } from '../api/autopilot'
import { CourseApi } from '../api/course'
import { ResourcesApi } from '../api/resources'
import { SERVERROUTESPREFIX } from '../constants'
@@ -469,9 +475,10 @@ module.exports = (theApp: any) => {
console.error(`${plugin.id}:no configuration data`)
safeConfiguration = {}
}
- onStopHandlers[plugin.id].push(() =>
+ onStopHandlers[plugin.id].push(() => {
app.resourcesApi.unRegister(plugin.id)
- )
+ app.autopilotApi.unRegister(plugin.id)
+ })
plugin.start(safeConfiguration, restart)
debug('Started plugin ' + plugin.name)
setPluginStartedMessage(plugin)
@@ -557,6 +564,29 @@ module.exports = (theApp: any) => {
resourcesApi.register(plugin.id, provider)
}
+ const autopilotApi: AutopilotApi = app.autopilotApi
+ _.omit(appCopy, 'autopilotApi') // don't expose the actual autopilot api manager
+ appCopy.registerAutopilotProvider = (
+ provider: AutopilotProvider,
+ devices: string[]
+ ) => {
+ autopilotApi.register(plugin.id, provider, devices)
+ }
+ appCopy.autopilotUpdate = (
+ deviceId: string,
+ attrib: AutopilotUpdateAttrib,
+ value: Value
+ ) => {
+ autopilotApi.apUpdate(plugin.id, deviceId, attrib, value)
+ }
+ appCopy.autopilotAlarm = (
+ deviceId: string,
+ alarmName: AutopilotAlarm,
+ value: Notification
+ ) => {
+ autopilotApi.apAlarm(plugin.id, deviceId, alarmName, value)
+ }
+
_.omit(appCopy, 'apiList') // don't expose the actual apiList
const courseApi: CourseApi = app.courseApi