emblemscanner wip
This commit is contained in:
parent
78a48c6cb0
commit
431f81faad
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
scanner/pages/emblemscanner/assets/qrtool.wx.wasm.br
Executable file
BIN
scanner/pages/emblemscanner/assets/qrtool.wx.wasm.br
Executable file
Binary file not shown.
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -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) -->
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
186
scanner/pages/emblemscanner/qrprocessor.js
Normal file
186
scanner/pages/emblemscanner/qrprocessor.js
Normal 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
|
||||||
|
};
|
||||||
5580
scanner/pages/emblemscanner/qrtool.wx.js
Normal file
5580
scanner/pages/emblemscanner/qrtool.wx.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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": {},
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user