Building Resilient Edge Agents with LBH: A PWA Heartbeat System Running on Android
15 de septiembre de 2026 | Nodo A16-SanMiguel-SV
Most edge demos look great on a laptop. They tend to fall apart the moment you put them on a real Android phone, switch apps, or lock the screen.
I'm currently building HormigasAIS, a lightweight edge architecture designed to run agents and services from constrained devices. One of the core pieces is a binary protocol called LBH (Lenguaje Binario HormigasAIS) and a small WebSocket client that keeps the channel alive under real mobile conditions.
This post shows the current state of that client — specifically the heartbeat + telemetry layer — a recent resilience improvement for Android background behavior, and what happened when I moved from a browser demo to an actual installed app on the phone.
The Core: LBHHeartbeatAnt
The client is a single class that manages:
- WebSocket connection to a local Edge Node (
ws://hostname:8765) - Binary LBH frames
- Heartbeat (pheromone) messages
- Inactivity detection
- Automatic reconnection
Key design decisions:
- Binary frames start with magic bytes
LA(0x4C,0x41) - Type
0x01→ Telemetry - Type
0x02→ Heartbeat / ACK (called "Feromona") - When the user is inactive for 30 seconds, the client enters a
HIBERNATINGstate while keeping the socket open - Any user activity (click, touch, keydown…) "unfreezes" the channel
Here's the essential part of the heartbeat emission:
emitHeartbeatPheromone() {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setUint8(0, 0x4C); // L
view.setUint8(1, 0x41); // A
view.setUint8(2, 0x02); // Heartbeat / Feromona
view.setUint8(3, 0x01); // Standby flag
view.setUint32(4, 9999);
view.setUint32(8, 0);
view.setUint32(12, Math.floor(Date.now() / 1000));
this.ws.send(buffer);
}
Telemetry frames follow the same structure (type 0x01) and carry a sensor ID + value.
Real Runtime Behavior
From the Edge Node logs you can see the system working:
🐜 Trama 0x01 | Sensor=1001 | Val=600 | TS=1789366095
⚠️ [AGENTE AUTÓNOMO] Alerta Sensor 1001 → Val=600
🐜 Respuesta LBH enviada
🐜 Trama 0x01 | Sensor=1001 | Val=400 | TS=1789366395
🐜 Respuesta LBH enviada
And the continuous heartbeat ACKs:
❄️ Feromona 0x02 ACK (socket preservado)
The UI on the phone simply shows a telemetry input and a green "Enviar Trama LBH" button. When the channel is healthy it displays "Canal Activo (Descongelado)".
The Android Problem and the Fix
Android browsers are aggressive. When the PWA goes to the background, the WebSocket is often killed without a clean onclose event. The normal 3-second reconnect timer is too slow for a good user experience.
I added a visibilitychange listener:
setupVisibilityListener() {
document.addEventListener("visibilitychange", () => {
if (document.hidden) return;
const isDead = !this.ws ||
this.ws.readyState === WebSocket.CLOSED ||
this.ws.readyState === WebSocket.CLOSING;
if (isDead) {
this._setState("OFFLINE", "🟡 Verificando canal tras reanudar...");
}
this.connect(); // safe to call repeatedly
});
}
Now, as soon as the user returns to the app, the client checks the socket state and reconnects immediately if needed.
After writing the piece above, I installed the PWA directly on the Android device (using the browser's install prompt, not just a
localhost tab) and tested it with split-screen: the app running in one pane, the Edge Node's live log in the other. With the screen locked or the app pushed to the background, the log kept showing Feromona 0x02 ACK entries continuously — confirming the socket survives backgrounding as an installed app, not only as an open tab.
A known rough edge, still open: switching visibility very quickly and repeatedly (fast app-switching) can occasionally log a "cliente conectado" entry before its matching "cliente desconectado" from the previous socket. The server handles both cleanly — no leaked connections, no corrupted frames — but the log ordering isn't strictly sequential in that edge case. It comes from a small gap in the connect() guard (it currently checks for OPEN/CONNECTING but not CLOSING). Tightening that is on the list.
Also worth being upfront about: the current WebSocket server has no authentication layer. Any client on the same local network can send LBH frames to it. Fine for a local dev/demo node — not something to expose on a public network as-is.
Current Focus
This is still early-stage infrastructure. The goal is to turn these lightweight LBH agents into injectable services that universities and small/medium businesses (Pymes) can use without depending on heavy cloud stacks.
The entire development loop (coding, testing, sealing, deploying) happens from an Android device running Termux.
What's Next
- More formal agent decision layer
- Cleaner packaging of the PWA + Node
- Authentication for the Edge Node's WebSocket endpoint
- Fixing the
CLOSING-state race in the reconnect guard - Documentation and examples aimed at educational and SME use cases
If you're working on edge systems, constrained devices, or protocol design and want to exchange notes, feel free to reach out.
VALIDADO_ARTICULO_LBH_HEARTBEAT_20260915_CLHQ
Built and tested on Node A16-SanMiguel-SV | Protocol + client: HormigasAIS
Volver al blog