speedandcurrent 1.7.8 → 1.7.9
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/README.md +262 -185
- package/correctionTable.js +35 -2
- package/index.js +30 -16
- package/package.json +1 -1
- package/public/app.js +25 -0
- package/public/index.html +4 -0
package/README.md
CHANGED
|
@@ -1,208 +1,285 @@
|
|
|
1
|
-
# Speed
|
|
1
|
+
# Speed and Current — Signal K Plugin
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
This SignalK plugin estimates **corrected speed through water**, **leeway**, and **water current (drift)** in real-time using adaptive sensor fusion and statistical filtering. It continuously refines its estimates based on vessel speed, heading, ground speed, and heel angle. Its primary goal is to significantly improve paddle wheel speed (navigation.speedThroughWater), where raw errors of 10% or more are common. It does this automatically from real-world observations—no manual calibration required. Leeway (navigation.leewayAngle) is derived from observed dynamics rather than formulas, and current is estimated only after speed is corrected, using a slowly adapting filter.
|
|
3
|
+
## What it does
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
1. Install / enable the plugin in the Signal K Server admin UI.
|
|
8
|
-
2. Open the plugin webapp (Signal K: Apps → Speed and Current).
|
|
9
|
-
3. Go to **Correction Table** and click **New** to create your first correction table (or use the default that was created for you).
|
|
10
|
-
4. Start learning by enabling **Correction Table Learning** in the webapp.
|
|
11
|
-
5. Once your correction table is filled enable **Boatspeed Estimation** in the webapp.
|
|
12
|
-
6. Watch the corrected speed, leeway, and current fields begin populating as data arrives. Initial stabilization may take some sailing at varying speeds and heel angles.
|
|
5
|
+
Most paddle wheel logs carry a systematic error that varies with boat speed and heel angle. A boat heeled at 20° with a fouled bottom at 6 knots may read 5% low; the same boat upright at 3 knots may read 2% high. Manual calibration with a single factor misses this structure entirely.
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
The plugin automatically starts collecting and learning once enabled, but waits the first ~60 seconds before producing estimates to stabilize.
|
|
7
|
+
This plugin builds and maintains a **2-dimensional correction table** — indexed by speed and heel — that is populated automatically from real sailing observations. No calibration runs, no spreadsheets, no manual entry. Once the table has enough coverage the plugin applies per-point corrections to `navigation.speedThroughWater`, derives leeway from the lateral component of the correction, and estimates water current by subtracting the corrected boat speed vector from the GPS ground speed vector.
|
|
16
8
|
|
|
17
|
-
|
|
18
|
-
- navigation.speedThroughWater (raw paddle wheel speed)
|
|
19
|
-
- navigation.speedOverGround
|
|
20
|
-
- navigation.courseOverGroundTrue (true)
|
|
21
|
-
- navigation.headingTrue (true)
|
|
22
|
-
- navigation.attitude
|
|
9
|
+
In broad terms the plugin:
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
- corrects paddle wheel speed using a learned, heel- and speed-aware correction table
|
|
12
|
+
- estimates leeway angle from the observed lateral correction
|
|
13
|
+
- estimates water current drift and set
|
|
14
|
+
- continuously refines all three as you sail
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install the plugin from the Signal K App Store, or manually by placing it in your Signal K plugin directory and running `npm install`. After installation:
|
|
21
|
+
|
|
22
|
+
1. Enable the plugin in the Signal K Server admin UI (**Server → Plugin Config → Speed and Current → Active**).
|
|
23
|
+
2. Open the plugin webapp from the Signal K app list (**Apps → Speed and Current**).
|
|
24
|
+
3. The plugin creates a default correction table on first start. You do not need to create one manually.
|
|
25
|
+
4. Enable **Update Correction Table** to start learning.
|
|
26
|
+
5. After the table has some coverage, enable **Estimate Boat Speed**.
|
|
27
|
+
|
|
28
|
+
The plugin requires no further configuration in the Signal K admin UI. All settings are managed from its own webapp.
|
|
29
|
+
|
|
30
|
+
---
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
- When the setting "Prevent speed duplication" = true (default): plugin overwrites navigation.speedThroughWater with corrected value.
|
|
36
|
-
- When false: both raw and corrected updates exist under navigation.speedThroughWater with distinct source attributes (viewable in deltas / data browser) allowing comparison.
|
|
32
|
+
## Required Signal K paths
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
| Path | Role |
|
|
35
|
+
|------|------|
|
|
36
|
+
| `navigation.speedThroughWater` | Raw paddle wheel speed — the signal being corrected |
|
|
37
|
+
| `navigation.speedOverGround` | GPS ground speed magnitude |
|
|
38
|
+
| `navigation.courseOverGroundTrue` | GPS ground speed direction |
|
|
39
|
+
| `navigation.headingTrue` | True heading — used to rotate the boat-frame correction into the ground frame |
|
|
40
|
+
| `navigation.attitude` | Roll angle — provides the heel index into the correction table |
|
|
39
41
|
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Output paths
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
| Path | Unit | Description |
|
|
47
|
+
|------|------|-------------|
|
|
48
|
+
| `navigation.speedThroughWater` | m/s | Corrected speed through water. Source attribute identifies it as the plugin output. |
|
|
49
|
+
| `navigation.leewayAngle` | rad | Leeway angle (starboard positive). Derived from the lateral correction component. |
|
|
50
|
+
| `environment.current.drift` | m/s | Estimated current speed. |
|
|
51
|
+
| `environment.current.setTrue` | rad | Estimated current direction (the direction the water moves *toward*). |
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
A 60-second stabilisation period applies after startup. No output is published during this window; the status bar shows **Stabilizing**.
|
|
54
|
+
|
|
55
|
+
---
|
|
45
56
|
|
|
46
|
-
##
|
|
47
|
-
All settings are managed from the plugin webapp. The Signal K admin UI is not used for configuration.
|
|
57
|
+
## The webapp
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
| Setting | Default | Description | When to Change |
|
|
51
|
-
|---------|---------|-------------|----------------|
|
|
52
|
-
| Estimate Boat Speed | Off | Enables speed correction, leeway and current estimation. | Turn on after creating a correction table. |
|
|
53
|
-
| Update Correction Table | On | Learns / refines correction table. | Temporarily disable in abnormal conditions (heavy seas, abnormal current). |
|
|
59
|
+
The plugin is configured and monitored through its own webapp (Signal K Apps → **Speed and Current**). The sidebar has four sections: **Inputs**, **Boatspeed Estimation**, **Correction Table Learning**, and **Correction Table**. Settings changes take effect immediately without restarting.
|
|
54
60
|
|
|
55
|
-
###
|
|
56
|
-
| Setting | Default | Description | Guidance |
|
|
57
|
-
|---------|---------|-------------|----------|
|
|
58
|
-
| Correction Table Stability | 7 | Higher = slower changes (more conservative). | Increase once table broadly populated; lower briefly if you think the sensor error has changed. |
|
|
59
|
-
| Assume Current (experimental) | Off | Learns table while assuming a current may be present. | Enable cautiously on tidal waters. |
|
|
60
|
-
| Show Statistics | Off | Displays σ (standard deviation) next to smoothed values. | Enable for debugging or to inspect signal quality. |
|
|
61
|
+
### Warning indicators
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
Whenever a required signal is not available, the relevant section shows a **Warnings** panel listing each affected input and why:
|
|
64
|
+
|
|
65
|
+
| Reason | Meaning |
|
|
66
|
+
|--------|---------|
|
|
67
|
+
| *not subscribed to Signal K* | Subscription did not succeed. Try restarting the plugin. |
|
|
68
|
+
| *path not found in Signal K* | No device is publishing this path. Check instrument connections and your multiplexer configuration. |
|
|
69
|
+
| *waiting for first data* | The path is known but no value has arrived since the plugin started. Normal for a few seconds at startup. |
|
|
70
|
+
| *data is stale* | Data was arriving but has stopped. The instrument may have gone offline, or its update rate has dropped below the staleness threshold. |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Inputs
|
|
75
|
+
|
|
76
|
+
The **Inputs** section shows live readings from the raw sensors as they arrive from Signal K, before any smoothing or correction. This is a useful first stop when diagnosing instrument problems.
|
|
77
|
+
|
|
78
|
+
### Live values
|
|
79
|
+
|
|
80
|
+
| Value | Signal K path |
|
|
81
|
+
|-------|--------------|
|
|
82
|
+
| Heading | `navigation.headingTrue` |
|
|
83
|
+
| Boat speed | `navigation.speedThroughWater` |
|
|
84
|
+
| Ground speed | `navigation.speedOverGround` + `navigation.courseOverGroundTrue` |
|
|
85
|
+
| Attitude (heel) | `navigation.attitude` (roll) |
|
|
86
|
+
|
|
87
|
+
### Source selection
|
|
88
|
+
|
|
89
|
+
For each path you can pin the plugin to a specific Signal K source. This is useful when multiple devices publish the same path — for example two GPS receivers, or a fluxgate and a satellite compass both publishing `navigation.headingTrue`. Leave a source blank or select **(any)** to accept values from all sources. This is independent of the Signal K server's global source priority settings.
|
|
90
|
+
|
|
91
|
+
### Settings
|
|
64
92
|
|
|
65
93
|
| Setting | Default | Description |
|
|
66
94
|
|---------|---------|-------------|
|
|
67
|
-
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
###
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
- **Settings**: Stability, Assume Current, and Show Statistics (σ display toggle).
|
|
136
|
-
- **Smoother**: Type selector (Moving average / Exponential / Kalman) and its single tuning parameter. Only the parameter relevant to the selected smoother is shown.
|
|
137
|
-
- **Inputs**: Smoothed values fed into the learning algorithm. When Show Statistics is on, σ values are shown.
|
|
138
|
-
|
|
139
|
-
### Correction Table
|
|
140
|
-
The **Correction Table** section shows the current correction table grid. Table management buttons appear top-right:
|
|
141
|
-
- **New** — create a new table, setting its name and grid dimensions.
|
|
142
|
-
- **Load** — switch to a previously saved table.
|
|
143
|
-
- **Copy** — duplicate the current table under a new name.
|
|
144
|
-
- **Resize** — resize the grid (existing cells are mapped to the new grid; some data may be lost).
|
|
145
|
-
|
|
146
|
-
Each populated cell shows:
|
|
147
|
-
- **Factor** (±%): how much the paddle wheel over- or under-reads at this speed and heel (green = reads slow, orange = reads fast).
|
|
148
|
-
- **Leeway** (°): the estimated sideways drift angle; also encoded visually as the stripe direction in the cell background.
|
|
149
|
-
|
|
150
|
-
Cell highlighting:
|
|
151
|
-
- **Blue values**: the most recently updated table cell (factor and leeway text rendered in blue).
|
|
152
|
-
- **Thin border**: cells contributing to the current interpolation.
|
|
153
|
-
- **No content**: not yet populated by learning.
|
|
154
|
-
|
|
155
|
-
### Correction Table Health Checklist
|
|
156
|
-
- Leeway ≈ 0 at zero heel; increases logically with heel. Non-zero leeway at zero heel may indicate a heading offset.
|
|
157
|
-
- Speed corrections should trend consistently with adjacent cells (direction & magnitude smooth across the table).
|
|
158
|
-
- Port / starboard symmetry: similar magnitudes at ± matching heel angles. Large asymmetry can indicate a paddle wheel mounting offset.
|
|
159
|
-
|
|
160
|
-
### Interpreting Live Values
|
|
161
|
-
- **Corrected Speed vs Raw**: Stable, plausible separation between the two indicates the table is working.
|
|
162
|
-
- **Corrected speed** includes leeway and therefore is not aligned with the ship's axis.
|
|
163
|
-
- **Residual**: Remaining ground-speed discrepancy after applying corrected STW and estimated current. Should trend toward small values as learning progresses.
|
|
164
|
-
- **Trace**: Uncertainty indicator derived from internal variance (higher = less reliable). Shown alongside most values.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
## How It Works
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
### 1. Speed Correction
|
|
172
|
-
- Vessel speed errors depend on **heel** and **speed** and are treated as locally constant within grid cells.
|
|
173
|
-
- The correction table stores a vector (magnitude & direction of sensor error) per (speed, heel) cell.
|
|
174
|
-
- A correction vector is computed by weighting (interpolation) of the nearest populated cells.
|
|
175
|
-
- The corrected speed-through-water vector = raw STW vector + correction vector (sign chosen so magnitude bias is reduced).
|
|
176
|
-
|
|
177
|
-
### 2. Leeway Estimation
|
|
178
|
-
- Leeway is derived from the angular difference between the corrected STW direction and the hull axis / heading reference.
|
|
179
|
-
|
|
180
|
-
### 3. Current Estimation
|
|
181
|
-
- The water current is estimated as a **slowly changing** ground-plane vector.
|
|
182
|
-
- A Kalman filter updates the current using residual between ground speed (GPS) and the heading-rotated corrected boat speed, weighted by estimated variances.
|
|
183
|
-
- 
|
|
184
|
-
|
|
185
|
-
### 4. Correction Estimating
|
|
186
|
-
- Each populated cell maintains its own lightweight Kalman filter.
|
|
187
|
-
- For each incoming raw boat speed sample the instantaneous sensor error candidate is derived from boat speed, heel, heading, ground speed and (optionally) current.
|
|
188
|
-
- That error sample updates the cell’s Kalman state (mean correction vector + variance).
|
|
189
|
-
- Sensor and estimation variances are continuously updated; the displayed Trace in the webApp reflects confidence (higher = less reliable).
|
|
190
|
-
- 
|
|
191
|
-
|
|
192
|
-
## Glossary
|
|
193
|
-
- Set: Direction the water current flows toward (true angle).
|
|
194
|
-
- Drift: Magnitude (speed) of the water current over ground.
|
|
195
|
-
- Leeway: Lateral angular difference between boat’s heading axis and its actual movement through water (starboard positive).
|
|
196
|
-
- Residual: Remaining ground-speed vector difference after applying corrected STW and estimated current; indicator of modeling error and sensor noise.
|
|
197
|
-
- Stability (setting): Damps rate of change in learned corrections; higher = slower adaptation.
|
|
198
|
-
- Trace: Reported uncertainty indicator derived from internal variance (higher = less confidence in that vector/cell).
|
|
199
|
-
- Kalman filter: An adaptive algorithm that estimates the true value of a variable (such as speed or current) by combining noisy measurements and prior estimates, updating both the value and its uncertainty over time. Used here to refine correction vectors (cell-by-cell) and current estimates .
|
|
200
|
-
|
|
201
|
-
### Coordinate Frames
|
|
202
|
-
- Boat Plane: Vectors relative to hull axis (used for raw & corrected STW, leeway angle computation).
|
|
203
|
-
- Ground Plane: Vectors in earth-referenced frame (used for GPS ground speed, current, residual).
|
|
95
|
+
| **Staleness Detection** | On | Mark an input as unavailable when it stops updating. Disable when testing the plugin with simulated or replayed data, where timing is irregular and would otherwise trigger spurious warnings. |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Boatspeed Estimation
|
|
100
|
+
|
|
101
|
+
The **Boatspeed Estimation** section is enabled with the toggle in the panel header. When off, no corrected output is published; the raw paddle wheel value passes through to the SK bus unchanged (subject to the Prevent Speed Duplication setting).
|
|
102
|
+
|
|
103
|
+
### What it shows
|
|
104
|
+
|
|
105
|
+
The panel is organised into three groups:
|
|
106
|
+
|
|
107
|
+
**Inputs** — the raw sensor values used for correction: heading, boat speed, ground speed, and attitude. Warnings appear here if any are unavailable.
|
|
108
|
+
|
|
109
|
+
**Intermediates** — computed vectors that give insight into what the plugin is doing:
|
|
110
|
+
- *Speed correction* — the correction vector currently being applied (longitudinal + lateral components).
|
|
111
|
+
- *Boat speed over ground* — the corrected STW vector rotated into the ground frame using heading.
|
|
112
|
+
- *Residual* and *Smoothed residual* — the difference between ground speed and (corrected boat speed + estimated current). Ideally near zero; a persistent non-zero residual indicates remaining systematic error or a current the plugin has not yet picked up.
|
|
113
|
+
|
|
114
|
+
**Outputs** — what is being published to Signal K:
|
|
115
|
+
- *Corrected boatspeed / Leeway* — corrected STW magnitude and leeway angle.
|
|
116
|
+
- *Current* — estimated current drift and set.
|
|
117
|
+
|
|
118
|
+
### Settings
|
|
119
|
+
|
|
120
|
+
| Setting | Default | Description |
|
|
121
|
+
|---------|---------|-------------|
|
|
122
|
+
| **Estimate Boat Speed** | Off | Master toggle. Apply the correction table and publish corrected STW, leeway, and current. Turn on once the table has reasonable coverage. |
|
|
123
|
+
| **Prevent Speed Duplication** | On | Replace the raw sensor value on the SK bus with the corrected value. When off, both raw and corrected exist under `navigation.speedThroughWater` with different source attributes — useful for comparing before and after. |
|
|
124
|
+
| **Groundspeed Fallback** | Off | When the paddle wheel reads zero and SOG is above a minimum threshold, publish SOG as boatspeed. Primarily intended for clogged or stuck paddle wheels where the instrument is physically present but not turning — giving the rest of the instrument system a usable boat speed until the wheel is cleared. |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Correction Table Learning
|
|
129
|
+
|
|
130
|
+
The **Correction Table Learning** section has its own independent toggle. Learning and estimation are decoupled: you can learn without estimating (building the table during a passage before trusting it), and you can estimate without learning (freezing the table once you are satisfied with it).
|
|
131
|
+
|
|
132
|
+
### What it shows
|
|
133
|
+
|
|
134
|
+
The panel shows the **smoothed** sensor inputs used for table updates — heading, boat speed, ground speed, and attitude. These are the same signals as in the Estimation panel, but averaged over the smoother window before being fed into the learning algorithm. This averaging reduces the influence of short-term fluctuations on the table update.
|
|
135
|
+
|
|
136
|
+
When **Assume Current** is enabled, the smoothed current estimate is also shown as a learning input.
|
|
137
|
+
|
|
138
|
+
Warnings appear here if any smoothed input is unavailable.
|
|
139
|
+
|
|
140
|
+
### Settings
|
|
141
|
+
|
|
142
|
+
| Setting | Default | Description |
|
|
143
|
+
|---------|---------|-------------|
|
|
144
|
+
| **Update Correction Table** | On | Master toggle. Allow the table to update from current observations. Disable temporarily in conditions you do not want recorded — motoring, surf sailing, very confused seas. |
|
|
145
|
+
| **Stability (1–20)** | 7 | How quickly the correction table adapts to new observations. Higher = slower, more conservative. Lower = faster but noisier. See the technical section for detail. |
|
|
146
|
+
| **Assume Current (experimental)** | Off | Include the running current estimate in the table update calculation. Only enable once the current estimate has had time to stabilise and tidal conditions are relatively steady. |
|
|
147
|
+
| **Show Statistics (σ)** | Off | Display standard deviation alongside each smoothed value. Useful for spotting noisy sensors. |
|
|
148
|
+
|
|
149
|
+
### Smoother settings
|
|
150
|
+
|
|
151
|
+
Controls how raw sensor values are averaged before being used for table updates. These settings have no effect on the published corrected values — only on the inputs to the learning algorithm.
|
|
152
|
+
|
|
153
|
+
The smoother serves a second purpose beyond noise reduction: its output variance tells the plugin how much to trust each observation. A tight, stable signal (low variance) produces an observation that is weighted heavily in the Kalman update. A signal that has been varying rapidly — because the boat is turning, the sails are flogging, or conditions are rough — has high variance, and the resulting observation is trusted less and moves the cell estimate by a smaller amount. The smoother therefore acts as an automatic quality gate: the table learns most from steady, settled sailing.
|
|
154
|
+
|
|
155
|
+
| Setting | Default | Description |
|
|
156
|
+
|---------|---------|-------------|
|
|
157
|
+
| **Smoother Type** | Moving average | Moving Average (window), Exponential decay (τ), or Kalman filter. Moving average is the most predictable. |
|
|
158
|
+
| **Window size** | 5 s | (Moving average) Integration window. Larger = smoother but slower to respond. Minimum 2 s. |
|
|
159
|
+
| **Time constant (τ)** | 3 s | (Exponential) Decay time constant. Larger = smoother. Minimum 1 s. |
|
|
160
|
+
| **Kalman gain** | 0.2 | (Kalman) Steady-state gain. Lower = smoother / slower. Range 0.01–0.99. |
|
|
161
|
+
|
|
162
|
+
A 5-second moving average window suits typical 1 Hz instrument update rates. In rough conditions with high sensor noise, increasing it to 8–10 seconds may improve table quality at the cost of temporal resolution.
|
|
204
163
|
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Correction Table
|
|
167
|
+
|
|
168
|
+
The **Correction Table** section displays the active correction table. Rows are speed bins, columns are heel bins — port heel on the left (negative values), starboard on the right (positive values).
|
|
169
|
+
|
|
170
|
+
### Reading the table
|
|
171
|
+
|
|
172
|
+
Each cell that has received at least one observation shows two values:
|
|
173
|
+
|
|
174
|
+
- **Speed factor** (e.g. `+3.2%` or `−1.5%`): how much faster or slower the true speed is compared to what the paddle wheel reads. **Green** = paddle wheel reads slow (the correction adds speed). **Orange** = paddle wheel reads fast (the correction reduces speed). Colour intensity scales with the magnitude of the factor relative to the largest factor in the table.
|
|
175
|
+
- **Leeway** (e.g. `+4°`): the observed lateral correction for that speed/heel combination. Positive is starboard. When leeway is non-zero the cell background shows diagonal stripes; the stripe angle encodes the leeway direction visually.
|
|
176
|
+
|
|
177
|
+
Empty cells have not yet received any observations and show no correction.
|
|
178
|
+
|
|
179
|
+
### Active cell and neighbours
|
|
180
|
+
|
|
181
|
+
The **active cell** — the one most recently updated by an incoming STW sample — is highlighted with a bold border and blue text. The **neighbour cells** contributing to the current interpolation are shown with a faint tint. Watching these as you sail shows exactly which part of the table is being applied and updated at any moment.
|
|
182
|
+
|
|
183
|
+
### Table management
|
|
184
|
+
|
|
185
|
+
| Action | Description |
|
|
186
|
+
|--------|-------------|
|
|
187
|
+
| **New** | Create a new table with specified speed and heel dimensions. The current table is replaced. |
|
|
188
|
+
| **Load** | Switch to a previously saved table. Tables are stored as JSON files in the plugin's data directory. |
|
|
189
|
+
| **Copy** | Save the current table under a new name without modifying it. |
|
|
190
|
+
| **Resize** | Change the speed/heel range or step size. Existing cell values are resampled onto the new grid — nothing is lost, but resampled cells benefit from a few more observations to consolidate on the new grid. |
|
|
191
|
+
|
|
192
|
+
Multiple tables can coexist on disk; only the active one is used. This makes it straightforward to keep separate tables for different configurations such as racing vs. cruising sails, or before and after antifouling.
|
|
205
193
|
|
|
206
194
|
---
|
|
207
195
|
|
|
196
|
+
## Operating notes
|
|
197
|
+
|
|
198
|
+
**Be patient with the correction table.** A fresh table has no data and produces no corrections. Cover a range of speeds and heel angles over a few sails and the table fills in progressively. Upwind sailing covers the heel bins well; downwind and reaching fill the low-heel, varying-speed bins.
|
|
199
|
+
|
|
200
|
+
**The table learns while sailing normally.** No dedicated calibration runs are needed. Just sail with **Update Correction Table** on.
|
|
201
|
+
|
|
202
|
+
**Port and starboard are tracked independently.** Heel is signed: starboard positive, port negative. An asymmetric paddle wheel installation will show different corrections on each tack, and the table captures this naturally.
|
|
203
|
+
|
|
204
|
+
**Corrections improve in context.** A correction derived from rough seas with high GPS variance receives a small Kalman gain and moves the cell estimate less than a correction from flat water at steady speed. The table naturally weights calm, steady observations more heavily.
|
|
205
|
+
|
|
206
|
+
**Resizing the table is non-destructive.** The Resize function resamples existing cell values onto the new grid. Nothing is lost — resampled cells just need a few more observations to consolidate at the new resolution.
|
|
207
|
+
|
|
208
|
+
**Current estimation lags behind reality.** The slow Kalman smoother is intentional — it prevents GPS noise and short-term manoeuvres from corrupting the estimate. In rapidly changing tidal conditions the estimate will lag the actual current by several minutes. For precise tidal navigation use an independent current source.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## How it works — technical detail
|
|
213
|
+
|
|
214
|
+
### How corrections are applied
|
|
215
|
+
|
|
216
|
+
Each cell in the correction table holds a 2-dimensional correction vector **[x, y]** in the boat frame:
|
|
217
|
+
- **x** is the longitudinal component (along the centreline, positive = forward). This is the main speed error.
|
|
218
|
+
- **y** is the lateral component (positive = starboard). This becomes the leeway estimate.
|
|
219
|
+
|
|
220
|
+
When a new paddle wheel sample arrives the plugin:
|
|
221
|
+
1. Looks up the current speed and heel.
|
|
222
|
+
2. Retrieves an interpolated correction vector from the table.
|
|
223
|
+
3. Adds that vector to the raw STW vector.
|
|
224
|
+
4. The corrected magnitude is published as `navigation.speedThroughWater`; the lateral component divided by the corrected forward speed gives `navigation.leewayAngle`.
|
|
225
|
+
|
|
226
|
+
### Neighbour interpolation
|
|
227
|
+
|
|
228
|
+
Rather than bi-linear interpolation the plugin uses **inverse-distance weighting** over the five nearest cells in speed/heel space:
|
|
229
|
+
|
|
230
|
+
$$w_i = \frac{1}{d_i + \epsilon}$$
|
|
231
|
+
|
|
232
|
+
where $d_i$ is the Euclidean distance from the query point to cell centre $i$ in normalised speed/heel space. Only cells that have received at least one observation ($N > 0$) contribute.
|
|
233
|
+
|
|
234
|
+
The final correction is the normalised weighted average:
|
|
235
|
+
|
|
236
|
+
$$\hat{x} = \frac{\sum_i w_i \cdot x_i}{\sum_i w_i}, \quad \hat{y} = \frac{\sum_i w_i \cdot y_i}{\sum_i w_i}$$
|
|
237
|
+
|
|
238
|
+
The variance of each axis is propagated through the same weighting, so cells with low confidence (high covariance) influence the result less. If only one or two cells have data, the correction extrapolates gradually to nearby conditions. As more cells fill in, the correction becomes tighter and more local.
|
|
239
|
+
|
|
240
|
+
### How the correction table is populated
|
|
241
|
+
|
|
242
|
+
Each cell is an independent **2-dimensional Kalman filter** tracking the correction vector $[x, y]$ for that speed/heel bin, with a 2×2 covariance matrix expressing confidence in each axis.
|
|
243
|
+
|
|
244
|
+
Every time conditions are right — plugin running for >60 seconds, smoothers settled, speed above minimum threshold — the plugin computes an **observation** of what the correction should be:
|
|
245
|
+
|
|
246
|
+
$$\text{observation} = R(\psi)^{-1} \cdot \vec{V}_{SOG} - R(\psi)^{-1} \cdot \vec{V}_{current} - \vec{V}_{STW}$$
|
|
247
|
+
|
|
248
|
+
where $R(\psi)$ rotates from ground frame to boat frame using true heading $\psi$. In plain terms: rotate GPS velocity into the boat frame, subtract the current estimate (also rotated), subtract the raw paddle wheel velocity. The residual is the implied sensor error for the current speed and heel.
|
|
249
|
+
|
|
250
|
+
The Kalman update combines this observation with the cell's existing belief:
|
|
251
|
+
|
|
252
|
+
$$K = P \cdot (P + R_{obs})^{-1}$$
|
|
253
|
+
$$x_{new} = x_{old} + K \cdot (\text{observation} - x_{old})$$
|
|
254
|
+
|
|
255
|
+
where $P$ is the cell's current covariance and $R_{obs}$ is the observation covariance derived from the measurement uncertainty of all contributing signals (SOG variance + current variance + STW variance, rotated appropriately). **Noisy observations produce a smaller gain and move the cell estimate less.**
|
|
256
|
+
|
|
257
|
+
### The stability setting in detail
|
|
258
|
+
|
|
259
|
+
Each cell has a small **process noise** that allows it to drift slowly over time, reflecting that a paddle wheel's error can change with fouling, recalibration, or crew weight distribution. Process noise is:
|
|
260
|
+
|
|
261
|
+
$$Q = 10^{-\text{stability}}$$
|
|
262
|
+
|
|
263
|
+
With stability = 7 (default), $Q = 10^{-7}$, making the cell very resistant to change — it effectively averages hundreds of observations before settling. Stability 4–5 makes the table react faster to recent conditions; stability 10–12 is appropriate for a well-characterised boat that changes rarely.
|
|
264
|
+
|
|
265
|
+
In practical terms: high stability = trust the accumulated history; low stability = trust recent observations more.
|
|
266
|
+
|
|
267
|
+
### How current is estimated
|
|
268
|
+
|
|
269
|
+
Current is estimated as:
|
|
270
|
+
|
|
271
|
+
$$\vec{V}_{current} = \vec{V}_{SOG} - R(\psi) \cdot \vec{V}_{STW, corrected}$$
|
|
272
|
+
|
|
273
|
+
The corrected STW vector is rotated into the ground frame using heading, then subtracted from the GPS velocity. The residual is the water velocity.
|
|
274
|
+
|
|
275
|
+
This raw estimate is fed into a **Kalman smoother with very low process noise** (process variance $\approx 10^{-6}$), so it changes very slowly, integrating over many minutes rather than chasing individual GPS fluctuations. At startup the estimate is strongly initialised to zero.
|
|
276
|
+
|
|
277
|
+
Current estimation requires an accurate boat speed, so it is gated by the 60-second stabilisation period and only runs when **Estimate Boat Speed** is enabled.
|
|
278
|
+
|
|
279
|
+
### Current and table learning
|
|
280
|
+
|
|
281
|
+
Current estimation always runs alongside speed correction — it is a direct byproduct of comparing the GPS velocity with the corrected STW vector. You cannot have one without the other.
|
|
282
|
+
|
|
283
|
+
For table *learning*, however, current can be left out of the equation without significant harm. When a boat sails different headings over time — tacking, gybing, reaching — any steady current appears as an error in one direction on one heading and the opposite direction on another. These errors cancel in the long-term average, so the table converges on the correct speed correction regardless. This is why the default (**Assume Current** off) uses a zero-current placeholder rather than the live estimate.
|
|
208
284
|
|
|
285
|
+
Including the current estimate (**Assume Current** on) would be more accurate in principle, but it introduces a dependency: the current estimate is only as good as the speed correction that produced it. Early in the table's life, when corrections are rough, the current estimate is also rough, and feeding it back into learning can amplify rather than reduce error. There is also a circularity: better speed correction → better current estimate → better speed correction. Enabling this before the table has had time to settle can cause the two to pull each other in the wrong direction, particularly in changing tidal conditions. Use it only once the table is reasonably well populated and current conditions are stable.
|
package/correctionTable.js
CHANGED
|
@@ -82,13 +82,15 @@ class CorrectionTable extends Table2D{
|
|
|
82
82
|
constructor(id, row, col, stability=5) {
|
|
83
83
|
super(id, row, col, CorrectionEstimator, CorrectionEstimator.getFilterModel(stability));
|
|
84
84
|
this.lastUpdatedCell = null;
|
|
85
|
+
this.lastUpdateResult = null;
|
|
85
86
|
this.neighbours = [];
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
update(speed, heel, groundSpeed, current, boatSpeed, heading) {
|
|
89
90
|
const cell = this.getCell(speed, heel);
|
|
90
|
-
cell?.update(groundSpeed, current, boatSpeed, heading);
|
|
91
|
+
const accepted = cell?.update(groundSpeed, current, boatSpeed, heading);
|
|
91
92
|
this.lastUpdatedCell = cell;
|
|
93
|
+
this.lastUpdateResult = accepted === true ? 'accepted' : 'rejected';
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
getCorrection(speed, heel) {
|
|
@@ -177,7 +179,8 @@ class CorrectionTable extends Table2D{
|
|
|
177
179
|
return cellReport;
|
|
178
180
|
})
|
|
179
181
|
),
|
|
180
|
-
displayAttributes: this.displayAttributes
|
|
182
|
+
displayAttributes: this.displayAttributes,
|
|
183
|
+
lastUpdateResult: this.lastUpdateResult ?? null
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
|
|
@@ -260,6 +263,36 @@ class CorrectionEstimator {
|
|
|
260
263
|
groundCov[1][0] + currentCov[1][0] + boatCov[1][0],
|
|
261
264
|
groundCov[1][1] + currentCov[1][1] + boatCov[1][1]],
|
|
262
265
|
];
|
|
266
|
+
// Mahalanobis distance check.
|
|
267
|
+
// For empty cells (filterState === null) we use a diffuse prior — mean (0,0),
|
|
268
|
+
// variance DIFFUSE_PRIOR_VAR — expressing "assume no correction needed, but
|
|
269
|
+
// with high uncertainty". This gates the very first observation instead of
|
|
270
|
+
// accepting it unconditionally, preventing a single outlier from locking a
|
|
271
|
+
// cell at a bad value.
|
|
272
|
+
const DIFFUSE_PRIOR_VAR = 1.0; // (m/s)² — rejects corrections > ~3 m/s from zero
|
|
273
|
+
const priorMean = this.filterState !== null
|
|
274
|
+
? [this.filterState.mean[0][0], this.filterState.mean[1][0]]
|
|
275
|
+
: [0, 0];
|
|
276
|
+
const priorCov = this.filterState !== null
|
|
277
|
+
? this.filterState.covariance
|
|
278
|
+
: [[DIFFUSE_PRIOR_VAR, 0], [0, DIFFUSE_PRIOR_VAR]];
|
|
279
|
+
const inno = [observation[0] - priorMean[0], observation[1] - priorMean[1]];
|
|
280
|
+
const S = [
|
|
281
|
+
[priorCov[0][0] + observationCovariance[0][0],
|
|
282
|
+
priorCov[0][1] + observationCovariance[0][1]],
|
|
283
|
+
[priorCov[1][0] + observationCovariance[1][0],
|
|
284
|
+
priorCov[1][1] + observationCovariance[1][1]]
|
|
285
|
+
];
|
|
286
|
+
const det = S[0][0] * S[1][1] - S[0][1] * S[1][0];
|
|
287
|
+
if (Number.isFinite(det) && det > 1e-12) {
|
|
288
|
+
const Sinv = [
|
|
289
|
+
[ S[1][1] / det, -S[0][1] / det],
|
|
290
|
+
[-S[1][0] / det, S[0][0] / det]
|
|
291
|
+
];
|
|
292
|
+
const d2 = inno[0] * (Sinv[0][0] * inno[0] + Sinv[0][1] * inno[1])
|
|
293
|
+
+ inno[1] * (Sinv[1][0] * inno[0] + Sinv[1][1] * inno[1]);
|
|
294
|
+
if (d2 > 9.21) return false;
|
|
295
|
+
}
|
|
263
296
|
this.filterState = this.filter.filter({ previousCorrected: this.filterState, observation, observationCovariance });
|
|
264
297
|
return true;
|
|
265
298
|
}
|
package/index.js
CHANGED
|
@@ -447,14 +447,17 @@ module.exports = function (app) {
|
|
|
447
447
|
reportFull.addPolar(residual);
|
|
448
448
|
reportFull.addPolar(smoothedResidual);
|
|
449
449
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
450
|
+
// Smoothed learning inputs are always subscribed regardless of updateCorrectionTable,
|
|
451
|
+
// so always add them to the report — otherwise warnings never clear when
|
|
452
|
+
// learning was disabled at startup and later toggled on.
|
|
453
|
+
reportFull.addDelta(smoothedHeading);
|
|
454
|
+
reportFull.addAttitude(smoothedAttitude);
|
|
455
|
+
reportFull.addDelta(smoothedBoatSpeed);
|
|
456
|
+
reportFull.addPolar(smoothedGroundSpeed);
|
|
457
|
+
// smoothedCurrent is already added by the estimateBoatSpeed block when that is on;
|
|
458
|
+
// only add it here when estimateBoatSpeed is off, to avoid a duplicate entry.
|
|
459
|
+
if (options.assumeCurrent && !options.estimateBoatSpeed) {
|
|
460
|
+
reportFull.addPolar(smoothedCurrent);
|
|
458
461
|
}
|
|
459
462
|
reportFull.addTable(table);
|
|
460
463
|
|
|
@@ -486,14 +489,20 @@ module.exports = function (app) {
|
|
|
486
489
|
|
|
487
490
|
setStatus(wellUnderway ? 'Running' : 'Stabilizing');
|
|
488
491
|
if (options.estimateBoatSpeed) correct(wellUnderway);
|
|
489
|
-
if (options.updateCorrectionTable
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
if (options.updateCorrectionTable) {
|
|
493
|
+
if (wellUnderway) {
|
|
494
|
+
updateTable();
|
|
495
|
+
// Save correction table periodically
|
|
496
|
+
const now = new Date();
|
|
497
|
+
if (now - lastSave > 60 * 1000) {
|
|
498
|
+
saveTable(table, path.join(app.getDataDirPath(), table.id + '.json'));
|
|
499
|
+
lastSave = now;
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
table.lastUpdateResult = 'stabilizing';
|
|
496
503
|
}
|
|
504
|
+
} else {
|
|
505
|
+
table.lastUpdateResult = null;
|
|
497
506
|
}
|
|
498
507
|
};
|
|
499
508
|
}
|
|
@@ -606,11 +615,16 @@ module.exports = function (app) {
|
|
|
606
615
|
*/
|
|
607
616
|
function updateTable() {
|
|
608
617
|
lrnBoatSpeed.setVectorValue({ x: smoothedBoatSpeed.value, y: 0 }, { x: smoothedBoatSpeed.variance ?? 0, y: 0 });
|
|
609
|
-
if (!smoothedAttitude.ready || !smoothedBoatSpeed.ready || !smoothedHeading.ready || !smoothedGroundSpeed.ready || (options.assumeCurrent && !smoothedCurrent.ready))
|
|
618
|
+
if (!smoothedAttitude.ready || !smoothedBoatSpeed.ready || !smoothedHeading.ready || !smoothedGroundSpeed.ready || (options.assumeCurrent && !smoothedCurrent.ready)) {
|
|
619
|
+
table.lastUpdateResult = 'invalid';
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
610
622
|
|
|
611
623
|
// update correction table
|
|
612
624
|
if (smoothedBoatSpeed.value > minSpeed) {
|
|
613
625
|
table.update(smoothedBoatSpeed.value, smoothedAttitude.value?.roll, smoothedGroundSpeed, options.assumeCurrent ? smoothedCurrent : noCurrent, lrnBoatSpeed, smoothedHeading.value);
|
|
626
|
+
} else {
|
|
627
|
+
table.lastUpdateResult = 'waiting';
|
|
614
628
|
}
|
|
615
629
|
|
|
616
630
|
}
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -609,6 +609,31 @@ function renderLiveSections() {
|
|
|
609
609
|
const learningWarningIds = ['boatSpeed.smoothed', 'groundSpeed.smoothed', 'heading.smoothed', 'attitude.smoothed'];
|
|
610
610
|
if (config && config.assumeCurrent) learningWarningIds.push('current.smoothed');
|
|
611
611
|
renderWarnings('learning-warnings', learningWarningIds);
|
|
612
|
+
// Learning status section
|
|
613
|
+
const statusTbody = document.querySelector('#learning-status-table tbody');
|
|
614
|
+
if (statusTbody) {
|
|
615
|
+
const tableData = Object.values(state.tablesById)[0];
|
|
616
|
+
const result = tableData?.lastUpdateResult ?? null;
|
|
617
|
+
const learningText = result === null ? 'Off'
|
|
618
|
+
: result === 'stabilizing' ? 'Stabilising'
|
|
619
|
+
: 'Active';
|
|
620
|
+
const learningClass = result === null ? 'text-muted'
|
|
621
|
+
: result === 'stabilizing' ? 'text-warning'
|
|
622
|
+
: 'text-success';
|
|
623
|
+
const obsText = result === 'accepted' ? 'Accepted'
|
|
624
|
+
: result === 'rejected' ? 'Rejected'
|
|
625
|
+
: result === 'invalid' ? 'Invalid'
|
|
626
|
+
: result === 'waiting' ? 'Below threshold'
|
|
627
|
+
: '\u2014';
|
|
628
|
+
const obsClass = result === 'accepted' ? 'text-success'
|
|
629
|
+
: result === 'rejected' ? 'text-danger'
|
|
630
|
+
: result === 'invalid' ? 'text-warning'
|
|
631
|
+
: result === 'waiting' ? 'text-muted'
|
|
632
|
+
: 'text-muted';
|
|
633
|
+
statusTbody.innerHTML =
|
|
634
|
+
`<tr><td class="text-muted small">Learning</td><td class="small fw-bold ${learningClass}">${learningText}</td></tr>` +
|
|
635
|
+
`<tr><td class="text-muted small">Observation</td><td class="small fw-bold ${obsClass}">${obsText}</td></tr>`;
|
|
636
|
+
}
|
|
612
637
|
// Correction table
|
|
613
638
|
const tableEl = document.getElementById('table-container');
|
|
614
639
|
if (tableEl) {
|
package/public/index.html
CHANGED
|
@@ -155,6 +155,10 @@
|
|
|
155
155
|
</table>
|
|
156
156
|
<h6 class="text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small">Inputs</h6>
|
|
157
157
|
<div id="learning-inputs"></div>
|
|
158
|
+
<h6 class="text-uppercase fw-bold text-muted border-bottom pb-1 mt-3 mb-1 small">Status</h6>
|
|
159
|
+
<table class="table table-sm table-borderless mb-0" id="learning-status-table">
|
|
160
|
+
<tbody></tbody>
|
|
161
|
+
</table>
|
|
158
162
|
<div id="learning-warnings"></div>
|
|
159
163
|
</div>
|
|
160
164
|
</div>
|