Real-Time Data via WebSocket
The Molecule platform exposes a WebSocket endpoint that allows your application to receive real-time telemetry data from a connected gateway. This guide covers everything you need to establish a connection, authenticate, and handle live data.
Overview
The connection flow has three stages:
- Authenticate - obtain a JWT from the Molecule identity server
- Connect - open a WebSocket and send a binary handshake containing your gateway ID and JWT
- Receive - handle incoming binary frames from the gateway
Prerequisites
| Requirement | Detail |
|---|---|
| Gateway ID | Provided by your Molecule account manager |
| Molecule credentials | Username and password for id.moleculesystems.com |
| WebSocket endpoint | wss://wsau.moleculesystems.com/ws |
Step 1: Authenticate
Obtain a JWT from the Molecule identity server using the password grant:
const authenticate = async (username, password) => {
const response = await fetch('https://id.moleculesystems.com/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
username,
password,
client_id: 'molecule-api',
grant_type: 'password',
}),
});
const data = await response.json();
if (!response.ok || data.error)
throw new Error('Authentication failed');
return {
token: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
};
Store the token securely. It must be included in the WebSocket handshake in Step 3.
Tokens expire after a fixed period. Always check expiresAt before connecting. If the token has expired, re-authenticate before opening the WebSocket.
Step 2: Connect
Open a WebSocket connection to the Molecule endpoint:
const ws = new WebSocket('wss://wsau.moleculesystems.com/ws');
ws.binaryType = 'arraybuffer'; // required- all frames are binary
You must set binaryType = 'arraybuffer' before the connection opens, otherwise binary frames will not be parsed correctly.
Step 3: Send the Handshake
Immediately after the connection opens, send a binary handshake packet. This identifies which gateway you want to connect to and proves your identity via the JWT.
Packet format
[0x01][0x02][idLen Hi][idLen Lo][gatewayId bytes][tokLen Hi][tokLen Lo][token bytes][0x01][0x04]
| Field | Size | Description |
|---|---|---|
0x01 0x02 | 2 bytes | Client connection marker |
idLen | 2 bytes | Gateway ID length (big-endian) |
gatewayId | variable | UTF-8 encoded gateway ID |
tokLen | 2 bytes | Token length (big-endian) |
token | variable | UTF-8 encoded JWT |
0x01 0x04 | 2 bytes | End-of-message sentinel |
Implementation
const buildHandshake = (gatewayId, token) => {
const idBytes = new TextEncoder().encode(gatewayId);
const tokBytes = new TextEncoder().encode(token);
// total = 2 (marker) + 2 (idLen) + id + 2 (tokLen) + token + 2 (sentinel)
const buf = new Uint8Array(2 + 2 + idBytes.length + 2 + tokBytes.length + 2);
let i = 0;
buf[i++] = 0x01;
buf[i++] = 0x02;
buf[i++] = (idBytes.length >> 8) & 0xFF;
buf[i++] = idBytes.length & 0xFF;
buf.set(idBytes, i); i += idBytes.length;
buf[i++] = (tokBytes.length >> 8) & 0xFF;
buf[i++] = tokBytes.length & 0xFF;
buf.set(tokBytes, i); i += tokBytes.length;
buf[i++] = 0x01;
buf[i++] = 0x04;
return buf;
};
ws.onopen = () => {
const handshake = buildHandshake(gatewayId, token);
ws.send(handshake);
};
Step 4: Handle the Auth Response
The server responds with a 3-byte binary frame to confirm the handshake result:
[requestType][status][0x04]
| Byte | Value | Meaning |
|---|---|---|
requestType | 2 | Remote access response |
status | 1 | Authenticated successfully |
status | 0 | Authentication failed |
0x04 | 0x04 | End of frame |
ws.onmessage = (event) => {
const data = new Uint8Array(event.data);
// Auth response- 3 bytes ending in 0x04
if (data.length === 3 && data[2] === 0x04) {
const requestType = data[0];
const success = data[1];
if (requestType === 2 && success === 1) {
console.log('Connected and authenticated');
// ready to receive telemetry
} else {
console.error('Auth failed- check gateway ID and token');
ws.close();
}
return;
}
// Telemetry frames arrive here
handleTelemetry(data);
};
Step 5: Receive Telemetry
After a successful auth response, the server forwards data frames from the gateway. All frames share a common structure:
Frame format
[messageType][reserved][payload bytes...]
| Byte | Description |
|---|---|
byte 0 | Message type |
byte 1 | Reserved |
byte 2+ | UTF-8 payload (may have a trailing 0x00 null terminator) |
Message types
| Type | Description |
|---|---|
100 | Real-time telemetry JSON- update your live data display |
1 | Command response- reply to a command you sent |
2 | Device log- plain text log line from the gateway |
| other | Unknown- inspect raw bytes |
Decoding frames
ws.onmessage = (event) => {
const bytes = new Uint8Array(event.data);
// Auth response- handle before anything else
if (!connected) {
const success = bytes[1] === 1;
if (success) {
connected = true;
console.log('Authenticated');
} else {
console.error('Auth failed');
ws.close();
}
return;
}
if (bytes.length < 1) return;
const messageType = bytes[0];
// Decode payload- strip 2-byte header, strip trailing null if present
let text;
try {
const raw = bytes.slice(2);
const payload = raw[raw.length - 1] === 0x00 ? raw.slice(0, raw.length - 1) : raw;
text = new TextDecoder('utf-8', { fatal: true }).decode(payload);
} catch {
console.warn('Decode error');
return;
}
if (messageType === 100) {
// Real-time telemetry
handleTelemetry(text);
} else if (messageType === 1) {
// Command response
console.log('Response:', text);
} else if (messageType === 2) {
// Device log line
console.log('Device log:', text);
} else {
console.log('Unknown type', messageType, bytes);
}
};
Real-time telemetry payload (type 100)
The payload is a JSON array of key-value items:
[
{ "key": "SOC", "lbl": "State of Charge", "val": "74.5" },
{ "key": "GRID", "lbl": "Grid Power", "val": "3200" }
]
| Field | Description |
|---|---|
key | Unique identifier for this data point |
lbl | Human-readable label- only sent periodically to reduce bandwidth |
val | Current value as a string |
const labels = {}; // cache key → label
const handleTelemetry = (text) => {
try {
const items = JSON.parse(text);
items.forEach(item => {
if (item.lbl) labels[item.key] = item.lbl; // cache when present
const label = labels[item.key] ?? item.key;
console.log(`${label}: ${item.val}`);
});
} catch (err) {
console.warn('Telemetry parse error', err);
}
};
lbl is only included periodically- not on every frame- to reduce bandwidth. Always cache it the first time you see it for a given key and reuse it when subsequent frames omit it.
Complete Example
class MoleculeWS {
constructor(gatewayId, token) {
this.gatewayId = gatewayId;
this.token = token;
this.ws = null;
this.labels = {}; // cache for key → label
}
connect() {
this.ws = new WebSocket('wss://wsau.moleculesystems.com/ws');
this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => this._sendHandshake();
this.ws.onmessage = (e) => this._onMessage(e);
this.ws.onerror = (e) => console.error('WS error', e);
this.ws.onclose = (e) => console.log('WS closed', e.code, e.reason);
}
disconnect() {
this.ws?.close();
}
_sendHandshake() {
const idBytes = new TextEncoder().encode(this.gatewayId);
const tokBytes = new TextEncoder().encode(this.token);
const buf = new Uint8Array(2 + 2 + idBytes.length + 2 + tokBytes.length + 2);
let i = 0;
buf[i++] = 0x01; buf[i++] = 0x02;
buf[i++] = (idBytes.length >> 8) & 0xFF;
buf[i++] = idBytes.length & 0xFF;
buf.set(idBytes, i); i += idBytes.length;
buf[i++] = (tokBytes.length >> 8) & 0xFF;
buf[i++] = tokBytes.length & 0xFF;
buf.set(tokBytes, i); i += tokBytes.length;
buf[i++] = 0x01; buf[i++] = 0x04;
this.ws.send(buf);
}
_onMessage(event) {
const data = new Uint8Array(event.data);
// Auth response- first message after handshake
if (!this.connected) {
if (data[1] === 1) {
this.connected = true;
console.log('Authenticated');
} else {
console.error('Auth failed');
this.ws.close();
}
return;
}
if (data.length < 1) return;
const messageType = data[0];
// Decode payload- strip 2-byte header and trailing null
let text;
try {
const raw = data.slice(2);
const payload = raw[raw.length - 1] === 0x00 ? raw.slice(0, raw.length - 1) : raw;
text = new TextDecoder('utf-8', { fatal: true }).decode(payload);
} catch {
return;
}
if (messageType === 100) this._handleTelemetry(text);
else if (messageType === 1) console.log('Response:', text);
else if (messageType === 2) console.log('Device log:', text);
else console.log('Unknown type', messageType);
}
_handleTelemetry(text) {
try {
const items = JSON.parse(text);
items.forEach(item => {
if (item.lbl) this.labels[item.key] = item.lbl;
const label = this.labels[item.key] ?? item.key;
console.log(`${label}: ${item.val}`);
});
} catch (err) {
console.warn('Telemetry parse error', err);
}
}
}
// Usage
const auth = await authenticate('user@example.com', 'password');
const client = new MoleculeWS('YOUR_GATEWAY_ID', auth.token);
client.connect();
Error Handling
| Scenario | Recommended action |
|---|---|
Auth response success = 0 | Re-authenticate and retry |
ws.onerror fires | Log the error, attempt reconnect after delay |
ws.onclose with code 1008 | Token rejected- re-authenticate |
ws.onclose with code 1009 | Handshake too large- contact support |
| Token expired before connect | Re-authenticate before calling connect() |
Reconnection with backoff
const connectWithRetry = async (gatewayId, token, attempt = 0) => {
const delay = Math.min(1000 * 2 ** attempt, 30000); // cap at 30s
await new Promise(r => setTimeout(r, delay));
const client = new MoleculeWS(gatewayId, token);
client.ws.onclose = async (e) => {
if (e.code !== 1000) { // 1000 = normal close
console.log(`Reconnecting in ${delay}ms...`);
await connectWithRetry(gatewayId, token, attempt + 1);
}
};
client.connect();
};
React Hook
If you are building a React application, here is a ready-to-use hook:
import { useEffect, useRef, useCallback } from 'react';
interface TelemetryItem {
key: string;
lbl?: string;
val: string;
}
export const useMoleculeWS = (
gatewayId: string,
token: string,
onTelemetry: (items: TelemetryItem[]) => void,
) => {
const wsRef = useRef<WebSocket | null>(null);
const labelsRef = useRef<Record<string, string>>({});
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) return;
const ws = new WebSocket('wss://wsau.moleculesystems.com/ws');
ws.binaryType = 'arraybuffer';
wsRef.current = ws;
ws.onopen = () => {
const idBytes = new TextEncoder().encode(gatewayId);
const tokBytes = new TextEncoder().encode(token);
const buf = new Uint8Array(2 + 2 + idBytes.length + 2 + tokBytes.length + 2);
let i = 0;
buf[i++] = 0x01; buf[i++] = 0x02;
buf[i++] = (idBytes.length >> 8) & 0xFF;
buf[i++] = idBytes.length & 0xFF;
buf.set(idBytes, i); i += idBytes.length;
buf[i++] = (tokBytes.length >> 8) & 0xFF;
buf[i++] = tokBytes.length & 0xFF;
buf.set(tokBytes, i); i += tokBytes.length;
buf[i++] = 0x01; buf[i++] = 0x04;
ws.send(buf);
};
ws.onmessage = (event) => {
const data = new Uint8Array(event.data);
if (data.length === 3 && data[2] === 0x04) return; // auth response
try {
const payload = data[data.length - 1] === 0x00
? data.slice(0, data.length - 1) : data;
const items: TelemetryItem[] = JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(payload)
);
items.forEach(item => {
if (item.lbl) labelsRef.current[item.key] = item.lbl;
});
onTelemetry(items);
} catch { /* ignore malformed frames */ }
};
}, [gatewayId, token, onTelemetry]);
const disconnect = useCallback(() => {
wsRef.current?.close();
wsRef.current = null;
}, []);
useEffect(() => () => { wsRef.current?.close(); }, []);
return { connect, disconnect };
};
Security Notes
- The JWT is transmitted inside the encrypted WebSocket (
wss://) connection and never appears in URLs or HTTP headers - Tokens have a limited lifetime- do not cache them beyond their
expires_invalue - Never log or store the raw JWT in a place accessible to other users
- If a token is compromised, contact Molecule support to revoke it immediately