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

This commit is contained in:
2025-10-04 01:10:40 +13:00
parent 6f9133b43e
commit 43feb5938f
3 changed files with 675 additions and 19 deletions

452
PER_PATTERN_PARAMETERS.md Normal file
View File

@@ -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 (
<div>
<h2>Pattern: {pattern}</h2>
<div>
<button onClick={() => changePattern('alternating')}>Alternating</button>
<button onClick={() => changePattern('segmented_movement')}>Segmented</button>
<button onClick={() => changePattern('rainbow')}>Rainbow</button>
</div>
<div>
<label>Delay: {parameters.delay}</label>
<input
type="range" min="10" max="500"
value={parameters.delay || 100}
onChange={(e) => updateParam('delay', e.target.value)}
/>
<label>N1: {parameters.n1}</label>
<input
type="range" min="1" max="50"
value={parameters.n1 || 10}
onChange={(e) => updateParam('n1', e.target.value)}
/>
<label>N2: {parameters.n2}</label>
<input
type="range" min="0" max="50"
value={parameters.n2 || 10}
onChange={(e) => updateParam('n2', e.target.value)}
/>
</div>
<p>
<small>Parameters are saved per-pattern. Switch patterns and come back - your settings are remembered!</small>
</p>
</div>
);
}
```
---
## 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!

View File

@@ -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
}
}
}

View File

@@ -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()