diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..384f01f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# Repository Guidelines + +## 项目结构与模块组织 + +本仓库是 AGV 智能巡检系统,后端、前端、ROS2 启动链路分目录维护。`agv_app/` 是 Flask 后端,包含 API、模板、静态资源与 `data/*.json` 持久化配置;`agv_app/utils/` 放置 AGV、Nav2、机械臂、二维码与任务执行逻辑。`arm_server/` 是机械臂端 TCP/摄像头服务及 systemd 配置。`scan_fixer/` 提供生产链路中的 ROS2 时间戳修正工具。`public-frontend/` 是 Next.js 平板端,源码在 `src/app`、`src/components`、`src/services`、`src/store`、`src/types`。`scripts/` 与 `Makefile` 负责本地开发、生产启动和停止流程,`docs/` 存放技术文档。 + +## 构建、测试与开发命令 + +- `make install`:执行 `uv sync` 并安装前端 npm 依赖。 +- `make dev`:显示本地开发启动说明。 +- `make dev-backend`:以 Mock 硬件模式启动 Flask 后端。 +- `make dev-frontend`:启动 Next.js 开发服务器。 +- `make prod`:在 AGV 上启动 ROS2、Nav2、scan fixer 与 Flask 完整生产链路。 +- `cd public-frontend && npm run build`:构建前端。 +- `cd public-frontend && npm run lint && npm run typecheck`:执行前端 lint 与 TypeScript 检查。 + +## 编码风格与命名约定 + +Python 目标版本为 3.10,依赖由根目录 `pyproject.toml` 和 `uv.lock` 管理。保持模块职责清晰,配置集中放在 `agv_app/config.py`,复用硬件抽象时优先放入 `agv_app/utils/`。前端使用 TypeScript、React、Next.js App Router 与 Ant Design;组件使用 PascalCase,例如 `CameraFrame.tsx`,服务和工具使用 camelCase,例如 `apiClient.ts`。新增代码应自解释命名,避免魔术值,必要注释只说明业务背景或非常规取舍。 + +## 测试指南 + +当前仓库未配置独立单元测试框架。提交前至少运行相关静态检查:前端改动运行 `npm run lint` 和 `npm run typecheck`,后端改动用 `make dev-backend` 做 Flask 启动与关键接口冒烟验证。涉及硬件、ROS2 或生产脚本的改动,应说明是否已在真实 AGV 或 Mock 模式验证。 + +## 提交与 Pull Request 规范 + +历史提交主要使用英文祈使句式,例如 `Improve camera status and production startup`、`Refactor ROS startup scripts`、`Fix shell compatibility issues in prod-backend.sh`。继续使用简短英文提交信息,避免无意义占位。PR 应包含变更摘要、验证命令、硬件/环境影响、相关 issue;前端界面变化需附截图或录屏,生产启动链路变化需列出影响的脚本和服务。 + +## 安全与配置提示 + +不要提交新的密钥、令牌、设备私有地址或本地日志。环境示例放在 `.env.example`,运行时覆盖优先使用环境变量,例如 `BACKEND_URL`、`MOCK_HARDWARE`、`AGV_PROJECT_DIR`。涉及 AGV、机械臂或 ROS2 生产配置时,先核对 `scripts/README.md` 中的默认路径与日志位置。 diff --git a/agv_app/app.py b/agv_app/app.py index c37c709..6ba1caa 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -46,6 +46,15 @@ logging.basicConfig( ) logger = logging.getLogger("agv_app") +ORIGIN_X = 0.0 +ORIGIN_Y = 0.0 +ORIGIN_YAW = 0.0 +ORIGIN_MATCH_TOLERANCE = 1e-6 +ARM_RETURN_SPEED = 500 +ARM_INITIAL_POSE_TIMEOUT = 15.0 +ARM_INITIAL_POSE_TOLERANCE = 2.0 +ARM_INITIAL_POSE_UNSET_TOLERANCE = 0.01 + app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/static") app.config["SECRET_KEY"] = SERVER_CONFIG["secret_key"] CORS(app) @@ -88,6 +97,60 @@ gs = GlobalState() # ========== 辅助函数 ========== +def _is_origin_goal(x: float, y: float, yaw: float = None) -> bool: + yaw_is_origin = yaw is None or abs(yaw - ORIGIN_YAW) <= ORIGIN_MATCH_TOLERANCE + return ( + abs(x - ORIGIN_X) <= ORIGIN_MATCH_TOLERANCE + and abs(y - ORIGIN_Y) <= ORIGIN_MATCH_TOLERANCE + and yaw_is_origin + ) + + +def _normalize_arm_pose(raw_pose): + if not isinstance(raw_pose, list) or len(raw_pose) != 6: + return None + try: + return [float(angle) for angle in raw_pose] + except (TypeError, ValueError): + return None + + +def _wait_arm_initial_pose(target_angles, timeout=ARM_INITIAL_POSE_TIMEOUT, tolerance=ARM_INITIAL_POSE_TOLERANCE): + deadline = time.time() + timeout + while time.time() < deadline: + try: + ok, current_angles = gs.arm_client.get_angles() + if ok and current_angles and len(current_angles) >= 6: + current_angles = [float(angle) for angle in current_angles[:6]] + if all(abs(current_angles[index] - target_angles[index]) <= tolerance for index in range(6)): + return True + except Exception as e: + logger.warning(f"等待机械臂初始姿态失败: {e}") + time.sleep(0.5) + return False + + +def _restore_arm_initial_pose_for_origin(): + arm_initial_pose = _normalize_arm_pose(gs.mission_config.get("arm_initial_pose")) + if not arm_initial_pose: + return {"ok": True, "skipped": True, "message": "未配置有效机械臂初始姿态,跳过复原"} + if not any(abs(angle) > ARM_INITIAL_POSE_UNSET_TOLERANCE for angle in arm_initial_pose): + return {"ok": True, "skipped": True, "message": "未配置机械臂初始姿态,跳过复原"} + if not gs.arm_client: + return {"ok": False, "error": "机械臂未连接,无法先复原再回原点"} + + try: + ok = gs.arm_client.set_angles(arm_initial_pose, ARM_RETURN_SPEED) + if not ok: + return {"ok": False, "error": "机械臂初始姿态指令发送失败,已取消回原点"} + if not _wait_arm_initial_pose(arm_initial_pose): + return {"ok": False, "error": "机械臂未在规定时间内复原,已取消回原点"} + return {"ok": True, "skipped": False, "message": "机械臂已复原到初始姿态"} + except Exception as e: + logger.error(f"回原点前复原机械臂失败: {e}") + return {"ok": False, "error": f"回原点前复原机械臂失败: {e}"} + + def get_data_path(name): return os.path.join(DATA_DIR, name) @@ -464,24 +527,38 @@ def api_navigate_to(): if not gs.map_config or "map_yaml" not in gs.map_config: return jsonify({"ok": False, "error": "地图未加载,请先在设置中加载地图"}), 400 - data = request.json + data = request.json or {} goal_x = data.get("x") goal_y = data.get("y") goal_yaw = data.get("yaw") # 姿态参数,可选 if goal_x is None or goal_y is None: return jsonify({"ok": False, "error": "缺少目标坐标 x, y"}), 400 + try: + goal_x = float(goal_x) + goal_y = float(goal_y) + yaw_arg = float(goal_yaw) if goal_yaw is not None else None + except (TypeError, ValueError): + return jsonify({"ok": False, "error": "目标坐标格式错误"}), 400 if not gs.agv_controller or not gs.agv_controller.is_connected(): return jsonify({"ok": False, "error": "AGV 未连接,请先连接 AGV"}), 400 try: + restore_message = "" + restore_arm = bool(data.get("restore_arm", True)) + if restore_arm and _is_origin_goal(goal_x, goal_y, yaw_arg): + restore_result = _restore_arm_initial_pose_for_origin() + if not restore_result["ok"]: + return jsonify({"ok": False, "error": restore_result["error"]}), 400 + restore_message = restore_result.get("message", "") + if gs.navigator is None: gs.navigator = Nav2Navigator() # navigate_to_pose(x, y, yaw=None, timeout_sec=120, blocking=False) - yaw_arg = float(goal_yaw) if goal_yaw is not None else None - ok = gs.navigator.navigate_to_pose(float(goal_x), float(goal_y), yaw_arg, blocking=False) + ok = gs.navigator.navigate_to_pose(goal_x, goal_y, yaw_arg, blocking=False) if ok: - return jsonify({"ok": True, "message": "导航已启动"}) + message = f"{restore_message},导航已启动" if restore_message else "导航已启动" + return jsonify({"ok": True, "message": message}) else: return jsonify({"ok": False, "error": "导航启动失败,可能是Nav2未运行或AGV未连接"}), 400 except Exception as e: diff --git a/agv_app/data/machines_config.json b/agv_app/data/machines_config.json index e7b572f..f5179f2 100644 --- a/agv_app/data/machines_config.json +++ b/agv_app/data/machines_config.json @@ -1,25 +1,4 @@ [ - { - "id": "m_2_0", - "row": 2, - "col": 0, - "front": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "back": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - } - }, { "id": "m_2_2", "row": 2, @@ -167,27 +146,6 @@ "poses": [] } }, - { - "id": "m_0_4", - "row": 0, - "col": 4, - "front": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "back": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - } - }, { "id": "m_0_5", "row": 0, @@ -251,27 +209,6 @@ "poses": [] } }, - { - "id": "m_2_1", - "row": 2, - "col": 1, - "front": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "back": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - } - }, { "id": "m_2_4", "row": 2, @@ -293,27 +230,6 @@ "poses": [] } }, - { - "id": "m_2_5", - "row": 2, - "col": 5, - "front": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "back": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - } - }, { "id": "m_0_0", "row": 0, @@ -356,36 +272,6 @@ "poses": [] } }, - { - "id": "m_0_1", - "row": 0, - "col": 1, - "front": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "back": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "qr": { - "coords": [ - 0, - 0, - 0 - ], - "qr_value": "", - "model_id": "" - } - }, { "id": "m_0_2", "row": 0, @@ -416,36 +302,6 @@ "model_id": "" } }, - { - "id": "m_0_3", - "row": 0, - "col": 3, - "front": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "back": { - "coords": [ - 0, - 0, - 0 - ], - "poses": [] - }, - "qr": { - "coords": [ - 0, - 0, - 0 - ], - "qr_value": "", - "model_id": "" - } - }, { "id": "m_1_1", "row": 1, @@ -506,6 +362,126 @@ "model_id": "" } }, + { + "id": "m_1_4", + "row": 1, + "col": 4, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, + { + "id": "m_0_6", + "row": 0, + "col": 6, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, + { + "id": "m_1_6", + "row": 1, + "col": 6, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, + { + "id": "m_0_3", + "row": 0, + "col": 3, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, { "id": "m_1_3", "row": 1, @@ -537,8 +513,98 @@ } }, { - "id": "m_1_4", - "row": 1, + "id": "m_2_0", + "row": 2, + "col": 0, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, + { + "id": "m_2_1", + "row": 2, + "col": 1, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, + { + "id": "m_0_1", + "row": 0, + "col": 1, + "front": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "back": { + "coords": [ + 0, + 0, + 0 + ], + "poses": [] + }, + "qr": { + "coords": [ + 0, + 0, + 0 + ], + "qr_value": "", + "model_id": "" + } + }, + { + "id": "m_0_4", + "row": 0, "col": 4, "front": { "coords": [ diff --git a/agv_app/data/mission_config.json b/agv_app/data/mission_config.json index 380a347..f0ba9f3 100644 --- a/agv_app/data/mission_config.json +++ b/agv_app/data/mission_config.json @@ -1,6 +1,6 @@ { - "rows": 2, - "cols": 5, + "rows": 3, + "cols": 7, "grid": [], "positions": [ { @@ -8,9 +8,9 @@ "col": 0, "side": "front", "coords": [ - 0.5391402360519819, - -1.3221212932804989, - -0.04968159116162075 + 0.726789462066099, + 0.38329959318460166, + 0.023383889548769105 ], "poses": [] }, @@ -19,9 +19,9 @@ "col": 1, "side": "front", "coords": [ - 1.1801154538454173, - -1.3641834306281595, - -0.0384636372066124 + 1.5997282224959652, + 0.3969445083875386, + 0.021611522117307245 ], "poses": [] }, @@ -30,9 +30,9 @@ "col": 1, "side": "back", "coords": [ - 1.3273254588744863, - -3.5287940020200854, - -3.11993523836094 + 1.5365709266996668, + -1.5897806027940793, + -3.138003565212702 ], "poses": [] }, @@ -41,108 +41,9 @@ "col": 0, "side": "back", "coords": [ - 0.6499623251724095, - -3.634895898964233, - -3.06371982741706 - ], - "poses": [] - }, - { - "row": 0, - "col": 2, - "side": "front", - "coords": [ - 1.9780660285152205, - -1.4118225222055494, - -0.03933461738640764 - ], - "poses": [] - }, - { - "row": 0, - "col": 3, - "side": "front", - "coords": [ - 2.783104887196572, - -1.4531680360293173, - -0.005407493209801511 - ], - "poses": [] - }, - { - "row": 0, - "col": 4, - "side": "front", - "coords": [ - 3.4135017183966694, - -1.463517938299615, - -0.0022379727318056074 - ], - "poses": [] - }, - { - "row": 1, - "col": 4, - "side": "back", - "coords": [ - 3.595502564320599, - -3.5861571623928663, - 3.105599537556842 - ], - "poses": [] - }, - { - "row": 1, - "col": 4, - "side": "front", - "coords": [ - 3.595502564320599, - -3.5861571623928663, - 3.105599537556842 - ], - "poses": [] - }, - { - "row": 1, - "col": 3, - "side": "back", - "coords": [ - 2.8436692518324937, - -3.5087893361886504, - -3.0640151322957476 - ], - "poses": [] - }, - { - "row": 1, - "col": 3, - "side": "front", - "coords": [ - 2.8436692518324937, - -3.5087893361886504, - -3.0640151322957476 - ], - "poses": [] - }, - { - "row": 1, - "col": 2, - "side": "back", - "coords": [ - 2.0238357078548397, - -3.519588818855445, - -3.0949553553741684 - ], - "poses": [] - }, - { - "row": 1, - "col": 2, - "side": "front", - "coords": [ - 2.0238357078548397, - -3.519588818855445, - -3.0949553553741684 + 0.7250933954217442, + -1.585626974855502, + 3.1407193246894285 ], "poses": [] }, @@ -151,9 +52,9 @@ "col": 1, "side": "front", "coords": [ - 1.3273254588744863, - -3.5287940020200854, - -3.11993523836094 + 1.5365709266996668, + -1.5897806027940793, + -3.138003565212702 ], "poses": [] }, @@ -162,9 +63,9 @@ "col": 0, "side": "front", "coords": [ - 0.6499623251724095, - -3.634895898964233, - -3.06371982741706 + 0.7250933954217442, + -1.585626974855502, + 3.1407193246894285 ], "poses": [] }, @@ -173,9 +74,9 @@ "col": 0, "side": "back", "coords": [ - 0.39025594509020667, - -5.593393651151741, - -0.09001000079607593 + 0.695891857743624, + -3.6093540626907155, + -0.01891669546737457 ], "poses": [] }, @@ -184,9 +85,196 @@ "col": 1, "side": "back", "coords": [ - 0.9989880876134289, - -5.633047903271201, - -0.08518177305398167 + 1.7054640840200619, + -3.6172190671922944, + 0.04050928996758301 + ], + "poses": [] + }, + { + "row": 0, + "col": 2, + "side": "front", + "coords": [ + 2.569630979384912, + 0.39887714413011477, + 0.013956327040945097 + ], + "poses": [] + }, + { + "row": 0, + "col": 3, + "side": "front", + "coords": [ + 3.5131599402572933, + 0.3616416504697895, + 0.0177534721641621 + ], + "poses": [] + }, + { + "row": 0, + "col": 4, + "side": "front", + "coords": [ + 4.385626456624971, + 0.37358124345384475, + 0.014464186351950986 + ], + "poses": [] + }, + { + "row": 0, + "col": 5, + "side": "front", + "coords": [ + 5.286651848873122, + 0.36270375923170595, + -0.02478120105661721 + ], + "poses": [] + }, + { + "row": 0, + "col": 6, + "side": "front", + "coords": [ + 6.301663107708812, + 0.35009837193686855, + 0.05028809910702322 + ], + "poses": [] + }, + { + "row": 1, + "col": 6, + "side": "back", + "coords": [ + 6.302211902696101, + -1.6741865723108142, + -3.141294889035836 + ], + "poses": [] + }, + { + "row": 1, + "col": 6, + "side": "front", + "coords": [ + 6.302211902696101, + -1.6741865723108142, + -3.141294889035836 + ], + "poses": [] + }, + { + "row": 1, + "col": 5, + "side": "back", + "coords": [ + 5.385423949389957, + -1.6569851054137088, + 3.114584306608913 + ], + "poses": [] + }, + { + "row": 1, + "col": 5, + "side": "front", + "coords": [ + 5.385423949389957, + -1.6569851054137088, + 3.114584306608913 + ], + "poses": [] + }, + { + "row": 1, + "col": 4, + "side": "back", + "coords": [ + 4.520056315813734, + -1.6370730446353792, + 3.12134578550089 + ], + "poses": [] + }, + { + "row": 1, + "col": 4, + "side": "front", + "coords": [ + 4.520056315813734, + -1.6370730446353792, + 3.12134578550089 + ], + "poses": [] + }, + { + "row": 1, + "col": 3, + "side": "back", + "coords": [ + 3.600184234659078, + -1.6299160740114962, + -3.127182700330921 + ], + "poses": [] + }, + { + "row": 1, + "col": 3, + "side": "front", + "coords": [ + 3.600184234659078, + -1.6299160740114962, + -3.127182700330921 + ], + "poses": [] + }, + { + "row": 1, + "col": 2, + "side": "back", + "coords": [ + 2.5627723519921295, + -1.606403204776104, + 3.1392697865149666 + ], + "poses": [] + }, + { + "row": 1, + "col": 2, + "side": "front", + "coords": [ + 2.5627723519921295, + -1.606403204776104, + 3.1392697865149666 + ], + "poses": [] + }, + { + "row": 2, + "col": 0, + "side": "front", + "coords": [ + 0.695891857743624, + -3.6093540626907155, + -0.01891669546737457 + ], + "poses": [] + }, + { + "row": 2, + "col": 1, + "side": "front", + "coords": [ + 1.7054640840200619, + -3.6172190671922944, + 0.04050928996758301 ], "poses": [] }, @@ -195,9 +283,20 @@ "col": 2, "side": "back", "coords": [ - 1.7493440267548326, - -5.7036971258959746, - -0.10505541857885684 + 2.49138966620064, + -3.6258193640543435, + 0.011734020269766346 + ], + "poses": [] + }, + { + "row": 2, + "col": 2, + "side": "front", + "coords": [ + 2.49138966620064, + -3.6258193640543435, + 0.011734020269766346 ], "poses": [] }, @@ -206,9 +305,20 @@ "col": 3, "side": "back", "coords": [ - 2.407336669431407, - -5.76958512207572, - -0.10251877401062247 + 3.384299605055195, + -3.633059580331103, + 0.00825489611393585 + ], + "poses": [] + }, + { + "row": 2, + "col": 3, + "side": "front", + "coords": [ + 3.384299605055195, + -3.633059580331103, + 0.00825489611393585 ], "poses": [] }, @@ -217,19 +327,129 @@ "col": 4, "side": "back", "coords": [ - 3.132062476985721, - -5.850166571217611, - -0.09127007182804729 + 4.499614777619532, + -3.6568143703418405, + 0.006128126167414207 + ], + "poses": [] + }, + { + "row": 2, + "col": 4, + "side": "front", + "coords": [ + 4.499614777619532, + -3.6568143703418405, + 0.006128126167414207 + ], + "poses": [] + }, + { + "row": 2, + "col": 5, + "side": "back", + "coords": [ + 5.358194832629194, + -3.6555884207351923, + 0.00220732555627522 + ], + "poses": [] + }, + { + "row": 2, + "col": 5, + "side": "front", + "coords": [ + 5.358194832629194, + -3.6555884207351923, + 0.00220732555627522 + ], + "poses": [] + }, + { + "row": 2, + "col": 6, + "side": "back", + "coords": [ + 6.186931240051006, + -3.660069561737178, + -0.004740651362042846 + ], + "poses": [] + }, + { + "row": 2, + "col": 6, + "side": "front", + "coords": [ + 6.186931240051006, + -3.660069561737178, + -0.004740651362042846 + ], + "poses": [] + }, + { + "row": 3, + "col": 4, + "side": "back", + "coords": [ + 4.437106056578727, + -5.675385129487837, + 3.113166940596223 + ], + "poses": [] + }, + { + "row": 3, + "col": 3, + "side": "back", + "coords": [ + 3.4577008440661285, + -5.703184532839961, + -3.109685273113602 + ], + "poses": [] + }, + { + "row": 3, + "col": 2, + "side": "back", + "coords": [ + 2.3341924779097645, + -5.623728684341702, + -3.095440697434253 + ], + "poses": [] + }, + { + "row": 3, + "col": 1, + "side": "back", + "coords": [ + 1.6126711501886855, + -5.64506932868408, + -3.0994397969930265 + ], + "poses": [] + }, + { + "row": 3, + "col": 0, + "side": "back", + "coords": [ + 0.6321940488248773, + -5.633649464598426, + 3.129000825841382 ], "poses": [] } ], "arm_initial_pose": [ - -90.333323, - -90.079952, - 0.160037, - -90.571318, - 0.093654, - 22.232898 + -89.999795, + -89.999946, + -0.000131, + -90.00001, + 4.1e-05, + 0.000116 ] } \ No newline at end of file diff --git a/agv_app/data/models_config.json b/agv_app/data/models_config.json index 2dc2ddc..eb4288e 100644 --- a/agv_app/data/models_config.json +++ b/agv_app/data/models_config.json @@ -9,12 +9,12 @@ "name": "front_1", "photo_type": "front", "arm_angles": [ - -93.586541, - -184.343305, - 50.583239, - -38.326674, - -85.153333, - 20.399989 + -81.796775, + -85.406752, + -5.803223, + -109.799747, + 91.66639, + 2.446712 ], "speed": 500, "description": "" @@ -24,12 +24,12 @@ "name": "back_1", "photo_type": "back", "arm_angles": [ - 15.860045, - -161.133416, - 137.999016, - -161.996719, - 168.000989, - 15.653445 + -81.796823, + -85.406717, + -5.803284, + -109.799953, + 91.666459, + 2.44676 ], "speed": 500, "description": "" diff --git a/agv_app/data/qr_config.json b/agv_app/data/qr_config.json index 6dcc00c..812c95c 100644 --- a/agv_app/data/qr_config.json +++ b/agv_app/data/qr_config.json @@ -3,12 +3,12 @@ "id": "qr_1779278140334", "name": "二维码1", "joint_angles": [ - -89.796645, - -2.013175, - -87.176721, - -82.49663, - -93.323403, - 20.399941 + 89.999979, + -0.000118, + -120.09949, + -30, + -105, + 0 ], "qr_value": "BG042110276", "model_id": "" diff --git a/agv_app/static/js/setting.js b/agv_app/static/js/setting.js index 4cbe574..9701826 100644 --- a/agv_app/static/js/setting.js +++ b/agv_app/static/js/setting.js @@ -381,8 +381,9 @@ createApp({ } catch (e) { alert('导航失败: ' + e.message) } }, async goToOrigin() { - if (!confirm('确认导航到原点 (0, 0, 0)?')) return + if (!confirm('确认先复原机械臂,再导航到原点 (0, 0, 0)?')) return try { + this.mapMsg = '正在复原机械臂并发送原点导航...' const res = await fetch(API + '/api/navigate/to', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -390,7 +391,7 @@ createApp({ }) const data = await res.json() if (data.ok) { - this.mapMsg = '✅ 已发送导航到原点' + this.mapMsg = '✅ ' + (data.message || '已发送导航到原点') } else { this.mapMsg = '❌ ' + (data.error || '导航失败') } diff --git a/agv_app/utils/mission_executor.py b/agv_app/utils/mission_executor.py index 120aa70..db18349 100644 --- a/agv_app/utils/mission_executor.py +++ b/agv_app/utils/mission_executor.py @@ -414,6 +414,14 @@ class MissionExecutorV3: model_name = self._lookup_model(qr_value) self._log(f" 🏷️ 机型: {model_name}") + if qr_value and opt_arm_init and has_arm_pose and opt_agv_move and not self._stop.is_set(): + self._log(" 🦾 扫码完成,恢复机械臂初始姿态") + try: + self.arm_client.set_angles(arm_initial_pose, speed=self.arm_speed) + self._wait_arm_ready(arm_initial_pose) + except Exception as e: + self._log(f" ⚠️ 机械臂复位失败: {e}") + if opt_front_photo and not self._stop.is_set(): model = self._find_model(models, model_name) if model: @@ -642,7 +650,7 @@ class MissionExecutorV3: self._log("↪️ 调用设置页同款接口返回原点") resp = requests.post( "http://127.0.0.1:5000/api/navigate/to", - json={"x": 0, "y": 0, "yaw": 0}, + json={"x": 0, "y": 0, "yaw": 0, "restore_arm": False}, timeout=8, ) data = resp.json() if resp.content else {} @@ -676,7 +684,7 @@ class MissionExecutorV3: return False def _scan_qr_with_poses(self, qr_configs: list, machine_row: int = 0) -> Optional[str]: """用二维码配置中的姿态依次尝试,逐一调整姿态+等2秒+扫码,全部失败才弹框 - + machine_row: 机器所在行,奇数行(1,3,5)时关节角度需取反 """ if not qr_configs: diff --git a/public-frontend/src/app/inspection/page.tsx b/public-frontend/src/app/inspection/page.tsx index f320c63..2aad651 100644 --- a/public-frontend/src/app/inspection/page.tsx +++ b/public-frontend/src/app/inspection/page.tsx @@ -39,6 +39,7 @@ import type { ActivityItem, CameraInfo, CustomsDeclaration, InspectionIssue, Ins const { Text } = Typography; const { TextArea } = Input; +const LOG_AUTO_SCROLL_THRESHOLD = 48; interface ProgressItem extends InspectionItem { currentInspected: number; @@ -78,7 +79,8 @@ function InspectionContent() { const [operationLoading, setOperationLoading] = useState(false); const [messageApi, contextHolder] = message.useMessage(); const { token } = theme.useToken(); - const logsEndRef = useRef(null); + const logsContainerRef = useRef(null); + const [isUserNearBottom, setIsUserNearBottom] = useState(true); const refreshRuntime = async () => { const [missionState, currentInspection, missionLogs, nextIssues] = await Promise.all([ @@ -231,8 +233,19 @@ function InspectionContent() { }, [setInspection]); useEffect(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [logs]); + const container = logsContainerRef.current; + if (container && isUserNearBottom) { + container.scrollTop = container.scrollHeight; + } + }, [logs, isUserNearBottom]); + + const handleLogsScroll = () => { + const container = logsContainerRef.current; + if (!container) return; + + const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight; + setIsUserNearBottom(distanceToBottom <= LOG_AUTO_SCROLL_THRESHOLD); + }; const calculateTotalProgress = () => { if (!progressData.length) return 0; @@ -524,7 +537,7 @@ function InspectionContent() { style={{ flex: 3, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} styles={{ body: { padding: 12, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, background: token.colorFillQuaternary } }} > -
+
{logs.length > 0 ? ( ({ @@ -540,7 +553,6 @@ function InspectionContent() { ) : ( )} -