signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
993 lines • 38.4 kB
JavaScript
"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