UNPKG

@sailboat-computer/validation

Version:

Validation framework for sailboat computer v3

355 lines 16.9 kB
"use strict"; /** * Marine-specific validation rules */ Object.defineProperty(exports, "__esModule", { value: true }); exports.createMarineValidationRules = exports.EngineTemperatureRule = exports.WindSpeedRule = exports.BatteryVoltageRule = exports.GPSPositionRule = exports.MARINE_RANGES = void 0; const base_rule_1 = require("./base-rule"); const types_1 = require("../types"); /** * Marine validation ranges - based on typical sailboat specifications */ exports.MARINE_RANGES = { navigation: { gps: { position: { latitude: { min: -90, max: 90 }, longitude: { min: -180, max: 180 }, altitude: { min: -100, max: 10000 } // Meters above sea level }, accuracy: { hdop: { max: 10, warning: 5 }, satellites: { min: 4, warning: 6 } }, movement: { maxSpeed: 50, // Knots - reasonable max for sailboat maxAcceleration: 5, // Knots/second maxPositionJump: 1000 // Meters between readings } }, compass: { heading: { min: 0, max: 360 }, variation: { min: -30, max: 30 }, deviation: { min: -15, max: 15 }, rateOfChange: { max: 10 } // Degrees/second }, wind: { speed: { min: 0, max: 100, // Knots typical: { min: 0, max: 40 } }, direction: { min: 0, max: 360 }, gustFactor: { min: 1.0, max: 3.0 }, rateOfChange: { speed: { max: 10 }, // Knots/second direction: { max: 30 } // Degrees/second } }, depth: { value: { min: 0, max: 1000 }, // Meters rateOfChange: { max: 5 } // Meters/second } }, environmental: { temperature: { seawater: { min: -2, max: 35 }, // Celsius air: { min: -40, max: 60 }, interior: { min: -10, max: 50 }, rateOfChange: { max: 2 } // °C/minute }, pressure: { atmospheric: { min: 950, // hPa max: 1050, typical: { min: 1000, max: 1030 } }, rateOfChange: { max: 10 } // hPa/hour }, humidity: { range: { min: 0, max: 100 }, // Percentage rateOfChange: { max: 20 } // %/hour } }, electrical: { battery48v: { voltage: { min: 40, max: 60, normal: { min: 48, max: 54 }, critical: { min: 44, max: 58 } }, current: { min: -200, max: 200 }, // Amps temperature: { min: -10, max: 60, charging: { min: 0, max: 45 } } }, battery12v: { voltage: { min: 10, max: 15, normal: { min: 12, max: 13.8 } }, current: { min: -100, max: 100 } }, solar: { voltage: { min: 0, max: 100 }, current: { min: 0, max: 50 }, power: { min: 0, max: 2000 } // Watts } }, engine: { rpm: { min: 0, max: 4000, idle: { min: 600, max: 1000 }, cruise: { min: 1500, max: 2500 } }, oilPressure: { min: 0, max: 100, // PSI idle: { min: 10, max: 30 }, cruise: { min: 30, max: 80 } }, temperature: { min: 0, max: 120, // Celsius normal: { min: 70, max: 95 }, rateOfChange: { max: 5 } // °C/minute }, fuelFlow: { min: 0, max: 50, // Liters/hour efficiency: { min: 2, max: 15 } // Liters/hour per 100 RPM } }, fluidSystems: { tankLevels: { percentage: { min: 0, max: 100 }, rateOfChange: { max: 10 } // %/hour }, pressure: { freshWater: { min: 0, max: 60 }, // PSI watermaker: { min: 0, max: 800 } }, flowRates: { watermaker: { raw: { min: 0, max: 20 }, // Liters/minute product: { min: 0, max: 10 } } }, anchor: { chainDeployed: { min: 0, max: 100 }, // Meters rateOfChange: { max: 2 } // Meters/second } } }; /** * GPS position validation rule */ class GPSPositionRule extends base_rule_1.BaseValidationRule { constructor() { super('gps_position_validation', 'Validates GPS position coordinates are within valid ranges', 'physical', types_1.AlertSeverity.ALARM); } validate(data, context) { if (typeof data.value !== 'object' || data.value === null) { return this.createResult(false, 'GPS data must be an object with lat/lon properties', { rawValue: data.value }); } const gpsData = data.value; const lat = gpsData['latitude'] || gpsData['lat']; const lon = gpsData['longitude'] || gpsData['lon']; if (typeof lat !== 'number' || typeof lon !== 'number') { return this.createResult(false, 'GPS data missing valid latitude/longitude values', { gpsData }); } const ranges = exports.MARINE_RANGES.navigation.gps.position; const latValid = this.isInRange(lat, ranges.latitude.min, ranges.latitude.max); const lonValid = this.isInRange(lon, ranges.longitude.min, ranges.longitude.max); if (!latValid || !lonValid) { return this.createResult(false, `GPS coordinates out of range: lat=${lat}, lon=${lon}`, { latitude: lat, longitude: lon, ranges }); } // Check for position jumps if we have previous data const previousData = this.getPreviousData(data, context); if (previousData && typeof previousData.value === 'object') { const prevGps = previousData.value; const prevLat = prevGps['latitude'] || prevGps['lat']; const prevLon = prevGps['longitude'] || prevGps['lon']; if (typeof prevLat === 'number' && typeof prevLon === 'number') { const distance = this.calculateDistance(lat, lon, prevLat, prevLon); const timeDelta = (data.timestamp.getTime() - previousData.timestamp.getTime()) / 1000; const maxJump = exports.MARINE_RANGES.navigation.gps.movement.maxPositionJump; if (distance > maxJump && timeDelta < 60) { // Only check for jumps within 1 minute return this.createResult(false, `GPS position jump of ${distance.toFixed(0)}m exceeds maximum ${maxJump}m`, { distance, timeDelta, maxJump }); } } } return this.createResult(true); } isApplicable(data) { return this.matchesSensorType(data, ['gps', 'gnss', 'position']); } /** * Calculate distance between two GPS coordinates using Haversine formula */ calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371000; // Earth's radius in meters const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } toRadians(degrees) { return degrees * (Math.PI / 180); } } exports.GPSPositionRule = GPSPositionRule; /** * Battery voltage validation with context-aware thresholds */ class BatteryVoltageRule extends base_rule_1.BaseValidationRule { constructor() { super('battery_voltage_validation', 'Validates battery voltage with context-aware thresholds', 'physical', types_1.AlertSeverity.CRITICAL); } validate(data, context) { const voltage = this.extractNumericValue(data); if (voltage === null) { return this.createResult(false, `Unable to extract voltage from ${data.sensorType} sensor`, { rawValue: data.value }); } // Determine battery type from sensor type or location const is48V = data.sensorType.toLowerCase().includes('48v') || data.sensorType.toLowerCase().includes('house') || data.location?.toLowerCase().includes('house'); const ranges = is48V ? exports.MARINE_RANGES.electrical.battery48v.voltage : exports.MARINE_RANGES.electrical.battery12v.voltage; // Check critical limits first if (!this.isInRange(voltage, ranges.min, ranges.max)) { return this.createResult(false, `Battery voltage ${voltage}V is outside safe operating range [${ranges.min}V, ${ranges.max}V]`, { voltage, ranges, batteryType: is48V ? '48V' : '12V' }); } // Check normal operating range if (is48V && ranges.normal) { const inNormalRange = this.isInRange(voltage, ranges.normal.min, ranges.normal.max); if (!inNormalRange) { // This is a warning, not a failure return this.createResult(true, `Battery voltage ${voltage}V is outside normal range [${ranges.normal.min}V, ${ranges.normal.max}V]`, { voltage, ranges, batteryType: '48V', severity: 'warning' }); } } return this.createResult(true); } isApplicable(data) { return this.matchesSensorType(data, ['battery', 'voltage', 'power']); } } exports.BatteryVoltageRule = BatteryVoltageRule; /** * Wind speed validation with gust factor checking */ class WindSpeedRule extends base_rule_1.BaseValidationRule { constructor() { super('wind_speed_validation', 'Validates wind speed and checks for reasonable gust factors', 'physical', types_1.AlertSeverity.WARNING); } validate(data, context) { const windSpeed = this.extractNumericValue(data); if (windSpeed === null) { return this.createResult(false, `Unable to extract wind speed from ${data.sensorType} sensor`, { rawValue: data.value }); } const ranges = exports.MARINE_RANGES.navigation.wind.speed; // Check absolute limits if (!this.isInRange(windSpeed, ranges.min, ranges.max)) { return this.createResult(false, `Wind speed ${windSpeed} knots is outside valid range [${ranges.min}, ${ranges.max}] knots`, { windSpeed, ranges }); } // Check for sudden wind speed changes (gust detection) const previousData = this.getPreviousData(data, context, 30); // Look back 30 seconds if (previousData) { const prevWindSpeed = this.extractNumericValue(previousData); if (prevWindSpeed !== null) { const gustFactor = windSpeed / Math.max(prevWindSpeed, 1); // Avoid division by zero const maxGustFactor = exports.MARINE_RANGES.navigation.wind.gustFactor.max; if (gustFactor > maxGustFactor) { return this.createResult(true, // Pass but with warning `High gust factor detected: ${gustFactor.toFixed(2)} (current: ${windSpeed}kts, previous: ${prevWindSpeed}kts)`, { windSpeed, prevWindSpeed, gustFactor, maxGustFactor }); } } } return this.createResult(true); } isApplicable(data) { return this.matchesSensorType(data, ['wind', 'anemometer']); } } exports.WindSpeedRule = WindSpeedRule; /** * Engine temperature validation with operational context */ class EngineTemperatureRule extends base_rule_1.BaseValidationRule { constructor() { super('engine_temperature_validation', 'Validates engine temperature with operational context awareness', 'operational', types_1.AlertSeverity.CRITICAL); } validate(data, context) { const temperature = this.extractNumericValue(data); if (temperature === null) { return this.createResult(false, `Unable to extract temperature from ${data.sensorType} sensor`, { rawValue: data.value }); } const ranges = exports.MARINE_RANGES.engine.temperature; // Check absolute safety limits if (!this.isInRange(temperature, ranges.min, ranges.max)) { return this.createResult(false, `Engine temperature ${temperature}°C is outside safe range [${ranges.min}°C, ${ranges.max}°C]`, { temperature, ranges }); } // Context-aware validation const isEngineRunning = context.operationalContext.toLowerCase().includes('motor') || context.operationalContext.toLowerCase().includes('engine'); if (isEngineRunning) { // Engine should be in normal operating range when running if (!this.isInRange(temperature, ranges.normal.min, ranges.normal.max)) { const severity = temperature > ranges.normal.max ? 'critical' : 'warning'; return this.createResult(severity !== 'critical', `Engine temperature ${temperature}°C is ${temperature > ranges.normal.max ? 'too high' : 'too low'} for running engine`, { temperature, ranges, operationalContext: context.operationalContext, severity }); } } else { // Engine should be cool when not running if (temperature > ranges.normal.min) { return this.createResult(true, `Engine temperature ${temperature}°C seems high for non-running engine`, { temperature, operationalContext: context.operationalContext }); } } return this.createResult(true); } isApplicable(data) { return this.matchesSensorType(data, ['engine', 'motor', 'temperature']) && (data.location?.toLowerCase().includes('engine') || data.sensorType.toLowerCase().includes('engine')); } } exports.EngineTemperatureRule = EngineTemperatureRule; /** * Factory function to create all standard marine validation rules */ function createMarineValidationRules() { const rules = []; // GPS and Navigation Rules rules.push(new GPSPositionRule()); rules.push(new base_rule_1.RangeValidationRule('compass_heading_range', 'Validates compass heading is within 0-360 degrees', exports.MARINE_RANGES.navigation.compass.heading.min, exports.MARINE_RANGES.navigation.compass.heading.max, ['compass', 'heading', 'magnetic'], types_1.AlertSeverity.ALARM)); // Wind Rules rules.push(new WindSpeedRule()); rules.push(new base_rule_1.RangeValidationRule('wind_direction_range', 'Validates wind direction is within 0-360 degrees', exports.MARINE_RANGES.navigation.wind.direction.min, exports.MARINE_RANGES.navigation.wind.direction.max, ['wind', 'direction'], types_1.AlertSeverity.WARNING)); // Depth Rules rules.push(new base_rule_1.RangeValidationRule('depth_range', 'Validates depth sounder readings', exports.MARINE_RANGES.navigation.depth.value.min, exports.MARINE_RANGES.navigation.depth.value.max, ['depth', 'sounder', 'echo'], types_1.AlertSeverity.WARNING)); // Battery Rules rules.push(new BatteryVoltageRule()); // Engine Rules rules.push(new EngineTemperatureRule()); rules.push(new base_rule_1.RangeValidationRule('engine_rpm_range', 'Validates engine RPM readings', exports.MARINE_RANGES.engine.rpm.min, exports.MARINE_RANGES.engine.rpm.max, ['engine', 'rpm', 'motor'], types_1.AlertSeverity.WARNING)); // Environmental Rules rules.push(new base_rule_1.RangeValidationRule('air_temperature_range', 'Validates air temperature readings', exports.MARINE_RANGES.environmental.temperature.air.min, exports.MARINE_RANGES.environmental.temperature.air.max, ['temperature', 'air', 'ambient'], types_1.AlertSeverity.INFO)); rules.push(new base_rule_1.RangeValidationRule('barometric_pressure_range', 'Validates barometric pressure readings', exports.MARINE_RANGES.environmental.pressure.atmospheric.min, exports.MARINE_RANGES.environmental.pressure.atmospheric.max, ['pressure', 'barometric', 'atmospheric'], types_1.AlertSeverity.INFO)); // Rate of Change Rules rules.push(new base_rule_1.RateOfChangeRule('wind_speed_rate_change', 'Validates wind speed rate of change', exports.MARINE_RANGES.navigation.wind.rateOfChange.speed.max, ['wind', 'anemometer'], types_1.AlertSeverity.INFO)); rules.push(new base_rule_1.RateOfChangeRule('engine_temp_rate_change', 'Validates engine temperature rate of change', exports.MARINE_RANGES.engine.temperature.rateOfChange.max / 60, // Convert per minute to per second ['engine', 'temperature'], types_1.AlertSeverity.WARNING)); return rules; } exports.createMarineValidationRules = createMarineValidationRules; //# sourceMappingURL=marine-rules.js.map