speedandcurrent 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
+ ## [2.6.0] - 2026-11-05
8
+
9
+ ### Added
10
+ - Current estimates are also published as an `environment.current` object, with magnetic set calculated from `navigation.magneticVariation` and published at `environment.current.setMagnetic`.
11
+ - The webapp now shows magnetic variation as an estimation input and magnetic current set as an output.
12
+
13
+ ### Fixed
14
+ - Magnetic variation is no longer marked idle or stale, since it may update too infrequently for telemetry staleness checks.
15
+
7
16
  ## [2.5.0] - 2026-10-26
8
17
 
9
18
  ### Added
package/README.md CHANGED
@@ -62,6 +62,7 @@ Once configured, all consumers on the Signal K bus — KIP, OpenCPN, Instrument
62
62
  | `navigation.courseOverGroundTrue` | GPS ground speed direction |
63
63
  | `navigation.headingTrue` | True heading — used to rotate the boat-frame correction into the ground frame |
64
64
  | `navigation.attitude` | Roll angle — provides the heel index into the correction table |
65
+ | `navigation.magneticVariation` | Magnetic variation — used to convert estimated current set to magnetic north |
65
66
 
66
67
  ---
67
68
 
@@ -73,6 +74,8 @@ Once configured, all consumers on the Signal K bus — KIP, OpenCPN, Instrument
73
74
  | `navigation.leewayAngle` | rad | Leeway angle (starboard positive). Derived from the lateral correction component. |
74
75
  | `environment.current.drift` | m/s | Estimated current speed. |
75
76
  | `environment.current.setTrue` | rad | Estimated current direction (the direction the water moves *toward*). |
77
+ | `environment.current.setMagnetic` | rad | Estimated current direction relative to magnetic north, calculated from `environment.current.setTrue` and `navigation.magneticVariation`. |
78
+ | `environment.current` | object | Current estimate containing `drift`, `setTrue`, and, when magnetic variation is available, `setMagnetic`. |
76
79
 
77
80
  A 60-second stabilisation period applies after startup. Corrected boatspeed may be published during this window, but current estimation and table learning wait until it ends.
78
81
 
@@ -107,6 +110,7 @@ The **Inputs** section shows live readings from the raw sensors as they arrive f
107
110
  | Boat speed | `navigation.speedThroughWater` |
108
111
  | Ground speed | `navigation.speedOverGround` + `navigation.courseOverGroundTrue` |
109
112
  | Attitude (heel) | `navigation.attitude` (roll) |
113
+ | Magnetic variation | `navigation.magneticVariation` |
110
114
 
111
115
  ### Settings
112
116
 
@@ -124,7 +128,7 @@ The **Boatspeed Estimation** section is enabled with the toggle in the panel hea
124
128
 
125
129
  The panel is organised into three groups:
126
130
 
127
- **Inputs** — the raw sensor values used for correction: heading, boat speed, ground speed, and attitude. Warnings appear here if any are unavailable.
131
+ **Inputs** — the raw signals used by the estimation calculations: heading, boat speed, ground speed, attitude, and magnetic variation. Warnings appear here if any are unavailable.
128
132
 
129
133
  **Intermediates** — computed vectors that give insight into what the plugin is doing:
130
134
  - *Speed correction* — the correction vector currently being applied (longitudinal + lateral components).
@@ -134,6 +138,7 @@ The panel is organised into three groups:
134
138
  **Outputs** — what is being published to Signal K:
135
139
  - *Corrected boatspeed / Leeway* — corrected STW magnitude and leeway angle.
136
140
  - *Current* — estimated current drift and set.
141
+ - *Magnetic set* — estimated current direction relative to magnetic north, calculated using magnetic variation.
137
142
 
138
143
  ### Settings
139
144
 
package/index.js CHANGED
@@ -295,6 +295,9 @@ module.exports = function (app) {
295
295
  let smoothedAttitude = null;
296
296
  let rawCurrent = null;
297
297
  let smoothedCurrent = null;
298
+ let magneticVariationHandler = null;
299
+ let setMagneticHandler = null;
300
+ let setMagneticPublished = false;
298
301
  let smoothedBoatSpeed = null;
299
302
  let correctedBoatSpeed = null;
300
303
  let lrnBoatSpeed = null;
@@ -684,6 +687,25 @@ module.exports = function (app) {
684
687
  // send metadata for current
685
688
  MessageHandler.setMeta(app, plugin.id, "environment.current.drift", {units: "m/s", type: "number", description: "Speed of the current"});
686
689
  MessageHandler.setMeta(app, plugin.id, "environment.current.setTrue", { units: "rad", type: "number", description: "Direction of the current" });
690
+ MessageHandler.setMeta(app, plugin.id, "environment.current", { type: "object", description: "Estimated water current" });
691
+ MessageHandler.setMeta(app, plugin.id, "environment.current.setMagnetic", {
692
+ units: "rad",
693
+ type: "number",
694
+ description: "Direction of the current relative to magnetic north",
695
+ displayName: "Magnetic set",
696
+ displayUnits: { category: "angle" }
697
+ });
698
+ magneticVariationHandler = new MessageHandler(app, plugin.id, "magneticVariation");
699
+ magneticVariationHandler.configure("navigation.magneticVariation");
700
+ magneticVariationHandler.idlePeriod = 0;
701
+ magneticVariationHandler.stalePeriod = 0;
702
+ magneticVariationHandler.onDelta = () => {
703
+ clearLifecycleWarning("magneticVariation");
704
+ publishCurrentObject();
705
+ };
706
+ magneticVariationHandler.subscribe();
707
+ setMagneticHandler = new MessageHandler(app, plugin.id, "setMagnetic");
708
+ setMagneticHandler.configure("environment.current.setMagnetic");
687
709
  rawCurrent = new Polar(app, plugin.id, "current");
688
710
  rawCurrent.configureMagnitude("environment.current.drift");
689
711
  rawCurrent.configureAngle("environment.current.setTrue");
@@ -843,6 +865,8 @@ module.exports = function (app) {
843
865
  reportFull.addPolar(residual);
844
866
  reportFull.addPolar(smoothedResidual);
845
867
  }
868
+ reportFull.addDelta(magneticVariationHandler);
869
+ reportFull.addDelta(setMagneticHandler);
846
870
  // Smoothed learning inputs are always subscribed regardless of updateCorrectionTable,
847
871
  // so always add them to the report — otherwise warnings never clear when
848
872
  // learning was disabled at startup and later toggled on.
@@ -874,12 +898,14 @@ module.exports = function (app) {
874
898
  }
875
899
  // Clear all active output paths from the SK bus before teardown.
876
900
  if (smoothedCurrent) PolarSmoother.clear(app, plugin.id, [smoothedCurrent]);
901
+ clearCurrentObjectOutputs();
877
902
  if (options.estimateBoatSpeed && correctedBoatSpeed) Polar.clear(app, plugin.id, [correctedBoatSpeed]);
878
903
  smoothedHeading = smoothedHeading?.terminate();
879
904
  smoothedAttitude = smoothedAttitude?.terminate();
880
905
  rawCurrent = rawCurrent?.terminate();
881
906
  smoothedCurrent = smoothedCurrent?.terminate?.();
882
907
  navigationStateHandler = navigationStateHandler?.terminate();
908
+ magneticVariationHandler = magneticVariationHandler?.terminate();
883
909
  smoothedBoatSpeed = smoothedBoatSpeed?.terminate();
884
910
  correctedBoatSpeed = correctedBoatSpeed?.terminate();
885
911
  lrnBoatSpeed = lrnBoatSpeed?.terminate();
@@ -895,6 +921,8 @@ module.exports = function (app) {
895
921
  noCurrent = null;
896
922
  rawBoatSpeed = null;
897
923
  rawGroundSpeed = null;
924
+ setMagneticHandler = null;
925
+ setMagneticPublished = false;
898
926
  lastNavigationStateValue = null;
899
927
  learningStabilizingUntil = 0;
900
928
  learningStabilizingReason = 'startup';
@@ -986,10 +1014,50 @@ module.exports = function (app) {
986
1014
  }
987
1015
 
988
1016
  PolarSmoother.send(app, plugin.id, [smoothedCurrent]);
1017
+ publishCurrentObject();
989
1018
 
990
1019
  Polar.send(app, plugin.id, [correctedBoatSpeed]);
991
1020
  }
992
1021
 
1022
+ function publishCurrentObject() {
1023
+ if (!options.estimateBoatSpeed || !smoothedCurrent?.ready) return;
1024
+ const drift = smoothedCurrent.magnitude;
1025
+ const setTrue = smoothedCurrent.angle;
1026
+ if (!Number.isFinite(drift) || !Number.isFinite(setTrue)) return;
1027
+
1028
+ const current = { drift, setTrue };
1029
+ const values = [{ path: "environment.current", value: current }];
1030
+ const magneticVariation = magneticVariationHandler?.value;
1031
+ if (magneticVariationHandler?.ready && Number.isFinite(magneticVariation)) {
1032
+ const setMagnetic = ((setTrue - magneticVariation) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI);
1033
+ current.setMagnetic = setMagnetic;
1034
+ setMagneticHandler.value = setMagnetic;
1035
+ values.push({ path: "environment.current.setMagnetic", value: setMagnetic });
1036
+ setMagneticPublished = true;
1037
+ } else {
1038
+ setMagneticHandler.invalidate();
1039
+ if (setMagneticPublished) {
1040
+ values.push({ path: "environment.current.setMagnetic", value: null });
1041
+ }
1042
+ setMagneticPublished = false;
1043
+ }
1044
+
1045
+ app.handleMessage(plugin.id, { updates: [{ values }] });
1046
+ }
1047
+
1048
+ function clearCurrentObjectOutputs() {
1049
+ setMagneticHandler?.invalidate();
1050
+ setMagneticPublished = false;
1051
+ app.handleMessage(plugin.id, {
1052
+ updates: [{
1053
+ values: [
1054
+ { path: "environment.current", value: null },
1055
+ { path: "environment.current.setMagnetic", value: null }
1056
+ ]
1057
+ }]
1058
+ });
1059
+ }
1060
+
993
1061
  /**
994
1062
  * Updates the correction table from the current smoothed inputs.
995
1063
  * Reads minSpeed from module-level state;
@@ -1188,6 +1256,7 @@ module.exports = function (app) {
1188
1256
  if (key === 'estimateBoatSpeed' && !value && correctedBoatSpeed) {
1189
1257
  Polar.clear(app, plugin.id, [correctedBoatSpeed]);
1190
1258
  PolarSmoother.clear(app, plugin.id, [smoothedCurrent]);
1259
+ clearCurrentObjectOutputs();
1191
1260
  }
1192
1261
 
1193
1262
  delete changedOptions[key];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speedandcurrent",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "A plugin that automatically corrects boat speed and calculates current and leeway using sensor fusion.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/public/app.js CHANGED
@@ -118,7 +118,7 @@ async function loadMeta() {
118
118
 
119
119
  const computedItemIds = new Set([
120
120
  'current.smoothed', 'boatSpeedRefGround', 'speedCorrection',
121
- 'correctedBoatSpeed', 'residual', 'residual.smoothed'
121
+ 'correctedBoatSpeed', 'residual', 'residual.smoothed', 'setMagnetic'
122
122
  ]);
123
123
 
124
124
  function getPathStatus(pathState, hasValue, computed = false, computedReady = false) {
@@ -564,6 +564,8 @@ function renderLiveSections() {
564
564
  const inputAttitudes = filterById(state.attitudesAll, ['attitude']);
565
565
  const fallbackInputPolars = inputPolars.length ? inputPolars : filterById(state.polarsAll, ['groundSpeed.smoothed']);
566
566
  const fallbackInputDeltas = inputDeltas.length ? inputDeltas : filterById(state.deltasAll, ['heading.smoothed', 'boatSpeed.smoothed']);
567
+ const magneticVariationInput = filterById(state.deltasAll, ['magneticVariation']);
568
+ fallbackInputDeltas.push(...magneticVariationInput);
567
569
  const fallbackInputAttitudes = inputAttitudes.length ? inputAttitudes : filterById(state.attitudesAll, ['attitude.smoothed']);
568
570
  const displayedInputs = [...fallbackInputPolars, ...fallbackInputDeltas, ...fallbackInputAttitudes];
569
571
 
@@ -584,7 +586,7 @@ function renderLiveSections() {
584
586
  ? [...groundSpeedInputs, ...boatSpeedInputs]
585
587
  : [...boatSpeedInputs, ...groundSpeedInputs];
586
588
  estimationInputs.push(
587
- ...filterById(state.deltasAll, ['heading.angle']),
589
+ ...filterById(state.deltasAll, ['heading.angle', 'magneticVariation']),
588
590
  ...filterById(state.attitudesAll, ['attitude'])
589
591
  );
590
592
  renderItemsInto('estimation-inputs', estimationInputs);
@@ -599,9 +601,10 @@ function renderLiveSections() {
599
601
  renderGroupInto('estimation-intermediates', estimationIntermediates, [], [], statisticRows);
600
602
  // Estimation — outputs
601
603
  const estimationOutputs = filterById(state.polarsAll, ['correctedBoatSpeed', 'current.smoothed']);
602
- renderGroupInto('estimation-outputs', estimationOutputs, [], []);
604
+ const estimationOutputDeltas = filterById(state.deltasAll, ['setMagnetic']);
605
+ renderGroupInto('estimation-outputs', estimationOutputs, estimationOutputDeltas, []);
603
606
  // Estimation — warnings
604
- renderWarnings('estimation-warnings', [...estimationInputs, ...estimationIntermediates, ...estimationOutputs]);
607
+ renderWarnings('estimation-warnings', [...estimationInputs, ...estimationIntermediates, ...estimationOutputs, ...estimationOutputDeltas]);
605
608
 
606
609
  // Learning — inputs
607
610
  const learningPolars = filterById(state.polarsAll, ['groundSpeed.smoothed']);