QR-Finder
Python-bibliotek der lader applikationer sætte fysiske genstande i kø til lokalisering via QR-kode-mærkater. Mobil browser-app scanner live med jsQR, bipper og vibrerer ved fund og fryser billedet til bekræftelse. Bruges af Archive3D og ArchivePCB.
Selvinitieret, selvhostet projekt. Bygget og drevet af mig som arkitekt, produktejer og reviewer, med AI som superviseret "lærling" i udviklingsprocessen.
DEL 1: Resume
QR-Finder er et lille Python-bibliotek der lader en anden applikation — Archive3D, ArchivePCB — sætte en fysisk genstand i kø til at en person skal finde den, med intet andet end en QR-mærkat og en telefons browser. Biblioteket indlejrer sin egen FastAPI-server med et selvgenereret TLS-certifikat; en mobil browser-app scanner løbende med telefonens kamera (jsQR, fuldt offline-kapabel når den først er loadet), bipper og vibrerer når målet kommer i syne, fryser billedet i tre sekunder så brugeren faktisk kan nå at læse det, og lader enhver tilsluttet telefon kvittere og rykke en fælles, server-side kø videre. Ingen app-installation, bare en URL. Det kører nu som en versioneret intern pakke og er allerede koblet ind i to andre live projekter.
Teknologistak
- Server: Python, FastAPI + uvicorn, kan indlejres (
get_fastapi_app()) eller køre standalone (start_background()) - TLS: selvgenereret selvsigneret certifikat ved første start, da kameraadgang kræver HTTPS
- Klient: vanilla JavaScript, jsQR til scanning i browseren, Web Audio API til bippet,
navigator.vibrate()til haptik - Distribution: publiceret til et privat PyPI-indeks på Gitea,
uv add qr-finder --index-url ... - Payload: en QR-kode der koder intet andet end et 16-tegns hex-hash —
sha256(f"{app}:{product_id}")[:16]
Største resultat
At designe mærkaten så den aldrig bliver forældet. QR-koden på en fysisk genstand koder kun et hash afledt af app + product_id — ikke produktnavnet, ikke et link, intet der kunne ændre sig — mens alle de faktiske metadata (navn, link, beskrivelse) ligger server-side og returneres friskt ved hvert poll. Omdøb delen, ændr dens beskrivelse, flyt den til en anden hylde-sporingsflow: mærkaten printet måneder tidligere virker stadig, fordi den aldrig bar information der kunne blive forældet. Kombineret med en server-side kø der holder al state, så telefonerne ikke behøver, kan et vilkårligt antal enheder søge samtidigt uden noget der kan komme ud af sync mellem dem.
DEL 2 — Dybdegående baggrund
Problemet
At finde én specifik fysisk genstand — en modstandsrulle, en filamentspole, et PCB — blandt mange opbevarede eller arkiverede er en langsom, manuel søgning selv når et system allerede ved præcis hvor den burde være. Gabet er ikke at vide hvilken genstand der ønskes; et system som Archive3D eller ArchivePCB ved allerede det. Gabet er at få en person, der står foran en hylde med ens-udseende kasser, hurtigt peget hen på den rigtige — uden at tvinge dem til at installere en app til noget de gør i tredive sekunder et par gange om ugen.
Arkitektur og løsning
Overordnet form. Et lille Python-bibliotek der pakker en server ind, med en selvstændig JS-klient den server'er:
| Komponent | Rolle |
|---|---|
generator.py | Bygger den minimale QR-PNG (version 2, 25×25) ud fra det afledte hash |
queue.py | Server-side FIFO-kø — den ene sandhedskilde for det aktuelle mål |
server.py | FastAPI-ruter: GET/POST /api/target, POST /api/acknowledge, server'er scanner-appen |
tls.py | Selvsigneret certifikat-generering ved første kørsel |
static/index.html | Den mobile scanner-app — kameravisning, jsQR-loop, tilstandsmaskine, sanse-feedback |
Kommunikationsmodel. Telefonen poller GET /api/target hvert 1–2 sekund og sammenligner det returnerede hash med hvad den sidst viste — den holder ingen kø-state selv. Enhver tilsluttet enhed kan POST /api/acknowledge, hvilket dequeuer det aktuelle emne server-side; alle andre pollende enheder opfanger det nye mål ved deres næste poll. Der er bevidst ingen websocket, ingen push-kanal og ingen per-enhed-session — polling plus en hash-sammenligning er hele synkroniseringsmekanismen, og det er nok.
Designbeslutninger værd at fremhæve
- Hashet afledes, vælges aldrig af kalderen. Både
generate_qr()ogenqueue()beregnersha256(f"{app}:{product_id}")[:16]på samme måde internt, så mærkaten og køens post aldrig kan drive fra hinanden fordi en kalder sender en lidt anden streng på hver side. - Fire tilstandsmaskine-tilstande, ikke to. Klienten modellerer Idle / Searching / Found / Offline eksplicit — Offline beholder det sidst kendte mål og fortsætter scanningen gennem N på hinanden følgende fejlede polls i stedet for at behandle en droppet forbindelse som en hård fejl, så et kort wifi-hik aldrig afbryder en igangværende søgning.
- Haptik er en tilføjelse, ikke en afhængighed.
navigator.vibrate()findes ikke i iOS Safari; bippet (Web Audio API) og det visuelle overlay virker alle steder, så den feedback der betyder mest overlever på enhver platform, og den der ikke gør, behandles som en bonus, ikke en afhængighed. - TLS håndteres så integratoren aldrig behøver det. Kameraadgang kræver HTTPS; i stedet for at dokumentere det krav og overlade det til hver kalder, genererer biblioteket sit eget selvsignerede certifikat ved første start og dokumenterer i stedet den engangs-browseradvarsel.
- Det offentlige API blev forenklet under reel brug, ikke i det abstrakte. Ni commits på én dag flyttede fra et oprindeligt hash-håndterings-API til ét hvor
generate_qr()ogenqueue()afleder hashet automatisk — en ændring drevet af faktisk at koble biblioteket ind i Archive3D og ArchivePCB og mærke friktionen, ikke af spekulation.
AI som lærling
AI inde i produktet. Ingen. QR-afkodning, hash-afledning og kø-state er alle deterministiske; der er intet her der har gavn af en models vurdering.
AI i udviklingsprocessen (human-in-the-loop). Specification.md — arkitektur, det fulde HTTP-API, tilstandsmaskinen, mapping af sanse-feedback, og kendte begrænsninger — blev skrevet før implementeringen og er det dokument koden tjekkes imod. En AI-kodeassistent implementerede mod det; jeg reviewede specifikt API-fladen, da dette bibliotek bruges af to andre projekter jeg også ejer, og en breaking change der har en reel blast radius.
Lederens refleksion
- Design payloaden til datas faktiske levetid, ikke systemets nuværende tilstand. En mærkat overlever de metadata den er tilknyttet; at kode kun et stabilt hash og holde alt andet foranderligt server-side var beslutningen der gjorde det sandt af konstruktion, ikke af disciplin.
- Modellér fejltilstanden eksplicit, lim den ikke på bagefter. At "Offline" er en førsteklasses tilstand i klientens tilstandsmaskine, ikke et try/catch omkring et poll, er hvorfor en ustabil forbindelse degraderer nådigt i stedet for at gå i stykker.
- Lad reel integration drive API-forenkling, gæt ikke på den på forhånd. Hash-aflednings-API'et blev først simpelt efter at være brugt af to faktiske kaldere på samme dag — friktionen var reel, ikke hypotetisk.
- En afhængighed der spænder over flere projekter fortjener mere review, ikke mindre. Fordi dette bibliotek ligger under to andre systemer jeg vedligeholder, behandlede jeg dets offentlige API-flade som den del af reviewet med højest indsats, selvom kodebasen i sig selv er lille.
- Løs deployment-irritationen inde i biblioteket, ikke i dets dokumentation. At autogenerere TLS-certifikatet i stedet for at skrive "du skal bruge HTTPS, held og lykke" gjorde et tilbagevendende opsætningsproblem til en engangs-browser-prompt.
Ærlige begrænsninger. Bygget på én dag (ni commits, 23. maj 2026), uden automatiseret testsuite — examples/basic_usage.py er p.t. det nærmeste der findes på én. To reelle platformsbegrænsninger findes frem for at være implementeringsgæld: navigator.vibrate() har ingen iOS Safari-understøttelse, og kameraadgang kræver HTTPS eller localhost, så enhver enhed skal acceptere en selvsigneret certifikat-advarsel én gang. Det kører dog allerede inde i to andre live projekter frem for at ligge utestet — reel nedstrøms-brug er det der har drevet API'et ned til dets nuværende, enklere form.