diff --git a/agv_app/app.py b/agv_app/app.py index f1a4afa..ab8ea28 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -1574,31 +1574,61 @@ def api_qr_read_angles(qr_id): @app.route("/api/qr/scan/", methods=["POST"]) def api_qr_config_scan(qr_id): - """获取机械臂摄像头图像,识别二维码并保存到指定配置项""" + """获取机械臂摄像头图像,识别二维码并保存到指定配置项(pyzbar 优先,OpenCV 兜底)""" import requests try: + jpg_bytes = None + # 多次尝试获取清晰帧 + for _ in range(3): + try: + r = requests.get(ARM_CAMERA_CONFIG.get("snapshot_url", ARM_CAMERA_CONFIG["url"]), timeout=8) + if r.status_code == 200 and r.content: + jpg_bytes = r.content + break + except: + pass + import time; time.sleep(0.3) + + if jpg_bytes is None: + return jsonify({"ok": False, "error": "无法连接机械臂摄像头"}), 400 + import cv2 import numpy as np - # 从机械臂摄像头 snapshot 端点拉取一帧 JPEG - r = requests.get(ARM_CAMERA_CONFIG.get("snapshot_url", ARM_CAMERA_CONFIG["url"]), timeout=8) - if r.status_code != 200 or not r.content: - return jsonify({"ok": False, "error": "无法连接机械臂摄像头"}), 400 - jpg_bytes = r.content - # 解码为 numpy 数组并检测二维码 nparr = np.frombuffer(jpg_bytes, np.uint8) frame = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if frame is None: return jsonify({"ok": False, "error": "图像解码失败"}), 400 - # 使用 OpenCV QRCodeDetector 检测 - detector = cv2.QRCodeDetector() - result, _, _ = detector.detectAndDecode(frame) - if result and len(result.strip()) > 0: - result = result.strip() + + result = None + + # 方法1: pyzbar(识别率更高) + try: + from PIL import Image + from pyzbar.pyzbar import decode as pyzbar_decode + pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + codes = pyzbar_decode(pil_img) + if codes and codes[0].data: + result = codes[0].data.decode("utf-8").strip() + logger.info(f"pyzbar 扫码成功: {result}") + except Exception as e: + logger.debug(f"pyzbar 扫码失败: {e}") + + # 方法2: OpenCV QRCodeDetector(兜底) + if not result: + try: + detector = cv2.QRCodeDetector() + val, _, _ = detector.detectAndDecode(frame) + if val and len(val.strip()) > 0: + result = val.strip() + logger.info(f"OpenCV 扫码成功: {result}") + except Exception as e: + logger.debug(f"OpenCV 扫码失败: {e}") + + if result: # 保存到配置项 for entry in gs.qr_config: if entry["id"] == qr_id: entry["qr_value"] = result - # 尝试匹配机型 matched_model = None for model in gs.models_config: prefix = model.get("serial_prefix", "") @@ -1615,10 +1645,10 @@ def api_qr_config_scan(qr_id): "model_name": matched_model["name"] if matched_model else "" }) return jsonify({"ok": False, "error": f"二维码 {qr_id} 不存在"}), 404 - else: - return jsonify({"ok": False, "error": "未检测到二维码"}) + + return jsonify({"ok": False, "error": "未检测到二维码,请调整机械臂姿态或手动输入"}) except Exception as ex: - logger.error(f"QR 扫描机械臂摄像头失败: {ex}") + logger.error(f"QR 扫描失败: {ex}") return jsonify({"ok": False, "error": f"扫描失败: {str(ex)}"}), 400 diff --git a/agv_app/static/js/setting.js b/agv_app/static/js/setting.js index d8e43e5..13c0e2a 100644 --- a/agv_app/static/js/setting.js +++ b/agv_app/static/js/setting.js @@ -57,7 +57,13 @@ createApp({ qrScanning: false, qrConfigs: [], qrScanningId: null, + showQrInputDialog: false, + qrInputId: null, + qrInputValue: '', armCameraUrl: API + '/api/camera/arm_preview', + armSnapshotUrl: '', + showArmSnapshot: false, + armSnapshotLoading: false, newQrName: '', armInitialPose: [0, 0, 0, 0, 0, 0], } @@ -1104,10 +1110,39 @@ createApp({ if (data.model_name) msg += ' 匹配机型: ' + data.model_name else msg += ' 未匹配到机型' alert(msg) - } else { alert(data.error || '扫描失败') } - } catch (e) { alert('扫描失败: ' + e.message) } + } else { + // 自动扫描失败,弹出手动输入框 + this.qrInputId = qrId + this.qrInputValue = '' + this.showQrInputDialog = true + } + } catch (e) { + this.qrInputId = qrId + this.qrInputValue = '' + this.showQrInputDialog = true + } this.qrScanningId = null }, + async submitManualQr() { + if (!this.qrInputId || !this.qrInputValue.trim()) return + try { + const res = await fetch(API + '/api/qr/configs/' + this.qrInputId, { + method: 'PUT', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ qr_value: this.qrInputValue.trim() }) + }) + const data = await res.json() + if (data.ok) { + await this.loadQrConfigs() + alert('✅ 二维码值已保存') + } else { + alert('保存失败') + } + } catch (e) { alert('保存失败: ' + e.message) } + this.showQrInputDialog = false + this.qrInputId = null + this.qrInputValue = '' + }, async applyQrAngles(qrId) { if (!this.armConnected) { alert('机械臂未连接'); return } const q = this.qrConfigs.find(x => x.id === qrId) @@ -1126,6 +1161,12 @@ createApp({ onArmPreviewError() { // 机械臂摄像头预览失败,静默处理 }, + async captureArmSnapshot() { + this.armSnapshotLoading = true + this.armSnapshotUrl = API + '/api/camera/arm_refresh?t=' + Date.now() + this.showArmSnapshot = true + setTimeout(() => { this.armSnapshotLoading = false }, 500) + }, async agvResetCollision() { if (!this.agvConnected) { alert('AGV 未连接') diff --git a/agv_app/templates/setting.html b/agv_app/templates/setting.html index f8af5f8..af079b2 100644 --- a/agv_app/templates/setting.html +++ b/agv_app/templates/setting.html @@ -433,11 +433,16 @@

📷 二维码配置

配置机械臂姿态(6个关节角度),通过机械臂摄像头识别二维码并匹配机型。

-
+
+
+ +
@@ -476,6 +481,7 @@ + @@ -582,9 +588,39 @@
-
+ + + + + + - +