@mxtommy/kip 3.10.0-beta.30 → 3.10.0-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ # KIP Plugin – Displays API
2
+
3
+ Base path for plugin routes: `/plugins/kip`
4
+
5
+ ## Endpoints
6
+
7
+ - GET `/plugins/kip/displays`
8
+ - Returns: `[{ displayId: string, displayName: string | null }]`
9
+
10
+ - GET `/plugins/kip/displays/{displayId}`
11
+ - Returns the raw object stored at `self.displays.{displayId}` (or `null`).
12
+
13
+ - PUT `/plugins/kip/displays/{displayId}`
14
+ - Body: arbitrary JSON object to store under `self.displays.{displayId}` (or `null` to clear).
15
+ - Returns: `{ state: "SUCCESS", statusCode: 200 }` on success.
16
+
17
+ - GET `/plugins/kip/displays/{displayId}/activeScreen`
18
+ - Returns: `number | null` – the active screen index.
19
+
20
+ - PUT `/plugins/kip/displays/{displayId}/activeScreen`
21
+ - Body: `{ "screenIdx": number | null }`
22
+ - Returns: `{ state: "SUCCESS", statusCode: 200 }` on success.
23
+
24
+ ## Examples
25
+
26
+ - List displays
27
+
28
+ ```sh
29
+ curl -s http://localhost:3000/plugins/kip/displays | jq
30
+ ```
31
+
32
+ - Read a display entry
33
+
34
+ ```sh
35
+ curl -s http://localhost:3000/plugins/kip/displays/<displayId> | jq
36
+ ```
37
+
38
+ - Set a display entry
39
+
40
+ ```sh
41
+ curl -s -X PUT \
42
+ -H 'Content-Type: application/json' \
43
+ -d '{"displayName":"Mast"}' \
44
+ http://localhost:3000/plugins/kip/displays/<displayId>
45
+ ```
46
+
47
+ - Get active screen
48
+
49
+ ```sh
50
+ curl -s http://localhost:3000/plugins/kip/displays/<displayId>/activeScreen
51
+ ```
52
+
53
+ - Set active screen
54
+
55
+ ```sh
56
+ curl -s -X PUT \
57
+ -H 'Content-Type: application/json' \
58
+ -d '{"screenIdx":1}' \
59
+ http://localhost:3000/plugins/kip/displays/<displayId>/activeScreen
60
+ ```
61
+
62
+ Notes:
63
+ - Replace `<displayId>` with the KIP instance UUID under `self.displays`.
64
+ - If your Signal K server requires auth, include cookies or bearer token accordingly.
@@ -1,14 +1,37 @@
1
1
  import { Path, Plugin, ServerAPI, SKVersion } from '@signalk/server-api'
2
- import { Request, Response } from 'express'
3
- //import * as openapi from './openApi.json';
2
+ import { Request, Response, NextFunction } from 'express'
3
+ import * as openapi from './openApi.json';
4
4
 
5
5
  export default (server: ServerAPI): Plugin => {
6
6
 
7
7
  const API_PATHS = {
8
- DISPLAYS_PATH: `/displays/:kipId`,
9
- ACTIVESCREEN_PATH: `/displays/:kipId/activeScreen`
8
+ DISPLAYS: `/displays`,
9
+ INSTANCE: `/displays/:displayId`,
10
+ ACTIVE_SCREEN: `/displays/:displayId/activeScreen`
10
11
  } as const;
11
12
 
13
+ // Helpers
14
+ function getDisplaySelfPath(displayId: string, suffix?: string): string {
15
+ const tail = suffix ? `.${suffix}` : ''
16
+ const full = server.getSelfPath(`displays.${displayId}${tail}`)
17
+ server.debug(`getDisplaySelfPath: displayId=${displayId}, suffix=${suffix}, fullPath transform output=${full}`)
18
+ return full
19
+ }
20
+
21
+ function readSelfDisplays(): Record<string, { displayName?: string }> | undefined {
22
+ const fullPath = server.getSelfPath('displays'); // e.g., vessels.self.displays
23
+ return server.getPath(fullPath) as Record<string, { displayName?: string }> | undefined;
24
+ }
25
+
26
+ function sendOk(res: Response, body?: unknown) {
27
+ if (body === undefined) return res.status(204).end()
28
+ return res.status(200).json(body)
29
+ }
30
+
31
+ function sendFail(res: Response, statusCode: number, message: string) {
32
+ return res.status(statusCode).json({ state: 'FAILED', statusCode, message })
33
+ }
34
+
12
35
  const plugin: Plugin = {
13
36
  id: 'kip',
14
37
  name: 'KIP',
@@ -16,9 +39,6 @@ export default (server: ServerAPI): Plugin => {
16
39
  start: (settings) => {
17
40
  server.debug(`Starting plugin with settings: ${JSON.stringify(settings)}`);
18
41
  server.setPluginStatus(`Starting...`)
19
-
20
- const p = server.getSelfPath('displays.*');
21
- server.debug(`Self path for displays.*: ${p}`);
22
42
  },
23
43
  stop: () => {
24
44
  server.debug(`Stopping plugin`);
@@ -26,19 +46,55 @@ export default (server: ServerAPI): Plugin => {
26
46
  server.setPluginStatus(msg)
27
47
  },
28
48
  schema: () => {
29
- return {
30
- type: "object",
31
- properties: {}
32
- };
49
+ return {
50
+ type: "object",
51
+ properties: {}
52
+ };
33
53
  },
34
54
  registerWithRouter(router) {
35
- server.debug(`Registering plugin routes: ${API_PATHS.DISPLAYS_PATH}`);
55
+ server.debug(`Registering plugin routes: ${API_PATHS.DISPLAYS}, ${API_PATHS.INSTANCE}, ${API_PATHS.ACTIVE_SCREEN}`);
56
+
57
+ // Validate/normalize :displayId where present
58
+ router.param('displayId', (req: Request & { displayId?: string }, res: Response, next: NextFunction, displayId: string) => {
59
+ if (displayId == null) return sendFail(res, 400, 'Missing displayId parameter')
60
+ try {
61
+ let id = String(displayId)
62
+ // Decode percent-encoding if present
63
+ try {
64
+ id = decodeURIComponent(id)
65
+ } catch {
66
+ // ignore decode errors, keep original id
67
+ }
68
+ // If someone sent JSON as the path segment, try to recover {"displayId":"..."}
69
+ if (id.trim().startsWith('{')) {
70
+ try {
71
+ const parsed = JSON.parse(id)
72
+ if (parsed && typeof parsed.displayId === 'string') {
73
+ id = parsed.displayId
74
+ } else {
75
+ return sendFail(res, 400, 'Invalid displayId format in JSON')
76
+ }
77
+ } catch {
78
+ return sendFail(res, 400, 'Invalid displayId JSON')
79
+ }
80
+ }
81
+ // Basic safety: allow UUID-like strings (alphanum + dash)
82
+ if (!/^[A-Za-z0-9-]+$/.test(id)) {
83
+ return sendFail(res, 400, 'Invalid displayId format')
84
+ }
85
+ req.displayId = id
86
+ next()
87
+ } catch {
88
+ return sendFail(res, 400, 'Missing or invalid displayId parameter')
89
+ }
90
+ })
36
91
 
37
- router.put(`${API_PATHS.DISPLAYS_PATH}`, async (req: Request, res: Response) => {
38
- server.debug(`** PUT path ${API_PATHS.DISPLAYS_PATH}. Request Params: ${JSON.stringify(req.params)}, Body: ${JSON.stringify(req.body)}, Method: ${req.method}, Path: ${req.path}`);
92
+ router.put(`${API_PATHS.INSTANCE}`, async (req: Request, res: Response) => {
93
+ server.debug(`** PUT ${API_PATHS.INSTANCE}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
39
94
  try {
40
- const dottedPath = pathToDotNotation(req.path);
41
- server.debug(`Converted request path ${req.path} to dot notation ${dottedPath} and updating SK path with req.body`);
95
+ const displayId = (req as Request & { displayId: string }).displayId
96
+ const fullPath = getDisplaySelfPath(displayId)
97
+ server.debug(`Updating SK path ${fullPath} with body`)
42
98
  server.handleMessage(
43
99
  plugin.id,
44
100
  {
@@ -46,8 +102,8 @@ export default (server: ServerAPI): Plugin => {
46
102
  {
47
103
  values: [
48
104
  {
49
- path: dottedPath as Path,
50
- value: req.body ? req.body : null
105
+ path: fullPath as Path,
106
+ value: req.body ?? null
51
107
  }
52
108
  ]
53
109
  }
@@ -55,29 +111,23 @@ export default (server: ServerAPI): Plugin => {
55
111
  },
56
112
  SKVersion.v1
57
113
  );
58
- return res.status(200).json({
59
- state: 'SUCCESS',
60
- statusCode: 200
61
- });
114
+ return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
62
115
 
63
116
  } catch (error) {
64
117
  const msg = `HandleMessage failed with errors!`
65
118
  server.setPluginError(msg)
66
119
  server.error(`Error in HandleMessage: ${error}`);
67
120
 
68
- return res.status(400).json({
69
- state: 'FAILED',
70
- statusCode: 400,
71
- message: (error as Error).message
72
- });
121
+ return sendFail(res, 400, (error as Error).message)
73
122
  }
74
123
  });
75
124
 
76
- router.put(`${API_PATHS.ACTIVESCREEN_PATH}`, async (req: Request, res: Response) => {
77
- server.debug(`** PUT path ${API_PATHS.ACTIVESCREEN_PATH}. Request Params: ${JSON.stringify(req.params)}, Body: ${JSON.stringify(req.body)}, Method: ${req.method}, Path: ${req.path}`);
125
+ router.put(`${API_PATHS.ACTIVE_SCREEN}`, async (req: Request, res: Response) => {
126
+ server.debug(`** PUT ${API_PATHS.ACTIVE_SCREEN}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
78
127
  try {
79
- const dottedPath = pathToDotNotation(req.path);
80
- server.debug(`Converted request path ${req.path} to dot notation ${dottedPath} and updating SK path with req.body`);
128
+ const displayId = (req as Request & { displayId: string }).displayId
129
+ const fullPath = getDisplaySelfPath(displayId, 'activeScreen')
130
+ server.debug(`Updating SK path ${fullPath} with body.screenIdx`)
81
131
  server.handleMessage(
82
132
  plugin.id,
83
133
  {
@@ -85,8 +135,8 @@ export default (server: ServerAPI): Plugin => {
85
135
  {
86
136
  values: [
87
137
  {
88
- path: dottedPath as Path,
89
- value: req.body.screenId
138
+ path: fullPath as Path,
139
+ value: req.body.screenIdx !== undefined ? req.body.screenIdx : null
90
140
  }
91
141
  ]
92
142
  }
@@ -94,21 +144,80 @@ export default (server: ServerAPI): Plugin => {
94
144
  },
95
145
  SKVersion.v1
96
146
  );
97
- return res.status(200).json({
98
- state: 'SUCCESS',
99
- statusCode: 200
100
- });
147
+ return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
101
148
 
102
149
  } catch (error) {
103
150
  const msg = `HandleMessage failed with errors!`
104
151
  server.setPluginError(msg)
105
152
  server.error(`Error in HandleMessage: ${error}`);
106
153
 
107
- return res.status(400).json({
108
- state: 'FAILED',
109
- statusCode: 400,
110
- message: (error as Error).message
111
- });
154
+ return sendFail(res, 400, (error as Error).message)
155
+ }
156
+ });
157
+
158
+ router.get(API_PATHS.DISPLAYS, (req: Request, res: Response) => {
159
+ server.debug(`** GET ${API_PATHS.DISPLAYS}. Params: ${JSON.stringify(req.params)}`);
160
+ try {
161
+ const displays = readSelfDisplays();
162
+ const items =
163
+ displays && typeof displays === 'object'
164
+ ? Object.entries(displays)
165
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
166
+ .filter(([_, v]) => v && typeof v === 'object')
167
+ .map(([displayId, v]: [string, { displayName?: string }]) => ({
168
+ displayId,
169
+ displayName: v.displayName ?? null
170
+ }))
171
+ : [];
172
+
173
+ return res.status(200).json(items);
174
+ } catch (error) {
175
+ server.error(`Error reading displays: ${String((error as Error).message || error)}`);
176
+ return sendFail(res, 400, (error as Error).message)
177
+ }
178
+ });
179
+
180
+ router.get(`${API_PATHS.INSTANCE}`, (req: Request, res: Response) => {
181
+ server.debug(`** GET ${API_PATHS.INSTANCE}. Params: ${JSON.stringify(req.params)}`);
182
+ try {
183
+ const displayId = (req as Request & { displayId?: string }).displayId
184
+ if (!displayId) {
185
+ return sendFail(res, 400, 'Missing displayId parameter')
186
+ }
187
+
188
+ const fullPath = getDisplaySelfPath(displayId);
189
+ const value = server.getPath(fullPath);
190
+
191
+ if (value === undefined) {
192
+ return sendFail(res, 404, `Display ${displayId} not found`)
193
+ }
194
+
195
+ return sendOk(res, value);
196
+ } catch (error) {
197
+ server.error(`Error reading display ${req.params?.displayId}: ${String((error as Error).message || error)}`);
198
+ return sendFail(res, 400, (error as Error).message)
199
+ }
200
+ });
201
+
202
+ router.get(`${API_PATHS.ACTIVE_SCREEN}`, (req: Request, res: Response) => {
203
+ server.debug(`** GET ${API_PATHS.ACTIVE_SCREEN}. Params: ${JSON.stringify(req.params)}`);
204
+ try {
205
+ const displayId = (req as Request & { displayId?: string }).displayId
206
+ if (!displayId) {
207
+ return sendFail(res, 400, 'Missing displayId parameter')
208
+ }
209
+
210
+ const fullPath = getDisplaySelfPath(displayId, 'activeScreen');
211
+ const value = server.getPath(fullPath);
212
+
213
+ if (value === undefined) {
214
+ return sendFail(res, 404, `Active screen for display ${displayId} not found`)
215
+ }
216
+
217
+ return sendOk(res, value);
218
+ } catch (error) {
219
+ server.error(`Error reading activeScreen for ${req.params?.displayId}: ${String((error as Error).message || error)}`);
220
+ return sendFail(res, 400, (error as Error).message)
112
221
  }
113
222
  });
114
223
 
@@ -121,16 +230,10 @@ export default (server: ServerAPI): Plugin => {
121
230
  });
122
231
  }
123
232
 
124
- server.setPluginStatus(`Providing remote display control`);
125
- }
233
+ server.setPluginStatus(`Providing remote display screen control`);
234
+ },
235
+ getOpenApi: () => openapi
126
236
  };
127
237
 
128
- /*
129
- * Replace all / with . and remove leading.
130
- */
131
- function pathToDotNotation(path: string): string {
132
- return path.replace(/\//g, '.').replace(/^\./, '');
133
- }
134
-
135
238
  return plugin;
136
239
  }
@@ -0,0 +1,141 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "version": "1.0.0",
5
+ "title": "KIP Remote Displays API",
6
+ "description": "API endpoints to list KIP displays and control the active screen for a given KIP instance via Signal K self.displays tree.\n\nUsage:\n- List displays:\n curl -s http://localhost:3000/plugins/kip/displays\n- Read a display entry:\n curl -s http://localhost:3000/plugins/kip/displays/{displayId}\n- Set a display entry:\n curl -s -X PUT -H 'Content-Type: application/json' -d '{\"displayName\":\"Mast\"}' http://localhost:3000/plugins/kip/displays/{displayId}\n- Get active screen:\n curl -s http://localhost:3000/plugins/kip/displays/{displayId}/activeScreen\n- Set active screen:\n curl -s -X PUT -H 'Content-Type: application/json' -d '{\"screenIdx\":1}' http://localhost:3000/plugins/kip/displays/{displayId}/activeScreen",
7
+ "termsOfService": "http://signalk.org/terms/",
8
+ "license": {
9
+ "name": "Apache 2.0",
10
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
11
+ }
12
+ },
13
+ "externalDocs": {
14
+ "url": "http://signalk.org/specification/",
15
+ "description": "Signal K specification."
16
+ },
17
+ "servers": [
18
+ { "url": "/" }
19
+ ],
20
+ "tags": [
21
+ { "name": "Displays", "description": "KIP display discovery and control." }
22
+ ],
23
+ "components": {
24
+ "schemas": {
25
+ "DisplayInfo": {
26
+ "type": "object",
27
+ "properties": {
28
+ "displayId": { "type": "string", "description": "KIP instance UUID" },
29
+ "displayName": { "type": "string", "nullable": true }
30
+ },
31
+ "required": ["displayId"]
32
+ },
33
+ "SuccessResponse": {
34
+ "type": "object",
35
+ "properties": {
36
+ "state": { "type": "string", "enum": ["SUCCESS"] },
37
+ "statusCode": { "type": "integer", "enum": [200] }
38
+ },
39
+ "required": ["state", "statusCode"]
40
+ },
41
+ "ErrorResponse": {
42
+ "type": "object",
43
+ "properties": {
44
+ "state": { "type": "string", "enum": ["FAILED"] },
45
+ "statusCode": { "type": "integer" },
46
+ "message": { "type": "string" }
47
+ },
48
+ "required": ["state", "statusCode", "message"]
49
+ },
50
+ "ActiveScreenSetRequest": {
51
+ "type": "object",
52
+ "properties": {
53
+ "screenIdx": { "type": "integer", "nullable": true, "description": "Index of active screen or null to clear" }
54
+ }
55
+ },
56
+ "AnyObjectOrNull": {
57
+ "oneOf": [
58
+ { "type": "object", "additionalProperties": true },
59
+ { "type": "null" }
60
+ ]
61
+ }
62
+ },
63
+ "parameters": {
64
+ "DisplayIdParam": {
65
+ "in": "path",
66
+ "required": true,
67
+ "name": "displayId",
68
+ "description": "KIP instance UUID",
69
+ "schema": { "type": "string" }
70
+ }
71
+ },
72
+ "securitySchemes": {
73
+ "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" },
74
+ "cookieAuth": { "type": "apiKey", "in": "cookie", "name": "JAUTHENTICATION" }
75
+ }
76
+ },
77
+ "security": [{ "cookieAuth": [] }, { "bearerAuth": [] }],
78
+ "paths": {
79
+ "/plugins/kip/displays": {
80
+ "get": {
81
+ "tags": ["Displays"],
82
+ "summary": "List available KIP displays",
83
+ "responses": {
84
+ "200": {
85
+ "description": "List of KIP instances discovered under self.displays",
86
+ "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/DisplayInfo" } } } }
87
+ },
88
+ "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
89
+ }
90
+ }
91
+ },
92
+ "/plugins/kip/displays/{displayId}": {
93
+ "parameters": [ { "$ref": "#/components/parameters/DisplayIdParam" } ],
94
+ "get": {
95
+ "tags": ["Displays"],
96
+ "summary": "Get raw display entry under self.displays.{displayId}",
97
+ "responses": {
98
+ "200": { "description": "Display object", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnyObjectOrNull" } } } },
99
+ "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
100
+ "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
101
+ }
102
+ },
103
+ "put": {
104
+ "tags": ["Displays"],
105
+ "summary": "Set display entry under self.displays.{displayId}",
106
+ "requestBody": {
107
+ "required": false,
108
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnyObjectOrNull" } } }
109
+ },
110
+ "responses": {
111
+ "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } },
112
+ "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
113
+ }
114
+ }
115
+ },
116
+ "/plugins/kip/displays/{displayId}/activeScreen": {
117
+ "parameters": [ { "$ref": "#/components/parameters/DisplayIdParam" } ],
118
+ "get": {
119
+ "tags": ["Displays"],
120
+ "summary": "Get active screen index for display",
121
+ "responses": {
122
+ "200": { "description": "Active screen index or null", "content": { "application/json": { "schema": { "type": "integer", "nullable": true } } } },
123
+ "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
124
+ "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
125
+ }
126
+ },
127
+ "put": {
128
+ "tags": ["Displays"],
129
+ "summary": "Set active screen index for display",
130
+ "requestBody": {
131
+ "required": false,
132
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ActiveScreenSetRequest" } } }
133
+ },
134
+ "responses": {
135
+ "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessResponse" } } } },
136
+ "400": { "description": "Bad request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "3.10.0-beta.30",
3
+ "version": "3.10.0-beta.32",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {