import tkinter as tk from tkinter import ttk, messagebox import serial import serial.tools.list_ports from PIL import Image, ImageTk # ---------- Global serial object ---------- ser = None # will hold serial.Serial instance # ---------- Serial helpers ---------- def list_serial_ports(): ports = serial.tools.list_ports.comports() return [p.device for p in ports] def connect(): global ser if ser is not None and ser.is_open: messagebox.showinfo("Info", "Already connected.") return port = port_var.get().strip() if not port: messagebox.showerror("Error", "Please enter a serial port.") return try: status_var.set(f"Connecting to {port}...") root.update_idletasks() ser = serial.Serial(port, baudrate=115200, timeout=0.1) status_var.set(f"Connected: {port}") on_button.config(state="normal") off_button.config(state="normal") ping_button.config(state="normal") except serial.SerialException as e: ser = None status_var.set("Disconnected") messagebox.showerror("Connection error", f"Could not open {port}\n\n{e}") def disconnect(): global ser if ser is not None: try: ser.close() except Exception: pass ser = None status_var.set("Disconnected") on_button.config(state="disabled") off_button.config(state="disabled") ping_button.config(state="disabled") def send_command(cmd): global ser if ser is None or not ser.is_open: messagebox.showwarning("Warning", "Not connected.") return try: ser.write((cmd + "\n").encode("ascii")) except serial.SerialException as e: status_var.set("Serial write error") messagebox.showerror("Serial error", str(e)) def handle_line(line): log_text.insert(tk.END, line + "\n") log_text.see(tk.END) line = line.strip() if line.startswith("DIST:"): value_str = line.split(":", 1)[1].strip() try: cm = float(value_str) distance_var.set(f"{cm:.1f} cm") except ValueError: distance_var.set("Invalid distance") elif line == "READY": status_var.set("READY (board initialized)") elif line.startswith("ACK:"): status_var.set(f"Acknowledged {line[4:]}") elif line == "PONG": status_var.set("PONG (board is alive)") def poll_serial(): global ser if ser is not None and ser.is_open: try: while ser.in_waiting: raw_line = ser.readline() if not raw_line: break try: line = raw_line.decode("utf-8", errors="ignore").strip() except Exception: line = raw_line.decode("latin1", errors="ignore").strip() if line: handle_line(line) except serial.SerialException: status_var.set("Disconnected (serial error)") disconnect() root.after(50, poll_serial) # ---------- GUI setup ---------- root = tk.Tk() root.title("VL53L5CX Distance Monitor") # ----- THEME / STYLE ----- root.configure(bg="#111111") # window background style = ttk.Style(root) style.theme_use("clam") # Base styles style.configure( "TFrame", background="#111111" ) style.configure( "TLabel", background="#111111", foreground="#f0f0f0", font=("Helvetica", 10) ) style.configure( "Header.TLabel", font=("Helvetica", 11, "bold"), foreground="#ffffff" ) style.configure( "Status.TLabel", foreground="#8ab4f8", font=("Helvetica", 10, "italic") ) style.configure( "TButton", background="#222222", foreground="#f0f0f0", padding=6 ) style.map( "TButton", background=[("active", "#333333")] ) # Distance display style style.configure( "Distance.TLabel", font=("Helvetica", 28, "bold"), foreground="#00e676", background="#111111" ) # Main container main_frame = ttk.Frame(root, padding=10) main_frame.grid(row=0, column=0, sticky="nsew") root.columnconfigure(0, weight=1) root.rowconfigure(0, weight=1) # ---- Load & Display Logo + Name (use main_frame, not 'main') ---- try: logo_img = Image.open("octoabbylogo.png") # make sure this file is next to your .py file logo_img = logo_img.resize((100, 100)) # tweak size if you want logo_tk = ImageTk.PhotoImage(logo_img) logo_label = ttk.Label(main_frame, image=logo_tk, background="#111111") logo_label.image = logo_tk # Prevent garbage collection logo_label.grid(row=0, column=0, columnspan=2, pady=(0, 4)) name_label = ttk.Label(main_frame, text="octoabby", style="Header.TLabel") name_label.grid(row=1, column=0, columnspan=2, pady=(0, 10)) except Exception as e: print("Logo load failed:", e) # Variables port_var = tk.StringVar(value="/dev/cu.usbmodem2101") status_var = tk.StringVar(value="Disconnected") distance_var = tk.StringVar(value="--.- cm") # ----- Connection section ----- conn_frame = ttk.LabelFrame(main_frame, text=" Connection ", padding=10) conn_frame.grid(row=2, column=0, columnspan=2, sticky="we", pady=(0, 10)) conn_frame.columnconfigure(1, weight=1) port_label = ttk.Label(conn_frame, text="Serial Port:") port_label.grid(row=0, column=0, sticky="w") port_entry = ttk.Entry(conn_frame, textvariable=port_var, width=30) port_entry.grid(row=0, column=1, sticky="we", padx=5) def fill_first_port(): ports = list_serial_ports() if ports: port_var.set(ports[0]) refresh_button = ttk.Button(conn_frame, text="Auto-detect", command=fill_first_port) refresh_button.grid(row=0, column=2, padx=5) connect_button = ttk.Button(conn_frame, text="Connect", command=connect) connect_button.grid(row=1, column=0, pady=8, sticky="we") disconnect_button = ttk.Button(conn_frame, text="Disconnect", command=disconnect) disconnect_button.grid(row=1, column=1, pady=8, sticky="we") status_label = ttk.Label(conn_frame, textvariable=status_var, style="Status.TLabel") status_label.grid(row=2, column=0, columnspan=3, sticky="w", pady=(4, 0)) # ----- Sensor control + distance ----- data_frame = ttk.Frame(main_frame, padding=(0, 5, 0, 10)) data_frame.grid(row=3, column=0, columnspan=2, sticky="we") # Left: buttons commands_frame = ttk.LabelFrame(data_frame, text=" Sensor Control ", padding=10) commands_frame.grid(row=0, column=0, sticky="nsw", padx=(0, 10)) on_button = ttk.Button(commands_frame, text="ON", width=10, command=lambda: send_command("ON"), state="disabled") on_button.grid(row=0, column=0, padx=5, pady=2) off_button = ttk.Button(commands_frame, text="OFF", width=10, command=lambda: send_command("OFF"), state="disabled") off_button.grid(row=1, column=0, padx=5, pady=2) ping_button = ttk.Button(commands_frame, text="PING", width=10, command=lambda: send_command("PING"), state="disabled") ping_button.grid(row=2, column=0, padx=5, pady=2) # Right: distance display distance_frame = ttk.LabelFrame(data_frame, text=" Distance ", padding=10) distance_frame.grid(row=0, column=1, sticky="nsew") data_frame.columnconfigure(1, weight=1) distance_title = ttk.Label(distance_frame, text="Latest Distance:", style="Header.TLabel") distance_title.grid(row=0, column=0, sticky="w") distance_value_label = ttk.Label( distance_frame, textvariable=distance_var, style="Distance.TLabel", anchor="center" ) distance_value_label.grid(row=1, column=0, sticky="nsew", pady=(5, 0)) distance_frame.columnconfigure(0, weight=1) # ----- Log section ----- log_frame = ttk.LabelFrame(main_frame, text=" Serial Log ", padding=10) log_frame.grid(row=4, column=0, columnspan=2, sticky="nsew") main_frame.rowconfigure(4, weight=1) main_frame.columnconfigure(1, weight=1) log_text = tk.Text( log_frame, height=10, width=70, bg="#000000", fg="#e0e0e0", insertbackground="#ffffff", borderwidth=0, highlightthickness=1, highlightbackground="#333333" ) log_text.grid(row=0, column=0, sticky="nsew") log_frame.rowconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1) # Scrollbar for log log_scroll = ttk.Scrollbar(log_frame, command=log_text.yview) log_scroll.grid(row=0, column=1, sticky="ns") log_text.config(yscrollcommand=log_scroll.set) # Start polling serial root.after(50, poll_serial) # Try to auto-fill first detected port on startup fill_first_port() root.mainloop()