Refactor infrastructure scripts and add mock hardware support

Changes:
- Refactor project scripts for better dev/prod workflow separation
- Add mock_hardware.py for local development without real hardware
- Add Makefile for common commands
- Add .env.example for environment variable reference
- Split scripts into dev-backend.sh, dev-frontend.sh, prod-backend.sh
- Add stop.sh for clean shutdown

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 12:31:32 +08:00
parent 1429442dbd
commit cb6498cd2b
12 changed files with 592 additions and 167 deletions
+21
View File
@@ -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
+57
View File
@@ -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 "完成"
+35 -9
View File
@@ -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()
+5
View File
@@ -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",
+382
View File
@@ -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:
"""连接 AGVMock"""
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
+32
View File
@@ -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
+26
View File
@@ -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
-47
View File
@@ -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
-75
View File
@@ -1,75 +0,0 @@
#!/bin/bash
# ============================================================
# restart_flask.sh - 语法检查 + 重启 Flask + 验证
# 用法: ssh elephant@<AGV_IP> '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 "=========================================="
-36
View File
@@ -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
+34
View File
@@ -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 "完成"