1078 lines
32 KiB
JavaScript
1078 lines
32 KiB
JavaScript
// 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 fromsimplify api
|
|
const {
|
|
get_system_info,
|
|
get_camera_rule,
|
|
make_query,
|
|
fetch_real_ip,
|
|
get_tenant_id,
|
|
is_emblem_qr_pattern
|
|
} = 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
|
|
});
|
|
|
|
// 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 local worker file
|
|
var worker = wx.createWorker('/pages/emblemscanner/worker/index.js', {
|
|
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
|
|
this.get_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.get_worker();
|
|
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 });
|
|
|
|
this.get_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();
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
var gd = getApp().globalData;
|
|
if (!gd.emblemscanner_worker) {
|
|
gd.emblemscanner_worker = this.setupWorker();
|
|
}
|
|
return gd.emblemscanner_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');
|
|
}
|
|
}
|
|
|
|
}); |