signalk-polar-performance-plugin 0.0.58 → 0.0.60
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/package.json +1 -1
- package/plugin/index.js +64 -14
- package/public/index.html +132 -30
- package/ratio_off.dump +8047 -0
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -116,13 +116,25 @@ module.exports = function (app) {
|
|
|
116
116
|
plugin.registerWithRouter = function (router) {
|
|
117
117
|
// Will appear here; plugins/signalk-polar-performance-plugin/
|
|
118
118
|
app.debug('registerWithRouter')
|
|
119
|
+
// Middleware to set Origin-Agent-Cluster on every response from this plugin
|
|
120
|
+
router.use((req, res, next) => {
|
|
121
|
+
res.set('Origin-Agent-Cluster', '?1')
|
|
122
|
+
next()
|
|
123
|
+
})
|
|
119
124
|
router.get('/polar', (req, res) => {
|
|
120
125
|
res.contentType('application/json')
|
|
121
126
|
res.send(JSON.stringify(polar))
|
|
122
127
|
})
|
|
123
128
|
router.get('/chartData', (req, res) => {
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
const chart = getChartData();
|
|
130
|
+
if (!chart || !chart.datasets || chart.datasets.length === 0) {
|
|
131
|
+
return res.status(500).json({
|
|
132
|
+
error: "No polar data",
|
|
133
|
+
reason: "Polar CSV is empty or invalid. Please configure a valid polar in plugin settings."
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
res.contentType('application/json');
|
|
137
|
+
res.send(JSON.stringify(chart));
|
|
126
138
|
})
|
|
127
139
|
}
|
|
128
140
|
|
|
@@ -472,7 +484,7 @@ module.exports = function (app) {
|
|
|
472
484
|
}
|
|
473
485
|
// Calculate opposite Tack True
|
|
474
486
|
// app.debug('tackTrue: port: %d HDG: %d targetTWA: %d', port, radToDeg(HDG), radToDeg(targetTWA))
|
|
475
|
-
if (port) {
|
|
487
|
+
if (port < 0) {
|
|
476
488
|
let tackTrue = HDG - targetTWA
|
|
477
489
|
if (tackTrue < 0) {
|
|
478
490
|
tackTrue = tackTrue + 2 * Math.PI
|
|
@@ -588,15 +600,39 @@ module.exports = function (app) {
|
|
|
588
600
|
var polar = {}
|
|
589
601
|
var beat = false
|
|
590
602
|
|
|
603
|
+
function upsertTwa (polarIndex, twaObj, angleDeg) {
|
|
604
|
+
const twaArray = polar[polarIndex].twa
|
|
605
|
+
const existingIndex = twaArray.findIndex(entry => entry.twa === twaObj.twa)
|
|
606
|
+
if (existingIndex >= 0) {
|
|
607
|
+
app.error(
|
|
608
|
+
'Duplicate TWA %s deg for TWS %s kts, keeping last value',
|
|
609
|
+
angleDeg,
|
|
610
|
+
msToKts(polar[polarIndex].tws).toFixed(1)
|
|
611
|
+
)
|
|
612
|
+
twaArray[existingIndex] = twaObj
|
|
613
|
+
} else {
|
|
614
|
+
twaArray.push(twaObj)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
591
618
|
// Create array of arrays from csv
|
|
592
619
|
csv.split('\n').forEach(row => {
|
|
593
|
-
|
|
620
|
+
const trimmedRow = row.trim()
|
|
621
|
+
if (!trimmedRow) {
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
const cells = trimmedRow.split(';').map(cell => cell.trim())
|
|
625
|
+
if (!cells[0]) {
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
csvArray.push(cells)
|
|
594
629
|
})
|
|
595
630
|
// app.debug('csvArray: %s', JSON.stringify(csvArray))
|
|
596
631
|
|
|
597
632
|
// Populate the polar object from CSV rows
|
|
598
633
|
csvArray.forEach(row => {
|
|
599
|
-
|
|
634
|
+
const rowKey = row[0].trim().toLowerCase()
|
|
635
|
+
if (rowKey == 'twa/tws') {
|
|
600
636
|
// The row with TWS columns
|
|
601
637
|
// Create empty TWS objects in polar object
|
|
602
638
|
app.debug('First row with TWS columns')
|
|
@@ -609,7 +645,8 @@ module.exports = function (app) {
|
|
|
609
645
|
} else if (row.filter(i => i === '0').length > 1) {
|
|
610
646
|
app.debug('beat and run angles are included')
|
|
611
647
|
// Beat / Run angle
|
|
612
|
-
let
|
|
648
|
+
let angleDeg = Number(row[0])
|
|
649
|
+
let angle = degToRad(angleDeg)
|
|
613
650
|
let angleName
|
|
614
651
|
let VMGName
|
|
615
652
|
app.debug('angle < halfPi %d < %d', angle, halfPi)
|
|
@@ -624,17 +661,23 @@ module.exports = function (app) {
|
|
|
624
661
|
}
|
|
625
662
|
|
|
626
663
|
for (let index = 0; index < row.length - 1; index++) {
|
|
627
|
-
if (row[index + 1]
|
|
664
|
+
if (!row[index + 1]) {
|
|
665
|
+
continue
|
|
666
|
+
}
|
|
667
|
+
const tbsCell = Number(row[index + 1])
|
|
668
|
+
if (Number.isNaN(tbsCell) || tbsCell === 0) {
|
|
669
|
+
continue
|
|
670
|
+
}
|
|
671
|
+
if (tbsCell != 0) {
|
|
628
672
|
polar[index][angleName] = angle
|
|
629
|
-
let tbs =
|
|
630
|
-
ktsToMs(Number(row[index + 1])) * (options.perfAdjust || 1)
|
|
673
|
+
let tbs = ktsToMs(tbsCell) * (options.perfAdjust || 1)
|
|
631
674
|
let vmg = tbs * Math.abs(Math.cos(angle))
|
|
632
675
|
polar[index][VMGName] = roundDec(vmg)
|
|
633
676
|
if (typeof polar[index]['twa'] == 'undefined') {
|
|
634
677
|
polar[index]['twa'] = []
|
|
635
678
|
}
|
|
636
679
|
let Obj = { twa: angle, tbs: tbs, vmg: vmg }
|
|
637
|
-
|
|
680
|
+
upsertTwa(index, Obj, angleDeg)
|
|
638
681
|
app.debug('Finding max speed')
|
|
639
682
|
if (
|
|
640
683
|
typeof polar[index]['Max speed'] == 'undefined' ||
|
|
@@ -648,17 +691,24 @@ module.exports = function (app) {
|
|
|
648
691
|
}
|
|
649
692
|
} else {
|
|
650
693
|
// Normal line
|
|
651
|
-
let
|
|
694
|
+
let angleDeg = Number(row[0])
|
|
695
|
+
let angle = degToRad(angleDeg) // CSV deg to internal rad
|
|
652
696
|
for (let index = 0; index < row.length - 1; index++) {
|
|
653
697
|
if (typeof polar[index].twa == 'undefined') {
|
|
654
698
|
polar[index].twa = []
|
|
655
699
|
}
|
|
656
|
-
|
|
657
|
-
|
|
700
|
+
if (!row[index + 1]) {
|
|
701
|
+
continue
|
|
702
|
+
}
|
|
703
|
+
const tbsCell = Number(row[index + 1])
|
|
704
|
+
if (Number.isNaN(tbsCell)) {
|
|
705
|
+
continue
|
|
706
|
+
}
|
|
707
|
+
let tbs = ktsToMs(tbsCell) * (options.perfAdjust || 1)
|
|
658
708
|
let vmg = tbs * Math.abs(Math.cos(angle))
|
|
659
709
|
let Obj = { twa: angle, tbs: tbs, vmg: vmg }
|
|
660
710
|
// app.debug('Adding Obj: %s', JSON.stringify(Obj))
|
|
661
|
-
|
|
711
|
+
upsertTwa(index, Obj, angleDeg)
|
|
662
712
|
// See if we need to set/overwrite beat/run angle
|
|
663
713
|
|
|
664
714
|
// See if this a new Max speed
|
package/public/index.html
CHANGED
|
@@ -35,6 +35,26 @@
|
|
|
35
35
|
padding: 4px;
|
|
36
36
|
font-size: 14px;
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
#error-banner {
|
|
40
|
+
position: fixed;
|
|
41
|
+
bottom: 0;
|
|
42
|
+
left: 0;
|
|
43
|
+
right: 0;
|
|
44
|
+
background: rgba(220, 60, 60, 0.95);
|
|
45
|
+
color: white;
|
|
46
|
+
text-align: center;
|
|
47
|
+
padding: 14px 20px;
|
|
48
|
+
font-size: 15px;
|
|
49
|
+
z-index: 1000;
|
|
50
|
+
box-shadow: 0 -2px 8px rgba(0,0,0,0.3);
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#error-banner.show {
|
|
55
|
+
display: block;
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
</style>
|
|
39
59
|
</head>
|
|
40
60
|
<body>
|
|
@@ -47,12 +67,17 @@
|
|
|
47
67
|
<label for="labelPos">Place polar wind speed label at TWA:</label>
|
|
48
68
|
<input type="number" id="labelPos" value="135" min="0" max="180" onchange="updateLabelPos(this.value)">
|
|
49
69
|
</div>
|
|
70
|
+
|
|
71
|
+
<div id="error-banner"></div>
|
|
50
72
|
</div>
|
|
51
73
|
|
|
52
74
|
<script>
|
|
53
75
|
|
|
54
|
-
var polarSpeed, boatSpeed, TWA, TWS
|
|
55
76
|
var myChart, chartData
|
|
77
|
+
let ws
|
|
78
|
+
let polarSpeed = 0, boatSpeed = 0, TWA = 0, TWS = 0
|
|
79
|
+
let dataReceived = { polar: false, boatSpeed: false, twa: false }
|
|
80
|
+
let dataTimeout
|
|
56
81
|
|
|
57
82
|
// GLOBAL CONFIG: Load from storage or default to 135
|
|
58
83
|
window.labelTWA = localStorage.getItem('polarLabelTWA') || 135;
|
|
@@ -143,41 +168,80 @@ Chart.plugins.register({
|
|
|
143
168
|
}
|
|
144
169
|
});
|
|
145
170
|
|
|
146
|
-
$.getJSON("/plugins/signalk-polar-performance-plugin/chartData"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
borderColor: 'rgba(0,255,0,0.2)',
|
|
153
|
-
borderWidth: 1,
|
|
154
|
-
radius: 10,
|
|
155
|
-
pointStyle: 'circle',
|
|
156
|
-
data: [{ y: 0, x: 0, r: 10 }]
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
label: 'Boat Speed',
|
|
160
|
-
backgroundColor: 'rgba(0,150,255,0.7)',
|
|
161
|
-
borderColor: 'rgba(0,150,255,0.2)',
|
|
162
|
-
borderWidth: 1,
|
|
163
|
-
radius: 10,
|
|
164
|
-
pointStyle: 'circle',
|
|
165
|
-
data: [{ y: 0, x: 0, r: 10 }]
|
|
166
|
-
})
|
|
171
|
+
$.getJSON("/plugins/signalk-polar-performance-plugin/chartData")
|
|
172
|
+
.done(function(json) {
|
|
173
|
+
if (!json || !json.datasets || json.datasets.length < 3) {
|
|
174
|
+
showError("No polar diagram data available.<br><br>Make sure you have entered a valid polar CSV in the plugin settings.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
// Add actual values dots (Polar Speed + Boat Speed)
|
|
179
|
+
json.datasets.unshift({
|
|
180
|
+
label: 'Polar Speed',
|
|
181
|
+
backgroundColor: 'rgba(0,255,0,0.7)',
|
|
182
|
+
borderColor: 'rgba(0,255,0,0.2)',
|
|
183
|
+
borderWidth: 1,
|
|
184
|
+
radius: 10,
|
|
185
|
+
pointStyle: 'circle',
|
|
186
|
+
data: [{ y: 0, x: 0, r: 10 }]
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
label: 'Boat Speed',
|
|
190
|
+
backgroundColor: 'rgba(0,150,255,0.7)',
|
|
191
|
+
borderColor: 'rgba(0,150,255,0.2)',
|
|
192
|
+
borderWidth: 1,
|
|
193
|
+
radius: 10,
|
|
194
|
+
pointStyle: 'circle',
|
|
195
|
+
data: [{ y: 0, x: 0, r: 10 }]
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Backup original colors
|
|
199
|
+
for (var i = 2; i < json.datasets.length; i++) {
|
|
170
200
|
if (!json.datasets[i].borderColor) {
|
|
171
|
-
|
|
201
|
+
json.datasets[i].borderColor = 'rgba(100, 100, 100, 0.5)';
|
|
172
202
|
}
|
|
173
203
|
json.datasets[i]._originalColor = json.datasets[i].borderColor;
|
|
174
204
|
json.datasets[i].borderWidth = 1;
|
|
175
|
-
|
|
205
|
+
}
|
|
176
206
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
})
|
|
207
|
+
chartData = json;
|
|
208
|
+
showChart(chartData);
|
|
209
|
+
})
|
|
210
|
+
.fail(function(jqXHR, textStatus, errorThrown) {
|
|
211
|
+
let reason = "Unknown error";
|
|
212
|
+
|
|
213
|
+
if (jqXHR.status === 404) {
|
|
214
|
+
reason = "Plugin endpoint not found — is the plugin enabled?";
|
|
215
|
+
} else if (jqXHR.status === 500) {
|
|
216
|
+
reason = "Server error — check plugin configuration (invalid CSV polar?)";
|
|
217
|
+
} else if (textStatus === "parsererror") {
|
|
218
|
+
reason = "Invalid data received from server";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
showError(`Failed to load polar data.<br><br>Reason: ${reason}`);
|
|
222
|
+
});
|
|
180
223
|
|
|
224
|
+
// New error display function
|
|
225
|
+
function showError(message) {
|
|
226
|
+
let banner = document.getElementById('error-banner');
|
|
227
|
+
|
|
228
|
+
if (!banner) {
|
|
229
|
+
// Fallback: create banner dynamically if missing
|
|
230
|
+
banner = document.createElement('div');
|
|
231
|
+
banner.id = 'error-banner';
|
|
232
|
+
document.body.appendChild(banner);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
banner.innerHTML = `
|
|
236
|
+
⚠️ ${message}
|
|
237
|
+
|
|
238
|
+
<button onclick="location.reload()" style="background:white; color:#c33; border:none; padding:4px 10px; border-radius:4px; cursor:pointer;">
|
|
239
|
+
Reload
|
|
240
|
+
</button>
|
|
241
|
+
`;
|
|
242
|
+
|
|
243
|
+
banner.classList.add('show');
|
|
244
|
+
}
|
|
181
245
|
function showChart (data) {
|
|
182
246
|
console.log(data)
|
|
183
247
|
myChart = new Chart("myChart", {
|
|
@@ -293,6 +357,11 @@ function startListeners () {
|
|
|
293
357
|
}
|
|
294
358
|
|
|
295
359
|
ws.send(JSON.stringify(subscriptionObject));
|
|
360
|
+
|
|
361
|
+
// Start timeout to detect missing data
|
|
362
|
+
dataReceived = { polar: false, boatSpeed: false, twa: false, tws: false };
|
|
363
|
+
clearTimeout(dataTimeout);
|
|
364
|
+
dataTimeout = setTimeout(checkDataReceived, 20000);
|
|
296
365
|
}
|
|
297
366
|
|
|
298
367
|
function handleData (data) {
|
|
@@ -304,18 +373,51 @@ function handleData (data) {
|
|
|
304
373
|
if (path == 'performance.polarSpeed') {
|
|
305
374
|
polarSpeed = roundDec(msToKts(value), 1)
|
|
306
375
|
chartData.datasets[0].data[0].y = polarSpeed
|
|
307
|
-
|
|
376
|
+
dataReceived.polar = true;
|
|
377
|
+
} else if (path === 'environment.wind.angleTrueWaterDamped') {
|
|
308
378
|
TWA = roundDec(Math.abs(radToDeg(value)),1)
|
|
309
379
|
chartData.datasets[0].data[0].x = TWA
|
|
310
380
|
chartData.datasets[1].data[0].x = TWA
|
|
381
|
+
dataReceived.twa = true;
|
|
311
382
|
} else if (path == 'performance.boatSpeedDamped') {
|
|
312
383
|
boatSpeed = roundDec(msToKts(value),1)
|
|
313
384
|
chartData.datasets[1].data[0].y = boatSpeed
|
|
385
|
+
dataReceived.boatSpeed = true;
|
|
314
386
|
} else if (path == 'environment.wind.speedTrue') {
|
|
315
387
|
TWS = msToKts(value);
|
|
388
|
+
dataReceived.tws = true;
|
|
316
389
|
}
|
|
317
390
|
}
|
|
318
391
|
|
|
392
|
+
function checkDataReceived() {
|
|
393
|
+
let missing = [];
|
|
394
|
+
|
|
395
|
+
if (!dataReceived.polar) missing.push("Polar Speed (performance.polarSpeed)");
|
|
396
|
+
if (!dataReceived.boatSpeed) missing.push("Boat Speed (performance.boatSpeedDamped)");
|
|
397
|
+
if (!dataReceived.twa) missing.push("True Wind Angle (environment.wind.angleTrueWater)");
|
|
398
|
+
if (!dataReceived.tws) missing.push("True Wind Speed (environment.wind.speedTrue)");
|
|
399
|
+
|
|
400
|
+
if (missing.length > 0) {
|
|
401
|
+
showError(`Missing data: ${missing.join(', ')}. Check your instruments and polar settings.`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Enhanced error display
|
|
406
|
+
function showError(message) {
|
|
407
|
+
const container = document.querySelector('.chart-container');
|
|
408
|
+
container.innerHTML = `
|
|
409
|
+
<div style="padding: 40px 20px; text-align: center; color: #ff7777;">
|
|
410
|
+
<h2 style="color: #ff6666;">⚠️ Cannot display Polar Performance</h2>
|
|
411
|
+
<div style="margin: 25px 0; line-height: 1.6; font-size: 17px;">
|
|
412
|
+
${message}
|
|
413
|
+
</div>
|
|
414
|
+
<button onclick="location.reload()"
|
|
415
|
+
style="padding: 12px 24px; font-size: 16px; background: #333; color: white; border: 1px solid #666; border-radius: 6px; cursor: pointer;">
|
|
416
|
+
Reload Page
|
|
417
|
+
</button>
|
|
418
|
+
</div>
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
319
421
|
|
|
320
422
|
connect()
|
|
321
423
|
|