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:
109
examples/preact/config-utils.js
Normal file
109
examples/preact/config-utils.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
/** @typedef {import('web-gphoto2').Config} Config */
|
||||
|
||||
/**
|
||||
* Depth-first lookup of a config node by its gphoto2 name.
|
||||
* @param {Config | undefined} config
|
||||
* @param {string} name
|
||||
* @returns {Config | undefined}
|
||||
*/
|
||||
export function findConfig(config, name) {
|
||||
if (!config) return undefined;
|
||||
if (config.name === name) return config;
|
||||
if (config.type !== 'window' && config.type !== 'section') return undefined;
|
||||
for (let child of Object.values(config.children)) {
|
||||
let found = findConfig(child, name);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current value of a config node, if it exists.
|
||||
* @param {Config | undefined} config
|
||||
* @param {string} name
|
||||
*/
|
||||
export function configValue(config, name) {
|
||||
let node = findConfig(config, name);
|
||||
return node && 'value' in node ? node.value : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The read-only summary shown at the bottom of the home screen. These are the
|
||||
* names the Canon EOS driver (450D included) exposes; anything missing is
|
||||
* simply skipped, so this stays useful on other bodies too.
|
||||
* @param {Config | undefined} config
|
||||
*/
|
||||
export function statusReadout(config) {
|
||||
return [
|
||||
['Mode', 'autoexposuremode'],
|
||||
['Shutter', 'shutterspeed'],
|
||||
['Aperture', 'aperture'],
|
||||
['ISO', 'iso'],
|
||||
['Format', 'imageformat'],
|
||||
['Battery', 'batterylevel'],
|
||||
['Shots left', 'availableshots'],
|
||||
['Target', 'capturetarget']
|
||||
]
|
||||
.map(([label, name]) => ({ label, value: configValue(config, name) }))
|
||||
.filter(({ value }) => value !== undefined && value !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a gphoto2 shutter speed string ("1/250", "30", "0.3", "bulb") into
|
||||
* seconds. Returns undefined when it can't be parsed.
|
||||
* @param {unknown} value
|
||||
*/
|
||||
export function shutterSpeedSeconds(value) {
|
||||
if (typeof value !== 'string') return undefined;
|
||||
let str = value.trim().toLowerCase();
|
||||
if (str === 'bulb' || str === '') return undefined;
|
||||
let fraction = /^(\d+(?:\.\d+)?)\/(\d+(?:\.\d+)?)$/.exec(str);
|
||||
if (fraction) {
|
||||
let denominator = Number(fraction[2]);
|
||||
return denominator ? Number(fraction[1]) / denominator : undefined;
|
||||
}
|
||||
let seconds = Number(str.replace(/s$/, ''));
|
||||
return Number.isFinite(seconds) ? seconds : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the camera can be held open for a long exposure, if at all.
|
||||
*
|
||||
* `bulb` is a plain toggle on many bodies; Canon EOS bodies instead drive the
|
||||
* shutter through `eosremoterelease`. Returns null when neither is available.
|
||||
* @param {Config | undefined} config
|
||||
* @returns {{ kind: 'bulb' } | { kind: 'eosremoterelease', press: string, release: string } | null}
|
||||
*/
|
||||
export function detectBulbSupport(config) {
|
||||
let bulb = findConfig(config, 'bulb');
|
||||
if (bulb && bulb.type === 'toggle' && !bulb.readonly) {
|
||||
return { kind: 'bulb' };
|
||||
}
|
||||
let remote = findConfig(config, 'eosremoterelease');
|
||||
if (remote && remote.type === 'menu' && !remote.readonly) {
|
||||
let press = remote.choices.find(c => /^press full/i.test(c));
|
||||
let release = remote.choices.find(c => /^release full/i.test(c));
|
||||
if (press && release) {
|
||||
return { kind: 'eosremoterelease', press, release };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user