UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

993 lines 38.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RadarApi = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const debug_1 = require("../../debug"); const debug = (0, debug_1.createDebug)('signalk-server:api:radar'); const __1 = require("../"); const server_api_1 = require("@signalk/server-api"); const RADAR_API_PATH = `/signalk/v2/api/vessels/self/radars`; const TWO_PI = 2 * Math.PI; class RadarApi { app; radarProviders = new Map(); defaultProviderId; constructor(app) { this.app = app; } async start() { this.initApiEndpoints(); return Promise.resolve(); } // ***** Plugin Interface methods ***** /** * Register plugin as radar provider. */ register(pluginId, provider) { debug(`** Registering radar provider... ${pluginId}`); if (!pluginId || !provider) { throw new Error(`Error registering radar provider ${pluginId}!`); } if (!server_api_1.radar.isRadarProvider(provider)) { throw new Error(`${pluginId} is missing RadarProvider properties/methods!`); } else { if (!this.radarProviders.has(pluginId)) { this.radarProviders.set(pluginId, provider); } if (this.radarProviders.size === 1) { this.defaultProviderId = pluginId; } } debug(`No. of RadarProviders registered =`, this.radarProviders.size); } /** * Unregister plugin as radar provider. */ unRegister(pluginId) { if (!pluginId) { return; } debug(`** Request to un-register radar provider... ${pluginId}`); if (!this.radarProviders.has(pluginId)) { debug(`** NOT FOUND... ${pluginId}... cannot un-register!`); return; } debug(`** Un-registering radar provider... ${pluginId}`); this.radarProviders.delete(pluginId); if (pluginId === this.defaultProviderId) { this.defaultProviderId = undefined; } // update defaultProviderId if required if (this.radarProviders.size !== 0 && !this.defaultProviderId) { this.defaultProviderId = this.radarProviders.keys().next().value; } debug(`Remaining number of Radar Providers registered =`, this.radarProviders.size, 'defaultProvider =', this.defaultProviderId); } // ***** Server API methods ***** /** * Get list of all radars from all providers. */ async getRadars() { const radars = []; for (const [pluginId, provider] of this.radarProviders) { try { const radarIds = await provider.methods.getRadars(); for (const radarId of radarIds) { const info = await provider.methods.getRadarInfo(radarId); if (info) { radars.push(info); } } } catch (err) { debug(`Error getting radars from ${pluginId}: ${err.message}`); } } return radars; } /** * Get info for a specific radar by ID. */ async getRadarInfo(radarId) { // Search all providers for this radar for (const [pluginId, provider] of this.radarProviders) { try { const radarIds = await provider.methods.getRadars(); if (radarIds.includes(radarId)) { return await provider.methods.getRadarInfo(radarId); } } catch (err) { debug(`Error checking radar ${radarId} in ${pluginId}: ${err.message}`); } } return null; } // ***** Private methods ***** updateAllowed(request) { return this.app.securityStrategy.shouldAllowPut(request, 'vessels.self', null, 'radar'); } /** * Find the provider that owns a specific radar. */ async findProviderForRadar(radarId) { for (const [pluginId, provider] of this.radarProviders) { try { const radarIds = await provider.methods.getRadars(); if (radarIds.includes(radarId)) { return provider.methods; } } catch (err) { debug(`Error checking radar ${radarId} in ${pluginId}: ${err.message}`); } } return null; } initApiEndpoints() { debug(`** Initialise ${RADAR_API_PATH} endpoints. **`); // Disable caching for all radar API endpoints // Radar data can change at any time (model identification, status, controls) this.app.use(`${RADAR_API_PATH}`, (_req, res, next) => { res.setHeader('Cache-Control', 'no-cache'); next(); }); // GET /radars - List all radars this.app.get(`${RADAR_API_PATH}`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const radars = await this.getRadars(); res.status(200).json(radars); } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // GET /radars/_providers - List registered providers this.app.get(`${RADAR_API_PATH}/_providers`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const r = {}; this.radarProviders.forEach((v, k) => { r[k] = { name: v.name, isDefault: k === this.defaultProviderId }; }); res.status(200).json(r); } catch (err) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: err.message }); } }); // GET /radars/_providers/_default - Get default provider this.app.get(`${RADAR_API_PATH}/_providers/_default`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { res.status(200).json({ id: this.defaultProviderId }); } catch (err) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: err.message }); } }); // POST /radars/_providers/_default/:id - Set default provider this.app.post(`${RADAR_API_PATH}/_providers/_default/:id`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { if (!req.params.id) { throw new Error('Provider id not supplied!'); } if (this.radarProviders.has(req.params.id)) { this.defaultProviderId = req.params.id; res.status(200).json({ statusCode: 200, state: 'COMPLETED', message: `Default provider set to ${req.params.id}.` }); } else { throw new Error(`Provider ${req.params.id} not found!`); } } catch (err) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: err.message }); } }); // GET /radars/:id - Get specific radar info this.app.get(`${RADAR_API_PATH}/:id`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const radarInfo = await this.getRadarInfo(req.params.id); if (radarInfo) { res.status(200).json(radarInfo); } else { res.status(404).json(__1.Responses.notFound); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id - Update radar controls this.app.put(`${RADAR_API_PATH}/:id`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json(__1.Responses.notFound); return; } if (!provider.setControls) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setControls' }); return; } const controls = req.body.value ?? req.body; const success = await provider.setControls(req.params.id, controls); if (success) { res.status(200).json(__1.Responses.ok); } else { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Failed to update radar controls' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/power - Set radar power state this.app.put(`${RADAR_API_PATH}/:id/power`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json(__1.Responses.notFound); return; } if (!provider.setPower) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setPower' }); return; } const state = req.body.value; if (!['off', 'standby', 'transmit', 'warming'].includes(state)) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid power state. Must be: off, standby, transmit, or warming' }); return; } const success = await provider.setPower(req.params.id, state); if (success) { res.status(200).json(__1.Responses.ok); } else { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Failed to set radar power state' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/range - Set radar range this.app.put(`${RADAR_API_PATH}/:id/range`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json(__1.Responses.notFound); return; } if (!provider.setRange) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setRange' }); return; } const range = req.body.value; if (typeof range !== 'number' || range <= 0) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid range value. Must be a positive number (meters)' }); return; } const success = await provider.setRange(req.params.id, range); if (success) { res.status(200).json(__1.Responses.ok); } else { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Failed to set radar range' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/gain - Set radar gain this.app.put(`${RADAR_API_PATH}/:id/gain`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json(__1.Responses.notFound); return; } if (!provider.setGain) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setGain' }); return; } const gain = typeof req.body.value === 'object' ? req.body.value : req.body; if (typeof gain.auto !== 'boolean') { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid gain value. Must have "auto" boolean property' }); return; } const success = await provider.setGain(req.params.id, gain); if (success) { res.status(200).json(__1.Responses.ok); } else { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Failed to set radar gain' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/sea - Set radar sea clutter this.app.put(`${RADAR_API_PATH}/:id/sea`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json(__1.Responses.notFound); return; } if (!provider.setSea) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setSea' }); return; } const sea = typeof req.body.value === 'object' ? req.body.value : req.body; if (typeof sea.auto !== 'boolean') { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid sea value. Must have "auto" boolean property' }); return; } const success = await provider.setSea(req.params.id, sea); if (success) { res.status(200).json(__1.Responses.ok); } else { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Failed to set radar sea clutter' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/rain - Set radar rain clutter this.app.put(`${RADAR_API_PATH}/:id/rain`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json(__1.Responses.notFound); return; } if (!provider.setRain) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setRain' }); return; } const rain = typeof req.body.value === 'object' ? req.body.value : req.body; if (typeof rain.auto !== 'boolean') { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid rain value. Must have "auto" boolean property' }); return; } const success = await provider.setRain(req.params.id, rain); if (success) { res.status(200).json(__1.Responses.ok); } else { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Failed to set radar rain clutter' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // ============================================ // Capability and State Endpoints // ============================================ // GET /radars/:id/capabilities - Get radar capability manifest (cacheable) this.app.get(`${RADAR_API_PATH}/:id/capabilities`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.getCapabilities) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support getCapabilities' }); return; } const capabilities = await provider.getCapabilities(req.params.id); if (capabilities) { res.status(200).json(capabilities); } else { res.status(404).json({ error: 'Radar not found', id: req.params.id }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // GET /radars/:id/state - Get current radar state this.app.get(`${RADAR_API_PATH}/:id/state`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.getState) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support getState' }); return; } const state = await provider.getState(req.params.id); if (state) { res.status(200).json(state); } else { res.status(404).json({ error: 'Radar not found', id: req.params.id }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // GET /radars/:id/controls - List all controls with current values this.app.get(`${RADAR_API_PATH}/:id/controls`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.getState) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support getState' }); return; } const state = await provider.getState(req.params.id); if (state && state.controls) { res.status(200).json(state.controls); } else { res.status(404).json({ error: 'Radar not found', id: req.params.id }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // GET /radars/:id/controls/:controlId - Get single control value this.app.get(`${RADAR_API_PATH}/:id/controls/:controlId`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.getControl) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support getControl' }); return; } const value = await provider.getControl(req.params.id, req.params.controlId); if (value !== null && value !== undefined) { res.status(200).json({ value }); } else { res.status(404).json({ error: 'Control not found', controlId: req.params.controlId }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/controls/:controlId - Set single control value this.app.put(`${RADAR_API_PATH}/:id/controls/:controlId`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.setControl) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support setControl' }); return; } const value = req.body.value !== undefined ? req.body.value : req.body; const result = await provider.setControl(req.params.id, req.params.controlId, value); if (result.success) { res.status(200).json({ success: true }); } else { res.status(400).json({ success: false, error: result.error || 'Failed to set control', controlId: req.params.controlId }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // Note: WebSocket stream endpoint (/radars/:id/stream) would require // additional WebSocket handling infrastructure. For now, providers // should expose their own streamUrl for direct client connection. // ============================================ // ARPA Target Endpoints // ============================================ // GET /radars/:id/targets - Get all tracked ARPA targets this.app.get(`${RADAR_API_PATH}/:id/targets`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.getTargets) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support ARPA targets' }); return; } const targets = await provider.getTargets(req.params.id); if (targets) { res.status(200).json(targets); } else { res.status(404).json({ error: 'Radar not found', id: req.params.id }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // POST /radars/:id/targets - Manually acquire a target this.app.post(`${RADAR_API_PATH}/:id/targets`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.acquireTarget) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support target acquisition' }); return; } const { bearing, distance } = req.body; if (typeof bearing !== 'number' || typeof distance !== 'number') { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid request. Must provide bearing (radians) and distance (meters)' }); return; } if (bearing < 0 || bearing >= TWO_PI) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Bearing must be in radians [0, 2π)' }); return; } if (distance <= 0) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Distance must be a positive number (meters)' }); return; } const result = await provider.acquireTarget(req.params.id, bearing, distance); if (result.success) { res.status(201).json({ success: true, targetId: result.targetId }); } else { res.status(400).json({ success: false, error: result.error || 'Failed to acquire target' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // DELETE /radars/:id/targets/:targetId - Cancel tracking of a target this.app.delete(`${RADAR_API_PATH}/:id/targets/:targetId`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.cancelTarget) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support target cancellation' }); return; } const targetId = parseInt(req.params.targetId, 10); if (isNaN(targetId)) { res.status(400).json({ statusCode: 400, state: 'FAILED', message: 'Invalid target ID. Must be a number' }); return; } const success = await provider.cancelTarget(req.params.id, targetId); if (success) { res.status(200).json({ success: true }); } else { res.status(404).json({ success: false, error: 'Target not found or already cancelled' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // GET /radars/:id/arpa/settings - Get ARPA settings this.app.get(`${RADAR_API_PATH}/:id/arpa/settings`, async (req, res) => { debug(`** ${req.method} ${req.path}`); try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.getArpaSettings) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support ARPA settings' }); return; } const settings = await provider.getArpaSettings(req.params.id); if (settings) { res.status(200).json(settings); } else { res.status(404).json({ error: 'Radar not found', id: req.params.id }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // PUT /radars/:id/arpa/settings - Update ARPA settings this.app.put(`${RADAR_API_PATH}/:id/arpa/settings`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { const provider = await this.findProviderForRadar(req.params.id); if (!provider) { res.status(404).json({ error: 'Radar not found', id: req.params.id }); return; } if (!provider.setArpaSettings) { res.status(501).json({ statusCode: 501, state: 'FAILED', message: 'Provider does not support ARPA settings' }); return; } const settings = req.body.value !== undefined ? req.body.value : req.body; const result = await provider.setArpaSettings(req.params.id, settings); if (result.success) { res.status(200).json({ success: true }); } else { res.status(400).json({ success: false, error: result.error || 'Failed to update ARPA settings' }); } } catch (err) { res.status(500).json({ statusCode: 500, state: 'FAILED', message: err.message }); } }); // Note: WebSocket target stream endpoint (/radars/:id/targets/stream) // would require additional WebSocket handling infrastructure. // For real-time target updates, clients should subscribe to the // main radar stream which includes target data. } } exports.RadarApi = RadarApi; //# sourceMappingURL=index.js.map