signalk-rec-bms 1.1.2 → 1.1.3
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
CHANGED
|
@@ -132,8 +132,11 @@ function parseWCBIResponse(input) {
|
|
|
132
132
|
if (!packet) {
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
135
|
-
const payload = packet.slice(4, packet.length - 3).toString('utf8');
|
|
135
|
+
const payload = packet.slice(4, packet.length - 3).toString('utf8').trim();
|
|
136
136
|
const value = parseInt(payload, 10);
|
|
137
|
+
if (isNaN(value)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
137
140
|
const result = {
|
|
138
141
|
type: "WCBI",
|
|
139
142
|
data: { balancingCellNumber: value }
|
|
@@ -294,4 +297,4 @@ module.exports = {
|
|
|
294
297
|
parseTIMEResponse,
|
|
295
298
|
parseDATEResponse,
|
|
296
299
|
parseWCBIResponse
|
|
297
|
-
};
|
|
300
|
+
};
|
|
@@ -53,41 +53,115 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
|
|
|
53
53
|
if (typeof v === 'number') { capacity = v; computeAndSendDeltas(); }
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
// ---- Cell diff logic ----
|
|
56
57
|
let totalCells = 0;
|
|
57
|
-
|
|
58
|
+
let unitsExpected = 0;
|
|
59
|
+
let cellVoltages = {};
|
|
58
60
|
let subscribedCellStreams = false;
|
|
59
61
|
|
|
62
|
+
// prevent duplicate subscriptions if units changes / reconnects
|
|
63
|
+
const subscribedCells = new Set();
|
|
64
|
+
|
|
65
|
+
// retry controls (avoid timer storms + max wait)
|
|
66
|
+
let pendingCellDiffTimer = null;
|
|
67
|
+
let firstCellSeenAtMs = null;
|
|
68
|
+
const CELL_DIFF_RETRY_MS = 200;
|
|
69
|
+
const CELL_DIFF_MAX_WAIT_MS = 8000;
|
|
70
|
+
|
|
60
71
|
app.streambundle.getSelfStream(prefix + 'numBMSUnits').forEach(units => {
|
|
61
|
-
if (
|
|
62
|
-
|
|
72
|
+
if (typeof units === 'number' && units > 0) {
|
|
73
|
+
// If units changes (or first time), reset expectations and cached values
|
|
74
|
+
if (units !== unitsExpected) {
|
|
75
|
+
unitsExpected = units;
|
|
76
|
+
totalCells = units * 4;
|
|
77
|
+
cellVoltages = {};
|
|
78
|
+
firstCellSeenAtMs = null;
|
|
79
|
+
|
|
80
|
+
// clear any pending retry from old expectations
|
|
81
|
+
if (pendingCellDiffTimer) {
|
|
82
|
+
clearTimeout(pendingCellDiffTimer);
|
|
83
|
+
pendingCellDiffTimer = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Ensure we have subscriptions for 1..totalCells (do not duplicate)
|
|
63
88
|
for (let i = 1; i <= totalCells; i++) {
|
|
89
|
+
if (subscribedCells.has(i)) continue;
|
|
90
|
+
|
|
91
|
+
subscribedCells.add(i);
|
|
64
92
|
const cellPath = prefix + 'cellVoltage' + i;
|
|
65
93
|
app.streambundle.getSelfStream(cellPath).forEach(val => {
|
|
66
|
-
if (typeof val === 'number') {
|
|
94
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
95
|
+
if (!firstCellSeenAtMs) firstCellSeenAtMs = Date.now();
|
|
67
96
|
cellVoltages[i] = val;
|
|
68
97
|
trySendCellDiff();
|
|
69
98
|
}
|
|
70
99
|
});
|
|
71
100
|
}
|
|
101
|
+
|
|
72
102
|
subscribedCellStreams = true;
|
|
103
|
+
|
|
104
|
+
// Attempt immediately in case we already have cached values after restart
|
|
105
|
+
trySendCellDiff();
|
|
73
106
|
}
|
|
74
107
|
});
|
|
75
108
|
|
|
109
|
+
function scheduleCellDiffRetry() {
|
|
110
|
+
if (pendingCellDiffTimer) return;
|
|
111
|
+
if (!subscribedCellStreams || totalCells <= 0) return;
|
|
112
|
+
|
|
113
|
+
pendingCellDiffTimer = setTimeout(() => {
|
|
114
|
+
pendingCellDiffTimer = null;
|
|
115
|
+
trySendCellDiff();
|
|
116
|
+
}, CELL_DIFF_RETRY_MS);
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
function trySendCellDiff() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
if (!subscribedCellStreams || totalCells <= 0) return;
|
|
121
|
+
|
|
122
|
+
// Only consider cells within expected range
|
|
123
|
+
const keys = Object.keys(cellVoltages)
|
|
124
|
+
.map(k => parseInt(k, 10))
|
|
125
|
+
.filter(i => Number.isInteger(i) && i >= 1 && i <= totalCells);
|
|
126
|
+
|
|
127
|
+
// A: publish as soon as we have enough to compute (>= 2 cells)
|
|
128
|
+
if (keys.length >= 2) {
|
|
129
|
+
const vals = keys.map(i => cellVoltages[i]).filter(v => typeof v === 'number' && Number.isFinite(v));
|
|
130
|
+
if (vals.length >= 2) {
|
|
131
|
+
const minV = Math.min(...vals);
|
|
132
|
+
const maxV = Math.max(...vals);
|
|
133
|
+
|
|
134
|
+
sendDeltaCallback({
|
|
135
|
+
updates: [{
|
|
136
|
+
values: [
|
|
137
|
+
{ path: prefix + 'cellVoltageDifference', value: maxV - minV }
|
|
138
|
+
]
|
|
139
|
+
}]
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// B: if we haven't seen all cells yet, retry safely with max-wait
|
|
145
|
+
if (keys.length < totalCells) {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (!firstCellSeenAtMs) firstCellSeenAtMs = now;
|
|
148
|
+
|
|
149
|
+
const elapsed = now - firstCellSeenAtMs;
|
|
150
|
+
if (elapsed < CELL_DIFF_MAX_WAIT_MS) {
|
|
151
|
+
scheduleCellDiffRetry();
|
|
152
|
+
} else {
|
|
153
|
+
// stop retrying after max wait; cell updates will still trigger publishes
|
|
154
|
+
if (pendingCellDiffTimer) {
|
|
155
|
+
clearTimeout(pendingCellDiffTimer);
|
|
156
|
+
pendingCellDiffTimer = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
89
159
|
} else {
|
|
90
|
-
|
|
160
|
+
// all cells present: no need to keep retry timer
|
|
161
|
+
if (pendingCellDiffTimer) {
|
|
162
|
+
clearTimeout(pendingCellDiffTimer);
|
|
163
|
+
pendingCellDiffTimer = null;
|
|
164
|
+
}
|
|
91
165
|
}
|
|
92
166
|
}
|
|
93
167
|
|
package/public/js/main.js
CHANGED
|
@@ -39,8 +39,7 @@ class BMSSystem {
|
|
|
39
39
|
|
|
40
40
|
cacheDomElements() {
|
|
41
41
|
const metrics = [
|
|
42
|
-
'voltage', 'current', 'power', 'stateOfCharge', '
|
|
43
|
-
'cellVoltageDifference', 'bmsTemperature', 'cellTemperature1',
|
|
42
|
+
'voltage', 'current', 'power', 'stateOfCharge', 'cellVoltageDifference', 'bmsTemperature', 'cellTemperature1',
|
|
44
43
|
'maxCellVoltage', 'minCellVoltage', 'ampHourRemaining',
|
|
45
44
|
'ampHourUsed', 'capacity'
|
|
46
45
|
];
|
|
@@ -48,7 +47,7 @@ class BMSSystem {
|
|
|
48
47
|
metrics.forEach(metric => {
|
|
49
48
|
this.dom.metricElements[metric] = document.getElementById(metric);
|
|
50
49
|
if (!this.dom.metricElements[metric]) {
|
|
51
|
-
console.
|
|
50
|
+
console.warn(`Missing metric element: ${metric}`);
|
|
52
51
|
}
|
|
53
52
|
});
|
|
54
53
|
}
|
|
@@ -120,20 +119,23 @@ class BMSSystem {
|
|
|
120
119
|
|
|
121
120
|
connectWebSocket() {
|
|
122
121
|
try {
|
|
123
|
-
const
|
|
124
|
-
|
|
122
|
+
const wsScheme = (window.location.protocol === "https:") ? "wss" : "ws";
|
|
123
|
+
const ws = new WebSocket(
|
|
124
|
+
`${wsScheme}://${window.location.host}/signalk/v1/stream?subscribe=self&path=electrical.batteries.bms.*`
|
|
125
|
+
);
|
|
126
|
+
|
|
125
127
|
ws.onopen = () => console.log("WebSocket connected");
|
|
126
128
|
ws.onerror = (error) => console.error("WebSocket error:", error);
|
|
127
129
|
ws.onclose = () => console.log("WebSocket connection closed");
|
|
128
|
-
|
|
130
|
+
|
|
129
131
|
ws.onmessage = (event) => {
|
|
130
132
|
const delta = JSON.parse(event.data);
|
|
131
133
|
if (!delta.updates) return;
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
delta.updates.forEach(update => {
|
|
134
136
|
update.values?.forEach(({ path, value }) => {
|
|
135
137
|
if (!path.startsWith(this.prefix)) return;
|
|
136
|
-
|
|
138
|
+
|
|
137
139
|
const key = path.slice(this.prefix.length);
|
|
138
140
|
if (key in this.liveData) {
|
|
139
141
|
this.liveData[key] = value;
|
|
@@ -144,7 +146,7 @@ class BMSSystem {
|
|
|
144
146
|
}
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
this.updateMetrics();
|
|
149
151
|
CellVisualizer.updateAllCells({
|
|
150
152
|
cellVoltages: this.liveData.cellVoltages,
|
|
@@ -154,7 +156,7 @@ class BMSSystem {
|
|
|
154
156
|
} catch (error) {
|
|
155
157
|
console.error("WebSocket initialization failed:", error);
|
|
156
158
|
}
|
|
157
|
-
}
|
|
159
|
+
}
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
document.addEventListener('DOMContentLoaded', () => {
|
|
Binary file
|