DownloadMeshCore 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
-
Overview
-
QR Payload Formats
-
Library Choice
-
UX Flow
-
Implementation Details
-
Files to Change
-
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.
|