UNPKG

@canboat/visual-analyzer

Version:

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

456 lines 19 kB
"use strict"; /** * Copyright 2025 Scott Bender (scott@scottbender.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.RecordingService = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const events_1 = require("events"); const canboatjs_1 = require("@canboat/canboatjs"); class RecordingService extends events_1.EventEmitter { constructor(configPath) { super(); this.isRecording = false; this.currentFileName = null; this.currentFormat = 'canboat-json'; this.messageCount = 0; this.startTime = null; this.fileStream = null; // Create recordings directory this.recordingsDir = path.join(configPath, 'recordings'); this.ensureRecordingsDirectory(); } ensureRecordingsDirectory() { try { if (!fs.existsSync(this.recordingsDir)) { fs.mkdirSync(this.recordingsDir, { recursive: true }); console.log(`Created recordings directory: ${this.recordingsDir}`); } } catch (error) { console.error('Failed to create recordings directory:', error); throw new Error(`Failed to create recordings directory: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getStatus() { let fileSize = 0; if (this.isRecording && this.currentFileName) { try { const filePath = path.join(this.recordingsDir, this.currentFileName); const stats = fs.statSync(filePath); fileSize = stats.size; } catch (error) { console.warn('Failed to get file size:', error); } } return { isRecording: this.isRecording, fileName: this.currentFileName || undefined, startTime: this.startTime?.toISOString(), messageCount: this.messageCount, fileSize, format: this.currentFormat, }; } startRecording(options = {}) { if (this.isRecording) { throw new Error('Recording is already in progress'); } try { // Generate filename if not provided let fileName = options.fileName; if (!fileName) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const format = options.format || 'passthrough'; const extension = this.getFileExtension(format); fileName = `recording_${timestamp}.${extension}`; } // Ensure filename has correct extension if (!fileName.includes('.')) { const extension = this.getFileExtension(options.format || 'passthrough'); fileName += `.${extension}`; } const filePath = path.join(this.recordingsDir, fileName); // Check if file already exists if (fs.existsSync(filePath)) { throw new Error(`File ${fileName} already exists`); } // Create file stream this.fileStream = fs.createWriteStream(filePath, { flags: 'w' }); // Handle stream errors this.fileStream.on('error', (error) => { console.error('Recording file stream error:', error); this.stopRecording(); this.emit('error', error); }); // Set recording state this.isRecording = true; this.currentFileName = fileName; this.currentFormat = options.format || 'passthrough'; this.messageCount = 0; this.startTime = new Date(); if (this.currentFormat === 'canboat-json-pretty') { this.fileStream.write('[\n'); // Start JSON array for canboat-json format } console.log(`Started recording to ${fileName} in ${this.currentFormat} format`); this.emit('started', this.getStatus()); return this.getStatus(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error('Failed to start recording:', errorMessage); throw error; } } stopRecording() { if (!this.isRecording) { throw new Error('No recording in progress'); } try { // Close file stream if (this.fileStream) { if (this.currentFormat === 'canboat-json-pretty') { this.fileStream.write('\n]\n'); // End JSON array for canboat-json format } this.fileStream.end(); this.fileStream = null; } const status = this.getStatus(); // Reset recording state this.isRecording = false; const fileName = this.currentFileName; this.currentFileName = null; this.currentFormat = 'passthrough'; this.messageCount = 0; this.startTime = null; console.log(`Stopped recording. Recorded ${status.messageCount} messages to ${fileName}`); this.emit('stopped', status); return { isRecording: false, messageCount: 0, fileSize: 0, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error('Failed to stop recording:', errorMessage); throw error; } } recordMessage(raw, pgn) { if (!this.isRecording || !this.fileStream) { return; } try { let formattedMessage = undefined; if (this.currentFormat === 'passthrough') { if (typeof raw === 'string') { formattedMessage = raw; } else { // right now, only canbus sends object data formattedMessage = (0, canboatjs_1.encodeCandump2)({ ...raw, data: Buffer.from(raw.data) })[0]; } } else if (pgn !== undefined) { switch (this.currentFormat) { case 'canboat-json': formattedMessage = JSON.stringify(pgn); break; case 'canboat-json-pretty': formattedMessage = JSON.stringify(pgn, null, 2) + ','; break; case 'actisense': { const actisenseResult = (0, canboatjs_1.pgnToActisenseSerialFormat)(pgn); if (!actisenseResult) { console.error(`Failed to convert PGN ${pgn.pgn} to Actisense format`); return; } formattedMessage = actisenseResult; break; } case 'actisense-n2k-ascii': { const n2kAsciiResult = (0, canboatjs_1.pgnToActisenseN2KAsciiFormat)(pgn); if (!n2kAsciiResult) { console.error(`Failed to convert PGN ${pgn.pgn} to Actisense N2K ASCII format`); return; } formattedMessage = n2kAsciiResult; break; } case 'ikonvert': { const ikonvertResult = (0, canboatjs_1.pgnToiKonvertSerialFormat)(pgn); if (!ikonvertResult) { console.error(`Failed to convert PGN ${pgn.pgn} to iKonvert format`); return; } formattedMessage = ikonvertResult; break; } case 'ydwg-raw': { const ydwgResult = (0, canboatjs_1.pgnToYdgwRawFormat)(pgn); if (!ydwgResult) { console.error(`Failed to convert PGN ${pgn.pgn} to YDWG RAW format`); return; } formattedMessage = Array.isArray(ydwgResult) ? ydwgResult.join('\n') : ydwgResult; break; } case 'ydwg-full-raw': { const ydwgFullResult = (0, canboatjs_1.pgnToYdgwFullRawFormat)(pgn); if (!ydwgFullResult) { console.error(`Failed to convert PGN ${pgn.pgn} to YDWG Full RAW format`); return; } formattedMessage = Array.isArray(ydwgFullResult) ? ydwgFullResult.join('\n') : ydwgFullResult; break; } case 'pcdin': { const pcdinResult = (0, canboatjs_1.pgnToPCDIN)(pgn); if (!pcdinResult) { console.error(`Failed to convert PGN ${pgn.pgn} to PCDIN format`); return; } formattedMessage = pcdinResult; break; } case 'mxpgn': { const mxpgnResult = (0, canboatjs_1.pgnToMXPGN)(pgn); if (!mxpgnResult) { console.error(`Failed to convert PGN ${pgn.pgn} to MXPGN format`); return; } formattedMessage = mxpgnResult; break; } case 'candump1': { const candump1Result = (0, canboatjs_1.pgnToCandump1)(pgn); if (!candump1Result) { console.error(`Failed to convert PGN ${pgn.pgn} to candump1 format`); return; } formattedMessage = Array.isArray(candump1Result) ? candump1Result.join('\n') : candump1Result; break; } case 'candump2': { const candump2Result = (0, canboatjs_1.pgnToCandump2)(pgn); if (!candump2Result) { console.error(`Failed to convert PGN ${pgn.pgn} to candump2 format`); return; } formattedMessage = Array.isArray(candump2Result) ? candump2Result.join('\n') : candump2Result; break; } case 'candump3': { const candump3Result = (0, canboatjs_1.pgnToCandump3)(pgn); if (!candump3Result) { console.error(`Failed to convert PGN ${pgn.pgn} to candump3 format`); return; } formattedMessage = Array.isArray(candump3Result) ? candump3Result.join('\n') : candump3Result; break; } } } if (formattedMessage !== undefined) { // Write message to file this.fileStream.write(formattedMessage + '\n'); this.messageCount++; // Emit progress update every 100 messages if (this.messageCount % 10 === 0) { this.emit('progress', this.getStatus()); } } } catch (error) { console.error('Failed to record message:', error); this.emit('error', error); } } getRecordedFiles() { try { if (!fs.existsSync(this.recordingsDir)) { return []; } const files = fs.readdirSync(this.recordingsDir); const recordingFiles = []; for (const file of files) { const filePath = path.join(this.recordingsDir, file); try { const stats = fs.statSync(filePath); if (stats.isFile()) { // Try to determine format from file extension or content const format = this.guessFileFormat(file); // Count messages (rough estimate based on file size and format) const messageCount = this.estimateMessageCount(filePath); recordingFiles.push({ name: file, size: stats.size, created: stats.birthtime.toISOString(), messageCount, format, }); } } catch (error) { console.warn(`Failed to get stats for file ${file}:`, error); } } // Sort by creation date (newest first) return recordingFiles.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()); } catch (error) { console.error('Failed to get recorded files:', error); return []; } } deleteRecordedFile(fileName) { const filePath = path.join(this.recordingsDir, fileName); // Prevent path traversal attacks if (!filePath.startsWith(this.recordingsDir)) { throw new Error('Invalid file path'); } if (!fs.existsSync(filePath)) { throw new Error('File not found'); } try { fs.unlinkSync(filePath); console.log(`Deleted recording file: ${fileName}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.error(`Failed to delete file ${fileName}:`, errorMessage); throw error; } } getRecordedFilePath(fileName) { const filePath = path.join(this.recordingsDir, fileName); // Prevent path traversal attacks if (!filePath.startsWith(this.recordingsDir)) { throw new Error('Invalid file path'); } if (!fs.existsSync(filePath)) { throw new Error('File not found'); } return filePath; } getFileExtension(format) { switch (format) { case 'canboat-json': return 'json'; case 'actisense': case 'actisense-n2k-ascii': return 'n2k'; case 'ikonvert': return 'iko'; case 'ydwg-full-raw': case 'ydwg-raw': return 'ydwg'; case 'pcdin': return 'pcd'; case 'mxpgn': return 'mxp'; case 'candump1': case 'candump2': case 'candump3': return 'log'; default: return 'txt'; } } guessFileFormat(fileName) { const extension = path.extname(fileName).toLowerCase().substring(1); switch (extension) { case 'json': return 'canboat-json'; case 'n2k': return 'actisense'; case 'iko': return 'ikonvert'; case 'ydwg': return 'ydwg-raw'; case 'pcd': return 'pcdin'; case 'mxp': return 'mxpgn'; case 'log': return 'candump1'; default: return 'unknown'; } } estimateMessageCount(filePath) { try { const stats = fs.statSync(filePath); const fileSize = stats.size; if (fileSize === 0) return 0; // Read a small sample to estimate average line length const sampleSize = Math.min(4096, fileSize); const buffer = Buffer.alloc(sampleSize); const fd = fs.openSync(filePath, 'r'); fs.readSync(fd, buffer, 0, sampleSize, 0); fs.closeSync(fd); const sample = buffer.toString('utf8'); const lines = sample.split('\n').filter((line) => line.trim().length > 0); if (lines.length === 0) return 0; const avgLineLength = sample.length / lines.length; const estimatedLines = Math.floor(fileSize / avgLineLength); return estimatedLines; } catch (error) { console.warn('Failed to estimate message count:', error); return 0; } } } exports.RecordingService = RecordingService; //# sourceMappingURL=recording-service.js.map