Compare commits
3 Commits
6f9133b43e
...
8ad7f41d77
Author | SHA1 | Date | |
---|---|---|---|
8ad7f41d77 | |||
dc6d48a44b | |||
43feb5938f |
452
PER_PATTERN_PARAMETERS.md
Normal file
452
PER_PATTERN_PARAMETERS.md
Normal 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!
|
||||||
|
|
@@ -16,13 +16,13 @@
|
|||||||
"b": 255
|
"b": 255
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"r": 128,
|
"r": 255,
|
||||||
"g": 0,
|
"g": 255,
|
||||||
"b": 128
|
"b": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"r": 255,
|
"r": 255,
|
||||||
"g": 179,
|
"g": 0,
|
||||||
"b": 255
|
"b": 255
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,17 +32,117 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"r": 255,
|
"r": 255,
|
||||||
"g": 255,
|
"g": 0,
|
||||||
"b": 255
|
"b": 255
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"r": 255,
|
"r": 108,
|
||||||
"g": 128,
|
"g": 255,
|
||||||
"b": 128
|
"b": 255
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"selected_color_indices": [
|
"selected_color_indices": [
|
||||||
0,
|
4,
|
||||||
1
|
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": 28,
|
||||||
|
"n4": 1
|
||||||
|
},
|
||||||
|
"ap": {
|
||||||
|
"delay": 100,
|
||||||
|
"n1": 10,
|
||||||
|
"n2": 10,
|
||||||
|
"n3": 1,
|
||||||
|
"n4": 1
|
||||||
|
},
|
||||||
|
"alternating_pulse": {
|
||||||
|
"delay": 100,
|
||||||
|
"n1": 10,
|
||||||
|
"n2": 10,
|
||||||
|
"n3": 1,
|
||||||
|
"n4": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -47,6 +47,8 @@ PATTERN_NAMES = {
|
|||||||
"specto": "s",
|
"specto": "s",
|
||||||
"radiate": "rd",
|
"radiate": "rd",
|
||||||
"segmented_movement": "sm",
|
"segmented_movement": "sm",
|
||||||
|
# New: alternate two palette colors by pulsing per beat (backend-only logical name)
|
||||||
|
"alternating_pulse": "apu",
|
||||||
# Short names pass through (for frontend use)
|
# Short names pass through (for frontend use)
|
||||||
"o": "o",
|
"o": "o",
|
||||||
"f": "f",
|
"f": "f",
|
||||||
@@ -124,18 +126,32 @@ class LightingController:
|
|||||||
|
|
||||||
# Lighting state
|
# Lighting state
|
||||||
self.current_pattern = ""
|
self.current_pattern = ""
|
||||||
self.delay = 100
|
|
||||||
self.brightness = 100
|
self.brightness = 100
|
||||||
self.color_r = 0
|
self.color_r = 0
|
||||||
self.color_g = 255
|
self.color_g = 255
|
||||||
self.color_b = 0
|
self.color_b = 0
|
||||||
self.n1 = 10
|
|
||||||
self.n2 = 10
|
|
||||||
self.n3 = 1
|
|
||||||
self.n4 = 1
|
|
||||||
self.beat_index = 0
|
self.beat_index = 0
|
||||||
self.beat_sending_enabled = True
|
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)
|
# Color palette (8 slots, 2 selected)
|
||||||
self.color_palette = [
|
self.color_palette = [
|
||||||
{"r": 255, "g": 0, "b": 0}, # Red
|
{"r": 255, "g": 0, "b": 0}, # Red
|
||||||
@@ -157,6 +173,35 @@ class LightingController:
|
|||||||
self.param_update_interval = 0.1
|
self.param_update_interval = 0.1
|
||||||
self.pending_param_update = False
|
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):
|
def _current_color_rgb(self):
|
||||||
"""Get current RGB color tuple from selected palette color (index 0)."""
|
"""Get current RGB color tuple from selected palette color (index 0)."""
|
||||||
# Use the first selected color from the palette
|
# Use the first selected color from the palette
|
||||||
@@ -172,6 +217,16 @@ class LightingController:
|
|||||||
b = max(0, min(255, int(self.color_b)))
|
b = max(0, min(255, int(self.color_b)))
|
||||||
return (r, g, 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):
|
def _load_config(self):
|
||||||
"""Load configuration from file."""
|
"""Load configuration from file."""
|
||||||
try:
|
try:
|
||||||
@@ -187,6 +242,10 @@ class LightingController:
|
|||||||
if "selected_color_indices" in config:
|
if "selected_color_indices" in config:
|
||||||
self.selected_color_indices = config["selected_color_indices"]
|
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}")
|
logging.info(f"Loaded config from {CONFIG_FILE}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading config: {e}")
|
logging.error(f"Error loading config: {e}")
|
||||||
@@ -197,6 +256,7 @@ class LightingController:
|
|||||||
config = {
|
config = {
|
||||||
"color_palette": self.color_palette,
|
"color_palette": self.color_palette,
|
||||||
"selected_color_indices": self.selected_color_indices,
|
"selected_color_indices": self.selected_color_indices,
|
||||||
|
"pattern_parameters": self.pattern_parameters,
|
||||||
}
|
}
|
||||||
with open(CONFIG_FILE, 'w') as f:
|
with open(CONFIG_FILE, 'w') as f:
|
||||||
json.dump(config, f, indent=2)
|
json.dump(config, f, indent=2)
|
||||||
@@ -266,12 +326,21 @@ class LightingController:
|
|||||||
|
|
||||||
async def _send_normal_pattern(self):
|
async def _send_normal_pattern(self):
|
||||||
"""Send normal pattern to all bars."""
|
"""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 = {
|
payload = {
|
||||||
"d": {
|
"d": {
|
||||||
"t": "b", # Message type: beat
|
"t": "b", # Message type: beat
|
||||||
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
|
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
|
||||||
|
"cl": [self._current_color_rgb()], # Always send color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,22 +381,80 @@ class LightingController:
|
|||||||
|
|
||||||
async def _handle_alternating_phase(self):
|
async def _handle_alternating_phase(self):
|
||||||
"""Handle alternating pattern with phase offset."""
|
"""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
|
||||||
|
|
||||||
|
# Set the default color based on phase so both bar groups swap each beat
|
||||||
|
default_color = color_a if phase == 0 else color_b
|
||||||
|
alt_color_for_swap = color_b if phase == 0 else color_a
|
||||||
|
# Avoid pure white edge-case on some bars by slightly reducing to 254
|
||||||
|
if default_color == (255, 255, 255):
|
||||||
|
default_color = (254, 254, 254)
|
||||||
|
if alt_color_for_swap == (255, 255, 255):
|
||||||
|
alt_color_for_swap = (254, 254, 254)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"d": {
|
"d": {
|
||||||
"t": "b",
|
"t": "b",
|
||||||
"pt": "a", # alternating
|
"pt": "a", # alternating
|
||||||
"n1": self.n1,
|
"n1": self.n1,
|
||||||
"n2": self.n2,
|
"n2": self.n2,
|
||||||
"s": self.beat_index % 2,
|
# Default color for non-swapped bars changes with phase
|
||||||
|
"cl": [default_color],
|
||||||
|
"s": phase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swap_bars = ["101", "103", "105", "107"]
|
# Bars in this list will have inverted phase and explicit color override
|
||||||
for bar_name in LED_BAR_NAMES:
|
# Flip grouping so first four bars (100-103) use default color (color_a on even beats)
|
||||||
if bar_name in swap_bars:
|
swap_bars = ["104", "105", "106", "107"]
|
||||||
payload[bar_name] = {"s": (self.beat_index + 1) % 2}
|
# Only include overrides for swapped bars to minimize payload size
|
||||||
|
for bar_name in swap_bars:
|
||||||
|
inv_phase = (phase + 1) % 2
|
||||||
|
payload[bar_name] = {"s": inv_phase, "cl": [alt_color_for_swap]}
|
||||||
|
|
||||||
|
await self.led_controller.send_data(payload)
|
||||||
|
|
||||||
|
async def _handle_alternating_pulse(self):
|
||||||
|
"""Handle APU: color1 on odd beat for odd bars, color2 on even beat for even bars."""
|
||||||
|
phase = self.beat_index % 2
|
||||||
|
color1 = self._palette_color(0)
|
||||||
|
color2 = self._palette_color(1)
|
||||||
|
if color1 == (255, 255, 255):
|
||||||
|
color1 = (254, 254, 254)
|
||||||
|
if color2 == (255, 255, 255):
|
||||||
|
color2 = (254, 254, 254)
|
||||||
|
|
||||||
|
# Define bar groups by numeric parity
|
||||||
|
even_bars = ["100", "102", "104", "106"]
|
||||||
|
odd_bars = ["101", "103", "105", "107"]
|
||||||
|
|
||||||
|
# Default: turn bars off this beat
|
||||||
|
payload = {
|
||||||
|
"d": {
|
||||||
|
"t": "b",
|
||||||
|
"pt": "o", # off by default
|
||||||
|
# Provide pulse envelope params in defaults for bars we enable
|
||||||
|
"n1": self.n1,
|
||||||
|
"n2": self.n2,
|
||||||
|
"dl": self.delay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Activate the correct half with the correct color
|
||||||
|
if phase == 0:
|
||||||
|
# Even beat -> even bars use color2
|
||||||
|
active_bars = even_bars
|
||||||
|
active_color = color2
|
||||||
else:
|
else:
|
||||||
payload[bar_name] = {}
|
# Odd beat -> odd bars use color1
|
||||||
|
active_bars = odd_bars
|
||||||
|
active_color = color1
|
||||||
|
|
||||||
|
for bar_name in active_bars:
|
||||||
|
payload[bar_name] = {"pt": "p", "cl": [active_color]}
|
||||||
|
|
||||||
await self.led_controller.send_data(payload)
|
await self.led_controller.send_data(payload)
|
||||||
|
|
||||||
@@ -351,13 +478,22 @@ class LightingController:
|
|||||||
await self._handle_sequential_pulse()
|
await self._handle_sequential_pulse()
|
||||||
elif self.current_pattern == "alternating_phase":
|
elif self.current_pattern == "alternating_phase":
|
||||||
await self._handle_alternating_phase()
|
await self._handle_alternating_phase()
|
||||||
|
elif self.current_pattern == "alternating_pulse":
|
||||||
|
await self._handle_alternating_pulse()
|
||||||
elif self.current_pattern:
|
elif self.current_pattern:
|
||||||
await self._send_normal_pattern()
|
await self._send_normal_pattern()
|
||||||
|
|
||||||
async def handle_ui_command(self, message_type, data):
|
async def handle_ui_command(self, message_type, data):
|
||||||
"""Handle command from UI client."""
|
"""Handle command from UI client."""
|
||||||
if message_type == "pattern_change":
|
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.current_pattern = data.get("pattern", "")
|
||||||
|
self._load_pattern_parameters(self.current_pattern)
|
||||||
|
|
||||||
await self._send_full_parameters()
|
await self._send_full_parameters()
|
||||||
logging.info(f"Pattern changed to: {self.current_pattern}")
|
logging.info(f"Pattern changed to: {self.current_pattern}")
|
||||||
|
|
||||||
@@ -380,10 +516,20 @@ class LightingController:
|
|||||||
self.n3 = data["n3"]
|
self.n3 = data["n3"]
|
||||||
if "n4" in data:
|
if "n4" in data:
|
||||||
self.n4 = data["n4"]
|
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()
|
await self._request_param_update()
|
||||||
|
|
||||||
elif message_type == "delay_change":
|
elif message_type == "delay_change":
|
||||||
self.delay = data.get("delay", self.delay)
|
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()
|
await self._request_param_update()
|
||||||
|
|
||||||
elif message_type == "beat_toggle":
|
elif message_type == "beat_toggle":
|
||||||
@@ -531,6 +677,7 @@ class ControlServer:
|
|||||||
"""HTTP POST /api/pattern"""
|
"""HTTP POST /api/pattern"""
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
logging.info(f"API received pattern change: {data}")
|
||||||
pattern = data.get("pattern")
|
pattern = data.get("pattern")
|
||||||
if not pattern:
|
if not pattern:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -538,13 +685,30 @@ class ControlServer:
|
|||||||
status=400
|
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
|
||||||
|
# Normalize shortnames for backend-only patterns
|
||||||
|
if pattern == "ap":
|
||||||
|
pattern = "alternating_pulse"
|
||||||
self.lighting_controller.current_pattern = pattern
|
self.lighting_controller.current_pattern = pattern
|
||||||
|
self.lighting_controller._load_pattern_parameters(pattern)
|
||||||
|
|
||||||
await self.lighting_controller._send_full_parameters()
|
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({
|
return web.json_response({
|
||||||
"status": "ok",
|
"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:
|
except Exception as e:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -563,6 +727,7 @@ class ControlServer:
|
|||||||
"""HTTP POST /api/parameters"""
|
"""HTTP POST /api/parameters"""
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
logging.info(f"API received parameter update: {data}")
|
||||||
|
|
||||||
# Update any provided parameters
|
# Update any provided parameters
|
||||||
if "brightness" in data:
|
if "brightness" in data:
|
||||||
@@ -578,6 +743,12 @@ class ControlServer:
|
|||||||
if "n4" in data:
|
if "n4" in data:
|
||||||
self.lighting_controller.n4 = int(data["n4"])
|
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
|
# Send updated parameters to LED bars
|
||||||
await self.lighting_controller._send_full_parameters()
|
await self.lighting_controller._send_full_parameters()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user