10 Commits
pi ... ui

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

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Lighting Controller UI Client Configuration
# WebSocket URI for the control server
# For local development (running UI on same machine as control server):
CONTROL_SERVER_URI=ws://localhost:8765
# 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

View File

@@ -5,14 +5,13 @@ name = "pypi"
[packages] [packages]
websockets = "*" websockets = "*"
spidev = "*"
watchfiles = "*" watchfiles = "*"
async-tkinter-loop = "*" async-tkinter-loop = "*"
mido = "*" mido = "*"
python-rtmidi = "*" python-rtmidi = "*"
pyaudio = "*"
aubio = "*"
websocket-client = "*" websocket-client = "*"
python-dotenv = "*"
[dev-packages] [dev-packages]

751
api.md Normal file
View File

@@ -0,0 +1,751 @@
# Lighting Controller REST API - Frontend Documentation
## Overview
Complete REST API for controlling the LED lighting system. **No WebSocket required** - all operations use simple HTTP requests.
**Base URL:** `http://10.42.0.1:8765`
**Local Testing:** `http://localhost:8765`
---
## Table of Contents
1. [Quick Start](#quick-start)
2. [Pattern Control](#pattern-control)
3. [Color Palette](#color-palette)
4. [Parameters](#parameters)
5. [System State](#system-state)
6. [Tempo Control](#tempo-control)
7. [Complete Examples](#complete-examples)
---
## Quick Start
### Load Initial State
```javascript
// Get everything in one call
const response = await fetch('http://10.42.0.1:8765/api/state');
const state = await response.json();
console.log(state.pattern); // Current pattern
console.log(state.parameters); // All parameters
console.log(state.color_palette); // 8 colors + 2 selected
console.log(state.beat_index); // Current beat number
```
### Change Pattern
```javascript
await fetch('http://10.42.0.1:8765/api/pattern', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern: 'alternating'})
});
```
### Change 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, 5]}) // Blue and Cyan
});
```
---
## Pattern Control
### GET /api/pattern
Get the currently active pattern.
**Request:**
```http
GET /api/pattern HTTP/1.1
```
**Response:**
```json
{
"pattern": "alternating"
}
```
---
### POST /api/pattern
Change the active pattern.
**Request:**
```http
POST /api/pattern HTTP/1.1
Content-Type: application/json
```
**Available Patterns:**
- `"on"` / `"o"` - Solid color
- `"off"` / `"f"` - All LEDs off
- `"flicker"` / `"f"` - Flickering effect
- `"fill_range"` / `"fr"` - Fill effect
- `"n_chase"` / `"nc"` - Chase pattern
- `"alternating"` / `"a"` - Alternating on/off
- `"pulse"` / `"p"` - Pulsing effect
- `"rainbow"` / `"r"` - Rainbow cycle
- `"specto"` / `"s"` - Spectograph effect
- `"radiate"` / `"rd"` - Radiate from center
- `"segmented_movement"` / `"sm"` - Moving segments
**Response:**
```json
{
"status": "ok",
"pattern": "alternating"
}
```
**JavaScript Example:**
```javascript
async function setPattern(patternName) {
const response = await fetch('http://10.42.0.1:8765/api/pattern', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern: patternName})
});
return await response.json();
}
// Usage
await setPattern('alternating');
await setPattern('rainbow');
```
---
## Color Palette
### GET /api/color-palette
Get the 8-color palette and selected colors.
**Response:**
```json
{
"palette": [
{"r": 255, "g": 0, "b": 0}, // Slot 0: Red
{"r": 0, "g": 255, "b": 0}, // Slot 1: Green
{"r": 0, "g": 0, "b": 255}, // Slot 2: Blue
{"r": 255, "g": 255, "b": 0}, // Slot 3: Yellow
{"r": 255, "g": 0, "b": 255}, // Slot 4: Magenta
{"r": 0, "g": 255, "b": 255}, // Slot 5: Cyan
{"r": 255, "g": 128, "b": 0}, // Slot 6: Orange
{"r": 255, "g": 255, "b": 255} // Slot 7: White
],
"selected_indices": [0, 1] // [0] = pattern color, [1] = reserved
}
```
**Important:** The **first selected color** (index 0) is used for all patterns!
---
### POST /api/color-palette
Update palette colors and/or selected colors.
**Request (Change Selected Colors):**
```json
{
"selected_indices": [2, 5] // Use slot 2 (Blue) for patterns
}
```
**Request (Update a Color):**
```json
{
"palette": [
{"r": 255, "g": 0, "b": 0},
{"r": 0, "g": 255, "b": 0},
{"r": 128, "g": 0, "b": 128}, // Changed to purple
// ... all 8 colors (must send complete array)
]
}
```
**Response:**
```json
{
"status": "ok",
"palette": {
"palette": [...],
"selected_indices": [2, 5]
}
}
```
**JavaScript Example:**
```javascript
// Change pattern color to slot 5 (Cyan)
async function selectColor(slotIndex) {
const response = await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [slotIndex, 1]})
});
return await response.json();
}
// Edit a color in the palette
async function updatePaletteColor(slotIndex, r, g, b) {
// First get current palette
const current = await fetch('http://10.42.0.1:8765/api/color-palette')
.then(res => res.json());
// Update the specific slot
const newPalette = [...current.palette];
newPalette[slotIndex] = {r, g, b};
// Send updated palette
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({palette: newPalette})
});
}
// Usage
await selectColor(2); // Use blue for patterns
await updatePaletteColor(3, 128, 0, 128); // Change slot 3 to purple
```
---
## Parameters
### GET /api/parameters
Get all current parameter values.
**Response:**
```json
{
"brightness": 100, // 0-100
"delay": 50, // milliseconds
"n1": 10, // Pattern parameter 1
"n2": 5, // Pattern parameter 2
"n3": 2, // Pattern parameter 3 (forward movement)
"n4": 1 // Pattern parameter 4 (backward movement)
}
```
---
### POST /api/parameters
Update one or more parameters. Only send the parameters you want to change.
**Request:**
```json
{
"brightness": 75,
"n1": 15
}
```
**Response:**
```json
{
"status": "ok",
"parameters": {
"brightness": 75,
"delay": 50,
"n1": 15,
"n2": 5,
"n3": 2,
"n4": 1
}
}
```
**Parameter Descriptions:**
| Parameter | Range | Description |
|-----------|-------|-------------|
| `brightness` | 0-100 | LED brightness percentage |
| `delay` | 1-1000 | Pattern speed (milliseconds) |
| `n1` | 0-255 | Pattern-specific (e.g., segment length) |
| `n2` | 0-255 | Pattern-specific (e.g., spacing) |
| `n3` | 0-255 | Pattern-specific (e.g., forward steps) |
| `n4` | 0-255 | Pattern-specific (e.g., backward steps) |
**JavaScript Example:**
```javascript
async function setBrightness(value) {
const response = await fetch('http://10.42.0.1:8765/api/parameters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({brightness: value})
});
return await response.json();
}
async function setSpeed(delayMs) {
await fetch('http://10.42.0.1:8765/api/parameters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({delay: delayMs})
});
}
// Usage
await setBrightness(75); // Set to 75%
await setSpeed(100); // Slow down pattern
```
---
## System State
### GET /api/state
Get complete system state in a single call. Perfect for initial UI load.
**Response:**
```json
{
"pattern": "alternating",
"parameters": {
"brightness": 100,
"delay": 50,
"n1": 10,
"n2": 5,
"n3": 2,
"n4": 1
},
"color_palette": {
"palette": [...8 colors...],
"selected_indices": [0, 1]
},
"beat_index": 42
}
```
**JavaScript Example:**
```javascript
// Load all state when UI starts
async function loadInitialState() {
const response = await fetch('http://10.42.0.1:8765/api/state');
const state = await response.json();
// Update UI with current state
updatePatternButtons(state.pattern);
updateColorPalette(state.color_palette);
updateSliders(state.parameters);
return state;
}
```
---
## Tempo Control
### POST /api/tempo/reset
Reset the tempo/beat detection in the sound system.
**Request:**
```http
POST /api/tempo/reset HTTP/1.1
```
**Response:**
```json
{
"status": "ok",
"message": "Tempo reset sent"
}
```
**JavaScript Example:**
```javascript
async function resetTempo() {
const response = await fetch('http://10.42.0.1:8765/api/tempo/reset', {
method: 'POST'
});
return await response.json();
}
// Usage: Call this when tempo detection seems off
await resetTempo();
```
---
## Complete Examples
### Example 1: Full UI Controller Class
```javascript
class LightingController {
constructor(baseUrl = 'http://10.42.0.1:8765') {
this.baseUrl = baseUrl;
}
// Load complete state
async loadState() {
const response = await fetch(`${this.baseUrl}/api/state`);
return await response.json();
}
// Pattern control
async setPattern(pattern) {
const response = await fetch(`${this.baseUrl}/api/pattern`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern})
});
return await response.json();
}
// Color selection
async selectColor(slotIndex) {
const response = await fetch(`${this.baseUrl}/api/color-palette`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [slotIndex, 1]})
});
return await response.json();
}
// Brightness control
async setBrightness(value) {
const response = await fetch(`${this.baseUrl}/api/parameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({brightness: value})
});
return await response.json();
}
// Pattern parameters
async setParameters(params) {
const response = await fetch(`${this.baseUrl}/api/parameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(params)
});
return await response.json();
}
}
// Usage
const lights = new LightingController();
// On page load
const state = await lights.loadState();
// User interactions
await lights.setPattern('rainbow');
await lights.selectColor(2); // Blue
await lights.setBrightness(75);
```
---
### Example 2: React Component
```jsx
import { useState, useEffect } from 'react';
function LightingControl() {
const [state, setState] = useState(null);
const BASE_URL = 'http://10.42.0.1:8765';
// Load initial state
useEffect(() => {
fetch(`${BASE_URL}/api/state`)
.then(res => res.json())
.then(setState);
}, []);
// Change pattern
const handlePatternChange = async (pattern) => {
await fetch(`${BASE_URL}/api/pattern`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern})
});
// Reload state
const newState = await fetch(`${BASE_URL}/api/state`).then(r => r.json());
setState(newState);
};
// Change color
const handleColorSelect = async (slotIndex) => {
await fetch(`${BASE_URL}/api/color-palette`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [slotIndex, 1]})
});
const newState = await fetch(`${BASE_URL}/api/state`).then(r => r.json());
setState(newState);
};
// Change brightness
const handleBrightnessChange = async (value) => {
await fetch(`${BASE_URL}/api/parameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({brightness: value})
});
};
if (!state) return <div>Loading...</div>;
return (
<div>
<h2>Pattern: {state.pattern}</h2>
<div>
<button onClick={() => handlePatternChange('alternating')}>Alternating</button>
<button onClick={() => handlePatternChange('rainbow')}>Rainbow</button>
<button onClick={() => handlePatternChange('pulse')}>Pulse</button>
</div>
<div>
<h3>Colors</h3>
{state.color_palette.palette.map((color, i) => (
<div
key={i}
onClick={() => handleColorSelect(i)}
style={{
background: `rgb(${color.r}, ${color.g}, ${color.b})`,
border: state.color_palette.selected_indices[0] === i ? '3px solid gold' : '1px solid black',
width: 50,
height: 50,
display: 'inline-block',
cursor: 'pointer'
}}
/>
))}
</div>
<div>
<label>Brightness: {state.parameters.brightness}</label>
<input
type="range"
min="0"
max="100"
value={state.parameters.brightness}
onChange={(e) => handleBrightnessChange(e.target.value)}
/>
</div>
</div>
);
}
```
---
### Example 3: Simple HTML + Vanilla JS
```html
<!DOCTYPE html>
<html>
<head>
<title>Lighting Controller</title>
</head>
<body>
<h1>LED Lighting Control</h1>
<div id="patterns"></div>
<div id="colors"></div>
<label>Brightness: <span id="brightness-value">100</span></label>
<input type="range" id="brightness" min="0" max="100" value="100">
<script>
const BASE_URL = 'http://10.42.0.1:8765';
// Load initial state
async function init() {
const response = await fetch(`${BASE_URL}/api/state`);
const state = await response.json();
// Create pattern buttons
const patterns = ['on', 'alternating', 'rainbow', 'pulse', 'segmented_movement'];
const patternsDiv = document.getElementById('patterns');
patterns.forEach(pattern => {
const btn = document.createElement('button');
btn.textContent = pattern;
btn.onclick = () => setPattern(pattern);
patternsDiv.appendChild(btn);
});
// Create color palette
const colorsDiv = document.getElementById('colors');
state.color_palette.palette.forEach((color, i) => {
const div = document.createElement('div');
div.style.cssText = `
background: rgb(${color.r}, ${color.g}, ${color.b});
width: 50px;
height: 50px;
display: inline-block;
cursor: pointer;
border: ${state.color_palette.selected_indices[0] === i ? '3px solid gold' : '1px solid black'};
`;
div.onclick = () => selectColor(i);
colorsDiv.appendChild(div);
});
// Brightness slider
document.getElementById('brightness').oninput = (e) => {
document.getElementById('brightness-value').textContent = e.target.value;
setBrightness(e.target.value);
};
}
async function setPattern(pattern) {
await fetch(`${BASE_URL}/api/pattern`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern})
});
}
async function selectColor(index) {
await fetch(`${BASE_URL}/api/color-palette`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({selected_indices: [index, 1]})
});
location.reload(); // Refresh to show updated selection
}
async function setBrightness(value) {
await fetch(`${BASE_URL}/api/parameters`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({brightness: parseInt(value)})
});
}
init();
</script>
</body>
</html>
```
---
## 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/color-palette` | Get color palette |
| `POST` | `/api/color-palette` | Update palette/selection |
| `GET` | `/api/parameters` | Get all parameters |
| `POST` | `/api/parameters` | Update parameters |
| `POST` | `/api/tempo/reset` | Reset tempo detection |
---
## Error Handling
All endpoints return standard HTTP status codes:
- `200 OK` - Success
- `400 Bad Request` - Invalid request data
- `500 Internal Server Error` - Server error
**Error Response Format:**
```json
{
"status": "error",
"message": "Pattern name required"
}
```
**JavaScript Error Handling Example:**
```javascript
async function safeApiCall(url, options) {
try {
const response = await fetch(url, options);
const data = await response.json();
if (data.status === 'error') {
console.error('API Error:', data.message);
return null;
}
return data;
} catch (error) {
console.error('Network Error:', error);
return null;
}
}
// Usage
const result = await safeApiCall('http://10.42.0.1:8765/api/pattern', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({pattern: 'rainbow'})
});
```
---
## Testing
### Test All Endpoints
```bash
# Get state
curl http://10.42.0.1:8765/api/state | jq
# Change pattern
curl -X POST http://10.42.0.1:8765/api/pattern \
-H "Content-Type: application/json" \
-d '{"pattern": "rainbow"}'
# Change color
curl -X POST http://10.42.0.1:8765/api/color-palette \
-H "Content-Type: application/json" \
-d '{"selected_indices": [2, 5]}'
# Update brightness
curl -X POST http://10.42.0.1:8765/api/parameters \
-H "Content-Type: application/json" \
-d '{"brightness": 75}'
# Reset tempo
curl -X POST http://10.42.0.1:8765/api/tempo/reset
```
---
## Notes
- **No WebSocket needed** - Everything uses simple HTTP REST API
- **CORS**: Not currently enabled. Host UI on same domain or add CORS middleware
- **Persistence**: Color palette persists to `lighting_config.json`
- **Real-time**: Changes take effect immediately (within one beat cycle)
- **Pattern Color**: First selected color (index 0) is used for all patterns
---
## Support Files
- Full API details: `COLOR_PALETTE_API.md`
- Migration notes: `AIOHTTP_MIGRATION.md`
- Test results: `PATTERN_COLOR_TEST_RESULTS.md`
- Migration notes: `AIOHTTP_MIGRATION.md`
- Test results: `PATTERN_COLOR_TEST_RESULTS.md`

405
colorpallet.md Normal file
View File

@@ -0,0 +1,405 @@
# Color Palette REST API - UI Integration Guide
## Overview
The lighting control server provides a REST API for managing an 8-color palette with 2 selected colors. This is designed for UI integration to allow users to:
- View the current 8 colors in the palette
- Edit any of the 8 colors
- Select which 2 colors are active (for patterns that use selected colors)
Configuration is automatically persisted to `lighting_config.json`.
## Base URLs
The API is available on **two ports** for flexibility:
**Primary (same as WebSocket):**
```
http://<server-ip>:8765/api/color-palette
```
**Backward Compatibility:**
```
http://<server-ip>:8766/api/color-palette
```
**Default IPs:**
- Remote: `http://10.42.0.1:8765` (Pi server)
- Local: `http://localhost:8765` (testing)
---
## Quick Start for UI Developers
### Recommended Usage Pattern
1. **On UI Load:** `GET /api/color-palette` to populate the color picker
2. **When User Edits a Color:** `POST /api/color-palette` with updated palette
3. **When User Selects Colors:** `POST /api/color-palette` with new selected_indices
---
## API Reference
### GET /api/color-palette
**Purpose:** Get the current palette state (for populating UI on load)
#### Request
```http
GET /api/color-palette HTTP/1.1
```
#### Response (200 OK)
```json
{
"palette": [
{"r": 255, "g": 0, "b": 0}, // Slot 0: Red
{"r": 0, "g": 255, "b": 0}, // Slot 1: Green
{"r": 0, "g": 0, "b": 255}, // Slot 2: Blue
{"r": 255, "g": 255, "b": 0}, // Slot 3: Yellow
{"r": 255, "g": 0, "b": 255}, // Slot 4: Magenta
{"r": 0, "g": 255, "b": 255}, // Slot 5: Cyan
{"r": 255, "g": 128, "b": 0}, // Slot 6: Orange
{"r": 255, "g": 255, "b": 255} // Slot 7: White
],
"selected_indices": [0, 1] // Currently selected: Red and Green
}
```
#### Response Fields
- **`palette`**: Array of exactly 8 color objects
- Each color has `r`, `g`, `b` (integers 0-255)
- Index corresponds to palette slot (0-7)
- **`selected_indices`**: Array of exactly 2 integers (0-7)
- Indicates which 2 palette slots are currently active
- Patterns may use these selected colors
---
### POST /api/color-palette (or PUT)
**Purpose:** Update palette colors and/or selected colors
#### Request Body (JSON)
Both fields are **optional** - send only what you want to update:
```json
{
"palette": [...], // Optional: Update all 8 colors
"selected_indices": [0, 3] // Optional: Change selected colors
}
```
#### Success Response (200 OK)
```json
{
"status": "ok",
"palette": {
"palette": [...],
"selected_indices": [0, 3]
}
}
```
#### Error Response (400/500)
```json
{
"status": "error",
"message": "Palette must be an array of 8 colors"
}
```
---
## Common Use Cases
### Use Case 1: Load Palette on UI Startup
```javascript
async function loadPalette() {
const response = await fetch('http://10.42.0.1:8765/api/color-palette');
const data = await response.json();
// data.palette = array of 8 colors
// data.selected_indices = [index1, index2]
return data;
}
```
### Use Case 2: User Edits a Single Color
When user changes color in slot 3 to purple:
```javascript
async function updateColor(slotIndex, r, g, b) {
// Get current palette
const current = await fetch('http://10.42.0.1:8765/api/color-palette')
.then(res => res.json());
// Update the specific slot
const newPalette = [...current.palette];
newPalette[slotIndex] = {r, g, b};
// Send updated palette
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({palette: newPalette})
});
}
// Example: Change slot 3 to purple (128, 0, 128)
updateColor(3, 128, 0, 128);
```
### Use Case 3: User Selects Different Active Colors
When user selects slots 2 and 5:
```javascript
async function selectColors(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]
})
});
}
// Example: Select blue (slot 2) and cyan (slot 5)
selectColors(2, 5);
```
### Use Case 4: Reset to Default Palette
```javascript
async function resetPalette() {
const defaultPalette = [
{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
];
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
palette: defaultPalette,
selected_indices: [0, 1]
})
});
}
```
---
## Validation & Error Handling
### Validation Rules
**Palette Array:**
- Must contain **exactly 8** color objects
- Each color must have `r`, `g`, `b` fields
- RGB values must be **integers between 0-255**
**Selected Indices:**
- Must be an array of **exactly 2** integers
- Each index must be **between 0-7** (inclusive)
- Can be the same index twice (e.g., `[3, 3]`)
### Error Responses
```javascript
// Example: Invalid palette length
{
"status": "error",
"message": "Palette must be an array of 8 colors"
}
// Example: Invalid RGB value
{
"status": "error",
"message": "RGB values must be 0-255"
}
// Example: Invalid index
{
"status": "error",
"message": "Selected indices must be 0-7"
}
```
---
## Data Persistence
- **Automatic Save:** All changes are immediately saved to `lighting_config.json`
- **Automatic Load:** Server loads saved config on startup
- **Default Values:** If no config file exists, server initializes with default palette
**Default Palette:**
```javascript
[
{r: 255, g: 0, b: 0}, // Slot 0: Red
{r: 0, g: 255, b: 0}, // Slot 1: Green
{r: 0, g: 0, b: 255}, // Slot 2: Blue
{r: 255, g: 255, b: 0}, // Slot 3: Yellow
{r: 255, g: 0, b: 255}, // Slot 4: Magenta
{r: 0, g: 255, b: 255}, // Slot 5: Cyan
{r: 255, g: 128, b: 0}, // Slot 6: Orange
{r: 255, g: 255, b: 255} // Slot 7: White
]
// Default selected: [0, 1] (Red and Green)
```
---
## Testing & Debugging
### Test API Connection
```bash
# Quick test - get current palette
curl http://10.42.0.1:8765/api/color-palette
# Pretty print with jq
curl http://10.42.0.1:8765/api/color-palette | jq
# Test update
curl -X POST http://10.42.0.1:8765/api/color-palette \
-H "Content-Type: application/json" \
-d '{"selected_indices": [3, 6]}'
```
### Browser DevTools
```javascript
// Test in browser console
fetch('http://10.42.0.1:8765/api/color-palette')
.then(r => r.json())
.then(console.log);
```
---
## Advanced Topics
### CORS (Cross-Origin Requests)
**Note:** The API does not currently include CORS headers.
If accessing from a different origin (e.g., UI on different domain):
1. Add CORS middleware to the server (contact backend team)
2. Use a proxy server
3. Host UI on same origin as API
### Helper Function: RGB to Hex
```javascript
function rgbToHex({r, g, b}) {
return '#' + [r, g, b]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
// Usage
const color = {r: 255, g: 128, b: 0};
const hex = rgbToHex(color); // "#ff8000"
```
### Helper Function: Hex to RGB
```javascript
function 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;
}
// Usage
const rgb = hexToRgb('#ff8000'); // {r: 255, g: 128, b: 0}
```
---
## Environment Configuration
### Server Environment Variables (`.env`)
```bash
# Server host (bind to all interfaces)
CONTROL_SERVER_HOST=0.0.0.0
# Primary port (WebSocket + HTTP API)
CONTROL_SERVER_PORT=8765
# Additional HTTP API port (optional, for backward compatibility)
HTTP_API_PORT=8766
```
### WebSocket Endpoint
**Important:** WebSocket is on a separate endpoint from the API:
- **WebSocket:** `ws://10.42.0.1:8765/ws`
- **HTTP API:** `http://10.42.0.1:8765/api/color-palette`
---
## Summary for UI Developers
### Key Points
**8 color slots**, each with RGB values (0-255)
**2 selected colors** (indices 0-7)
**Auto-persistence** to `lighting_config.json`
**Optional updates** - send only what changed
**Two ports available** - 8765 (primary) and 8766 (backup)
### Integration Checklist
- [ ] Load palette on UI startup with `GET`
- [ ] Display 8 color slots with current colors
- [ ] Highlight the 2 selected colors
- [ ] Allow editing individual colors
- [ ] Allow selecting which 2 colors are active
- [ ] Send updates with `POST` when user makes changes
- [ ] Handle errors gracefully
- [ ] Test with both local and remote server
### Quick Reference
```javascript
// GET - Load palette
const data = await fetch('http://10.42.0.1:8765/api/color-palette')
.then(r => r.json());
// POST - Update color in slot 3
await fetch('http://10.42.0.1:8765/api/color-palette', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
palette: modifiedPaletteArray
})
});
// POST - Change selected colors to slots 2 and 5
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]
})
});
```

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,27 @@ import asyncio
import json import json
import websockets import websockets
import re import re
import os
def load_dotenv(filepath: str = ".env"):
try:
if not os.path.exists(filepath):
return
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' not in line:
continue
key, value = line.split('=', 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = value
except Exception:
pass
def build_messages(args): def build_messages(args):
@@ -76,8 +97,11 @@ async def run_test(uri: str, messages: list[dict], sleep_s: float):
def parse_args(): def parse_args():
p = argparse.ArgumentParser(description="Send UI commands to control_server WebSocket") p = argparse.ArgumentParser(description="Send UI commands to control_server WebSocket")
p.add_argument("--uri", default="ws://localhost:8765", help="WebSocket URI (default ws://localhost:8765)") 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("--pattern", help="Pattern name for pattern_change")
p.add_argument("--r", type=int, help="Red 0-255 for color_change") p.add_argument("--r", type=int, help="Red 0-255 for color_change")
p.add_argument("--g", type=int, help="Green 0-255 for color_change") p.add_argument("--g", type=int, help="Green 0-255 for color_change")