基本界面达成
This commit is contained in:
17
config(example).json
Normal file
17
config(example).json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"api_url": "http://127.0.0.1:5001/api/v1",
|
||||
"api_key": "############################################",
|
||||
"gcode_dir": "/home/octo/.octoprint/uploads",
|
||||
"move_axis_area": {
|
||||
"x_min": -14,
|
||||
"x_max": 222,
|
||||
"y_min": -16,
|
||||
"y_max": 228,
|
||||
"z_min": -0.5,
|
||||
"z_max": 250
|
||||
},
|
||||
"move_max_speed": {
|
||||
"xy": 3000,
|
||||
"z": 200
|
||||
}
|
||||
}
|
||||
22
install.sh
Normal file → Executable file
22
install.sh
Normal file → Executable file
@@ -2,4 +2,24 @@
|
||||
|
||||
# apt install -y python3-pyqt6.qtwebengine
|
||||
# apt install -y python3-pyqt6.qtquick3d
|
||||
# apt install -y python3-pyqt6.qtquick
|
||||
# apt install -y python3-pyqt6.qtquick
|
||||
|
||||
echo "Installing Printer Screen Menu..."
|
||||
echo "Cleaning up old installation..."
|
||||
rm -rf /etc/systemd/system/printer-screen.service
|
||||
rm -rf /opt/Printer_Screen_Menu
|
||||
|
||||
echo "Copying new files..."
|
||||
mkdir -p /opt/Printer_Screen_Menu
|
||||
cp -r ./. /opt/Printer_Screen_Menu/
|
||||
|
||||
echo "Setting up systemd service..."
|
||||
chmod +x /opt/Printer_Screen_Menu/run.sh
|
||||
|
||||
systemctl disable printer-screen.service
|
||||
cp printer-screen.service /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable printer-screen.service
|
||||
systemctl start printer-screen.service
|
||||
|
||||
echo "Installation complete. Printer Screen Menu should now be running."
|
||||
108
main.py
108
main.py
@@ -1,36 +1,96 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from PyQt6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QStackedWidget)
|
||||
from PyQt6.QtCore import Qt, QSize
|
||||
from PyQt6.QtCore import Qt, QSize, QTimer
|
||||
from PyQt6.QtGui import QIcon, QFont
|
||||
|
||||
from pages.status_page import StatusPage
|
||||
from pages.control_page import ControlPage
|
||||
from pages.setting_page import SettingPage
|
||||
from utils.aio_print_api import AIOPrrintSystemAPI
|
||||
from utils.auto_fan_status import AutoFanStatus
|
||||
from utils.config_parse import ConfigParse
|
||||
from utils.wifi_manager import WifiManager
|
||||
|
||||
def load_config():
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Failed to load config: {e}")
|
||||
return {}
|
||||
|
||||
class MainWindow(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# 初始化 API 客户端
|
||||
config = load_config()
|
||||
self.config_parser = ConfigParse()
|
||||
self.config_parser.config_changed.connect(self._on_config_changed)
|
||||
self.api_client = AIOPrrintSystemAPI(
|
||||
api_url=config.get("api_url", "http://127.0.0.1:5001/api/v1"),
|
||||
api_key=config.get("api_key", "")
|
||||
api_url=self.config_parser.api_url,
|
||||
api_key=self.config_parser.api_key
|
||||
)
|
||||
self.auto_fan_status = AutoFanStatus()
|
||||
self.wifi_manager = WifiManager()
|
||||
self._last_network_check = 0.0
|
||||
self._is_network_connected = False
|
||||
self.init_ui()
|
||||
|
||||
# 定时刷新风扇状态显示
|
||||
self._fan_timer = QTimer(self)
|
||||
self._fan_timer.timeout.connect(self._update_top_bar)
|
||||
self._fan_timer.start(1000)
|
||||
|
||||
def _check_network(self):
|
||||
"""检查网络连接状态(每30秒检测一次,避免频繁调用)"""
|
||||
now = time.time()
|
||||
if now - self._last_network_check < 30:
|
||||
return self._is_network_connected
|
||||
self._last_network_check = now
|
||||
try:
|
||||
status = self.wifi_manager.get_current_status()
|
||||
self._is_network_connected = status.get("wpa_state") == "COMPLETED"
|
||||
except Exception:
|
||||
self._is_network_connected = False
|
||||
return self._is_network_connected
|
||||
|
||||
def _on_config_changed(self, config_instance):
|
||||
"""配置变化时更新 API 客户端等相关设置"""
|
||||
self.api_client = AIOPrrintSystemAPI(
|
||||
api_url=self.config_parser.api_url,
|
||||
api_key=self.config_parser.api_key
|
||||
)
|
||||
|
||||
def _update_top_bar(self):
|
||||
"""更新风扇状态横条显示"""
|
||||
s = self.auto_fan_status
|
||||
temp = f"{s.cpu_temp:.1f}°C" if s.is_auto_fan_service_running else "--.-°C"
|
||||
speed_pct = min(s.fan_speed / 255 * 100, 100)
|
||||
speed = f"{speed_pct:.1f}%" if s.is_auto_fan_service_running else "--.-%"
|
||||
state = s.fan_state if s.is_auto_fan_service_running else "Auto Fan service is not running"
|
||||
rpm = f"{s.fan_rpm:.1f} r/min" if s.is_auto_fan_service_running else "--.-- r/min"
|
||||
|
||||
# 根据运行状态改变颜色
|
||||
if s.is_auto_fan_service_running:
|
||||
if s.fan_state == "Stalled":
|
||||
color = "#e86c60" # 风扇异常-红色
|
||||
else:
|
||||
color = "#a0d8a0" # 正常-浅绿色
|
||||
else:
|
||||
color = "#e8a060" # 异常-橙色
|
||||
self._top_bar.setStyleSheet(
|
||||
f"background-color: #2a2a2a; color: {color}; "
|
||||
f"font-size: 18px; font-weight: 600; padding: 4px 16px;"
|
||||
)
|
||||
self._fan_label.setText(
|
||||
f"🌡 {temp} {state} 𖣘 {speed} {rpm}"
|
||||
)
|
||||
|
||||
# 更新时钟(有网络时显示)
|
||||
if self._check_network():
|
||||
now = datetime.now()
|
||||
self._clock_label.setText(now.strftime("%H:%M:%S"))
|
||||
self._clock_label.show()
|
||||
else:
|
||||
self._clock_label.hide()
|
||||
|
||||
def init_ui(self):
|
||||
# 整体布局
|
||||
main_layout = QVBoxLayout(self)
|
||||
@@ -50,6 +110,27 @@ class MainWindow(QWidget):
|
||||
self.stacked_widget.addWidget(self.page_control)
|
||||
self.stacked_widget.addWidget(self.page_settings)
|
||||
|
||||
# 风扇状态横条
|
||||
self._top_bar = QWidget()
|
||||
self._top_bar.setFixedHeight(36)
|
||||
self._top_bar.setStyleSheet(
|
||||
"background-color: #2a2a2a; color: #a0d8a0; "
|
||||
"font-size: 18px; font-weight: 600; padding: 4px 16px;"
|
||||
)
|
||||
top_layout = QHBoxLayout(self._top_bar)
|
||||
top_layout.setContentsMargins(16, 0, 16, 0)
|
||||
self._fan_label = QLabel("🌡 --°C -- 𖣘 --% -- r/min")
|
||||
self._fan_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
top_layout.addWidget(self._fan_label)
|
||||
top_layout.addStretch()
|
||||
|
||||
# 时钟标签(有网络时显示)
|
||||
self._clock_label = QLabel("--:--:--")
|
||||
self._clock_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._clock_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
||||
self._clock_label.hide()
|
||||
top_layout.addWidget(self._clock_label)
|
||||
|
||||
# 底部按钮区
|
||||
bottom_layout = QHBoxLayout()
|
||||
bottom_layout.setContentsMargins(10, 10, 10, 10)
|
||||
@@ -70,7 +151,8 @@ class MainWindow(QWidget):
|
||||
bottom_layout.addWidget(self.btn_control)
|
||||
bottom_layout.addWidget(self.btn_settings)
|
||||
|
||||
# 将主显示区和底部按钮区加入整体布局
|
||||
# 将主显示区、风扇横条、底部按钮加入整体布局
|
||||
main_layout.addWidget(self._top_bar)
|
||||
main_layout.addWidget(self.stacked_widget)
|
||||
main_layout.addWidget(bottom_widget)
|
||||
|
||||
|
||||
@@ -1,26 +1,535 @@
|
||||
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
||||
QFrame, QGridLayout, QSizePolicy, QLineEdit,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, QPointF, QRectF
|
||||
from PyQt6.QtGui import QFont, QPainter, QColor, QBrush, QPen, QDoubleValidator
|
||||
from utils.config_parse import ConfigParse
|
||||
|
||||
MOVE_STEP = 10 # 每次点击移动 mm (保留备用)
|
||||
|
||||
MOVE_DETECT_MS = 100
|
||||
|
||||
|
||||
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(120) # ~8次/秒
|
||||
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)
|
||||
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)
|
||||
|
||||
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()
|
||||
print(f"z-max-s:{self.move_speed_z}")
|
||||
|
||||
# ── 状态管理 ──────────────────────────────────────────
|
||||
|
||||
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 = 5.0
|
||||
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}"
|
||||
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
|
||||
print(f"d:{direction} s:{speed}")
|
||||
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}"
|
||||
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.pos_y = self.pos_z = 0.0
|
||||
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()
|
||||
|
||||
# ── UI 构建 ──────────────────────────────────────────────
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
label = QLabel("控制页面")
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(label)
|
||||
|
||||
# 添加一些控制按钮
|
||||
button_layout = QHBoxLayout()
|
||||
start_button = QPushButton("开始打印")
|
||||
stop_button = QPushButton("停止打印")
|
||||
button_layout.addWidget(start_button)
|
||||
button_layout.addWidget(stop_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
|
||||
main_layout = QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(14, 14, 14, 14)
|
||||
main_layout.setSpacing(14)
|
||||
|
||||
self.setLayout(layout)
|
||||
# =========================== 左栏 ===========================
|
||||
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)
|
||||
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()
|
||||
@@ -14,10 +14,16 @@ from PyQt6.QtWidgets import (
|
||||
QFrame,
|
||||
QScrollArea,
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, pyqtSignal
|
||||
from PyQt6.QtGui import QMouseEvent
|
||||
from PyQt6.QtCore import Qt, QEvent, QObject, QThread, QTimer, QSize, pyqtSignal
|
||||
from PyQt6.QtGui import QMouseEvent, QPixmap, QImage, QFont
|
||||
import codecs
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
import qrcode
|
||||
from utils.wifi_manager import WifiManager
|
||||
from utils.floating_keyboard import FloatingKeyboard
|
||||
|
||||
@@ -139,42 +145,85 @@ class SettingPage(QWidget):
|
||||
self._keyboard = FloatingKeyboard()
|
||||
self._keyboard_attached = False
|
||||
|
||||
# 监听全局焦点变化,可靠处理键盘消失
|
||||
self._focus_changed_connected = False
|
||||
|
||||
self.init_ui()
|
||||
self._start_status_timer()
|
||||
|
||||
def _on_app_focus_changed(self, old_widget, new_widget):
|
||||
"""全局焦点变化时检查是否需要关闭键盘"""
|
||||
if not self._keyboard_attached:
|
||||
return
|
||||
# 如果新焦点在输入控件上,什么都不做
|
||||
if isinstance(new_widget, (QLineEdit, QComboBox, QPushButton)):
|
||||
return
|
||||
# 如果新焦点是键盘自身的子控件,也不关闭
|
||||
if new_widget and self._keyboard.isAncestorOf(new_widget):
|
||||
return
|
||||
# 如果新焦点是键盘本身,不关闭
|
||||
if new_widget is self._keyboard:
|
||||
return
|
||||
# 如果是热点开关按钮的 ON/OFF 文本区域,不关闭
|
||||
if isinstance(new_widget, QWidget) and new_widget.parent() is self.hotspot_toggle:
|
||||
return
|
||||
# 其他情况 → 关闭键盘
|
||||
self._dismiss_keyboard()
|
||||
|
||||
def _connect_focus_signal(self):
|
||||
"""连接全局焦点信号"""
|
||||
if not self._focus_changed_connected:
|
||||
QApplication.instance().focusChanged.connect(self._on_app_focus_changed)
|
||||
self._focus_changed_connected = True
|
||||
|
||||
def _disconnect_focus_signal(self):
|
||||
"""断开全局焦点信号"""
|
||||
if self._focus_changed_connected:
|
||||
QApplication.instance().focusChanged.disconnect(self._on_app_focus_changed)
|
||||
self._focus_changed_connected = False
|
||||
|
||||
def _attach_keyboard(self, widget):
|
||||
"""将悬浮键盘绑定到指定输入框并显示"""
|
||||
self._keyboard.attach(widget)
|
||||
self._keyboard.show_below(widget)
|
||||
self._keyboard_attached = True
|
||||
self._connect_focus_signal()
|
||||
|
||||
def _dismiss_keyboard(self):
|
||||
"""关闭悬浮键盘"""
|
||||
self._keyboard.hide()
|
||||
self._keyboard.detach()
|
||||
self._keyboard_attached = False
|
||||
self._disconnect_focus_signal()
|
||||
|
||||
def _on_input_focus_in(self, widget):
|
||||
"""输入框获得焦点时的处理"""
|
||||
self._attach_keyboard(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.ssid_input, self.identity_input, self.password_input):
|
||||
self._attach_keyboard(obj)
|
||||
if obj in (self.ssid_input, self.identity_input, self.password_input,
|
||||
self.hotspot_ssid, self.hotspot_password):
|
||||
self._on_input_focus_in(obj)
|
||||
# 如果不是输入框获得焦点且键盘正显示,则关闭键盘
|
||||
elif self._keyboard_attached and not isinstance(obj, (QLineEdit, QComboBox, QPushButton)):
|
||||
self._dismiss_keyboard()
|
||||
elif event.type() == QEvent.Type.FocusOut:
|
||||
# 延迟检查:如果焦点真的离开了所有输入框,就关闭键盘
|
||||
if obj in (self.ssid_input, self.identity_input, self.password_input):
|
||||
if obj in (self.ssid_input, self.identity_input, self.password_input,
|
||||
self.hotspot_ssid, self.hotspot_password):
|
||||
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.ssid_input, self.identity_input, self.password_input, self.auth_combo):
|
||||
if w not in (self.ssid_input, self.identity_input, self.password_input,
|
||||
self.auth_combo, self.hotspot_ssid, self.hotspot_password):
|
||||
self._dismiss_keyboard()
|
||||
|
||||
@staticmethod
|
||||
@@ -262,25 +311,23 @@ class SettingPage(QWidget):
|
||||
"""
|
||||
)
|
||||
self.item_list.addItem("WiFi设置")
|
||||
self.item_list.addItem("热点设置")
|
||||
self.item_list.addItem("电源设置")
|
||||
self.item_list.addItem("其它设置(待定)")
|
||||
self.item_list.currentRowChanged.connect(self.display_setting)
|
||||
left_layout.addWidget(self.item_list)
|
||||
layout.addWidget(left_panel, 1)
|
||||
|
||||
# 右侧设置参数区域(放入可滚动区域,便于触屏上下滑动)
|
||||
# 右侧设置参数区域
|
||||
self.settings_stack = QStackedWidget()
|
||||
self.settings_stack.setStyleSheet("background-color: #444444; border-radius: 10px;")
|
||||
# 每个 init 方法返回一个包裹了 DragScrollArea 的页面
|
||||
self.init_wifi_settings()
|
||||
self.init_hotspot_settings()
|
||||
self.init_power_settings()
|
||||
self.init_todo_settings()
|
||||
|
||||
right_scroll = DragScrollArea()
|
||||
right_scroll.setWidgetResizable(True)
|
||||
right_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
# 将堆叠面板放入 scroll area
|
||||
right_scroll.setWidget(self.settings_stack)
|
||||
right_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
right_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
layout.addWidget(right_scroll, 3)
|
||||
layout.addWidget(self.settings_stack, 3)
|
||||
|
||||
self.item_list.setCurrentRow(0)
|
||||
|
||||
@@ -527,11 +574,316 @@ class SettingPage(QWidget):
|
||||
wifi_layout.addWidget(connect_button)
|
||||
|
||||
wifi_layout.addStretch()
|
||||
self.settings_stack.addWidget(wifi_widget)
|
||||
self.settings_stack.addWidget(self._wrap_scroll(wifi_widget))
|
||||
self.update_auth_fields()
|
||||
self.refresh_saved_wifi()
|
||||
self.refresh_current_status()
|
||||
|
||||
def init_hotspot_settings(self):
|
||||
hotspot_widget = QWidget()
|
||||
hotspot_layout = QVBoxLayout(hotspot_widget)
|
||||
hotspot_layout.setContentsMargins(20, 20, 20, 20)
|
||||
hotspot_layout.setSpacing(16)
|
||||
|
||||
title = QLabel("热点设置")
|
||||
title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;")
|
||||
hotspot_layout.addWidget(title)
|
||||
|
||||
# ── 左右分栏 ──
|
||||
split_row = QHBoxLayout()
|
||||
split_row.setSpacing(20)
|
||||
|
||||
# ====== 左栏:信息设置 ======
|
||||
left_col = QVBoxLayout()
|
||||
left_col.setSpacing(12)
|
||||
|
||||
desc = QLabel("将设备设置为WiFi热点,其他设备可扫描连接")
|
||||
desc.setStyleSheet("color: #aaaaaa; font-size: 16px;")
|
||||
desc.setWordWrap(True)
|
||||
left_col.addWidget(desc)
|
||||
|
||||
# SSID 输入
|
||||
ssid_label = QLabel("热点名称 (SSID)")
|
||||
ssid_label.setStyleSheet("color: #d0d0d0; font-size: 18px; font-weight: 500;")
|
||||
left_col.addWidget(ssid_label)
|
||||
|
||||
self.hotspot_ssid = QLineEdit("PrinterScreen-AP")
|
||||
self.hotspot_ssid.setPlaceholderText("输入热点名称")
|
||||
self.hotspot_ssid.setStyleSheet(
|
||||
"min-height: 44px; font-size: 20px; color: #f2f2f2; "
|
||||
"background-color: #3a3a3a; border: 2px solid #707070; "
|
||||
"border-radius: 8px; padding: 4px 12px;"
|
||||
)
|
||||
self.hotspot_ssid.installEventFilter(self)
|
||||
left_col.addWidget(self.hotspot_ssid)
|
||||
|
||||
# 密码输入
|
||||
pw_label = QLabel("热点密码")
|
||||
pw_label.setStyleSheet("color: #d0d0d0; font-size: 18px; font-weight: 500;")
|
||||
left_col.addWidget(pw_label)
|
||||
|
||||
self.hotspot_password = QLineEdit("12345678")
|
||||
self.hotspot_password.setPlaceholderText("至少8位")
|
||||
self.hotspot_password.setStyleSheet(
|
||||
"min-height: 44px; font-size: 20px; color: #f2f2f2; "
|
||||
"background-color: #3a3a3a; border: 2px solid #707070; "
|
||||
"border-radius: 8px; padding: 4px 12px;"
|
||||
)
|
||||
self.hotspot_password.installEventFilter(self)
|
||||
left_col.addWidget(self.hotspot_password)
|
||||
|
||||
# 热点开关行
|
||||
toggle_row = QHBoxLayout()
|
||||
toggle_row.setSpacing(16)
|
||||
|
||||
toggle_label = QLabel("热点开关")
|
||||
toggle_label.setStyleSheet("color: #d0d0d0; font-size: 20px; font-weight: 600;")
|
||||
toggle_row.addWidget(toggle_label)
|
||||
|
||||
self.hotspot_toggle = QPushButton()
|
||||
self.hotspot_toggle.setCheckable(True)
|
||||
self.hotspot_toggle.setFixedSize(80, 40)
|
||||
self.hotspot_toggle.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self._apply_toggle_style(False)
|
||||
self.hotspot_toggle.toggled.connect(self._on_hotspot_toggled)
|
||||
toggle_row.addWidget(self.hotspot_toggle)
|
||||
toggle_row.addStretch()
|
||||
left_col.addLayout(toggle_row)
|
||||
|
||||
# 热点状态标签
|
||||
self.hotspot_status = QLabel("热点状态:关闭")
|
||||
self.hotspot_status.setStyleSheet("color: #cccccc; font-size: 17px;")
|
||||
left_col.addWidget(self.hotspot_status)
|
||||
|
||||
left_col.addStretch()
|
||||
split_row.addLayout(left_col, 1)
|
||||
|
||||
# ====== 右栏:二维码 ======
|
||||
right_col = QVBoxLayout()
|
||||
right_col.setSpacing(8)
|
||||
right_col.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
qr_title = QLabel("扫码连接")
|
||||
qr_title.setStyleSheet("color: #dcdcdc; font-size: 20px; font-weight: 600;")
|
||||
qr_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
right_col.addWidget(qr_title)
|
||||
|
||||
self.qr_label = QLabel()
|
||||
self.qr_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.qr_label.setFixedSize(240, 240)
|
||||
self.qr_label.setStyleSheet(
|
||||
"background-color: #ffffff; border: 2px solid #646464; border-radius: 8px;"
|
||||
)
|
||||
right_col.addWidget(self.qr_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.qr_hint = QLabel("开启热点后自动生成二维码")
|
||||
self.qr_hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.qr_hint.setStyleSheet("color: #909090; font-size: 15px;")
|
||||
right_col.addWidget(self.qr_hint)
|
||||
|
||||
right_col.addStretch()
|
||||
split_row.addLayout(right_col, 1)
|
||||
|
||||
hotspot_layout.addLayout(split_row)
|
||||
self.settings_stack.addWidget(self._wrap_scroll(hotspot_widget))
|
||||
|
||||
def _apply_toggle_style(self, checked):
|
||||
if checked:
|
||||
self.hotspot_toggle.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #66BB6A;
|
||||
border-radius: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #66BB6A;
|
||||
}
|
||||
""")
|
||||
self.hotspot_toggle.setText("ON")
|
||||
else:
|
||||
self.hotspot_toggle.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #555555;
|
||||
border: 2px solid #707070;
|
||||
border-radius: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #aaaaaa;
|
||||
}
|
||||
QPushButton:checked {
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #66BB6A;
|
||||
}
|
||||
""")
|
||||
self.hotspot_toggle.setText("OFF")
|
||||
|
||||
def _on_hotspot_toggled(self, checked):
|
||||
if checked:
|
||||
ssid = self.hotspot_ssid.text().strip()
|
||||
password = self.hotspot_password.text().strip()
|
||||
if not ssid:
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请输入热点名称")
|
||||
self.hotspot_toggle.blockSignals(True)
|
||||
self.hotspot_toggle.setChecked(False)
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
return
|
||||
if len(password) < 8:
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "密码至少需要8位")
|
||||
self.hotspot_toggle.blockSignals(True)
|
||||
self.hotspot_toggle.setChecked(False)
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
return
|
||||
try:
|
||||
ret = self.wifi_manager.open_hotspot(ssid, password)
|
||||
if ret:
|
||||
self._apply_toggle_style(True)
|
||||
self.hotspot_status.setText(f"热点状态:已开启 ({ssid})")
|
||||
self.hotspot_ssid.setEnabled(False)
|
||||
self.hotspot_password.setEnabled(False)
|
||||
self._generate_qr_code(ssid, password)
|
||||
else:
|
||||
raise RuntimeError("wpa_cli 返回失败")
|
||||
except Exception as e:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"开启热点失败: {str(e)}")
|
||||
self.hotspot_toggle.blockSignals(True)
|
||||
self.hotspot_toggle.setChecked(False)
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
else:
|
||||
try:
|
||||
self.wifi_manager.close_hotspot()
|
||||
except Exception:
|
||||
pass
|
||||
self._apply_toggle_style(False)
|
||||
self.hotspot_status.setText("热点状态:关闭")
|
||||
self.hotspot_ssid.setEnabled(True)
|
||||
self.hotspot_password.setEnabled(True)
|
||||
self.qr_label.clear()
|
||||
self.qr_hint.setText("开启热点后自动生成二维码")
|
||||
|
||||
def _generate_qr_code(self, ssid, password):
|
||||
"""生成 WiFi 二维码并显示"""
|
||||
wifi_str = f"WIFI:S:{ssid};T:WPA;P:{password};;"
|
||||
try:
|
||||
qr = qrcode.QRCode(box_size=6, border=2)
|
||||
qr.add_data(wifi_str)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
# 转换为 QPixmap
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(buffer.getvalue())
|
||||
scaled = pixmap.scaled(220, 220, Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
self.qr_label.setPixmap(scaled)
|
||||
self.qr_hint.setText(f"扫码连接 WiFi: {ssid}")
|
||||
except Exception as e:
|
||||
self.qr_hint.setText(f"二维码生成失败: {str(e)}")
|
||||
|
||||
def init_power_settings(self):
|
||||
power_widget = QWidget()
|
||||
power_layout = QVBoxLayout(power_widget)
|
||||
power_layout.setContentsMargins(20, 20, 20, 20)
|
||||
power_layout.setSpacing(16)
|
||||
|
||||
title = QLabel("电源设置")
|
||||
title.setStyleSheet("color: #f2f2f2; font-size: 26px; font-weight: 700;")
|
||||
power_layout.addWidget(title)
|
||||
|
||||
desc = QLabel("重启或关闭设备")
|
||||
desc.setStyleSheet("color: #aaaaaa; font-size: 16px;")
|
||||
power_layout.addWidget(desc)
|
||||
|
||||
power_layout.addStretch()
|
||||
|
||||
# 重启按钮
|
||||
reboot_btn = QPushButton("↻ 重启系统")
|
||||
reboot_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
min-height: 70px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
background-color: #2f6f91;
|
||||
border: 2px solid #4a9fc8;
|
||||
border-radius: 14px;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3a85b3;
|
||||
border-color: #6fb8dd;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1e4d66;
|
||||
border-color: #2f6f91;
|
||||
}
|
||||
""")
|
||||
reboot_btn.clicked.connect(self._confirm_reboot)
|
||||
power_layout.addWidget(reboot_btn)
|
||||
|
||||
power_layout.addSpacing(20)
|
||||
|
||||
# 关机按钮
|
||||
shutdown_btn = QPushButton("⏻ 关机")
|
||||
shutdown_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
min-height: 70px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
background-color: #c0392b;
|
||||
border: 2px solid #e74c3c;
|
||||
border-radius: 14px;
|
||||
padding: 10px 24px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e74c3c;
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #922b21;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
""")
|
||||
shutdown_btn.clicked.connect(self._confirm_shutdown)
|
||||
power_layout.addWidget(shutdown_btn)
|
||||
|
||||
power_layout.addStretch()
|
||||
self.settings_stack.addWidget(self._wrap_scroll(power_widget))
|
||||
|
||||
def _confirm_reboot(self):
|
||||
reply = QMessageBox.question(
|
||||
self, "确认重启",
|
||||
"确定要重启系统吗?\n所有未保存的数据将丢失。",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self._styled_message(
|
||||
QMessageBox.Icon.Information, self, "重启",
|
||||
"系统正在重启..."
|
||||
)
|
||||
QTimer.singleShot(500, lambda: os.system("sudo reboot"))
|
||||
|
||||
def _confirm_shutdown(self):
|
||||
reply = QMessageBox.question(
|
||||
self, "确认关机",
|
||||
"确定要关闭系统吗?\n关闭后需要手动重新开机。",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self._styled_message(
|
||||
QMessageBox.Icon.Information, self, "关机",
|
||||
"系统正在关机..."
|
||||
)
|
||||
QTimer.singleShot(500, lambda: os.system("sudo poweroff"))
|
||||
|
||||
def init_todo_settings(self):
|
||||
todo_widget = QWidget()
|
||||
todo_widget.setStyleSheet(
|
||||
@@ -570,7 +922,7 @@ class SettingPage(QWidget):
|
||||
|
||||
todo_layout.addStretch()
|
||||
|
||||
self.settings_stack.addWidget(todo_widget)
|
||||
self.settings_stack.addWidget(self._wrap_scroll(todo_widget))
|
||||
|
||||
def refresh_saved_wifi(self):
|
||||
try:
|
||||
@@ -734,7 +1086,6 @@ class SettingPage(QWidget):
|
||||
def _detect_auth_mode(network):
|
||||
"""根据 wpa_supplicant 返回的标准 flags 判断认证方式"""
|
||||
flags = network.get("flags", "").strip()
|
||||
|
||||
# 无加密标记 → 开放网络
|
||||
if not flags or flags in ("", "[ESS]", "[NONE]", "NONE"):
|
||||
return "open"
|
||||
@@ -892,4 +1243,15 @@ class SettingPage(QWidget):
|
||||
def display_setting(self, index):
|
||||
if index < 0:
|
||||
return
|
||||
self.settings_stack.setCurrentIndex(index)
|
||||
self.settings_stack.setCurrentIndex(index)
|
||||
|
||||
@staticmethod
|
||||
def _wrap_scroll(widget):
|
||||
"""将页面 widget 放入独立的 DragScrollArea 中"""
|
||||
scroll = DragScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setWidget(widget)
|
||||
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
return scroll
|
||||
@@ -2,9 +2,12 @@ import os
|
||||
import json
|
||||
import re
|
||||
from PyQt6.QtWidgets import (QWidget, QHBoxLayout, QVBoxLayout,
|
||||
QPushButton, QLabel, QFrame, QGraphicsView, QGraphicsScene, QGraphicsPathItem)
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QUrl, QObject, pyqtProperty, QRectF
|
||||
from PyQt6.QtGui import QColor, QPen, QPainter, QPainterPath
|
||||
QPushButton, QLabel, QFrame, QGraphicsView,
|
||||
QGraphicsScene, QGraphicsPathItem, QProgressBar)
|
||||
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QUrl, QObject, pyqtProperty, QRectF, QSize
|
||||
from PyQt6.QtGui import QColor, QPen, QPainter, QPainterPath, QFont, QLinearGradient, QBrush
|
||||
from utils.config_parse import ConfigParse
|
||||
|
||||
|
||||
def get_gcode_dir():
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
|
||||
@@ -17,167 +20,390 @@ def get_gcode_dir():
|
||||
|
||||
GCODE_DIR = get_gcode_dir()
|
||||
|
||||
class GCode2DPreviewWidget(QGraphicsView):
|
||||
def __init__(self, parent=None):
|
||||
# ── 状态主题色 ──────────────────────────────────────────
|
||||
STATUS_COLORS = {
|
||||
"Operational": "#4CAF50",
|
||||
"Printing": "#2196F3",
|
||||
"Paused": "#FF9800",
|
||||
"Error": "#F44336",
|
||||
"Offline": "#9E9E9E",
|
||||
"Cancelling": "#FF5722",
|
||||
"Finishing": "#8BC34A",
|
||||
}
|
||||
STATUS_LABELS = {
|
||||
"Operational": "就绪",
|
||||
"Printing": "打印中",
|
||||
"Paused": "已暂停",
|
||||
"Error": "错误",
|
||||
"Offline": "离线",
|
||||
"Cancelling": "取消中",
|
||||
"Finishing": "整理中",
|
||||
}
|
||||
|
||||
|
||||
class CardFrame(QFrame):
|
||||
"""统一的信息卡片样式"""
|
||||
|
||||
def __init__(self, title="", parent=None):
|
||||
super().__init__(parent)
|
||||
self.scene = QGraphicsScene(self)
|
||||
self.setScene(self.scene)
|
||||
self.setStyleSheet("background-color: #111111; border-radius: 5px; border: 1px solid #666;")
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# 翻转Y轴让(0,0)位于左下角,适配物理坐标系
|
||||
self.scale(1, -1)
|
||||
self.setStyleSheet("""
|
||||
CardFrame {
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #555555;
|
||||
border-radius: 8px;
|
||||
}
|
||||
""")
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(12, 10, 12, 10)
|
||||
layout.setSpacing(4)
|
||||
|
||||
def draw_paths(self, lines_data):
|
||||
self.scene.clear()
|
||||
bounding_rect = QRectF()
|
||||
|
||||
for color_str, data in lines_data.items():
|
||||
points = data.get("points", [])
|
||||
line_width = data.get("line_width", 2)
|
||||
|
||||
if not points:
|
||||
continue
|
||||
|
||||
path = QPainterPath()
|
||||
|
||||
# 判断是一组连续点的坐标(单线条)[[x1,y1], [x2,y2]]
|
||||
# 还是包含多根独立线条 [[[x1,y1], [x2,y2]], [[x3,y3], [x4,y4]]]
|
||||
if isinstance(points[0][0], (int, float)):
|
||||
path.moveTo(float(points[0][0]), float(points[0][1]))
|
||||
for pt in points[1:]:
|
||||
path.lineTo(float(pt[0]), float(pt[1]))
|
||||
else:
|
||||
for line_pts in points:
|
||||
if not line_pts:
|
||||
continue
|
||||
path.moveTo(float(line_pts[0][0]), float(line_pts[0][1]))
|
||||
for pt in line_pts[1:]:
|
||||
path.lineTo(float(pt[0]), float(pt[1]))
|
||||
|
||||
path_item = QGraphicsPathItem(path)
|
||||
|
||||
# 安全获取颜色
|
||||
pen_color = QColor(color_str) if QColor.isValidColor(color_str) else QColor("white")
|
||||
|
||||
pen = QPen(pen_color)
|
||||
pen.setWidth(int(line_width))
|
||||
# 保持 Cosmetic,以确保视图缩放时线条粗细在屏幕上看起来一致
|
||||
pen.setCosmetic(True)
|
||||
path_item.setPen(pen)
|
||||
|
||||
self.scene.addItem(path_item)
|
||||
bounding_rect = bounding_rect.united(path.boundingRect())
|
||||
|
||||
# 设置场景范围以便居中缩放
|
||||
if bounding_rect.width() < 1 or bounding_rect.height() < 1:
|
||||
bounding_rect = QRectF(0, 0, 220, 220)
|
||||
|
||||
bounding_rect.adjust(-10, -10, 10, 10)
|
||||
self.scene.setSceneRect(bounding_rect)
|
||||
self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
if title:
|
||||
title_lbl = QLabel(title)
|
||||
title_lbl.setStyleSheet("color: #aaaaaa; font-size: 13px; font-weight: 500; border: none;")
|
||||
layout.addWidget(title_lbl)
|
||||
|
||||
self.content_layout = layout
|
||||
|
||||
def add_row(self, label, value_widget):
|
||||
row = QHBoxLayout()
|
||||
row.setSpacing(8)
|
||||
lbl = QLabel(label)
|
||||
lbl.setStyleSheet("color: #cccccc; font-size: 15px; border: none;")
|
||||
lbl.setFixedWidth(70)
|
||||
row.addWidget(lbl)
|
||||
row.addWidget(value_widget, 1)
|
||||
self.content_layout.addLayout(row)
|
||||
|
||||
|
||||
class TempGauge(QWidget):
|
||||
"""温度计指示器"""
|
||||
|
||||
def __init__(self, label="Tool", parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(100, 80)
|
||||
self._label = label
|
||||
self._actual = 0.0
|
||||
self._target = 0.0
|
||||
|
||||
def set_value(self, actual, target):
|
||||
self._actual = actual
|
||||
self._target = target
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
w, h = self.width(), self.height()
|
||||
|
||||
# 背景条
|
||||
bar_x, bar_w = 16, 20
|
||||
bar_y, bar_h = 10, 56
|
||||
p.setPen(QPen(QColor("#555555"), 1))
|
||||
p.setBrush(QBrush(QColor("#2a2a2a")))
|
||||
p.drawRoundedRect(bar_x, bar_y, bar_w, bar_h, 4, 4)
|
||||
|
||||
# 填充柱(按温度比例,最高 300°C)
|
||||
ratio = min(max(self._actual / 300, 0), 1)
|
||||
fill_h = int((bar_h - 4) * ratio)
|
||||
if fill_h > 0:
|
||||
grad = QLinearGradient(0, bar_y + bar_h, 0, bar_y)
|
||||
grad.setColorAt(0, QColor("#f57c00"))
|
||||
grad.setColorAt(1, QColor("#e53935") if self._actual > 200 else QColor("#ffb74d"))
|
||||
p.setBrush(QBrush(grad))
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.drawRoundedRect(bar_x + 2, bar_y + bar_h - 2 - fill_h, bar_w - 4, fill_h, 3, 3)
|
||||
|
||||
# 目标值标记线
|
||||
if self._target > 0:
|
||||
tgt_y = bar_y + bar_h - int((bar_h - 4) * min(self._target / 300, 1))
|
||||
p.setPen(QPen(QColor("#ffffff"), 2))
|
||||
p.drawLine(bar_x - 2, tgt_y, bar_x + bar_w + 2, tgt_y)
|
||||
|
||||
# 文字
|
||||
font = QFont("sans-serif", 11, QFont.Weight.Bold)
|
||||
p.setFont(font)
|
||||
p.setPen(QPen(QColor("#e0e0e0")))
|
||||
p.drawText(44, 16, w - 44, 24, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
||||
f"{self._actual:.0f}°")
|
||||
if self._target > 0:
|
||||
font2 = QFont("sans-serif", 10)
|
||||
p.setFont(font2)
|
||||
p.setPen(QPen(QColor("#888888")))
|
||||
p.drawText(44, 34, w - 44, 20, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
|
||||
f"→ {self._target:.0f}°")
|
||||
|
||||
font3 = QFont("sans-serif", 10, QFont.Weight.Bold)
|
||||
p.setFont(font3)
|
||||
p.setPen(QPen(QColor("#aaaaaa")))
|
||||
p.drawText(0, h - 20, w, 20, Qt.AlignmentFlag.AlignCenter, self._label)
|
||||
|
||||
|
||||
# ── GCode 2D 预览(暂注释,待开发)─────────────────────
|
||||
# class GCode2DPreviewWidget(QGraphicsView):
|
||||
# def __init__(self, parent=None):
|
||||
# super().__init__(parent)
|
||||
# self.scene = QGraphicsScene(self)
|
||||
# self.setScene(self.scene)
|
||||
# self.setStyleSheet("background-color: #111111; border-radius: 5px; border: 1px solid #666;")
|
||||
# self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
# self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# self.scale(1, -1)
|
||||
#
|
||||
# def draw_paths(self, lines_data):
|
||||
# self.scene.clear()
|
||||
# bounding_rect = QRectF()
|
||||
# for color_str, data in lines_data.items():
|
||||
# points = data.get("points", [])
|
||||
# line_width = data.get("line_width", 2)
|
||||
# if not points:
|
||||
# continue
|
||||
# path = QPainterPath()
|
||||
# if isinstance(points[0][0], (int, float)):
|
||||
# path.moveTo(float(points[0][0]), float(points[0][1]))
|
||||
# for pt in points[1:]:
|
||||
# path.lineTo(float(pt[0]), float(pt[1]))
|
||||
# else:
|
||||
# for line_pts in points:
|
||||
# if not line_pts:
|
||||
# continue
|
||||
# path.moveTo(float(line_pts[0][0]), float(line_pts[0][1]))
|
||||
# for pt in line_pts[1:]:
|
||||
# path.lineTo(float(pt[0]), float(pt[1]))
|
||||
# path_item = QGraphicsPathItem(path)
|
||||
# pen_color = QColor(color_str) if QColor.isValidColor(color_str) else QColor("white")
|
||||
# pen = QPen(pen_color)
|
||||
# pen.setWidth(int(line_width))
|
||||
# pen.setCosmetic(True)
|
||||
# path_item.setPen(pen)
|
||||
# self.scene.addItem(path_item)
|
||||
# bounding_rect = bounding_rect.united(path.boundingRect())
|
||||
# if bounding_rect.width() < 1 or bounding_rect.height() < 1:
|
||||
# bounding_rect = QRectF(0, 0, 220, 220)
|
||||
# bounding_rect.adjust(-10, -10, 10, 10)
|
||||
# self.scene.setSceneRect(bounding_rect)
|
||||
# self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
|
||||
# ── 状态页面 ────────────────────────────────────────────
|
||||
class StatusPage(QWidget):
|
||||
def __init__(self, api_client, parent=None):
|
||||
super().__init__(parent)
|
||||
self.api_client = api_client
|
||||
|
||||
self.file_name = "None"
|
||||
self.progress = 0
|
||||
self.progress = 0.0
|
||||
self.display_name = "None"
|
||||
self.state = "Unknown"
|
||||
self.print_time = 0
|
||||
self.print_time_left = 0
|
||||
self.tool_temp_actual = 0.0
|
||||
self.tool_temp_target = 0.0
|
||||
self.bed_temp_actual = 0.0
|
||||
self.bed_temp_target = 0.0
|
||||
|
||||
self.config_parser = ConfigParse()
|
||||
self.config_parser.config_changed.connect(self._on_config_changed)
|
||||
self.gcode_dir = self.config_parser.gcode_dir
|
||||
|
||||
self.init_ui()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update_status)
|
||||
self.timer.start(1000)
|
||||
self.update_status()
|
||||
|
||||
def _on_config_changed(self, config_instance):
|
||||
self.gcode_dir = self.config_parser.gcode_dir
|
||||
|
||||
def fresh_status_valve(self):
|
||||
data = self.api_client.get_status()
|
||||
# print("Status Data:", data)
|
||||
if data:
|
||||
status = data.get("status", {})
|
||||
job = data.get("job", {})
|
||||
self.file_name = job.get("job", {}).get("file", {}).get("name", "None")
|
||||
self.progress = job.get("progress", {}).get("completion", 0)
|
||||
self.progress = job.get("progress", {}).get("completion", 0) or 0
|
||||
self.display_name = job.get("job", {}).get("file", {}).get("display_name", "None")
|
||||
self.state = status.get("state", {}).get("text", "Offline")
|
||||
self.print_time = job.get("progress", {}).get("printTime", 0)
|
||||
self.print_time_left = job.get("progress", {}).get("printTimeLeft", 0)
|
||||
self.print_time = job.get("progress", {}).get("printTime", 0) or 0
|
||||
self.print_time_left = job.get("progress", {}).get("printTimeLeft", 0) or 0
|
||||
temp = status.get("temperature", {})
|
||||
self.tool_temp_actual = (temp.get("tool0", {}) or {}).get("actual", 0) or 0
|
||||
self.tool_temp_target = (temp.get("tool0", {}) or {}).get("target", 0) or 0
|
||||
self.bed_temp_actual = (temp.get("bed", {}) or {}).get("actual", 0) or 0
|
||||
self.bed_temp_target = (temp.get("bed", {}) or {}).get("target", 0) or 0
|
||||
|
||||
# print(f"Updated Status: state={self.state}, file_name={self.file_name}, progress={self.progress:.1f}%, display_name={self.display_name}")
|
||||
def format_time(self, seconds):
|
||||
if seconds is None:
|
||||
@staticmethod
|
||||
def format_time(seconds):
|
||||
if seconds is None or seconds == 0:
|
||||
return "N/A"
|
||||
m, s = divmod(seconds, 60)
|
||||
m, s = divmod(int(seconds), 60)
|
||||
h, m = divmod(m, 60)
|
||||
if h > 0:
|
||||
return f"{int(h)}h {int(m)}m {int(s)}s"
|
||||
return f"{h}h {m:02d}m {s:02d}s"
|
||||
elif m > 0:
|
||||
return f"{int(m)}m {int(s)}s"
|
||||
return f"{m}m {s:02d}s"
|
||||
else:
|
||||
return f"{int(s)}s"
|
||||
return f"{s}s"
|
||||
|
||||
def _make_card(self, title=""):
|
||||
card = CardFrame(title)
|
||||
return card
|
||||
|
||||
def _truncate(self, text, max_len=22):
|
||||
return text if len(text) <= max_len else text[:max_len - 2] + "…"
|
||||
|
||||
def init_ui(self):
|
||||
self.fresh_status_valve()
|
||||
main_layout = QHBoxLayout(self)
|
||||
|
||||
self.left_frame = QFrame()
|
||||
self.left_frame.setStyleSheet("background-color: #444444; border-radius: 10px; color: white;")
|
||||
left_layout = QVBoxLayout(self.left_frame)
|
||||
|
||||
self.lbl_status = QLabel(f"Status: {self.state}")
|
||||
left_layout.addWidget(self.lbl_status)
|
||||
self.lbl_job = QLabel(f"File: {self.display_name}")
|
||||
left_layout.addWidget(self.lbl_job)
|
||||
self.lbl_progress = QLabel(f"Progress: {self.progress if self.progress else 0:.1f}%")
|
||||
left_layout.addWidget(self.lbl_progress)
|
||||
self.lbl_print_time = QLabel(f"Print Time: {self.format_time(self.print_time)}")
|
||||
left_layout.addWidget(self.lbl_print_time)
|
||||
self.lbl_print_time_left = QLabel(f"Print Time Left: {self.format_time(self.print_time_left)}")
|
||||
left_layout.addWidget(self.lbl_print_time_left)
|
||||
left_layout.addStretch()
|
||||
|
||||
#TODO: 3D Gcode View in right frame, use QML and QtQuick3D to render the Gcode vertices, pass the progress to QML to show the current layer
|
||||
# Due to the complexity of parsing Gcode and rendering it in 3D, this part will be implemented in a separate thread to avoid blocking the UI, and the vertices will be passed to QML for rendering. The progress will also be passed to QML to show the current layer being printed.
|
||||
# Load QtQuick3D View
|
||||
# self.right_frame = QFrame()
|
||||
# right_layout = QVBoxLayout(self.right_frame)
|
||||
# self.view = QQuickView()
|
||||
# self.view.setResizeMode(QQuickView.ResizeMode.SizeRootObjectToView)
|
||||
# qml_file = os.path.join(os.path.dirname(__file__), "gcode_view2.qml")
|
||||
|
||||
# self.view.setSource(QUrl.fromLocalFile(qml_file))
|
||||
# gcode_data = self.load_gcode_vertices(os.path.join(GCODE_DIR, self.file_name))
|
||||
# self.view.rootContext().setContextProperty("gcodeData", gcode_data)
|
||||
# container = QWidget.createWindowContainer(self.view, self)
|
||||
# right_layout.addWidget(container)
|
||||
main_layout.setContentsMargins(8, 8, 8, 8)
|
||||
main_layout.setSpacing(8)
|
||||
|
||||
self.right_frame = QFrame()
|
||||
self.right_frame.setStyleSheet("background-color: #444444; border-radius: 10px;")
|
||||
right_layout = QVBoxLayout(self.right_frame)
|
||||
|
||||
self.gcode_view = GCode2DPreviewWidget()
|
||||
right_layout.addWidget(self.gcode_view)
|
||||
|
||||
main_layout.addWidget(self.left_frame, 1)
|
||||
main_layout.addWidget(self.right_frame, 2)
|
||||
# ── 左侧信息面板 ──────────────────────────────────
|
||||
left_frame = QFrame()
|
||||
left_frame.setStyleSheet("background-color: #333333; border-radius: 10px;")
|
||||
left_layout = QVBoxLayout(left_frame)
|
||||
left_layout.setContentsMargins(10, 10, 10, 10)
|
||||
left_layout.setSpacing(6)
|
||||
|
||||
# — 状态徽章 —
|
||||
self._status_badge = QLabel()
|
||||
self._status_badge.setFixedHeight(40)
|
||||
self._status_badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
font_badge = QFont("sans-serif", 18, QFont.Weight.Bold)
|
||||
self._status_badge.setFont(font_badge)
|
||||
left_layout.addWidget(self._status_badge)
|
||||
|
||||
# — 文件信息卡片 —
|
||||
self._file_card = CardFrame("当前文件")
|
||||
self._file_name_lbl = QLabel("--")
|
||||
self._file_name_lbl.setStyleSheet("color: #ffffff; font-size: 16px; font-weight: 600; border: none;")
|
||||
self._file_name_lbl.setWordWrap(True)
|
||||
self._file_card.content_layout.addWidget(self._file_name_lbl)
|
||||
left_layout.addWidget(self._file_card)
|
||||
|
||||
# — 进度卡片 —
|
||||
self._progress_card = CardFrame("打印进度")
|
||||
# 进度条
|
||||
self._progress_bar = QProgressBar()
|
||||
self._progress_bar.setTextVisible(True)
|
||||
self._progress_bar.setFixedHeight(28)
|
||||
self._progress_bar.setStyleSheet("""
|
||||
QProgressBar {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #4CAF50;
|
||||
border-radius: 5px;
|
||||
}
|
||||
""")
|
||||
self._progress_card.content_layout.addWidget(self._progress_bar)
|
||||
# 时间行
|
||||
time_row = QHBoxLayout()
|
||||
time_row.setSpacing(10)
|
||||
self._time_elapsed_lbl = QLabel("已用: --")
|
||||
self._time_elapsed_lbl.setStyleSheet("color: #bbbbbb; font-size: 14px; border: none;")
|
||||
self._time_left_lbl = QLabel("剩余: --")
|
||||
self._time_left_lbl.setStyleSheet("color: #bbbbbb; font-size: 14px; border: none;")
|
||||
time_row.addWidget(self._time_elapsed_lbl)
|
||||
time_row.addStretch()
|
||||
time_row.addWidget(self._time_left_lbl)
|
||||
self._progress_card.content_layout.addLayout(time_row)
|
||||
left_layout.addWidget(self._progress_card)
|
||||
|
||||
# — 温度卡片 —
|
||||
self._temp_card = CardFrame("温度")
|
||||
temp_row = QHBoxLayout()
|
||||
temp_row.setSpacing(8)
|
||||
self._tool_gauge = TempGauge("喷头")
|
||||
self._bed_gauge = TempGauge("热床")
|
||||
temp_row.addWidget(self._tool_gauge)
|
||||
temp_row.addWidget(self._bed_gauge)
|
||||
temp_row.addStretch()
|
||||
self._temp_card.content_layout.addLayout(temp_row)
|
||||
left_layout.addWidget(self._temp_card)
|
||||
|
||||
left_layout.addStretch()
|
||||
|
||||
# ── 右侧预留区域 ─────────────────────────────
|
||||
right_frame = QFrame()
|
||||
right_frame.setStyleSheet("background-color: #3a3a3a; border-radius: 10px;")
|
||||
right_layout = QVBoxLayout(right_frame)
|
||||
right_layout.setContentsMargins(6, 6, 6, 6)
|
||||
|
||||
placeholder = QLabel("GCode 预览\n⚙ 待开发")
|
||||
placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
placeholder.setStyleSheet("color: #666666; font-size: 24px; font-weight: 600; border: none;")
|
||||
right_layout.addWidget(placeholder)
|
||||
|
||||
main_layout.addWidget(left_frame, 2)
|
||||
main_layout.addWidget(right_frame, 3)
|
||||
|
||||
def update_status(self):
|
||||
self.fresh_status_valve()
|
||||
self.lbl_status.setText(f"Status: {self.state}")
|
||||
|
||||
self.lbl_job.setText(f"File: {self.display_name}")
|
||||
|
||||
self.lbl_progress.setText(f"Progress: {self.progress if self.progress else 0:.1f}%")
|
||||
|
||||
# Pass progress to QML
|
||||
# root_obj = self.view.rootObject()
|
||||
# if root_obj:
|
||||
# root_obj.setProperty("progress", prog if prog else 0)
|
||||
# 状态徽章
|
||||
status_key = self.state.split()[0] if self.state else "Offline"
|
||||
color = STATUS_COLORS.get(status_key, "#9E9E9E")
|
||||
label = STATUS_LABELS.get(status_key, self.state)
|
||||
self._status_badge.setText(f"● {label}")
|
||||
self._status_badge.setStyleSheet(
|
||||
f"background-color: #2a2a2a; color: {color}; "
|
||||
f"border: 2px solid {color}; border-radius: 8px; "
|
||||
f"font-size: 18px; font-weight: bold; padding: 4px;"
|
||||
)
|
||||
|
||||
# 文件
|
||||
self._file_name_lbl.setText(self._truncate(self.display_name, 28))
|
||||
|
||||
# 进度
|
||||
prog = min(max(self.progress, 0), 100)
|
||||
self._progress_bar.setValue(int(prog))
|
||||
self._progress_bar.setFormat(f"{prog:.1f}%")
|
||||
self._time_elapsed_lbl.setText(f"已用: {self.format_time(self.print_time)}")
|
||||
self._time_left_lbl.setText(f"剩余: {self.format_time(self.print_time_left)}")
|
||||
# 打印中时进度条变蓝
|
||||
if self.state.startswith("Printing"):
|
||||
self._progress_bar.setStyleSheet("""
|
||||
QProgressBar {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #2196F3;
|
||||
border-radius: 5px;
|
||||
}
|
||||
""")
|
||||
else:
|
||||
self._progress_bar.setStyleSheet("""
|
||||
QProgressBar {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background-color: #4CAF50;
|
||||
border-radius: 5px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 温度
|
||||
self._tool_gauge.set_value(self.tool_temp_actual, self.tool_temp_target)
|
||||
self._bed_gauge.set_value(self.bed_temp_actual, self.bed_temp_target)
|
||||
|
||||
|
||||
|
||||
|
||||
24
printer-screen.service
Normal file
24
printer-screen.service
Normal file
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Printer Screen Touch UI
|
||||
After=systemd-udev-settle.service local-fs.target
|
||||
Conflicts=getty@tty1.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
WorkingDirectory=/opt/Printer_Screen_Menu
|
||||
Environment=QT_QPA_PLATFORM=eglfs
|
||||
Environment=QT_QPA_EGLFS_INTEGRATION=eglfs_kms
|
||||
|
||||
ExecStart=/opt/Printer_Screen_Menu/run.sh
|
||||
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
StandardInput=tty
|
||||
TTYPath=/dev/tty1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
run.sh
9
run.sh
@@ -1,8 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# echo "Running Printer Screen Menu..."
|
||||
# 激活虚拟环境
|
||||
source "$(dirname "$0")/.venv/bin/activate"
|
||||
|
||||
# echo "Virtual environment activated."
|
||||
# 告诉Qt使用 eglfs 插件(无桌面环境直接利用 KMS/DRM/EGL 渲染)
|
||||
export QT_QPA_PLATFORM=eglfs
|
||||
# export QT_QUICK_BACKEND=software
|
||||
@@ -14,7 +15,11 @@ export QT_QPA_PLATFORM=eglfs
|
||||
|
||||
# 若启动报错,可以打开以下变量排查 eglfs 相关日志
|
||||
# export QT_DEBUG_PLUGINS=1
|
||||
# echo "QT_QPA_PLATFORM: $QT_QPA_PLATFORM"
|
||||
# echo "directory: $(dirname "$0")"
|
||||
|
||||
# echo "Starting Printer Screen Menu..."
|
||||
# 启动界面
|
||||
python "$(dirname "$0")/main.py"
|
||||
python "$(dirname "$0")/main.py" > /dev/null 2>&1
|
||||
# python "$(dirname "$0")/main.py"
|
||||
# .venv/bin/python main.py
|
||||
|
||||
@@ -15,63 +15,60 @@ class AIOPrrintSystemAPI:
|
||||
except:
|
||||
return None
|
||||
|
||||
def print(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def get_status(self):
|
||||
|
||||
test_data = {
|
||||
'job': {
|
||||
'job': {
|
||||
'estimatedPrintTime': 1234,
|
||||
'filament': {'length': 765, 'volume': 24356},
|
||||
'file': {'display_name': 'Test File','date': None, 'name': '20260414135441_42bff5215c6148b8b5f4d8c4f15d5ddc.gcode', 'origin': 'local', 'path': None, 'size': 1468987},
|
||||
'lastPrintTime': None,
|
||||
'user': None
|
||||
},
|
||||
'progress': {
|
||||
'completion': 74.8,
|
||||
'filepos': 1234,
|
||||
'printTime': 1235,
|
||||
'printTimeLeft': 6353,
|
||||
'printTimeLeftOrigin': 5366
|
||||
},
|
||||
'state': 'Operational'
|
||||
},
|
||||
'status': {
|
||||
'sd': {'ready': False},
|
||||
'state': {
|
||||
'error': '',
|
||||
'flags': {
|
||||
'cancelling': False,
|
||||
'closedOrError': False,
|
||||
'error': False,
|
||||
'finishing': False,
|
||||
'operational': True,
|
||||
'paused': False,
|
||||
'pausing': False,
|
||||
'printing': False,
|
||||
'ready': True,
|
||||
'resuming': False,
|
||||
'sdReady': False
|
||||
},
|
||||
'text': 'Operational test'
|
||||
},
|
||||
'temperature': {
|
||||
'bed': {'actual': 85, 'offset': 0, 'target': 56},
|
||||
'tool0': {'actual': 0.0, 'offset': 0, 'target': 340}
|
||||
}
|
||||
}
|
||||
}
|
||||
return test_data
|
||||
# test_data = {
|
||||
# 'job': {
|
||||
# 'job': {
|
||||
# 'estimatedPrintTime': 1234,
|
||||
# 'filament': {'length': 765, 'volume': 24356},
|
||||
# 'file': {'display_name': 'Test File','date': None, 'name': '20260414135441_42bff5215c6148b8b5f4d8c4f15d5ddc.gcode', 'origin': 'local', 'path': None, 'size': 1468987},
|
||||
# 'lastPrintTime': None,
|
||||
# 'user': None
|
||||
# },
|
||||
# 'progress': {
|
||||
# 'completion': 74.8,
|
||||
# 'filepos': 1234,
|
||||
# 'printTime': 1235,
|
||||
# 'printTimeLeft': 6353,
|
||||
# 'printTimeLeftOrigin': 5366
|
||||
# },
|
||||
# 'state': 'Operational'
|
||||
# },
|
||||
# 'status': {
|
||||
# 'sd': {'ready': False},
|
||||
# 'state': {
|
||||
# 'error': '',
|
||||
# 'flags': {
|
||||
# 'cancelling': False,
|
||||
# 'closedOrError': False,
|
||||
# 'error': False,
|
||||
# 'finishing': False,
|
||||
# 'operational': True,
|
||||
# 'paused': False,
|
||||
# 'pausing': False,
|
||||
# 'printing': False,
|
||||
# 'ready': True,
|
||||
# 'resuming': False,
|
||||
# 'sdReady': False
|
||||
# },
|
||||
# 'text': 'Operational test'
|
||||
# },
|
||||
# 'temperature': {
|
||||
# 'bed': {'actual': 85, 'offset': 0, 'target': 56},
|
||||
# 'tool0': {'actual': 0.0, 'offset': 0, 'target': 340}
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# return test_data
|
||||
|
||||
# url = f"{self.api_url}/status"
|
||||
# try:
|
||||
# r = requests.get(url, headers=self.headers, timeout=5)
|
||||
# r.raise_for_status()
|
||||
# return r.json()
|
||||
# except:
|
||||
# return {"status": {}, "job": {}}
|
||||
url = f"{self.api_url}/status"
|
||||
try:
|
||||
r = requests.get(url, headers=self.headers, timeout=5)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except:
|
||||
return {"status": {}, "job": {}}
|
||||
|
||||
def pause_print(self):
|
||||
return self._post_action("pause_print", action="pause")
|
||||
@@ -79,4 +76,16 @@ class AIOPrrintSystemAPI:
|
||||
def stop_print(self):
|
||||
return self._post_action("cancel_print")
|
||||
|
||||
def home_axes(self, axes=["x", "y", "z"]):
|
||||
return self._post_action("home_axes", axes=axes)
|
||||
|
||||
def auto_leveling(self):
|
||||
return self._post_action("auto_leveling")
|
||||
|
||||
def send_gcode(self, gcode):
|
||||
return self._post_action("send_gcode", gcode=gcode)
|
||||
|
||||
def off_motors(self):
|
||||
return self.send_gcode("M84")
|
||||
|
||||
|
||||
40
utils/auto_fan_status.py
Normal file
40
utils/auto_fan_status.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import json
|
||||
from PyQt6.QtCore import QTimer
|
||||
|
||||
|
||||
class AutoFanStatus:
|
||||
def __init__(self, update_interval_ms=1000):
|
||||
self.cpu_temp = 0.0
|
||||
self.fan_speed = 0
|
||||
self.fan_state = "Unknown"
|
||||
self.fan_rpm = 0
|
||||
self.is_auto_fan_service_running = False
|
||||
|
||||
self._last_temp = 0.0
|
||||
self._same_counter = 0
|
||||
self._update_interval_ms = update_interval_ms
|
||||
self._timer = QTimer()
|
||||
self._timer.setSingleShot(False)
|
||||
self._timer.timeout.connect(self.update_status)
|
||||
self._timer.start(self._update_interval_ms)
|
||||
|
||||
def update_status(self):
|
||||
try:
|
||||
with open("/dev/shm/fan_status.json", "r") as f:
|
||||
data = json.load(f)
|
||||
self.cpu_temp = data.get("temp", 0.0)
|
||||
self.fan_speed = data.get("pwm", 0)
|
||||
self.fan_state = "Stalled" if data.get("is_stalled", False) else "Running"
|
||||
self.fan_rpm = data.get("rpm", 0)
|
||||
if self.cpu_temp == self._last_temp:
|
||||
self._same_counter += 1
|
||||
else:
|
||||
self._same_counter = 0
|
||||
self._last_temp = self.cpu_temp
|
||||
self.is_auto_fan_service_running = self._same_counter < 5
|
||||
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||||
self.cpu_temp = 0.0
|
||||
self.fan_speed = 0
|
||||
self.fan_state = "Unknown"
|
||||
self.fan_rpm = 0
|
||||
self.is_auto_fan_service_running = False
|
||||
71
utils/config_parse.py
Normal file
71
utils/config_parse.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import json
|
||||
import os
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, QFileSystemWatcher
|
||||
|
||||
|
||||
class ConfigParse(QObject):
|
||||
"""配置文件解析与自动重载"""
|
||||
config_changed = pyqtSignal(object) # 发送自身实例
|
||||
|
||||
def __init__(self, config_file="config.json"):
|
||||
super().__init__()
|
||||
self.config_file = config_file
|
||||
self.api_url = None
|
||||
self.api_key = None
|
||||
self.gcode_dir = None
|
||||
self.move_axis_area = None
|
||||
self.move_max_speed = None
|
||||
self.config = self._load_config()
|
||||
self._parse_config()
|
||||
|
||||
# 使用 QFileSystemWatcher 监视文件变化
|
||||
self._watcher = QFileSystemWatcher(self)
|
||||
self._watch_path = os.path.abspath(self.config_file)
|
||||
self._last_mtime = 0
|
||||
self._start_watching()
|
||||
|
||||
def _start_watching(self):
|
||||
"""启动文件监视"""
|
||||
if not os.path.isfile(self._watch_path):
|
||||
return
|
||||
self._last_mtime = os.path.getmtime(self._watch_path)
|
||||
if self._watch_path not in self._watcher.files():
|
||||
self._watcher.addPath(self._watch_path)
|
||||
self._watcher.fileChanged.connect(self._on_file_changed)
|
||||
|
||||
def _on_file_changed(self, path):
|
||||
"""文件变化时重新加载(防止保存中多次触发,用 mtime 去抖动)"""
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
if mtime <= self._last_mtime:
|
||||
return
|
||||
self._last_mtime = mtime
|
||||
except OSError:
|
||||
return
|
||||
|
||||
print("Config changed")
|
||||
|
||||
old_config = self.config
|
||||
new_config = self._load_config()
|
||||
if new_config == old_config:
|
||||
return
|
||||
|
||||
self.config = new_config
|
||||
self._parse_config()
|
||||
self.config_changed.emit(self)
|
||||
|
||||
def _load_config(self):
|
||||
try:
|
||||
with open(self.config_file, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Error loading config: {e}")
|
||||
return {}
|
||||
|
||||
def _parse_config(self):
|
||||
self.api_url = self.config.get("api_url", None)
|
||||
self.api_key = self.config.get("api_key", None)
|
||||
self.gcode_dir = self.config.get("gcode_dir", None)
|
||||
self.move_axis_area = self.config.get("move_axis_area", None)
|
||||
self.move_max_speed = self.config.get("move_max_speed", {"x": 3000, "y": 3000, "z": 200})
|
||||
self.move_max_speed = self.config.get("move_max_speed", None)
|
||||
@@ -8,12 +8,17 @@
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||
QApplication, QLineEdit, QTextEdit,
|
||||
QApplication, QLineEdit, QTextEdit, QGridLayout
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QEvent, QPoint, pyqtSignal
|
||||
from PyQt6.QtCore import Qt, QEvent, QPoint, QTimer, pyqtSignal
|
||||
from PyQt6.QtGui import QMouseEvent, QTextCursor
|
||||
|
||||
|
||||
# ─── 长按重复配置 ─────────────────────────────────────────
|
||||
LONG_PRESS_DELAY = 400 # 按住多少毫秒后开始重复
|
||||
LONG_PRESS_REPEAT = 80 # 开始重复后每多少毫秒触发一次
|
||||
|
||||
|
||||
# ─── 键盘布局定义 ──────────────────────────────────────────────
|
||||
|
||||
KEY_ROWS = {
|
||||
@@ -31,14 +36,18 @@ KEY_ROWS = {
|
||||
|
||||
NUMBER_ROW = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
|
||||
|
||||
# 符号键:按 Shift 或符号切换时与数字行互换
|
||||
# 符号键:按 ?!(. 切换时替换数字行
|
||||
SYMBOL_ROW = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"]
|
||||
# 显示用(Qt 中 & 需要写 && 才能显示一个 &)
|
||||
SYMBOL_ROW_DISPLAY = ["!", "@", "#", "$", "%", "^", "&&", "*", "(", ")"]
|
||||
|
||||
# 右侧额外符号(放在字母区行末或单独行)
|
||||
# 额外符号:切换到符号键盘后显示在字母区
|
||||
EXTRA_SYMBOLS = [
|
||||
["-", "=", "[", "]", "\\", ";", "'", ",", ".", "/"],
|
||||
["_", "+", "{", "}", "|", ":", "\"", "<", ">", "?"],
|
||||
]
|
||||
# 常见后缀(放在符号键盘多余的字母位置上)
|
||||
COMMON_SUFFIXES = [".com", ".cn", ".org", ".net", ".edu", ".co"]
|
||||
|
||||
KEY_STYLE = """
|
||||
QPushButton {
|
||||
@@ -64,7 +73,7 @@ QPushButton:pressed {
|
||||
|
||||
CTRL_KEY_STYLE = """
|
||||
QPushButton {
|
||||
min-width: 72px;
|
||||
min-width: 52px;
|
||||
min-height: 52px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
@@ -84,29 +93,9 @@ QPushButton:pressed {
|
||||
}
|
||||
"""
|
||||
|
||||
SPACE_STYLE = """
|
||||
QPushButton {
|
||||
min-width: 280px;
|
||||
min-height: 52px;
|
||||
font-size: 18px;
|
||||
color: transparent;
|
||||
background-color: #4a4a4a;
|
||||
border: 2px solid #646464;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #5a5a5a;
|
||||
border-color: #888888;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2f6f91;
|
||||
border-color: #5a9fcf;
|
||||
}
|
||||
"""
|
||||
|
||||
ACTIVE_CTRL_STYLE = """
|
||||
QPushButton {
|
||||
min-width: 72px;
|
||||
min-width: 52px;
|
||||
min-height: 52px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
@@ -136,6 +125,9 @@ class FloatingKeyboard(QWidget):
|
||||
| Qt.WindowType.Tool
|
||||
)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
# 防止键盘窗口/按钮抢走输入框焦点
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||||
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.setStyleSheet("background: transparent;")
|
||||
|
||||
self._drag_pos = None
|
||||
@@ -154,9 +146,9 @@ class FloatingKeyboard(QWidget):
|
||||
outer.setStyleSheet(
|
||||
"background-color: #333333; border: 2px solid #555555; border-radius: 12px;"
|
||||
)
|
||||
layout = QVBoxLayout(outer)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setSpacing(4)
|
||||
outer_layout = QVBoxLayout(outer)
|
||||
outer_layout.setContentsMargins(8, 8, 8, 8)
|
||||
outer_layout.setSpacing(4)
|
||||
|
||||
# ── 顶部拖拽条 ──
|
||||
title_bar = QWidget()
|
||||
@@ -168,8 +160,8 @@ class FloatingKeyboard(QWidget):
|
||||
title_layout.setContentsMargins(8, 0, 8, 0)
|
||||
|
||||
drag_label = QPushButton("≡ 键盘")
|
||||
drag_label.setStyleSheet(
|
||||
"""
|
||||
drag_label.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
drag_label.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
color: #aaaaaa;
|
||||
@@ -177,15 +169,14 @@ class FloatingKeyboard(QWidget):
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
"""
|
||||
)
|
||||
""")
|
||||
title_layout.addWidget(drag_label)
|
||||
title_layout.addStretch()
|
||||
|
||||
close_btn = QPushButton("✕")
|
||||
close_btn.setFixedSize(28, 28)
|
||||
close_btn.setStyleSheet(
|
||||
"""
|
||||
close_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
close_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: transparent;
|
||||
color: #aaaaaa;
|
||||
@@ -197,107 +188,175 @@ class FloatingKeyboard(QWidget):
|
||||
background-color: #555555;
|
||||
color: #ffffff;
|
||||
}
|
||||
"""
|
||||
)
|
||||
""")
|
||||
close_btn.clicked.connect(self.hide)
|
||||
title_layout.addWidget(close_btn)
|
||||
layout.addWidget(title_bar)
|
||||
outer_layout.addWidget(title_bar)
|
||||
|
||||
# ── 数字/符号行 ──
|
||||
self.num_layout = QHBoxLayout()
|
||||
self.num_layout.setSpacing(4)
|
||||
# ── 内容区:左侧 4 行按键 + 右侧功能键 ──
|
||||
content = QHBoxLayout()
|
||||
content.setSpacing(6)
|
||||
|
||||
# ====== 左侧:QGridLayout 4行×10列,等宽列 ======
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(4)
|
||||
for col in range(10):
|
||||
grid.setColumnStretch(col, 1)
|
||||
|
||||
# Row 0: 数字行
|
||||
self.num_buttons = []
|
||||
for ch in NUMBER_ROW:
|
||||
btn = self._make_key(ch)
|
||||
for col, ch in enumerate(NUMBER_ROW):
|
||||
btn = self._make_key(ch, ch)
|
||||
self.num_buttons.append(btn)
|
||||
self.num_layout.addWidget(btn)
|
||||
layout.addLayout(self.num_layout)
|
||||
grid.addWidget(btn, 0, col)
|
||||
|
||||
# ── 字母行 ──
|
||||
self._letter_buttons = [] # flat list for shift toggle
|
||||
for row_keys in KEY_ROWS["normal"]:
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.setSpacing(4)
|
||||
row_layout.addStretch()
|
||||
for ch in row_keys:
|
||||
btn = self._make_key(ch)
|
||||
self._letter_buttons.append(btn)
|
||||
row_layout.addWidget(btn)
|
||||
row_layout.addStretch()
|
||||
layout.addLayout(row_layout)
|
||||
# Row 1: 字母行 q ~ p
|
||||
self._letter_buttons = []
|
||||
for col, ch in enumerate(KEY_ROWS["normal"][0]):
|
||||
btn = self._make_key(ch, ch)
|
||||
self._letter_buttons.append(btn)
|
||||
grid.addWidget(btn, 1, col)
|
||||
|
||||
# ── 额外符号行(- = [ ] 等) ──
|
||||
self.extra_layout = QHBoxLayout()
|
||||
self.extra_layout.setSpacing(4)
|
||||
self.extra_layout.addStretch()
|
||||
self.extra_buttons = []
|
||||
for ch in EXTRA_SYMBOLS[0]:
|
||||
btn = self._make_key(ch)
|
||||
self.extra_buttons.append(btn)
|
||||
self.extra_layout.addWidget(btn)
|
||||
self.extra_layout.addStretch()
|
||||
layout.addLayout(self.extra_layout)
|
||||
# Row 2: 字母行 a ~ l (9个,第10列空位留给符号模式)
|
||||
for col, ch in enumerate(KEY_ROWS["normal"][1]):
|
||||
btn = self._make_key(ch, ch)
|
||||
self._letter_buttons.append(btn)
|
||||
grid.addWidget(btn, 2, col)
|
||||
|
||||
# ── 功能键行 ──
|
||||
ctrl_layout = QHBoxLayout()
|
||||
ctrl_layout.setSpacing(4)
|
||||
# Row 3: [?!(.] + 字母 z ~ m + [⇧] + [Caps]
|
||||
self.sym_btn = self._make_ctrl_btn("?!(.", self._toggle_symbol)
|
||||
grid.addWidget(self.sym_btn, 3, 0)
|
||||
|
||||
self.shift_btn = QPushButton("⇧")
|
||||
self.shift_btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
self.shift_btn.clicked.connect(self._toggle_shift)
|
||||
ctrl_layout.addWidget(self.shift_btn)
|
||||
for i, ch in enumerate(KEY_ROWS["normal"][2]):
|
||||
btn = self._make_key(ch, ch)
|
||||
self._letter_buttons.append(btn)
|
||||
grid.addWidget(btn, 3, i + 1)
|
||||
|
||||
self.caps_btn = QPushButton("A/a")
|
||||
self.caps_btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
self.caps_btn.clicked.connect(self._toggle_caps)
|
||||
ctrl_layout.addWidget(self.caps_btn)
|
||||
self.shift_btn = self._make_ctrl_btn("⇧", self._toggle_shift)
|
||||
grid.addWidget(self.shift_btn, 3, 8)
|
||||
|
||||
self.sym_btn = QPushButton("?123")
|
||||
self.sym_btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
self.sym_btn.clicked.connect(self._toggle_symbol)
|
||||
ctrl_layout.addWidget(self.sym_btn)
|
||||
self.caps_btn = self._make_ctrl_btn("Caps", self._toggle_caps)
|
||||
grid.addWidget(self.caps_btn, 3, 9)
|
||||
|
||||
backspace_btn = QPushButton("⌫")
|
||||
backspace_btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
# 各行等高分占布局
|
||||
for row in range(4):
|
||||
grid.setRowStretch(row, 1)
|
||||
|
||||
content.addLayout(grid)
|
||||
|
||||
# ====== 右侧:4个竖向功能键,拉伸填满高度 ======
|
||||
right_col = QVBoxLayout()
|
||||
right_col.setSpacing(4)
|
||||
|
||||
RIGHT_BTN_STYLE = """
|
||||
QPushButton {
|
||||
min-width: 80px;
|
||||
min-height: 52px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f2f2f2;
|
||||
background-color: #555555;
|
||||
border: 2px solid #707070;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #666666;
|
||||
border-color: #909090;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2f6f91;
|
||||
border-color: #5a9fcf;
|
||||
}
|
||||
"""
|
||||
|
||||
backspace_btn = QPushButton("⌫ 退格")
|
||||
backspace_btn.setStyleSheet(RIGHT_BTN_STYLE)
|
||||
backspace_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
backspace_btn.clicked.connect(lambda: self._send_key("\b"))
|
||||
ctrl_layout.addWidget(backspace_btn)
|
||||
self._enable_long_press(backspace_btn, lambda: self._send_key("\b"))
|
||||
right_col.addWidget(backspace_btn, stretch=1)
|
||||
|
||||
ctrl_layout.addStretch()
|
||||
del_btn = QPushButton("Del")
|
||||
del_btn.setStyleSheet(RIGHT_BTN_STYLE)
|
||||
del_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
del_btn.clicked.connect(lambda: self._send_key("\x7f"))
|
||||
self._enable_long_press(del_btn, lambda: self._send_key("\x7f"))
|
||||
right_col.addWidget(del_btn, stretch=1)
|
||||
|
||||
enter_btn = QPushButton("↵ 回车")
|
||||
enter_btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
enter_btn.setStyleSheet(RIGHT_BTN_STYLE)
|
||||
enter_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
enter_btn.clicked.connect(lambda: self._send_key("\n"))
|
||||
ctrl_layout.addWidget(enter_btn)
|
||||
self._enable_long_press(enter_btn, lambda: self._send_key("\n"))
|
||||
right_col.addWidget(enter_btn, stretch=1)
|
||||
|
||||
layout.addLayout(ctrl_layout)
|
||||
|
||||
# ── 空格行 ──
|
||||
space_layout = QHBoxLayout()
|
||||
space_layout.setSpacing(4)
|
||||
space_layout.addStretch()
|
||||
|
||||
self.space_btn = QPushButton(" ") # full-width space as placeholder
|
||||
self.space_btn.setStyleSheet(SPACE_STYLE)
|
||||
self.space_btn = QPushButton(" 空格")
|
||||
self.space_btn.setStyleSheet(RIGHT_BTN_STYLE)
|
||||
self.space_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.space_btn.clicked.connect(lambda: self._send_key(" "))
|
||||
space_layout.addWidget(self.space_btn)
|
||||
self._enable_long_press(self.space_btn, lambda: self._send_key(" "))
|
||||
right_col.addWidget(self.space_btn, stretch=1)
|
||||
|
||||
space_layout.addStretch()
|
||||
layout.addLayout(space_layout)
|
||||
content.addLayout(right_col)
|
||||
outer_layout.addLayout(content)
|
||||
|
||||
outer_layout = QVBoxLayout(self)
|
||||
outer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
outer_layout.addWidget(outer)
|
||||
outer_container = QVBoxLayout(self)
|
||||
outer_container.setContentsMargins(0, 0, 0, 0)
|
||||
outer_container.addWidget(outer)
|
||||
|
||||
self.setFixedWidth(680)
|
||||
self.setFixedWidth(840)
|
||||
|
||||
# ── 按键工厂 ──
|
||||
|
||||
def _make_key(self, text):
|
||||
def _make_key(self, text, send_text=None):
|
||||
"""创建按键,send_text 为实际发送字符(如 & 按钮显示 && 但发送 &)"""
|
||||
btn = QPushButton(text)
|
||||
btn.setStyleSheet(KEY_STYLE)
|
||||
btn.clicked.connect(lambda checked, t=text: self._send_key(t))
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) # 不抢输入框焦点
|
||||
btn._real_text = send_text if send_text is not None else text
|
||||
btn.clicked.connect(lambda: self._send_key(btn._real_text))
|
||||
# 普通按键自动加长按
|
||||
self._enable_long_press(btn, lambda: self._send_key(btn._real_text))
|
||||
return btn
|
||||
|
||||
@staticmethod
|
||||
def _make_ctrl_btn(text, callback):
|
||||
"""创建控制类按钮(?!(./⇧/Caps等)"""
|
||||
btn = QPushButton(text)
|
||||
btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) # 不抢输入框焦点
|
||||
btn.clicked.connect(callback)
|
||||
return btn
|
||||
|
||||
def _enable_long_press(self, btn, callback):
|
||||
"""为按键添加长按连续触发功能(按住 LONG_PRESS_DELAY ms 后每 LONG_PRESS_REPEAT ms 重复)
|
||||
callback 为点击/重复时执行的 callable(无参数)
|
||||
"""
|
||||
timer = QTimer(btn)
|
||||
timer.setSingleShot(True)
|
||||
btn._repeat_timer = timer
|
||||
|
||||
def on_pressed():
|
||||
timer.setSingleShot(True)
|
||||
timer.setInterval(LONG_PRESS_DELAY)
|
||||
timer.start()
|
||||
|
||||
def on_released():
|
||||
timer.stop()
|
||||
|
||||
def on_timer():
|
||||
# 首次超时:进入快速重复模式
|
||||
if timer.isSingleShot():
|
||||
timer.setSingleShot(False)
|
||||
timer.setInterval(LONG_PRESS_REPEAT)
|
||||
timer.start()
|
||||
callback()
|
||||
|
||||
btn.pressed.connect(on_pressed)
|
||||
btn.released.connect(on_released)
|
||||
timer.timeout.connect(on_timer)
|
||||
|
||||
# ── 按键发送 ──
|
||||
|
||||
def _send_key(self, text):
|
||||
@@ -305,6 +364,9 @@ class FloatingKeyboard(QWidget):
|
||||
if text == "\b":
|
||||
self._backspace()
|
||||
return
|
||||
if text == "\x7f":
|
||||
self._delete_forward()
|
||||
return
|
||||
if self._symbol_mode:
|
||||
self._toggle_symbol()
|
||||
|
||||
@@ -323,6 +385,10 @@ class FloatingKeyboard(QWidget):
|
||||
tc.insertText(text)
|
||||
widget.setTextCursor(tc)
|
||||
|
||||
# 上档键:激活一次后自动取消
|
||||
if self._shift_on:
|
||||
self._toggle_shift()
|
||||
|
||||
def _backspace(self):
|
||||
widget = self._get_target()
|
||||
if widget is None:
|
||||
@@ -344,6 +410,25 @@ class FloatingKeyboard(QWidget):
|
||||
tc.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor)
|
||||
tc.removeSelectedText()
|
||||
|
||||
def _delete_forward(self):
|
||||
"""向前删除(删除光标后的字符)"""
|
||||
widget = self._get_target()
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
if isinstance(widget, QLineEdit):
|
||||
cursor = widget.cursorPosition()
|
||||
current = widget.text()
|
||||
if cursor < len(current):
|
||||
new_text = current[:cursor] + current[cursor + 1:]
|
||||
widget.setText(new_text)
|
||||
widget.setCursorPosition(cursor)
|
||||
elif isinstance(widget, QTextEdit):
|
||||
tc = widget.textCursor()
|
||||
if not tc.hasSelection():
|
||||
tc.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor)
|
||||
tc.removeSelectedText()
|
||||
|
||||
def _get_target(self):
|
||||
"""获取当前有效的目标输入控件"""
|
||||
if self._target_widget and self._target_widget.hasFocus():
|
||||
@@ -368,26 +453,47 @@ class FloatingKeyboard(QWidget):
|
||||
def _toggle_symbol(self):
|
||||
self._symbol_mode = not self._symbol_mode
|
||||
if self._symbol_mode:
|
||||
for btn, ch in zip(self.num_buttons, SYMBOL_ROW):
|
||||
btn.setText(ch)
|
||||
for btn, ch in zip(self.extra_buttons, EXTRA_SYMBOLS[1]):
|
||||
btn.setText(ch)
|
||||
# 进入符号键盘
|
||||
# Row 0: 数字行 → SYMBOL_ROW
|
||||
for btn, ch, disp in zip(self.num_buttons, SYMBOL_ROW, SYMBOL_ROW_DISPLAY):
|
||||
btn.setText(disp)
|
||||
btn._real_text = ch
|
||||
# Row 1: 字母行 q-p (indices 0-9) → EXTRA_SYMBOLS[0]
|
||||
for i, ch in enumerate(EXTRA_SYMBOLS[0]):
|
||||
self._letter_buttons[i].setText(ch)
|
||||
self._letter_buttons[i]._real_text = ch
|
||||
# Row 2: 字母行 a-l (indices 10-18) → EXTRA_SYMBOLS[1][0:9]
|
||||
for i, ch in enumerate(EXTRA_SYMBOLS[1][:9]):
|
||||
self._letter_buttons[10 + i].setText(ch)
|
||||
self._letter_buttons[10 + i]._real_text = ch
|
||||
# Row 3: z-m (indices 19-25) → EXTRA_SYMBOLS[1][9] + 后缀
|
||||
self._letter_buttons[19].setText(EXTRA_SYMBOLS[1][9])
|
||||
self._letter_buttons[19]._real_text = EXTRA_SYMBOLS[1][9]
|
||||
for i, suffix in enumerate(COMMON_SUFFIXES):
|
||||
self._letter_buttons[20 + i].setText(suffix)
|
||||
self._letter_buttons[20 + i]._real_text = suffix
|
||||
|
||||
self.sym_btn.setStyleSheet(ACTIVE_CTRL_STYLE)
|
||||
self.sym_btn.setText("ABC")
|
||||
else:
|
||||
# 退出符号键盘 → 恢复数字和字母
|
||||
for btn, ch in zip(self.num_buttons, NUMBER_ROW):
|
||||
btn.setText(ch)
|
||||
for btn, ch in zip(self.extra_buttons, EXTRA_SYMBOLS[0]):
|
||||
btn.setText(ch)
|
||||
btn._real_text = ch
|
||||
# 字母区根据当前 shift/caps 状态恢复
|
||||
self._apply_shift_caps()
|
||||
self.sym_btn.setStyleSheet(CTRL_KEY_STYLE)
|
||||
self.sym_btn.setText("?123")
|
||||
self.sym_btn.setText("?!(.")
|
||||
|
||||
def _apply_shift_caps(self):
|
||||
if self._symbol_mode:
|
||||
return # 符号键盘下不改变字母区显示
|
||||
use_shift = self._shift_on != self._caps_on # XOR
|
||||
rows = KEY_ROWS["shift"] if use_shift else KEY_ROWS["normal"]
|
||||
flat = [ch for row in rows for ch in row]
|
||||
for btn, ch in zip(self._letter_buttons, flat):
|
||||
btn.setText(ch)
|
||||
btn._real_text = ch
|
||||
|
||||
# ── 窗口拖拽 ──
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ class WifiManager:
|
||||
return ""
|
||||
|
||||
def list_saved_networks(self):
|
||||
"""列出已保存的 wifi"""
|
||||
"""列出已保存的 wifi(wpa_cli list_networks 以 Tab 分隔)"""
|
||||
output = self._run_wpa_cli("list_networks")
|
||||
networks = []
|
||||
lines = output.splitlines()
|
||||
if len(lines) > 1:
|
||||
for line in lines[1:]: # skip header
|
||||
parts = line.split()
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 4:
|
||||
networks.append({
|
||||
"network_id": parts[0],
|
||||
|
||||
Reference in New Issue
Block a user