Turn the demo app into a Canon 450D intervalometer
Some checks failed
CI / build-and-deploy (push) Has been cancelled
Some checks failed
CI / build-and-deploy (push) Has been cancelled
Replaces the gPhoto2 demo UI (live view beside the raw config tree) with a timelapse intervalometer. The home screen carries only what changes between runs - interval, frame count, start delay, start/stop, countdown, progress and a log - while every camera setting moves into a settings drawer. - intervalometer.js: schedules frames on an absolute grid (start + n * interval) so transfer time doesn't accumulate as drift over a long run. An overrun logs and fires as soon as the camera is free rather than dropping a frame; three consecutive failures abort. - storage.js: frames stream into a folder via the File System Access API, named for capture time (20260801-172713_00001.JPG) so sorting by name is sorting by time. Falls back to downloads. Also holds the screen wake lock, since background tabs get their timers throttled. - config-utils.js: config tree lookups, shutter speed parsing, and bulb capability detection (bulb toggle or Canon eosremoterelease). - home.js: sequence controls plus a read-only exposure readout and pre-flight warnings when the interval can't fit the exposure and transfer. - settings.js / index.js: app prefs and the full config tree behind a drawer. Config polling now only runs while that drawer is open, leaving the USB link to the captures during a sequence. Live view stays up between frames and steps aside only while the shutter fires, which is what the EOS driver requires; it recovers afterwards with backoff instead of hammering a busy camera. Deployed as an assets-only Cloudflare Worker. The WASM is built with pthreads and allocates a shared WebAssembly.Memory, so _headers reproduces the COOP/COEP pair from serve.json - without cross-origin isolation the app fails to start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb
This commit is contained in:
56
README.md
56
README.md
@@ -48,9 +48,61 @@ a.href = URL.createObjectURL(file);
|
||||
a.download = file.name;
|
||||
```
|
||||
|
||||
## Demo
|
||||
## Intervalometer app
|
||||
|
||||
This repository also contains a [demo app](https://web.dev/porting-libusb-to-webusb/) running gPhoto2 on the Web:
|
||||
`examples/preact` is an intervalometer for shooting timelapses, built around a Canon EOS 450D but not specific to it — anything the PTP driver supports will work.
|
||||
|
||||
```bash
|
||||
cd examples/preact
|
||||
npm install
|
||||
npx serve . # serve.json sets the COOP/COEP headers the WASM module needs
|
||||
```
|
||||
|
||||
Open the printed URL in Chrome, plug the camera in over USB, switch it on and pick it from the device picker.
|
||||
|
||||
**Home screen** — only the things you change between runs:
|
||||
|
||||
- **Interval**, **frames** (or ∞ for open-ended) and **start delay**
|
||||
- Start/stop, plus a single-shot button for framing
|
||||
- Live countdown to the next frame, progress, estimated finish time, and a log of the last few events
|
||||
- A read-only strip showing shutter / aperture / ISO / format / battery / shots remaining, so you can sanity-check exposure against the interval without opening anything
|
||||
- Pre-flight warnings when the interval is too tight for the current exposure and transfer time, when you're shooting RAW on a short interval, or when no output folder is set
|
||||
|
||||
**Settings drawer** (⚙, top right) — everything else, including the full gPhoto2 config tree, so the home screen stays uncluttered:
|
||||
|
||||
- Where frames go: a folder on disk via the File System Access API, one-at-a-time downloads, or nothing at all if you're keeping them on the card. Frames are named after the moment they were taken — `20260801-172713_00007.JPG` in a sequence, `20260801-172713.JPG` for a single shot — so sorting by name is sorting by capture time. The camera's own `IMG_1234` is dropped: it wraps at 9999 and resets on a card format, so it can't order a long run.
|
||||
- Live view on/off, and whether it stays up between frames during a sequence (on by default — the feed drops only while each shot fires, since the camera needs live view down to take it, then recovers on its own)
|
||||
- Screen wake lock, so a backgrounded tab doesn't get its timers throttled mid-run
|
||||
- Bulb exposures, if the camera exposes `bulb` or `eosremoterelease`
|
||||
- The camera's own settings: shutter, aperture, ISO, image format, capture target, drive mode, and so on
|
||||
|
||||
### Deploying
|
||||
|
||||
Deployed as an assets-only Cloudflare Worker:
|
||||
|
||||
```bash
|
||||
cd examples/preact
|
||||
./build-dist.sh # copies just the browser files into dist/
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
Live at <https://dslr-intervalometer.bournemouthtech.workers.dev>.
|
||||
|
||||
`build-dist.sh` is an explicit allowlist rather than an ignore file, because everything in the assets directory becomes publicly readable and `.assetsignore` was not excluding `node_modules`.
|
||||
|
||||
The one thing a static host has to get right here: the WASM module is built with pthreads and allocates a **shared** `WebAssembly.Memory`, so the page must be [cross-origin isolated](https://web.dev/coop-coep/) or it fails to start outright. `_headers` sets the same COOP/COEP pair `serve.json` uses locally. If you host this anywhere else, set those two headers or nothing will work. Verify with `crossOriginIsolated === true` in the console.
|
||||
|
||||
Notes on the timing and its limits:
|
||||
|
||||
- Frames are scheduled on an absolute grid (`start + n × interval`), so transfer time doesn't accumulate as drift over a long run. If a capture overruns its slot the next frame fires as soon as the camera is free and the overrun is logged, rather than a frame being dropped.
|
||||
- Three failed captures in a row abort the sequence.
|
||||
- Config polling only runs while the settings drawer is open; during a sequence the USB link is left to the captures.
|
||||
- Keep the tab in the foreground. Chrome throttles timers hard in hidden tabs, which will stretch your intervals.
|
||||
- **Bulb is experimental.** Bulb frames are written to the camera's card and are *not* downloaded — the WASM API only returns files produced by an explicit `captureImageAsFile`, and there's no hook for images arriving any other way. Set the capture target to the memory card and pull the card afterwards. For exposures of 30s or less, just set the shutter speed normally and leave bulb off.
|
||||
|
||||
## Original demo
|
||||
|
||||
`examples/preact` started out as the upstream gPhoto2-on-the-Web demo — a live view next to the raw camera config tree — and the intervalometer above replaced it. The original is still hosted and documented upstream:
|
||||

|
||||
|
||||
For the detailed technical write-up, see [the official blog post](https://web.dev/porting-libusb-to-webusb/). To see the demo in action, visit the hosted version [here](https://web-gphoto2.rreverser.com/) (but make sure to read the [cross-platform compatibility notes](https://web.dev/porting-libusb-to-webusb/#important-cross-platform-compatibility-notes) first).
|
||||
|
||||
Reference in New Issue
Block a user