emblemscanner wip

This commit is contained in:
Fam Zheng 2025-09-13 11:04:00 +01:00
parent 78a48c6cb0
commit 431f81faad
10 changed files with 6122 additions and 5414 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,28 @@
// QR Scanner Module - Self-contained QR scanning page // QR Scanner Module - Self-contained QR scanning page
// Adapted from existing camera implementation // 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 // Import utility functions from library
const { const {
@ -12,6 +35,15 @@ const {
is_emblem_qr_pattern is_emblem_qr_pattern
} = require('./libemblemscanner.js'); } = 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({
/** /**
* Page initial data * Page initial data
@ -40,6 +72,13 @@ Page({
tenant_id: '', // Tenant identifier tenant_id: '', // Tenant identifier
debug_msgs: [], debug_msgs: [],
debug_image_data_url: '', 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, rule_zoom: -1,
camera_rule: null, camera_rule: null,
use_web_view: false, use_web_view: false,
@ -47,7 +86,7 @@ Page({
// State machine: loading -> scanning -> verifying -> result // State machine: loading -> scanning -> verifying -> result
app_state: 'loading', // 'loading', 'scanning_camera', 'scanning_webview', 'verifying', 'result' app_state: 'loading', // 'loading', 'scanning_camera', 'scanning_webview', 'verifying', 'result'
scan_mode: 'unknown', // 'camera', 'webview' scan_mode: 'unknown', // 'camera', 'webview'
force_camera: false // Override web-view rule, force native camera no_web_view: false // Override web-view rule, force native camera
}, },
/** /**
@ -57,11 +96,11 @@ Page({
console.log('QR Scanner module loaded', options); console.log('QR Scanner module loaded', options);
// Store query parameters // Store query parameters
const force_camera = options.force_camera === '1' || options.force_camera === 'true'; const no_web_view = options.no_web_view === '1' || options.no_web_view === 'true';
this.setData({ this.setData({
return_page: options.return_page || '', return_page: options.return_page || '',
force_camera: force_camera no_web_view: no_web_view
}); });
// Initialize image data storage // Initialize image data storage
@ -74,6 +113,9 @@ Page({
// Get system information // Get system information
this.initializeSystem(enable_debug); this.initializeSystem(enable_debug);
// Initialize QRTool WASM
this.initializeQRTool();
// Fetch IP address and tenant info, then load camera rules // Fetch IP address and tenant info, then load camera rules
this.fetchRealIP(); this.fetchRealIP();
this.fetchTenantID(); this.fetchTenantID();
@ -97,6 +139,25 @@ Page({
this.setData({ 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 * Initialize system information and device detection
*/ */
@ -121,8 +182,8 @@ Page({
get_camera_rule(null, (rule) => { get_camera_rule(null, (rule) => {
console.log('Camera rule loaded:', rule); console.log('Camera rule loaded:', rule);
// Check for force_camera override // Check for no_web_view override
const should_use_webview = rule.web_view && !this.data.force_camera; const should_use_webview = rule.web_view && !this.data.no_web_view;
let emblem_camera_url = null; let emblem_camera_url = null;
// Set up web-view URL if needed // Set up web-view URL if needed
@ -130,11 +191,7 @@ Page({
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); 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}`); this.addDebugMessage(`Using web-view camera: ${emblem_camera_url}`);
} else { } else {
if (this.data.force_camera && rule.web_view) { this.addDebugMessage('Using native camera with local WASM processing');
this.addDebugMessage('Forcing native camera (override web-view rule)');
} else {
this.addDebugMessage('Using native WeChat camera');
}
} }
this.setData({ this.setData({
@ -147,7 +204,7 @@ Page({
}); });
// Add rule info to debug messages // Add rule info to debug messages
this.addDebugMessage(`Camera rule: zoom=${rule.zoom}, web_view=${rule.web_view}${this.data.force_camera ? ' (FORCED_CAMERA)' : ''}`); 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 // Transition to appropriate scanning state
if (should_use_webview) { if (should_use_webview) {
@ -164,7 +221,7 @@ Page({
onCameraReady(e) { onCameraReady(e) {
console.log('Camera ready', e); console.log('Camera ready', e);
this.camera_context = wx.createCameraContext(); this.camera_context = wx.createCameraContext();
this.addDebugMessage('Camera initialized in scanCode mode'); this.addDebugMessage('Camera initialized for WASM processing');
// State transition is handled in loadCameraRules // State transition is handled in loadCameraRules
}, },
@ -273,6 +330,134 @@ Page({
}); });
}, },
/**
* 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) * State: Loading -> Scanning (Web-view)
*/ */
@ -327,11 +512,17 @@ Page({
show_modal: '', show_modal: '',
hint_text: '初始化相机...', hint_text: '初始化相机...',
busy: true, busy: true,
qr_position: null, // Reset frame processing statistics
qr_sharpness: 0, frames_processed: 0,
qr_size: 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 // Reload camera rules to restart the flow
this.loadCameraRules(); this.loadCameraRules();
}, },
@ -387,5 +578,29 @@ Page({
return is_emblem_qr_pattern(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');
}
}
}); });

View File

@ -30,6 +30,7 @@
<!-- WeChat native camera --> <!-- WeChat native camera -->
<camera class="camera" <camera class="camera"
flash="{{ camera_flash }}" flash="{{ camera_flash }}"
frame-size="medium"
bindready="onCameraReady" bindready="onCameraReady"
binderror="onCameraError"> binderror="onCameraError">
</camera> </camera>
@ -43,19 +44,61 @@
</web-view> </web-view>
</block> </block>
<!-- Debug overlay (available in all states) --> <!-- Debug overlay (available in all states) -->
<view class="debug" wx:if="{{ enable_debug }}"> <view class="debug" wx:if="{{ enable_debug }}">
<view><image src="{{ debug_image_data_url }}"></image></view> <view><image src="{{ debug_image_data_url }}"></image></view>
<view wx:for="{{ debug_msgs }}">{{ item }}</view> <view class="debug-messages">
<view>state: {{ app_state }} ({{ scan_mode }})</view> <text wx:for="{{ debug_msgs }}" class="debug-msg">{{ item }}</text>
<view>model: {{ phone_model }}</view> </view>
<view>zoom: {{ zoom }} (rule: {{ rule_zoom }})</view> <view class="debug-items">
<view>camera rule: {{ camera_rule.model || 'default' }} (web_view: {{ use_web_view }})</view> <view class="debug-item">
<view wx:if="{{ force_camera }}">FORCE_CAMERA: enabled</view> <text class="debug-label">state:</text>
<view>sensitivity: {{ camera_sensitivity }}</view> <text class="debug-value">{{ app_state }}</text>
<view>max zoom: {{ max_zoom }}</view> </view>
<view>result: {{ result }}</view> <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>
<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>
</view>
<view class="debug-item">
<text class="debug-label">sensitivity:</text>
<text class="debug-value">{{ camera_sensitivity }}</text>
</view>
<view class="debug-item">
<text class="debug-label">frames:</text>
<text class="debug-value">{{ frames_processed }}/{{ frames_processed + frames_skipped }}</text>
</view>
<view class="debug-item">
<text class="debug-label">skipped:</text>
<text class="debug-value">{{ 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>
</view>
<view class="debug-item">
<text class="debug-label">last:</text>
<text class="debug-value">{{ last_frame_time.toFixed(2) }}ms</text>
</view>
<view class="debug-item" wx:if="{{ result }}">
<text class="debug-label">result:</text>
<text class="debug-value">{{ result }}</text>
</view>
</view>
</view> </view>
<!-- Bottom action controls (only for camera scanning) --> <!-- Bottom action controls (only for camera scanning) -->

View File

@ -186,29 +186,86 @@ view.brighter {
/* Debug overlay */ /* Debug overlay */
view.debug { view.debug {
position: absolute; position: absolute;
width: 80%; width: 94%;
max-height: 30vh; max-height: 30vh;
bottom: 240rpx; bottom: 240rpx;
left: 10px; left: 50%;
transform: translateX(-50%);
padding: 0.3rem; padding: 0.3rem;
border: 1px solid yellow; border: 1px solid rgba(239, 72, 35, 0.8);
border-radius: 3px; border-radius: 3px;
color: yellow; color: rgba(239, 72, 35, 0.8);
background-color: rgba(100, 100, 100, 0.5); background-color: rgba(100, 100, 100, 0.5);
z-index: 1000; z-index: 1000;
font-size: 13px; font-size: 10px;
word-break: break-all;
overflow-y: auto; overflow-y: auto;
opacity: 0.5; opacity: 0.75;
display: flex;
flex-direction: column;
} }
view.debug image { view.debug image {
position: fixed;
right: 10px; right: 10px;
top: 10px; top: 10px;
width: 64px; width: 64px;
height: 64px; height: 64px;
border: 1px solid green; border: 1px solid rgba(239, 72, 35, 0.8);
}
/* Debug messages section */
.debug-messages {
margin-bottom: 4px;
}
.debug-msg {
display: block;
margin: 1px 0;
line-height: 1.2;
color: #ffffff;
}
/* Debug items container */
.debug-items {
display: flex;
flex-wrap: wrap;
gap: 2px;
align-items: flex-start;
}
/* Individual debug item boxes */
.debug-item {
display: inline-flex;
align-items: center;
gap: 2px;
margin: 0;
padding: 1px 3px;
border: 1px solid rgba(239, 72, 35, 0.5);
border-radius: 2px;
background-color: rgba(0, 0, 0, 0.2);
line-height: 1.2;
white-space: nowrap;
flex-shrink: 0;
}
.debug-label {
color: #ffffff;
font-weight: bold;
}
.debug-value {
color: #99ff99;
word-break: break-word;
}
/* Special styling for no_web_view flag box */
.debug-item.debug-flag-box {
border: 1px solid rgba(255, 0, 0, 0.6);
background-color: rgba(255, 0, 0, 0.1);
}
.debug-flag {
color: #ff9999;
font-weight: bold;
} }
/* Tooltip */ /* Tooltip */

View File

@ -0,0 +1,186 @@
// QR Processing Module for EmblemScanner
// Self-contained WASM-based QR code processing
var qrtool = null;
var qrtool_ready = false;
/**
* Load qrtool WASM module
*/
function load_qrtool() {
var m = require('./qrtool.wx.js');
m.onRuntimeInitialized = () => {
console.log("QRTool runtime initialized");
qrtool_ready = true;
qrtool = m;
}
}
/**
* Check if qrtool is ready for processing
*/
function is_qrtool_ready() {
return qrtool_ready;
}
/**
* Process camera frame for QR code detection
*/
function process_frame(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);
// 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]
);
// Clean up buffer
qrtool._free(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 || '')
};
} 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
*/
const offscreenCanvas = wx.createOffscreenCanvas({
type: '2d',
width: 100,
height: 100,
});
/**
* Convert raw frame data to data URL for debug visualization
*/
function data_url_from_frame(width, height, image_data) {
offscreenCanvas.width = width;
offscreenCanvas.height = height;
var ctx = offscreenCanvas.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);
}
/**
* Check if QR code matches Emblem pattern
*/
function is_emblem_qr_pattern(p) {
if (!p) return false;
if (p.search(/code=[0-9a-zA-Z]+/) >= 0) return true;
if (p.search(/id=[0-9a-zA-Z]+/) >= 0) return true;
if (p.search(/c=[0-9a-zA-Z]+/) >= 0) return true;
if (p.search(/https:\/\/xy\.ltd\/v\/[0-9a-zA-Z]+/) >= 0) return true;
return false;
}
/**
* Generate hint text based on QR processing result
*/
function make_hint_text(result) {
if (!result) {
return "查找二维码";
}
if (result.qrcode && result.qrcode.length > 0) {
if (!result.valid_pattern) {
return "无效编码";
}
if (result.ok) {
return "识别成功";
}
// Check specific error conditions
var err = result.err || "";
if (err.includes("margin too small")) {
return "对齐定位点";
} else if (err.includes("energy check failed") || err.includes("cannot detect angle")) {
return "移近一点";
}
}
return "对齐定位点";
}
module.exports = {
load_qrtool,
is_qrtool_ready,
process_frame,
process_frame_with_debug,
data_url_from_frame,
is_emblem_qr_pattern,
make_hint_text
};

File diff suppressed because it is too large Load Diff

View File

@ -38,10 +38,11 @@
"useStaticServer": true, "useStaticServer": true,
"ignoreUploadUnusedFiles": true, "ignoreUploadUnusedFiles": true,
"condition": false, "condition": false,
"compileWorklet": false, "compileWorklet": true,
"localPlugins": false, "localPlugins": false,
"swc": false, "swc": false,
"disableSWC": true "disableSWC": true,
"experimentalWorker": true
}, },
"compileType": "miniprogram", "compileType": "miniprogram",
"condition": {}, "condition": {},

View File

@ -3,16 +3,16 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "emblemscanner", "name": "emblemscanner force camera",
"pathName": "pages/emblemscanner/emblemscanner", "pathName": "pages/emblemscanner/emblemscanner",
"query": "debug=1&return_page=/pages/test_result_page/test_result_page", "query": "debug=1&no_web_view=1",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{ {
"name": "emblemscanner force camera", "name": "emblemscanner",
"pathName": "pages/emblemscanner/emblemscanner", "pathName": "pages/emblemscanner/emblemscanner",
"query": "debug=1&force_camera=1", "query": "debug=1&return_page=/pages/test_result_page/test_result_page",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
@ -22,13 +22,6 @@
"query": "q=https%3A%2F%2Fthemblem.com%2Fapi%2Fmini-prog-entry%2F%3Fcode%3D1279885739283%0A", "query": "q=https%3A%2F%2Fthemblem.com%2Fapi%2Fmini-prog-entry%2F%3Fcode%3D1279885739283%0A",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
},
{
"name": "pages/camera/camera",
"pathName": "pages/camera/camera",
"query": "",
"launchMode": "default",
"scene": null
} }
] ]
} }