GUI Meastro Python App For Raspberry Pi5


import tkinter as tk
from tkinter import ttk, messagebox
import serial
import threading

class ServoControllerGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Servo Controller")
        self.root.geometry("500x400")
        
        # Serial connection
        self.maestro = None
        self.port = '/dev/ttyACM0'
        self.connected = False
        
        # Create GUI elements
        self.create_widgets()
        
    def create_widgets(self):
        # Main frame
        main_frame = ttk.Frame(self.root, padding="20")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Port configuration
        port_frame = ttk.LabelFrame(main_frame, text="Connection", padding="10")
        port_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
        
        ttk.Label(port_frame, text="Port:").grid(row=0, column=0, sticky=tk.W)
        self.port_entry = ttk.Entry(port_frame, width=20)
        self.port_entry.insert(0, self.port)
        self.port_entry.grid(row=0, column=1, padx=5)
        
        self.connect_btn = ttk.Button(port_frame, text="Connect", command=self.toggle_connection)
        self.connect_btn.grid(row=0, column=2, padx=5)
        
        self.status_label = ttk.Label(port_frame, text="Disconnected", foreground="red")
        self.status_label.grid(row=0, column=3, padx=5)
        
        # Channel input
        channel_frame = ttk.LabelFrame(main_frame, text="Channel", padding="10")
        channel_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
        
        ttk.Label(channel_frame, text="Channel Number:").grid(row=0, column=0, sticky=tk.W)
        self.channel_var = tk.IntVar(value=3)
        self.channel_spinbox = ttk.Spinbox(channel_frame, from_=0, to=23, textvariable=self.channel_var, width=10)
        self.channel_spinbox.grid(row=0, column=1, padx=5)
        
        # Servo position control
        position_frame = ttk.LabelFrame(main_frame, text="Servo Position", padding="10")
        position_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10)
        
        # Position slider (in microseconds)
        ttk.Label(position_frame, text="Position (µs):").grid(row=0, column=0, sticky=tk.W, pady=5)
        
        self.position_var = tk.IntVar(value=1500)
        self.position_slider = ttk.Scale(
            position_frame, 
            from_=500, 
            to=2500, 
            orient=tk.HORIZONTAL, 
            length=400,
            variable=self.position_var,
            command=self.on_slider_change
        )
        self.position_slider.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
        
        # Position display
        self.position_label = ttk.Label(position_frame, text="1500 µs", font=("Arial", 12, "bold"))
        self.position_label.grid(row=2, column=0, pady=5)
        
        # Quick position buttons
        button_frame = ttk.Frame(position_frame)
        button_frame.grid(row=3, column=0, columnspan=3, pady=10)
        
        ttk.Button(button_frame, text="Min (500µs)", command=lambda: self.set_position(500)).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Center (1500µs)", command=lambda: self.set_position(1500)).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="Max (2500µs)", command=lambda: self.set_position(2500)).pack(side=tk.LEFT, padx=5)
        
        # Info label
        info_label = ttk.Label(main_frame, text="Move the slider to control servo position", 
                              foreground="gray", font=("Arial", 9, "italic"))
        info_label.grid(row=3, column=0, columnspan=2, pady=10)
        
    def toggle_connection(self):
        if not self.connected:
            self.connect()
        else:
            self.disconnect()
    
    def connect(self):
        try:
            self.port = self.port_entry.get()
            self.maestro = serial.Serial(self.port, 9600, timeout=1)
            self.connected = True
            self.status_label.config(text="Connected", foreground="green")
            self.connect_btn.config(text="Disconnect")
            self.port_entry.config(state="disabled")
            messagebox.showinfo("Success", f"Connected to {self.port}")
        except Exception as e:
            messagebox.showerror("Connection Error", f"Failed to connect:\n{str(e)}")
            self.connected = False
    
    def disconnect(self):
        if self.maestro:
            self.maestro.close()
            self.maestro = None
        self.connected = False
        self.status_label.config(text="Disconnected", foreground="red")
        self.connect_btn.config(text="Connect")
        self.port_entry.config(state="normal")
    
    def set_servo_target(self, channel, target):
        """
        Set servo target position.
        `target` is in quarter-microseconds.
        """
        if self.maestro and self.connected:
            try:
                command = bytearray([0x84, channel, target & 0x7F, (target >> 7) & 0x7F])
                self.maestro.write(command)
            except Exception as e:
                messagebox.showerror("Communication Error", f"Failed to send command:\n{str(e)}")
                self.disconnect()
    
    def on_slider_change(self, value):
        position = int(float(value))
        self.position_label.config(text=f"{position} µs")
        
        if self.connected:
            channel = self.channel_var.get()
            # Convert microseconds to quarter-microseconds
            target = position * 4
            threading.Thread(target=self.set_servo_target, args=(channel, target), daemon=True).start()
    
    def set_position(self, position):
        self.position_var.set(position)
        self.on_slider_change(position)
    
    def on_closing(self):
        self.disconnect()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = ServoControllerGUI(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

Thank you for sharing your code. Can you explain what it does and maybe post some screenshots of the GUI?

Brandon

Bonus! If anyone is interested I can hook them up with this more advance setup.