From 8e3169e49b2ede369e757c65d113d37066035d2d Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 25 Apr 2026 13:46:47 -0400 Subject: [PATCH] =?UTF-8?q?fix(shopfloor):=20jsQR=20loop=20=E2=80=94=20ful?= =?UTF-8?q?l-res=20frame=20+=20canvas=20blank-pixel=20check=20+=20last-res?= =?UTF-8?q?ult=20trace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 271 attempts at 720x1280 with no detection means either downsampling killed the finder patterns or drawImage is silently painting blank pixels (a known iOS WebKit failure mode for some video-stream sources). - Drop the 600px scaling cap. Feed the full native video frame to jsQR. Per-frame cost goes up but is still fine; the win is jsQR sees finder patterns at full sharpness. - Add a one-time sanity check that walks the first ImageData buffer looking for a non-zero pixel. If everything is 0,0,0 the canvas is blank (drawImage failed) and we surface that as 'px:BLANK' in the status line — telling us instantly to switch decoder strategies rather than chasing tuning. - Status line now also shows the last jsQR call outcome: r:found / r:no_code / r:empty / r:error: ... So we can confirm whether jsQR is even being invoked successfully. Status format compacted to fit one line on phone: jsqr · f1234 a567 720x1280 rs4 px:ok r:no_code Version: shopfloor 19.0.20 -> 19.0.21. --- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../static/src/js/qr_scanner.js | 60 ++++++++++++------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index ceab2104..df688423 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.20.0.0', + 'version': '19.0.21.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, ' 'first-piece inspection gates.', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/qr_scanner.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/qr_scanner.js index d6c37b1a..42f8baec 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/qr_scanner.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/qr_scanner.js @@ -211,8 +211,10 @@ export class QrScanner extends Component { let attempts = 0; let lastDecode = 0; let lastStatus = 0; + let lastResult = "—"; // "found" | "no_code" | "empty" | error msg + let firstNonZeroPixel = -1; // sanity check that drawImage works const MIN_INTERVAL_MS = 100; - const STATUS_INTERVAL_MS = 400; + const STATUS_INTERVAL_MS = 500; const tick = (now) => { if (!this.decodeLoopActive || !this.state.open) return; frames++; @@ -226,38 +228,54 @@ export class QrScanner extends Component { try { const w = v.videoWidth; const h = v.videoHeight; - // Cap the working frame at 600px on the long side — - // enough resolution for jsQR to find an ~33-module - // QR, while keeping per-frame cost reasonable. - const scale = Math.min(1, 600 / Math.max(w, h)); - const cw = Math.round(w * scale); - const ch = Math.round(h * scale); - if (this._canvas.width !== cw) this._canvas.width = cw; - if (this._canvas.height !== ch) this._canvas.height = ch; - this._ctx.drawImage(v, 0, 0, cw, ch); - const imageData = this._ctx.getImageData(0, 0, cw, ch); - // attemptBoth tries the image as-is AND inverted — - // covers cases where a camera's auto-exposure makes - // the QR look light-on-dark to the decoder. - const code = window.jsQR(imageData.data, cw, ch, { + // Use the native video resolution directly — no + // downscaling. jsQR's runtime cost is acceptable + // even at 1080p, and downsampling can blur the + // finder patterns just enough to defeat detection + // when the QR is small in the frame. + if (this._canvas.width !== w) this._canvas.width = w; + if (this._canvas.height !== h) this._canvas.height = h; + this._ctx.drawImage(v, 0, 0, w, h); + const imageData = this._ctx.getImageData(0, 0, w, h); + if (firstNonZeroPixel < 0) { + // One-time sanity check: confirm drawImage is + // actually painting the video onto the canvas. + // If every pixel is 0,0,0 we'd never decode + // anything regardless of jsQR settings (this + // can happen with tainted canvases on some + // older WebKit builds). + for (let i = 0; i < imageData.data.length; i += 4) { + if (imageData.data[i] | imageData.data[i + 1] | imageData.data[i + 2]) { + firstNonZeroPixel = i; + break; + } + } + if (firstNonZeroPixel < 0) firstNonZeroPixel = -2; + } + const code = window.jsQR(imageData.data, w, h, { inversionAttempts: "attemptBoth", }); if (code && code.data) { + lastResult = "found"; this._handleCode(code.data); return; } + lastResult = code ? "empty" : "no_code"; } catch (e) { - this.state.statusLine = "Decoder: jsqr — error: " + - (e.message || String(e)).slice(0, 80); + lastResult = "error: " + + (e.message || String(e)).slice(0, 60); } } if (now - lastStatus > STATUS_INTERVAL_MS) { lastStatus = now; this.state.statusLine = - "Decoder: jsqr · frames " + frames + - " · attempts " + attempts + - " · video " + (v.videoWidth || 0) + "x" + (v.videoHeight || 0) + - " rs" + v.readyState; + "jsqr · f" + frames + + " a" + attempts + + " " + (v.videoWidth || 0) + "x" + (v.videoHeight || 0) + + " rs" + v.readyState + + " px:" + (firstNonZeroPixel === -2 + ? "BLANK" : firstNonZeroPixel < 0 ? "?" : "ok") + + " r:" + lastResult; } requestAnimationFrame(tick); };