UNPKG

@canboat/visual-analyzer

Version:

NMEA 2000 data visualization utility (requires SK Server >= 2.15)

293 lines (259 loc) 10.2 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { ServerAPI, Plugin, //Delta, //Path } from '@signalk/server-api' import { ApiResponse } from './types' import { FromPgn } from '@canboat/canboatjs' import { N2kMapper } from '@signalk/n2k-signalk' import { translateToSignalK } from './server' import { RecordingService } from './recording-service' const PLUGIN_ID = 'canboat-visual-analyzer' const PLUGIN_NAME = 'Canboat Visual Analyzer' module.exports = function (app: ServerAPI) { let onStop: any[] = [] //let dbusSetValue: any let canboatParser: FromPgn let n2kMapper: N2kMapper let recordingService: RecordingService const plugin: Plugin = { id: PLUGIN_ID, name: PLUGIN_NAME, description: 'Canboat Visual Analyzer', schema: () => { return { title: PLUGIN_NAME, type: 'object', properties: {}, } }, stop: () => { onStop.forEach((f) => f()) onStop = [] }, start: (_options: any) => { canboatParser = new FromPgn({ checkForInvalidFields: true, useCamel: true, // Default value useCamelCompat: false, returnNonMatches: true, createPGNObjects: true, includeInputData: true, includeRawData: true, includeByteMapping: true, }) canboatParser.on('error', (error) => { ;(app as any).debug('Canboat Parser error:', error) }) n2kMapper = new N2kMapper({}) recordingService = new RecordingService(`${(app as any).config.configPath}/visual-analyzer`) const anyapp = app as any recordingService.on('started', (status) => { console.log('Recording started:', status) anyapp.emit('recording:started', status) }) recordingService.on('stopped', (status) => { console.log('Recording stopped:', status) anyapp.emit('recording:stopped', status) }) recordingService.on('error', (error) => { console.error('Recording error:', error) anyapp.emit('recording:error', { error: error.message, }) }) recordingService.on('progress', (status) => { anyapp.emit('recording:progress', status) }) recordingService.on('error', (error) => { console.error('Recording error:', error) anyapp.emit('recording:error', { error: error.message, }) }) anyapp.on('canboatjs:rawoutput', (output: any) => { if (recordingService.getStatus().isRecording) { if (recordingService.getStatus().format === 'passthrough') { recordingService.recordMessage(output, undefined) } else { try { const pgn = canboatParser.parse(output) if (pgn) { recordingService.recordMessage(undefined, pgn) } } catch (error) { console.debug('Failed to parse raw NMEA data:', error) } } } }) }, registerWithRouter: (router: any) => { router.post('/api/send-n2k', (req: any, res: any) => { try { const values = req.body.values if (!values) { return res.status(400).json({ success: false, error: 'Missing required field: values', } as ApiResponse) } const pgnDataArray: any[] = [] for (const value of values) { // Check if input is a string (NMEA 2000 format) or JSON if (typeof value === 'string') { const lines = value.split(/\r?\n/).filter((line) => line.trim()) if (lines.length === 0) { return res.status(400).json({ success: false, error: 'No valid lines found in input', } as ApiResponse) } try { for (const line of lines) { const trimmedLine = line.trim() if (trimmedLine) { try { const parsed = canboatParser.parseString(trimmedLine) if (parsed) { pgnDataArray.push(parsed) } else { console.warn(`Unable to parse line: ${trimmedLine}`) } } catch (lineParseError) { const errorMessage = lineParseError instanceof Error ? lineParseError.message : 'Unknown error' console.warn(`Error parsing line "${trimmedLine}": ${errorMessage}`) // Continue processing other lines instead of failing } } } if (pgnDataArray.length === 0) { return res.status(400).json({ success: false, error: 'Unable to parse any NMEA 2000 strings from input', } as ApiResponse) } console.log( `Parsed ${pgnDataArray.length} NMEA 2000 messages from ${lines.length} lines using canboatjs`, ) } catch (canboatParseError) { const errorMessage = canboatParseError instanceof Error ? canboatParseError.message : 'Unknown error' return res.status(400).json({ success: false, error: 'Error parsing NMEA 2000 strings: ' + errorMessage, } as ApiResponse) } } else if (typeof value === 'object') { // Value is already a JSON object pgnDataArray.push(value) } else { return res.status(400).json({ success: false, error: 'Value must be a string (JSON or NMEA 2000 format) or object', } as ApiResponse) } } // Process each parsed message const results: any[] = [] for (const pgnData of pgnDataArray) { console.log('Processing NMEA 2000 message for transmission:', { pgn: pgnData.pgn, data: pgnData, }) ;(app as any).emit('nmea2000JsonOut', pgnData) } // Return success response in SignalK format res.json({ success: true, message: `${pgnDataArray.length} message(s) processed successfully`, messagesProcessed: pgnDataArray.length, results: results, // Include detailed results for each message } as ApiResponse) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' console.error('Error processing input test request:', error) res.status(500).json({ success: false, error: errorMessage, } as ApiResponse) } }) router.post('/api/transform/signalk', (req: any, res: any) => { translateToSignalK(req, res, canboatParser, n2kMapper) }) // Recording API routes router.get('/api/recording/status', (req: any, res: any) => { try { const status = recordingService.getStatus() res.json({ success: true, result: status } as ApiResponse) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' res.status(500).json({ success: false, error: errorMessage }) } }) router.post('/api/recording/start', (req: any, res: any) => { try { const { fileName, format } = req.body.value const result = recordingService.startRecording({ fileName, format }) res.json({ success: true, fileName: result.fileName, message: 'Recording started successfully', } as ApiResponse) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' res.status(400).json({ success: false, error: errorMessage } as ApiResponse) } }) router.post('/api/recording/stop', (req: any, res: any) => { try { const result = recordingService.stopRecording() res.json({ success: true, message: 'Recording stopped successfully', finalStats: result }) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' res.status(400).json({ success: false, error: errorMessage }) } }) router.get('/api/recording/files', (req: any, res: any) => { try { const files = recordingService.getRecordedFiles() res.json({ success: true, results: files, // Include detailed results for each message } as ApiResponse) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' res.status(500).json({ success: false, error: errorMessage } as ApiResponse) } }) router.delete('/api/recording/files/:fileName', (req: any, res: any) => { try { recordingService.deleteRecordedFile(req.params.fileName) res.json({ success: true, message: 'File deleted successfully' }) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' res.status(400).json({ success: false, error: errorMessage }) } }) router.get('/api/recording/files/:fileName/download', (req: any, res: any) => { try { const filePath = recordingService.getRecordedFilePath(req.params.fileName) res.download(filePath, req.params.fileName, (err: any) => { if (err) { console.error('Download error:', err) if (!res.headersSent) { res.status(500).json({ success: false, error: 'Download failed' }) } } }) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' res.status(404).json({ success: false, error: errorMessage }) } }) }, } return plugin }