UNPKG

signalk-server

Version:

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

598 lines (597 loc) 23.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AutopilotApi = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const debug_1 = require("../../debug"); const debug = (0, debug_1.createDebug)('signalk-server:api:autopilot'); const __1 = require("../"); const server_api_1 = require("@signalk/server-api"); const AUTOPILOT_API_PATH = `/signalk/v2/api/vessels/self/autopilots`; const DEFAULTIDPATH = '_default'; class AutopilotApi { server; autopilotProviders = new Map(); defaultProviderId; defaultDeviceId; deviceToProvider = new Map(); settings = { maxTurn: 20 * (Math.PI / 180) }; constructor(server) { this.server = server; } async start() { this.initApiEndpoints(); return Promise.resolve(); } // ***** Plugin Interface methods ***** // Register plugin as provider. register(pluginId, provider, devices) { debug(`** Registering provider(s)....${pluginId}`); if (!provider) { throw new Error(`Error registering provider ${pluginId}!`); } if (!devices) { throw new Error(`${pluginId} has not supplied a device list!`); } if (!(0, server_api_1.isAutopilotProvider)(provider)) { throw new Error(`${pluginId} is missing AutopilotProvider properties/methods!`); } else { if (!this.autopilotProviders.has(pluginId)) { this.autopilotProviders.set(pluginId, provider); } devices.forEach((id) => { if (!this.deviceToProvider.has(id)) { this.deviceToProvider.set(id, pluginId); } }); } debug(`No. of AutoPilotProviders registered =`, this.autopilotProviders.size); } // Unregister plugin as provider. unRegister(pluginId) { if (!pluginId) { return; } debug(`** Request to un-register plugin.....${pluginId}`); if (!this.autopilotProviders.has(pluginId)) { debug(`** NOT FOUND....${pluginId}... cannot un-register!`); return; } debug(`** Un-registering autopilot provider....${pluginId}`); this.autopilotProviders.delete(pluginId); debug(`** Update deviceToProvider Map .....${pluginId}`); this.deviceToProvider.forEach((v, k) => { debug('k', k, 'v', v); if (v === pluginId) { this.deviceToProvider.delete(k); } }); // update default if required if (pluginId === this.defaultProviderId) { debug(`** Resetting defaults .....`); this.defaultDeviceId = undefined; this.defaultProviderId = undefined; this.initDefaults(); /*this.emitUpdates( [ this.buildPathValue( 'defaultPilot' as Path, this.defaultDeviceId ?? null ) ], 'autopilotApi' as SourceRef )*/ } debug(`Remaining number of AutoPilot Providers registered =`, this.autopilotProviders.size, 'defaultProvider =', this.defaultProviderId); } /** Emit updates from autopilot device as `steering.autopilot.*` deltas. * This should be used by provider plugins to: * - Ensure API state is consistant * - trigger the sending of deltas. */ apUpdate(pluginId, deviceId = pluginId, apInfo) { try { if (deviceId && !this.deviceToProvider.has(deviceId)) { this.deviceToProvider.set(deviceId, pluginId); } if (!this.defaultDeviceId) { this.initDefaults(deviceId); } } catch (err) { debug(`ERROR apUpdate(): ${pluginId}->${deviceId}`, err); return; } const values = []; Object.keys(apInfo).forEach((attrib) => { if ((0, server_api_1.isAutopilotUpdateAttrib)(attrib) && attrib !== 'options') { if (attrib === 'alarm') { const alarm = apInfo[attrib]; if ((0, server_api_1.isAutopilotAlarm)(alarm.path)) { values.push({ path: `notifications.steering.autopilot.${alarm.path}`, value: alarm.value }); } } else { values.push({ path: `steering.autopilot.${attrib}`, value: apInfo[attrib] }); } } }); if (values.length !== 0) { this.emitUpdates(values, deviceId); } } // ***** /Plugin Interface methods ***** updateAllowed(request) { return this.server.securityStrategy.shouldAllowPut(request, 'vessels.self', null, 'autopilot'); } initApiEndpoints() { debug(`** Initialise ${AUTOPILOT_API_PATH} endpoints. **`); this.server.use(`${AUTOPILOT_API_PATH}/*`, (req, res, next) => { debug(`Autopilot path`, req.method, req.params); try { if (['PUT', 'POST'].includes(req.method)) { debug(`Autopilot`, req.method, req.path, req.body); if (!this.updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); } else { next(); } } else { debug(`Autopilot`, req.method, req.path, req.query, req.body); next(); } } catch (err) { res.status(500).json({ state: 'FAILED', statusCode: 500, message: err.message ?? 'No autopilots available!' }); } }); // get autopilot provider information this.server.get(`${AUTOPILOT_API_PATH}`, (req, res) => { res.status(200).json(this.getDevices()); }); // get default autopilot device this.server.get(`${AUTOPILOT_API_PATH}/_providers/_default`, (req, res) => { debug(`params:`, req.params); res.status(__1.Responses.ok.statusCode).json({ id: this.defaultDeviceId }); }); // set default autopilot device this.server.post(`${AUTOPILOT_API_PATH}/_providers/_default/:id`, (req, res) => { debug(`params:`, req.params); if (!this.deviceToProvider.has(req.params.id)) { debug('** Invalid device id supplied...'); res.status(__1.Responses.invalid.statusCode).json(__1.Responses.invalid); return; } this.initDefaults(req.params.id); res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }); // get default autopilot status & options this.server.get(`${AUTOPILOT_API_PATH}/:id`, (req, res) => { this.useProvider(req) .getData(req.params.id) .then((data) => { res.json(data); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // get autopilot options this.server.get(`${AUTOPILOT_API_PATH}/:id/options`, (req, res) => { this.useProvider(req) .getData(req.params.id) .then((r) => { res.json(r.options); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // engage / enable the autopilot this.server.post(`${AUTOPILOT_API_PATH}/:id/engage`, (req, res) => { this.useProvider(req) .engage(req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // disengage / disable the autopilot this.server.post(`${AUTOPILOT_API_PATH}/:id/disengage`, (req, res) => { this.useProvider(req) .disengage(req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // get state this.server.get(`${AUTOPILOT_API_PATH}/:id/state`, (req, res) => { this.useProvider(req) .getState(req.params.id) .then((r) => { res.json({ value: r }); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // set state this.server.put(`${AUTOPILOT_API_PATH}/:id/state`, (req, res) => { if (typeof req.body.value === 'undefined') { res.status(__1.Responses.invalid.statusCode).json(__1.Responses.invalid); return; } this.useProvider(req) .setState(req.body.value, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // get mode this.server.get(`${AUTOPILOT_API_PATH}/:id/mode`, (req, res) => { this.useProvider(req) .getMode(req.params.id) .then((r) => { res.json({ value: r }); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // set mode this.server.put(`${AUTOPILOT_API_PATH}/:id/mode`, (req, res) => { if (typeof req.body.value === 'undefined') { res.status(400).json(__1.Responses.invalid); return; } this.useProvider(req) .setMode(req.body.value, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // get target this.server.get(`${AUTOPILOT_API_PATH}/:id/target`, (req, res) => { this.useProvider(req) .getTarget(req.params.id) .then((r) => { res.json({ value: r }); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // set target this.server.put(`${AUTOPILOT_API_PATH}/:id/target`, (req, res) => { if (typeof req.body.value !== 'number') { res.status(__1.Responses.invalid.statusCode).json(__1.Responses.invalid); return; } const u = req.body.units ?? 'rad'; let v = typeof u === 'string' && u.toLocaleLowerCase() === 'deg' ? req.body.value * (Math.PI / 180) : req.body.value; v = v < 0 - Math.PI ? Math.max(...[0 - Math.PI, v]) : Math.min(...[2 * Math.PI, v]); debug('target = ', v); this.useProvider(req) .setTarget(v, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // adjust target this.server.put(`${AUTOPILOT_API_PATH}/:id/target/adjust`, (req, res) => { if (typeof req.body.value !== 'number') { res.status(__1.Responses.invalid.statusCode).json(__1.Responses.invalid); return; } const u = req.body.units ?? 'rad'; const v = typeof u === 'string' && u.toLocaleLowerCase() === 'deg' ? req.body.value * (Math.PI / 180) : req.body.value; debug('target = ', v); this.useProvider(req) .adjustTarget(v, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // port tack this.server.post(`${AUTOPILOT_API_PATH}/:id/tack/port`, (req, res) => { this.useProvider(req) .tack('port', req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // starboard tack this.server.post(`${AUTOPILOT_API_PATH}/:id/tack/starboard`, (req, res) => { this.useProvider(req) .tack('starboard', req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // port gybe this.server.post(`${AUTOPILOT_API_PATH}/:id/gybe/port`, (req, res) => { this.useProvider(req) .gybe('port', req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // starboard gybe this.server.post(`${AUTOPILOT_API_PATH}/:id/gybe/starboard`, (req, res) => { this.useProvider(req) .gybe('starboard', req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // dodge mode ON this.server.post(`${AUTOPILOT_API_PATH}/:id/dodge`, (req, res) => { this.useProvider(req) .dodge(0, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // dodge mode OFF this.server.delete(`${AUTOPILOT_API_PATH}/:id/dodge`, (req, res) => { this.useProvider(req) .dodge(null, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); /** dodge port (-ive) / starboard (+ive) degrees */ this.server.put(`${AUTOPILOT_API_PATH}/:id/dodge`, (req, res) => { if (typeof req.body.value !== 'number') { res.status(__1.Responses.invalid.statusCode).json(__1.Responses.invalid); return; } const u = req.body.units ?? 'rad'; let v = typeof u === 'string' && u.toLocaleLowerCase() === 'deg' ? req.body.value * (Math.PI / 180) : req.body.value; debug('dodge pre-normalisation) = ', v); v = v < 0 ? Math.max(...[0 - this.settings.maxTurn, v]) : Math.min(...[this.settings.maxTurn, v]); debug('dodge = ', v); this.useProvider(req) .dodge(v, req.params.id) .then(() => { res.status(__1.Responses.ok.statusCode).json(__1.Responses.ok); }) .catch((err) => { res.status(err.statusCode ?? 500).json({ state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }); }); }); // error response this.server.use(`${AUTOPILOT_API_PATH}/*`, (err, req, res, next) => { const msg = { state: err.state ?? 'FAILED', statusCode: err.statusCode ?? 500, message: err.message ?? 'No autopilots available!' }; if (res.headersSent) { console.log('EXCEPTION: headersSent'); return next(msg); } res.status(500).json(msg); }); } // returns provider to use. useProvider(req) { debug(`useProvider(${req.params.id})`); if (req.params.id === DEFAULTIDPATH) { if (!this.defaultDeviceId) { this.initDefaults(); } if (this.defaultProviderId && this.autopilotProviders.has(this.defaultProviderId)) { debug(`Using default device provider...`); return this.autopilotProviders.get(this.defaultProviderId); } else { debug(`No default device provider...`); throw __1.Responses.invalid; } } else { const pid = this.deviceToProvider.get(req.params.id); if (this.autopilotProviders.has(pid)) { debug(`Found provider...using ${pid}`); return this.autopilotProviders.get(pid); } else { debug('Cannot get Provider!'); throw __1.Responses.invalid; } } } // Returns an array of provider info getDevices() { const pilots = {}; this.deviceToProvider.forEach((providerId, deviceId) => { pilots[deviceId] = { provider: providerId, isDefault: deviceId === this.defaultDeviceId }; }); return pilots; } /** Initialises the value of default device / provider. * If id is not supplied sets first registered device as the default. **/ initDefaults(deviceId) { debug(`initDefaults()...${deviceId}`); // set to supplied deviceId if (deviceId && this.deviceToProvider.has(deviceId)) { this.defaultDeviceId = deviceId; this.defaultProviderId = this.deviceToProvider.get(this.defaultDeviceId); } // else set to first AP device registered else if (this.deviceToProvider.size !== 0) { const k = this.deviceToProvider.keys(); this.defaultDeviceId = k.next().value; this.defaultProviderId = this.deviceToProvider.get(this.defaultDeviceId); } else { this.defaultDeviceId = undefined; this.defaultProviderId = undefined; } this.emitUpdates([ this.buildPathValue('defaultPilot', this.defaultDeviceId ?? null) ], 'autopilotApi'); debug(`Default Device = ${this.defaultDeviceId}`); debug(`Default Provider = ${this.defaultProviderId}`); } // build autopilot delta PathValue buildPathValue(path, value) { return { path: `steering.autopilot${path ? '.' + path : ''}`, value: value }; } // emit delta updates on operation success emitUpdates(values, source) { const msg = { updates: [ { values: values } ] }; debug(`delta -> ${source}:`, msg.updates[0]); this.server.handleMessage(source, msg, server_api_1.SKVersion.v2); this.server.handleMessage(source, msg, server_api_1.SKVersion.v1); } } exports.AutopilotApi = AutopilotApi;