From 7c7c94fa7b54fe720120a8dc3f86934c8408f5b5 Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Sat, 13 Sep 2025 19:52:21 +0100 Subject: [PATCH] emblemscanner: native wasm works --- scanner/pages/emblemscanner/emblemscanner.js | 137 +++++++----------- .../pages/emblemscanner/emblemscanner.wxml | 83 ++++++++--- .../pages/emblemscanner/emblemscanner.wxss | 33 ++++- scanner/pages/emblemscanner/qrprocessor.js | 114 +++++++-------- scanner/pages/emblemscanner/qrtool.wx.js | 2 +- 5 files changed, 197 insertions(+), 172 deletions(-) diff --git a/scanner/pages/emblemscanner/emblemscanner.js b/scanner/pages/emblemscanner/emblemscanner.js index fcdce13..32944d0 100644 --- a/scanner/pages/emblemscanner/emblemscanner.js +++ b/scanner/pages/emblemscanner/emblemscanner.js @@ -27,7 +27,6 @@ // Import utility functions from library const { get_system_info, - get_phone_model, get_camera_rule, make_query, fetch_real_ip, @@ -40,7 +39,6 @@ const { load_qrtool, is_qrtool_ready, process_frame, - process_frame_with_debug, make_hint_text } = require('./qrprocessor.js'); @@ -72,30 +70,28 @@ Page({ 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: 0, - last_frame_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: loading -> scanning -> verifying -> result - app_state: 'loading', // 'loading', 'scanning_camera', 'scanning_webview', 'verifying', 'result' + // 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 }, - /** - * Lifecycle function--Called when page load - */ onLoad(options) { console.log('QR Scanner module loaded', options); - // Store query parameters const no_web_view = options.no_web_view === '1' || options.no_web_view === 'true'; this.setData({ @@ -103,54 +99,38 @@ Page({ no_web_view: no_web_view }); - // Initialize image data storage this.image_data_urls = []; - // Handle debug mode locally options = options || {}; const enable_debug = options.debug || options.scene == 'debug' || false; - // Get system information this.initializeSystem(enable_debug); - - // Initialize QRTool WASM this.initializeQRTool(); - - // Fetch IP address and tenant info, then load camera rules this.fetchRealIP(); this.fetchTenantID(); this.loadCameraRules(); }, - /** - * Fetch real IP address - */ fetchRealIP() { fetch_real_ip((err, ip) => { this.setData({ real_ip: ip }); }); }, - /** - * Fetch tenant ID (can be customized based on app logic) - */ fetchTenantID() { const tenant_id = get_tenant_id(); this.setData({ tenant_id }); }, - /** - * Initialize QRTool WASM module - */ initializeQRTool() { load_qrtool(); - // Check readiness periodically const checkReady = () => { if (is_qrtool_ready()) { this.setData({ qrtool_ready: true }); this.addDebugMessage('QRTool WASM loaded and ready'); - this.startFrameProcessing(); + this.transitionToState('loading'); + this.startFrameProcessingMaybe(); } else { setTimeout(checkReady, 100); } @@ -158,9 +138,6 @@ Page({ checkReady(); }, - /** - * Initialize system information and device detection - */ initializeSystem(enable_debug) { const systemInfo = get_system_info(); const phone_model = systemInfo.model; @@ -175,18 +152,13 @@ Page({ console.log(`Phone model: ${phone_model}, Using native camera mode`); }, - /** - * Load camera rules from API - */ loadCameraRules() { get_camera_rule(null, (rule) => { console.log('Camera rule loaded:', rule); - // Check for no_web_view override const should_use_webview = rule.web_view && !this.data.no_web_view; let emblem_camera_url = null; - // Set up web-view URL if needed 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}`); @@ -203,10 +175,8 @@ Page({ emblem_camera_url: emblem_camera_url }); - // Add rule info to debug messages this.addDebugMessage(`Camera rule: zoom=${rule.zoom}, web_view=${rule.web_view}${this.data.no_web_view ? ' (NO_WEB_VIEW)' : ''}`); - // Transition to appropriate scanning state if (should_use_webview) { this.startWebviewScanning(); } else { @@ -215,19 +185,13 @@ Page({ }); }, - /** - * Camera ready callback - */ onCameraReady(e) { console.log('Camera ready', e); this.camera_context = wx.createCameraContext(); this.addDebugMessage('Camera initialized for WASM processing'); - // State transition is handled in loadCameraRules + this.startFrameProcessingMaybe(); }, - /** - * Camera error callback - */ onCameraError(e) { console.error('Camera error', e); this.addDebugMessage(`Camera error: ${JSON.stringify(e.detail)}`); @@ -237,11 +201,6 @@ Page({ }); }, - - - /** - * Toggle torch/flash - */ toggle_torch() { const newFlash = this.data.camera_flash === 'torch' ? 'off' : 'torch'; this.setData({ @@ -250,9 +209,6 @@ Page({ console.log('Torch toggled to:', newFlash); }, - /** - * Show scan guide - */ show_scanguide() { this.setData({ show_modal: 'scanguide', @@ -260,34 +216,22 @@ Page({ }); }, - /** - * Show service modal - */ show_service() { this.setData({ show_modal: 'service' }); }, - /** - * Close modal and restart camera (legacy method name for WXML compatibility) - */ restart_camera() { this.restartScanning(); }, - /** - * Close any modal - */ close_modal() { this.setData({ show_modal: '' }); }, - /** - * Debug tap handler - */ debug_tap() { const count = (this.debug_tap_count || 0) + 1; this.debug_tap_count = count; @@ -299,7 +243,6 @@ Page({ this.debug_tap_count = 0; } - // Clear count after 3 seconds setTimeout(() => { this.debug_tap_count = 0; }, 3000); @@ -331,11 +274,19 @@ Page({ }, /** - * Start frame processing - sets up camera frame listener + * Start frame processing if both QRTool and camera are ready */ - startFrameProcessing() { + 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) { - this.addDebugMessage('QRTool or camera not ready, skipping frame processing'); + 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; } @@ -349,6 +300,9 @@ Page({ // Start the listener this.listener.start(); + + // Mark as started + this.setData({ frame_processing_started: true }); }, /** @@ -371,28 +325,27 @@ Page({ this.lastFrameTime = now; // Start timing frame processing - const processStart = performance.now(); + const processStart = Date.now(); // Process frame data directly try { - const result = this.data.enable_debug ? - process_frame_with_debug(frame.width, frame.height, frame.data, this.data.camera_sensitivity) : - process_frame(frame.width, frame.height, frame.data, this.data.camera_sensitivity); + const result = process_frame(frame.width, frame.height, frame.data, this.data.camera_sensitivity, this.data.enable_debug); // Calculate processing time - const processEnd = performance.now(); + 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 = newTotalTime / newFramesProcessed; + const newAvgTime = Math.round(newTotalTime / newFramesProcessed); this.setData({ frames_processed: newFramesProcessed, total_processing_time: newTotalTime, - avg_processing_time: newAvgTime, - last_frame_time: processingTime + avg_processing_time_ms: newAvgTime, + last_frame_time_ms: Math.round(processingTime), + debug_last_result: result }); if (result) { @@ -401,18 +354,19 @@ Page({ } catch (error) { this.addDebugMessage(`Frame processing error: ${error.message}`); // Still count failed processing attempts - const processEnd = performance.now(); + 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 = newTotalTime / newFramesProcessed; + const newAvgTime = Math.round(newTotalTime / newFramesProcessed); this.setData({ frames_processed: newFramesProcessed, total_processing_time: newTotalTime, - avg_processing_time: newAvgTime, - last_frame_time: processingTime + avg_processing_time_ms: newAvgTime, + last_frame_time_ms: Math.round(processingTime), + debug_last_result: null }); } }, @@ -507,24 +461,31 @@ Page({ * State: Any -> Loading (restart) */ restartScanning() { - this.transitionToState('loading'); + // 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: '初始化相机...', + hint_text: hintText, busy: true, // Reset frame processing statistics frames_processed: 0, frames_skipped: 0, total_processing_time: 0, - avg_processing_time: 0, - last_frame_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 - this.loadCameraRules(); + // Reload camera rules to restart the flow (only if QRTool is ready) + if (this.data.qrtool_ready) { + this.loadCameraRules(); + } }, /** @@ -537,7 +498,7 @@ Page({ const debugMsg = `${timestamp}: ${message}`; this.setData({ - debug_msgs: [...this.data.debug_msgs, debugMsg].slice(-10) // Keep last 10 messages + debug_msgs: [debugMsg, ...this.data.debug_msgs].slice(0, 5) // Keep first 5 messages (newest on top) }); }, diff --git a/scanner/pages/emblemscanner/emblemscanner.wxml b/scanner/pages/emblemscanner/emblemscanner.wxml index 77e6e56..75bc013 100644 --- a/scanner/pages/emblemscanner/emblemscanner.wxml +++ b/scanner/pages/emblemscanner/emblemscanner.wxml @@ -1,4 +1,10 @@ + + + + 初始化QR工具... + + @@ -31,7 +37,7 @@ @@ -46,58 +52,95 @@ - - - {{ item }} - - + + + + state: {{ app_state }} + + model: {{ phone_model }} - - zoom: - {{ zoom }}/{{ max_zoom }} - - - no_web_view - + + rule: {{ camera_rule.model || 'default' }} - web_view: - {{ use_web_view }} + zoom: + {{ zoom }}/{{ max_zoom }} sensitivity: - {{ camera_sensitivity }} + {{ camera_sensitivity }} + + web_view: + {{ use_web_view }} + + + no_web_view + + + + + qrtool: + {{ qrtool_ready ? 'ready' : 'loading' }} + + + frames: - {{ frames_processed }}/{{ frames_processed + frames_skipped }} + {{ frames_processed }} + / + {{ frames_processed + frames_skipped }} skipped: - {{ frames_skipped }} + {{ frames_skipped }} avg: - {{ avg_processing_time.toFixed(2) }}ms + {{ avg_processing_time_ms }}ms last: - {{ last_frame_time.toFixed(2) }}ms + {{ last_frame_time_ms }}ms + + + + qr: + {{ debug_last_result.qrcode || 'none' }} + + + ok: + {{ debug_last_result.ok ? 'yes' : 'no' }} + + + err: + {{ debug_last_result.err }} + + + result: {{ result }} + + + + + + + + {{ item }} diff --git a/scanner/pages/emblemscanner/emblemscanner.wxss b/scanner/pages/emblemscanner/emblemscanner.wxss index 198c8cb..55fdc14 100644 --- a/scanner/pages/emblemscanner/emblemscanner.wxss +++ b/scanner/pages/emblemscanner/emblemscanner.wxss @@ -198,23 +198,37 @@ view.debug { background-color: rgba(100, 100, 100, 0.5); z-index: 1000; font-size: 10px; - overflow-y: auto; opacity: 0.75; display: flex; flex-direction: column; } -view.debug image { - right: 10px; - top: 10px; +.debug-top-row { + display: flex; + gap: 4px; + margin-bottom: 4px; +} + +.debug-image-box { + flex-shrink: 0; +} + +.debug-image-box image { width: 64px; height: 64px; border: 1px solid rgba(239, 72, 35, 0.8); } +.debug-info-panel { + flex: 1; + display: flex; + flex-direction: column; +} + /* Debug messages section */ .debug-messages { - margin-bottom: 4px; + margin-top: 4px; + flex-shrink: 0; } .debug-msg { @@ -222,6 +236,8 @@ view.debug image { margin: 1px 0; line-height: 1.2; color: #ffffff; + font-family: monospace; + font-size: 9px; } /* Debug items container */ @@ -257,6 +273,13 @@ view.debug image { word-break: break-word; } +.debug-value-number { + color: #99ff99; + font-family: monospace; + min-width: 32px; + text-align: right; +} + /* Special styling for no_web_view flag box */ .debug-item.debug-flag-box { border: 1px solid rgba(255, 0, 0, 0.6); diff --git a/scanner/pages/emblemscanner/qrprocessor.js b/scanner/pages/emblemscanner/qrprocessor.js index 246a486..4329dfb 100644 --- a/scanner/pages/emblemscanner/qrprocessor.js +++ b/scanner/pages/emblemscanner/qrprocessor.js @@ -26,91 +26,90 @@ function is_qrtool_ready() { /** * Process camera frame for QR code detection */ -function process_frame(width, height, image_data, camera_sensitivity) { +function process_frame(width, height, image_data, camera_sensitivity, enable_debug = false) { if (!qrtool_ready) { console.log("qrtool not ready"); return null; } + console.log('process_frame called:', { + width, + height, + data_type: typeof image_data, + data_constructor: image_data ? image_data.constructor.name : 'undefined', + data_length: image_data ? image_data.length : 'undefined', + data_byteLength: image_data ? image_data.byteLength : 'undefined', + bytes_per_element: image_data ? image_data.BYTES_PER_ELEMENT : 'undefined', + camera_sensitivity, + enable_debug + }); + try { - // Allocate buffer for image data - var buf = qrtool._malloc(image_data.length * image_data.BYTES_PER_ELEMENT); - qrtool.HEAPU8.set(image_data, buf); + // Copy frame data to avoid TOCTOU + var uca1 = new Uint8ClampedArray(image_data); + var uca = new Uint8ClampedArray(uca1); + + console.log('Frame data copied:', { + uca_length: uca.length, + uca_bytes_per_element: uca.BYTES_PER_ELEMENT + }); + + var buf = qrtool._malloc(uca.length * uca.BYTES_PER_ELEMENT); + qrtool.HEAPU8.set(uca, buf); + + console.log('Buffer allocated:', { + buffer_size: uca.length * uca.BYTES_PER_ELEMENT + }); + + var dot_area_buf = 0; + var debug_data_url = null; + + if (enable_debug) { + const dot_area_size = 32; + const da_len = dot_area_size * dot_area_size * uca.BYTES_PER_ELEMENT * 4; + dot_area_buf = qrtool._malloc(da_len); + } - // Process QR code detection with angle var result_str = qrtool.ccall('qrtool_angle', 'string', ['number', 'number', 'number', 'number', 'number'], - [buf, width, height, 0, camera_sensitivity || 1.0] + [buf, width, height, dot_area_buf, camera_sensitivity || 1] ); - // Clean up buffer + console.log('qrtool_angle result:', result_str); + + if (enable_debug && dot_area_buf) { + const dot_area_size = 32; + const da_len = dot_area_size * dot_area_size * uca.BYTES_PER_ELEMENT * 4; + var debug_view = qrtool.HEAPU8.subarray(dot_area_buf, dot_area_buf + da_len); + debug_data_url = data_url_from_frame(dot_area_size, dot_area_size, debug_view); + } qrtool._free(buf); + if (dot_area_buf) { + qrtool._free(dot_area_buf); + } // Parse result var result = JSON.parse(result_str); - return { + var returnValue = { qrcode: result.qrcode || '', angle: result.angle || 0, ok: result.ok || false, err: result.err || '', valid_pattern: is_emblem_qr_pattern(result.qrcode || '') }; + + if (enable_debug && debug_data_url) { + returnValue.debug_data_url = debug_data_url; + } + + return returnValue; } catch (error) { console.error('QR processing error:', error); return null; } } -/** - * Process frame with dot area extraction (for debug visualization) - */ -function process_frame_with_debug(width, height, image_data, camera_sensitivity) { - if (!qrtool_ready) { - console.log("qrtool not ready"); - return null; - } - - try { - // Allocate buffer for image data - var buf = qrtool._malloc(image_data.length * image_data.BYTES_PER_ELEMENT); - qrtool.HEAPU8.set(image_data, buf); - - // Allocate buffer for debug dot area - const dot_area_size = 32; - const da_len = dot_area_size * dot_area_size * image_data.BYTES_PER_ELEMENT * 4; - var dot_area_buf = qrtool._malloc(da_len); - - // Process QR code detection with debug output - var result_str = qrtool.ccall('qrtool_angle', 'string', - ['number', 'number', 'number', 'number', 'number'], - [buf, width, height, dot_area_buf, camera_sensitivity || 1.0] - ); - - // Extract debug image - var debug_view = qrtool.HEAPU8.subarray(dot_area_buf, dot_area_buf + da_len); - var debug_data_url = data_url_from_frame(dot_area_size, dot_area_size, debug_view); - - // Clean up buffers - qrtool._free(buf); - qrtool._free(dot_area_buf); - - // Parse result - var result = JSON.parse(result_str); - - return { - qrcode: result.qrcode || '', - angle: result.angle || 0, - ok: result.ok || false, - err: result.err || '', - valid_pattern: is_emblem_qr_pattern(result.qrcode || ''), - debug_data_url: debug_data_url - }; - } catch (error) { - console.error('QR processing error:', error); - return null; - } -} /** * Create offscreen canvas for debug image generation @@ -179,7 +178,6 @@ module.exports = { load_qrtool, is_qrtool_ready, process_frame, - process_frame_with_debug, data_url_from_frame, is_emblem_qr_pattern, make_hint_text diff --git a/scanner/pages/emblemscanner/qrtool.wx.js b/scanner/pages/emblemscanner/qrtool.wx.js index 75dc389..4fe9018 100644 --- a/scanner/pages/emblemscanner/qrtool.wx.js +++ b/scanner/pages/emblemscanner/qrtool.wx.js @@ -34,7 +34,7 @@ var performance = { }; Module["instantiateWasm"] = (info, receiveInstance) => { console.log("loading wasm...", info); - WebAssembly.instantiate("assets/qrtool.wx.wasm.br", info).then(result => { + WebAssembly.instantiate("pages/emblemscanner/assets/qrtool.wx.wasm.br", info).then(result => { console.log("result:", result); var inst = result["instance"]; receiveInstance(inst);