import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import requests import json import time import threading import csv import os import math from datetime import datetime # ========= CONFIG HTTP ========== BASE_URL = "https://192.168.110.30:8081" ROBOT_APP_URL = "https://192.168.110.30:8081" MOVE_ENDPOINT = "/api/RobotManager/MoveToNode" ACTION_ENDPOINT = "/api/RobotManager/InstantActions" STATE_ENDPOINT = "/api/RobotManager/State" MARKER_DETECTION_POSE_ENDPOINT = "/api/marker-detection/pose/current" MARKER_DETECTION_ENABLE_ENDPOINT = "/api/marker-detection/detection/enable" VERIFY_SSL = False # self-signed cert nội bộ if not VERIFY_SSL: import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) class HikQrReader: """ Class đọc dữ liệu QR từ Robot App (device hik-qr-001) qua marker-detection API. Giữ tên class cũ để không ảnh hưởng luồng UI hiện tại. """ def __init__(self, callback=None): self.current_pose = None self.current_error = 0.0 self.is_enabled = False self.callback = callback self.position_data = [] # Lưu các đo lường self.node_data = {} # {node_name: [(pose, error), ...]} self.reading_thread = None self.stop_reading = False # Bắt đầu luồng để định kỳ lấy dữ liệu từ HTTP self.start_http_polling() def start_http_polling(self): """Bắt đầu polling dữ liệu từ HTTP server trên robot""" self.reading_thread = threading.Thread(target=self._http_polling_loop, daemon=True) self.reading_thread.start() def _http_polling_loop(self): """Luồng định kỳ lấy dữ liệu marker pose từ Robot App""" last_pose = None error_count = 0 while not self.stop_reading: try: response = requests.get( f"{ROBOT_APP_URL}{MARKER_DETECTION_POSE_ENDPOINT}", timeout=2, verify=VERIFY_SSL ) if response.status_code == 200: data = response.json() pose = data.get('pose') or {} position = pose.get('position') or {} orientation = pose.get('orientation') or {} if position and orientation: x = float(position.get('x', 0.0)) y = float(position.get('y', 0.0)) # Chuyển quaternion -> yaw (theta) theo chuẩn ROS qx = float(orientation.get('x', 0.0)) qy = float(orientation.get('y', 0.0)) qz = float(orientation.get('z', 0.0)) qw = float(orientation.get('w', 1.0)) siny_cosp = 2.0 * (qw * qz + qx * qy) cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz) theta = math.atan2(siny_cosp, cosy_cosp) pose_tuple = (x, y, theta) if pose_tuple != last_pose: self.current_pose = pose_tuple self.current_error = (x**2 + y**2) ** 0.5 self.is_enabled = True self.position_data.append({ 'timestamp': datetime.now(), 'pose': self.current_pose, 'error': self.current_error }) if self.callback: try: self.callback( f"HIK-QR-001: X={x:.4f}, Y={y:.4f}, θ={theta:.4f}, Error={self.current_error:.4f}m" ) except: pass # Ignore callback errors if UI not ready last_pose = pose_tuple error_count = 0 # Reset error count on success elif response.status_code == 404: # Marker detection chưa có pose hợp lệ self.current_pose = None else: error_count += 1 except requests.exceptions.RequestException as e: error_count += 1 # Only log error once every 10 attempts to avoid spam if error_count == 1 and self.callback: try: self.callback("⚠ Không đọc được marker pose từ Robot App (hik-qr-001).") except: pass except Exception as e: if self.callback: try: self.callback(f"❌ Lỗi lấy QR data từ hik-qr-001: {e}") except: pass time.sleep(0.1) # Poll mỗi 100ms def enable_hik_qr(self, enabled=True): """Bật/tắt marker detection trong Robot App (nguồn hik-qr-001).""" self.is_enabled = enabled try: endpoint = f"{ROBOT_APP_URL}{MARKER_DETECTION_ENABLE_ENDPOINT}" response = requests.post(endpoint, json={'enable': enabled}, timeout=2, verify=VERIFY_SSL) status = "enabled" if enabled else "disabled" if response.status_code == 200: if self.callback: self.callback(f"✅ Marker detection ({'hik-qr-001'}) {status}") else: if self.callback: self.callback(f"⚠ Marker detection {status} (HTTP {response.status_code})") except requests.exceptions.ConnectionError: if self.callback: self.callback("⚠ Không thể kết nối Robot App để bật/tắt marker detection.") except Exception as e: if self.callback: self.callback(f"⚠ Lỗi khi bật/tắt marker detection: {str(e)[:100]}") def record_node_data(self, node_name, trial_number): """Ghi lại dữ liệu cho node này vào dict - CHỈ 1 SNAPSHOT DUY NHẤT""" if self.current_pose and self.current_error is not None: if node_name not in self.node_data: self.node_data[node_name] = [] # Kiểm tra xem trial này đã ghi chưa - tránh trùng lặp existing = [d for d in self.node_data[node_name] if d['trial'] == trial_number] if existing: if self.callback: self.callback(f"⚠ Node {node_name} trial {trial_number} đã được ghi, bỏ qua\n") return False # Snapshot giá trị hiện tại (chỉ lấy 1 lần) snapshot_pose = self.current_pose snapshot_error = self.current_error self.node_data[node_name].append({ 'trial': trial_number, 'pose': snapshot_pose, 'error': snapshot_error, 'timestamp': datetime.now() }) if self.callback: self.callback(f"📍 Ghi dữ liệu node {node_name} (trial {trial_number}): Error={snapshot_error:.4f}m\n") return True else: if self.callback: self.callback(f"⚠ Chưa nhận dữ liệu từ HIK-QR-001 cho node {node_name}\n") return False def get_node_average_error(self, node_name): """Tính sai số trung bình cho node""" if node_name not in self.node_data or not self.node_data[node_name]: return 0.0 errors = [d['error'] for d in self.node_data[node_name]] return sum(errors) / len(errors) def save_to_csv(self, filename=None): """Lưu dữ liệu vào file CSV""" if filename is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"hik_qr_positioning_data_{timestamp}.csv" try: with open(filename, 'w', newline='', encoding='utf-8') as csvfile: fieldnames = ['Node', 'Trial', 'X(m)', 'Y(m)', 'Theta(rad)', 'Error(m)', 'Avg_Error(m)', 'Timestamp'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for node_name in sorted(self.node_data.keys()): avg_error = self.get_node_average_error(node_name) for data in self.node_data[node_name]: x, y, theta = data['pose'] writer.writerow({ 'Node': node_name, 'Trial': data['trial'], 'X(m)': f"{x:.6f}", 'Y(m)': f"{y:.6f}", 'Theta(rad)': f"{theta:.6f}", 'Error(m)': f"{data['error']:.6f}", 'Avg_Error(m)': f"{avg_error:.6f}", 'Timestamp': data['timestamp'].strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] }) if self.callback: self.callback(f"✅ Dữ liệu đã lưu vào: {filename}\n") return filename except Exception as e: if self.callback: self.callback(f"❌ Lỗi khi lưu CSV: {e}\n") return None def clear_data(self): """Xóa dữ liệu đã ghi""" self.node_data = {} self.position_data = [] class RobotApiClient: """Class API client để điều khiển robot qua HTTP""" def __init__(self, base_url=BASE_URL, verify_ssl=VERIFY_SSL): self.base_url = base_url self.verify_ssl = verify_ssl # -------- MoveToNode ---------- def move_to_node(self, robot_id, node_name, final_action=None): if final_action is None: final_action = [] url = self.base_url + MOVE_ENDPOINT body = { "robotId": robot_id, "nodeName": node_name, "finalAction": final_action } resp = requests.post(url, json=body, verify=self.verify_ssl) return resp, body def wait_for_order_finished(self, robot_id, timeout=60, poll_interval=0.2, callback=None): """ Poll State API cho đến khi nhận được "[order] Finished Order" của order MỚI. Chiến lược: Lưu order_id hiện tại, chờ order_id mới + Finished Order. Nếu order_id không đổi sau 1.5s → robot đã ở node đích, coi như hoàn thành. Adaptive polling: Poll nhanh lúc đầu (0.1s), sau đó chậm dần (0.3s). Returns: True nếu thành công, False nếu timeout. """ start_time = time.time() initial_order_id = None initial_has_finished = False min_elapsed_for_empty_path = 0.7 # Tránh trả sớm khi server vừa cập nhật trạng thái # === Bước 1: Lấy order_id hiện tại (nếu có) === try: resp, _ = self.get_state(robot_id) if resp.status_code == 200: state_data = resp.json() info_list = state_data.get("data", {}).get("information", []) for info_item in info_list: if info_item.get("infoType") == "order": refs = info_item.get("infoReferences", []) for ref in refs: if ref.get("referenceKey") == "order_id": initial_order_id = ref.get("referenceValue") break desc = info_item.get("infoDescription", "") initial_has_finished = "[order] Finished Order" in desc break except Exception as e: if callback: callback(f"⚠ Lỗi khi lấy initial order_id: {e}\n") if callback: callback(f"📌 Initial order_id: {initial_order_id} (finished: {initial_has_finished})\n") # === Bước 2: Chờ order_id MỚI + Finished Order === no_change_timeout = 1.5 # Nếu order_id không đổi sau 1.5s → robot đã ở đích poll_count = 0 order_changed = False # Flag để track xem order_id đã thay đổi chưa while True: elapsed = time.time() - start_time if elapsed > timeout: if callback: callback(f"⚠ Timeout {timeout}s khi chờ Finished Order\n") return False try: resp, _ = self.get_state(robot_id) if resp.status_code == 200: state_data = resp.json() data = state_data.get("data", {}) # --- CASE Empty node/edge state: coi như đã đến đích --- # Một số response không cung cấp "[order] Finished Order", nhưng khi # nodeStates/edgeStates rỗng và robot đứng yên thì cho phép gửi order tiếp. node_states = data.get("nodeStates", data.get("node_states")) edge_states = data.get("edgeStates", data.get("edge_states")) safety = data.get("safetyState", data.get("safety_state", {})) or {} e_stop = safety.get("eStop", safety.get("e_stop")) vel = data.get("velocity", {}) or {} vx = float(vel.get("vx", 0) or 0) vy = float(vel.get("vy", 0) or 0) omega = float(vel.get("omega", 0) or 0) is_empty_path = ( isinstance(node_states, list) and isinstance(edge_states, list) and len(node_states) == 0 and len(edge_states) == 0 ) is_safety_ok = (e_stop is None or e_stop == "NONE") is_velocity_stop = (abs(vx) <= 1e-6 and abs(vy) <= 1e-6 and abs(omega) <= 1e-6) if ( elapsed >= min_elapsed_for_empty_path and is_empty_path and is_safety_ok and is_velocity_stop ): if callback: callback( f"✅ nodeStates/edgeStates rỗng => coi như hoàn thành (sau {elapsed:.1f}s)\n" ) return True info_list = data.get("information", []) for info_item in info_list: if info_item.get("infoType") == "order": # Lấy order_id current_order_id = None refs = info_item.get("infoReferences", []) for ref in refs: if ref.get("referenceKey") == "order_id": current_order_id = ref.get("referenceValue") break # Kiểm tra Finished Order desc = info_item.get("infoDescription", "") has_finished = "[order] Finished Order" in desc # Track nếu order_id đã thay đổi if current_order_id and current_order_id != initial_order_id: order_changed = True # CASE 1: order_id MỚI và đã Finished → Done! if current_order_id and current_order_id != initial_order_id and has_finished: if callback: callback(f"✅ Order mới hoàn thành sau {elapsed:.1f}s (order_id: {current_order_id})\n") return True # CASE 2: order_id KHÔNG ĐỔI sau 1.5s + đã finished → Robot đã ở đích! # CHỈ áp dụng nếu order CHƯA BAO GIỜ đổi (tránh trường hợp order đổi rồi đổi lại) if (elapsed > no_change_timeout and not order_changed and current_order_id == initial_order_id and has_finished): if callback: callback(f"✅ Robot đã ở node đích, không cần di chuyển (sau {elapsed:.1f}s)\n") return True break except Exception as e: if callback: callback(f"⚠ Lỗi khi poll State: {e}\n") # Adaptive polling: nhanh lúc đầu, chậm sau poll_count += 1 if poll_count < 10: time.sleep(0.1) # 10 lần đầu: poll mỗi 0.1s (nhanh!) else: time.sleep(0.3) # Sau đó: poll mỗi 0.3s def move_multiple_nodes( self, robot_id, nodes, delay=1.0, callback=None, node_actions=None, use_polling=True, poll_timeout=60, ): """ Gửi lần lượt nhiều node. node_actions: dict[nodeName] = [ { "type": ..., "height": ... }, ... ] use_polling: True = chờ "Finished Order", False = dùng delay cố định poll_timeout: timeout khi chờ Finished Order (giây) """ if node_actions is None: node_actions = {} for i, node in enumerate(nodes, start=1): if callback: callback(f"===== [{i}/{len(nodes)}] MoveToNode: {node} =====\n") # Gửi MoveToNode resp, body = self.move_to_node(robot_id, node) if callback: callback(self._format_response("POST", MOVE_ENDPOINT, resp, body)) # === Chờ Finished Order (nếu bật polling) === if use_polling: if callback: callback(f"⏳ Đang chờ Finished Order cho node '{node}'...\n") # Delay nhỏ để đảm bảo server đã xử lý MoveToNode request time.sleep(0.2) success = self.wait_for_order_finished( robot_id, timeout=poll_timeout, poll_interval=0.5, callback=callback ) if not success: if callback: callback(f"⚠ Không nhận được Finished Order, tiếp tục node tiếp theo\n") else: # Dùng delay cố định (chế độ cũ) if delay > 0 and i < len(nodes): if callback: callback(f"--- Wait {delay} seconds ---\n") time.sleep(delay) # === Gửi action cho node này (nếu có) === actions_for_node = node_actions.get(node, []) for act in actions_for_node: act_type = act.get("type") if not act_type: continue if callback: callback(f"--> Node '{node}' có action: {act_type}\n") if act_type == "liftCameraByHeight": height = act.get("height") if height is None: if callback: callback("⚠ Thiếu height cho liftCameraByHeight, bỏ qua.\n") continue a_resp, a_body = self.call_action_lift_camera_by_height( robot_id, height ) if callback: callback(self._format_response("POST", ACTION_ENDPOINT, a_resp, a_body)) else: if callback: callback(f"⚠ Action '{act_type}' chưa implement trong client.\n") # -------- InstantActions ---------- def call_action_lift_camera_by_height(self, robot_id, height): url = self.base_url + ACTION_ENDPOINT action_body = { "robotId": robot_id, "Action": { "actionType": "liftCameraByHeight", "actionId": "liftCameraByHeight", "actionDescription": "liftCamera height (unit: m)", "blockingType": "NONE", "actionParameters": [ {"key": "ROBOT_ID", "value": robot_id}, {"key": "HEIGHT", "value": str(height)}, ] } } resp = requests.post(url, json=action_body, verify=self.verify_ssl) return resp, action_body # -------- Cancel Move ---------- def cancel_move(self, robot_id): url = f"{self.base_url}{MOVE_ENDPOINT}/{robot_id}" resp = requests.delete(url, verify=self.verify_ssl) return resp, None # -------- Get State ---------- def get_state(self, robot_id): url = f"{self.base_url}{STATE_ENDPOINT}/{robot_id}" resp = requests.get(url, verify=self.verify_ssl) return resp, None # -------- Helper format ---------- @staticmethod def _format_response(method, endpoint, response, body=None): parts = [f"{method} {endpoint} -> Status: {response.status_code}\n"] if body is not None: try: parts.append("Request body:\n") parts.append(json.dumps(body, indent=2, ensure_ascii=False)) parts.append("\n") except Exception: pass try: js = response.json() parts.append("Response JSON:\n") parts.append(json.dumps(js, indent=2, ensure_ascii=False)) parts.append("\n") except Exception: parts.append("Response text:\n") parts.append(response.text) parts.append("\n") parts.append("-" * 50 + "\n") return "".join(parts) class RobotApp(tk.Tk): def __init__(self): super().__init__() self.title("🤖 Robot Manager Control Panel") self.geometry("1450x900") self.minsize(1300, 800) self.client = RobotApiClient() self.gls621 = HikQrReader(callback=self.append_log) # Khởi tạo reader cho hik-qr-001 # Premium color scheme - Dark theme with vibrant accents self.colors = { 'bg': '#0a0e1a', # Very dark blue 'bg_light': '#131827', # Lighter dark 'surface': '#1a1f35', # Card background 'surface_hover': '#252b45', # Hover state 'primary': '#6366f1', # Indigo 'primary_light': '#818cf8', # Light indigo 'success': '#22c55e', # Vibrant green 'success_dark': '#16a34a', # Dark green 'warning': '#f59e0b', # Orange 'warning_dark': '#d97706', # Dark orange 'danger': '#ef4444', # Red 'danger_dark': '#dc2626', # Dark red 'text': '#f8fafc', # Almost white 'text_dim': '#94a3b8', # Gray 'text_darker': '#64748b', # Darker gray 'border': '#334155', # Border color 'accent': '#8b5cf6', # Purple accent } self.configure(bg=self.colors['bg']) # Configure modern style style = ttk.Style(self) style.theme_use("clam") # Custom styles self._configure_styles(style) self.loop_running = False self.current_loop_trial = 0 # Đếm số lần loop self._create_widgets() # Bắt đầu update HIK-QR display self.after(100, self.update_gls621_display) def _configure_styles(self, style): """Configure premium custom styles with modern effects""" # Frame styles style.configure('Card.TFrame', background=self.colors['surface'], relief='raised', borderwidth=1) style.configure('TFrame', background=self.colors['bg']) # Label styles style.configure('TLabel', background=self.colors['bg'], foreground=self.colors['text'], font=('Segoe UI', 10)) style.configure('Title.TLabel', background=self.colors['surface'], foreground=self.colors['text'], font=('Segoe UI', 11, 'bold')) style.configure('Header.TLabel', background=self.colors['bg'], foreground=self.colors['text'], font=('Segoe UI', 14, 'bold')) # Button styles with gradient effects style.configure('Primary.TButton', background=self.colors['primary'], foreground='white', borderwidth=0, focuscolor='none', font=('Segoe UI', 10, 'bold'), padding=(16, 10)) style.map('Primary.TButton', background=[('active', self.colors['primary_light']), ('pressed', '#4f46e5')], foreground=[('active', 'white')]) style.configure('Success.TButton', background=self.colors['success'], foreground='white', borderwidth=0, font=('Segoe UI', 10, 'bold'), padding=(16, 10)) style.map('Success.TButton', background=[('active', '#4ade80'), ('pressed', self.colors['success_dark'])], foreground=[('active', 'white')]) style.configure('Danger.TButton', background=self.colors['danger'], foreground='white', borderwidth=0, font=('Segoe UI', 10, 'bold'), padding=(16, 10)) style.map('Danger.TButton', background=[('active', '#f87171'), ('pressed', self.colors['danger_dark'])], foreground=[('active', 'white')]) style.configure('Warning.TButton', background=self.colors['warning'], foreground='white', borderwidth=0, font=('Segoe UI', 10, 'bold'), padding=(16, 10)) style.map('Warning.TButton', background=[('active', '#fbbf24'), ('pressed', self.colors['warning_dark'])], foreground=[('active', 'white')]) # Entry styles with border style.configure('TEntry', fieldbackground='#1e293b', foreground=self.colors['text'], borderwidth=1, bordercolor=self.colors['border'], lightcolor=self.colors['border'], darkcolor=self.colors['border'], font=('Segoe UI', 10), padding=8) # LabelFrame styles with glow effect style.configure('Card.TLabelframe', background=self.colors['surface'], foreground=self.colors['text'], borderwidth=1, relief='solid', bordercolor=self.colors['border'], font=('Segoe UI', 11, 'bold')) style.configure('Card.TLabelframe.Label', background=self.colors['surface'], foreground=self.colors['primary_light'], font=('Segoe UI', 12, 'bold')) # Checkbutton styles style.configure('TCheckbutton', background=self.colors['surface'], foreground=self.colors['text'], font=('Segoe UI', 10)) def _create_widgets(self): # Premium header with gradient header_frame = tk.Frame(self, bg=self.colors['surface'], height=100) header_frame.pack(side=tk.TOP, fill=tk.X, padx=0, pady=0) header_frame.pack_propagate(False) # Add subtle border at bottom border = tk.Frame(header_frame, bg=self.colors['primary'], height=2) border.pack(side=tk.BOTTOM, fill=tk.X) # Title with icon title_label = tk.Label( header_frame, text="🤖 ROBOT CONTROL PANEL", bg=self.colors['surface'], fg=self.colors['text'], font=('Segoe UI', 22, 'bold') ) title_label.pack(side=tk.TOP, pady=(20, 5)) # Subtitle with better styling subtitle = tk.Label( header_frame, text="#Ngocquynobel", bg=self.colors['surface'], fg=self.colors['text_dim'], font=('Segoe UI', 11) ) subtitle.pack(side=tk.TOP, pady=(0, 10)) # Control bar with shadow effect control_frame = tk.Frame(self, bg=self.colors['surface'], highlightthickness=1, highlightbackground=self.colors['border']) control_frame.pack(side=tk.TOP, fill=tk.X, padx=20, pady=(20, 15)) control_inner = tk.Frame(control_frame, bg=self.colors['surface']) control_inner.pack(fill=tk.X, padx=20, pady=15) tk.Label( control_inner, text="🆔 Robot ID:", bg=self.colors['surface'], fg=self.colors['text'], font=('Segoe UI', 10, 'bold') ).grid(row=0, column=0, sticky="w", padx=(0, 10)) self.robot_id_var = tk.StringVar(value="I150") robot_entry = tk.Entry( control_inner, textvariable=self.robot_id_var, width=18, bg='#1e293b', fg=self.colors['text'], font=('Consolas', 11, 'bold'), relief='solid', borderwidth=1, highlightthickness=1, highlightcolor=self.colors['primary'], highlightbackground=self.colors['border'], insertbackground=self.colors['primary'] ) robot_entry.grid(row=0, column=1, padx=(0, 30), ipady=8) self.btn_get_state = ttk.Button( control_inner, text="📊 Get State", command=self.on_get_state, style='Warning.TButton' ) self.btn_get_state.grid(row=0, column=2, padx=8) self.btn_cancel_move = ttk.Button( control_inner, text="🛑 Cancel Move", command=self.on_cancel_move, style='Danger.TButton' ) self.btn_cancel_move.grid(row=0, column=3, padx=8) # Frame giữa với spacing tốt hơn mid_frame = ttk.Frame(self) mid_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=20, pady=(0, 20)) # --- Cột trái: Move + Action (với scrollbar) --- left_outer = ttk.Frame(mid_frame) left_outer.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=(0, 15)) # Canvas + Scrollbar cho cột trái left_canvas = tk.Canvas(left_outer, bg=self.colors['bg'], highlightthickness=0, width=520) left_scrollbar = ttk.Scrollbar(left_outer, orient="vertical", command=left_canvas.yview) left_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) left_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) left_canvas.configure(yscrollcommand=left_scrollbar.set) # Frame bên trong canvas left_frame = ttk.Frame(left_canvas) left_canvas_window = left_canvas.create_window((0, 0), window=left_frame, anchor="nw") # Update scroll region khi resize def on_left_frame_configure(event): left_canvas.configure(scrollregion=left_canvas.bbox("all")) left_frame.bind("", on_left_frame_configure) # Mouse wheel scroll def on_mousewheel(event): left_canvas.yview_scroll(int(-1*(event.delta/120)), "units") left_canvas.bind_all("", on_mousewheel) # MoveToNode Card with glow move_card = tk.Frame(left_frame, bg=self.colors['surface'], highlightthickness=1, highlightbackground=self.colors['border']) move_card.pack(fill=tk.X, pady=(0, 15)) move_labelframe = ttk.LabelFrame(move_card, text=" 📍 NAVIGATION ", style='Card.TLabelframe') move_labelframe.pack(fill=tk.X, padx=2, pady=2) move_inner = tk.Frame(move_labelframe, bg=self.colors['surface']) move_inner.pack(fill=tk.BOTH, padx=18, pady=15) # Single node tk.Label( move_inner, text="🎯 Single Node:", bg=self.colors['surface'], fg=self.colors['text'], font=('Segoe UI', 10, 'bold') ).grid(row=0, column=0, sticky="w", pady=(0, 10)) self.node_var = tk.StringVar(value="Node5") node_entry = tk.Entry( move_inner, textvariable=self.node_var, width=15, bg='#1e293b', fg=self.colors['text'], font=('Consolas', 10, 'bold'), relief='solid', borderwidth=1, highlightthickness=1, highlightcolor=self.colors['primary'], highlightbackground=self.colors['border'], insertbackground=self.colors['primary'] ) node_entry.grid(row=0, column=1, padx=10, pady=(0, 10), ipady=6) ttk.Button( move_inner, text="▶ Move", command=self.on_move_single, style='Primary.TButton' ).grid(row=0, column=2, padx=5, pady=(0, 5)) # Multi node section tk.Label( move_inner, text="🔗 Multiple Nodes (comma separated):", bg=self.colors['surface'], fg=self.colors['text'], font=('Segoe UI', 10, 'bold') ).grid(row=1, column=0, columnspan=3, sticky="w", pady=(15, 8)) self.nodes_multi_var = tk.StringVar(value="Node1, Node2, Node3, Node4, Node5, Node6, Node7, Node8, Node9, Node10, Node11, Node12, Node13, Node14, Node15, Node16, Node15, Node14, Node13 ,Node12, Node11, Node10, Node9, Node8, Node7, Node6, Node5, Node4, Node3, Node2") nodes_entry = tk.Entry( move_inner, textvariable=self.nodes_multi_var, width=55, bg='#1e293b', fg=self.colors['text'], font=('Consolas', 10), relief='solid', borderwidth=1, highlightthickness=1, highlightcolor=self.colors['primary'], highlightbackground=self.colors['border'], insertbackground=self.colors['primary'] ) nodes_entry.grid(row=2, column=0, columnspan=3, pady=(0, 12), sticky="ew", ipady=6) # Polling options với border polling_frame = tk.Frame(move_inner, bg=self.colors['bg_light'], highlightthickness=1, highlightbackground=self.colors['border']) polling_frame.grid(row=3, column=0, columnspan=3, sticky="ew", pady=(0, 12)) polling_inner = tk.Frame(polling_frame, bg=self.colors['bg_light']) polling_inner.pack(fill=tk.X, padx=12, pady=10) self.use_polling_var = tk.BooleanVar(value=True) polling_check = ttk.Checkbutton( polling_inner, text="⏱ Wait for 'Finished Order' (Smart Polling)", variable=self.use_polling_var ) polling_check.configure(style='TCheckbutton') polling_check.pack(anchor="w", pady=(0, 8)) # Timeout và delay options_frame = tk.Frame(polling_inner, bg=self.colors['bg_light']) options_frame.pack(fill=tk.X) tk.Label( options_frame, text="⏰ Timeout:", bg=self.colors['bg_light'], fg=self.colors['text_dim'], font=('Segoe UI', 9) ).pack(side=tk.LEFT, padx=(0, 8)) self.poll_timeout_var = tk.StringVar(value="60") timeout_entry = tk.Entry( options_frame, textvariable=self.poll_timeout_var, width=8, bg='#1e293b', fg=self.colors['text'], font=('Consolas', 10), relief='solid', borderwidth=1, highlightthickness=0, insertbackground=self.colors['primary'] ) timeout_entry.pack(side=tk.LEFT, padx=(0, 5), ipady=4) tk.Label( options_frame, text="s", bg=self.colors['bg_light'], fg=self.colors['text_darker'], font=('Segoe UI', 9) ).pack(side=tk.LEFT, padx=(0, 25)) tk.Label( options_frame, text="⏲ Delay:", bg=self.colors['bg_light'], fg=self.colors['text_dim'], font=('Segoe UI', 9) ).pack(side=tk.LEFT, padx=(0, 8)) self.delay_var = tk.StringVar(value="1.0") delay_entry = tk.Entry( options_frame, textvariable=self.delay_var, width=8, bg='#1e293b', fg=self.colors['text'], font=('Consolas', 10), relief='solid', borderwidth=1, highlightthickness=0, insertbackground=self.colors['primary'] ) delay_entry.pack(side=tk.LEFT, padx=(0, 5), ipady=4) tk.Label( options_frame, text="s", bg=self.colors['bg_light'], fg=self.colors['text_darker'], font=('Segoe UI', 9) ).pack(side=tk.LEFT) # === Node–Action mapping Card === action_card = tk.Frame(left_frame, bg=self.colors['surface'], highlightthickness=1, highlightbackground=self.colors['border']) action_card.pack(fill=tk.BOTH, expand=False, pady=(0, 15)) map_labelframe = ttk.LabelFrame(action_card, text=" ⚙️ NODE ACTIONS ", style='Card.TLabelframe') map_labelframe.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) map_inner = tk.Frame(map_labelframe, bg=self.colors['surface']) map_inner.pack(fill=tk.BOTH, expand=True, padx=18, pady=15) self.node_action_text = scrolledtext.ScrolledText( map_inner, width=55, height=8, font=("Cascadia Code", 9), bg='#0a0e1a', fg='#e2e8f0', insertbackground='#6366f1', relief='solid', borderwidth=1, highlightthickness=0, padx=10, pady=8 ) self.node_action_text.pack(fill=tk.BOTH, expand=True) # ví dụ mặc định example = ( # "Node1,liftCameraByHeight,0.45\n" # "Node14,liftCameraByHeight,0.75\n" # "Node19,liftCameraByHeight,0.55\n" # "Node15,liftCameraByHeight,0.35\n" # "Node2,liftCameraByHeight,0.2\n" # "Node4,liftCameraByHeight,0.55\n" # "Node12,liftCameraByHeight,0.45\n" # "Node20,liftCameraByHeight,0.35\n" # "Node11,liftCameraByHeight,0.65\n" # "Node3,liftCameraByHeight,0.35\n" ) self.node_action_text.insert(tk.END, example) tk.Label( map_inner, text="📝 Format: NodeName,actionType,param... (e.g., Node3,liftCameraByHeight,0.52)", bg=self.colors['surface'], fg=self.colors['text_dim'], font=('Segoe UI', 9), wraplength=450, justify='left' ).pack(anchor="w", pady=(8, 0)) # Buttons: chạy 1 lần & loop với spacing đẹp btn_frame = tk.Frame(left_frame, bg=self.colors['bg']) btn_frame.pack(fill=tk.X, pady=(8, 15)) ttk.Button( btn_frame, text="▶ Run Sequence", command=self.on_move_multi, style='Success.TButton' ).pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True) ttk.Button( btn_frame, text="🔁 Loop Mode", command=self.on_loop_multi, style='Primary.TButton' ).pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True) ttk.Button( btn_frame, text="⏹ Stop", command=self.on_stop_loop, style='Danger.TButton' ).pack(side=tk.LEFT, fill=tk.X, expand=True) # InstantActions Card với border instant_card = tk.Frame(left_frame, bg=self.colors['surface'], highlightthickness=1, highlightbackground=self.colors['border']) instant_card.pack(fill=tk.X) action_labelframe = ttk.LabelFrame(instant_card, text=" ⚡ INSTANT ACTIONS ", style='Card.TLabelframe') action_labelframe.pack(fill=tk.X, padx=2, pady=2) action_inner = tk.Frame(action_labelframe, bg=self.colors['surface']) action_inner.pack(fill=tk.BOTH, padx=18, pady=15) tk.Label( action_inner, text="Action Type:", bg=self.colors['surface'], fg=self.colors['text_dim'], font=('Segoe UI', 9) ).grid(row=0, column=0, sticky="w", pady=(0, 8)) self.inst_action_type_var = tk.StringVar() self.inst_action_type_combo = ttk.Combobox( action_inner, textvariable=self.inst_action_type_var, state="readonly", width=25, values=[ "liftCameraByHeight", ], font=('Segoe UI', 10) ) self.inst_action_type_combo.grid(row=0, column=1, columnspan=2, padx=5, pady=(0, 8), sticky="ew") self.inst_action_type_combo.current(0) tk.Label( action_inner, text="Height (m):", bg=self.colors['surface'], fg=self.colors['text_dim'], font=('Segoe UI', 9) ).grid(row=1, column=0, sticky="w") self.height_var = tk.StringVar(value="0.52") height_entry = tk.Entry( action_inner, textvariable=self.height_var, width=12, bg='#1e293b', fg=self.colors['text'], font=('Consolas', 10, 'bold'), relief='solid', borderwidth=1, highlightthickness=1, highlightcolor=self.colors['primary'], highlightbackground=self.colors['border'], insertbackground=self.colors['primary'] ) height_entry.grid(row=1, column=1, padx=10, ipady=6) ttk.Button( action_inner, text="🚀 Execute", command=self.on_instant_action, style='Warning.TButton' ).grid(row=1, column=2, padx=5) # HIK-QR Detector Card gls621_card = tk.Frame(left_frame, bg=self.colors['surface'], highlightthickness=1, highlightbackground=self.colors['border']) gls621_card.pack(fill=tk.X, pady=(15, 0)) gls621_labelframe = ttk.LabelFrame(gls621_card, text=" 🔍 HIK-QR-001 DETECTOR ", style='Card.TLabelframe') gls621_labelframe.pack(fill=tk.X, padx=2, pady=2) gls621_inner = tk.Frame(gls621_labelframe, bg=self.colors['surface']) gls621_inner.pack(fill=tk.BOTH, padx=18, pady=15) # Buttons to enable/disable HIK-QR gls621_btn_frame = tk.Frame(gls621_inner, bg=self.colors['surface']) gls621_btn_frame.pack(fill=tk.X, pady=(0, 12)) ttk.Button( gls621_btn_frame, text="✅ Enable HIK-QR", command=lambda: self.gls621.enable_hik_qr(True), style='Success.TButton' ).pack(side=tk.LEFT, padx=(0, 8), fill=tk.X, expand=True) ttk.Button( gls621_btn_frame, text="❌ Disable HIK-QR", command=lambda: self.gls621.enable_hik_qr(False), style='Danger.TButton' ).pack(side=tk.LEFT, fill=tk.X, expand=True) # Display current position and error tk.Label( gls621_inner, text="Current Position & Error:", bg=self.colors['surface'], fg=self.colors['text'], font=('Segoe UI', 9, 'bold') ).pack(anchor="w", pady=(8, 4)) self.gls621_display_text = tk.Label( gls621_inner, text="X: --- Y: --- θ: --- Error: ---", bg=self.colors['bg_light'], fg=self.colors['text'], font=('Consolas', 9), relief='solid', borderwidth=1, padx=10, pady=8, wraplength=450, justify='left' ) self.gls621_display_text.pack(fill=tk.X, pady=(0, 12)) # CSV export buttons csv_btn_frame = tk.Frame(gls621_inner, bg=self.colors['surface']) csv_btn_frame.pack(fill=tk.X, pady=(0, 8)) ttk.Button( csv_btn_frame, text="💾 Save to CSV", command=self.on_save_gls621_csv, style='Primary.TButton' ).pack(side=tk.LEFT, padx=(0, 8), fill=tk.X, expand=True) ttk.Button( csv_btn_frame, text="🗑 Clear Data", command=self.on_clear_gls621_data, style='Warning.TButton' ).pack(side=tk.LEFT, fill=tk.X, expand=True) # --- Cột phải: Log Terminal với glow --- right_frame = ttk.Frame(mid_frame) right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) log_card = tk.Frame(right_frame, bg=self.colors['surface'], highlightthickness=1, highlightbackground=self.colors['border']) log_card.pack(fill=tk.BOTH, expand=True) log_labelframe = ttk.LabelFrame(log_card, text=" 📝 CONSOLE LOG ", style='Card.TLabelframe') log_labelframe.pack(fill=tk.BOTH, expand=True, padx=2, pady=2) log_inner = tk.Frame(log_labelframe, bg=self.colors['surface']) log_inner.pack(fill=tk.BOTH, expand=True, padx=18, pady=15) self.log_text = scrolledtext.ScrolledText( log_inner, wrap=tk.WORD, height=35, font=("Cascadia Code", 9), bg='#0a0e1a', fg='#e2e8f0', insertbackground='#6366f1', relief='solid', borderwidth=1, highlightthickness=0, padx=12, pady=12 ) self.log_text.pack(fill=tk.BOTH, expand=True) # ------ Helpers GUI ------ def append_log(self, text: str): self.log_text.insert(tk.END, text) self.log_text.see(tk.END) def get_robot_id(self) -> str: rid = self.robot_id_var.get().strip() return rid or "I150" def _parse_nodes_and_delay(self): nodes_line = self.nodes_multi_var.get().strip() if not nodes_line: messagebox.showwarning("Thiếu thông tin", "Vui lòng nhập danh sách node.") return None, None nodes = [n.strip() for n in nodes_line.split(",") if n.strip()] if not nodes: messagebox.showwarning("Thiếu thông tin", "Danh sách node không hợp lệ.") return None, None try: delay = float(self.delay_var.get().strip()) except ValueError: delay = 1.0 return nodes, delay def _parse_node_actions(self): """ Đọc multi-line Node–Action mapping. Trả về dict[nodeName] = [ { "type": ..., "height": ... }, ... ] Ví dụ dòng: Node3,liftCameraByHeight,0.3 """ text = self.node_action_text.get("1.0", tk.END).strip() if not text: return {} node_actions = {} lines = text.splitlines() for idx, line in enumerate(lines, start=1): line = line.strip() if not line: continue parts = [p.strip() for p in line.split(",") if p.strip()] if len(parts) < 2: self.append_log(f"⚠ Dòng {idx} trong Node–Action mapping không hợp lệ: '{line}'\n") continue node_name = parts[0] action_type = parts[1] action_cfg = {"type": action_type} # xử lý param tuỳ loại action if action_type == "liftCameraByHeight": if len(parts) < 3: self.append_log( f"⚠ Dòng {idx} thiếu HEIGHT cho liftCameraByHeight, bỏ qua.\n" ) continue height_str = parts[2] try: float(height_str) except ValueError: self.append_log( f"⚠ Dòng {idx}: HEIGHT không hợp lệ '{height_str}', bỏ qua.\n" ) continue action_cfg["height"] = height_str else: # các actionType khác, sau này có thể parse thêm param self.append_log(f"⚠ Dòng {idx}: action '{action_type}' chưa implement, vẫn lưu type.\n") node_actions.setdefault(node_name, []).append(action_cfg) return node_actions # ------ Button handlers ------ def on_move_single(self): robot_id = self.get_robot_id() node = self.node_var.get().strip() if not node: messagebox.showwarning("Thiếu thông tin", "Vui lòng nhập tên node.") return def task(): try: self.append_log(f"\n[MoveToNode] robotId={robot_id}, node={node}\n") resp, body = self.client.move_to_node(robot_id, node) txt = self.client._format_response("POST", MOVE_ENDPOINT, resp, body) self.append_log(txt) except Exception as e: self.append_log(f"Error: {e}\n") threading.Thread(target=task, daemon=True).start() def on_move_multi(self): robot_id = self.get_robot_id() nodes, delay = self._parse_nodes_and_delay() if nodes is None: return node_actions = self._parse_node_actions() use_polling = self.use_polling_var.get() try: poll_timeout = float(self.poll_timeout_var.get().strip()) except ValueError: poll_timeout = 60.0 def task(): mode_str = "Polling mode" if use_polling else f"Delay mode ({delay}s)" self.append_log( f"\n[MoveMultiNode] robotId={robot_id}, " f"nodes={nodes}, {mode_str}\n" ) if use_polling: self.append_log(f"Timeout polling: {poll_timeout}s\n") self.append_log(f"Node–Action mapping: {node_actions}\n") try: self.client.move_multiple_nodes( robot_id, nodes, delay=delay, callback=self.append_log, node_actions=node_actions, use_polling=use_polling, poll_timeout=poll_timeout, ) except Exception as e: self.append_log(f"Error: {e}\n") threading.Thread(target=task, daemon=True).start() def on_loop_multi(self): """ Loop qua list node, mỗi vòng: - Gọi move_multiple_nodes với node_actions. - Sử dụng polling hoặc delay tuỳ setting. - GHI DỮ LIỆU HIK-QR-001 tại mỗi node. """ if self.loop_running: messagebox.showinfo("Loop", "Loop đã chạy rồi.") return robot_id = self.get_robot_id() nodes, delay = self._parse_nodes_and_delay() if nodes is None: return node_actions = self._parse_node_actions() use_polling = self.use_polling_var.get() try: poll_timeout = float(self.poll_timeout_var.get().strip()) except ValueError: poll_timeout = 60.0 self.loop_running = True self.current_loop_trial = 0 def task(): loop_count = 0 mode_str = "Polling mode" if use_polling else f"Delay mode ({delay}s)" self.append_log(f"\n[LoopMultiNode + HIK-QR-001] Bắt đầu loop... ({mode_str})\n") # Bật HIK-QR từ đầu self.gls621.enable_hik_qr(True) self.append_log("🔍 HIK-QR-001 đã bật cho loop\n") while self.loop_running: loop_count += 1 self.current_loop_trial = loop_count self.append_log( f"\n🔁 Loop vòng {loop_count}\n" f"nodes={nodes}, node_actions={node_actions}\n" ) try: for i, node in enumerate(nodes, start=1): if not self.loop_running: # Check stop break self.append_log(f"===== [{i}/{len(nodes)}] MoveToNode: {node} =====\n") # Gửi MoveToNode resp, body = self.client.move_to_node(robot_id, node) self.append_log(self.client._format_response("POST", MOVE_ENDPOINT, resp, body)) # Chờ Finished Order if use_polling: self.append_log(f"⏳ Đang chờ Finished Order cho node '{node}'...\n") time.sleep(0.2) success = self.client.wait_for_order_finished( robot_id, timeout=poll_timeout, poll_interval=0.5, callback=self.append_log ) if not success: self.append_log(f"⚠ Không nhận được Finished Order, tiếp tục node tiếp theo\n") else: if delay > 0 and i < len(nodes): self.append_log(f"--- Wait {delay} seconds ---\n") time.sleep(delay) # Chờ HIK-QR ổn định self.append_log("⏳ Chờ HIK-QR-001 ổn định...\n") time.sleep(1.5) # GHI DỮ LIỆU HIK-QR TẠI NODE - CHỜ HOÀN THÀNH MỚI TIẾP TỤC self.append_log(f"💾 Đang ghi dữ liệu HIK-QR-001 cho node {node}...\n") record_success = self.gls621.record_node_data(node, loop_count) # Chờ thêm để đảm bảo dữ liệu được ghi hoàn toàn if record_success: self.append_log(f"✅ Dữ liệu đã được ghi, tiếp tục node tiếp theo\n") time.sleep(0.5) else: self.append_log(f"⚠ Không ghi được dữ liệu, vẫn tiếp tục\n") self.update_gls621_display() # Gửi action cho node actions_for_node = node_actions.get(node, []) for act in actions_for_node: act_type = act.get("type") if not act_type: continue self.append_log(f"--> Node '{node}' có action: {act_type}\n") if act_type == "liftCameraByHeight": height = act.get("height") if height is None: self.append_log("⚠ Thiếu height cho liftCameraByHeight, bỏ qua.\n") continue a_resp, a_body = self.client.call_action_lift_camera_by_height( robot_id, height ) self.append_log(self.client._format_response("POST", ACTION_ENDPOINT, a_resp, a_body)) except Exception as e: self.append_log(f"Error trong loop: {e}\n") break # Tắt HIK-QR self.gls621.enable_hik_qr(False) self.append_log("\n🔍 HIK-QR-001 đã tắt\n") self.append_log(f"\n[LoopMultiNode] Hoàn thành {loop_count} vòng.\n") threading.Thread(target=task, daemon=True).start() def on_stop_loop(self): """Dừng loop, cancel order, tự động lưu CSV""" self.loop_running = False robot_id = self.get_robot_id() # Cancel order đang thực hiện self.append_log("\n🛑 Đang dừng loop và cancel order...\n") try: resp, _ = self.client.cancel_move(robot_id) if resp.status_code == 200: self.append_log("✅ Order đã được cancel\n") else: self.append_log(f"⚠ Cancel order HTTP {resp.status_code}\n") except Exception as e: self.append_log(f"⚠ Lỗi cancel order: {e}\n") # Tự động lưu CSV nếu có dữ liệu HIK-QR if self.gls621.node_data: self.append_log("\n💾 Đang tự động lưu dữ liệu HIK-QR-001...\n") filename = self.gls621.save_to_csv() if filename: self.append_log(f"✅ Đã lưu: {filename}\n") else: self.append_log("⚠ Không có dữ liệu HIK-QR-001 để lưu\n") def on_instant_action(self): """ Gửi action độc lập (theo combo Instant Actions). Hiện tại chỉ implement liftCameraByHeight. """ robot_id = self.get_robot_id() action_type = self.inst_action_type_var.get() if not action_type: messagebox.showwarning("Thiếu thông tin", "Vui lòng chọn loại Action.") return if action_type == "liftCameraByHeight": height_str = self.height_var.get().strip() if not height_str: messagebox.showwarning("Thiếu thông tin", "Vui lòng nhập Height.") return try: float(height_str) except ValueError: messagebox.showwarning("Sai định dạng", "Height phải là số (vd: 0.52).") return def task(): try: self.append_log(f"\n[InstantAction] {action_type} height={height_str}\n") resp, body = self.client.call_action_lift_camera_by_height(robot_id, height_str) txt = self.client._format_response("POST", ACTION_ENDPOINT, resp, body) self.append_log(txt) except Exception as e: self.append_log(f"Error: {e}\n") threading.Thread(target=task, daemon=True).start() else: messagebox.showinfo( "Chưa hỗ trợ", f"Action '{action_type}' chưa được implement ở client. Gửi mình JSON mẫu, mình thêm vào cho." ) def on_cancel_move(self): robot_id = self.get_robot_id() def task(): try: self.append_log(f"\n[CancelMove] robotId={robot_id}\n") resp, _ = self.client.cancel_move(robot_id) txt = self.client._format_response("DELETE", f"{MOVE_ENDPOINT}/{robot_id}", resp) self.append_log(txt) except Exception as e: self.append_log(f"Error: {e}\n") threading.Thread(target=task, daemon=True).start() def on_get_state(self): robot_id = self.get_robot_id() def task(): try: self.append_log(f"\n[GetState] robotId={robot_id}\n") resp, _ = self.client.get_state(robot_id) txt = self.client._format_response("GET", f"{STATE_ENDPOINT}/{robot_id}", resp) self.append_log(txt) except Exception as e: self.append_log(f"Error: {e}\n") threading.Thread(target=task, daemon=True).start() def on_save_gls621_csv(self): """Lưu dữ liệu HIK-QR-001 vào CSV""" if not self.gls621.node_data: messagebox.showwarning("Không có dữ liệu", "Chưa có dữ liệu HIK-QR-001 để lưu.") return filename = self.gls621.save_to_csv() if filename: messagebox.showinfo("Thành công", f"Dữ liệu đã lưu vào:\n{filename}") self.append_log(f"✅ CSV file: {filename}\n") def on_clear_gls621_data(self): """Xóa dữ liệu HIK-QR-001""" self.gls621.clear_data() self.gls621_display_text.config(text="X: --- Y: --- θ: --- Error: ---") self.append_log("🗑 Dữ liệu HIK-QR-001 đã xóa\n") def update_gls621_display(self): """Cập nhật display vị trí và sai số từ HIK-QR-001""" if self.gls621.current_pose: x, y, theta = self.gls621.current_pose error = self.gls621.current_error text = f"X: {x:.4f}m | Y: {y:.4f}m | θ: {theta:.4f}rad | Error: {error:.4f}m" self.gls621_display_text.config(text=text) else: self.gls621_display_text.config(text="X: --- Y: --- θ: --- Error: ---") # Schedule next update self.after(100, self.update_gls621_display) def on_move_multi_with_gls621(self): """Chạy move_multi nhưng lưu dữ liệu HIK-QR-001 tại mỗi node""" robot_id = self.get_robot_id() nodes, delay = self._parse_nodes_and_delay() if nodes is None: return node_actions = self._parse_node_actions() use_polling = self.use_polling_var.get() try: poll_timeout = float(self.poll_timeout_var.get().strip()) except ValueError: poll_timeout = 60.0 def task(): mode_str = "Polling mode" if use_polling else f"Delay mode ({delay}s)" self.append_log( f"\n[MoveMultiNode + HIK-QR-001] robotId={robot_id}, " f"nodes={nodes}, {mode_str}\n" ) if use_polling: self.append_log(f"Timeout polling: {poll_timeout}s\n") # Bật HIK-QR trước khi bắt đầu self.gls621.enable_hik_qr(True) self.append_log("🔍 HIK-QR-001 đã bật\n") try: for i, node in enumerate(nodes, start=1): if not self.loop_running: # Kiểm tra nếu dừng break self.append_log(f"\n===== [{i}/{len(nodes)}] MoveToNode: {node} =====\n") # Gửi MoveToNode resp, body = self.client.move_to_node(robot_id, node) self.append_log(self.client._format_response("POST", MOVE_ENDPOINT, resp, body)) # Chờ Finished Order if use_polling: self.append_log(f"⏳ Đang chờ Finished Order cho node '{node}'...\n") time.sleep(0.2) success = self.client.wait_for_order_finished( robot_id, timeout=poll_timeout, poll_interval=0.5, callback=self.append_log ) if not success: self.append_log(f"⚠ Không nhận được Finished Order, tiếp tục node tiếp theo\n") else: if delay > 0 and i < len(nodes): self.append_log(f"--- Wait {delay} seconds ---\n") time.sleep(delay) # Chờ thêm 1s để cảm biến HIK-QR ổn định time.sleep(1.0) # GHI DỮ LIỆU HIK-QR trial = self.current_loop_trial if self.loop_running else 1 self.gls621.record_node_data(node, trial) self.update_gls621_display() # Gửi action cho node actions_for_node = node_actions.get(node, []) for act in actions_for_node: act_type = act.get("type") if not act_type: continue self.append_log(f"--> Node '{node}' có action: {act_type}\n") if act_type == "liftCameraByHeight": height = act.get("height") if height is None: self.append_log("⚠ Thiếu height cho liftCameraByHeight, bỏ qua.\n") continue a_resp, a_body = self.client.call_action_lift_camera_by_height( robot_id, height ) self.append_log(self.client._format_response("POST", ACTION_ENDPOINT, a_resp, a_body)) # Tắt HIK-QR sau khi hoàn thành self.gls621.enable_hik_qr(False) self.append_log("\n🔍 HIK-QR-001 đã tắt\n") except Exception as e: self.append_log(f"Error trong move multi: {e}\n") threading.Thread(target=task, daemon=True).start() if __name__ == "__main__": app = RobotApp() app.mainloop()