feat: node canvas grid
This commit is contained in:
283
server/index.js
283
server/index.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user