diff --git a/tool.py b/tool.py new file mode 100755 index 0000000..95b38b9 --- /dev/null +++ b/tool.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +""" +LED Bar Configuration Tool +A tkinter GUI for downloading, editing, and uploading settings.json to/from MicroPython devices via mpremote. +""" + +import tkinter as tk +from tkinter import ttk, messagebox, filedialog +import json +import subprocess +import os +import tempfile +import serial +from pathlib import Path + + +class LEDConfigTool: + def __init__(self, root): + self.root = root + self.root.title("LED Bar Configuration Tool") + self.root.geometry("600x700") + + self.settings = {} + self.temp_file = None + + # Create main frame + main_frame = ttk.Frame(root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Title + title_label = ttk.Label(main_frame, text="LED Bar Configuration", font=("Arial", 16, "bold")) + title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20)) + + # Device connection section + device_frame = ttk.LabelFrame(main_frame, text="Device Connection", padding="10") + device_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + ttk.Label(device_frame, text="Device:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) + self.device_entry = ttk.Entry(device_frame, width=30) + self.device_entry.insert(0, "/dev/ttyACM0") # Default device + self.device_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10)) + + ttk.Button(device_frame, text="Download Settings", command=self.download_settings).grid(row=0, column=2) + + # Settings section + settings_frame = ttk.LabelFrame(main_frame, text="Settings", padding="10") + settings_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) + + # Create scrollable frame for settings + canvas = tk.Canvas(settings_frame, height=400) + scrollbar = ttk.Scrollbar(settings_frame, orient="vertical", command=canvas.yview) + scrollable_frame = ttk.Frame(canvas) + + scrollable_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + # Settings fields + self.setting_widgets = {} + settings_config = [ + ("led_pin", "LED Pin", "number"), + ("num_leds", "Number of LEDs", "number"), + ("color_order", "Color Order", "choice", ["rgb", "rbg", "grb", "gbr", "brg", "bgr"]), + ("name", "Device Name", "text"), + ("pattern", "Pattern", "text"), + ("color1", "Color 1", "color"), + ("color2", "Color 2", "color"), + ("delay", "Delay (ms)", "number"), + ("brightness", "Brightness", "number"), + ("n1", "N1", "number"), + ("n2", "N2", "number"), + ("n3", "N3", "number"), + ("n4", "N4", "number"), + ("n5", "N5", "number"), + ("n6", "N6", "number"), + ("ap_password", "AP Password", "text"), + ("id", "ID", "number"), + ("debug", "Debug Mode", "choice", ["True", "False"]), + ] + + for idx, config in enumerate(settings_config): + key = config[0] + label_text = config[1] + field_type = config[2] + + ttk.Label(scrollable_frame, text=f"{label_text}:").grid(row=idx, column=0, sticky=tk.W, padx=(0, 10), pady=5) + + if field_type == "number": + widget = ttk.Entry(scrollable_frame, width=20) + elif field_type == "choice": + widget = ttk.Combobox(scrollable_frame, width=17, values=config[3], state="readonly") + elif field_type == "color": + widget = ttk.Entry(scrollable_frame, width=20) + else: # text + widget = ttk.Entry(scrollable_frame, width=20) + + widget.grid(row=idx, column=1, sticky=(tk.W, tk.E), pady=5) + self.setting_widgets[key] = widget + + canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + settings_frame.grid_rowconfigure(0, weight=1) + settings_frame.grid_columnconfigure(0, weight=1) + + # Buttons section + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=3, column=0, columnspan=2, pady=(10, 0)) + + ttk.Button(button_frame, text="Load from File", command=self.load_from_file).grid(row=0, column=0, padx=5) + ttk.Button(button_frame, text="Save to File", command=self.save_to_file).grid(row=0, column=1, padx=5) + ttk.Button(button_frame, text="Upload Settings", command=self.upload_settings).grid(row=0, column=2, padx=5) + + # Status bar + self.status_label = ttk.Label(main_frame, text="Ready", relief=tk.SUNKEN, anchor=tk.W) + self.status_label.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0)) + + # Configure grid weights + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + main_frame.columnconfigure(0, weight=1) + main_frame.rowconfigure(2, weight=1) + device_frame.columnconfigure(1, weight=1) + + def update_status(self, message): + """Update the status bar message.""" + self.status_label.config(text=message) + self.root.update_idletasks() + + def download_settings(self): + """Download settings.json from the device using mpremote.""" + device = self.device_entry.get().strip() + if not device: + messagebox.showerror("Error", "Please specify a device") + return + + self.update_status("Downloading settings...") + + try: + # Create temporary file + self.temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) + temp_path = self.temp_file.name + self.temp_file.close() + + # Download file using mpremote + cmd = ["mpremote", "connect", device, "cp", ":/settings.json", temp_path] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + raise Exception(f"mpremote error: {result.stderr}") + + # Load the downloaded file + with open(temp_path, 'r') as f: + self.settings = json.load(f) + + # Update UI with loaded settings + self.update_ui_from_settings() + + self.update_status(f"Settings downloaded successfully from {device}") + messagebox.showinfo("Success", "Settings downloaded successfully!") + + except subprocess.TimeoutExpired: + self.update_status("Error: Connection timeout") + messagebox.showerror("Error", "Connection timeout. Check device connection.") + except FileNotFoundError: + self.update_status("Error: mpremote not found") + messagebox.showerror("Error", "mpremote not found. Please install it:\npip install mpremote") + except Exception as e: + self.update_status(f"Error: {str(e)}") + messagebox.showerror("Error", f"Failed to download settings:\n{str(e)}") + finally: + # Clean up temp file + if self.temp_file and os.path.exists(temp_path): + try: + os.unlink(temp_path) + except: + pass + + def upload_settings(self): + """Upload settings.json to the device using mpremote.""" + device = self.device_entry.get().strip() + if not device: + messagebox.showerror("Error", "Please specify a device") + return + + if not self.settings: + messagebox.showerror("Error", "No settings to upload. Please download or load settings first.") + return + + self.update_status("Uploading settings...") + + try: + # Get current settings from UI + self.update_settings_from_ui() + + # Create temporary file with current settings + temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) + temp_path = temp_file.name + json.dump(self.settings, temp_file, indent=2) + temp_file.close() + + # Upload file using mpremote + cmd = ["mpremote", "connect", device, "cp", temp_path, ":/settings.json"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + raise Exception(f"mpremote error: {result.stderr}") + + # Reset the device + self.update_status("Resetting device...") + try: + with serial.Serial(device, baudrate=115200) as ser: + ser.write(b'\x03\x03\x04') + except Exception as e: + # If serial reset fails, try mpremote method as fallback + reset_cmd = ["mpremote", "connect", device, "exec", "import machine; machine.reset()"] + subprocess.run(reset_cmd, capture_output=True, text=True, timeout=5) + + self.update_status(f"Settings uploaded and device reset on {device}") + messagebox.showinfo("Success", "Settings uploaded successfully and device reset!") + + except subprocess.TimeoutExpired: + self.update_status("Error: Connection timeout") + messagebox.showerror("Error", "Connection timeout. Check device connection.") + except FileNotFoundError: + self.update_status("Error: mpremote not found") + messagebox.showerror("Error", "mpremote not found. Please install it:\npip install mpremote") + except Exception as e: + self.update_status(f"Error: {str(e)}") + messagebox.showerror("Error", f"Failed to upload settings:\n{str(e)}") + finally: + # Clean up temp file + if os.path.exists(temp_path): + try: + os.unlink(temp_path) + except: + pass + + def load_from_file(self): + """Load settings from a local JSON file.""" + file_path = filedialog.askopenfilename( + title="Load Settings", + filetypes=[("JSON files", "*.json"), ("All files", "*.*")] + ) + + if not file_path: + return + + try: + with open(file_path, 'r') as f: + self.settings = json.load(f) + + self.update_ui_from_settings() + self.update_status(f"Settings loaded from {os.path.basename(file_path)}") + messagebox.showinfo("Success", "Settings loaded successfully!") + + except Exception as e: + self.update_status(f"Error: {str(e)}") + messagebox.showerror("Error", f"Failed to load settings:\n{str(e)}") + + def save_to_file(self): + """Save current settings to a local JSON file.""" + if not self.settings: + messagebox.showerror("Error", "No settings to save. Please download or load settings first.") + return + + file_path = filedialog.asksaveasfilename( + title="Save Settings", + defaultextension=".json", + filetypes=[("JSON files", "*.json"), ("All files", "*.*")] + ) + + if not file_path: + return + + try: + # Get current settings from UI + self.update_settings_from_ui() + + with open(file_path, 'w') as f: + json.dump(self.settings, f, indent=2) + + self.update_status(f"Settings saved to {os.path.basename(file_path)}") + messagebox.showinfo("Success", "Settings saved successfully!") + + except Exception as e: + self.update_status(f"Error: {str(e)}") + messagebox.showerror("Error", f"Failed to save settings:\n{str(e)}") + + def update_ui_from_settings(self): + """Update UI widgets with current settings values.""" + for key, widget in self.setting_widgets.items(): + if key in self.settings: + value = self.settings[key] + if isinstance(widget, ttk.Combobox): + # For debug, convert boolean to string + if key == "debug": + widget.set(str(value)) + else: + widget.set(str(value)) + else: + widget.delete(0, tk.END) + widget.insert(0, str(value)) + + def update_settings_from_ui(self): + """Update settings dictionary from UI widget values.""" + for key, widget in self.setting_widgets.items(): + value = widget.get().strip() + if value: + # Try to convert to appropriate type + if key in ["led_pin", "num_leds", "delay", "brightness", "id", "n1", "n2", "n3", "n4", "n5", "n6"]: + try: + self.settings[key] = int(value) + except ValueError: + pass # Keep as string if conversion fails + elif key == "debug": + # Convert string "True"/"False" to boolean + self.settings[key] = value == "True" + else: + self.settings[key] = value + elif key in self.settings: + # Keep existing value if widget is empty + pass + + +def main(): + root = tk.Tk() + app = LEDConfigTool(root) + root.mainloop() + + +if __name__ == "__main__": + main() +