484 lines
13 KiB
JavaScript
484 lines
13 KiB
JavaScript
// LEDLab Server - Main server file
|
|
|
|
const express = require('express');
|
|
const http = require('http');
|
|
const WebSocket = require('ws');
|
|
const path = require('path');
|
|
|
|
// Import services
|
|
const UdpDiscovery = require('./udp-discovery');
|
|
const PresetRegistry = require('../presets/preset-registry');
|
|
|
|
class LEDLabServer {
|
|
constructor(options = {}) {
|
|
this.port = options.port || 8080;
|
|
this.udpPort = options.udpPort || 4210;
|
|
this.matrixWidth = options.matrixWidth || 16;
|
|
this.matrixHeight = options.matrixHeight || 16;
|
|
|
|
this.app = express();
|
|
this.server = http.createServer(this.app);
|
|
this.wss = new WebSocket.Server({ server: this.server });
|
|
|
|
this.udpDiscovery = new UdpDiscovery(this.udpPort);
|
|
this.presetRegistry = new PresetRegistry();
|
|
|
|
this.currentPreset = null;
|
|
this.currentPresetName = null;
|
|
this.streamingInterval = null;
|
|
this.connectedClients = new Set();
|
|
|
|
// Per-node configurations and current target
|
|
this.nodeConfigurations = new Map(); // ip -> {presetName, parameters, matrixSize}
|
|
this.currentTarget = null; // 'broadcast' or specific IP
|
|
|
|
this.setupExpress();
|
|
this.setupWebSocket();
|
|
this.setupUdpDiscovery();
|
|
this.setupPresetManager();
|
|
}
|
|
|
|
setupExpress() {
|
|
// Serve static files
|
|
this.app.use(express.static(path.join(__dirname, '../public')));
|
|
|
|
// API routes
|
|
this.app.get('/api/nodes', (req, res) => {
|
|
const nodes = this.udpDiscovery.getNodes();
|
|
res.json({ nodes });
|
|
});
|
|
|
|
this.app.get('/api/presets', (req, res) => {
|
|
const presets = this.presetRegistry.getAllPresetMetadata();
|
|
res.json({ presets });
|
|
});
|
|
|
|
this.app.get('/api/status', (req, res) => {
|
|
res.json({
|
|
streaming: this.currentPreset !== null,
|
|
currentPreset: this.currentPreset ? this.currentPreset.getMetadata().name : null,
|
|
matrixSize: { width: this.matrixWidth, height: this.matrixHeight },
|
|
nodeCount: this.udpDiscovery.getNodeCount(),
|
|
currentTarget: this.currentTarget,
|
|
});
|
|
});
|
|
|
|
// Matrix configuration endpoint
|
|
this.app.post('/api/matrix/config', express.json(), (req, res) => {
|
|
const { width, height } = req.body;
|
|
|
|
if (width && height && width > 0 && height > 0) {
|
|
this.matrixWidth = width;
|
|
this.matrixHeight = height;
|
|
|
|
// Restart current preset with new dimensions if one is active
|
|
if (this.currentPreset) {
|
|
this.stopStreaming();
|
|
this.startPreset(this.currentPreset.constructor.name.toLowerCase().replace('-preset', ''), width, height);
|
|
}
|
|
|
|
this.broadcastToClients({
|
|
type: 'matrixConfig',
|
|
config: { width, height }
|
|
});
|
|
|
|
// Save updated configuration for current target
|
|
if (this.currentTarget && this.currentTarget !== 'broadcast') {
|
|
this.saveCurrentConfiguration(this.currentTarget);
|
|
}
|
|
|
|
res.json({ success: true, config: { width, height } });
|
|
} else {
|
|
res.status(400).json({ error: 'Invalid matrix dimensions' });
|
|
}
|
|
});
|
|
}
|
|
|
|
setupWebSocket() {
|
|
this.wss.on('connection', (ws) => {
|
|
console.log('WebSocket client connected');
|
|
this.connectedClients.add(ws);
|
|
|
|
ws.on('message', (message) => {
|
|
try {
|
|
const data = JSON.parse(message.toString());
|
|
this.handleWebSocketMessage(ws, data);
|
|
} catch (error) {
|
|
console.error('Error parsing WebSocket message:', error);
|
|
}
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
console.log('WebSocket client disconnected');
|
|
this.connectedClients.delete(ws);
|
|
});
|
|
|
|
// Send current status to new client
|
|
const currentState = {
|
|
streaming: this.currentPreset !== null,
|
|
currentPreset: this.currentPreset ? this.currentPreset.getMetadata().name : null,
|
|
matrixSize: { width: this.matrixWidth, height: this.matrixHeight },
|
|
nodes: this.udpDiscovery.getNodes(),
|
|
presetParameters: this.currentPreset ? this.currentPreset.getParameters() : null,
|
|
currentTarget: this.currentTarget,
|
|
};
|
|
|
|
this.sendToClient(ws, {
|
|
type: 'status',
|
|
data: currentState
|
|
});
|
|
});
|
|
}
|
|
|
|
handleWebSocketMessage(ws, data) {
|
|
switch (data.type) {
|
|
case 'startPreset':
|
|
this.startPreset(data.presetName, data.width, data.height);
|
|
break;
|
|
|
|
case 'stopStreaming':
|
|
this.stopStreaming();
|
|
break;
|
|
|
|
case 'updatePresetParameter':
|
|
this.updatePresetParameter(data.parameter, data.value);
|
|
break;
|
|
|
|
case 'setMatrixSize':
|
|
this.setMatrixSize(data.width, data.height);
|
|
break;
|
|
|
|
case 'selectNode':
|
|
this.selectNode(data.nodeIp);
|
|
break;
|
|
|
|
case 'selectBroadcast':
|
|
this.selectBroadcast();
|
|
break;
|
|
|
|
case 'sendToNode':
|
|
this.sendToSpecificNode(data.nodeIp, data.message);
|
|
break;
|
|
|
|
case 'broadcastToAll':
|
|
this.broadcastToAllNodes(data.message);
|
|
break;
|
|
|
|
default:
|
|
console.warn('Unknown WebSocket message type:', data.type);
|
|
}
|
|
}
|
|
|
|
setupUdpDiscovery() {
|
|
this.udpDiscovery.on('nodeDiscovered', (node) => {
|
|
console.log('Node discovered:', node.ip);
|
|
|
|
this.broadcastToClients({
|
|
type: 'nodeDiscovered',
|
|
node
|
|
});
|
|
});
|
|
|
|
this.udpDiscovery.on('nodeLost', (node) => {
|
|
console.log('Node lost:', node.ip);
|
|
|
|
this.broadcastToClients({
|
|
type: 'nodeLost',
|
|
node
|
|
});
|
|
});
|
|
|
|
this.udpDiscovery.start();
|
|
}
|
|
|
|
setupPresetManager() {
|
|
// Start with no active preset
|
|
this.currentPreset = null;
|
|
}
|
|
|
|
startPreset(presetName, width = this.matrixWidth, height = this.matrixHeight) {
|
|
try {
|
|
// Stop current streaming if active
|
|
if (this.currentPreset) {
|
|
this.stopStreaming();
|
|
}
|
|
|
|
// Create new preset instance
|
|
this.currentPreset = this.presetRegistry.createPreset(presetName, width, height);
|
|
this.currentPresetName = presetName; // Store the registry key
|
|
this.currentPreset.start();
|
|
|
|
console.log(`Started preset: ${presetName} (${width}x${height})`);
|
|
|
|
// Start streaming interval
|
|
this.streamingInterval = setInterval(() => {
|
|
this.streamFrame();
|
|
}, 50); // 20 FPS
|
|
|
|
// Notify clients
|
|
this.broadcastToClients({
|
|
type: 'streamingStarted',
|
|
preset: this.currentPreset.getMetadata()
|
|
});
|
|
|
|
// Also send updated state to keep all clients in sync
|
|
this.broadcastCurrentState();
|
|
|
|
} catch (error) {
|
|
console.error('Error starting preset:', error);
|
|
this.sendToClient(ws, {
|
|
type: 'error',
|
|
message: `Failed to start preset: ${error.message}`
|
|
});
|
|
}
|
|
}
|
|
|
|
stopStreaming() {
|
|
if (this.streamingInterval) {
|
|
clearInterval(this.streamingInterval);
|
|
this.streamingInterval = null;
|
|
}
|
|
|
|
if (this.currentPreset) {
|
|
this.currentPreset.stop();
|
|
this.currentPreset = null;
|
|
this.currentPresetName = null;
|
|
}
|
|
|
|
console.log('Streaming stopped');
|
|
|
|
this.broadcastToClients({
|
|
type: 'streamingStopped'
|
|
});
|
|
|
|
// Save current configuration for the current target if it exists
|
|
if (this.currentTarget && this.currentTarget !== 'broadcast') {
|
|
this.saveCurrentConfiguration(this.currentTarget);
|
|
}
|
|
|
|
// Also send updated state to keep all clients in sync
|
|
this.broadcastCurrentState();
|
|
}
|
|
|
|
updatePresetParameter(parameter, value) {
|
|
if (this.currentPreset) {
|
|
this.currentPreset.setParameter(parameter, value);
|
|
|
|
this.broadcastToClients({
|
|
type: 'presetParameterUpdated',
|
|
parameter,
|
|
value
|
|
});
|
|
|
|
// Save updated configuration for current target
|
|
if (this.currentTarget && this.currentTarget !== 'broadcast') {
|
|
this.saveCurrentConfiguration(this.currentTarget);
|
|
}
|
|
|
|
// Also send updated state to keep all clients in sync
|
|
this.broadcastCurrentState();
|
|
}
|
|
}
|
|
|
|
setMatrixSize(width, height) {
|
|
this.matrixWidth = width;
|
|
this.matrixHeight = height;
|
|
|
|
if (this.currentPreset) {
|
|
this.stopStreaming();
|
|
this.startPreset(this.currentPreset.constructor.name.toLowerCase().replace('-preset', ''), width, height);
|
|
}
|
|
|
|
this.broadcastToClients({
|
|
type: 'matrixSizeChanged',
|
|
size: { width, height }
|
|
});
|
|
|
|
// Save updated configuration for current target
|
|
if (this.currentTarget && this.currentTarget !== 'broadcast') {
|
|
this.saveCurrentConfiguration(this.currentTarget);
|
|
}
|
|
}
|
|
|
|
|
|
streamFrame() {
|
|
if (!this.currentPreset) {
|
|
return;
|
|
}
|
|
|
|
const frameData = this.currentPreset.generateFrame();
|
|
if (frameData) {
|
|
// Send to specific target or broadcast
|
|
if (this.currentTarget === 'broadcast') {
|
|
this.udpDiscovery.broadcastToAll(frameData);
|
|
} else if (this.currentTarget) {
|
|
this.udpDiscovery.sendToNode(this.currentTarget, frameData);
|
|
}
|
|
|
|
// Send frame data to WebSocket clients for preview
|
|
this.broadcastToClients({
|
|
type: 'frame',
|
|
data: frameData,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
sendToSpecificNode(nodeIp, message) {
|
|
return this.udpDiscovery.sendToNode(nodeIp, message);
|
|
}
|
|
|
|
broadcastToAllNodes(message) {
|
|
return this.udpDiscovery.broadcastToAll(message);
|
|
}
|
|
|
|
broadcastCurrentState() {
|
|
const currentState = {
|
|
streaming: this.currentPreset !== null,
|
|
currentPreset: this.currentPreset ? this.currentPreset.getMetadata().name : null,
|
|
matrixSize: { width: this.matrixWidth, height: this.matrixHeight },
|
|
nodes: this.udpDiscovery.getNodes(),
|
|
presetParameters: this.currentPreset ? this.currentPreset.getParameters() : null,
|
|
currentTarget: this.currentTarget,
|
|
};
|
|
|
|
this.broadcastToClients({
|
|
type: 'status',
|
|
data: currentState
|
|
});
|
|
}
|
|
|
|
// Node selection and configuration management
|
|
selectNode(nodeIp) {
|
|
this.currentTarget = nodeIp;
|
|
|
|
// Load configuration for this node if it exists, otherwise use current settings
|
|
const nodeConfig = this.nodeConfigurations.get(nodeIp);
|
|
if (nodeConfig) {
|
|
this.loadNodeConfiguration(nodeConfig);
|
|
} else {
|
|
// Save current configuration for this node
|
|
this.saveCurrentConfiguration(nodeIp);
|
|
}
|
|
|
|
this.broadcastCurrentState();
|
|
}
|
|
|
|
selectBroadcast() {
|
|
this.currentTarget = 'broadcast';
|
|
this.broadcastCurrentState();
|
|
}
|
|
|
|
saveCurrentConfiguration(nodeIp) {
|
|
if (this.currentPreset && this.currentPresetName) {
|
|
this.nodeConfigurations.set(nodeIp, {
|
|
presetName: this.currentPresetName, // Use registry key, not display name
|
|
parameters: this.currentPreset.getParameters(),
|
|
matrixSize: { width: this.matrixWidth, height: this.matrixHeight }
|
|
});
|
|
}
|
|
}
|
|
|
|
loadNodeConfiguration(config) {
|
|
// Stop current streaming
|
|
this.stopStreaming();
|
|
|
|
// Load the node's configuration
|
|
this.matrixWidth = config.matrixSize.width;
|
|
this.matrixHeight = config.matrixSize.height;
|
|
|
|
// Start the preset with saved parameters
|
|
this.startPreset(config.presetName, config.matrixSize.width, config.matrixSize.height);
|
|
|
|
// Set the parameters after preset is created
|
|
if (this.currentPreset) {
|
|
Object.entries(config.parameters).forEach(([param, value]) => {
|
|
this.currentPreset.setParameter(param, value);
|
|
});
|
|
}
|
|
}
|
|
|
|
broadcastToClients(message) {
|
|
const messageStr = JSON.stringify(message);
|
|
this.connectedClients.forEach(ws => {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(messageStr);
|
|
}
|
|
});
|
|
}
|
|
|
|
sendToClient(ws, message) {
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
startServer() {
|
|
this.server.listen(this.port, () => {
|
|
console.log(`LEDLab server running on port ${this.port}`);
|
|
console.log(`UDP discovery on port ${this.udpPort}`);
|
|
console.log(`Matrix size: ${this.matrixWidth}x${this.matrixHeight}`);
|
|
});
|
|
}
|
|
|
|
stopServer() {
|
|
console.log('Stopping LEDLab server...');
|
|
|
|
// Stop streaming first
|
|
this.stopStreaming();
|
|
|
|
// Stop UDP discovery
|
|
this.udpDiscovery.stop();
|
|
|
|
// Close all WebSocket connections immediately
|
|
this.wss.close();
|
|
|
|
// Close the server
|
|
this.server.close((err) => {
|
|
if (err) {
|
|
console.error('Error closing server:', err);
|
|
}
|
|
console.log('LEDLab server stopped');
|
|
process.exit(0);
|
|
});
|
|
|
|
// Force exit after 2 seconds if server doesn't close cleanly
|
|
setTimeout(() => {
|
|
console.log('Forcing server shutdown...');
|
|
process.exit(1);
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
// Global flag to ensure shutdown handlers are only registered once
|
|
let shutdownHandlersRegistered = false;
|
|
|
|
// Start server if this file is run directly
|
|
if (require.main === module) {
|
|
const server = new LEDLabServer({
|
|
port: process.env.PORT || 8080,
|
|
udpPort: process.env.UDP_PORT || 4210,
|
|
matrixWidth: parseInt(process.env.MATRIX_WIDTH) || 16,
|
|
matrixHeight: parseInt(process.env.MATRIX_HEIGHT) || 16,
|
|
});
|
|
|
|
// Graceful shutdown (only register once)
|
|
if (!shutdownHandlersRegistered) {
|
|
shutdownHandlersRegistered = true;
|
|
|
|
process.on('SIGINT', () => {
|
|
console.log('\nShutting down LEDLab server...');
|
|
server.stopServer();
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
console.log('\nShutting down LEDLab server...');
|
|
server.stopServer();
|
|
});
|
|
}
|
|
|
|
server.startServer();
|
|
}
|
|
|
|
module.exports = LEDLabServer;
|