From 43feb5938ff2fbb85b933ad1dfc29baa575523bc Mon Sep 17 00:00:00 2001 From: jimmy Date: Sat, 4 Oct 2025 01:10:40 +1300 Subject: [PATCH] backend: per-pattern parameters (n1-n4, delay) with persistence; REST responses include loaded params; logging of API inputs; alternating_phase: alternate colors between selected palette color 1/2 across bars with compact payload under 230 bytes; docs: add PER_PATTERN_PARAMETERS.md --- PER_PATTERN_PARAMETERS.md | 452 ++++++++++++++++++++++++++++++++++++++ lighting_config.json | 100 ++++++++- src/control_server.py | 142 +++++++++++- 3 files changed, 675 insertions(+), 19 deletions(-) create mode 100644 PER_PATTERN_PARAMETERS.md 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 ( +
+

Pattern: {pattern}

+ +
+ + + +
+ +
+ + updateParam('delay', e.target.value)} + /> + + + updateParam('n1', e.target.value)} + /> + + + updateParam('n2', e.target.value)} + /> +
+ +

+ Parameters are saved per-pattern. Switch patterns and come back - your settings are remembered! +

+
+ ); +} +``` + +--- + +## 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()