From 567085ead3f6adaabd884f16ab4b17c62e8f0403 Mon Sep 17 00:00:00 2001
From: yincheng.zhong <634916154@qq.com>
Date: 星期日, 21 十二月 2025 22:28:09 +0800
Subject: [PATCH] OTA升级功能调通,准备增加boot的代码

---
 python/hitl/run_sim.py |  625 +++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 543 insertions(+), 82 deletions(-)

diff --git a/python/hitl/run_sim.py b/python/hitl/run_sim.py
index 99b60f3..2e14e09 100644
--- a/python/hitl/run_sim.py
+++ b/python/hitl/run_sim.py
@@ -23,7 +23,7 @@
 except Exception:  # pragma: no cover - serial 鍙兘涓嶅瓨鍦�
     list_ports = None
 
-from .protocols import ControlStatus, PoseStatus, StackStatus, StateStatus
+from .protocols import ControlStatus, PoseStatus, StackStatus, StateStatus, nmea_checksum
 from .simulator import HitlConfig, HitlSimulator, RunLogger
 
 DEFAULT_UART2_PORT = "COM11"
@@ -33,7 +33,11 @@
 DEFAULT_HEADING_DEG = 0.0
 DEFAULT_UART2_BAUD = 115200
 DEFAULT_UART5_BAUD = 921600
-RUN_LOG_PATH = Path(__file__).resolve().parent / "runlog.txt"
+BASE_DIR = Path(__file__).resolve().parent
+RUN_LOG_PATH = BASE_DIR / "runlog.txt"
+PATH_TEXT_FILE = BASE_DIR / "path_text_input.txt"
+PATH_EXPORT_FILE = PATH_TEXT_FILE.with_suffix(".c")
+STATE_FILE = BASE_DIR / "dashboard_state.json"
 
 
 def load_path_points(path_file: Path) -> List[Tuple[float, float]]:
@@ -66,6 +70,89 @@
     return points
 
 
+def parse_path_text(text: str) -> List[Tuple[float, float]]:
+    tokens = text.replace("\n", " ").split(";")
+    points: List[Tuple[float, float]] = []
+    for token in tokens:
+        token = token.strip()
+        if not token:
+            continue
+        parts = token.split(",")
+        if len(parts) < 2:
+            continue
+        try:
+            x = float(parts[0].strip())
+            y = float(parts[1].strip())
+            points.append((x, y))
+        except ValueError:
+            continue
+    return points
+
+
+def format_path_for_mcu(points: List[Tuple[float, float]], decimals: int = 2) -> str:
+    if not points:
+        return "const float g_motion_path_xy[] = {};\n"
+    fmt = f"{{:.{decimals}f}}"
+    lines = ["const float g_motion_path_xy[] = {"]
+    for idx, (x, y) in enumerate(points):
+        comma = "," if idx < len(points) - 1 else ""
+        lines.append(f"    {fmt.format(x)}f, {fmt.format(y)}f{comma}")
+    lines.append("};")
+    return "\n".join(lines)
+
+
+def gga_to_short_origin(gga: str) -> str:
+    text = (gga or "").strip()
+    if not text:
+        return ""
+    body = text.split("*")[0]
+    parts = [p.strip() for p in body.split(",")]
+    if len(parts) < 6:
+        return ""
+    lat = parts[2]
+    lat_dir = parts[3]
+    lon = parts[4]
+    lon_dir = parts[5]
+    alt = parts[9] if len(parts) > 9 and parts[9] else ""
+    short = f"{lat},{lat_dir},{lon},{lon_dir}"
+    if alt:
+        short += f",{alt}"
+    return short
+
+
+def build_gga_from_short_origin(text: str, template: str) -> str:
+    tokens = [t.strip() for t in text.replace("\n", " ").split(",") if t.strip()]
+    if len(tokens) < 4:
+        raise ValueError("鍘熺偣鏍煎紡搴斾负 'lat,N,lon,E[,alt]'銆�")
+    lat_dm = tokens[0]
+    lat_dir = tokens[1].upper()
+    lon_dm = tokens[2]
+    lon_dir = tokens[3].upper()
+    alt = tokens[4] if len(tokens) >= 5 else None
+    if lat_dir not in ("N", "S") or lon_dir not in ("E", "W"):
+        raise ValueError("鏂瑰悜蹇呴』鏄� N/S/E/W銆�")
+
+    base = template or DEFAULT_ORIGIN_GGA
+    newline = ""
+    if base.endswith("\r\n"):
+        base = base[:-2]
+        newline = "\r\n"
+    body = base.split("*")[0]
+    fields = body.split(",")
+    while len(fields) <= 13:
+        fields.append("")
+    fields[0] = fields[0] or "$GNGGA"
+    fields[2] = lat_dm
+    fields[3] = lat_dir
+    fields[4] = lon_dm
+    fields[5] = lon_dir
+    if alt is not None:
+        fields[9] = alt
+    body = ",".join(fields)
+    checksum = nmea_checksum(body)
+    return f"{body}*{checksum}{newline}"
+
+
 class HitlGuiBridge:
     def __init__(self):
         self.queue: queue.Queue = queue.Queue(maxsize=200)
@@ -97,6 +184,108 @@
                 pass
 
 
+class ZoomableGraphicsView(QtWidgets.QGraphicsView):
+    sceneMouseMoved = QtCore.pyqtSignal(float, float)
+    """鏀寔榧犳爣婊氳疆缂╂斁 + 鎸変綇宸﹂敭骞崇Щ鐨勮鍥�"""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.setRenderHint(QtGui.QPainter.Antialiasing, True)
+        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
+        self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
+        self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
+        self.setCursor(QtCore.Qt.OpenHandCursor)
+        self._zoom_factor = 1.15  # 姣忔婊氳疆鐨勭缉鏀惧洜瀛�
+        self._panning = False
+        self._pan_last_pos = QtCore.QPoint()
+        self.on_user_pan = None  # 鐢卞閮ㄦ敞鍏ョ殑鍥炶皟
+
+    def wheelEvent(self, event: QtGui.QWheelEvent):
+        delta = event.angleDelta().y()
+        scale_factor = self._zoom_factor if delta > 0 else 1.0 / self._zoom_factor
+        self.scale(scale_factor, scale_factor)
+
+    def mousePressEvent(self, event: QtGui.QMouseEvent):
+        if event.button() == QtCore.Qt.LeftButton:
+            self._panning = True
+            self._pan_last_pos = event.pos()
+            self.setCursor(QtCore.Qt.ClosedHandCursor)
+            event.accept()
+            return
+        super().mousePressEvent(event)
+
+    def mouseMoveEvent(self, event: QtGui.QMouseEvent):
+        if self._panning:
+            scene_pos = self.mapToScene(event.pos())
+            last_scene_pos = self.mapToScene(self._pan_last_pos)
+            delta = scene_pos - last_scene_pos
+            self._pan_last_pos = event.pos()
+            self.translate(-delta.x(), -delta.y())
+            if self.on_user_pan:
+                self.on_user_pan()
+            event.accept()
+            return
+        self.sceneMouseMoved.emit(*self._scene_coords(event.pos()))
+        super().mouseMoveEvent(event)
+
+    def _scene_coords(self, view_pos: QtCore.QPoint) -> Tuple[float, float]:
+        scene_pos = self.mapToScene(view_pos)
+        return scene_pos.x(), scene_pos.y()
+
+    def mouseReleaseEvent(self, event: QtGui.QMouseEvent):
+        if event.button() == QtCore.Qt.LeftButton and self._panning:
+            self._panning = False
+            self.setCursor(QtCore.Qt.OpenHandCursor)
+            event.accept()
+            return
+        super().mouseReleaseEvent(event)
+
+
+class DashboardStateStore:
+    def __init__(self, path: Path):
+        self.path = Path(path)
+        self.data: Dict[str, object] = {}
+        self._load()
+
+    def _load(self):
+        if not self.path.exists():
+            self.data = {}
+            return
+        try:
+            self.data = json.loads(self.path.read_text(encoding="utf-8"))
+        except Exception:
+            self.data = {}
+
+    def save(self):
+        try:
+            self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8")
+        except Exception:
+            pass
+
+    def get_pose(self) -> Optional[Tuple[float, float, float, float]]:
+        pose = self.data.get("pose")
+        if isinstance(pose, list) and len(pose) == 4:
+            try:
+                return tuple(float(v) for v in pose)  # type: ignore
+            except (TypeError, ValueError):
+                return None
+        return None
+
+    def set_pose(self, east: float, north: float, up: float, heading: float):
+        self.data["pose"] = [east, north, up, heading]
+        self.save()
+
+    def get_origin(self) -> Optional[str]:
+        origin = self.data.get("origin")
+        if isinstance(origin, str):
+            return origin
+        return None
+
+    def set_origin(self, origin_str: str):
+        self.data["origin"] = origin_str
+        self.save()
+
+
 class HitlDashboard(QtWidgets.QMainWindow):
     def __init__(self,
                  simulator: HitlSimulator,
@@ -113,6 +302,7 @@
         self.pose_status: Optional[PoseStatus] = None
         self.state_status: Optional[StateStatus] = None
         self.stack_status: Dict[str, StackStatus] = {}
+        self._auto_follow = True
 
         self.setWindowTitle("HITL 浠跨湡鐘舵�侀潰鏉�")
         self.resize(1280, 720)
@@ -120,7 +310,13 @@
         self._serial_open = False
         self._last_ports: List[str] = []
 
+        self.state_store = DashboardStateStore(STATE_FILE)
+
         self._build_ui(initial_uart2, initial_uart5)
+        self._load_saved_path_text()
+        if not self.path_text.toPlainText().strip() and self.path_points:
+            self._update_path_text_from_points(self.path_points)
+        self._load_saved_pose()
         self._init_scene()
 
         self.timer = QtCore.QTimer(self)
@@ -133,11 +329,39 @@
         self.setCentralWidget(central)
 
         self.scene = QtWidgets.QGraphicsScene(self)
-        self.view = QtWidgets.QGraphicsView(self.scene)
-        self.view.setRenderHint(QtGui.QPainter.Antialiasing, True)
-        self.view.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
-        self.view.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
-        layout.addWidget(self.view, stretch=3)
+        left_panel = QtWidgets.QWidget()
+        left_layout = QtWidgets.QVBoxLayout(left_panel)
+        left_layout.setContentsMargins(0, 0, 0, 0)
+        left_layout.setSpacing(4)
+
+        self.view = ZoomableGraphicsView(self.scene)
+        self.view.on_user_pan = self._handle_user_pan
+        self.view.sceneMouseMoved.connect(self._update_mouse_pos)
+        left_layout.addWidget(self.view, stretch=1)
+
+        # Bottom bar for map controls
+        bottom_bar = QtWidgets.QWidget()
+        bottom_layout = QtWidgets.QHBoxLayout(bottom_bar)
+        bottom_layout.setContentsMargins(0, 4, 0, 0)
+
+        self.follow_checkbox = QtWidgets.QCheckBox("鑷姩璺熼殢杞﹁締")
+        self.follow_checkbox.setChecked(True)
+        self.follow_checkbox.toggled.connect(self._on_follow_toggled)
+        bottom_layout.addWidget(self.follow_checkbox)
+
+        self.clear_trail_btn = QtWidgets.QPushButton("娓呴櫎杞ㄨ抗")
+        self.clear_trail_btn.clicked.connect(self._clear_trail)
+        bottom_layout.addWidget(self.clear_trail_btn)
+
+        bottom_layout.addStretch(1)
+
+        self.mouse_pos_label = QtWidgets.QLabel("E: 0.00  N: 0.00")
+        self.mouse_pos_label.setStyleSheet("color:#222; background:rgba(255,255,255,190); padding:2px 6px; border-radius:4px;")
+        bottom_layout.addWidget(self.mouse_pos_label)
+
+        left_layout.addWidget(bottom_bar)
+
+        layout.addWidget(left_panel, stretch=3)
 
         self._port_refresh_supported = list_ports is not None
 
@@ -169,25 +393,33 @@
             self.port2_combo.setEditable(True)
             self.port5_combo.setEditable(True)
 
-        # Path
-        path_group = QtWidgets.QGroupBox("璺緞鏂囦欢")
-        path_layout = QtWidgets.QHBoxLayout(path_group)
-        self.path_line = QtWidgets.QLineEdit()
-        self.path_browse_btn = QtWidgets.QPushButton("娴忚")
-        self.path_load_btn = QtWidgets.QPushButton("鍔犺浇")
-        path_layout.addWidget(self.path_line)
-        path_layout.addWidget(self.path_browse_btn)
-        path_layout.addWidget(self.path_load_btn)
+        # Path text input
+        path_group = QtWidgets.QGroupBox("璺緞杞ㄨ抗")
+        path_layout = QtWidgets.QVBoxLayout(path_group)
+        self.path_text = QtWidgets.QPlainTextEdit()
+        self.path_text.setPlaceholderText("绀轰緥锛�23.36,-42.93;29.26,-52.99; ... ;")
+        self.path_text.setWordWrapMode(QtGui.QTextOption.NoWrap)
+        button_row = QtWidgets.QHBoxLayout()
+        self.path_load_btn = QtWidgets.QPushButton("鍔犺浇鍒板湴鍥�")
+        self.path_export_btn = QtWidgets.QPushButton("瀵煎嚭 MCU 鏍煎紡")
+        button_row.addWidget(self.path_load_btn)
+        button_row.addWidget(self.path_export_btn)
+        path_layout.addWidget(self.path_text)
+        path_layout.addLayout(button_row)
         right_layout.addWidget(path_group)
 
         # Origin / pose reset
         origin_group = QtWidgets.QGroupBox("鍘熺偣/璧风偣")
         grid = QtWidgets.QGridLayout(origin_group)
-        self.origin_edit = QtWidgets.QLineEdit(DEFAULT_ORIGIN_GGA)
+        default_origin_short = gga_to_short_origin(self.simulator.config.origin_gga)
+        self.origin_text = QtWidgets.QLineEdit(default_origin_short)
+        self.origin_text.setPlaceholderText("3949.9120,N,11616.8544,E[,alt]")
         self.origin_btn = QtWidgets.QPushButton("鏇存柊鍘熺偣")
-        grid.addWidget(QtWidgets.QLabel("GGA:"), 0, 0)
-        grid.addWidget(self.origin_edit, 0, 1, 1, 2)
-        grid.addWidget(self.origin_btn, 0, 3)
+        self.copy_origin_btn = QtWidgets.QPushButton("澶嶅埗瀹忓畾涔�") # 鏂板鎸夐挳
+        grid.addWidget(QtWidgets.QLabel("鍧愭爣 (lat,N,lon,E):"), 0, 0, 1, 2)
+        grid.addWidget(self.origin_text, 0, 2, 1, 2)
+        grid.addWidget(self.origin_btn, 0, 4)
+        grid.addWidget(self.copy_origin_btn, 0, 5) # 娣诲姞鍒板竷灞�
 
         labels = ["E (m)", "N (m)", "U (m)", "Heading (deg)"]
         self.pos_spin: List[QtWidgets.QDoubleSpinBox] = []
@@ -211,12 +443,13 @@
         status_layout = QtWidgets.QVBoxLayout(status_group)
         self.info_label = QtWidgets.QLabel("绛夊緟鏁版嵁...")
         self.info_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
-        self.info_label.setStyleSheet("font-family: Consolas, 'Courier New'; font-size: 12px;")
+        self.info_label.setStyleSheet("font-family: Consolas, 'Courier New'; font-size: 14px;")
         info_scroll = QtWidgets.QScrollArea()
         info_scroll.setWidgetResizable(True)
         info_scroll.setWidget(self.info_label)
+        info_scroll.setMinimumHeight(500)  # 杩涗竴姝ュ鍔犳渶灏忛珮搴�
         status_layout.addWidget(info_scroll)
-        right_layout.addWidget(status_group, stretch=1)
+        right_layout.addWidget(status_group, stretch=4)  # 杩涗竴姝ュ鍔犲竷灞�鏉冮噸锛屼粠2澧炲姞鍒�4
 
         # Stack table
         stack_group = QtWidgets.QGroupBox("鍫嗘爤鐩戞祴")
@@ -236,15 +469,38 @@
         right_layout.addWidget(log_group, stretch=1)
 
         # Scene items
-        self.path_item = self.scene.addPath(QtGui.QPainterPath(), QtGui.QPen(QtGui.QColor("gray"), 0.8))
-        self.trail_item = self.scene.addPath(QtGui.QPainterPath(), QtGui.QPen(QtGui.QColor("blue"), 1.0))
-        self.robot_item = self.scene.addEllipse(-0.3, -0.3, 0.6, 0.6, QtGui.QPen(QtGui.QColor("red")), QtGui.QBrush(QtGui.QColor("red")))
-        self.heading_item = self.scene.addLine(0, 0, 0, 0, QtGui.QPen(QtGui.QColor("red"), 2))
-        self.target_item = self.scene.addEllipse(-0.2, -0.2, 0.4, 0.4, QtGui.QPen(QtGui.QColor("green")), QtGui.QBrush(QtGui.QColor("green")))
+        self.path_item = self.scene.addPath(QtGui.QPainterPath(), QtGui.QPen(QtGui.QColor("gray"), 0))
+        self.trail_item = self.scene.addPath(QtGui.QPainterPath(), QtGui.QPen(QtGui.QColor("blue"), 0))
+        self.robot_size = 0.25
+        self.robot_item = self.scene.addEllipse(
+            -self.robot_size,
+            -self.robot_size,
+            self.robot_size * 2,
+            self.robot_size * 2,
+            QtGui.QPen(QtGui.QColor("#d11d29"), 0),
+            QtGui.QBrush(QtGui.QColor("#ff4b5c")),
+        )
+        arrow_pen = QtGui.QPen(QtGui.QColor("#ff8c00"), 0)
+        arrow_pen.setJoinStyle(QtCore.Qt.RoundJoin)
+        self.heading_item = self.scene.addPath(
+            QtGui.QPainterPath(),
+            arrow_pen,
+            QtGui.QBrush(QtGui.QColor("#ff8c00")),
+        )
+        self.target_radius = 0.08
+        self.target_item = self.scene.addEllipse(
+            -self.target_radius,
+            -self.target_radius,
+            self.target_radius * 2,
+            self.target_radius * 2,
+            QtGui.QPen(QtGui.QColor("green")),
+            QtGui.QBrush(QtGui.QColor("green")),
+        )
 
-        self.path_browse_btn.clicked.connect(self._browse_path)
-        self.path_load_btn.clicked.connect(self._load_path)
+        self.path_load_btn.clicked.connect(self._load_path_from_text)
+        self.path_export_btn.clicked.connect(self._export_path_to_mcu)
         self.origin_btn.clicked.connect(self._update_origin)
+        self.copy_origin_btn.clicked.connect(self._copy_origin_macro) # 缁戝畾浜嬩欢
         self.reset_pose_btn.clicked.connect(self._reset_position)
         self.port_refresh_btn.clicked.connect(self._refresh_serial_ports)
         self.serial_toggle_btn.clicked.connect(self._toggle_serial)
@@ -253,6 +509,77 @@
         self._set_controls_enabled(False)
         self._update_serial_ui()
 
+    def _copy_origin_macro(self):
+        text = self.origin_text.text().strip()
+        if not text:
+            QtWidgets.QMessageBox.warning(self, "澶嶅埗", "鍘熺偣杈撳叆妗嗕负绌�")
+            return
+        
+        # 鏍煎紡: lat_val,N,lon_val,E,alt
+        # 渚嬪: 3949.9120,N,11616.8544,E,47.5
+        parts = [p.strip() for p in text.split(",")]
+        if len(parts) < 4:
+            QtWidgets.QMessageBox.warning(self, "澶嶅埗", "鍘熺偣鏍煎紡閿欒锛岄渶鑷冲皯鍖呭惈缁忕含搴�")
+            return
+            
+        try:
+            # 瑙f瀽 DDMM.MMMM 鏍煎紡杞� DD.DDDD
+            # 绾害
+            lat_raw = parts[0]
+            lat_deg_int = int(float(lat_raw) / 100)
+            lat_min = float(lat_raw) % 100
+            lat_dd = lat_deg_int + lat_min / 60.0
+            if parts[1].upper() == 'S':
+                lat_dd = -lat_dd
+                
+            # 缁忓害
+            lon_raw = parts[2]
+            lon_deg_int = int(float(lon_raw) / 100)
+            lon_min = float(lon_raw) % 100
+            lon_dd = lon_deg_int + lon_min / 60.0
+            if parts[3].upper() == 'W':
+                lon_dd = -lon_dd
+                
+            # 楂樺害
+            alt = "0.0"
+            if len(parts) >= 5:
+                alt = parts[4]
+                
+            macro = (
+                f"#define MC_CFG_ORIGIN_LAT_DEG            ({lat_dd:.6f})\n"
+                f"#define MC_CFG_ORIGIN_LON_DEG            ({lon_dd:.6f})\n"
+                f"#define MC_CFG_ORIGIN_ALT_M              ({alt})"
+            )
+            
+            clipboard = QtWidgets.QApplication.clipboard()
+            clipboard.setText(macro)
+            QtWidgets.QMessageBox.information(self, "澶嶅埗", "宸插鍒跺畯瀹氫箟鍒板壀璐存澘锛�")
+            
+        except ValueError as e:
+            QtWidgets.QMessageBox.warning(self, "澶嶅埗", f"鏁板�艰В鏋愰敊璇�: {e}")
+
+    def _handle_user_pan(self):
+        if self._auto_follow:
+            self.follow_checkbox.blockSignals(True)
+            self.follow_checkbox.setChecked(False)
+            self.follow_checkbox.blockSignals(False)
+            self._auto_follow = False
+
+    def _on_follow_toggled(self, checked: bool):
+        self._auto_follow = checked
+        if checked and self.pose_status:
+            self.view.centerOn(self.pose_status.east, -self.pose_status.north)
+
+    def _clear_trail(self):
+        """娓呴櫎鍦板浘涓婄殑杞ㄨ抗鐐�"""
+        self.trail_points = []
+        # Update scene to remove trail
+        self._update_scene()
+
+    def _update_mouse_pos(self, east: float, scene_y: float):
+        north = -scene_y
+        self.mouse_pos_label.setText(f"E: {east: .2f}  N: {north: .2f}")
+
     def _refresh_serial_ports(self, initial_uart2: Optional[str] = None, initial_uart5: Optional[str] = None):
         ports: List[str] = []
         if list_ports:
@@ -283,17 +610,69 @@
         self._last_ports = ports
 
     def _set_controls_enabled(self, enabled: bool):
-        for widget in (
-            self.path_line,
-            self.path_browse_btn,
-            self.path_load_btn,
-            self.origin_edit,
-            self.origin_btn,
-            self.reset_pose_btn,
-        ):
-            widget.setEnabled(enabled)
-        self.path_line.setReadOnly(not enabled)
-        self.origin_edit.setReadOnly(not enabled)
+        self.reset_pose_btn.setEnabled(enabled)
+        self.origin_text.setReadOnly(False)
+        self.origin_btn.setEnabled(True)
+
+    def _load_saved_path_text(self):
+        if PATH_TEXT_FILE.exists():
+            try:
+                text = PATH_TEXT_FILE.read_text(encoding="utf-8")
+            except Exception:
+                text = ""
+            self.path_text.setPlainText(text)
+            points = parse_path_text(text)
+            if points:
+                self.path_points = points
+
+    def _update_path_text_from_points(self, points: List[Tuple[float, float]]):
+        if not points:
+            self.path_text.clear()
+            return
+        text = ";".join(f"{x:.2f},{y:.2f}" for x, y in points) + ";"
+        self.path_text.setPlainText(text)
+
+    def _save_path_text(self, text: str):
+        try:
+            PATH_TEXT_FILE.write_text(text.strip(), encoding="utf-8")
+        except Exception as exc:
+            QtWidgets.QMessageBox.warning(self, "璺緞", f"淇濆瓨璺緞鏂囨湰澶辫触: {exc}")
+
+    def _load_saved_pose(self):
+        pose = self.state_store.get_pose()
+        if pose:
+            self.pos_spin[0].setValue(pose[0])
+            self.pos_spin[1].setValue(pose[1])
+            self.pos_spin[2].setValue(pose[2])
+            self.pos_spin[3].setValue(pose[3])
+
+    def _load_path_from_text(self):
+        text = self.path_text.toPlainText().strip()
+        points = parse_path_text(text)
+        if not points:
+            QtWidgets.QMessageBox.warning(self, "璺緞", "鏈В鏋愬埌鏈夋晥鐨勮建杩圭偣锛岃妫�鏌ヨ緭鍏ユ牸寮忋��")
+            return
+        self.path_points = points
+        self.trail_points.clear()
+        self._save_path_text(text)
+        self._init_scene()
+        self._refresh_view()
+        QtWidgets.QMessageBox.information(self, "璺緞", f"宸插姞杞� {len(points)} 涓矾寰勭偣銆�")
+
+    def _export_path_to_mcu(self):
+        text = self.path_text.toPlainText().strip()
+        points = parse_path_text(text)
+        if not points:
+            QtWidgets.QMessageBox.warning(self, "瀵煎嚭", "娌℃湁鍙鍑虹殑璺緞鐐癸紝璇峰厛鍔犺浇璺緞銆�")
+            return
+        formatted = format_path_for_mcu(points)
+        clipboard = QtWidgets.QApplication.clipboard()
+        clipboard.setText(formatted)
+        try:
+            PATH_EXPORT_FILE.write_text(formatted, encoding="utf-8")
+        except Exception as exc:
+            QtWidgets.QMessageBox.warning(self, "瀵煎嚭", f"鍐欏叆鏂囦欢澶辫触: {exc}")
+        QtWidgets.QMessageBox.information(self, "瀵煎嚭", "宸插鍒跺埌鍓创鏉匡紝骞跺啓鍏�:\n" + str(PATH_EXPORT_FILE))
 
     def _update_serial_ui(self):
         self.serial_toggle_btn.setText("鍏抽棴涓插彛" if self._serial_open else "鎵撳紑涓插彛")
@@ -357,37 +736,22 @@
             self._close_serial()
         super().closeEvent(event)
 
-    def _browse_path(self):
-        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self, "閫夋嫨璺緞鏂囦欢", str(Path.cwd()), "璺緞鏂囦欢 (*.txt *.json);;鎵�鏈夋枃浠� (*)")
-        if file_name:
-            self.path_line.setText(file_name)
-
-    def _load_path(self):
-        file_text = self.path_line.text().strip()
-        if not file_text:
-            QtWidgets.QMessageBox.warning(self, "璺緞", "璇峰厛閫夋嫨璺緞鏂囦欢銆�")
-            return
-        try:
-            new_points = load_path_points(Path(file_text))
-        except Exception as exc:
-            QtWidgets.QMessageBox.warning(self, "璺緞", f"鍔犺浇澶辫触: {exc}")
-            return
-        if not new_points:
-            QtWidgets.QMessageBox.warning(self, "璺緞", "鏈В鏋愬埌鏈夋晥璺緞鐐广��")
-            return
-        self.path_points = new_points
-        self._init_scene()
-        QtWidgets.QMessageBox.information(self, "璺緞", f"宸插姞杞� {len(new_points)} 涓矾寰勭偣銆�")
-
     def _update_origin(self):
-        gga = self.origin_edit.text().strip()
-        if not gga:
-            QtWidgets.QMessageBox.warning(self, "鍘熺偣", "璇疯緭鍏� GGA 瀛楃涓层��")
+        text = self.origin_text.text().strip()
+        if not text:
+            QtWidgets.QMessageBox.warning(self, "鍘熺偣", "璇疯緭鍏ュ潗鏍囷紝鏍煎紡锛歭at,N,lon,E[,alt]")
             return
-        if self.simulator.update_origin(gga):
-            QtWidgets.QMessageBox.information(self, "鍘熺偣", "鍘熺偣宸叉洿鏂般��")
-        else:
-            QtWidgets.QMessageBox.warning(self, "鍘熺偣", "鍘熺偣鏇存柊澶辫触锛岃妫�鏌ユ牸寮忋��")
+        template = self.simulator.config.origin_gga or DEFAULT_ORIGIN_GGA
+        try:
+            gga = build_gga_from_short_origin(text, template)
+        except ValueError as exc:
+            QtWidgets.QMessageBox.warning(self, "鍘熺偣", f"鏍煎紡閿欒: {exc}")
+            return
+        self.simulator.update_origin(gga)
+        self.simulator.config.origin_gga = gga
+        self.origin_text.setText(gga_to_short_origin(gga))
+        self.state_store.set_origin(gga)
+        QtWidgets.QMessageBox.information(self, "鍘熺偣", "鍘熺偣宸叉洿鏂般��")
 
     def _reset_position(self):
         east = self.pos_spin[0].value()
@@ -395,11 +759,12 @@
         up = self.pos_spin[2].value()
         heading = self.pos_spin[3].value()
         self.simulator.reset_state(east, north, up, heading)
+        self.state_store.set_pose(east, north, up, heading)
         QtWidgets.QMessageBox.information(self, "浣嶇疆", "宸查噸缃豢鐪熺姸鎬併��")
 
     def _init_scene(self):
         if not self.path_points:
-            self.scene.setSceneRect(-10, -10, 20, 20)
+            self.scene.setSceneRect(-250, -250, 500, 500)
             return
         path = QtGui.QPainterPath()
         first = True
@@ -414,8 +779,22 @@
             else:
                 path.lineTo(px, -py)
         self.path_item.setPath(path)
-        rect = QtCore.QRectF(min(xs) - 2, min(ys) - 2, (max(xs) - min(xs)) + 4, (max(ys) - min(ys)) + 4)
-        self.scene.setSceneRect(rect)
+        min_x = min(xs)
+        max_x = max(xs)
+        min_y = min(ys)
+        max_y = max(ys)
+        width = max_x - min_x
+        height = max_y - min_y
+        pad_x = max(10.0, width * 0.75)
+        pad_y = max(10.0, height * 0.75)
+        scene_rect = QtCore.QRectF(
+            min_x - pad_x,
+            min_y - pad_y,
+            (width if width > 0 else 1.0) + pad_x * 2.0,
+            (height if height > 0 else 1.0) + pad_y * 2.0,
+        )
+        self.scene.setSceneRect(scene_rect)
+        self.view.fitInView(scene_rect, QtCore.Qt.KeepAspectRatio)
 
     def _append_log(self, text: str):
         self.log_view.appendPlainText(text)
@@ -515,13 +894,81 @@
             pose = self.pose_status
             x = pose.east
             y = -pose.north
-            self.robot_item.setRect(x - 0.3, y - 0.3, 0.6, 0.6)
-            heading_rad = math.radians(pose.heading_deg)
-            dx = math.cos(heading_rad)
-            dy = math.sin(heading_rad)
-            self.heading_item.setLine(x, y, x + dx, y - dy)
-            self.target_item.setRect(pose.target_east - 0.2, -pose.target_north - 0.2, 0.4, 0.4)
-            self.view.centerOn(x, y)
+            size = self.robot_size
+            self.robot_item.setRect(x - size, y - size, size * 2, size * 2)
+            
+            # 缁樺埗鑸悜瑙掔澶达紙涓庤溅浣撳垎绂荤殑姗欒壊鎸囧悜绠ご锛�
+            # pose.heading_deg 涓虹綏鐩樿锛堝寳=0掳锛岄『鏃堕拡涓烘锛夛紝闇�杞崲鍒版暟瀛﹀潗鏍囷紙涓�=0掳锛岄�嗘椂閽堜负姝o級
+            heading_rad = math.radians(90.0 - pose.heading_deg)
+            dir_x = math.cos(heading_rad)
+            dir_y = -math.sin(heading_rad)  # 瑙嗗浘鍧愭爣 Y 鍚戜笅锛屽洜姝ゅ彇鍙�
+            perp_x = -dir_y
+            perp_y = dir_x
+
+            # 灏勭嚎鍙傛暟
+            offset = 0.25  # 绠ご绂昏溅浣撲腑蹇冪殑鍋忕Щ锛岄伩鍏嶉伄鎸�
+            shaft_length = 0.85
+            shaft_half_width = 0.07
+            head_length = 0.45
+            head_half_width = 0.28
+
+            start = (
+                x + dir_x * offset,
+                y + dir_y * offset,
+            )
+            shaft_end = (
+                start[0] + dir_x * shaft_length,
+                start[1] + dir_y * shaft_length,
+            )
+            tip = (
+                shaft_end[0] + dir_x * head_length,
+                shaft_end[1] + dir_y * head_length,
+            )
+
+            points = [
+                (
+                    start[0] + perp_x * shaft_half_width,
+                    start[1] + perp_y * shaft_half_width,
+                ),
+                (
+                    shaft_end[0] + perp_x * shaft_half_width,
+                    shaft_end[1] + perp_y * shaft_half_width,
+                ),
+                (
+                    shaft_end[0] + perp_x * head_half_width,
+                    shaft_end[1] + perp_y * head_half_width,
+                ),
+                tip,
+                (
+                    shaft_end[0] - perp_x * head_half_width,
+                    shaft_end[1] - perp_y * head_half_width,
+                ),
+                (
+                    shaft_end[0] - perp_x * shaft_half_width,
+                    shaft_end[1] - perp_y * shaft_half_width,
+                ),
+                (
+                    start[0] - perp_x * shaft_half_width,
+                    start[1] - perp_y * shaft_half_width,
+                ),
+            ]
+
+            arrow_path = QtGui.QPainterPath()
+            arrow_path.moveTo(*points[0])
+            for pt in points[1:]:
+                arrow_path.lineTo(*pt)
+            arrow_path.closeSubpath()
+
+            self.heading_item.setPath(arrow_path)
+            target_size = self.target_radius
+            self.target_item.setRect(
+                pose.target_east - target_size,
+                -pose.target_north - target_size,
+                target_size * 2,
+                target_size * 2,
+            )
+            if self._auto_follow:
+                self.view.centerOn(x, y)
 
 
 def parse_args() -> argparse.Namespace:
@@ -542,12 +989,26 @@
 
 def main():
     args = parse_args()
+    
+    # 浼樺厛浠� dashboard_state.json 鍔犺浇淇濆瓨鐨勫師鐐瑰拰鍒濆浣嶇疆
+    state_store = DashboardStateStore(STATE_FILE)
+    saved_origin = state_store.get_origin()
+    saved_pose = state_store.get_pose()
+    
+    origin_gga = saved_origin if saved_origin else args.origin
+    if saved_pose:
+        initial_enu = (saved_pose[0], saved_pose[1], saved_pose[2])
+        initial_heading = saved_pose[3]
+    else:
+        initial_enu = (args.east, args.north, args.up)
+        initial_heading = args.heading
+    
     cfg = HitlConfig(
         uart2_port=args.uart2,
         uart5_port=args.uart5,
-        origin_gga=args.origin,
-        initial_enu=(args.east, args.north, args.up),
-        initial_heading_deg=args.heading,
+        origin_gga=origin_gga,
+        initial_enu=initial_enu,
+        initial_heading_deg=initial_heading,
         gps_baudrate=args.gps_baud,
         log_baudrate=args.log_baud,
     )

--
Gitblit v1.10.0