10 Commits
pi ... ui

Author SHA1 Message Date
9cf1855b51 UI: Add rate limiting to brightness control
- Add 100ms minimum interval between brightness updates to backend
- Keep UI responsive with immediate local brightness updates
- Prevent backend overload from rapid MIDI CC33 changes
- Add time import for rate limiting functionality
- Add debug output to show when updates are sent vs rate limited
- Improve smoothness and reduce network traffic
- Protect lighting controller from too many rapid parameter changes
2025-10-04 10:02:47 +13:00
763a2053ad UI: Remove knobs section and make window responsive
- Remove knobs section (CC38-45) to simplify interface
- Make window fit screen with cross-platform maximization
- Use weight-based grid resizing instead of fixed minimum sizes
- Add responsive layout with expandable frames
- Fix Linux compatibility for window maximization
- Disable window geometry loading to maintain maximized state
- Elements now resize proportionally to fit any screen size
- Cleaner, more focused interface without redundant controls
2025-10-04 09:53:48 +13:00
324fa463be UI: Make all elements 50% bigger for better touch interface
- Increase window size from 1800x1200 to 2700x1800
- Scale all font sizes by 50% (14pt → 21pt, 12pt → 18pt, etc.)
- Make all buttons and controls 50% larger
- Increase grid minimum sizes from 140x70 to 210x105
- Scale button dimensions from 14x4 to 21x6 characters
- Increase padding and spacing by 50% (6px → 9px)
- Make border widths thicker (2px → 3px)
- Improve touch-friendliness and readability
- Maintain proportional scaling across all UI elements
2025-10-04 09:29:59 +13:00
aaf515d8f4 UI: Fix MIDI dropdown contrast and device detection
- Improve MIDI dropdown contrast with better colors (#FFFFFF text on #2C2C2C background)
- Add bold font and larger size for better visibility
- Fix MIDI device detection to show all available devices
- Add port validation to only show accessible MIDI devices
- Use direct widget reference instead of searching for dropdown
- Add delayed initialization to ensure UI is ready before populating dropdown
- Improve debugging output for MIDI port detection
- Add placeholder text 'No MIDI device selected' when no devices available
- Restore window geometry persistence for proper window positioning
2025-10-04 09:20:14 +13:00
7beca0cf53 UI: improve MIDI Combobox contrast in dark theme (field/list colors, selection highlight) 2025-10-04 02:16:35 +13:00
ace47b7835 UI: default CONTROL_SERVER_URI -> ws://10.42.0.1:8765 2025-10-04 02:13:21 +13:00
9045b10631 UI: MIDI 44–47 -> Color 2, 48–51 -> Color 1; label 'alternating' as 'alternating pulse'; MIDI note 39 sends 'ap'; fix async UI scheduler usage (no create_task) 2025-10-04 02:09:55 +13:00
f2e775f6f5 UI: replace 'on' with pattern 'alternating_phase' (MIDI note 36, grid label/icons); remove WebSocket usage; per-pattern parameters with state hydration; REST-only palette/state/parameters 2025-10-04 01:01:32 +13:00
a654527dc3 UI: Color palette REST integration, MIDI 44–51 color slot selection, Color 1/2 previews with next indicator and click-to-select target; use REST for pattern changes and parameter updates (brightness, delay, n1–n3); send colors only on confirm; load palette on startup; fix NoneType await issue in async handlers 2025-10-03 23:40:20 +13:00
Pi User
0906cb22e6 Add .env file support for UI client configuration
- Use python-dotenv to load environment variables
- Add CONTROL_SERVER_URI environment variable for WebSocket connection
- Create .env.example with configuration examples
- Update Pipfile to include python-dotenv dependency
- Allows easy configuration for running UI on desktop pointing to Pi
2025-10-03 20:08:36 +13:00
22 changed files with 1018 additions and 3432 deletions

11
.env
View File

@@ -1,11 +0,0 @@
# Lighting Controller Configuration
CONTROL_SERVER_URI=ws://localhost:8765
CONTROL_SERVER_HOST=0.0.0.0
CONTROL_SERVER_PORT=8765
TRANSPORT=spi
AUDIO_INPUT_DEVICE=1
MIDI_TCP_HOST=127.0.0.1
MIDI_TCP_PORT=65432
SOUND_CONTROL_HOST=127.0.0.1
SOUND_CONTROL_PORT=65433
HTTP_API_PORT=8766

View File

@@ -1,48 +1,9 @@
# Lighting Controller Configuration # Lighting Controller UI Client Configuration
# ==============================
# WebSocket Configuration
# ==============================
# WebSocket URI for the control server # WebSocket URI for the control server
# Used by UI client and test scripts to connect to the control server # For local development (running UI on same machine as control server):
# CONTROL_SERVER_URI=ws://localhost:8765
# Default: Use wlan0 IP for remote connections
CONTROL_SERVER_URI=ws://10.42.0.1:8765
#
# For local development (running on same machine as control server):
# CONTROL_SERVER_URI=ws://localhost:8765
#
# For custom IP (if your Pi has a different address):
# CONTROL_SERVER_URI=ws://YOUR_PI_IP:8765
# Control server WebSocket host (bind address) # For remote connection (running UI on desktop, control server on Pi):
# 0.0.0.0 = listen on all interfaces (allows external connections) # Replace with your Raspberry Pi's IP address
# localhost or 127.0.0.1 = listen only on localhost (local only) # CONTROL_SERVER_URI=ws://10.1.1.117:8765
CONTROL_SERVER_HOST=0.0.0.0
# Control server WebSocket port
CONTROL_SERVER_PORT=8765
# ==============================
# Transport Configuration
# ==============================
# Transport method for LED communication
# Options: spi, websocket
TRANSPORT=spi
# ==============================
# Sound Detection Configuration
# ==============================
# Audio input device index (use sound.py to list available devices)
AUDIO_INPUT_DEVICE=1
# MIDI TCP configuration
MIDI_TCP_HOST=127.0.0.1
MIDI_TCP_PORT=65432
# Sound control server configuration
SOUND_CONTROL_HOST=127.0.0.1
SOUND_CONTROL_PORT=65433
# HTTP API server port (for color palette API)
HTTP_API_PORT=8766

View File

@@ -1,148 +0,0 @@
# Migration to aiohttp for WebSocket and HTTP
## Summary
The control server has been refactored to use **aiohttp** for both WebSocket and HTTP endpoints, replacing the previous `websockets` library for the WebSocket server.
## What Changed
### 1. Server Architecture
**Before:**
- Separate `websockets` server on port 8765
- Separate `aiohttp` HTTP API server on port 8766
**After:**
- Single `aiohttp` application serving both:
- WebSocket endpoint: `ws://host:8765/ws`
- HTTP API endpoints: `http://host:8765/api/*`
- HTTP API also available on port 8766 for backward compatibility
### 2. Code Changes
#### `/home/pi/lighting-controller/src/control_server.py`
**Removed:**
- `import websockets`
- `handle_ui_client()` method (used websockets library)
- `start_websocket_server()` method
- `_websocket_server_task()` method
**Added:**
- `handle_websocket()` method using aiohttp's WebSocket support
- Updated `start_http_server()` to include WebSocket endpoint
- Combined server startup (HTTP and WebSocket on same port)
**Key Differences:**
```python
# OLD (websockets library)
async def handle_ui_client(self, websocket):
async for message in websocket:
data = json.loads(message)
await websocket.send(json.dumps(response))
# NEW (aiohttp)
async def handle_websocket(self, request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
data = json.loads(msg.data)
await ws.send_json(response)
return ws
```
#### `/home/pi/lighting-controller/Pipfile`
Both `aiohttp` and `websockets` are now in the dependencies:
- `aiohttp` - for server WebSocket and HTTP
- `websockets` - for client connections to LED bars (in `networking.py`)
### 3. Client Changes Required
**WebSocket URL Update:**
Clients must update their connection URL to include the `/ws` path:
```javascript
// OLD
const ws = new WebSocket('ws://10.42.0.1:8765');
// NEW
const ws = new WebSocket('ws://10.42.0.1:8765/ws');
```
**Python Client:**
```python
# OLD
uri = "ws://10.42.0.1:8765"
# NEW
uri = "ws://10.42.0.1:8765/ws"
```
### 4. HTTP API
The HTTP API endpoints remain the same, but are now available on both ports:
- **Primary:** `http://10.42.0.1:8765/api/color-palette`
- **Backward Compatibility:** `http://10.42.0.1:8766/api/color-palette`
### 5. Configuration
Environment variables remain the same:
```bash
CONTROL_SERVER_HOST=0.0.0.0
CONTROL_SERVER_PORT=8765
HTTP_API_PORT=8766
```
## Benefits
1. **Unified Server:** Single aiohttp application handles all HTTP and WebSocket traffic
2. **Easier CORS:** Can add CORS middleware once for both REST and WebSocket
3. **Simpler Deployment:** One port for UI clients (WebSocket + API)
4. **Standard REST Patterns:** aiohttp's routing and middleware ecosystem
5. **Less Dependencies:** One web framework instead of two
## Testing
### Test WebSocket Connection
```bash
# Python test client
pipenv run python test/test_control_server.py --pattern rainbow
```
### Test HTTP API
```bash
# Get color palette
curl http://localhost:8765/api/color-palette
# Update selected colors
curl -X POST http://localhost:8765/api/color-palette \
-H "Content-Type: application/json" \
-d '{"selected_indices": [3, 6]}'
```
## Backward Compatibility
- HTTP API still available on port 8766
- WebSocket protocol unchanged (JSON message format)
- Only the WebSocket URL path changed (added `/ws`)
## Files Modified
1. `/home/pi/lighting-controller/src/control_server.py` - Main refactor
2. `/home/pi/lighting-controller/Pipfile` - Dependencies (kept both aiohttp and websockets)
3. `/home/pi/lighting-controller/COLOR_PALETTE_API.md` - Updated documentation
## Files NOT Modified
- `/home/pi/lighting-controller/src/networking.py` - Still uses `websockets` for client connections
- `/home/pi/lighting-controller/src/ui_client.py` - UI being worked on elsewhere (needs URL update)
- `/home/pi/led-bar/*` - No changes needed

View File

@@ -1,290 +0,0 @@
# Color Palette API - Test Results ✅
## Test Summary
All tests **PASSED** successfully! The Color Palette REST API is fully functional.
---
## Test Environment
- **Server:** Raspberry Pi (localhost / 10.42.0.1)
- **Primary Port:** 8765
- **Backup Port:** 8766
- **Framework:** aiohttp (unified WebSocket + HTTP)
- **Persistence:** lighting_config.json
---
## Test Results
### ✅ Test 1: GET Current Palette
**Endpoint:** `GET /api/color-palette`
**Result:** SUCCESS
- Returns 8 colors with RGB values (0-255)
- Returns 2 selected indices (0-7)
- JSON format is correct
**Sample Response:**
```json
{
"palette": [
{"r": 255, "g": 0, "b": 0},
{"r": 0, "g": 255, "b": 0},
...
],
"selected_indices": [3, 5]
}
```
---
### ✅ Test 2: Update Selected Colors
**Endpoint:** `POST /api/color-palette`
**Request:**
```json
{"selected_indices": [0, 2]}
```
**Result:** SUCCESS
- Selected indices updated to [0, 2]
- Returns status "ok"
- Returns full updated palette
- Change persisted to config file
---
### ✅ Test 3: Update Palette Colors
**Endpoint:** `POST /api/color-palette`
**Request:**
```json
{
"palette": [
{"r": 255, "g": 0, "b": 0},
{"r": 0, "g": 255, "b": 0},
{"r": 0, "g": 0, "b": 255},
{"r": 128, "g": 0, "b": 128}, // Changed slot 3 to purple
...
]
}
```
**Result:** SUCCESS
- Palette colors updated correctly
- Slot 3 changed to purple (128, 0, 128)
- Change persisted to config file
---
### ✅ Test 4: Combined Update
**Endpoint:** `POST /api/color-palette`
**Request:**
```json
{
"palette": [...],
"selected_indices": [3, 5]
}
```
**Result:** SUCCESS
- Both palette and selected indices updated
- Changes persisted
---
### ✅ Test 5: Persistence
**File:** `lighting_config.json`
**Result:** SUCCESS
- All changes saved to config file
- File format is valid JSON
- Contains both `color_palette` and `selected_color_indices`
**Config Structure:**
```json
{
"color_palette": [...],
"selected_color_indices": [3, 5]
}
```
---
### ✅ Test 6: Backup Port (8766)
**Endpoint:** `GET http://localhost:8766/api/color-palette`
**Result:** SUCCESS
- API accessible on backup port
- Returns same data as primary port
- Useful for backward compatibility
---
### ⚠️ Test 7: Invalid Data Handling
**Request:** Invalid index (10, out of range 0-7)
```json
{"selected_indices": [0, 10]}
```
**Result:** GRACEFUL FAILURE
- Invalid data is rejected silently
- Previous valid state is maintained
- Warning logged to server logs
- Returns current valid state (not an error response)
**Note:** This is by design - the server maintains valid state and logs warnings rather than returning 400 errors. UI developers should validate input client-side.
---
## Performance
- **Response Time:** < 50ms for all requests (localhost)
- **Server Startup:** ~2 seconds
- **Persistence:** Immediate (synchronous file write)
---
## Integration Checklist for UI
- [x] GET endpoint working
- [x] POST endpoint working
- [x] Both ports accessible (8765, 8766)
- [x] Data persistence verified
- [x] JSON format validated
- [x] Invalid data handled gracefully
- [x] Test script provided (`test_color_api.sh`)
---
## Usage for UI Developers
### Quick Test
```bash
# Get current palette
curl http://10.42.0.1:8765/api/color-palette | jq
# Update selected colors
curl -X POST http://10.42.0.1:8765/api/color-palette \
-H "Content-Type: application/json" \
-d '{"selected_indices": [0, 2]}'
```
### Run Full Test Suite
```bash
./test_color_api.sh 10.42.0.1
```
### JavaScript Example (Ready to Use)
```javascript
// Load palette on UI startup
const loadPalette = async () => {
const response = await fetch('http://10.42.0.1:8765/api/color-palette');
return await response.json();
};
// Update color in slot 3
const updateColor = async (slotIndex, r, g, b) => {
const current = await loadPalette();
const newPalette = [...current.palette];
newPalette[slotIndex] = {r, g, b};
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({palette: newPalette})
});
};
// Select active colors
const selectColors = async (index1, index2) => {
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [index1, index2]})
});
};
```
---
## Files Modified/Created
### Backend Implementation
- `src/control_server.py` - Color palette logic + HTTP endpoints
- `lighting_config.json` - Persistent storage (auto-created)
### Documentation
- `COLOR_PALETTE_API.md` - Full API documentation for UI
- `AIOHTTP_MIGRATION.md` - Server architecture details
- `API_TEST_RESULTS.md` - This file
### Testing
- `test_color_api.sh` - Automated test script
---
## Pattern Integration
### ✅ Color Selection for Patterns
The system now uses **the first selected color (index 0)** as the primary RGB color for LED patterns.
**Example:**
```javascript
// If selected_indices is [4, 5]
// Patterns will use the color from palette slot 4
```
**Test Result:**
```
Selected Indices: [4, 4]
First Selected: Slot 4 → RGB(255, 179, 255)
Pattern RGB: RGB(255, 179, 255)
✅ SUCCESS: Pattern RGB matches first selected color!
```
This means:
- When UI changes `selected_indices[0]`, the pattern color changes immediately
- The second selected color (index 1) is reserved for future dual-color patterns
- Legacy RGB sliders are used as fallback if palette is not configured
---
## Known Issues / Notes
1. **Pattern Color:** The first selected color (index 0) is automatically used for patterns. Change `selected_indices[0]` to change pattern color.
2. **Validation:** Invalid data is logged but doesn't return error responses. UI should validate client-side:
- Palette must be exactly 8 colors
- RGB values must be 0-255
- Selected indices must be 0-7
3. **CORS:** No CORS headers currently. UI on same domain works fine. Cross-origin requests may need CORS middleware.
4. **WebSocket Path:** WebSocket moved to `/ws` path (not root). Update client connections to `ws://10.42.0.1:8765/ws`.
---
## Status: ✅ READY FOR UI INTEGRATION
The Color Palette API is fully tested and ready for integration into the UI client. All endpoints are working correctly with proper data persistence.
**Next Steps:**
1. UI developer implements color picker using API
2. Test from UI client (may need to update WebSocket connection to `/ws`)
3. Consider adding CORS if needed for cross-origin requests
---
## Support
- **Documentation:** See `COLOR_PALETTE_API.md`
- **Test Script:** Run `./test_color_api.sh 10.42.0.1`
- **Server Logs:** Check terminal output for warnings/errors
- **Config File:** `lighting_config.json` (can be edited manually if needed)

View File

@@ -1,129 +0,0 @@
# Color Palette API - Quick Reference Card
## 🎯 Endpoints
| Method | URL | Purpose |
|--------|-----|---------|
| `GET` | `http://10.42.0.1:8765/api/color-palette` | Get current palette |
| `POST` | `http://10.42.0.1:8765/api/color-palette` | Update palette/selection |
| `PUT` | `http://10.42.0.1:8765/api/color-palette` | Same as POST |
**Backup:** Same endpoints available on port `8766`
---
## 📦 Data Structure
```javascript
{
palette: [
{r: 255, g: 0, b: 0}, // Slot 0
{r: 0, g: 255, b: 0}, // Slot 1
{r: 0, g: 0, b: 255}, // Slot 2
{r: 255, g: 255, b: 0}, // Slot 3
{r: 255, g: 0, b: 255}, // Slot 4
{r: 0, g: 255, b: 255}, // Slot 5
{r: 255, g: 128, b: 0}, // Slot 6
{r: 255, g: 255, b: 255} // Slot 7
],
selected_indices: [0, 1] // Active colors
// [0] = Primary RGB for patterns
// [1] = Reserved for future use
}
```
---
## 💻 Copy-Paste Code
### Get Palette
```javascript
const res = await fetch('http://10.42.0.1:8765/api/color-palette');
const {palette, selected_indices} = await res.json();
```
### Update Color (e.g., slot 3 to purple)
```javascript
const current = await fetch('http://10.42.0.1:8765/api/color-palette')
.then(r => r.json());
const newPalette = [...current.palette];
newPalette[3] = {r: 128, g: 0, b: 128};
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({palette: newPalette})
});
```
### Select Colors (e.g., slots 2 and 5)
```javascript
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [2, 5]})
});
```
---
## 🛠️ Helper Functions
### RGB ↔ Hex Conversion
```javascript
const rgbToHex = ({r, g, b}) =>
'#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
```
---
## ✅ Validation Rules
- **8 colors** in palette (exactly)
- **RGB values** 0-255 (integers)
- **2 selected indices** (exactly)
- **Indices** 0-7 (valid range)
---
## 🧪 Test
```bash
# Get
curl http://10.42.0.1:8765/api/color-palette | jq
# Update selection
curl -X POST http://10.42.0.1:8765/api/color-palette \
-H "Content-Type: application/json" \
-d '{"selected_indices": [0, 2]}'
# Run full test suite
./test_color_api.sh 10.42.0.1
```
---
## 📝 Notes
- **First selected color (index 0) is used as the primary RGB for LED patterns**
- Changes auto-save to `lighting_config.json`
- Invalid data is ignored (not rejected)
- Validate client-side before sending
- WebSocket is at `ws://10.42.0.1:8765/ws` (different path)
---
## 📚 Full Docs
See `COLOR_PALETTE_API.md` for complete documentation

View File

@@ -1,231 +0,0 @@
# Pattern Color Integration - Test Results ✅
## Test Date
October 3, 2025
## Summary
**ALL TESTS PASSED** ✅
The pattern system now successfully uses the first selected color from the color palette as the primary RGB value for all LED patterns.
---
## Test Results
### Test 1: Color Selection Loading
**Status:** ✅ PASS
```
Current palette has 8 colors
Selected indices: [3, 4]
Current color (slot 3): RGB(128, 0, 128) [Purple]
```
**Result:** System correctly loads selected color from persistent config.
---
### Test 2: Pattern Activation with Palette Color
**Status:** ✅ PASS
```
Pattern: 'on'
Expected Color: RGB(128, 0, 128) [Purple - from slot 3]
```
**Result:** Pattern activated using the selected palette color instead of legacy RGB sliders.
---
### Test 3: Dynamic Color Change to RED
**Status:** ✅ PASS
```
API Call: POST /api/color-palette {"selected_indices": [0, 1]}
Expected Color: RGB(255, 0, 0) [Red - slot 0]
```
**Result:** Pattern color updated immediately when palette selection changed via API.
---
### Test 4: Dynamic Color Change to BLUE
**Status:** ✅ PASS
```
API Call: POST /api/color-palette {"selected_indices": [2, 1]}
Expected Color: RGB(0, 0, 255) [Blue - slot 2]
```
**Result:** Pattern color updated again, confirming real-time color switching.
---
### Test 5: Pattern Change with New Color
**Status:** ✅ PASS
```
Pattern Change: 'alternating'
Current Color: RGB(0, 0, 255) [Blue]
```
**Result:** New pattern activated using the currently selected palette color.
---
### Test 6: Restore Original Selection
**Status:** ✅ PASS
```
API Call: POST /api/color-palette {"selected_indices": [3, 4]}
Expected Color: RGB(128, 0, 128) [Purple - slot 3]
```
**Result:** System correctly restored original color selection.
---
## Implementation Verification
### Code Implementation
`src/control_server.py` - `_current_color_rgb()` method updated
✅ Uses first selected index (`selected_color_indices[0]`)
✅ Falls back to legacy RGB sliders if palette not configured
✅ Persistent storage in `lighting_config.json`
### Integration Points
✅ WebSocket commands trigger pattern changes
✅ HTTP API updates palette selection
✅ Pattern system uses `_current_color_rgb()` for color data
✅ Changes persist across server restarts
---
## Performance
- **Color Change Latency:** < 100ms
- **Pattern Update:** Immediate on next beat
- **API Response Time:** < 50ms (localhost)
- **WebSocket Connection:** Stable throughout test
---
## User Experience Flow
1. **UI loads** `GET /api/color-palette` Shows 8 colors + 2 selected
2. **User clicks color slot 5** `POST {"selected_indices": [5, 1]}`
3. **Pattern immediately uses** slot 5's color on next LED update
4. **Visual feedback** LED bar shows new color within 1 beat cycle
---
## Test Sequence Demonstrated
```
[Purple] → Pattern 'on' → Purple LEDs
[Red] → Same pattern → Red LEDs
[Blue] → Same pattern → Blue LEDs
[Blue] → Pattern 'alternating' → Blue alternating LEDs
[Purple] → Same pattern → Purple alternating LEDs
```
---
## WebSocket Endpoint Update
**Important:** WebSocket moved to `/ws` path in aiohttp migration
- **Old:** `ws://localhost:8765`
- **New:** `ws://localhost:8765/ws`
Test script updated: `test/test_control_server.py` now uses correct path.
---
## Test Scripts Created
1. **`test_color_selection.py`** - Unit test for `_current_color_rgb()`
2. **`test_pattern_color.py`** - Basic WebSocket pattern test
3. **`test_color_patterns.py`** - Full integration test (used for these results)
---
## Files Modified
### Backend
- `src/control_server.py` - Updated `_current_color_rgb()` method
- `test/test_control_server.py` - Updated WebSocket path to `/ws`
### Documentation
- `COLOR_PALETTE_API.md` - Added pattern integration notes
- `COLOR_API_QUICK_REF.md` - Added pattern color behavior
- `API_TEST_RESULTS.md` - Added pattern integration section
- `PATTERN_COLOR_TEST_RESULTS.md` - This file
---
## Example Code for UI
### Check Current Pattern Color
```javascript
// Get the color that patterns are currently using
const response = await fetch('http://10.42.0.1:8765/api/color-palette');
const {palette, selected_indices} = await response.json();
const patternColor = palette[selected_indices[0]];
console.log(`Pattern RGB: (${patternColor.r}, ${patternColor.g}, ${patternColor.b})`);
```
### Change Pattern Color
```javascript
// Change pattern to use slot 5 (Cyan)
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [5, 1]})
});
// Pattern will now use cyan color
```
---
## Status
🟢 **PRODUCTION READY**
The color palette system is fully integrated with the pattern system and ready for UI implementation.
### What Works
Palette selection persists across restarts
Pattern colors update dynamically via API
Real-time color switching without pattern interruption
Fallback to legacy RGB sliders for backward compatibility
WebSocket and HTTP API both functional
### Known Limitations
- Second selected color (index 1) not yet used by patterns (reserved for future dual-color patterns)
- Legacy RGB sliders still functional but overridden when palette is configured
---
## Next Steps for UI
1. Display 8 palette colors as clickable slots
2. Highlight first selected color as "active for patterns"
3. On click, send `POST {"selected_indices": [clicked_slot, current_second_slot]}`
4. Show visual feedback when pattern color changes
5. Consider hiding/disabling legacy RGB sliders when palette is active
---
## Conclusion
The pattern color integration is **complete and tested**. Changing the selected palette color via the REST API immediately updates the color used by all LED patterns.
**Test Confidence:** 100%
**Ready for UI Integration:** Yes
**Documentation:** Complete

View File

@@ -1,452 +0,0 @@
# 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

@@ -4,15 +4,12 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
aiohttp = "*"
websockets = "*" websockets = "*"
spidev = "*"
watchfiles = "*" watchfiles = "*"
async-tkinter-loop = "*" async-tkinter-loop = "*"
mido = "*" mido = "*"
python-rtmidi = "*" python-rtmidi = "*"
pyaudio = "*"
aubio = "*"
websocket-client = "*" websocket-client = "*"
python-dotenv = "*" python-dotenv = "*"

566
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "dd26ac6e21af21bc1d009aba24fc011d458a5e81129873206715309f12d58690" "sha256": "3f3ca9af45dd4382aac3c649ae11cefbe97059ad14f40172735213c4919baada"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -16,114 +16,6 @@
] ]
}, },
"default": { "default": {
"aiohappyeyeballs": {
"hashes": [
"sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558",
"sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"
],
"markers": "python_version >= '3.9'",
"version": "==2.6.1"
},
"aiohttp": {
"hashes": [
"sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe",
"sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645",
"sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af",
"sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263",
"sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142",
"sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6",
"sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6",
"sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09",
"sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84",
"sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1",
"sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50",
"sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a",
"sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79",
"sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c",
"sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd",
"sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0",
"sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75",
"sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77",
"sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c",
"sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab",
"sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4",
"sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9",
"sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421",
"sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685",
"sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b",
"sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf",
"sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693",
"sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c",
"sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2",
"sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519",
"sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d",
"sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02",
"sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea",
"sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05",
"sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b",
"sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0",
"sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd",
"sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98",
"sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb",
"sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8",
"sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f",
"sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89",
"sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16",
"sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64",
"sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb",
"sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7",
"sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728",
"sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7",
"sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830",
"sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d",
"sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8",
"sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d",
"sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406",
"sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2",
"sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9",
"sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315",
"sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d",
"sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd",
"sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d",
"sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51",
"sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3",
"sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34",
"sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461",
"sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b",
"sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc",
"sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530",
"sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5",
"sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d",
"sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7",
"sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5",
"sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54",
"sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d",
"sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7",
"sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117",
"sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4",
"sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1",
"sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676",
"sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b",
"sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d",
"sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0",
"sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d",
"sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444",
"sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0",
"sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065",
"sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545",
"sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"
],
"index": "pypi",
"version": "==3.12.15"
},
"aiosignal": {
"hashes": [
"sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e",
"sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"
],
"markers": "python_version >= '3.9'",
"version": "==1.4.0"
},
"anyio": { "anyio": {
"hashes": [ "hashes": [
"sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc",
@@ -140,14 +32,6 @@
"index": "pypi", "index": "pypi",
"version": "==0.9.3" "version": "==0.9.3"
}, },
"attrs": {
"hashes": [
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"
],
"markers": "python_version >= '3.8'",
"version": "==25.3.0"
},
"aubio": { "aubio": {
"hashes": [ "hashes": [
"sha256:df1244f6c4cf5bea382c8c2d35aa43bc31f4cf631fe325ae3992c219546a4202" "sha256:df1244f6c4cf5bea382c8c2d35aa43bc31f4cf631fe325ae3992c219546a4202"
@@ -155,116 +39,6 @@
"index": "pypi", "index": "pypi",
"version": "==0.4.9" "version": "==0.4.9"
}, },
"frozenlist": {
"hashes": [
"sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f",
"sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b",
"sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949",
"sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615",
"sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6",
"sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718",
"sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df",
"sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf",
"sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677",
"sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5",
"sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50",
"sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb",
"sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56",
"sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa",
"sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7",
"sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43",
"sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f",
"sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938",
"sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c",
"sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd",
"sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c",
"sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e",
"sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d",
"sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81",
"sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e",
"sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657",
"sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478",
"sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2",
"sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca",
"sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e",
"sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e",
"sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3",
"sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63",
"sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898",
"sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd",
"sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca",
"sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2",
"sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104",
"sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba",
"sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a",
"sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1",
"sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae",
"sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577",
"sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60",
"sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee",
"sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464",
"sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61",
"sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86",
"sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01",
"sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87",
"sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb",
"sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f",
"sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71",
"sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8",
"sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d",
"sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2",
"sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00",
"sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b",
"sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b",
"sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146",
"sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59",
"sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878",
"sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08",
"sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890",
"sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e",
"sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750",
"sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb",
"sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d",
"sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30",
"sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3",
"sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d",
"sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a",
"sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8",
"sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c",
"sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1",
"sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9",
"sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e",
"sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e",
"sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384",
"sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98",
"sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb",
"sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4",
"sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65",
"sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08",
"sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb",
"sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43",
"sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a",
"sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7",
"sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630",
"sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d",
"sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31",
"sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d",
"sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44",
"sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319",
"sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e",
"sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025",
"sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35",
"sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee",
"sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1",
"sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd",
"sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74",
"sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b",
"sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981",
"sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"
],
"markers": "python_version >= '3.9'",
"version": "==1.7.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
@@ -281,122 +55,6 @@
"index": "pypi", "index": "pypi",
"version": "==1.3.3" "version": "==1.3.3"
}, },
"multidict": {
"hashes": [
"sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9",
"sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729",
"sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5",
"sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e",
"sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138",
"sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495",
"sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f",
"sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1",
"sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e",
"sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6",
"sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8",
"sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded",
"sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae",
"sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69",
"sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364",
"sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f",
"sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f",
"sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e",
"sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3",
"sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0",
"sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657",
"sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c",
"sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb",
"sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7",
"sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0",
"sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d",
"sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b",
"sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141",
"sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf",
"sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f",
"sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf",
"sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f",
"sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24",
"sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a",
"sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa",
"sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f",
"sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b",
"sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0",
"sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb",
"sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d",
"sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879",
"sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c",
"sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a",
"sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d",
"sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812",
"sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da",
"sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb",
"sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e",
"sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287",
"sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb",
"sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb",
"sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4",
"sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad",
"sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f",
"sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395",
"sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5",
"sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0",
"sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793",
"sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e",
"sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db",
"sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b",
"sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c",
"sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45",
"sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987",
"sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796",
"sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92",
"sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978",
"sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802",
"sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438",
"sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6",
"sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a",
"sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace",
"sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f",
"sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4",
"sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665",
"sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f",
"sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402",
"sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9",
"sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb",
"sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7",
"sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17",
"sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb",
"sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c",
"sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877",
"sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683",
"sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e",
"sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0",
"sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3",
"sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8",
"sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd",
"sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e",
"sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd",
"sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0",
"sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7",
"sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7",
"sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52",
"sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0",
"sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50",
"sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb",
"sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2",
"sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6",
"sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb",
"sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210",
"sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53",
"sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e",
"sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605",
"sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9",
"sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e",
"sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a",
"sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"
],
"markers": "python_version >= '3.9'",
"version": "==6.6.4"
},
"numpy": { "numpy": {
"hashes": [ "hashes": [
"sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b",
@@ -485,110 +143,6 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==25.0" "version": "==25.0"
}, },
"propcache": {
"hashes": [
"sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c",
"sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81",
"sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f",
"sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6",
"sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535",
"sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be",
"sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba",
"sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3",
"sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0",
"sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4",
"sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168",
"sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b",
"sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea",
"sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770",
"sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2",
"sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892",
"sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154",
"sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf",
"sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb",
"sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1",
"sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef",
"sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe",
"sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897",
"sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3",
"sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70",
"sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330",
"sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44",
"sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0",
"sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88",
"sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1",
"sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3",
"sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43",
"sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4",
"sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1",
"sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220",
"sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7",
"sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9",
"sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50",
"sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e",
"sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2",
"sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66",
"sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1",
"sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb",
"sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe",
"sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c",
"sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7",
"sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9",
"sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e",
"sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701",
"sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9",
"sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8",
"sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b",
"sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f",
"sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e",
"sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02",
"sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e",
"sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1",
"sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10",
"sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387",
"sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198",
"sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f",
"sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b",
"sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e",
"sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614",
"sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252",
"sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9",
"sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5",
"sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c",
"sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1",
"sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770",
"sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339",
"sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251",
"sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db",
"sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf",
"sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95",
"sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df",
"sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2",
"sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945",
"sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474",
"sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b",
"sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615",
"sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06",
"sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33",
"sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec",
"sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886",
"sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb",
"sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1",
"sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05",
"sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d",
"sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39",
"sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67",
"sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e",
"sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28",
"sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a",
"sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394",
"sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725",
"sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c",
"sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"
],
"markers": "python_version >= '3.9'",
"version": "==0.3.2"
},
"pyaudio": { "pyaudio": {
"hashes": [ "hashes": [
"sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57", "sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57",
@@ -608,14 +162,6 @@
"index": "pypi", "index": "pypi",
"version": "==0.2.14" "version": "==0.2.14"
}, },
"python-dotenv": {
"hashes": [
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
],
"index": "pypi",
"version": "==1.1.1"
},
"python-rtmidi": { "python-rtmidi": {
"hashes": [ "hashes": [
"sha256:052c89933cae4fca354012d8ca7248f4f9e1e3f062471409d48415a7f7d7e59e", "sha256:052c89933cae4fca354012d8ca7248f4f9e1e3f062471409d48415a7f7d7e59e",
@@ -866,116 +412,6 @@
], ],
"index": "pypi", "index": "pypi",
"version": "==15.0.1" "version": "==15.0.1"
},
"yarl": {
"hashes": [
"sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845",
"sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53",
"sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a",
"sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed",
"sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2",
"sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02",
"sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf",
"sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010",
"sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3",
"sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef",
"sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04",
"sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23",
"sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e",
"sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6",
"sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e",
"sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a",
"sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a",
"sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8",
"sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805",
"sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2",
"sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458",
"sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc",
"sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d",
"sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b",
"sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73",
"sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7",
"sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309",
"sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e",
"sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698",
"sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c",
"sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691",
"sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16",
"sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f",
"sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f",
"sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004",
"sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3",
"sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240",
"sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28",
"sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513",
"sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773",
"sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba",
"sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4",
"sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e",
"sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1",
"sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31",
"sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16",
"sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819",
"sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d",
"sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3",
"sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8",
"sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf",
"sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723",
"sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13",
"sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1",
"sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b",
"sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f",
"sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d",
"sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30",
"sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77",
"sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a",
"sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389",
"sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e",
"sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e",
"sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c",
"sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1",
"sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833",
"sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b",
"sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee",
"sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000",
"sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38",
"sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8",
"sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd",
"sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16",
"sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d",
"sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a",
"sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06",
"sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb",
"sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4",
"sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9",
"sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8",
"sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390",
"sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8",
"sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be",
"sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c",
"sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac",
"sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b",
"sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5",
"sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4",
"sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e",
"sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f",
"sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee",
"sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5",
"sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70",
"sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1",
"sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24",
"sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653",
"sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3",
"sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00",
"sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983",
"sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d",
"sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7",
"sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce",
"sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e",
"sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"
],
"markers": "python_version >= '3.9'",
"version": "==1.20.1"
} }
}, },
"develop": {} "develop": {}

View File

@@ -1,246 +0,0 @@
# REST API Implementation - Complete ✅
## Summary
The lighting controller now has a **complete REST API** - no WebSocket required for frontend operations!
All lighting control functions are available via simple HTTP requests.
---
## What Was Added
### New API Endpoints
1. **`GET /api/state`** - Get complete system state
2. **`GET/POST /api/pattern`** - Get/change active pattern
3. **`GET/POST /api/parameters`** - Get/change brightness, delay, n1-n4
4. **`POST /api/tempo/reset`** - Reset tempo detection
5. **`GET/POST /api/color-palette`** - Color palette (already existed)
### WebSocket Status
- WebSocket endpoint still available at `/ws` for legacy support
- **New frontends should use REST API only**
- Simpler, more standard, easier to debug
---
## Test Results
All endpoints tested and working:
```bash
✅ GET /api/state - Returns complete system state
✅ POST /api/pattern - Changes pattern successfully
✅ GET /api/pattern - Returns current pattern
✅ POST /api/parameters - Updates brightness/delay/n1-n4
✅ GET /api/parameters - Returns all parameters
✅ POST /api/color-palette - Updates palette/selection
✅ GET /api/color-palette - Returns palette state
```
**Sample Test Output:**
```json
// POST /api/pattern
{
"status": "ok",
"pattern": "rainbow"
}
// GET /api/parameters
{
"brightness": 75,
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 1,
"n4": 1
}
```
---
## Documentation
**Main Documentation:** `FRONTEND_API.md`
Contains:
- Complete API reference
- JavaScript examples
- React component examples
- Vanilla JS examples
- Error handling
- Full working code samples
**Quick Reference:** `COLOR_API_QUICK_REF.md`
---
## Usage Examples
### Load Initial State
```javascript
const response = await fetch('http://10.42.0.1:8765/api/state');
const state = await response.json();
// state contains: pattern, parameters, color_palette, beat_index
```
### Change Pattern
```javascript
await fetch('http://10.42.0.1:8765/api/pattern', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern: 'rainbow'})
});
```
### Adjust Brightness
```javascript
await fetch('http://10.42.0.1:8765/api/parameters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({brightness: 75})
});
```
### Select Color
```javascript
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [2, 1]})
});
```
---
## Benefits Over WebSocket
1. **Simpler** - No connection management
2. **Standard** - Works with any HTTP library
3. **Debuggable** - Use curl, browser, Postman
4. **Stateless** - No connection drops
5. **Cacheable** - GET requests can be cached
6. **RESTful** - Standard patterns
---
## Files Modified
### Backend
- `src/control_server.py` - Added all REST endpoints
### Documentation
- `FRONTEND_API.md` - Complete API documentation (new)
- `REST_API_COMPLETE.md` - This file (new)
### Testing
- `test_rest_api.sh` - Automated test script (new)
---
## Test Script
Run comprehensive tests:
```bash
./test_rest_api.sh localhost
./test_rest_api.sh 10.42.0.1 # Remote testing
```
---
## API Endpoint Summary
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/state` | Get complete system state |
| GET | `/api/pattern` | Get current pattern |
| POST | `/api/pattern` | Change pattern |
| GET | `/api/parameters` | Get all parameters |
| POST | `/api/parameters` | Update parameters |
| GET | `/api/color-palette` | Get color palette |
| POST | `/api/color-palette` | Update palette/selection |
| POST | `/api/tempo/reset` | Reset tempo detection |
---
## Available Patterns
Use these pattern names in POST requests:
- `"on"` / `"o"` - Solid color
- `"off"` - All LEDs off
- `"alternating"` / `"a"` - Alternating on/off
- `"rainbow"` / `"r"` - Rainbow cycle
- `"pulse"` / `"p"` - Pulsing effect
- `"segmented_movement"` / `"sm"` - Moving segments
- `"flicker"` / `"f"` - Flickering
- `"n_chase"` / `"nc"` - Chase effect
- `"radiate"` / `"rd"` - Radiate from center
- `"specto"` / `"s"` - Spectograph
---
## Status
🟢 **PRODUCTION READY**
The REST API is fully implemented, tested, and documented. Frontend developers can now build UIs using only HTTP requests - no WebSocket needed.
---
## Next Steps for Frontend
1. Read `FRONTEND_API.md` for complete documentation
2. Use `/api/state` to load initial UI state
3. Use POST endpoints to control lights
4. Implement UI with any framework (React, Vue, vanilla JS)
5. No WebSocket connection needed!
---
## Quick Start for Frontend
```html
<!DOCTYPE html>
<html>
<head><title>Lighting Control</title></head>
<body>
<button onclick="setPattern('rainbow')">Rainbow</button>
<button onclick="setPattern('alternating')">Alternating</button>
<input type="range" min="0" max="100" onchange="setBrightness(this.value)">
<script>
const API = 'http://10.42.0.1:8765';
async function setPattern(pattern) {
await fetch(`${API}/api/pattern`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern})
});
}
async function setBrightness(value) {
await fetch(`${API}/api/parameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({brightness: parseInt(value)})
});
}
</script>
</body>
</html>
```
That's it! No WebSocket, no complex setup.
---
## Support
- Full documentation: `FRONTEND_API.md`
- Test script: `./test_rest_api.sh`
- Color palette details: `COLOR_PALETTE_API.md`

View File

@@ -72,8 +72,7 @@ GET /api/color-palette HTTP/1.1
- Index corresponds to palette slot (0-7) - Index corresponds to palette slot (0-7)
- **`selected_indices`**: Array of exactly 2 integers (0-7) - **`selected_indices`**: Array of exactly 2 integers (0-7)
- Indicates which 2 palette slots are currently active - Indicates which 2 palette slots are currently active
- **The first selected color (index 0) is used as the primary RGB color for patterns** - Patterns may use these selected colors
- The second selected color (index 1) is available for future pattern features
--- ---
@@ -364,7 +363,6 @@ HTTP_API_PORT=8766
**8 color slots**, each with RGB values (0-255) **8 color slots**, each with RGB values (0-255)
**2 selected colors** (indices 0-7) **2 selected colors** (indices 0-7)
**First selected color is used as the primary RGB for patterns**
**Auto-persistence** to `lighting_config.json` **Auto-persistence** to `lighting_config.json`
**Optional updates** - send only what changed **Optional updates** - send only what changed
**Two ports available** - 8765 (primary) and 8766 (backup) **Two ports available** - 8765 (primary) and 8766 (backup)

View File

@@ -1,155 +0,0 @@
{
"color_palette": [
{
"r": 255,
"g": 0,
"b": 255
},
{
"r": 255,
"g": 255,
"b": 0
},
{
"r": 0,
"g": 0,
"b": 255
},
{
"r": 255,
"g": 255,
"b": 0
},
{
"r": 255,
"g": 0,
"b": 255
},
{
"r": 255,
"g": 255,
"b": 0
},
{
"r": 255,
"g": 0,
"b": 255
},
{
"r": 108,
"g": 255,
"b": 255
}
],
"selected_color_indices": [
4,
3
],
"pattern_parameters": {
"alternating": {
"delay": 327,
"n1": 226,
"n2": 60,
"n3": 1,
"n4": 1
},
"segmented_movement": {
"delay": 100,
"n1": 6,
"n2": 28,
"n3": 6,
"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": 1,
"n1": 43,
"n2": 11,
"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": 33,
"n2": 35,
"n3": 1,
"n4": 1
},
"ap": {
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 1,
"n4": 1
},
"alternating_pulse": {
"delay": 100,
"n1": 90,
"n2": 78,
"n3": 1,
"n4": 1
},
"pulse": {
"delay": 100,
"n1": 10,
"n2": 10,
"n3": 1,
"n4": 1
}
}
}

View File

@@ -6,38 +6,27 @@ Receives commands from UI client via WebSocket.
""" """
import asyncio import asyncio
import websockets
import json import json
import logging import logging
import socket import socket
import threading import threading
import time import time
import argparse import argparse
import os
from aiohttp import web
from dotenv import load_dotenv
from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS
from color_utils import adjust_brightness from color_utils import adjust_brightness
from networking import SPIClient, WebSocketClient from networking import SPIClient, WebSocketClient
# Load environment variables from .env file
load_dotenv()
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Configuration # Configuration
CONTROL_SERVER_PORT = int(os.getenv("CONTROL_SERVER_PORT", "8765")) CONTROL_SERVER_PORT = 8765
HTTP_API_PORT = int(os.getenv("HTTP_API_PORT", "8766")) SOUND_CONTROL_HOST = "127.0.0.1"
SOUND_CONTROL_HOST = os.getenv("SOUND_CONTROL_HOST", "127.0.0.1") SOUND_CONTROL_PORT = 65433
SOUND_CONTROL_PORT = int(os.getenv("SOUND_CONTROL_PORT", "65433"))
CONFIG_FILE = "lighting_config.json"
# Pattern name mapping for shorter JSON payloads # Pattern name mapping for shorter JSON payloads
# Frontend sends shortnames, backend can use either long or short names
# These map to the shortnames defined in led-bar/src/patterns.py
PATTERN_NAMES = { PATTERN_NAMES = {
# Long names to short names (for backend use)
"off": "o",
"flicker": "f", "flicker": "f",
"fill_range": "fr", "fill_range": "fr",
"n_chase": "nc", "n_chase": "nc",
@@ -46,23 +35,9 @@ PATTERN_NAMES = {
"rainbow": "r", "rainbow": "r",
"specto": "s", "specto": "s",
"radiate": "rd", "radiate": "rd",
"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)
"o": "o",
"f": "f",
"fr": "fr",
"nc": "nc",
"a": "a",
"p": "p",
"r": "r",
"s": "s",
"rd": "rd",
"sm": "sm",
# Backend-specific patterns
"sequential_pulse": "sp", "sequential_pulse": "sp",
"alternating_phase": "ap", "alternating_phase": "ap",
"segmented_movement": "sm",
} }
@@ -126,170 +101,29 @@ 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)
self.color_palette = [
{"r": 255, "g": 0, "b": 0}, # Red
{"r": 0, "g": 255, "b": 0}, # Green
{"r": 0, "g": 0, "b": 255}, # Blue
{"r": 255, "g": 255, "b": 0}, # Yellow
{"r": 255, "g": 0, "b": 255}, # Magenta
{"r": 0, "g": 255, "b": 255}, # Cyan
{"r": 255, "g": 128, "b": 0}, # Orange
{"r": 255, "g": 255, "b": 255}, # White
]
self.selected_color_indices = [0, 1] # Default: Red and Green
# Load config
self._load_config()
# Rate limiting # Rate limiting
self.last_param_update = 0.0 self.last_param_update = 0.0
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."""
# Use the first selected color from the palette
if self.selected_color_indices and len(self.selected_color_indices) > 0:
color_index = self.selected_color_indices[0]
if 0 <= color_index < len(self.color_palette):
color = self.color_palette[color_index]
return (color['r'], color['g'], color['b'])
# Fallback to legacy color sliders if palette not set
r = max(0, min(255, int(self.color_r))) r = max(0, min(255, int(self.color_r)))
g = max(0, min(255, int(self.color_g))) g = max(0, min(255, int(self.color_g)))
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):
"""Load configuration from file."""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
# Load color palette
if "color_palette" in config:
self.color_palette = config["color_palette"]
# Load selected color indices
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}")
def _save_config(self):
"""Save configuration to file."""
try:
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)
logging.info(f"Saved config to {CONFIG_FILE}")
except Exception as e:
logging.error(f"Error saving config: {e}")
def get_color_palette_config(self):
"""Get current color palette configuration."""
return {
"palette": self.color_palette,
"selected_indices": self.selected_color_indices
}
def set_color_palette(self, palette_data):
"""Set color palette configuration."""
if "palette" in palette_data:
# Validate palette has 8 colors
if len(palette_data["palette"]) == 8:
self.color_palette = palette_data["palette"]
else:
logging.warning(f"Invalid palette size: {len(palette_data['palette'])}, expected 8")
if "selected_indices" in palette_data:
# Validate indices
indices = palette_data["selected_indices"]
if len(indices) == 2 and all(0 <= i < 8 for i in indices):
self.selected_color_indices = indices
else:
logging.warning(f"Invalid selected indices: {indices}")
self._save_config()
logging.info(f"Color palette updated: selected indices {self.selected_color_indices}")
async def _send_full_parameters(self): async def _send_full_parameters(self):
"""Send all parameters to LED bars.""" """Send all parameters to LED bars."""
@@ -326,21 +160,12 @@ 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 that need parameters (both long and short names) patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate", "segmented_movement"]
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
} }
} }
@@ -381,81 +206,23 @@ 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,
# Default color for non-swapped bars changes with phase "s": self.beat_index % 2,
"cl": [default_color],
"s": phase,
} }
} }
# Bars in this list will have inverted phase and explicit color override swap_bars = ["101", "103", "105", "107"]
# Flip grouping so first four bars (100-103) use default color (color_a on even beats) for bar_name in LED_BAR_NAMES:
swap_bars = ["104", "105", "106", "107"] if bar_name in swap_bars:
# Only include overrides for swapped bars to minimize payload size payload[bar_name] = {"s": (self.beat_index + 1) % 2}
for bar_name in swap_bars: else:
inv_phase = (phase + 1) % 2 payload[bar_name] = {}
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:
# 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)
async def handle_beat(self, bpm_value): async def handle_beat(self, bpm_value):
@@ -465,6 +232,10 @@ class LightingController:
self.beat_index = (self.beat_index + 1) % 1000000 self.beat_index = (self.beat_index + 1) % 1000000
# Send periodic parameter updates every 8 beats
if self.beat_index % 8 == 0:
await self._send_full_parameters()
# Check for pending parameter updates # Check for pending parameter updates
if self.pending_param_update: if self.pending_param_update:
current_time = time.time() current_time = time.time()
@@ -478,22 +249,13 @@ 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}")
@@ -516,20 +278,10 @@ 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":
@@ -538,15 +290,6 @@ class LightingController:
elif message_type == "reset_tempo": elif message_type == "reset_tempo":
await self.sound_controller.send_reset_tempo() await self.sound_controller.send_reset_tempo()
elif message_type == "get_color_palette":
# Return color palette configuration
return self.get_color_palette_config()
elif message_type == "set_color_palette":
# Set color palette configuration
self.set_color_palette(data)
return {"status": "ok", "palette": self.get_color_palette_config()}
class ControlServer: class ControlServer:
@@ -556,51 +299,34 @@ class ControlServer:
self.lighting_controller = LightingController(transport=transport, **transport_kwargs) self.lighting_controller = LightingController(transport=transport, **transport_kwargs)
self.clients = set() self.clients = set()
self.tcp_server = None self.tcp_server = None
self.http_app = None
self.http_runner = None
self.enable_heartbeat = enable_heartbeat self.enable_heartbeat = enable_heartbeat
async def handle_websocket(self, request): async def handle_ui_client(self, websocket):
"""Handle WebSocket connection for UI client.""" """Handle UI client WebSocket connection."""
ws = web.WebSocketResponse() self.clients.add(websocket)
await ws.prepare(request) client_addr = websocket.remote_address
self.clients.add(ws)
client_addr = request.remote
logging.info(f"UI client connected: {client_addr}") logging.info(f"UI client connected: {client_addr}")
try: try:
async for msg in ws: async for message in websocket:
if msg.type == web.WSMsgType.TEXT: try:
try: data = json.loads(message)
data = json.loads(msg.data) message_type = data.get("type")
message_type = data.get("type") message_data = data.get("data", {})
message_data = data.get("data", {})
response = await self.lighting_controller.handle_ui_command(message_type, message_data)
# Send response if command returned data
if response is not None:
await ws.send_json({
"type": f"{message_type}_response",
"data": response
})
except json.JSONDecodeError:
logging.error(f"Invalid JSON from client {client_addr}: {msg.data}")
except Exception as e:
logging.error(f"Error handling message from client {client_addr}: {e}")
elif msg.type == web.WSMsgType.ERROR:
logging.error(f"WebSocket error from {client_addr}: {ws.exception()}")
except Exception as e: await self.lighting_controller.handle_ui_command(message_type, message_data)
logging.error(f"Error in WebSocket handler: {e}")
finally: except json.JSONDecodeError:
self.clients.discard(ws) logging.error(f"Invalid JSON from client {client_addr}: {message}")
except Exception as e:
logging.error(f"Error handling message from client {client_addr}: {e}")
except websockets.exceptions.ConnectionClosed:
logging.info(f"UI client disconnected: {client_addr}") logging.info(f"UI client disconnected: {client_addr}")
except Exception as e:
return ws logging.error(f"Error in UI client handler: {e}")
finally:
self.clients.discard(websocket)
async def handle_tcp_client(self, reader, writer): async def handle_tcp_client(self, reader, writer):
"""Handle TCP client (sound detector) connection.""" """Handle TCP client (sound detector) connection."""
@@ -643,216 +369,12 @@ class ControlServer:
addrs = ', '.join(str(sock.getsockname()) for sock in self.tcp_server.sockets) addrs = ', '.join(str(sock.getsockname()) for sock in self.tcp_server.sockets)
logging.info(f"TCP server listening on {addrs}") logging.info(f"TCP server listening on {addrs}")
# HTTP API Handlers async def start_websocket_server(self):
"""Start WebSocket server for UI clients."""
# Color Palette API server = await websockets.serve(
async def http_get_color_palette(self, request): self.handle_ui_client, "localhost", CONTROL_SERVER_PORT
"""HTTP GET /api/color-palette""" )
palette_config = self.lighting_controller.get_color_palette_config() logging.info(f"WebSocket server listening on localhost:{CONTROL_SERVER_PORT}")
return web.json_response(palette_config)
async def http_set_color_palette(self, request):
"""HTTP POST/PUT /api/color-palette"""
try:
data = await request.json()
self.lighting_controller.set_color_palette(data)
palette_config = self.lighting_controller.get_color_palette_config()
return web.json_response({
"status": "ok",
"palette": palette_config
})
except json.JSONDecodeError:
return web.json_response(
{"status": "error", "message": "Invalid JSON"},
status=400
)
except Exception as e:
return web.json_response(
{"status": "error", "message": str(e)},
status=500
)
# Pattern API
async def http_set_pattern(self, request):
"""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(
{"status": "error", "message": "Pattern name required"},
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._load_pattern_parameters(pattern)
await self.lighting_controller._send_full_parameters()
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,
"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(
{"status": "error", "message": str(e)},
status=500
)
async def http_get_pattern(self, request):
"""HTTP GET /api/pattern"""
return web.json_response({
"pattern": self.lighting_controller.current_pattern
})
# Parameters API
async def http_set_parameters(self, request):
"""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:
self.lighting_controller.brightness = int(data["brightness"])
if "delay" in data:
self.lighting_controller.delay = int(data["delay"])
if "n1" in data:
self.lighting_controller.n1 = int(data["n1"])
if "n2" in data:
self.lighting_controller.n2 = int(data["n2"])
if "n3" in data:
self.lighting_controller.n3 = int(data["n3"])
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()
return web.json_response({
"status": "ok",
"parameters": {
"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
}
})
except Exception as e:
return web.json_response(
{"status": "error", "message": str(e)},
status=500
)
async def http_get_parameters(self, request):
"""HTTP GET /api/parameters"""
return web.json_response({
"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
})
# State API
async def http_get_state(self, request):
"""HTTP GET /api/state - Get complete system state"""
palette_config = self.lighting_controller.get_color_palette_config()
return web.json_response({
"pattern": self.lighting_controller.current_pattern,
"parameters": {
"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
},
"color_palette": palette_config,
"beat_index": self.lighting_controller.beat_index
})
# Tempo API
async def http_reset_tempo(self, request):
"""HTTP POST /api/tempo/reset"""
try:
await self.lighting_controller.sound_controller.send_reset_tempo()
return web.json_response({"status": "ok", "message": "Tempo reset sent"})
except Exception as e:
return web.json_response(
{"status": "error", "message": str(e)},
status=500
)
async def start_http_server(self):
"""Start combined HTTP and WebSocket server."""
self.http_app = web.Application()
# WebSocket endpoint (legacy support)
self.http_app.router.add_get('/ws', self.handle_websocket)
# REST API endpoints
# Color Palette
self.http_app.router.add_get('/api/color-palette', self.http_get_color_palette)
self.http_app.router.add_post('/api/color-palette', self.http_set_color_palette)
self.http_app.router.add_put('/api/color-palette', self.http_set_color_palette)
# Pattern
self.http_app.router.add_get('/api/pattern', self.http_get_pattern)
self.http_app.router.add_post('/api/pattern', self.http_set_pattern)
# Parameters
self.http_app.router.add_get('/api/parameters', self.http_get_parameters)
self.http_app.router.add_post('/api/parameters', self.http_set_parameters)
# State (complete system state)
self.http_app.router.add_get('/api/state', self.http_get_state)
# Tempo
self.http_app.router.add_post('/api/tempo/reset', self.http_reset_tempo)
self.http_runner = web.AppRunner(self.http_app)
await self.http_runner.setup()
host = os.getenv("CONTROL_SERVER_HOST", "0.0.0.0")
# Start WebSocket server on CONTROL_SERVER_PORT
ws_site = web.TCPSite(self.http_runner, host, CONTROL_SERVER_PORT)
await ws_site.start()
logging.info(f"WebSocket server listening on {host}:{CONTROL_SERVER_PORT}/ws")
logging.info(f"HTTP API server listening on {host}:{CONTROL_SERVER_PORT}")
# Also start on HTTP_API_PORT for backward compatibility
if HTTP_API_PORT != CONTROL_SERVER_PORT:
api_site = web.TCPSite(self.http_runner, host, HTTP_API_PORT)
await api_site.start()
logging.info(f"HTTP API also available on {host}:{HTTP_API_PORT}")
async def run(self): async def run(self):
"""Run the control server.""" """Run the control server."""
@@ -860,26 +382,26 @@ class ControlServer:
await self.lighting_controller.led_controller.connect() await self.lighting_controller.led_controller.connect()
# Start servers (optionally include heartbeat) # Start servers (optionally include heartbeat)
websocket_task = asyncio.create_task(self._websocket_server_task())
tcp_task = asyncio.create_task(self._tcp_server_task()) tcp_task = asyncio.create_task(self._tcp_server_task())
http_task = asyncio.create_task(self._http_server_task()) # Handles both WebSocket and HTTP
tasks = [tcp_task, http_task] tasks = [websocket_task, tcp_task]
if self.enable_heartbeat: if self.enable_heartbeat:
heartbeat_task = asyncio.create_task(self._heartbeat_loop()) heartbeat_task = asyncio.create_task(self._heartbeat_loop())
tasks.append(heartbeat_task) tasks.append(heartbeat_task)
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
async def _tcp_server_task(self): async def _websocket_server_task(self):
"""Keep TCP server running.""" """Keep WebSocket server running."""
await self.start_tcp_server() await self.start_websocket_server()
# Keep the server running indefinitely # Keep the server running indefinitely
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
async def _http_server_task(self): async def _tcp_server_task(self):
"""Keep HTTP and WebSocket server running.""" """Keep TCP server running."""
await self.start_http_server() await self.start_tcp_server()
# Keep the server running indefinitely # Keep the server running indefinitely
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
@@ -941,12 +463,11 @@ def parse_arguments():
# Transport selection # Transport selection
transport_group = parser.add_argument_group("Transport Options") transport_group = parser.add_argument_group("Transport Options")
default_transport = os.getenv("TRANSPORT", "spi")
transport_group.add_argument( transport_group.add_argument(
"--transport", "--transport",
choices=["spi", "websocket"], choices=["spi", "websocket"],
default=default_transport, default="spi",
help=f"Transport method for LED communication (default from .env or {default_transport})" help="Transport method for LED communication (default: spi)"
) )
# Control options # Control options

View File

@@ -11,23 +11,18 @@ import time
import logging # Added logging import import logging # Added logging import
import asyncio # Re-added asyncio import import asyncio # Re-added asyncio import
import threading # Added threading for control server import threading # Added threading for control server
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Configure logging # Configure logging
DEBUG_MODE = True # Set to False for INFO level logging DEBUG_MODE = True # Set to False for INFO level logging
logging.basicConfig(level=logging.DEBUG if DEBUG_MODE else logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.DEBUG if DEBUG_MODE else logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# TCP Server Configuration (assuming midi.py runs this) # TCP Server Configuration (assuming midi.py runs this)
MIDI_TCP_HOST = os.getenv("MIDI_TCP_HOST", "127.0.0.1") MIDI_TCP_HOST = "127.0.0.1"
MIDI_TCP_PORT = int(os.getenv("MIDI_TCP_PORT", "65432")) MIDI_TCP_PORT = 65432
# Sound Control Server Configuration (for midi.py to control sound.py) # Sound Control Server Configuration (for midi.py to control sound.py)
SOUND_CONTROL_HOST = os.getenv("SOUND_CONTROL_HOST", "127.0.0.1") SOUND_CONTROL_HOST = "127.0.0.1"
SOUND_CONTROL_PORT = int(os.getenv("SOUND_CONTROL_PORT", "65433")) SOUND_CONTROL_PORT = 65433
class SoundBeatDetector: class SoundBeatDetector:
def __init__(self, tcp_host: str, tcp_port: int, *, input_device: int | None = None): def __init__(self, tcp_host: str, tcp_port: int, *, input_device: int | None = None):
@@ -40,8 +35,7 @@ class SoundBeatDetector:
self.bufferSize = 512 self.bufferSize = 512
self.windowSizeMultiple = 2 self.windowSizeMultiple = 2
default_device = int(os.getenv("AUDIO_INPUT_DEVICE", "7")) self.audioInputDeviceIndex = 7 if input_device is None else int(input_device)
self.audioInputDeviceIndex = default_device if input_device is None else int(input_device)
self.audioInputChannels = 1 self.audioInputChannels = 1
self.pa = pyaudio.PyAudio() self.pa = pyaudio.PyAudio()
@@ -66,7 +60,7 @@ class SoundBeatDetector:
except Exception as e: except Exception as e:
logging.error(f"Error getting audio device info for index {self.audioInputDeviceIndex}: {e}") logging.error(f"Error getting audio device info for index {self.audioInputDeviceIndex}: {e}")
self.pa.terminate() self.pa.terminate()
raise RuntimeError(f"Audio device {self.audioInputDeviceIndex} not available: {e}") exit()
self.hopSize = self.bufferSize self.hopSize = self.bufferSize
self.winSize = self.hopSize * self.windowSizeMultiple self.winSize = self.hopSize * self.windowSizeMultiple
@@ -211,26 +205,11 @@ if __name__ == "__main__":
MIDI_TCP_HOST = "127.0.0.1" MIDI_TCP_HOST = "127.0.0.1"
MIDI_TCP_PORT = 65432 MIDI_TCP_PORT = 65432
sound_detector = SoundBeatDetector(MIDI_TCP_HOST, MIDI_TCP_PORT, input_device=args.input_device)
logging.info("Starting SoundBeatDetector...") logging.info("Starting SoundBeatDetector...")
while True: try:
try: sound_detector.start_stream()
sound_detector = SoundBeatDetector(MIDI_TCP_HOST, MIDI_TCP_PORT, input_device=args.input_device) except KeyboardInterrupt:
sound_detector.start_stream() logging.info("\nProgram interrupted by user.")
break # If we get here, the stream ended normally except Exception as e:
except KeyboardInterrupt: logging.error(f"An error occurred during main execution: {e}")
logging.info("\nProgram interrupted by user.")
break
except RuntimeError as e:
if "Audio device" in str(e):
logging.error(f"Audio device error: {e}")
logging.info("Waiting 10 seconds before retrying...")
time.sleep(10)
continue
else:
logging.error(f"Runtime error: {e}")
break
except Exception as e:
logging.error(f"An error occurred during main execution: {e}")
logging.info("Waiting 5 seconds before retrying...")
time.sleep(5)
continue

File diff suppressed because it is too large Load Diff

View File

@@ -19,10 +19,26 @@ import json
import websockets import websockets
import re import re
import os import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv() def load_dotenv(filepath: str = ".env"):
try:
if not os.path.exists(filepath):
return
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' not in line:
continue
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
except Exception:
pass
def build_messages(args): def build_messages(args):
@@ -81,9 +97,11 @@ async def run_test(uri: str, messages: list[dict], sleep_s: float):
def parse_args(): def parse_args():
p = argparse.ArgumentParser(description="Send UI commands to control_server WebSocket") p = argparse.ArgumentParser(description="Send UI commands to control_server WebSocket")
default_uri = os.getenv("CONTROL_SERVER_URI", "ws://localhost:8765/ws") load_dotenv()
p.add_argument("--uri", default=default_uri, help=f"WebSocket URI (default from .env or {default_uri})") default_uri = os.getenv("CONTROL_SERVER_URI", "ws://10.1.1.117:8765")
p.add_argument("--uri", default=default_uri, help=f"WebSocket URI (default {default_uri})")
p.add_argument("--pattern", help="Pattern name for pattern_change") p.add_argument("--pattern", help="Pattern name for pattern_change")
p.add_argument("--r", type=int, help="Red 0-255 for color_change") p.add_argument("--r", type=int, help="Red 0-255 for color_change")
p.add_argument("--g", type=int, help="Green 0-255 for color_change") p.add_argument("--g", type=int, help="Green 0-255 for color_change")

View File

@@ -1,75 +0,0 @@
#!/bin/bash
# Color Palette API Test Script
# Usage: ./test_color_api.sh [server_ip]
SERVER=${1:-localhost}
PORT=8765
API_URL="http://${SERVER}:${PORT}/api/color-palette"
echo "Testing Color Palette API at ${API_URL}"
echo "=========================================="
echo ""
# Test 1: GET current palette
echo "Test 1: GET current palette"
echo "----------------------------"
curl -s "${API_URL}" | python3 -m json.tool
echo ""
echo ""
# Test 2: Update selected colors to slots 0 and 2 (Red and Blue)
echo "Test 2: Update selected colors to [0, 2] (Red and Blue)"
echo "--------------------------------------------------------"
curl -s -X POST "${API_URL}" \
-H "Content-Type: application/json" \
-d '{"selected_indices": [0, 2]}' | python3 -m json.tool
echo ""
echo ""
# Test 3: Update slot 3 to purple (128, 0, 128)
echo "Test 3: Update slot 3 to purple (128, 0, 128)"
echo "----------------------------------------------"
curl -s -X POST "${API_URL}" \
-H "Content-Type: application/json" \
-d '{"palette": [
{"r": 255, "g": 0, "b": 0},
{"r": 0, "g": 255, "b": 0},
{"r": 0, "g": 0, "b": 255},
{"r": 128, "g": 0, "b": 128},
{"r": 255, "g": 179, "b": 255},
{"r": 0, "g": 255, "b": 255},
{"r": 255, "g": 255, "b": 255},
{"r": 128, "g": 128, "b": 128}
]}' | python3 -m json.tool
echo ""
echo ""
# Test 4: Select the new purple color (slots 3 and 5)
echo "Test 4: Select slots 3 and 5 (Purple and Cyan)"
echo "-----------------------------------------------"
curl -s -X POST "${API_URL}" \
-H "Content-Type: application/json" \
-d '{"selected_indices": [3, 5]}' | python3 -m json.tool
echo ""
echo ""
# Test 5: GET final state
echo "Test 5: GET final state"
echo "-----------------------"
curl -s "${API_URL}" | python3 -m json.tool
echo ""
echo ""
# Test 6: Test backup port (8766)
echo "Test 6: Test backup port 8766"
echo "------------------------------"
curl -s "http://${SERVER}:8766/api/color-palette" | python3 -m json.tool | head -15
echo "..."
echo ""
echo "=========================================="
echo "All tests completed!"
echo ""
echo "Config file location: lighting_config.json"
echo "To view: cat lighting_config.json | python3 -m json.tool"

View File

@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""
Complete test: Change palette selection and verify pattern color updates
"""
import asyncio
import aiohttp
import json
async def test_color_pattern_integration():
"""Test that changing selected color updates pattern color"""
print("=" * 60)
print("Color Palette Pattern Integration Test")
print("=" * 60)
# Step 1: Get current palette
print("\n1⃣ Getting current palette...")
session = aiohttp.ClientSession()
async with session.get('http://localhost:8765/api/color-palette') as response:
data = await response.json()
print(f" Current palette has {len(data['palette'])} colors")
print(f" Selected indices: {data['selected_indices']}")
current_index = data['selected_indices'][0]
current_color = data['palette'][current_index]
print(f" Current color (slot {current_index}): RGB({current_color['r']}, {current_color['g']}, {current_color['b']})")
# Step 2: Connect to WebSocket and send a pattern
print("\n2⃣ Connecting to WebSocket and activating pattern...")
try:
async with session.ws_connect('http://localhost:8765/ws') as ws:
print(" ✅ Connected to WebSocket")
# Send 'on' pattern
await ws.send_json({
"type": "pattern_change",
"data": {"pattern": "on"}
})
print(f" 📤 Sent pattern: 'on'")
print(f" 🎨 LED bar should show: RGB({current_color['r']}, {current_color['g']}, {current_color['b']})")
await asyncio.sleep(2)
# Step 3: Change to RED (slot 0)
print("\n3⃣ Changing selected color to RED (slot 0)...")
async with session.post(
'http://localhost:8765/api/color-palette',
json={"selected_indices": [0, 1]}
) as response:
if response.status == 200:
print(" ✅ Color changed to slot 0")
red_color = data['palette'][0]
print(f" 🎨 LED bar should now show: RGB({red_color['r']}, {red_color['g']}, {red_color['b']})")
await asyncio.sleep(2)
# Step 4: Change to BLUE (slot 2)
print("\n4⃣ Changing selected color to BLUE (slot 2)...")
async with session.post(
'http://localhost:8765/api/color-palette',
json={"selected_indices": [2, 1]}
) as response:
if response.status == 200:
print(" ✅ Color changed to slot 2")
blue_color = data['palette'][2]
print(f" 🎨 LED bar should now show: RGB({blue_color['r']}, {blue_color['g']}, {blue_color['b']})")
await asyncio.sleep(2)
# Step 5: Test alternating pattern
print("\n5⃣ Testing alternating pattern...")
await ws.send_json({
"type": "pattern_change",
"data": {"pattern": "alternating"}
})
print(" 📤 Sent pattern: 'alternating'")
print(" 🎨 Pattern should alternate with blue color")
await asyncio.sleep(3)
# Step 6: Restore original selection
print(f"\n6⃣ Restoring original selection (slot {current_index})...")
async with session.post(
'http://localhost:8765/api/color-palette',
json={"selected_indices": data['selected_indices']}
) as response:
if response.status == 200:
print(f" ✅ Restored to slot {current_index}")
except Exception as e:
print(f" ❌ Error: {e}")
finally:
await session.close()
print("\n" + "=" * 60)
print("✅ Test Complete!")
print("=" * 60)
print("\n💡 What to verify:")
print(" - LED bar changed color when palette selection changed")
print(" - Colors matched: Red → Blue → back to original")
print(" - Pattern continued running with new colors")
if __name__ == "__main__":
asyncio.run(test_color_pattern_integration())

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify that selected color is used for patterns
"""
import json
import os
import sys
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from control_server import LightingController
def test_color_selection():
"""Test that the first selected color is used as RGB"""
# Create controller (without connecting)
controller = LightingController(transport="spi")
# Load config
controller._load_config()
print("Color Palette Selection Test")
print("=" * 50)
print(f"\nColor Palette:")
for i, color in enumerate(controller.color_palette):
r, g, b = color['r'], color['g'], color['b']
print(f" Slot {i}: RGB({r:3d}, {g:3d}, {b:3d})")
print(f"\nSelected Indices: {controller.selected_color_indices}")
if controller.selected_color_indices:
first_index = controller.selected_color_indices[0]
print(f"First Selected: Slot {first_index}")
selected_color = controller.color_palette[first_index]
print(f" RGB({selected_color['r']}, {selected_color['g']}, {selected_color['b']})")
# Test the _current_color_rgb method
rgb = controller._current_color_rgb()
print(f"\nPattern RGB (from _current_color_rgb): RGB{rgb}")
# Verify it matches the first selected color
if controller.selected_color_indices:
first_index = controller.selected_color_indices[0]
expected_color = controller.color_palette[first_index]
expected_rgb = (expected_color['r'], expected_color['g'], expected_color['b'])
if rgb == expected_rgb:
print("\n✅ SUCCESS: Pattern RGB matches first selected color!")
else:
print(f"\n❌ FAIL: Expected {expected_rgb}, got {rgb}")
print("=" * 50)
if __name__ == "__main__":
test_color_selection()

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify patterns are using the selected palette color
"""
import asyncio
import aiohttp
import json
async def test_pattern_with_color():
"""Test sending a pattern and verifying the color"""
# Connect to WebSocket
session = aiohttp.ClientSession()
try:
async with session.ws_connect('http://localhost:8765/ws') as ws:
print("✅ Connected to WebSocket at ws://localhost:8765/ws")
# Send pattern change to 'on'
msg = {
"type": "pattern_change",
"data": {"pattern": "on"}
}
print(f"\n📤 Sending: {json.dumps(msg, indent=2)}")
await ws.send_json(msg)
# Wait a moment
await asyncio.sleep(1)
# Send alternating pattern
msg = {
"type": "pattern_change",
"data": {"pattern": "alternating"}
}
print(f"\n📤 Sending: {json.dumps(msg, indent=2)}")
await ws.send_json(msg)
await asyncio.sleep(2)
print("\n✅ Patterns sent successfully!")
print("Check the LED bar to see if it's using purple RGB(128, 0, 128)")
except Exception as e:
print(f"❌ Error: {e}")
finally:
await session.close()
if __name__ == "__main__":
asyncio.run(test_pattern_with_color())

View File

@@ -1,70 +0,0 @@
#!/bin/bash
# Test all REST API endpoints
SERVER=${1:-localhost}
PORT=8765
BASE="http://${SERVER}:${PORT}"
echo "Testing Lighting Controller REST API"
echo "====================================="
echo ""
# Test 1: GET /api/state
echo "1. GET /api/state"
echo "-----------------"
curl -s "${BASE}/api/state" | python3 -m json.tool | head -20
echo ""
# Test 2: POST /api/pattern
echo "2. POST /api/pattern (set to 'alternating')"
echo "--------------------------------------------"
curl -s -X POST "${BASE}/api/pattern" \
-H "Content-Type: application/json" \
-d '{"pattern": "alternating"}' | python3 -m json.tool
echo ""
# Test 3: GET /api/pattern
echo "3. GET /api/pattern"
echo "-------------------"
curl -s "${BASE}/api/pattern" | python3 -m json.tool
echo ""
# Test 4: POST /api/parameters (brightness)
echo "4. POST /api/parameters (brightness=75)"
echo "----------------------------------------"
curl -s -X POST "${BASE}/api/parameters" \
-H "Content-Type: application/json" \
-d '{"brightness": 75}' | python3 -m json.tool
echo ""
# Test 5: GET /api/parameters
echo "5. GET /api/parameters"
echo "----------------------"
curl -s "${BASE}/api/parameters" | python3 -m json.tool
echo ""
# Test 6: POST /api/color-palette (select slot 2)
echo "6. POST /api/color-palette (select slot 2)"
echo "-------------------------------------------"
curl -s -X POST "${BASE}/api/color-palette" \
-H "Content-Type: application/json" \
-d '{"selected_indices": [2, 1]}' | python3 -m json.tool | head -10
echo ""
# Test 7: GET /api/color-palette
echo "7. GET /api/color-palette"
echo "-------------------------"
curl -s "${BASE}/api/color-palette" | python3 -m json.tool | head -15
echo ""
# Test 8: POST /api/pattern (rainbow)
echo "8. POST /api/pattern (set to 'rainbow')"
echo "----------------------------------------"
curl -s -X POST "${BASE}/api/pattern" \
-H "Content-Type: application/json" \
-d '{"pattern": "rainbow"}' | python3 -m json.tool
echo ""
echo "====================================="
echo "All tests complete!"