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
110 lines
3.8 KiB
JavaScript
110 lines
3.8 KiB
JavaScript
/*
|
|
* 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;
|
|
}
|