diff --git a/PER_PATTERN_PARAMETERS.md b/PER_PATTERN_PARAMETERS.md
new file mode 100644
index 0000000..2004794
--- /dev/null
+++ b/PER_PATTERN_PARAMETERS.md
@@ -0,0 +1,452 @@
+# Per-Pattern Parameters - Frontend Documentation
+
+## Overview
+
+Each pattern now has its **own unique set of parameters** (n1, n2, n3, n4, delay). When you switch patterns, the system automatically loads that pattern's saved parameters.
+
+This means:
+- Alternating can have n1=10, n2=10, delay=100
+- Segmented Movement can have n1=5, n2=20, n3=10, n4=7, delay=50
+- Each pattern remembers its own settings!
+
+---
+
+## How It Works
+
+### Pattern Switching
+When you change patterns via `POST /api/pattern`:
+1. Current pattern's parameters are **automatically saved**
+2. New pattern's **saved parameters are loaded**
+3. Parameters are sent to LED bars with the pattern
+
+### Parameter Updates
+When you update parameters via `POST /api/parameters`:
+1. Parameters update immediately
+2. **Saved for the current pattern only**
+3. Other patterns keep their own settings
+
+---
+
+## API Changes
+
+### POST /api/pattern (Enhanced)
+
+**Before:** Only returned pattern name
+**Now:** Returns pattern name AND its parameters
+
+**Request:**
+```json
+{
+ "pattern": "alternating"
+}
+```
+
+**Response:**
+```json
+{
+ "status": "ok",
+ "pattern": "alternating",
+ "parameters": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ }
+}
+```
+
+**Important:** The parameters returned are the **loaded parameters for this pattern**, not global values.
+
+---
+
+### POST /api/parameters (Unchanged API, Enhanced Behavior)
+
+Parameters are now saved per-pattern automatically.
+
+**Request:**
+```json
+{
+ "n1": 20,
+ "delay": 150
+}
+```
+
+**Response:**
+```json
+{
+ "status": "ok",
+ "parameters": {
+ "brightness": 100,
+ "delay": 150,
+ "n1": 20,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ }
+}
+```
+
+**What happens:** These parameters are saved for the **currently active pattern only**.
+
+---
+
+### GET /api/state (Enhanced)
+
+Now returns parameters for the current pattern.
+
+**Response:**
+```json
+{
+ "pattern": "segmented_movement",
+ "parameters": {
+ "brightness": 100,
+ "delay": 50,
+ "n1": 5,
+ "n2": 20,
+ "n3": 10,
+ "n4": 7
+ },
+ "color_palette": {...},
+ "beat_index": 42
+}
+```
+
+---
+
+## Usage Examples
+
+### Example 1: Pattern-Specific Configuration
+
+```javascript
+const api = 'http://10.42.0.1:8765';
+
+// Configure alternating pattern
+await fetch(`${api}/api/pattern`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({pattern: 'alternating'})
+});
+
+await fetch(`${api}/api/parameters`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({n1: 10, n2: 10, delay: 100})
+});
+
+// Configure segmented_movement pattern
+await fetch(`${api}/api/pattern`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({pattern: 'segmented_movement'})
+});
+
+await fetch(`${api}/api/parameters`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({n1: 5, n2: 20, n3: 10, n4: 7, delay: 50})
+});
+
+// Switch back to alternating
+await fetch(`${api}/api/pattern`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({pattern: 'alternating'})
+});
+// Parameters are now back to n1=10, n2=10, delay=100 automatically!
+```
+
+---
+
+### Example 2: UI Pattern Selector with Parameter Memory
+
+```javascript
+class PatternController {
+ constructor(apiBase = 'http://10.42.0.1:8765') {
+ this.api = apiBase;
+ }
+
+ async setPattern(patternName) {
+ const response = await fetch(`${this.api}/api/pattern`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({pattern: patternName})
+ });
+
+ const result = await response.json();
+
+ // Update UI with the loaded parameters for this pattern
+ this.updateParameterSliders(result.parameters);
+
+ return result;
+ }
+
+ async updateParameter(paramName, value) {
+ const response = await fetch(`${this.api}/api/parameters`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({[paramName]: value})
+ });
+
+ return await response.json();
+ }
+
+ updateParameterSliders(parameters) {
+ // Update UI sliders with pattern's saved parameters
+ document.getElementById('delay-slider').value = parameters.delay;
+ document.getElementById('n1-slider').value = parameters.n1;
+ document.getElementById('n2-slider').value = parameters.n2;
+ document.getElementById('n3-slider').value = parameters.n3;
+ document.getElementById('n4-slider').value = parameters.n4;
+ }
+}
+
+// Usage
+const patterns = new PatternController();
+
+// Switch to alternating - UI automatically shows its parameters
+await patterns.setPattern('alternating');
+
+// Adjust parameters for alternating
+await patterns.updateParameter('n1', 15);
+
+// Switch to rainbow - UI automatically shows its parameters
+await patterns.setPattern('rainbow');
+// Alternating's n1=15 is saved and will reload when you switch back!
+```
+
+---
+
+### Example 3: React Component
+
+```jsx
+import { useState, useEffect } from 'react';
+
+function PatternControl() {
+ const [pattern, setPattern] = useState('');
+ const [parameters, setParameters] = useState({});
+ const API = 'http://10.42.0.1:8765';
+
+ // Load initial state
+ useEffect(() => {
+ fetch(`${API}/api/state`)
+ .then(r => r.json())
+ .then(data => {
+ setPattern(data.pattern);
+ setParameters(data.parameters);
+ });
+ }, []);
+
+ // Change pattern - parameters auto-update
+ const changePattern = async (newPattern) => {
+ const response = await fetch(`${API}/api/pattern`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({pattern: newPattern})
+ });
+
+ const result = await response.json();
+ setPattern(result.pattern);
+ setParameters(result.parameters); // Auto-loaded for this pattern!
+ };
+
+ // Update parameter - saves for current pattern
+ const updateParam = async (param, value) => {
+ await fetch(`${API}/api/parameters`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({[param]: parseInt(value)})
+ });
+
+ setParameters({...parameters, [param]: parseInt(value)});
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Configuration Storage
+
+Parameters are stored in `lighting_config.json`:
+
+```json
+{
+ "pattern_parameters": {
+ "alternating": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "segmented_movement": {
+ "delay": 50,
+ "n1": 5,
+ "n2": 20,
+ "n3": 10,
+ "n4": 7
+ },
+ "rainbow": {
+ "delay": 80,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ }
+ },
+ "color_palette": [...],
+ "selected_color_indices": [0, 1]
+}
+```
+
+---
+
+## Pattern-Specific Parameter Usage
+
+Different patterns use parameters differently:
+
+### Alternating
+- `n1`: Number of LEDs ON in each segment
+- `n2`: Number of LEDs OFF in each segment
+- `delay`: Speed of pattern
+- `n3`, `n4`: Not used
+
+**Typical values:** n1=10, n2=10, delay=100
+
+---
+
+### Segmented Movement
+- `n1`: Length of each segment
+- `n2`: Spacing between segments
+- `n3`: Forward movement steps per beat
+- `n4`: Backward movement steps per beat
+- `delay`: Speed of pattern
+
+**Typical values:** n1=5, n2=20, n3=10, n4=7, delay=50
+
+---
+
+### Rainbow
+- `delay`: Speed of color cycling
+- `n1`, `n2`, `n3`, `n4`: Not typically used
+
+**Typical values:** delay=80
+
+---
+
+## Migration from Global Parameters
+
+**Old behavior:** All patterns shared the same parameters
+**New behavior:** Each pattern has its own parameters
+
+**For existing apps:**
+- API is **backward compatible**
+- Parameters will automatically save per-pattern
+- First time a pattern is used, it gets default values
+- After that, it remembers its settings
+
+**No changes needed to existing code!** Just be aware that:
+- Changing parameters for pattern A doesn't affect pattern B
+- When you switch patterns, parameters automatically update
+
+---
+
+## Benefits
+
+✅ **Pattern-Specific Settings** - Each pattern remembers its configuration
+✅ **No Manual Switching** - Parameters load automatically
+✅ **Persistent** - Saved across server restarts
+✅ **Intuitive** - Configure once, use forever
+✅ **Backward Compatible** - Existing code works unchanged
+
+---
+
+## UI Recommendations
+
+1. **Show Current Parameters:** When pattern changes, update UI sliders with the loaded parameters
+2. **Label Appropriately:** Show which parameters each pattern uses
+3. **Provide Presets:** Let users save/load parameter sets
+4. **Visual Feedback:** Indicate when parameters are auto-loaded vs user-changed
+
+---
+
+## Testing
+
+```bash
+# Set alternating with specific parameters
+curl -X POST http://localhost:8765/api/pattern \
+ -H "Content-Type: application/json" \
+ -d '{"pattern": "alternating"}'
+
+curl -X POST http://localhost:8765/api/parameters \
+ -H "Content-Type: application/json" \
+ -d '{"n1": 15, "n2": 8}'
+
+# Switch to another pattern
+curl -X POST http://localhost:8765/api/pattern \
+ -H "Content-Type: application/json" \
+ -d '{"pattern": "rainbow"}'
+
+# Switch back - parameters are restored!
+curl -X POST http://localhost:8765/api/pattern \
+ -H "Content-Type: application/json" \
+ -d '{"pattern": "alternating"}'
+# Response includes: "parameters": {"n1": 15, "n2": 8, ...}
+```
+
+---
+
+## Summary
+
+**Key Points:**
+- Parameters are now **per-pattern**, not global
+- Switching patterns **automatically loads** that pattern's parameters
+- Updating parameters **saves for current pattern** only
+- All automatic - no extra API calls needed
+- Fully backward compatible
+
+**For Frontend Developers:**
+- Update your UI to display loaded parameters when pattern changes
+- Parameters in `POST /api/pattern` response show what was loaded
+- Each pattern can have completely different settings
+- Users can configure patterns once and they stay configured!
+
diff --git a/lighting_config.json b/lighting_config.json
index 9a61f5d..12f1328 100644
--- a/lighting_config.json
+++ b/lighting_config.json
@@ -22,12 +22,12 @@
},
{
"r": 255,
- "g": 179,
- "b": 255
+ "g": 255,
+ "b": 0
},
{
- "r": 0,
- "g": 255,
+ "r": 255,
+ "g": 0,
"b": 255
},
{
@@ -42,7 +42,93 @@
}
],
"selected_color_indices": [
- 0,
- 1
- ]
+ 4,
+ 3
+ ],
+ "pattern_parameters": {
+ "alternating": {
+ "delay": 100,
+ "n1": 25,
+ "n2": 15,
+ "n3": 1,
+ "n4": 1
+ },
+ "segmented_movement": {
+ "delay": 100,
+ "n1": 37,
+ "n2": 39,
+ "n3": 7,
+ "n4": 21
+ },
+ "rd": {
+ "delay": 1000,
+ "n1": 68,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "sm": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "a": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "radiate": {
+ "delay": 3,
+ "n1": 32,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "f": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "r": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "on": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "o": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "p": {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ },
+ "alternating_phase": {
+ "delay": 100,
+ "n1": 21,
+ "n2": 60,
+ "n3": 1,
+ "n4": 1
+ }
+ }
}
\ No newline at end of file
diff --git a/src/control_server.py b/src/control_server.py
index 63c39ee..51f474b 100644
--- a/src/control_server.py
+++ b/src/control_server.py
@@ -124,18 +124,32 @@ class LightingController:
# Lighting state
self.current_pattern = ""
- self.delay = 100
self.brightness = 100
self.color_r = 0
self.color_g = 255
self.color_b = 0
- self.n1 = 10
- self.n2 = 10
- self.n3 = 1
- self.n4 = 1
self.beat_index = 0
self.beat_sending_enabled = True
+ # Per-pattern parameters (pattern_name -> {delay, n1, n2, n3, n4})
+ self.pattern_parameters = {}
+
+ # Default parameters for new patterns
+ self.default_params = {
+ "delay": 100,
+ "n1": 10,
+ "n2": 10,
+ "n3": 1,
+ "n4": 1
+ }
+
+ # Current active parameters (loaded from current pattern)
+ self.delay = self.default_params["delay"]
+ self.n1 = self.default_params["n1"]
+ self.n2 = self.default_params["n2"]
+ self.n3 = self.default_params["n3"]
+ self.n4 = self.default_params["n4"]
+
# Color palette (8 slots, 2 selected)
self.color_palette = [
{"r": 255, "g": 0, "b": 0}, # Red
@@ -157,6 +171,35 @@ class LightingController:
self.param_update_interval = 0.1
self.pending_param_update = False
+ def _load_pattern_parameters(self, pattern_name):
+ """Load parameters for a specific pattern."""
+ if pattern_name in self.pattern_parameters:
+ params = self.pattern_parameters[pattern_name]
+ self.delay = params.get("delay", self.default_params["delay"])
+ self.n1 = params.get("n1", self.default_params["n1"])
+ self.n2 = params.get("n2", self.default_params["n2"])
+ self.n3 = params.get("n3", self.default_params["n3"])
+ self.n4 = params.get("n4", self.default_params["n4"])
+ else:
+ # Use defaults for new pattern
+ self.delay = self.default_params["delay"]
+ self.n1 = self.default_params["n1"]
+ self.n2 = self.default_params["n2"]
+ self.n3 = self.default_params["n3"]
+ self.n4 = self.default_params["n4"]
+
+ def _save_pattern_parameters(self, pattern_name):
+ """Save current parameters for the active pattern."""
+ if pattern_name:
+ self.pattern_parameters[pattern_name] = {
+ "delay": self.delay,
+ "n1": self.n1,
+ "n2": self.n2,
+ "n3": self.n3,
+ "n4": self.n4
+ }
+ self._save_config()
+
def _current_color_rgb(self):
"""Get current RGB color tuple from selected palette color (index 0)."""
# Use the first selected color from the palette
@@ -172,6 +215,16 @@ class LightingController:
b = max(0, min(255, int(self.color_b)))
return (r, g, b)
+ def _palette_color(self, selected_index_position: int):
+ """Return RGB tuple for the selected palette color at given position (0 or 1)."""
+ if not self.selected_color_indices or selected_index_position >= len(self.selected_color_indices):
+ return self._current_color_rgb()
+ color_index = self.selected_color_indices[selected_index_position]
+ if 0 <= color_index < len(self.color_palette):
+ color = self.color_palette[color_index]
+ return (color['r'], color['g'], color['b'])
+ return self._current_color_rgb()
+
def _load_config(self):
"""Load configuration from file."""
try:
@@ -187,6 +240,10 @@ class LightingController:
if "selected_color_indices" in config:
self.selected_color_indices = config["selected_color_indices"]
+ # Load per-pattern parameters
+ if "pattern_parameters" in config:
+ self.pattern_parameters = config["pattern_parameters"]
+
logging.info(f"Loaded config from {CONFIG_FILE}")
except Exception as e:
logging.error(f"Error loading config: {e}")
@@ -197,6 +254,7 @@ class LightingController:
config = {
"color_palette": self.color_palette,
"selected_color_indices": self.selected_color_indices,
+ "pattern_parameters": self.pattern_parameters,
}
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
@@ -266,12 +324,21 @@ class LightingController:
async def _send_normal_pattern(self):
"""Send normal pattern to all bars."""
- patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate", "segmented_movement"]
+ # Patterns that need parameters (both long and short names)
+ patterns_needing_params = [
+ "alternating", "a",
+ "flicker", "f",
+ "n_chase", "nc",
+ "rainbow", "r",
+ "radiate", "rd",
+ "segmented_movement", "sm"
+ ]
payload = {
"d": {
"t": "b", # Message type: beat
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
+ "cl": [self._current_color_rgb()], # Always send color
}
}
@@ -312,23 +379,35 @@ class LightingController:
async def _handle_alternating_phase(self):
"""Handle alternating pattern with phase offset."""
+ # Determine two colors from selected palette (fallback to current color if not set)
+ color_a = self._palette_color(0)
+ color_b = self._palette_color(1)
+ phase = self.beat_index % 2
+
payload = {
"d": {
"t": "b",
"pt": "a", # alternating
"n1": self.n1,
"n2": self.n2,
- "s": self.beat_index % 2,
+ # Default color for bars not overridden (keeps payload small)
+ "cl": [color_a],
+ "s": phase,
}
}
-
+
+ # Bars in this list will have inverted phase and explicit color override
swap_bars = ["101", "103", "105", "107"]
for bar_name in LED_BAR_NAMES:
if bar_name in swap_bars:
- payload[bar_name] = {"s": (self.beat_index + 1) % 2}
+ # Invert phase and explicitly set alternate color
+ inv_phase = (phase + 1) % 2
+ alt_color = color_b if phase == 0 else color_a
+ payload[bar_name] = {"s": inv_phase, "cl": [alt_color]}
else:
+ # Keep default (no extra fields) to save bytes
payload[bar_name] = {}
-
+
await self.led_controller.send_data(payload)
async def handle_beat(self, bpm_value):
@@ -357,7 +436,14 @@ class LightingController:
async def handle_ui_command(self, message_type, data):
"""Handle command from UI client."""
if message_type == "pattern_change":
+ # Save current pattern's parameters before switching
+ if self.current_pattern:
+ self._save_pattern_parameters(self.current_pattern)
+
+ # Switch to new pattern and load its parameters
self.current_pattern = data.get("pattern", "")
+ self._load_pattern_parameters(self.current_pattern)
+
await self._send_full_parameters()
logging.info(f"Pattern changed to: {self.current_pattern}")
@@ -380,10 +466,20 @@ class LightingController:
self.n3 = data["n3"]
if "n4" in data:
self.n4 = data["n4"]
+
+ # Save parameters for current pattern
+ if self.current_pattern:
+ self._save_pattern_parameters(self.current_pattern)
+
await self._request_param_update()
elif message_type == "delay_change":
self.delay = data.get("delay", self.delay)
+
+ # Save parameters for current pattern
+ if self.current_pattern:
+ self._save_pattern_parameters(self.current_pattern)
+
await self._request_param_update()
elif message_type == "beat_toggle":
@@ -531,6 +627,7 @@ class ControlServer:
"""HTTP POST /api/pattern"""
try:
data = await request.json()
+ logging.info(f"API received pattern change: {data}")
pattern = data.get("pattern")
if not pattern:
return web.json_response(
@@ -538,13 +635,27 @@ class ControlServer:
status=400
)
+ # Save current pattern's parameters before switching
+ if self.lighting_controller.current_pattern:
+ self.lighting_controller._save_pattern_parameters(self.lighting_controller.current_pattern)
+
+ # Switch to new pattern and load its parameters
self.lighting_controller.current_pattern = pattern
+ self.lighting_controller._load_pattern_parameters(pattern)
+
await self.lighting_controller._send_full_parameters()
- logging.info(f"Pattern changed to: {pattern}")
+ logging.info(f"Pattern changed to: {pattern} with params: delay={self.lighting_controller.delay}, n1={self.lighting_controller.n1}, n2={self.lighting_controller.n2}, n3={self.lighting_controller.n3}, n4={self.lighting_controller.n4}")
return web.json_response({
"status": "ok",
- "pattern": self.lighting_controller.current_pattern
+ "pattern": self.lighting_controller.current_pattern,
+ "parameters": {
+ "delay": self.lighting_controller.delay,
+ "n1": self.lighting_controller.n1,
+ "n2": self.lighting_controller.n2,
+ "n3": self.lighting_controller.n3,
+ "n4": self.lighting_controller.n4
+ }
})
except Exception as e:
return web.json_response(
@@ -563,6 +674,7 @@ class ControlServer:
"""HTTP POST /api/parameters"""
try:
data = await request.json()
+ logging.info(f"API received parameter update: {data}")
# Update any provided parameters
if "brightness" in data:
@@ -578,6 +690,12 @@ class ControlServer:
if "n4" in data:
self.lighting_controller.n4 = int(data["n4"])
+ logging.info(f"Updated parameters for pattern '{self.lighting_controller.current_pattern}': brightness={self.lighting_controller.brightness}, delay={self.lighting_controller.delay}, n1={self.lighting_controller.n1}, n2={self.lighting_controller.n2}, n3={self.lighting_controller.n3}, n4={self.lighting_controller.n4}")
+
+ # Save parameters for current pattern
+ if self.lighting_controller.current_pattern:
+ self.lighting_controller._save_pattern_parameters(self.lighting_controller.current_pattern)
+
# Send updated parameters to LED bars
await self.lighting_controller._send_full_parameters()