Done a heap

This commit is contained in:
Jimmy 2025-07-12 00:55:30 +12:00
parent 65774837c7
commit c77fd30f8f
6 changed files with 841 additions and 160 deletions

View File

@ -1,23 +1,68 @@
def adjust_brightness(color, brightness):
"""Adjust brightness of an RGB color."""
r, g, b = color
return (int(r * brightness/255), int(g * brightness/255), int(b * brightness/255))
def adjust_brightness(rgb_color, brightness):
r, g, b = rgb_color
# Convert 0-255 brightness to a scale of 0-1
scale_factor = brightness / 255.0
def rgb_to_hex(color):
"""Convert an RGB color to hex format."""
return '#{:02x}{:02x}{:02x}'.format(color[0], color[1], color[2])
adjusted_r = int(r * scale_factor)
adjusted_g = int(g * scale_factor)
adjusted_b = int(b * scale_factor)
# Ensure values are within 0-255
adjusted_r = max(0, min(255, adjusted_r))
adjusted_g = max(0, min(255, adjusted_g))
adjusted_b = max(0, min(255, adjusted_b))
return (adjusted_r, adjusted_g, adjusted_b)
def generate_color_transition(start_color, end_color, steps):
"""Generate a list of colors transitioning from start_color to end_color."""
r1, g1, b1 = start_color
r2, g2, b2 = end_color
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
"""Converts a hex color string (e.g., "#RRGGBB") to an RGB tuple."""
hex_color = hex_color.lstrip('#')
return int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
transition_colors = []
for i in range(steps):
r = r1 + (r2 - r1) * i // (steps - 1)
g = g1 + (g2 - g1) * i // (steps - 1)
b = b1 + (b2 - b1) * i // (steps - 1)
transition_colors.append((r, g, b))
return transition_colors
def rgb_to_hex(r: int, g: int, b: int) -> str:
"""Converts an RGB tuple to a hex color string (e.g., "#RRGGBB")."""
return f"#{r:02x}{g:02x}{b:02x}"
def get_contrast_text_color(background_hex_color: str) -> str:
"""
Determines whether black or white text is more readable on a given background color.
Uses the WCAG 2.0 contrast recommendations (Luminosity calculation).
"""
r, g, b = hex_to_rgb(background_hex_color)
# Convert RGB to sRGB (0-1 range)
# The linear RGB values are normalized by dividing by 255
r_linear = r / 255.0
g_linear = g / 255.0
b_linear = b / 255.0
# Apply the sRGB to linear conversion for gamma correction
# This is a simplified approximation for readability, a more accurate one involves if/else for values <= 0.03928
# For a general "light vs dark" determination, this simplified approach is often sufficient.
# The formula used here is often simplified as (R*0.299 + G*0.587 + B*0.114) for quick luminance.
# A more precise relative luminance (L) calculation:
def srgb_to_linear(c):
if c <= 0.03928:
return c / 12.92
else:
return ((c + 0.055) / 1.055) ** 2.4
L = (0.2126 * srgb_to_linear(r_linear) +
0.7152 * srgb_to_linear(g_linear) +
0.0722 * srgb_to_linear(b_linear))
# For general UI elements, a luminance threshold around 0.179 (sqrt(0.032)) is often used
# or simply checking if (R*0.299 + G*0.587 + B*0.114) is > 186 for light background / dark text
# A common rule of thumb for perceived brightness (closer to the one used in many UIs):
# (R*299 + G*587 + B*114) / 1000
# Let's use a simpler luminance check based on the first example's intention:
# If the perceived brightness is above a certain threshold, use black text. Otherwise, use white.
# A simpler luminance check often used for text contrast:
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
if luminance > 0.5: # Adjust this threshold as needed for perceived contrast
return "black"
else:
return "white"

View File

@ -1,49 +0,0 @@
import json
import os
default_config = {
'color': (255, 0, 0),
'brightness': 1.0,
'color_format': 'RGB',
'servers': [('192.168.0.201', 80)]
}
config_file = 'config.json'
class ConfigHandler:
def load_config(self):
"""Load color, brightness, format, and server settings from config file."""
if os.path.exists(config_file):
with open(config_file, 'r') as f:
return json.load(f)
else:
return default_config
def save_config(self, color, brightness, color_format, servers):
"""Save color, brightness, format, and server settings to config file."""
config = {
'color': color,
'brightness': brightness,
'color_format': color_format,
'servers': servers
}
with open(config_file, 'w') as f:
json.dump(config, f)
print("Configuration saved.")
def add_server(self, ip, port):
"""Add a new server IP and port."""
config = self.load_config()
config['servers'].append((ip, port))
self.save_config(config['color'], config['brightness'], config['color_format'], config['servers'])
print(f"Server {ip}:{port} added.")
def remove_server(self, ip, port):
"""Remove an existing server IP and port."""
config = self.load_config()
if (ip, port) in config['servers']:
config['servers'].remove((ip, port))
self.save_config(config['color'], config['brightness'], config['color_format'], config['servers'])
print(f"Server {ip}:{port} removed.")
else:
print(f"Server {ip}:{port} not found.")

748
main.py
View File

@ -1,118 +1,726 @@
import asyncio
import tkinter as tk
from tkinter import ttk
from tkinter import ttk, messagebox # Import messagebox for confirmations
import json
from async_tkinter_loop import async_handler, async_mainloop
from networking import send_to_server
from networking import WebSocketClient
import color_utils
from settings import Settings
import time
# Dark theme colors (unchanged)
bg_color = "#2e2e2e"
fg_color = "white"
trough_color_red = "#4a0000"
trough_color_green = "#004a00"
trough_color_blue = "#00004a"
trough_color_brightness = "#4a4a4a"
trough_color_delay = "#4a4a4a"
active_bg_color = "#4a4a4a"
highlight_pattern_color = "#6a5acd"
# New color for active color in palette
active_palette_color_border = "#FFD700" # Gold color
class App:
def __init__(self) -> None:
self.settings = Settings()
self.root = tk.Tk()
self.root.attributes('-fullscreen', True)
self.root.configure(bg=bg_color) # Set background color
self.root.attributes("-fullscreen", True)
self.root.configure(bg=bg_color)
# List of servers (IP, Port)
self.lightgroups = {
"light1": {"ids": [0], "settings": {"color": "#00ff00"}},
"light2": {"ids": [0], "settings": {"color": "#ff0000"}}
}
# Debouncing variables (remain the same)
self.last_rgb_update_time = 0
self.rgb_update_interval_ms = 100
self.pending_rgb_update_id = None
# Create Notebook for tabs
self.last_brightness_update_time = 0
self.brightness_update_interval_ms = 100
self.pending_brightness_update_id = None
self.last_delay_update_time = 0
self.delay_update_interval_ms = 100
self.pending_delay_update_id = None
# --- WebSocketClient ---
self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws")
self.root.after(100, async_handler(self.websocket_client.connect))
# Configure ttk style (unchanged)
style = ttk.Style()
style.theme_use("alt")
style.configure(".", background=bg_color, foreground=fg_color, font=("Arial", 14))
style.configure("TNotebook", background=bg_color, borderwidth=0)
style.configure(
"TNotebook.Tab", background=bg_color, foreground=fg_color, font=("Arial", 30), padding=[10, 5]
)
style.map("TNotebook.Tab", background=[("selected", active_bg_color)], foreground=[("selected", fg_color)])
style.configure("TFrame", background=bg_color)
# Create Notebook for tabs (unchanged)
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(expand=1, fill="both")
# Create a tab for each server
self.tabs = {}
for key, value in self.lightgroups.items():
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text=key)
self.create_sliders(tab)
self.tabs[key] = tab
self.create_tabs()
self.notebook.bind("<<NotebookTabChanged>>", self.on_tab_change)
# Add Reload Config Button (unchanged)
reload_button = tk.Button(
self.root,
text="Reload Config",
command=self.reload_config,
bg=active_bg_color,
fg=fg_color,
font=("Arial", 20),
padx=20,
pady=10,
relief=tk.FLAT,
)
reload_button.pack(side=tk.BOTTOM, pady=20)
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
async_mainloop(self.root)
def create_sliders(self, tab):
"""Create sliders for each tab."""
def on_closing(self):
print("Closing application...")
asyncio.create_task(self.websocket_client.close())
self.root.destroy()
def create_tabs(self):
for tab_name in list(self.tabs.keys()):
self.notebook.forget(self.tabs[tab_name])
del self.tabs[tab_name]
for key, value in self.settings["lights"].items():
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text=key)
self.create_light_control_widgets(tab, key, value["names"], value["settings"])
self.tabs[key] = tab
def create_light_control_widgets(self, tab, tab_name, ids, initial_settings):
slider_length = 800
slider_width = 50
# Red Slider
red_slider = tk.Scale(tab, from_=255, to=0, orient=tk.VERTICAL, length=slider_length, width=slider_width, label="Red")
red_slider.set(0)
red_slider.pack(side=tk.LEFT, padx=10)
red_slider.bind("<ButtonRelease-1>", lambda _: self.update_colour(tab))
# Extract initial color, brightness, and delay
# The 'colors' entry can now be a list. We'll pick the first one for initial display.
initial_colors = initial_settings.get("colors", ["#000000"])
initial_hex_color = initial_colors[0] if initial_colors else "#000000"
initial_brightness = initial_settings.get("brightness", 127)
initial_delay = initial_settings.get("delay", 0)
initial_pattern = initial_settings.get("pattern", "on")
# Green Slider
green_slider = tk.Scale(tab, from_=255, to=0, orient=tk.VERTICAL, length=slider_length, width=slider_width, label="Green")
green_slider.set(0)
green_slider.pack(side=tk.LEFT, padx=10)
green_slider.bind("<ButtonRelease-1>", lambda _: self.update_colour(tab))
initial_r, initial_g, initial_b = color_utils.hex_to_rgb(initial_hex_color)
# Blue Slider
blue_slider = tk.Scale(tab, from_=255, to=0, orient=tk.VERTICAL, length=slider_length, width=slider_width, label="Blue")
blue_slider.set(0)
blue_slider.pack(side=tk.LEFT, padx=10)
blue_slider.bind("<ButtonRelease-1>", lambda _: self.update_colour(tab))
# Main frame to hold everything within the tab
main_tab_frame = tk.Frame(tab, bg=bg_color)
main_tab_frame.pack(expand=True, fill="both", padx=10, pady=10)
# Brightness Slider
brightness_slider = tk.Scale(tab, from_=255, to=0, orient=tk.VERTICAL, length=slider_length, width=slider_width, label="Brightness")
brightness_slider.set(127)
brightness_slider.pack(side=tk.LEFT, padx=10)
brightness_slider.bind("<ButtonRelease-1>", lambda _: self.update_colour(tab))
# Left panel for sliders
slider_panel_frame = tk.Frame(main_tab_frame, bg=bg_color)
slider_panel_frame.pack(side=tk.LEFT, padx=10, pady=10)
tab.widgets = {
'red_slider': red_slider,
'green_slider': green_slider,
'blue_slider': blue_slider,
'brightness_slider': brightness_slider,
# Common slider configuration
slider_config = {
"from_": 255,
"to": 0,
"orient": tk.VERTICAL,
"length": slider_length,
"width": slider_width,
"bg": bg_color,
"fg": fg_color,
"highlightbackground": bg_color,
"activebackground": active_bg_color,
"resolution": 1,
"sliderlength": 70,
}
# Red Slider
red_slider = tk.Scale(slider_panel_frame, label="Red", troughcolor=trough_color_red, **slider_config)
red_slider.set(initial_r)
red_slider.pack(side=tk.LEFT, padx=10)
red_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
red_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
# Green Slider
green_slider = tk.Scale(slider_panel_frame, label="Green", troughcolor=trough_color_green, **slider_config)
green_slider.set(initial_g)
green_slider.pack(side=tk.LEFT, padx=10)
green_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
green_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
# Blue Slider
blue_slider = tk.Scale(slider_panel_frame, label="Blue", troughcolor=trough_color_blue, **slider_config)
blue_slider.set(initial_b)
blue_slider.pack(side=tk.LEFT, padx=10)
blue_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
blue_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
# Brightness Slider
brightness_slider = tk.Scale(
slider_panel_frame, label="Brightness", troughcolor=trough_color_brightness, **slider_config
)
brightness_slider.set(initial_brightness)
brightness_slider.pack(side=tk.LEFT, padx=10)
brightness_slider.bind("<B1-Motion>", lambda _: self.schedule_update_brightness(tab))
brightness_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_brightness(tab, force_send=True))
# Delay Slider
delay_slider_config = slider_config.copy()
delay_slider_config.update(
{
"from_": 1000,
"to": 0,
"resolution": 10,
"label": "Delay (ms)",
"troughcolor": trough_color_delay,
}
)
delay_slider = tk.Scale(slider_panel_frame, **delay_slider_config)
delay_slider.set(initial_delay)
delay_slider.pack(side=tk.LEFT, padx=10)
delay_slider.bind("<B1-Motion>", lambda _: self.schedule_update_delay(tab))
delay_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_delay(tab, force_send=True))
# Store references to widgets for this tab
tab.widgets = {
"red_slider": red_slider,
"green_slider": green_slider,
"blue_slider": blue_slider,
"brightness_slider": brightness_slider,
"delay_slider": delay_slider,
"selected_color_index": 0, # Default to the first color
}
tab.colors_in_palette = initial_colors.copy() # Store the list of hex colors for this tab
tab.color_swatch_frames = [] # To hold references to the color swatches
# Right panel for IDs, Patterns, and NEW Color Palette
right_panel_frame = tk.Frame(main_tab_frame, bg=bg_color)
right_panel_frame.pack(side=tk.LEFT, padx=20, pady=10, anchor="n", expand=True, fill="both")
# IDs section (unchanged)
ids_frame = tk.Frame(right_panel_frame, bg=bg_color)
ids_frame.pack(pady=10, fill=tk.X)
tk.Label(ids_frame, text="Associated Names:", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(pady=10)
for light_id in ids:
tk.Label(ids_frame, text=str(light_id), font=("Arial", 18), bg=bg_color, fg=fg_color).pack(pady=2)
# --- New Frame to hold Patterns and Color Palette side-by-side ---
patterns_and_palette_frame = tk.Frame(right_panel_frame, bg=bg_color)
patterns_and_palette_frame.pack(pady=20, fill=tk.BOTH, expand=True)
# Patterns section
patterns_frame = tk.Frame(patterns_and_palette_frame, bg=bg_color, bd=2, relief=tk.GROOVE)
patterns_frame.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.BOTH, expand=True) # Pack to the left
tk.Label(patterns_frame, text="Patterns:", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(pady=10)
tab.pattern_buttons = {}
patterns = self.settings.get("patterns", [])
for pattern_name in patterns:
button = tk.Button(
patterns_frame,
text=pattern_name,
command=lambda p=pattern_name: self.send_pattern(tab_name, p),
bg=active_bg_color,
fg=fg_color,
font=("Arial", 18),
padx=15,
pady=5,
relief=tk.FLAT,
)
button.pack(pady=5, fill=tk.X)
tab.pattern_buttons[pattern_name] = button
self.highlight_pattern_button(tab, initial_pattern)
# --- Color Palette Editor Section ---
color_palette_editor_frame = tk.Frame(patterns_and_palette_frame, bg=bg_color, bd=2, relief=tk.GROOVE)
color_palette_editor_frame.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.BOTH, expand=True) # Pack to the left
tab.color_palette_editor_frame = color_palette_editor_frame # Store reference for update_ui_for_pattern
tk.Label(color_palette_editor_frame, text="Color Palette:", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(
pady=10
)
# Frame to hold color swatches (will be dynamic)
tab.color_swatches_container = tk.Frame(color_palette_editor_frame, bg=bg_color)
tab.color_swatches_container.pack(pady=5, fill=tk.BOTH, expand=True)
# Buttons for Add/Remove Color
palette_buttons_frame = tk.Frame(color_palette_editor_frame, bg=bg_color)
palette_buttons_frame.pack(pady=10, fill=tk.X)
add_color_button = tk.Button(
palette_buttons_frame,
text="Add Color",
command=lambda t=tab: self.add_color_to_palette(t),
bg=active_bg_color,
fg=fg_color,
font=("Arial", 16),
padx=10,
pady=5,
relief=tk.FLAT,
)
add_color_button.pack(side=tk.LEFT, expand=True, padx=5)
remove_color_button = tk.Button(
palette_buttons_frame,
text="Remove Selected",
command=lambda t=tab: self.remove_selected_color_from_palette(t),
bg=active_bg_color,
fg=fg_color,
font=("Arial", 16),
padx=10,
pady=5,
relief=tk.FLAT,
)
remove_color_button.pack(side=tk.RIGHT, expand=True, padx=5)
# Initial population of the color palette
self.refresh_color_palette_display(tab)
# The initial call to update_ui_for_pattern now only sets slider values and highlights
self.update_ui_for_pattern(tab, initial_pattern)
def refresh_color_palette_display(self, tab):
"""Clears and repopulates the color swatches in the palette display."""
# Clear existing swatches
for frame in tab.color_swatch_frames:
frame.destroy()
tab.color_swatch_frames.clear()
for i, hex_color in enumerate(tab.colors_in_palette):
swatch_frame = tk.Frame(
tab.color_swatches_container, bg=hex_color, width=100, height=50, bd=2, relief=tk.SOLID
)
swatch_frame.pack(pady=2, padx=5, fill=tk.X)
# Bind click to select this color for editing
swatch_frame.bind("<Button-1>", lambda event, idx=i, t=tab: self.select_color_in_palette(t, idx))
# Add a label inside to make it clickable too
swatch_label = tk.Label(
swatch_frame,
text=f"Color {i+1}",
bg=hex_color,
fg=color_utils.get_contrast_text_color(hex_color),
font=("Arial", 14),
width=10,
height=2,
)
swatch_label.pack(expand=True, fill=tk.BOTH)
swatch_label.bind("<Button-1>", lambda event, idx=i, t=tab: self.select_color_in_palette(t, idx))
tab.color_swatch_frames.append(swatch_frame)
# Re-highlight the currently selected color
self._highlight_selected_color_swatch(tab)
def _highlight_selected_color_swatch(self, tab):
"""Applies/removes highlight border to the selected color swatch."""
current_index = tab.widgets["selected_color_index"]
for i, swatch_frame in enumerate(tab.color_swatch_frames):
if i == current_index:
swatch_frame.config(highlightbackground=active_palette_color_border, highlightthickness=3)
else:
swatch_frame.config(highlightbackground=swatch_frame.cget("bg"), highlightthickness=0) # Reset to no highlight
def select_color_in_palette(self, tab, index: int):
"""Selects a color in the palette, updates sliders, and highlights swatch."""
if not (0 <= index < len(tab.colors_in_palette)):
return
tab.widgets["selected_color_index"] = index
self._highlight_selected_color_swatch(tab)
# Update RGB sliders with the selected color
hex_color = tab.colors_in_palette[index]
r, g, b = color_utils.hex_to_rgb(hex_color)
tab.widgets["red_slider"].set(r)
tab.widgets["green_slider"].set(g)
tab.widgets["blue_slider"].set(b)
print(f"Selected color index {index}: {hex_color}")
def add_color_to_palette(self, tab):
"""Adds a new black color to the palette and selects it, with a limit of 10 colors."""
MAX_COLORS = 10 # Define the maximum number of colors allowed
if len(tab.colors_in_palette) >= MAX_COLORS:
messagebox.showwarning("Color Limit Reached", f"You can add a maximum of {MAX_COLORS} colors to the palette.")
return
tab.colors_in_palette.append("#000000") # Add black as default
self.refresh_color_palette_display(tab)
# Select the newly added color
self.select_color_in_palette(tab, len(tab.colors_in_palette) - 1)
self.save_current_tab_settings() # Save changes to settings.json
def remove_selected_color_from_palette(self, tab):
"""Removes the currently selected color from the palette."""
current_index = tab.widgets["selected_color_index"]
if len(tab.colors_in_palette) <= 1:
messagebox.showwarning("Cannot Remove", "There must be at least one color in the palette.")
return
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to remove Color {current_index + 1}?"):
del tab.colors_in_palette[current_index]
# Adjust selected index if the removed color was the last one
if current_index >= len(tab.colors_in_palette):
tab.widgets["selected_color_index"] = len(tab.colors_in_palette) - 1
if tab.widgets["selected_color_index"] < 0: # Should not happen if check above works
tab.widgets["selected_color_index"] = 0
self.refresh_color_palette_display(tab)
# Update sliders with the new selected color (if any)
if tab.colors_in_palette:
self.select_color_in_palette(tab, tab.widgets["selected_color_index"])
else: # If palette became empty (shouldn't happen with 1-color check)
tab.widgets["red_slider"].set(0)
tab.widgets["green_slider"].set(0)
tab.widgets["blue_slider"].set(0)
self.save_current_tab_settings() # Save changes to settings.json
def update_ui_for_pattern(self, tab, current_pattern: str):
"""
Manages the state of the UI elements based on the selected pattern.
The Color Palette Editor is now always visible when 'transition' is selected,
and RGB sliders update based on the selected color in the palette.
When not 'transition', RGB sliders revert to the first color in settings.
"""
# The color_palette_editor_frame is now *always* packed in create_light_control_widgets
# when patterns_and_palette_frame is created with side-by-side packing.
# So we no longer need to pack/pack_forget it here.
# Its visibility is handled by its initial creation and packing alongside the patterns.
# If the pattern is "transition", select the current color in the palette
# and ensure the RGB sliders reflect that color.
if current_pattern == "transition":
# This handles refreshing the display and setting sliders to the selected color
self.refresh_color_palette_display(tab)
self.select_color_in_palette(tab, tab.widgets["selected_color_index"])
else:
# When switching away from transition, ensure RGB sliders show the first color from settings
# and reset selected_color_index.
initial_colors = self.settings["lights"][self.notebook.tab(self.notebook.select(), "text")]["settings"].get(
"colors", ["#000000"]
)
initial_hex_color = initial_colors[0] if initial_colors else "#000000"
r, g, b = color_utils.hex_to_rgb(initial_hex_color)
tab.widgets["red_slider"].set(r)
tab.widgets["green_slider"].set(g)
tab.widgets["blue_slider"].set(b)
tab.widgets["selected_color_index"] = 0 # Reset selected color index to the first (default)
self._highlight_selected_color_swatch(
tab
) # Remove highlight if active color is no longer relevant for editing
# (or just highlight the 0th if you want)
# Brightness and Delay sliders are always visible.
def highlight_pattern_button(self, tab_widget, active_pattern_name):
if hasattr(tab_widget, "pattern_buttons"):
for pattern_name, button in tab_widget.pattern_buttons.items():
if pattern_name == active_pattern_name:
button.config(bg=highlight_pattern_color)
else:
button.config(bg=active_bg_color)
def on_tab_change(self, event):
selected_tab_name = self.notebook.tab(self.notebook.select(), "text")
current_tab_widget = self.notebook.nametowidget(self.notebook.select())
initial_settings = self.settings["lights"][selected_tab_name]["settings"]
# Ensure current_tab_widget has the necessary attributes
if not hasattr(current_tab_widget, "colors_in_palette"):
# This tab might not have been fully initialized yet, or recreated
# In a full reload, create_tabs ensures it is.
return
# Update the local colors_in_palette list for the tab
current_tab_widget.colors_in_palette = initial_settings.get("colors", ["#000000"]).copy()
current_tab_widget.widgets["selected_color_index"] = 0 # Default to first color
# Refresh the color palette display and select the first color
self.refresh_color_palette_display(current_tab_widget)
if current_tab_widget.colors_in_palette:
self.select_color_in_palette(current_tab_widget, 0)
else: # If palette became empty (shouldn't happen with default ["#000000"])
current_tab_widget.widgets["red_slider"].set(0)
current_tab_widget.widgets["green_slider"].set(0)
current_tab_widget.widgets["blue_slider"].set(0)
# Update brightness and delay sliders
current_tab_widget.widgets["brightness_slider"].set(initial_settings.get("brightness", 127))
current_tab_widget.widgets["delay_slider"].set(initial_settings.get("delay", 0))
# Highlight the active pattern button
initial_pattern = initial_settings.get("pattern", "on")
self.highlight_pattern_button(current_tab_widget, initial_pattern)
# Update UI visibility based on the current pattern
self.update_ui_for_pattern(current_tab_widget, initial_pattern)
def reload_config(self):
print("Reloading configuration...")
self.settings = Settings()
self.create_tabs()
# After recreating, ensure the currently selected tab's sliders are updated
# Trigger on_tab_change manually for the currently selected tab
self.on_tab_change(None)
# --- Debouncing functions (no change to core logic, just how they call update_rgb) ---
def schedule_update_rgb(self, tab, force_send=False):
current_time = time.time() * 1000
if force_send:
if self.pending_rgb_update_id:
self.root.after_cancel(self.pending_rgb_update_id)
self.pending_rgb_update_id = None
self.update_rgb(tab)
self.last_rgb_update_time = current_time
elif current_time - self.last_rgb_update_time >= self.rgb_update_interval_ms:
if self.pending_rgb_update_id:
self.root.after_cancel(self.pending_rgb_update_id)
self.pending_rgb_update_id = None
self.update_rgb(tab)
self.last_rgb_update_time = current_time
else:
if self.pending_rgb_update_id:
self.root.after_cancel(self.pending_rgb_update_id)
time_to_wait = int(self.rgb_update_interval_ms - (current_time - self.last_rgb_update_time))
self.pending_rgb_update_id = self.root.after(time_to_wait, lambda: self.update_rgb(tab))
def schedule_update_brightness(self, tab, force_send=False):
current_time = time.time() * 1000
if force_send:
if self.pending_brightness_update_id:
self.root.after_cancel(self.pending_brightness_update_id)
self.pending_brightness_update_id = None
self.update_brightness(tab)
self.last_brightness_update_time = current_time
elif current_time - self.last_brightness_update_time >= self.brightness_update_interval_ms:
if self.pending_brightness_update_id:
self.root.after_cancel(self.pending_brightness_update_id)
self.pending_brightness_update_id = None
self.update_brightness(tab)
self.last_brightness_update_time = current_time
else:
if self.pending_brightness_update_id:
self.root.after_cancel(self.pending_brightness_update_id)
time_to_wait = int(self.brightness_update_interval_ms - (current_time - self.last_brightness_update_time))
self.pending_brightness_update_id = self.root.after(time_to_wait, lambda: self.update_brightness(tab))
def schedule_update_delay(self, tab, force_send=False):
current_time = time.time() * 1000
if force_send:
if self.pending_delay_update_id:
self.root.after_cancel(self.pending_delay_update_id)
self.pending_delay_update_id = None
self.update_delay(tab)
self.last_delay_update_time = current_time
elif current_time - self.last_delay_update_time >= self.delay_update_interval_ms:
if self.pending_delay_update_id:
self.root.after_cancel(self.pending_delay_update_id)
self.pending_delay_update_id = None
self.update_delay(tab)
self.last_delay_update_time = current_time
else:
if self.pending_delay_update_id:
self.root.after_cancel(self.pending_delay_update_id)
time_to_wait = int(self.delay_update_interval_ms - (current_time - self.last_delay_update_time))
self.pending_delay_update_id = self.root.after(time_to_wait, lambda: self.update_delay(tab))
# --- Asynchronous Update Functions ---
@async_handler
async def update_colour(self, tab):
"""Update color based on the slider values and send to the selected server."""
async def update_rgb(self, tab):
"""Update the currently selected color in the palette and send to the server."""
try:
# Retrieve slider values
red_slider = tab.widgets['red_slider']
green_slider = tab.widgets['green_slider']
blue_slider = tab.widgets['blue_slider']
brightness_slider = tab.widgets['brightness_slider']
red_slider = tab.widgets["red_slider"]
green_slider = tab.widgets["green_slider"]
blue_slider = tab.widgets["blue_slider"]
r = red_slider.get()
g = green_slider.get()
b = blue_slider.get()
brightness = brightness_slider.get()
# Adjust brightness
color = color_utils.adjust_brightness((r, g, b), brightness)
print(f"Adjusted color: {color}, Brightness: {brightness}")
hex_color = f"#{r:02x}{g:02x}{b:02x}"
print(f"Updating selected color to: {hex_color}")
# Convert RGB to hex color
hex_color = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
selected_color_index = tab.widgets["selected_color_index"]
if 0 <= selected_color_index < len(tab.colors_in_palette):
tab.colors_in_palette[selected_color_index] = hex_color
self.refresh_color_palette_display(tab) # Update swatch immediately
# Get the selected server
selected_server = self.notebook.tab(self.notebook.select(), "text")
ids = self.lightgroups[selected_server]["ids"]
names = self.settings["lights"][selected_server]["names"]
# ALWAYS send the full current palette, or at least the first color,
# along with other relevant settings, when an RGB slider is moved.
# The device firmware will interpret 'colors' based on its current pattern.
# Determine which colors to send. It's generally safest to send the
# full current palette, as the device might need all of them.
colors_to_send = tab.colors_in_palette.copy() # Send a copy to be safe
# Construct WebSocket payload
payload = {
"save": True,
"ids": [0],
"save": True, # Always save this change to config
"names": names,
"settings": {
"colors": [hex_color], # Use the dynamically calculated hex color
"brightness": brightness, # Use the brightness slider value
"pattern": "on"
}
"colors": colors_to_send,
# We might also want to send the current brightness and delay
# to ensure the device has the complete state, or at least
# ensures these don't get 'unset' if they weren't explicitly changed.
# This depends on your firmware's expected payload.
"brightness": tab.widgets["brightness_slider"].get(),
"delay": tab.widgets["delay_slider"].get(),
# Also include the current pattern, so the device knows how to apply colors
"pattern": self.settings["lights"][selected_server]["settings"].get("pattern", "on"),
},
}
# Send the payload to the server
await send_to_server(payload)
print(f"Sent payload: {payload}")
# Update the settings object with the new color list (and potentially other synced values)
self.settings["lights"][selected_server]["settings"]["colors"] = tab.colors_in_palette.copy()
# Also ensure brightness, delay, and pattern are up-to-date in settings before saving
self.settings["lights"][selected_server]["settings"]["brightness"] = tab.widgets["brightness_slider"].get()
self.settings["lights"][selected_server]["settings"]["delay"] = tab.widgets["delay_slider"].get()
# Pattern is generally set by the pattern buttons, but including it in the save here
# for completeness might be useful depending on your app's state management.
# self.settings["lights"][selected_server]["settings"]["pattern"] = current_pattern # Already updated by send_pattern
self.settings.save()
await self.websocket_client.send_data(payload)
print(f"Sent RGB payload: {payload}")
except Exception as e:
print(f"Error updating color: {e}")
# Optionally, display the error in the GUI
print(f"Error updating RGB: {e}")
@async_handler
async def update_brightness(self, tab):
try:
brightness_slider = tab.widgets["brightness_slider"]
brightness = brightness_slider.get()
print(f"Brightness: {brightness}")
selected_server = self.notebook.tab(self.notebook.select(), "text")
names = self.settings["lights"][selected_server]["names"]
payload = {
"save": True,
"names": names,
"settings": {
"brightness": brightness,
},
}
# Update the settings object with the new brightness
self.settings["lights"][selected_server]["settings"]["brightness"] = brightness
self.settings.save()
await self.websocket_client.send_data(payload)
print(f"Sent brightness payload: {payload}")
except Exception as e:
print(f"Error updating brightness: {e}")
@async_handler
async def update_delay(self, tab):
try:
delay_slider = tab.widgets["delay_slider"]
delay = delay_slider.get()
print(f"Delay: {delay}")
selected_server = self.notebook.tab(self.notebook.select(), "text")
names = self.settings["lights"][selected_server]["names"]
payload = {
"save": True,
"names": names,
"settings": {
"delay": delay,
},
}
# Update the settings object with the new delay
self.settings["lights"][selected_server]["settings"]["delay"] = delay
self.settings.save()
await self.websocket_client.send_data(payload)
print(f"Sent delay payload: {payload}")
except Exception as e:
print(f"Error updating delay: {e}")
@async_handler
async def send_pattern(self, tab_name: str, pattern_name: str):
try:
names = self.settings["lights"][tab_name]["names"]
# Get the actual tab widget to access its `colors_in_palette` and other attributes
current_tab_widget = None
for key, tab_widget in self.tabs.items():
if key == tab_name:
current_tab_widget = tab_widget
break
if not current_tab_widget:
print(f"Error: Could not find tab widget for {tab_name}")
return
current_settings_for_tab = self.settings["lights"][tab_name]["settings"]
payload_settings = {
"pattern": pattern_name,
"brightness": current_settings_for_tab.get("brightness", 127),
"delay": current_settings_for_tab.get("delay", 0),
}
# Only include "colors" in the payload if the pattern specifically uses them
# For "transition", send the entire palette
# For "on", "off", "blink", usually just the first color from the palette is relevant
if pattern_name == "transition":
# Ensure we send the *current state* of the palette
payload_settings["colors"] = current_tab_widget.colors_in_palette
elif pattern_name in ["on", "blink"]: # Add other patterns that use a single color here
if current_tab_widget.colors_in_palette:
payload_settings["colors"] = [current_tab_widget.colors_in_palette[0]]
else:
payload_settings["colors"] = ["#000000"] # Default if palette is empty
# For patterns like "off" or "rainbow", "colors" might not be needed or handled differently
payload = {
"save": True,
"names": names,
"settings": payload_settings,
}
# Update the settings object with the new pattern and current colors/brightness/delay
# It's important to save the state that *was active* when the pattern was set,
# or the state that should *persist* with the pattern.
self.settings["lights"][tab_name]["settings"]["pattern"] = pattern_name
# Ensure the saved colors are always the palette's current state
self.settings["lights"][tab_name]["settings"]["colors"] = current_tab_widget.colors_in_palette
self.settings.save()
self.highlight_pattern_button(current_tab_widget, pattern_name)
self.update_ui_for_pattern(current_tab_widget, pattern_name) # Update visibility
await self.websocket_client.send_data(payload)
print(f"Sent pattern payload: {payload}")
except Exception as e:
print(f"Error sending pattern: {e}")
def save_current_tab_settings(self):
"""Saves the current state of the active tab's settings (colors, brightness, delay, pattern) to config."""
selected_server = self.notebook.tab(self.notebook.select(), "text")
current_tab_widget = self.notebook.nametowidget(self.notebook.select())
if not hasattr(current_tab_widget, "colors_in_palette"):
return # Tab not fully initialized yet
# Update settings for the current tab in the self.settings object
self.settings["lights"][selected_server]["settings"]["colors"] = current_tab_widget.colors_in_palette
self.settings["lights"][selected_server]["settings"]["brightness"] = current_tab_widget.widgets["brightness_slider"].get()
self.settings["lights"][selected_server]["settings"]["delay"] = current_tab_widget.widgets["delay_slider"].get()
# The pattern is updated in send_pattern already, but ensure consistency
# For simplicity, we assume send_pattern is the primary way to change pattern.
self.settings.save()
print(f"Saved settings for {selected_server}")
if __name__ == "__main__":

View File

@ -2,28 +2,52 @@ import asyncio
import websockets
import json
async def send_to_server(data):
"""
Send WebSocket data to the server.
"""
try:
# Connect to the WebSocket server
async with websockets.connect("ws://192.168.4.1:80/ws") as websocket:
# Serialize data to JSON and send it
await websocket.send(json.dumps(data))
except (ConnectionError, websockets.exceptions.ConnectionClosed) as e:
print(f"Error sending to {e}")
if __name__ == "__main__":
# Define the data to be sent
data = {
"settings": {
"color": "#00ff00"
}
}
class WebSocketClient:
def __init__(self, uri):
self.uri = uri
self.websocket = None
self.is_connected = False
# Server details
server = ("192.168.4.1", 80) # Example WebSocket server port
async def connect(self):
"""Establishes the WebSocket connection."""
if self.is_connected and self.websocket:
print("Already connected.")
return
# Run the asynchronous function using asyncio.run
asyncio.run(send_to_server(data, server))
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}")
self.is_connected = False
self.websocket = None
async def send_data(self, data):
print(data)
"""Sends data over the open WebSocket connection."""
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.")
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}")
self.is_connected = False
self.websocket = None # Reset connection on error
await self.connect() # Attempt to reconnect
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.")

26
settings.json Normal file
View File

@ -0,0 +1,26 @@
{
"lights": {
"light1": {
"names": [
"test"
],
"settings": {
"colors": [
"#ff0000",
"#0000ff",
"#00ff00"
],
"brightness": 255,
"pattern": "blink",
"delay": 40
}
}
},
"patterns": [
"on",
"off",
"blink",
"rainbow_cycle",
"color_transition"
]
}

27
settings.py Normal file
View File

@ -0,0 +1,27 @@
import json
class Settings(dict):
SETTINGS_FILE = "settings.json"
def __init__(self):
super().__init__()
self.load() # Load settings from file during initialization
def save(self):
try:
j = json.dumps(self, indent=4)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
except Exception as e:
print(f"Error saving settings: {e}")
def load(self):
try:
with open(self.SETTINGS_FILE, 'r') as file:
loaded_settings = json.load(file)
self.update(loaded_settings)
print("Settings loaded successfully.")
except Exception as e:
print(f"Error loading settings {e}")
self.save()