// QR Scanner Module - Self-contained QR scanning page // Adapted from existing camera implementation // // 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 from library const { get_system_info, get_camera_rule, make_query, fetch_real_ip, get_tenant_id, is_emblem_qr_pattern } = require('./libemblemscanner.js'); // Import QR processing module const { load_qrtool, is_qrtool_ready, process_frame, make_hint_text } = 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, should_check_auto_torch: true, done_checking_auto_torch: false, camera_sensitivity: 1, frame_uploaded: 0, frame_upload_time_cost: 0, qrarc_class: 'sm', qrmarkers_class: 'hidden', frame_upload_interval_ms: 2000, return_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 debug_msgs: [], debug_image_data_url: '', debug_last_result: null, qrtool_ready: false, frame_processing_started: false, // Frame processing statistics frames_processed: 0, frames_skipped: 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, emblem_camera_url: null, // State machine: initializing -> loading -> scanning -> verifying -> result app_state: 'initializing', // 'initializing', 'loading', 'scanning_camera', 'scanning_webview', 'verifying', 'result' scan_mode: 'unknown', // 'camera', 'webview' no_web_view: false // Override web-view rule, force native camera }, onLoad(options) { console.log('QR Scanner module loaded', options); const no_web_view = options.no_web_view === '1' || options.no_web_view === 'true'; this.setData({ return_page: options.return_page || '', no_web_view: no_web_view }); this.image_data_urls = []; options = options || {}; const enable_debug = options.debug || options.scene == 'debug' || false; this.initializeSystem(enable_debug); this.initializeQRTool(); this.fetchRealIP(); this.fetchTenantID(); this.loadCameraRules(); }, fetchRealIP() { fetch_real_ip((err, ip) => { this.setData({ real_ip: ip }); }); }, fetchTenantID() { const tenant_id = get_tenant_id(); this.setData({ tenant_id }); }, initializeQRTool() { load_qrtool(); const checkReady = () => { if (is_qrtool_ready()) { this.setData({ qrtool_ready: true }); this.addDebugMessage('QRTool WASM loaded and ready'); this.transitionToState('loading'); this.startFrameProcessingMaybe(); } else { setTimeout(checkReady, 100); } }; checkReady(); }, initializeSystem(enable_debug) { const systemInfo = get_system_info(); const phone_model = systemInfo.model; this.setData({ enable_debug, phone_model, window_width: systemInfo.windowWidth, window_height: systemInfo.windowHeight }); console.log(`Phone model: ${phone_model}, Using native camera mode`); }, loadCameraRules() { get_camera_rule(null, (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.addDebugMessage(`Using web-view camera: ${emblem_camera_url}`); } else { this.addDebugMessage('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.addDebugMessage(`Camera rule: zoom=${rule.zoom}, web_view=${rule.web_view}${this.data.no_web_view ? ' (NO_WEB_VIEW)' : ''}`); if (should_use_webview) { this.startWebviewScanning(); } else { this.startCameraScanning(); } }); }, onCameraReady(e) { console.log('Camera ready', e); this.camera_context = wx.createCameraContext(); this.addDebugMessage('Camera initialized for WASM processing'); this.startFrameProcessingMaybe(); }, onCameraError(e) { console.error('Camera error', e); this.addDebugMessage(`Camera error: ${JSON.stringify(e.detail)}`); this.setData({ show_modal: 'verifyfailed', hint_text: '相机初始化失败' }); }, 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); }, /** * State Machine: Transition to new state */ transitionToState(newState, mode = null) { const oldState = this.data.app_state; this.addDebugMessage(`State: ${oldState} -> ${newState}${mode ? ` (${mode})` : ''}`); const stateData = { app_state: newState }; if (mode) stateData.scan_mode = mode; this.setData(stateData); }, /** * State: Loading -> Scanning (Camera) */ startCameraScanning() { this.transitionToState('scanning_camera', 'camera'); this.setData({ hint_text: '查找二维码', busy: false }); }, /** * Start frame processing if both QRTool and camera are ready */ startFrameProcessingMaybe() { // Already started, nothing to do if (this.data.frame_processing_started) { return; } // Check if both are ready if (!this.data.qrtool_ready || !this.camera_context) { const qrStatus = this.data.qrtool_ready ? 'ready' : 'not ready'; const cameraStatus = this.camera_context ? 'ready' : 'not ready'; this.addDebugMessage(`Cannot start frame processing - QRTool: ${qrStatus}, Camera: ${cameraStatus}`); return; } this.addDebugMessage('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 this.listener.start(); // Mark as started this.setData({ frame_processing_started: true }); }, /** * Camera frame callback - receives live camera frames */ onCameraFrame(frame) { // Only process frames when in camera scanning state and QRTool is ready if (this.data.app_state !== 'scanning_camera' || !this.data.qrtool_ready) { return; } // Throttle frame processing to avoid overwhelming the system const now = Date.now(); if (this.lastFrameTime && (now - this.lastFrameTime) < 200) { this.setData({ frames_skipped: this.data.frames_skipped + 1 }); return; } this.lastFrameTime = now; // Start timing frame processing const processStart = Date.now(); // Process frame data directly try { const result = process_frame(frame.width, frame.height, frame.data, 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); } } catch (error) { this.addDebugMessage(`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) { // Update debug info if available if (result.debug_data_url) { this.setData({ debug_image_data_url: result.debug_data_url }); } // Generate hint text const hint = make_hint_text(result); // Check if we have a valid QR code if (result.qrcode && result.valid_pattern && result.ok) { this.addDebugMessage(`QR detected: ${result.qrcode}`); this.onQRCodeDetected(result.qrcode); } else { // Update hint for user guidance this.setData({ hint_text: hint }); if (result.qrcode) { this.addDebugMessage(`QR found but not valid: ${result.qrcode} (${result.err})`); } } }, /** * Handle successful QR code detection */ onQRCodeDetected(qrCode) { // Start verification process this.startVerifying(); // Simulate verification delay, then go to result setTimeout(() => { this.goToResult(qrCode); }, 2000); }, /** * State: Loading -> Scanning (Web-view) */ startWebviewScanning() { this.transitionToState('scanning_webview', 'webview'); this.setData({ hint_text: '查找二维码', busy: false }); }, /** * State: Scanning -> Verifying (only for camera mode) */ startVerifying() { this.transitionToState('verifying'); this.setData({ hint_text: '识别成功', show_modal: 'verifying' }); }, /** * State: Any -> Result (jump to return page) */ goToResult(qrCode) { this.transitionToState('result'); if (this.data.return_page) { wx.navigateTo({ url: `${this.data.return_page}?qr_code=${encodeURIComponent(qrCode)}`, success: () => { this.addDebugMessage(`Navigated to: ${this.data.return_page}`); }, fail: (err) => { this.addDebugMessage(`Navigation failed: ${err.errMsg}`); this.restartScanning(); } }); } else { this.addDebugMessage('No return page specified'); this.restartScanning(); } }, /** * State: Any -> Loading (restart) */ restartScanning() { // 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, total_processing_time: 0, avg_processing_time_ms: 0, last_frame_time_ms: 0, frame_processing_started: false }); // 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(); } }, /** * Add debug message to debug overlay */ addDebugMessage(message) { if (!this.data.enable_debug) return; const timestamp = new Date().toLocaleTimeString(); const debugMsg = `${timestamp}: ${message}`; this.setData({ debug_msgs: [debugMsg, ...this.data.debug_msgs].slice(0, 5) // Keep first 5 messages (newest on top) }); }, /** * Log function for debugging */ log(...args) { console.log(...args); this.addDebugMessage(args.join(' ')); }, /** * Handle messages from web-view camera */ on_webview_message(e) { console.log('Web-view message received:', e); this.addDebugMessage(`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); } else if (messageData.error) { this.addDebugMessage(`Web-view error: ${messageData.error}`); this.setData({ hint_text: '识别失败' }); } } }, /** * Check if QR code matches Emblem pattern */ isEmblemQRPattern(qrCode) { return is_emblem_qr_pattern(qrCode); }, /** * Lifecycle - page hide */ onHide() { this.cleanupListener(); }, /** * Lifecycle - page unload */ onUnload() { this.cleanupListener(); }, /** * Clean up camera frame listener */ cleanupListener() { if (this.listener) { this.listener.stop(); this.listener = null; this.addDebugMessage('Camera frame listener stopped'); } } });