diff --git a/scanner/pages/emblemscanner/emblemscanner.js b/scanner/pages/emblemscanner/emblemscanner.js index aa44ca0..b736e23 100644 --- a/scanner/pages/emblemscanner/emblemscanner.js +++ b/scanner/pages/emblemscanner/emblemscanner.js @@ -40,16 +40,15 @@ const { } = require('./upload.js'); // Import precheck utilities for image processing -const { - data_url_from_frame -} = require('../../precheck.js'); +// 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 + make_hint_text, + data_url_from_frame } = require('./qrprocessor.js'); Page({ @@ -74,17 +73,19 @@ Page({ qrarc_class: 'sm', qrmarkers_class: 'hidden', frame_upload_interval_ms: 2000, - return_page: '', // Page to navigate to after successful scan + 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 debug_msgs: [], debug_image_data_url: '', debug_last_result: null, + debug_current_frame_url: '', // Current frame being processed 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, @@ -93,6 +94,7 @@ Page({ use_web_view: false, use_worker: false, emblem_camera_url: null, + first_qr_found: false, // Track if first QR has been detected to trigger zoom-in // State machine: loading_rules -> loading_qrtool -> init_camera -> scanning -> verifying -> result app_state: 'loading_rules', // 'loading_rules', 'loading_qrtool', 'init_camera', 'scanning', 'webview_scanning', 'verifying', 'result' scan_mode: 'unknown', // 'camera', 'webview' @@ -105,7 +107,7 @@ Page({ const no_web_view = options.no_web_view === '1' || options.no_web_view === 'true'; this.setData({ - return_page: options.return_page || '', + return_page: options.return_page || '/pages/test_result_page/test_result_page', no_web_view: no_web_view }); @@ -202,17 +204,18 @@ Page({ if (result) { // For worker, we need to trigger image collection when we find a good QR - if (result.ok && result.qrcode && result.valid_pattern && is_emblem_qr_pattern(result.qrcode)) { + if (result.qrcode && result.valid_pattern && is_emblem_qr_pattern(result.qrcode)) { this.addDebugMessage(`Worker QR detected: ${result.qrcode}`); // Trigger zoom-in if function is set up - if (this.on_qr_found && !this.data.qr_found) { - this.on_qr_found(); - this.setData({ qr_found: true }); + if (this.on_first_qr_found && !this.data.first_qr_found) { + this.on_first_qr_found(); } - // Request worker to submit image data - this.worker.postMessage({ type: "ready_to_submit" }); + if (result.ok) { + // Request worker to submit image data + this.worker.postMessage({ type: "ready_to_submit" }); + } } this.handleQRResult(result, this.lastWorkerFrame); @@ -222,14 +225,20 @@ Page({ const result = msg.res; const imageData = msg.image_data; if (imageData) { - const dataUrl = data_url_from_frame(imageData.width, imageData.height, new Uint8ClampedArray(imageData.data)); - this.image_data_urls.push(dataUrl); + // Worker now sends data URL directly, no need to convert + this.image_data_urls.push(imageData.data); + + // Update ok frames counter + this.setData({ + ok_frames: this.image_data_urls.length + }); if (this.image_data_urls.length >= 3) { this.addDebugMessage('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.addDebugMessage(`Collected ${this.image_data_urls.length}/3 worker images`); } @@ -382,8 +391,9 @@ Page({ this.camera_context.setZoom({ zoom: initial_zoom }); // Set up zoom-in behavior when QR is found - this.on_qr_found = () => { - this.addDebugMessage(`QR found, zoom to ${zoom}x`); + this.on_first_qr_found = () => { + this.setData({ first_qr_found: true }); + this.addDebugMessage(`First QR found, zoom to ${zoom}x`); this.camera_context.setZoom({ zoom: zoom }); this.setData({ zoom: zoom, @@ -465,10 +475,22 @@ Page({ if (this.data.use_worker && this.worker) { // 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); + } + this.worker.postMessage({ type: 'frame', width: frame.width, height: frame.height, + data: uca, // Pass actual frame data camera_sensitivity: this.data.camera_sensitivity }); } else { @@ -484,7 +506,17 @@ Page({ const processStart = Date.now(); try { - const result = process_frame(frame.width, frame.height, frame.data, this.data.camera_sensitivity, this.data.enable_debug); + // 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(); @@ -504,7 +536,7 @@ Page({ }); if (result) { - this.handleQRResult(result, frame); + this.handleQRResult(result, frame, uca); } } catch (error) { this.addDebugMessage(`Frame processing error: ${error.message}`); @@ -529,7 +561,7 @@ Page({ /** * Handle QR processing result */ - handleQRResult(result, frame) { + handleQRResult(result, frame, copiedFrameData = null) { // Update debug info if available if (result.debug_data_url) { this.setData({ @@ -543,18 +575,17 @@ Page({ // 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 (!this.data.qr_found && result.qrcode && result.valid_pattern && is_emblem_qr_pattern(result.qrcode)) { + if (result.qrcode && result.valid_pattern && is_emblem_qr_pattern(result.qrcode)) { this.addDebugMessage(`QR detected and ready: ${result.qrcode}`); // Trigger zoom-in if function is set up - if (this.on_qr_found) { - this.on_qr_found(); + if (!this.data.first_qr_found && this.on_first_qr_found) { + this.on_first_qr_found(); } - this.onQRCodeDetected(result.qrcode, frame); - this.setData({ - qr_found: true - }); + if (result.ok) { + this.onQRCodeDetected(result.qrcode, frame, copiedFrameData); // Pass the copied frame data + } } else { // Update hint for user guidance this.setData({ hint_text: hint }); @@ -566,23 +597,38 @@ Page({ }, /** - * Handle successful QR code detection - collect images from "ok" frames and verify + * Handle successful QR code detection - collect images from "ok" frames and verify (non-worker case) */ - onQRCodeDetected(qrCode, frameData) { - // Convert frame data to data URL for upload (only called for "ok" frames) - if (frameData) { - const dataUrl = data_url_from_frame(frameData.width, frameData.height, frameData.data); + 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); - this.addDebugMessage(`Collected ${this.image_data_urls.length}/3 good images`); - } - - // Need 3 "ok" frames before verification (like camera.js) - if (this.image_data_urls.length >= 3) { - this.addDebugMessage('3 good images collected, starting verification'); - this.startVerifying(); - this.submitImageForVerification(this.image_data_urls, qrCode); - this.image_data_urls = []; // Reset for next scan + + // Update ok frames counter + this.setData({ + ok_frames: this.image_data_urls.length + }); + + this.addDebugMessage(`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.addDebugMessage(`Submitted image preview: ${dataUrl.substring(0, 50)}...`); + } + + // Need 3 "ok" frames before verification (like camera.js) + if (this.image_data_urls.length >= 3) { + this.addDebugMessage('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 }, @@ -671,25 +717,20 @@ Page({ goToResult(qrCode, serialCode) { this.transitionToState('result'); - if (this.data.return_page) { - // Pass both qr_code and serial_code parameters like camera-5.1 does - // serialCode comes from server verification response - const url = `${this.data.return_page}?qr_code=${encodeURIComponent(qrCode)}&serial_code=${encodeURIComponent(serialCode)}`; - - wx.redirectTo({ - url: url, - 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(); - } + // Pass both qr_code and serial_code parameters like camera-5.1 does + // serialCode comes from server verification response + const url = `${this.data.return_page}?qr_code=${encodeURIComponent(qrCode)}&serial_code=${encodeURIComponent(serialCode)}`; + + wx.redirectTo({ + url: url, + success: () => { + this.addDebugMessage(`Navigated to: ${this.data.return_page}`); + }, + fail: (err) => { + this.addDebugMessage(`Navigation failed: ${err.errMsg}`); + this.restartScanning(); + } + }); }, /** @@ -708,9 +749,11 @@ Page({ // 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 + last_frame_time_ms: 0, + first_qr_found: false }); // Reset frame timing @@ -736,6 +779,18 @@ Page({ }); }, + /** + * Update debug frame URLs for visualization + */ + updateDebugFrameUrls(frameDataUrl) { + if (!this.data.enable_debug) return; + + // Just show the current frame + this.setData({ + debug_current_frame_url: frameDataUrl + }); + }, + /** * Log function for debugging */ diff --git a/scanner/pages/emblemscanner/emblemscanner.wxml b/scanner/pages/emblemscanner/emblemscanner.wxml index 06e0c2d..94dc05c 100644 --- a/scanner/pages/emblemscanner/emblemscanner.wxml +++ b/scanner/pages/emblemscanner/emblemscanner.wxml @@ -71,6 +71,14 @@ + + + Processed Frame + + + + Raw Frame + @@ -84,34 +92,18 @@ model: {{ phone_model }} - - - - rule: - {{ camera_rule.model || 'default' }} - zoom: {{ zoom }}/{{ max_zoom }} sensitivity: - {{ camera_sensitivity }} - - - web_view: - {{ use_web_view }} + {{ camera_sensitivity }} no_web_view - - - qrtool: - {{ qrtool_ready ? 'ready' : 'loading' }} - - frames: @@ -120,17 +112,14 @@ {{ frames_processed + frames_skipped }} - skipped: - {{ frames_skipped }} + ok: + {{ ok_frames }} + /3 avg: {{ avg_processing_time_ms }}ms - - last: - {{ last_frame_time_ms }}ms - @@ -153,9 +142,6 @@ - - - {{ item }} diff --git a/scanner/pages/emblemscanner/emblemscanner.wxss b/scanner/pages/emblemscanner/emblemscanner.wxss index 55fdc14..0109170 100644 --- a/scanner/pages/emblemscanner/emblemscanner.wxss +++ b/scanner/pages/emblemscanner/emblemscanner.wxss @@ -219,6 +219,25 @@ view.debug { border: 1px solid rgba(239, 72, 35, 0.8); } +.debug-frame-box { + flex-shrink: 0; +} + +.debug-frame-box image { + width: 64px; + height: 64px; + border: 1px solid rgba(35, 150, 239, 0.8); +} + +.debug-frame-box .debug-label, +.debug-image-box .debug-label { + font-size: 8px; + color: #ffffff; + text-align: center; + margin-top: 2px; + font-weight: bold; +} + .debug-info-panel { flex: 1; display: flex; @@ -291,6 +310,7 @@ view.debug { font-weight: bold; } + /* Tooltip */ view.tooltip { position: fixed; diff --git a/scanner/pages/emblemscanner/qrprocessor.js b/scanner/pages/emblemscanner/qrprocessor.js index c6d1bb2..836042a 100644 --- a/scanner/pages/emblemscanner/qrprocessor.js +++ b/scanner/pages/emblemscanner/qrprocessor.js @@ -89,23 +89,31 @@ function process_frame(width, height, image_data, camera_sensitivity, enable_deb /** * Create offscreen canvas for debug image generation */ -const offscreenCanvas = wx.createOffscreenCanvas({ - type: '2d', - width: 100, - height: 100, -}); +let offscreenCanvas = null; + +function getOffscreenCanvas() { + if (!offscreenCanvas) { + offscreenCanvas = wx.createOffscreenCanvas({ + type: '2d', + width: 100, + height: 100, + }); + } + return offscreenCanvas; +} /** - * Convert raw frame data to data URL for debug visualization + * Convert raw frame data to data URL for image visualization */ function data_url_from_frame(width, height, image_data) { - offscreenCanvas.width = width; - offscreenCanvas.height = height; - var ctx = offscreenCanvas.getContext('2d'); + const canvas = getOffscreenCanvas(); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); var imgd = ctx.createImageData(width, height); imgd.data.set(image_data); ctx.putImageData(imgd, 0, 0); - return offscreenCanvas.toDataURL("image/jpeg", 1.0); + return canvas.toDataURL("image/jpeg", 1.0); } /** diff --git a/scanner/pages/emblemscanner/upload.js b/scanner/pages/emblemscanner/upload.js index 9dc0b8a..58fe480 100644 --- a/scanner/pages/emblemscanner/upload.js +++ b/scanner/pages/emblemscanner/upload.js @@ -1,7 +1,3 @@ -function upload_image_data_urls(image_data_urls, success, fail, log) { - do_upload(image_data_urls, success, fail, log); -} - function check_auto_torch(qrcode, cb) { var gd = getApp().globalData; var url = gd.server_url + '/api/v1/check-auto-torch/?qrcode=' + encodeURIComponent(qrcode); @@ -33,7 +29,7 @@ function check_auto_torch(qrcode, cb) { }); } -function do_upload(image_data_urls, success, fail, log) { +function upload_image_data_urls(image_data_urls, success, fail, log) { var ui = wx.getStorageSync('userinfo'); var gd = getApp().globalData; var fd = { diff --git a/scanner/pages/emblemscanner/worker/index.js b/scanner/pages/emblemscanner/worker/index.js index ca8816e..bd06929 100644 --- a/scanner/pages/emblemscanner/worker/index.js +++ b/scanner/pages/emblemscanner/worker/index.js @@ -2,6 +2,34 @@ console.log("hello from emblemscanner worker"); let qrtool = require('../qrtool.wx.js'); +// Create offscreen canvas for image generation (self-contained) +let offscreenCanvas = null; + +function getOffscreenCanvas() { + if (!offscreenCanvas) { + offscreenCanvas = wx.createOffscreenCanvas({ + type: '2d', + width: 100, + height: 100, + }); + } + return offscreenCanvas; +} + +/** + * Convert raw frame data to data URL for image visualization + */ +function data_url_from_frame(width, height, image_data) { + const canvas = getOffscreenCanvas(); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + var imgd = ctx.createImageData(width, height); + imgd.data.set(image_data); + ctx.putImageData(imgd, 0, 0); + return canvas.toDataURL("image/jpeg", 1.0); +} + var qrtool_ready = false; qrtool.onRuntimeInitialized = () => { @@ -23,7 +51,7 @@ function handle_frame(data, width, height, camera_sensitivity) { ok: false, err: "qrtool not ready", }, - processing_time: Date.now() - begin, + processing_time: 0, }); return; } @@ -43,10 +71,11 @@ function handle_frame(data, width, height, camera_sensitivity) { if (res.ok) { // Since image_data takes seconds to serialize, send it in a separate // message to the UI can update with the good news + const dataUrl = data_url_from_frame(width, height, uca); submit_message = { type: "submit", res, - image_data: { data, width, height }, + image_data: { data: dataUrl, width, height }, }; } } @@ -56,9 +85,9 @@ worker.onMessage((msg) => { switch (msg.type) { case "frame": try { - const data = worker.getCameraFrameData(); - if (data) { - handle_frame(data, msg.width, msg.height, msg.camera_sensitivity); + // Use frame data from message instead of getCameraFrameData() + if (msg.data) { + handle_frame(msg.data, msg.width, msg.height, msg.camera_sensitivity); } } catch (e) { console.log(e); @@ -68,6 +97,6 @@ worker.onMessage((msg) => { worker.postMessage(submit_message); break; default: - console.log("Unknown message type:", msg.data.type); + console.log("Unknown message type:", msg.type); } }); \ No newline at end of file diff --git a/scanner/pages/index/index.js b/scanner/pages/index/index.js index aee203e..0e40e42 100644 --- a/scanner/pages/index/index.js +++ b/scanner/pages/index/index.js @@ -129,7 +129,7 @@ Page({ goto_camera: function () { this.getUserProfile(() => { wx.navigateTo({ - url: '/pages/emblemscanner/emblemscanner?return_page=/pages/test_result_page/test_result_page' + url: '/pages/emblemscanner/emblemscanner?return_page=/pages/productinfo/productinfo' }); }); }, diff --git a/scanner/project.private.config.json b/scanner/project.private.config.json index c1062fb..4a0a90d 100644 --- a/scanner/project.private.config.json +++ b/scanner/project.private.config.json @@ -5,7 +5,7 @@ { "name": "emblemscanner force camera", "pathName": "pages/emblemscanner/emblemscanner", - "query": "debug=1&no_web_view=1", + "query": "debug=1&no_web_view=1&return_page=/pages/test_result_page/test_result_page", "scene": null, "launchMode": "default" },