diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9d32e81 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# 环境变量配置示例 +# 复制此文件为 .env 并修改需要的值 + +# ========== 开发模式 ========== +# 设置为 1 启用 Mock 硬件模式(本地开发,无需真实硬件) +MOCK_HARDWARE=0 + +# ========== 后端配置 ========== +FLASK_PORT=5000 + +# ========== 前端配置 ========== +# 前端开发服务器会代理 API 请求到后端 +BACKEND_URL=http://127.0.0.1:5000 + +# ========== 硬件配置(生产环境) ========== +AGV_HOST=192.168.60.80 +ARM_HOST=192.168.60.120 + +# ========== 外部 API ========== +# TEST_MODE: true=测试环境, false=正式环境 +TEST_MODE=false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a4b6cf2 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +.PHONY: help dev dev-backend dev-frontend stop prod install + +help: ## 显示帮助信息 + @echo "==========================================" + @echo " Smart Inspection DevOps" + @echo "==========================================" + @echo "" + @echo "本地开发命令:" + @echo " make dev 显示开发模式说明" + @echo " make dev-backend 启动后端 (Mock 硬件模式)" + @echo " make dev-frontend 启动前端 (Next.js)" + @echo " make stop 停止所有开发服务" + @echo "" + @echo "生产部署命令:" + @echo " make prod 启动生产环境 (完整系统)" + @echo "" + @echo "安装命令:" + @echo " make install 安装依赖" + @echo "" + +dev: ## 显示开发模式说明 + @echo "==========================================" + @echo " 本地开发模式" + @echo "==========================================" + @echo "" + @echo "需要两个终端:" + @echo " 终端 1: make dev-backend" + @echo " 终端 2: make dev-frontend" + @echo "" + @echo "或使用 tmux:" + @echo " tmux new-session 'make dev-backend' \\; split-window 'make dev-frontend'" + @echo "" + +dev-backend: ## 启动后端开发服务器 (Mock 硬件模式) + @./scripts/dev-backend.sh + +dev-frontend: ## 启动前端开发服务器 + @./scripts/dev-frontend.sh + +stop: ## 停止所有开发服务 + @./scripts/stop.sh + +prod: ## 启动生产环境 (完整系统) + @echo "==========================================" + @echo " 生产环境启动" + @echo "==========================================" + @echo "" + @echo "请在 AGV 上运行此命令" + @echo "" + @./scripts/prod-backend.sh + +install: ## 安装依赖 + @echo "安装 Python 依赖..." + @cd agv_app && uv sync + @echo "安装前端依赖..." + @cd public-frontend && npm install + @echo "完成" diff --git a/agv_app/app.py b/agv_app/app.py index 33d0512..1b1a915 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -12,13 +12,32 @@ import requests from flask import Flask, render_template, jsonify, request, Response, send_from_directory from flask_cors import CORS -from config import SERVER_CONFIG, ARM_CONFIG, AGV_CONFIG, UPLOAD_CONFIG, MAP_CONFIG, ARM_CAMERA_CONFIG, CAMERA_CONFIG, DATA_DIR, State, ZHIJIAN_BASE_URL, ZHIJIAN_AUTH_TOKEN, set_api_mode -from utils.arm_client import ArmClient -from utils.agv_controller_ros2 import AGVController -from utils.qr_scanner import QRScanner +from config import ( + SERVER_CONFIG, ARM_CONFIG, AGV_CONFIG, UPLOAD_CONFIG, MAP_CONFIG, + ARM_CAMERA_CONFIG, CAMERA_CONFIG, DATA_DIR, State, ZHIJIAN_BASE_URL, + ZHIJIAN_AUTH_TOKEN, set_api_mode, MOCK_HARDWARE +) + +# 根据 MOCK_HARDWARE 配置选择导入真实或 Mock 实现 +if MOCK_HARDWARE: + print("[启动] ===========================================") + print("[启动] Mock 硬件模式 - 本地开发环境") + print("[启动] ===========================================") + from utils.mock_hardware import ( + MockArmClient as ArmClient, + MockAGVController as AGVController, + MockQRScanner as QRScanner, + MockNav2Navigator as Nav2Navigator, + MockNav2Status as Nav2Status + ) +else: + from utils.arm_client import ArmClient + from utils.agv_controller_ros2 import AGVController + from utils.qr_scanner import QRScanner + from utils.nav2_navigator import Nav2Navigator, Nav2Status + from utils.image_uploader import ImageUploader from utils.mission_executor import MissionExecutorV3 -from utils.nav2_navigator import Nav2Navigator, Nav2Status # 配置日志 logging.basicConfig( @@ -133,9 +152,17 @@ try: import threading def _auto_connect_all(): time.sleep(2) # 等待 Flask 完全就绪 - # 连接 AGV + if MOCK_HARDWARE: + print("[启动] Mock 模式跳过硬件自动连接") + # Mock 模式下也创建实例供 API 使用 + gs.agv_controller = AGVController() + gs.arm_client = ArmClient(ARM_CONFIG["host"], ARM_CONFIG["port"]) + gs.qr_scanner = QRScanner(CAMERA_CONFIG["device_index"]) + gs.navigator = Nav2Navigator() + return + + # 连接 AGV(真实硬件) try: - from utils.agv_controller_ros2 import AGVController gs.agv_controller = AGVController() if gs.agv_controller.connect(): print("[启动] AGV 自动连接成功") @@ -143,9 +170,8 @@ try: print("[启动] AGV 自动连接失败,请手动连接") except Exception as e: print(f"[启动] AGV 自动连接异常: {e}") - # 连接机械臂 + # 连接机械臂(真实硬件) 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() diff --git a/agv_app/config.py b/agv_app/config.py index 95f938c..88f8a7c 100644 --- a/agv_app/config.py +++ b/agv_app/config.py @@ -10,6 +10,11 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) AGV_HOST = "192.168.60.80" ARM_HOST = "192.168.60.120" +# ========== 开发模式 ========== +# 设置为 True 时使用 Mock 硬件实现(无需真实硬件) +# 通过环境变量 MOCK_HARDWARE=1 启用 +MOCK_HARDWARE = os.getenv("MOCK_HARDWARE", "0") == "1" + # ========== AGV 参数 ========== AGV_CONFIG = { "device": "/dev/agvpro_controller", diff --git a/agv_app/utils/mock_hardware.py b/agv_app/utils/mock_hardware.py new file mode 100644 index 0000000..b4546ed --- /dev/null +++ b/agv_app/utils/mock_hardware.py @@ -0,0 +1,382 @@ +""" +Mock 硬件实现 - 用于本地开发环境(无真实硬件) + +通过环境变量 MOCK_HARDWARE=1 启用,模拟所有硬件组件的行为。 +""" +import time +import logging +import numpy as np +from typing import Tuple, List, Optional +from enum import Enum + +logger = logging.getLogger(__name__) +logger.info("[Mock] 使用 Mock 硬件实现 - 本地开发模式") + + +class MockArmClient: + """Mock 机械臂客户端""" + + def __init__(self, host: str = "127.0.0.1", port: int = 5002, timeout: float = 10): + self.host = host + self.port = port + self.timeout = timeout + self._connected = False + # 默认关节角度 [J1, J2, J3, J4, J5, J6] + self._angles = [0.0, -90.0, 90.0, 0.0, 90.0, 0.0] + # 默认坐标 [x, y, z, rx, ry, rz] + self._coords = [200.0, 0.0, 300.0, 0.0, 180.0, 0.0] + self._power_on = False + self._state_on = False + # Mock socket for compatibility with real ArmClient interface + self._sock = None + + class _MockSocket: + """Mock socket for API compatibility""" + def settimeout(self, timeout): + pass + + def connect(self) -> bool: + """建立连接(Mock)""" + logger.info(f"[Mock] 连接机械臂 {self.host}:{self.port}") + self._connected = True + self._sock = self._MockSocket() # 创建 mock socket + return True + + def close(self): + """关闭连接""" + self._connected = False + self._sock = None + logger.info("[Mock] 关闭机械臂连接") + + def send_command(self, cmd: str) -> Tuple[bool, str]: + """发送命令(Mock)""" + if not self._connected: + return False, "未连接" + logger.debug(f"[Mock] 发送命令: {cmd}") + return True, "ok" + + def reconnect(self) -> bool: + """重新连接""" + self.close() + time.sleep(0.5) + return self.connect() + + # ========== 机械臂命令 ========== + + def get_angles(self) -> Tuple[bool, List[float]]: + """获取所有关节角度""" + logger.debug(f"[Mock] 获取关节角度: {self._angles}") + return True, self._angles + + def set_angles(self, angles: List[float], speed: int = 500) -> bool: + """设置所有关节角度""" + if len(angles) != 6: + return False + logger.info(f"[Mock] 设置关节角度: {angles}, 速度: {speed}") + self._angles = list(angles) + return True + + def set_angle(self, joint: str, angle: float, speed: int = 500) -> bool: + """设置单个关节角度""" + logger.info(f"[Mock] 设置关节 {joint} 角度: {angle}, 速度: {speed}") + joint_map = {"J1": 0, "J2": 1, "J3": 2, "J4": 3, "J5": 4, "J6": 5} + idx = joint_map.get(joint) + if idx is not None: + self._angles[idx] = angle + return True + + def jog_angle(self, joint: str, direction: int, speed: int = 500) -> bool: + """连续调节关节角度""" + logger.debug(f"[Mock] jog_angle({joint}, {direction}, {speed})") + return True + + def get_coords(self) -> Tuple[bool, List[float]]: + """获取当前坐标和姿态""" + logger.debug(f"[Mock] 获取坐标: {self._coords}") + return True, self._coords + + def set_coords(self, coords: List[float], speed: int = 500) -> bool: + """设置坐标和姿态""" + if len(coords) != 6: + return False + logger.info(f"[Mock] 设置坐标: {coords}, 速度: {speed}") + self._coords = list(coords) + return True + + def jog_coord(self, axis: str, direction: int, speed: int = 500) -> bool: + """连续调节坐标轴""" + logger.debug(f"[Mock] jog_coord({axis}, {direction}, {speed})") + return True + + def power_on(self) -> bool: + """上电""" + logger.info("[Mock] 机械臂上电") + self._power_on = True + return True + + def state_on(self) -> bool: + """启用状态""" + logger.info("[Mock] 机械臂状态启用") + self._state_on = True + return True + + def state_off(self) -> bool: + """禁用状态""" + logger.info("[Mock] 机械臂状态禁用") + self._state_on = False + return True + + def state_check(self) -> bool: + """检查机械臂状态""" + return self._state_on + + def check_running(self) -> bool: + """检查机械臂是否在运行""" + return False # Mock模式下始终不在运行 + + def wait_done(self, timeout: float = 30) -> bool: + """等待上一条命令执行完成""" + logger.debug(f"[Mock] wait_done({timeout}s) - 立即返回") + return True + + def task_stop(self) -> bool: + """停止任务""" + logger.info("[Mock] 停止机械臂任务") + return True + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *args): + self.close() + + +class MockAGVController: + """Mock AGV 控制器""" + + def __init__(self, device: str = "/dev/agvpro_controller", baudrate: int = 1000000): + self.device = device + self.baudrate = baudrate + self._connected = False + self._position = [0.0, 0.0, 0.0] # [x, y, yaw] + self._voltage = 48.0 # 模拟电压 + self._moving = False + + def _run_ros2_cmd(self, cmd: str, timeout: float = 5.0) -> tuple: + """执行 ros2 命令(Mock)""" + logger.debug(f"[Mock] ros2 命令: {cmd}") + return 0, "", "" + + def connect(self) -> bool: + """连接 AGV(Mock)""" + logger.info(f"[Mock] 连接 AGV 控制器 {self.device}") + self._connected = True + return True + + def is_connected(self) -> bool: + """检查是否已连接""" + return self._connected + + def move_forward(self, speed: float = 1.0, duration: float = None): + """前进""" + logger.info(f"[Mock] AGV 前进,速度: {speed}, 时长: {duration}") + self._moving = True + if duration: + time.sleep(min(duration, 0.1)) # Mock 模式下只短暂等待 + self.stop() + + def move_backward(self, speed: float = 1.0, duration: float = None): + """后退""" + logger.info(f"[Mock] AGV 后退,速度: {speed}, 时长: {duration}") + if duration: + time.sleep(min(duration, 0.1)) + self.stop() + + def turn_left(self, speed: float = 1.0, duration: float = None): + """左转""" + logger.info(f"[Mock] AGV 左转,速度: {speed}, 时长: {duration}") + if duration: + time.sleep(min(duration, 0.1)) + self.stop() + + def turn_right(self, speed: float = 1.0, duration: float = None): + """右转""" + logger.info(f"[Mock] AGV 右转,速度: {speed}, 时长: {duration}") + if duration: + time.sleep(min(duration, 0.1)) + self.stop() + + def move_left_lateral(self, speed: float = 1.0, duration: float = None): + """向左横向移动""" + logger.info(f"[Mock] AGV 向左横向移动,速度: {speed}, 时长: {duration}") + if duration: + time.sleep(min(duration, 0.1)) + self.stop() + + def move_right_lateral(self, speed: float = 1.0, duration: float = None): + """向右横向移动""" + logger.info(f"[Mock] AGV 向右横向移动,速度: {speed}, 时长: {duration}") + if duration: + time.sleep(min(duration, 0.1)) + self.stop() + + def stop(self): + """停止""" + logger.debug("[Mock] AGV 停止") + self._moving = False + + def get_position(self) -> Optional[List[float]]: + """获取 AGV 当前位置""" + logger.debug(f"[Mock] 获取 AGV 位置: {self._position}") + return self._position + + def go_to_point(self, x: float, y: float, rz: float = None, speed: float = 1.0) -> bool: + """移动到目标点""" + logger.info(f"[Mock] AGV 移动到目标点: ({x}, {y}), yaw: {rz}") + # 模拟移动完成 + self._position = [x, y, rz if rz is not None else self._position[2]] + return True + + def get_battery(self) -> Optional[float]: + """获取电池电压""" + logger.debug(f"[Mock] 获取电池电压: {self._voltage}V") + return self._voltage + + def disconnect(self): + """断开连接""" + self.stop() + self._connected = False + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *args): + self.disconnect() + + +class MockQRScanner: + """Mock 二维码扫描器""" + + def __init__(self, device_index: int = 0, width: int = 640, height: int = 400, prefer_v4l2_ctl: bool = True): + self.device_index = device_index + self.width = width + self.height = height + self._opened = False + # Mock 二维码内容 + self._mock_qr_code = "MOCK_QR_SN_12345" + + def open(self) -> bool: + """打开摄像头(Mock)""" + logger.info(f"[Mock] 打开摄像头 /dev/video{self.device_index}") + self._opened = True + return True + + def close(self): + """关闭摄像头""" + self._opened = False + logger.info("[Mock] 关闭摄像头") + + def read_frame(self, timeout: float = 2.0) -> Optional[np.ndarray]: + """读取一帧(Mock)""" + if not self._opened: + return None + # 返回空白图像 + return np.zeros((self.height, self.width, 3), dtype=np.uint8) + + def detect_qr(self, frame: np.ndarray) -> Optional[str]: + """从图像帧中检测二维码(Mock)""" + # 始终返回模拟二维码 + logger.debug(f"[Mock] 检测到二维码: {self._mock_qr_code}") + return self._mock_qr_code + + def scan_once(self) -> Optional[str]: + """扫描一次(Mock)""" + logger.debug("[Mock] 扫描二维码...") + return self._mock_qr_code + + def scan_with_retry(self, max_attempts: int = 5, interval: float = 0.5) -> Optional[str]: + """多次扫描(Mock)""" + logger.info(f"[Mock] scan_with_retry - 直接返回模拟二维码") + return self._mock_qr_code + + def get_preview_frame(self) -> Optional[np.ndarray]: + """获取预览帧(Mock)""" + return self.read_frame() + + def set_mock_qr_code(self, code: str): + """设置模拟二维码内容(仅 Mock 模式)""" + self._mock_qr_code = code + logger.info(f"[Mock] 设置模拟二维码: {code}") + + def __enter__(self): + self.open() + return self + + def __exit__(self, *args): + self.close() + + +class MockNav2Status(Enum): + """Mock Nav2 状态""" + IDLE = "idle" + NAVIGATING = "navigating" + SUCCEEDED = "succeeded" + FAILED = "failed" + CANCELLED = "cancelled" + + +class MockNav2Navigator: + """Mock Nav2 导航器""" + + def __init__(self): + self.status = MockNav2Status.IDLE + self._current_pose = [0.0, 0.0, 0.0] # [x, y, yaw] + + def _get_current_pose(self) -> List[float]: + """获取当前位置(Mock)""" + return self._current_pose + + def navigate_to_pose(self, x: float, y: float, yaw: float = None, + timeout_sec: float = 120.0, + blocking: bool = True) -> bool: + """导航到目标坐标(Mock)""" + logger.info(f"[Mock] 导航到: ({x:.3f}, {y:.3f}), yaw={yaw:.1f}°") + self.status = MockNav2Status.NAVIGATING + # 模拟导航成功 + self._current_pose = [x, y, yaw if yaw is not None else 0.0] + self.status = MockNav2Status.SUCCEEDED + return True + + def navigate_through_poses(self, poses: List[Tuple[float, float, float]], + timeout_per_pose: float = 120.0, + blocking: bool = True) -> bool: + """通过多个点位导航(Mock)""" + logger.info(f"[Mock] 通过 {len(poses)} 个点位导航") + for i, (x, y, yaw) in enumerate(poses): + logger.debug(f"[Mock] 点位 {i+1}: ({x}, {y}), yaw={yaw}") + if poses: + self._current_pose = list(poses[-1]) + self.status = MockNav2Status.SUCCEEDED + return True + + def stop(self): + """停止导航(Mock)""" + logger.info("[Mock] 停止导航") + self.status = MockNav2Status.IDLE + + def get_status(self) -> dict: + """获取导航状态(Mock)""" + return { + "status": self.status.value, + "position": self._current_pose + } + + def get_current_position(self) -> List[float]: + """获取当前位置(Mock)""" + return self._current_pose + + +# 导出兼容别名 +Nav2Status = MockNav2Status diff --git a/scripts/dev-backend.sh b/scripts/dev-backend.sh new file mode 100755 index 0000000..3b5ff2f --- /dev/null +++ b/scripts/dev-backend.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# ============================================================ +# dev-backend.sh - 本地后端开发启动(Mock 硬件模式) +# 用法: ./scripts/dev-backend.sh +# 说明: 启动 Flask 后端,使用 Mock 硬件实现,无需真实硬件 +# ============================================================ +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +AGV_APP_DIR="$PROJECT_DIR/agv_app" + +echo "==========================================" +echo " 本地开发模式 - Flask 后端 (Mock 硬件)" +echo "==========================================" +echo "" +echo " Mock 硬件模式已启用:" +echo " - AGV 控制器: Mock" +echo " - 机械臂: Mock" +echo " - 摄像头: Mock" +echo " - Nav2 导航: Mock" +echo "" +echo " 访问: http://127.0.0.1:5000" +echo " Ctrl+C 停止" +echo "" + +# 设置环境变量启用 Mock 模式 +export MOCK_HARDWARE=1 +export FLASK_PORT=5000 + +cd "$AGV_APP_DIR" +exec uv run --locked python app.py diff --git a/scripts/dev-frontend.sh b/scripts/dev-frontend.sh new file mode 100755 index 0000000..2cbc1a1 --- /dev/null +++ b/scripts/dev-frontend.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# ============================================================ +# dev-frontend.sh - 前端开发启动 +# 用法: ./scripts/dev-frontend.sh +# 说明: 启动 Next.js 开发服务器,API 代理到后端 +# ============================================================ +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +FRONTEND_DIR="$SCRIPT_DIR/../public-frontend" + +echo "==========================================" +echo " 前端开发模式 - Next.js" +echo "==========================================" +echo "" +echo " 后端 URL: ${BACKEND_URL:-http://127.0.0.1:5000}" +echo " 访问: http://localhost:3000" +echo " Ctrl+C 停止" +echo "" + +# 确保后端 URL 设置(默认本地) +export BACKEND_URL=${BACKEND_URL:-http://127.0.0.1:5000} +export NEXT_PUBLIC_BACKEND_URL=${BACKEND_URL} + +cd "$FRONTEND_DIR" +exec npm run dev diff --git a/scripts/dev_start.sh b/scripts/dev_start.sh deleted file mode 100755 index c1e02ed..0000000 --- a/scripts/dev_start.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# ============================================================ -# dev_start.sh - 本地开发环境启动(不启动 ROS2/机械臂硬件) -# 用法: ./scripts/dev_start.sh -# ============================================================ -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -AGV_APP_DIR="$PROJECT_DIR/agv_app" -AGV_ROS2_DIR="${AGV_ROS2_DIR:-$HOME/agv_pro_ros2}" -ROS_DISTRO="${ROS_DISTRO:-humble}" -ROS_SETUP="${ROS_SETUP:-/opt/ros/$ROS_DISTRO/setup.bash}" -ROS_WORKSPACE_SETUP="${ROS_WORKSPACE_SETUP:-$AGV_ROS2_DIR/install/setup.bash}" -FLASK_PORT="${FLASK_PORT:-5000}" - -echo "==========================================" -echo " 本地开发模式 - 仅启动 Flask" -echo "==========================================" -echo "" - -# 切换到项目目录 -source "$ROS_SETUP" 2>/dev/null || true -source "$ROS_WORKSPACE_SETUP" 2>/dev/null || true - -cd "$AGV_APP_DIR" - -# 检查是否有运行的 Flask 进程 -FLASK_PID=$(pgrep -f "python.*app.py" 2>/dev/null || true) -if [ -n "$FLASK_PID" ]; then - echo "Flask 已在运行 (PID: $FLASK_PID)" - read -p "是否重启? [y/N] " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - kill "$FLASK_PID" 2>/dev/null - sleep 1 - else - echo "保持现有进程,退出" - exit 0 - fi -fi - -# 使用前台模式运行(方便看日志和 Ctrl+C 停止) -echo "启动 Flask (前台模式,Ctrl+C 停止)..." -echo "访问: http://127.0.0.1:$FLASK_PORT" -echo "" -exec uv run --locked python app.py diff --git a/scripts/start_all.sh b/scripts/prod-backend.sh similarity index 100% rename from scripts/start_all.sh rename to scripts/prod-backend.sh diff --git a/scripts/restart_flask.sh b/scripts/restart_flask.sh deleted file mode 100755 index 3f5250a..0000000 --- a/scripts/restart_flask.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# ============================================================ -# restart_flask.sh - 语法检查 + 重启 Flask + 验证 -# 用法: ssh elephant@ 'bash -s' < scripts/restart_flask.sh -# 或在 AGV 上: cd ~/work/smart-inspection && ./scripts/restart_flask.sh -# ============================================================ -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -AGV_PROJECT_DIR="${AGV_PROJECT_DIR:-$PROJECT_DIR}" -AGV_APP_DIR="${AGV_APP_DIR:-$AGV_PROJECT_DIR/agv_app}" -AGV_ROS2_DIR="${AGV_ROS2_DIR:-$HOME/agv_pro_ros2}" -ROS_DISTRO="${ROS_DISTRO:-humble}" -ROS_SETUP="${ROS_SETUP:-/opt/ros/$ROS_DISTRO/setup.bash}" -ROS_WORKSPACE_SETUP="${ROS_WORKSPACE_SETUP:-$AGV_ROS2_DIR/install/setup.bash}" -LOG_DIR="${LOG_DIR:-/tmp}" -FLASK_PORT="${FLASK_PORT:-5000}" -FLASK_LOG="$LOG_DIR/agv_flask.log" - -mkdir -p "$LOG_DIR" - -source "$ROS_SETUP" 2>/dev/null || true -source "$ROS_WORKSPACE_SETUP" 2>/dev/null || true - -cd "$AGV_APP_DIR" - -echo "==========================================" -echo " 重启 Flask 服务" -echo "==========================================" -echo "" - -# 1. 语法检查 -echo "[1/3] Python 语法检查..." -uv run --locked python -m py_compile app.py -if [ $? -ne 0 ]; then - echo "❌ 语法错误,请先修复" - exit 1 -fi -echo " ✅ 语法检查通过" - -# 2. 清缓存 + 重启 -echo "[2/3] 清理缓存并重启..." -find "$AGV_APP_DIR" -name '*.pyc' -delete 2>/dev/null -find "$AGV_APP_DIR" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null - -pkill -f "python.*app.py" 2>/dev/null || true -pkill -f "uv run .*python app.py" 2>/dev/null || true -sleep 1 -nohup uv run --locked python app.py > "$FLASK_LOG" 2>&1 & -FLASK_PID=$! -echo " Flask PID: $FLASK_PID" - -# 3. 验证 -echo "[3/3] 验证服务..." -sleep 3 -if ss -tlnp 2>/dev/null | grep -q ":$FLASK_PORT " || netstat -tlnp 2>/dev/null | grep -q ":$FLASK_PORT "; then - echo " ✅ 端口 $FLASK_PORT 正常监听" - # 测试机械臂摄像头单帧 - result=$(curl -s --max-time 5 "http://127.0.0.1:$FLASK_PORT/api/camera/arm_refresh" 2>/dev/null | head -c 4) - if [ "$result" = "$(echo -en '\xff\xd8\xff\xe0')" ]; then - echo " ✅ arm_refresh 返回 JPEG" - else - echo " ⚠️ arm_refresh 返回异常(机械臂可能未连接)" - fi -else - echo " ❌ 端口 $FLASK_PORT 未监听,查看日志:" - tail -10 "$FLASK_LOG" - exit 1 -fi - -echo "" -echo "==========================================" -echo " ✅ 重启完成" -echo "==========================================" diff --git a/scripts/start_flask.sh b/scripts/start_flask.sh deleted file mode 100755 index b293a3e..0000000 --- a/scripts/start_flask.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -# ============================================================ -# start_flask.sh - 仅启动/重启 Flask 服务(不启动 ROS2) -# 适用于: 修改了前端/API 代码后快速重启 -# ============================================================ -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -AGV_PROJECT_DIR="${AGV_PROJECT_DIR:-$PROJECT_DIR}" -AGV_APP_DIR="${AGV_APP_DIR:-$AGV_PROJECT_DIR/agv_app}" -AGV_ROS2_DIR="${AGV_ROS2_DIR:-$HOME/agv_pro_ros2}" -ROS_DISTRO="${ROS_DISTRO:-humble}" -ROS_SETUP="${ROS_SETUP:-/opt/ros/$ROS_DISTRO/setup.bash}" -ROS_WORKSPACE_SETUP="${ROS_WORKSPACE_SETUP:-$AGV_ROS2_DIR/install/setup.bash}" -LOG_DIR="${LOG_DIR:-/tmp}" -FLASK_PORT="${FLASK_PORT:-5000}" -FLASK_LOG="$LOG_DIR/agv_flask.log" - -mkdir -p "$LOG_DIR" - -pkill -f "python.*app.py" 2>/dev/null || true -pkill -f "uv run .*python app.py" 2>/dev/null || true -sleep 1 - -source "$ROS_SETUP" 2>/dev/null || true -source "$ROS_WORKSPACE_SETUP" 2>/dev/null || true - -cd "$AGV_APP_DIR" -nohup uv run --locked python app.py > "$FLASK_LOG" 2>&1 & -echo "Flask started, PID: $!" -sleep 2 - -if ss -tlnp 2>/dev/null | grep -q ":$FLASK_PORT " || netstat -tlnp 2>/dev/null | grep -q ":$FLASK_PORT "; then - echo "✅ 端口 $FLASK_PORT 正常" -else - echo "⚠️ 端口 $FLASK_PORT 未监听,检查 $FLASK_LOG" -fi diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 0000000..2f4c2b8 --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# ============================================================ +# stop.sh - 停止开发服务 +# 用法: ./scripts/stop.sh +# 说明: 停止 Flask 和 Next.js 开发服务器 +# ============================================================ +set -e + +echo "==========================================" +echo " 停止开发服务" +echo "==========================================" +echo "" + +# 停止 Flask +if pgrep -f "python.*app.py" > /dev/null 2>&1; then + echo "停止 Flask..." + pkill -f "python.*app.py" || true + pkill -f "uv run .*python app.py" || true + echo " ✓ Flask 已停止" +else + echo " - Flask 未运行" +fi + +# 停止 Next.js +if pgrep -f "next dev" > /dev/null 2>&1; then + echo "停止 Next.js..." + pkill -f "next dev" || true + echo " ✓ Next.js 已停止" +else + echo " - Next.js 未运行" +fi + +echo "" +echo "完成"