@marineyachtradar/signalk-playback-plugin 0.1.2 → 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.
@@ -0,0 +1,510 @@
1
+ import van from "./van-1.5.2.debug.js";
2
+ import { fetchRadars, fetchInterfaces, isStandaloneMode, detectMode } from "./api.js";
3
+
4
+ const { a, tr, td, div, p, strong, details, summary, code, br, span } = van.tags;
5
+
6
+ // Network requirements for different radar brands
7
+ const NETWORK_REQUIREMENTS = {
8
+ furuno: {
9
+ ipRange: "172.31.x.x/16",
10
+ description: "Furuno DRS radars require the host to have an IP address in the 172.31.x.x range.",
11
+ setup: [
12
+ "Configure your network interface with an IP like 172.31.3.100/16",
13
+ "Connect to the radar network (usually via ethernet)",
14
+ "Ensure no firewall blocks UDP ports 10010, 10024, 10021"
15
+ ],
16
+ example: "ip addr add 172.31.3.100/16 dev eth1"
17
+ },
18
+ navico: {
19
+ ipRange: "236.6.7.x (multicast)",
20
+ description: "Navico (Simrad/Lowrance/B&G) radars use multicast.",
21
+ setup: ["Ensure your network supports multicast routing"]
22
+ },
23
+ raymarine: {
24
+ ipRange: "232.1.1.x (multicast)",
25
+ description: "Raymarine radars use multicast.",
26
+ setup: ["Ensure your network supports multicast routing"]
27
+ },
28
+ garmin: {
29
+ ipRange: "239.254.2.x (multicast)",
30
+ description: "Garmin xHD radars use multicast.",
31
+ setup: ["Ensure your network supports multicast routing"]
32
+ }
33
+ };
34
+
35
+ // Detect operating system
36
+ function detectOS() {
37
+ const ua = navigator.userAgent.toLowerCase();
38
+ const platform = navigator.platform?.toLowerCase() || '';
39
+
40
+ // Check mobile/tablet FIRST (iPadOS reports as macOS in Safari)
41
+ if (ua.includes('iphone') || ua.includes('ipad')) return 'ios';
42
+ // Also detect iPad via touch + macOS combination (iPadOS 13+ desktop mode)
43
+ if (navigator.maxTouchPoints > 1 && (ua.includes('mac') || platform.includes('mac'))) return 'ios';
44
+ if (ua.includes('android')) return 'android';
45
+
46
+ // Desktop OS detection
47
+ if (ua.includes('win') || platform.includes('win')) return 'windows';
48
+ if (ua.includes('mac') || platform.includes('mac')) return 'macos';
49
+ if (ua.includes('linux') || platform.includes('linux')) return 'linux';
50
+ return 'unknown';
51
+ }
52
+
53
+ // Detect browser
54
+ function detectBrowser() {
55
+ const ua = navigator.userAgent.toLowerCase();
56
+
57
+ if (ua.includes('edg/')) return 'edge';
58
+ if (ua.includes('chrome')) return 'chrome';
59
+ if (ua.includes('firefox')) return 'firefox';
60
+ if (ua.includes('safari') && !ua.includes('chrome')) return 'safari';
61
+ return 'unknown';
62
+ }
63
+
64
+ // Check if using a secure context
65
+ // Note: localhost and 127.0.0.1 are treated as secure contexts by browsers
66
+ function isSecureContext() {
67
+ return window.isSecureContext;
68
+ }
69
+
70
+ // Check WebGPU support and show appropriate warnings
71
+ async function checkWebGPUSupport() {
72
+ const warningDiv = document.getElementById('webgpu_warning');
73
+ if (!warningDiv) return true;
74
+
75
+ const os = detectOS();
76
+ const browser = detectBrowser();
77
+ const isSecure = isSecureContext();
78
+ const hasWebGPUApi = !!navigator.gpu;
79
+
80
+ // Track why WebGPU failed
81
+ let failureReason = null;
82
+ let hasWorkingAdapter = false;
83
+
84
+ if (hasWebGPUApi) {
85
+ try {
86
+ const adapter = await navigator.gpu.requestAdapter();
87
+ if (adapter) {
88
+ // WebGPU fully working!
89
+ warningDiv.style.display = 'none';
90
+ return true;
91
+ } else {
92
+ failureReason = 'no-adapter'; // API exists but no GPU adapter found
93
+ }
94
+ } catch (e) {
95
+ console.warn('WebGPU adapter request failed:', e);
96
+ failureReason = 'adapter-error';
97
+ }
98
+ } else {
99
+ // API doesn't exist - could be secure context issue or browser doesn't support it
100
+ failureReason = isSecure ? 'no-api' : 'insecure-context';
101
+ }
102
+
103
+ // Build warning message based on situation
104
+ warningDiv.style.display = 'block';
105
+ warningDiv.innerHTML = '';
106
+
107
+ const title = div({ class: 'myr_warning_title' }, 'WebGPU Not Available');
108
+ van.add(warningDiv, title);
109
+
110
+ const content = div({ class: 'myr_warning_content' });
111
+ van.add(warningDiv, content);
112
+
113
+ // Secure context warning (if not secure)
114
+ if (!isSecure) {
115
+ const hostname = window.location.hostname;
116
+ const port = window.location.port || '80';
117
+ const isMobile = (os === 'ios' || os === 'android');
118
+
119
+ van.add(content, div({ class: 'myr_warning_item myr_warning_https' },
120
+ strong('Secure Context Required'),
121
+ p('WebGPU requires a secure context. You are currently using HTTP on "', hostname, '".'),
122
+ p('Options:'),
123
+ div({ class: 'myr_warning_options' },
124
+ // Only show localhost option for desktop (SignalK won't run on mobile)
125
+ !isMobile ? div({ class: 'myr_warning_option' },
126
+ strong('Option 1 (easiest): '), 'Access via localhost instead:',
127
+ div({ class: 'myr_code_block' },
128
+ p(code('http://localhost:' + port), ' or ', code('http://127.0.0.1:' + port)),
129
+ p({ class: 'myr_note' }, 'Browsers treat localhost as a secure context')
130
+ )
131
+ ) : null,
132
+ div({ class: 'myr_warning_option' },
133
+ strong(isMobile ? 'Option 1: ' : 'Option 2: '), 'Add this site to browser exceptions:',
134
+ getInsecureOriginInstructions(browser, os)
135
+ ),
136
+ div({ class: 'myr_warning_option' },
137
+ strong(isMobile ? 'Option 2: ' : 'Option 3: '), 'Use HTTPS (requires server configuration)'
138
+ )
139
+ )
140
+ ));
141
+ }
142
+
143
+ // Always show browser-specific WebGPU/hardware acceleration instructions
144
+ van.add(content, div({ class: 'myr_warning_item' },
145
+ strong('Enable WebGPU / Hardware Acceleration'),
146
+ getBrowserInstructions(browser, os)
147
+ ));
148
+
149
+ return false;
150
+ }
151
+
152
+ function getInsecureOriginInstructions(browser, os) {
153
+ const origin = window.location.origin;
154
+
155
+ // iOS Safari has no way to add insecure origin exceptions
156
+ if (os === 'ios') {
157
+ return div({ class: 'myr_code_block' },
158
+ p('Safari on iOS/iPadOS does not support insecure origin exceptions.'),
159
+ p('Alternatives:'),
160
+ p('• Configure HTTPS on your SignalK server'),
161
+ p('• Use a tunneling service (e.g., ngrok) to get an HTTPS URL'),
162
+ p('• Access from a desktop browser where you can set the flag')
163
+ );
164
+ }
165
+
166
+ // Android Chrome
167
+ if (os === 'android' && browser === 'chrome') {
168
+ return div({ class: 'myr_code_block' },
169
+ p('1. Open Chrome on your Android device'),
170
+ p('2. Go to: ', code('chrome://flags/#unsafely-treat-insecure-origin-as-secure')),
171
+ p('3. Add: ', code(origin)),
172
+ p('4. Set to "Enabled"'),
173
+ p('5. Tap "Relaunch"')
174
+ );
175
+ }
176
+
177
+ switch (browser) {
178
+ case 'chrome':
179
+ case 'edge':
180
+ const flagPrefix = browser === 'edge' ? 'edge' : 'chrome';
181
+ const flagUrl = `${flagPrefix}://flags/#unsafely-treat-insecure-origin-as-secure`;
182
+ return div({ class: 'myr_code_block' },
183
+ p('1. Copy and paste this into your address bar:'),
184
+ p(a({ href: flagUrl, class: 'myr_flag_link' }, code(flagUrl))),
185
+ p('2. In the text field, add: ', code(origin)),
186
+ p('3. Set dropdown to "Enabled"'),
187
+ p('4. Click "Relaunch" at the bottom')
188
+ );
189
+ case 'firefox':
190
+ return div({ class: 'myr_code_block' },
191
+ p('1. Open: ', a({ href: 'about:config', class: 'myr_flag_link' }, code('about:config'))),
192
+ p('2. Click "Accept the Risk and Continue"'),
193
+ p('3. Search for: ', code('dom.securecontext.allowlist')),
194
+ p('4. Click the + button to add: ', code(window.location.hostname)),
195
+ p('5. Restart Firefox')
196
+ );
197
+ default:
198
+ return div({ class: 'myr_code_block' },
199
+ p('Check your browser settings for allowing insecure origins.')
200
+ );
201
+ }
202
+ }
203
+
204
+ function getBrowserInstructions(browser, os) {
205
+ // iOS/iPadOS Safari
206
+ if (browser === 'safari' && os === 'ios') {
207
+ return div({ class: 'myr_code_block' },
208
+ p('Safari on iOS/iPadOS 17+:'),
209
+ p('1. Open ', strong('Settings'), ' app'),
210
+ p('2. Scroll down and tap ', strong('Safari')),
211
+ p('3. Scroll down and tap ', strong('Advanced')),
212
+ p('4. Tap ', strong('Feature Flags')),
213
+ p('5. Enable ', strong('WebGPU')),
214
+ p('6. Return to Safari and reload this page'),
215
+ p({ class: 'myr_note' }, 'Note: Requires iOS/iPadOS 17 or later.')
216
+ );
217
+ }
218
+
219
+ switch (browser) {
220
+ case 'chrome':
221
+ return div({ class: 'myr_code_block' },
222
+ p('Chrome should have WebGPU enabled by default (v113+).'),
223
+ p('If not working, try:'),
224
+ p('1. Open: ', code('chrome://flags/#enable-unsafe-webgpu')),
225
+ p('2. Set to "Enabled"'),
226
+ p('3. Relaunch Chrome'),
227
+ os === 'linux' ? p({ class: 'myr_note' },
228
+ 'Note: On Linux, you may need Vulkan drivers installed.') : null
229
+ );
230
+ case 'edge':
231
+ return div({ class: 'myr_code_block' },
232
+ p('Edge should have WebGPU enabled by default.'),
233
+ p('If not working, try:'),
234
+ p('1. Open: ', code('edge://flags/#enable-unsafe-webgpu')),
235
+ p('2. Set to "Enabled"'),
236
+ p('3. Relaunch Edge')
237
+ );
238
+ case 'firefox':
239
+ return div({ class: 'myr_code_block' },
240
+ p('Firefox WebGPU is experimental:'),
241
+ p('1. Open: ', code('about:config')),
242
+ p('2. Search for: ', code('dom.webgpu.enabled')),
243
+ p('3. Set to: ', code('true')),
244
+ p('4. Restart Firefox'),
245
+ p({ class: 'myr_note' }, 'Note: Firefox WebGPU support is still in development.')
246
+ );
247
+ case 'safari':
248
+ return div({ class: 'myr_code_block' },
249
+ p('Safari WebGPU (macOS 14+):'),
250
+ p('1. Open Safari menu > Settings'),
251
+ p('2. Go to Advanced tab'),
252
+ p('3. Check "Show features for web developers"'),
253
+ p('4. Go to Feature Flags tab'),
254
+ p('5. Enable "WebGPU"'),
255
+ p('6. Restart Safari')
256
+ );
257
+ default:
258
+ return div({ class: 'myr_code_block' },
259
+ p('WebGPU requires a modern browser:'),
260
+ p('- Chrome 113+ (recommended)'),
261
+ p('- Edge 113+'),
262
+ p('- Safari 17+ (macOS/iOS)'),
263
+ p('- Firefox Nightly (experimental)')
264
+ );
265
+ }
266
+ }
267
+
268
+ function getHardwareAccelerationInstructions(browser, os) {
269
+ // iOS/iPadOS - no hardware acceleration toggle
270
+ if (os === 'ios') {
271
+ return div({ class: 'myr_code_block' },
272
+ p('On iOS/iPadOS, hardware acceleration cannot be disabled.'),
273
+ p('If WebGPU is not working:'),
274
+ p('• Ensure you have iOS/iPadOS 17 or later'),
275
+ p('• Try closing and reopening Safari'),
276
+ p('• Restart your device')
277
+ );
278
+ }
279
+
280
+ switch (browser) {
281
+ case 'chrome':
282
+ return div({ class: 'myr_code_block' },
283
+ p('1. Open: ', code('chrome://settings/system')),
284
+ p('2. Enable "Use graphics acceleration when available"'),
285
+ p('3. Relaunch Chrome')
286
+ );
287
+ case 'edge':
288
+ return div({ class: 'myr_code_block' },
289
+ p('1. Open: ', code('edge://settings/system')),
290
+ p('2. Enable "Use graphics acceleration when available"'),
291
+ p('3. Relaunch Edge')
292
+ );
293
+ case 'firefox':
294
+ return div({ class: 'myr_code_block' },
295
+ p('1. Open: ', code('about:preferences')),
296
+ p('2. Scroll to "Performance"'),
297
+ p('3. Uncheck "Use recommended performance settings"'),
298
+ p('4. Check "Use hardware acceleration when available"'),
299
+ p('5. Restart Firefox')
300
+ );
301
+ case 'safari':
302
+ return div({ class: 'myr_code_block' },
303
+ p('Safari uses hardware acceleration by default on macOS.'),
304
+ p('If WebGPU is not working:'),
305
+ p('• Ensure you have macOS 14 (Sonoma) or later'),
306
+ p('• Check that WebGPU is enabled in Feature Flags'),
307
+ p('• Try restarting Safari')
308
+ );
309
+ default:
310
+ return div({ class: 'myr_code_block' },
311
+ p('Check your browser settings for "Hardware acceleration"'),
312
+ p('or "Use GPU" and ensure it is enabled.'),
313
+ p('Then restart the browser.')
314
+ );
315
+ }
316
+ }
317
+
318
+ const RadarEntry = (radar) => {
319
+ // Build display name: "Brand Model (Name)" or "Brand Name" if no model
320
+ const brand = radar.brand || '';
321
+ const model = radar.model || '';
322
+ const name = radar.name || '';
323
+
324
+ let displayName;
325
+ if (model && model !== 'Unknown') {
326
+ displayName = `${brand} ${model} (${name})`;
327
+ } else {
328
+ displayName = `${brand} ${name}`;
329
+ }
330
+
331
+ return tr({ class: 'myr_radar_row' },
332
+ td({ class: 'myr_radar_name' }, displayName),
333
+ td({ class: 'myr_radar_actions' },
334
+ a({ href: "viewer.html?id=" + radar.id, class: 'myr_radar_link myr_radar_link_primary' },
335
+ 'Open Radar Display'
336
+ ),
337
+ a({ href: "control.html?id=" + radar.id, class: 'myr_radar_link myr_radar_link_secondary' },
338
+ 'Controls Only'
339
+ )
340
+ )
341
+ );
342
+ };
343
+
344
+ // Track previous radar count to avoid unnecessary DOM rebuilds
345
+ let previousRadarCount = -1;
346
+
347
+ function radarsLoaded(d) {
348
+ let radarIds = Object.keys(d);
349
+ let c = radarIds.length;
350
+ let r = document.getElementById("radars");
351
+
352
+ // Only rebuild if radar count changed (avoids collapsing the help details)
353
+ if (c === previousRadarCount && c === 0) {
354
+ // No change, just reschedule poll
355
+ setTimeout(loadRadars, 2000);
356
+ return;
357
+ }
358
+ previousRadarCount = c;
359
+
360
+ // Clear previous content
361
+ r.innerHTML = "";
362
+
363
+ if (c > 0) {
364
+ van.add(r, div({ class: 'myr_section_title' },
365
+ span({ class: 'myr_radar_count' }, c),
366
+ ' Radar' + (c > 1 ? 's' : '') + ' Detected'
367
+ ));
368
+
369
+ let table = document.createElement("table");
370
+ table.className = 'myr_radar_table';
371
+ r.appendChild(table);
372
+
373
+ radarIds.sort().forEach(function (v, i) {
374
+ // Pass the full radar object (includes id, name, brand, model)
375
+ const radar = { ...d[v], id: v };
376
+ van.add(table, RadarEntry(radar));
377
+ });
378
+
379
+ // Radar found, poll less frequently
380
+ setTimeout(loadRadars, 15000);
381
+ } else {
382
+ van.add(r, div({ class: 'myr_detecting' },
383
+ span({ class: 'myr_pulse' }),
384
+ 'Searching for radars...'
385
+ ));
386
+
387
+ // Show network requirements help
388
+ van.add(r,
389
+ details({ class: 'myr_network_help' },
390
+ summary('Network Configuration Help'),
391
+ div({ class: 'myr_help_content' },
392
+ // Furuno section
393
+ div({ class: 'myr_brand_section' },
394
+ div({ class: 'myr_brand_header' }, 'Furuno DRS (DRS4D-NXT, DRS6A-NXT, etc.)'),
395
+ p(NETWORK_REQUIREMENTS.furuno.description),
396
+ div({ class: 'myr_setup_steps' },
397
+ NETWORK_REQUIREMENTS.furuno.setup.map((step, i) =>
398
+ div({ class: 'myr_setup_step' }, (i + 1) + '. ' + step)
399
+ )
400
+ ),
401
+ div({ class: 'myr_code_example' },
402
+ code(NETWORK_REQUIREMENTS.furuno.example)
403
+ )
404
+ ),
405
+
406
+ // Other brands
407
+ div({ class: 'myr_brand_section myr_brand_other' },
408
+ div({ class: 'myr_brand_header' }, 'Navico (Simrad, Lowrance, B&G)'),
409
+ p(NETWORK_REQUIREMENTS.navico.description)
410
+ ),
411
+
412
+ div({ class: 'myr_brand_section myr_brand_other' },
413
+ div({ class: 'myr_brand_header' }, 'Raymarine'),
414
+ p(NETWORK_REQUIREMENTS.raymarine.description)
415
+ ),
416
+
417
+ div({ class: 'myr_brand_section myr_brand_other' },
418
+ div({ class: 'myr_brand_header' }, 'Garmin xHD'),
419
+ p(NETWORK_REQUIREMENTS.garmin.description)
420
+ )
421
+ )
422
+ )
423
+ );
424
+
425
+ // No radar found, poll more frequently (every 2 seconds)
426
+ setTimeout(loadRadars, 2000);
427
+ }
428
+ }
429
+
430
+ function interfacesLoaded(d) {
431
+ if (!d || !d.interfaces) {
432
+ return;
433
+ }
434
+
435
+ let c = Object.keys(d.interfaces).length;
436
+ if (c > 0) {
437
+ let r = document.getElementById("interfaces");
438
+ r.innerHTML = "";
439
+
440
+ van.add(r, div({ class: 'myr_section_title' }, 'Network Interfaces'));
441
+
442
+ let table = document.createElement("table");
443
+ table.className = 'myr_interface_table';
444
+ r.appendChild(table);
445
+
446
+ let brands = ["Interface", ...d.brands];
447
+ let hdr = van.add(table, tr({ class: 'myr_interface_header' }));
448
+ brands.forEach((v) => van.add(hdr, td(v)));
449
+
450
+ let interfaces = d.interfaces;
451
+ if (interfaces) {
452
+ console.log("interfaces", interfaces);
453
+ Object.keys(interfaces).forEach(function (v, i) {
454
+ let row = van.add(table, tr());
455
+
456
+ van.add(row, td({ class: 'myr_interface_name' }, v));
457
+ if (interfaces[v].status) {
458
+ van.add(row, td({ class: 'myr_interface_error', colspan: d.brands.length }, interfaces[v].status));
459
+ } else {
460
+ d.brands.forEach((b) => {
461
+ let status = interfaces[v].listeners[b];
462
+ let className = (status == "Listening" || status == "Active")
463
+ ? 'myr_interface_ok'
464
+ : 'myr_interface_error';
465
+ van.add(row, td({ class: className }, status));
466
+ });
467
+ }
468
+ });
469
+ }
470
+ }
471
+ }
472
+
473
+ async function loadRadars() {
474
+ try {
475
+ const radars = await fetchRadars();
476
+ radarsLoaded(radars);
477
+ } catch (err) {
478
+ console.error("Failed to load radars:", err);
479
+ setTimeout(loadRadars, 15000);
480
+ }
481
+ }
482
+
483
+ async function loadInterfaces() {
484
+ try {
485
+ const interfaces = await fetchInterfaces();
486
+ if (interfaces) {
487
+ interfacesLoaded(interfaces);
488
+ } else {
489
+ // Hide interfaces section in SignalK mode
490
+ let r = document.getElementById("interfaces");
491
+ if (r) {
492
+ r.style.display = "none";
493
+ }
494
+ }
495
+ } catch (err) {
496
+ console.error("Failed to load interfaces:", err);
497
+ }
498
+ }
499
+
500
+ window.onload = async function () {
501
+ // Check WebGPU support first
502
+ await checkWebGPUSupport();
503
+
504
+ // Detect mode
505
+ await detectMode();
506
+
507
+ // Load data
508
+ loadRadars();
509
+ loadInterfaces();
510
+ };