feat: ledlab
This commit is contained in:
483
server/index.js
Normal file
483
server/index.js
Normal file
@@ -0,0 +1,483 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user