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
August 4, 2026 · 2 min

The keylayout file Light left behind

Light PhoneAndroid

I want to be clear up front: I didn’t discover anything here. Everything in this post was sitting in a plain text file on the phone, readable by anyone with adb and some patience. I just seem to be the one who wrote it down.

The mystery

The Light Phone III has a scroll wheel, a camera button, and a home button. In Light’s own apps they do lovely things — the wheel ramps brightness, tap it and the flashlight comes on. Install your own app and all three buttons go dead. Nothing. For a while I assumed there was some private API, some system service Light’s apps talked to that mine couldn’t.

There isn’t. It’s much simpler and much stranger.

What’s actually in there

Android maps hardware keys through keylayout files, and Light patched the generic one — /system/usr/keylayout/Generic.kl, the layout every input device on the phone loads. Pull it off the phone and there it is:

key 19    WHEEL_CCW      # wheel up       (Pixart pat9126ja, was R)
key 20    WHEEL_CW       # wheel down     (Pixart pat9126ja, was T)
key 66    WHEEL_CLICK    # wheel press    (gpio-keys, was F8)
key 80    FOCUS          # camera stage 1
key 27    CAMERA         # camera stage 2

The wheel is a Pixart optical sensor — the same family of part that lives in a mouse — and its rotations arrive at whichever app has focus as ordinary KeyEvents. Keys that used to be R and T, remapped. That’s it. That’s the whole system.

Which means the brightness ramp in Light’s tools isn’t the OS doing something privileged. It’s app-layer code inside those tools, listening for key 19 and key 20 like any app could. Nothing intercepts these keys in the window manager. In a sideloaded app, they arrive, and nothing is listening.

Building the missing layer

Once you know that, the fix suggests itself: something needs to listen everywhere. You can’t inject events on Android without a signature-level permission, but an AccessibilityService with flagRequestFilterKeyEvents can see keys for windows it doesn’t own — it’s the one sanctioned way to do this, and it’s the same mechanism a voice assistant uses for its push-to-talk key.

So that’s what BrightControl is. A service that catches the wheel, the camera button, and the home button in every app, and gives each one behavior: brightness or per-notch scrolling on the wheel (double-tap to switch, and it tells you which), flashlight on a tap, anything you like on a hold. Per-app overrides, so LightOS’s own screens keep LightOS’s behavior unless you say otherwise.

None of this is clever engineering. It’s one accessibility service and a switch statement. The work was the archaeology — pulling files off the phone, reading them, and testing what listens to what. The README carries the full writeup, because it’s the documentation I spent an evening wishing existed.