/* * Express 5 web server — REST bridge to video-node. * * Usage: * node server.mjs [--host IP] [--port PORT] [--discover] [--listen PORT] * * If --host and --port are given, connects on startup. * If --discover is given, runs UDP discovery to find the node automatically. * Otherwise, call POST /api/connect to connect at runtime. */ import express from 'express'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import { Node_Client } from './node_client.mjs'; import { start_discovery } from './discovery.mjs'; /* ------------------------------------------------------------------------- * Argument parsing * ------------------------------------------------------------------------- */ const args = process.argv.slice(2); const arg = name => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : null; }; const opt_host = arg('--host'); const opt_port = arg('--port') ? parseInt(arg('--port')) : null; const opt_discover = args.includes('--discover'); const listen_port = arg('--listen') ? parseInt(arg('--listen')) : 3000; /* ------------------------------------------------------------------------- * Node client (singleton) * ------------------------------------------------------------------------- */ const client = new Node_Client(); client.on('disconnect', () => { console.log('video-node disconnected'); }); /* ------------------------------------------------------------------------- * Express app * ------------------------------------------------------------------------- */ const app = express(); app.use(express.json()); const __dir = path.dirname(fileURLToPath(import.meta.url)); app.use(express.static(path.join(__dir, 'public'))); /* -- Status ---------------------------------------------------------------- */ app.get('/api/status', (_req, res) => { res.json({ connected: client.connected, host: client.host, port: client.port, }); }); /* -- Connect --------------------------------------------------------------- */ app.post('/api/connect', async (req, res) => { const { host, port } = req.body ?? {}; if (!host || !port) { return res.status(400).json({ error: 'host and port required' }); } try { await client.connect(host, parseInt(port)); res.json({ ok: true, host: client.host, port: client.port }); } catch (err) { res.status(502).json({ error: err.message }); } }); /* * SSE stream: each discovered peer arrives as a JSON event. * The client closes the connection when done (user picked a node or dismissed). */ app.get('/api/discover', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); const stop = start_discovery(peer => { res.write(`data: ${JSON.stringify(peer)}\n\n`); }); req.on('close', stop); }); app.post('/api/disconnect', (_req, res) => { client.disconnect(); res.json({ ok: true }); }); /* -- Devices --------------------------------------------------------------- */ app.get('/api/devices', async (_req, res) => { try { const result = await client.enum_devices(); res.json(result); } catch (err) { res.status(502).json({ error: err.message }); } }); /* -- Controls -------------------------------------------------------------- */ app.get('/api/devices/:idx/controls', async (req, res) => { const idx = parseInt(req.params.idx); if (isNaN(idx)) { return res.status(400).json({ error: 'invalid device index' }); } try { const result = await client.enum_controls(idx); res.json(result); } catch (err) { res.status(502).json({ error: err.message }); } }); app.get('/api/devices/:idx/controls/:ctrl_id', async (req, res) => { const idx = parseInt(req.params.idx); const ctrl_id = parseInt(req.params.ctrl_id, 16) || parseInt(req.params.ctrl_id); if (isNaN(idx) || isNaN(ctrl_id)) { return res.status(400).json({ error: 'invalid params' }); } try { const result = await client.get_control(idx, ctrl_id); res.json(result); } catch (err) { res.status(502).json({ error: err.message }); } }); app.post('/api/devices/:idx/controls/:ctrl_id', async (req, res) => { const idx = parseInt(req.params.idx); const ctrl_id = parseInt(req.params.ctrl_id, 16) || parseInt(req.params.ctrl_id); const value = req.body?.value; if (isNaN(idx) || isNaN(ctrl_id) || value === undefined) { return res.status(400).json({ error: 'invalid params' }); } try { const result = await client.set_control(idx, ctrl_id, parseInt(value)); res.json(result); } catch (err) { res.status(502).json({ error: err.message }); } }); /* ------------------------------------------------------------------------- * Startup * ------------------------------------------------------------------------- */ app.listen(listen_port, () => { console.log(`listening on http://localhost:${listen_port}`); }); if (opt_discover) { console.log('discovering video-nodes...'); const stop = start_discovery(peer => { console.log(`found ${peer.name} at ${peer.addr}:${peer.tcp_port} — connecting`); stop(); client.connect(peer.addr, peer.tcp_port) .then(() => console.log('connected')) .catch(err => console.error('connect failed:', err.message)); }); } else if (opt_host && opt_port) { client.connect(opt_host, opt_port) .then(() => console.log(`connected to ${opt_host}:${opt_port}`)) .catch(err => console.error('connect failed:', err.message)); }