UNPKG

nmea-streamer

Version:

The plugin streams a file with NMEA0183 messages to the SignalK server at a configurable speed. It also provides a web app with play controls.

288 lines (249 loc) 8.42 kB
module.exports = function (app) { var plugin = {}; plugin.id = "nmea-streamer"; plugin.name = "NMEA-streamer"; plugin.description = "NMEA streamer plays a file with NMEA0183 messages at a configurable speed."; plugin.author = "aswin.bouwmeester@gmail.com"; // Configuratie schema plugin.schema = { title: "NMEA0183 Log Streamer", description: `Configure the NMEA0183 Log Streamer. The NMEA0183 Log Streamer streams a file containing NMEA messages to the signal K server at real time or increased speed. In order for the plugin to work one has to define a data connection of type NMEA0183 with the NMEA0183 data source being a TCP connection on default port of 10110. `, type: "object", properties: { filename: { type: "string", title: "Filename", description: "Full path to the log file" }, } }; const play = { status: "stopped", speed: 1, markerCurrent: null, markerTarget: null, markerStart: null, markerEnd: null, startedBefore: null, lastSyncTime: null, lastSyncRmc: null }; const file = { baseName: null, path: null, start: null, end: null, }; const fs = require('fs'); const readline = require('readline'); const net = require('net'); const path = require('path'); const client = new net.Socket(); const { getDate, getDateTimeRange, isNmeaMessage, isAisMessage, } = require('./nmeautils'); // Connect to TCP server { client.connect(10110, 'localhost', () => { app.debug('Connected to TCP server'); }); client.on('close', () => { //stopEvent = true; app.debug('Connection to TCP server closed'); }); client.on('error', (error) => { app.error('TCP Connection Error: ', error); }); } plugin.registerWithRouter = function (router) { app.debug('registerWithRouter'); router.get('/getFile', (req, res) => { const { baseName, path, start, end } = file; res.json({ baseName, path, start: start?.toISOString() || null, end: end?.toISOString() || null, }); }); router.get('/getPlay', (req, res) => { res.status(200).send(wrapPlay()); }); router.post('/markerStart', (req, res) => { const markerStart = new Date(req.body.value); if (play.markerEnd > markerStart && markerStart < play.markerCurrent) play.markerStart = markerStart; res.status(200).json(play.markerStart.toISOString()); }) router.post('/markerEnd', (req, res) => { const markerEnd = new Date(req.body.value); if (play.markerStart < markerEnd && markerEnd > play.markerCurrent) play.markerEnd = markerEnd; res.status(200).json(play.markerEnd.toISOString()); }) router.post('/markerTarget', (req, res) => { const markerTarget = new Date(req.body.value); play.markerTarget = markerTarget; res.status(200).json(play.markerTarget.toISOString()); }) router.post('/status', (req, res) => { const valid = ["playing", "paused"]; const status = req.body.value; if (play.status != "stopped" && valid.includes(status)) play.status = status; res.status(200).json(play.status); }) router.post('/speed', (req, res) => { app.debug(req.body.value); play.speed = req.body.value; play.lastSyncRmc = play.markerCurrent; play.lastSyncTime = Date.now(); res.status(200).json(play.speed); }) } function wrapPlay() { return { status: play.status, speed: play.speed, markerTarget: play.markerTarget ? new Date(play.markerTarget) : null, markerStart: play.markerStart ? new Date(play.markerStart) : null, markerEnd: play.markerEnd ? new Date(play.markerEnd) : null, markerCurrent: play.markerCurrent ? new Date(play.markerCurrent) : null }; } plugin.start = async function (options, restartPlugin) { // Proces log file const processFile = async () => { const fileStream = fs.createReadStream(file.path); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); const processLine = (line) => { return new Promise((resolve) => { wait = handleLine(line); if (wait) { setTimeout( resolve, wait); } else { resolve(); } }); }; play.markerCurrent = play.markerStart; resetSync(); for await (const line of rl) { //if (stopEvent) { if (play.status == "stopping") { break; } while (play.status == 'paused') { await new Promise(resolve => setTimeout(resolve, 1000)); resetSync(); } await processLine(line); } rl.close(); }; const startReading = async () => { while (true) { app.debug(`Start streaming ${file.baseName}`); play.status = "playing"; await processFile(); app.debug(`Finish streaming ${file.baseName}`); if (play.status == "stopping") { play.status = "stopped"; break; } } }; function handleLine(line) { let wait = 0; if (!isNmeaMessage(line) && !isAisMessage(line)) return 0; const rmcTime = getDate(line); if (rmcTime) { play.markerCurrent = rmcTime; } play.status = 'skipping'; //lookingForTarget(); if (lookingForTarget()) { resetSync(); return 0 }; if (!withinTimeWindow()) { resetSync(); return 0 }; play.status = 'playing'; if (rmcTime) { wait = timeToWait(rmcTime); if (wait < 50) { wait = 0; } } client.write(line + '\n'); return wait; } function resetSync() { play.lastSyncRmc = play.markerCurrent; play.lastSyncTime = Date.now(); } function timeToWait(rmcTime) { const deltaTime = Date.now() - play.lastSyncTime; const deltaRmc = (rmcTime - play.lastSyncRmc); return Math.max(deltaRmc / play.speed - deltaTime, 0); } function lookingForTarget() { if (play.markerTarget === null) return false; if (play.startedBefore === null) { play.startedBefore = play.markerCurrent < play.markerTarget ? true : false; return true; } const before = play.markerCurrent <= play.markerTarget ? true : false; //app.debug(`startedBefore=${play.startedBefore}, before=${before}`); if ((play.startedBefore && !before) || (!play.startedBefore && before)) { play.startedBefore = null; play.markerTarget = null; return false; } else { //app.debug('Still looking for target'); return true; } } function withinTimeWindow() { return (play.markerCurrent > play.markerStart && play.markerCurrent < play.markerEnd); } app.debug("Plugin started with options: ", options); try { // Check if the file exists and is readable await fs.promises.access(options.filename, fs.constants.R_OK); } catch (error) { play.status = 'stopped'; app.status("Stopped"); throw new Error(`Failed to access: ${options.filename}`); } file.baseName = path.basename(options.filename); file.path = options.filename; play.speed = 1; play.status = 'initialising'; app.debug("Analysing file"); Object.assign(file, await getDateTimeRange(file.path)); play.markerStart = new Date(file.start); play.markerCurrent = new Date(play.start); play.markerEnd = new Date(file.end); play.markerTarget = null; play.startedBefore = null; startReading(); }; plugin.stop = function () { //stopEvent = true; play.status = "stopping"; app.debug("Stopping"); return new Promise((resolve, reject) => { const interval = setInterval(() => { if (play.status == "stopped") { clearInterval(interval); app.debug("Stopped"); resolve('Value met!'); } }, 100); }); } return plugin; };