From d45846ec7407395eeeee7947575e30ab23a033b0 Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 30 Nov 2025 18:35:31 +1300 Subject: [PATCH] Update main.py, tests, and add presets, circle test, and tool directory --- src/main.py | 7 +- src/presets.py | 31 +++ test/circle.py | 63 ++++++ test/patterns/n_chase.py | 1 + test/test_patterns_save_load.py | 1 + test/test_save_load.py | 1 + tool/README.md | 58 ++++++ tool/led_config.py | 329 ++++++++++++++++++++++++++++++++ 8 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 src/presets.py create mode 100644 test/circle.py create mode 100644 tool/README.md create mode 100755 tool/led_config.py diff --git a/src/main.py b/src/main.py index 50877fa..d43a4c3 100644 --- a/src/main.py +++ b/src/main.py @@ -15,8 +15,11 @@ async def main(): settings = Settings() patterns = Patterns(settings["led_pin"], settings["num_leds"], selected=settings["pattern"]) - if settings["color_order"] == "rbg": color_order = (1, 5, 3) - if settings["color_order"] == "grb": color_order = (3, 1, 5) + if settings["color_order"] == "rbg": + color_order = (1, 5, 3) + print("RBG") + if settings["color_order"] == "grb": + color_order = (3, 1, 5) else: color_order = (1, 3, 5) patterns.colors = [(8,0,0)] diff --git a/src/presets.py b/src/presets.py new file mode 100644 index 0000000..79b2041 --- /dev/null +++ b/src/presets.py @@ -0,0 +1,31 @@ +import json +import wifi +import ubinascii +import machine + +class Presets(dict): + FILE = "/presets.json" + + def __init__(self): + super().__init__() + self.load() # Load settings from file during initialization + + def save(self): + try: + j = json.dumps(self) + with open(self.FILE, 'w') as file: + file.write(j) + print("Presets saved successfully.") + except Exception as e: + print(f"Error saving settings: {e}") + + def load(self): + try: + with open(self.FILE, 'r') as file: + self.update(json.load(file)) + print("Presets loaded successfully.") + except Exception as e: + print(f"Error loading presets") + self.save() + + \ No newline at end of file diff --git a/test/circle.py b/test/circle.py new file mode 100644 index 0000000..6236347 --- /dev/null +++ b/test/circle.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Circle test: n1=50, n2=100, n3=200, n4=0 (Red) +Runs forever +Run with: mpremote run test/circle.py +""" + +import patterns +import utime +import _thread +from settings import Settings +from machine import WDT + +print("Starting Circle Test: n1=50, n2=100, n3=200, n4=0 (Red)") +print("Press Ctrl+C to stop") + +# Load settings +settings = Settings() + +# Initialize patterns using settings +p = patterns.Patterns( + pin=settings["led_pin"], + num_leds=settings["num_leds"], + brightness=255, + delay=2000 +) + +# Configure test parameters +p.n1 = 50 # Head moves 50 LEDs/second +p.n2 = 100 # Max length 100 LEDs +p.n3 = 200 # Tail moves 200 LEDs/second +p.n4 = 0 # Min length 0 LEDs +p.colors = [(255, 0, 0)] # Red + +print(f"LED Pin: {settings['led_pin']}") +print(f"LEDs: {settings['num_leds']}") +print(f"Brightness: {p.brightness}") +print(f"Parameters: n1={p.n1}, n2={p.n2}, n3={p.n3}, n4={p.n4}") +print(f"Color: {p.colors[0]}") + +# Initialize watchdog timer +wdt = WDT(timeout=10000) +wdt.feed() + +# Start pattern +p.select("circle") +if p.selected in p.patterns: + _thread.start_new_thread(p.patterns[p.selected], ()) + print("Pattern started. Running forever...") +else: + print(f"Pattern {p.selected} not found") + +# Run forever +try: + while True: + wdt.feed() + utime.sleep_ms(100) +except KeyboardInterrupt: + print("\nStopping...") + p.running = False + p.off() + print("LEDs turned off") + diff --git a/test/patterns/n_chase.py b/test/patterns/n_chase.py index e89d242..3d9b1e1 100644 --- a/test/patterns/n_chase.py +++ b/test/patterns/n_chase.py @@ -141,3 +141,4 @@ if __name__ == "__main__": + diff --git a/test/test_patterns_save_load.py b/test/test_patterns_save_load.py index 4afcd3b..a7c72c3 100644 --- a/test/test_patterns_save_load.py +++ b/test/test_patterns_save_load.py @@ -108,3 +108,4 @@ async def test_patterns_save_load(): if __name__ == "__main__": asyncio.run(test_patterns_save_load()) + diff --git a/test/test_save_load.py b/test/test_save_load.py index 2ebe493..1b453df 100644 --- a/test/test_save_load.py +++ b/test/test_save_load.py @@ -109,3 +109,4 @@ def test_save_load(): if __name__ == "__main__": test_save_load() + diff --git a/tool/README.md b/tool/README.md new file mode 100644 index 0000000..f268b94 --- /dev/null +++ b/tool/README.md @@ -0,0 +1,58 @@ +# LED Bar Configuration Tool + +A tkinter GUI tool for configuring LED bar settings via mpremote. + +## Features + +- Download `settings.json` from MicroPython device using mpremote +- Edit LED configuration settings +- Upload modified `settings.json` back to device +- Load/save settings from/to local files + +## Requirements + +- Python 3.x with tkinter (usually included) +- mpremote: `pip install mpremote` + +## Usage + +```bash +python3 tool/led_config.py +``` + +Or make it executable: +```bash +chmod +x tool/led_config.py +./tool/led_config.py +``` + +## Configuration Fields + +- **LED Pin**: GPIO pin number for LED strip +- **Number of LEDs**: Total number of LEDs in the strip +- **Color Order**: RGB or RBG color order +- **Device Name**: Name identifier for the device +- **Pattern**: Current LED pattern +- **Color 1/Color 2**: Primary colors (hex format, e.g., #ff0000) +- **Delay**: Pattern delay in milliseconds +- **Brightness**: LED brightness level +- **N1-N6**: Pattern-specific parameters +- **AP Password**: WiFi access point password +- **ID**: Device ID + +## Device Connection + +Default device is `/dev/ttyUSB0`. Change it in the "Device" field if your device is on a different port (e.g., `/dev/ttyACM0`, `COM3` on Windows). + +## Workflow + +1. Enter your device path (e.g., `/dev/ttyUSB0`) +2. Click "Download Settings" to fetch current settings from device +3. Edit any settings as needed +4. Click "Upload Settings" to save changes back to device + +You can also: +- Load settings from a local JSON file +- Save current settings to a local JSON file + + diff --git a/tool/led_config.py b/tool/led_config.py new file mode 100755 index 0000000..ad6574d --- /dev/null +++ b/tool/led_config.py @@ -0,0 +1,329 @@ +#!/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"]), + ("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"), + ] + + 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): + 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 + 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() +