Compare commits
15 Commits
f9188b694e
...
ui
Author | SHA1 | Date | |
---|---|---|---|
9cf1855b51 | |||
763a2053ad | |||
324fa463be | |||
aaf515d8f4 | |||
7beca0cf53 | |||
ace47b7835 | |||
9045b10631 | |||
f2e775f6f5 | |||
a654527dc3 | |||
|
0906cb22e6 | ||
|
e4a83e8f0d | ||
|
f4e9f8fff7 | ||
|
d57fce77fb | ||
|
fbeb365932 | ||
|
ed35d6b838 |
9
.env.example
Normal file
9
.env.example
Normal 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
|
13
Pipfile
13
Pipfile
@@ -5,14 +5,13 @@ name = "pypi"
|
||||
|
||||
[packages]
|
||||
websockets = "*"
|
||||
spidev = "*"
|
||||
watchfiles = "*"
|
||||
async-tkinter-loop = "*"
|
||||
mido = "*"
|
||||
python-rtmidi = "*"
|
||||
pyaudio = "*"
|
||||
aubio = "*"
|
||||
|
||||
websocket-client = "*"
|
||||
python-dotenv = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
@@ -23,9 +22,13 @@ python_version = "3.11"
|
||||
run = "python src/main.py"
|
||||
ui = "python src/ui_client.py"
|
||||
control = "python src/control_server.py"
|
||||
control-spi = "python src/control_server.py --transport spi"
|
||||
control-ws = "python src/control_server.py --transport websocket"
|
||||
sound = "python src/sound.py"
|
||||
dev-ui = 'watchfiles "python src/ui_client.py" src'
|
||||
dev-control = 'watchfiles "python src/control_server.py" src'
|
||||
dev-control = 'watchfiles --args "--transport spi" "python src/control_server.py" src'
|
||||
dev-control-spi = 'watchfiles --args "--transport spi" "python src/control_server.py" src'
|
||||
dev-control-ws = 'watchfiles --args "--transport websocket" "python src/control_server.py" src'
|
||||
install = "pipenv install"
|
||||
install-system = "bash -c 'sudo apt-get update && sudo apt-get install -y python3-spidev python3-pip python3-dev portaudio19-dev libasound2-dev'"
|
||||
monitor-esp32 = "bash -c 'source $HOME/esp/esp-idf/export.sh && cd esp32 && idf.py -p ${ESPPORT:-/dev/ttyACM0} monitor'"
|
||||
@@ -33,3 +36,5 @@ build-esp32 = "bash -c 'source $HOME/esp/esp-idf/export.sh && cd esp32 && idf.py
|
||||
flash-esp32 = "bash -c 'source $HOME/esp/esp-idf/export.sh && cd esp32 && idf.py -p $ESPPORT -b ${ESPSPEED:-460800} flash'"
|
||||
watch-esp32 = "watchfiles 'bash -c \"source $HOME/esp/esp-idf/export.sh && cd esp32 && idf.py -p ${ESPPORT:-/dev/ttyACM0} -b ${ESPSPEED:-460800} flash monitor\"' esp32/main"
|
||||
send-json = "python test/send_json.py"
|
||||
send-net = "python test/test_networking.py"
|
||||
sound-run = "python src/sound.py"
|
||||
|
751
api.md
Normal file
751
api.md
Normal 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
405
colorpallet.md
Normal 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]
|
||||
})
|
||||
});
|
||||
```
|
107
debug_espnow.py
Normal file
107
debug_espnow.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Real-time ESP NOW traffic monitor for debugging pattern pausing issues.
|
||||
Monitors both the ESP32-C3 USB CDC output and LED bar debug info.
|
||||
"""
|
||||
|
||||
import serial
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
class ESPNowDebugger:
|
||||
def __init__(self):
|
||||
self.esp32_port = "/dev/ttyACM0"
|
||||
self.running = False
|
||||
|
||||
def monitor_esp32_serial(self):
|
||||
"""Monitor ESP32-C3 USB CDC output for ESP NOW debug info"""
|
||||
try:
|
||||
ser = serial.Serial(self.esp32_port, 115200, timeout=1)
|
||||
print("🔌 Monitoring ESP32-C3 USB CDC output...")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
line = ser.readline().decode('utf-8').strip()
|
||||
if line:
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
print(f"[{timestamp}] ESP32-C3: {line}")
|
||||
except serial.SerialTimeoutException:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"❌ ESP32-C3 monitor error: {e}")
|
||||
break
|
||||
|
||||
ser.close()
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to connect to ESP32-C3: {e}")
|
||||
|
||||
def check_lighting_controller_logs(self):
|
||||
"""Check lighting controller logs for message sending"""
|
||||
try:
|
||||
# Monitor control server output for ESP NOW messages
|
||||
print("🔌 Monitoring lighting controller ESP NOW messages...")
|
||||
|
||||
# Check if control server is running
|
||||
proc = subprocess.run(['pgrep', '-f', 'lighting-controller'],
|
||||
capture_output=True, text=True)
|
||||
if not proc.stdout.strip():
|
||||
print("❌ Control server not running!")
|
||||
return
|
||||
|
||||
print("✅ Control server running, monitor logs manually")
|
||||
print("💡 Tips:")
|
||||
print(" - Watch control server terminal output")
|
||||
print(" - Look for SPI/ESP NOW communication messages")
|
||||
print(" - Check for timing gaps between messages")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error checking control server: {e}")
|
||||
|
||||
def run(self):
|
||||
"""Start all monitoring threads"""
|
||||
print("🔍 ESP NOW Communication Debugger")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Check if ESP32-C3 is connected
|
||||
if not os.path.exists(self.esp32_port):
|
||||
print(f"❌ ESP32-C3 not found on {self.esp32_port}")
|
||||
print("💡 Make sure ESP32-C3 is connected via USB")
|
||||
return
|
||||
|
||||
print(f"✅ ESP32-C3 found on {self.esp32_port}")
|
||||
print()
|
||||
|
||||
self.running = True
|
||||
|
||||
# Start ESP32-C3 monitoring thread
|
||||
esp32_thread = threading.Thread(target=self.monitor_esp32_serial)
|
||||
esp32_thread.daemon = True
|
||||
esp32_thread.start()
|
||||
|
||||
# Monitor lighting controller
|
||||
self.check_lighting_controller_logs()
|
||||
|
||||
print()
|
||||
print("🔍 Monitoring active. Press Ctrl+C to stop...")
|
||||
print("📝 Watch for:")
|
||||
print(" - ESP NOW message transmission timing")
|
||||
print(" - Any error messages or delays")
|
||||
print(" - Status updates every 5 seconds")
|
||||
print(" - Pattern interrupt patterns")
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Stopping debugger...")
|
||||
self.running = False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
debugger = ESPNowDebugger()
|
||||
debugger.run()
|
137
debug_led_bar.py
Normal file
137
debug_led_bar.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enhanced LED bar debugging script.
|
||||
Adds timestamp and message sequence debugging to LED bar main.py
|
||||
"""
|
||||
|
||||
debug_main_content = '''
|
||||
import patterns
|
||||
from settings import Settings
|
||||
from web import web
|
||||
from patterns import Patterns
|
||||
import gc
|
||||
import utime
|
||||
import machine
|
||||
import time
|
||||
import wifi
|
||||
import json
|
||||
from p2p import p2p
|
||||
import espnow
|
||||
import network
|
||||
|
||||
def main():
|
||||
settings = Settings()
|
||||
print(settings)
|
||||
|
||||
if settings.get("color_order", "RGB") == "RBG":
|
||||
color_order = (1, 5, 3)
|
||||
else:
|
||||
color_order = (1, 3, 5)
|
||||
patterns = Patterns(settings["led_pin"], settings["num_leds"], selected="off")
|
||||
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
sta_if.active(True)
|
||||
|
||||
e = espnow.ESPNow()
|
||||
e.config(rxbuf=1024)
|
||||
e.active(True)
|
||||
|
||||
wdt = machine.WDT(timeout=10000)
|
||||
wdt.feed()
|
||||
|
||||
# Debug counters
|
||||
msg_count = 0
|
||||
last_msg_time = 0
|
||||
gap_count = 0
|
||||
|
||||
print(f"[DEBUG] Bar '{settings.get('name', 'unknown')}' starting ESP NOW debug mode")
|
||||
print(f"[DEBUG] Expected message types: 'b' (beat), 'u' (update)")
|
||||
|
||||
while True:
|
||||
# advance pattern based on its own returned schedule
|
||||
# due = patterns.tick(due)
|
||||
wdt.feed()
|
||||
|
||||
# Drain all pending packets and only process the latest
|
||||
last_msg = None
|
||||
msg_received = False
|
||||
|
||||
while True:
|
||||
host, msg = e.recv(0)
|
||||
if not msg:
|
||||
break
|
||||
last_msg = msg
|
||||
msg_received = True
|
||||
|
||||
if last_msg:
|
||||
msg_count += 1
|
||||
current_time = time.ticks_ms()
|
||||
|
||||
# Calculate gap between messages
|
||||
if last_msg_time > 0:
|
||||
gap = time.ticks_diff(current_time, last_msg_time)
|
||||
if gap > 1000: # > 1 second gap
|
||||
gap_count += 1
|
||||
print(f"[DEBUG] Message gap detected: {gap}ms (gap #{gap_count})")
|
||||
|
||||
last_msg_time = current_time
|
||||
|
||||
try:
|
||||
data = json.loads(last_msg)
|
||||
msg_type = data.get("d", {}).get("t", "unknown")
|
||||
timestamp = time.strftime("%H:%M:%S")
|
||||
|
||||
print(f"[{timestamp}] MSG#{msg_count}: type='{msg_type}' gap={time.ticks_diff(current_time, last_msg_time) if last_msg_time > 0 else 0}ms")
|
||||
|
||||
# Full data print for debugging
|
||||
print(f"[DEBUG] Full message: {data}")
|
||||
|
||||
defaults = data.get("d", {})
|
||||
bar = data.get(settings.get("name"), {})
|
||||
|
||||
# Check message type
|
||||
message_type = defaults.get("t", "b") # Default to beat if not specified
|
||||
|
||||
# Always update parameters from message
|
||||
patterns.brightness = bar.get("br", defaults.get("br", patterns.brightness))
|
||||
patterns.delay = bar.get("dl", defaults.get("dl", patterns.delay))
|
||||
patterns.colors = bar.get("cl", defaults.get("cl", patterns.colors))
|
||||
patterns.n1 = bar.get("n1", defaults.get("n1", patterns.n1))
|
||||
patterns.n2 = bar.get("n2", defaults.get("n2", patterns.n2))
|
||||
patterns.n3 = bar.get("n3", defaults.get("n3", patterns.n3))
|
||||
patterns.step = bar.get("s", defaults.get("s", patterns.step))
|
||||
|
||||
# Only execute pattern if it's a beat message
|
||||
if message_type == "b": # Beat message
|
||||
selected_pattern = bar.get("pt", defaults.get("pt", "off"))
|
||||
if selected_pattern in patterns.patterns:
|
||||
print(f"[DEBUG] Executing pattern: {selected_pattern}")
|
||||
patterns.patterns[selected_pattern]()
|
||||
else:
|
||||
print(f"[DEBUG] Pattern '{selected_pattern}' not found")
|
||||
elif message_type == "u": # Update message
|
||||
print(f"[DEBUG] Parameters updated: brightness={patterns.brightness}, delay={patterns.delay}")
|
||||
else:
|
||||
print(f"[DEBUG] Unknown message type: '{message_type}'")
|
||||
|
||||
except Exception as ex:
|
||||
print(f"[DEBUG] Failed to load espnow data {last_msg}: {ex}")
|
||||
continue
|
||||
|
||||
# Periodic status every 100 loops (about every 10 seconds)
|
||||
if msg_count > 0 and msg_count % 100 == 0:
|
||||
print(f"[STATUS] Processed {msg_count} messages, {gap_count} gaps detected")
|
||||
|
||||
|
||||
main()
|
||||
'''
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Enhanced LED bar debugging output generated.")
|
||||
print("This script would replace the main.py with enhanced debugging.")
|
||||
print("The debug version adds:")
|
||||
print("- Timestamped messages")
|
||||
print("- Message sequence numbers")
|
||||
print("- Gap detection between messages")
|
||||
print("- Detailed pattern execution logging")
|
||||
print("- Status summaries")
|
45
esp32_debug_patch.md
Normal file
45
esp32_debug_patch.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# ESP32-C3 Debug Patch
|
||||
|
||||
## Issue Found
|
||||
The ESP32-C3 firmware sends status messages every 5 seconds when SPI transactions fail (line 208 in main.c), which could interfere with ESP NOW communication and cause pattern pauses.
|
||||
|
||||
## Debugging Steps
|
||||
|
||||
### 1. Monitor ESP32-C3 Status Messages
|
||||
The ESP32-C3 sends status messages every 5 seconds, which may interrupt ESP NOW communication.
|
||||
|
||||
### 2. Check SPI Communication
|
||||
Pattern stops could be caused by:
|
||||
- SPI transaction failures causing 5-second delays
|
||||
- ESP NOW interference with SPI operations
|
||||
- Memory/buffer issues during concurrent operations
|
||||
|
||||
### 3. Monitor ESP NOW Traffic
|
||||
Use the debug scripts to monitor:
|
||||
- ESP NOW message transmission timing
|
||||
- Message gaps between transmissions
|
||||
- ESP32-C3 vs LED bar timing differences
|
||||
|
||||
### 4. Potential Fixes
|
||||
|
||||
#### Immediate fixes:
|
||||
1. **Reduce status message frequency** from 5 seconds to 30 seconds
|
||||
2. **Add ESP NOW debug logging** to see message transmission times
|
||||
3. **Remove blocking delays** on SPI failures
|
||||
|
||||
#### Firmware modifications needed:
|
||||
1. Change `pdMS_TO_TICKS(5000)` to `pdMS_TO_TICKS(30000)` for status messages
|
||||
2. Add debug printf statements for ESP NOW transmissions
|
||||
3. Make SPI error handling non-blocking
|
||||
|
||||
### 5. Testing Strategy
|
||||
1. Apply firmware patches
|
||||
2. Monitor ESP NOW traffic with debug scripts
|
||||
3. Observe pattern continuity
|
||||
4. Check timing of ESP NOW vs SPI operations
|
||||
|
||||
## Current Status
|
||||
- ESP32-C3 runs status sender task every 10 seconds
|
||||
- ESP32-C3 sends status via ESP NOW every 5 seconds on SPI errors
|
||||
- Original heartbeat in control server was disabled (this was correct)
|
||||
- Issue likely in ESP32-C3 firmware timing
|
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Control Server for Lighting Controller
|
||||
Handles lighting control logic and communicates with LED bars via WebSocket.
|
||||
Handles lighting control logic and communicates with LED bars via SPI or WebSocket.
|
||||
Receives commands from UI client via WebSocket.
|
||||
"""
|
||||
|
||||
@@ -12,14 +12,15 @@ import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import argparse
|
||||
from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS
|
||||
from color_utils import adjust_brightness
|
||||
from networking import SPIClient, WebSocketClient
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Configuration
|
||||
LED_SERVER_URI = "ws://192.168.4.1:80/ws"
|
||||
CONTROL_SERVER_PORT = 8765
|
||||
SOUND_CONTROL_HOST = "127.0.0.1"
|
||||
SOUND_CONTROL_PORT = 65433
|
||||
@@ -36,61 +37,37 @@ PATTERN_NAMES = {
|
||||
"radiate": "rd",
|
||||
"sequential_pulse": "sp",
|
||||
"alternating_phase": "ap",
|
||||
"segmented_movement": "sm",
|
||||
}
|
||||
|
||||
|
||||
class LEDController:
|
||||
"""Handles communication with LED bars via WebSocket."""
|
||||
|
||||
def __init__(self, led_server_uri):
|
||||
self.led_server_uri = led_server_uri
|
||||
self.websocket = None
|
||||
self.is_connected = False
|
||||
self.reconnect_task = None
|
||||
"""Handles communication with LED bars via SPI or WebSocket."""
|
||||
|
||||
def __init__(self, transport="spi", **kwargs):
|
||||
if transport == "spi":
|
||||
self.client = SPIClient(
|
||||
bus=kwargs.get('spi_bus', 0),
|
||||
device=kwargs.get('spi_device', 0),
|
||||
speed_hz=kwargs.get('spi_speed_hz', 1_000_000)
|
||||
)
|
||||
elif transport == "websocket":
|
||||
self.client = WebSocketClient(uri=kwargs.get('uri', 'ws://192.168.4.1/ws'))
|
||||
else:
|
||||
raise ValueError(f"Invalid transport: {transport}. Must be 'spi' or 'websocket'")
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return getattr(self.client, "is_connected", False)
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to LED server."""
|
||||
if self.is_connected and self.websocket:
|
||||
return
|
||||
|
||||
try:
|
||||
logging.info(f"Connecting to LED server at {self.led_server_uri}...")
|
||||
self.websocket = await websockets.connect(self.led_server_uri)
|
||||
self.is_connected = True
|
||||
logging.info("Connected to LED server")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to connect to LED server: {e}")
|
||||
self.is_connected = False
|
||||
self.websocket = None
|
||||
|
||||
await self.client.connect()
|
||||
|
||||
async def send_data(self, data):
|
||||
"""Send data to LED server."""
|
||||
if not self.is_connected or not self.websocket:
|
||||
logging.warning("Not connected to LED server. Attempting to reconnect...")
|
||||
await self.connect()
|
||||
if not self.is_connected:
|
||||
logging.error("Failed to reconnect to LED server. Cannot send data.")
|
||||
return
|
||||
|
||||
try:
|
||||
await self.websocket.send(json.dumps(data))
|
||||
logging.debug(f"Sent to LED server: {data}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send data to LED server: {e}")
|
||||
self.is_connected = False
|
||||
self.websocket = None
|
||||
# Attempt to reconnect
|
||||
await self.connect()
|
||||
await self.client.send_data(data)
|
||||
|
||||
async def close(self):
|
||||
"""Close LED server connection."""
|
||||
if self.websocket and self.is_connected:
|
||||
await self.websocket.close()
|
||||
self.is_connected = False
|
||||
self.websocket = None
|
||||
logging.info("Disconnected from LED server")
|
||||
await self.client.close()
|
||||
|
||||
|
||||
class SoundController:
|
||||
@@ -118,8 +95,8 @@ class SoundController:
|
||||
class LightingController:
|
||||
"""Main lighting control logic."""
|
||||
|
||||
def __init__(self):
|
||||
self.led_controller = LEDController(LED_SERVER_URI)
|
||||
def __init__(self, transport="spi", **transport_kwargs):
|
||||
self.led_controller = LEDController(transport=transport, **transport_kwargs)
|
||||
self.sound_controller = SoundController(SOUND_CONTROL_HOST, SOUND_CONTROL_PORT)
|
||||
|
||||
# Lighting state
|
||||
@@ -132,6 +109,7 @@ class LightingController:
|
||||
self.n1 = 10
|
||||
self.n2 = 10
|
||||
self.n3 = 1
|
||||
self.n4 = 1
|
||||
self.beat_index = 0
|
||||
self.beat_sending_enabled = True
|
||||
|
||||
@@ -159,6 +137,7 @@ class LightingController:
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
"n3": self.n3,
|
||||
"n4": self.n4,
|
||||
"s": self.beat_index % 256,
|
||||
}
|
||||
}
|
||||
@@ -181,7 +160,7 @@ class LightingController:
|
||||
|
||||
async def _send_normal_pattern(self):
|
||||
"""Send normal pattern to all bars."""
|
||||
patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate"]
|
||||
patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate", "segmented_movement"]
|
||||
|
||||
payload = {
|
||||
"d": {
|
||||
@@ -297,6 +276,8 @@ class LightingController:
|
||||
self.n2 = data["n2"]
|
||||
if "n3" in data:
|
||||
self.n3 = data["n3"]
|
||||
if "n4" in data:
|
||||
self.n4 = data["n4"]
|
||||
await self._request_param_update()
|
||||
|
||||
elif message_type == "delay_change":
|
||||
@@ -314,10 +295,11 @@ class LightingController:
|
||||
class ControlServer:
|
||||
"""WebSocket server for UI client communication and TCP server for sound."""
|
||||
|
||||
def __init__(self):
|
||||
self.lighting_controller = LightingController()
|
||||
def __init__(self, transport="spi", enable_heartbeat=False, **transport_kwargs):
|
||||
self.lighting_controller = LightingController(transport=transport, **transport_kwargs)
|
||||
self.clients = set()
|
||||
self.tcp_server = None
|
||||
self.enable_heartbeat = enable_heartbeat
|
||||
|
||||
async def handle_ui_client(self, websocket):
|
||||
"""Handle UI client WebSocket connection."""
|
||||
@@ -399,12 +381,30 @@ class ControlServer:
|
||||
# Connect to LED server
|
||||
await self.lighting_controller.led_controller.connect()
|
||||
|
||||
# Start servers and heartbeat task
|
||||
await asyncio.gather(
|
||||
self.start_websocket_server(),
|
||||
self.start_tcp_server(),
|
||||
self._heartbeat_loop()
|
||||
)
|
||||
# Start servers (optionally include heartbeat)
|
||||
websocket_task = asyncio.create_task(self._websocket_server_task())
|
||||
tcp_task = asyncio.create_task(self._tcp_server_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 _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 _tcp_server_task(self):
|
||||
"""Keep TCP server running."""
|
||||
await self.start_tcp_server()
|
||||
# Keep the server running indefinitely
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _heartbeat_loop(self):
|
||||
"""Send periodic heartbeats to keep LED connection alive."""
|
||||
@@ -427,7 +427,25 @@ class ControlServer:
|
||||
|
||||
async def main():
|
||||
"""Main entry point."""
|
||||
server = ControlServer()
|
||||
args = parse_arguments()
|
||||
|
||||
transport_kwargs = {}
|
||||
if args.transport == "spi":
|
||||
transport_kwargs = {
|
||||
'spi_bus': args.spi_bus,
|
||||
'spi_device': args.spi_device,
|
||||
'spi_speed_hz': args.spi_speed
|
||||
}
|
||||
elif args.transport == "websocket":
|
||||
transport_kwargs = {
|
||||
'uri': args.uri
|
||||
}
|
||||
|
||||
server = ControlServer(
|
||||
transport=args.transport,
|
||||
enable_heartbeat=args.enable_heartbeat,
|
||||
**transport_kwargs
|
||||
)
|
||||
|
||||
try:
|
||||
await server.run()
|
||||
@@ -439,5 +457,61 @@ async def main():
|
||||
await server.lighting_controller.led_controller.close()
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(description="Control Server for Lighting Controller")
|
||||
|
||||
# Transport selection
|
||||
transport_group = parser.add_argument_group("Transport Options")
|
||||
transport_group.add_argument(
|
||||
"--transport",
|
||||
choices=["spi", "websocket"],
|
||||
default="spi",
|
||||
help="Transport method for LED communication (default: spi)"
|
||||
)
|
||||
|
||||
# Control options
|
||||
control_group = parser.add_argument_group("Control Options")
|
||||
control_group.add_argument(
|
||||
"--enable-heartbeat",
|
||||
action="store_true",
|
||||
help="Enable heartbeat system (may cause pattern interruptions)"
|
||||
)
|
||||
|
||||
# SPI options
|
||||
spi_group = parser.add_argument_group("SPI Options")
|
||||
spi_group.add_argument(
|
||||
"--spi-bus",
|
||||
type=int,
|
||||
default=0,
|
||||
help="SPI bus number (default: 0)"
|
||||
)
|
||||
spi_group.add_argument(
|
||||
"--spi-device",
|
||||
type=int,
|
||||
default=0,
|
||||
help="SPI device number (default: 0)"
|
||||
)
|
||||
spi_group.add_argument(
|
||||
"--spi-speed",
|
||||
type=int,
|
||||
default=1_000_000,
|
||||
help="SPI speed in Hz (default: 1000000)"
|
||||
)
|
||||
|
||||
# WebSocket options
|
||||
ws_group = parser.add_argument_group("WebSocket Options")
|
||||
ws_group.add_argument(
|
||||
"--uri",
|
||||
type=str,
|
||||
default="ws://192.168.4.1/ws",
|
||||
help="WebSocket URI for LED communication (default: ws://192.168.4.1/ws)"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
#
|
@@ -1,53 +1,132 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
|
||||
try:
|
||||
import spidev
|
||||
except Exception as e:
|
||||
spidev = None
|
||||
|
||||
|
||||
class SPIClient:
|
||||
"""SPI transport client."""
|
||||
def __init__(self, bus=None, device=None, speed_hz=None):
|
||||
# SPI configuration (defaults can be overridden by args or env)
|
||||
self.bus = 0 if bus is None else int(bus)
|
||||
self.device = 0 if device is None else int(device)
|
||||
self.speed_hz = (
|
||||
int(os.getenv("SPI_SPEED_HZ", "1000000")) if speed_hz is None else int(speed_hz)
|
||||
)
|
||||
|
||||
self.spi = None
|
||||
self.is_connected = False
|
||||
|
||||
async def connect(self):
|
||||
"""Initializes the SPI connection."""
|
||||
if self.is_connected and self.spi:
|
||||
return
|
||||
|
||||
if spidev is None:
|
||||
print("spidev not available; cannot open SPI")
|
||||
self.is_connected = False
|
||||
self.spi = None
|
||||
return
|
||||
|
||||
try:
|
||||
spi = spidev.SpiDev()
|
||||
spi.open(self.bus, self.device)
|
||||
spi.max_speed_hz = self.speed_hz
|
||||
spi.mode = 0
|
||||
spi.bits_per_word = 8
|
||||
self.spi = spi
|
||||
self.is_connected = True
|
||||
print(f"SPI connected: bus={self.bus} device={self.device} speed={self.speed_hz}Hz mode=0")
|
||||
except Exception as e:
|
||||
print(f"Error opening SPI: {e}")
|
||||
self.is_connected = False
|
||||
self.spi = None
|
||||
|
||||
async def send_data(self, data):
|
||||
"""Sends a JSON object over SPI as UTF-8 bytes."""
|
||||
if not self.is_connected or not self.spi:
|
||||
await self.connect()
|
||||
if not self.is_connected:
|
||||
print("SPI not connected; cannot send")
|
||||
return
|
||||
|
||||
try:
|
||||
json_str = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
payload = list(json_str.encode("utf-8"))
|
||||
if not payload:
|
||||
return
|
||||
# Keep payload comfortably below ESP-NOW max; trim if necessary
|
||||
if len(payload) > 240:
|
||||
payload = payload[:240]
|
||||
self.spi.xfer2(payload)
|
||||
except Exception as e:
|
||||
print(f"SPI send failed: {e}")
|
||||
# Attempt simple reopen on next call
|
||||
self.is_connected = False
|
||||
self.spi = None
|
||||
|
||||
async def close(self):
|
||||
"""Closes the SPI connection."""
|
||||
try:
|
||||
if self.spi:
|
||||
self.spi.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.is_connected = False
|
||||
self.spi = None
|
||||
|
||||
|
||||
class WebSocketClient:
|
||||
def __init__(self, uri):
|
||||
self.uri = uri
|
||||
"""WebSocket transport client."""
|
||||
def __init__(self, uri=None, *, bus=None, device=None, speed_hz=None):
|
||||
self.uri = uri or "ws://192.168.4.1/ws"
|
||||
self.websocket = None
|
||||
self.is_connected = False
|
||||
|
||||
async def connect(self):
|
||||
"""Establishes the WebSocket connection."""
|
||||
"""Initializes the WebSocket connection."""
|
||||
if self.is_connected and self.websocket:
|
||||
print("Already connected.")
|
||||
return
|
||||
|
||||
try:
|
||||
print(f"Connecting to {self.uri}...")
|
||||
self.websocket = await websockets.connect(self.uri)
|
||||
self.is_connected = True
|
||||
print("WebSocket connected.")
|
||||
except (ConnectionError, websockets.exceptions.ConnectionClosedOK) as e:
|
||||
print(f"Error connecting: {e}")
|
||||
print(f"WebSocket connected: {self.uri}")
|
||||
except Exception as e:
|
||||
print(f"Error opening WebSocket: {e}")
|
||||
self.is_connected = False
|
||||
self.websocket = None
|
||||
|
||||
async def send_data(self, data):
|
||||
print(data)
|
||||
"""Sends data over the open WebSocket connection."""
|
||||
"""Sends a JSON object over WebSocket."""
|
||||
if not self.is_connected or not self.websocket:
|
||||
print("WebSocket not connected. Attempting to reconnect...")
|
||||
await self.connect()
|
||||
if not self.is_connected:
|
||||
print("Failed to reconnect. Cannot send data.")
|
||||
print("WebSocket not connected; cannot send")
|
||||
return
|
||||
|
||||
try:
|
||||
await self.websocket.send(json.dumps(data))
|
||||
print(f"Sent: {data}")
|
||||
except (ConnectionError, websockets.exceptions.ConnectionClosed) as e:
|
||||
print(f"Error sending data: {e}")
|
||||
json_str = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
await self.websocket.send(json_str)
|
||||
print(f"WebSocket sent: {json_str}")
|
||||
except Exception as e:
|
||||
print(f"WebSocket send failed: {e}")
|
||||
# Attempt simple reopen on next call
|
||||
self.is_connected = False
|
||||
self.websocket = None # Reset connection on error
|
||||
await self.connect() # Attempt to reconnect
|
||||
self.websocket = None
|
||||
|
||||
async def close(self):
|
||||
"""Closes the WebSocket connection."""
|
||||
if self.websocket and self.is_connected:
|
||||
await self.websocket.close()
|
||||
self.is_connected = False
|
||||
self.websocket = None
|
||||
print("WebSocket closed.")
|
||||
try:
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.is_connected = False
|
||||
self.websocket = None
|
11
src/sound.py
11
src/sound.py
@@ -5,6 +5,7 @@ import aubio
|
||||
import numpy as np
|
||||
from time import sleep
|
||||
import json
|
||||
import argparse
|
||||
import socket
|
||||
import time
|
||||
import logging # Added logging import
|
||||
@@ -24,7 +25,7 @@ SOUND_CONTROL_HOST = "127.0.0.1"
|
||||
SOUND_CONTROL_PORT = 65433
|
||||
|
||||
class SoundBeatDetector:
|
||||
def __init__(self, tcp_host: str, tcp_port: int):
|
||||
def __init__(self, tcp_host: str, tcp_port: int, *, input_device: int | None = None):
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_port = tcp_port
|
||||
self.tcp_socket = None
|
||||
@@ -34,7 +35,7 @@ class SoundBeatDetector:
|
||||
|
||||
self.bufferSize = 512
|
||||
self.windowSizeMultiple = 2
|
||||
self.audioInputDeviceIndex = 7
|
||||
self.audioInputDeviceIndex = 7 if input_device is None else int(input_device)
|
||||
self.audioInputChannels = 1
|
||||
|
||||
self.pa = pyaudio.PyAudio()
|
||||
@@ -196,11 +197,15 @@ class SoundBeatDetector:
|
||||
# Removed async def run(self)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Sound beat detector")
|
||||
parser.add_argument("--input-device", type=int, help="Audio input device index to use")
|
||||
args = parser.parse_args()
|
||||
|
||||
# TCP Server Configuration (should match midi.py)
|
||||
MIDI_TCP_HOST = "127.0.0.1"
|
||||
MIDI_TCP_PORT = 65432
|
||||
|
||||
sound_detector = SoundBeatDetector(MIDI_TCP_HOST, MIDI_TCP_PORT)
|
||||
sound_detector = SoundBeatDetector(MIDI_TCP_HOST, MIDI_TCP_PORT, input_device=args.input_device)
|
||||
logging.info("Starting SoundBeatDetector...")
|
||||
try:
|
||||
sound_detector.start_stream()
|
||||
|
1126
src/ui_client.py
1126
src/ui_client.py
File diff suppressed because it is too large
Load Diff
132
test/test_control_server.py
Normal file
132
test/test_control_server.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for control_server.py UI WebSocket API.
|
||||
|
||||
Starts a client to localhost:8765 and sends a small sequence of UI commands:
|
||||
- pattern_change
|
||||
- color_change
|
||||
- brightness_change
|
||||
- parameter_change (n1/n2/n3/n4)
|
||||
|
||||
Usage examples:
|
||||
python test/test_control_server.py --pattern on --r 255 --g 0 --b 0 --brightness 150 --n1 5 --n2 5 --n3 1 --n4 2
|
||||
python test/test_control_server.py --pattern rainbow
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import websockets
|
||||
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):
|
||||
msgs = []
|
||||
# Prioritize delay_change so a single send applies delay when both are provided
|
||||
if args.delay is not None:
|
||||
msgs.append({"type": "delay_change", "data": {"delay": args.delay}})
|
||||
if args.pattern is not None:
|
||||
msgs.append({"type": "pattern_change", "data": {"pattern": args.pattern}})
|
||||
|
||||
# Optional colors flag: parse first color and map to r,g,b if explicit r/g/b not given
|
||||
if args.colors and (args.r is None and args.g is None and args.b is None):
|
||||
hex_re = re.compile(r"^#?[0-9a-fA-F]{6}$")
|
||||
first = args.colors.split(',')[0].strip()
|
||||
if hex_re.match(first):
|
||||
v = first[1:] if first.startswith('#') else first
|
||||
args.r = int(v[0:2], 16)
|
||||
args.g = int(v[2:4], 16)
|
||||
args.b = int(v[4:6], 16)
|
||||
|
||||
if args.r is not None or args.g is not None or args.b is not None:
|
||||
payload = {}
|
||||
if args.r is not None:
|
||||
payload["r"] = args.r
|
||||
if args.g is not None:
|
||||
payload["g"] = args.g
|
||||
if args.b is not None:
|
||||
payload["b"] = args.b
|
||||
msgs.append({"type": "color_change", "data": payload})
|
||||
|
||||
if args.brightness is not None:
|
||||
msgs.append({"type": "brightness_change", "data": {"brightness": args.brightness}})
|
||||
|
||||
if any(v is not None for v in (args.n1, args.n2, args.n3, args.n4)):
|
||||
payload = {}
|
||||
if args.n1 is not None:
|
||||
payload["n1"] = args.n1
|
||||
if args.n2 is not None:
|
||||
payload["n2"] = args.n2
|
||||
if args.n3 is not None:
|
||||
payload["n3"] = args.n3
|
||||
if args.n4 is not None:
|
||||
payload["n4"] = args.n4
|
||||
msgs.append({"type": "parameter_change", "data": payload})
|
||||
|
||||
return msgs
|
||||
|
||||
|
||||
async def run_test(uri: str, messages: list[dict], sleep_s: float):
|
||||
async with websockets.connect(uri) as ws:
|
||||
# Send all messages with a delay between them
|
||||
for m in messages:
|
||||
await ws.send(json.dumps(m))
|
||||
if len(messages) > 1:
|
||||
await asyncio.sleep(sleep_s)
|
||||
|
||||
|
||||
def parse_args():
|
||||
|
||||
p = argparse.ArgumentParser(description="Send UI commands to control_server WebSocket")
|
||||
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")
|
||||
p.add_argument("--b", type=int, help="Blue 0-255 for color_change")
|
||||
p.add_argument("--brightness", type=int, help="Brightness value for brightness_change")
|
||||
p.add_argument("--delay", type=int, help="Pattern delay (ms) via delay_change")
|
||||
p.add_argument("--n1", type=int, help="n1 for parameter_change")
|
||||
p.add_argument("--n2", type=int, help="n2 for parameter_change")
|
||||
p.add_argument("--n3", type=int, help="n3 for parameter_change")
|
||||
p.add_argument("--n4", type=int, help="n4 for parameter_change")
|
||||
p.add_argument("--sleep", type=float, default=0.2, help="Seconds to wait between messages (default 0.2)")
|
||||
p.add_argument("--colors", help="Comma-separated hex colors (uses first as r,g,b)")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
messages = build_messages(args)
|
||||
if not messages:
|
||||
# Default minimal test: just pattern_change to 'on'
|
||||
messages = [{"type": "pattern_change", "data": {"pattern": "on"}}]
|
||||
asyncio.run(run_test(args.uri, messages, args.sleep))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
118
test/test_networking.py
Normal file
118
test/test_networking.py
Normal file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Networking SPI test: builds a legacy led-bar payload and sends it via src/networking.py SPI client.
|
||||
|
||||
Usage examples:
|
||||
python test/test_networking.py --type b --pattern on --colors ff0000,00ff00,0000ff
|
||||
python test/test_networking.py --type u --brightness 128 --delay 50
|
||||
python test/test_networking.py --data '{"d":{"t":"b","pt":"off"}}'
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Import SPI networking client
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(SCRIPT_DIR)
|
||||
if ROOT_DIR not in sys.path:
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
from src.networking import WebSocketClient # SPI client with same API
|
||||
|
||||
|
||||
HEX6_RE = re.compile(r"^[0-9a-fA-F]{6}$")
|
||||
|
||||
|
||||
def parse_hex6_to_rgb(value: str):
|
||||
v = value.strip()
|
||||
if v.startswith("0x") or v.startswith("0X"):
|
||||
v = v[2:]
|
||||
if v.startswith("#"):
|
||||
v = v[1:]
|
||||
if not HEX6_RE.match(v):
|
||||
raise ValueError(f"Invalid hex color: {value}")
|
||||
return [int(v[0:2], 16), int(v[2:4], 16), int(v[4:6], 16)]
|
||||
|
||||
|
||||
def build_payload(args: argparse.Namespace) -> dict:
|
||||
if args.data:
|
||||
return json.loads(args.data)
|
||||
if args.file:
|
||||
with open(args.file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
d = {"t": args.type}
|
||||
if args.pattern:
|
||||
d["pt"] = args.pattern
|
||||
if args.brightness is not None:
|
||||
d["br"] = int(args.brightness)
|
||||
if args.delay is not None:
|
||||
d["dl"] = int(args.delay)
|
||||
if args.n1 is not None:
|
||||
d["n1"] = int(args.n1)
|
||||
if args.n2 is not None:
|
||||
d["n2"] = int(args.n2)
|
||||
if args.n3 is not None:
|
||||
d["n3"] = int(args.n3)
|
||||
if args.step is not None:
|
||||
d["s"] = int(args.step)
|
||||
|
||||
if args.colors:
|
||||
items = [c.strip() for c in args.colors.split(',') if c.strip()]
|
||||
d["cl"] = [parse_hex6_to_rgb(c) for c in items]
|
||||
|
||||
payload = {"d": d}
|
||||
|
||||
if args.name:
|
||||
# For convenience, mirror defaults as per-device override
|
||||
payload[args.name] = {k: v for k, v in d.items() if k != "t"}
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Send SPI networking test payload (legacy led-bar format)")
|
||||
src = p.add_mutually_exclusive_group()
|
||||
src.add_argument("--data", help="Raw JSON payload to send")
|
||||
src.add_argument("--file", help="Path to JSON file to send")
|
||||
|
||||
p.add_argument("--type", choices=["b", "u"], default="b", help="Message type (beat/update)")
|
||||
p.add_argument("--pattern", help="Pattern name (pt)")
|
||||
p.add_argument("--brightness", type=int, help="Brightness (br)")
|
||||
p.add_argument("--delay", type=int, help="Delay (dl)")
|
||||
p.add_argument("--n1", type=int, help="n1")
|
||||
p.add_argument("--n2", type=int, help="n2")
|
||||
p.add_argument("--n3", type=int, help="n3")
|
||||
p.add_argument("--step", type=int, help="step (s)")
|
||||
p.add_argument("--colors", help="Comma-separated hex colors for cl (e.g. ff0000,00ff00,0000ff)")
|
||||
p.add_argument("--name", help="Per-device override key (device name)")
|
||||
|
||||
# SPI config overrides
|
||||
p.add_argument("--bus", type=int, default=0, help="SPI bus (default 0)")
|
||||
p.add_argument("--device", type=int, default=0, help="SPI device/CE (default 0)")
|
||||
p.add_argument("--speed", type=int, default=1_000_000, help="SPI speed Hz (default 1MHz)")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
async def main_async() -> int:
|
||||
args = parse_args()
|
||||
payload = build_payload(args)
|
||||
|
||||
client = WebSocketClient(uri=None, bus=args.bus, device=args.device, speed_hz=args.speed)
|
||||
await client.connect()
|
||||
await client.send_data(payload)
|
||||
await client.close()
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
return asyncio.run(main_async())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
Reference in New Issue
Block a user