Note
ClickNova Shutter is a DIY Bluetooth camera remote for iOS, built on a LilyGO TTGO T-Display (ESP32). It pairs with an iPhone as a BLE HID Consumer Control device and triggers the native Camera app over the volume-up shutter shortcut — no companion app needed. It supports single-press shooting, a timelapse/astrophotography intervalometer with selectable cadence and shot count, automatic reconnection to the bonded phone, and deep sleep with wake-on-shutter for months of battery life on a single 18650 cell. The 1.14" color display shows pairing, reconnect, battery, and error states at a glance. Firmware is C++/Arduino on PlatformIO with NimBLE, and most of the decision logic lives in header-only modules covered by 120+ native unit tests. GitHub Actions runs the full test suite and a clean firmware build on every push, and tags produce validated factory images. This is a fully vibecoded side project — designed, implemented, tested, and stabilized in conversation with an AI coding agents (chatgpt5.6sol/kimi3), then verified on real hardware against an iPhone. It reliably takes exactly one photo per press, which is all it was ever meant to do.
ClickNovaShutter is greenfield firmware for turning an original ESP32-based LilyGO TTGO T-Display into a dedicated Bluetooth camera shutter for an iPhone. It is designed to pair securely, reconnect to one bonded phone, send one BLE HID Volume Increment action per shutter press, and show connection and battery status on the built-in display.
Release status: v1.1 is functionally accepted on the original T-Display and iPhone 16 Pro. It preserves v1.0 behavior, adds bounded automatic bonded reconnect recovery, and publishes one tag-gated Linux factory image.
The only v1 target is the original 240x135 LilyGO TTGO T-Display with an ESP32 and integrated 1.14-inch ST7789 TFT. T-Display S3 and other similarly named boards are not compatible targets.
| Function | GPIO | Notes |
|---|---|---|
| Shutter button / deep-sleep wake | 35 | Active-low; use plain input mode because GPIO35 has no internal pull-up |
| Utility button | 0 | Active-low |
| Battery voltage ADC | 34 | Reads the board's switched voltage divider |
| Battery-divider enable | 14 | Drive high only while sampling |
| TFT MOSI | 19 | Project-local TFT configuration |
| TFT clock | 18 | Project-local TFT configuration |
| TFT chip select | 5 | Project-local TFT configuration |
| TFT data/command | 16 | Project-local TFT configuration |
| TFT reset | 23 | Project-local TFT configuration |
| TFT backlight | 4 | PWM brightness control |
Use only a protected 1S 3.7 V 18650 Li-ion cell connected through the
board's two-pin JST battery socket. Confirm the board's + and - markings
against the actual plug with a multimeter before connection: two-pin JST leads
are not universally wired with the same polarity. Reversed polarity can
permanently damage the board or battery and can create a fire hazard. Do not
connect a multi-cell pack, an unprotected cell, or a damaged cell.
- Advertise as ClickNova Shutter and bond securely with one iPhone.
- Act as an encrypted BLE HID Consumer Control device. A valid shutter press
sends Volume Increment (
0xE9), followed by a release, exactly once. - Ignore shutter presses until the encrypted HID connection is ready; presses while disconnected are never queued for later.
- Recover a bonded connection that remains non-ready for 15 seconds, and refresh filtered reconnect advertising through a directed attempt every 15 seconds without deleting the bond or rebooting.
- Optionally run a finite intervalometer at a selectable 2, 5, 10, 15, 30, or 60-second start-to-start cadence for 10, 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, or 1000 shutter actions.
- Keep the screen at full brightness for 60 seconds of inactivity, dim it to 10% for a further 10 minutes, then enter deep sleep at 11 minutes total.
- Re-sample the battery every 10 minutes. The ready view shows an orange
warning bar with a
Low battery - charge soonfooter at 10% or below, and the device enters protective deep sleep at 5% or below—even while an intervalometer session is running—to protect the cell from deep discharge. - Wake from deep sleep with the shutter button. The wake-causing press only wakes the device; release it before taking a photo.
- Use a short utility-button press to restore full brightness and refresh the battery estimate.
- Start bond reset after a ten-second utility-button hold. After release, confirm with another short utility press within 10 seconds; timeout or a shutter press cancels the reset.
The shutter command works through iOS's volume-button behavior. The native Camera app must already be open in the foreground; the remote cannot launch Camera, select a mode, or confirm that iOS actually saved a photo.
The reproducible PlatformIO configuration uses these pinned versions:
- PlatformIO platform
espressif32@7.0.1 - Board
lilygo-t-display - Arduino framework
NimBLE-Arduino@2.5.0TFT_eSPI@2.5.43, using a project-local T-Display setup
Install Git, Python 3 with its venv module, and ordinary serial-port access
for your user account. No global PlatformIO installation is required.
The following Linux commands create an isolated development environment and run both quality gates:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/pio test -e native
.venv/bin/pio run -e lilygo-t-displayConnect the T-Display with a USB data cable. The original board normally
appears as a Silicon Labs CP210x bridge (10c4:ea60) and a serial port such as
/dev/ttyUSB0. Find the current port after each reconnect because its number
may change:
.venv/bin/pio device list
lsusbFlash and monitor using the discovered port:
.venv/bin/pio run -e lilygo-t-display -t upload --upload-port /dev/ttyUSB0
.venv/bin/pio device monitor --port /dev/ttyUSB0 --baud 115200Exit the monitor with Ctrl-C before another upload. A successful boot
starts its serial log with firmware=1.2.0, reports the 240x135 display, and
then reports either general pairing or stored-bond reconnect advertising.
Starting with v1.1, each accepted vX.Y release publishes one
ClickNovaShutter-vX.Y-factory.bin for Linux. v1.0 is not backfilled. Verify
the image's SHA-256 value against the digest in that release's notes before
flashing.
The factory image starts at 0x0 and clears the ESP32 NVS bond store. It
requires fresh pairing in iPhone Settings and must not be used as an in-place
update image.
sha256sum ClickNovaShutter-vX.Y-factory.bin
python3 -m venv .clicknova-flash
.clicknova-flash/bin/python -m pip install esptool==4.11.0
.clicknova-flash/bin/python -m esptool --chip esp32 --port /dev/ttyUSB0 \
--baud 460800 write_flash --flash_mode dio --flash_freq 40m \
--flash_size 4MB 0x0 ClickNovaShutter-vX.Y-factory.bin- Build and flash the
lilygo-t-displayenvironment, then power or reset the board. - On an iPhone, open Settings > Bluetooth and select ClickNova Shutter while the device shows Pairing Mode.
- Wait until the display reports Ready. Securing and Connecting are normal intermediate states.
- Open Apple's Camera app in the foreground.
- Press and release the shutter button once for one photo.
After a successful bond, rebooting the device should reconnect without a new pairing prompt. Shutter presses made before Ready are discarded and are never replayed after reconnection.
| Control | Action |
|---|---|
| Shutter press while Ready | Send one camera shutter action |
| Shutter press while disconnected | Consume the press without sending or queueing it |
| Utility release before 1 second | Restore full brightness and refresh battery |
| Utility release from 1 to under 2 seconds | No secondary action |
| Utility release from 2 to under 5 seconds while Ready | Enter intervalometer setup |
| Utility release from 5 to under 10 seconds | No secondary action |
| Utility hold for 10 seconds | Show bond-reset prompt; release to open confirmation |
| Short Utility release during reset confirmation | Delete bonds and reboot |
| Shutter during reset confirmation | Cancel reset without taking a photo |
To use the intervalometer while Ready, hold Utility for at least two but less than five seconds and release. Shutter cycles each value and Utility confirms it. Confirm the interval, then the action count; the first action is immediate. Either button stops a running session. A Bluetooth loss aborts it without replay or automatic resume.
The UI shows a thin red bar while Utility is held on the normal Ready view. It fills to the two-second entry threshold, stays full and bright while release will enter setup, and clears on release, connection loss, mode change, or at the five-second interval-entry release limit. It remains hidden while HID is unavailable. Setup shows the total plan duration prominently.
The landscape UI follows the real BLE lifecycle with distinct pairing,
reconnecting, securing, connecting, ready, recovery, and error views.
An accepted manual shutter notification produces a brief full-screen flash,
then returns to the current underlying view. The ready screen displays an
approximate battery percentage from a protected calibrated sample, or
BATTERY --% when the reading is unavailable or implausible.
After 60 seconds without accepted activity, the backlight dims to 10%. After a further 10 minutes—11 minutes total—the firmware disconnects BLE, turns off the display and divider supply, and enters deep sleep. Press Shutter to wake it. The wake-causing press is consumed; release it, wait for Ready, then make a new complete press to take a photo. A running intervalometer remains awake.
- Hold Utility continuously for 10 seconds until the reset prompt appears.
- Release Utility.
- Short-press and release Utility within 10 seconds.
- Wait for Bonds Cleared, reboot, and Pairing Mode.
- On the old iPhone, forget ClickNova Shutter if it is still listed.
- Pair the intended iPhone from Settings > Bluetooth.
Waiting 10 seconds or pressing Shutter during confirmation safely cancels the reset and retains the bond.
The 115200-baud log is the primary troubleshooting record. Useful prefixes are:
[boot]for firmware version, reset reason, and wake reason.[init]for pins, display dimensions, backlight PWM, and BLE advertising.[event]for connection, security, HID subscription, and shutter reports.[state]for BLE, power, UI, intervalometer, and error projections.[battery]for raw/converted millivolts, sample count, validity, and time.[reconnect]for phase, elapsed time, retry attempt, action, and result.[reset]and[sleep]for confirmed reset and shutdown/wake sequences.[error]for fail-closed initialization or recoverable runtime failures.
- No serial port: use a data-capable USB cable, reconnect the board, and
rerun
pio device list. The port may return as/dev/ttyUSB0even if it previously used another number. - Permission denied on
/dev/ttyUSB*: add the user to the distribution's serial-access group (commonlydialout), sign out/in, and verify the port permissions. Do not run the complete toolchain as root. - Upload says the port is busy: exit every serial monitor with
Ctrl-C, confirm the current port, and retry the explicit--upload-portcommand. - ClickNova is absent from Other Devices: confirm the board says Pairing Mode. Toggle Bluetooth off/on in the iPhone Settings app, remain on the Bluetooth page, and reset the board once. If an old entry exists, use the confirmed bond reset before forgetting that entry on iOS.
- iPhone says connected but ClickNova is not Ready: wait for Securing and Connecting to finish. v1.1 automatically drops a non-ready link after 15 seconds and retries; allow up to 30 seconds before manually toggling Bluetooth in Settings. Do not press Shutter until Ready.
- No photo: put Apple's native Camera in the foreground, confirm Ready, and use a new complete press/release. ClickNova cannot launch Camera or confirm that iOS saved the image.
BATTERY --%: the ADC result was unavailable or outside the plausible single-cell range. This is intentionally safer than displaying a fabricated percentage; verify battery polarity/source and inspect the[battery]log.- Wrong display or pin behavior: confirm the board is the original 240x135 ESP32 T-Display, not a T-Display S3 or another similarly named model.
- Implementation plan — authoritative target behavior, staged delivery, and acceptance criteria
- Product requirements — original product brief
- Work breakdown — original task inventory
- v1.1 stabilization log — seven-day, 100-photo post-release field gate
Where the original requirements or task inventory disagree with the implementation plan, follow the implementation plan.
The 118-case native logic suite, four release-tool tests, local and hosted pinned firmware builds, fail-closed CI gates, and all recorded v1.0 and v1.1 hardware gates pass. The project does not claim measured shutter latency or microamp deep-sleep current without suitable instrumentation. Android, multiple-phone selection, OTA updates, camera launching, and other T-Display variants are outside v1.