139 lines
4.4 KiB
JavaScript
139 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* DreamStack Relay Bridge — UDP ↔ WebSocket
|
|
*
|
|
* Bridges the hub's UDP binary frames to the browser previewer
|
|
* via WebSocket. This lets you test the full signal pipeline
|
|
* without ESP32 hardware.
|
|
*
|
|
* Architecture:
|
|
* ds-hub (Rust) → UDP:9200 → [this relay] → WS:9201 → browser previewer
|
|
* browser → WS:9201 → [this relay] → UDP:9200 → ds-hub
|
|
*
|
|
* Usage:
|
|
* node relay-bridge.js
|
|
* Then open previewer with: ?ws=ws://localhost:9201
|
|
*/
|
|
|
|
const dgram = require('dgram');
|
|
const { WebSocketServer } = require('ws');
|
|
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const UDP_PORT = 9200;
|
|
const WS_PORT = 9201;
|
|
const HTTP_PORT = 9876; // Serve previewer HTML too
|
|
|
|
// ─── WebSocket Server ───
|
|
const wss = new WebSocketServer({ host: '0.0.0.0', port: WS_PORT });
|
|
const clients = new Set();
|
|
|
|
wss.on('connection', (ws, req) => {
|
|
clients.add(ws);
|
|
console.log(`[WS] Client connected (${clients.size} total)`);
|
|
|
|
ws.on('message', (data) => {
|
|
// Forward binary messages from browser → UDP (hub)
|
|
if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) {
|
|
const buf = Buffer.from(data);
|
|
udp.send(buf, 0, buf.length, UDP_PORT, '127.0.0.1', (err) => {
|
|
if (err) console.error('[UDP] Send error:', err);
|
|
});
|
|
}
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
clients.delete(ws);
|
|
console.log(`[WS] Client disconnected (${clients.size} remaining)`);
|
|
});
|
|
|
|
// If we have a cached IR, send it immediately
|
|
if (cachedIR) {
|
|
ws.send(cachedIR);
|
|
console.log('[WS] Sent cached IR to new client');
|
|
}
|
|
});
|
|
|
|
// ─── UDP Receiver ───
|
|
const udp = dgram.createSocket('udp4');
|
|
let cachedIR = null; // Cache latest IR push for new WS clients
|
|
let hubAddr = null; // Remember hub address for replies
|
|
|
|
udp.on('message', (msg, rinfo) => {
|
|
hubAddr = rinfo;
|
|
|
|
// Check if this is an IR push (has magic bytes + IR type)
|
|
if (msg.length >= 6 && msg[0] === 0xD5 && msg[1] === 0x7A && msg[2] === 0x40) {
|
|
// Extract and cache the IR JSON
|
|
const len = msg.readUInt16LE(4);
|
|
const json = msg.slice(6, 6 + len).toString();
|
|
cachedIR = json;
|
|
console.log(`[UDP] IR push received (${len} bytes), broadcasting to ${clients.size} WS clients`);
|
|
|
|
// Send as JSON text to all WS clients
|
|
for (const ws of clients) {
|
|
if (ws.readyState === 1) ws.send(json);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Forward all other binary frames to WS clients
|
|
for (const ws of clients) {
|
|
if (ws.readyState === 1) ws.send(msg);
|
|
}
|
|
|
|
// Log signal updates
|
|
if (msg[0] === 0x20 && msg.length >= 7) {
|
|
const sigId = msg.readUInt16LE(1);
|
|
const value = msg.readInt32LE(3);
|
|
// Only log occasionally to avoid spam
|
|
if (sigId === 0 || value % 10 === 0) {
|
|
console.log(`[UDP] Signal ${sigId} = ${value}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
udp.on('error', (err) => {
|
|
console.error('[UDP] Error:', err);
|
|
});
|
|
|
|
udp.bind(UDP_PORT, () => {
|
|
console.log(`[UDP] Listening on port ${UDP_PORT}`);
|
|
});
|
|
|
|
// ─── HTTP Server (serve previewer) ───
|
|
const server = http.createServer((req, res) => {
|
|
let filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url.split('?')[0]);
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
res.writeHead(404);
|
|
res.end('Not found');
|
|
return;
|
|
}
|
|
|
|
const ext = path.extname(filePath);
|
|
const contentType = {
|
|
'.html': 'text/html',
|
|
'.js': 'application/javascript',
|
|
'.css': 'text/css',
|
|
'.json': 'application/json',
|
|
'.png': 'image/png',
|
|
}[ext] || 'application/octet-stream';
|
|
|
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
fs.createReadStream(filePath).pipe(res);
|
|
});
|
|
|
|
server.listen(HTTP_PORT, '0.0.0.0', () => {
|
|
console.log(`\n DreamStack Relay Bridge`);
|
|
console.log(` ─────────────────────────`);
|
|
console.log(` HTTP: http://localhost:${HTTP_PORT}/`);
|
|
console.log(` WebSocket: ws://localhost:${WS_PORT}`);
|
|
console.log(` UDP: port ${UDP_PORT}`);
|
|
console.log(`\n Open previewer in live mode:`);
|
|
console.log(` http://localhost:${HTTP_PORT}/index.html?ws=ws://localhost:${WS_PORT}`);
|
|
console.log(`\n Or file mode (no hub needed):`);
|
|
console.log(` http://localhost:${HTTP_PORT}/index.html`);
|
|
console.log('');
|
|
});
|