Newer
Older
Hardware / Plotter / plotterStepperMaker.py
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk
from plotter import Plotter
from plotterStepperMakerBackend import PlotterBackend
import argparse
import time
import os
import json
import sys


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

        self.save_file = save_file

        # Backend connection
        self.backend = PlotterBackend(Plotter())
        pos = self.backend.get_position()

        # Variables
        self.step_size = tk.DoubleVar(value=1.0)
        self.z_value = tk.DoubleVar(value=pos.get("z", 0.0))

        self.x_step = tk.DoubleVar(value=1.0)
        self.y_step = tk.DoubleVar(value=1.0)
        self.z_step = tk.DoubleVar(value=1.0)

        self.point_a = {"x": 0.0, "y": 0.0, "z": self.z_value.get()}
        self.point_b = {"x": 0.0, "y": 0.0, "z": self.z_value.get()}

        # --- GUI Layout ---
        self._build_gui(pos)
        self._center_window()

        # Load saved state if available
        self._load_state()

        # Periodic updates
        self._schedule_update()

        

    def _build_gui(self, pos):
        top_frame = ttk.Frame(self.root, padding=8)
        top_frame.grid(row=0, column=0, columnspan=3, sticky="ew")
        self.coord_label = ttk.Label(
            top_frame,
            text=self._fmt_pos(pos),
            font=("Arial", 12, "bold")
        )
        self.coord_label.pack(side="left", padx=(0, 15))

        self._build_move_controls()
        self._build_z_controls()
        self._build_step_controls()
        self._build_ab_controls()
        self._build_save_controls()

    # --- Build Frames ---
    def _build_move_controls(self):
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.grid(row=1, column=0, sticky="nw")
        move_frame = ttk.Frame(main_frame, padding=5, relief="groove")
        move_frame.grid(row=0, column=0, padx=5, pady=5)

        ttk.Button(move_frame, text="↑", width=5,
                   command=lambda: self.backend.move(0, self.step_size.get())).grid(row=0, column=1, pady=3)
        ttk.Button(move_frame, text="←", width=5,
                   command=lambda: self.backend.move(-self.step_size.get(), 0)).grid(row=1, column=0, padx=3)
        ttk.Button(move_frame, text="Home", width=7,
                   command=self.backend.set_home).grid(row=1, column=1, padx=3)
        ttk.Button(move_frame, text="→", width=5,
                   command=lambda: self.backend.move(self.step_size.get(), 0)).grid(row=1, column=2, padx=3)
        ttk.Button(move_frame, text="↓", width=5,
                   command=lambda: self.backend.move(0, -self.step_size.get())).grid(row=2, column=1, pady=3)

        tk.Scale(main_frame, from_=0.1, to=10, resolution=0.1,
                 orient="horizontal", variable=self.step_size,
                 length=260, label="Step size (mm)", font=("Arial", 10)).grid(row=1, column=0, pady=(10, 0))

    def _build_z_controls(self):
        z_frame = ttk.Frame(self.root, padding=8, relief="groove")
        z_frame.grid(row=0, rowspan=3, column=1, sticky="n", padx=5, pady=5)

        ttk.Label(z_frame, text="", font=("Arial", 10, "bold")).pack(side="top", pady=(0, 2))
        self.z_slider = tk.Scale(
            z_frame, from_=0, to=40, orient="vertical",
            variable=self.z_value, length=180, font=("Arial", 9)
        )
        self.z_slider.pack(side="top", fill="y", padx=2, pady=2)

        ttk.Button(
            z_frame, text="Set Z",
            command=lambda: self.backend.set_z(self.z_value.get())
        ).pack(side="top", pady=(5, 2))

    def _build_step_controls(self):
        step_frame = ttk.Frame(self.root, padding=8, relief="groove")
        step_frame.grid(row=0, rowspan=3, column=2, sticky="n", padx=5, pady=5)

        ttk.Label(step_frame, text="X", font=("Arial", 10, "bold")).grid(row=0, column=0)
        ttk.Label(step_frame, text="Y", font=("Arial", 10, "bold")).grid(row=0, column=1)
        ttk.Label(step_frame, text="Z", font=("Arial", 10, "bold")).grid(row=0, column=2)

        self.x_step_slider = tk.Scale(
            step_frame, from_=0.1, to=10, resolution=0.1,
            orient="vertical", variable=self.x_step,
            length=180, font=("Arial", 9)
        )
        self.x_step_slider.grid(row=1, column=0, padx=2, pady=2)

        self.y_step_slider = tk.Scale(
            step_frame, from_=0.1, to=10, resolution=0.1,
            orient="vertical", variable=self.y_step,
            length=180, font=("Arial", 9)
        )
        self.y_step_slider.grid(row=1, column=1, padx=2, pady=2)

        self.z_step_slider = tk.Scale(
            step_frame, from_=0, to=40, resolution=0.1,
            orient="vertical", variable=self.z_step,
            length=180, font=("Arial", 9)
        )
        self.z_step_slider.grid(row=1, column=2, padx=2, pady=2)

        ttk.Button(step_frame, text="Save Step Values", command=self.save_steps).grid(
            row=2, column=0, columnspan=3, pady=(5, 0)
        )

    def _build_ab_controls(self):
        ab_frame = ttk.Frame(self.root, padding=10, relief="groove")
        ab_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=10)

        ttk.Button(ab_frame, text="Set A", width=8, command=self.set_a).grid(row=0, column=0, padx=5)
        ttk.Button(ab_frame, text="Go A", width=8, command=self.backend.go_a).grid(row=0, column=1, padx=5)
        ttk.Button(ab_frame, text="Set B", width=8, command=self.set_b).grid(row=0, column=2, padx=5)
        ttk.Button(ab_frame, text="Go B", width=8, command=self.backend.go_b).grid(row=0, column=4, padx=5)
        ttk.Button(ab_frame, text="Go 1", width=8, command=self.backend.go_one).grid(row=0, column=5, padx=5)

        tk.Label(ab_frame, text="", font=("Arial", 10, "bold")).grid(row=2, column=0, sticky="w")
        self.point_a_value = tk.Label(ab_frame, text="X=+000.00, Y=+000.00, Z=+000.00", font=("Arial", 10))

        tk.Label(ab_frame, text="A:", font=("Arial", 10, "bold")).grid(row=3, column=0, sticky="w")
        self.point_a_value = tk.Label(ab_frame, text="X=+000.00, Y=+000.00, Z=+000.00", font=("Arial", 10))
        self.point_a_value.grid(row=3, column=1, columnspan=3, sticky="w")

        tk.Label(ab_frame, text="B:", font=("Arial", 10, "bold")).grid(row=4, column=0, sticky="w")
        self.point_b_value = tk.Label(ab_frame, text="X=+000.00, Y=+000.00, Z=+000.00", font=("Arial", 10))
        self.point_b_value.grid(row=4, column=1, columnspan=3, sticky="w")

        tk.Label(ab_frame, text="Steps:", font=("Arial", 10, "bold")).grid(row=5, column=0, sticky="w")
        self.step_values_label = tk.Label(ab_frame, text="X=0.0, Y=0.0, Z=0.0", font=("Arial", 10))
        self.step_values_label.grid(row=5, column=1, columnspan=3, sticky="w")

        tk.Label(ab_frame, text="Step count:", font=("Arial", 10, "bold")).grid(row=6, column=0, sticky="w")
        self.step_count_label = tk.Label(ab_frame, text="0", font=("Arial", 10))
        self.step_count_label.grid(row=6, column=1, columnspan=3, sticky="w")

        tk.Label(ab_frame, text="Current step:", font=("Arial", 10, "bold")).grid(row=7, column=0, sticky="w")
        self.current_step_label = tk.Label(ab_frame, text="0", font=("Arial", 10))
        self.current_step_label.grid(row=7, column=1, columnspan=3, sticky="w")

        tk.Label(ab_frame, text="Next step:", font=("Arial", 10, "bold")).grid(row=8, column=0, sticky="w")
        self.next_step_label = tk.Label(ab_frame, text="0", font=("Arial", 10))
        self.next_step_label.grid(row=8, column=1, columnspan=3, sticky="w")

    def _build_save_controls(self):
        save_frame = ttk.Frame(self.root, padding=8)
        save_frame.grid(row=4, column=0, columnspan=3, sticky="ew")

        tk.Label(save_frame, text="Save file:", font=("Arial", 10, "bold")).pack(side="left", padx=(0, 5))
        self.file_entry = ttk.Entry(save_frame, width=50)
        self.file_entry.insert(0, self.save_file)
        self.file_entry.pack(side="left", fill="x", expand=True)
        ttk.Button(save_frame, text="Save", command=self._save_state).pack(side="left", padx=(5, 0))

    # --- State management ---
    def _save_state(self):
        # prefer GUI-stored values, fall back to backend values, then sensible defaults
        point_a = getattr(self, "point_a", None) or getattr(self.backend, "point_a", None) or {"x": 0.0, "y": 0.0, "z": self.z_value.get()}
        point_b = getattr(self, "point_b", None) or getattr(self.backend, "point_b", None) or {"x": 0.0, "y": 0.0, "z": self.z_value.get()}

        state = {
            "point_a": point_a,
            "point_b": point_b,
            "steps": {
                "x": float(self.x_step.get()),
                "y": float(self.y_step.get()),
                "z": float(self.z_step.get())
            }
        }

        with open(self.file_entry.get(), "w") as f:
            json.dump(state, f, indent=2)

    def _load_state(self, filename=None):
        if filename is None:
            filename = self.save_file

        try:
            with open(filename, "r") as f:
                state = json.load(f)
        except (OSError, json.JSONDecodeError):
            return

        # Restore A
        if "point_a" in state:
            self.point_a = state["point_a"]
            self.point_a_value.config(
                text=f"X={self.point_a['x']:+07.2f}, Y={self.point_a['y']:+07.2f}, Z={self.point_a['z']:+07.2f}"
            )
            # Restore in backend
            self.backend.point_a = dict(self.point_a)

        # Restore B
        if "point_b" in state:
            self.point_b = state["point_b"]
            self.point_b_value.config(
                text=f"X={self.point_b['x']:+07.2f}, Y={self.point_b['y']:+07.2f}, Z={self.point_b['z']:+07.2f}"
            )
            # Restore in backend
            self.backend.point_b = dict(self.point_b)

        # Restore step sliders
        if "steps" in state:
            self.x_step.set(state["steps"].get("x", 1.0))
            self.y_step.set(state["steps"].get("y", 1.0))
            self.z_step.set(state["steps"].get("z", 1.0))
            # Restore in backend
            self.backend.set_step_sizes(self.x_step.get(), self.y_step.get(), self.z_step.get())

        # Update frontend step display
        self.step_values_label.config(
            text=f"X={self.x_step.get():.1f}, Y={self.y_step.get():.1f}, Z={self.z_step.get():.1f}"
        )
        # Update step info (current/next step)
        self.update_step_info()




    # --- Other GUI logic (unchanged from earlier split) ---
    def save_steps(self):
        self.backend.set_step_sizes(self.x_step.get(), self.y_step.get(), self.z_step.get())
        self.step_values_label.config(
            text=f"X={self.x_step.get():.1f}, Y={self.y_step.get():.1f}, Z={self.z_step.get():.1f}"
        )
        self.update_step_info()

    def set_a(self):
        pos = self.backend.set_a()
        if pos:
            # store the coordinates in the GUI instance for saving later
            self.point_a = {"x": float(pos["x"]), "y": float(pos["y"]), "z": float(pos["z"])}
            self.point_a_value.config(text=f"X={self.point_a['x']:+07.2f}, Y={self.point_a['y']:+07.2f}, Z={self.point_a['z']:+07.2f}")
            self.update_step_info()
        return pos

    def set_b(self):
        pos = self.backend.set_b()
        if pos:
            self.point_b = {"x": float(pos["x"]), "y": float(pos["y"]), "z": float(pos["z"])}
            self.point_b_value.config(text=f"X={self.point_b['x']:+07.2f}, Y={self.point_b['y']:+07.2f}, Z={self.point_b['z']:+07.2f}")
            self.update_step_info()
        return pos

    def go_one(self):
        first_coords = self.backend.go_one()
        if first_coords:
            self.update_step_info()

    def update_step_info(self):
        current_step, total_steps, next_step_number, next_pos = self.backend.next_raster_step()
        if current_step is None:
            self.step_count_label.config(text="0")
            self.current_step_label.config(text="N/A")
            self.next_step_label.config(text="N/A")
            return
        self.step_count_label.config(text=f"{total_steps}")
        self.current_step_label.config(text=f"{current_step}")
        if current_step >= total_steps:
            self.next_step_label.config(text="End reached")
        else:
            self.next_step_label.config(
                text=f"{next_step_number}, X={next_pos['x']:+07.2f}, Y={next_pos['y']:+07.2f}, Z={next_pos['z']:+07.2f}"
            )

    def _schedule_update(self):
        try:
            pos = self.backend.get_position()
            self.coord_label.config(text=self._fmt_pos(pos))
            self.update_step_info()
        except Exception:
            pass
        self.root.after(500, self._schedule_update)

    def _fmt_pos(self, pos):
        try:
            return f"X: {pos['x']:+07.2f}  Y: {pos['y']:+07.2f}  Z: {pos['z']:+07.2f}"
        except Exception:
            return "X: +000.00  Y: +000.00  Z: +000.00"

    def _center_window(self):
        self.root.update_idletasks()
        w, h = self.root.winfo_width(), self.root.winfo_height()
        sw, sh = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
        x, y = (sw // 2) - (w // 2), (sh // 2) - (h // 2)
        self.root.geometry(f"+{x}+{y}")


class DummyPlotter:
    def move_to(self, x, y, z=None):
        return {"x": x, "y": y, "z": z}
    def set_z(self, z):
        return {"z": z}
    def move_absolute(self, x=None, y=None, feed=1000):
        # Simulate absolute move by updating state only
        if x is not None:
            self.state["x"] = float(x)
        if y is not None:
            self.state["y"] = float(y)
        # Feed rate ignored in dummy
        print(f"[DummyPlotter] move_absolute -> {self.state}")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--file", help="Save file path")
    parser.add_argument("-xyz", help="JSON position to check step number")
    parser.add_argument("-n", type=int, help="Step number to get coordinates for")
    parser.add_argument("-count", action="store_true", help="Return total number of steps in grid")
    args = parser.parse_args()

    # Save file
    if args.file:
        save_file = args.file
    else:
        timestamp = int(time.time())
        save_file = f"/tmp/plotter_{timestamp}.json"

    # Use dummy if not starting GUI
    plotter = DummyPlotter()
    backend = PlotterBackend(plotter)

    # Load state if available
    if os.path.exists(save_file):
        with open(save_file, "r") as f:
            state = json.load(f)
            backend.load_state(state)

    if args.count and args.file:
        with open(args.file, "r") as f:
            state = json.load(f)
        backend.load_state(state)
        print(backend.get_total_steps())
        sys.exit(0)

    if args.xyz:
        pos = json.loads(args.xyz)
        step_no = backend.get_step_number(pos)
        print(step_no)
        sys.exit(0)

    if args.n is not None:
        coords = backend.get_step_coords(args.n)
        print(json.dumps(coords))
        sys.exit(0)

    # GUI mode
    root = tk.Tk()
    from plotterStepperMaker import PlotterGUI
    app = PlotterGUI(root, save_file)
    root.mainloop()

if __name__ == "__main__":
    main()