@marineyachtradar/signalk-plugin 0.2.1 → 0.5.0-beta.2

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.
Files changed (55) hide show
  1. package/README.md +48 -70
  2. package/package.json +42 -6
  3. package/plugin/config/schema.d.ts +13 -0
  4. package/plugin/config/schema.d.ts.map +1 -0
  5. package/plugin/config/schema.js +48 -0
  6. package/plugin/config/schema.js.map +1 -0
  7. package/plugin/index.d.ts +2 -0
  8. package/plugin/index.d.ts.map +1 -0
  9. package/plugin/index.js +359 -289
  10. package/plugin/index.js.map +1 -0
  11. package/plugin/mayara-client.d.ts +28 -0
  12. package/plugin/mayara-client.d.ts.map +1 -0
  13. package/plugin/mayara-client.js +102 -194
  14. package/plugin/mayara-client.js.map +1 -0
  15. package/plugin/radar-provider.d.ts +5 -0
  16. package/plugin/radar-provider.d.ts.map +1 -0
  17. package/plugin/radar-provider.js +192 -305
  18. package/plugin/radar-provider.js.map +1 -0
  19. package/plugin/spoke-forwarder.d.ts +30 -0
  20. package/plugin/spoke-forwarder.d.ts.map +1 -0
  21. package/plugin/spoke-forwarder.js +104 -136
  22. package/plugin/spoke-forwarder.js.map +1 -0
  23. package/plugin/types.d.ts +21 -0
  24. package/plugin/types.d.ts.map +1 -0
  25. package/plugin/types.js +3 -0
  26. package/plugin/types.js.map +1 -0
  27. package/public/540.js +2 -0
  28. package/public/540.js.LICENSE.txt +9 -0
  29. package/public/805.js +1 -0
  30. package/public/index.html +27 -39
  31. package/public/main.js +1 -0
  32. package/public/remoteEntry.js +1 -0
  33. package/public/api.js +0 -402
  34. package/public/assets/mayara_logo.png +0 -0
  35. package/public/base.css +0 -91
  36. package/public/control.html +0 -23
  37. package/public/control.js +0 -1155
  38. package/public/controls.css +0 -538
  39. package/public/discovery.css +0 -478
  40. package/public/favicon.ico +0 -0
  41. package/public/layout.css +0 -87
  42. package/public/mayara.js +0 -510
  43. package/public/proto/RadarMessage.proto +0 -41
  44. package/public/protobuf/protobuf.js +0 -9112
  45. package/public/protobuf/protobuf.js.map +0 -1
  46. package/public/protobuf/protobuf.min.js +0 -8
  47. package/public/protobuf/protobuf.min.js.map +0 -1
  48. package/public/radar.svg +0 -29
  49. package/public/render_webgpu.js +0 -886
  50. package/public/responsive.css +0 -29
  51. package/public/van-1.5.2.debug.js +0 -126
  52. package/public/van-1.5.2.js +0 -140
  53. package/public/van-1.5.2.min.js +0 -1
  54. package/public/viewer.html +0 -30
  55. package/public/viewer.js +0 -797
package/plugin/index.js CHANGED
@@ -1,306 +1,376 @@
1
- /**
2
- * MaYaRa Radar SignalK Plugin
3
- *
4
- * Connects to a remote mayara-server and exposes its radar(s) via SignalK's Radar API.
5
- * The plugin acts as a thin proxy layer - all radar logic runs on mayara-server.
6
- */
7
-
8
- const MayaraClient = require('./mayara-client')
9
- const createRadarProvider = require('./radar-provider')
10
- const SpokeForwarder = require('./spoke-forwarder')
11
-
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const child_process_1 = require("child_process");
4
+ const util_1 = require("util");
5
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
6
+ const mayara_client_1 = require("./mayara-client");
7
+ const radar_provider_1 = require("./radar-provider");
8
+ const spoke_forwarder_1 = require("./spoke-forwarder");
9
+ const schema_1 = require("./config/schema");
10
+ const MAYARA_IMAGE = 'ghcr.io/marineyachtradar/mayara-server';
11
+ const SAFE_TAG = /^[a-zA-Z0-9._-]+$/;
12
12
  module.exports = function (app) {
13
- let client = null
14
- let provider = null
15
- let spokeForwarders = new Map() // radarId -> SpokeForwarder
16
- let discoveryInterval = null
17
- let reconnectInterval = null
18
- let isConnected = false
19
- let knownRadars = new Set()
20
-
21
- const plugin = {
22
- id: 'mayara-server-signalk-plugin',
23
- name: 'MaYaRa Radar (Server)',
24
- description: 'Connect SignalK to mayara-server for multi-brand marine radar integration',
25
-
26
- schema: () => ({
27
- type: 'object',
28
- title: 'MaYaRa Server Connection',
29
- required: ['host', 'port'],
30
- properties: {
31
- host: {
32
- type: 'string',
33
- title: 'mayara-server Host',
34
- description: 'IP address or hostname of mayara-server',
35
- default: 'localhost'
13
+ let client = null;
14
+ let currentSettings = null;
15
+ const spokeForwarders = new Map();
16
+ let discoveryInterval = null;
17
+ let reconnectInterval = null;
18
+ let isConnected = false;
19
+ const knownRadars = new Set();
20
+ const plugin = {
21
+ id: 'mayara-server-signalk-plugin',
22
+ name: 'MaYaRa Radar (Server)',
23
+ description: 'Connect SignalK to mayara-server for multi-brand marine radar integration',
24
+ enabledByDefault: true,
25
+ schema: schema_1.ConfigSchema,
26
+ start(config) {
27
+ app.debug('Starting mayara-server-signalk-plugin');
28
+ currentSettings = config;
29
+ void asyncStart(config).catch((err) => {
30
+ app.setPluginError(`Startup failed: ${err instanceof Error ? err.message : String(err)}`);
31
+ });
36
32
  },
37
- port: {
38
- type: 'number',
39
- title: 'mayara-server Port',
40
- description: 'HTTP port of mayara-server REST API',
41
- default: 6502,
42
- minimum: 1,
43
- maximum: 65535
33
+ stop() {
34
+ app.debug('Stopping mayara-server-signalk-plugin');
35
+ try {
36
+ app.radarApi.unRegister(plugin.id);
37
+ }
38
+ catch (err) {
39
+ app.debug(`Error unregistering: ${err instanceof Error ? err.message : String(err)}`);
40
+ }
41
+ if (discoveryInterval) {
42
+ clearInterval(discoveryInterval);
43
+ discoveryInterval = null;
44
+ }
45
+ if (reconnectInterval) {
46
+ clearInterval(reconnectInterval);
47
+ reconnectInterval = null;
48
+ }
49
+ for (const forwarder of spokeForwarders.values()) {
50
+ forwarder.stop();
51
+ }
52
+ spokeForwarders.clear();
53
+ knownRadars.clear();
54
+ if (client) {
55
+ client.close();
56
+ client = null;
57
+ }
58
+ isConnected = false;
59
+ app.setPluginStatus('Stopped');
44
60
  },
45
- secure: {
46
- type: 'boolean',
47
- title: 'Use HTTPS/WSS',
48
- description: 'Use secure connections (requires TLS on mayara-server)',
49
- default: false
50
- },
51
- discoveryPollInterval: {
52
- type: 'number',
53
- title: 'Discovery Poll Interval (seconds)',
54
- description: 'How often to poll for new/disconnected radars',
55
- default: 10,
56
- minimum: 5,
57
- maximum: 60
58
- },
59
- reconnectInterval: {
60
- type: 'number',
61
- title: 'Reconnect Interval (seconds)',
62
- description: 'How often to retry connection when mayara-server is unreachable',
63
- default: 5,
64
- minimum: 1,
65
- maximum: 30
61
+ registerWithRouter(router) {
62
+ router.get('/status', async (req, res) => {
63
+ let containerState = 'unknown';
64
+ let containerImage = '';
65
+ try {
66
+ const containers = globalThis.__signalk_containerManager;
67
+ if (containers) {
68
+ containerState = await containers.getState('mayara-server');
69
+ }
70
+ }
71
+ catch {
72
+ // ignore
73
+ }
74
+ try {
75
+ const tag = currentSettings?.mayaraVersion ?? 'latest';
76
+ containerImage = `${MAYARA_IMAGE}:${tag}`;
77
+ }
78
+ catch {
79
+ // ignore
80
+ }
81
+ res.json({
82
+ connected: isConnected,
83
+ radars: Array.from(knownRadars),
84
+ spokeForwarders: Array.from(spokeForwarders.keys()).map((id) => ({
85
+ radarId: id,
86
+ connected: spokeForwarders.get(id)?.isConnected() ?? false
87
+ })),
88
+ container: {
89
+ state: containerState,
90
+ image: containerImage,
91
+ managed: currentSettings?.managedContainer !== false
92
+ }
93
+ });
94
+ });
95
+ router.post('/api/check-update', async (req, res) => {
96
+ try {
97
+ const containers = globalThis.__signalk_containerManager;
98
+ if (!containers) {
99
+ res.status(400).json({ error: 'signalk-container not available' });
100
+ return;
101
+ }
102
+ const runtime = containers.getRuntime();
103
+ if (!runtime) {
104
+ res.status(400).json({ error: 'No container runtime detected' });
105
+ return;
106
+ }
107
+ const rt = runtime.runtime;
108
+ const tag = req.body.tag ??
109
+ currentSettings?.mayaraVersion ??
110
+ 'latest';
111
+ if (!SAFE_TAG.test(tag)) {
112
+ res.status(400).json({ error: 'Invalid tag format' });
113
+ return;
114
+ }
115
+ const image = `${MAYARA_IMAGE}:${tag}`;
116
+ // Get image ID of running container
117
+ let runningImageId = '';
118
+ try {
119
+ const { stdout } = await execAsync(`${rt} inspect sk-mayara-server --format '{{.Image}}'`);
120
+ runningImageId = stdout.trim();
121
+ }
122
+ catch {
123
+ // container not running
124
+ }
125
+ // Pull latest
126
+ app.debug(`Checking for update: pulling ${image}`);
127
+ await containers.pullImage(image);
128
+ // Get image ID of pulled image
129
+ let pulledImageId = '';
130
+ try {
131
+ const { stdout } = await execAsync(`${rt} image inspect ${image} --format '{{.Id}}'`);
132
+ pulledImageId = stdout.trim();
133
+ }
134
+ catch {
135
+ // image inspect failed
136
+ }
137
+ if (!runningImageId) {
138
+ res.json({ updateAvailable: false, message: 'Container not running' });
139
+ }
140
+ else if (runningImageId === pulledImageId) {
141
+ res.json({ updateAvailable: false, message: `Up to date (${tag})` });
142
+ }
143
+ else {
144
+ res.json({ updateAvailable: true, message: `Update available for ${tag}` });
145
+ }
146
+ }
147
+ catch (err) {
148
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
149
+ }
150
+ });
151
+ router.post('/api/update', async (req, res) => {
152
+ try {
153
+ const containers = globalThis.__signalk_containerManager;
154
+ if (!containers) {
155
+ res.status(400).json({ error: 'signalk-container not available' });
156
+ return;
157
+ }
158
+ const tag = req.body.tag ??
159
+ currentSettings?.mayaraVersion ??
160
+ 'latest';
161
+ if (!SAFE_TAG.test(tag)) {
162
+ res.status(400).json({ error: 'Invalid tag format' });
163
+ return;
164
+ }
165
+ const image = `${MAYARA_IMAGE}:${tag}`;
166
+ app.setPluginStatus(`Updating mayara-server to ${image}...`);
167
+ await containers.pullImage(image);
168
+ await containers.stop('mayara-server');
169
+ await containers.remove('mayara-server');
170
+ const args = currentSettings?.mayaraArgs ?? [];
171
+ await containers.ensureRunning('mayara-server', {
172
+ image: MAYARA_IMAGE,
173
+ tag,
174
+ networkMode: 'host',
175
+ command: args.length > 0 ? ['mayara-server', ...args] : undefined,
176
+ restart: 'unless-stopped'
177
+ });
178
+ res.json({ success: true, tag });
179
+ app.setPluginStatus(`Updated to ${tag} and running.`);
180
+ }
181
+ catch (err) {
182
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
183
+ }
184
+ });
185
+ router.get('/api/gui-url', (req, res) => {
186
+ const host = currentSettings?.host ?? 'localhost';
187
+ const port = currentSettings?.port ?? 6502;
188
+ const proto = currentSettings?.secure ? 'https' : 'http';
189
+ res.json({ url: `${proto}://${host}:${port}/gui/` });
190
+ });
191
+ router.get('/api/versions', async (req, res) => {
192
+ try {
193
+ const ghRes = await fetch('https://api.github.com/repos/MarineYachtRadar/mayara-server/releases?per_page=10', {
194
+ headers: { Accept: 'application/vnd.github+json' },
195
+ signal: AbortSignal.timeout(10000)
196
+ });
197
+ if (!ghRes.ok) {
198
+ res.status(502).json({ error: 'Failed to fetch releases' });
199
+ return;
200
+ }
201
+ const releases = (await ghRes.json());
202
+ res.json(releases
203
+ .filter((r) => !r.draft)
204
+ .map((r) => ({ tag: r.tag_name, prerelease: r.prerelease })));
205
+ }
206
+ catch (err) {
207
+ res.status(500).json({
208
+ error: err instanceof Error ? err.message : 'Unknown error'
209
+ });
210
+ }
211
+ });
66
212
  }
67
- }
68
- }),
69
-
70
- start: function (settings) {
71
- app.debug('Starting mayara-server-signalk-plugin')
72
-
73
- // Initialize client to mayara-server
74
- client = new MayaraClient({
75
- host: settings.host || 'localhost',
76
- port: settings.port || 6502,
77
- secure: settings.secure || false,
78
- debug: app.debug.bind(app)
79
- })
80
-
81
- // Create RadarProvider implementation
82
- provider = createRadarProvider(client, app)
83
-
84
- // Check if radar API is available
85
- if (!app.radarApi) {
86
- app.setPluginError('SignalK Radar API not available (requires SignalK >= 2.0.0)')
87
- return
88
- }
89
-
90
- // Register with SignalK Radar API
91
- try {
92
- app.radarApi.register(plugin.id, {
93
- name: plugin.name,
94
- methods: provider
95
- })
96
- app.debug('Registered as radar provider')
97
- } catch (err) {
98
- app.setPluginError(`Failed to register radar provider: ${err.message}`)
99
- return
100
- }
101
-
102
- // Start connection and discovery
103
- connectAndDiscover(settings)
104
- },
105
-
106
- stop: function () {
107
- app.debug('Stopping mayara-server-signalk-plugin')
108
-
109
- // Unregister from radar API
110
- if (app.radarApi) {
213
+ };
214
+ async function asyncStart(settings) {
215
+ if (settings.managedContainer) {
216
+ await startManagedContainer(settings);
217
+ }
218
+ client = new mayara_client_1.MayaraClient({
219
+ host: settings.host ?? 'localhost',
220
+ port: settings.port ?? 6502,
221
+ secure: settings.secure ?? false,
222
+ debug: app.debug.bind(app)
223
+ });
224
+ const provider = (0, radar_provider_1.createRadarProvider)(client, app);
111
225
  try {
112
- app.radarApi.unRegister(plugin.id)
113
- } catch (err) {
114
- app.debug(`Error unregistering: ${err.message}`)
226
+ app.radarApi.register(plugin.id, {
227
+ name: plugin.name,
228
+ methods: provider
229
+ });
230
+ app.debug('Registered as radar provider');
231
+ }
232
+ catch (err) {
233
+ app.setPluginError(`Failed to register radar provider: ${err instanceof Error ? err.message : String(err)}`);
234
+ return;
115
235
  }
116
- }
117
-
118
- // Clear intervals
119
- if (discoveryInterval) {
120
- clearInterval(discoveryInterval)
121
- discoveryInterval = null
122
- }
123
- if (reconnectInterval) {
124
- clearInterval(reconnectInterval)
125
- reconnectInterval = null
126
- }
127
-
128
- // Stop all spoke forwarders
129
- for (const forwarder of spokeForwarders.values()) {
130
- forwarder.stop()
131
- }
132
- spokeForwarders.clear()
133
- knownRadars.clear()
134
-
135
- // Close client
136
- if (client) {
137
- client.close()
138
- client = null
139
- }
140
-
141
- isConnected = false
142
- app.setPluginStatus('Stopped')
143
- },
144
-
145
- registerWithRouter: function (router) {
146
- // Health check endpoint
147
- router.get('/status', (req, res) => {
148
- res.json({
149
- connected: isConnected,
150
- radars: Array.from(knownRadars),
151
- spokeForwarders: Array.from(spokeForwarders.keys()).map(id => ({
152
- radarId: id,
153
- connected: spokeForwarders.get(id)?.isConnected() || false
154
- }))
155
- })
156
- })
236
+ await connectAndDiscover(settings);
157
237
  }
158
- }
159
-
160
- async function connectAndDiscover(settings) {
161
- try {
162
- // Try to connect to mayara-server
163
- const radars = await client.getRadars()
164
- isConnected = true
165
-
166
- const radarIds = Object.keys(radars)
167
- app.setPluginStatus(`Connected - ${radarIds.length} radar(s) found`)
168
-
169
- // Update known radars and start spoke forwarders
170
- await updateRadars(radarIds, settings)
171
-
172
- // Start discovery polling
173
- const pollMs = (settings.discoveryPollInterval || 10) * 1000
174
- discoveryInterval = setInterval(() => {
175
- pollForRadarChanges(settings)
176
- }, pollMs)
177
-
178
- } catch (err) {
179
- isConnected = false
180
- app.setPluginError(`Cannot connect to mayara-server: ${err.message}`)
181
-
182
- // Schedule reconnect
183
- const reconnectMs = (settings.reconnectInterval || 5) * 1000
184
- reconnectInterval = setInterval(async () => {
238
+ async function startManagedContainer(settings) {
239
+ let containers;
240
+ const waitDeadline = Date.now() + 30000;
241
+ while (Date.now() < waitDeadline) {
242
+ containers = globalThis.__signalk_containerManager;
243
+ if (containers?.getRuntime())
244
+ break;
245
+ app.setPluginStatus('Waiting for container runtime detection...');
246
+ await new Promise((resolve) => setTimeout(resolve, 1000));
247
+ }
248
+ if (!containers) {
249
+ app.setPluginError('signalk-container plugin required for managed mode. Install it or set managedContainer=false.');
250
+ throw new Error('Container manager not available');
251
+ }
252
+ if (!containers.getRuntime()) {
253
+ app.setPluginError('No container runtime detected. Check signalk-container plugin.');
254
+ throw new Error('Container runtime not detected');
255
+ }
256
+ app.debug('Container runtime ready, starting mayara-server');
257
+ app.setPluginStatus('Starting mayara-server container...');
258
+ const args = settings.mayaraArgs ?? [];
259
+ await containers.ensureRunning('mayara-server', {
260
+ image: MAYARA_IMAGE,
261
+ tag: settings.mayaraVersion ?? 'latest',
262
+ networkMode: 'host',
263
+ command: args.length > 0 ? ['mayara-server', ...args] : undefined,
264
+ restart: 'unless-stopped'
265
+ });
266
+ app.debug('mayara-server container ready');
267
+ }
268
+ async function connectAndDiscover(settings) {
269
+ if (!client)
270
+ return;
185
271
  try {
186
- const radars = await client.getRadars()
187
- isConnected = true
188
-
189
- const radarIds = Object.keys(radars)
190
- app.setPluginStatus(`Connected - ${radarIds.length} radar(s) found`)
191
-
192
- // Clear reconnect timer
193
- clearInterval(reconnectInterval)
194
- reconnectInterval = null
195
-
196
- // Update radars
197
- await updateRadars(radarIds, settings)
198
-
199
- // Start discovery polling
200
- const pollMs = (settings.discoveryPollInterval || 10) * 1000
201
- discoveryInterval = setInterval(() => {
202
- pollForRadarChanges(settings)
203
- }, pollMs)
204
-
205
- } catch (e) {
206
- // Still disconnected, keep trying
207
- app.debug(`Reconnect failed: ${e.message}`)
272
+ const radars = await client.getRadars();
273
+ isConnected = true;
274
+ const radarIds = Object.keys(radars);
275
+ app.setPluginStatus(`Connected - ${radarIds.length} radar(s) found`);
276
+ updateRadars(radarIds, settings);
277
+ const pollMs = (settings.discoveryPollInterval || 10) * 1000;
278
+ discoveryInterval = setInterval(() => {
279
+ void pollForRadarChanges(settings);
280
+ }, pollMs);
281
+ }
282
+ catch (err) {
283
+ isConnected = false;
284
+ app.setPluginError(`Cannot connect to mayara-server: ${err instanceof Error ? err.message : String(err)}`);
285
+ const reconnectMs = (settings.reconnectInterval || 5) * 1000;
286
+ reconnectInterval = setInterval(() => {
287
+ void attemptReconnect(settings);
288
+ }, reconnectMs);
208
289
  }
209
- }, reconnectMs)
210
290
  }
211
- }
212
-
213
- async function updateRadars(radarIds, settings) {
214
- const currentIds = new Set(radarIds)
215
-
216
- // Add new radars
217
- for (const radarId of currentIds) {
218
- if (!knownRadars.has(radarId)) {
219
- app.debug(`New radar discovered: ${radarId}`)
220
- knownRadars.add(radarId)
221
-
222
- // Start spoke forwarder for this radar
223
- if (app.binaryStreamManager) {
224
- const forwarder = new SpokeForwarder({
225
- radarId: radarId,
226
- url: client.getSpokeStreamUrl(radarId),
227
- binaryStreamManager: app.binaryStreamManager,
228
- debug: app.debug.bind(app),
229
- reconnectInterval: (settings.reconnectInterval || 5) * 1000
230
- })
231
- spokeForwarders.set(radarId, forwarder)
232
- forwarder.start()
233
- } else {
234
- app.debug('binaryStreamManager not available - spoke streaming disabled')
291
+ async function attemptReconnect(settings) {
292
+ if (!client)
293
+ return;
294
+ try {
295
+ const radars = await client.getRadars();
296
+ isConnected = true;
297
+ const radarIds = Object.keys(radars);
298
+ app.setPluginStatus(`Connected - ${radarIds.length} radar(s) found`);
299
+ if (reconnectInterval) {
300
+ clearInterval(reconnectInterval);
301
+ reconnectInterval = null;
302
+ }
303
+ updateRadars(radarIds, settings);
304
+ if (discoveryInterval) {
305
+ clearInterval(discoveryInterval);
306
+ }
307
+ const pollMs = (settings.discoveryPollInterval || 10) * 1000;
308
+ discoveryInterval = setInterval(() => {
309
+ void pollForRadarChanges(settings);
310
+ }, pollMs);
311
+ }
312
+ catch {
313
+ // Still disconnected, keep trying
235
314
  }
236
- }
237
315
  }
238
-
239
- // Remove disconnected radars
240
- for (const radarId of knownRadars) {
241
- if (!currentIds.has(radarId)) {
242
- app.debug(`Radar disconnected: ${radarId}`)
243
- knownRadars.delete(radarId)
244
-
245
- // Stop spoke forwarder
246
- const forwarder = spokeForwarders.get(radarId)
247
- if (forwarder) {
248
- forwarder.stop()
249
- spokeForwarders.delete(radarId)
316
+ function updateRadars(radarIds, settings) {
317
+ if (!client)
318
+ return;
319
+ const currentIds = new Set(radarIds);
320
+ for (const radarId of currentIds) {
321
+ if (!knownRadars.has(radarId)) {
322
+ app.debug(`New radar discovered: ${radarId}`);
323
+ knownRadars.add(radarId);
324
+ if (app.binaryStreamManager) {
325
+ const forwarder = new spoke_forwarder_1.SpokeForwarder({
326
+ radarId,
327
+ url: client.getSpokeStreamUrl(radarId),
328
+ binaryStreamManager: app.binaryStreamManager,
329
+ debug: app.debug.bind(app),
330
+ reconnectInterval: (settings.reconnectInterval || 5) * 1000
331
+ });
332
+ spokeForwarders.set(radarId, forwarder);
333
+ forwarder.start();
334
+ }
335
+ else {
336
+ app.debug('binaryStreamManager not available - spoke streaming disabled');
337
+ }
338
+ }
339
+ }
340
+ for (const radarId of knownRadars) {
341
+ if (!currentIds.has(radarId)) {
342
+ app.debug(`Radar disconnected: ${radarId}`);
343
+ knownRadars.delete(radarId);
344
+ const forwarder = spokeForwarders.get(radarId);
345
+ if (forwarder) {
346
+ forwarder.stop();
347
+ spokeForwarders.delete(radarId);
348
+ }
349
+ }
250
350
  }
251
- }
252
351
  }
253
- }
254
-
255
- async function pollForRadarChanges(settings) {
256
- try {
257
- const radars = await client.getRadars()
258
- const radarIds = Object.keys(radars)
259
-
260
- await updateRadars(radarIds, settings)
261
-
262
- app.setPluginStatus(`Connected - ${radarIds.length} radar(s)`)
263
-
264
- } catch (err) {
265
- // Lost connection
266
- isConnected = false
267
- app.setPluginError(`Lost connection: ${err.message}`)
268
-
269
- // Stop discovery polling
270
- if (discoveryInterval) {
271
- clearInterval(discoveryInterval)
272
- discoveryInterval = null
273
- }
274
-
275
- // Start reconnect timer
276
- const reconnectMs = (settings.reconnectInterval || 5) * 1000
277
- reconnectInterval = setInterval(async () => {
352
+ async function pollForRadarChanges(settings) {
353
+ if (!client)
354
+ return;
278
355
  try {
279
- const radars = await client.getRadars()
280
- isConnected = true
281
-
282
- const radarIds = Object.keys(radars)
283
- app.setPluginStatus(`Connected - ${radarIds.length} radar(s) found`)
284
-
285
- // Clear reconnect timer
286
- clearInterval(reconnectInterval)
287
- reconnectInterval = null
288
-
289
- // Update radars
290
- await updateRadars(radarIds, settings)
291
-
292
- // Restart discovery polling
293
- const pollMs = (settings.discoveryPollInterval || 10) * 1000
294
- discoveryInterval = setInterval(() => {
295
- pollForRadarChanges(settings)
296
- }, pollMs)
297
-
298
- } catch (e) {
299
- // Still disconnected
356
+ const radars = await client.getRadars();
357
+ const radarIds = Object.keys(radars);
358
+ updateRadars(radarIds, settings);
359
+ app.setPluginStatus(`Connected - ${radarIds.length} radar(s)`);
360
+ }
361
+ catch (err) {
362
+ isConnected = false;
363
+ app.setPluginError(`Lost connection: ${err instanceof Error ? err.message : String(err)}`);
364
+ if (discoveryInterval) {
365
+ clearInterval(discoveryInterval);
366
+ discoveryInterval = null;
367
+ }
368
+ const reconnectMs = (settings.reconnectInterval || 5) * 1000;
369
+ reconnectInterval = setInterval(() => {
370
+ void attemptReconnect(settings);
371
+ }, reconnectMs);
300
372
  }
301
- }, reconnectMs)
302
373
  }
303
- }
304
-
305
- return plugin
306
- }
374
+ return plugin;
375
+ };
376
+ //# sourceMappingURL=index.js.map