diff --git a/src/main.py b/src/main.py index 1ffb123..43f0ca6 100644 --- a/src/main.py +++ b/src/main.py @@ -7,6 +7,7 @@ from networking import WebSocketClient import color_utils from settings import Settings import time +import math # Dark theme colors (unchanged) bg_color = "#2e2e2e" @@ -22,6 +23,26 @@ highlight_pattern_color = "#6a5acd" active_palette_color_border = "#FFD700" # Gold color +def delay_to_slider(delay_ms): + """Convert delay in ms (10-10000) to slider position (0-1000) using logarithmic scale.""" + if delay_ms <= 10: + return 0 + if delay_ms >= 10000: + return 1000 + # Logarithmic conversion: delay = 10 * (10000/10) ^ (position/1000) + # Solving for position: position = 1000 * log(delay/10) / log(1000) + return 1000 * math.log(delay_ms / 10) / math.log(1000) + +def slider_to_delay(slider_value): + """Convert slider position (0-1000) to delay in ms (10-10000) using logarithmic scale.""" + if slider_value <= 0: + return 10 + if slider_value >= 1000: + return 10000 + # Logarithmic conversion: delay = 10 * 1000 ^ (position/1000) + return int(10 * (1000 ** (slider_value / 1000))) + + class App: def __init__(self) -> None: self.settings = Settings() @@ -42,6 +63,10 @@ class App: self.delay_update_interval_ms = 100 self.pending_delay_update_id = None + self.last_n_params_update_time = 0 + self.n_params_update_interval_ms = 100 + self.pending_n_params_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)) @@ -66,20 +91,6 @@ class App: self.notebook.bind("<>", 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) @@ -101,7 +112,7 @@ class App: self.tabs[key] = tab def create_light_control_widgets(self, tab, tab_name, ids, initial_settings): - slider_length = 800 + slider_length = 600 slider_width = 50 # Extract initial color, brightness, and delay @@ -110,6 +121,12 @@ class App: initial_brightness = initial_settings.get("brightness", 127) initial_delay = initial_settings.get("delay", 0) initial_pattern = initial_settings.get("pattern", "on") + initial_n1 = initial_settings.get("n1", 10) + initial_n2 = initial_settings.get("n2", 10) + initial_n3 = initial_settings.get("n3", 10) + initial_n4 = initial_settings.get("n4", 10) + initial_n5 = initial_settings.get("n5", 10) + initial_n6 = initial_settings.get("n6", 10) initial_r, initial_g, initial_b = color_utils.hex_to_rgb(initial_hex_color) @@ -117,9 +134,13 @@ class App: main_tab_frame = tk.Frame(tab, bg=bg_color) main_tab_frame.pack(expand=True, fill="both", padx=10, pady=10) - # 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) + # Left panel container for sliders and n inputs + left_panel_container = tk.Frame(main_tab_frame, bg=bg_color) + left_panel_container.pack(side=tk.LEFT, padx=10, pady=10) + + # Slider panel + slider_panel_frame = tk.Frame(left_panel_container, bg=bg_color) + slider_panel_frame.pack(side=tk.TOP, padx=10, pady=10) # Common slider configuration slider_config = { @@ -166,22 +187,142 @@ class App: brightness_slider.bind("", lambda _: self.schedule_update_brightness(tab)) brightness_slider.bind("", lambda _: self.schedule_update_brightness(tab, force_send=True)) - # Delay Slider + # Delay Slider with logarithmic scale (10ms to 10000ms) delay_slider_config = slider_config.copy() delay_slider_config.update( { - "from_": 1000, + "from_": 1000, # Slider range 0-1000 (will be converted to 10-10000ms logarithmically) "to": 0, - "resolution": 10, + "resolution": 1, "label": "Delay (ms)", + "showvalue": False, # Hide default value, we'll show logarithmic value in custom label "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("", lambda _: self.schedule_update_delay(tab)) - delay_slider.bind("", lambda _: self.schedule_update_delay(tab, force_send=True)) + # Create a frame to contain the slider and its value label (to make them appear as one unit) + delay_container = tk.Frame(slider_panel_frame, bg=bg_color) + delay_container.pack(side=tk.LEFT, padx=10) + + delay_slider = tk.Scale(delay_container, **delay_slider_config) + # Convert initial delay to slider position using logarithmic scale + initial_slider_pos = delay_to_slider(initial_delay) + delay_slider.set(initial_slider_pos) + + # Create a custom label to show the actual delay value, positioned like the default Scale value + delay_value_label = tk.Label(delay_container, text=f"{initial_delay}", font=("Arial", 12), bg=bg_color, fg=fg_color, width=5, anchor="e") + delay_value_label.pack(side=tk.LEFT, padx=(0, 5)) + + # Function to update the side label when slider moves + def update_delay_value_label(event=None): + slider_value = delay_slider.get() + actual_delay = slider_to_delay(slider_value) + delay_value_label.config(text=f"{actual_delay}") + + delay_slider.pack(side=tk.LEFT) + delay_slider.bind("", lambda e: (update_delay_value_label(), self.schedule_update_delay(tab))) + delay_slider.bind("", lambda e: (update_delay_value_label(), self.schedule_update_delay(tab, force_send=True))) + + # N inputs panel below sliders (moved down to make sliders longer) + n_inputs_frame = tk.Frame(left_panel_container, bg=bg_color) + n_inputs_frame.pack(side=tk.TOP, padx=10, pady=10) + + n_inputs_inner_frame = tk.Frame(n_inputs_frame, bg=bg_color) + n_inputs_inner_frame.pack() + + # Common spinbox config for n inputs - larger for touch screen + n_spinbox_config = { + "from_": 0, + "to": 255, + "increment": 1, + "width": 12, + "bg": bg_color, + "fg": fg_color, + "font": ("Arial", 24), + "buttonbackground": active_bg_color, + } + + # Create n1-n6 inputs in a grid with arrows on both sides + n_inputs = {} + for i in range(1, 7): + n_frame = tk.Frame(n_inputs_inner_frame, bg=bg_color) + n_frame.grid(row=(i-1)//3, column=(i-1)%3, padx=10, pady=10) + + tk.Label(n_frame, text=f"n{i}", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(pady=(0, 5)) + + # Create a frame for the input with arrows on both sides + input_container = tk.Frame(n_frame, bg=bg_color) + input_container.pack(pady=5) + + # Use StringVar for the value + n_var = tk.StringVar(value=str(initial_settings.get(f"n{i}", 10))) + + # Left arrow button (decrease) + def decrease_value(var=n_var): + try: + current_str = var.get() + if not current_str: + current = 0 + else: + current = int(current_str) + new_value = current - 1 + var.set(str(new_value)) + self.schedule_update_n_params(tab, force_send=True) + except (ValueError, TypeError): + # If value is invalid, set to -1 + var.set("-1") + self.schedule_update_n_params(tab, force_send=True) + + left_arrow = tk.Button( + input_container, + text="−", + font=("Arial", 32, "bold"), + bg=active_bg_color, + fg=fg_color, + relief=tk.FLAT, + command=decrease_value, + width=3, + height=1, + ) + left_arrow.pack(side=tk.LEFT, padx=2) + + # Entry in the middle + n_entry = tk.Entry( + input_container, + textvariable=n_var, + font=("Arial", 24), + bg=bg_color, + fg=fg_color, + width=8, + justify=tk.CENTER, + relief=tk.SUNKEN, + bd=2, + ) + n_entry.pack(side=tk.LEFT, padx=2, ipady=8) + n_entry.bind("", lambda event: self.schedule_update_n_params(tab)) + n_entry.bind("", lambda event: self.schedule_update_n_params(tab, force_send=True)) + + # Right arrow button (increase) + def increase_value(var=n_var): + current = int(var.get()) + new_value = current + 1 + var.set(str(new_value)) + self.schedule_update_n_params(tab, force_send=True) + + right_arrow = tk.Button( + input_container, + text="+", + font=("Arial", 32, "bold"), + bg=active_bg_color, + fg=fg_color, + relief=tk.FLAT, + command=increase_value, + width=3, + height=1, + ) + right_arrow.pack(side=tk.LEFT, padx=2) + + n_inputs[f"n{i}"] = n_entry + n_inputs[f"n{i}_var"] = n_var # Store the variable for later updates # Store references to widgets for this tab tab.widgets = { @@ -190,6 +331,19 @@ class App: "blue_slider": blue_slider, "brightness_slider": brightness_slider, "delay_slider": delay_slider, + "delay_value_label": delay_value_label, # Store the delay value label for updates + "n1": n_inputs["n1"], + "n1_var": n_inputs["n1_var"], + "n2": n_inputs["n2"], + "n2_var": n_inputs["n2_var"], + "n3": n_inputs["n3"], + "n3_var": n_inputs["n3_var"], + "n4": n_inputs["n4"], + "n4_var": n_inputs["n4_var"], + "n5": n_inputs["n5"], + "n5_var": n_inputs["n5_var"], + "n6": n_inputs["n6"], + "n6_var": n_inputs["n6_var"], "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 @@ -469,7 +623,17 @@ class App: # 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)) + # Convert delay to slider position using logarithmic scale + initial_delay = initial_settings.get("delay", 0) + initial_delay_slider_pos = delay_to_slider(initial_delay) + current_tab_widget.widgets["delay_slider"].set(initial_delay_slider_pos) + # Update the delay value label to show the actual delay value + if "delay_value_label" in current_tab_widget.widgets: + current_tab_widget.widgets["delay_value_label"].config(text=f"{initial_delay}") + + # Update n parameter inputs + for i in range(1, 7): + current_tab_widget.widgets[f"n{i}_var"].set(str(initial_settings.get(f"n{i}", 10))) # Highlight the active pattern button initial_pattern = initial_settings.get("pattern", "on") @@ -547,6 +711,26 @@ class App: 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)) + def schedule_update_n_params(self, tab, force_send=False): + current_time = time.time() * 1000 + if force_send: + if self.pending_n_params_update_id: + self.root.after_cancel(self.pending_n_params_update_id) + self.pending_n_params_update_id = None + self.update_n_params(tab) + self.last_n_params_update_time = current_time + elif current_time - self.last_n_params_update_time >= self.n_params_update_interval_ms: + if self.pending_n_params_update_id: + self.root.after_cancel(self.pending_n_params_update_id) + self.pending_n_params_update_id = None + self.update_n_params(tab) + self.last_n_params_update_time = current_time + else: + if self.pending_n_params_update_id: + self.root.after_cancel(self.pending_n_params_update_id) + time_to_wait = int(self.n_params_update_interval_ms - (current_time - self.last_n_params_update_time)) + self.pending_n_params_update_id = self.root.after(time_to_wait, lambda: self.update_n_params(tab)) + # --- Asynchronous Update Functions --- @async_handler async def update_rgb(self, tab): @@ -598,15 +782,25 @@ class App: "settings": { "colors": colors_to_send, # This now dynamically changes based on pattern "brightness": tab.widgets["brightness_slider"].get(), - "delay": tab.widgets["delay_slider"].get(), + "delay": slider_to_delay(tab.widgets["delay_slider"].get()), # Convert from logarithmic slider "pattern": current_pattern, # Always send the current pattern + "n1": int(tab.widgets["n1_var"].get()), + "n2": int(tab.widgets["n2_var"].get()), + "n3": int(tab.widgets["n3_var"].get()), + "n4": int(tab.widgets["n4_var"].get()), + "n5": int(tab.widgets["n5_var"].get()), + "n6": int(tab.widgets["n6_var"].get()), }, } # 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() self.settings["lights"][selected_server]["settings"]["brightness"] = tab.widgets["brightness_slider"].get() - self.settings["lights"][selected_server]["settings"]["delay"] = tab.widgets["delay_slider"].get() + # Convert slider position to actual delay using logarithmic scale + slider_value = tab.widgets["delay_slider"].get() + self.settings["lights"][selected_server]["settings"]["delay"] = slider_to_delay(slider_value) + for i in range(1, 7): + self.settings["lights"][selected_server]["settings"][f"n{i}"] = int(tab.widgets[f"n{i}_var"].get()) self.settings.save() await self.websocket_client.send_data(payload) @@ -644,8 +838,9 @@ class App: async def update_delay(self, tab): try: delay_slider = tab.widgets["delay_slider"] - delay = delay_slider.get() - print(f"Delay: {delay}") + slider_value = delay_slider.get() + delay = slider_to_delay(slider_value) # Convert from logarithmic slider to actual delay + print(f"Delay: {delay}ms (slider: {slider_value})") selected_server = self.notebook.tab(self.notebook.select(), "text") names = self.settings["lights"][selected_server]["names"] @@ -664,6 +859,32 @@ class App: except Exception as e: print(f"Error updating delay: {e}") + @async_handler + async def update_n_params(self, tab): + try: + n_params = {} + for i in range(1, 7): + n_var = tab.widgets[f"n{i}_var"] + n_params[f"n{i}"] = int(n_var.get()) + + print(f"N Parameters: {n_params}") + + selected_server = self.notebook.tab(self.notebook.select(), "text") + names = self.settings["lights"][selected_server]["names"] + payload = { + "save": True, + "names": names, + "settings": n_params, + } + # Update the settings object with the new n params + for key, value in n_params.items(): + self.settings["lights"][selected_server]["settings"][key] = value + self.settings.save() + await self.websocket_client.send_data(payload) + print(f"Sent n params payload: {payload}") + except Exception as e: + print(f"Error updating n params: {e}") + @async_handler async def send_pattern(self, tab_name: str, pattern_name: str): try: @@ -684,8 +905,12 @@ class App: payload_settings = { "pattern": pattern_name, "brightness": current_settings_for_tab.get("brightness", 127), - "delay": current_settings_for_tab.get("delay", 0), + "delay": slider_to_delay(current_tab_widget.widgets["delay_slider"].get()), # Convert from logarithmic slider } + + # Include n parameters + for i in range(1, 7): + payload_settings[f"n{i}"] = int(current_tab_widget.widgets[f"n{i}_var"].get()) # Determine colors to send based on the *newly selected* pattern if pattern_name == "transition": @@ -712,6 +937,8 @@ class App: self.settings["lights"][tab_name]["settings"]["pattern"] = pattern_name # Always save the full current palette state in settings. self.settings["lights"][tab_name]["settings"]["colors"] = current_tab_widget.colors_in_palette.copy() + for i in range(1, 7): + self.settings["lights"][tab_name]["settings"][f"n{i}"] = int(current_tab_widget.widgets[f"n{i}"].get()) self.settings.save() self.highlight_pattern_button(current_tab_widget, pattern_name) @@ -733,7 +960,12 @@ class App: # 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() + # Convert slider position to actual delay using logarithmic scale + slider_value = current_tab_widget.widgets["delay_slider"].get() + delay = slider_to_delay(slider_value) + self.settings["lights"][selected_server]["settings"]["delay"] = delay + for i in range(1, 7): + self.settings["lights"][selected_server]["settings"][f"n{i}"] = int(current_tab_widget.widgets[f"n{i}"].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.