#!/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()