Newer
Older
Hardware / Plotter / plotter_gui.py
#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk
import json
import os
import pyperclip
from plotter import Plotter, STATUS_FILE

POS_FILE = os.path.expanduser("~/.plotter_gui_state.json")


class PlotterGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Plotter Control")
        self.root.resizable(False, False)

        # Add global padding
        for i in range(7):
            self.root.grid_rowconfigure(i, pad=5)
        for i in range(5):
            self.root.grid_columnconfigure(i, pad=5)

        self.plotter = Plotter()

        # Load saved state if available
        saved_state = self.load_state()

        # Load saved positions
        pos = self.plotter.get_position()

        # Step size slider value
        self.step_size = tk.DoubleVar(value=saved_state.get("step_size", 1.0))

        # Z slider value (spindle speed), fall back to saved state if available
        self.z_value = tk.DoubleVar(value=saved_state.get("z_value", pos.get("z", 0.0)))

        # Coordinates display
        self.coord_label = ttk.Label(root, text=self.format_position(pos))
        self.coord_label.grid(row=0, column=0, columnspan=4, pady=5)

        # Copy JSON button
        self.copy_button = ttk.Button(root, text="Copy JSON", command=self.copy_position)
        self.copy_button.grid(row=0, column=4, padx=5)

        # Clipboard feedback directly under Copy JSON button
        self.feedback_label = ttk.Label(root, text="", foreground="green")
        self.feedback_label.grid(row=1, column=4, pady=2)

        # Directional buttons
        self.up_btn = ttk.Button(root, text="^", command=lambda: self.move(0, self.step_size.get()))
        self.up_btn.grid(row=2, column=1)

        self.left_btn = ttk.Button(root, text="<", command=lambda: self.move(-self.step_size.get(), 0))
        self.left_btn.grid(row=3, column=0)

        self.home_btn = ttk.Button(root, text="Set Home", command=self.set_home)
        self.home_btn.grid(row=3, column=1)

        self.right_btn = ttk.Button(root, text=">", command=lambda: self.move(self.step_size.get(), 0))
        self.right_btn.grid(row=3, column=2)

        self.down_btn = ttk.Button(root, text="V", command=lambda: self.move(0, -self.step_size.get()))
        self.down_btn.grid(row=4, column=1)

        # Step size slider spans across the movement button area
        self.step_slider = tk.Scale(root, from_=0.1, to=10, resolution=0.1,
                                    orient="horizontal", variable=self.step_size, length=250)
        self.step_slider.grid(row=5, column=0, columnspan=3, pady=5)
        self.step_label = ttk.Label(root, text="Step size (mm)")
        self.step_label.grid(row=6, column=0, columnspan=3)

        # Z slider (spindle speed) with 40 at bottom and 0 at top
        self.z_slider = tk.Scale(root, from_=0, to=40, orient="vertical", variable=self.z_value)
        self.z_slider.grid(row=2, column=4, rowspan=3, padx=10)

        self.set_z_btn = ttk.Button(root, text="Set Z", command=self.set_z)
        self.set_z_btn.grid(row=5, column=4)

        # Restore last window position or centre
        self.restore_window_position(saved_state)
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

        # Update display
        self.update_position()

    def format_position(self, pos):
        return f"X: {pos['x']:.2f}  Y: {pos['y']:.2f}  Z: {pos['z']:.2f}"

    def update_position(self):
        pos = self.plotter.get_position()
        self.coord_label.config(text=self.format_position(pos))
        self.root.after(500, self.update_position)

    def move(self, x, y):
        self.plotter.move(x=x, y=y)
        self.update_position()

    def set_z(self):
        z = self.z_value.get()
        self.plotter.set_spindle(z)
        self.update_position()

    def set_home(self):
        self.plotter.set_home()
        self.update_position()

    def copy_position(self):
        pos = self.plotter.get_position()
        json_str = json.dumps(pos)
        pyperclip.copy(json_str)
        self.feedback_label.config(text="Copied JSON")
        self.root.after(2000, lambda: self.feedback_label.config(text=""))

    def load_state(self):
        if os.path.exists(POS_FILE):
            try:
                with open(POS_FILE, "r") as f:
                    return json.load(f)
            except Exception:
                return {}
        return {}

    def restore_window_position(self, state):
        geom = state.get("geometry")
        if geom:
            self.root.geometry(geom)
        else:
            self.center_window()

    def center_window(self):
        self.root.update_idletasks()
        w = self.root.winfo_width()
        h = self.root.winfo_height()
        ws = self.root.winfo_screenwidth()
        hs = self.root.winfo_screenheight()
        x = (ws // 2) - (w // 2)
        y = (hs // 2) - (h // 2)
        self.root.geometry(f"+{x}+{y}")

    def on_close(self):
        try:
            state = {
                "geometry": self.root.geometry(),
                "step_size": self.step_size.get(),
                "z_value": self.z_value.get()
            }
            with open(POS_FILE, "w") as f:
                json.dump(state, f)
        except Exception:
            pass
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    app = PlotterGUI(root)
    root.mainloop()