emblemscanner: native wasm works

This commit is contained in:
Fam Zheng 2025-09-13 19:52:21 +01:00
parent 431f81faad
commit 7c7c94fa7b
5 changed files with 197 additions and 172 deletions

View File

@ -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
// 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)
});
},

View File

@ -1,4 +1,10 @@
<view class="wrapper">
<!-- STATE: INITIALIZING -->
<view wx:if="{{ app_state == 'initializing' }}" class="loading-spinner">
<view class="spinner"></view>
<text>初始化QR工具...</text>
</view>
<!-- STATE: LOADING -->
<view wx:if="{{ app_state == 'loading' }}" class="loading-spinner">
<view class="spinner"></view>
@ -31,7 +37,7 @@
<camera class="camera"
flash="{{ camera_flash }}"
frame-size="medium"
bindready="onCameraReady"
bindinitdone="onCameraReady"
binderror="onCameraError">
</camera>
</block>
@ -46,60 +52,97 @@
<!-- Debug overlay (available in all states) -->
<view class="debug" wx:if="{{ enable_debug }}">
<view><image src="{{ debug_image_data_url }}"></image></view>
<view class="debug-messages">
<text wx:for="{{ debug_msgs }}" class="debug-msg">{{ item }}</text>
</view>
<view class="debug-top-row">
<view class="debug-info-panel">
<view class="debug-items">
<!-- Application State -->
<view class="debug-item">
<text class="debug-label">state:</text>
<text class="debug-value">{{ app_state }}</text>
</view>
<!-- System Info -->
<view class="debug-item">
<text class="debug-label">model:</text>
<text class="debug-value">{{ phone_model }}</text>
</view>
<view class="debug-item">
<text class="debug-label">zoom:</text>
<text class="debug-value">{{ zoom }}/{{ max_zoom }}</text>
</view>
<view class="debug-item debug-flag-box" wx:if="{{ no_web_view }}">
<text class="debug-flag">no_web_view</text>
</view>
<!-- Camera Configuration -->
<view class="debug-item">
<text class="debug-label">rule:</text>
<text class="debug-value">{{ camera_rule.model || 'default' }}</text>
</view>
<view class="debug-item">
<text class="debug-label">web_view:</text>
<text class="debug-value">{{ use_web_view }}</text>
<text class="debug-label">zoom:</text>
<text class="debug-value-number">{{ zoom }}/{{ max_zoom }}</text>
</view>
<view class="debug-item">
<text class="debug-label">sensitivity:</text>
<text class="debug-value">{{ camera_sensitivity }}</text>
<text class="debug-value-number">{{ camera_sensitivity }}</text>
</view>
<view class="debug-item">
<text class="debug-label">web_view:</text>
<text class="debug-value">{{ use_web_view }}</text>
</view>
<view class="debug-item debug-flag-box" wx:if="{{ no_web_view }}">
<text class="debug-flag">no_web_view</text>
</view>
<!-- Processing Status -->
<view class="debug-item">
<text class="debug-label">qrtool:</text>
<text class="debug-value">{{ qrtool_ready ? 'ready' : 'loading' }}</text>
</view>
<!-- Frame Statistics -->
<view class="debug-item">
<text class="debug-label">frames:</text>
<text class="debug-value">{{ frames_processed }}/{{ frames_processed + frames_skipped }}</text>
<text class="debug-value-number">{{ frames_processed }}</text>
<text class="debug-value">/</text>
<text class="debug-value-number">{{ frames_processed + frames_skipped }}</text>
</view>
<view class="debug-item">
<text class="debug-label">skipped:</text>
<text class="debug-value">{{ frames_skipped }}</text>
<text class="debug-value-number">{{ frames_skipped }}</text>
</view>
<view class="debug-item">
<text class="debug-label">avg:</text>
<text class="debug-value">{{ avg_processing_time.toFixed(2) }}ms</text>
<text class="debug-value-number">{{ avg_processing_time_ms }}ms</text>
</view>
<view class="debug-item">
<text class="debug-label">last:</text>
<text class="debug-value">{{ last_frame_time.toFixed(2) }}ms</text>
<text class="debug-value-number">{{ last_frame_time_ms }}ms</text>
</view>
<!-- QR Detection Results -->
<view class="debug-item" wx:if="{{ debug_last_result }}">
<text class="debug-label">qr:</text>
<text class="debug-value">{{ debug_last_result.qrcode || 'none' }}</text>
</view>
<view class="debug-item" wx:if="{{ debug_last_result }}">
<text class="debug-label">ok:</text>
<text class="debug-value">{{ debug_last_result.ok ? 'yes' : 'no' }}</text>
</view>
<view class="debug-item" wx:if="{{ debug_last_result && debug_last_result.err }}">
<text class="debug-label">err:</text>
<text class="debug-value">{{ debug_last_result.err }}</text>
</view>
<!-- Legacy Result -->
<view class="debug-item" wx:if="{{ result }}">
<text class="debug-label">result:</text>
<text class="debug-value">{{ result }}</text>
</view>
</view>
</view>
<view class="debug-image-box">
<image src="{{ debug_image_data_url }}"></image>
</view>
</view>
<view class="debug-messages">
<text wx:for="{{ debug_msgs }}" class="debug-msg">{{ item }}</text>
</view>
</view>
<!-- Bottom action controls (only for camera scanning) -->
<view wx:if="{{ app_state == 'scanning_camera' }}" class="bottomfixed">

View File

@ -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);

View File

@ -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;
}
try {
// Allocate buffer for image data
var buf = qrtool._malloc(image_data.length * image_data.BYTES_PER_ELEMENT);
qrtool.HEAPU8.set(image_data, buf);
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 {
// 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

View File

@ -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);