2025-03-01 13:36:26 +00:00

495 lines
13 KiB
JavaScript

// pages/camera.js
//
import {
upload_image_data_url,
check_auto_torch,
} from '../../upload.js'
import {
frame_pre_check,
data_url_from_frame,
load_qrtool,
is_emblem_qr_pattern,
} from '../../precheck.js'
import {
get_camera_rule,
get_system_info,
} from '../../utils.js'
import { debug, info, error } from '../../log.js'
Page({
/**
* Page initial data
*/
data: {
hint_text: '查找二维码',
enable_debug: false,
debug_countdown: 10,
camera_flash: "off",
camoverlay_grayscale: 0,
phone_model: "unknown",
progress: 0,
zoom: -1,
rule_zoom: -1,
max_zoom: 1,
use_worker: false,
show_tip: false,
show_modal: '',
busy: true,
logs: [],
should_check_auto_torch: true,
done_checking_auto_torch: false,
camera_sensitivity: 1,
frame_uploaded: 0,
last_frame_upload_time: 0,
frame_upload_seq_num: 0,
qrarc_class: "sm",
qrmarkers_class: "hidden",
frame_upload_in_flight: 0,
frame_upload_time_cost: 0,
frame_upload_interval_ms: 2000,
},
make_hint_text(r) {
var qr_is_valid = false;
if (r.qrcode && r.qrcode.length > 0) {
qr_is_valid = is_emblem_qr_pattern(r.qrcode);
if (!qr_is_valid) {
return "无效编码";
}
}
if (qr_is_valid) {
if (this.on_qr_found) {
this.on_qr_found();
this.on_qr_found = null;
}
var err = r.err || "";
if (err.includes("margin too small")) {
return "对齐定位点";
} else if (err.includes("energy check failed") || err.includes("cannot detect angle")) {
return "移近一点";
}
return "对齐定位点";
}
return "查找二维码";
},
log(...what) {
console.log(...what);
this.setData({
logs: this.data.logs.concat([new Date() + ": " + what.join(" ")])
});
},
onLoad(options) {
console.log("camera page load", options);
this.setData({
ai_chat_mode: options.ai_chat_mode,
});
var new_session_id = Date.now();
getApp().globalData.session_id = new_session_id;
if (options.env == 'dev') {
console.log("Using dev env settings.");
getApp().globalData.server_url = 'https://dev.themblem.com';
}
this.log("camera page load (build240622.2120)");
get_camera_rule(null, () => {});
this.log("options", options);
options = options || {};
if (options.debug || options.scene == 'debug') {
getApp().globalData.debug = true
}
var enable_debug = getApp().globalData.debug;
const si = get_system_info();
const phone_model = si.model;
this.log("phone model: " + phone_model);
getApp().globalData.phone_model = phone_model;
this.log("window width", si.windowWidth, "height", si.windowHeight);
const use_worker = phone_model.toLowerCase().includes("iphone");
this.setData({
enable_debug,
phone_model,
window_width: si.windowWidth,
window_height: si.windowHeight,
use_worker,
});
if (use_worker) {
this.worker = getApp().globalData.worker;
this.worker.onMessage((msg) => {
if (msg.type == "result") {
var res = msg.res;
this.setData({
processing_time: Date.now() - this.processing_begin,
result: JSON.stringify(res),
});
if (this.data.should_check_auto_torch && is_emblem_qr_pattern(res.qrcode)) {
this.start_check_auto_torch(res.qrcode);
}
if (this.data.done_checking_auto_torch && res.ok && res.qrcode.length && is_emblem_qr_pattern(res.qrcode)) {
this.pending_hint_text = null;
this.setData({
hint_text: "识别成功",
show_modal: "verifying",
});
this.fetch_from_worker();
} else {
this.pending_hint_text = this.make_hint_text(res);
this.reset_busy();
}
} else if (this.data.done_checking_auto_torch && msg.type == "submit") {
this.log("got submit");
var res = msg.res;
const image_data = msg.image_data;
var uca = new Uint8ClampedArray(image_data.data);
var data_url = data_url_from_frame(image_data.width, image_data.height, uca);
this.submit_image(data_url, res.qrcode, res.angle);
}
});
} else {
load_qrtool();
}
},
start_check_auto_torch(qrcode) {
this.setData({
should_check_auto_torch: false,
});
check_auto_torch(qrcode, (auto_torch, camera_sensitivity) => {
this.log("check_auto_torch cb", auto_torch);
if (auto_torch) {
this.torch_on();
}
setTimeout(() => {
this.setData({
done_checking_auto_torch: true,
camera_sensitivity: camera_sensitivity || 1,
});
}, 300);
}
);
},
fetch_from_worker() {
// send the signal message to worker to fetch the image data
this.log("fetch_from_worker");
this.worker.postMessage({
type: "ready_to_submit",
});
},
onShow() {
this.log("camera page show");
this.setup_camera();
this.setData({
show_tip: false,
})
this.reset_busy();
this.hint_interval = setInterval(() => {
this.update_hint_text();
}, 1000);
this.setup_tooltip_timer();
},
update_hint_text() {
if (this.pending_hint_text) {
this.setData({
hint_text: this.pending_hint_text,
});
this.pending_hint_text = null;
}
},
setup_tooltip_timer() {
if (this.tooltip_timer) {
clearTimeout(this.tooltip_timer);
this.tooltip_timer = null;
}
this.tooltip_timer = setTimeout(() => {
if (this.data.show_modal != '') return;
wx.vibrateShort({ type: "heavy", });
this.setData({
show_tip: true,
});
}, 15000);
},
clean_up() {
clearTimeout(this.tooltip_timer);
clearTimeout(this.hint_interval);
this.listener.stop();
this.listener = null;
},
onHide() {
this.busy = true;
this.clean_up();
},
onUnload() {
this.clean_up();
},
submit_image(data_url, qr_code, angle) {
this.log("submit image", qr_code, angle);
this.busy = true;
var begin = Date.now();
wx.vibrateShort({ type: "heavy", });
var gd = getApp().globalData;
gd.image_data_url = data_url;
gd.qr_code = qr_code;
gd.angle = angle;
this.log("uploading image");
const fail = (e) => {
info("upload failed, goto not found: " + JSON.stringify(e));
this.show_verifyfailed();
};
const success = (res) => {
info("upload success, code", res.statusCode);
this.log("upload success, code", res.statusCode);
if (res.statusCode == 200) {
var resp;
if (typeof res.data == "string") {
resp = JSON.parse(res.data);
} else {
resp = res.data;
}
this.log(resp);
info("resp", resp);
gd.verify_resp = resp;
if (resp.serial_code) {
// Let the first part of the loading animation more "noticable" and
// the ui experience less "jumpy"
var delay = 3000 - (Date.now() - begin);
setTimeout(() => {
this.show_result_page(resp.serial_code);
}, delay > 0 ? delay : 0);
} else if (gd.debug && resp.info == "ok") {
// this is from the debug backend sigifying the upload has succeeded
// we have no product info to show here because "ok" is the only returned information
// just show a temporary page
info("redirecting to debuguploaded");
wx.redirectTo({ url: '/pages/debuguploaded/debuguploaded' })
} else {
this.show_verifyfailed();
}
} else {
info("invalid status code");
this.show_verifyfailed();
}
};
upload_image_data_url(data_url, success, fail, this.data.logs.join("\n"));
},
show_result_page(serial_code) {
this.busy = true;
if (this.data.ai_chat_mode) {
wx.redirectTo({
url: '/pages/chat/chat?serial_code=' + serial_code,
});
} else {
wx.redirectTo({
url: '/pages/productinfo/productinfo?serial_code=' + serial_code,
});
}
},
show_verifyfailed() {
this.busy = true;
this.setData({
show_modal: "verifyfailed",
});
},
show_service() {
this.busy = true;
this.setData({
show_modal: "service",
});
},
show_scanguide() {
this.busy = true;
this.setData({
show_modal: "scanguide",
});
},
restart_camera() {
wx.redirectTo({
url: '/pages/camera/camera',
})
},
reset_busy() {
setTimeout(() => {
this.busy = false;
}, 20)
},
setup_camera: function (opts) {
var max_zoom = null;
if (opts) {
console.log(opts);
max_zoom = opts.detail.maxZoom;
this.setData({
max_zoom,
});
}
get_camera_rule(max_zoom, (rule) => {
var zoom = rule.zoom;
var initial_zoom = 2;
this.setData({
zoom: initial_zoom,
rule_zoom: zoom,
});
const ctx = wx.createCameraContext();
this.log(`camera set initial zoom to ${initial_zoom}x, will zoom in to ${rule.zoom}x when qr is found`);
ctx.setZoom({ zoom: initial_zoom });
this.on_qr_found = () => {
this.log(`qr found, zoom to ${rule.zoom}x`);
ctx.setZoom({ zoom: rule.zoom });
this.setData({
zoom: rule.zoom,
qrmarkers_class: "",
qrarc_class: "lg"
});
}
if (!this.listener) {
this.log("creating camera frame listener...");
const listener = ctx.onCameraFrame((res) => { this.handle_frame(res) });
this.listener = listener;
}
const worker = getApp().globalData.worker;
this.listener.start({
worker,
});
});
},
handle_frame(res) {
if (this.failed) return;
try {
this.do_handle_frame(res);
} catch (e) {
console.log(e);
this.failed = true;
}
},
handle_image_data_url(data_url) {
const now = Date.now();
if (
now - this.data.last_frame_upload_time < this.data.frame_upload_interval_ms ||
this.data.frame_upload_in_flight > 0
) {
return;
}
var begin = Date.now();
this.setData({
last_frame_upload_time: now,
frame_upload_seq_num: this.data.frame_upload_seq_num + 1,
frame_upload_in_flight: this.data.frame_upload_in_flight + 1,
});
var seq_num = this.data.frame_upload_seq_num.toString().padStart(3, '0');
var fd = {
session_id: getApp().globalData.session_id,
phone_model: getApp().globalData.phone_model,
seq_num: seq_num,
image_data_url: data_url,
}
wx.request({
url: "https://research.themblem.com/event/camera-frame",
method: "POST",
data: JSON.stringify(fd),
header: {
"Content-Type": "application/json",
},
success: (res) => {
this.setData({
frame_uploaded: this.data.frame_uploaded + 1,
frame_upload_in_flight: this.data.frame_upload_in_flight - 1,
frame_upload_time_cost: Date.now() - begin,
frame_upload_interval_ms: this.data.frame_upload_interval_ms * 2,
});
},
fail: (e) => {
this.log("frame upload failed", e);
this.setData({
frame_upload_in_flight: this.data.frame_upload_in_flight - 1,
frame_upload_time_cost: Date.now() - begin,
frame_upload_interval_ms: this.data.frame_upload_interval_ms * 2,
});
},
});
},
do_handle_frame(res) {
if (this.busy) return;
this.busy = true;
if (this.data.use_worker) {
this.processing_begin = Date.now();
console.log("postMessage frame");
this.worker.postMessage({
type: 'frame',
width: res.width,
height: res.height,
camera_sensitivity: this.data.camera_sensitivity,
});
} else {
/* Make sure we are copying the frame data to avoid TOCTOU race condition
* with camera event (update of res.data while we're working) */
var uca1 = new Uint8ClampedArray(res.data);
var uca = new Uint8ClampedArray(uca1);
const data_url = data_url_from_frame(res.width, res.height, uca);
var r = frame_pre_check(res.width, res.height, uca, this.data.camera_sensitivity);
const result = r.result;
this.log("frame_pre_check: ", JSON.stringify(result));
this.setData({
result: JSON.stringify(r.result),
debug_image_data_url: r.data_url,
});
this.handle_image_data_url(data_url);
if (this.data.done_checking_auto_torch && result.ok && result.angle >= 0 && result.qrcode.length && is_emblem_qr_pattern(result.qrcode)) {
this.setData({
show_modal: "verifying",
});
this.submit_image(data_url, result.qrcode, result.angle);
} else {
if (this.data.should_check_auto_torch && is_emblem_qr_pattern(result.qrcode)) {
this.start_check_auto_torch(result.qrcode);
}
this.pending_hint_text = this.make_hint_text(result);
this.reset_busy();
}
}
},
torch_on() {
this.log("torch on");
this.setData({
camera_flash: "torch",
});
},
toggle_torch() {
this.log("toggle torch");
var cf = this.data.camera_flash == "torch" ? "off" : "torch";
this.setData({
camera_flash: cf,
});
},
debug_tap() {
if (this.data.enable_debug) {
return;
}
this.data.debug_countdown -= 1;
if (this.data.debug_countdown <= 0) {
this.log("enabling debug");
this.setData({
enable_debug: true,
})
}
},
})