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.

