gzl.dev/blog
field notes, mostly
18 posts
August 27, 2026
7 min
Nine ways to say yes
I taught a Light Phone to speak an Oura ring's protocol over BLE. Then I spent a week failing to tap a button that doesn't exist.
Light PhoneBrightOuraBluetoothAndroid
August 21, 2026
6 min
Field notes on LightOS
What the Light Phone III's OS actually is underneath: Android with the Google removed, hardware that talks in key events, and conventions nobody voted on.
Light PhoneLightOSAndroid
August 21, 2026
1 min
A tape recorder, not a voice recorder
BrightRecorder: clips named for where and when, tapes that are just folders of WAVs, and the wheel finally doing the job it was born for.
Light PhoneBrightRecorder
August 18, 2026
2 min
Why I put an app store on a phone designed to have none
Yes, I see the irony. The argument for building it, why there's no server behind it, and the afternoon I accidentally dropped everyone's apps from the index.
Light PhoneBrightMarket
August 14, 2026
2 min
A good fork says thank you
A lot of what I ship is other people's work with my name nearer the top than it deserves. Some rules I try to fork by.
Light Phoneopen source
August 11, 2026
2 min
My bug reports file themselves
Shake the phone three times and the app files its own GitHub issue. Then an AI triages the queue every hour — under strict instructions that guessing is worse than waiting.
Light PhoneAItooling
August 8, 2026
2 min
Fifteen copies of the same file
The least interesting code I maintain is the code I'd save first in a fire. On finally building the shared library.
Light PhoneKotlin
August 8, 2026
2 min
BrightMarket is out
The app store ships: browse on a desktop, install by QR, track updates on the phone, and get your own app into the catalog.
Light PhoneBrightMarket
August 7, 2026
2 min
One column of scores
Twenty-two leagues from four keyless public APIs, in one grayscale column. How BrightSports gets scores with no account and no push.
Light PhoneBrightSports
August 6, 2026
3 min
The MetroCard inside my phone
I dissolved a transit card in acetone and moved its nervous system into the battery compartment. Tap-to-pay is back, sort of.
Light PhonehardwareNFC
August 6, 2026
2 min
Every button does what you tell it to
The follow-up to the keylayout post: BrightControl itself, why the defaults took longer than the code, and the trust problem with accessibility services.
Light PhoneBrightControldesign
August 5, 2026
1 min
Roll is out
The camera app ships: a filter wheel on real hardware, purikura with four-shot strips, date backs, and a privacy lesson from launch day.
Light PhoneRoll
August 4, 2026
2 min
The keylayout file Light left behind
The scroll wheel is just key events. Everything I learned reading Generic.kl off the phone, and the accessibility service that became BrightControl.
Light PhoneAndroid
August 2, 2026
3 min
Rewriting the camera
Roll's whole design is one gesture, stolen from a contact sheet. Plus the filters that got away from me — purikura on a monochrome phone, and datamoshing experiments that mostly produce mud.
Light Phonedesign
July 29, 2026
1 min
A QR scanner, and its desktop twin
The modern world hands you addresses as QR codes and the Light Phone couldn't read them. A scanner for the phone, a generator page for every other screen.
Light PhoneLightQR
March 22, 2026
1 min
BasilNet, a year later
What broke, what I'd trim, and how the media server quietly became infrastructure for everything else I build.
Dockerhomelabself-hosting
March 8, 2026
1 min
I built a bad DSLR on purpose
It took worse pictures than my phone. It's one of the most useful things I've ever built.
hardwarephotography
January 10, 2025
1 min
Building BasilNet: My Home Server Stack
How I built a 15-service media server with Docker Compose, Gluetun VPN, and Home Assistant integrations.
Dockerhomelabself-hosting
← all posts
July 29, 2026 · 1 min

A QR scanner, and its desktop twin

Light PhoneLightQR

The modern world hands you addresses as QR codes — menus, tickets, Wi-Fi, the poster at the bus stop — and the Light Phone III shipped unable to read a single one of them. The stock camera doesn’t scan. There’s no browser worth typing into even if you could bear typing a URL on a 3.9-inch keyboard. So the codes just sit there, smug.

LightQR is two small tools that meet in the middle:

The scanner is an Android app. CameraX supplies frames, and ZXing decodes them off the luminance plane — a detail that matters here, because ZXing is pure Java. No Google vision services, no Play dependency, which on LightOS isn’t a preference but a requirement: there is nothing else to call. Scan a code, read the text, open the link if it is one. Every scan lands in a local history. No cloud, no account, nothing leaves the phone.

The generator is the desktop twin: a static page at gi-os.github.io/LightQR, served straight from the repo’s docs/ folder by GitHub Pages. Type anything on a machine with a real keyboard, get a code, scan it with the phone. It sounds almost too dumb to build. It’s become the single most-used piece of plumbing in my whole setup, because “get a long string onto a phone with a tiny keyboard” turns out to be a constant of dumbphone life — my voice assistant’s setup literally depends on it for API keys.

There’s no server anywhere in this. The scanner talks to nothing; the generator is a file. I keep rediscovering that the same architecture — a plain APK plus, at most, a static page — covers a remarkable share of what this phone is missing, and every part of it keeps working even if I get hit by the B62.

Update, August 9: the scanner half of this post is now Roll’s QR mode, and this repo is archived. A scan takes two seconds; it never justified its own launcher entry and its own camera bind when the camera app already has a viewfinder open. Moving it also flushed out two bugs worth confessing: I’d been handing ZXing the camera buffer whole, ignoring that camera planes are padded to a hardware-friendly row stride (so the buffer is bigger than the picture — sometimes it decoded a sheared image); and I’d used android.util.Patterns.WEB_URL as a URL test, which happily accepts 1.2, so a code containing a version number offered to open https://1.2. Roll copies the plane row by row, and the URL check is now dependency-free and unit-tested. The generator page is unaffected and lives on.