signalk-rec-bms 1.1.2 → 1.1.4

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-rec-bms",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "SignalK plugin for REC-BMS",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
@@ -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
- const cellVoltages = {};
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 (!subscribedCellStreams && typeof units === 'number' && units > 0) {
62
- totalCells = units * 4;
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
- const keys = Object.keys(cellVoltages).map(k => parseInt(k, 10));
78
- if (keys.length === totalCells) {
79
- const vals = keys.map(i => cellVoltages[i]);
80
- const minV = Math.min(...vals);
81
- const maxV = Math.max(...vals);
82
- sendDeltaCallback({
83
- updates: [{
84
- values: [
85
- { path: prefix + 'cellVoltageDifference', value: maxV - minV }
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
- setTimeout(trySendCellDiff, 10);
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', 'stateOfHealth',
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.error(`Missing metric element: ${metric}`);
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 ws = new WebSocket(`ws://${window.location.host}/signalk/v1/stream?subscribe=self&path=electrical.batteries.bms.*`);
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', () => {