UNPKG

@sailboat-computer/validation

Version:

Validation framework for sailboat computer v3

528 lines 22.5 kB
"use strict"; /** * Signal K specific validation rules * * This file defines validation rules specialized for Signal K data. * These rules leverage Signal K metadata and structure to validate sensor data. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.createSignalKValidationRules = exports.SignalKDeltaValidationRule = exports.SignalKUpdateFrequencyRule = exports.SignalKSourceReliabilityRule = exports.SignalKPathValidationRule = void 0; const base_rule_1 = require("./base-rule"); const types_1 = require("../types"); const types_2 = require("../types"); /** * Signal K path validation rule * * Validates that a Signal K path follows the expected structure * and contains valid components. */ class SignalKPathValidationRule extends base_rule_1.BaseValidationRule { constructor() { super('signal_k_path_validation', 'Validates Signal K path structure and format', 'physical', types_1.AlertSeverity.ALARM); } validate(data, context) { // Cast to Signal K context const signalKContext = context; const path = signalKContext.signalKPath; if (!path) { return this.createResult(false, 'Missing Signal K path', { data }); } // Check path format if (!this.isValidPath(path)) { return this.createResult(false, `Invalid Signal K path format: ${path}`, { path }); } // Check path components const components = this.getPathComponents(path); if (components.length < 2) { return this.createResult(false, `Signal K path has too few components: ${path}`, { path, components }); } // Validate specific path types if (path.startsWith('navigation')) { return this.validateNavigationPath(path, components); } else if (path.startsWith('environment')) { return this.validateEnvironmentPath(path, components); } else if (path.startsWith('electrical')) { return this.validateElectricalPath(path, components); } else if (path.startsWith('propulsion')) { return this.validatePropulsionPath(path, components); } // Default pass for other paths return this.createResult(true); } isApplicable(data) { if (!data.source) return false; // Check if source is an object with an interface property if (typeof data.source === 'object' && data.source !== null && 'interface' in data.source) { // Check if interface is SIGNAL_K const interfaceType = data.source.interface; return interfaceType === types_2.SensorInterface.SIGNAL_K; } // Check if source is a string that might indicate Signal K if (typeof data.source === 'string') { const sourceStr = data.source; const sourceLower = sourceStr.toLowerCase(); return sourceLower.includes('nmea') || sourceLower.includes('signalk') || sourceLower.includes('signal_k'); } return false; } /** * Check if a path follows valid Signal K format */ isValidPath(path) { // Basic path validation if (!path || typeof path !== 'string') { return false; } // Check for invalid characters if (/[^a-zA-Z0-9.[\]_-]/.test(path)) { return false; } // Check for balanced brackets const openBrackets = (path.match(/\[/g) || []).length; const closeBrackets = (path.match(/\]/g) || []).length; if (openBrackets !== closeBrackets) { return false; } return true; } /** * Extract path components */ getPathComponents(path) { if (!path) return []; return path.split('.'); } /** * Validate navigation paths */ validateNavigationPath(path, components) { // Check for required components if (components.length < 3) { return this.createResult(false, `Navigation path missing required components: ${path}`, { path, components }); } // Validate specific navigation paths const navType = components[1]; switch (navType) { case 'position': // Check for valid position path if (components[2] && !['latitude', 'longitude', 'altitude'].includes(components[2])) { return this.createResult(false, `Invalid position component: ${components[2]}`, { path, components }); } break; case 'course': // Check for valid course path if (components[2] && !['overGroundTrue', 'overGroundMagnetic', 'magneticVariation'].includes(components[2])) { return this.createResult(false, `Invalid course component: ${components[2]}`, { path, components }); } break; case 'speed': // Check for valid speed path if (components[2] && !['overGround', 'throughWater', 'throughWaterTransverse'].includes(components[2])) { return this.createResult(false, `Invalid speed component: ${components[2]}`, { path, components }); } break; } return this.createResult(true); } /** * Validate environment paths */ validateEnvironmentPath(path, components) { // Check for required components if (components.length < 3) { return this.createResult(false, `Environment path missing required components: ${path}`, { path, components }); } // Validate specific environment paths const envType = components[1]; switch (envType) { case 'wind': // Check for valid wind path if (components[2] && !['angleApparent', 'angleTrueGround', 'angleTrueWater', 'speedApparent', 'speedTrue', 'speedOverGround'].includes(components[2])) { return this.createResult(false, `Invalid wind component: ${components[2]}`, { path, components }); } break; case 'depth': // Check for valid depth path if (components[2] && !['belowKeel', 'belowTransducer', 'belowSurface', 'transducerToKeel'].includes(components[2])) { return this.createResult(false, `Invalid depth component: ${components[2]}`, { path, components }); } break; case 'temperature': // Check for valid temperature path if (components[2] && !['water', 'air', 'engineRoom', 'refrigerator', 'freezer', 'heating'].includes(components[2])) { return this.createResult(false, `Invalid temperature component: ${components[2]}`, { path, components }); } break; } return this.createResult(true); } /** * Validate electrical paths */ validateElectricalPath(path, components) { // Check for required components if (components.length < 3) { return this.createResult(false, `Electrical path missing required components: ${path}`, { path, components }); } // Validate specific electrical paths const elecType = components[1]; if (elecType === 'batteries') { // Check for valid battery instance if (components.length < 4) { return this.createResult(false, `Battery path missing instance: ${path}`, { path, components }); } // Check for valid battery property if (components.length < 5) { return this.createResult(false, `Battery path missing property: ${path}`, { path, components }); } const batteryProperty = components[4]; if (batteryProperty && !['voltage', 'current', 'temperature', 'capacity', 'state', 'timeRemaining'].includes(batteryProperty)) { return this.createResult(false, `Invalid battery property: ${batteryProperty}`, { path, components }); } } return this.createResult(true); } /** * Validate propulsion paths */ validatePropulsionPath(path, components) { // Check for required components if (components.length < 3) { return this.createResult(false, `Propulsion path missing required components: ${path}`, { path, components }); } // Check for valid engine instance if (components.length < 4) { return this.createResult(false, `Propulsion path missing property: ${path}`, { path, components }); } const propulsionProperty = components[3]; if (propulsionProperty && !['state', 'revolutions', 'temperature', 'oilPressure', 'oilTemperature', 'coolantTemperature', 'alternatorVoltage'].includes(propulsionProperty)) { return this.createResult(false, `Invalid propulsion property: ${propulsionProperty}`, { path, components }); } return this.createResult(true); } } exports.SignalKPathValidationRule = SignalKPathValidationRule; /** * Signal K source reliability rule * * Validates the reliability of a Signal K data source * based on source metadata and history. */ class SignalKSourceReliabilityRule extends base_rule_1.BaseValidationRule { constructor() { super('signal_k_source_reliability', 'Validates reliability of Signal K data source', 'quality', types_1.AlertSeverity.WARNING); } validate(data, context) { // Cast to Signal K context const signalKContext = context; const source = signalKContext.signalKSource; const sourceMetadata = signalKContext.signalKSourceMetadata; if (!source) { return this.createResult(false, 'Missing Signal K source', { data }); } // Check source type const sourceType = this.getSourceType(source); const reliability = this.getSourceTypeReliability(sourceType); if (reliability < 0.7) { return this.createResult(false, `Low reliability source type: ${sourceType}`, { source, sourceType, reliability }); } // Check source metadata if available if (sourceMetadata) { // Check for talker ID in NMEA sources if (sourceType === 'nmea0183' && sourceMetadata['talker']) { const talkerReliability = this.getTalkerReliability(sourceMetadata['talker']); if (talkerReliability < 0.7) { return this.createResult(false, `Low reliability NMEA talker: ${sourceMetadata['talker']}`, { source, talker: sourceMetadata['talker'], reliability: talkerReliability }); } } // Check for PGN in NMEA 2000 sources if (sourceType === 'nmea2000' && sourceMetadata['pgn']) { const pgnReliability = this.getPGNReliability(sourceMetadata['pgn']); if (pgnReliability < 0.7) { return this.createResult(false, `Low reliability NMEA 2000 PGN: ${sourceMetadata['pgn']}`, { source, pgn: sourceMetadata['pgn'], reliability: pgnReliability }); } } } return this.createResult(true); } isApplicable(data) { if (!data.source) return false; // Check if source is an object with an interface property if (typeof data.source === 'object' && data.source !== null && 'interface' in data.source) { // Check if interface is SIGNAL_K const interfaceType = data.source.interface; return interfaceType === types_2.SensorInterface.SIGNAL_K; } // Check if source is a string that might indicate Signal K if (typeof data.source === 'string') { const sourceStr = data.source; const sourceLower = sourceStr.toLowerCase(); return sourceLower.includes('nmea') || sourceLower.includes('signalk') || sourceLower.includes('signal_k'); } return false; } /** * Extract source type from source string */ getSourceType(source) { if (source.includes('nmea0183')) { return 'nmea0183'; } else if (source.includes('nmea2000')) { return 'nmea2000'; } else if (source.includes('signalk')) { return 'signalk'; } else if (source.includes('ais')) { return 'ais'; } else { return 'unknown'; } } /** * Get reliability score for source type */ getSourceTypeReliability(sourceType) { switch (sourceType) { case 'nmea2000': return 0.9; // High reliability case 'signalk': return 0.85; // Good reliability case 'nmea0183': return 0.8; // Decent reliability case 'ais': return 0.85; // Good reliability default: return 0.6; // Unknown source type } } /** * Get reliability score for NMEA 0183 talker ID */ getTalkerReliability(talker) { // Reliability scores for common talker IDs const talkerScores = { 'GP': 0.9, // GPS 'GL': 0.85, // GLONASS 'GA': 0.85, // Galileo 'GB': 0.85, // BeiDou 'GN': 0.9, // Multiple GNSS systems 'HC': 0.8, // Heading/Compass 'RA': 0.75, // Radar 'SD': 0.7, // Depth sounder 'VW': 0.7, // Wind 'II': 0.6, // Integrated instrument 'YX': 0.5, // Transducer 'WI': 0.7, // Weather instruments 'VD': 0.7, // Velocity sensor (doppler) }; return talkerScores[talker] || 0.6; // Default for unknown talkers } /** * Get reliability score for NMEA 2000 PGN */ getPGNReliability(pgn) { // Reliability scores for common PGNs const pgnScores = { 129025: 0.9, // Position, Rapid Update 129026: 0.9, // COG & SOG, Rapid Update 129029: 0.95, // GNSS Position Data 127250: 0.85, // Vessel Heading 128259: 0.8, // Speed 128267: 0.8, // Water Depth 130306: 0.75, // Wind Data 127245: 0.8, // Rudder 127508: 0.85, // Battery Status 127506: 0.85, // DC Detailed Status 127505: 0.8, // Fluid Level 130310: 0.75, // Environmental Parameters 130311: 0.75, // Environmental Parameters 130312: 0.8, // Temperature 127488: 0.85, // Engine Parameters, Rapid Update 127489: 0.85, // Engine Parameters, Dynamic }; return pgnScores[pgn] || 0.7; // Default for unknown PGNs } } exports.SignalKSourceReliabilityRule = SignalKSourceReliabilityRule; /** * Signal K update frequency rule * * Validates that Signal K data is being updated at an appropriate frequency * based on the data type. */ class SignalKUpdateFrequencyRule extends base_rule_1.BaseValidationRule { constructor() { super('signal_k_update_frequency', 'Validates update frequency of Signal K data', 'operational', types_1.AlertSeverity.INFO); } validate(data, context) { // Cast to Signal K context const signalKContext = context; const path = signalKContext.signalKPath; if (!path) { return this.createResult(false, 'Missing Signal K path', { data }); } // Get expected update frequency for this path const expectedFrequency = this.getExpectedUpdateFrequency(path); // Check if we have previous data to compare const previousData = this.getPreviousData(data, context); if (!previousData) { return this.createResult(true, 'No previous data available for frequency validation', { path }); } // Calculate actual update frequency const timeDelta = (data.timestamp.getTime() - previousData.timestamp.getTime()) / 1000; const actualFrequency = 1 / timeDelta; // Check if frequency is within acceptable range const minFrequency = expectedFrequency * 0.5; const maxFrequency = expectedFrequency * 2; if (actualFrequency < minFrequency) { return this.createResult(false, `Update frequency too low: ${actualFrequency.toFixed(2)}Hz (expected: ${expectedFrequency}Hz)`, { path, actualFrequency, expectedFrequency, timeDelta }); } if (actualFrequency > maxFrequency && maxFrequency > 0) { return this.createResult(false, `Update frequency too high: ${actualFrequency.toFixed(2)}Hz (expected: ${expectedFrequency}Hz)`, { path, actualFrequency, expectedFrequency, timeDelta }); } return this.createResult(true); } isApplicable(data) { if (!data.source) return false; // Check if source is an object with an interface property if (typeof data.source === 'object' && data.source !== null && 'interface' in data.source) { // Check if interface is SIGNAL_K const interfaceType = data.source.interface; return interfaceType === types_2.SensorInterface.SIGNAL_K; } // Check if source is a string that might indicate Signal K if (typeof data.source === 'string') { const sourceStr = data.source; const sourceLower = sourceStr.toLowerCase(); return sourceLower.includes('nmea') || sourceLower.includes('signalk') || sourceLower.includes('signal_k'); } return false; } /** * Get expected update frequency for a Signal K path */ getExpectedUpdateFrequency(path) { // High frequency paths (1Hz or higher) if (path.startsWith('navigation.position') || path.startsWith('navigation.speedOverGround') || path.startsWith('navigation.courseOverGroundTrue') || path.startsWith('navigation.headingTrue') || path.startsWith('navigation.headingMagnetic')) { return 1.0; // 1Hz } // Medium frequency paths (0.5Hz) if (path.startsWith('environment.wind') || path.startsWith('environment.depth') || path.startsWith('navigation.attitude')) { return 0.5; // 0.5Hz } // Low frequency paths (0.1Hz) if (path.startsWith('environment.temperature') || path.startsWith('environment.humidity') || path.startsWith('environment.pressure')) { return 0.1; // 0.1Hz } // Very low frequency paths (0.033Hz - once every 30 seconds) if (path.startsWith('electrical.batteries') || path.startsWith('tanks')) { return 0.033; // 0.033Hz } // Default frequency (0.2Hz) return 0.2; } } exports.SignalKUpdateFrequencyRule = SignalKUpdateFrequencyRule; /** * Signal K delta validation rule * * Validates that Signal K delta updates are well-formed * and contain the required fields. */ class SignalKDeltaValidationRule extends base_rule_1.BaseValidationRule { constructor() { super('signal_k_delta_validation', 'Validates Signal K delta update format', 'physical', types_1.AlertSeverity.ALARM); } validate(data, context) { // Cast to Signal K context const signalKContext = context; const delta = signalKContext.signalKDelta; if (!delta) { return this.createResult(false, 'Missing Signal K delta', { data }); } // Check delta structure if (!delta.updates || !Array.isArray(delta.updates)) { return this.createResult(false, 'Invalid delta structure: missing updates array', { delta }); } // Check each update for (let i = 0; i < delta.updates.length; i++) { const update = delta.updates[i]; // Check update structure if (!update.source) { return this.createResult(false, `Invalid update structure: missing source in update ${i}`, { update }); } if (!update.values || !Array.isArray(update.values)) { return this.createResult(false, `Invalid update structure: missing values array in update ${i}`, { update }); } // Check each value for (let j = 0; j < update.values.length; j++) { const value = update.values[j]; if (!value.path) { return this.createResult(false, `Invalid value structure: missing path in update ${i}, value ${j}`, { value }); } if (value.value === undefined) { return this.createResult(false, `Invalid value structure: missing value in update ${i}, value ${j}`, { value }); } } } return this.createResult(true); } isApplicable(data) { if (!data.source) return false; // Check if source is an object with an interface property if (typeof data.source === 'object' && data.source !== null && 'interface' in data.source) { // Check if interface is SIGNAL_K const interfaceType = data.source.interface; return interfaceType === types_2.SensorInterface.SIGNAL_K; } // Check if source is a string that might indicate Signal K if (typeof data.source === 'string') { const sourceStr = data.source; const sourceLower = sourceStr.toLowerCase(); return sourceLower.includes('nmea') || sourceLower.includes('signalk') || sourceLower.includes('signal_k'); } return false; } } exports.SignalKDeltaValidationRule = SignalKDeltaValidationRule; /** * Create all Signal K validation rules */ function createSignalKValidationRules() { return [ new SignalKPathValidationRule(), new SignalKSourceReliabilityRule(), new SignalKUpdateFrequencyRule(), new SignalKDeltaValidationRule() ]; } exports.createSignalKValidationRules = createSignalKValidationRules; //# sourceMappingURL=signal-k-rules.js.map