Some checks failed
CI / build-and-deploy (push) Has been cancelled
Home screen gains shutter, aperture and ISO as dropdowns plus a focus stepper (three nudge sizes each way, and AF) where the body drives focus over PTP. Those three drop out of the read-only strip rather than being shown twice. Everything is built from what the camera actually reports, so a body without one of these controls doesn't get it rather than showing something that silently fails. The 450D marks shutter and aperture readonly unless the mode dial is somewhere they apply, so the dropdowns disable themselves and say why. Focus uses the EOS manualfocusdrive steps and autofocusdrive, and warns when live view is off, which Canon bodies generally require for focus commands. The settings drawer is now tabbed: app settings stay on the first tab, and each config section the camera reports gets its own. Empty sections are dropped, and a selected section that disappears falls back to the first tab. Tabs wrap rather than scroll - a scrolled-off tab is an undiscoverable one. Reverts the automatic camera search added in the previous commit, restoring the Select camera button. Auto-connect could park with no way to reach the device chooser: WebUSB permissions are per-origin, so a grant on localhost doesn't carry to the deployed copy, and a camera that's asleep or held by another app never arrives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb
170 lines
5.9 KiB
JavaScript
170 lines
5.9 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, exclude = []) {
|
|
return [
|
|
['Mode', 'autoexposuremode'],
|
|
['Shutter', 'shutterspeed'],
|
|
['Aperture', 'aperture'],
|
|
['ISO', 'iso'],
|
|
['Format', 'imageformat'],
|
|
['Battery', 'batterylevel'],
|
|
['Shots left', 'availableshots'],
|
|
['Target', 'capturetarget']
|
|
]
|
|
.filter(([, name]) => !exclude.includes(name))
|
|
.map(([label, name]) => ({ label, value: configValue(config, name) }))
|
|
.filter(({ value }) => value !== undefined && value !== '');
|
|
}
|
|
|
|
/**
|
|
* The exposure controls worth putting on the home screen, in the order a
|
|
* photographer expects them. Only settable menus qualify - on a 450D these are
|
|
* readonly unless the mode dial is somewhere they apply (shutter speed is fixed
|
|
* in Av, aperture in Tv, both in the green square).
|
|
*
|
|
* @param {Config | undefined} config
|
|
*/
|
|
export function exposureControls(config) {
|
|
return [
|
|
{ name: 'shutterspeed', label: 'Shutter' },
|
|
{ name: 'aperture', label: 'Aperture' },
|
|
{ name: 'iso', label: 'ISO' }
|
|
]
|
|
.map(entry => ({ ...entry, node: findConfig(config, entry.name) }))
|
|
.filter(
|
|
entry =>
|
|
entry.node &&
|
|
(entry.node.type === 'menu' || entry.node.type === 'radio')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* What focus control this body exposes over PTP.
|
|
*
|
|
* Canon EOS bodies drive the lens through `manualfocusdrive`, a menu whose
|
|
* choices are step sizes in each direction ("Near 1".."Far 3"); setting one
|
|
* nudges focus and the value falls back to None. `autofocusdrive` is a
|
|
* momentary toggle that triggers AF. Neither is guaranteed to exist, and on
|
|
* EOS both generally need live view running.
|
|
*
|
|
* @param {Config | undefined} config
|
|
*/
|
|
export function detectFocusControls(config) {
|
|
let drive = findConfig(config, 'manualfocusdrive');
|
|
/** @type {{ name: string, near: string[], far: string[] } | null} */
|
|
let manual = null;
|
|
if (
|
|
drive &&
|
|
(drive.type === 'menu' || drive.type === 'radio') &&
|
|
!drive.readonly
|
|
) {
|
|
// Sorted so index 0 is the smallest nudge in each direction.
|
|
let byStep = (a, b) => (parseInt(a, 10) || 0) - (parseInt(b, 10) || 0);
|
|
let near = drive.choices.filter(c => /near/i.test(c)).sort(byStep);
|
|
let far = drive.choices.filter(c => /far/i.test(c)).sort(byStep);
|
|
if (near.length && far.length) manual = { name: drive.name, near, far };
|
|
}
|
|
|
|
let auto = findConfig(config, 'autofocusdrive');
|
|
let mode = findConfig(config, 'focusmode');
|
|
|
|
return {
|
|
manual,
|
|
autofocus: auto && auto.type === 'toggle' && !auto.readonly ? auto.name : null,
|
|
mode: mode && 'value' in mode ? mode : null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|