@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.
package/plugin/index.js CHANGED
@@ -1,12 +1,65 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  const server_api_1 = require("@signalk/server-api");
4
- //import * as openapi from './openApi.json';
37
+ const openapi = __importStar(require("./openApi.json"));
5
38
  exports.default = (server) => {
6
39
  const API_PATHS = {
7
- DISPLAYS_PATH: `/displays/:kipId`,
8
- ACTIVESCREEN_PATH: `/displays/:kipId/activeScreen`
40
+ DISPLAYS: `/displays`,
41
+ INSTANCE: `/displays/:displayId`,
42
+ ACTIVE_SCREEN: `/displays/:displayId/activeScreen`
9
43
  };
44
+ // Helpers
45
+ function getDisplaySelfPath(displayId, suffix) {
46
+ const tail = suffix ? `.${suffix}` : '';
47
+ const full = server.getSelfPath(`displays.${displayId}${tail}`);
48
+ server.debug(`getDisplaySelfPath: displayId=${displayId}, suffix=${suffix}, fullPath transform output=${full}`);
49
+ return full;
50
+ }
51
+ function readSelfDisplays() {
52
+ const fullPath = server.getSelfPath('displays'); // e.g., vessels.self.displays
53
+ return server.getPath(fullPath);
54
+ }
55
+ function sendOk(res, body) {
56
+ if (body === undefined)
57
+ return res.status(204).end();
58
+ return res.status(200).json(body);
59
+ }
60
+ function sendFail(res, statusCode, message) {
61
+ return res.status(statusCode).json({ state: 'FAILED', statusCode, message });
62
+ }
10
63
  const plugin = {
11
64
  id: 'kip',
12
65
  name: 'KIP',
@@ -14,8 +67,6 @@ exports.default = (server) => {
14
67
  start: (settings) => {
15
68
  server.debug(`Starting plugin with settings: ${JSON.stringify(settings)}`);
16
69
  server.setPluginStatus(`Starting...`);
17
- const p = server.getSelfPath('displays.*');
18
- server.debug(`Self path for displays.*: ${p}`);
19
70
  },
20
71
  stop: () => {
21
72
  server.debug(`Stopping plugin`);
@@ -29,71 +80,156 @@ exports.default = (server) => {
29
80
  };
30
81
  },
31
82
  registerWithRouter(router) {
32
- server.debug(`Registering plugin routes: ${API_PATHS.DISPLAYS_PATH}`);
33
- router.put(`${API_PATHS.DISPLAYS_PATH}`, async (req, res) => {
34
- 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}`);
83
+ server.debug(`Registering plugin routes: ${API_PATHS.DISPLAYS}, ${API_PATHS.INSTANCE}, ${API_PATHS.ACTIVE_SCREEN}`);
84
+ // Validate/normalize :displayId where present
85
+ router.param('displayId', (req, res, next, displayId) => {
86
+ if (displayId == null)
87
+ return sendFail(res, 400, 'Missing displayId parameter');
35
88
  try {
36
- const dottedPath = pathToDotNotation(req.path);
37
- server.debug(`Converted request path ${req.path} to dot notation ${dottedPath} and updating SK path with req.body`);
89
+ let id = String(displayId);
90
+ // Decode percent-encoding if present
91
+ try {
92
+ id = decodeURIComponent(id);
93
+ }
94
+ catch {
95
+ // ignore decode errors, keep original id
96
+ }
97
+ // If someone sent JSON as the path segment, try to recover {"displayId":"..."}
98
+ if (id.trim().startsWith('{')) {
99
+ try {
100
+ const parsed = JSON.parse(id);
101
+ if (parsed && typeof parsed.displayId === 'string') {
102
+ id = parsed.displayId;
103
+ }
104
+ else {
105
+ return sendFail(res, 400, 'Invalid displayId format in JSON');
106
+ }
107
+ }
108
+ catch {
109
+ return sendFail(res, 400, 'Invalid displayId JSON');
110
+ }
111
+ }
112
+ // Basic safety: allow UUID-like strings (alphanum + dash)
113
+ if (!/^[A-Za-z0-9-]+$/.test(id)) {
114
+ return sendFail(res, 400, 'Invalid displayId format');
115
+ }
116
+ req.displayId = id;
117
+ next();
118
+ }
119
+ catch {
120
+ return sendFail(res, 400, 'Missing or invalid displayId parameter');
121
+ }
122
+ });
123
+ router.put(`${API_PATHS.INSTANCE}`, async (req, res) => {
124
+ server.debug(`** PUT ${API_PATHS.INSTANCE}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
125
+ try {
126
+ const displayId = req.displayId;
127
+ const fullPath = getDisplaySelfPath(displayId);
128
+ server.debug(`Updating SK path ${fullPath} with body`);
38
129
  server.handleMessage(plugin.id, {
39
130
  updates: [
40
131
  {
41
132
  values: [
42
133
  {
43
- path: dottedPath,
44
- value: req.body ? req.body : null
134
+ path: fullPath,
135
+ value: req.body ?? null
45
136
  }
46
137
  ]
47
138
  }
48
139
  ]
49
140
  }, server_api_1.SKVersion.v1);
50
- return res.status(200).json({
51
- state: 'SUCCESS',
52
- statusCode: 200
53
- });
141
+ return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
54
142
  }
55
143
  catch (error) {
56
144
  const msg = `HandleMessage failed with errors!`;
57
145
  server.setPluginError(msg);
58
146
  server.error(`Error in HandleMessage: ${error}`);
59
- return res.status(400).json({
60
- state: 'FAILED',
61
- statusCode: 400,
62
- message: error.message
63
- });
147
+ return sendFail(res, 400, error.message);
64
148
  }
65
149
  });
66
- router.put(`${API_PATHS.ACTIVESCREEN_PATH}`, async (req, res) => {
67
- 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}`);
150
+ router.put(`${API_PATHS.ACTIVE_SCREEN}`, async (req, res) => {
151
+ server.debug(`** PUT ${API_PATHS.ACTIVE_SCREEN}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
68
152
  try {
69
- const dottedPath = pathToDotNotation(req.path);
70
- server.debug(`Converted request path ${req.path} to dot notation ${dottedPath} and updating SK path with req.body`);
153
+ const displayId = req.displayId;
154
+ const fullPath = getDisplaySelfPath(displayId, 'activeScreen');
155
+ server.debug(`Updating SK path ${fullPath} with body.screenIdx`);
71
156
  server.handleMessage(plugin.id, {
72
157
  updates: [
73
158
  {
74
159
  values: [
75
160
  {
76
- path: dottedPath,
77
- value: req.body.screenId
161
+ path: fullPath,
162
+ value: req.body.screenIdx !== undefined ? req.body.screenIdx : null
78
163
  }
79
164
  ]
80
165
  }
81
166
  ]
82
167
  }, server_api_1.SKVersion.v1);
83
- return res.status(200).json({
84
- state: 'SUCCESS',
85
- statusCode: 200
86
- });
168
+ return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
87
169
  }
88
170
  catch (error) {
89
171
  const msg = `HandleMessage failed with errors!`;
90
172
  server.setPluginError(msg);
91
173
  server.error(`Error in HandleMessage: ${error}`);
92
- return res.status(400).json({
93
- state: 'FAILED',
94
- statusCode: 400,
95
- message: error.message
96
- });
174
+ return sendFail(res, 400, error.message);
175
+ }
176
+ });
177
+ router.get(API_PATHS.DISPLAYS, (req, res) => {
178
+ server.debug(`** GET ${API_PATHS.DISPLAYS}. Params: ${JSON.stringify(req.params)}`);
179
+ try {
180
+ const displays = readSelfDisplays();
181
+ const items = displays && typeof displays === 'object'
182
+ ? Object.entries(displays)
183
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
184
+ .filter(([_, v]) => v && typeof v === 'object')
185
+ .map(([displayId, v]) => ({
186
+ displayId,
187
+ displayName: v.displayName ?? null
188
+ }))
189
+ : [];
190
+ return res.status(200).json(items);
191
+ }
192
+ catch (error) {
193
+ server.error(`Error reading displays: ${String(error.message || error)}`);
194
+ return sendFail(res, 400, error.message);
195
+ }
196
+ });
197
+ router.get(`${API_PATHS.INSTANCE}`, (req, res) => {
198
+ server.debug(`** GET ${API_PATHS.INSTANCE}. Params: ${JSON.stringify(req.params)}`);
199
+ try {
200
+ const displayId = req.displayId;
201
+ if (!displayId) {
202
+ return sendFail(res, 400, 'Missing displayId parameter');
203
+ }
204
+ const fullPath = getDisplaySelfPath(displayId);
205
+ const value = server.getPath(fullPath);
206
+ if (value === undefined) {
207
+ return sendFail(res, 404, `Display ${displayId} not found`);
208
+ }
209
+ return sendOk(res, value);
210
+ }
211
+ catch (error) {
212
+ server.error(`Error reading display ${req.params?.displayId}: ${String(error.message || error)}`);
213
+ return sendFail(res, 400, error.message);
214
+ }
215
+ });
216
+ router.get(`${API_PATHS.ACTIVE_SCREEN}`, (req, res) => {
217
+ server.debug(`** GET ${API_PATHS.ACTIVE_SCREEN}. Params: ${JSON.stringify(req.params)}`);
218
+ try {
219
+ const displayId = req.displayId;
220
+ if (!displayId) {
221
+ return sendFail(res, 400, 'Missing displayId parameter');
222
+ }
223
+ const fullPath = getDisplaySelfPath(displayId, 'activeScreen');
224
+ const value = server.getPath(fullPath);
225
+ if (value === undefined) {
226
+ return sendFail(res, 404, `Active screen for display ${displayId} not found`);
227
+ }
228
+ return sendOk(res, value);
229
+ }
230
+ catch (error) {
231
+ server.error(`Error reading activeScreen for ${req.params?.displayId}: ${String(error.message || error)}`);
232
+ return sendFail(res, 400, error.message);
97
233
  }
98
234
  });
99
235
  // List all registered routes for debugging
@@ -104,14 +240,9 @@ exports.default = (server) => {
104
240
  }
105
241
  });
106
242
  }
107
- server.setPluginStatus(`Providing remote display control`);
108
- }
243
+ server.setPluginStatus(`Providing remote display screen control`);
244
+ },
245
+ getOpenApi: () => openapi
109
246
  };
110
- /*
111
- * Replace all / with . and remove leading.
112
- */
113
- function pathToDotNotation(path) {
114
- return path.replace(/\//g, '.').replace(/^\./, '');
115
- }
116
247
  return plugin;
117
248
  };
@@ -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
+ }