signalk-polar-performance-plugin 0.0.10 → 0.0.13
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 -5
- package/package.json +1 -1
- package/plugin/index.js +139 -65
package/README.md
CHANGED
|
@@ -33,11 +33,13 @@ In the plugin configuration you can toggle the following options:
|
|
|
33
33
|
- Polar Speed Ratio (performance.polarSpeedRatio)
|
|
34
34
|
- Plugin option to use SOG as boat speed
|
|
35
35
|
- Wind angle for maximum speed at this wind speed
|
|
36
|
+
- Fill up the ends of the polar diagram
|
|
36
37
|
|
|
37
38
|
### To-do list
|
|
38
|
-
- Fill up the ends of the polar diagram
|
|
39
39
|
- Improved interpolation
|
|
40
40
|
- Make moment to do calculation smarter/configurable
|
|
41
|
+
- API to see JSON of polar
|
|
42
|
+
- Visualisation of the polar diagram
|
|
41
43
|
- Create polar from live data
|
|
42
44
|
-- Save polar info to file
|
|
43
45
|
-- Save new record speed for angle in polar
|
|
@@ -46,7 +48,6 @@ In the plugin configuration you can toggle the following options:
|
|
|
46
48
|
-- Configure the resolution of the polar diagram
|
|
47
49
|
- Support multiple polar diagrams
|
|
48
50
|
- Capture heel in polar diagram
|
|
49
|
-
- Visualisation of the polar diagram
|
|
50
51
|
|
|
51
52
|
## MFD configuration
|
|
52
53
|
|
|
@@ -59,15 +60,15 @@ To get the values calculated by this plugin from SignalK to your B&G MFD/Triton2
|
|
|
59
60
|
To see lay lines you need to set:
|
|
60
61
|
- Settings -> Chart -> Laylines -> Targets... -> True wind angle to 'Actual'
|
|
61
62
|
|
|
62
|
-

|
|
63
|
+

|
|
63
64
|
|
|
64
65
|
- SailSteer screen -> Long press tile to add 'Performance -> Target TWA -> decollapse, choose SignalK'
|
|
65
66
|
|
|
66
|
-

|
|
67
|
+

|
|
67
68
|
|
|
68
69
|
Now the Target TWA is coming from SignalK and the laylines will be drawn based on it's value.
|
|
69
70
|
|
|
70
|
-

|
|
71
|
+

|
|
71
72
|
|
|
72
73
|
### Raymarine
|
|
73
74
|
If you have a Raymarine MFD and can tell more about this, please add to the README or tell me.
|
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -61,8 +61,7 @@ module.exports = function (app) {
|
|
|
61
61
|
|
|
62
62
|
// Global variables
|
|
63
63
|
var BSP, STW, TWA, TWS, port // Angles in rad, speed in m/s
|
|
64
|
-
var
|
|
65
|
-
var halfPi = Pi / 2
|
|
64
|
+
var halfPi = Math.PI / 2
|
|
66
65
|
|
|
67
66
|
// Subscribe to paths
|
|
68
67
|
let localSubscription = {
|
|
@@ -195,79 +194,106 @@ module.exports = function (app) {
|
|
|
195
194
|
let gap = upper - lower
|
|
196
195
|
let twsGapRatio = (1 / gap) * (TWS - lower)
|
|
197
196
|
//app.debug('twsGapRatio: %d', twsGapRatio)
|
|
198
|
-
|
|
197
|
+
// Calculate beat/run angle
|
|
199
198
|
if (TWA < halfPi) {
|
|
200
199
|
// app.debug('Upwind')
|
|
201
200
|
if (options.beatAngle == true) {
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
// Take beat angle
|
|
202
|
+
if (typeof polar[indexTWS]['Beat angle'] != 'undefined') {
|
|
203
|
+
let beatLower = polar[indexTWS]['Beat angle']
|
|
204
|
+
let beatUpper = polar[indexTWS+1]['Beat angle']
|
|
205
|
+
performance.beatAngle = beatLower + ((beatUpper - beatLower) * twsGapRatio)
|
|
206
|
+
}
|
|
206
207
|
}
|
|
207
208
|
// Calculate optimum wind angle (B&G thing)
|
|
208
209
|
if (options.optimumWindAngle == true) {
|
|
209
|
-
|
|
210
|
+
if (typeof performance.beatAngle != 'undefined') {
|
|
211
|
+
performance.optimumWindAngle = (TWA - performance.beatAngle) * port
|
|
212
|
+
}
|
|
210
213
|
}
|
|
211
214
|
// Calculate beat VMG
|
|
212
215
|
if (options.beatVMG == true) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
if (typeof polar[indexTWS]['Beat VMG'] != 'undefined') {
|
|
217
|
+
let VMGLower = polar[indexTWS]['Beat VMG']
|
|
218
|
+
let VMGUpper = polar[indexTWS+1]['Beat VMG']
|
|
219
|
+
performance.beatVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
220
|
+
}
|
|
216
221
|
}
|
|
217
222
|
} else {
|
|
218
223
|
// app.debug('Downwind')
|
|
219
224
|
if (options.beatAngle == true) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
if (typeof polar[indexTWS]['Run angle'] != 'undefined') {
|
|
226
|
+
// Calculate run angle
|
|
227
|
+
let runLower = polar[indexTWS]['Run angle']
|
|
228
|
+
let runUpper = polar[indexTWS+1]['Run angle']
|
|
229
|
+
//app.debug('runLower: %s runUpper: %s', runLower, runUpper)
|
|
230
|
+
performance.runAngle = runLower + ((runUpper - runLower) * twsGapRatio)
|
|
231
|
+
}
|
|
225
232
|
}
|
|
226
233
|
if (options.optimumWindAngle == true) {
|
|
227
|
-
|
|
228
|
-
|
|
234
|
+
if (typeof performance.runAngle != 'undefined') {
|
|
235
|
+
// Calculate optimum wind angle
|
|
236
|
+
performance.optimumWindAngle = (performance.runAngle - TWA) * port * -1
|
|
237
|
+
}
|
|
229
238
|
}
|
|
230
239
|
if (options.beatVMG == true) {
|
|
231
240
|
// Calculate run VMG
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
if (typeof polar[indexTWS]['Run VMG'] != 'undefined') {
|
|
242
|
+
let VMGLower = polar[indexTWS]['Run VMG']
|
|
243
|
+
let VMGUpper = polar[indexTWS+1]['Run VMG']
|
|
234
244
|
//app.debug('VMGLower: %s VMGUpper: %s', VMGLower, VMGUpper)
|
|
235
|
-
|
|
245
|
+
performance.runVMG = VMGLower + ((VMGUpper - VMGLower) * twsGapRatio)
|
|
246
|
+
}
|
|
236
247
|
}
|
|
237
248
|
}
|
|
238
249
|
// Calculate polar target boat speed
|
|
239
250
|
|
|
240
|
-
// Define the 4 near data points
|
|
251
|
+
// Interpolate Define the 4 near data points
|
|
241
252
|
let lowerTWA = polar[indexTWS].twa
|
|
242
253
|
let upperTWA = polar[indexTWS+1].twa
|
|
243
254
|
// app.debug('TWAlower: %s', JSON.stringify(TWAlower))
|
|
255
|
+
// First find lowerTWA
|
|
244
256
|
for (let indexTWA = 0 ; indexTWA < lowerTWA.length-1; indexTWA++) {
|
|
245
257
|
let lowerTWAlower = lowerTWA[indexTWA]
|
|
246
258
|
let lowerTWAupper = lowerTWA[indexTWA+1]
|
|
247
|
-
let upperTWAlower = upperTWA[indexTWA]
|
|
248
|
-
let upperTWAupper = upperTWA[indexTWA+1]
|
|
249
|
-
|
|
250
259
|
// app.debug('lowerTWAlower: %s lowerTWAupper: %s', lowerTWAlower, lowerTWAupper)
|
|
251
260
|
if (TWA >= lowerTWAlower.twa && TWA <= lowerTWAupper.twa) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
// Now find upperTWA
|
|
262
|
+
for (let indexTWA = 0 ; indexTWA < upperTWA.length-1; indexTWA++) {
|
|
263
|
+
let upperTWAlower = upperTWA[indexTWA]
|
|
264
|
+
let upperTWAupper = upperTWA[indexTWA+1]
|
|
265
|
+
if (TWA >= upperTWAlower.twa && TWA <= upperTWAupper.twa) {
|
|
266
|
+
// Found the 4 points
|
|
267
|
+
// app.debug('lowerTWAlower: %d TWA: %d lowerTWAupper: %d', lowerTWAlower.twa, TWA, lowerTWAupper.twa)
|
|
268
|
+
// Calculate gap ratio
|
|
269
|
+
let gap = lowerTWAupper.twa - lowerTWAlower.twa
|
|
270
|
+
let twaGapRatio = (1 / gap) * (TWA - lowerTWAlower.twa)
|
|
271
|
+
// app.debug('twaGapRatio: %d', twaGapRatio)
|
|
272
|
+
// Calculate lower tws boat speed
|
|
273
|
+
let lowerTBS = lowerTWAlower.tbs + ((lowerTWAupper.tbs - lowerTWAlower.tbs) * twaGapRatio)
|
|
274
|
+
// Calculate upper tws boat speed
|
|
275
|
+
let upperTBS = upperTWAlower.tbs + ((upperTWAupper.tbs - upperTWAlower.tbs) * twaGapRatio)
|
|
276
|
+
// Calculate polar boat speed
|
|
277
|
+
performance.polarSpeed = lowerTBS + ((upperTBS - lowerTBS) * twsGapRatio)
|
|
278
|
+
// app.debug('lowerTBS: %d TBS: %d upperTBS: %d', lowerTBS, performance.polarSpeed, upperTBS)
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
268
282
|
break
|
|
269
283
|
}
|
|
270
284
|
}
|
|
285
|
+
if (typeof performance.polarSpeed == 'undefined') {
|
|
286
|
+
if (TWA < performance.beatAngle) {
|
|
287
|
+
// In case TWA < lowest polar angle
|
|
288
|
+
app.debug('Low angle %d not present in table', radToDeg(TWA))
|
|
289
|
+
// performance.polarSpeed =
|
|
290
|
+
} else if (TWA > performance.runAngle) {
|
|
291
|
+
// In case TWA > highest polar angle
|
|
292
|
+
app.debug('High angle %d not present in table', radToDeg(TWA))
|
|
293
|
+
// performance.polarSpeed =
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
271
297
|
if (options.maxSpeed == true) {
|
|
272
298
|
let lowerMax = polar[indexTWS]['Max speed']
|
|
273
299
|
let upperMax = polar[indexTWS+1]['Max speed']
|
|
@@ -278,10 +304,20 @@ module.exports = function (app) {
|
|
|
278
304
|
let maxSpeedAngle = lowerMaxAngle + ((upperMaxAngle - lowerMaxAngle) * twsGapRatio)
|
|
279
305
|
performance.maxSpeedAngle = maxSpeedAngle
|
|
280
306
|
}
|
|
281
|
-
} else {
|
|
282
|
-
|
|
307
|
+
} else if (indexTWS == 0 && TWS < lower) {
|
|
308
|
+
app.debug('No data for low wind speed (%d kts)', msToKts(TWS))
|
|
309
|
+
} else if (indexTWS == polar.length-1 && TWS > upper) {
|
|
310
|
+
app.debug('No data for high wind speed (%d kts)', msToKts(TWS))
|
|
283
311
|
}
|
|
284
312
|
}
|
|
313
|
+
// Calculate polar performance ratio
|
|
314
|
+
if (typeof performance.polarSpeed != 'undefined') {
|
|
315
|
+
performance.polarSpeedRatio = (1 / performance.polarSpeed) * BSP
|
|
316
|
+
} else {
|
|
317
|
+
// No value would create stale values
|
|
318
|
+
performance.polarSpeed = 0
|
|
319
|
+
performance.polarSpeedRatio = 0
|
|
320
|
+
}
|
|
285
321
|
return performance
|
|
286
322
|
}
|
|
287
323
|
|
|
@@ -322,35 +358,37 @@ module.exports = function (app) {
|
|
|
322
358
|
angleName = 'Run angle'
|
|
323
359
|
VMGName = 'Run VMG'
|
|
324
360
|
}
|
|
325
|
-
for (let index =
|
|
326
|
-
if (row[index] != 0) {
|
|
327
|
-
polar[index
|
|
328
|
-
polar[index
|
|
329
|
-
if (typeof polar[index
|
|
330
|
-
polar[index
|
|
361
|
+
for (let index = 0; index < row.length-1; index++) {
|
|
362
|
+
if (row[index+1] != 0) {
|
|
363
|
+
polar[index][angleName] = degToRad(angle)
|
|
364
|
+
polar[index][VMGName] = roundDec(Number((row[index+1]) * Math.abs(Math.cos(angle))))
|
|
365
|
+
if (typeof polar[index]['twa'] == 'undefined') {
|
|
366
|
+
polar[index]['twa'] = []
|
|
331
367
|
}
|
|
332
|
-
let tbs = ktsToMs(Number(row[index]))
|
|
368
|
+
let tbs = ktsToMs(Number(row[index+1]))
|
|
333
369
|
let Obj = {"twa": degToRad(angle), "tbs": tbs}
|
|
334
|
-
polar[index
|
|
335
|
-
if (typeof polar[index
|
|
336
|
-
polar[index
|
|
337
|
-
polar[index
|
|
370
|
+
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
371
|
+
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
372
|
+
polar[index]['Max speed'] = tbs
|
|
373
|
+
polar[index]['Max speed angle'] = degToRad(angle)
|
|
338
374
|
}
|
|
339
375
|
}
|
|
340
376
|
}
|
|
341
377
|
} else {
|
|
342
378
|
// Normal line
|
|
343
379
|
let angle = degToRad(Number(row[0])) // CSV deg to internal rad
|
|
344
|
-
for (let index =
|
|
345
|
-
if (typeof polar[index
|
|
346
|
-
polar[index
|
|
380
|
+
for (let index = 0; index < row.length-1; index++) {
|
|
381
|
+
if (typeof polar[index].twa == 'undefined') {
|
|
382
|
+
polar[index].twa = []
|
|
347
383
|
}
|
|
348
|
-
let tbs = ktsToMs(Number(row[index]))
|
|
384
|
+
let tbs = ktsToMs(Number(row[index+1]))
|
|
349
385
|
let Obj = {"twa": angle, "tbs": tbs}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
386
|
+
app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
387
|
+
polar[index]['twa'] = polar[index]['twa'].concat(Obj)
|
|
388
|
+
// See if this a new Max speed
|
|
389
|
+
if (typeof polar[index]['Max speed'] == 'undefined' || tbs > polar[index]['Max speed']) {
|
|
390
|
+
polar[index]['Max speed'] = tbs
|
|
391
|
+
polar[index]['Max speed angle'] = angle
|
|
354
392
|
}
|
|
355
393
|
}
|
|
356
394
|
}
|
|
@@ -364,14 +402,50 @@ module.exports = function (app) {
|
|
|
364
402
|
tws.twa = twaArray
|
|
365
403
|
})
|
|
366
404
|
|
|
405
|
+
// And now fill in some missing ends to avoid doing expensive calculations in the main loop
|
|
406
|
+
for (let index = 0; index < polar.length-1; index++) {
|
|
407
|
+
// Sorted on angle, so first is lowest
|
|
408
|
+
let twaArray = polar[index].twa
|
|
409
|
+
let lowTBS = twaArray[0].tbs
|
|
410
|
+
let lowTWA = twaArray[0].twa
|
|
411
|
+
|
|
412
|
+
// app.debug('Padding polar from 0 to first given angle (%d deg, %d kts) to twaArray %s' , radToDeg(lowTWA), msToKts(lowTBS), JSON.stringify(twaArray))
|
|
413
|
+
|
|
414
|
+
// Now put some extra values at the beginning
|
|
415
|
+
var topArray = []
|
|
416
|
+
for (let angle = 0; angle < lowTWA; angle = angle + degToRad(5)) {
|
|
417
|
+
let tbs = (angle / lowTWA) * Math.pow(Math.cos((-1*lowTWA + angle)*3),3) * lowTBS
|
|
418
|
+
let Obj = {'twa': angle, 'tbs': tbs}
|
|
419
|
+
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
420
|
+
topArray.push(Obj)
|
|
421
|
+
// app.debug('twaArray: %s', JSON.stringify(twaArray))
|
|
422
|
+
}
|
|
423
|
+
twaArray = topArray.concat(twaArray)
|
|
424
|
+
// app.debug('twaArray: %s', JSON.stringify(polar[index].twa))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (let index = 0; index < polar.length-1; index++) {
|
|
428
|
+
let twaArray = polar[index].twa
|
|
429
|
+
let highTBS = twaArray[twaArray.length-1].tbs
|
|
430
|
+
let highTWA = twaArray[twaArray.length-1].twa
|
|
431
|
+
|
|
432
|
+
// app.debug('Padding polar from highTWA to last given angle (%d, %d kts) to twaArray %s' , highTWA, msToKts(highTBS), JSON.stringify(twaArray))
|
|
433
|
+
|
|
434
|
+
// Now put some extra values at the beginning
|
|
435
|
+
var tailArray = []
|
|
436
|
+
for (let angle = Math.PI; angle > highTWA; angle = angle - degToRad(5)) {
|
|
437
|
+
let tbs = Math.pow(highTWA/angle, 2) * highTBS
|
|
438
|
+
let Obj = {'twa': angle, 'tbs': tbs}
|
|
439
|
+
tailArray.unshift(Obj)
|
|
440
|
+
}
|
|
441
|
+
twaArray = twaArray.concat(tailArray)
|
|
442
|
+
}
|
|
443
|
+
|
|
367
444
|
// app.debug(JSON.stringify(polar))
|
|
368
445
|
return (polar)
|
|
369
446
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
447
|
}
|
|
373
448
|
|
|
374
|
-
|
|
375
449
|
plugin.stop = function () {
|
|
376
450
|
// Here we put logic we need when the plugin stops
|
|
377
451
|
app.debug('Plugin stopped');
|