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/index.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
const { PolarStat, DeltaStat, DeltaBase, DeltaSubscribe, PolarDeltaBase, PolarDeltaCatch, PolarDeltaSubscribe, SI, Reporter } = require('signalkutilities');
|
|
2
|
+
const { CorrectionTable } = require('./correctionTable.js');
|
|
3
|
+
const { KalmanFilter } = require('kalman-filter');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
module.exports = function (app) {
|
|
9
|
+
|
|
10
|
+
let unsubscribes = [];
|
|
11
|
+
let options = {};
|
|
12
|
+
let isRunning = false;
|
|
13
|
+
let corrTable = null;
|
|
14
|
+
let lastSave = null;
|
|
15
|
+
var residual, heading, attitude, boatSpeed, boatSpeedPolar, correctedBoatSpeed, groundSpeed, current, currentDelta, speedCorrection, reporter;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const plugin = {};
|
|
19
|
+
plugin.id = "SpeedAndCurrent";
|
|
20
|
+
plugin.name = "Speed and current";
|
|
21
|
+
plugin.description = "An experimental plugin that uses sensor fusion to get boat speed, current and leeway.";
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
plugin.schema = {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
mode: {
|
|
29
|
+
type: "string",
|
|
30
|
+
title: "Plugin mode",
|
|
31
|
+
description: "The mode of the plugin.",
|
|
32
|
+
enum: ["Start with new correction table, no current",
|
|
33
|
+
"Start with new correction table, with current",
|
|
34
|
+
"Fresh correction table, no current",
|
|
35
|
+
"Fresh correction table, with current",
|
|
36
|
+
"Mature correction table, no current",
|
|
37
|
+
"Mature correction table, with current",
|
|
38
|
+
"Locked correction table",
|
|
39
|
+
"Manual configuration"],
|
|
40
|
+
default: "Start with new correction table, no current",
|
|
41
|
+
},
|
|
42
|
+
heelStep: {
|
|
43
|
+
type: "number",
|
|
44
|
+
title: "Step size for heel",
|
|
45
|
+
description: "Correction table stepsize for heel.",
|
|
46
|
+
default: 8
|
|
47
|
+
},
|
|
48
|
+
maxHeel: {
|
|
49
|
+
type: "number",
|
|
50
|
+
title: "Maximum heel in correction table",
|
|
51
|
+
description: "Correction table maximum heel.",
|
|
52
|
+
default: 32
|
|
53
|
+
},
|
|
54
|
+
speedStep: {
|
|
55
|
+
type: "number",
|
|
56
|
+
title: "Step size for speed",
|
|
57
|
+
description: "Correction table stepsize for speed.",
|
|
58
|
+
default: 1
|
|
59
|
+
},
|
|
60
|
+
maxSpeed: {
|
|
61
|
+
type: "number",
|
|
62
|
+
title: "Maximum speed in correction table",
|
|
63
|
+
description: "Correction table maximum speed.",
|
|
64
|
+
default: 9
|
|
65
|
+
},
|
|
66
|
+
preventDuplication: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
title: "Prevent duplication of boat speed",
|
|
69
|
+
description: "Overwrite boat speed from sensor with corrected boat speed.",
|
|
70
|
+
default: true,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
updateCorrectionTable: {
|
|
74
|
+
type: "boolean",
|
|
75
|
+
title: "Update correction table",
|
|
76
|
+
description: "Update estimations in the correction table.",
|
|
77
|
+
default: true,
|
|
78
|
+
},
|
|
79
|
+
estimateBoatSpeed: {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
title: "Correct boat speed",
|
|
82
|
+
description: "Correct boat speed using correction table. This includes adding leeway.",
|
|
83
|
+
default: true,
|
|
84
|
+
},
|
|
85
|
+
correctionStability: {
|
|
86
|
+
type: "number",
|
|
87
|
+
title: "Stability of correction estimation",
|
|
88
|
+
description: `The stability of the correction estimation indicates how much the estimation is trusted
|
|
89
|
+
opposed to the calculation of . Biggeboat speed. Bigger values give more trust to the estimation and more stable estimations`,
|
|
90
|
+
default: 7,
|
|
91
|
+
minimum: 5,
|
|
92
|
+
maximum: 12
|
|
93
|
+
},
|
|
94
|
+
doStartFresh: {
|
|
95
|
+
type: "boolean",
|
|
96
|
+
title: "Empty correction table",
|
|
97
|
+
description: "Restart with a fresh correction table. WARNING: all current corrections will be lost permanently!",
|
|
98
|
+
default: false
|
|
99
|
+
},
|
|
100
|
+
assumeCurrent: {
|
|
101
|
+
type: "boolean",
|
|
102
|
+
title: "assume current",
|
|
103
|
+
description: "The estimator assumes that there is current.",
|
|
104
|
+
default: false,
|
|
105
|
+
},
|
|
106
|
+
estimateCurrent: {
|
|
107
|
+
type: "boolean",
|
|
108
|
+
title: "Estimate current",
|
|
109
|
+
description: "Estimate water current.",
|
|
110
|
+
default: true,
|
|
111
|
+
},
|
|
112
|
+
currentStability: {
|
|
113
|
+
type: "number",
|
|
114
|
+
title: "Stability of current estimation ",
|
|
115
|
+
description: `The stability of the current estimation indicates how much the estimation is trusted
|
|
116
|
+
opposed to the calculation of current. Bigger values give more trust to the estimation and more sable estimations`,
|
|
117
|
+
default: 5,
|
|
118
|
+
minimum: 1,
|
|
119
|
+
maximum: 8
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
plugin.uiSchema = {
|
|
125
|
+
'ui:order': ["mode", "maxSpeed", "speedStep", "maxHeel", "heelStep", "preventDuplication", "updateCorrectionTable", "estimateBoatSpeed", "correctionStability", "doStartFresh", "assumeCurrent", "estimateCurrent", "currentStability"],
|
|
126
|
+
doStartFresh: {
|
|
127
|
+
"ui:widget": "hidden",
|
|
128
|
+
},
|
|
129
|
+
heelStep: {
|
|
130
|
+
"ui:widget": "updown",
|
|
131
|
+
},
|
|
132
|
+
maxHeel: {
|
|
133
|
+
"ui:widget": "updown",
|
|
134
|
+
},
|
|
135
|
+
speedStep: {
|
|
136
|
+
"ui:widget": "updown",
|
|
137
|
+
},
|
|
138
|
+
maxSpeed: {
|
|
139
|
+
"ui:widget": "updown",
|
|
140
|
+
},
|
|
141
|
+
mode: {
|
|
142
|
+
"ui:widget": "select",
|
|
143
|
+
},
|
|
144
|
+
updateCorrectionTable: {
|
|
145
|
+
"ui:widget": "hidden",
|
|
146
|
+
},
|
|
147
|
+
estimateBoatSpeed: {
|
|
148
|
+
"ui:widget": "hidden",
|
|
149
|
+
},
|
|
150
|
+
correctionStability: {
|
|
151
|
+
"ui:widget": "hidden",
|
|
152
|
+
},
|
|
153
|
+
preventDuplication: {
|
|
154
|
+
"ui:widget": "checkbox",
|
|
155
|
+
},
|
|
156
|
+
assumeCurrent: {
|
|
157
|
+
"ui:widget": "hidden",
|
|
158
|
+
},
|
|
159
|
+
estimateCurrent: {
|
|
160
|
+
"ui:widget": "hidden",
|
|
161
|
+
},
|
|
162
|
+
currentStability: {
|
|
163
|
+
"ui:widget": "hidden",
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
plugin.registerWithRouter = function (router) {
|
|
168
|
+
app.debug('registerWithRouter');
|
|
169
|
+
|
|
170
|
+
router.get('/getResults', (req, res) => {
|
|
171
|
+
if (!isRunning) {
|
|
172
|
+
res.status(503).json({ error: "Plugin is not running" });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
res.json(reporter?.getReport());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
router.get('/getVectors', (req, res) => {
|
|
181
|
+
if (!isRunning) {
|
|
182
|
+
res.status(503).json({ error: "Plugin is not running" });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const v = { deltas: [], polars: [] };
|
|
186
|
+
const d = [heading, attitude];
|
|
187
|
+
const p = [groundSpeed, boatSpeedPolar, correctedBoatSpeed, current, speedCorrection, residual];
|
|
188
|
+
p.forEach(polar => {
|
|
189
|
+
v.polars.push({
|
|
190
|
+
id: polar.id,
|
|
191
|
+
plane: polar.plane,
|
|
192
|
+
label: polar.label,
|
|
193
|
+
speed: polar.pValue.speed,
|
|
194
|
+
angle: polar.pValue.angle,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
d.forEach(delta => {
|
|
198
|
+
v.deltas.push({
|
|
199
|
+
id: delta.id,
|
|
200
|
+
value: delta.value,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
res.json(v);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function saveTable(options, correctionTable) {
|
|
210
|
+
options.correctionTable = correctionTable.toJSON();
|
|
211
|
+
app.savePluginOptions(options, () => { app.debug('Correction table saved') });
|
|
212
|
+
return new Date();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
plugin.start = (opts) => {
|
|
217
|
+
app.debug("plugin started");
|
|
218
|
+
|
|
219
|
+
// make options survive to the stop function
|
|
220
|
+
options = opts;
|
|
221
|
+
loadPresets();
|
|
222
|
+
|
|
223
|
+
// correction table
|
|
224
|
+
const table = loadTable(options);
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
// heading
|
|
228
|
+
// TBD: headingTrue or headingMagnetic
|
|
229
|
+
heading = new DeltaSubscribe("navigation.headingTrue");
|
|
230
|
+
heading.subscribe(app, plugin.id, unsubscribes);
|
|
231
|
+
heading.setDisplayAttributes("heading","heading");
|
|
232
|
+
|
|
233
|
+
//attitude
|
|
234
|
+
attitude = new DeltaSubscribe("navigation.attitude");
|
|
235
|
+
attitude.value = { pitch: 0, roll: 0, yaw: 0 }; //prevents errors when there is no attitude sensor
|
|
236
|
+
attitude.setDisplayAttributes("attitude","attitude");
|
|
237
|
+
attitude.subscribe(app, plugin.id, unsubscribes);
|
|
238
|
+
|
|
239
|
+
// current
|
|
240
|
+
currentDelta = new DeltaBase("environment.current");
|
|
241
|
+
current = new PolarDeltaBase();
|
|
242
|
+
current.setDisplayAttributes("current", "ref_ground", "current");
|
|
243
|
+
|
|
244
|
+
const currentStat = new PolarStat(current);
|
|
245
|
+
|
|
246
|
+
// magnetic variance
|
|
247
|
+
variance = new DeltaSubscribe("navigation.magneticVariation");
|
|
248
|
+
|
|
249
|
+
// boat speed
|
|
250
|
+
if (options.preventDuplication) {
|
|
251
|
+
// DeltaCatch needed here
|
|
252
|
+
boatSpeed = new DeltaSubscribe("navigation.speedThroughWater");
|
|
253
|
+
boatSpeed.subscribe(app, plugin.id, unsubscribes, calculate);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
boatSpeed = new DeltaSubscribe("navigation.speedThroughWater");
|
|
257
|
+
boatSpeed.subscribe(app, plugin.id, unsubscribes, calculate);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
boatSpeedPolar = new PolarDeltaBase("navigation.speedThroughWater", "environment.wind.directionTruenavigation.leewayAngle");
|
|
261
|
+
boatSpeedPolar.setDisplayAttributes("boatSpeed", "ref_boat", "observed boat speed");
|
|
262
|
+
const boatSpeedStat = new PolarStat(boatSpeedPolar);
|
|
263
|
+
|
|
264
|
+
correctedBoatSpeed = new PolarDeltaBase("navigation.speedThroughWater", "environment.wind.directionTruenavigation.leewayAngle");
|
|
265
|
+
correctedBoatSpeed.setDisplayAttributes("correctedSpeed", "ref_boat", "estimated boat Speed");
|
|
266
|
+
|
|
267
|
+
boatSpeedRefGround = new PolarDeltaBase();
|
|
268
|
+
boatSpeedRefGround.setDisplayAttributes("boatSpeedRefGround", "ref_ground","boat speed over ground");
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
// ground speed
|
|
272
|
+
groundSpeed = new PolarDeltaSubscribe("navigation.speedOverGround", "navigation.courseOverGroundTrue");
|
|
273
|
+
groundSpeed.subscribe(app, plugin.id, unsubscribes);
|
|
274
|
+
groundSpeed.setDisplayAttributes("groundSpeed", "ref_ground", "observed speed over ground");
|
|
275
|
+
const groundSpeedStat = new PolarStat(groundSpeed);
|
|
276
|
+
|
|
277
|
+
// correction vector
|
|
278
|
+
speedCorrection = new PolarDeltaBase();
|
|
279
|
+
speedCorrection.setDisplayAttributes("correction", "ref_boat", "correction");
|
|
280
|
+
|
|
281
|
+
// residual
|
|
282
|
+
residual = new PolarDeltaBase();
|
|
283
|
+
residual.setDisplayAttributes("residual", "ref_ground", "residual");
|
|
284
|
+
|
|
285
|
+
// Make reporting object for webApp
|
|
286
|
+
reporter = new Reporter();
|
|
287
|
+
|
|
288
|
+
// filter for current
|
|
289
|
+
stability = 10 ** (options.currentStability);
|
|
290
|
+
const currentEstimator = new KalmanFilter({
|
|
291
|
+
observation: {
|
|
292
|
+
stateProjection: [[1, 0], [0, 1]], // observation matrix H
|
|
293
|
+
covariance: [[1 , 0], [0, 1 ]], //measurement noise R
|
|
294
|
+
dimension: 2
|
|
295
|
+
},
|
|
296
|
+
dynamic: {
|
|
297
|
+
transition: [[1, 0], [0, 1]], // state transition matrix F
|
|
298
|
+
covariance: [1 / stability, 1 / stability],// process noise covariance matrix Q
|
|
299
|
+
init: {
|
|
300
|
+
mean: [[0], [0]],
|
|
301
|
+
covariance: [[1 / stability, 0], [0, 1 / stability]]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
let currentEstimation = null;
|
|
306
|
+
|
|
307
|
+
// filter for boat speed
|
|
308
|
+
stability = 10 ** (1);
|
|
309
|
+
const boatSpeedEstimator = new KalmanFilter({
|
|
310
|
+
observation: {
|
|
311
|
+
stateProjection: [[1, 0], [0, 1]], // observation matrix H
|
|
312
|
+
covariance: [[1 * stability, 0], [0, 1 * stability]], //measurement noise R
|
|
313
|
+
dimension: 2
|
|
314
|
+
},
|
|
315
|
+
dynamic: {
|
|
316
|
+
transition: [[1, 0], [0, 1]], // state transition matrix F
|
|
317
|
+
covariance: [1, 1],// process noise covariance matrix Q
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
let boatSpeedEstimation = null;
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
isRunning = true;
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
// Main function, estimates boatSpeed, leeway and current
|
|
328
|
+
function calculate(timestamp) {
|
|
329
|
+
// prepare iteration
|
|
330
|
+
boatSpeedPolar.setVValue({ x: boatSpeed.value, y: 0 });
|
|
331
|
+
groundSpeedStat.sample();
|
|
332
|
+
currentStat.sample();
|
|
333
|
+
boatSpeedStat.sample();
|
|
334
|
+
heel = attitude.value.roll;
|
|
335
|
+
speed = boatSpeed.value;
|
|
336
|
+
theta = heading.value;
|
|
337
|
+
if (!Number.isFinite(heel)) {
|
|
338
|
+
app.debug("Heel is not a valid number, skipping calculation");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!Number.isFinite(speed)) {
|
|
342
|
+
app.debug("Speed is not a valid number, skipping calculation");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!Number.isFinite(theta)) {
|
|
346
|
+
app.debug("Heading is not a valid number, skipping calculation");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
reporter.newReport();
|
|
350
|
+
reporter.addOptions(options);
|
|
351
|
+
reporter.addPolar( groundSpeed);
|
|
352
|
+
reporter.addPolar( boatSpeedPolar);
|
|
353
|
+
reporter.addAttitude( attitude);
|
|
354
|
+
reporter.addDelta( heading);
|
|
355
|
+
|
|
356
|
+
// update correction table
|
|
357
|
+
if (options.updateCorrectionTable) {
|
|
358
|
+
table.update(speed, heel, groundSpeedStat, currentStat, boatSpeedStat, theta);
|
|
359
|
+
}
|
|
360
|
+
reporter.addTable(table, speed, heel);
|
|
361
|
+
|
|
362
|
+
// correct and estimate boat speed
|
|
363
|
+
estimateBoatSpeed();
|
|
364
|
+
|
|
365
|
+
if (options.assumeCurrent) {
|
|
366
|
+
// estimate current ;
|
|
367
|
+
estimateCurrent();
|
|
368
|
+
}
|
|
369
|
+
calcResidual();
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
// Send calculated delta's
|
|
373
|
+
if (options.estimateCurrent) {
|
|
374
|
+
// translate current into signalK data structure
|
|
375
|
+
const c = current.getPValue();
|
|
376
|
+
const cc = { drift: current.speed, setTrue: current.angle, setMagnetic: (current.angle - variance.value + Math.PI) % (2 * Math.PI) - Math.PI };
|
|
377
|
+
currentDelta.setValue(cc);
|
|
378
|
+
DeltaBase.sendDeltas(app, plugin.id, [currentDelta]);
|
|
379
|
+
}
|
|
380
|
+
if (options.estimateBoatSpeed) PolarDeltaBase.sendDeltas(app, plugin.id, [correctedBoatSpeed]);
|
|
381
|
+
|
|
382
|
+
// Save correction table
|
|
383
|
+
// periodically
|
|
384
|
+
if (options.updateCorrectionTable && new Date() - lastSave > 5 * 60 * 1000) {
|
|
385
|
+
lastSave = saveTable(options, table);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// estimates boat speed from observed boatSpeed and correction
|
|
390
|
+
function estimateBoatSpeed() {
|
|
391
|
+
cor = table.getKalmanCorrection(speed, heel);
|
|
392
|
+
// app.debug(cor);
|
|
393
|
+
correctedBoatSpeed.copyFrom(boatSpeedPolar);
|
|
394
|
+
speedCorrection.setVValue(cor);
|
|
395
|
+
|
|
396
|
+
reporter.addPolar(speedCorrection);
|
|
397
|
+
correctedBoatSpeed.add(speedCorrection);
|
|
398
|
+
const v = correctedBoatSpeed.getVValue();
|
|
399
|
+
const observation = [v.x, v.y];
|
|
400
|
+
boatSpeedEstimation = boatSpeedEstimator.filter({ previousCorrected: boatSpeedEstimation, observation });
|
|
401
|
+
correctedBoatSpeed.setKValue(boatSpeedEstimation);
|
|
402
|
+
reporter.addPolar(correctedBoatSpeed);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// estimates current from observed groundspeed and estimated boatspeed
|
|
406
|
+
function estimateCurrent() {
|
|
407
|
+
boatSpeedRefGround.copyFrom(correctedBoatSpeed);
|
|
408
|
+
boatSpeedRefGround.rotate(theta);
|
|
409
|
+
reporter.addPolar( boatSpeedRefGround);
|
|
410
|
+
current.copyFrom(groundSpeed);
|
|
411
|
+
current.substract(boatSpeedRefGround);
|
|
412
|
+
const v = current.getVValue();
|
|
413
|
+
const observation = [v.x, v.y];
|
|
414
|
+
currentEstimation = currentEstimator.filter({ previousCorrected: currentEstimation, observation });
|
|
415
|
+
current.setKValue(currentEstimation);
|
|
416
|
+
reporter.addPolar( current);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Calculate what ground speed is observed that is not attributed to estimated boat speed or estimated current
|
|
420
|
+
function calcResidual() {
|
|
421
|
+
residual.copyFrom(groundSpeed);
|
|
422
|
+
boatSpeedRefGround.copyFrom(correctedBoatSpeed);
|
|
423
|
+
boatSpeedRefGround.rotate(theta);
|
|
424
|
+
residual.substract(boatSpeedRefGround);
|
|
425
|
+
residual.substract(current);
|
|
426
|
+
reporter.addPolar(residual);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Make sure the table matches the settings
|
|
430
|
+
function enforceConsistancy(row, col ) {
|
|
431
|
+
// function is bugged due to IS units vs knots and degrees
|
|
432
|
+
if (!options?.correctionTable) return false;
|
|
433
|
+
const table = options.correctionTable;
|
|
434
|
+
if (!table) return false;
|
|
435
|
+
if (!table.row) return false;
|
|
436
|
+
if (table.row.min != row.min) return false;
|
|
437
|
+
if (table.row.max != row.max) return false;
|
|
438
|
+
if (table.row.step != row.step) return false;
|
|
439
|
+
if (table.col.min != col.min) return false;
|
|
440
|
+
if (table.col.max != col.max) return false;
|
|
441
|
+
if (table.col.step != col.step) return false;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function loadTable(options) {
|
|
446
|
+
const row = { min: 0, max: SI.fromKnots(options.maxSpeed), step: SI.fromKnots(options.speedStep) };
|
|
447
|
+
const col = { min: -SI.fromDegrees(options.maxHeel), max: SI.fromDegrees(options.maxHeel), step: SI.fromDegrees(options.heelStep) };
|
|
448
|
+
let table;
|
|
449
|
+
if ( enforceConsistancy(row, col) && !options.doStartFresh) {
|
|
450
|
+
table = CorrectionTable.fromJSON(options.correctionTable, options.correctionStability);
|
|
451
|
+
app.debug("Correction table loaded");
|
|
452
|
+
lastSave = new Date();
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
table = new CorrectionTable(row, col, options.correctionStability);
|
|
456
|
+
app.debug("Correction table created");
|
|
457
|
+
options.doStartFresh = false;
|
|
458
|
+
lastSave = saveTable(options, table);
|
|
459
|
+
}
|
|
460
|
+
table.setDisplayAttributes("correctionTable", "Speed / Heel");
|
|
461
|
+
return table;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
function loadPresets() {
|
|
466
|
+
const modeEnum = plugin.schema.properties.mode.enum;
|
|
467
|
+
app.debug("Loading presests for mode: " + options.mode);
|
|
468
|
+
|
|
469
|
+
switch (modeEnum.indexOf(options.mode)) {
|
|
470
|
+
case -1:
|
|
471
|
+
app.debug("Invalid mode");
|
|
472
|
+
app.debug(options);
|
|
473
|
+
//options.mode = modeEnum[2];
|
|
474
|
+
break;
|
|
475
|
+
case 0:
|
|
476
|
+
// new table, no current
|
|
477
|
+
options.doStartFresh = true;
|
|
478
|
+
options.updateCorrectionTable = true;
|
|
479
|
+
options.estimateBoatSpeed = false;
|
|
480
|
+
options.assumeCurrent = false;
|
|
481
|
+
options.estimateCurrent = false;
|
|
482
|
+
options.correctionStability = 5;
|
|
483
|
+
options.mode = modeEnum[2];
|
|
484
|
+
break;
|
|
485
|
+
case 1:
|
|
486
|
+
// new table, with current
|
|
487
|
+
options.doStartFresh = true;
|
|
488
|
+
options.updateCorrectionTable = true;
|
|
489
|
+
options.estimateBoatSpeed = false;
|
|
490
|
+
options.assumeCurrent = true;
|
|
491
|
+
options.estimateCurrent = false;
|
|
492
|
+
options.correctionStability = 5;
|
|
493
|
+
options.currentStability = 7;
|
|
494
|
+
options.mode = modeEnum[3];
|
|
495
|
+
break;
|
|
496
|
+
case 2:
|
|
497
|
+
// fresh table, no current
|
|
498
|
+
options.doStartFresh = false;
|
|
499
|
+
options.updateCorrectionTable = true;
|
|
500
|
+
options.estimateBoatSpeed = false;
|
|
501
|
+
options.assumeCurrent = false;
|
|
502
|
+
options.estimateCurrent = false;
|
|
503
|
+
options.correctionStability = 6;
|
|
504
|
+
break;
|
|
505
|
+
case 3:
|
|
506
|
+
// fresh table, with current
|
|
507
|
+
options.doStartFresh = false;
|
|
508
|
+
options.updateCorrectionTable = true;
|
|
509
|
+
options.estimateBoatSpeed = false;
|
|
510
|
+
options.assumeCurrent = true;
|
|
511
|
+
options.estimateCurrent = false;
|
|
512
|
+
options.correctionStability = 6;
|
|
513
|
+
options.currentStability = 6;
|
|
514
|
+
break;
|
|
515
|
+
case 4:
|
|
516
|
+
// mature table, no current
|
|
517
|
+
options.doStartFresh = false;
|
|
518
|
+
options.updateCorrectionTable = true;
|
|
519
|
+
options.estimateBoatSpeed = true;
|
|
520
|
+
options.assumeCurrent = false;
|
|
521
|
+
options.estimateCurrent = false;
|
|
522
|
+
options.correctionStability = 8;
|
|
523
|
+
break;
|
|
524
|
+
case 5:
|
|
525
|
+
// mature table, with current
|
|
526
|
+
options.doStartFresh = false;
|
|
527
|
+
options.updateCorrectionTable = true;
|
|
528
|
+
options.estimateBoatSpeed = true;
|
|
529
|
+
options.assumeCurrent = true;
|
|
530
|
+
options.estimateCurrent = true;
|
|
531
|
+
options.correctionStability = 8;
|
|
532
|
+
options.currentStability = 3;
|
|
533
|
+
break;
|
|
534
|
+
case 6:
|
|
535
|
+
// locked table
|
|
536
|
+
options.doStartFresh = false;
|
|
537
|
+
options.updateCorrectionTable = false;
|
|
538
|
+
options.estimateBoatSpeed = false;
|
|
539
|
+
options.assumeCurrent = true;
|
|
540
|
+
options.estimateCurrent = true;
|
|
541
|
+
options.currentStability = 2;
|
|
542
|
+
break;
|
|
543
|
+
case 7:
|
|
544
|
+
// all manual configuration;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// compare floats
|
|
550
|
+
function isEqual(a, b) {
|
|
551
|
+
const TOLERANCE = 1e-5;
|
|
552
|
+
if (Math.abs(a - b) < TOLERANCE) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
plugin.stop = () => {
|
|
561
|
+
return new Promise((resolve, reject) => {
|
|
562
|
+
try {
|
|
563
|
+
unsubscribes.forEach(f => f());
|
|
564
|
+
unsubscribes = [];
|
|
565
|
+
//corrTable.save(app);
|
|
566
|
+
if (corrTable != null) {
|
|
567
|
+
lastSave = saveTable(options, corrTable);
|
|
568
|
+
}
|
|
569
|
+
options = {};
|
|
570
|
+
|
|
571
|
+
residual = null;
|
|
572
|
+
heading = null;
|
|
573
|
+
attitude = null;
|
|
574
|
+
boatSpeed = null;
|
|
575
|
+
boatSpeedPolar = null;
|
|
576
|
+
correctedBoatSpeed = null;
|
|
577
|
+
groundSpeed = null;
|
|
578
|
+
current = null;
|
|
579
|
+
currentDelta = null;
|
|
580
|
+
speedCorrection = null;
|
|
581
|
+
reporter = null;
|
|
582
|
+
isRunning = false;
|
|
583
|
+
corrTable = null;
|
|
584
|
+
|
|
585
|
+
resolve();
|
|
586
|
+
} catch (error) {
|
|
587
|
+
reject(error);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
return plugin;
|
|
592
|
+
};
|
package/models.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
## Correction model
|
|
2
|
+
### raw
|
|
3
|
+
\vec{correction} = \mathbb{R}(-heading) \cdot \vec{groundSpeed} - \vec{boatSpeed} - \mathbb{R}(-heading) \cdot \vec{waterSpeed}
|
|
4
|
+
|
|
5
|
+
### link
|
|
6
|
+
https://latex.codecogs.com/svg.image?\vec{correction}=\mathbb{R}(-heading)\cdot\vec{groundSpeed}-\vec{boatSpeed}-\mathbb{R}(-heading)\cdot\vec{waterSpeed}
|
|
7
|
+
|
|
8
|
+
### html
|
|
9
|
+
<img src="https://latex.codecogs.com/png.image?\dpi{110}\vec{correction}=\mathbb{R}(-heading)\cdot\vec{groundSpeed}-\vec{boatSpeed}-\mathbb{R}(-heading)\cdot\vec{waterSpeed}" title="\vec{correction}=\mathbb{R}(-heading)\cdot\vec{groundSpeed}-\vec{boatSpeed}-\mathbb{R}(-heading)\cdot\vec{waterSpeed}" />
|
|
10
|
+
|
|
11
|
+
### image
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## current model
|
|
16
|
+
### raw
|
|
17
|
+
\vec{waterSpeed} = \vec{groundSpeed} - \mathbb{R}(heading) \cdot(\vec{boatSpeed}+\vec{correction})
|
|
18
|
+
### link
|
|
19
|
+
https://latex.codecogs.com/svg.image?\vec{waterSpeed}=\vec{groundSpeed}-\mathbb{R}(heading)\cdot(\vec{boatSpeed}+\vec{correction})
|
|
20
|
+
### html
|
|
21
|
+
<img src="https://latex.codecogs.com/svg.image?\vec{waterSpeed}=\vec{groundSpeed}-\mathbb{R}(heading)\cdot(\vec{boatSpeed}+\vec{correction})" title="\vec{waterSpeed}=\vec{groundSpeed}-\mathbb{R}(heading)\cdot(\vec{boatSpeed}+\vec{correction})" />
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### image
|
|
25
|
+
|
|
26
|
+

|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "speedandcurrent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An experimental plugin that uses sensor fusion to get boat speed, current and leeway.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"author": "aswin.bouwmeester@gmail.com",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Asw1n/speedandcurrent.git",
|
|
14
|
+
"directory": "./CHANGELOG.md"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/Asw1n/speedandcurrent/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/Asw1n/speedandcurrent#readme",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"signalk-webapp",
|
|
22
|
+
"signalk-node-server-plugin",
|
|
23
|
+
"signalk-category-utility"
|
|
24
|
+
],
|
|
25
|
+
"signalk-plugin-enabled-by-default": false,
|
|
26
|
+
"signalk": {
|
|
27
|
+
"appIcon": "./icon.png",
|
|
28
|
+
"displayName": "Speed and current"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"kalman-filter": "^2.3.0",
|
|
32
|
+
"signalkutilities": "^1.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|