Yesterday, 16:47
Setup: LM firmware 2025, Visu, apps/js/localbus.js (v=20251031), Android kiosk panels (WebView) plus desktop Chrome for comparison.
PROBLEM
On some corporate networks a middlebox (IPS / application control / proxy / NAC policy) resets established TCP sessions a few seconds after they open. Visu's live-data WebSocket is hit by this; the classic HTTP-polling visualization is not. With the current client logic the visible effect and the server load are much larger than necessary:
- on every WebSocket close, localbus.js waits 1 s,
- then re-downloads the FULL object registry via POST /apps/localbus.lp (about 285 KB in our project, 2832 objects),
- and only then reopens the WebSocket.
So each reset costs the user ~3 s of "reconnecting" spinner with frozen values, and costs the LM a full registry regeneration.
Field symptom that started this: WebSocket killed 1-1.5 s after open, floor plan stays rendered, spinner flashes every ~3 s, values freeze. A laptop on the same LAN (not subject to the policy) worked fine.
WHAT WE MEASURED
Lab: a transparent TCP relay between client and LM that kills every session after N seconds.
1) Current client, Android panel, kill after 5 s
WS lifetime 5.0 s, visible gap 2.9-3.0 s per cycle, ~37 % of the time disconnected,
server work per cycle: registry POST 285 KB + WS handshake.
2) Current client, Android panel, kill after 2 s
The registry POST never completes, page stays blank with a permanent spinner.
3) Patched client (see below), desktop Chrome, kill after 5 s
WS lifetime 5.06 s, visible gap 22-37 ms, < 1 % disconnected, server work: WS handshake only (~4 KB).
4) Patched client, desktop Chrome, kill after 1.5 s (the field case)
WS lifetime 1.55 s, gap avg 25 ms / max 29 ms, 1.5 % disconnected, live values keep flowing.
5) Control, kill only after 15 s idle
No disconnects at all: the client "ping" every 10 s keeps the socket alive, so idle timeouts >= 11 s are not the cause.
Also verified: the server ACCEPTS a new WebSocket in the same session without a preceding registry POST (opened in 21 ms, live messages received).
OBSERVED CLIENT LOGIC (localbus.js)
onclose -> setstate(false) -> onerror() -> setTimeout(init, 1000) -> init(): POST /apps/localbus.lp (full registry) -> register() -> start(): new WebSocket.
No backoff. The registry is re-fetched on every reconnect although objectstore already holds it.
REQUESTED CHANGE (small)
1. On WebSocket close, if objectstore is already populated, reopen the WebSocket directly (start()), without the 1 s delay and without re-downloading the registry.
2. Keep the full init() path only when the registry is empty or the previous session was rejected (lived < ~300 ms), so an expired session still recovers.
3. Add a simple cap/backoff (e.g. max 60 fast reconnects per minute, then 1 s delay) to protect the server under persistent resets.
4. Optional: expose reconnect counters (count, last lifetime, last gap) on the localbus object for diagnostics.
WHY IT MATTERS FOR THE SERVER TOO
Under a 1.5 s reset policy with 20 panels the current logic makes the LM regenerate about 1.4 MB/s of registry JSON continuously; the patched logic reduces that to WebSocket handshakes (about 60 KB/s). On healthy networks the change has zero effect.
REFERENCE IMPLEMENTATION (tested as Visu Custom JS, wraps start/onerror only)
(function () {
var CFG = { FAST_LIMIT_PER_MIN: 60, SLOW_WAIT_MS: 1000, REJECT_LIFE_MS: 300, WAIT_FOR_LOCALBUS_MS: 30000 };
if (window.__sonaLb) return;
window.__sonaLb = { version: 'V1', active: false, fast: 0, slow: 0, full: 0, lastLifeMs: null, lastGapMs: null, sessions: 0 };
function install() {
var lb = window.localbus;
if (!lb || typeof lb.start !== 'function' || typeof lb.onerror !== 'function' || typeof lb.init !== 'function') {
console.warn('[sona-lb] unexpected localbus structure, patch not applied'); return;
}
var S = window.__sonaLb, hist = [], startT = 0, closeT = 0;
var origStart = lb.start, origOnError = lb.onerror;
lb.start = function () {
startT = Date.now(); S.sessions++;
if (closeT) { S.lastGapMs = startT - closeT; closeT = 0; }
return origStart.apply(this, arguments);
};
lb.onerror = function () {
var now = Date.now();
clearTimeout(this.inittimer); this.inittimer = null;
closeT = now;
S.lastLifeMs = startT ? now - startT : null;
var haveRegistry = this.objectstore && Object.keys(this.objectstore).length > 0;
var rejected = S.lastLifeMs !== null && S.lastLifeMs < CFG.REJECT_LIFE_MS;
if (!haveRegistry || rejected) { S.full++; return origOnError.call(this); } // default path: 1 s + POST + WS
hist = hist.filter(function (t) { return now - t < 60000; });
var self = this;
if (hist.length >= CFG.FAST_LIMIT_PER_MIN) { // cap: wait, but no POST
S.slow++;
this.inittimer = setTimeout(function () { self.inittimer = null; self.start(); }, CFG.SLOW_WAIT_MS);
return;
}
hist.push(now); S.fast++;
this.start(); // fast path: new WebSocket now
};
S.active = true;
console.info('[sona-lb] fast reconnect active (' + S.version + ')');
}
if (window.localbus) { install(); return; }
var t0 = Date.now();
var timer = setInterval(function () {
if (window.localbus) { clearInterval(timer); install(); }
else if (Date.now() - t0 > CFG.WAIT_FOR_LOCALBUS_MS) { clearInterval(timer); console.warn('[sona-lb] localbus not found'); }
}, 200);
})();
We can share the raw measurement logs and the relay tool used to reproduce this.
PROBLEM
On some corporate networks a middlebox (IPS / application control / proxy / NAC policy) resets established TCP sessions a few seconds after they open. Visu's live-data WebSocket is hit by this; the classic HTTP-polling visualization is not. With the current client logic the visible effect and the server load are much larger than necessary:
- on every WebSocket close, localbus.js waits 1 s,
- then re-downloads the FULL object registry via POST /apps/localbus.lp (about 285 KB in our project, 2832 objects),
- and only then reopens the WebSocket.
So each reset costs the user ~3 s of "reconnecting" spinner with frozen values, and costs the LM a full registry regeneration.
Field symptom that started this: WebSocket killed 1-1.5 s after open, floor plan stays rendered, spinner flashes every ~3 s, values freeze. A laptop on the same LAN (not subject to the policy) worked fine.
WHAT WE MEASURED
Lab: a transparent TCP relay between client and LM that kills every session after N seconds.
1) Current client, Android panel, kill after 5 s
WS lifetime 5.0 s, visible gap 2.9-3.0 s per cycle, ~37 % of the time disconnected,
server work per cycle: registry POST 285 KB + WS handshake.
2) Current client, Android panel, kill after 2 s
The registry POST never completes, page stays blank with a permanent spinner.
3) Patched client (see below), desktop Chrome, kill after 5 s
WS lifetime 5.06 s, visible gap 22-37 ms, < 1 % disconnected, server work: WS handshake only (~4 KB).
4) Patched client, desktop Chrome, kill after 1.5 s (the field case)
WS lifetime 1.55 s, gap avg 25 ms / max 29 ms, 1.5 % disconnected, live values keep flowing.
5) Control, kill only after 15 s idle
No disconnects at all: the client "ping" every 10 s keeps the socket alive, so idle timeouts >= 11 s are not the cause.
Also verified: the server ACCEPTS a new WebSocket in the same session without a preceding registry POST (opened in 21 ms, live messages received).
OBSERVED CLIENT LOGIC (localbus.js)
onclose -> setstate(false) -> onerror() -> setTimeout(init, 1000) -> init(): POST /apps/localbus.lp (full registry) -> register() -> start(): new WebSocket.
No backoff. The registry is re-fetched on every reconnect although objectstore already holds it.
REQUESTED CHANGE (small)
1. On WebSocket close, if objectstore is already populated, reopen the WebSocket directly (start()), without the 1 s delay and without re-downloading the registry.
2. Keep the full init() path only when the registry is empty or the previous session was rejected (lived < ~300 ms), so an expired session still recovers.
3. Add a simple cap/backoff (e.g. max 60 fast reconnects per minute, then 1 s delay) to protect the server under persistent resets.
4. Optional: expose reconnect counters (count, last lifetime, last gap) on the localbus object for diagnostics.
WHY IT MATTERS FOR THE SERVER TOO
Under a 1.5 s reset policy with 20 panels the current logic makes the LM regenerate about 1.4 MB/s of registry JSON continuously; the patched logic reduces that to WebSocket handshakes (about 60 KB/s). On healthy networks the change has zero effect.
REFERENCE IMPLEMENTATION (tested as Visu Custom JS, wraps start/onerror only)
(function () {
var CFG = { FAST_LIMIT_PER_MIN: 60, SLOW_WAIT_MS: 1000, REJECT_LIFE_MS: 300, WAIT_FOR_LOCALBUS_MS: 30000 };
if (window.__sonaLb) return;
window.__sonaLb = { version: 'V1', active: false, fast: 0, slow: 0, full: 0, lastLifeMs: null, lastGapMs: null, sessions: 0 };
function install() {
var lb = window.localbus;
if (!lb || typeof lb.start !== 'function' || typeof lb.onerror !== 'function' || typeof lb.init !== 'function') {
console.warn('[sona-lb] unexpected localbus structure, patch not applied'); return;
}
var S = window.__sonaLb, hist = [], startT = 0, closeT = 0;
var origStart = lb.start, origOnError = lb.onerror;
lb.start = function () {
startT = Date.now(); S.sessions++;
if (closeT) { S.lastGapMs = startT - closeT; closeT = 0; }
return origStart.apply(this, arguments);
};
lb.onerror = function () {
var now = Date.now();
clearTimeout(this.inittimer); this.inittimer = null;
closeT = now;
S.lastLifeMs = startT ? now - startT : null;
var haveRegistry = this.objectstore && Object.keys(this.objectstore).length > 0;
var rejected = S.lastLifeMs !== null && S.lastLifeMs < CFG.REJECT_LIFE_MS;
if (!haveRegistry || rejected) { S.full++; return origOnError.call(this); } // default path: 1 s + POST + WS
hist = hist.filter(function (t) { return now - t < 60000; });
var self = this;
if (hist.length >= CFG.FAST_LIMIT_PER_MIN) { // cap: wait, but no POST
S.slow++;
this.inittimer = setTimeout(function () { self.inittimer = null; self.start(); }, CFG.SLOW_WAIT_MS);
return;
}
hist.push(now); S.fast++;
this.start(); // fast path: new WebSocket now
};
S.active = true;
console.info('[sona-lb] fast reconnect active (' + S.version + ')');
}
if (window.localbus) { install(); return; }
var t0 = Date.now();
var timer = setInterval(function () {
if (window.localbus) { clearInterval(timer); install(); }
else if (Date.now() - t0 > CFG.WAIT_FOR_LOCALBUS_MS) { clearInterval(timer); console.warn('[sona-lb] localbus not found'); }
}, 200);
})();
We can share the raw measurement logs and the relay tool used to reproduce this.