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