feat: node canvas grid

This commit is contained in:
2025-10-11 21:18:21 +02:00
parent 294d86f24b
commit 32f35c70cf
11 changed files with 1444 additions and 421 deletions

View File

@@ -24,14 +24,16 @@ class LEDLabServer {
this.udpDiscovery = new UdpDiscovery(this.udpPort);
this.presetRegistry = new PresetRegistry();
// Legacy single-stream support (kept for backwards compatibility)
this.currentPreset = null;
this.currentPresetName = null;
this.streamingInterval = null;
this.connectedClients = new Set();
// Per-node configurations and current target
// Multi-node streaming support
this.nodeStreams = new Map(); // ip -> {preset, presetName, interval, matrixSize, parameters}
this.nodeConfigurations = new Map(); // ip -> {presetName, parameters, matrixSize}
this.currentTarget = null; // 'broadcast' or specific IP
this.currentTarget = null; // Currently selected node IP
this.setupExpress();
this.setupWebSocket();
@@ -85,7 +87,7 @@ class LEDLabServer {
});
// Save updated configuration for current target
if (this.currentTarget && this.currentTarget !== 'broadcast') {
if (this.currentTarget) {
this.saveCurrentConfiguration(this.currentTarget);
}
@@ -136,15 +138,15 @@ class LEDLabServer {
handleWebSocketMessage(ws, data) {
switch (data.type) {
case 'startPreset':
this.startPreset(data.presetName, data.width, data.height);
this.startPreset(data.presetName, data.width, data.height, data.nodeIp, data.parameters);
break;
case 'stopStreaming':
this.stopStreaming();
this.stopStreaming(data.nodeIp);
break;
case 'updatePresetParameter':
this.updatePresetParameter(data.parameter, data.value);
this.updatePresetParameter(data.parameter, data.value, data.nodeIp);
break;
case 'setMatrixSize':
@@ -155,18 +157,10 @@ class LEDLabServer {
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;
case 'updateFrameRate':
this.updateFrameRate(data.fps);
break;
@@ -199,34 +193,66 @@ class LEDLabServer {
}
setupPresetManager() {
// Start with no active preset
// Start with no active presets
this.currentPreset = null;
this.nodeStreams.clear();
}
startPreset(presetName, width = this.matrixWidth, height = this.matrixHeight) {
startPreset(presetName, width = this.matrixWidth, height = this.matrixHeight, nodeIp = null, parameters = null) {
try {
// Stop current streaming if active
if (this.currentPreset) {
this.stopStreaming();
const targetIp = nodeIp || this.currentTarget;
if (!targetIp) {
console.warn('No target specified for streaming');
return;
}
// Stop current streaming for this node if active
this.stopStreaming(targetIp);
// Create new preset instance
this.currentPreset = this.presetRegistry.createPreset(presetName, width, height);
this.currentPresetName = presetName; // Store the registry key
this.currentPreset.start();
const preset = this.presetRegistry.createPreset(presetName, width, height);
preset.start();
console.log(`Started preset: ${presetName} (${width}x${height})`);
// Apply parameters if provided
if (parameters) {
Object.entries(parameters).forEach(([param, value]) => {
preset.setParameter(param, value);
});
}
// Start streaming interval
console.log(`Started preset: ${presetName} (${width}x${height}) for ${targetIp}`);
// Start streaming interval for this node
const intervalMs = Math.floor(1000 / this.fps);
this.streamingInterval = setInterval(() => {
this.streamFrame();
const interval = setInterval(() => {
this.streamFrameForNode(targetIp);
}, intervalMs);
// Store stream information
this.nodeStreams.set(targetIp, {
preset,
presetName,
interval,
matrixSize: { width, height },
parameters: preset.getParameters()
});
// Update legacy support
if (targetIp === this.currentTarget) {
this.currentPreset = preset;
this.currentPresetName = presetName;
this.streamingInterval = interval;
}
// Save configuration
this.saveCurrentConfiguration(targetIp);
// Notify clients
this.broadcastToClients({
type: 'streamingStarted',
preset: this.currentPreset.getMetadata()
preset: preset.getMetadata(),
nodeIp: targetIp
});
// Also send updated state to keep all clients in sync
@@ -234,58 +260,85 @@ class LEDLabServer {
} catch (error) {
console.error('Error starting preset:', error);
this.sendToClient(ws, {
this.broadcastToClients({
type: 'error',
message: `Failed to start preset: ${error.message}`
});
}
}
stopStreaming() {
if (this.streamingInterval) {
clearInterval(this.streamingInterval);
this.streamingInterval = null;
stopStreaming(nodeIp = null) {
const targetIp = nodeIp || this.currentTarget;
if (targetIp) {
this.stopNodeStream(targetIp);
} else {
// Legacy: stop current streaming
if (this.streamingInterval) {
clearInterval(this.streamingInterval);
this.streamingInterval = null;
}
if (this.currentPreset) {
this.currentPreset.stop();
this.currentPreset = null;
this.currentPresetName = null;
}
}
if (this.currentPreset) {
this.currentPreset.stop();
this.currentPreset = null;
this.currentPresetName = null;
}
console.log('Streaming stopped');
console.log(`Streaming stopped for ${targetIp || 'current target'}`);
this.broadcastToClients({
type: 'streamingStopped'
type: 'streamingStopped',
nodeIp: targetIp
});
// 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);
stopNodeStream(nodeIp) {
const stream = this.nodeStreams.get(nodeIp);
if (stream) {
clearInterval(stream.interval);
stream.preset.stop();
this.nodeStreams.delete(nodeIp);
this.broadcastToClients({
// Update legacy support if this was the current target
if (nodeIp === this.currentTarget) {
this.currentPreset = null;
this.currentPresetName = null;
this.streamingInterval = null;
}
}
}
updatePresetParameter(parameter, value, nodeIp = null) {
const targetIp = nodeIp || this.currentTarget;
if (targetIp) {
const stream = this.nodeStreams.get(targetIp);
if (stream) {
stream.preset.setParameter(parameter, value);
stream.parameters = stream.preset.getParameters();
this.saveCurrentConfiguration(targetIp);
}
}
// Legacy support
if (this.currentPreset && targetIp === this.currentTarget) {
this.currentPreset.setParameter(parameter, value);
}
this.broadcastToClients({
type: 'presetParameterUpdated',
parameter,
value
});
value,
nodeIp: targetIp
});
// Save updated configuration for current target
if (this.currentTarget && this.currentTarget !== 'broadcast') {
this.saveCurrentConfiguration(this.currentTarget);
}
// Don't broadcast full state on every parameter change to avoid UI flickering
// State is already updated via presetParameterUpdated event
}
// Don't broadcast full state on every parameter change to avoid UI flickering
// State is already updated via presetParameterUpdated event
}
setMatrixSize(width, height) {
@@ -303,7 +356,7 @@ class LEDLabServer {
});
// Save updated configuration for current target
if (this.currentTarget && this.currentTarget !== 'broadcast') {
if (this.currentTarget) {
this.saveCurrentConfiguration(this.currentTarget);
}
}
@@ -316,10 +369,8 @@ class LEDLabServer {
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) {
// Send to specific target
if (this.currentTarget) {
this.udpDiscovery.sendToNode(this.currentTarget, frameData);
}
@@ -332,12 +383,29 @@ class LEDLabServer {
}
}
sendToSpecificNode(nodeIp, message) {
return this.udpDiscovery.sendToNode(nodeIp, message);
streamFrameForNode(nodeIp) {
const stream = this.nodeStreams.get(nodeIp);
if (!stream || !stream.preset) {
return;
}
const frameData = stream.preset.generateFrame();
if (frameData) {
// Send to specific node
this.udpDiscovery.sendToNode(nodeIp, frameData);
// Send frame data to WebSocket clients for preview
this.broadcastToClients({
type: 'frame',
data: frameData,
nodeIp: nodeIp,
timestamp: Date.now()
});
}
}
broadcastToAllNodes(message) {
return this.udpDiscovery.broadcastToAll(message);
sendToSpecificNode(nodeIp, message) {
return this.udpDiscovery.sendToNode(nodeIp, message);
}
broadcastCurrentState() {
@@ -366,10 +434,21 @@ class LEDLabServer {
this.fps = fps;
console.log(`Frame rate updated to ${fps} FPS`);
// If streaming is active, restart the interval with new frame rate
const intervalMs = Math.floor(1000 / this.fps);
// Update all active node streams
this.nodeStreams.forEach((stream, nodeIp) => {
if (stream.interval) {
clearInterval(stream.interval);
stream.interval = setInterval(() => {
this.streamFrameForNode(nodeIp);
}, intervalMs);
}
});
// Legacy: If streaming is active, restart the interval with new frame rate
if (this.currentPreset && this.streamingInterval) {
clearInterval(this.streamingInterval);
const intervalMs = Math.floor(1000 / this.fps);
this.streamingInterval = setInterval(() => {
this.streamFrame();
}, intervalMs);
@@ -386,50 +465,54 @@ class LEDLabServer {
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);
// Check if this node already has an active stream
const stream = this.nodeStreams.get(nodeIp);
if (stream) {
// Node is already streaming, update legacy references
this.currentPreset = stream.preset;
this.currentPresetName = stream.presetName;
this.matrixWidth = stream.matrixSize.width;
this.matrixHeight = stream.matrixSize.height;
} else {
// Save current configuration for this node
this.saveCurrentConfiguration(nodeIp);
// Load configuration for this node if it exists
const nodeConfig = this.nodeConfigurations.get(nodeIp);
if (nodeConfig) {
this.matrixWidth = nodeConfig.matrixSize.width;
this.matrixHeight = nodeConfig.matrixSize.height;
// Don't auto-start streaming, just load the configuration
}
}
this.broadcastCurrentState();
}
selectBroadcast() {
this.currentTarget = 'broadcast';
this.broadcastCurrentState();
}
saveCurrentConfiguration(nodeIp) {
if (this.currentPreset && this.currentPresetName) {
const stream = this.nodeStreams.get(nodeIp);
if (stream && stream.preset && stream.presetName) {
this.nodeConfigurations.set(nodeIp, {
presetName: this.currentPresetName, // Use registry key, not display name
presetName: stream.presetName,
parameters: stream.preset.getParameters(),
matrixSize: stream.matrixSize
});
} else if (this.currentPreset && this.currentPresetName) {
// Legacy fallback
this.nodeConfigurations.set(nodeIp, {
presetName: this.currentPresetName,
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);
});
}
loadNodeConfiguration(config, nodeIp) {
// Start the preset with saved parameters for this specific node
this.startPreset(
config.presetName,
config.matrixSize.width,
config.matrixSize.height,
nodeIp,
config.parameters
);
}
broadcastToClients(message) {

View File

@@ -157,15 +157,6 @@ class UdpDiscovery extends EventEmitter {
...node
}));
// Add broadcast option
nodes.unshift({
ip: 'broadcast',
status: 'broadcast',
address: '255.255.255.255',
port: this.port,
isBroadcast: true
});
return nodes;
}