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); };