@sailboat-computer/health-monitoring
Version:
Comprehensive health monitoring system for sailboat computer v3 with marine-specific health checks
247 lines (224 loc) • 7.61 kB
text/typescript
/**
* GPS Health Check for marine navigation systems
*/
import {
BaseHealthCheck,
HealthCheckResult,
HealthCheckConfig,
HealthCheckType,
HealthStatus,
HealthStatusType,
OperationalContext,
OperationalContextType,
MarineSystemType,
MarineSystemTypeType,
MarineEnvironmentStatus
} from '../index';
/**
* GPS data interface for health checking
*/
export interface GPSData {
latitude: number;
longitude: number;
altitude: number;
satelliteCount: number;
hdop: number; // Horizontal Dilution of Precision
vdop: number; // Vertical Dilution of Precision
speed: number; // Speed over ground in knots
course: number; // Course over ground in degrees
timestamp: Date;
fix: 'none' | '2d' | '3d';
}
/**
* GPS Health Check implementation
*/
export class GPSHealthCheck extends BaseHealthCheck {
private gpsDataProvider: () => Promise<GPSData>;
private lastKnownPosition?: { lat: number; lon: number; timestamp: Date };
constructor(
config: HealthCheckConfig,
gpsDataProvider: () => Promise<GPSData>
) {
super(config);
this.gpsDataProvider = gpsDataProvider;
}
protected async performCheck(
operationalContext: OperationalContextType,
marineEnvironment?: MarineEnvironmentStatus
): Promise<HealthCheckResult> {
try {
const gpsData = await this.gpsDataProvider();
// Calculate health score based on multiple factors
let score = 1.0;
let status: HealthStatusType = HealthStatus.HEALTHY;
const issues: string[] = [];
const details: Record<string, any> = {
satelliteCount: gpsData.satelliteCount,
hdop: gpsData.hdop,
vdop: gpsData.vdop,
fix: gpsData.fix,
dataAge: Date.now() - gpsData.timestamp.getTime()
};
// Check GPS fix quality
if (gpsData.fix === 'none') {
score = 0.0;
status = HealthStatus.CRITICAL;
issues.push('No GPS fix available');
} else if (gpsData.fix === '2d') {
score -= 0.3;
issues.push('Only 2D GPS fix available');
}
// Check satellite count
if (gpsData.satelliteCount < 4) {
score -= 0.4;
issues.push(`Low satellite count: ${gpsData.satelliteCount}`);
} else if (gpsData.satelliteCount < 6) {
score -= 0.2;
issues.push(`Marginal satellite count: ${gpsData.satelliteCount}`);
}
// Check HDOP (Horizontal Dilution of Precision)
if (gpsData.hdop > 5.0) {
score -= 0.3;
issues.push(`Poor horizontal accuracy: HDOP ${gpsData.hdop}`);
} else if (gpsData.hdop > 2.0) {
score -= 0.1;
issues.push(`Marginal horizontal accuracy: HDOP ${gpsData.hdop}`);
}
// Check data freshness
const dataAge = Date.now() - gpsData.timestamp.getTime();
if (dataAge > 30000) { // 30 seconds
score -= 0.4;
issues.push(`Stale GPS data: ${Math.round(dataAge / 1000)}s old`);
} else if (dataAge > 10000) { // 10 seconds
score -= 0.2;
issues.push(`Old GPS data: ${Math.round(dataAge / 1000)}s old`);
}
// Check for position jumps (if we have previous position)
if (this.lastKnownPosition) {
const distance = this.calculateDistance(
this.lastKnownPosition.lat,
this.lastKnownPosition.lon,
gpsData.latitude,
gpsData.longitude
);
const timeDiff = (gpsData.timestamp.getTime() - this.lastKnownPosition.timestamp.getTime()) / 1000;
const maxReasonableSpeed = 50; // knots
const maxDistance = (maxReasonableSpeed * 0.514444 * timeDiff) / 1000; // km
if (distance > maxDistance * 2) {
score -= 0.3;
issues.push(`Suspicious position jump: ${distance.toFixed(2)}km`);
details['positionJump'] = distance;
}
}
// Environmental adjustments
if (marineEnvironment) {
if (marineEnvironment.weather === 'storm') {
// GPS can be affected by heavy weather
score = Math.max(score - 0.1, 0);
details['environmentalImpact'] = 'Storm conditions may affect GPS accuracy';
}
}
// Update last known position
if (gpsData.fix !== 'none') {
this.lastKnownPosition = {
lat: gpsData.latitude,
lon: gpsData.longitude,
timestamp: gpsData.timestamp
};
}
// Determine final status
score = Math.max(0, Math.min(1, score));
if (score >= 0.8) {
status = HealthStatus.HEALTHY;
} else if (score >= 0.6) {
status = HealthStatus.DEGRADED;
} else if (score >= 0.3) {
status = HealthStatus.UNHEALTHY;
} else {
status = HealthStatus.CRITICAL;
}
const message = issues.length > 0
? `GPS issues detected: ${issues.join(', ')}`
: 'GPS operating normally';
return {
checkId: this.config.checkId,
name: this.config.name,
type: this.config.type,
marineSystemType: this.config.marineSystemType,
status,
score,
message,
details,
timestamp: new Date(),
executionTime: 0, // Will be set by base class
marineContext: {
operationalContext,
environmentalImpact: marineEnvironment?.weather === 'storm',
safetyImpact: true, // GPS is critical for navigation safety
powerImpact: 5 // GPS typically uses ~5W
},
thresholds: this.config.thresholds
};
} catch (error) {
throw new Error(`GPS health check failed: ${(error as Error).message}`);
}
}
/**
* Calculate distance between two GPS coordinates using Haversine formula
*/
private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Earth's radius in kilometers
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;
}
private toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}
}
/**
* Factory function to create GPS health check with standard marine configuration
*/
export function createGPSHealthCheck(
gpsDataProvider: () => Promise<GPSData>,
customConfig?: Partial<HealthCheckConfig>
): GPSHealthCheck {
const defaultConfig: HealthCheckConfig = {
checkId: 'gps-health',
name: 'GPS Navigation Health',
type: HealthCheckType.SENSOR,
marineSystemType: MarineSystemType.NAVIGATION as MarineSystemTypeType,
interval: 10000, // Check every 10 seconds
timeout: 5000, // 5 second timeout
retryCount: 2,
thresholds: {
warning: 0.6,
critical: 0.3,
unit: 'health_score'
},
marineSettings: {
operationalContexts: [
OperationalContext.SAILING,
OperationalContext.MOTORING,
OperationalContext.ANCHORED,
OperationalContext.DOCKED
] as OperationalContextType[],
environmentalSensitivity: 0.3, // Moderately sensitive to weather
powerAware: true,
safetyImpact: true
},
advanced: {
trendAnalysis: true,
predictiveAlerts: true,
adaptiveThresholds: false,
historicalComparison: true
},
...customConfig
};
return new GPSHealthCheck(defaultConfig, gpsDataProvider);
}