|
|
|
@ -5,8 +5,8 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout
|
|
|
|
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
|
|
|
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
|
|
|
QHeaderView, QPushButton, QCheckBox, QLineEdit,
|
|
|
|
QHeaderView, QPushButton, QCheckBox, QLineEdit,
|
|
|
|
QComboBox, QDialog, QPlainTextEdit, QSlider)
|
|
|
|
QComboBox, QDialog, QPlainTextEdit, QSlider)
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent, QPropertyAnimation, pyqtProperty, QEasingCurve
|
|
|
|
from PyQt6.QtGui import QColor, QFont
|
|
|
|
from PyQt6.QtGui import QColor, QFont, QPainter, QPen
|
|
|
|
import sys
|
|
|
|
import sys
|
|
|
|
import asyncio
|
|
|
|
import asyncio
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
@ -72,6 +72,76 @@ class StreamRedirector(QObject):
|
|
|
|
self.text_written.emit(line)
|
|
|
|
self.text_written.emit(line)
|
|
|
|
self._buffer = ""
|
|
|
|
self._buffer = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToggleSwitch(QWidget):
|
|
|
|
|
|
|
|
"""自訂滑動開關 — ON 綠色 / OFF 紅色"""
|
|
|
|
|
|
|
|
toggled = pyqtSignal(bool)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, checked=True, parent=None):
|
|
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
|
|
self._checked = checked
|
|
|
|
|
|
|
|
self._knob_x = 1.0 if checked else 0.0
|
|
|
|
|
|
|
|
self.setFixedSize(44, 24)
|
|
|
|
|
|
|
|
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self._animation = QPropertyAnimation(self, b"knob_position", self)
|
|
|
|
|
|
|
|
self._animation.setDuration(150)
|
|
|
|
|
|
|
|
self._animation.setEasingCurve(QEasingCurve.Type.InOutCubic)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_knob_position(self):
|
|
|
|
|
|
|
|
return self._knob_x
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _set_knob_position(self, val):
|
|
|
|
|
|
|
|
self._knob_x = val
|
|
|
|
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
knob_position = pyqtProperty(float, _get_knob_position, _set_knob_position)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def isChecked(self):
|
|
|
|
|
|
|
|
return self._checked
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setChecked(self, checked):
|
|
|
|
|
|
|
|
if self._checked != checked:
|
|
|
|
|
|
|
|
self._checked = checked
|
|
|
|
|
|
|
|
self._animate(checked)
|
|
|
|
|
|
|
|
self.toggled.emit(checked)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mousePressEvent(self, event):
|
|
|
|
|
|
|
|
self.setChecked(not self._checked)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _animate(self, checked):
|
|
|
|
|
|
|
|
self._animation.stop()
|
|
|
|
|
|
|
|
self._animation.setStartValue(self._knob_x)
|
|
|
|
|
|
|
|
self._animation.setEndValue(1.0 if checked else 0.0)
|
|
|
|
|
|
|
|
self._animation.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def paintEvent(self, event):
|
|
|
|
|
|
|
|
p = QPainter(self)
|
|
|
|
|
|
|
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
|
|
|
|
|
|
w, h = self.width(), self.height()
|
|
|
|
|
|
|
|
r = h / 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 背景軌道:依 knob_x 插值 灰→綠
|
|
|
|
|
|
|
|
red = QColor(0x88, 0x88, 0x88)
|
|
|
|
|
|
|
|
green = QColor(0x4C, 0xAF, 0x50)
|
|
|
|
|
|
|
|
t = self._knob_x
|
|
|
|
|
|
|
|
bg = QColor(
|
|
|
|
|
|
|
|
int(red.red() + (green.red() - red.red()) * t),
|
|
|
|
|
|
|
|
int(red.green() + (green.green() - red.green()) * t),
|
|
|
|
|
|
|
|
int(red.blue() + (green.blue() - red.blue()) * t),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
p.setPen(Qt.PenStyle.NoPen)
|
|
|
|
|
|
|
|
p.setBrush(bg)
|
|
|
|
|
|
|
|
p.drawRoundedRect(0, 0, w, h, r, r)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 滑塊圓形
|
|
|
|
|
|
|
|
knob_d = h - 4
|
|
|
|
|
|
|
|
knob_range = w - knob_d - 4
|
|
|
|
|
|
|
|
knob_cx = 2 + self._knob_x * knob_range + knob_d / 2
|
|
|
|
|
|
|
|
p.setBrush(QColor(255, 255, 255))
|
|
|
|
|
|
|
|
p.drawEllipse(int(knob_cx - knob_d / 2), 2, int(knob_d), int(knob_d))
|
|
|
|
|
|
|
|
p.end()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
class ControlStationUI(QMainWindow):
|
|
|
|
VERSION = '2.2.0'
|
|
|
|
VERSION = '2.2.0'
|
|
|
|
FONT_SCALE_MIN = 70
|
|
|
|
FONT_SCALE_MIN = 70
|
|
|
|
@ -276,6 +346,12 @@ class ControlStationUI(QMainWindow):
|
|
|
|
group_header.addLayout(title_layout)
|
|
|
|
group_header.addLayout(title_layout)
|
|
|
|
group_header.addStretch()
|
|
|
|
group_header.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
verify_label = QLabel("模擬驗證")
|
|
|
|
|
|
|
|
verify_label.setStyleSheet("color: #CCC; font-size: 12px;")
|
|
|
|
|
|
|
|
self.verify_toggle = ToggleSwitch(checked=True)
|
|
|
|
|
|
|
|
group_header.addWidget(verify_label)
|
|
|
|
|
|
|
|
group_header.addWidget(self.verify_toggle)
|
|
|
|
|
|
|
|
|
|
|
|
clear_traj_btn = QPushButton("清除軌跡")
|
|
|
|
clear_traj_btn = QPushButton("清除軌跡")
|
|
|
|
clear_traj_btn.setStyleSheet("""
|
|
|
|
clear_traj_btn.setStyleSheet("""
|
|
|
|
QPushButton { background-color: #EF5350; color: white; border: none;
|
|
|
|
QPushButton { background-color: #EF5350; color: white; border: none;
|
|
|
|
@ -1935,6 +2011,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
selected_drones, waypoints_per_drone, origin,
|
|
|
|
selected_drones, waypoints_per_drone, origin,
|
|
|
|
target_gps=None, rect_corners=None, route_waypoints=None):
|
|
|
|
target_gps=None, rect_corners=None, route_waypoints=None):
|
|
|
|
"""存 JSON + 啟動 matplotlib 視覺化驗證 (獨立 process)"""
|
|
|
|
"""存 JSON + 啟動 matplotlib 視覺化驗證 (獨立 process)"""
|
|
|
|
|
|
|
|
if not self.verify_toggle.isChecked():
|
|
|
|
|
|
|
|
_log("INFO", "模擬驗證已關閉,跳過驗證視窗")
|
|
|
|
|
|
|
|
return
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
data = {
|
|
|
|
data = {
|
|
|
|
'mission_type': mission_type,
|
|
|
|
'mission_type': mission_type,
|
|
|
|
|