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