diff --git a/__pycache__/gcode_viewer.cpython-311.pyc b/__pycache__/gcode_viewer.cpython-311.pyc
new file mode 100644
index 0000000..1ff0fa7
Binary files /dev/null and b/__pycache__/gcode_viewer.cpython-311.pyc differ
diff --git a/assets/img/logo.jpg b/assets/img/logo.jpg
new file mode 100644
index 0000000..92ef0cf
Binary files /dev/null and b/assets/img/logo.jpg differ
diff --git a/main.py b/main.py
index 529394a..88e04b6 100644
--- a/main.py
+++ b/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"{s.cpu_load:.2f}/4.0"
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;")
diff --git a/pages/control_page.py b/pages/control_page.py
index 831a706..8e82ecf 100644
--- a/pages/control_page.py
+++ b/pages/control_page.py
@@ -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)
diff --git a/pages/setting_page.py b/pages/setting_page.py
index f9f8ba0..ae833c5 100644
--- a/pages/setting_page.py
+++ b/pages/setting_page.py
@@ -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', '')} {network.get('flags', '')}"
+ item_text = f"{network.get('ssid', '')}"
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:
diff --git a/pages/status_page.py b/pages/status_page.py
index 4697899..3247edb 100644
--- a/pages/status_page.py
+++ b/pages/status_page.py
@@ -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
diff --git a/refer/test.py b/refer/test.py
new file mode 100644
index 0000000..76598c0
--- /dev/null
+++ b/refer/test.py
@@ -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())
\ No newline at end of file
diff --git a/refer/test_line_width.py b/refer/test_line_width.py
new file mode 100644
index 0000000..308de68
--- /dev/null
+++ b/refer/test_line_width.py
@@ -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()
diff --git a/refer/test_linewidth.py b/refer/test_linewidth.py
new file mode 100644
index 0000000..a44d97b
--- /dev/null
+++ b/refer/test_linewidth.py
@@ -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()
diff --git a/refer/test_qmat.py b/refer/test_qmat.py
new file mode 100644
index 0000000..ca48913
--- /dev/null
+++ b/refer/test_qmat.py
@@ -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)
+
diff --git a/refer/test_rot.py b/refer/test_rot.py
new file mode 100644
index 0000000..15514d0
--- /dev/null
+++ b/refer/test_rot.py
@@ -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)
+
diff --git a/run.sh b/run.sh
index 7fa4b19..8b9c8be 100755
--- a/run.sh
+++ b/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
diff --git a/utils/aio_print_api.py b/utils/aio_print_api.py
index 39e19c5..ac5a1b3 100644
--- a/utils/aio_print_api.py
+++ b/utils/aio_print_api.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")
diff --git a/utils/auto_fan_status.py b/utils/auto_fan_status.py
index 3cf207b..84e4ce2 100644
--- a/utils/auto_fan_status.py
+++ b/utils/auto_fan_status.py
@@ -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
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/utils/config_parse.py b/utils/config_parse.py
index cf46b1d..70c5e05 100644
--- a/utils/config_parse.py
+++ b/utils/config_parse.py
@@ -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)
\ No newline at end of file
diff --git a/utils/gcode_viewer.py b/utils/gcode_viewer.py
new file mode 100644
index 0000000..54aecb8
--- /dev/null
+++ b/utils/gcode_viewer.py
@@ -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()
\ No newline at end of file
diff --git a/utils/wifi_manager.py b/utils/wifi_manager.py
index 8b3a7ae..9f9ab0f 100644
--- a/utils/wifi_manager.py
+++ b/utils/wifi_manager.py
@@ -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())