gcode预览测试
This commit is contained in:
BIN
__pycache__/gcode_viewer.cpython-311.pyc
Normal file
BIN
__pycache__/gcode_viewer.cpython-311.pyc
Normal file
Binary file not shown.
BIN
assets/img/logo.jpg
Normal file
BIN
assets/img/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
71
main.py
71
main.py
@@ -31,6 +31,7 @@ class MainWindow(QWidget):
|
||||
self.wifi_manager = WifiManager()
|
||||
self._last_network_check = 0.0
|
||||
self._is_network_connected = False
|
||||
self._clock_has_synced = False # 是否曾成功获取到时间(断网后继续显示)
|
||||
self.init_ui()
|
||||
|
||||
# 定时刷新风扇状态显示
|
||||
@@ -58,8 +59,35 @@ class MainWindow(QWidget):
|
||||
api_key=self.config_parser.api_key
|
||||
)
|
||||
|
||||
def _signal_to_bars(self, signal_val):
|
||||
"""将信号强度转换为条形图标字符串
|
||||
支持 dBm(负值,如 -45)和百分比(0-100,nmcli 格式)
|
||||
"""
|
||||
if signal_val is None:
|
||||
return "⬜⬜⬜"
|
||||
if signal_val < 0:
|
||||
# dBm 格式
|
||||
if signal_val >= -50:
|
||||
return "■■■"
|
||||
elif signal_val >= -60:
|
||||
return "■■□"
|
||||
elif signal_val >= -70:
|
||||
return "■□□"
|
||||
else:
|
||||
return "□□□"
|
||||
else:
|
||||
# 百分比格式 (0-100)
|
||||
if signal_val >= 75:
|
||||
return "■■■"
|
||||
elif signal_val >= 50:
|
||||
return "■■□"
|
||||
elif signal_val >= 25:
|
||||
return "■□□"
|
||||
else:
|
||||
return "□□□"
|
||||
|
||||
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)
|
||||
@@ -79,17 +107,42 @@ class MainWindow(QWidget):
|
||||
f"background-color: #2a2a2a; color: {color}; "
|
||||
f"font-size: 18px; font-weight: 600; padding: 4px 16px;"
|
||||
)
|
||||
load_color = "#a0d8a0" if s.cpu_load < 1.0 else ("#e8a060" if s.cpu_load < 2.0 else "#e86c60")
|
||||
cpu_load_str = f"<span style='color:{load_color}'>{s.cpu_load:.2f}</span><span>/4.0</span>"
|
||||
self._fan_label.setText(
|
||||
f"🌡 {temp} {state} 𖣘 {speed} {rpm}"
|
||||
f"🌡 {temp} {state} 𖣘 {speed} {rpm} 🖥 {cpu_load_str}"
|
||||
)
|
||||
self._fan_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
|
||||
# 更新时钟(有网络时显示)
|
||||
if self._check_network():
|
||||
# --- WiFi 状态指示 ---
|
||||
is_connected = self._check_network()
|
||||
if is_connected:
|
||||
try:
|
||||
status = self.wifi_manager.get_current_status()
|
||||
raw_signal = status.get("signal_level")
|
||||
signal_dbm = int(raw_signal) if raw_signal else None
|
||||
except Exception:
|
||||
signal_dbm = None
|
||||
bars = self._signal_to_bars(signal_dbm)
|
||||
self._wifi_label.setText(f"Signal: {bars}")
|
||||
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
||||
else:
|
||||
self._wifi_label.setText("No Signal")
|
||||
self._wifi_label.setStyleSheet("color: #e86c60; font-size: 18px; font-weight: 600;")
|
||||
|
||||
# --- 时钟(有网络时更新;断网后保留最后一次的时间) ---
|
||||
if is_connected:
|
||||
now = datetime.now()
|
||||
self._clock_label.setText(now.strftime("%H:%M:%S"))
|
||||
if not self._clock_has_synced:
|
||||
self._clock_has_synced = True
|
||||
self._clock_label.show()
|
||||
else:
|
||||
self._clock_label.hide()
|
||||
# 从未同步过则隐藏,否则保留上次时间
|
||||
if self._clock_has_synced:
|
||||
self._clock_label.show()
|
||||
else:
|
||||
self._clock_label.hide()
|
||||
|
||||
def init_ui(self):
|
||||
# 整体布局
|
||||
@@ -124,7 +177,13 @@ class MainWindow(QWidget):
|
||||
top_layout.addWidget(self._fan_label)
|
||||
top_layout.addStretch()
|
||||
|
||||
# 时钟标签(有网络时显示)
|
||||
# WiFi 状态指示
|
||||
self._wifi_label = QLabel("📶 --")
|
||||
self._wifi_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._wifi_label.setStyleSheet("color: #a0d8a0; font-size: 18px; font-weight: 600;")
|
||||
top_layout.addWidget(self._wifi_label)
|
||||
|
||||
# 时钟标签(有网络时显示,获取过一次后断网也不隐藏)
|
||||
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;")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
||||
QFrame, QGridLayout, QSizePolicy, QLineEdit,
|
||||
QFrame, QGridLayout, QSizePolicy, QLineEdit, QApplication,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer, QPointF, QRectF
|
||||
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 (保留备用)
|
||||
|
||||
@@ -222,6 +223,16 @@ class ControlPage(QWidget):
|
||||
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()
|
||||
@@ -371,7 +382,9 @@ class ControlPage(QWidget):
|
||||
|
||||
def _cmd_home(self):
|
||||
self.api_client.home_axes(["x", "y", "z"])
|
||||
self.pos_x = self.pos_y = self.pos_z = 0.0
|
||||
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()
|
||||
@@ -385,6 +398,45 @@ class ControlPage(QWidget):
|
||||
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):
|
||||
@@ -511,6 +563,7 @@ class ControlPage(QWidget):
|
||||
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)
|
||||
|
||||
|
||||
@@ -132,6 +132,94 @@ class WifiScanWorker(QObject):
|
||||
self.scan_error.emit(str(e))
|
||||
|
||||
|
||||
class WifiConnectWorker(QObject):
|
||||
"""在后台线程中连接新WiFi"""
|
||||
finished = pyqtSignal(bool, str) # (success, ssid)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, wifi_manager, auth_mode, ssid, password=None, identity=None):
|
||||
super().__init__()
|
||||
self.wifi_manager = wifi_manager
|
||||
self.auth_mode = auth_mode
|
||||
self.ssid = ssid
|
||||
self.password = password
|
||||
self.identity = identity
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.auth_mode == "open":
|
||||
ok = self.wifi_manager.connect_wifi(self.ssid, None)
|
||||
elif self.auth_mode == "psk":
|
||||
ok = self.wifi_manager.connect_wifi(self.ssid, self.password)
|
||||
else:
|
||||
ok = self.wifi_manager.connect_eap(self.ssid, self.identity, self.password)
|
||||
self.finished.emit(ok, self.ssid)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class WifiConnectSavedWorker(QObject):
|
||||
"""在后台线程中连接已保存的网络"""
|
||||
finished = pyqtSignal(bool, str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, wifi_manager, network_id, ssid):
|
||||
super().__init__()
|
||||
self.wifi_manager = wifi_manager
|
||||
self.network_id = network_id
|
||||
self.ssid = ssid
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
ok = self.wifi_manager.connect_network_id(self.network_id)
|
||||
self.finished.emit(ok, self.ssid)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class WifiRemoveWorker(QObject):
|
||||
"""在后台线程中删除已保存的网络"""
|
||||
finished = pyqtSignal(bool, str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, wifi_manager, network_id, ssid):
|
||||
super().__init__()
|
||||
self.wifi_manager = wifi_manager
|
||||
self.network_id = network_id
|
||||
self.ssid = ssid
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.wifi_manager.remove_network(self.network_id)
|
||||
self.finished.emit(True, self.ssid)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class WifiHotspotWorker(QObject):
|
||||
"""在后台线程中开启/关闭热点"""
|
||||
finished = pyqtSignal(bool, str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, wifi_manager, action, ssid=None, password=None):
|
||||
super().__init__()
|
||||
self.wifi_manager = wifi_manager
|
||||
self.action = action
|
||||
self.ssid = ssid
|
||||
self.password = password
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.action == "open":
|
||||
ret = self.wifi_manager.open_hotspot(self.ssid, self.password)
|
||||
self.finished.emit(bool(ret), self.ssid or "")
|
||||
else:
|
||||
self.wifi_manager.close_hotspot()
|
||||
self.finished.emit(True, "")
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class SettingPage(QWidget):
|
||||
def __init__(self, api_client, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -451,13 +539,13 @@ class SettingPage(QWidget):
|
||||
|
||||
saved_buttons_layout = QHBoxLayout()
|
||||
saved_buttons_layout.setSpacing(12)
|
||||
connect_saved_button = QPushButton("连接到此网络")
|
||||
connect_saved_button.clicked.connect(self.connect_to_saved_wifi)
|
||||
saved_buttons_layout.addWidget(connect_saved_button)
|
||||
self.connect_saved_button = QPushButton("连接到此网络")
|
||||
self.connect_saved_button.clicked.connect(self.connect_to_saved_wifi)
|
||||
saved_buttons_layout.addWidget(self.connect_saved_button)
|
||||
|
||||
remove_saved_button = QPushButton("删除选中")
|
||||
remove_saved_button.clicked.connect(self.remove_selected_saved_wifi)
|
||||
saved_buttons_layout.addWidget(remove_saved_button)
|
||||
self.remove_saved_button = QPushButton("删除选中")
|
||||
self.remove_saved_button.clicked.connect(self.remove_selected_saved_wifi)
|
||||
saved_buttons_layout.addWidget(self.remove_saved_button)
|
||||
wifi_layout.addLayout(saved_buttons_layout)
|
||||
|
||||
nearby_title = QLabel("附近网络")
|
||||
@@ -547,8 +635,8 @@ class SettingPage(QWidget):
|
||||
wifi_layout.addLayout(form)
|
||||
|
||||
# 连接按钮使用醒目的强调色
|
||||
connect_button = QPushButton("连接")
|
||||
connect_button.setStyleSheet(
|
||||
self.connect_button = QPushButton("连接")
|
||||
self.connect_button.setStyleSheet(
|
||||
"""
|
||||
QPushButton {
|
||||
min-height: 52px;
|
||||
@@ -570,8 +658,8 @@ class SettingPage(QWidget):
|
||||
}
|
||||
"""
|
||||
)
|
||||
connect_button.clicked.connect(self.connect_to_wifi)
|
||||
wifi_layout.addWidget(connect_button)
|
||||
self.connect_button.clicked.connect(self.connect_to_wifi)
|
||||
wifi_layout.addWidget(self.connect_button)
|
||||
|
||||
wifi_layout.addStretch()
|
||||
self.settings_stack.addWidget(self._wrap_scroll(wifi_widget))
|
||||
@@ -722,47 +810,101 @@ class SettingPage(QWidget):
|
||||
self.hotspot_toggle.setText("OFF")
|
||||
|
||||
def _on_hotspot_toggled(self, checked):
|
||||
# 立即阻塞信号,防止递归触发
|
||||
self.hotspot_toggle.blockSignals(True)
|
||||
|
||||
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)
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请输入热点名称")
|
||||
return
|
||||
if len(password) < 8:
|
||||
self.hotspot_toggle.setChecked(False)
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
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)
|
||||
|
||||
# 按钮UI反馈 → 显示"开启中……"
|
||||
self.hotspot_toggle.setEnabled(False)
|
||||
self.hotspot_toggle.setText("开启中……")
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
|
||||
self._hotspot_thread = QThread()
|
||||
self._hotspot_worker = WifiHotspotWorker(self.wifi_manager, "open", ssid, password)
|
||||
self._hotspot_worker.moveToThread(self._hotspot_thread)
|
||||
self._hotspot_thread.started.connect(self._hotspot_worker.run)
|
||||
self._hotspot_worker.finished.connect(self._on_hotspot_open_finished)
|
||||
self._hotspot_worker.error.connect(self._on_hotspot_open_error)
|
||||
self._hotspot_worker.finished.connect(self._hotspot_thread.quit)
|
||||
self._hotspot_worker.error.connect(self._hotspot_thread.quit)
|
||||
self._hotspot_worker.finished.connect(self._hotspot_worker.deleteLater)
|
||||
self._hotspot_worker.error.connect(self._hotspot_worker.deleteLater)
|
||||
self._hotspot_thread.finished.connect(self._hotspot_thread.deleteLater)
|
||||
self._hotspot_thread.start()
|
||||
else:
|
||||
try:
|
||||
self.wifi_manager.close_hotspot()
|
||||
except Exception:
|
||||
pass
|
||||
# 关闭热点
|
||||
self.hotspot_toggle.setEnabled(False)
|
||||
self.hotspot_toggle.setText("关闭中……")
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
|
||||
self._hotspot_thread = QThread()
|
||||
self._hotspot_worker = WifiHotspotWorker(self.wifi_manager, "close")
|
||||
self._hotspot_worker.moveToThread(self._hotspot_thread)
|
||||
self._hotspot_thread.started.connect(self._hotspot_worker.run)
|
||||
self._hotspot_worker.finished.connect(self._on_hotspot_close_finished)
|
||||
self._hotspot_worker.error.connect(self._on_hotspot_close_error)
|
||||
self._hotspot_worker.finished.connect(self._hotspot_thread.quit)
|
||||
self._hotspot_worker.error.connect(self._hotspot_thread.quit)
|
||||
self._hotspot_worker.finished.connect(self._hotspot_worker.deleteLater)
|
||||
self._hotspot_worker.error.connect(self._hotspot_worker.deleteLater)
|
||||
self._hotspot_thread.finished.connect(self._hotspot_thread.deleteLater)
|
||||
self._hotspot_thread.start()
|
||||
|
||||
def _on_hotspot_open_finished(self, ok, ssid):
|
||||
if ok:
|
||||
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, self.hotspot_password.text().strip())
|
||||
else:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "开启热点失败: wpa_cli 返回失败")
|
||||
self.hotspot_toggle.blockSignals(True)
|
||||
self.hotspot_toggle.setChecked(False)
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
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("开启热点后自动生成二维码")
|
||||
self.hotspot_toggle.setEnabled(True)
|
||||
|
||||
def _on_hotspot_open_error(self, err_msg):
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"开启热点失败: {err_msg}")
|
||||
self.hotspot_toggle.blockSignals(True)
|
||||
self.hotspot_toggle.setChecked(False)
|
||||
self.hotspot_toggle.blockSignals(False)
|
||||
self._apply_toggle_style(False)
|
||||
self.hotspot_toggle.setEnabled(True)
|
||||
|
||||
def _on_hotspot_close_finished(self, ok, _msg):
|
||||
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("开启热点后自动生成二维码")
|
||||
self.hotspot_toggle.setEnabled(True)
|
||||
|
||||
def _on_hotspot_close_error(self, err_msg):
|
||||
# 关闭失败仍尝试恢复UI
|
||||
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("开启热点后自动生成二维码")
|
||||
self.hotspot_toggle.setEnabled(True)
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", f"关闭热点时出现异常: {err_msg}")
|
||||
|
||||
def _generate_qr_code(self, ssid, password):
|
||||
"""生成 WiFi 二维码并显示"""
|
||||
@@ -943,7 +1085,7 @@ class SettingPage(QWidget):
|
||||
|
||||
self.saved_wifi_list.clear()
|
||||
for network in saved_networks:
|
||||
item_text = f"[{network.get('network_id', '-')}] {network.get('ssid', '<hidden>')} {network.get('flags', '')}"
|
||||
item_text = f"{network.get('ssid', '<hidden>')}"
|
||||
item = QListWidgetItem(item_text)
|
||||
item.setData(Qt.ItemDataRole.UserRole, network)
|
||||
self.saved_wifi_list.addItem(item)
|
||||
@@ -1016,7 +1158,11 @@ class SettingPage(QWidget):
|
||||
|
||||
ssid_label = QLabel(decoded_ssid)
|
||||
ssid_label.setStyleSheet("background: transparent; color: #f2f2f2; font-size: 18px;")
|
||||
signal_label = QLabel(f"{signal} dBm" if signal else "")
|
||||
try:
|
||||
signal = int(signal)
|
||||
except (ValueError, TypeError):
|
||||
signal = 0
|
||||
signal_label = QLabel(f"{signal} dBm" if signal < 0 else f"{signal}%")
|
||||
signal_label.setStyleSheet("background: transparent; color: #aaaaaa; font-size: 16px;")
|
||||
|
||||
item_layout.addWidget(ssid_label)
|
||||
@@ -1165,7 +1311,7 @@ class SettingPage(QWidget):
|
||||
self.refresh_current_status()
|
||||
|
||||
def connect_to_saved_wifi(self):
|
||||
"""连接已保存列表中选中的网络"""
|
||||
"""连接已保存列表中选中的网络(后台线程)"""
|
||||
item = self.saved_wifi_list.currentItem()
|
||||
if item is None:
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "请先选择一个已保存网络")
|
||||
@@ -1176,14 +1322,38 @@ class SettingPage(QWidget):
|
||||
if network_id is None:
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效")
|
||||
return
|
||||
try:
|
||||
ok = self.wifi_manager.connect_network_id(network_id)
|
||||
if not ok:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接请求下发失败")
|
||||
return
|
||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已发起连接: {ssid}")
|
||||
except Exception as e:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接失败: {str(e)}")
|
||||
|
||||
# 按钮UI反馈
|
||||
self.connect_saved_button.setEnabled(False)
|
||||
self.connect_saved_button.setText("连接中……")
|
||||
|
||||
self._saved_connect_thread = QThread()
|
||||
self._saved_connect_worker = WifiConnectSavedWorker(self.wifi_manager, network_id, ssid)
|
||||
self._saved_connect_worker.moveToThread(self._saved_connect_thread)
|
||||
self._saved_connect_thread.started.connect(self._saved_connect_worker.run)
|
||||
self._saved_connect_worker.finished.connect(self._on_saved_connect_finished)
|
||||
self._saved_connect_worker.error.connect(self._on_saved_connect_error)
|
||||
self._saved_connect_worker.finished.connect(self._saved_connect_thread.quit)
|
||||
self._saved_connect_worker.error.connect(self._saved_connect_thread.quit)
|
||||
self._saved_connect_worker.finished.connect(self._saved_connect_worker.deleteLater)
|
||||
self._saved_connect_worker.error.connect(self._saved_connect_worker.deleteLater)
|
||||
self._saved_connect_thread.finished.connect(self._saved_connect_thread.deleteLater)
|
||||
self._saved_connect_thread.start()
|
||||
|
||||
def _on_saved_connect_finished(self, ok, ssid):
|
||||
self.connect_saved_button.setEnabled(True)
|
||||
self.connect_saved_button.setText("连接到此网络")
|
||||
if ok:
|
||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已连接: {ssid}")
|
||||
else:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接失败")
|
||||
self.refresh_saved_wifi()
|
||||
self.refresh_current_status()
|
||||
|
||||
def _on_saved_connect_error(self, err_msg):
|
||||
self.connect_saved_button.setEnabled(True)
|
||||
self.connect_saved_button.setText("连接到此网络")
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接失败: {err_msg}")
|
||||
|
||||
def remove_selected_saved_wifi(self):
|
||||
item = self.saved_wifi_list.currentItem()
|
||||
@@ -1196,13 +1366,36 @@ class SettingPage(QWidget):
|
||||
if network_id is None:
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "提示", "选中网络无效,无法删除")
|
||||
return
|
||||
try:
|
||||
self.wifi_manager.remove_network(network_id)
|
||||
|
||||
# 按钮UI反馈
|
||||
self.remove_saved_button.setEnabled(False)
|
||||
self.remove_saved_button.setText("删除中……")
|
||||
|
||||
self._remove_thread = QThread()
|
||||
self._remove_worker = WifiRemoveWorker(self.wifi_manager, network_id, ssid)
|
||||
self._remove_worker.moveToThread(self._remove_thread)
|
||||
self._remove_thread.started.connect(self._remove_worker.run)
|
||||
self._remove_worker.finished.connect(self._on_remove_finished)
|
||||
self._remove_worker.error.connect(self._on_remove_error)
|
||||
self._remove_worker.finished.connect(self._remove_thread.quit)
|
||||
self._remove_worker.error.connect(self._remove_thread.quit)
|
||||
self._remove_worker.finished.connect(self._remove_worker.deleteLater)
|
||||
self._remove_worker.error.connect(self._remove_worker.deleteLater)
|
||||
self._remove_thread.finished.connect(self._remove_thread.deleteLater)
|
||||
self._remove_thread.start()
|
||||
|
||||
def _on_remove_finished(self, ok, ssid):
|
||||
self.remove_saved_button.setEnabled(True)
|
||||
self.remove_saved_button.setText("删除选中")
|
||||
if ok:
|
||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已删除网络: {ssid}")
|
||||
self.refresh_saved_wifi()
|
||||
self.refresh_current_status()
|
||||
except Exception as e:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"删除失败: {str(e)}")
|
||||
self.refresh_saved_wifi()
|
||||
self.refresh_current_status()
|
||||
|
||||
def _on_remove_error(self, err_msg):
|
||||
self.remove_saved_button.setEnabled(True)
|
||||
self.remove_saved_button.setText("删除选中")
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"删除失败: {err_msg}")
|
||||
|
||||
def connect_to_wifi(self):
|
||||
ssid = self.ssid_input.text().strip()
|
||||
@@ -1222,23 +1415,37 @@ class SettingPage(QWidget):
|
||||
self._styled_message(QMessageBox.Icon.Warning, self, "警告", "WPA-EAP 认证需要身份和密码")
|
||||
return
|
||||
|
||||
try:
|
||||
if auth_mode == "open":
|
||||
ok = self.wifi_manager.connect_wifi(ssid, None)
|
||||
elif auth_mode == "psk":
|
||||
ok = self.wifi_manager.connect_wifi(ssid, password)
|
||||
else:
|
||||
ok = self.wifi_manager.connect_eap(ssid, identity, password)
|
||||
# 按钮UI反馈
|
||||
self.connect_button.setEnabled(False)
|
||||
self.connect_button.setText("连接中……")
|
||||
|
||||
if not ok:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接请求下发失败,请检查系统日志")
|
||||
return
|
||||
self._connect_thread = QThread()
|
||||
self._connect_worker = WifiConnectWorker(self.wifi_manager, auth_mode, ssid, password, identity)
|
||||
self._connect_worker.moveToThread(self._connect_thread)
|
||||
self._connect_thread.started.connect(self._connect_worker.run)
|
||||
self._connect_worker.finished.connect(self._on_connect_finished)
|
||||
self._connect_worker.error.connect(self._on_connect_error)
|
||||
self._connect_worker.finished.connect(self._connect_thread.quit)
|
||||
self._connect_worker.error.connect(self._connect_thread.quit)
|
||||
self._connect_worker.finished.connect(self._connect_worker.deleteLater)
|
||||
self._connect_worker.error.connect(self._connect_worker.deleteLater)
|
||||
self._connect_thread.finished.connect(self._connect_thread.deleteLater)
|
||||
self._connect_thread.start()
|
||||
|
||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已发起连接: {ssid}")
|
||||
self.refresh_saved_wifi()
|
||||
self.refresh_current_status()
|
||||
except Exception as e:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接WiFi失败: {str(e)}")
|
||||
def _on_connect_finished(self, ok, ssid):
|
||||
self.connect_button.setEnabled(True)
|
||||
self.connect_button.setText("连接")
|
||||
if ok:
|
||||
self._styled_message(QMessageBox.Icon.Information, self, "成功", f"已连接: {ssid}")
|
||||
else:
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", "连接失败")
|
||||
self.refresh_saved_wifi()
|
||||
self.refresh_current_status()
|
||||
|
||||
def _on_connect_error(self, err_msg):
|
||||
self.connect_button.setEnabled(True)
|
||||
self.connect_button.setText("连接")
|
||||
self._styled_message(QMessageBox.Icon.Critical, self, "错误", f"连接WiFi失败: {err_msg}")
|
||||
|
||||
def display_setting(self, index):
|
||||
if index < 0:
|
||||
|
||||
@@ -7,18 +7,11 @@ from PyQt6.QtWidgets import (QWidget, QHBoxLayout, QVBoxLayout,
|
||||
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
|
||||
import sys
|
||||
import os
|
||||
from utils.gcode_viewer import GCodeViewerWidget
|
||||
|
||||
|
||||
def get_gcode_dir():
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config.get("GCODE_DIR", "/home/lhye200/.octoprint/uploads")
|
||||
except:
|
||||
return "/home/lhye200/.octoprint/uploads"
|
||||
|
||||
GCODE_DIR = get_gcode_dir()
|
||||
|
||||
# ── 状态主题色 ──────────────────────────────────────────
|
||||
STATUS_COLORS = {
|
||||
@@ -138,52 +131,6 @@ class TempGauge(QWidget):
|
||||
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):
|
||||
@@ -193,6 +140,7 @@ class StatusPage(QWidget):
|
||||
|
||||
self.file_name = "None"
|
||||
self.progress = 0.0
|
||||
self.filepos = 0
|
||||
self.display_name = "None"
|
||||
self.state = "Unknown"
|
||||
self.print_time = 0
|
||||
@@ -205,6 +153,7 @@ class StatusPage(QWidget):
|
||||
self.config_parser = ConfigParse()
|
||||
self.config_parser.config_changed.connect(self._on_config_changed)
|
||||
self.gcode_dir = self.config_parser.gcode_dir
|
||||
self._loaded_file = None
|
||||
|
||||
self.init_ui()
|
||||
self.timer = QTimer(self)
|
||||
@@ -222,6 +171,7 @@ class StatusPage(QWidget):
|
||||
job = data.get("job", {})
|
||||
self.file_name = job.get("job", {}).get("file", {}).get("name", "None")
|
||||
self.progress = job.get("progress", {}).get("completion", 0) or 0
|
||||
self.filepos = job.get("progress", {}).get("filepos", 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) or 0
|
||||
@@ -336,10 +286,8 @@ class StatusPage(QWidget):
|
||||
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)
|
||||
self.gcode_viewer = GCodeViewerWidget()
|
||||
right_layout.addWidget(self.gcode_viewer)
|
||||
|
||||
main_layout.addWidget(left_frame, 2)
|
||||
main_layout.addWidget(right_frame, 3)
|
||||
@@ -405,6 +353,22 @@ class StatusPage(QWidget):
|
||||
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)
|
||||
|
||||
# G-code 模型加载与进度更新
|
||||
if self.file_name and self.file_name != "None":
|
||||
if self.file_name != self._loaded_file:
|
||||
gcode_path = os.path.join(self.gcode_dir, self.file_name)
|
||||
if os.path.exists(gcode_path):
|
||||
try:
|
||||
self.gcode_viewer.load_gcode(gcode_path)
|
||||
self._loaded_file = self.file_name
|
||||
except Exception as e:
|
||||
print("Failed to load G-code:", e)
|
||||
|
||||
# 使用 filepos 替代进度百分比进行精准的偏移量层级更新
|
||||
if self._loaded_file == self.file_name:
|
||||
is_printing = self.state.startswith("Printing") or self.state.startswith("Paused")
|
||||
self.gcode_viewer.update_by_filepos(self.filepos, is_printing)
|
||||
|
||||
|
||||
|
||||
#TODO: Better Gcode Parser, this one is too slow for large files, need to optimize or use a separate thread to load
|
||||
|
||||
53
refer/test.py
Normal file
53
refer/test.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QCheckBox, QSlider
|
||||
from PyQt6.QtCore import Qt
|
||||
from gcode_viewer import GCodeViewerWidget
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("G-code 预览")
|
||||
self.viewer = GCodeViewerWidget()
|
||||
|
||||
# 加载文件(请替换为你的 G-code 路径)
|
||||
self.viewer.load_gcode("/home/lhye200/.octoprint/uploads/20260508141659_085359c9908947bebcaa0fe7490641e8.gcode")
|
||||
|
||||
# 进度滑块(0~100%)
|
||||
self.slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.slider.setRange(0, 100)
|
||||
self.slider.setValue(100)
|
||||
self.slider.valueChanged.connect(
|
||||
lambda v: self.viewer.update_processes(v / 100.0))
|
||||
|
||||
# 类型开关
|
||||
self.chk_support = QCheckBox("显示支撑")
|
||||
self.chk_support.setChecked(True)
|
||||
self.chk_support.toggled.connect(
|
||||
lambda v: self.viewer.update_switch('SUPPORT', v))
|
||||
|
||||
self.chk_infill = QCheckBox("显示填充")
|
||||
self.chk_infill.setChecked(True)
|
||||
self.chk_infill.toggled.connect(
|
||||
lambda v: self.viewer.update_switch('FILL', v))
|
||||
|
||||
self.chk_perimeter = QCheckBox("显示外壳")
|
||||
self.chk_perimeter.setChecked(True)
|
||||
self.chk_perimeter.toggled.connect(
|
||||
lambda v: self.viewer.update_switch('WALL-OUTER', v))
|
||||
self.chk_perimeter.toggled.connect(
|
||||
lambda v: self.viewer.update_switch('WALL-INNER', v))
|
||||
|
||||
central = QWidget()
|
||||
layout = QVBoxLayout(central)
|
||||
layout.addWidget(self.viewer, 1)
|
||||
layout.addWidget(self.slider)
|
||||
layout.addWidget(self.chk_support)
|
||||
layout.addWidget(self.chk_infill)
|
||||
layout.addWidget(self.chk_perimeter)
|
||||
self.setCentralWidget(central)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
win = MainWindow()
|
||||
win.show()
|
||||
sys.exit(app.exec())
|
||||
14
refer/test_line_width.py
Normal file
14
refer/test_line_width.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import OpenGL.GL as gl
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||
|
||||
class View(QOpenGLWidget):
|
||||
def initializeGL(self):
|
||||
v = gl.glGetFloatv(gl.GL_ALIASED_LINE_WIDTH_RANGE)
|
||||
print("Line width range:", v)
|
||||
QApplication.instance().quit()
|
||||
|
||||
app = QApplication([])
|
||||
v = View()
|
||||
v.show()
|
||||
app.exec()
|
||||
13
refer/test_linewidth.py
Normal file
13
refer/test_linewidth.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||
class W(QOpenGLWidget):
|
||||
def initializeGL(self):
|
||||
import OpenGL.GL as gl
|
||||
bounds = gl.glGetFloatv(gl.GL_ALIASED_LINE_WIDTH_RANGE)
|
||||
print("Line width range:", bounds)
|
||||
sys.exit(0)
|
||||
app = QApplication([])
|
||||
w = W()
|
||||
w.show()
|
||||
app.exec()
|
||||
15
refer/test_qmat.py
Normal file
15
refer/test_qmat.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from PyQt6.QtGui import QMatrix4x4
|
||||
import numpy as np
|
||||
|
||||
mat = QMatrix4x4()
|
||||
mat.translate(0.0, 0.0, -250.0)
|
||||
mat.rotate(30.0, 1.0, 0.0, 0.0)
|
||||
mat.rotate(45.0, 0.0, 0.0, 1.0)
|
||||
|
||||
# If QMatrix4x4 passes list to uniformMatrix4fv, does it need column-major or row-major?
|
||||
# When I used numpy: ``mvp.flatten().tolist()`` -> it gave row-major. And it WORKED.
|
||||
# Let's check QMatrix4x4 data.
|
||||
data = mat.data()
|
||||
print("QMatrix4x4 data (length %d):" % len(data))
|
||||
print(data)
|
||||
|
||||
25
refer/test_rot.py
Normal file
25
refer/test_rot.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import numpy as np
|
||||
import math
|
||||
|
||||
rx = math.radians(-60.0)
|
||||
rz = math.radians(45.0)
|
||||
cosX, sinX = math.cos(rx), math.sin(rx)
|
||||
cosZ, sinZ = math.cos(rz), math.sin(rz)
|
||||
# Original code rotation matrix
|
||||
rot = np.array([
|
||||
[cosZ, -sinZ*cosX, sinZ*sinX, 0],
|
||||
[sinZ, cosZ*cosX, -cosZ*sinX, 0],
|
||||
[0, sinX, cosX, 0],
|
||||
[0, 0, 0, 1]
|
||||
])
|
||||
print("Original rot:\\n", rot)
|
||||
|
||||
# Correct orbit Rx @ Rz
|
||||
rot_correct = np.array([
|
||||
[cosZ, -sinZ, 0, 0],
|
||||
[cosX*sinZ, cosX*cosZ, -sinX, 0],
|
||||
[sinX*sinZ, sinX*cosZ, cosX, 0],
|
||||
[0, 0, 0, 1]
|
||||
])
|
||||
print("Rx @ Rz:\\n", rot_correct)
|
||||
|
||||
4
run.sh
4
run.sh
@@ -20,6 +20,6 @@ export QT_QPA_PLATFORM=eglfs
|
||||
|
||||
# echo "Starting Printer Screen Menu..."
|
||||
# 启动界面
|
||||
python "$(dirname "$0")/main.py" > /dev/null 2>&1
|
||||
# python "$(dirname "$0")/main.py"
|
||||
# python "$(dirname "$0")/main.py" > /dev/null 2>&1
|
||||
python "$(dirname "$0")/main.py"
|
||||
# .venv/bin/python main.py
|
||||
|
||||
@@ -17,58 +17,58 @@ class AIOPrrintSystemAPI:
|
||||
|
||||
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': '20260508141659_085359c9908947bebcaa0fe7490641e8.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")
|
||||
@@ -83,7 +83,7 @@ class AIOPrrintSystemAPI:
|
||||
return self._post_action("auto_leveling")
|
||||
|
||||
def send_gcode(self, gcode):
|
||||
return self._post_action("send_gcode", gcode=gcode)
|
||||
return self._post_action("send_gcode", commands=gcode)
|
||||
|
||||
def off_motors(self):
|
||||
return self.send_gcode("M84")
|
||||
|
||||
@@ -5,6 +5,7 @@ from PyQt6.QtCore import QTimer
|
||||
class AutoFanStatus:
|
||||
def __init__(self, update_interval_ms=1000):
|
||||
self.cpu_temp = 0.0
|
||||
self.cpu_load = 0.0 # 1 分钟 CPU 负载
|
||||
self.fan_speed = 0
|
||||
self.fan_state = "Unknown"
|
||||
self.fan_rpm = 0
|
||||
@@ -37,4 +38,12 @@ class AutoFanStatus:
|
||||
self.fan_speed = 0
|
||||
self.fan_state = "Unknown"
|
||||
self.fan_rpm = 0
|
||||
self.is_auto_fan_service_running = False
|
||||
self.is_auto_fan_service_running = False
|
||||
|
||||
# 读取 CPU 负载(始终执行)
|
||||
try:
|
||||
with open("/proc/loadavg", "r") as f:
|
||||
fields = f.read().split()
|
||||
self.cpu_load = float(fields[0]) # 1 分钟平均负载
|
||||
except (OSError, IndexError, ValueError):
|
||||
self.cpu_load = 0.0
|
||||
@@ -42,8 +42,6 @@ class ConfigParse(QObject):
|
||||
self._last_mtime = mtime
|
||||
except OSError:
|
||||
return
|
||||
|
||||
print("Config changed")
|
||||
|
||||
old_config = self.config
|
||||
new_config = self._load_config()
|
||||
@@ -68,4 +66,5 @@ class ConfigParse(QObject):
|
||||
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.home_positions = self.config.get("home_positions", {"x": 134, "y": 123, "z": 10})
|
||||
self.move_max_speed = self.config.get("move_max_speed", None)
|
||||
574
utils/gcode_viewer.py
Normal file
574
utils/gcode_viewer.py
Normal file
@@ -0,0 +1,574 @@
|
||||
import numpy as np
|
||||
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
|
||||
from PyQt6.QtCore import Qt, QEvent, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QTouchEvent, QSurfaceFormat
|
||||
from PyQt6.QtOpenGL import QOpenGLShaderProgram, QOpenGLShader, QOpenGLBuffer
|
||||
|
||||
class GCodeParseWorker(QThread):
|
||||
finished = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, filepath, type_map, default_colors, parent=None):
|
||||
super().__init__(parent)
|
||||
self.filepath = filepath
|
||||
self.TYPE_MAP = type_map
|
||||
self.DEFAULT_COLORS = default_colors
|
||||
|
||||
def run(self):
|
||||
points = []
|
||||
colors = []
|
||||
type_segments = {}
|
||||
type_visibility = {}
|
||||
|
||||
x = y = z = e = 0.0
|
||||
vertex_idx = 0
|
||||
feature_type = 'OTHER'
|
||||
current_segment_type = 'OTHER'
|
||||
segment_start = 0
|
||||
type_visibility['TRAVEL'] = False
|
||||
relative_e = False
|
||||
|
||||
min_x = min_y = min_z = float('inf')
|
||||
max_x = max_y = max_z = float('-inf')
|
||||
|
||||
current_offset = 0
|
||||
max_z_seen = -999.0
|
||||
layer_map = [(0, 0)]
|
||||
|
||||
def add_segment(t_name, start, length):
|
||||
if length > 0:
|
||||
type_segments.setdefault(t_name, []).append((start, length))
|
||||
|
||||
try:
|
||||
with open(self.filepath, 'rb') as f:
|
||||
for line_bytes in f:
|
||||
current_offset += len(line_bytes)
|
||||
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith('M82'):
|
||||
relative_e = False
|
||||
continue
|
||||
if line.startswith('M83'):
|
||||
relative_e = True
|
||||
continue
|
||||
|
||||
if line.startswith(';'):
|
||||
if line.startswith(';TYPE:'):
|
||||
raw_type = line.split(':', 1)[1].strip()
|
||||
else:
|
||||
raw_type = line[1:].strip()
|
||||
if raw_type not in self.TYPE_MAP and not any(k in raw_type.lower() for k in ('perimeter', 'infill', 'material', 'skirt/brim')):
|
||||
continue
|
||||
if 'Skirt/Brim' in raw_type:
|
||||
raw_type = 'Skirt'
|
||||
|
||||
new_type = self.TYPE_MAP.get(raw_type, 'OTHER')
|
||||
if new_type != feature_type:
|
||||
feature_type = new_type
|
||||
if feature_type not in type_visibility:
|
||||
type_visibility[feature_type] = True
|
||||
continue
|
||||
|
||||
if line.startswith(('G0', 'G1')):
|
||||
new_x, new_y, new_z = x, y, z
|
||||
e_val = 0.0
|
||||
has_e = False
|
||||
parts = line.split(';')[0].split()
|
||||
for p in parts[1:]:
|
||||
try:
|
||||
if p.startswith('X'): new_x = float(p[1:])
|
||||
elif p.startswith('Y'): new_y = float(p[1:])
|
||||
elif p.startswith('Z'): new_z = float(p[1:])
|
||||
elif p.startswith('E'):
|
||||
e_val = float(p[1:])
|
||||
has_e = True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if new_x == x and new_y == y and new_z == z:
|
||||
if has_e:
|
||||
if relative_e: e += e_val
|
||||
else: e = e_val
|
||||
continue
|
||||
|
||||
is_extrusion = False
|
||||
if has_e:
|
||||
if relative_e:
|
||||
is_extrusion = e_val > 0
|
||||
new_e = e + e_val
|
||||
else:
|
||||
is_extrusion = e_val > e
|
||||
new_e = e_val
|
||||
else:
|
||||
new_e = e
|
||||
|
||||
if is_extrusion:
|
||||
seg_type = feature_type
|
||||
c = self.DEFAULT_COLORS.get(feature_type, self.DEFAULT_COLORS['OTHER'])
|
||||
else:
|
||||
seg_type = 'TRAVEL'
|
||||
c = self.DEFAULT_COLORS['TRAVEL']
|
||||
if 'TRAVEL' not in type_visibility:
|
||||
type_visibility['TRAVEL'] = False
|
||||
|
||||
if seg_type != current_segment_type:
|
||||
if vertex_idx > segment_start:
|
||||
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
||||
current_segment_type = seg_type
|
||||
segment_start = vertex_idx
|
||||
|
||||
if new_x < min_x: min_x = new_x
|
||||
if new_x > max_x: max_x = new_x
|
||||
if new_y < min_y: min_y = new_y
|
||||
if new_y > max_y: max_y = new_y
|
||||
if new_z < min_z: min_z = new_z
|
||||
if new_z > max_z: max_z = new_z
|
||||
|
||||
if new_z > max_z_seen and is_extrusion:
|
||||
max_z_seen = new_z
|
||||
layer_map.append((current_offset, vertex_idx))
|
||||
|
||||
points.extend([x, y, z, new_x, new_y, new_z])
|
||||
colors.extend([*c, *c])
|
||||
vertex_idx += 2
|
||||
x, y, z, e = new_x, new_y, new_z, new_e
|
||||
elif line.startswith('G92'):
|
||||
parts = line.split(';')[0].split()
|
||||
for p in parts[1:]:
|
||||
try:
|
||||
if p.startswith('E'): e = float(p[1:])
|
||||
elif p.startswith('X'): x = float(p[1:])
|
||||
elif p.startswith('Y'): y = float(p[1:])
|
||||
elif p.startswith('Z'): z = float(p[1:])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if vertex_idx > segment_start:
|
||||
add_segment(current_segment_type, segment_start, vertex_idx - segment_start)
|
||||
|
||||
cx = (min_x + max_x) / 2.0 if max_x >= min_x else 110.0
|
||||
cy = (min_y + max_y) / 2.0 if max_y >= min_y else 110.0
|
||||
cz = (min_z + max_z) / 2.0 if max_z >= min_z else 0.0
|
||||
|
||||
result = {
|
||||
'vertices': np.array(points, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
||||
'colors': np.array(colors, dtype=np.float32) if vertex_idx > 0 else np.zeros((0,), dtype=np.float32),
|
||||
'vertex_count': vertex_idx,
|
||||
'center_x': cx,
|
||||
'center_y': cy,
|
||||
'center_z': cz,
|
||||
'type_segments': type_segments,
|
||||
'type_visibility': type_visibility,
|
||||
'layer_map': layer_map
|
||||
}
|
||||
self.finished.emit(result)
|
||||
except Exception as e:
|
||||
print("ParseGCode Error:", e)
|
||||
self.finished.emit({})
|
||||
|
||||
class GCodeViewerWidget(QOpenGLWidget):
|
||||
"""
|
||||
3D G-code 预览控件(OpenGL ES / eglfs 兼容版)
|
||||
使用可编程管线替代固定管线,适配树莓派 4B
|
||||
"""
|
||||
|
||||
# PrusaSlicer 类型映射、颜色定义等保持不变
|
||||
TYPE_MAP = {
|
||||
'External perimeter': 'WALL-OUTER',
|
||||
'Perimeter': 'WALL-INNER',
|
||||
'Overhang perimeter': 'WALL-OUTER',
|
||||
'Solid infill': 'SKIN',
|
||||
'Top solid infill': 'SKIN',
|
||||
'Bridge infill': 'SKIN',
|
||||
'Internal infill': 'FILL',
|
||||
'Support material': 'SUPPORT',
|
||||
'Support material interface': 'SUPPORT-INTERFACE',
|
||||
'Skirt': 'SKIRT',
|
||||
'Brim': 'SKIRT',
|
||||
'Custom': 'OTHER',
|
||||
}
|
||||
|
||||
DEFAULT_COLORS = {
|
||||
'WALL-OUTER': (0.92, 0.55, 0.22), # 0xeb8b38
|
||||
'WALL-INNER': (0.25, 0.50, 0.81), # 0x4080cf
|
||||
'FILL': (0.80, 0.75, 0.29), # 0xccc04b
|
||||
'SKIN': (0.62, 0.38, 0.70), # 0x9e60b3
|
||||
'SUPPORT': (0.34, 0.70, 0.34), # 0x57b357
|
||||
'SUPPORT-INTERFACE': (0.17, 0.42, 0.17), # 0x2b6b2b
|
||||
'SKIRT': (0.00, 1.00, 1.00), # 0x00ffff
|
||||
'OTHER': (0.67, 0.67, 0.67), # 0xaaaaaa
|
||||
'TRAVEL': (0.25, 0.31, 0.38), # 0x405060
|
||||
}
|
||||
|
||||
# 顶点着色器(GLSL ES 1.00)
|
||||
VERTEX_SHADER = """
|
||||
attribute vec3 aPos;
|
||||
attribute vec3 aColor;
|
||||
varying vec3 vColor;
|
||||
uniform mat4 uMVP;
|
||||
void main() {
|
||||
gl_Position = uMVP * vec4(aPos, 1.0);
|
||||
vColor = aColor;
|
||||
}
|
||||
"""
|
||||
|
||||
# 片段着色器(添加可调节颜色深度的 uniform 以实现边界加深)
|
||||
FRAGMENT_SHADER = """
|
||||
precision mediump float;
|
||||
varying vec3 vColor;
|
||||
uniform float uDarken;
|
||||
void main() {
|
||||
gl_FragColor = vec4(vColor * uDarken, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
|
||||
|
||||
# 请求 OpenGL ES 2.0 上下文
|
||||
fmt = QSurfaceFormat()
|
||||
fmt.setRenderableType(QSurfaceFormat.RenderableType.OpenGLES)
|
||||
fmt.setVersion(2, 0)
|
||||
|
||||
super().__init__(parent)
|
||||
self.setMinimumSize(400, 300)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_AcceptTouchEvents, True)
|
||||
self.setFormat(fmt)
|
||||
# 数据缓冲区
|
||||
self.vertices = None
|
||||
self.colors = None
|
||||
self.vertex_count = 0
|
||||
self.vbo_vertices = None
|
||||
self.vbo_colors = None
|
||||
self.vbo_ready = False
|
||||
|
||||
# 类型分段
|
||||
self.type_segments = {}
|
||||
self.type_visibility = {}
|
||||
self.current_type = 'OTHER'
|
||||
|
||||
# 视角参数
|
||||
self.view_rot_x = -60.0
|
||||
self.view_rot_z = 45.0
|
||||
self.view_zoom = -250.0 # 稍微退后一点以便看到整个打印平台
|
||||
self.view_trans_x = 0.0
|
||||
self.view_trans_y = 0.0
|
||||
|
||||
# ...
|
||||
self.progress_ratio = 1.0
|
||||
self.progress_vertices = 0
|
||||
|
||||
# 按需渲染:缓存当前的 filepos 对应的顶点数
|
||||
self.layer_map = [(0, 0)] # (offset_bytes, vertex_idx)
|
||||
self.last_reported_filepos = -1
|
||||
|
||||
# 模型中心点
|
||||
self.center_x = 110.0
|
||||
self.center_y = 110.0
|
||||
self.center_z = 0.0
|
||||
|
||||
# 触摸状态
|
||||
self.last_mouse_pos = None
|
||||
self._touch_points = {}
|
||||
self._pinch_start_dist = 0.0
|
||||
self._pinch_start_zoom = 0.0
|
||||
self._pinch_start_center = None
|
||||
self._pinch_start_trans = (0.0, 0.0)
|
||||
self._ignore_wheel = False
|
||||
|
||||
# 着色器程序
|
||||
self.shader_program = None
|
||||
self.aPos_location = None
|
||||
self.aColor_location = None
|
||||
self.uMVP_location = None
|
||||
|
||||
# ── 公开接口 ──
|
||||
def load_gcode(self, filepath: str):
|
||||
if hasattr(self, '_worker') and self._worker.isRunning():
|
||||
self._worker.terminate()
|
||||
self._worker.wait()
|
||||
|
||||
self._worker = GCodeParseWorker(filepath, self.TYPE_MAP, self.DEFAULT_COLORS)
|
||||
self._worker.finished.connect(self._on_parse_finished)
|
||||
self._worker.start()
|
||||
|
||||
def _on_parse_finished(self, result: dict):
|
||||
if not result:
|
||||
return
|
||||
|
||||
self.vertices = result['vertices']
|
||||
self.colors = result['colors']
|
||||
self.vertex_count = result['vertex_count']
|
||||
self.center_x = result['center_x']
|
||||
self.center_y = result['center_y']
|
||||
self.center_z = result['center_z']
|
||||
self.type_segments = result['type_segments']
|
||||
self.type_visibility = result['type_visibility']
|
||||
self.layer_map = result['layer_map']
|
||||
|
||||
self.vbo_ready = False
|
||||
# 初始时先不显示,让 update_by_filepos 决定,或者如果是0则自动更新
|
||||
# 这里把 progress_vertices 赋予当前的 target
|
||||
target = 0
|
||||
if self.layer_map:
|
||||
target = self.layer_map[-1][1] # 全显
|
||||
|
||||
self.progress_vertices = target
|
||||
self.last_reported_filepos = -1
|
||||
self.update()
|
||||
|
||||
def update_processes(self, progress: float):
|
||||
pass
|
||||
|
||||
def update_by_filepos(self, filepos: int, is_printing: bool = True):
|
||||
import bisect
|
||||
if not hasattr(self, 'layer_map') or not self.layer_map:
|
||||
return
|
||||
|
||||
if not is_printing:
|
||||
# 不在打印途中时,渲染包含所有顶点(完整模型)
|
||||
target_vertices = self.vertex_count
|
||||
else:
|
||||
# 使用二分查找快速定位当前 filepos 对应的最多展示顶点数
|
||||
keys = [item[0] for item in self.layer_map]
|
||||
idx = bisect.bisect_right(keys, filepos)
|
||||
target_vertices = 0 if idx == 0 else self.layer_map[idx-1][1]
|
||||
|
||||
# 按需渲染:只有当层级(计算出的可见顶点数)真正发生跳变时才调用 update()
|
||||
if target_vertices != getattr(self, 'progress_vertices', -1):
|
||||
self.progress_vertices = target_vertices
|
||||
self.update()
|
||||
|
||||
def update_switch(self, type_name: str, visible: bool):
|
||||
if type_name in self.type_visibility:
|
||||
self.type_visibility[type_name] = visible
|
||||
self.update()
|
||||
|
||||
def set_view_angles(self, rot_x: float, rot_z: float, zoom: float = None):
|
||||
self.view_rot_x = max(-90.0, min(0.0, rot_x))
|
||||
self.view_rot_z = rot_z
|
||||
if zoom is not None:
|
||||
self.view_zoom = zoom
|
||||
self.update()
|
||||
|
||||
if length == 0:
|
||||
return
|
||||
self.type_segments.setdefault(type_name, []).append((start, length))
|
||||
|
||||
# ── OpenGL 可编程管线初始化 ──
|
||||
def initializeGL(self):
|
||||
import OpenGL.GL as gl
|
||||
gl.glClearColor(0.15, 0.15, 0.15, 1.0)
|
||||
gl.glEnable(gl.GL_DEPTH_TEST)
|
||||
gl.glLineWidth(1.0)
|
||||
|
||||
# 编译着色器
|
||||
self.shader_program = QOpenGLShaderProgram()
|
||||
self.shader_program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Vertex, self.VERTEX_SHADER)
|
||||
self.shader_program.addShaderFromSourceCode(QOpenGLShader.ShaderTypeBit.Fragment, self.FRAGMENT_SHADER)
|
||||
self.shader_program.link()
|
||||
|
||||
# 获取属性和 uniform 位置
|
||||
self.aPos_location = self.shader_program.attributeLocation("aPos")
|
||||
self.aColor_location = self.shader_program.attributeLocation("aColor")
|
||||
self.uMVP_location = self.shader_program.uniformLocation("uMVP")
|
||||
self.uDarken_location = self.shader_program.uniformLocation("uDarken")
|
||||
|
||||
# 创建缓冲对象
|
||||
self.vbo_vertices = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer)
|
||||
self.vbo_colors = QOpenGLBuffer(QOpenGLBuffer.Type.VertexBuffer)
|
||||
|
||||
def resizeGL(self, w, h):
|
||||
import OpenGL.GL as gl
|
||||
gl.glViewport(0, 0, w, h)
|
||||
|
||||
# ── 构建 MVP 矩阵(替代 glOrtho/glRotate) ──
|
||||
def _build_mvp(self):
|
||||
from PyQt6.QtGui import QMatrix4x4
|
||||
mat = QMatrix4x4()
|
||||
aspect = self.width() / self.height() if self.height() else 1
|
||||
mat.perspective(45.0, aspect, 1.0, 1000.0)
|
||||
|
||||
mat.translate(self.view_trans_x, self.view_trans_y, self.view_zoom)
|
||||
mat.rotate(self.view_rot_x, 1.0, 0.0, 0.0)
|
||||
mat.rotate(self.view_rot_z, 0.0, 0.0, 1.0)
|
||||
mat.translate(-self.center_x, -self.center_y, -self.center_z)
|
||||
|
||||
return mat
|
||||
|
||||
# ── 渲染 ──
|
||||
def paintGL(self):
|
||||
import OpenGL.GL as gl
|
||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
if self.vertices is None or self.vertex_count == 0:
|
||||
return
|
||||
|
||||
if not self.vbo_ready:
|
||||
self._create_vbos()
|
||||
self.vbo_ready = True
|
||||
|
||||
# 使用着色器程序
|
||||
self.shader_program.bind()
|
||||
|
||||
# 设置 MVP 矩阵
|
||||
import math
|
||||
from PyQt6.QtGui import QMatrix4x4
|
||||
mvp_mat = self._build_mvp()
|
||||
self.shader_program.setUniformValue(self.uMVP_location, mvp_mat)
|
||||
|
||||
# 绑定 VBO
|
||||
self.vbo_vertices.bind()
|
||||
self.shader_program.setAttributeBuffer(self.aPos_location, gl.GL_FLOAT, 0, 3, 0)
|
||||
self.shader_program.enableAttributeArray(self.aPos_location)
|
||||
|
||||
self.vbo_colors.bind()
|
||||
self.shader_program.setAttributeBuffer(self.aColor_location, gl.GL_FLOAT, 0, 3, 0)
|
||||
self.shader_program.enableAttributeArray(self.aColor_location)
|
||||
|
||||
# 允许 z-fighting 覆盖,用于同一位置多次渲染线条
|
||||
gl.glDepthFunc(gl.GL_LEQUAL)
|
||||
|
||||
# 渲染两次:一次绘制加粗加深的边界底线,一次绘制正常宽度的原色骨架线
|
||||
# (在树莓派等性能有限的平台上,使用真实的3D圆柱/方块代替线条会导致顶点数暴增十几倍直接卡顿,
|
||||
# 因此通过动态加粗像素级线宽来性能无损地模拟出“体积感”)
|
||||
for pass_idx in range(2):
|
||||
if pass_idx == 0:
|
||||
gl.glLineWidth(6.0) # 底线宽度(加大以模拟体积轮廓)
|
||||
self.shader_program.setUniformValue(self.uDarken_location, 0.8) # 加深颜色至 40% 亮度
|
||||
else:
|
||||
gl.glLineWidth(3.0) # 主体宽度(加大以模拟线条厚度)
|
||||
self.shader_program.setUniformValue(self.uDarken_location, 1.0) # 保持原色
|
||||
|
||||
# 按类型分段绘制
|
||||
for type_name, segments in self.type_segments.items():
|
||||
if not self.type_visibility.get(type_name, True):
|
||||
continue
|
||||
for start, length in segments:
|
||||
if start >= self.progress_vertices:
|
||||
continue
|
||||
end = start + length
|
||||
visible_start = start
|
||||
visible_count = length
|
||||
if end > self.progress_vertices:
|
||||
visible_count = self.progress_vertices - start
|
||||
if visible_count > 0:
|
||||
gl.glDrawArrays(gl.GL_LINES, visible_start, visible_count)
|
||||
|
||||
# 恢复默认深度测试模式
|
||||
gl.glDepthFunc(gl.GL_LESS)
|
||||
|
||||
self.shader_program.disableAttributeArray(self.aPos_location)
|
||||
self.shader_program.disableAttributeArray(self.aColor_location)
|
||||
self.vbo_vertices.release()
|
||||
self.vbo_colors.release()
|
||||
self.shader_program.release()
|
||||
|
||||
def _create_vbos(self):
|
||||
if self.vbo_vertices.isCreated():
|
||||
self.vbo_vertices.destroy()
|
||||
if self.vbo_colors.isCreated():
|
||||
self.vbo_colors.destroy()
|
||||
|
||||
self.vbo_vertices.create()
|
||||
self.vbo_vertices.bind()
|
||||
self.vbo_vertices.allocate(self.vertices.tobytes(), self.vertices.nbytes)
|
||||
self.vbo_vertices.release()
|
||||
|
||||
self.vbo_colors.create()
|
||||
self.vbo_colors.bind()
|
||||
self.vbo_colors.allocate(self.colors.tobytes(), self.colors.nbytes)
|
||||
self.vbo_colors.release()
|
||||
|
||||
# ── 触摸/鼠标交互(完全不变) ──
|
||||
def mousePressEvent(self, event):
|
||||
self.last_mouse_pos = event.position()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self.last_mouse_pos is None:
|
||||
return
|
||||
dx = event.position().x() - self.last_mouse_pos.x()
|
||||
dy = event.position().y() - self.last_mouse_pos.y()
|
||||
if event.buttons() & Qt.MouseButton.LeftButton:
|
||||
self.view_rot_x += dy * 0.5
|
||||
self.view_rot_x = max(-90.0, min(0.0, self.view_rot_x)) # 限制垂直视角的翻转
|
||||
self.view_rot_z += dx * 0.5
|
||||
self.last_mouse_pos = event.position()
|
||||
self.update()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
delta = event.angleDelta().y() / 120
|
||||
self.view_zoom += delta * 10
|
||||
self.update()
|
||||
|
||||
def event(self, e):
|
||||
if e.type() in (QEvent.Type.TouchBegin, QEvent.Type.TouchUpdate, QEvent.Type.TouchEnd):
|
||||
self._ignore_wheel = True
|
||||
self.touchEvent(e)
|
||||
return True
|
||||
elif e.type() == QEvent.Type.Wheel:
|
||||
if self._ignore_wheel:
|
||||
self._ignore_wheel = False
|
||||
return True
|
||||
return super().event(e)
|
||||
|
||||
def touchEvent(self, event: QTouchEvent):
|
||||
points = event.points()
|
||||
if not points:
|
||||
return
|
||||
|
||||
if event.type() == QEvent.Type.TouchEnd:
|
||||
self._touch_points.clear()
|
||||
self._pinch_start_center = None
|
||||
self._pinch_start_dist = 0.0
|
||||
event.accept()
|
||||
return
|
||||
|
||||
if len(points) == 1:
|
||||
p = points[0]
|
||||
# 如果是刚检测到单指,或者从双指变回单指
|
||||
if p.id() not in self._touch_points or self._pinch_start_center is not None:
|
||||
self._touch_points.clear()
|
||||
self._touch_points[p.id()] = p.position()
|
||||
self._pinch_start_center = None
|
||||
else:
|
||||
last = self._touch_points[p.id()]
|
||||
dx = p.position().x() - last.x()
|
||||
dy = p.position().y() - last.y()
|
||||
self.view_rot_x += dy * 0.5
|
||||
self.view_rot_x = max(-90.0, min(0.0, self.view_rot_x)) # 限制垂直视角的翻转
|
||||
self.view_rot_z += dx * 0.5
|
||||
self._touch_points[p.id()] = p.position()
|
||||
self.update()
|
||||
|
||||
elif len(points) == 2:
|
||||
p1, p2 = points[0], points[1]
|
||||
dx_p = p1.position().x() - p2.position().x()
|
||||
dy_p = p1.position().y() - p2.position().y()
|
||||
dist = (dx_p**2 + dy_p**2)**0.5
|
||||
center_x = (p1.position().x() + p2.position().x()) / 2.0
|
||||
center_y = (p1.position().y() + p2.position().y()) / 2.0
|
||||
|
||||
# 初始化双指状态
|
||||
if self._pinch_start_center is None:
|
||||
self._pinch_start_dist = dist
|
||||
self._pinch_start_zoom = self.view_zoom
|
||||
self._pinch_start_center = (center_x, center_y)
|
||||
self._pinch_start_trans = (self.view_trans_x, self.view_trans_y)
|
||||
self._touch_points.clear() # 清除单指记录
|
||||
else:
|
||||
# 缩放 (双指捏合)
|
||||
if self._pinch_start_dist > 0:
|
||||
scale = dist / self._pinch_start_dist
|
||||
self.view_zoom = self._pinch_start_zoom * (1 / scale)
|
||||
|
||||
# 平移 (双指并行移动)
|
||||
dcx = center_x - self._pinch_start_center[0]
|
||||
dcy = center_y - self._pinch_start_center[1]
|
||||
pan_speed = abs(self.view_zoom) * 0.002
|
||||
self.view_trans_x = self._pinch_start_trans[0] + dcx * pan_speed
|
||||
self.view_trans_y = self._pinch_start_trans[1] - dcy * pan_speed
|
||||
|
||||
self.update()
|
||||
event.accept()
|
||||
@@ -3,8 +3,9 @@ import time
|
||||
import re
|
||||
|
||||
class WifiManager:
|
||||
def __init__(self, interface="wlan0"):
|
||||
def __init__(self, interface="wlan0", backend="nmcli"):
|
||||
self.interface = interface
|
||||
self.backend = backend # "nmcli" or "wpa_cli"
|
||||
|
||||
def _run_wpa_cli(self, *args):
|
||||
"""执行 wpa_cli 命令"""
|
||||
@@ -16,128 +17,209 @@ class WifiManager:
|
||||
print(f"执行 wpa_cli {args} 失败: {e.stderr}")
|
||||
return ""
|
||||
|
||||
def _run_nmcli(self, *args):
|
||||
"""执行 nmcli 命令"""
|
||||
cmd = ["nmcli"] + list(args)
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"执行 nmcli {args} 失败: {e.stderr}")
|
||||
return ""
|
||||
|
||||
def list_saved_networks(self):
|
||||
"""列出已保存的 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('\t')
|
||||
if len(parts) >= 4:
|
||||
if self.backend == "wpa_cli":
|
||||
output = self._run_wpa_cli("list_networks")
|
||||
networks = []
|
||||
lines = output.splitlines()
|
||||
if len(lines) > 1:
|
||||
for line in lines[1:]:
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 4:
|
||||
networks.append({
|
||||
"network_id": parts[0],
|
||||
"ssid": parts[1],
|
||||
"bssid": parts[2],
|
||||
"flags": parts[3]
|
||||
})
|
||||
return networks
|
||||
else:
|
||||
output = self._run_nmcli("-t", "-f", "UUID,NAME,TYPE", "connection", "show")
|
||||
networks = []
|
||||
for line in output.splitlines():
|
||||
parts = line.split(':')
|
||||
# UUID:NAME:TYPE
|
||||
if len(parts) >= 3 and "wireless" in parts[-1]:
|
||||
networks.append({
|
||||
"network_id": parts[0],
|
||||
"ssid": parts[1],
|
||||
"bssid": parts[2],
|
||||
"flags": parts[3]
|
||||
"bssid": "",
|
||||
"flags": parts[-1]
|
||||
})
|
||||
return networks
|
||||
return networks
|
||||
|
||||
def scan_networks(self):
|
||||
"""扫描范围内的 wifi"""
|
||||
self._run_wpa_cli("scan")
|
||||
time.sleep(3) # 等待扫描完成
|
||||
output = self._run_wpa_cli("scan_results")
|
||||
networks = []
|
||||
lines = output.splitlines()
|
||||
if len(lines) > 1:
|
||||
for line in lines[1:]:
|
||||
# bssid / frequency / signal level / flags / ssid
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 5:
|
||||
networks.append({
|
||||
"bssid": parts[0],
|
||||
"frequency": parts[1],
|
||||
"signal_level": parts[2],
|
||||
"flags": parts[3],
|
||||
"ssid": parts[4]
|
||||
})
|
||||
return networks
|
||||
if self.backend == "wpa_cli":
|
||||
self._run_wpa_cli("scan")
|
||||
time.sleep(3)
|
||||
output = self._run_wpa_cli("scan_results")
|
||||
networks = []
|
||||
lines = output.splitlines()
|
||||
if len(lines) > 1:
|
||||
for line in lines[1:]:
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 5:
|
||||
networks.append({
|
||||
"bssid": parts[0],
|
||||
"frequency": parts[1],
|
||||
"signal_level": parts[2],
|
||||
"flags": parts[3],
|
||||
"ssid": parts[4]
|
||||
})
|
||||
return networks
|
||||
else:
|
||||
output = self._run_nmcli("-t", "-m", "multiline", "-f", "BSSID,FREQ,SIGNAL,SECURITY,SSID", "device", "wifi", "list", "--rescan", "yes")
|
||||
networks = []
|
||||
current = {}
|
||||
for line in output.splitlines():
|
||||
if ':' in line:
|
||||
k, v = line.split(':', 1)
|
||||
current[k] = v
|
||||
if k == "SSID":
|
||||
networks.append({
|
||||
"bssid": current.get("BSSID", ""),
|
||||
"frequency": current.get("FREQ", "").replace(" MHz", ""),
|
||||
"signal_level": current.get("SIGNAL", ""),
|
||||
"flags": current.get("SECURITY", ""),
|
||||
"ssid": current.get("SSID", "")
|
||||
})
|
||||
current = {}
|
||||
return networks
|
||||
|
||||
def connect_wifi(self, ssid, password=None):
|
||||
"""连接普通 Wi-Fi (WPA2-PSK)"""
|
||||
network_id = self._run_wpa_cli("add_network")
|
||||
if not network_id.isdigit():
|
||||
return False
|
||||
|
||||
self._run_wpa_cli("set_network", network_id, "ssid", f'"{ssid}"')
|
||||
if password:
|
||||
self._run_wpa_cli("set_network", network_id, "psk", f'"{password}"')
|
||||
if self.backend == "wpa_cli":
|
||||
network_id = self._run_wpa_cli("add_network")
|
||||
if not network_id.isdigit(): return False
|
||||
self._run_wpa_cli("set_network", network_id, "ssid", f'"{ssid}"')
|
||||
if password: self._run_wpa_cli("set_network", network_id, "psk", f'"{password}"')
|
||||
else: self._run_wpa_cli("set_network", network_id, "key_mgmt", "NONE")
|
||||
self._run_wpa_cli("enable_network", network_id)
|
||||
self._run_wpa_cli("select_network", network_id)
|
||||
self._run_wpa_cli("save_config")
|
||||
return True
|
||||
else:
|
||||
self._run_wpa_cli("set_network", network_id, "key_mgmt", "NONE")
|
||||
|
||||
self._run_wpa_cli("enable_network", network_id)
|
||||
self._run_wpa_cli("select_network", network_id)
|
||||
self._run_wpa_cli("save_config")
|
||||
return True
|
||||
args = ["device", "wifi", "connect", ssid, "ifname", self.interface]
|
||||
if password:
|
||||
args.extend(["password", password])
|
||||
res = self._run_nmcli(*args)
|
||||
return "successfully" in res.lower() or "成功" in res
|
||||
|
||||
def connect_eap(self, ssid, identity, password):
|
||||
"""连接企业级 Wi-Fi (WPA-EAP PEAP/MSCHAPv2)"""
|
||||
network_id = self._run_wpa_cli("add_network")
|
||||
if not network_id.isdigit():
|
||||
if self.backend == "wpa_cli":
|
||||
network_id = self._run_wpa_cli("add_network")
|
||||
if not network_id.isdigit(): return False
|
||||
self._run_wpa_cli("set_network", network_id, "ssid", f'"{ssid}"')
|
||||
self._run_wpa_cli("set_network", network_id, "key_mgmt", "WPA-EAP")
|
||||
self._run_wpa_cli("set_network", network_id, "eap", "PEAP")
|
||||
self._run_wpa_cli("set_network", network_id, "phase2", '"auth=MSCHAPV2"')
|
||||
self._run_wpa_cli("set_network", network_id, "identity", f'"{identity}"')
|
||||
self._run_wpa_cli("set_network", network_id, "password", f'"{password}"')
|
||||
self._run_wpa_cli("enable_network", network_id)
|
||||
self._run_wpa_cli("select_network", network_id)
|
||||
self._run_wpa_cli("save_config")
|
||||
return True
|
||||
else:
|
||||
res = self._run_nmcli("con", "add", "type", "wifi", "con-name", ssid, "ifname", self.interface, "ssid", ssid,
|
||||
"--", "802-11-wireless-security.key-mgmt", "wpa-eap", "802-1x.eap", "peap",
|
||||
"802-1x.phase2-auth", "mschapv2", "802-1x.identity", identity, "802-1x.password", password)
|
||||
if "successfully" in res.lower() or "成功" in res:
|
||||
self._run_nmcli("con", "up", ssid)
|
||||
return True
|
||||
return False
|
||||
|
||||
self._run_wpa_cli("set_network", network_id, "ssid", f'"{ssid}"')
|
||||
self._run_wpa_cli("set_network", network_id, "key_mgmt", "WPA-EAP")
|
||||
self._run_wpa_cli("set_network", network_id, "eap", "PEAP")
|
||||
self._run_wpa_cli("set_network", network_id, "phase2", '"auth=MSCHAPV2"')
|
||||
self._run_wpa_cli("set_network", network_id, "identity", f'"{identity}"')
|
||||
self._run_wpa_cli("set_network", network_id, "password", f'"{password}"')
|
||||
|
||||
self._run_wpa_cli("enable_network", network_id)
|
||||
self._run_wpa_cli("select_network", network_id)
|
||||
self._run_wpa_cli("save_config")
|
||||
return True
|
||||
|
||||
def connect_network_id(self, network_id):
|
||||
"""通过 network_id 连接已保存的网络"""
|
||||
ret1 = self._run_wpa_cli("select_network", str(network_id))
|
||||
self._run_wpa_cli("save_config")
|
||||
return bool(ret1)
|
||||
if self.backend == "wpa_cli":
|
||||
ret1 = self._run_wpa_cli("select_network", str(network_id))
|
||||
self._run_wpa_cli("save_config")
|
||||
return bool(ret1)
|
||||
else:
|
||||
res = self._run_nmcli("connection", "up", "uuid", str(network_id))
|
||||
return "successfully" in res.lower() or "成功" in res
|
||||
|
||||
def remove_network(self, network_id):
|
||||
"""删除某个已保存的网络"""
|
||||
self._run_wpa_cli("remove_network", str(network_id))
|
||||
self._run_wpa_cli("save_config")
|
||||
if self.backend == "wpa_cli":
|
||||
self._run_wpa_cli("remove_network", str(network_id))
|
||||
self._run_wpa_cli("save_config")
|
||||
else:
|
||||
self._run_nmcli("connection", "delete", "uuid", str(network_id))
|
||||
|
||||
def get_current_status(self):
|
||||
"""获取当前网络状态"""
|
||||
output = self._run_wpa_cli("status")
|
||||
status = {}
|
||||
for line in output.splitlines():
|
||||
if '=' in line:
|
||||
key, val = line.split('=', 1)
|
||||
status[key] = val
|
||||
return status
|
||||
if self.backend == "wpa_cli":
|
||||
output = self._run_wpa_cli("status")
|
||||
status = {}
|
||||
for line in output.splitlines():
|
||||
if '=' in line:
|
||||
key, val = line.split('=', 1)
|
||||
status[key] = val
|
||||
return status
|
||||
else:
|
||||
output = self._run_nmcli("-t", "-m", "multiline", "-f", "GENERAL.STATE,GENERAL.CONNECTION,IP4.ADDRESS", "device", "show", self.interface)
|
||||
status = {"wpa_state": "DISCONNECTED", "ssid": "", "signal_level": None}
|
||||
for line in output.splitlines():
|
||||
if line.startswith("GENERAL.STATE:"):
|
||||
state_val = line.split(':', 1)[1]
|
||||
if "connected" in state_val.lower():
|
||||
status["wpa_state"] = "COMPLETED"
|
||||
elif line.startswith("GENERAL.CONNECTION:"):
|
||||
conn_val = line.split(':', 1)[1]
|
||||
if conn_val:
|
||||
status["ssid"] = conn_val
|
||||
elif line.startswith("IP4.ADDRESS[1]:"):
|
||||
ip_val = line.split(':', 1)[1]
|
||||
status["ip_address"] = ip_val.split('/')[0]
|
||||
|
||||
# 额外获取当前连接网络的信号强度(nmcli device wifi 才有 SIGNAL 字段)
|
||||
if status["wpa_state"] == "COMPLETED":
|
||||
wifi_out = self._run_nmcli("-t", "-f", "SSID,SIGNAL,IN-USE", "device", "wifi")
|
||||
for line in wifi_out.splitlines():
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 3 and parts[2] == '*':
|
||||
status["signal_level"] = parts[1] # SIGNAL 字段(百分比 0-100)
|
||||
break
|
||||
return status
|
||||
|
||||
def open_hotspot(self, ssid, password, channel=6):
|
||||
"""
|
||||
开启热点 (AP 模式)
|
||||
注意:要求网卡和 wpa_supplicant 均支持 AP 模式 (mode=2)
|
||||
"""
|
||||
network_id = self._run_wpa_cli("add_network")
|
||||
if not network_id.isdigit():
|
||||
return False
|
||||
|
||||
self._run_wpa_cli("set_network", network_id, "ssid", f'"{ssid}"')
|
||||
self._run_wpa_cli("set_network", network_id, "mode", "2")
|
||||
self._run_wpa_cli("set_network", network_id, "key_mgmt", "WPA-PSK")
|
||||
self._run_wpa_cli("set_network", network_id, "psk", f'"{password}"')
|
||||
self._run_wpa_cli("set_network", network_id, "frequency", str(2412 + (channel - 1) * 5))
|
||||
|
||||
self._run_wpa_cli("select_network", network_id)
|
||||
self._run_wpa_cli("save_config")
|
||||
return network_id
|
||||
if self.backend == "wpa_cli":
|
||||
network_id = self._run_wpa_cli("add_network")
|
||||
if not network_id.isdigit(): return False
|
||||
self._run_wpa_cli("set_network", network_id, "ssid", f'"{ssid}"')
|
||||
self._run_wpa_cli("set_network", network_id, "mode", "2")
|
||||
self._run_wpa_cli("set_network", network_id, "key_mgmt", "WPA-PSK")
|
||||
self._run_wpa_cli("set_network", network_id, "psk", f'"{password}"')
|
||||
self._run_wpa_cli("set_network", network_id, "frequency", str(2412 + (channel - 1) * 5))
|
||||
self._run_wpa_cli("select_network", network_id)
|
||||
self._run_wpa_cli("save_config")
|
||||
return network_id
|
||||
else:
|
||||
self._run_nmcli("device", "wifi", "hotspot", "ifname", self.interface, "con-name", ssid, "ssid", ssid, "band", "bg", "channel", str(channel), "password", password)
|
||||
networks = self.list_saved_networks()
|
||||
for net in networks:
|
||||
if net.get("ssid") == ssid:
|
||||
return net.get("network_id")
|
||||
return ssid
|
||||
|
||||
def close_hotspot(self, network_id=None):
|
||||
"""关闭热点"""
|
||||
if network_id is not None:
|
||||
self._run_wpa_cli("remove_network", str(network_id))
|
||||
self._run_wpa_cli("reconfigure")
|
||||
self._run_wpa_cli("save_config")
|
||||
if self.backend == "wpa_cli":
|
||||
if network_id is not None:
|
||||
self._run_wpa_cli("remove_network", str(network_id))
|
||||
self._run_wpa_cli("reconfigure")
|
||||
self._run_wpa_cli("save_config")
|
||||
else:
|
||||
if network_id is not None:
|
||||
self._run_nmcli("connection", "down", str(network_id))
|
||||
self._run_nmcli("connection", "delete", str(network_id))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example Usage
|
||||
wifi = WifiManager("wlan0")
|
||||
wifi = WifiManager("wlan0", backend="nmcli")
|
||||
print("Current status:", wifi.get_current_status())
|
||||
print("Saved networks:", wifi.list_saved_networks())
|
||||
|
||||
Reference in New Issue
Block a user