@sailboat-computer/validation
Version:
Validation framework for sailboat computer v3
528 lines • 22.5 kB
JavaScript
"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