606 lines
16 KiB
JavaScript
606 lines
16 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 from library
|
|
const {
|
|
get_system_info,
|
|
get_phone_model,
|
|
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,
|
|
process_frame_with_debug,
|
|
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: '',
|
|
qrtool_ready: false,
|
|
// Frame processing statistics
|
|
frames_processed: 0,
|
|
frames_skipped: 0,
|
|
total_processing_time: 0,
|
|
avg_processing_time: 0,
|
|
last_frame_time: 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'
|
|
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({
|
|
return_page: options.return_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();
|
|
} else {
|
|
setTimeout(checkReady, 100);
|
|
}
|
|
};
|
|
checkReady();
|
|
},
|
|
|
|
/**
|
|
* Initialize system information and device detection
|
|
*/
|
|
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`);
|
|
},
|
|
|
|
/**
|
|
* 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}`);
|
|
} 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
|
|
});
|
|
|
|
// 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 {
|
|
this.startCameraScanning();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
},
|
|
|
|
/**
|
|
* Camera error callback
|
|
*/
|
|
onCameraError(e) {
|
|
console.error('Camera error', e);
|
|
this.addDebugMessage(`Camera error: ${JSON.stringify(e.detail)}`);
|
|
this.setData({
|
|
show_modal: 'verifyfailed',
|
|
hint_text: '相机初始化失败'
|
|
});
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
* Toggle torch/flash
|
|
*/
|
|
toggle_torch() {
|
|
const newFlash = this.data.camera_flash === 'torch' ? 'off' : 'torch';
|
|
this.setData({
|
|
camera_flash: newFlash
|
|
});
|
|
console.log('Torch toggled to:', newFlash);
|
|
},
|
|
|
|
/**
|
|
* Show scan guide
|
|
*/
|
|
show_scanguide() {
|
|
this.setData({
|
|
show_modal: 'scanguide',
|
|
show_tip: false
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
|
|
if (count >= 5) {
|
|
this.setData({
|
|
enable_debug: !this.data.enable_debug
|
|
});
|
|
this.debug_tap_count = 0;
|
|
}
|
|
|
|
// Clear count after 3 seconds
|
|
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 - sets up camera frame listener
|
|
*/
|
|
startFrameProcessing() {
|
|
if (!this.data.qrtool_ready || !this.camera_context) {
|
|
this.addDebugMessage('QRTool or camera not ready, skipping frame processing');
|
|
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();
|
|
},
|
|
|
|
/**
|
|
* 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 = performance.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);
|
|
|
|
// Calculate processing time
|
|
const processEnd = performance.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;
|
|
|
|
this.setData({
|
|
frames_processed: newFramesProcessed,
|
|
total_processing_time: newTotalTime,
|
|
avg_processing_time: newAvgTime,
|
|
last_frame_time: processingTime
|
|
});
|
|
|
|
if (result) {
|
|
this.handleQRResult(result);
|
|
}
|
|
} catch (error) {
|
|
this.addDebugMessage(`Frame processing error: ${error.message}`);
|
|
// Still count failed processing attempts
|
|
const processEnd = performance.now();
|
|
const processingTime = processEnd - processStart;
|
|
|
|
const newFramesProcessed = this.data.frames_processed + 1;
|
|
const newTotalTime = this.data.total_processing_time + processingTime;
|
|
const newAvgTime = newTotalTime / newFramesProcessed;
|
|
|
|
this.setData({
|
|
frames_processed: newFramesProcessed,
|
|
total_processing_time: newTotalTime,
|
|
avg_processing_time: newAvgTime,
|
|
last_frame_time: processingTime
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
this.transitionToState('loading');
|
|
this.setData({
|
|
show_modal: '',
|
|
hint_text: '初始化相机...',
|
|
busy: true,
|
|
// Reset frame processing statistics
|
|
frames_processed: 0,
|
|
frames_skipped: 0,
|
|
total_processing_time: 0,
|
|
avg_processing_time: 0,
|
|
last_frame_time: 0
|
|
});
|
|
|
|
// Reset frame timing
|
|
this.lastFrameTime = 0;
|
|
|
|
// Reload camera rules to restart the flow
|
|
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: [...this.data.debug_msgs, debugMsg].slice(-10) // Keep last 10 messages
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
}
|
|
|
|
}); |