Files
web-dslr/examples/preact/config-utils.js
Jon 1af4b215b2
Some checks failed
CI / build-and-deploy (push) Has been cancelled
Turn the demo app into a Canon 450D intervalometer
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
2026-08-01 18:47:04 +01:00

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;
}