UNPKG

@signalk/course-provider

Version:
462 lines (461 loc) 18.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const alarms_1 = require("./lib/alarms"); const types_1 = require("./types"); const path_1 = __importDefault(require("path")); const worker_threads_1 = require("worker_threads"); const CONFIG_SCHEMA = { properties: { notifications: { type: 'object', title: 'Notifications', description: 'Configure the options for generated notifications.', properties: { sound: { type: 'boolean', title: 'Enable sound' } } }, calculations: { type: 'object', title: 'Calculations', description: 'Configure course calculations options.', properties: { method: { type: 'string', default: 'GreatCircle', enum: ['GreatCircle', 'Rhumbline'] } } } } }; const CONFIG_UISCHEMA = { notifications: { sound: { 'ui:widget': 'checkbox', 'ui:title': ' ', 'ui:help': '' } }, calculations: { method: { 'ui:widget': 'radio', 'ui:title': 'Course calculation method', 'ui:help': ' ' } } }; const SRC_PATHS = [ 'navigation.position', 'navigation.magneticVariation', 'navigation.headingTrue', 'navigation.speedOverGround', 'navigation.datetime', 'navigation.course.arrivalCircle', 'navigation.course.startTime', 'navigation.course.targetArrivalTime', 'navigation.course.nextPoint', 'navigation.course.previousPoint' ]; module.exports = (server) => { const watchArrival = new alarms_1.Watcher(); // watch distance from arrivalCircle const watchPassedDest = new alarms_1.Watcher(); // watch passedPerpendicular watchPassedDest.rangeMin = 1; watchPassedDest.rangeMax = 2; let unsubscribes = []; // delta stream subscriptions let obs = []; // Observables subscription let worker; const SIGNALK_API_PATH = `/signalk/v2/api`; const COURSE_CALCS_PATH = `${SIGNALK_API_PATH}/vessels/self/navigation/course/calcValues`; const srcPaths = {}; let courseCalcs; let metaSent = false; // ******** REQUIRED PLUGIN DEFINITION ******* const plugin = { id: 'course-provider', name: 'Course Data provider', schema: () => CONFIG_SCHEMA, uiSchema: () => CONFIG_UISCHEMA, start: (options) => { doStartup(options); }, stop: () => { doShutdown(); } }; // ************************************ let config = { notifications: { sound: false }, calculations: { method: 'GreatCircle' } }; const doStartup = (options) => { var _a, _b; try { server.debug(`${plugin.name} starting.......`); if (typeof ((_a = options.notifications) === null || _a === void 0 ? void 0 : _a.sound) !== 'undefined' && typeof ((_b = options.calculations) === null || _b === void 0 ? void 0 : _b.method) !== 'undefined') { config = options; } server.debug(`Applied config: ${JSON.stringify(config)}`); // setup subscriptions initSubscriptions(SRC_PATHS); // setup worker(s) initWorkers(); // setup routes initEndpoints(); const msg = 'Started'; server.setPluginStatus(msg); } catch (error) { const msg = 'Started with errors!'; server.setPluginError(msg); server.error('** EXCEPTION: **'); server.error(error.stack); return error; } }; const doShutdown = () => { server.debug('** shutting down **'); server.debug('** Un-subscribing from events **'); unsubscribes.forEach((s) => s()); unsubscribes = []; obs.forEach((o) => o.unsubscribe()); obs = []; if (worker) { server.debug('** Stopping Worker(s) **'); worker.unref(); } const msg = 'Stopped'; server.setPluginStatus(msg); }; // ***************************************** // register DELTA stream message handler const initSubscriptions = (skPaths) => { getPaths(skPaths); const subscription = { context: 'vessels.self', subscribe: skPaths.map((p) => ({ path: p, period: 500 })) }; server.subscriptionmanager.subscribe(subscription, unsubscribes, (error) => { server.error(`${plugin.id} Error: ${error}`); }, (delta) => { if (!delta.updates) { return; } delta.updates.forEach((u) => { if (!u.values) { return; } u.values.forEach((v) => { srcPaths[v.path] = v.value; if (v.path === 'navigation.position') { server.debug(`navigation.position ${JSON.stringify(v.value)} => calc()`); calc(); } }); }); }); obs.push(watchArrival.change$.subscribe((event) => { onArrivalCircleEvent(event); })); obs.push(watchPassedDest.change$.subscribe((event) => { onPassedDestEvent(event); })); }; // initialise calculation worker(s) const initWorkers = () => { worker = new worker_threads_1.Worker(path_1.default.resolve(__dirname, './worker/course.js')); worker.on('message', (msg) => { calcResult(msg); }); worker.on('error', (error) => console.error('** worker.error:', error)); worker.on('exit', (code) => { if (code !== 0) { console.error('** worker.exit:', `Stopped with exit code ${code}`); } }); }; // initialise api endpoints const initEndpoints = () => { server.get(`${COURSE_CALCS_PATH}`, (req, res) => __awaiter(void 0, void 0, void 0, function* () { server.debug(`** GET ${COURSE_CALCS_PATH}`); const calcs = config.calculations.method === 'Rhumbline' ? courseCalcs === null || courseCalcs === void 0 ? void 0 : courseCalcs.rl : courseCalcs === null || courseCalcs === void 0 ? void 0 : courseCalcs.gc; if (!calcs) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: `No active destination!` }); return; } return res.status(200).json(calcs); })); }; // ********* Course Calculations ******************* // retrieve initial values of target paths const getPaths = (paths) => { paths.forEach((path) => { var _a; const v = server.getSelfPath(path); srcPaths[path] = (_a = v === null || v === void 0 ? void 0 : v.value) !== null && _a !== void 0 ? _a : null; }); server.debug(`[srcPaths]: ${JSON.stringify(srcPaths)}`); }; // trigger course calculations const calc = () => { if (srcPaths['navigation.position']) { worker === null || worker === void 0 ? void 0 : worker.postMessage(srcPaths); } }; // send calculation results delta const calcResult = (result) => __awaiter(void 0, void 0, void 0, function* () { var _a, _b, _c; server.debug(`*** calculation result ***`); watchArrival.rangeMax = (_a = srcPaths['navigation.course.arrivalCircle']) !== null && _a !== void 0 ? _a : -1; watchArrival.value = (_c = (_b = result.gc) === null || _b === void 0 ? void 0 : _b.distance) !== null && _c !== void 0 ? _c : -1; watchPassedDest.value = result.passedPerpendicular ? 1 : 0; courseCalcs = result; server.handleMessage(plugin.id, buildDeltaMsg(courseCalcs), 'v2'); server.debug(`*** course data delta sent***`); if (!metaSent) { server.handleMessage(plugin.id, buildMetaDeltaMsg(), 'v2'); server.debug(`*** meta delta sent***`); metaSent = true; } }); const buildDeltaMsg = (course) => { var _a, _b; const values = []; const calcPath = 'navigation.course.calcValues'; const source = config.calculations.method === 'Rhumbline' ? course.rl : course.gc; server.debug(`*** building course data delta ***`); values.push({ path: `${calcPath}.calcMethod`, value: config.calculations.method }); values.push({ path: `${calcPath}.bearingTrackTrue`, value: typeof source.bearingTrackTrue === 'undefined' ? null : source.bearingTrackTrue }); values.push({ path: `${calcPath}.bearingTrackMagnetic`, value: typeof source.bearingTrackMagnetic === 'undefined' ? null : source.bearingTrackMagnetic }); values.push({ path: `${calcPath}.crossTrackError`, value: typeof source.crossTrackError === 'undefined' ? null : source.crossTrackError }); values.push({ path: `${calcPath}.previousPoint.distance`, value: typeof ((_a = source.previousPoint) === null || _a === void 0 ? void 0 : _a.distance) === 'undefined' ? null : (_b = source.previousPoint) === null || _b === void 0 ? void 0 : _b.distance }); values.push({ path: `${calcPath}.distance`, value: typeof (source === null || source === void 0 ? void 0 : source.distance) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.distance }); values.push({ path: `${calcPath}.bearingTrue`, value: typeof (source === null || source === void 0 ? void 0 : source.bearingTrue) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.bearingTrue }); values.push({ path: `${calcPath}.bearingMagnetic`, value: typeof (source === null || source === void 0 ? void 0 : source.bearingMagnetic) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.bearingMagnetic }); values.push({ path: `${calcPath}.velocityMadeGood`, value: typeof (source === null || source === void 0 ? void 0 : source.velocityMadeGood) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.velocityMadeGood }); values.push({ path: `performance.velocityMadeGoodToWaypoint`, value: typeof (source === null || source === void 0 ? void 0 : source.velocityMadeGood) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.velocityMadeGood }); values.push({ path: `${calcPath}.timeToGo`, value: typeof (source === null || source === void 0 ? void 0 : source.timeToGo) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.timeToGo }); values.push({ path: `${calcPath}.estimatedTimeOfArrival`, value: typeof (source === null || source === void 0 ? void 0 : source.estimatedTimeOfArrival) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.estimatedTimeOfArrival }); values.push({ path: `${calcPath}.targetSpeed`, value: typeof (source === null || source === void 0 ? void 0 : source.targetSpeed) === 'undefined' ? null : source === null || source === void 0 ? void 0 : source.targetSpeed }); return { updates: [ { values: values } ] }; }; const buildMetaDeltaMsg = () => { const metas = []; const calcPath = 'navigation.course.calcValues'; server.debug(`*** building meta delta ***`); metas.push({ path: `${calcPath}.calcMethod`, value: { description: 'Calculation type used (GreatCircle or Rhumbline).' } }); metas.push({ path: `${calcPath}.bearingTrackTrue`, value: { description: 'The bearing of a line between previousPoint and nextPoint, relative to true north.', units: 'rad' } }); metas.push({ path: `${calcPath}.bearingTrackMagnetic`, value: { description: 'The bearing of a line between previousPoint and nextPoint, relative to magnetic north.', units: 'rad' } }); metas.push({ path: `${calcPath}.crossTrackError`, value: { description: "The distance from the vessel's present position to the closest point on a line (track) between previousPoint and nextPoint. A negative number indicates that the vessel is currently to the left of this line (and thus must steer right to compensate), a positive number means the vessel is to the right of the line (steer left to compensate).", units: 'm' } }); metas.push({ path: `${calcPath}.previousPoint.distance`, value: { description: "The distance in meters between the vessel's present position and the previousPoint.", units: 'm' } }); metas.push({ path: `${calcPath}.distance`, value: { description: "The distance in meters between the vessel's present position and the nextPoint.", units: 'm' } }); metas.push({ path: `${calcPath}.bearingTrue`, value: { description: "The bearing of a line between the vessel's current position and nextPoint, relative to true north.", units: 'rad' } }); metas.push({ path: `${calcPath}.bearingMagnetic`, value: { description: "The bearing of a line between the vessel's current position and nextPoint, relative to magnetic north.", units: 'rad' } }); metas.push({ path: `${calcPath}.velocityMadeGood`, value: { description: 'The velocity component of the vessel towards the nextPoint.', units: 'm/s' } }); metas.push({ path: `${calcPath}.timeToGo`, value: { description: "Time in seconds to reach nextPoint's perpendicular) with current speed & direction.", units: 's' } }); metas.push({ path: `${calcPath}.estimatedTimeOfArrival`, value: { description: 'The estimated time of arrival at nextPoint position.', units: 's' } }); metas.push({ path: `${calcPath}.targetSpeed`, value: { description: 'The average speed required to arrive at the destination at the targetArrivalTime.', units: 'm/s' } }); return { updates: [ { meta: metas } ] }; }; // ********* Arrival circle events ***************** const onArrivalCircleEvent = (event) => { server.debug(JSON.stringify(event)); const alarmMethod = config.notifications.sound ? [types_1.ALARM_METHOD.sound, types_1.ALARM_METHOD.visual] : [types_1.ALARM_METHOD.visual]; if (event.type === 'enter') { if (srcPaths['navigation.position']) { emitNotification(new alarms_1.Notification('navigation.course.arrivalCircleEntered', `Entered arrival zone: ${event.value.toFixed(0)}m < ${watchArrival.rangeMax.toFixed(0)}`, types_1.ALARM_STATE.alert, alarmMethod)); } } if (event.type === 'exit') { emitNotification(new alarms_1.Notification('navigation.course.arrivalCircleEntered', null)); } }; // ********* Passed Destination events ***************** const onPassedDestEvent = (event) => { server.debug(JSON.stringify(event)); if (event.type === 'enter') { if (srcPaths['navigation.position']) { emitNotification(new alarms_1.Notification('navigation.course.perpendicularPassed', watchPassedDest.value.toString(), types_1.ALARM_STATE.alert, [])); } } if (event.type === 'exit') { emitNotification(new alarms_1.Notification('navigation.course.perpendicularPassed', null)); } }; // send notification delta message const emitNotification = (notification) => { server.debug(JSON.stringify(notification === null || notification === void 0 ? void 0 : notification.message)); server.handleMessage(plugin.id, { updates: [{ values: [notification.message] }] }); }; return plugin; };