Files
AIO_3D_Print_Local_Screen/pages/control_page.py

590 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QFrame, QGridLayout, QSizePolicy, QLineEdit, QApplication,
)
from PyQt6.QtCore import Qt, QTimer, QPointF, QRectF, QEvent
from PyQt6.QtGui import QFont, QPainter, QColor, QBrush, QPen, QDoubleValidator
from utils.config_parse import ConfigParse
from utils.floating_keyboard import FloatingKeyboard
MOVE_STEP = 10 # 每次点击移动 mm (保留备用)
MOVE_DETECT_MS = 300
class JoystickWidget(QWidget):
"""圆形 XY 摇杆 — 拖动小圆控制方向,松开回中,越远越快"""
def __init__(self, parent=None, max_speed=3000):
super().__init__(parent)
self.setFixedSize(320, 320)
self.setMouseTracking(True)
self._max_speed = max_speed # 最大进给速度 mm/min
self._radius_outer = 140 # 外圈半径
self._radius_inner = 30 # 内圈小圆半径
self._deadzone = 10 # 死区像素
# 当前小圆偏移(相对中心,像素)
self._dx = 0.0
self._dy = 0.0
self._pressed = False
# 定时发送 GCode
self._move_timer = QTimer(self)
self._move_timer.setInterval(MOVE_DETECT_MS)
self._move_timer.timeout.connect(self._emit_move)
# 回调(上层设置)
self.on_move = None # callable(dx_norm, dy_norm, feedrate)
def _emit_move(self):
"""定时器触发:计算速度方向并回调"""
dist = (self._dx ** 2 + self._dy ** 2) ** 0.5
if dist < self._deadzone:
return
ratio = min(dist / (self._radius_outer - self._radius_inner), 1.0)
# 速度映射30% ~ 100% * max_speed
# speed = int((0.3 + ratio * 0.7) * self._max_speed)
# ratio = min(dist / max_dist, 1.0)
speed = ratio * self._max_speed / (1000/MOVE_DETECT_MS)
if dist > 0:
nx = self._dx / dist
ny = self._dy / dist
if self.on_move:
self.on_move(nx, ny, speed)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
cx, cy = self.width() / 2, self.height() / 2
# 外圈背景
painter.setBrush(QBrush(QColor("#2a2a2a")))
painter.setPen(QPen(QColor("#666666"), 3))
painter.drawEllipse(QPointF(cx, cy), self._radius_outer, self._radius_outer)
# 十字准线(浅)
painter.setPen(QPen(QColor("#555555"), 1))
painter.drawLine(int(cx - self._radius_outer), int(cy),
int(cx + self._radius_outer), int(cy))
painter.drawLine(int(cx), int(cy - self._radius_outer),
int(cx), int(cy + self._radius_outer))
# 小圆(可拖动)
knob_x = cx + self._dx
knob_y = cy + self._dy
painter.setBrush(QBrush(QColor("#4a9fc8" if self._pressed else "#6fb8dd")))
painter.setPen(QPen(QColor("#2f6f91"), 2))
painter.drawEllipse(QPointF(knob_x, knob_y), self._radius_inner, self._radius_inner)
# 中心小点标记
painter.setBrush(QBrush(QColor("#888888")))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawEllipse(QPointF(cx, cy), 4, 4)
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = True
self._update_knob(event.position())
self._move_timer.start()
def mouseMoveEvent(self, event):
if self._pressed:
self._update_knob(event.position())
def mouseReleaseEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = False
self._move_timer.stop()
self._dx = 0.0
self._dy = 0.0
self.update()
def _update_knob(self, pos):
cx, cy = self.width() / 2, self.height() / 2
dx = pos.x() - cx
dy = pos.y() - cy
max_r = self._radius_outer - self._radius_inner
dist = (dx ** 2 + dy ** 2) ** 0.5
if dist > max_r:
dx = dx / dist * max_r
dy = dy / dist * max_r
self._dx = dx
self._dy = dy
self.update()
class ZFaderWidget(QWidget):
"""Z 轴拨杆 — 上下拖动控制 Z 速度,松开回中,越远越快"""
def __init__(self, parent=None, max_speed=200):
super().__init__(parent)
self.setFixedSize(120, 320)
self.setMouseTracking(True)
self._max_speed = max_speed
self._track_h = 260 # 滑道高度
self._knob_h = 40 # 滑块高度
self._knob_w = 80 # 滑块宽度
self._deadzone = 8
self._dy = 0.0 # 相对中心的偏移
self._pressed = False
self._move_timer = QTimer(self)
self._move_timer.setInterval(MOVE_DETECT_MS)
self._move_timer.timeout.connect(self._emit_move)
self.on_move = None # callable(direction, feedrate) direction: +1 or -1
def _emit_move(self):
dist = abs(self._dy)
if dist < self._deadzone:
return
max_dist = (self._track_h - self._knob_h) / 2
ratio = min(dist / max_dist, 1.0)
# speed = int((0.3 + ratio * 0.7) * self._max_speed / 10)
speed = ratio * self._max_speed / (1000/MOVE_DETECT_MS)
direction = 1 if self._dy < 0 else -1 # dy<0 → 向上
if self.on_move:
self.on_move(direction, speed)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
cx = self.width() / 2
# 滑道
rect = QRectF(cx - 12, (self.height() - self._track_h) / 2, 24, self._track_h)
painter.setBrush(QBrush(QColor("#2a2a2a")))
painter.setPen(QPen(QColor("#666666"), 2))
painter.drawRoundedRect(rect, 12, 12)
# 中心刻度线
mid_y = self.height() / 2
painter.setPen(QPen(QColor("#777777"), 2))
painter.drawLine(int(cx - 20), int(mid_y), int(cx + 20), int(mid_y))
# 滑块
knob_center_y = mid_y + self._dy
knob_y = knob_center_y - self._knob_h / 2
knob_rect = QRectF(cx - self._knob_w / 2, knob_y, self._knob_w, self._knob_h)
color = "#4a9fc8" if self._pressed else "#6fb8dd"
painter.setBrush(QBrush(QColor(color)))
painter.setPen(QPen(QColor("#2f6f91"), 2))
painter.drawRoundedRect(knob_rect, 10, 10)
# Z 标签
painter.setPen(QPen(QColor("#a0a0a0"), 1))
font = QFont("sans-serif", 14, QFont.Weight.Bold)
painter.setFont(font)
painter.drawText(knob_rect, Qt.AlignmentFlag.AlignCenter, "Z")
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = True
self._update_knob(event.position().y())
self._move_timer.start()
def mouseMoveEvent(self, event):
if self._pressed:
self._update_knob(event.position().y())
def mouseReleaseEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._pressed = False
self._move_timer.stop()
self._dy = 0.0
self.update()
def _update_knob(self, py):
mid_y = self.height() / 2
max_d = (self._track_h - self._knob_h) / 2
dy = py - mid_y
dy = max(-max_d, min(max_d, dy))
self._dy = dy
self.update()
class ControlPage(QWidget):
def __init__(self, api_client, parent=None):
super().__init__(parent)
self.api_client = api_client
self.config_parser = ConfigParse()
self.config_parser.config_changed.connect(self._on_config_changed)
self._load_limits()
self._load_speeds()
# 当前坐标(本地跟踪)
self.pos_x = 0.0
self.pos_y = 0.0
self.pos_z = 0.0
self._is_printing = False
self._homed = False # 是否已轴回零
self._motor_on = True # 电机是否已使能默认True关电机后置False
# 归零后的停留位置
hp = self.config_parser.home_positions or {}
self._home_x = hp.get("x", 0.0)
self._home_y = hp.get("y", 0.0)
self._home_z = hp.get("z", 0.0)
# 悬浮键盘
self._keyboard = FloatingKeyboard()
self._keyboard_attached = False
self.init_ui()
self._sync_inputs()
self._apply_state()
self._start_status_poll()
def _load_speeds(self):
ms = self.config_parser.move_max_speed or {}
self.move_speed_xy = ms.get("xy", 3000)
self.move_speed_z = ms.get("z", 200)
def _start_status_poll(self):
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._poll_status)
self._status_timer.start(2000)
def _poll_status(self):
"""定时轮询打印机状态,更新按钮启用状态和坐标"""
try:
data = self.api_client.get_status()
flags = data.get("status", {}).get("state", {}).get("flags", {})
printing = flags.get("printing", False)
paused = flags.get("paused", False)
self._is_printing = printing or paused
except Exception:
self._is_printing = False
self._update_buttons()
def _load_limits(self):
ma = self.config_parser.move_axis_area or {}
self.x_min = ma.get("x_min", 0)
self.x_max = ma.get("x_max", 300)
self.y_min = ma.get("y_min", 0)
self.y_max = ma.get("y_max", 300)
self.z_min = ma.get("z_min", 0)
self.z_max = ma.get("z_max", 300)
def _on_config_changed(self, config_instance):
self._load_limits()
self._load_speeds()
# ── 状态管理 ──────────────────────────────────────────
def _is_control_enabled(self):
"""摇杆/拨杆/坐标输入是否可用:已回零 + 电机已使能 + 非打印中"""
return self._homed and self._motor_on and not self._is_printing
def _apply_state(self):
"""根据回零/电机状态同步所有控件"""
enabled = self._is_control_enabled()
self.joystick.setEnabled(enabled)
self.z_fader.setEnabled(enabled)
for inp in (self.input_x, self.input_y, self.input_z):
inp.setEnabled(enabled)
if not enabled:
inp.setText("---")
if enabled:
self._sync_inputs()
def _sync_inputs(self):
"""将 pos 值同步到输入框"""
if self._is_control_enabled():
self.input_x.setText(f"{self.pos_x:.3f}")
self.input_y.setText(f"{self.pos_y:.3f}")
self.input_z.setText(f"{self.pos_z:.3f}")
def _on_coord_changed(self):
"""输入框手动输入触发运动"""
if not self._is_control_enabled():
return
try:
tx = float(self.input_x.text())
ty = float(self.input_y.text())
tz = float(self.input_z.text())
except ValueError:
return
tx = max(self.x_min, min(self.x_max, tx))
ty = max(self.y_min, min(self.y_max, ty))
tz = max(self.z_min, min(self.z_max, tz))
gcode = f"G1 X{tx:.3f} Y{ty:.3f} Z{tz:.3f} F3000"
self.api_client.send_gcode(gcode)
self.pos_x, self.pos_y, self.pos_z = tx, ty, tz
self._sync_inputs()
def _update_buttons(self):
printing = self._is_printing
self.btn_pause.setEnabled(printing)
self.btn_stop.setEnabled(printing)
idle = not printing
self.btn_home.setEnabled(idle)
self.btn_level.setEnabled(idle)
self.btn_motor_off.setEnabled(idle)
self._apply_state()
# ── GCode 发送 ──────────────────────────────────────────
def _send_move(self, x=None, y=None, z=None, feedrate=None):
if not self._is_control_enabled():
return
target_x = self.pos_x if x is None else x
target_y = self.pos_y if y is None else y
target_z = self.pos_z if z is None else z
target_x = max(self.x_min, min(self.x_max, target_x))
target_y = max(self.y_min, min(self.y_max, target_y))
target_z = max(self.z_min, min(self.z_max, target_z))
f = feedrate or 3000
gcode = f"G1 X{target_x:.3f} Y{target_y:.3f} Z{target_z:.3f} F{f}"
self.api_client.send_gcode(gcode)
self.pos_x, self.pos_y, self.pos_z = target_x, target_y, target_z
self._sync_inputs()
# ── 摇杆/拨杆回调 ──
def _on_joystick_move(self, nx, ny, speed):
if not self._is_control_enabled():
return
step = speed * MOVE_DETECT_MS / 1000
dx = nx * step
dy = ny * step
tx = max(self.x_min, min(self.x_max, self.pos_x + dx))
ty = max(self.y_min, min(self.y_max, self.pos_y + dy))
gcode = f"G1 X{tx:.1f} Y{ty:.1f} F{speed}"
print(f"Moving: X{tx:.1f} Y{ty:.1f} F{speed}")
self.api_client.send_gcode(gcode)
self.pos_x, self.pos_y = tx, ty
self._sync_inputs()
def _on_fader_move(self, direction, speed):
if not self._is_control_enabled():
return
step = speed * MOVE_DETECT_MS / 1000
tz = self.pos_z + direction * step
tz = max(self.z_min, min(self.z_max, tz))
gcode = f"G1 Z{tz:.3f} F{speed}"
print(f"Moving: Z{tz:.3f} F{speed}")
self.api_client.send_gcode(gcode)
self.pos_z = tz
self._sync_inputs()
# ── 指令按钮 ──
def _cmd_pause(self):
self.api_client.pause_print()
def _cmd_stop(self):
self.api_client.stop_print()
def _cmd_home(self):
self.api_client.home_axes(["x", "y", "z"])
self.pos_x = self._home_x
self.pos_y = self._home_y
self.pos_z = self._home_z
self._homed = True
self._motor_on = True
self._sync_inputs()
self._apply_state()
def _cmd_level(self):
self.api_client.auto_leveling()
def _cmd_motor_off(self):
self.api_client.off_motors()
self._motor_on = False
self._apply_state()
# ── 悬浮键盘 ─────────────────────────────────────────────
def _attach_keyboard(self, widget):
"""将悬浮键盘绑定到指定输入框并显示"""
self._keyboard.attach(widget)
self._keyboard.show_below(widget)
self._keyboard_attached = True
def _dismiss_keyboard(self):
"""关闭悬浮键盘"""
self._keyboard.hide()
self._keyboard.detach()
self._keyboard_attached = False
def _on_input_focus_in(self, widget):
"""输入框获得焦点时的处理"""
if self._keyboard_attached:
self._keyboard.attach(widget)
self._keyboard.show_below(widget)
else:
self._attach_keyboard(widget)
def eventFilter(self, obj, event):
if event.type() == QEvent.Type.FocusIn:
if obj in (self.input_x, self.input_y, self.input_z):
self._on_input_focus_in(obj)
elif self._keyboard_attached and not isinstance(obj, QLineEdit):
self._dismiss_keyboard()
elif event.type() == QEvent.Type.FocusOut:
if obj in (self.input_x, self.input_y, self.input_z):
QTimer.singleShot(100, self._check_dismiss_keyboard)
return super().eventFilter(obj, event)
def _check_dismiss_keyboard(self):
"""检查当前焦点是否还在坐标输入框上,不在则关闭键盘"""
w = self.focusWidget()
if w not in (self.input_x, self.input_y, self.input_z):
self._dismiss_keyboard()
# ── UI 构建 ──────────────────────────────────────────────
def init_ui(self):
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(14, 14, 14, 14)
main_layout.setSpacing(14)
# =========================== 左栏 ===========================
left_panel = QFrame()
left_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
left_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(16, 14, 16, 14)
left_layout.setSpacing(8)
left_title = QLabel("控制")
left_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
left_title.setStyleSheet("color: #efefef; font-size: 22px; font-weight: 700;")
left_layout.addWidget(left_title)
btn_style = """
QPushButton {
min-height: 50px;
font-size: 18px;
font-weight: 600;
color: #f8f8f8;
background-color: #555555;
border: 2px solid #888888;
border-radius: 8px;
padding: 4px 12px;
}
QPushButton:hover {
background-color: #636363;
border-color: #aaaaaa;
}
QPushButton:pressed {
background-color: #3d3d3d;
border-color: #5a9fcf;
}
QPushButton:disabled {
color: #606060;
background-color: #3a3a3a;
border-color: #505050;
}
"""
grid_btns = QGridLayout()
grid_btns.setSpacing(8)
self.btn_pause = QPushButton("⏸ 暂停打印")
self.btn_pause.setStyleSheet(btn_style)
self.btn_pause.clicked.connect(self._cmd_pause)
grid_btns.addWidget(self.btn_pause, 0, 0)
self.btn_stop = QPushButton("⏹ 停止打印")
self.btn_stop.setStyleSheet(btn_style)
self.btn_stop.clicked.connect(self._cmd_stop)
grid_btns.addWidget(self.btn_stop, 0, 1)
self.btn_home = QPushButton("⌂ 轴回零")
self.btn_home.setStyleSheet(btn_style)
self.btn_home.clicked.connect(self._cmd_home)
grid_btns.addWidget(self.btn_home, 1, 0)
self.btn_level = QPushButton("◉ 自动调平")
self.btn_level.setStyleSheet(btn_style)
self.btn_level.clicked.connect(self._cmd_level)
grid_btns.addWidget(self.btn_level, 1, 1)
self.btn_motor_off = QPushButton("⏻ 关电机")
self.btn_motor_off.setStyleSheet(btn_style)
self.btn_motor_off.clicked.connect(self._cmd_motor_off)
grid_btns.addWidget(self.btn_motor_off, 2, 0, 1, 2)
left_layout.addLayout(grid_btns)
main_layout.addWidget(left_panel, 2)
# =========================== 右栏 ===========================
right_panel = QFrame()
right_panel.setStyleSheet("background-color: #3f3f3f; border-radius: 10px;")
right_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(16, 14, 16, 14)
right_layout.setSpacing(10)
# ── 坐标输入 ──
coord_row = QHBoxLayout()
coord_row.setSpacing(12)
coord_row.setAlignment(Qt.AlignmentFlag.AlignCenter)
input_style = """
QLineEdit {
min-width: 80px;
max-width: 120px;
min-height: 44px;
font-size: 24px;
font-weight: 700;
color: #e0e0e0;
background-color: #2a2a2a;
border: 2px solid #646464;
border-radius: 8px;
padding: 4px 10px;
text-align: center;
}
QLineEdit:focus {
border-color: #5a9fcf;
}
QLineEdit:disabled {
color: #606060;
background-color: #222222;
border-color: #444444;
}
"""
for axis, val in [("X", 0.0), ("Y", 0.0), ("Z", 0.0)]:
lbl = QLabel(f"{axis}:")
lbl.setStyleSheet("color: #d0d0d0; font-size: 24px; font-weight: 700;")
coord_row.addWidget(lbl)
inp = QLineEdit(f"{val:.1f}")
inp.setStyleSheet(input_style)
inp.setAlignment(Qt.AlignmentFlag.AlignCenter)
inp.setValidator(QDoubleValidator(-9999, 9999, 1))
inp.returnPressed.connect(self._on_coord_changed)
inp.installEventFilter(self)
setattr(self, f"input_{axis.lower()}", inp)
coord_row.addWidget(inp)
right_layout.addLayout(coord_row)
# ── 操作器 ──
manip_row = QHBoxLayout()
manip_row.setSpacing(14)
manip_row.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.joystick = JoystickWidget(max_speed=self.move_speed_xy)
self.joystick.on_move = self._on_joystick_move
manip_row.addWidget(self.joystick)
self.z_fader = ZFaderWidget(max_speed=self.move_speed_z)
self.z_fader.on_move = self._on_fader_move
manip_row.addWidget(self.z_fader)
right_layout.addLayout(manip_row)
main_layout.addWidget(right_panel, 3)
self._update_buttons()