One press, one frame

I built a small Bluetooth shutter for the iPhone—and learned how much engineering hides inside one dependable click.

I wanted one physical button that would take one photograph. No companion app, no account, no cloud. Just a small device that could stay in a camera bag, reconnect to my iPhone and release the shutter when I pressed it.

That became ClickNova Shutter: firmware for the original LilyGO TTGO T-Display, a tiny ESP32 board with two buttons and a 1.14-inch colour screen.

The finished object is deliberately modest. The route to making it dependable was anything but.

A useful shortcut

The native iPhone Camera app already treats the volume-up button as a shutter. Bluetooth keyboards and remotes can send the same command, so ClickNova presents itself to iOS as a Bluetooth Low Energy input device. Press the hardware button and it sends one volume-up action followed by a release. The Camera app does the rest.

This is the small, elegant trick at the centre of the project. It also sets a clear boundary: Camera must already be open in the foreground. The remote cannot launch it, choose a mode or know whether a photograph was actually saved.

I like tools that are honest about their limits. ClickNova does not pretend to control the whole camera. It does one job.

“Exactly once” is a feature

Sending a command is easy. Sending it once—and only when the connection is truly ready—is the real work.

A button can bounce electrically. A Bluetooth link can look connected before it is encrypted and ready to accept input. A phone can move out of range halfway through an action. The remote can wake while its shutter button is still being held. Each of those cases creates an opportunity for a missed frame or, worse, an unexpected one.

So the rule became strict: one complete press produces one shutter action. A press made while disconnected is discarded, never stored for later. The press that wakes the device only wakes it; I have to release the button, wait for the Ready screen and press again. If an interval sequence loses its connection, it stops instead of catching up in a burst.

Most of the firmware exists to protect that simple promise.

Two buttons, a very small screen

The display has only 240 by 135 pixels, but that is enough to answer the questions that matter: Is it pairing? Reconnecting? Ready? How much battery remains? Did it accept the shutter action?

The second button handles the less frequent work. A short press wakes the screen and refreshes the battery estimate. A longer press opens an intervalometer for timelapse and astrophotography. I can choose a cadence and a finite number of frames; while it runs, the interface turns red and dims to ten per cent so it does not become a small torch in the dark.

Holding the same button for ten seconds starts a deliberately awkward two-step bond reset. Changing phones should be possible, but not by accident.

After a minute without activity the display dims. After eleven minutes the board disconnects, turns off the screen and enters deep sleep. The shutter button wakes it again. Low-battery warnings appear before the protected cell reaches the point where the firmware puts itself to sleep.

These are quiet details. They are also what separates a prototype that works on a desk from an object I can trust in a bag.

Vibecoded, then verified

ClickNova was built in conversation with AI coding agents. I wrote the behaviour I wanted, turned edge cases into acceptance criteria, reviewed what came back and kept tightening the specification when the hardware disagreed with the theory. The agents produced and revised much of the implementation; my job shifted from typing every line to directing, testing and deciding what “done” actually meant.

That did not make verification optional. It made verification the centre of the work.

The decision logic was separated from the Arduino hardware wherever possible, so it could run as ordinary native code. The project now has more than 120 automated checks around buttons, timing, battery estimates, reconnection, bond reset and shutter delivery. Every change also gets a clean firmware build in continuous integration, and tagged releases produce a factory image.

Then came the tests that matter most: the original T-Display, a real battery and an iPhone with Camera open. I counted photographs, not log messages. I checked that ten scheduled actions made ten files, that disconnected presses never returned later, and that waking the board did not take a surprise picture.

Hardware has a useful way of ending arguments.

What the side quest taught me

The project started as a shortcut to a photograph and became an exercise in restraint.

Narrow scope made the decisions clearer. The remote supports one board, one phone and one camera behaviour. It refuses commands when it cannot deliver them safely. It shows uncertainty instead of inventing certainty: an implausible battery reading becomes --%, not a reassuring number.

Working with coding agents changed the speed and scale of the build, but not the responsibility. The valuable part was not generating more code. It was being able to spend more time on behaviour, failure modes and the physical test loop. An agent can propose a state machine. It cannot press the button, look at the camera roll and decide whether the object feels trustworthy.

In the end, ClickNova reliably does what the first sentence asked of it.

One press. One frame.

Book a session.

  • Write a few lines about what you need — the more concrete, the better.
  • I reply personally, usually within a day.
  • Based in Poznan, working wherever the story is.

Prefer plain email:hello@tomaszcholewa.com