signalk-rec-bms 1.1.1 → 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/README.md +4 -4
- package/package.json +1 -1
- package/plugin/lib/schema.js +1 -1
- package/plugin/lib/serial/modules/abms.js +5 -2
- package/plugin/lib/serial/modules/array.js +2 -2
- package/plugin/lib/serial/modules/derived.js +116 -22
- package/plugin/lib/websocket/derived.js +78 -0
- package/plugin/lib/websocket/settings.js +0 -5
- package/plugin/lib/websocket/status.js +2 -7
- package/plugin/lib/websocket/ws.js +7 -2
- package/public/assets/images/bms_serial.png +0 -0
- package/public/assets/images/rs485.jpg +0 -0
- package/public/js/main.js +32 -16
- package/public/assets/icons/rec_ball-72x72.png +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# signalk-rec-bms
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -24,11 +24,11 @@ Tested with the 4 cell ABMS model only
|
|
|
24
24
|
|
|
25
25
|
#### Serial connection:
|
|
26
26
|
|
|
27
|
-
- RS485 to USB adapter with
|
|
27
|
+
- RS485 to USB adapter with 3v output. The REC-BMS serial chip is not powered internally and you must supply 3v to power it.
|
|
28
28
|
|
|
29
|
-

|
|
30
30
|
|
|
31
|
-

|
|
32
32
|
|
|
33
33
|
## Dependencies
|
|
34
34
|
|
package/package.json
CHANGED
package/plugin/lib/schema.js
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
|
+
};
|
|
@@ -35,8 +35,8 @@ function parseLCD1Response(input) {
|
|
|
35
35
|
current: packet.readFloatLE(12),
|
|
36
36
|
temperature: packet.readFloatLE(16),
|
|
37
37
|
packVoltage: packet.readFloatLE(20),
|
|
38
|
-
soc: packet.readFloatLE(24)
|
|
39
|
-
soh: packet.readFloatLE(28)
|
|
38
|
+
soc: packet.readFloatLE(24),
|
|
39
|
+
soh: packet.readFloatLE(28)
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
return result;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
module.exports = function Derived(app, sendDeltaCallback, config) {
|
|
2
4
|
app.debug("[DERIVED] Module instantiated with config: " + JSON.stringify(config));
|
|
3
5
|
let voltage, current, ampHourUsed, capacity;
|
|
@@ -11,13 +13,29 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
|
|
|
11
13
|
if ([voltage, current, ampHourUsed, capacity].every(v => typeof v === 'number')) {
|
|
12
14
|
const power = voltage * current;
|
|
13
15
|
const ahRemaining = capacity - ampHourUsed;
|
|
16
|
+
|
|
17
|
+
const values = [
|
|
18
|
+
{ path: prefix + 'power', value: power },
|
|
19
|
+
{ path: prefix + 'ampHourRemaining', value: ahRemaining }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (current > 0 && ahRemaining < capacity) {
|
|
23
|
+
const missingAh = capacity - ahRemaining;
|
|
24
|
+
const timeToFull = (missingAh / current) * 3600;
|
|
25
|
+
values.push({ path: prefix + 'timeToFull', value: timeToFull });
|
|
26
|
+
values.push({ path: prefix + 'timeToEmpty', value: 0 });
|
|
27
|
+
} else if (current < 0 && ahRemaining > 0) {
|
|
28
|
+
const timeToEmpty = (ahRemaining / Math.abs(current)) * 3600;
|
|
29
|
+
values.push({ path: prefix + 'timeToFull', value: 0 });
|
|
30
|
+
values.push({ path: prefix + 'timeToEmpty', value: timeToEmpty });
|
|
31
|
+
} else {
|
|
32
|
+
// fallback when current is 0 or data is abnormal
|
|
33
|
+
values.push({ path: prefix + 'timeToFull', value: 0 });
|
|
34
|
+
values.push({ path: prefix + 'timeToEmpty', value: 0 });
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
sendDeltaCallback({
|
|
15
|
-
updates: [{
|
|
16
|
-
values: [
|
|
17
|
-
{ path: prefix + 'power', value: power },
|
|
18
|
-
{ path: prefix + 'ampHourRemaining', value: ahRemaining }
|
|
19
|
-
]
|
|
20
|
-
}]
|
|
38
|
+
updates: [{ values }]
|
|
21
39
|
});
|
|
22
40
|
}
|
|
23
41
|
}
|
|
@@ -35,39 +53,115 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
|
|
|
35
53
|
if (typeof v === 'number') { capacity = v; computeAndSendDeltas(); }
|
|
36
54
|
});
|
|
37
55
|
|
|
56
|
+
// ---- Cell diff logic ----
|
|
38
57
|
let totalCells = 0;
|
|
39
|
-
|
|
58
|
+
let unitsExpected = 0;
|
|
59
|
+
let cellVoltages = {};
|
|
40
60
|
let subscribedCellStreams = false;
|
|
41
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
|
+
|
|
42
71
|
app.streambundle.getSelfStream(prefix + 'numBMSUnits').forEach(units => {
|
|
43
|
-
if (
|
|
44
|
-
|
|
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)
|
|
45
88
|
for (let i = 1; i <= totalCells; i++) {
|
|
89
|
+
if (subscribedCells.has(i)) continue;
|
|
90
|
+
|
|
91
|
+
subscribedCells.add(i);
|
|
46
92
|
const cellPath = prefix + 'cellVoltage' + i;
|
|
47
93
|
app.streambundle.getSelfStream(cellPath).forEach(val => {
|
|
48
|
-
if (typeof val === 'number') {
|
|
94
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
95
|
+
if (!firstCellSeenAtMs) firstCellSeenAtMs = Date.now();
|
|
49
96
|
cellVoltages[i] = val;
|
|
50
97
|
trySendCellDiff();
|
|
51
98
|
}
|
|
52
99
|
});
|
|
53
100
|
}
|
|
101
|
+
|
|
54
102
|
subscribedCellStreams = true;
|
|
103
|
+
|
|
104
|
+
// Attempt immediately in case we already have cached values after restart
|
|
105
|
+
trySendCellDiff();
|
|
55
106
|
}
|
|
56
107
|
});
|
|
57
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
|
+
|
|
58
119
|
function trySendCellDiff() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
// all cells present: no need to keep retry timer
|
|
161
|
+
if (pendingCellDiffTimer) {
|
|
162
|
+
clearTimeout(pendingCellDiffTimer);
|
|
163
|
+
pendingCellDiffTimer = null;
|
|
164
|
+
}
|
|
71
165
|
}
|
|
72
166
|
}
|
|
73
167
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Cache latest values across messages
|
|
4
|
+
let latestMaster = null;
|
|
5
|
+
let latestSettings = null;
|
|
6
|
+
|
|
7
|
+
module.exports = function(parsedData, prefix) {
|
|
8
|
+
const values = [];
|
|
9
|
+
|
|
10
|
+
// Update cache if master data is present
|
|
11
|
+
if (parsedData?.bms_array?.master) {
|
|
12
|
+
latestMaster = parsedData.bms_array.master;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Update cache if settings data is present
|
|
16
|
+
if (parsedData?.type === "settings") {
|
|
17
|
+
latestSettings = parsedData;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// If we don't have both yet, don't compute anything
|
|
21
|
+
if (!latestMaster || !latestSettings) return values;
|
|
22
|
+
|
|
23
|
+
const master = latestMaster;
|
|
24
|
+
const settings = latestSettings;
|
|
25
|
+
|
|
26
|
+
// power = voltage * current
|
|
27
|
+
if (typeof master.vbat === "number" && typeof master.ibat === "number") {
|
|
28
|
+
values.push({
|
|
29
|
+
path: `${prefix}.power`,
|
|
30
|
+
value: master.vbat * master.ibat
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// cellVoltageDifference = max - min
|
|
35
|
+
if (typeof master.maxcell === "number" && typeof master.mincell === "number") {
|
|
36
|
+
values.push({
|
|
37
|
+
path: `${prefix}.cellVoltageDifference`,
|
|
38
|
+
value: master.maxcell - master.mincell
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ampHourRemaining = capacity - ampHourUsed
|
|
43
|
+
if (typeof settings.capa === "number" && typeof settings.Ah === "number") {
|
|
44
|
+
values.push({
|
|
45
|
+
path: `${prefix}.ampHourRemaining`,
|
|
46
|
+
value: settings.capa - settings.Ah
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// timeToFull / timeToEmpty in seconds
|
|
51
|
+
if (
|
|
52
|
+
typeof settings.capa === "number" &&
|
|
53
|
+
typeof settings.Ah === "number" &&
|
|
54
|
+
typeof master.ibat === "number"
|
|
55
|
+
) {
|
|
56
|
+
const capacity = settings.capa;
|
|
57
|
+
const current = master.ibat;
|
|
58
|
+
const soc = typeof master.soc === "number" ? master.soc : null;
|
|
59
|
+
const remainingAh = (soc !== null) ? (soc / 100) * capacity : (capacity - settings.Ah);
|
|
60
|
+
|
|
61
|
+
if (current > 0 && remainingAh < capacity) {
|
|
62
|
+
const missingAh = capacity - remainingAh;
|
|
63
|
+
const timeToFull = (missingAh / current) * 3600;
|
|
64
|
+
values.push({ path: `${prefix}.timeToFull`, value: timeToFull });
|
|
65
|
+
values.push({ path: `${prefix}.timeToEmpty`, value: 0 });
|
|
66
|
+
} else if (current < 0 && remainingAh > 0) {
|
|
67
|
+
const timeToEmpty = (remainingAh / Math.abs(current)) * 3600;
|
|
68
|
+
values.push({ path: `${prefix}.timeToFull`, value: 0 });
|
|
69
|
+
values.push({ path: `${prefix}.timeToEmpty`, value: timeToEmpty });
|
|
70
|
+
} else {
|
|
71
|
+
// idle or insufficient current
|
|
72
|
+
values.push({ path: `${prefix}.timeToFull`, value: 0 });
|
|
73
|
+
values.push({ path: `${prefix}.timeToEmpty`, value: 0 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return values;
|
|
78
|
+
};
|
|
@@ -8,11 +8,6 @@ module.exports = function(parsedData, prefix, config) {
|
|
|
8
8
|
const settings = parsedData;
|
|
9
9
|
const values = [];
|
|
10
10
|
|
|
11
|
-
// Derived data - NOT FROM BMS
|
|
12
|
-
const AhR = (parsedData.capa !== undefined && parsedData.Ah !== undefined)
|
|
13
|
-
? parsedData.capa - parsedData.Ah
|
|
14
|
-
: null;
|
|
15
|
-
values.push({ path: `${prefix}.ampHourRemaining`, value: AhR });
|
|
16
11
|
// Data from WiFi module WebSocket under SETTINGS message
|
|
17
12
|
values.push({ path: `${prefix}.address`, value: parsedData.addr });
|
|
18
13
|
values.push({ path: `${prefix}.ampHourUsed`, value: parsedData.Ah });
|
|
@@ -23,15 +23,10 @@ module.exports = function(parsedData, prefix, config) {
|
|
|
23
23
|
return null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// Derived data - NOT FROM BMS
|
|
27
|
-
const power = (master.vbat !== undefined && master.ibat !== undefined)
|
|
28
|
-
? master.vbat * master.ibat
|
|
29
|
-
: null;
|
|
30
|
-
values.push({ path: `${prefix}.power`, value: power });
|
|
31
26
|
// Data from WiFi module WebSocket under SETTINGS message
|
|
32
27
|
values.push({ path: `${prefix}.date`, value: master.date });
|
|
33
28
|
values.push({ path: `${prefix}.time`, value: master.time });
|
|
34
|
-
values.push({ path: `${prefix}.cellVoltageDifference`, value: master.maxcell - master.mincell });
|
|
29
|
+
//values.push({ path: `${prefix}.cellVoltageDifference`, value: master.maxcell - master.mincell });
|
|
35
30
|
values.push({ path: `${prefix}.current`, value: master.ibat });
|
|
36
31
|
values.push({ path: `${prefix}.errorAddress`, value: master.erro.addr });
|
|
37
32
|
values.push({ path: `${prefix}.errorConnectionState`, value: master.erro.con_st });
|
|
@@ -42,7 +37,7 @@ module.exports = function(parsedData, prefix, config) {
|
|
|
42
37
|
values.push({ path: `${prefix}.minCellVoltage`, value: master.mincell });
|
|
43
38
|
values.push({ path: `${prefix}.stateOfCharge`, value: master.soc });
|
|
44
39
|
values.push({ path: `${prefix}.stateOfHealth`, value: master.soh });
|
|
45
|
-
values.push({ path: `${prefix}.timeRemaining`, value: extractMinutes(master.time_remaining) });
|
|
40
|
+
//values.push({ path: `${prefix}.timeRemaining`, value: extractMinutes(master.time_remaining) });
|
|
46
41
|
values.push({ path: `${prefix}.voltage`, value: master.vbat });
|
|
47
42
|
values.push({ path: `${prefix}.bmsTemperature`, value: slave.temp_bms });
|
|
48
43
|
values.push({ path: `${prefix}.slaveAddress`, value: slave.address });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const WebSocket = require('ws');
|
|
3
3
|
const status = require('./status');
|
|
4
4
|
const settings = require('./settings');
|
|
5
|
+
const derived = require('./derived');
|
|
5
6
|
const schema = require('../schema');
|
|
6
7
|
|
|
7
8
|
module.exports = function(app, publishDelta) {
|
|
@@ -84,6 +85,11 @@ module.exports = function(app, publishDelta) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Add derived values
|
|
89
|
+
const derivedValues = derived(parsedData, prefix) || [];
|
|
90
|
+
delta.updates[0].values.push(...derivedValues);
|
|
91
|
+
|
|
92
|
+
// Filter out null/undefined
|
|
87
93
|
delta.updates[0].values = delta.updates[0].values.filter(item => item.value !== null && item.value !== undefined);
|
|
88
94
|
|
|
89
95
|
if (delta.updates[0].values.length > 0) {
|
|
@@ -111,9 +117,8 @@ module.exports = function(app, publishDelta) {
|
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
ws.on('error', (error) => {
|
|
114
|
-
app.error(`[WEBSOCKET] WebSocket error: ${error.message}`);
|
|
115
120
|
app.setPluginStatus(`Error: ${error.message} - ERROR`);
|
|
116
|
-
app.debug(
|
|
121
|
+
app.debug(`[WEBSOCKET] WebSocket error: "${error.message}", terminating connection.`);
|
|
117
122
|
clearInterval(pingInterval);
|
|
118
123
|
if (ws) ws.terminate();
|
|
119
124
|
if (shouldReconnect) {
|
|
Binary file
|
|
Binary file
|
package/public/js/main.js
CHANGED
|
@@ -15,6 +15,7 @@ class BMSSystem {
|
|
|
15
15
|
current: null,
|
|
16
16
|
power: null,
|
|
17
17
|
stateOfCharge: null,
|
|
18
|
+
stateOfHealth: null,
|
|
18
19
|
cellVoltages: {},
|
|
19
20
|
cellResistances: {},
|
|
20
21
|
cellVoltageDifference: null,
|
|
@@ -38,15 +39,15 @@ class BMSSystem {
|
|
|
38
39
|
|
|
39
40
|
cacheDomElements() {
|
|
40
41
|
const metrics = [
|
|
41
|
-
'voltage', 'current', 'power', 'stateOfCharge', 'cellVoltageDifference',
|
|
42
|
-
'
|
|
43
|
-
'
|
|
42
|
+
'voltage', 'current', 'power', 'stateOfCharge', 'cellVoltageDifference', 'bmsTemperature', 'cellTemperature1',
|
|
43
|
+
'maxCellVoltage', 'minCellVoltage', 'ampHourRemaining',
|
|
44
|
+
'ampHourUsed', 'capacity'
|
|
44
45
|
];
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
metrics.forEach(metric => {
|
|
47
48
|
this.dom.metricElements[metric] = document.getElementById(metric);
|
|
48
49
|
if (!this.dom.metricElements[metric]) {
|
|
49
|
-
console.
|
|
50
|
+
console.warn(`Missing metric element: ${metric}`);
|
|
50
51
|
}
|
|
51
52
|
});
|
|
52
53
|
}
|
|
@@ -54,7 +55,7 @@ class BMSSystem {
|
|
|
54
55
|
setupTabSwitching() {
|
|
55
56
|
const statusTab = document.getElementById('nav-status');
|
|
56
57
|
const settingsTab = document.getElementById('nav-settings');
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
if (!statusTab || !settingsTab) {
|
|
59
60
|
console.warn("Tab elements not found");
|
|
60
61
|
return;
|
|
@@ -68,7 +69,7 @@ class BMSSystem {
|
|
|
68
69
|
switchView(view) {
|
|
69
70
|
const statusView = document.getElementById('status-view');
|
|
70
71
|
const settingsView = document.getElementById('settings-view');
|
|
71
|
-
|
|
72
|
+
|
|
72
73
|
if (view === 'status') {
|
|
73
74
|
statusView?.classList.add('active');
|
|
74
75
|
settingsView?.classList.remove('active');
|
|
@@ -90,11 +91,23 @@ class BMSSystem {
|
|
|
90
91
|
setValue(this.dom.metricElements.voltage, this.liveData.voltage, "V");
|
|
91
92
|
setValue(this.dom.metricElements.current, this.liveData.current, "A");
|
|
92
93
|
setValue(this.dom.metricElements.power, this.liveData.power, "W");
|
|
93
|
-
|
|
94
|
+
|
|
95
|
+
setValue(
|
|
96
|
+
this.dom.metricElements.stateOfCharge,
|
|
97
|
+
this.liveData.stateOfCharge !== null ? this.liveData.stateOfCharge * 100 : null,
|
|
98
|
+
"%"
|
|
99
|
+
);
|
|
100
|
+
setValue(
|
|
101
|
+
this.dom.metricElements.stateOfHealth,
|
|
102
|
+
this.liveData.stateOfHealth !== null ? this.liveData.stateOfHealth * 100 : null,
|
|
103
|
+
"%"
|
|
104
|
+
);
|
|
105
|
+
|
|
94
106
|
if (this.dom.metricElements.cellVoltageDifference) {
|
|
95
107
|
const v = this.liveData.cellVoltageDifference;
|
|
96
108
|
this.dom.metricElements.cellVoltageDifference.textContent = v !== null ? `${v.toFixed(4)}V` : "--";
|
|
97
|
-
}
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
setValue(this.dom.metricElements.bmsTemperature, this.liveData.bmsTemperature, "°C");
|
|
99
112
|
setValue(this.dom.metricElements.cellTemperature1, this.liveData.cellTemperature1, "°C");
|
|
100
113
|
setValue(this.dom.metricElements.maxCellVoltage, this.liveData.maxCellVoltage, "V");
|
|
@@ -106,20 +119,23 @@ class BMSSystem {
|
|
|
106
119
|
|
|
107
120
|
connectWebSocket() {
|
|
108
121
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
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
|
+
|
|
111
127
|
ws.onopen = () => console.log("WebSocket connected");
|
|
112
128
|
ws.onerror = (error) => console.error("WebSocket error:", error);
|
|
113
129
|
ws.onclose = () => console.log("WebSocket connection closed");
|
|
114
|
-
|
|
130
|
+
|
|
115
131
|
ws.onmessage = (event) => {
|
|
116
132
|
const delta = JSON.parse(event.data);
|
|
117
133
|
if (!delta.updates) return;
|
|
118
|
-
|
|
134
|
+
|
|
119
135
|
delta.updates.forEach(update => {
|
|
120
136
|
update.values?.forEach(({ path, value }) => {
|
|
121
137
|
if (!path.startsWith(this.prefix)) return;
|
|
122
|
-
|
|
138
|
+
|
|
123
139
|
const key = path.slice(this.prefix.length);
|
|
124
140
|
if (key in this.liveData) {
|
|
125
141
|
this.liveData[key] = value;
|
|
@@ -130,7 +146,7 @@ class BMSSystem {
|
|
|
130
146
|
}
|
|
131
147
|
});
|
|
132
148
|
});
|
|
133
|
-
|
|
149
|
+
|
|
134
150
|
this.updateMetrics();
|
|
135
151
|
CellVisualizer.updateAllCells({
|
|
136
152
|
cellVoltages: this.liveData.cellVoltages,
|
|
@@ -140,7 +156,7 @@ class BMSSystem {
|
|
|
140
156
|
} catch (error) {
|
|
141
157
|
console.error("WebSocket initialization failed:", error);
|
|
142
158
|
}
|
|
143
|
-
}
|
|
159
|
+
}
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
document.addEventListener('DOMContentLoaded', () => {
|
|
Binary file
|