PHP Classes

File: docs/proposals/MeshcoreQRScanner.md

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   docs/proposals/MeshcoreQRScanner.md   Download  
File: docs/proposals/MeshcoreQRScanner.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 8,232 bytes
 

Contents

Class file image Download

MeshCore QR Code Contact Scanner

> Draft Notice: This proposal is a draft, generated by AI assistance, and may not have been reviewed for accuracy. All implementation details should be validated against the actual codebase before work begins.

Table of Contents

  1. Overview
  2. QR Payload Formats
  3. Library Choice
  4. UX Flow
  5. Implementation Details
  6. Files to Change
  7. Constraints and Notes

Overview

Add a Scan QR button to the MeshCore tab in user settings. Clicking it opens a camera-based QR scanner modal. On a successful scan the code is parsed for a MeshCore contact public key and optional display name, the scanner closes, and the existing Add Radio modal opens pre-filled with the extracted data. The user reviews and saves through the existing flow ? no new backend API is needed.

QR Payload Formats

MeshCore devices share contact info via QR code. The scanner must handle at least these formats, checked in order:

1. MeshCore URI (primary)

meshcore://contact/add?name=claudes.lovelybits.org&public_key=7d8f5034dbec9b6b8986fb8b316363dc54005f257e3f24bdfd68dc2f1b118d3e&type=1

| Parameter | Required | Description | |---|---|---| | public_key | Yes | 64-char lowercase hex public key (pub_key_full) | | name | No | Display name to pre-fill | | type | No | Advertised type integer; stored for future use |

Parse with URL constructor: new URL(raw), check protocol === 'meshcore:' and pathname === '/contact/add' (or equivalent after parsing), then read searchParams.

2. Raw hex key (fallback)

A bare 64-character lowercase hex string with no surrounding structure. Accept this silently with no pre-filled name.

7d8f5034dbec9b6b8986fb8b316363dc54005f257e3f24bdfd68dc2f1b118d3e

3. Unknown format

If neither pattern matches, display the raw QR text in the scanner modal with an error message and a manual-entry prompt. Do not auto-close the scanner ? let the user dismiss it and type the key manually.

Library Choice

jsQR (public_html/js/vendor/jsqr.min.js, ~30 KB gzipped, BSD-2-Clause).

jsQR takes raw ImageData from a canvas and returns the decoded string synchronously. No WebAssembly, no CDN dependency, no opinionated UI.

const code = jsQR(imageData.data, imageData.width, imageData.height);
if (code) { /code.data = decoded string/ }

Alternatives ruled out: - BarcodeDetector (native browser API) ? Chrome/Edge only; no Safari support as of mid-2025. - html5-qrcode ? larger bundle, opinionated camera UI that would need to be overridden. - ZXing-js ? significantly larger bundle than jsQR for equivalent QR-only capability.

UX Flow

[MeshCore tab header]
  [+ Add Radio]  [Scan QR]       ? new button alongside existing one

  ? user clicks Scan QR

[Scanner modal opens]
  <video> ? live camera feed (rear camera preferred via facingMode: 'environment')
  CSS viewfinder overlay (animated corner brackets, no canvas drawing)
  Status: "Point camera at a MeshCore QR code..."
  [Cancel] button stops stream and closes modal

  ? QR code detected

  Camera stream stopped, scanner modal closed
  Add Radio modal opens, pre-filled:
    - Node ID field: public_key from QR
    - Display Name field: name from QR (if present)
  User reviews and clicks Save ? existing POST /api/user/meshcore/contacts

  ? Parse failure (unknown QR format)

  Status line updates: "Unrecognized QR code. Raw text: <...>"
  [Cancel] button still available
  User dismisses and enters key manually

On desktop (no rear camera) facingMode: 'environment' gracefully falls back to whichever camera the browser selects.

Implementation Details

Camera acquisition

const stream = await navigator.mediaDevices.getUserMedia({
    video: { facingMode: { ideal: 'environment' } }
});
videoEl.srcObject = stream;

Use { ideal: 'environment' } (not { exact: ... }) so it falls back on desktop instead of throwing.

Frame scanning loop

Draw frames at ~10 fps via a setTimeout-based loop (not requestAnimationFrame) to keep CPU use low while the scanner is open. Stop the loop and call stream.getTracks().forEach(t => t.stop()) on close or on successful scan.

function tick() {
    ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const code = jsQR(imageData.data, imageData.width, imageData.height);
    if (code) { handleScan(code.data); return; }
    scanTimer = setTimeout(tick, 100);
}

QR parsing

function parseMeshcoreQr(raw) {
    raw = raw.trim();

    // Format 1: meshcore:// URI
    try {
        const url = new URL(raw);
        if (url.protocol === 'meshcore:') {
            const key  = url.searchParams.get('public_key') || '';
            const name = url.searchParams.get('name') || '';
            if (/^[0-9a-f]{64}$/.test(key)) {
                return { key, name };
            }
        }
    } catch (_) {}

    // Format 2: raw 64-char hex
    if (/^[0-9a-f]{64}$/i.test(raw)) {
        return { key: raw.toLowerCase(), name: '' };
    }

    return null; // unknown format
}

Secure context guard

getUserMedia is only available in secure contexts (HTTPS or localhost). If window.isSecureContext is false, hide the Scan QR button and show a tooltip: "QR scanning requires HTTPS". Do not show a broken button.

i18n keys

All new UI strings go through the i18n system. New keys under ui.settings.meshcore.* and errors.meshcore.* in every locale directory under config/i18n/.

Suggested keys:

| Key | English value | |---|---| | ui.settings.meshcore.scan_qr | Scan QR | | ui.settings.meshcore.scan_qr_title | Scan MeshCore QR Code | | ui.settings.meshcore.scan_qr_prompt | Point camera at a MeshCore QR code | | ui.settings.meshcore.scan_qr_success | QR code found! | | ui.settings.meshcore.scan_qr_https_required | QR scanning requires HTTPS | | errors.meshcore.qr_unrecognized | Unrecognized QR code format | | errors.meshcore.qr_camera_denied | Camera access denied |

Files to Change

| File | Change | |---|---| | templates/settings.twig | Add Scan QR button, scanner modal HTML, scanner JS (parseMeshcoreQr, camera loop, modal wiring) | | public_html/js/vendor/jsqr.min.js | New: vendored jsQR library | | public_html/sw.js | Bump CACHE_NAME ? new JS vendor file added | | config/i18n/en/common.php | New ui.settings.meshcore.scan_qr_* keys | | config/i18n/fr/common.php | Same keys, French | | config/i18n/es/common.php | Same keys, Spanish | | config/i18n/de/common.php | Same keys, German | | config/i18n/it/common.php | Same keys, Italian | | config/i18n/en/errors.php | New errors.meshcore.qr_* keys | | config/i18n/fr/errors.php | Same keys, French | | config/i18n/es/errors.php | Same keys, Spanish | | config/i18n/de/errors.php | Same keys, German | | config/i18n/it/errors.php | Same keys, Italian |

No backend changes required. The existing POST /api/user/meshcore/contacts endpoint accepts the parsed public_key as node_id.

Constraints and Notes

  • HTTPS required: `getUserMedia` is a secure context API. The button must be hidden (not broken) on plain HTTP deployments.
  • Mobile rear camera: `facingMode: { ideal: 'environment' }` requests the rear-facing camera on phones without failing on desktop.
  • Stream cleanup: The camera stream must be stopped (`track.stop()`) whenever the modal closes ? whether by Cancel, successful scan, or ESC key. Wire to Bootstrap's `hidden.bs.modal` event to guarantee cleanup.
  • Canvas sizing: Set the hidden canvas to a fixed small resolution (e.g. 640×480) rather than the native camera resolution to keep jsQR decode time under 10ms per frame.
  • QR format confirmation: The `meshcore://contact/add?...` URI scheme was confirmed against a live MeshCore device. If the scheme changes in future MeshCore firmware, only `parseMeshcoreQr()` needs updating.