speedandcurrent 1.0.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 +10 -0
- package/LICENSE +21 -0
- package/README.md +86 -0
- package/correctionModel.png +0 -0
- package/correctionTable.js +179 -0
- package/currentModel.png +0 -0
- package/index.js +592 -0
- package/models.md +26 -0
- package/package.json +34 -0
- package/public/TableRenderer.js +186 -0
- package/public/app.js +278 -0
- package/public/icon.png +0 -0
- package/public/index.html +107 -0
- package/public/style.css +184 -0
- package/public/surfaceplot.html +78 -0
- package/public/vectors.css +59 -0
- package/public/vectors.html +30 -0
- package/public/vectors.js +165 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2025-1-? First version
|
|
8
|
+
### Added
|
|
9
|
+
- basic functionality to estimate speed, leeway and current using Kalman filters
|
|
10
|
+
- basic functionality for a webapp that shows what is going on
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aswin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# **Speed Correction & Current Estimation Plugin**
|
|
2
|
+
|
|
3
|
+
## **Overview**
|
|
4
|
+
This SignalK plugin estimates **corrected speed through water**, **leeway**, and **water current** in real-time using advanced filter techniques. The plugin continuously refines its estimates based on vessel speed, heading, ground speed, and heel angle.
|
|
5
|
+
|
|
6
|
+
### **Key Features**
|
|
7
|
+
- ✅ Real-time estimation of vessel speed (STW).
|
|
8
|
+
- ✅ Real-time estimation of leeway.
|
|
9
|
+
- ✅ Real-time estimation of water current (both speed and direction).
|
|
10
|
+
- ✅ Adaptive learning: The plugin refines corrections over time.
|
|
11
|
+
- ✅ A web app to provide real-time insights into the plugin's operation.
|
|
12
|
+
- ✅ Use of Kalman filters for optimal estimations of the correction table, boat speed, and current.
|
|
13
|
+
- ✅ A user-configurable 2D correction table for speed errors based on heel and speed.
|
|
14
|
+
|
|
15
|
+
## **Introduction**
|
|
16
|
+
The plugin is based on the idea that a vessel's speed sensor is imperfect and can be corrected. Speed sensors, most often paddle wheels, are imperfect for two reasons:
|
|
17
|
+
1. **Measurement Precision**: The water flow over the hull influences the paddle wheel's measurement.
|
|
18
|
+
2. **Leeway**: A paddle wheel does not measure lateral speed or leeway, which is crucial for sailing vessels.
|
|
19
|
+
|
|
20
|
+
A GPS sensor provides the vessel's speed over ground, which differs from the paddle wheel's speed through water when there is current. The GPS sensor also has its imperfections.
|
|
21
|
+
|
|
22
|
+
In theory, one could use the GPS sensor to estimate the paddle wheel's error. However, estimating the paddle wheel's error requires knowing the water current, which cannot be measured directly from a floating vessel. Given enough measurements, one could estimate both current and paddle wheel error.
|
|
23
|
+
|
|
24
|
+
The plugin estimates both current and sensor error simultaneously. Using these estimations, the plugin corrects boat speed, estimates leeway, and estimates the current in real-time.
|
|
25
|
+
|
|
26
|
+
### **Assumptions**
|
|
27
|
+
- The paddle wheel's error depends on boat speed and heel angle, and it may change slowly over time.
|
|
28
|
+
- The speed and direction of the current change slowly and gradually.
|
|
29
|
+
- The vessel changes direction regularly.
|
|
30
|
+
|
|
31
|
+
## **Configuration of the Plugin**
|
|
32
|
+
|
|
33
|
+
### **1. Plugin Mode**
|
|
34
|
+
The plugin has different modes optimized for various circumstances. If there is no (or very little) current, use one of the no-current modes for the best paddle wheel corrections. If there is current, select one of the modes that account for current.
|
|
35
|
+
|
|
36
|
+
You must start with a mode that creates a new correction table. These modes use the specified dimensions to create an empty correction table. Once the table is created, the plugin will automatically switch to one of the fresh correction table modes. Be careful when selecting to start with a new correction table, as the previous table will be permanently deleted.
|
|
37
|
+
|
|
38
|
+
The modes that assume a fresh correction table are optimized to estimate valid correction values quickly. Corrections are not applied, and no leeway or current is estimated. Once the correction table has matured, select one of the mature correction table modes. In these modes, corrections are calculated and updated conservatively, and corrections are applied to the boat speed. Leeway and, optionally, current are estimated.
|
|
39
|
+
|
|
40
|
+
If circumstances are very dynamic (e.g., unstable wind, currents, or big waves), you can lock the correction table. Corrections will no longer be updated but will be applied.
|
|
41
|
+
|
|
42
|
+
| Mode | Updates Correction Table | Assume Current | Correct Boat Speed | Estimate Leeway | Estimate Current |
|
|
43
|
+
|------|--------------------------|----------------|---------------------|-----------------|------------------|
|
|
44
|
+
| Start with new correction table, no current | Yes, starting with a new one | No | No | No | No |
|
|
45
|
+
| Start with new correction table, with current | Yes, starting with a new one | Yes | No | No | No |
|
|
46
|
+
| Fresh correction table, no current | Fast | No | No | No | No |
|
|
47
|
+
| Fresh correction table, with current | Fast | Yes | No | No | No |
|
|
48
|
+
| Mature correction table, no current | Slow | No | Yes | Yes | No |
|
|
49
|
+
| Mature correction table, with current | Slow | Yes | Yes | Yes | Yes |
|
|
50
|
+
| Locked correction table | No | Yes | Yes | Yes | Yes |
|
|
51
|
+
|
|
52
|
+
### **2. Correction Table**
|
|
53
|
+
The other parameters for the plugin deal with the correction table. These include step size for heel and speed, and maximum heel and speed.
|
|
54
|
+
- The correction table should cover the vessel's normal dynamic range. Set maximum speed and maximum heel to values the vessel would typically achieve. Avoid setting values for exceptional circumstances, as this may result in unreliable estimations.
|
|
55
|
+
- Choose the step size carefully. Too small step sizes may lead to insufficient observations for a good estimation, while too large step sizes may result in imprecise corrections.
|
|
56
|
+
- Changing any table dimensions will result in the loss of all existing estimations.
|
|
57
|
+
|
|
58
|
+
## **WebApp**
|
|
59
|
+
The plugin comes with a WebApp that allows you to view the correction table and see the estimations in real-time. The WebApp can be launched from the SignalK server. The WebApp:
|
|
60
|
+
- Graphically shows the boat speeds (observed and estimated), ground speed, current, and correction.
|
|
61
|
+
- Displays incoming deltas.
|
|
62
|
+
- Displays the boat speeds (observed and estimated), ground speed, current, and correction.
|
|
63
|
+
- Displays the content of the correction table in either Cartesian values or polar coordinates.
|
|
64
|
+
- Displays the plugin configuration and internal settings.
|
|
65
|
+
|
|
66
|
+
The graphical representation of the speed vectors, current vector, and correction vector can be embedded in KIP displays using the path `/speedandcurrent/vectors.html`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## **How It Works**
|
|
71
|
+
|
|
72
|
+
### **1. Speed Correction Model**
|
|
73
|
+
- Vessel speed errors depend on **heel** and **speed** and are assumed constant for a given speed and heel angle.
|
|
74
|
+
- A correction table stores speed errors for steps in speed and heel angle.
|
|
75
|
+
- Each cell in the correction table contains a **Kalman filter** that updates speed corrections with new measurements.
|
|
76
|
+
- The speed measurement is corrected by subtracting the correction from the correction table. The correction is **interpolated** based on nearby table cells, weighted by the uncertainty provided by the Kalman filter.
|
|
77
|
+
- 
|
|
78
|
+
|
|
79
|
+
### **2. Current Estimation Model**
|
|
80
|
+
- The water current is estimated as a **slowly changing** vector.
|
|
81
|
+
- A **Kalman filter** updates the current based on the difference between GPS speed and corrected speed.
|
|
82
|
+
- 
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
|
|
Binary file
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const { Table2D} = require('signalkutilities');
|
|
2
|
+
const { KalmanFilter, State } = require('kalman-filter');
|
|
3
|
+
|
|
4
|
+
class CorrectionTable extends Table2D{
|
|
5
|
+
/**
|
|
6
|
+
* Represents a 2D correction table for heel and speed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
static fromJSON(data, stability) {
|
|
12
|
+
const table = new CorrectionTable(data.row, data.col, stability);
|
|
13
|
+
table.table = data.table.map(row => row.map(cellData => CorrectionEstimator.fromJSON(cellData)));
|
|
14
|
+
return table;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor(row, col, stability=5) {
|
|
18
|
+
super(row, col, CorrectionEstimator, CorrectionEstimator.getFilterModel(stability));
|
|
19
|
+
this.id = "correctionTable";
|
|
20
|
+
this.label = "Correction Table";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update(speed, heel, groundSpeed, current, boatSpeed, heading) {
|
|
24
|
+
this.getCell(speed, heel)?.update(groundSpeed, current, boatSpeed, heading);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getKalmanCorrection(speed, heel) {
|
|
28
|
+
/**
|
|
29
|
+
* Return a estimated correction based on interpolation and weighting of correction variance;
|
|
30
|
+
*/
|
|
31
|
+
if (speed == 0) return { x: 0, y: 0 };
|
|
32
|
+
const neighbours = this.findNeighbours(speed, heel);
|
|
33
|
+
let totalWeightX = 0;
|
|
34
|
+
let totalWeightY = 0;
|
|
35
|
+
let x = 0;
|
|
36
|
+
let y = 0;
|
|
37
|
+
for (const neighbour of neighbours) {
|
|
38
|
+
const { cell:correction, xdist, ydist } = neighbour;
|
|
39
|
+
if (correction.N > 0) {
|
|
40
|
+
const weightX = 1 / (xdist + 1e-6) ;
|
|
41
|
+
const weightY = 1 / (ydist + 1e-6) ;
|
|
42
|
+
const varianceX = correction.covariance[0][0];
|
|
43
|
+
const varianceY = correction.covariance[1][1];
|
|
44
|
+
const N = correction.N;
|
|
45
|
+
const varWeightX = weightX * N / (varianceX + 1e-6);
|
|
46
|
+
const varWeightY = weightY * N / (varianceY + 1e-6);
|
|
47
|
+
x += correction.x * varWeightX;
|
|
48
|
+
y += correction.y * varWeightY;
|
|
49
|
+
totalWeightX += varWeightX;
|
|
50
|
+
totalWeightY += varWeightY;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (totalWeightX == 0 || totalWeightY == 0) return { x: 0, y: 0 };
|
|
54
|
+
x = x / totalWeightX;
|
|
55
|
+
y = y / totalWeightY;
|
|
56
|
+
return { x, y };
|
|
57
|
+
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getInfo() {
|
|
61
|
+
return {
|
|
62
|
+
row: { min: this.min[0], max: this.max[0], step: this.step[0] },
|
|
63
|
+
col: { min: this.min[1], max: this.max[1], step: this.step[1] },
|
|
64
|
+
table: this.table.map(row => row.map(correction => correction.getInfo()))
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class CorrectionEstimator {
|
|
71
|
+
/**
|
|
72
|
+
* Represents a Kalman correction at a cell in a correction table
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
static fromJSON(data, stability) {
|
|
76
|
+
const filterModel = CorrectionEstimator.getFilterModel(stability);
|
|
77
|
+
const estimator = new CorrectionEstimator(filterModel, data.state);
|
|
78
|
+
return estimator;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static getFilterModel(stability = 5) {
|
|
82
|
+
return {
|
|
83
|
+
observation: {
|
|
84
|
+
stateProjection: [[1, 0], [0, 1]], // observation matrix H
|
|
85
|
+
covariance: [[10 ** stability, 0], [0, 10 ** stability]], //measurement noise R
|
|
86
|
+
dimension: 2
|
|
87
|
+
},
|
|
88
|
+
dynamic: {
|
|
89
|
+
transition: [[1, 0], [0, 1]], // state transition matrix F
|
|
90
|
+
covariance: [1, 1],// process noise covariance matrix Q
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
constructor(filterModel, initialState) {
|
|
96
|
+
this.filter = new KalmanFilter(filterModel);
|
|
97
|
+
this.filterState = null;
|
|
98
|
+
if (initialState != null) {
|
|
99
|
+
this.filterState = new State(initialState);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
update(groundSpeed, current, boatSpeed, heading) {
|
|
104
|
+
if (groundSpeed.n < 2 || current.n < 2 || boatSpeed.n < 2) return;
|
|
105
|
+
// Rotation matrix for -theta
|
|
106
|
+
const cosTheta = Math.cos(heading);
|
|
107
|
+
const sinTheta = Math.sin(heading);
|
|
108
|
+
|
|
109
|
+
const rotateValue = (vector) => ([
|
|
110
|
+
cosTheta * vector[0] + sinTheta * vector[1],
|
|
111
|
+
-sinTheta * vector[0] + cosTheta * vector[1]]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const rotateVariance = (vector) => (
|
|
115
|
+
[
|
|
116
|
+
[vector[0] * cosTheta ** 2 + vector[1] * sinTheta ** 2,
|
|
117
|
+
(vector[0] - vector[1]) * cosTheta * sinTheta],
|
|
118
|
+
[(vector[0] - vector[1]) * cosTheta * sinTheta,
|
|
119
|
+
vector[0] * sinTheta ** 2 + vector[1] * cosTheta ** 2]
|
|
120
|
+
]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
var groundVector = rotateValue(groundSpeed.obs);
|
|
124
|
+
var currentVector = rotateValue(current.obs);
|
|
125
|
+
var boatVector = boatSpeed.obs;
|
|
126
|
+
|
|
127
|
+
const observation = [
|
|
128
|
+
-boatVector[0] + groundVector[0] - currentVector[0],
|
|
129
|
+
-boatVector[1] + groundVector[1] - currentVector[1]
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
var groundCov = rotateVariance(groundSpeed.variance);
|
|
133
|
+
var currentCov = rotateVariance(current.variance);
|
|
134
|
+
var boatCov = [[boatSpeed.variance[0], 0], [0, boatSpeed.variance[1]]];
|
|
135
|
+
|
|
136
|
+
const observationCovariance = [[
|
|
137
|
+
groundCov[0][0] + currentCov[0][0] + boatCov[0][0],
|
|
138
|
+
groundCov[0][1] + currentCov[0][1] + boatCov[0][1]],
|
|
139
|
+
[
|
|
140
|
+
groundCov[1][0] + currentCov[1][0] + boatCov[1][0],
|
|
141
|
+
groundCov[1][1] + currentCov[1][1] + boatCov[1][1]],
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
this.filterState = this.filter.filter({ previousCorrected: this.filterState, observation, observationCovariance });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
getInfo() {
|
|
149
|
+
return { x: this.x, y: this.y, N: this.N };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get N() {
|
|
153
|
+
if (this.filterState == null) return 0;
|
|
154
|
+
return this.filterState.index;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get x() {
|
|
158
|
+
if (this.filterState == null) return 0;
|
|
159
|
+
return this.filterState.mean[0][0];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get y() {
|
|
163
|
+
if (this.filterState == null) return 0;
|
|
164
|
+
return this.filterState.mean[1][0];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get covariance() {
|
|
168
|
+
return this.filterState.covariance;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
toJSON() {
|
|
173
|
+
return this.N != 0 ? { state: this.filterState } : { state: null };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
module.exports = { CorrectionTable };
|
package/currentModel.png
ADDED
|
Binary file
|