Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
9cf1855b51 | |||
763a2053ad | |||
324fa463be | |||
aaf515d8f4 | |||
7beca0cf53 | |||
ace47b7835 | |||
9045b10631 | |||
f2e775f6f5 | |||
a654527dc3 | |||
|
0906cb22e6 |
11
.env
11
.env
@@ -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
|
51
.env.example
51
.env.example
@@ -1,48 +1,9 @@
|
||||
# Lighting Controller Configuration
|
||||
# Lighting Controller UI Client Configuration
|
||||
|
||||
# ==============================
|
||||
# WebSocket Configuration
|
||||
# ==============================
|
||||
# WebSocket URI for the control server
|
||||
# Used by UI client and test scripts to connect to the control server
|
||||
#
|
||||
# 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
|
||||
# For local development (running UI on same machine as control server):
|
||||
CONTROL_SERVER_URI=ws://localhost:8765
|
||||
|
||||
# Control server WebSocket host (bind address)
|
||||
# 0.0.0.0 = listen on all interfaces (allows external connections)
|
||||
# localhost or 127.0.0.1 = listen only on localhost (local only)
|
||||
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
|
||||
# For remote connection (running UI on desktop, control server on Pi):
|
||||
# Replace with your Raspberry Pi's IP address
|
||||
# CONTROL_SERVER_URI=ws://10.1.1.117:8765
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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 ✅
|
||||
|
@@ -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!
|
||||
|
5
Pipfile
5
Pipfile
@@ -4,15 +4,12 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
aiohttp = "*"
|
||||
websockets = "*"
|
||||
spidev = "*"
|
||||
watchfiles = "*"
|
||||
async-tkinter-loop = "*"
|
||||
mido = "*"
|
||||
python-rtmidi = "*"
|
||||
pyaudio = "*"
|
||||
aubio = "*"
|
||||
|
||||
websocket-client = "*"
|
||||
python-dotenv = "*"
|
||||
|
||||
|
566
Pipfile.lock
generated
566
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "dd26ac6e21af21bc1d009aba24fc011d458a5e81129873206715309f12d58690"
|
||||
"sha256": "3f3ca9af45dd4382aac3c649ae11cefbe97059ad14f40172735213c4919baada"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,114 +16,6 @@
|
||||
]
|
||||
},
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc",
|
||||
@@ -140,14 +32,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.9.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3",
|
||||
"sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==25.3.0"
|
||||
},
|
||||
"aubio": {
|
||||
"hashes": [
|
||||
"sha256:df1244f6c4cf5bea382c8c2d35aa43bc31f4cf631fe325ae3992c219546a4202"
|
||||
@@ -155,116 +39,6 @@
|
||||
"index": "pypi",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
|
||||
@@ -281,122 +55,6 @@
|
||||
"index": "pypi",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b",
|
||||
@@ -485,110 +143,6 @@
|
||||
"markers": "python_version >= '3.8'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57",
|
||||
@@ -608,14 +162,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.2.14"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc",
|
||||
"sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"python-rtmidi": {
|
||||
"hashes": [
|
||||
"sha256:052c89933cae4fca354012d8ca7248f4f9e1e3f062471409d48415a7f7d7e59e",
|
||||
@@ -866,116 +412,6 @@
|
||||
],
|
||||
"index": "pypi",
|
||||
"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": {}
|
||||
|
@@ -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`
|
||||
|
@@ -72,8 +72,7 @@ GET /api/color-palette HTTP/1.1
|
||||
- Index corresponds to palette slot (0-7)
|
||||
- **`selected_indices`**: Array of exactly 2 integers (0-7)
|
||||
- Indicates which 2 palette slots are currently active
|
||||
- **The first selected color (index 0) is used as the primary RGB color for patterns**
|
||||
- The second selected color (index 1) is available for future pattern features
|
||||
- Patterns may use these selected colors
|
||||
|
||||
---
|
||||
|
||||
@@ -364,7 +363,6 @@ HTTP_API_PORT=8766
|
||||
|
||||
✅ **8 color slots**, each with RGB values (0-255)
|
||||
✅ **2 selected colors** (indices 0-7)
|
||||
✅ **First selected color is used as the primary RGB for patterns**
|
||||
✅ **Auto-persistence** to `lighting_config.json`
|
||||
✅ **Optional updates** - send only what changed
|
||||
✅ **Two ports available** - 8765 (primary) and 8766 (backup)
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,38 +6,27 @@ Receives commands from UI client via WebSocket.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import argparse
|
||||
import os
|
||||
from aiohttp import web
|
||||
from dotenv import load_dotenv
|
||||
from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS
|
||||
from color_utils import adjust_brightness
|
||||
from networking import SPIClient, WebSocketClient
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Configuration
|
||||
CONTROL_SERVER_PORT = int(os.getenv("CONTROL_SERVER_PORT", "8765"))
|
||||
HTTP_API_PORT = int(os.getenv("HTTP_API_PORT", "8766"))
|
||||
SOUND_CONTROL_HOST = os.getenv("SOUND_CONTROL_HOST", "127.0.0.1")
|
||||
SOUND_CONTROL_PORT = int(os.getenv("SOUND_CONTROL_PORT", "65433"))
|
||||
CONFIG_FILE = "lighting_config.json"
|
||||
CONTROL_SERVER_PORT = 8765
|
||||
SOUND_CONTROL_HOST = "127.0.0.1"
|
||||
SOUND_CONTROL_PORT = 65433
|
||||
|
||||
# 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 = {
|
||||
# Long names to short names (for backend use)
|
||||
"off": "o",
|
||||
"flicker": "f",
|
||||
"fill_range": "fr",
|
||||
"n_chase": "nc",
|
||||
@@ -46,23 +35,9 @@ PATTERN_NAMES = {
|
||||
"rainbow": "r",
|
||||
"specto": "s",
|
||||
"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",
|
||||
"alternating_phase": "ap",
|
||||
"segmented_movement": "sm",
|
||||
}
|
||||
|
||||
|
||||
@@ -126,171 +101,30 @@ class LightingController:
|
||||
|
||||
# Lighting state
|
||||
self.current_pattern = ""
|
||||
self.delay = 100
|
||||
self.brightness = 100
|
||||
self.color_r = 0
|
||||
self.color_g = 255
|
||||
self.color_b = 0
|
||||
self.n1 = 10
|
||||
self.n2 = 10
|
||||
self.n3 = 1
|
||||
self.n4 = 1
|
||||
self.beat_index = 0
|
||||
self.beat_sending_enabled = True
|
||||
|
||||
# Per-pattern parameters (pattern_name -> {delay, n1, n2, n3, n4})
|
||||
self.pattern_parameters = {}
|
||||
|
||||
# Default parameters for new patterns
|
||||
self.default_params = {
|
||||
"delay": 100,
|
||||
"n1": 10,
|
||||
"n2": 10,
|
||||
"n3": 1,
|
||||
"n4": 1
|
||||
}
|
||||
|
||||
# Current active parameters (loaded from current pattern)
|
||||
self.delay = self.default_params["delay"]
|
||||
self.n1 = self.default_params["n1"]
|
||||
self.n2 = self.default_params["n2"]
|
||||
self.n3 = self.default_params["n3"]
|
||||
self.n4 = self.default_params["n4"]
|
||||
|
||||
# Color palette (8 slots, 2 selected)
|
||||
self.color_palette = [
|
||||
{"r": 255, "g": 0, "b": 0}, # Red
|
||||
{"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
|
||||
self.last_param_update = 0.0
|
||||
self.param_update_interval = 0.1
|
||||
self.pending_param_update = False
|
||||
|
||||
def _load_pattern_parameters(self, pattern_name):
|
||||
"""Load parameters for a specific pattern."""
|
||||
if pattern_name in self.pattern_parameters:
|
||||
params = self.pattern_parameters[pattern_name]
|
||||
self.delay = params.get("delay", self.default_params["delay"])
|
||||
self.n1 = params.get("n1", self.default_params["n1"])
|
||||
self.n2 = params.get("n2", self.default_params["n2"])
|
||||
self.n3 = params.get("n3", self.default_params["n3"])
|
||||
self.n4 = params.get("n4", self.default_params["n4"])
|
||||
else:
|
||||
# Use defaults for new pattern
|
||||
self.delay = self.default_params["delay"]
|
||||
self.n1 = self.default_params["n1"]
|
||||
self.n2 = self.default_params["n2"]
|
||||
self.n3 = self.default_params["n3"]
|
||||
self.n4 = self.default_params["n4"]
|
||||
|
||||
def _save_pattern_parameters(self, pattern_name):
|
||||
"""Save current parameters for the active pattern."""
|
||||
if pattern_name:
|
||||
self.pattern_parameters[pattern_name] = {
|
||||
"delay": self.delay,
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
"n3": self.n3,
|
||||
"n4": self.n4
|
||||
}
|
||||
self._save_config()
|
||||
|
||||
def _current_color_rgb(self):
|
||||
"""Get current RGB color tuple from selected palette color (index 0)."""
|
||||
# Use the first selected color from the palette
|
||||
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
|
||||
"""Get current RGB color tuple."""
|
||||
r = max(0, min(255, int(self.color_r)))
|
||||
g = max(0, min(255, int(self.color_g)))
|
||||
b = max(0, min(255, int(self.color_b)))
|
||||
return (r, g, b)
|
||||
|
||||
def _palette_color(self, selected_index_position: int):
|
||||
"""Return RGB tuple for the selected palette color at given position (0 or 1)."""
|
||||
if not self.selected_color_indices or selected_index_position >= len(self.selected_color_indices):
|
||||
return self._current_color_rgb()
|
||||
color_index = self.selected_color_indices[selected_index_position]
|
||||
if 0 <= color_index < len(self.color_palette):
|
||||
color = self.color_palette[color_index]
|
||||
return (color['r'], color['g'], color['b'])
|
||||
return self._current_color_rgb()
|
||||
|
||||
def _load_config(self):
|
||||
"""Load configuration from file."""
|
||||
try:
|
||||
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):
|
||||
"""Send all parameters to LED bars."""
|
||||
full_payload = {
|
||||
@@ -326,21 +160,12 @@ class LightingController:
|
||||
|
||||
async def _send_normal_pattern(self):
|
||||
"""Send normal pattern to all bars."""
|
||||
# Patterns that need parameters (both long and short names)
|
||||
patterns_needing_params = [
|
||||
"alternating", "a",
|
||||
"flicker", "f",
|
||||
"n_chase", "nc",
|
||||
"rainbow", "r",
|
||||
"radiate", "rd",
|
||||
"segmented_movement", "sm"
|
||||
]
|
||||
patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate", "segmented_movement"]
|
||||
|
||||
payload = {
|
||||
"d": {
|
||||
"t": "b", # Message type: beat
|
||||
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
|
||||
"cl": [self._current_color_rgb()], # Always send color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,80 +206,22 @@ class LightingController:
|
||||
|
||||
async def _handle_alternating_phase(self):
|
||||
"""Handle alternating pattern with phase offset."""
|
||||
# Determine two colors from selected palette (fallback to current color if not set)
|
||||
color_a = self._palette_color(0)
|
||||
color_b = self._palette_color(1)
|
||||
phase = self.beat_index % 2
|
||||
|
||||
# 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 = {
|
||||
"d": {
|
||||
"t": "b",
|
||||
"pt": "a", # alternating
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
# Default color for non-swapped bars changes with phase
|
||||
"cl": [default_color],
|
||||
"s": phase,
|
||||
"s": self.beat_index % 2,
|
||||
}
|
||||
}
|
||||
|
||||
# Bars in this list will have inverted phase and explicit color override
|
||||
# Flip grouping so first four bars (100-103) use default color (color_a on even beats)
|
||||
swap_bars = ["104", "105", "106", "107"]
|
||||
# Only include overrides for swapped bars to minimize payload size
|
||||
for bar_name in swap_bars:
|
||||
inv_phase = (phase + 1) % 2
|
||||
payload[bar_name] = {"s": inv_phase, "cl": [alt_color_for_swap]}
|
||||
|
||||
await self.led_controller.send_data(payload)
|
||||
|
||||
async def _handle_alternating_pulse(self):
|
||||
"""Handle APU: color1 on odd beat for odd bars, color2 on even beat for even bars."""
|
||||
phase = self.beat_index % 2
|
||||
color1 = self._palette_color(0)
|
||||
color2 = self._palette_color(1)
|
||||
if color1 == (255, 255, 255):
|
||||
color1 = (254, 254, 254)
|
||||
if color2 == (255, 255, 255):
|
||||
color2 = (254, 254, 254)
|
||||
|
||||
# Define bar groups by numeric parity
|
||||
even_bars = ["100", "102", "104", "106"]
|
||||
odd_bars = ["101", "103", "105", "107"]
|
||||
|
||||
# Default: turn bars off this beat
|
||||
payload = {
|
||||
"d": {
|
||||
"t": "b",
|
||||
"pt": "o", # off by default
|
||||
# Provide pulse envelope params in defaults for bars we enable
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
"dl": self.delay,
|
||||
}
|
||||
}
|
||||
|
||||
# Activate the correct half with the correct color
|
||||
if phase == 0:
|
||||
# Even beat -> even bars use color2
|
||||
active_bars = even_bars
|
||||
active_color = color2
|
||||
swap_bars = ["101", "103", "105", "107"]
|
||||
for bar_name in LED_BAR_NAMES:
|
||||
if bar_name in swap_bars:
|
||||
payload[bar_name] = {"s": (self.beat_index + 1) % 2}
|
||||
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]}
|
||||
payload[bar_name] = {}
|
||||
|
||||
await self.led_controller.send_data(payload)
|
||||
|
||||
@@ -465,6 +232,10 @@ class LightingController:
|
||||
|
||||
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
|
||||
if self.pending_param_update:
|
||||
current_time = time.time()
|
||||
@@ -478,22 +249,13 @@ class LightingController:
|
||||
await self._handle_sequential_pulse()
|
||||
elif self.current_pattern == "alternating_phase":
|
||||
await self._handle_alternating_phase()
|
||||
elif self.current_pattern == "alternating_pulse":
|
||||
await self._handle_alternating_pulse()
|
||||
elif self.current_pattern:
|
||||
await self._send_normal_pattern()
|
||||
|
||||
async def handle_ui_command(self, message_type, data):
|
||||
"""Handle command from UI client."""
|
||||
if message_type == "pattern_change":
|
||||
# Save current pattern's parameters before switching
|
||||
if self.current_pattern:
|
||||
self._save_pattern_parameters(self.current_pattern)
|
||||
|
||||
# Switch to new pattern and load its parameters
|
||||
self.current_pattern = data.get("pattern", "")
|
||||
self._load_pattern_parameters(self.current_pattern)
|
||||
|
||||
await self._send_full_parameters()
|
||||
logging.info(f"Pattern changed to: {self.current_pattern}")
|
||||
|
||||
@@ -516,20 +278,10 @@ class LightingController:
|
||||
self.n3 = data["n3"]
|
||||
if "n4" in data:
|
||||
self.n4 = data["n4"]
|
||||
|
||||
# Save parameters for current pattern
|
||||
if self.current_pattern:
|
||||
self._save_pattern_parameters(self.current_pattern)
|
||||
|
||||
await self._request_param_update()
|
||||
|
||||
elif message_type == "delay_change":
|
||||
self.delay = data.get("delay", self.delay)
|
||||
|
||||
# Save parameters for current pattern
|
||||
if self.current_pattern:
|
||||
self._save_pattern_parameters(self.current_pattern)
|
||||
|
||||
await self._request_param_update()
|
||||
|
||||
elif message_type == "beat_toggle":
|
||||
@@ -539,15 +291,6 @@ class LightingController:
|
||||
elif message_type == "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:
|
||||
"""WebSocket server for UI client communication and TCP server for sound."""
|
||||
@@ -556,51 +299,34 @@ class ControlServer:
|
||||
self.lighting_controller = LightingController(transport=transport, **transport_kwargs)
|
||||
self.clients = set()
|
||||
self.tcp_server = None
|
||||
self.http_app = None
|
||||
self.http_runner = None
|
||||
self.enable_heartbeat = enable_heartbeat
|
||||
|
||||
async def handle_websocket(self, request):
|
||||
"""Handle WebSocket connection for UI client."""
|
||||
ws = web.WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
|
||||
self.clients.add(ws)
|
||||
client_addr = request.remote
|
||||
async def handle_ui_client(self, websocket):
|
||||
"""Handle UI client WebSocket connection."""
|
||||
self.clients.add(websocket)
|
||||
client_addr = websocket.remote_address
|
||||
logging.info(f"UI client connected: {client_addr}")
|
||||
|
||||
try:
|
||||
async for msg in ws:
|
||||
if msg.type == web.WSMsgType.TEXT:
|
||||
async for message in websocket:
|
||||
try:
|
||||
data = json.loads(msg.data)
|
||||
data = json.loads(message)
|
||||
message_type = data.get("type")
|
||||
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
|
||||
})
|
||||
await self.lighting_controller.handle_ui_command(message_type, message_data)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logging.error(f"Invalid JSON from client {client_addr}: {msg.data}")
|
||||
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}")
|
||||
|
||||
elif msg.type == web.WSMsgType.ERROR:
|
||||
logging.error(f"WebSocket error from {client_addr}: {ws.exception()}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in WebSocket handler: {e}")
|
||||
finally:
|
||||
self.clients.discard(ws)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logging.info(f"UI client disconnected: {client_addr}")
|
||||
|
||||
return ws
|
||||
except Exception as e:
|
||||
logging.error(f"Error in UI client handler: {e}")
|
||||
finally:
|
||||
self.clients.discard(websocket)
|
||||
|
||||
async def handle_tcp_client(self, reader, writer):
|
||||
"""Handle TCP client (sound detector) connection."""
|
||||
@@ -643,216 +369,12 @@ class ControlServer:
|
||||
addrs = ', '.join(str(sock.getsockname()) for sock in self.tcp_server.sockets)
|
||||
logging.info(f"TCP server listening on {addrs}")
|
||||
|
||||
# HTTP API Handlers
|
||||
|
||||
# Color Palette API
|
||||
async def http_get_color_palette(self, request):
|
||||
"""HTTP GET /api/color-palette"""
|
||||
palette_config = self.lighting_controller.get_color_palette_config()
|
||||
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
|
||||
async def start_websocket_server(self):
|
||||
"""Start WebSocket server for UI clients."""
|
||||
server = await websockets.serve(
|
||||
self.handle_ui_client, "localhost", CONTROL_SERVER_PORT
|
||||
)
|
||||
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}")
|
||||
logging.info(f"WebSocket server listening on localhost:{CONTROL_SERVER_PORT}")
|
||||
|
||||
async def run(self):
|
||||
"""Run the control server."""
|
||||
@@ -860,26 +382,26 @@ class ControlServer:
|
||||
await self.lighting_controller.led_controller.connect()
|
||||
|
||||
# Start servers (optionally include heartbeat)
|
||||
websocket_task = asyncio.create_task(self._websocket_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:
|
||||
heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
||||
tasks.append(heartbeat_task)
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def _tcp_server_task(self):
|
||||
"""Keep TCP server running."""
|
||||
await self.start_tcp_server()
|
||||
async def _websocket_server_task(self):
|
||||
"""Keep WebSocket server running."""
|
||||
await self.start_websocket_server()
|
||||
# Keep the server running indefinitely
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _http_server_task(self):
|
||||
"""Keep HTTP and WebSocket server running."""
|
||||
await self.start_http_server()
|
||||
async def _tcp_server_task(self):
|
||||
"""Keep TCP server running."""
|
||||
await self.start_tcp_server()
|
||||
# Keep the server running indefinitely
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
@@ -941,12 +463,11 @@ def parse_arguments():
|
||||
|
||||
# Transport selection
|
||||
transport_group = parser.add_argument_group("Transport Options")
|
||||
default_transport = os.getenv("TRANSPORT", "spi")
|
||||
transport_group.add_argument(
|
||||
"--transport",
|
||||
choices=["spi", "websocket"],
|
||||
default=default_transport,
|
||||
help=f"Transport method for LED communication (default from .env or {default_transport})"
|
||||
default="spi",
|
||||
help="Transport method for LED communication (default: spi)"
|
||||
)
|
||||
|
||||
# Control options
|
||||
|
37
src/sound.py
37
src/sound.py
@@ -11,23 +11,18 @@ import time
|
||||
import logging # Added logging import
|
||||
import asyncio # Re-added asyncio import
|
||||
import threading # Added threading for control server
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure 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')
|
||||
|
||||
# TCP Server Configuration (assuming midi.py runs this)
|
||||
MIDI_TCP_HOST = os.getenv("MIDI_TCP_HOST", "127.0.0.1")
|
||||
MIDI_TCP_PORT = int(os.getenv("MIDI_TCP_PORT", "65432"))
|
||||
MIDI_TCP_HOST = "127.0.0.1"
|
||||
MIDI_TCP_PORT = 65432
|
||||
|
||||
# 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_PORT = int(os.getenv("SOUND_CONTROL_PORT", "65433"))
|
||||
SOUND_CONTROL_HOST = "127.0.0.1"
|
||||
SOUND_CONTROL_PORT = 65433
|
||||
|
||||
class SoundBeatDetector:
|
||||
def __init__(self, tcp_host: str, tcp_port: int, *, input_device: int | None = None):
|
||||
@@ -40,8 +35,7 @@ class SoundBeatDetector:
|
||||
|
||||
self.bufferSize = 512
|
||||
self.windowSizeMultiple = 2
|
||||
default_device = int(os.getenv("AUDIO_INPUT_DEVICE", "7"))
|
||||
self.audioInputDeviceIndex = default_device if input_device is None else int(input_device)
|
||||
self.audioInputDeviceIndex = 7 if input_device is None else int(input_device)
|
||||
self.audioInputChannels = 1
|
||||
|
||||
self.pa = pyaudio.PyAudio()
|
||||
@@ -66,7 +60,7 @@ class SoundBeatDetector:
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting audio device info for index {self.audioInputDeviceIndex}: {e}")
|
||||
self.pa.terminate()
|
||||
raise RuntimeError(f"Audio device {self.audioInputDeviceIndex} not available: {e}")
|
||||
exit()
|
||||
|
||||
self.hopSize = self.bufferSize
|
||||
self.winSize = self.hopSize * self.windowSizeMultiple
|
||||
@@ -211,26 +205,11 @@ if __name__ == "__main__":
|
||||
MIDI_TCP_HOST = "127.0.0.1"
|
||||
MIDI_TCP_PORT = 65432
|
||||
|
||||
logging.info("Starting SoundBeatDetector...")
|
||||
while True:
|
||||
try:
|
||||
sound_detector = SoundBeatDetector(MIDI_TCP_HOST, MIDI_TCP_PORT, input_device=args.input_device)
|
||||
logging.info("Starting SoundBeatDetector...")
|
||||
try:
|
||||
sound_detector.start_stream()
|
||||
break # If we get here, the stream ended normally
|
||||
except KeyboardInterrupt:
|
||||
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
|
1114
src/ui_client.py
1114
src/ui_client.py
File diff suppressed because it is too large
Load Diff
@@ -19,10 +19,26 @@ import json
|
||||
import websockets
|
||||
import re
|
||||
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):
|
||||
@@ -81,9 +97,11 @@ async def run_test(uri: str, messages: list[dict], sleep_s: float):
|
||||
|
||||
|
||||
def parse_args():
|
||||
|
||||
p = argparse.ArgumentParser(description="Send UI commands to control_server WebSocket")
|
||||
default_uri = os.getenv("CONTROL_SERVER_URI", "ws://localhost:8765/ws")
|
||||
p.add_argument("--uri", default=default_uri, help=f"WebSocket URI (default from .env or {default_uri})")
|
||||
load_dotenv()
|
||||
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("--r", type=int, help="Red 0-255 for color_change")
|
||||
p.add_argument("--g", type=int, help="Green 0-255 for color_change")
|
||||
|
@@ -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"
|
||||
|
@@ -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())
|
||||
|
@@ -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()
|
||||
|
@@ -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())
|
||||
|
@@ -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!"
|
||||
|
Reference in New Issue
Block a user