diff --git a/agv_app/app.py b/agv_app/app.py index c834760..6a05065 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -16,7 +16,7 @@ from utils.arm_client import ArmClient from utils.agv_controller_ros2 import AGVController from utils.qr_scanner import QRScanner from utils.image_uploader import ImageUploader -from utils.mission_executor import MissionExecutor, TaskStatus +from utils.mission_executor import MissionExecutorV3 from utils.nav2_navigator import Nav2Navigator, Nav2Status # 配置日志 @@ -1241,41 +1241,39 @@ def api_agv_reset(): # ========== 任务执行 API ========== @app.route("/api/mission/start", methods=["POST"]) def api_mission_start(): - """开始执行任务""" + """开始执行任务(V3: M×N Grid 蛇形路径)""" if gs.state == State.RUNNING: return jsonify({"ok": False, "error": "任务已在运行中"}), 400 - data = request.json or {} - mission_data = { - "map": gs.map_config, - "points": gs.points_config, - } - def run(): - from config import AGV_CONFIG, UPLOAD_CONFIG - executor_config = { + from config import AGV_CONFIG + config = { "device": AGV_CONFIG.get("device", "/dev/agvpro_controller"), "baudrate": AGV_CONFIG.get("baudrate", 1000000), "arm": ARM_CONFIG, - "upload_url": UPLOAD_CONFIG["url"], - "upload_timeout": UPLOAD_CONFIG["timeout"], - "upload_retries": UPLOAD_CONFIG["max_retries"], - "camera_index": 0, } - executor = MissionExecutor(executor_config) + executor = MissionExecutorV3(config) - # 连接 - conn_results = executor.connect_all() - if not conn_results.get("arm") or not conn_results.get("camera"): - gs.mission_report = {"error": "连接失败", "details": conn_results} + conn = executor.connect_all() + if not conn.get("agv") or not conn.get("arm"): + gs.mission_report = {"error": "连接失败", "details": conn} gs.state = State.IDLE return gs.state = State.RUNNING - report = executor.execute_mission(mission_data) + + machines_list = gs.machines_config if isinstance(gs.machines_config, list) else gs.machines_config.get("machines", []) + models_list = gs.models_config if isinstance(gs.models_config, list) else gs.models_config.get("models", []) + + report = executor.execute_mission( + mission_config=gs.mission_config, + machines=machines_list, + qr_configs=gs.qr_config, + models=models_list, + ) gs.mission_report = report executor.disconnect_all() - gs.state = State.IDLE if report["failed"] == 0 else State.PAUSED + gs.state = State.IDLE if report.get("error") is None else State.PAUSED thread = threading.Thread(target=run, daemon=True) thread.start() @@ -1284,23 +1282,62 @@ def api_mission_start(): @app.route("/api/mission/stop", methods=["POST"]) def api_mission_stop(): """停止任务""" - if hasattr(MissionExecutor, "_instance"): - MissionExecutor._instance.stop() + if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + MissionExecutorV3._instance.stop() gs.state = State.IDLE return jsonify({"ok": True}) @app.route("/api/mission/pause", methods=["POST"]) def api_mission_pause(): + if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + MissionExecutorV3._instance.pause() gs.state = State.PAUSED return jsonify({"ok": True}) +@app.route("/api/mission/resume", methods=["POST"]) +def api_mission_resume(): + if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + MissionExecutorV3._instance.resume() + gs.state = State.RUNNING + return jsonify({"ok": True}) + @app.route("/api/mission/report", methods=["GET"]) def api_mission_report(): return jsonify({"report": gs.mission_report}) @app.route("/api/mission/state", methods=["GET"]) def api_mission_state(): - return jsonify({"state": gs.state}) + """返回任务状态 + 执行器详情""" + result = {"state": gs.state} + if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + ex = MissionExecutorV3._instance + result.update(ex.get_status()) + else: + # 空闲时预生成任务列表(基于网格和机器配置) + mc = gs.mission_config + if mc: + result["tasks"] = MissionExecutorV3.pre_generate_tasks(mc) + return jsonify(result) + +@app.route("/api/mission/log", methods=["GET"]) +def api_mission_log(): + """返回实时日志""" + if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + ex = MissionExecutorV3._instance + return jsonify(ex.get_logs()) + return jsonify(gs.mission_report or {"log": []}) + +@app.route("/api/mission/manual-qr", methods=["POST"]) +def api_mission_manual_qr(): + """手动输入二维码值""" + data = request.json or {} + qr = data.get("qr", "").strip() + if not qr: + return jsonify({"ok": False, "error": "二维码不能为空"}), 400 + if hasattr(MissionExecutorV3, "_instance") and MissionExecutorV3._instance: + MissionExecutorV3._instance.set_manual_qr(qr) + return jsonify({"ok": True}) + return jsonify({"ok": False, "error": "没有运行中的任务"}), 400 # ========== 二维码配置 API ========== diff --git a/agv_app/static/css/style.css b/agv_app/static/css/style.css index 8126dc0..ad3eda8 100644 --- a/agv_app/static/css/style.css +++ b/agv_app/static/css/style.css @@ -774,3 +774,141 @@ a:hover { text-decoration: underline; } color: #666; font-size: 12px; } + +/* ========== 实时日志 ========== */ +.log-box { + background: #0a0a0a; + color: #00ff88; + font-family: 'Courier New', 'Menlo', monospace; + font-size: 13px; + line-height: 1.6; + max-height: 320px; + overflow-y: auto; + padding: 12px 16px; + border-radius: 6px; + margin-top: 8px; + border: 1px solid #1a1a1a; +} +.log-line { + padding: 2px 0; + border-bottom: 1px solid #111; + word-break: break-all; +} +.log-empty { + color: #555; + font-style: italic; + padding: 12px 0; +} + +/* ========== 弹窗 ========== */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.modal { + background: #1a1a2e; + padding: 28px 32px; + border-radius: 12px; + min-width: 400px; + max-width: 90%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); +} +.modal h3 { + margin: 0 0 12px 0; + color: #e0e0e0; +} +.modal p { + color: #aaa; + margin: 0 0 16px 0; +} +.modal input[type="text"] { + width: 100%; + padding: 10px 12px; + background: #0a0a0a; + border: 1px solid #333; + border-radius: 6px; + color: #e0e0e0; + font-size: 15px; + outline: none; + box-sizing: border-box; +} +.modal input[type="text"]:focus { + border-color: #409eff; +} +.modal-actions { + display: flex; + gap: 12px; + margin-top: 20px; +} +.modal-actions .btn { + flex: 1; +} + +/* ========== 任务清单 ========== */ +.task-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 10px; + margin-top: 10px; +} +.task-cell { + background: #0a0a0a; + border: 1px solid #1a1a1a; + border-radius: 8px; + padding: 12px; + text-align: center; + transition: all 0.3s; +} +.task-cell.task-active { + border-color: #409eff; + background: #0d1b2a; +} +.task-cell.task-completed { + border-color: #4caf50; + opacity: 0.7; +} +.task-cell.task-active .task-step-text { + color: #409eff; + font-weight: bold; +} +.task-pos { + font-size: 16px; + font-weight: bold; + color: #e0e0e0; + margin-bottom: 6px; +} +.task-status-icon { + font-size: 20px; + margin-bottom: 4px; +} +.task-step-text { + font-size: 12px; + color: #888; + margin-bottom: 4px; +} +.task-info { + font-size: 11px; + color: #666; +} +.task-qr { + font-family: monospace; + color: #aaa; +} +.task-photos { + color: #888; +} +.pulse-icon { + animation: taskPulse 1s infinite; +} +@keyframes taskPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} diff --git a/agv_app/static/js/running.js b/agv_app/static/js/running.js index bcb3e9a..aa01478 100644 --- a/agv_app/static/js/running.js +++ b/agv_app/static/js/running.js @@ -7,22 +7,27 @@ createApp({ data() { return { missionState: 'idle', - currentPoint: 0, - totalPoints: 0, + progress: 0, + tasks: [], report: null, previewUrl: API + '/api/camera/preview', - polling: null + polling: null, + logs: [], + showQrModal: false, + qrValue: '', } }, computed: { missionStateText() { - const map = { idle: '空闲', running: '任务运行中', paused: '已暂停', completed: '已完成' } + const map = { + idle: '空闲', + running: '任务运行中', + paused: '已暂停', + completed: '已完成', + waiting_qr: '等待输入二维码' + } return map[this.missionState] || '未知' }, - progressPercent() { - if (!this.totalPoints) return 0 - return Math.round((this.currentPoint / this.totalPoints) * 100) - } }, mounted() { this.poll() @@ -33,35 +38,55 @@ createApp({ methods: { poll() { this.refresh() - this.polling = setInterval(this.refresh, 2000) + this.pollLogs() + this.polling = setInterval(() => { + this.refresh() + this.pollLogs() + }, 2000) }, async refresh() { try { const res = await fetch(API + '/api/mission/state') const data = await res.json() - this.missionState = data.state || 'idle' + this.missionState = data.status || 'idle' + this.progress = data.progress || 0 + if (data.tasks) this.tasks = data.tasks - if (this.missionState === 'running') { - const reportRes = await fetch(API + '/api/mission/report') - const reportData = await reportRes.json() - if (reportData.report) { - this.totalPoints = reportData.report.total_points || 0 - this.currentPoint = reportData.report.details?.length || 0 - this.report = reportData.report - } - } else if (this.missionState === 'idle') { - const reportRes = await fetch(API + '/api/mission/report') - const reportData = await reportRes.json() - if (reportData.report) { - this.report = reportData.report - this.totalPoints = reportData.report.total_points || 0 - this.currentPoint = reportData.report.details?.length || 0 - } + // QR 弹窗 + if (this.missionState === 'waiting_qr' && !this.showQrModal) { + this.showQrModal = true + this.qrValue = '' } + + // 完成后获取报告 + if (this.missionState === 'idle' && this.tasks.length > 0) { + const reportRes = await fetch(API + '/api/mission/report') + const reportData = await reportRes.json() + this.report = reportData.report + } + } catch (e) {} + }, + async pollLogs() { + if (this.missionState !== 'running' && this.missionState !== 'waiting_qr') return + try { + const res = await fetch(API + '/api/mission/log') + const data = await res.json() + if (data.log) this.logs = data.log + if (data.progress != null) this.progress = data.progress + if (data.tasks) this.tasks = data.tasks + // 自动滚到底 + this.$nextTick(() => { + const box = this.$refs.logBox + if (box) box.scrollTop = box.scrollHeight + }) } catch (e) {} }, async startMission() { if (this.missionState !== 'idle') return + this.logs = [] + this.progress = 0 + this.report = null + this.showQrModal = false await fetch(API + '/api/mission/start', { method: 'POST' }) this.missionState = 'running' }, @@ -69,12 +94,37 @@ createApp({ await fetch(API + '/api/mission/pause', { method: 'POST' }) this.missionState = 'paused' }, + async resumeMission() { + await fetch(API + '/api/mission/resume', { method: 'POST' }) + this.missionState = 'running' + this.showQrModal = false + }, async stopMission() { await fetch(API + '/api/mission/stop', { method: 'POST' }) this.missionState = 'idle' + this.showQrModal = false + }, + async submitQr() { + const val = this.qrValue.trim() + await fetch(API + '/api/mission/manual-qr', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ qr: val || ' ' }) + }) + this.showQrModal = false + this.qrValue = '' + }, + cancelQr() { + this.showQrModal = false + this.qrValue = '' + fetch(API + '/api/mission/manual-qr', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ qr: 'SKIP' }) + }) }, onPreviewError(e) { e.target.style.display = 'none' } } -}).mount('#app') +}).mount('#app') \ No newline at end of file diff --git a/agv_app/templates/running.html b/agv_app/templates/running.html index 9294256..5536159 100644 --- a/agv_app/templates/running.html +++ b/agv_app/templates/running.html @@ -25,10 +25,10 @@ [[ missionStateText ]] -