Add segmented_movement pattern with n4 parameter support
- Add n4 parameter to control server, LED bar receiver, and test script - Create segmented_movement pattern with alternating forward/backward movement - Pattern supports n1 (segment length), n2 (spacing), n3 (forward speed), n4 (backward speed) - Fix test script to send all messages instead of just the first one - Add segmented_movement to patterns_needing_params for proper parameter transmission - Pattern intelligently handles all cases: alternating, forward-only, backward-only, or static - Implements repeating segments with configurable spacing across LED strip
This commit is contained in:
6
Pipfile
6
Pipfile
@@ -23,9 +23,13 @@ python_version = "3.11"
|
|||||||
run = "python src/main.py"
|
run = "python src/main.py"
|
||||||
ui = "python src/ui_client.py"
|
ui = "python src/ui_client.py"
|
||||||
control = "python src/control_server.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"
|
sound = "python src/sound.py"
|
||||||
dev-ui = 'watchfiles "python src/ui_client.py" src'
|
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 = "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'"
|
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'"
|
monitor-esp32 = "bash -c 'source $HOME/esp/esp-idf/export.sh && cd esp32 && idf.py -p ${ESPPORT:-/dev/ttyACM0} monitor'"
|
||||||
|
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
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Control Server for Lighting Controller
|
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.
|
Receives commands from UI client via WebSocket.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -12,9 +12,10 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import argparse
|
||||||
from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS
|
from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS
|
||||||
from color_utils import adjust_brightness
|
from color_utils import adjust_brightness
|
||||||
from networking import WebSocketClient as SPIClient # SPI transport client
|
from networking import SPIClient, WebSocketClient
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
@@ -36,14 +37,24 @@ PATTERN_NAMES = {
|
|||||||
"radiate": "rd",
|
"radiate": "rd",
|
||||||
"sequential_pulse": "sp",
|
"sequential_pulse": "sp",
|
||||||
"alternating_phase": "ap",
|
"alternating_phase": "ap",
|
||||||
|
"segmented_movement": "sm",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LEDController:
|
class LEDController:
|
||||||
"""Handles communication with LED bars via SPI (through ESP32 relay)."""
|
"""Handles communication with LED bars via SPI or WebSocket."""
|
||||||
|
|
||||||
def __init__(self, spi_bus: int = 0, spi_device: int = 0, spi_speed_hz: int = 1_000_000):
|
def __init__(self, transport="spi", **kwargs):
|
||||||
self.client = SPIClient(bus=spi_bus, device=spi_device, speed_hz=spi_speed_hz)
|
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
|
@property
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
@@ -84,9 +95,8 @@ class SoundController:
|
|||||||
class LightingController:
|
class LightingController:
|
||||||
"""Main lighting control logic."""
|
"""Main lighting control logic."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, transport="spi", **transport_kwargs):
|
||||||
# SPI defaults: bus 0, CE0, 1MHz; adjust here if needed
|
self.led_controller = LEDController(transport=transport, **transport_kwargs)
|
||||||
self.led_controller = LEDController(spi_bus=0, spi_device=0, spi_speed_hz=1_000_000)
|
|
||||||
self.sound_controller = SoundController(SOUND_CONTROL_HOST, SOUND_CONTROL_PORT)
|
self.sound_controller = SoundController(SOUND_CONTROL_HOST, SOUND_CONTROL_PORT)
|
||||||
|
|
||||||
# Lighting state
|
# Lighting state
|
||||||
@@ -99,6 +109,7 @@ class LightingController:
|
|||||||
self.n1 = 10
|
self.n1 = 10
|
||||||
self.n2 = 10
|
self.n2 = 10
|
||||||
self.n3 = 1
|
self.n3 = 1
|
||||||
|
self.n4 = 1
|
||||||
self.beat_index = 0
|
self.beat_index = 0
|
||||||
self.beat_sending_enabled = True
|
self.beat_sending_enabled = True
|
||||||
|
|
||||||
@@ -126,6 +137,7 @@ class LightingController:
|
|||||||
"n1": self.n1,
|
"n1": self.n1,
|
||||||
"n2": self.n2,
|
"n2": self.n2,
|
||||||
"n3": self.n3,
|
"n3": self.n3,
|
||||||
|
"n4": self.n4,
|
||||||
"s": self.beat_index % 256,
|
"s": self.beat_index % 256,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +160,7 @@ class LightingController:
|
|||||||
|
|
||||||
async def _send_normal_pattern(self):
|
async def _send_normal_pattern(self):
|
||||||
"""Send normal pattern to all bars."""
|
"""Send normal pattern to all bars."""
|
||||||
patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate"]
|
patterns_needing_params = ["alternating", "flicker", "n_chase", "rainbow", "radiate", "segmented_movement"]
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"d": {
|
"d": {
|
||||||
@@ -264,6 +276,8 @@ class LightingController:
|
|||||||
self.n2 = data["n2"]
|
self.n2 = data["n2"]
|
||||||
if "n3" in data:
|
if "n3" in data:
|
||||||
self.n3 = data["n3"]
|
self.n3 = data["n3"]
|
||||||
|
if "n4" in data:
|
||||||
|
self.n4 = data["n4"]
|
||||||
await self._request_param_update()
|
await self._request_param_update()
|
||||||
|
|
||||||
elif message_type == "delay_change":
|
elif message_type == "delay_change":
|
||||||
@@ -281,10 +295,11 @@ class LightingController:
|
|||||||
class ControlServer:
|
class ControlServer:
|
||||||
"""WebSocket server for UI client communication and TCP server for sound."""
|
"""WebSocket server for UI client communication and TCP server for sound."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, transport="spi", enable_heartbeat=False, **transport_kwargs):
|
||||||
self.lighting_controller = LightingController()
|
self.lighting_controller = LightingController(transport=transport, **transport_kwargs)
|
||||||
self.clients = set()
|
self.clients = set()
|
||||||
self.tcp_server = None
|
self.tcp_server = None
|
||||||
|
self.enable_heartbeat = enable_heartbeat
|
||||||
|
|
||||||
async def handle_ui_client(self, websocket):
|
async def handle_ui_client(self, websocket):
|
||||||
"""Handle UI client WebSocket connection."""
|
"""Handle UI client WebSocket connection."""
|
||||||
@@ -366,12 +381,30 @@ class ControlServer:
|
|||||||
# Connect to LED server
|
# Connect to LED server
|
||||||
await self.lighting_controller.led_controller.connect()
|
await self.lighting_controller.led_controller.connect()
|
||||||
|
|
||||||
# Start servers and heartbeat task
|
# Start servers (optionally include heartbeat)
|
||||||
await asyncio.gather(
|
websocket_task = asyncio.create_task(self._websocket_server_task())
|
||||||
self.start_websocket_server(),
|
tcp_task = asyncio.create_task(self._tcp_server_task())
|
||||||
self.start_tcp_server(),
|
|
||||||
self._heartbeat_loop()
|
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):
|
async def _heartbeat_loop(self):
|
||||||
"""Send periodic heartbeats to keep LED connection alive."""
|
"""Send periodic heartbeats to keep LED connection alive."""
|
||||||
@@ -394,7 +427,25 @@ class ControlServer:
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
"""Main entry point."""
|
"""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:
|
try:
|
||||||
await server.run()
|
await server.run()
|
||||||
@@ -406,5 +457,61 @@ async def main():
|
|||||||
await server.lighting_controller.led_controller.close()
|
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__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
|
#
|
@@ -1,6 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import spidev
|
import spidev
|
||||||
@@ -8,8 +10,9 @@ except Exception as e:
|
|||||||
spidev = None
|
spidev = None
|
||||||
|
|
||||||
|
|
||||||
class WebSocketClient:
|
class SPIClient:
|
||||||
def __init__(self, uri=None, *, bus=None, device=None, speed_hz=None):
|
"""SPI transport client."""
|
||||||
|
def __init__(self, bus=None, device=None, speed_hz=None):
|
||||||
# SPI configuration (defaults can be overridden by args or env)
|
# SPI configuration (defaults can be overridden by args or env)
|
||||||
self.bus = 0 if bus is None else int(bus)
|
self.bus = 0 if bus is None else int(bus)
|
||||||
self.device = 0 if device is None else int(device)
|
self.device = 0 if device is None else int(device)
|
||||||
@@ -77,3 +80,53 @@ class WebSocketClient:
|
|||||||
pass
|
pass
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
self.spi = None
|
self.spi = None
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClient:
|
||||||
|
"""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):
|
||||||
|
"""Initializes the WebSocket connection."""
|
||||||
|
if self.is_connected and self.websocket:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.websocket = await websockets.connect(self.uri)
|
||||||
|
self.is_connected = True
|
||||||
|
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):
|
||||||
|
"""Sends a JSON object over WebSocket."""
|
||||||
|
if not self.is_connected or not self.websocket:
|
||||||
|
await self.connect()
|
||||||
|
if not self.is_connected:
|
||||||
|
print("WebSocket not connected; cannot send")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Closes the WebSocket connection."""
|
||||||
|
try:
|
||||||
|
if self.websocket:
|
||||||
|
await self.websocket.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.is_connected = False
|
||||||
|
self.websocket = None
|
@@ -6,10 +6,10 @@ Starts a client to localhost:8765 and sends a small sequence of UI commands:
|
|||||||
- pattern_change
|
- pattern_change
|
||||||
- color_change
|
- color_change
|
||||||
- brightness_change
|
- brightness_change
|
||||||
- parameter_change (n1/n2/n3)
|
- parameter_change (n1/n2/n3/n4)
|
||||||
|
|
||||||
Usage examples:
|
Usage examples:
|
||||||
python test/test_control_server.py --pattern on --r 255 --g 0 --b 0 --brightness 150 --n1 5 --n2 5 --n3 1
|
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
|
python test/test_control_server.py --pattern rainbow
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ def build_messages(args):
|
|||||||
if args.brightness is not None:
|
if args.brightness is not None:
|
||||||
msgs.append({"type": "brightness_change", "data": {"brightness": args.brightness}})
|
msgs.append({"type": "brightness_change", "data": {"brightness": args.brightness}})
|
||||||
|
|
||||||
if any(v is not None for v in (args.n1, args.n2, args.n3)):
|
if any(v is not None for v in (args.n1, args.n2, args.n3, args.n4)):
|
||||||
payload = {}
|
payload = {}
|
||||||
if args.n1 is not None:
|
if args.n1 is not None:
|
||||||
payload["n1"] = args.n1
|
payload["n1"] = args.n1
|
||||||
@@ -59,6 +59,8 @@ def build_messages(args):
|
|||||||
payload["n2"] = args.n2
|
payload["n2"] = args.n2
|
||||||
if args.n3 is not None:
|
if args.n3 is not None:
|
||||||
payload["n3"] = args.n3
|
payload["n3"] = args.n3
|
||||||
|
if args.n4 is not None:
|
||||||
|
payload["n4"] = args.n4
|
||||||
msgs.append({"type": "parameter_change", "data": payload})
|
msgs.append({"type": "parameter_change", "data": payload})
|
||||||
|
|
||||||
return msgs
|
return msgs
|
||||||
@@ -66,9 +68,11 @@ def build_messages(args):
|
|||||||
|
|
||||||
async def run_test(uri: str, messages: list[dict], sleep_s: float):
|
async def run_test(uri: str, messages: list[dict], sleep_s: float):
|
||||||
async with websockets.connect(uri) as ws:
|
async with websockets.connect(uri) as ws:
|
||||||
# Send only one message (first in list)
|
# Send all messages with a delay between them
|
||||||
m = messages[0]
|
for m in messages:
|
||||||
await ws.send(json.dumps(m))
|
await ws.send(json.dumps(m))
|
||||||
|
if len(messages) > 1:
|
||||||
|
await asyncio.sleep(sleep_s)
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
@@ -83,6 +87,7 @@ def parse_args():
|
|||||||
p.add_argument("--n1", type=int, help="n1 for parameter_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("--n2", type=int, help="n2 for parameter_change")
|
||||||
p.add_argument("--n3", type=int, help="n3 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("--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)")
|
p.add_argument("--colors", help="Comma-separated hex colors (uses first as r,g,b)")
|
||||||
return p.parse_args()
|
return p.parse_args()
|
||||||
|
Reference in New Issue
Block a user