signalk-polar-performance-plugin 0.0.13 → 0.0.15
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 +6 -2
- package/package.json +1 -1
- package/plugin/index.js +139 -56
package/README.md
CHANGED
|
@@ -17,8 +17,11 @@ The polar diagram can be configured through CSV notation as used on [ORC sailboa
|
|
|
17
17
|
|
|
18
18
|
### Plugin options
|
|
19
19
|
In the plugin configuration you can toggle the following options:
|
|
20
|
-
- Enable calculation of beat/upwind and run/gybe/downwind angle
|
|
21
|
-
- Enable calculation of beat/upwind and run/gybe/downwind VMG
|
|
20
|
+
- Enable calculation/sending of beat/upwind and run/gybe/downwind angle
|
|
21
|
+
- Enable calculation/sending of beat/upwind and run/gybe/downwind VMG
|
|
22
|
+
- Enable sending Target TWA
|
|
23
|
+
- Enable calculation of Optimum Wind Angle (difference between TWA and beat/run angle (depends on beat/run angle)
|
|
24
|
+
- Enable sending of maximum speed angle and boat speed for a given TWS
|
|
22
25
|
- Enable calculation of Optimum Wind Angle (difference between TWA and beat/run angle (depends on beat/run angle)
|
|
23
26
|
|
|
24
27
|
## Calculated performance data
|
|
@@ -40,6 +43,7 @@ In the plugin configuration you can toggle the following options:
|
|
|
40
43
|
- Make moment to do calculation smarter/configurable
|
|
41
44
|
- API to see JSON of polar
|
|
42
45
|
- Visualisation of the polar diagram
|
|
46
|
+
- Extrapolation of polar data
|
|
43
47
|
- Create polar from live data
|
|
44
48
|
-- Save polar info to file
|
|
45
49
|
-- Save new record speed for angle in polar
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -18,11 +18,19 @@ module.exports = function (app) {
|
|
|
18
18
|
properties: {
|
|
19
19
|
beatAngle: {
|
|
20
20
|
type: 'boolean',
|
|
21
|
-
title: 'Enable calculation of beat/upwind and run/gybe/downwind angle'
|
|
21
|
+
title: 'Enable calculation/sending of beat/upwind and run/gybe/downwind angle'
|
|
22
22
|
},
|
|
23
23
|
beatVMG: {
|
|
24
24
|
type: 'boolean',
|
|
25
|
-
title: 'Enable calculation of beat/upwind and run/gybe/downwind VMG'
|
|
25
|
+
title: 'Enable calculation/sending of beat/upwind and run/gybe/downwind VMG'
|
|
26
|
+
},
|
|
27
|
+
targetTWA: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
title: 'Enable sending Target TWA (performance.targetAngle)'
|
|
30
|
+
},
|
|
31
|
+
tackTrue: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
title: 'Enable calculating Opposite Tack True (performance.tackTrue)'
|
|
26
34
|
},
|
|
27
35
|
optimumWindAngle: {
|
|
28
36
|
type: 'boolean',
|
|
@@ -60,7 +68,7 @@ module.exports = function (app) {
|
|
|
60
68
|
app.debug('polar: %s', JSON.stringify(polar))
|
|
61
69
|
|
|
62
70
|
// Global variables
|
|
63
|
-
var BSP, STW, TWA, TWS, port // Angles in rad, speed in m/s
|
|
71
|
+
var BSP, STW, TWA, targetTWA, TWS, HDG, port // Angles in rad, speed in m/s
|
|
64
72
|
var halfPi = Math.PI / 2
|
|
65
73
|
|
|
66
74
|
// Subscribe to paths
|
|
@@ -89,6 +97,12 @@ module.exports = function (app) {
|
|
|
89
97
|
policy: 'instant'
|
|
90
98
|
})
|
|
91
99
|
}
|
|
100
|
+
if (options.tackTrue == true) {
|
|
101
|
+
localSubscription.subscribe.push({
|
|
102
|
+
path: 'navigation.headingTrue',
|
|
103
|
+
policy: 'instant'
|
|
104
|
+
})
|
|
105
|
+
}
|
|
92
106
|
|
|
93
107
|
app.subscriptionmanager.subscribe(
|
|
94
108
|
localSubscription,
|
|
@@ -116,6 +130,9 @@ module.exports = function (app) {
|
|
|
116
130
|
SOG = delta.value
|
|
117
131
|
BSP = SOG
|
|
118
132
|
// app.debug('speedThroughWater (STW): %d', STW)
|
|
133
|
+
} else if (delta.path == 'navigation.headingTrue') {
|
|
134
|
+
HDG = delta.value
|
|
135
|
+
// app.debug('heading (HDG): %d', HDG)
|
|
119
136
|
} else if (delta.path == 'environment.wind.speedTrue') {
|
|
120
137
|
TWS = delta.value
|
|
121
138
|
// app.debug('environment.wind.speedTrue (TWS): %d', TWS)
|
|
@@ -136,22 +153,34 @@ module.exports = function (app) {
|
|
|
136
153
|
let values = []
|
|
137
154
|
let metas = []
|
|
138
155
|
if (typeof perfObj.beatAngle != 'undefined') {
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
if (options.beatAngle == true) {
|
|
157
|
+
values.push({path: 'performance.beatAngle', value: roundDec(perfObj.beatAngle)})
|
|
158
|
+
}
|
|
159
|
+
if (options.targetTWA == true) {
|
|
160
|
+
values.push({path: 'performance.targetAngle', value: roundDec(perfObj.beatAngle)})
|
|
161
|
+
}
|
|
141
162
|
}
|
|
142
163
|
if (typeof perfObj.beatVMG != 'undefined') {
|
|
143
164
|
values.push({path: 'performance.beatAngleVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
if (options.targetTWA == true) {
|
|
166
|
+
values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.beatVMG)})
|
|
167
|
+
metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
|
|
168
|
+
}
|
|
146
169
|
}
|
|
147
170
|
if (typeof perfObj.runAngle != 'undefined') {
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
if (options.beatAngle == true) {
|
|
172
|
+
values.push({path: 'performance.gybeAngle', value: roundDec(perfObj.runAngle)})
|
|
173
|
+
}
|
|
174
|
+
if (options.targetTWA == true) {
|
|
175
|
+
values.push({path: 'performance.targetAngle', value: roundDec(perfObj.runAngle)})
|
|
176
|
+
}
|
|
150
177
|
}
|
|
151
178
|
if (typeof perfObj.runVMG != 'undefined') {
|
|
152
179
|
values.push({path: 'performance.gybeAngleVelocityMadeGood', value: roundDec(perfObj.runVMG)})
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
if (options.targetTWA == true) {
|
|
181
|
+
values.push({path: 'performance.targetVelocityMadeGood', value: roundDec(perfObj.runVMG)})
|
|
182
|
+
metas.push({path: 'performance.targetVelocityMadeGood', value: {"units": "rad"}})
|
|
183
|
+
}
|
|
155
184
|
}
|
|
156
185
|
if (typeof perfObj.optimumWindAngle != 'undefined') {
|
|
157
186
|
values.push({path: 'performance.optimumWindAngle', value: roundDec(perfObj.optimumWindAngle)})
|
|
@@ -170,6 +199,11 @@ module.exports = function (app) {
|
|
|
170
199
|
values.push({path: 'performance.maxSpeedAngle', value: roundDec(perfObj.maxSpeedAngle)})
|
|
171
200
|
metas.push({path: 'performance.maxSpeedAngle', value: {"units": "rad"}})
|
|
172
201
|
}
|
|
202
|
+
if (options.tackTrue == true) {
|
|
203
|
+
if (typeof perfObj.tackTrue != 'undefined') {
|
|
204
|
+
values.push({path: 'performance.tackTrue', value: roundDec(perfObj.tackTrue)})
|
|
205
|
+
}
|
|
206
|
+
}
|
|
173
207
|
|
|
174
208
|
app.debug('sendUpdates: %s', JSON.stringify(values))
|
|
175
209
|
app.handleMessage(plugin.id, {
|
|
@@ -197,13 +231,12 @@ module.exports = function (app) {
|
|
|
197
231
|
// Calculate beat/run angle
|
|
198
232
|
if (TWA < halfPi) {
|
|
199
233
|
// app.debug('Upwind')
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
234
|
+
// Take beat angle
|
|
235
|
+
if (typeof polar[indexTWS]['Beat angle'] != 'undefined') {
|
|
236
|
+
let beatLower = polar[indexTWS]['Beat angle']
|
|
237
|
+
let beatUpper = polar[indexTWS+1]['Beat angle']
|
|
238
|
+
performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
|
|
239
|
+
targetTWA = performance.beatAngle
|
|
207
240
|
}
|
|
208
241
|
// Calculate optimum wind angle (B&G thing)
|
|
209
242
|
if (options.optimumWindAngle == true) {
|
|
@@ -212,23 +245,20 @@ module.exports = function (app) {
|
|
|
212
245
|
}
|
|
213
246
|
}
|
|
214
247
|
// Calculate beat VMG
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
220
|
-
}
|
|
248
|
+
if (typeof polar[indexTWS]['Beat VMG'] != 'undefined') {
|
|
249
|
+
let VMGLower = polar[indexTWS]['Beat VMG']
|
|
250
|
+
let VMGUpper = polar[indexTWS+1]['Beat VMG']
|
|
251
|
+
performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
221
252
|
}
|
|
222
253
|
} else {
|
|
223
254
|
// app.debug('Downwind')
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
255
|
+
if (typeof polar[indexTWS]['Run angle'] != 'undefined') {
|
|
256
|
+
// Calculate run angle
|
|
257
|
+
let runLower = polar[indexTWS]['Run angle']
|
|
258
|
+
let runUpper = polar[indexTWS+1]['Run angle']
|
|
259
|
+
//app.debug('runLower: %s runUpper: %s', runLower, runUpper)
|
|
260
|
+
performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
|
|
261
|
+
targetTWA = performance.runAngle
|
|
232
262
|
}
|
|
233
263
|
if (options.optimumWindAngle == true) {
|
|
234
264
|
if (typeof performance.runAngle != 'undefined') {
|
|
@@ -236,16 +266,29 @@ module.exports = function (app) {
|
|
|
236
266
|
performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
|
|
237
267
|
}
|
|
238
268
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
246
|
-
}
|
|
269
|
+
// Calculate run VMG
|
|
270
|
+
if (typeof polar[indexTWS]['Run VMG'] != 'undefined') {
|
|
271
|
+
let VMGLower = polar[indexTWS]['Run VMG']
|
|
272
|
+
let VMGUpper = polar[indexTWS+1]['Run VMG']
|
|
273
|
+
//app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
|
|
274
|
+
performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
247
275
|
}
|
|
248
276
|
}
|
|
277
|
+
// Calculate opposite Tack True
|
|
278
|
+
// app.debug('tackTrue: port: %d HDG: %d targetTWA: %d', port, radToDeg(HDG), radToDeg(targetTWA))
|
|
279
|
+
if (port) {
|
|
280
|
+
let tackTrue = HDG - targetTWA
|
|
281
|
+
if (tackTrue < 0) {
|
|
282
|
+
tackTrue = tackTrue + (2*Math.PI)
|
|
283
|
+
}
|
|
284
|
+
performance.tackTrue = tackTrue
|
|
285
|
+
} else {
|
|
286
|
+
let tackTrue = HDG + targetTWA
|
|
287
|
+
if (tackTrue > (2*Math.PI)) {
|
|
288
|
+
tackTrue = tackTrue - (2*Math.PI)
|
|
289
|
+
}
|
|
290
|
+
performance.tackTrue = tackTrue
|
|
291
|
+
}
|
|
249
292
|
// Calculate polar target boat speed
|
|
250
293
|
|
|
251
294
|
// Interpolate Define the 4 near data points
|
|
@@ -348,10 +391,10 @@ module.exports = function (app) {
|
|
|
348
391
|
app.debug('beat and run angles are included')
|
|
349
392
|
} else if (row.includes('0')) {
|
|
350
393
|
// Beat / Run angle
|
|
351
|
-
let angle = Number(row[0])
|
|
394
|
+
let angle = degToRad(Number(row[0]))
|
|
352
395
|
let angleName
|
|
353
396
|
let VMGName
|
|
354
|
-
if (angle <
|
|
397
|
+
if (angle < halfPi) {
|
|
355
398
|
angleName = 'Beat angle'
|
|
356
399
|
VMGName = 'Beat VMG'
|
|
357
400
|
} else {
|
|
@@ -360,17 +403,18 @@ module.exports = function (app) {
|
|
|
360
403
|
}
|
|
361
404
|
for (let index = 0; index < row.length-1; index++) {
|
|
362
405
|
if (row[index+1] != 0) {
|
|
363
|
-
polar[index][angleName] =
|
|
364
|
-
|
|
406
|
+
polar[index][angleName] = angle
|
|
407
|
+
let tbs = ktsToMs(Number(row[index+1]))
|
|
408
|
+
let vmg = tbs * Math.abs(Math.cos(angle))
|
|
409
|
+
polar[index][VMGName] = roundDec(vmg)
|
|
365
410
|
if (typeof polar[index]['twa'] == 'undefined') {
|
|
366
411
|
polar[index]['twa'] = []
|
|
367
412
|
}
|
|
368
|
-
let
|
|
369
|
-
let Obj = {"twa": degToRad(angle), "tbs": tbs}
|
|
413
|
+
let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
|
|
370
414
|
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
371
415
|
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
372
416
|
polar[index]['Max speed'] = tbs
|
|
373
|
-
polar[index]['Max speed angle'] =
|
|
417
|
+
polar[index]['Max speed angle'] = angle
|
|
374
418
|
}
|
|
375
419
|
}
|
|
376
420
|
}
|
|
@@ -382,9 +426,13 @@ module.exports = function (app) {
|
|
|
382
426
|
polar[index].twa = []
|
|
383
427
|
}
|
|
384
428
|
let tbs = ktsToMs(Number(row[index+1]))
|
|
385
|
-
let
|
|
386
|
-
|
|
429
|
+
let vmg = tbs * Math.abs(Math.cos(angle))
|
|
430
|
+
let Obj = {"twa": angle, "tbs": tbs, "vmg": vmg}
|
|
431
|
+
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
387
432
|
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
433
|
+
// See if we need to set/overwrite beat/run angle
|
|
434
|
+
|
|
435
|
+
|
|
388
436
|
// See if this a new Max speed
|
|
389
437
|
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
390
438
|
polar[index]['Max speed'] = tbs
|
|
@@ -402,34 +450,68 @@ module.exports = function (app) {
|
|
|
402
450
|
tws.twa = twaArray
|
|
403
451
|
})
|
|
404
452
|
|
|
453
|
+
// Find the beat/run angle if not set yet.
|
|
454
|
+
if (typeof polar[0].beatAngle == 'undefined') {
|
|
455
|
+
for (let index = 0; index < polar.length-1; index++) {
|
|
456
|
+
let tws = polar[index].tws
|
|
457
|
+
let beatVMG = 0
|
|
458
|
+
let beatElement = 0
|
|
459
|
+
let runVMG = 0
|
|
460
|
+
let runElement = 0
|
|
461
|
+
|
|
462
|
+
let twaArray = polar[index].twa
|
|
463
|
+
for (let element = 0; element < twaArray.length-1; element++) {
|
|
464
|
+
let Obj = twaArray[element]
|
|
465
|
+
if (Obj.twa < halfPi) {
|
|
466
|
+
// Beat
|
|
467
|
+
if (Obj.vmg > beatVMG) {
|
|
468
|
+
beatElement = element
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
// Run
|
|
472
|
+
if (Obj.vmg > runVMG) {
|
|
473
|
+
runElement = element
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
app.debug('beatVMG for %d is %d (angel %d)', tws, twaArray[beatElement].vmg, twaArray[beatElement].twa)
|
|
478
|
+
app.debug(' runVMG for %d is %d (angel %d)', tws, twaArray[runElement].vmg, twaArray[runElement].twa)
|
|
479
|
+
polar[index]['Beat angle'] = twaArray[beatElement].twa
|
|
480
|
+
polar[index]['Beat VMG'] = twaArray[beatElement].vmg
|
|
481
|
+
polar[index]['Run angle'] = twaArray[runElement].twa
|
|
482
|
+
polar[index]['Run VMG'] = twaArray[runElement].vmg
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
}
|
|
486
|
+
|
|
405
487
|
// And now fill in some missing ends to avoid doing expensive calculations in the main loop
|
|
406
488
|
for (let index = 0; index < polar.length-1; index++) {
|
|
407
489
|
// Sorted on angle, so first is lowest
|
|
490
|
+
let tws = polar[index].tws
|
|
408
491
|
let twaArray = polar[index].twa
|
|
409
492
|
let lowTBS = twaArray[0].tbs
|
|
410
493
|
let lowTWA = twaArray[0].twa
|
|
411
494
|
|
|
412
|
-
|
|
495
|
+
app.debug('Padding polar for %s from 0 to first given angle (%d deg, %d kts)', msToKts(tws).toFixed(0), radToDeg(lowTWA), msToKts(lowTBS))
|
|
413
496
|
|
|
414
497
|
// Now put some extra values at the beginning
|
|
415
498
|
var topArray = []
|
|
416
499
|
for (let angle = 0; angle < lowTWA; angle = angle + degToRad(5)) {
|
|
417
500
|
let tbs = (angle / lowTWA) * Math.pow(Math.cos((-1*lowTWA + angle)*3),3) * lowTBS
|
|
418
501
|
let Obj = {'twa': angle, 'tbs': tbs}
|
|
419
|
-
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
420
502
|
topArray.push(Obj)
|
|
421
|
-
// app.debug('twaArray: %s', JSON.stringify(twaArray))
|
|
422
503
|
}
|
|
423
|
-
|
|
424
|
-
|
|
504
|
+
// app.debug('Adding values: %s', JSON.stringify(topArray))
|
|
505
|
+
polar[index].twa = topArray.concat(twaArray)
|
|
425
506
|
}
|
|
426
507
|
|
|
427
508
|
for (let index = 0; index < polar.length-1; index++) {
|
|
509
|
+
let tws = polar[index].tws
|
|
428
510
|
let twaArray = polar[index].twa
|
|
429
511
|
let highTBS = twaArray[twaArray.length-1].tbs
|
|
430
512
|
let highTWA = twaArray[twaArray.length-1].twa
|
|
431
513
|
|
|
432
|
-
|
|
514
|
+
app.debug('Padding polar for %s from highTWA to last given angle (%d, %d kts)', msToKts(tws).toFixed(0), highTWA, msToKts(highTBS))
|
|
433
515
|
|
|
434
516
|
// Now put some extra values at the beginning
|
|
435
517
|
var tailArray = []
|
|
@@ -438,7 +520,8 @@ module.exports = function (app) {
|
|
|
438
520
|
let Obj = {'twa': angle, 'tbs': tbs}
|
|
439
521
|
tailArray.unshift(Obj)
|
|
440
522
|
}
|
|
441
|
-
|
|
523
|
+
// app.debug('Adding values: %s', JSON.stringify(tailArray))
|
|
524
|
+
polar[index].twa = twaArray.concat(tailArray)
|
|
442
525
|
}
|
|
443
526
|
|
|
444
527
|
// app.debug(JSON.stringify(polar))
|