UNPKG

signalk-weatherflow

Version:

SignalK plugin for WeatherFlow (Tempest) weather station data ingestion

250 lines 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WindCalculations = void 0; class WindCalculations { constructor(app, vesselName) { this.headingTrue = 0; this.headingMagnetic = 0; this.courseOverGroundMagnetic = null; this.speedOverGround = 0; this.airTemp = 0; this.humidity = 0; this.anchorSet = false; this.anchorApparentBearing = 0; this.app = app; this.vesselName = vesselName; } // Utility method to format name according to source naming rules formatSourceName(name) { return name .toLowerCase() .replace(/[^a-z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-+|-+$/g, ''); } // Utility method to get formatted vessel name for source getVesselBasedSource(suffix) { // Use configured prefix if provided, otherwise default to "zennora" const vesselPrefix = this.vesselName && this.vesselName.trim() ? this.vesselName : 'zennora'; const formattedName = this.formatSourceName(vesselPrefix); return `${formattedName}-weatherflow-${suffix}`; } // Helper function to convert degrees to radians degToRad(deg) { return (deg * Math.PI) / 180; } // Helper function to convert radians to degrees radToDeg(rad) { return (rad * 180) / Math.PI; } // Helper function: normalize angle to [-π, π] normalizeAngle(angle) { while (angle > Math.PI) angle -= 2 * Math.PI; while (angle < -Math.PI) angle += 2 * Math.PI; return angle; } // Helper function to convert atan2 result to compass bearing [0, 2π] toCompassBearing(radians) { return radians < 0 ? radians + 2 * Math.PI : radians; } // Update navigation data from SignalK updateNavigationData(path, value) { switch (path) { case 'navigation.headingTrue': this.headingTrue = value; break; case 'navigation.headingMagnetic': this.headingMagnetic = value; break; case 'navigation.courseOverGroundMagnetic': this.courseOverGroundMagnetic = value; break; case 'navigation.speedOverGround': this.speedOverGround = value; break; case 'environment.outside.tempest.observations.airTemperature': this.airTemp = value; break; case 'environment.outside.tempest.observations.relativeHumidity': this.humidity = value; break; } } // Calculate apparent wind values calculateApparentWind(windData) { // Calculate apparent wind angles and directions const headingTrueDeg = this.radToDeg(this.headingTrue); const headingMagneticDeg = this.radToDeg(this.headingMagnetic); // Wind angle - relative to bow const windAngleRelative = windData.windDirection; const windAngleRelativeRad = this.degToRad(windData.windDirection); // Wind direction - calculate absolute compass direction const apparentTrueDeg = (headingTrueDeg + windData.windDirection) % 360; const apparentMagneticDeg = (headingMagneticDeg + windData.windDirection) % 360; return { windSpeed: windData.windSpeed, windAngleRelative, windAngleRelativeRad, apparentTrueDeg, apparentMagneticDeg, apparentTrueRad: this.degToRad(apparentTrueDeg), apparentMagneticRad: this.degToRad(apparentMagneticDeg), airTemperature: windData.airTemperature || this.airTemp, }; } // Calculate derived wind values (true wind, wind chill, heat index, etc.) calculateDerivedWindValues(apparentWindData) { const timestamp = new Date().toISOString(); const source = this.getVesselBasedSource('derived'); const effectiveHeadingTrueRad = this.anchorSet ? this.anchorApparentBearing : this.headingTrue; const effectiveHeadingMagneticRad = this.anchorSet ? this.anchorApparentBearing : this.headingMagnetic; // Compute the apparent wind angle relative to the boat const angleApparent = this.normalizeAngle(apparentWindData.windAngleRelativeRad || apparentWindData.apparentTrueRad - effectiveHeadingTrueRad); // True Wind Calculation in the True Frame const effectiveSOG = this.anchorSet ? 0 : this.speedOverGround; const Vx = effectiveSOG * Math.cos(effectiveHeadingTrueRad); const Vy = effectiveSOG * Math.sin(effectiveHeadingTrueRad); const Ax = apparentWindData.windSpeed * Math.cos(apparentWindData.apparentTrueRad); const Ay = apparentWindData.windSpeed * Math.sin(apparentWindData.apparentTrueRad); const Wx = Ax + Vx; const Wy = Ay + Vy; const trueWindSpeed = Math.sqrt(Wx * Wx + Wy * Wy); const rawTrueDirection = Math.atan2(Wy, Wx); const trueWindDirTrueRad = this.toCompassBearing(rawTrueDirection); const angleTrueGround = this.normalizeAngle(trueWindDirTrueRad - effectiveHeadingTrueRad); // True Wind Calculation in the Magnetic Frame const VxMag = effectiveSOG * Math.cos(effectiveHeadingMagneticRad); const VyMag = effectiveSOG * Math.sin(effectiveHeadingMagneticRad); const AxMag = apparentWindData.windSpeed * Math.cos(apparentWindData.apparentMagneticRad); const AyMag = apparentWindData.windSpeed * Math.sin(apparentWindData.apparentMagneticRad); const WxMag = AxMag + VxMag; const WyMag = AyMag + VyMag; const rawMagneticDirection = Math.atan2(WyMag, WxMag); const trueWindDirMagRad = this.toCompassBearing(rawMagneticDirection); const angleTrueWater = this.normalizeAngle(trueWindDirMagRad - effectiveHeadingMagneticRad); // Wind Chill Calculation (K) const airTempC = this.airTemp; const windSpeedKmh = trueWindSpeed * 3.6; let windChillK = null; if (airTempC <= 10 && windSpeedKmh > 4.8) { const windChillC = 13.12 + 0.6215 * airTempC - 11.37 * Math.pow(windSpeedKmh, 0.16) + 0.3965 * airTempC * Math.pow(windSpeedKmh, 0.16); windChillK = windChillC + 273.15; } // Heat Index Calculation (K) const airTempF = (airTempC * 9) / 5 + 32; let heatIndexK = null; if (airTempF >= 80 && this.humidity >= 40) { const T = airTempF; const R = this.humidity; const heatIndexF = -42.379 + 2.04901523 * T + 10.14333127 * R - 0.22475541 * T * R - 0.00683783 * T * T - 0.05481717 * R * R + 0.00122874 * T * T * R + 0.00085282 * T * R * R - 0.00000199 * T * T * R * R; const heatIndexC = ((heatIndexF - 32) * 5) / 9; heatIndexK = heatIndexC + 273.15; } // Feels Like Calculation (K) let feelsLikeK = this.airTemp; if (windChillK !== null && airTempC <= 10) { feelsLikeK = windChillK; } else if (heatIndexK !== null && airTempC >= 27) { feelsLikeK = heatIndexK; } return { speedApparent: apparentWindData.windSpeed, angleApparent, angleTrueGround, angleTrueWater, directionTrue: trueWindDirTrueRad, directionMagnetic: trueWindDirMagRad, speedTrue: trueWindSpeed, windChill: windChillK, heatIndex: heatIndexK, feelsLike: feelsLikeK, timestamp, source, }; } // Create SignalK deltas for all wind calculations createWindDeltas(derivedValues) { const deltas = []; const windPaths = { speedApparent: 'environment.wind.speedApparent', angleApparent: 'environment.wind.angleApparent', angleTrueGround: 'environment.wind.angleTrueGround', angleTrueWater: 'environment.wind.angleTrueWater', directionTrue: 'environment.wind.directionTrue', directionMagnetic: 'environment.wind.directionMagnetic', speedTrue: 'environment.wind.speedTrue', }; const tempestPaths = { windChill: 'environment.outside.tempest.observations.windChill', heatIndex: 'environment.outside.tempest.observations.heatIndex', feelsLike: 'environment.outside.tempest.observations.feelsLike', }; // Create deltas for wind values Object.entries(windPaths).forEach(([key, path]) => { const typedKey = key; if (derivedValues[typedKey] !== undefined) { deltas.push({ context: 'vessels.self', updates: [ { $source: derivedValues.source, timestamp: derivedValues.timestamp, values: [ { path: path, value: derivedValues[typedKey], }, ], }, ], }); } }); // Create deltas for temperature-related values Object.entries(tempestPaths).forEach(([key, path]) => { const typedKey = key; if (derivedValues[typedKey] !== undefined && derivedValues[typedKey] !== null) { deltas.push({ context: 'vessels.self', updates: [ { $source: derivedValues.source, timestamp: derivedValues.timestamp, values: [ { path: path, value: derivedValues[typedKey], }, ], }, ], }); } }); return deltas; } } exports.WindCalculations = WindCalculations; //# sourceMappingURL=windCalculations.js.map