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:
Pi User
2025-10-03 19:54:43 +13:00
parent f4e9f8fff7
commit e4a83e8f0d
7 changed files with 485 additions and 27 deletions

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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())
#

View File

@@ -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

View File

@@ -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()