// QR Scanner Module - Self-contained QR scanning page // Adapted from existing camera implementation // // DYNAMIC PATH RESOLUTION: // - The system now dynamically computes the correct WASM file and worker paths based on the current page location // - This allows the emblemscanner to work in both main pages directory and subpackages // - Paths are computed using getCurrentPages() API and stored in global data during page load // - WASM files and workers use these dynamic paths instead of hardcoded absolute paths // // SCANNING MODES: // 1. Web View Mode - Uses external camera-5.1 web interface in web-view component // - Fallback for devices that need special handling // - Configured via camera rules API (web_view: true) // - Redirects back with wx_redirect_to parameter // // 2. Plain WASM Mode - Direct WASM processing in main thread // - Uses qrtool.wx.js/qrtool.wx.wasm for QR detection // - Camera frame capture via takePhoto() API // - Suitable for most Android devices // // 3. Worker WASM Mode - WASM processing in worker thread (experimental) // - Uses efficient camera frame API for high-performance processing // - Enabled for iPhone devices (phone model contains "iphone") // - Reduces main thread blocking during QR processing // // RUNTIME CONFIGURATION: // - Camera rules fetched from API based on phone model substring matching // - Default behavior: Plain WASM mode for most devices // - iPhone detection: phone_model.toLowerCase().includes('iphone') → Worker WASM // - Special device rules: camera rules API can override with web_view: true // - no_web_view parameter can override web_view rules for testing // Import utility functions fromsimplify api const { get_system_info, get_camera_rule, make_query, fetch_real_ip, get_tenant_id, is_emblem_qr_pattern, get_current_package_path, get_wasm_file_path } = require('./libemblemscanner.js'); // Import upload functionality for verification (self-contained) const { upload_image_data_urls, upload_image_data_urls_with_metadata, check_auto_torch } = require('./upload.js'); // Import precheck utilities for image processing // Note: We now use our own data_url_from_frame from qrprocessor.js // Import QR processing module const { load_qrtool, is_qrtool_ready, process_frame, make_hint_text, data_url_from_frame } = require('./qrprocessor.js'); Page({ /** * Page initial data */ data: { hint_text: '查找二维码', enable_debug: false, camera_flash: 'off', phone_model: 'unknown', zoom: -1, max_zoom: 1, show_tip: false, show_modal: '', busy: true, camera_sensitivity: 1, qrarc_class: 'sm', qrmarkers_class: 'hidden', frame_upload_interval_ms: 2000, return_page: '/pages/test_result_page/test_result_page', // Page to navigate to after successful scan server_url: 'https://themblem.com', // Default server URL real_ip: '', // User's real IP address tenant_id: '', // Tenant identifier userinfo: null, // User information from API debug_image_data_url: '', debug_last_result: null, debug_current_frame_url: '', // Current frame being processed logs: [], // Unified logging messages for both backend reporting and debug overlay debug_msgs: [], // Recent logs formatted for debug overlay (computed from logs) qrtool_ready: false, // Frame processing statistics frames_processed: 0, frames_skipped: 0, ok_frames: 0, total_processing_time: 0, avg_processing_time_ms: 0, last_frame_time_ms: 0, rule_zoom: -1, camera_rule: null, use_web_view: false, use_worker: false, emblem_camera_url: null, // State machine: loading_rules -> loading_qrtool -> init_camera -> qr_detecting -> check_auto_torch -> final_scanning -> verifying -> result app_state: 'loading_rules', // 'loading_rules', 'loading_qrtool', 'init_camera', 'qr_detecting', 'webview_scanning', 'check_auto_torch', 'final_scanning', 'verifying', 'result' scan_mode: 'unknown', // 'camera', 'webview' no_web_view: false, // Override web-view rule, force native camera worker_processing: false, // Track if worker is currently processing a frame verifying_stage: 0 // Verification spinner animation stage }, onLoad(options) { console.log('QR Scanner module loaded', options); // Store page options for use in onShow this.pageOptions = options || {}; const no_web_view = options.no_web_view === '1' || options.no_web_view === 'true'; this.setData({ return_page: options.return_page || '/pages/test_result_page/test_result_page', no_web_view: no_web_view }); // Store current package path in global data for dynamic WASM loading const currentPackagePath = get_current_package_path(); getApp().globalData.currentPackagePath = currentPackagePath; getApp().globalData.wasmFilePath = get_wasm_file_path('qrtool.wx.wasm.br'); console.log('Current package path:', currentPackagePath); console.log('WASM file path:', getApp().globalData.wasmFilePath); // Log page load for backend reporting (like camera.js) this.log("emblemscanner page load"); this.log("options", JSON.stringify(options)); }, fetchRealIP() { fetch_real_ip((_err, ip) => { this.setData({ real_ip: ip }); }); }, fetchTenantID() { const tenant_id = get_tenant_id(); this.setData({ tenant_id }); }, fetchUserInfo() { // Use WeChat official API to get user info wx.getUserInfo({ success: (res) => { const userInfo = res.userInfo; this.setData({ userinfo: userInfo }); this.log('User info fetched via API:', userInfo.nickName || 'unknown'); }, fail: (err) => { this.log('Failed to fetch user info:', err.errMsg); // Fallback to empty userinfo if API fails this.setData({ userinfo: {} }); } }); }, initializeQRTool() { load_qrtool(); const checkReady = () => { if (is_qrtool_ready()) { this.setData({ qrtool_ready: true }); this.log('QRTool WASM loaded and ready'); // Step 4: QRTool loaded, now initialize camera this.startCameraInit(); } else { setTimeout(checkReady, 100); } }; checkReady(); }, initializeSystem(enable_debug) { const systemInfo = get_system_info(); const phone_model = systemInfo.model; const use_worker = phone_model.toLowerCase().includes("iphone"); this.setData({ enable_debug, phone_model, window_width: systemInfo.windowWidth, window_height: systemInfo.windowHeight, use_worker: use_worker }); console.log(`Phone model: ${phone_model}, Use worker: ${use_worker}`); // Log system info for backend reporting (like camera.js) this.log("phone model:", phone_model); this.log("window width", systemInfo.windowWidth, "height", systemInfo.windowHeight); this.log("use worker:", use_worker); }, /** * Set up worker for iPhone processing like camera.js */ setupWorker() { // Create worker with dynamic path based on current package location var packagePath = getApp().globalData.currentPackagePath || 'pages/emblemscanner'; var workerPath = `${packagePath}/worker/index.js`; console.log('Creating worker with path:', workerPath); var worker = wx.createWorker(workerPath, { useExperimentalWorker: true, }); worker.onMessage((msg) => { console.log('Worker message:', msg.type); if (msg.type === "result") { // Clear processing flag when we get a result this.setData({ worker_processing: false }); const result = msg.res; const processingTime = msg.processing_time; // Add WASM response to debug messages if (this.data.enable_debug) { this.log(`WASM response: ${JSON.stringify(result)}`); } // Update statistics const newFramesProcessed = this.data.frames_processed + 1; const newTotalTime = this.data.total_processing_time + processingTime; const newAvgTime = Math.round(newTotalTime / newFramesProcessed); this.setData({ frames_processed: newFramesProcessed, total_processing_time: newTotalTime, avg_processing_time_ms: newAvgTime, last_frame_time_ms: Math.round(processingTime) }); if (result) { // For worker, we need to trigger image collection when we find a good QR if (result.qrcode && is_emblem_qr_pattern(result.qrcode)) { this.log(`Worker QR detected: ${result.qrcode}`); // Check auto-torch on first valid QR detection (only when in QR detecting state) if (this.data.app_state === 'qr_detecting') { this.startCheckingAutoTorch(result.qrcode); // Trigger zoom-in if function is set up if (this.on_first_qr_found) { this.on_first_qr_found(); } return; // Exit early, auto-torch check will return to final_scanning } if (this.data.app_state === 'final_scanning' && result.ok) { // Request worker to submit image data const worker = this.get_worker(); if (worker) { worker.postMessage({ type: "ready_to_submit" }); } } } this.handleQRResult(result, this.lastWorkerFrame); } } else if (msg.type === "submit") { // Handle submit message from worker (image data for upload) const result = msg.res; const imageData = msg.image_data; if (imageData) { console.log(`worker submitted image data: ${imageData.data}`); const uca = new Uint8ClampedArray(imageData.data); console.log(`first 4 bytes of image data in message: ${uca[0]}, ${uca[1]}, ${uca[2]}, ${uca[3]}`); const dataUrl = data_url_from_frame(imageData.width, imageData.height, uca); this.updateDebugFrameUrls(dataUrl); this.image_data_urls.push(dataUrl); // Update ok frames counter this.setData({ ok_frames: this.image_data_urls.length }); if (this.image_data_urls.length >= 3) { this.log('3 good images collected via worker, starting verification'); this.startVerifying(); this.submitImageForVerification(this.image_data_urls, result.qrcode); this.image_data_urls = []; // Reset for next scan this.setData({ ok_frames: 0 }); // Reset counter } else { this.log(`Collected ${this.image_data_urls.length}/3 worker images`); } } } }); this.log('Worker set up for iPhone processing'); return worker; }, loadCameraRules(max_zoom = null) { get_camera_rule(max_zoom, (rule) => { console.log('Camera rule loaded:', rule); const should_use_webview = rule.web_view && !this.data.no_web_view; let emblem_camera_url = null; if (should_use_webview) { emblem_camera_url = "https://themblem.com/camera-5.1/?" + make_query(rule.zoom, this.data.return_page, this.data.real_ip, this.data.tenant_id); this.log(`Using web-view camera: ${emblem_camera_url}`); } else { this.log('Using native camera with local WASM processing'); } this.setData({ camera_rule: rule, zoom: rule.zoom, rule_zoom: rule.zoom, camera_sensitivity: rule.sensitivity || 1, use_web_view: should_use_webview, emblem_camera_url: emblem_camera_url }); this.log(`Camera rule: zoom=${rule.zoom}, web_view=${rule.web_view}${this.data.no_web_view ? ' (NO_WEB_VIEW)' : ''}`); if (should_use_webview) { // Step 3a: Go directly to webview scanning (no QRTool needed) this.startWebviewScanning(); } else { // Step 3b: Load QRTool for camera mode this.log('Starting QRTool initialization'); this.transitionToState('loading_qrtool'); this.initializeQRTool(); } }); }, onCameraReady(e) { console.log('Camera ready', e); // Get max zoom from camera initialization let max_zoom = null; if (e && e.detail && e.detail.maxZoom) { max_zoom = e.detail.maxZoom; this.setData({ max_zoom: max_zoom }); this.log(`Camera max zoom: ${max_zoom}`); } this.log("onCameraReady: create camera context"); this.camera_context = wx.createCameraContext(); this.log('Camera context created'); // Step 5: Set up initial zoom and start scanning if (this.data.camera_rule) { this.setupCameraZoom(this.data.camera_rule); this.log('Initial zoom set up'); } // Step 6: Transition to scanning (which also starts frame processing) this.startCameraScanning(); }, onCameraError(e) { console.error('Camera error', e); this.log(`Camera error: ${JSON.stringify(e.detail)}`); // Redirect with failure instead of showing modal this.goToResult(null, null, false); }, toggle_torch() { const newFlash = this.data.camera_flash === 'torch' ? 'off' : 'torch'; this.setData({ camera_flash: newFlash }); console.log('Torch toggled to:', newFlash); }, show_scanguide() { this.setData({ show_modal: 'scanguide', show_tip: false }); }, show_service() { this.setData({ show_modal: 'service' }); }, restart_camera() { this.restartScanning(); }, close_modal() { this.setData({ show_modal: '' }); }, debug_tap() { const count = (this.debug_tap_count || 0) + 1; this.debug_tap_count = count; if (count >= 5) { this.setData({ enable_debug: !this.data.enable_debug }); this.debug_tap_count = 0; } setTimeout(() => { this.debug_tap_count = 0; }, 3000); }, /** * Set up camera zoom functionality similar to camera.js */ setupCameraZoom(rule) { if (!this.camera_context) { this.log('Cannot setup zoom: camera context not ready'); return; } console.log('setupCameraZoom called:', rule); const zoom = rule.zoom; const initial_zoom = 2; this.setData({ zoom: initial_zoom, rule_zoom: zoom }); this.log(`Camera set initial zoom to ${initial_zoom}x, will zoom in to ${zoom}x when QR is found`); this.camera_context.setZoom({ zoom: initial_zoom }); // Set up zoom-in behavior when QR is found this.on_first_qr_found = () => { this.log(`First QR found, zoom to ${zoom}x`); this.camera_context.setZoom({ zoom: zoom }); this.setData({ zoom: zoom, qrmarkers_class: '', qrarc_class: 'lg' }); }; }, /** * State Machine: Transition to new state */ transitionToState(newState, mode = null) { const oldState = this.data.app_state; this.log(`State: ${oldState} -> ${newState}${mode ? ` (${mode})` : ''}`); const stateData = { app_state: newState }; if (mode) stateData.scan_mode = mode; this.setData(stateData); }, /** * State: Loading -> Init Camera (prepare camera context) */ startCameraInit() { this.transitionToState('init_camera', 'camera'); this.setData({ hint_text: '初始化相机...', busy: true }); }, /** * State: Init Camera -> QR Detecting (Camera) * Also starts frame processing since all conditions are implied to be ready */ startCameraScanning() { this.transitionToState('qr_detecting', 'camera'); this.setData({ hint_text: '查找二维码', busy: false }); // Start frame processing - all conditions are implied to be ready at this point this.log('Starting camera frame listener'); this.lastFrameTime = 0; // Set up camera frame listener this.listener = this.camera_context.onCameraFrame((frame) => { this.onCameraFrame(frame); }); // Start the listener with worker if using worker mode var worker = this.data.use_worker ? this.get_worker() : null; this.listener.start({ worker, }); }, /** * Camera frame callback - receives live camera frames */ onCameraFrame(frame) { // Only process frames when in QR detecting or final scanning states if (this.data.app_state !== 'qr_detecting' && this.data.app_state !== 'final_scanning') { return; } // Throttle frame processing to avoid overwhelming the system const now = Date.now(); if (this.lastFrameTime && (now - this.lastFrameTime) < 100) { this.setData({ frames_skipped: this.data.frames_skipped + 1 }); return; } this.lastFrameTime = now; // Use worker for iPhone, direct processing for other devices if (this.data.use_worker) { // Skip if worker is already processing a frame if (this.data.worker_processing) { this.setData({ frames_skipped: this.data.frames_skipped + 1 }); return; } // Worker processing (iPhone) this.lastWorkerFrame = frame; // Store for handleQRResult // Copy frame data to avoid TOCTOU race condition (like camera.js) var uca1 = new Uint8ClampedArray(frame.data); var uca = new Uint8ClampedArray(uca1); // Generate debug frame data URL if debug is enabled if (this.data.enable_debug) { const frameDataUrl = data_url_from_frame(frame.width, frame.height, uca); this.updateDebugFrameUrls(frameDataUrl); } // Set processing flag before sending message this.setData({ worker_processing: true }); const worker = this.get_worker(); if (worker) { worker.postMessage({ type: 'frame', width: frame.width, height: frame.height, camera_sensitivity: this.data.camera_sensitivity }); } } else { // Direct processing (other devices) this.processFrameDirect(frame); } }, /** * Process frame directly (non-iPhone devices) */ processFrameDirect(frame) { const processStart = Date.now(); try { // Copy frame data to avoid TOCTOU race condition (like camera.js) var uca1 = new Uint8ClampedArray(frame.data); var uca = new Uint8ClampedArray(uca1); // Generate debug frame data URL if debug is enabled if (this.data.enable_debug) { const frameDataUrl = data_url_from_frame(frame.width, frame.height, uca); this.updateDebugFrameUrls(frameDataUrl); } const result = process_frame(frame.width, frame.height, uca, this.data.camera_sensitivity, this.data.enable_debug); // Calculate processing time const processEnd = Date.now(); const processingTime = processEnd - processStart; // Update statistics const newFramesProcessed = this.data.frames_processed + 1; const newTotalTime = this.data.total_processing_time + processingTime; const newAvgTime = Math.round(newTotalTime / newFramesProcessed); this.setData({ frames_processed: newFramesProcessed, total_processing_time: newTotalTime, avg_processing_time_ms: newAvgTime, last_frame_time_ms: Math.round(processingTime), debug_last_result: result }); if (result) { this.handleQRResult(result, frame, uca); } } catch (error) { this.log(`Frame processing error: ${error.message}`); // Still count failed processing attempts const processEnd = Date.now(); const processingTime = processEnd - processStart; const newFramesProcessed = this.data.frames_processed + 1; const newTotalTime = this.data.total_processing_time + processingTime; const newAvgTime = Math.round(newTotalTime / newFramesProcessed); this.setData({ frames_processed: newFramesProcessed, total_processing_time: newTotalTime, avg_processing_time_ms: newAvgTime, last_frame_time_ms: Math.round(processingTime), debug_last_result: null }); } }, /** * Handle QR processing result */ handleQRResult(result, frame, copiedFrameData = null) { // Update debug info - prioritize WASM debug image, fallback to original frame if (this.data.enable_debug) { if (result.debug_data_url) { // Priority 1: WASM returned a processed debug image (with QR detection overlay, etc.) this.setData({ debug_image_data_url: result.debug_data_url }); } else if (this.data.debug_current_frame_url) { // Priority 2: No WASM debug image available, show the raw camera frame this.setData({ debug_image_data_url: this.data.debug_current_frame_url }); } // If neither exists, the debug image box will remain empty/previous image } // Generate hint text const hint = make_hint_text(result); // Check if we have a valid QR code that's ready for upload (like camera.js) // don't require ok as we only care about the view has a valid qrcode in it // zooming in so that it's more likely to be clear enough for upload if (result.qrcode && is_emblem_qr_pattern(result.qrcode)) { this.log(`QR detected: ${result.qrcode} ok: ${result.ok}: err ${result.err}`); // Check auto-torch on first valid QR detection (only when in QR detecting state) if (this.data.app_state === 'qr_detecting') { this.startCheckingAutoTorch(result.qrcode); // Trigger zoom-in if function is set up if (this.on_first_qr_found) { this.on_first_qr_found(); } return; // Exit early, auto-torch check will return to final_scanning state } if (this.data.app_state === 'final_scanning' && result.ok) { this.onQRCodeDetected(result.qrcode, frame, copiedFrameData); // Pass the copied frame data } } else { // Update hint for user guidance this.setData({ hint_text: hint }); if (result.qrcode) { this.log(`QR found but not valid: ${result.qrcode} (${result.err})`); } } }, /** * Handle successful QR code detection - collect images from "ok" frames and verify (non-worker case) */ onQRCodeDetected(qrCode, frameData, copiedFrameData = null) { // Only collect images for non-worker case (worker handles its own image collection) if (!this.data.use_worker && frameData && copiedFrameData) { // Convert frame data to data URL for upload (only called for "ok" frames) // Use the copied frame data instead of original frameData.data to avoid corruption const dataUrl = data_url_from_frame(frameData.width, frameData.height, copiedFrameData); this.image_data_urls.push(dataUrl); // Update ok frames counter this.setData({ ok_frames: this.image_data_urls.length }); this.log(`Collected ${this.image_data_urls.length}/3 good images (direct) - using copied data`); // Add debug info about the submitted image if (this.data.enable_debug) { this.log(`Submitted image preview: ${dataUrl.substring(0, 50)}...`); } // Need 3 "ok" frames before verification (like camera.js) if (this.image_data_urls.length >= 3) { this.log('3 good images collected, starting verification'); this.startVerifying(); this.submitImageForVerification(this.image_data_urls, qrCode); this.image_data_urls = []; // Reset for next scan this.setData({ ok_frames: 0 }); // Reset counter } } // For worker case, image collection is handled in setupWorker() message handler // If less than 3 images, continue scanning to collect more }, /** * Submit images for verification like camera.js */ submitImageForVerification(dataUrls, qrCode) { this.log('Submitting images for verification'); const begin = Date.now(); const success = (res) => { this.log(`Upload success, code: ${res.statusCode}`); if (res.statusCode === 200) { let resp; if (typeof res.data === "string") { resp = JSON.parse(res.data); } else { resp = res.data; } // Store verification response getApp().globalData.verify_resp = resp; if (resp.serial_code) { // Let verification animation run for a bit, then redirect with success const delay = 3000 - (Date.now() - begin); setTimeout(() => { this.goToResult(qrCode, resp.serial_code, true); }, delay > 0 ? delay : 0); } else { // Redirect with failure instead of showing modal const delay = 3000 - (Date.now() - begin); setTimeout(() => { this.goToResult(qrCode, null, false); }, delay > 0 ? delay : 0); } } else { // Redirect with failure instead of showing modal this.goToResult(qrCode, null, false); } }; const fail = (e) => { this.log(`Upload failed: ${JSON.stringify(e)}`); // Redirect with failure instead of showing modal this.goToResult(qrCode, null, false); }; // Store global data like camera.js const gd = getApp().globalData; gd.image_data_urls = dataUrls; gd.qr_code = qrCode; // Use enhanced upload function with local metadata instead of global data const metadata = { real_ip: this.data.real_ip, phone_model: this.data.phone_model, server_url: this.data.server_url, userinfo: this.data.userinfo }; upload_image_data_urls_with_metadata(dataUrls, qrCode, metadata, success, fail, this.data.logs.join("\n")); }, /** * State: Loading Rules -> Webview Scanning */ startWebviewScanning() { this.transitionToState('webview_scanning', 'webview'); this.setData({ hint_text: '查找二维码', busy: false }); }, /** * State: QR Detecting -> Check Auto-torch */ startCheckingAutoTorch(qrcode) { this.transitionToState('check_auto_torch'); this.setData({ hint_text: '检查补光设置...' }); check_auto_torch(qrcode, (auto_torch, camera_sensitivity) => { this.log(`Auto-torch check: ${auto_torch}, sensitivity: ${camera_sensitivity}`); if (auto_torch) { this.setData({ camera_flash: 'torch' }); this.log('Auto-torch enabled'); } // Update camera sensitivity and continue scanning this.setData({ camera_sensitivity: camera_sensitivity || this.data.camera_sensitivity }); // Transition to final_scanning state (ready to collect frames for verification) this.transitionToState('final_scanning'); this.setData({ hint_text: '查找二维码' }); }); }, /** * State: Final Scanning -> Verifying (show spinner while waiting for backend) */ startVerifying() { this.transitionToState('verifying'); this.setData({ hint_text: '识别成功', show_modal: 'verifying', verifying_stage: 0 }); // Start verification animation like verifyspin component setTimeout(() => { this.setData({ verifying_stage: 1 }); }, 3000); }, /** * State: Any -> Result (jump to return page) */ goToResult(qrCode, serialCode, success = true) { this.transitionToState('result'); // Always redirect to return_page, pass ok=1 for success, ok=0 for failure // Only pass qr_code and serial_code if ok=1 (success) let url; if (success && qrCode) { if (serialCode) { url = `${this.data.return_page}?ok=1&qr_code=${encodeURIComponent(qrCode)}&serial_code=${encodeURIComponent(serialCode)}`; } else { url = `${this.data.return_page}?ok=1&qr_code=${encodeURIComponent(qrCode)}`; } } else { url = `${this.data.return_page}?ok=0`; } wx.redirectTo({ url: url, success: () => { this.log(`Navigated to: ${this.data.return_page}`); }, fail: (err) => { this.log(`Navigation failed: ${err.errMsg}`); this.restart_camera(); } }); }, /** * State: Any -> Loading (restart) */ restart_camera() { // Go to initializing if QRTool isn't ready, otherwise loading const newState = this.data.qrtool_ready ? 'loading' : 'initializing'; this.transitionToState(newState); const hintText = this.data.qrtool_ready ? '初始化相机...' : '初始化QR工具...'; this.setData({ show_modal: '', hint_text: hintText, busy: true, // Reset frame processing statistics frames_processed: 0, frames_skipped: 0, ok_frames: 0, total_processing_time: 0, avg_processing_time_ms: 0, last_frame_time_ms: 0 }); // Reset frame timing this.lastFrameTime = 0; // Reload camera rules to restart the flow (only if QRTool is ready) if (this.data.qrtool_ready) { this.loadCameraRules(); } }, /** * Unified logging function for both backend reporting and debug overlay */ log(...what) { const message = what.join(" "); // Always log to console console.log(...what); // Always add to backend logs for reporting const newLogs = this.data.logs.concat([new Date() + ": " + message]); // Update debug overlay with recent logs if debug is enabled let debugMsgs = []; if (this.data.enable_debug) { // Show only the 5 most recent logs, formatted with short timestamp debugMsgs = newLogs.slice(-5).reverse().map(log => { const timestamp = new Date().toLocaleTimeString(); const logMessage = log.substring(log.indexOf(": ") + 2); // Remove full date, keep message return `${timestamp}: ${logMessage}`; }); } this.setData({ logs: newLogs, debug_msgs: debugMsgs }); }, /** * Update debug frame URLs for visualization */ updateDebugFrameUrls(frameDataUrl) { if (!this.data.enable_debug) return; // Store current frame URL for potential use as fallback this.setData({ debug_current_frame_url: frameDataUrl }); }, /** * Handle messages from web-view camera */ on_webview_message(e) { console.log('Web-view message received:', e); this.log(`Web-view message: ${JSON.stringify(e.detail)}`); // Handle QR code results from web-view if (e.detail && e.detail.data && e.detail.data.length > 0) { const messageData = e.detail.data[0]; if (messageData.qr_code) { // Web-view results go directly to result (no verification step) this.goToResult(messageData.qr_code, messageData.serial_code, true); } else if (messageData.error) { this.log(`Web-view error: ${messageData.error}`); // Redirect with failure instead of showing hint this.goToResult(null, null, false); } } }, /** * Check if QR code matches Emblem pattern */ isEmblemQRPattern(qrCode) { return is_emblem_qr_pattern(qrCode); }, /** * Lifecycle - page show */ onShow() { console.log('EmblemScanner page shown - initializing'); // Log page show for backend reporting (like camera.js) this.log("emblemscanner page show"); // Reset state machine to initial state this.setData({ app_state: 'loading_rules', scan_mode: 'unknown', busy: true, hint_text: '查找二维码', show_modal: '', worker_processing: false, verifying_stage: 0, // Reset frame processing statistics frames_processed: 0, frames_skipped: 0, ok_frames: 0, total_processing_time: 0, avg_processing_time_ms: 0, last_frame_time_ms: 0, // Clear logs and debug messages logs: [], debug_msgs: [], // Clear userinfo userinfo: null }); // Initialize image collection for verification this.image_data_urls = []; const enable_debug = this.pageOptions.debug || this.pageOptions.scene == 'debug' || false; // Step 1: Initialize system and get phone model this.initializeSystem(enable_debug); this.fetchRealIP(); this.fetchTenantID(); this.fetchUserInfo(); // Step 2: Load camera rules based on phone model this.loadCameraRules(); }, /** * Lifecycle - page hide */ onHide() { console.log('EmblemScanner page hidden - cleaning up'); this.cleanupListener(); // Reset worker processing state to prevent stale state this.setData({ worker_processing: false, app_state: 'loading_rules', busy: true }); // Clear frame timing and image collection this.lastFrameTime = 0; this.image_data_urls = []; // Clear logs to prevent memory buildup this.setData({ logs: [], debug_msgs: [] }); // Clear zoom function reference this.on_first_qr_found = null; }, /** * Lifecycle - page unload */ onUnload() { this.cleanupListener(); this.terminateWorker(); }, /** * Clean up camera frame listener and camera context */ cleanupListener() { if (this.listener) { this.log("cleanupListener: stop camera frame listener"); this.listener.stop(); this.listener = null; this.log('Camera frame listener stopped'); } // Clear camera context to prevent multiple camera elements if (this.camera_context) { this.camera_context = null; this.log('Camera context cleared'); } // Clean up worker state this.cleanupWorkerState(); }, get_worker() { if (!this.worker && this.data.use_worker) { this.worker = this.setupWorker(); } return this.worker; }, /** * Clean up worker state without terminating the worker */ cleanupWorkerState() { if (this.data.use_worker) { // Reset worker processing flag this.setData({ worker_processing: false }); // Clear stored worker frame reference this.lastWorkerFrame = null; this.log('Worker state cleaned up'); } }, /** * Terminate and clean up worker on page unload */ terminateWorker() { if (this.worker) { this.worker.terminate(); this.worker = null; this.log('Worker terminated'); } } });