// 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, send_event, } 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_failed: 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, }); 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 event_data = { session_id: getApp().globalData.session_id, phone_model: getApp().globalData.phone_model, seq_num: seq_num, image_data_url: data_url, } var success_cb = (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, }); } var fail_cb = (e) => { this.log("frame upload failed", e); this.setData({ frame_upload_failed: this.data.frame_upload_failed + 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, }); } send_event("camera-frame", event_data, success_cb, fail_cb); }, 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, }) } }, })