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