From 7dadcb8bcce3efebfa380fa9cefb36e032a46bb6 Mon Sep 17 00:00:00 2001 From: FaulknerWu Date: Mon, 22 Jun 2026 13:54:07 +0800 Subject: [PATCH] Improve camera status and production startup --- agv_app/app.py | 24 +++++-------- agv_app/utils/arm_client.py | 6 +++- .../src/components/CameraFrame.tsx | 2 +- scripts/prod-backend.sh | 34 +++++++++++++----- scripts/ros-common.sh | 36 +++++++++++++++++-- 5 files changed, 72 insertions(+), 30 deletions(-) diff --git a/agv_app/app.py b/agv_app/app.py index 1b1a915..c37c709 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -207,16 +207,7 @@ def running_page(): def api_status(): """获取系统整体状态""" with gs.lock: - # 实际验证机械臂连接(尝试发送一个简单命令) - arm_connected = False - if gs.arm_client and gs.arm_client._sock: - try: - # 设置短超时尝试获取角度,验证连接是否有效 - gs.arm_client._sock.settimeout(2) - ok, _ = gs.arm_client.get_angles() - arm_connected = ok - except: - arm_connected = False + arm_connected = gs.arm_client.is_connected() if gs.arm_client else False # 实际验证 AGV 连接 agv_connected = False @@ -239,6 +230,8 @@ def api_status(): "camera_opened": gs.camera_opened, "arm_camera_opened": gs.arm_camera_opened, "map_loaded": bool(gs.map_config), + "map": gs.map_config, + "has_agv_camera": gs.camera_opened, "points_count": len(gs.points_config), "models_count": len(gs.models_config), "mission_rows": gs.mission_config.get("rows", 0), @@ -1111,7 +1104,7 @@ def api_arm_state_off(): @app.route("/api/camera/preview") def api_camera_preview(): """MJPEG 视频流""" - if not gs.qr_scanner or not gs.qr_scanner._cap: + if not gs.qr_scanner or not gs.camera_opened: return "camera not opened", 400 import time as _time @@ -1137,7 +1130,7 @@ def api_camera_preview(): @app.route("/api/camera/capture") def api_camera_capture(): """拍摄一张照片""" - if not gs.qr_scanner or not gs.qr_scanner._cap: + if not gs.qr_scanner or not gs.camera_opened: return jsonify({"ok": False, "error": "摄像头未打开"}), 400 import cv2 frame = gs.qr_scanner.read_frame() @@ -2107,7 +2100,7 @@ def api_camera_refresh(): """AGV 摄像头单帧 JPEG(polling 模式)""" if not gs.qr_scanner: return jsonify({"error": "scanner not initialized"}), 400 - if not gs.qr_scanner._cap or not gs.qr_scanner._cap.isOpened(): + if not gs.camera_opened: return jsonify({"error": "camera not opened"}), 400 import cv2 frame = gs.qr_scanner.read_frame() @@ -2126,8 +2119,8 @@ def api_camera_refresh(): def api_camera_capabilities(): """返回摄像头能力信息,前端据此决定如何展示""" return jsonify({ - "has_agv_camera": False, # Orbbec 深度相机不提供可用的彩色画面 - "has_arm_camera": True, + "has_agv_camera": bool(gs.camera_opened), + "has_arm_camera": bool(gs.arm_camera_opened), }) # ========== 启动 ========== if __name__ == "__main__": @@ -2155,4 +2148,3 @@ if __name__ == "__main__": debug=SERVER_CONFIG["debug"], threaded=True ) - diff --git a/agv_app/utils/arm_client.py b/agv_app/utils/arm_client.py index 9dd9d9a..7f684cb 100644 --- a/agv_app/utils/arm_client.py +++ b/agv_app/utils/arm_client.py @@ -51,6 +51,10 @@ class ArmClient: self._sock.close() self._sock = None + def is_connected(self) -> bool: + """返回当前 TCP 连接是否仍由客户端持有。""" + return self._sock is not None + def reconnect(self) -> bool: self.close() time.sleep(1) @@ -167,4 +171,4 @@ class ArmClient: return self def __exit__(self, *args): - self.close() \ No newline at end of file + self.close() diff --git a/public-frontend/src/components/CameraFrame.tsx b/public-frontend/src/components/CameraFrame.tsx index aab3e7e..f198a85 100644 --- a/public-frontend/src/components/CameraFrame.tsx +++ b/public-frontend/src/components/CameraFrame.tsx @@ -49,7 +49,7 @@ export const CameraFrame: React.FC = ({ camera, active = true, {/* 后端的 AGV 接口是单帧 JPEG,机械臂接口是 MJPEG;img 可同时承载两者。 */} {/* eslint-disable-next-line @next/next/no-img-element */} {camera.name}/dev/null || true rm -rf "$FASTRTPS_SHM_DIR"/fastrtps_* 2>/dev/null || true start_ros2_daemon || true @@ -55,12 +54,16 @@ start_ros2_daemon || true # ============================================================================ step "3/8" "启动 AGV Bringup" cd "$AGV_ROS2_DIR" -source "$ROS_WORKSPACE_SETUP" rm -rf "$FASTRTPS_SHM_DIR"/fastrtps_* 2>/dev/null || true -nohup bash -c "export ROS_DOMAIN_ID=$ROS_DOMAIN_ID && \ - ros2 launch agv_pro_bringup agv_pro_bringup.launch.py port_name:=$AGV_CONTROLLER_DEVICE" \ +nohup bash -c ' + source "$1" || exit 1 + source "$2" || exit 1 + export ROS_DOMAIN_ID="$3" + cd "$4" || exit 1 + ros2 launch agv_pro_bringup agv_pro_bringup.launch.py port_name:="$5" +' _ "$ROS_SETUP" "$ROS_WORKSPACE_SETUP" "$ROS_DOMAIN_ID" "$AGV_ROS2_DIR" "$AGV_CONTROLLER_DEVICE" \ > "$BRINGUP_LOG" 2>&1 & BRINGUP_PID=$! echo " bringup PID: $BRINGUP_PID" @@ -123,7 +126,7 @@ step "6/8" "启动 Nav2 导航" nohup bash -c "source \"$ROS_SETUP\" && source \"$ROS_WORKSPACE_SETUP\" && \ export ROS_DOMAIN_ID=$ROS_DOMAIN_ID && \ - ros2 launch agv_pro_navigation2 navigation2_active.launch.py autostart:=True" \ + ros2 launch agv_pro_navigation2 navigation2_active.launch.py autostart:=True use_rviz:=False" \ > "$NAV2_LOG" 2>&1 & NAV2_PID=$! echo " Nav2 PID: $NAV2_PID" @@ -153,9 +156,22 @@ info "ok" "精度参数已设置" step "8/8" "启动 Flask API" cd "$AGV_APP_DIR" -nohup uv run --locked python app.py > "$FLASK_LOG" 2>&1 & -FLASK_PID=$! -echo " Flask PID: $FLASK_PID" +UV_EXECUTABLE=$(resolve_uv_bin || true) +FLASK_PID="" +if [ -z "$UV_EXECUTABLE" ]; then + echo " [ERROR] 未找到 uv,可设置 UV_BIN=/path/to/uv" +else + nohup bash -c ' + source "$1" || exit 1 + source "$2" || exit 1 + export ROS_DOMAIN_ID="$3" + cd "$4" || exit 1 + exec "$5" run --locked python app.py + ' _ "$ROS_SETUP" "$ROS_WORKSPACE_SETUP" "$ROS_DOMAIN_ID" "$AGV_APP_DIR" "$UV_EXECUTABLE" \ + > "$FLASK_LOG" 2>&1 & + FLASK_PID=$! + echo " Flask PID: $FLASK_PID" +fi sleep 4 # ============================================================================ @@ -223,7 +239,7 @@ echo " 进程状态:" for proc_info in "bringup:$BRINGUP_PID" "Nav2:$NAV2_PID" "fixer:$FIXER_PID" "Flask:$FLASK_PID"; do name="${proc_info%%:*}" pid="${proc_info##*:}" - if ps aux | grep -w "$pid" | grep -v grep >/dev/null 2>&1; then + if [ -n "$pid" ] && ps -p "$pid" >/dev/null 2>&1; then echo " $name : 运行中 (PID: $pid)" else echo " $name : 已退出" diff --git a/scripts/ros-common.sh b/scripts/ros-common.sh index d5819b5..71d9dd4 100755 --- a/scripts/ros-common.sh +++ b/scripts/ros-common.sh @@ -16,6 +16,7 @@ readonly SCAN_FIXER_DIR="${SCAN_FIXER_DIR:-$AGV_PROJECT_DIR/scan_fixer}" readonly LOG_DIR="${LOG_DIR:-/tmp}" readonly FASTRTPS_SHM_DIR="${FASTRTPS_SHM_DIR:-/dev/shm}" readonly AGV_CONTROLLER_DEVICE="${AGV_CONTROLLER_DEVICE:-/dev/agvpro_controller}" +readonly UV_BIN="${UV_BIN:-}" export ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-1}" # 日志文件 @@ -131,6 +132,32 @@ ros2_exec() { ' _ "$ROS_SETUP" "$ROS_WORKSPACE_SETUP" "$ROS_DOMAIN_ID" "$@" } +resolve_uv_bin() { + if [ -n "$UV_BIN" ]; then + if [ -x "$UV_BIN" ]; then + echo "$UV_BIN" + return 0 + fi + return 1 + fi + + local candidate + candidate=$(command -v uv 2>/dev/null || true) + if [ -n "$candidate" ]; then + echo "$candidate" + return 0 + fi + + for candidate in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv"; do + if [ -x "$candidate" ]; then + echo "$candidate" + return 0 + fi + done + + return 1 +} + # ============================================================================ # ROS2 环境操作 # ============================================================================ @@ -184,8 +211,11 @@ stop_ros2_daemon() { pkill -9 -f "ros2-daemon" 2>/dev/null || true sleep 2 - source "$ROS_SETUP" 2>/dev/null || true - ros2 daemon stop 2>/dev/null || true + bash -c ' + source "$1" || exit 0 + export ROS_DOMAIN_ID="$2" + ros2 daemon stop + ' _ "$ROS_SETUP" "$ROS_DOMAIN_ID" 2>/dev/null || true echo " [OK] ros2 daemon 已重置" } @@ -223,7 +253,7 @@ wait_for_nodes() { while [ "$elapsed" -lt "$max_wait" ]; do local nodes - nodes=$(ros2_exec ros2 node list 2>/dev/null || true) + nodes=$(ros2_exec timeout 8 ros2 node list --no-daemon --spin-time 3 2>/dev/null || true) count=$(printf '%s\n' "$nodes" | grep -cE "$pattern" || true) if [ "$count" -ge "$expected" ]; then echo " [OK] 已检测到 $count 个节点"