@marineyachtradar/signalk-playback-plugin 0.1.1 → 0.2.0

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/README.md CHANGED
@@ -119,27 +119,22 @@ This plugin:
119
119
 
120
120
  ## Development
121
121
 
122
- Build options:
122
+ After cloning, install dependencies and build the GUI:
123
123
 
124
124
  ```bash
125
- # Build with GUI from npm (default)
126
- node build.js
127
-
128
- # Build with local mayara-gui (for development)
129
- node build.js --local-gui
130
-
131
- # Create tarball for manual install (includes public/)
132
- node build.js --local-gui --pack
125
+ npm install
126
+ npm run build
133
127
  ```
134
128
 
135
- The `--local-gui` option copies GUI files from the sibling `../mayara-gui` directory instead of from npm.
136
-
137
- The `--pack` option creates a `.tgz` tarball with `public/` included (normally excluded by `.npmignore`). Install with:
129
+ To use a local `mayara-gui` checkout (sibling directory) instead of npm:
138
130
 
139
131
  ```bash
140
- npm install /path/to/marineyachtradar-signalk-playback-plugin-0.1.0.tgz
132
+ npm run build -- --local-gui
141
133
  ```
142
134
 
135
+ > **Note:** The `public/` directory is gitignored but included in the npm tarball.
136
+ > It's built automatically during `npm publish` via `prepublishOnly`.
137
+
143
138
  ## Related Projects
144
139
 
145
140
  - **[mayara-server](https://github.com/MaYaRa-Marine/mayara-server)** - Standalone radar server (creates recordings)
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@marineyachtradar/signalk-playback-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MaYaRa Radar Playback - Play .mrr radar recordings through SignalK Radar API (Developer Tool)",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
7
7
  "build": "node build.js",
8
- "postinstall": "node build.js",
8
+ "prepublishOnly": "node build.js",
9
9
  "test": "echo \"No tests yet\" && exit 0"
10
10
  },
11
11
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "plugin/**/*",
32
- "build.js"
32
+ "public/**/*"
33
33
  ],
34
34
  "engines": {
35
35
  "signalk": ">=2.0.0"
package/public/api.js ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * API adapter for Mayara Radar v5
3
+ *
4
+ * Automatically detects whether running in SignalK or standalone mode
5
+ * and provides a unified API interface for the capabilities-driven v5 API.
6
+ */
7
+
8
+ // API endpoints for different modes
9
+ const SIGNALK_RADARS_API = "/signalk/v2/api/vessels/self/radars";
10
+ const STANDALONE_RADARS_API = "/v2/api/radars";
11
+ const STANDALONE_INTERFACES_API = "/v2/api/interfaces";
12
+
13
+ // Application Data API path - aligned with WASM SignalK plugin
14
+ // Uses same path so settings are shared between standalone and SignalK modes
15
+ const APPDATA_PATH = "/signalk/v1/applicationData/global/@mayara/signalk-radar";
16
+
17
+ // Detected mode (null = not detected yet)
18
+ let detectedMode = null;
19
+
20
+ // Cache for capabilities (fetched once per radar)
21
+ const capabilitiesCache = new Map();
22
+
23
+ /**
24
+ * Detect which API mode we're running in
25
+ * @returns {Promise<string>} 'signalk' or 'standalone'
26
+ */
27
+ export async function detectMode() {
28
+ if (detectedMode) {
29
+ return detectedMode;
30
+ }
31
+
32
+ // Try standalone first - check if /v2/api/radars returns 200
33
+ try {
34
+ const response = await fetch(STANDALONE_RADARS_API, { method: 'HEAD' });
35
+ if (response.ok) {
36
+ detectedMode = 'standalone';
37
+ console.log("Detected standalone mode");
38
+ return detectedMode;
39
+ }
40
+ } catch (e) {
41
+ // Standalone not available
42
+ }
43
+
44
+ // Try SignalK - check if endpoint returns 200
45
+ try {
46
+ const response = await fetch(SIGNALK_RADARS_API, { method: 'HEAD' });
47
+ if (response.ok) {
48
+ detectedMode = 'signalk';
49
+ console.log("Detected SignalK mode");
50
+ return detectedMode;
51
+ }
52
+ } catch (e) {
53
+ // SignalK not available
54
+ }
55
+
56
+ // Default to standalone
57
+ detectedMode = 'standalone';
58
+ console.log("Defaulting to standalone mode");
59
+ return detectedMode;
60
+ }
61
+
62
+ /**
63
+ * Get the radars API URL for current mode
64
+ * @returns {string} API URL
65
+ */
66
+ export function getRadarsUrl() {
67
+ return detectedMode === 'signalk' ? SIGNALK_RADARS_API : STANDALONE_RADARS_API;
68
+ }
69
+
70
+ /**
71
+ * Get the interfaces API URL (standalone only)
72
+ * @returns {string|null} API URL or null if not available
73
+ */
74
+ export function getInterfacesUrl() {
75
+ return detectedMode === 'standalone' ? STANDALONE_INTERFACES_API : null;
76
+ }
77
+
78
+ /**
79
+ * Fetch list of radar IDs
80
+ * @returns {Promise<string[]>} Array of radar IDs
81
+ */
82
+ export async function fetchRadarIds() {
83
+ await detectMode();
84
+
85
+ const response = await fetch(getRadarsUrl());
86
+ const data = await response.json();
87
+
88
+ // SignalK v5 returns array of IDs: ["Furuno-RD003212", "Navico-HALO"]
89
+ if (Array.isArray(data)) {
90
+ // Could be array of IDs (strings) or array of radar objects
91
+ if (data.length > 0 && typeof data[0] === 'string') {
92
+ return data;
93
+ }
94
+ // Legacy: array of radar objects
95
+ return data.map(r => r.id);
96
+ }
97
+
98
+ // Standalone returns object keyed by ID
99
+ return Object.keys(data);
100
+ }
101
+
102
+ /**
103
+ * Fetch list of radars (legacy compatibility)
104
+ * @returns {Promise<Object>} Radars object keyed by ID
105
+ */
106
+ export async function fetchRadars() {
107
+ await detectMode();
108
+
109
+ const response = await fetch(getRadarsUrl());
110
+ const data = await response.json();
111
+
112
+ // SignalK returns an array, standalone returns an object
113
+ if (detectedMode === 'signalk' && Array.isArray(data)) {
114
+ // Convert array to object keyed by id
115
+ const radars = {};
116
+ for (const radar of data) {
117
+ radars[radar.id] = radar;
118
+ }
119
+ return radars;
120
+ }
121
+
122
+ return data;
123
+ }
124
+
125
+ /**
126
+ * Fetch radar capabilities (v5 API)
127
+ * Returns the capability manifest with controls schema, characteristics, etc.
128
+ * @param {string} radarId - The radar ID
129
+ * @returns {Promise<Object>} Capability manifest
130
+ */
131
+ export async function fetchCapabilities(radarId) {
132
+ await detectMode();
133
+
134
+ // Don't cache capabilities - model info may be updated after TCP connects
135
+ // The radar model is identified via TCP $N96 response, which happens after
136
+ // initial discovery. Caching would return stale "Unknown" model.
137
+
138
+ const url = `${getRadarsUrl()}/${radarId}/capabilities`;
139
+ console.log(`Fetching capabilities: GET ${url}`);
140
+
141
+ const response = await fetch(url);
142
+ if (!response.ok) {
143
+ throw new Error(`Failed to fetch capabilities: ${response.status}`);
144
+ }
145
+
146
+ return response.json();
147
+ }
148
+
149
+ /**
150
+ * Fetch radar state (v5 API)
151
+ * Returns current values of all controls
152
+ * @param {string} radarId - The radar ID
153
+ * @returns {Promise<Object>} Radar state
154
+ */
155
+ export async function fetchState(radarId) {
156
+ await detectMode();
157
+
158
+ const url = `${getRadarsUrl()}/${radarId}/state`;
159
+
160
+ const response = await fetch(url);
161
+ if (!response.ok) {
162
+ throw new Error(`Failed to fetch state: ${response.status}`);
163
+ }
164
+
165
+ return response.json();
166
+ }
167
+
168
+ /**
169
+ * Clear cached capabilities (e.g., when radar disconnects)
170
+ * @param {string} radarId - The radar ID, or omit to clear all
171
+ */
172
+ export function clearCapabilitiesCache(radarId) {
173
+ if (radarId) {
174
+ capabilitiesCache.delete(radarId);
175
+ } else {
176
+ capabilitiesCache.clear();
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Fetch list of interfaces (standalone mode only)
182
+ * @returns {Promise<Object|null>} Interfaces object or null
183
+ */
184
+ export async function fetchInterfaces() {
185
+ await detectMode();
186
+
187
+ const url = getInterfacesUrl();
188
+ if (!url) {
189
+ return null;
190
+ }
191
+
192
+ const response = await fetch(url);
193
+ return response.json();
194
+ }
195
+
196
+ /**
197
+ * Check if we're in SignalK mode
198
+ * @returns {boolean}
199
+ */
200
+ export function isSignalKMode() {
201
+ return detectedMode === 'signalk';
202
+ }
203
+
204
+ /**
205
+ * Check if we're in standalone mode
206
+ * @returns {boolean}
207
+ */
208
+ export function isStandaloneMode() {
209
+ return detectedMode === 'standalone';
210
+ }
211
+
212
+ /**
213
+ * Map power control values to SignalK RadarStatus
214
+ * SignalK expects: 'off' | 'standby' | 'transmit' | 'warming'
215
+ */
216
+ function mapPowerValue(value) {
217
+ // Handle numeric or string values
218
+ const v = String(value);
219
+ if (v === '0' || v === 'off' || v === 'Off') return 'standby';
220
+ if (v === '1' || v === 'on' || v === 'On') return 'transmit';
221
+ // Pass through if already a valid RadarStatus
222
+ if (['off', 'standby', 'transmit', 'warming'].includes(v)) return v;
223
+ return v;
224
+ }
225
+
226
+ /**
227
+ * Send a control command to a radar via REST API (v5 format)
228
+ *
229
+ * SignalK Radar API v5 format:
230
+ * PUT /signalk/v2/api/vessels/self/radars/{radarId}/controls/{controlId}
231
+ * Body: { value: ... }
232
+ *
233
+ * @param {string} radarId - The radar ID
234
+ * @param {string} controlId - The control ID (e.g., "power", "gain", "range")
235
+ * @param {any} value - The value to set (type depends on control)
236
+ * @returns {Promise<boolean>} True if successful
237
+ */
238
+ export async function setControl(radarId, controlId, value) {
239
+ await detectMode();
240
+
241
+ const url = `${getRadarsUrl()}/${radarId}/controls/${controlId}`;
242
+ const body = { value };
243
+
244
+ console.log(`Setting control: PUT ${url}`, body);
245
+
246
+ try {
247
+ const response = await fetch(url, {
248
+ method: 'PUT',
249
+ headers: {
250
+ 'Content-Type': 'application/json',
251
+ },
252
+ body: JSON.stringify(body),
253
+ });
254
+
255
+ if (response.ok) {
256
+ console.log(`Control ${controlId} set successfully`);
257
+ return true;
258
+ } else {
259
+ const errorText = await response.text();
260
+ console.error(`Control command failed: ${response.status} ${response.statusText} for ${url}`, errorText);
261
+ return false;
262
+ }
263
+ } catch (e) {
264
+ console.error(`Control command error: ${e}`);
265
+ return false;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Get installation settings for a radar from Application Data API
271
+ * Uses same path as WASM SignalK plugin: @mayara/signalk-radar/1.0.0
272
+ * Structure: { "radars": { "radar-id": { "bearingAlignment": ..., ... } } }
273
+ * @param {string} radarId - The radar ID
274
+ * @returns {Promise<Object>} Installation settings object for this radar
275
+ */
276
+ export async function getInstallationSettings(radarId) {
277
+ const url = `${APPDATA_PATH}/1.0.0`;
278
+ try {
279
+ const response = await fetch(url);
280
+ if (!response.ok) return {};
281
+ const data = await response.json();
282
+ return data?.radars?.[radarId] || {};
283
+ } catch (e) {
284
+ console.warn('Failed to load installation settings:', e.message);
285
+ return {};
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Save an installation setting to Application Data API
291
+ * Preserves the nested structure used by WASM SignalK plugin
292
+ * @param {string} radarId - The radar ID
293
+ * @param {string} key - The setting key (e.g., "bearingAlignment")
294
+ * @param {any} value - The value to save
295
+ * @returns {Promise<boolean>} True if successful
296
+ */
297
+ export async function saveInstallationSetting(radarId, key, value) {
298
+ const url = `${APPDATA_PATH}/1.0.0`;
299
+ try {
300
+ // Load full structure (preserve other radars' settings)
301
+ const getResponse = await fetch(url);
302
+ const data = getResponse.ok ? await getResponse.json() : { radars: {} };
303
+
304
+ // Update nested value
305
+ if (!data.radars) data.radars = {};
306
+ if (!data.radars[radarId]) data.radars[radarId] = {};
307
+ data.radars[radarId][key] = value;
308
+
309
+ // Save back
310
+ const putResponse = await fetch(url, {
311
+ method: 'PUT',
312
+ headers: { 'Content-Type': 'application/json' },
313
+ body: JSON.stringify(data)
314
+ });
315
+ if (putResponse.ok) {
316
+ console.log(`Installation setting '${key}' saved for ${radarId}`);
317
+ return true;
318
+ } else {
319
+ console.error(`Failed to save installation setting: ${putResponse.status}`);
320
+ return false;
321
+ }
322
+ } catch (e) {
323
+ console.error('Failed to save installation setting:', e);
324
+ return false;
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Send a control command to a radar via REST API (legacy format)
330
+ *
331
+ * SignalK Radar API format:
332
+ * PUT /signalk/v2/api/vessels/self/radars/{radarId}/{controlName}
333
+ * Body: { value: ... }
334
+ *
335
+ * Power endpoint expects: { value: 'off' | 'standby' | 'transmit' | 'warming' }
336
+ * Range endpoint expects: { value: number } (meters)
337
+ * Gain endpoint expects: { auto: boolean, value?: number }
338
+ *
339
+ * @param {string} radarId - The radar ID
340
+ * @param {Object} controlData - The control data (id, value, auto, enabled)
341
+ * @param {Object} controls - The radar controls definition to map id to name
342
+ * @returns {Promise<boolean>} True if successful
343
+ */
344
+ export async function sendControlCommand(radarId, controlData, controls) {
345
+ await detectMode();
346
+
347
+ // Map control id to control name for the endpoint
348
+ // controlData.id is the control key (e.g., "1" for Power)
349
+ const controlDef = controls ? controls[controlData.id] : null;
350
+ const controlName = controlDef ? controlDef.name.toLowerCase() : `control-${controlData.id}`;
351
+
352
+ const url = `${getRadarsUrl()}/${radarId}/${controlName}`;
353
+
354
+ // Build the request body based on controlData and control type
355
+ let body;
356
+ if (controlName === 'power') {
357
+ // Power expects RadarStatus string
358
+ body = { value: mapPowerValue(controlData.value) };
359
+ } else if (controlName === 'range') {
360
+ // Range expects number in meters
361
+ body = { value: parseFloat(controlData.value) };
362
+ } else if (controlName === 'gain' || controlName === 'sea' || controlName === 'rain') {
363
+ // Gain/sea/rain expect { auto: boolean, value?: number }
364
+ body = {};
365
+ if ('auto' in controlData) {
366
+ body.auto = controlData.auto;
367
+ }
368
+ if (controlData.value !== undefined) {
369
+ body.value = parseFloat(controlData.value);
370
+ }
371
+ } else {
372
+ // Generic control
373
+ body = { value: controlData.value };
374
+ if ('auto' in controlData) {
375
+ body.auto = controlData.auto;
376
+ }
377
+ }
378
+
379
+ console.log(`Sending control: PUT ${url}`, body);
380
+
381
+ try {
382
+ const response = await fetch(url, {
383
+ method: 'PUT',
384
+ headers: {
385
+ 'Content-Type': 'application/json',
386
+ },
387
+ body: JSON.stringify(body),
388
+ });
389
+
390
+ if (response.ok) {
391
+ console.log(`Control command sent successfully: PUT ${url}`);
392
+ return true;
393
+ } else {
394
+ const errorText = await response.text();
395
+ console.error(`Control command failed: ${response.status} ${response.statusText} for ${url}`, errorText);
396
+ return false;
397
+ }
398
+ } catch (e) {
399
+ console.error(`Control command error: ${e}`);
400
+ return false;
401
+ }
402
+ }
Binary file
@@ -0,0 +1,91 @@
1
+ /* ============================================
2
+ Base Styles - Reset, Typography, Variables
3
+ ============================================ */
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+ }
8
+
9
+ html, body {
10
+ margin: 0;
11
+ padding: 0;
12
+ height: 100%;
13
+ overflow: hidden;
14
+ }
15
+
16
+ html {
17
+ box-sizing: content-box;
18
+ border: none;
19
+ font: normal 16px/1 Verdana, Geneva, sans-serif;
20
+ color: rgb(152, 217, 204);
21
+ -o-text-overflow: ellipsis;
22
+ text-overflow: ellipsis;
23
+ background-color: #000000;
24
+ }
25
+
26
+ /* Base table cells */
27
+ td.myr {
28
+ background-color: rgb(3, 37, 37);
29
+ }
30
+
31
+ td.myr_error {
32
+ color: firebrick;
33
+ background-color: rgb(3, 37, 37);
34
+ }
35
+
36
+ /* Error and warning messages */
37
+ div.myr_error {
38
+ background-color: firebrick;
39
+ color: wheat;
40
+ transition-property: opacity, display;
41
+ transition-duration: 0.5s;
42
+ transition-behavior: allow-discrete;
43
+ }
44
+
45
+ div.myr_error.myr_vanish {
46
+ display: none;
47
+ opacity: 0;
48
+ }
49
+
50
+ div.myr_warning {
51
+ background-color: rgb(60, 50, 10);
52
+ color: rgb(255, 200, 100);
53
+ padding: 10px;
54
+ border-radius: 5px;
55
+ margin: 10px 0;
56
+ }
57
+
58
+ /* Network help details styling for dark mode */
59
+ details {
60
+ color: rgb(152, 217, 204);
61
+ }
62
+
63
+ details div {
64
+ background: rgb(20, 40, 40) !important;
65
+ color: rgb(180, 200, 200);
66
+ }
67
+
68
+ details p {
69
+ margin: 5px 0;
70
+ }
71
+
72
+ details strong {
73
+ color: rgb(100, 200, 180);
74
+ }
75
+
76
+ /* Base form elements */
77
+ label {
78
+ display: block;
79
+ max-width: 150px;
80
+ }
81
+
82
+ input {
83
+ display: block;
84
+ background-color: darkblue;
85
+ color: wheat;
86
+ }
87
+
88
+ input:read-only {
89
+ background-color: #000000;
90
+ color:rgb(124, 124, 237);
91
+ }
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Mayara - Marine Yacht Radar Client Control</title>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
8
+ <meta http-equiv="Pragma" content="no-cache" />
9
+ <meta http-equiv="Expires" content="0" />
10
+ <link type="text/css" rel="stylesheet" href="base.css?v=1" />
11
+ <link type="text/css" rel="stylesheet" href="controls.css?v=1" />
12
+ <link type="text/css" rel="stylesheet" href="responsive.css?v=1" />
13
+ <script type="module" src="control.js?v=10"></script>
14
+ </head>
15
+ <body>
16
+ <div id="myr_controller" class="myr_controller">
17
+ <div id="myr_title">Radar Controls</div>
18
+ <div id="myr_error" class="myr_error" style="visibility: hidden;"></div>
19
+ <div id="myr_controls" class="myr_control">
20
+ </div>
21
+ </div>
22
+ </body>
23
+ </html>