Skip to content

Commit

Permalink
feature: Add Feature discovery API endpoint and plugin interface (#1630)
Browse files Browse the repository at this point in the history

Co-authored-by: Teppo Kurki <[email protected]>
  • Loading branch information
panaaj and tkurki authored Apr 22, 2024
1 parent 9ade3e5 commit abbc56c
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 26 deletions.
58 changes: 56 additions & 2 deletions docs/src/develop/plugins/server_plugin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,66 @@

# Server API for plugins

SignalK server provides an interface to allow plugins to access / update the full data model, operations and send / receive deltas (updates).
SignalK server provides an interface to allow plugins to:
- Discover Features.
- Access / update the full data model
- send / receive deltas (updates)
- Interact with APIs
- Expose HTTP endpoints

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

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

---

### Discover Features

#### `app.getFeatures(enabed)`

Returns an object detailing the available APIs and Plugins.

The `enabled` parameter is optional and has the following values:
- `undefined` (not provided): list all features
- `true`: list only enabled features
- `false`: list only disabled features

_Example:_
```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
}
]
}
```

### Accessing the Data Model

#### `app.getPath(path)`
Expand Down
49 changes: 49 additions & 0 deletions docs/src/develop/webapps.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,55 @@ GET /signalk/v1/applicationData/user/my-application
[ "1.0", "1.1"]
```

## Discovering Server Features

To assist in tailoring a WebApps UI, it can "discover" the features supported by the server by sending a request to `/signalk/v2/features`.

The response wil contain an object detailing the available APIs and Plugins.

You can use the `enabled` parameter to specify to only return enabled or disabled features.

To list only enabled features:
`/signalk/v2/features?enable=1`

To list only disabled features:
`/signalk/v2/features?enable=0`

_Example response:_
```JSON
{
"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
}
]
}
```

## Embedded Components and Admin UI / Server interfaces

Embedded components are implemented using [Webpack Federated Modules](https://webpack.js.org/concepts/module-federation/) and [React Code Splitting](https://reactjs.org/docs/code-splitting.html).
Expand Down
23 changes: 23 additions & 0 deletions packages/server-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ import { PointDestination, RouteDestination, CourseInfo } from './coursetypes'

export * from './autopilotapi'

export type SignalKApiId =
| 'resources'
| 'course'
| 'history'
| 'autopilot'
| 'anchor'
| 'logbook'
| 'historyplayback' //https://signalk.org/specification/1.7.0/doc/streaming_api.html#history-playback
| 'historysnapshot' //https://signalk.org/specification/1.7.0/doc/rest_api.html#history-snapshot-retrieval

export {
PropertyValue,
PropertyValues,
Expand Down Expand Up @@ -113,6 +123,16 @@ export interface Metadata {
description?: string
}

export interface FeatureInfo {
apis: SignalKApiId[]
plugins: Array<{
id: string
name: string
version: string
enabled: boolean
}>
}

export interface ServerAPI extends PluginServerApp {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSelfPath: (path: string) => any
Expand Down Expand Up @@ -172,6 +192,9 @@ export interface ServerAPI extends PluginServerApp {
) => void
}) => void
getSerialPorts: () => Promise<Ports>

getFeatures: () => FeatureInfo

getCourse: () => Promise<CourseInfo>
clearDestination: () => Promise<void>
setDestination: (
Expand Down
40 changes: 40 additions & 0 deletions src/api/discovery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createDebug } from '../../debug'
const debug = createDebug('signalk-server:api:features')

import { IRouter, Request, Response } from 'express'

import { WithFeatures } from '../../app'

const FEATURES_API_PATH = `/signalk/v2/features`

interface FeaturesApplication extends IRouter, WithFeatures {}

export class FeaturesApi {
constructor(private app: FeaturesApplication) {}

async start() {
return new Promise<void>((resolve) => {
this.initApiRoutes()
resolve()
})
}

private initApiRoutes() {
debug(`** Initialise ${FEATURES_API_PATH} path handlers **`)
// return Feature information
this.app.get(
`${FEATURES_API_PATH}`,
async (req: Request, res: Response) => {
debug(`** GET ${req.path}`)

const enabled = ['true', '1'].includes(req.query.enabled as string)
? true
: ['false', '0'].includes(req.query.enabled as string)
? false
: undefined

res.json(await this.app.getFeatures(enabled))
}
)
}
}
143 changes: 139 additions & 4 deletions src/api/discovery/openApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@
},
"servers": [
{
"url": "/"
"url": "/signalk"
}
],
"tags": [
{
"name": "server",
"description": "Signal K Server."
},
{
"name": "features",
"description": "Signal K Server features."
}
],
"tags": [],
"components": {
"schemas": {
"DiscoveryData": {
Expand Down Expand Up @@ -70,6 +79,46 @@
}
}
}
},
"PluginMetaData": {
"type": "object",
"required": ["id", "name", "version"],
"description": "Plugin metadata.",
"properties": {
"id": {
"type": "string",
"description": "Plugin ID."
},
"name": {
"type": "string",
"description": "Plugin name."
},
"version": {
"type": "string",
"description": "Plugin verison."
}
}
},
"FeaturesModel": {
"type": "object",
"required": ["apis", "plugins"],
"description": "Features response",
"properties": {
"apis": {
"type": "array",
"description": "Implemented APIs.",
"items": {
"type": "string"
}
},
"plugins": {
"type": "array",
"description": "Installed Plugins.",
"items": {
"$ref": "#/components/schemas/PluginMetaData"
}
}
}
}
},
"responses": {
Expand All @@ -82,21 +131,107 @@
}
}
}
},
"200Ok": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["COMPLETED"]
},
"statusCode": {
"type": "number",
"enum": [200]
}
},
"required": ["state", "statusCode"]
}
}
}
},
"ErrorResponse": {
"description": "Failed operation",
"content": {
"application/json": {
"schema": {
"type": "object",
"description": "Request error response",
"properties": {
"state": {
"type": "string",
"enum": ["FAILED"]
},
"statusCode": {
"type": "number",
"enum": [404]
},
"message": {
"type": "string"
}
},
"required": ["state", "statusCode", "message"]
}
}
}
},
"FeaturesResponse": {
"description": "Server features response.",
"content": {
"application/json": {
"schema": {
"description": "Features response.",
"$ref": "#/components/schemas/FeaturesModel"
}
}
}
}
}
},

"paths": {
"/signalk": {
"/": {
"get": {
"tags": [],
"tags": ["server"],
"summary": "Retrieve server version and service endpoints.",
"description": "Returns data about server's endpoints and versions.",
"responses": {
"200": {
"$ref": "#/components/responses/DiscoveryResponse"
}
}
}
},
"/v2/features": {
"get": {
"tags": ["features"],
"parameters": [
{
"name": "enabled",
"in": "query",
"description": "Limit results to enabled features.",
"required": false,
"explode": false,
"schema": {
"type": "string",
"enum": ["enabled", "1", "false", "0"]
}
}
],
"summary": "Retrieve available server features.",
"description": "Returns object detailing the available server features.",
"responses": {
"200": {
"$ref": "#/components/responses/FeaturesResponse"
},
"default": {
"$ref": "#/components/responses/ErrorResponse"
}
}
}
}
}
}
Loading

0 comments on commit abbc56c

Please sign in to comment.