From a4f4be4c8e6c5e17c75956a21bcd21347c44d90b Mon Sep 17 00:00:00 2001 From: ywb <347742090@qq.com> Date: Mon, 8 Jun 2026 11:42:41 +0800 Subject: [PATCH] - --- agv_app/app.py | 34 ++++++++++++--- agv_app/config.py | 6 +-- agv_app/static/js/running.js | 22 +++++++--- agv_app/static/js/setting.js | 2 +- agv_app/utils/agv_controller_ros2.py | 14 +++--- agv_app/utils/config.py | 6 +-- agv_app/utils/mission_executor.py | 45 ++++++++++++++----- arm_server/arm_server.py | 64 ++++++++++++++++++++++------ 8 files changed, 141 insertions(+), 52 deletions(-) diff --git a/agv_app/app.py b/agv_app/app.py index ab8ea28..1da7d64 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -126,10 +126,11 @@ try: # Flask 2.3+ 方式 with app.app_context(): load_persisted_config() - # 启动时自动重连 AGV(异步,不阻塞 Flask 启动) + # 启动时自动连接所有设备(异步,不阻塞 Flask 启动) import threading - def _auto_reconnect(): + def _auto_connect_all(): time.sleep(2) # 等待 Flask 完全就绪 + # 连接 AGV try: from utils.agv_controller_ros2 import AGVController gs.agv_controller = AGVController() @@ -139,7 +140,19 @@ try: print("[启动] AGV 自动连接失败,请手动连接") except Exception as e: print(f"[启动] AGV 自动连接异常: {e}") - threading.Thread(target=_auto_reconnect, daemon=True).start() + # 连接机械臂 + try: + from utils.arm_client import ArmClient + gs.arm_client = ArmClient(ARM_CONFIG["host"], ARM_CONFIG["port"]) + if gs.arm_client.connect(): + gs.arm_client.power_on() + gs.arm_client.state_on() + print("[启动] 机械臂自动连接成功") + else: + print("[启动] 机械臂自动连接失败,请手动连接") + except Exception as e: + print(f"[启动] 机械臂自动连接异常: {e}") + threading.Thread(target=_auto_connect_all, daemon=True).start() except: # 兼容旧版 Flask @app.before_first_request @@ -184,6 +197,15 @@ def api_status(): if gs.agv_controller: agv_connected = gs.agv_controller.is_connected() + # 实时检测机械臂摄像头是否可用 + try: + import requests as _armcam_req + _r = _armcam_req.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=3) + gs.arm_camera_opened = (_r.status_code == 200) + _r.close() + except: + gs.arm_camera_opened = False + return jsonify({ "state": gs.state, "agv_connected": agv_connected, @@ -1179,7 +1201,7 @@ def api_agv_move(): """控制 AGV 移动(前进/后退/左转/右转/停止)""" data = request.json direction = data.get("direction", "stop") # forward / backward / left / right / stop - speed = data.get("speed", AGV_CONFIG.get("move_speed", 0.5)) + speed = data.get("speed", AGV_CONFIG.get("move_speed", 1.0)) if not gs.agv_controller or not gs.agv_controller.is_connected(): return jsonify({"ok": False, "error": "AGV 未连接"}), 400 try: @@ -1250,8 +1272,8 @@ def api_mission_start(): "qr_scan": bool(data.get("qr_scan", True)), "front_photo": bool(data.get("front_photo", True)), "back_photo": bool(data.get("back_photo", True)), - "agv_speed": float(data.get("agv_speed", 0.5)), - "arm_speed": int(data.get("arm_speed", 500)), + "agv_speed": float(data.get("agv_speed", 1.0)), + "arm_speed": int(data.get("arm_speed", 1000)), } print(f"[Mission] options: {options}") diff --git a/agv_app/config.py b/agv_app/config.py index dac5698..4125625 100644 --- a/agv_app/config.py +++ b/agv_app/config.py @@ -14,8 +14,8 @@ ARM_HOST = "192.168.60.88" AGV_CONFIG = { "device": "/dev/agvpro_controller", "baudrate": 10000000, - "move_speed": 0.5, - "turn_speed": 0.5, + "move_speed": 1.0, + "turn_speed": 1.0, } # ========== 机械臂 TCP 客户端 ========== @@ -77,7 +77,7 @@ JOINT_LIMITS = { } # ========== 机械臂默认速度 ========== -DEFAULT_ARM_SPEED = 500 +DEFAULT_ARM_SPEED = 1000 # ========== 状态定义 ========== class State: diff --git a/agv_app/static/js/running.js b/agv_app/static/js/running.js index b666260..b97d1c1 100644 --- a/agv_app/static/js/running.js +++ b/agv_app/static/js/running.js @@ -33,8 +33,8 @@ createApp({ frontPhotoEnabled: true, backPhotoEnabled: true, // 速度控制 - agvSpeed: 0.5, - armSpeed: 500, + agvSpeed: 1.0, + armSpeed: 1000, } }, computed: { @@ -63,10 +63,10 @@ createApp({ try { const res = await fetch(API + '/api/status') const data = await res.json() - if (!this.armCameraOpened) { - this.armPreviewUrl = '' - } else if (!this.armPreviewUrl) { - this.armPreviewUrl = API + '/api/camera/arm_preview' + const opened = data.arm_camera_opened + if (opened !== this.armCameraOpened || (opened && !this.armPreviewUrl)) { + this.armCameraOpened = opened + this.armPreviewUrl = opened ? API + '/api/camera/arm_preview' : '' } } catch (e) {} }, @@ -94,6 +94,9 @@ createApp({ if (data.point_status) this.pointStatus = data.point_status if (data.machine_status) this.machineStatus = data.machine_status this.armCameraOpened = data.arm_camera_opened + if (this.armCameraOpened && !this.armPreviewUrl) { + this.armPreviewUrl = API + '/api/camera/arm_preview' + } // 错误弹窗 if (data.waiting_error) { @@ -260,7 +263,12 @@ createApp({ e.target.style.display = 'none' }, onArmPreviewError(e) { - e.target.style.display = 'none' + // 重新加载:加时间戳避免缓存 + const url = this.armPreviewUrl + if (url) { + const sep = url.includes('?') ? '&' : '?' + this.armPreviewUrl = url + sep + '_t=' + Date.now() + } }, // ===== 网格任务显示方法 ===== getPointStatus(pr, c) { diff --git a/agv_app/static/js/setting.js b/agv_app/static/js/setting.js index 13c0e2a..30ea170 100644 --- a/agv_app/static/js/setting.js +++ b/agv_app/static/js/setting.js @@ -45,7 +45,7 @@ createApp({ agvConnected: false, agvBattery: null, agvPosition: null, - agvSpeed: 0.5, + agvSpeed: 1.0, agvMoveInterval: null, agvCameraUrl: API + '/api/camera/refresh', agvCameraTimer: null, diff --git a/agv_app/utils/agv_controller_ros2.py b/agv_app/utils/agv_controller_ros2.py index 20369d9..2d001f9 100644 --- a/agv_app/utils/agv_controller_ros2.py +++ b/agv_app/utils/agv_controller_ros2.py @@ -76,7 +76,7 @@ class AGVController: if rc != 0: logger.warning(f"发布 cmd_vel 失败: {err}") - def move_forward(self, speed: float = 0.5, duration: float = None): + def move_forward(self, speed: float = 1.0, duration: float = None): """前进""" if not self.is_connected(): return @@ -85,7 +85,7 @@ class AGVController: time.sleep(duration) self.stop() - def move_backward(self, speed: float = 0.5, duration: float = None): + def move_backward(self, speed: float = 1.0, duration: float = None): """后退""" if not self.is_connected(): return @@ -94,7 +94,7 @@ class AGVController: time.sleep(duration) self.stop() - def turn_left(self, speed: float = 0.5, duration: float = None): + def turn_left(self, speed: float = 1.0, duration: float = None): """左转""" if not self.is_connected(): return @@ -103,7 +103,7 @@ class AGVController: time.sleep(duration) self.stop() - def turn_right(self, speed: float = 0.5, duration: float = None): + def turn_right(self, speed: float = 1.0, duration: float = None): """右转""" if not self.is_connected(): return @@ -112,7 +112,7 @@ class AGVController: time.sleep(duration) self.stop() - def move_left_lateral(self, speed: float = 0.5, duration: float = None): + def move_left_lateral(self, speed: float = 1.0, duration: float = None): """向左横向移动""" if not self.is_connected(): return @@ -121,7 +121,7 @@ class AGVController: time.sleep(duration) self.stop() - def move_right_lateral(self, speed: float = 0.5, duration: float = None): + def move_right_lateral(self, speed: float = 1.0, duration: float = None): """向右横向移动""" if not self.is_connected(): return @@ -176,7 +176,7 @@ class AGVController: return self._position return None - def go_to_point(self, x: float, y: float, rz: float = None, speed: float = 0.5) -> bool: + def go_to_point(self, x: float, y: float, rz: float = None, speed: float = 1.0) -> bool: """移动到目标点(需要 ROS2 导航栈)""" logger.warning("go_to_point 需要 ROS2 Nav2 支持,当前仅记录目标") return True diff --git a/agv_app/utils/config.py b/agv_app/utils/config.py index dac5698..4125625 100644 --- a/agv_app/utils/config.py +++ b/agv_app/utils/config.py @@ -14,8 +14,8 @@ ARM_HOST = "192.168.60.88" AGV_CONFIG = { "device": "/dev/agvpro_controller", "baudrate": 10000000, - "move_speed": 0.5, - "turn_speed": 0.5, + "move_speed": 1.0, + "turn_speed": 1.0, } # ========== 机械臂 TCP 客户端 ========== @@ -77,7 +77,7 @@ JOINT_LIMITS = { } # ========== 机械臂默认速度 ========== -DEFAULT_ARM_SPEED = 500 +DEFAULT_ARM_SPEED = 1000 # ========== 状态定义 ========== class State: diff --git a/agv_app/utils/mission_executor.py b/agv_app/utils/mission_executor.py index 1fd8858..980602a 100644 --- a/agv_app/utils/mission_executor.py +++ b/agv_app/utils/mission_executor.py @@ -101,11 +101,10 @@ class MissionExecutorV3: self._nav = Nav2Navigator() # 速度控制(默认值,可在 execute_mission 时覆写) - self.arm_speed = 500 - self.agv_speed = 0.5 + self.arm_speed = 1000 + self.agv_speed = 1.0 # 照片上传序号计数器(连续递增,从1开始) - self.next_upload_index = 1 # ==================== 连接 ==================== @@ -239,8 +238,7 @@ class MissionExecutorV3: self._log(f"📍 点位蛇形路径: {len(path)} 个点位, {total_machines} 台机器") # 重置照片上传序号(每次任务开始时重置,从1开始) - self.next_upload_index = 1 - + # 任务步骤控制开关 if options is None: options = {} @@ -258,8 +256,8 @@ class MissionExecutorV3: has_arm_pose = self.arm_client and any(abs(a) > 0.01 for a in arm_initial_pose) # 速度控制(从前端传入) - self.arm_speed = int(options.get("arm_speed", 500)) - self.agv_speed = float(options.get("agv_speed", 0.5)) + self.arm_speed = int(options.get("arm_speed", 1000)) + self.agv_speed = float(options.get("agv_speed", 1.0)) self._log(f"🚀 AGV速度={self.agv_speed:.1f}m/s, 机械臂速度={self.arm_speed}") # 设置 Nav2 导航速度(仅在任务开始时设一次) @@ -831,9 +829,8 @@ class MissionExecutorV3: self.arm_client.set_angles(angles, speed=self.arm_speed) self._wait_arm_ready(angles) - # 拍照(upload_index 连续递增) - path = self._capture_arm_photo(row, col, side, pi + 1, qr_value, upload_index=self.next_upload_index) - self.next_upload_index += 1 + # 拍照(每台机器独立从0开始编号) + path = self._capture_arm_photo(row, col, side, pi + 1, qr_value, upload_index=pi + 1) if path: self._log(f" 💾 {os.path.basename(path)}") @@ -842,7 +839,7 @@ class MissionExecutorV3: upload_index: int = 0) -> Optional[str]: """从机械臂摄像头拍照,直接上传到服务器(不保存本地) - upload_index: 从1开始,先正面后背面,由调用方维护 + upload_index: 每台机器独立,从0开始 """ try: resp = requests.get(ARM_CAMERA_SNAPSHOT, timeout=10) @@ -892,6 +889,32 @@ class MissionExecutorV3: self._log(f" ❌ 上传异常 [{index}]: {e}") return False + def _upload_photo_bytes(self, filename: str, image_data: bytes, serial_number: str, index: int) -> bool: + """上传照片 bytes 到远程服务器(不保存本地文件) + + Args: + filename: 文件名(用于服务器存储) + image_data: 图片二进制数据 + serial_number: 二维码/序列号 + index: 上传序号(从1开始递增) + """ + try: + headers = { + "Authorization": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX2tleSI6ImZhNTNkZTZiLWE3NjYtNDZmNC05MDUyLTQ2MjUzZTAyNjdmNSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.lC4vKThZo4aAOLsekm2kPgaEJRqRx-YDQWKfHFqxdPNESCKy57l3eIqaKTj2ZjAMaoYAwYlMrv5M1zAOJsO_PA" + } + files = {"file": (filename, image_data, "image/jpeg")} + data = {"serialNumber": serial_number, "index": str(index)} + resp = requests.post(UPLOAD_URL, files=files, data=data, headers=headers, timeout=30) + if resp.status_code == 200: + self._log(f" ☁️ 上传成功 [{index}]: {filename}") + return True + else: + self._log(f" ⚠️ 上传失败 [{index}] HTTP {resp.status_code}: {resp.text[:200]}") + return False + except Exception as e: + self._log(f" ❌ 上传异常 [{index}]: {e}") + return False + # ==================== 控制 ==================== def _wait_pause(self): diff --git a/arm_server/arm_server.py b/arm_server/arm_server.py index e236ae4..fd53767 100644 --- a/arm_server/arm_server.py +++ b/arm_server/arm_server.py @@ -254,19 +254,7 @@ class AGVCommandServer: def _connect_roboflow(self): self.roboflow = RoboFlowClient() if self.roboflow.connect(): - logger.info("RoboFlow 连接成功") - # 连接成功后自动上电并激活机械臂 - time.sleep(1) - try: - resp = self.roboflow.send_recv("power_on()") - logger.info(f"机械臂上电: {resp}") - except Exception as e: - logger.warning(f"机械臂上电失败: {e}") - try: - resp = self.roboflow.send_recv("state_on()") - logger.info(f"机械臂激活: {resp}") - except Exception as e: - logger.warning(f"机械臂激活失败: {e}") + logger.info("RoboFlow 连接成功(上电由 power_on_arm() 完成)") else: logger.warning("RoboFlow 连接失败,服务将以 limited 模式运行") @@ -342,13 +330,61 @@ class AGVCommandServer: # ========== 入口 ========== +import time + +def power_on_arm(max_retries: int = 5) -> bool: + """通过 ElephantRobot 给机械臂上电并激活(带重试)""" + from pymycobot import ElephantRobot + + for attempt in range(1, max_retries + 1): + try: + logger.info(f"正在通过 ElephantRobot 连接 RoboFlow (尝试 {attempt}/{max_retries})...") + el = ElephantRobot("127.0.0.1", 5001) + el.start_client() + logger.info("ElephantRobot start_client 成功,等待2秒...") + time.sleep(2) + + el._power_on() + logger.info("power_on 指令已发送,等待2秒...") + time.sleep(2) + + el.start_robot() + logger.info("start_robot 指令已发送,等待5秒...") + time.sleep(5) + logger.info("✅ 机械臂上电+激活 全部完成") + return True + except Exception as e: + logger.warning(f"⚠️ 第 {attempt} 次尝试失败: {e}") + if attempt < max_retries: + logger.info(f"等待 3 秒后重试...") + time.sleep(3) + else: + logger.error(f"❌ 所有 {max_retries} 次尝试均失败,将以 limited 模式运行") + return False + return False + + def main(): import signal + # 先通过 ElephantRobot 给机械臂上电并激活 + power_on_arm() + server = AGVCommandServer(port=5002) # 启动 Flask 视频流服务(端口 5003) - arm_server_http = make_server("0.0.0.0", 5003, arm_video_app, threaded=True) + from werkzeug.serving import make_server + arm_server_http = None + for attempt in range(5): + try: + arm_server_http = make_server("0.0.0.0", 5003, arm_video_app, threaded=True) + break + except OSError as e: + if attempt < 4 and "Address already in use" in str(e): + logger.warning(f"端口 5003 被占用(第{attempt+1}次),等待...") + time.sleep(3) + else: + raise http_thread = threading.Thread(target=arm_server_http.serve_forever, daemon=True) http_thread.start() logger.info("机械臂视频流服务已启动: http://0.0.0.0:5003")