signalk-rec-bms 1.1.6 → 1.1.7
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 +91 -18
- package/plugin/lib/schema.js +6 -1
- package/plugin/lib/serial/modules/abms.js +2 -2
- package/plugin/lib/serial/modules/array.js +61 -46
- package/plugin/lib/serial/modules/bat.js +3 -1
- package/plugin/lib/serial/modules/derived.js +16 -11
- package/plugin/lib/serial/modules/temp.js +5 -5
- package/public/index.html +41 -34
- package/public/js/main.js +75 -38
- package/public/js/settings.js +28 -21
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -6,25 +6,97 @@ const connection = require('./lib/connection');
|
|
|
6
6
|
module.exports = function(app) {
|
|
7
7
|
let conn;
|
|
8
8
|
let pluginOptions;
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
function publishMeta(options) {
|
|
11
|
+
const p = options.deltaPrefix || "electrical.batteries.bms";
|
|
12
|
+
|
|
13
|
+
const meta = [
|
|
14
|
+
// Live data
|
|
15
|
+
{ path: `${p}.minCellVoltage`, value: { units: "V", description: "Minimum cell voltage across all cells" } },
|
|
16
|
+
{ path: `${p}.maxCellVoltage`, value: { units: "V", description: "Maximum cell voltage across all cells" } },
|
|
17
|
+
{ path: `${p}.cellVoltageDifference`, value: { units: "V", description: "Difference between maximum and minimum cell voltage" } },
|
|
18
|
+
{ path: `${p}.numBMSUnits`, value: { description: "Number of ABMS units connected in series" } },
|
|
19
|
+
{ path: `${p}.bmsTemperature`, value: { units: "K", description: "BMS board internal temperature" } },
|
|
20
|
+
{ path: `${p}.slaveAddress`, value: { description: "RS-485 address of the BMS unit with the lowest cell voltage" } },
|
|
21
|
+
{ path: `${p}.minCellNumber`, value: { description: "Cell number with the lowest voltage" } },
|
|
22
|
+
{ path: `${p}.maxCellNumber`, value: { description: "Cell number with the highest voltage" } },
|
|
23
|
+
{ path: `${p}.maxTempSensBmsAddress`, value: { description: "RS-485 address of BMS unit with the highest temperature sensor reading" } },
|
|
24
|
+
{ path: `${p}.maxTempSensNumber`, value: { description: "Sensor number with the highest temperature reading" } },
|
|
25
|
+
{ path: `${p}.id`, value: { description: "Device identification string" } },
|
|
26
|
+
{ path: `${p}.errorPresent`, value: { description: "1 if an error is active, 0 if none" } },
|
|
27
|
+
{ path: `${p}.errorAddress`, value: { description: "RS-485 address of BMS unit reporting the error" } },
|
|
28
|
+
{ path: `${p}.errorCode`, value: { description: "Error code (1-16)" } },
|
|
29
|
+
{ path: `${p}.errorSource`, value: { description: "Cell or sensor number associated with the error" } },
|
|
30
|
+
// Derived
|
|
31
|
+
{ path: `${p}.power`, value: { units: "W", description: "Instantaneous power (voltage × current)" } },
|
|
32
|
+
{ path: `${p}.capacity.remaining`, value: { units: "Ah", description: "Estimated capacity remaining" } },
|
|
33
|
+
{ path: `${p}.capacity.nominal`, value: { units: "Ah", description: "Nominal battery pack capacity as configured in BMS" } },
|
|
34
|
+
{ path: `${p}.capacity.timeRemaining`, value: { units: "s", description: "Estimated time to full discharge at current load" } },
|
|
35
|
+
{ path: `${p}.timeToFull`, value: { units: "s", description: "Estimated time to full charge at current charge rate" } },
|
|
36
|
+
// Battery
|
|
37
|
+
{ path: `${p}.batteryCycleCount`, value: { description: "Number of full charge/discharge cycles" } },
|
|
38
|
+
// Voltage thresholds
|
|
39
|
+
{ path: `${p}.balEndVoltage`, value: { units: "V", description: "Cell voltage at which balancing ends" } },
|
|
40
|
+
{ path: `${p}.balStartVoltage`, value: { units: "V", description: "Cell voltage at which balancing starts" } },
|
|
41
|
+
{ path: `${p}.maxAllowedCellVoltage`, value: { units: "V", description: "Cell over-voltage switch-off threshold" } },
|
|
42
|
+
{ path: `${p}.maxAllowedVoltageHysteresis`, value: { units: "V", description: "Over-voltage switch-off hysteresis per cell" } },
|
|
43
|
+
{ path: `${p}.minAllowedCellVoltage`, value: { units: "V", description: "Cell under-voltage switch-off threshold" } },
|
|
44
|
+
{ path: `${p}.minAllowedVoltageHysteresis`, value: { units: "V", description: "Under-voltage switch-off hysteresis per cell" } },
|
|
45
|
+
{ path: `${p}.endChargeVoltage`, value: { units: "V", description: "Cell end-of-charge voltage" } },
|
|
46
|
+
{ path: `${p}.endChargeHysteresis`, value: { units: "V", description: "End-of-charge voltage hysteresis per cell" } },
|
|
47
|
+
{ path: `${p}.endOfChargeCellDifference`, value: { units: "V", description: "Maximum allowed cell voltage difference at end of charge" } },
|
|
48
|
+
{ path: `${p}.floatVoltageCoefficient`, value: { description: "Float voltage coefficient (multiplier on charge voltage)" } },
|
|
49
|
+
{ path: `${p}.maxAllowedCellVoltDiff`, value: { units: "V", description: "Maximum allowed cell voltage difference during operation" } },
|
|
50
|
+
// Temperature thresholds
|
|
51
|
+
{ path: `${p}.cellOverTempSwitchOff`, value: { units: "K", description: "Cell over-temperature switch-off threshold" } },
|
|
52
|
+
{ path: `${p}.underTempChargeDisable`, value: { units: "K", description: "Under-temperature charge disable threshold" } },
|
|
53
|
+
{ path: `${p}.bmsOverTempSwitchOff`, value: { units: "K", description: "BMS board over-temperature switch-off threshold" } },
|
|
54
|
+
{ path: `${p}.bmsOverTempSwitchOffHysteresis`, value: { units: "K", description: "BMS board over-temperature switch-off hysteresis" } },
|
|
55
|
+
// Current sensor
|
|
56
|
+
{ path: `${p}.currentSensorOffset`, value: { units: "A", description: "Current measurement zero offset" } },
|
|
57
|
+
{ path: `${p}.currentSensorCoefficient`, value: { units: "A/V", description: "Voltage-to-current conversion coefficient" } },
|
|
58
|
+
// SOC
|
|
59
|
+
{ path: `${p}.socHysteresis`, value: { units: "ratio", description: "SOC end-of-charge hysteresis (0-1)" } },
|
|
60
|
+
{ path: `${p}.socReset`, value: { units: "ratio", description: "SOC value applied on manual reset (0-1)" } },
|
|
61
|
+
// Outputs
|
|
62
|
+
{ path: `${p}.relay1Voltage`, value: { units: "V", description: "Relay 1 activation voltage threshold" } },
|
|
63
|
+
{ path: `${p}.relay1Hysteresis`, value: { units: "V", description: "Relay 1 voltage hysteresis" } },
|
|
64
|
+
{ path: `${p}.opto2Voltage`, value: { units: "V", description: "Optocoupler 2 activation voltage threshold" } },
|
|
65
|
+
{ path: `${p}.opto2Hysteresis`, value: { units: "V", description: "Optocoupler 2 voltage hysteresis" } },
|
|
66
|
+
// Victron/Wakespeed
|
|
67
|
+
{ path: `${p}.chargeCoefficient`, value: { description: "Charge current coefficient (0-5C)" } },
|
|
68
|
+
{ path: `${p}.dischargeCoefficient`, value: { description: "Discharge current coefficient (0-5C)" } },
|
|
69
|
+
{ path: `${p}.numberOfInverterDevices`, value: { description: "Number of inverter/charger devices on the CAN bus" } },
|
|
70
|
+
{ path: `${p}.maxChargeCurrent`, value: { units: "A", description: "Maximum charge current per inverter device" } },
|
|
71
|
+
{ path: `${p}.maxDischargeCurrent`, value: { units: "A", description: "Maximum discharge current per inverter device" } },
|
|
72
|
+
{ path: `${p}.minDischargeCellVoltage`, value: { units: "V", description: "Cell under-voltage discharge protection threshold" } },
|
|
73
|
+
// ABMS
|
|
74
|
+
{ path: `${p}.deviceName`, value: { description: "BMS device identifier (serial number formatted as 1A-XXXX)" } },
|
|
75
|
+
{ path: `${p}.abmsSoftwareVersion`, value: { description: "ABMS firmware software version" } },
|
|
76
|
+
{ path: `${p}.abmsHardwareVersion`, value: { description: "ABMS hardware version" } },
|
|
77
|
+
{ path: `${p}.time`, value: { description: "BMS real-time clock time (hh:mm:ss)" } },
|
|
78
|
+
{ path: `${p}.date`, value: { description: "BMS real-time clock date (dd.mm.yyyy)" } },
|
|
79
|
+
{ path: `${p}.balancingCellNumber`, value: { description: "Cell number currently being balanced" } },
|
|
80
|
+
{ path: `${p}.calibrationOffsetCell1`, value: { units: "V", description: "Voltage calibration offset for cell 1" } },
|
|
81
|
+
{ path: `${p}.calibrationOffsetCell2`, value: { units: "V", description: "Voltage calibration offset for cell 2" } },
|
|
82
|
+
{ path: `${p}.calibrationOffsetCell3`, value: { units: "V", description: "Voltage calibration offset for cell 3" } },
|
|
83
|
+
{ path: `${p}.calibrationOffsetCell4`, value: { units: "V", description: "Voltage calibration offset for cell 4" } },
|
|
84
|
+
// Error log
|
|
85
|
+
{ path: `${p}.errorLogData`, value: { description: "Last error log entry (format: code,cell;hh:mm:ss;dd.mm.yyyy)" } },
|
|
86
|
+
{ path: `${p}.errorLogDelete`, value: { description: "Error log delete flag (set to 1 to clear, then restart BMS)" } },
|
|
87
|
+
{ path: `${p}.vmaxExceededCount`, value: { description: "Number of times cell over-voltage threshold was exceeded" } },
|
|
88
|
+
{ path: `${p}.vminExceededCount`, value: { description: "Number of times cell under-voltage threshold was exceeded" } },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
app.handleMessage("signalk-rec-bms", {
|
|
92
|
+
updates: [{ meta }]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
10
95
|
|
|
11
96
|
function publishDelta(delta) {
|
|
12
|
-
let mergedConfig = {};
|
|
13
|
-
if (pluginOptions && pluginOptions.deltas) {
|
|
14
|
-
if (pluginOptions.deltas.common) {
|
|
15
|
-
Object.assign(mergedConfig, pluginOptions.deltas.common);
|
|
16
|
-
}
|
|
17
|
-
if (pluginOptions.deltas.ws) {
|
|
18
|
-
Object.assign(mergedConfig, pluginOptions.deltas.ws);
|
|
19
|
-
}
|
|
20
|
-
if (pluginOptions.deltas.serial) {
|
|
21
|
-
Object.assign(mergedConfig, pluginOptions.deltas.serial);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
97
|
const precision = 5;
|
|
26
98
|
const factor = Math.pow(10, precision);
|
|
27
|
-
|
|
99
|
+
|
|
28
100
|
delta.updates.forEach(update => {
|
|
29
101
|
update.values.forEach(item => {
|
|
30
102
|
if (typeof item.value === "number") {
|
|
@@ -32,7 +104,7 @@ module.exports = function(app) {
|
|
|
32
104
|
}
|
|
33
105
|
});
|
|
34
106
|
});
|
|
35
|
-
|
|
107
|
+
|
|
36
108
|
if (delta && delta.updates && delta.updates.some(u => u.values.length > 0)) {
|
|
37
109
|
app.debug(`[INDEX] Publishing delta: ${JSON.stringify(delta)}`);
|
|
38
110
|
app.handleMessage("signalk-rec-bms", delta, "v1");
|
|
@@ -40,7 +112,7 @@ module.exports = function(app) {
|
|
|
40
112
|
app.debug("[INDEX] Delta empty after processing, not publishing.");
|
|
41
113
|
}
|
|
42
114
|
}
|
|
43
|
-
|
|
115
|
+
|
|
44
116
|
var plugin = {
|
|
45
117
|
id: "signalk-rec-bms",
|
|
46
118
|
name: "SignalK-REC-BMS",
|
|
@@ -50,6 +122,7 @@ module.exports = function(app) {
|
|
|
50
122
|
pluginOptions = options;
|
|
51
123
|
app.debug(`[INDEX] START invoked with options: ${JSON.stringify(options)}`);
|
|
52
124
|
app.setPluginStatus("Connecting to BMS…");
|
|
125
|
+
publishMeta(options);
|
|
53
126
|
conn = connection(options, app, publishDelta);
|
|
54
127
|
conn.start(options);
|
|
55
128
|
},
|
|
@@ -64,4 +137,4 @@ module.exports = function(app) {
|
|
|
64
137
|
};
|
|
65
138
|
|
|
66
139
|
return plugin;
|
|
67
|
-
};
|
|
140
|
+
};
|
package/plugin/lib/schema.js
CHANGED
|
@@ -208,7 +208,7 @@ function getDelta(parsed, options, app) {
|
|
|
208
208
|
updates: [{
|
|
209
209
|
timestamp: new Date().toISOString(),
|
|
210
210
|
values: [
|
|
211
|
-
{ path: `${prefix}.
|
|
211
|
+
{ path: `${prefix}.deviceName`, value: "1A-" + d.abmsSerialNumber }
|
|
212
212
|
]
|
|
213
213
|
}]
|
|
214
214
|
};
|
|
@@ -297,4 +297,4 @@ module.exports = {
|
|
|
297
297
|
parseTIMEResponse,
|
|
298
298
|
parseDATEResponse,
|
|
299
299
|
parseWCBIResponse
|
|
300
|
-
};
|
|
300
|
+
};
|
|
@@ -123,20 +123,22 @@ function parsePTEMResponse(packets) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const sizePacket = packets.find(p => p[3] === 1);
|
|
126
|
-
const
|
|
126
|
+
const dataPackets = packets.filter(p => p[3] !== 1);
|
|
127
127
|
|
|
128
|
-
if (!sizePacket ||
|
|
128
|
+
if (!sizePacket || dataPackets.length === 0) {
|
|
129
129
|
return null;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
const numBMSUnits = parseInt(sizePacket.slice(4, 5).toString('utf8'), 10);
|
|
133
|
-
const floatCount = (dataPacket.length - 7) / 4;
|
|
134
133
|
|
|
135
134
|
let temperature = [];
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
dataPackets.forEach(packet => {
|
|
136
|
+
const floatCount = (packet.length - 7) / 4;
|
|
137
|
+
for (let i = 0; i < floatCount; i++) {
|
|
138
|
+
const offset = 4 + i * 4;
|
|
139
|
+
temperature.push(packet.readFloatLE(offset));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
140
142
|
|
|
141
143
|
const result = {
|
|
142
144
|
type: "PTEM",
|
|
@@ -235,7 +237,7 @@ function getDelta(parsed, options, app) {
|
|
|
235
237
|
let vesselId = (typeof app.getSelfId === 'function') ? app.getSelfId() : (app.selfId || "self");
|
|
236
238
|
const context = `vessels.${vesselId}`;
|
|
237
239
|
const prefix = options.deltaPrefix || "electrical.batteries.bms";
|
|
238
|
-
|
|
240
|
+
|
|
239
241
|
let delta = null;
|
|
240
242
|
switch (parsed.type) {
|
|
241
243
|
case "LCD1": {
|
|
@@ -245,13 +247,13 @@ function getDelta(parsed, options, app) {
|
|
|
245
247
|
updates: [{
|
|
246
248
|
timestamp: new Date().toISOString(),
|
|
247
249
|
values: [
|
|
248
|
-
{ path: `${prefix}.minCellVoltage`,
|
|
249
|
-
{ path: `${prefix}.maxCellVoltage`,
|
|
250
|
-
{ path: `${prefix}.current`,
|
|
251
|
-
//{ path: `${prefix}.
|
|
252
|
-
{ path: `${prefix}.voltage`,
|
|
253
|
-
{ path: `${prefix}.stateOfCharge`,
|
|
254
|
-
{ path: `${prefix}.stateOfHealth`,
|
|
250
|
+
{ path: `${prefix}.minCellVoltage`, value: d.minVoltage },
|
|
251
|
+
{ path: `${prefix}.maxCellVoltage`, value: d.maxVoltage },
|
|
252
|
+
{ path: `${prefix}.current`, value: d.current },
|
|
253
|
+
//{ path: `${prefix}.temperature`, value: d.temperature },
|
|
254
|
+
{ path: `${prefix}.voltage`, value: d.packVoltage },
|
|
255
|
+
{ path: `${prefix}.capacity.stateOfCharge`, value: d.soc },
|
|
256
|
+
{ path: `${prefix}.capacity.stateOfHealth`, value: d.soh }
|
|
255
257
|
]
|
|
256
258
|
}]
|
|
257
259
|
};
|
|
@@ -264,11 +266,12 @@ function getDelta(parsed, options, app) {
|
|
|
264
266
|
updates: [{
|
|
265
267
|
timestamp: new Date().toISOString(),
|
|
266
268
|
values: [
|
|
267
|
-
{ path: `${prefix}.slaveAddress`,
|
|
268
|
-
{ path: `${prefix}.
|
|
269
|
-
{ path: `${prefix}.
|
|
270
|
-
{ path: `${prefix}.
|
|
271
|
-
{ path: `${prefix}.
|
|
269
|
+
{ path: `${prefix}.slaveAddress`, value: d.minCellBmsAddress },
|
|
270
|
+
{ path: `${prefix}.minCellNumber`, value: d.minCellNumber },
|
|
271
|
+
{ path: `${prefix}.maxCellNumber`, value: d.maxCellNumber },
|
|
272
|
+
{ path: `${prefix}.maxTempSensBmsAddress`, value: d.maxTempSensBmsAddress },
|
|
273
|
+
{ path: `${prefix}.maxTempSensNumber`, value: d.maxTempSensNumber },
|
|
274
|
+
{ path: `${prefix}.capacity.dischargeSinceFull`, value: d.ah * 3600 }
|
|
272
275
|
]
|
|
273
276
|
}]
|
|
274
277
|
};
|
|
@@ -291,50 +294,62 @@ function getDelta(parsed, options, app) {
|
|
|
291
294
|
let values = [
|
|
292
295
|
{ path: `${prefix}.numBMSUnits`, value: d.numBMSUnits }
|
|
293
296
|
];
|
|
297
|
+
let metaEntries = [];
|
|
294
298
|
d.cellVoltages.forEach((voltage, index) => {
|
|
295
|
-
|
|
299
|
+
const path = `${prefix}.cellVoltage${index + 1}`;
|
|
300
|
+
values.push({ path, value: voltage });
|
|
301
|
+
metaEntries.push({ path, value: { units: "V", description: `Voltage of cell ${index + 1}` } });
|
|
296
302
|
});
|
|
297
303
|
delta = {
|
|
298
304
|
context,
|
|
299
305
|
updates: [{
|
|
300
306
|
timestamp: new Date().toISOString(),
|
|
301
|
-
values
|
|
307
|
+
values,
|
|
308
|
+
meta: metaEntries
|
|
302
309
|
}]
|
|
303
310
|
};
|
|
304
311
|
break;
|
|
305
312
|
}
|
|
306
313
|
case "PTEM": {
|
|
307
314
|
const d = parsed.data;
|
|
315
|
+
const sensorCount = (options && options.numTempSensors) ? options.numTempSensors : 1;
|
|
316
|
+
const temperatures = d.temperature.slice(0, sensorCount);
|
|
308
317
|
let values = [
|
|
309
318
|
{ path: `${prefix}.numBMSUnits`, value: d.numBMSUnits }
|
|
310
319
|
];
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
320
|
+
let metaEntries = [];
|
|
321
|
+
temperatures.forEach((temp, index) => {
|
|
322
|
+
const path = `${prefix}.cellTemperature${index + 1}`;
|
|
323
|
+
values.push({ path, value: temp + 273.15 });
|
|
324
|
+
metaEntries.push({ path, value: { units: "K", description: `Temperature of sensor ${index + 1}` } });
|
|
314
325
|
});
|
|
315
|
-
|
|
316
326
|
delta = {
|
|
317
327
|
context,
|
|
318
328
|
updates: [{
|
|
319
329
|
timestamp: new Date().toISOString(),
|
|
320
|
-
values
|
|
330
|
+
values,
|
|
331
|
+
meta: metaEntries
|
|
321
332
|
}]
|
|
322
333
|
};
|
|
323
334
|
break;
|
|
324
|
-
}
|
|
335
|
+
}
|
|
325
336
|
case "RINT": {
|
|
326
337
|
const d = parsed.data;
|
|
327
338
|
let values = [
|
|
328
339
|
{ path: `${prefix}.numBMSUnits`, value: d.numBMSUnits }
|
|
329
340
|
];
|
|
341
|
+
let metaEntries = [];
|
|
330
342
|
d.resistances.forEach((res, index) => {
|
|
331
|
-
|
|
343
|
+
const path = `${prefix}.cellResistance${index + 1}`;
|
|
344
|
+
values.push({ path, value: res });
|
|
345
|
+
metaEntries.push({ path, value: { units: "Ohm", description: `Internal DC resistance of cell ${index + 1}` } });
|
|
332
346
|
});
|
|
333
347
|
delta = {
|
|
334
348
|
context,
|
|
335
349
|
updates: [{
|
|
336
350
|
timestamp: new Date().toISOString(),
|
|
337
|
-
values
|
|
351
|
+
values,
|
|
352
|
+
meta: metaEntries
|
|
338
353
|
}]
|
|
339
354
|
};
|
|
340
355
|
break;
|
|
@@ -346,7 +361,7 @@ function getDelta(parsed, options, app) {
|
|
|
346
361
|
updates: [{
|
|
347
362
|
timestamp: new Date().toISOString(),
|
|
348
363
|
values: [
|
|
349
|
-
{ path: `${prefix}.bmsTemperature`, value: d.bmsTemperature }
|
|
364
|
+
{ path: `${prefix}.bmsTemperature`, value: d.bmsTemperature + 273.15 }
|
|
350
365
|
]
|
|
351
366
|
}]
|
|
352
367
|
};
|
|
@@ -361,13 +376,13 @@ function getDelta(parsed, options, app) {
|
|
|
361
376
|
values: [
|
|
362
377
|
{ path: `${prefix}.errorPresent`, value: d.hasError },
|
|
363
378
|
{ path: `${prefix}.errorAddress`, value: d.bmsUnit },
|
|
364
|
-
{ path: `${prefix}.errorCode`,
|
|
365
|
-
{ path: `${prefix}.errorSource`,
|
|
379
|
+
{ path: `${prefix}.errorCode`, value: d.errorCode },
|
|
380
|
+
{ path: `${prefix}.errorSource`, value: d.cellOrSensor }
|
|
366
381
|
]
|
|
367
382
|
}]
|
|
368
383
|
};
|
|
369
384
|
break;
|
|
370
|
-
}
|
|
385
|
+
}
|
|
371
386
|
|
|
372
387
|
default:
|
|
373
388
|
break;
|
|
@@ -379,15 +394,15 @@ function getDelta(parsed, options, app) {
|
|
|
379
394
|
4. EXPORTS
|
|
380
395
|
Export functions for building commands, parsing responses, and building deltas.
|
|
381
396
|
============================================================================ */
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
397
|
+
module.exports = {
|
|
398
|
+
buildCommand,
|
|
399
|
+
getDelta,
|
|
400
|
+
parseLCD1Response,
|
|
401
|
+
parseLCD3Response,
|
|
402
|
+
parseIDNResponse,
|
|
403
|
+
parseCELLResponse,
|
|
404
|
+
parsePTEMResponse,
|
|
405
|
+
parseRINTResponse,
|
|
406
|
+
parseBTEMResponse,
|
|
407
|
+
parseERROResponse
|
|
408
|
+
};
|
|
@@ -90,7 +90,9 @@ function getDelta(parsed, options, app) {
|
|
|
90
90
|
updates: [{
|
|
91
91
|
timestamp: new Date().toISOString(),
|
|
92
92
|
values: [
|
|
93
|
-
|
|
93
|
+
// Published in Ah. SK spec wants Joules for capacity.nominal but Ah is
|
|
94
|
+
// what the BMS provides and what is meaningful for battery management.
|
|
95
|
+
{ path: `${prefix}.capacity.nominal`, value: d.capacity }
|
|
94
96
|
]
|
|
95
97
|
}]
|
|
96
98
|
};
|
|
@@ -9,29 +9,32 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
|
|
|
9
9
|
? (base.endsWith('.') ? base : base + '.')
|
|
10
10
|
: "";
|
|
11
11
|
|
|
12
|
+
// 'capacity' is stored internally in Ah (as received from capacity.nominal).
|
|
13
|
+
// 'ampHourUsed' is stored internally in Ah (converted from Coulombs off the stream).
|
|
14
|
+
|
|
12
15
|
function computeAndSendDeltas() {
|
|
13
16
|
if ([voltage, current, ampHourUsed, capacity].every(v => typeof v === 'number')) {
|
|
14
17
|
const power = voltage * current;
|
|
15
18
|
const ahRemaining = capacity - ampHourUsed;
|
|
16
19
|
|
|
17
20
|
const values = [
|
|
18
|
-
{ path: prefix + 'power',
|
|
19
|
-
{ path: prefix + '
|
|
21
|
+
{ path: prefix + 'power', value: power },
|
|
22
|
+
{ path: prefix + 'capacity.remaining', value: ahRemaining }
|
|
20
23
|
];
|
|
21
24
|
|
|
22
25
|
if (current > 0 && ahRemaining < capacity) {
|
|
23
26
|
const missingAh = capacity - ahRemaining;
|
|
24
27
|
const timeToFull = (missingAh / current) * 3600;
|
|
25
|
-
values.push({ path: prefix + 'timeToFull',
|
|
26
|
-
values.push({ path: prefix + '
|
|
28
|
+
values.push({ path: prefix + 'timeToFull', value: timeToFull });
|
|
29
|
+
values.push({ path: prefix + 'capacity.timeRemaining', value: 0 });
|
|
27
30
|
} else if (current < 0 && ahRemaining > 0) {
|
|
28
31
|
const timeToEmpty = (ahRemaining / Math.abs(current)) * 3600;
|
|
29
|
-
values.push({ path: prefix + 'timeToFull',
|
|
30
|
-
values.push({ path: prefix + '
|
|
32
|
+
values.push({ path: prefix + 'timeToFull', value: 0 });
|
|
33
|
+
values.push({ path: prefix + 'capacity.timeRemaining', value: timeToEmpty });
|
|
31
34
|
} else {
|
|
32
35
|
// fallback when current is 0 or data is abnormal
|
|
33
|
-
values.push({ path: prefix + 'timeToFull',
|
|
34
|
-
values.push({ path: prefix + '
|
|
36
|
+
values.push({ path: prefix + 'timeToFull', value: 0 });
|
|
37
|
+
values.push({ path: prefix + 'capacity.timeRemaining', value: 0 });
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
sendDeltaCallback({
|
|
@@ -46,10 +49,12 @@ module.exports = function Derived(app, sendDeltaCallback, config) {
|
|
|
46
49
|
app.streambundle.getSelfStream(prefix + 'current').forEach(v => {
|
|
47
50
|
if (typeof v === 'number') { current = v; computeAndSendDeltas(); }
|
|
48
51
|
});
|
|
49
|
-
app.streambundle.getSelfStream(prefix + '
|
|
50
|
-
|
|
52
|
+
app.streambundle.getSelfStream(prefix + 'capacity.dischargeSinceFull').forEach(v => {
|
|
53
|
+
// value on stream is Coulombs; convert to Ah for internal arithmetic
|
|
54
|
+
if (typeof v === 'number') { ampHourUsed = v / 3600; computeAndSendDeltas(); }
|
|
51
55
|
});
|
|
52
|
-
app.streambundle.getSelfStream(prefix + 'capacity').forEach(v => {
|
|
56
|
+
app.streambundle.getSelfStream(prefix + 'capacity.nominal').forEach(v => {
|
|
57
|
+
// value on stream is Ah
|
|
53
58
|
if (typeof v === 'number') { capacity = v; computeAndSendDeltas(); }
|
|
54
59
|
});
|
|
55
60
|
|
|
@@ -81,7 +81,7 @@ function getDelta(parsed, options, app) {
|
|
|
81
81
|
let vesselId = (typeof app.getSelfId === 'function') ? app.getSelfId() : (app.selfId || "self");
|
|
82
82
|
const context = `vessels.${vesselId}`;
|
|
83
83
|
const prefix = options.deltaPrefix || "electrical.batteries.bms";
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
let delta = null;
|
|
86
86
|
switch (parsed.type) {
|
|
87
87
|
case "TMAX": {
|
|
@@ -91,7 +91,7 @@ function getDelta(parsed, options, app) {
|
|
|
91
91
|
updates: [{
|
|
92
92
|
timestamp: new Date().toISOString(),
|
|
93
93
|
values: [
|
|
94
|
-
{ path: `${prefix}.cellOverTempSwitchOff`, value: d.cellOverTempSwitchOff }
|
|
94
|
+
{ path: `${prefix}.cellOverTempSwitchOff`, value: d.cellOverTempSwitchOff + 273.15 }
|
|
95
95
|
]
|
|
96
96
|
}]
|
|
97
97
|
};
|
|
@@ -104,7 +104,7 @@ function getDelta(parsed, options, app) {
|
|
|
104
104
|
updates: [{
|
|
105
105
|
timestamp: new Date().toISOString(),
|
|
106
106
|
values: [
|
|
107
|
-
{ path: `${prefix}.underTempChargeDisable`, value: d.underTempChargeDisable }
|
|
107
|
+
{ path: `${prefix}.underTempChargeDisable`, value: d.underTempChargeDisable + 273.15 }
|
|
108
108
|
]
|
|
109
109
|
}]
|
|
110
110
|
};
|
|
@@ -117,7 +117,7 @@ function getDelta(parsed, options, app) {
|
|
|
117
117
|
updates: [{
|
|
118
118
|
timestamp: new Date().toISOString(),
|
|
119
119
|
values: [
|
|
120
|
-
{ path: `${prefix}.bmsOverTempSwitchOff`, value: d.bmsOverTempSwitchOff }
|
|
120
|
+
{ path: `${prefix}.bmsOverTempSwitchOff`, value: d.bmsOverTempSwitchOff + 273.15 }
|
|
121
121
|
]
|
|
122
122
|
}]
|
|
123
123
|
};
|
|
@@ -130,7 +130,7 @@ function getDelta(parsed, options, app) {
|
|
|
130
130
|
updates: [{
|
|
131
131
|
timestamp: new Date().toISOString(),
|
|
132
132
|
values: [
|
|
133
|
-
{ path: `${prefix}.bmsOverTempSwitchOffHysteresis`, value: d.bmsOverTempSwitchOffHysteresis }
|
|
133
|
+
{ path: `${prefix}.bmsOverTempSwitchOffHysteresis`, value: d.bmsOverTempSwitchOffHysteresis + 273.15 }
|
|
134
134
|
]
|
|
135
135
|
}]
|
|
136
136
|
};
|
package/public/index.html
CHANGED
|
@@ -24,20 +24,28 @@
|
|
|
24
24
|
<div class="stats-panel">
|
|
25
25
|
<div class="metric-box">
|
|
26
26
|
<div class="metric-label">Capacity</div>
|
|
27
|
-
<div class="metric-value" id="capacity">--.-- Ah</div>
|
|
27
|
+
<div class="metric-value" id="capacity-nominal">--.-- Ah</div>
|
|
28
28
|
</div>
|
|
29
29
|
<div class="metric-box">
|
|
30
30
|
<div class="metric-label">Amp Hours Remaining</div>
|
|
31
|
-
<div class="metric-value" id="
|
|
31
|
+
<div class="metric-value" id="capacity-remaining">--.-- Ah</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="metric-box">
|
|
34
|
+
<div class="metric-label">Time Remaining</div>
|
|
35
|
+
<div class="metric-value" id="capacity-timeRemaining">--.-- h</div>
|
|
32
36
|
</div>
|
|
33
37
|
<div class="metric-box">
|
|
34
38
|
<div class="metric-label">Amp Hours Used</div>
|
|
35
|
-
<div class="metric-value" id="
|
|
39
|
+
<div class="metric-value" id="capacity-dischargeSinceFull">--.-- Ah</div>
|
|
36
40
|
</div>
|
|
37
41
|
<div class="metric-box">
|
|
38
42
|
<div class="metric-label">State of Charge</div>
|
|
39
43
|
<div class="metric-value" id="stateOfCharge">-- %</div>
|
|
40
44
|
</div>
|
|
45
|
+
<div class="metric-box">
|
|
46
|
+
<div class="metric-label">State of Health</div>
|
|
47
|
+
<div class="metric-value" id="stateOfHealth">-- %</div>
|
|
48
|
+
</div>
|
|
41
49
|
<div class="metric-box">
|
|
42
50
|
<div class="metric-label">Pack Voltage</div>
|
|
43
51
|
<div class="metric-value" id="voltage">--.-- V</div>
|
|
@@ -77,39 +85,38 @@
|
|
|
77
85
|
<div id="cell-towers" class="towers-container"></div>
|
|
78
86
|
</div>
|
|
79
87
|
|
|
80
|
-
<div id="settings-view" class="content-view">
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
<div id="settings-view" class="content-view">
|
|
89
|
+
<div class="settings-container">
|
|
90
|
+
<h2 class="settings-title">BMS Settings</h2>
|
|
91
|
+
<div class="settings-section" id="settings-command-grid">
|
|
92
|
+
</div>
|
|
93
|
+
<div class="settings-section" style="margin-top: 25px;">
|
|
94
|
+
<h3>Manual BMS Command</h3>
|
|
95
|
+
<form id="command-form" class="settings-form-group">
|
|
96
|
+
<label for="command-input" class="settings-label">
|
|
97
|
+
Enter Command (e.g., <code>BVOL 3.25</code>)
|
|
98
|
+
</label>
|
|
99
|
+
<input
|
|
100
|
+
type="text"
|
|
101
|
+
id="command-input"
|
|
102
|
+
class="settings-input"
|
|
103
|
+
placeholder="Enter command like 'BVOL 3.25'"
|
|
104
|
+
required
|
|
105
|
+
/>
|
|
106
|
+
<div class="settings-buttons">
|
|
107
|
+
<button type="submit" class="settings-button primary">Send Command</button>
|
|
108
|
+
</div>
|
|
109
|
+
</form>
|
|
110
|
+
<div
|
|
111
|
+
id="command-response"
|
|
112
|
+
class="settings-form-group settings-label"
|
|
113
|
+
style="margin-top: 10px;"
|
|
114
|
+
></div>
|
|
115
|
+
</div>
|
|
100
116
|
</div>
|
|
101
|
-
</
|
|
102
|
-
<div
|
|
103
|
-
id="command-response"
|
|
104
|
-
class="settings-form-group settings-label"
|
|
105
|
-
style="margin-top: 10px;"
|
|
106
|
-
></div>
|
|
117
|
+
</div>
|
|
107
118
|
</div>
|
|
108
|
-
|
|
109
119
|
</div>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
|
|
113
120
|
|
|
114
121
|
<div class="info-box">
|
|
115
122
|
<a href="https://github.com/ofernander/signalk-rec-bms" target="_blank">signalk-rec-bms on GitHub</a>
|
|
@@ -123,7 +130,7 @@
|
|
|
123
130
|
document.getElementById('nav-settings').classList.remove('active');
|
|
124
131
|
});
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
document.getElementById('nav-settings').addEventListener('click', function() {
|
|
127
134
|
document.getElementById('status-view').classList.remove('active');
|
|
128
135
|
document.getElementById('settings-view').classList.add('active');
|
|
129
136
|
document.getElementById('nav-settings').classList.add('active');
|
package/public/js/main.js
CHANGED
|
@@ -14,18 +14,19 @@ class BMSSystem {
|
|
|
14
14
|
voltage: null,
|
|
15
15
|
current: null,
|
|
16
16
|
power: null,
|
|
17
|
-
stateOfCharge: null,
|
|
18
|
-
stateOfHealth: null,
|
|
17
|
+
'capacity.stateOfCharge': null,
|
|
18
|
+
'capacity.stateOfHealth': null,
|
|
19
|
+
'capacity.remaining': null,
|
|
20
|
+
'capacity.dischargeSinceFull': null,
|
|
21
|
+
'capacity.nominal': null,
|
|
22
|
+
'capacity.timeRemaining': null,
|
|
19
23
|
cellVoltages: {},
|
|
20
24
|
cellResistances: {},
|
|
21
25
|
cellVoltageDifference: null,
|
|
22
26
|
bmsTemperature: null,
|
|
23
27
|
cellTemperature1: null,
|
|
24
28
|
maxCellVoltage: null,
|
|
25
|
-
minCellVoltage: null
|
|
26
|
-
ampHourRemaining: null,
|
|
27
|
-
ampHourUsed: null,
|
|
28
|
-
capacity: null
|
|
29
|
+
minCellVoltage: null
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -38,16 +39,27 @@ class BMSSystem {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
cacheDomElements() {
|
|
41
|
-
const
|
|
42
|
-
'voltage'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
const metricMap = {
|
|
43
|
+
'voltage': 'voltage',
|
|
44
|
+
'current': 'current',
|
|
45
|
+
'power': 'power',
|
|
46
|
+
'capacity.stateOfCharge': 'stateOfCharge',
|
|
47
|
+
'capacity.stateOfHealth': 'stateOfHealth',
|
|
48
|
+
'cellVoltageDifference': 'cellVoltageDifference',
|
|
49
|
+
'bmsTemperature': 'bmsTemperature',
|
|
50
|
+
'cellTemperature1': 'cellTemperature1',
|
|
51
|
+
'maxCellVoltage': 'maxCellVoltage',
|
|
52
|
+
'minCellVoltage': 'minCellVoltage',
|
|
53
|
+
'capacity.remaining': 'capacity-remaining',
|
|
54
|
+
'capacity.dischargeSinceFull': 'capacity-dischargeSinceFull',
|
|
55
|
+
'capacity.nominal': 'capacity-nominal',
|
|
56
|
+
'capacity.timeRemaining': 'capacity-timeRemaining'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
Object.entries(metricMap).forEach(([key, id]) => {
|
|
60
|
+
this.dom.metricElements[key] = document.getElementById(id);
|
|
61
|
+
if (!this.dom.metricElements[key]) {
|
|
62
|
+
console.warn(`Missing metric element for key: ${key} (id: ${id})`);
|
|
51
63
|
}
|
|
52
64
|
});
|
|
53
65
|
}
|
|
@@ -88,33 +100,58 @@ class BMSSystem {
|
|
|
88
100
|
if (el) el.textContent = value !== null ? `${value.toFixed(2)}${unit}` : "--";
|
|
89
101
|
};
|
|
90
102
|
|
|
91
|
-
setValue(this.dom.metricElements
|
|
92
|
-
setValue(this.dom.metricElements
|
|
93
|
-
setValue(this.dom.metricElements
|
|
103
|
+
setValue(this.dom.metricElements['voltage'], this.liveData.voltage, "V");
|
|
104
|
+
setValue(this.dom.metricElements['current'], this.liveData.current, "A");
|
|
105
|
+
setValue(this.dom.metricElements['power'], this.liveData.power, "W");
|
|
94
106
|
|
|
95
107
|
setValue(
|
|
96
|
-
this.dom.metricElements.stateOfCharge,
|
|
97
|
-
this.liveData.stateOfCharge !== null ? this.liveData.stateOfCharge * 100 : null,
|
|
108
|
+
this.dom.metricElements['capacity.stateOfCharge'],
|
|
109
|
+
this.liveData['capacity.stateOfCharge'] !== null ? this.liveData['capacity.stateOfCharge'] * 100 : null,
|
|
98
110
|
"%"
|
|
99
111
|
);
|
|
100
112
|
setValue(
|
|
101
|
-
this.dom.metricElements.stateOfHealth,
|
|
102
|
-
this.liveData.stateOfHealth !== null ? this.liveData.stateOfHealth * 100 : null,
|
|
113
|
+
this.dom.metricElements['capacity.stateOfHealth'],
|
|
114
|
+
this.liveData['capacity.stateOfHealth'] !== null ? this.liveData['capacity.stateOfHealth'] * 100 : null,
|
|
103
115
|
"%"
|
|
104
116
|
);
|
|
105
117
|
|
|
106
|
-
if (this.dom.metricElements
|
|
118
|
+
if (this.dom.metricElements['cellVoltageDifference']) {
|
|
107
119
|
const v = this.liveData.cellVoltageDifference;
|
|
108
|
-
this.dom.metricElements
|
|
120
|
+
this.dom.metricElements['cellVoltageDifference'].textContent = v !== null ? `${v.toFixed(4)}V` : "--";
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
setValue(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
// Values arrive in Kelvin; subtract 273.15 for °C display
|
|
124
|
+
// Note: stopgap until frontend rewrite — SK will handle unit display natively
|
|
125
|
+
setValue(
|
|
126
|
+
this.dom.metricElements['bmsTemperature'],
|
|
127
|
+
this.liveData.bmsTemperature !== null ? this.liveData.bmsTemperature - 273.15 : null,
|
|
128
|
+
"°C"
|
|
129
|
+
);
|
|
130
|
+
setValue(
|
|
131
|
+
this.dom.metricElements['cellTemperature1'],
|
|
132
|
+
this.liveData.cellTemperature1 !== null ? this.liveData.cellTemperature1 - 273.15 : null,
|
|
133
|
+
"°C"
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
setValue(this.dom.metricElements['maxCellVoltage'], this.liveData.maxCellVoltage, "V");
|
|
137
|
+
setValue(this.dom.metricElements['minCellVoltage'], this.liveData.minCellVoltage, "V");
|
|
138
|
+
|
|
139
|
+
setValue(this.dom.metricElements['capacity.nominal'], this.liveData['capacity.nominal'], "Ah");
|
|
140
|
+
setValue(this.dom.metricElements['capacity.remaining'], this.liveData['capacity.remaining'], "Ah");
|
|
141
|
+
|
|
142
|
+
// dischargeSinceFull arrives in Coulombs; display as Ah
|
|
143
|
+
setValue(
|
|
144
|
+
this.dom.metricElements['capacity.dischargeSinceFull'],
|
|
145
|
+
this.liveData['capacity.dischargeSinceFull'] !== null ? this.liveData['capacity.dischargeSinceFull'] / 3600 : null,
|
|
146
|
+
"Ah"
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// timeRemaining arrives in seconds; display as hours
|
|
150
|
+
setValue(
|
|
151
|
+
this.dom.metricElements['capacity.timeRemaining'],
|
|
152
|
+
this.liveData['capacity.timeRemaining'] !== null ? this.liveData['capacity.timeRemaining'] / 3600 : null,
|
|
153
|
+
"h"
|
|
154
|
+
);
|
|
118
155
|
}
|
|
119
156
|
|
|
120
157
|
connectWebSocket() {
|
|
@@ -123,19 +160,19 @@ class BMSSystem {
|
|
|
123
160
|
const ws = new WebSocket(
|
|
124
161
|
`${wsScheme}://${window.location.host}/signalk/v1/stream?subscribe=self&path=electrical.batteries.bms.*`
|
|
125
162
|
);
|
|
126
|
-
|
|
163
|
+
|
|
127
164
|
ws.onopen = () => console.log("WebSocket connected");
|
|
128
165
|
ws.onerror = (error) => console.error("WebSocket error:", error);
|
|
129
166
|
ws.onclose = () => console.log("WebSocket connection closed");
|
|
130
|
-
|
|
167
|
+
|
|
131
168
|
ws.onmessage = (event) => {
|
|
132
169
|
const delta = JSON.parse(event.data);
|
|
133
170
|
if (!delta.updates) return;
|
|
134
|
-
|
|
171
|
+
|
|
135
172
|
delta.updates.forEach(update => {
|
|
136
173
|
update.values?.forEach(({ path, value }) => {
|
|
137
174
|
if (!path.startsWith(this.prefix)) return;
|
|
138
|
-
|
|
175
|
+
|
|
139
176
|
const key = path.slice(this.prefix.length);
|
|
140
177
|
if (key in this.liveData) {
|
|
141
178
|
this.liveData[key] = value;
|
|
@@ -146,7 +183,7 @@ class BMSSystem {
|
|
|
146
183
|
}
|
|
147
184
|
});
|
|
148
185
|
});
|
|
149
|
-
|
|
186
|
+
|
|
150
187
|
this.updateMetrics();
|
|
151
188
|
CellVisualizer.updateAllCells({
|
|
152
189
|
cellVoltages: this.liveData.cellVoltages,
|
|
@@ -156,7 +193,7 @@ class BMSSystem {
|
|
|
156
193
|
} catch (error) {
|
|
157
194
|
console.error("WebSocket initialization failed:", error);
|
|
158
195
|
}
|
|
159
|
-
}
|
|
196
|
+
}
|
|
160
197
|
}
|
|
161
198
|
|
|
162
199
|
document.addEventListener('DOMContentLoaded', () => {
|
package/public/js/settings.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export class BMSSettings {
|
|
2
2
|
constructor() {
|
|
3
|
+
this.tempCommands = new Set(['TMAX', 'TMIN', 'TBAL', 'BMTH']);
|
|
4
|
+
|
|
3
5
|
this.commandGroups = [
|
|
4
6
|
{
|
|
5
7
|
title: "Voltage Settings",
|
|
@@ -54,7 +56,7 @@ export class BMSSettings {
|
|
|
54
56
|
IOFF: "currentSensorOffset",
|
|
55
57
|
IOJA: "currentSensorCoefficient",
|
|
56
58
|
CYCL: "batteryCycleCount",
|
|
57
|
-
CAPA: "capacity",
|
|
59
|
+
CAPA: "capacity.nominal",
|
|
58
60
|
CHEM: "chemistry",
|
|
59
61
|
SOCH: "socHysteresis",
|
|
60
62
|
SOCS: "socReset",
|
|
@@ -72,7 +74,7 @@ export class BMSSettings {
|
|
|
72
74
|
CAL2: "calibrationOffsetCell2",
|
|
73
75
|
CAL3: "calibrationOffsetCell3",
|
|
74
76
|
CAL4: "calibrationOffsetCell4",
|
|
75
|
-
SERI: "
|
|
77
|
+
SERI: "deviceName",
|
|
76
78
|
TIME: "time",
|
|
77
79
|
DATE: "date",
|
|
78
80
|
RSBR: "balancingCellNumber"
|
|
@@ -155,24 +157,23 @@ export class BMSSettings {
|
|
|
155
157
|
});
|
|
156
158
|
});
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
160
|
+
container.querySelectorAll("button[data-command]").forEach(button => {
|
|
161
|
+
button.addEventListener("click", async () => {
|
|
162
|
+
const cmd = button.getAttribute("data-command");
|
|
163
|
+
const inputField = document.getElementById(cmd);
|
|
164
|
+
const value = inputField.value.trim();
|
|
165
|
+
if (!value) return;
|
|
166
|
+
|
|
167
|
+
const response = await this.sendCommand(`${cmd} ${value}`);
|
|
168
|
+
const success = response && typeof response === 'object' &&
|
|
169
|
+
Object.keys(response).length > 0;
|
|
170
|
+
|
|
171
|
+
inputField.value = success ? "OK" : "FAIL";
|
|
172
|
+
|
|
173
|
+
setTimeout(() => this.prefillValues(), 5000);
|
|
174
|
+
});
|
|
173
175
|
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
+
}
|
|
176
177
|
|
|
177
178
|
async prefillValues() {
|
|
178
179
|
try {
|
|
@@ -185,10 +186,16 @@ export class BMSSettings {
|
|
|
185
186
|
if (!field) continue;
|
|
186
187
|
|
|
187
188
|
const path = this.commandToDeltaPath[cmd];
|
|
188
|
-
|
|
189
|
+
if (!path) continue;
|
|
190
|
+
|
|
191
|
+
// Support dot-notation paths (e.g. "capacity.nominal")
|
|
192
|
+
const val = path.split('.').reduce((obj, key) => obj?.[key], bmsData)?.value;
|
|
189
193
|
|
|
190
194
|
if (val !== undefined) {
|
|
191
|
-
|
|
195
|
+
// Temperature values are stored in Kelvin; display in °C for user
|
|
196
|
+
field.value = this.tempCommands.has(cmd)
|
|
197
|
+
? (val - 273.15).toFixed(3)
|
|
198
|
+
: val;
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
}
|