#!/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"), ("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()