UNPKG

tethr

Version:

Controlls USB-connected cameras, webcam, and smartphone camera from browser

1,245 lines (1,240 loc) 66.2 kB
import { __decorate } from "tslib"; import { BiMap } from 'bim'; import { scalar, vec2 } from 'linearly'; import { isEqual, minBy } from 'lodash'; import sleep from 'sleep-promise'; import { MemoizeExpiring } from 'typescript-memoize'; import { CanvasMediaStream } from '../CanvasMediaStream'; import { decodeIFD, encodeIFD, IFDType } from '../IFD'; import { ResCode } from '../PTPDatacode'; import { PTPDataView } from '../PTPDataView'; import { computeShutterSpeedSeconds, isntNil, readonlyConfigDesc, UnsupportedConfigDesc, } from '../util'; import { TethrPTPUSB } from '.'; var OpCodeSigma; (function (OpCodeSigma) { OpCodeSigma[OpCodeSigma["GetCamConfig"] = 36880] = "GetCamConfig"; OpCodeSigma[OpCodeSigma["GetCamStatus"] = 36881] = "GetCamStatus"; OpCodeSigma[OpCodeSigma["GetCamDataGroup1"] = 36882] = "GetCamDataGroup1"; OpCodeSigma[OpCodeSigma["GetCamDataGroup2"] = 36883] = "GetCamDataGroup2"; OpCodeSigma[OpCodeSigma["GetCamDataGroup3"] = 36884] = "GetCamDataGroup3"; OpCodeSigma[OpCodeSigma["GetCamCaptStatus"] = 36885] = "GetCamCaptStatus"; OpCodeSigma[OpCodeSigma["SetCamDataGroup1"] = 36886] = "SetCamDataGroup1"; OpCodeSigma[OpCodeSigma["SetCamDataGroup2"] = 36887] = "SetCamDataGroup2"; OpCodeSigma[OpCodeSigma["SetCamDataGroup3"] = 36888] = "SetCamDataGroup3"; OpCodeSigma[OpCodeSigma["SetCamClockAdj"] = 36889] = "SetCamClockAdj"; OpCodeSigma[OpCodeSigma["GetCamCanSetInfo"] = 36890] = "GetCamCanSetInfo"; OpCodeSigma[OpCodeSigma["SnapCommand"] = 36891] = "SnapCommand"; OpCodeSigma[OpCodeSigma["ClearImageDBSingle"] = 36892] = "ClearImageDBSingle"; OpCodeSigma[OpCodeSigma["GetPictFileInfo"] = 36896] = "GetPictFileInfo"; OpCodeSigma[OpCodeSigma["GetPartialPictFile"] = 36897] = "GetPartialPictFile"; OpCodeSigma[OpCodeSigma["GetBigPartialPictFile"] = 36898] = "GetBigPartialPictFile"; OpCodeSigma[OpCodeSigma["GetCamDataGroup4"] = 36899] = "GetCamDataGroup4"; OpCodeSigma[OpCodeSigma["SetCamDataGroup4"] = 36900] = "SetCamDataGroup4"; OpCodeSigma[OpCodeSigma["GetCamCanSetInfo2"] = 36901] = "GetCamCanSetInfo2"; OpCodeSigma[OpCodeSigma["GetCamCanSetInfo3"] = 36902] = "GetCamCanSetInfo3"; OpCodeSigma[OpCodeSigma["GetCamDataGroup5"] = 36903] = "GetCamDataGroup5"; OpCodeSigma[OpCodeSigma["SetCamDataGroup5"] = 36904] = "SetCamDataGroup5"; OpCodeSigma[OpCodeSigma["GetCamDataGroup6"] = 36905] = "GetCamDataGroup6"; OpCodeSigma[OpCodeSigma["SetCamDataGroup6"] = 36906] = "SetCamDataGroup6"; OpCodeSigma[OpCodeSigma["GetViewFrame"] = 36907] = "GetViewFrame"; OpCodeSigma[OpCodeSigma["GetCamCanSetInfo4"] = 36910] = "GetCamCanSetInfo4"; OpCodeSigma[OpCodeSigma["GetCamStatus2"] = 36908] = "GetCamStatus2"; OpCodeSigma[OpCodeSigma["GetPictFileInfo2"] = 36909] = "GetPictFileInfo2"; OpCodeSigma[OpCodeSigma["CloseApplication"] = 36911] = "CloseApplication"; OpCodeSigma[OpCodeSigma["GetCamCanSetInfo5"] = 36912] = "GetCamCanSetInfo5"; OpCodeSigma[OpCodeSigma["GetCamDataGroupFocus"] = 36913] = "GetCamDataGroupFocus"; OpCodeSigma[OpCodeSigma["SetCamDataGroupFocus"] = 36914] = "SetCamDataGroupFocus"; OpCodeSigma[OpCodeSigma["GetCamDataGroupMovie"] = 36915] = "GetCamDataGroupMovie"; OpCodeSigma[OpCodeSigma["SetCamDataGroupMovie"] = 36916] = "SetCamDataGroupMovie"; OpCodeSigma[OpCodeSigma["ConfigApi"] = 36917] = "ConfigApi"; OpCodeSigma[OpCodeSigma["GetMovieFileInfo"] = 36918] = "GetMovieFileInfo"; OpCodeSigma[OpCodeSigma["GetPartialMovieFile"] = 36919] = "GetPartialMovieFile"; })(OpCodeSigma || (OpCodeSigma = {})); var CaptStatus; (function (CaptStatus) { CaptStatus[CaptStatus["runSnap"] = 1] = "runSnap"; CaptStatus[CaptStatus["compSnap"] = 2] = "compSnap"; CaptStatus[CaptStatus["runImageCreate"] = 4] = "runImageCreate"; CaptStatus[CaptStatus["compImageCreate"] = 5] = "compImageCreate"; CaptStatus[CaptStatus["compMovieStopStandby"] = 6] = "compMovieStopStandby"; CaptStatus[CaptStatus["compMovieCreate"] = 7] = "compMovieCreate"; CaptStatus[CaptStatus["okAf"] = 32769] = "okAf"; CaptStatus[CaptStatus["okCwb"] = 32770] = "okCwb"; CaptStatus[CaptStatus["okImageSave"] = 32771] = "okImageSave"; CaptStatus[CaptStatus["okNoerrorEtc"] = 32772] = "okNoerrorEtc"; CaptStatus[CaptStatus["ngAf"] = 24577] = "ngAf"; CaptStatus[CaptStatus["ngBaffaFull"] = 24578] = "ngBaffaFull"; CaptStatus[CaptStatus["ngCwb"] = 24579] = "ngCwb"; CaptStatus[CaptStatus["ngImageCreate"] = 24580] = "ngImageCreate"; CaptStatus[CaptStatus["ngGeneral"] = 24581] = "ngGeneral"; })(CaptStatus || (CaptStatus = {})); var SnapCaptureMode; (function (SnapCaptureMode) { SnapCaptureMode[SnapCaptureMode["GeneralCapture"] = 1] = "GeneralCapture"; SnapCaptureMode[SnapCaptureMode["NonAFCapture"] = 2] = "NonAFCapture"; SnapCaptureMode[SnapCaptureMode["AFDriveOnly"] = 3] = "AFDriveOnly"; SnapCaptureMode[SnapCaptureMode["StartAF"] = 4] = "StartAF"; SnapCaptureMode[SnapCaptureMode["StopAF"] = 5] = "StopAF"; SnapCaptureMode[SnapCaptureMode["StartCapture"] = 6] = "StartCapture"; SnapCaptureMode[SnapCaptureMode["StopCapture"] = 7] = "StopCapture"; SnapCaptureMode[SnapCaptureMode["StartRecordingMovieWithAF"] = 16] = "StartRecordingMovieWithAF"; SnapCaptureMode[SnapCaptureMode["StartRecordingMovieWithoutAF"] = 32] = "StartRecordingMovieWithoutAF"; SnapCaptureMode[SnapCaptureMode["StopRecordingMovie"] = 48] = "StopRecordingMovie"; })(SnapCaptureMode || (SnapCaptureMode = {})); const ConfigListSigma = [ 'aperture', 'autoFocusFrameCenter', 'autoFocusFrameSize', 'batteryLevel', 'iso', 'canRunAutoFocus', 'colorTemperature', 'colorMode', 'destinationToSave', 'focalLength', 'focusDistance', 'focusMeteringMode', // 'focusPeaking', 'exposureComp', 'exposureMode', 'imageAspect', 'imageQuality', 'imageSize', 'liveviewMagnifyRatio', 'shutterSpeed', 'shutterSound', 'whiteBalance', ]; const SigmaExpirationMs = 16; const SigmaCheckConfigIntervalMs = 1000; export class TethrSigma extends TethrPTPUSB { #isCapturing = false; async open() { await super.open(); await this.device.receiveData({ label: 'SigmaFP ConfigApi', opcode: OpCodeSigma.ConfigApi, parameters: [0x0], }); /** * Don't need this IFD data * decodeIFD(data, { cameraModel: {tag: 1, type: IFDType.Ascii}, serialNumber: {tag: 2, type: IFDType.Ascii}, firmwareVersion: {tag: 3, type: IFDType.Ascii}, communiationVersion: {tag: 5, type: IFDType.Float}, }) */ const checkPropChangeInterval = () => { if (!this.opened) return; if (!this.#isCapturing) { this.checkConfigChange(); } setTimeout(checkPropChangeInterval, SigmaCheckConfigIntervalMs); }; checkPropChangeInterval(); } async close() { await this.stopLiveview(); await super.close(); } async getAperture() { const { aperture } = await this.getCamStatus(); if (aperture === 0x0) return 'auto'; return (this.apertureOneThirdTable.get(aperture) ?? this.apertureHalfTable.get(aperture) ?? null); } async setAperture(aperture) { if (aperture === 'auto') return { status: 'invalid parameter' }; const byte = this.apertureOneThirdTable.getKey(aperture); if (!byte) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup1, 1, byte); } async getApertureDesc() { const { fValue: range } = await this.getCamCanSetInfo(); const value = await this.getAperture(); if (range.length === 0) { // Should be auto aperture return { writable: false, value, }; } const [svMin, svMax, step] = range; const isStepOneThird = Math.abs(step - 1 / 3) < Math.abs(step - 1 / 2); const table = isStepOneThird ? this.apertureOneThirdTable : this.apertureHalfTable; const apertures = Array.from(table.values()); const fMinRaw = Math.sqrt(2 ** svMin); const fMaxRaw = Math.sqrt(2 ** svMax); const fMin = minBy(apertures, a => Math.abs(a - fMinRaw)); const fMax = minBy(apertures, a => Math.abs(a - fMaxRaw)); if (!fMin || !fMax) throw new Error('Invalid aperture range'); const values = apertures.filter(a => fMin <= a && a <= fMax); return { writable: true, value, option: { type: 'enum', values, }, }; } async #getLVCoordinateSize() { const { focusValidArea } = await this.getCamCanSetInfo(); const [lvTop, lvBottom, lvLeft, lvRight] = focusValidArea; // Assume the margins are symmetrical return [lvLeft + lvRight, lvTop + lvBottom]; } async #enableSpotAutoFocus() { await this.device.sendData({ label: 'SigmaFP SetCamDataGroupFocus', opcode: OpCodeSigma.SetCamDataGroupFocus, data: encodeIFD({ // 2 === 1-spot selection focusArea: { tag: 10, type: IFDType.Byte, value: [2] }, }), }); await this.device.sendData({ label: 'SigmaFP SetCamDataGroupFocus', opcode: OpCodeSigma.SetCamDataGroupFocus, data: encodeIFD({ // 49 === 49-point selection mode onePointSelectionMethod: { tag: 11, type: IFDType.Byte, value: [49] }, }), }); } async getAutoFocusFrameCenterDesc() { if (!(await this.getCanRunAutoFocus())) { return { writable: false, value: null }; } // await this.#enableSpotAutoFocus() // Then, get the current position const { distanceMeasurementFramePosition } = await this.getCamStatus(); const { distanceMeasurementFrameMovementAmount, focusValidArea } = await this.getCamCanSetInfo(); const [lvY, lvX] = new Uint16Array(distanceMeasurementFramePosition); const [lvStepY, lvStepX] = distanceMeasurementFrameMovementAmount; const lvSize = await this.#getLVCoordinateSize(); const [lvTop, , lvLeft] = focusValidArea; const value = vec2.invlerp([0, 0], lvSize, [lvX, lvY]); const step = vec2.div([lvStepX, lvStepY], lvSize); // The value of focusValidArea includes the maximum 64x64 focus frame, // so to find the valid area of the center of the focus frame, // it is necessary to offset it inside. // 80 and 64 are the additional margins for the focus area. const min = vec2.div([lvLeft + 80, lvTop + 64], lvSize); const max = vec2.sub(vec2.one, min); return { writable: true, value, option: { type: 'range', min: min, max: max, step, }, }; } async setAutoFocusFrameCenter(center) { if (!(await this.get('canRunAutoFocus'))) { return { status: 'invalid parameter', message: 'Auto focus is not available', }; } await this.#enableSpotAutoFocus(); const lvSize = await this.#getLVCoordinateSize(); const [x, y] = vec2.round(vec2.mul(center, lvSize)); const { resCode } = await this.device.sendData({ label: 'SigmaFP SetCamDataGroupFocus', opcode: OpCodeSigma.SetCamDataGroupFocus, data: encodeIFD({ distanceMeasurementFramePosition: { tag: 13, type: IFDType.Short, value: [y, x], }, }), }); if (resCode === ResCode.OK) { return { status: 'ok' }; } else { return { status: 'invalid parameter' }; } } async getAutoFocusFrameSizeDesc() { if (!(await this.get('canRunAutoFocus'))) { return { writable: false, value: null }; } const { distanceMeasurementFrameSize: [lvSizeId], } = await this.getCamStatus(); const { distanceMeasurementFrameSize: [sizeCount], } = await this.getCamCanSetInfo(); const values = ['large', 'medium', 'small'].slice(0, sizeCount); const value = values[lvSizeId]; return { writable: sizeCount > 0, value, option: { type: 'enum', values: values.reverse(), }, }; } async setAutoFocusFrameSize(size) { if (!(await this.get('canRunAutoFocus'))) { return { status: 'invalid parameter' }; } const id = ['large', 'medium', 'small'].indexOf(size); if (id === -1) return { status: 'invalid parameter' }; try { await this.device.sendData({ label: 'SigmaFP SetCamDataGroupFocus', opcode: OpCodeSigma.SetCamDataGroupFocus, data: encodeIFD({ distanceMeasurementFrameSize: { tag: 12, type: IFDType.Byte, value: [id], }, }), }); } catch (err) { return { status: 'invalid parameter' }; } return { status: 'ok' }; } async getBatteryLevelDesc() { const { batteryLevel } = await this.getCamStatus(); const value = this.batteryLevelTable.get(batteryLevel) ?? null; return { writable: false, value, }; } async getCanTakePhotoDesc() { return readonlyConfigDesc(true); } async getCanRunAutoFocusDesc() { const { focusMode } = await this.getCamCanSetInfo(); // 3 == AF-S const canRun = focusMode.includes(3); return readonlyConfigDesc(canRun); } async getCanStartLiveviewDesc() { const { lvImageTransfer } = await this.getCamCanSetInfo(); return readonlyConfigDesc(lvImageTransfer.length > 0); } async setColorMode(colorMode) { const id = this.colorModeTable.getKey(colorMode); if (id === undefined) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup3, 4, id); } async getColorModeDesc() { const decodeColorMode = (id) => { return this.colorModeTable.get(id) ?? `unknown:${id.toString(16)}`; }; const { colorMode } = await this.getCamStatus(); const { colorMode: colorModeOptions } = await this.getCamCanSetInfo(); // NOTE: the colorModeOptions lacks Warm Gold (0xf0). // it must be manually added only if the firmware is 5.0, // but configAPI doesn't return the version information by some reason... if (!colorModeOptions.includes(0x11)) { colorModeOptions.push(0x11); } return { writable: colorModeOptions.length > 0, value: decodeColorMode(colorMode), option: { type: 'enum', values: colorModeOptions.map(decodeColorMode), }, }; } async getColorTemperature() { const wb = await this.getWhiteBalance(); if (wb !== 'manual') return null; const { colorTemperature } = await this.getCamStatus(); return colorTemperature; } async setColorTemperature(value) { const r0 = await this.setWhiteBalance('manual'); if (r0.status !== 'ok') { return { status: 'general error' }; } return await this.setCamData(OpCodeSigma.SetCamDataGroup5, 1, value); } async getColorTemperatureDesc() { const { colorTemerature: range } = await this.getCamCanSetInfo(); const value = await this.getColorTemperature(); if (range.length !== 3) { // When WB is not set to 'manual' return { writable: false, value, }; } const [min, max, step] = range; return { writable: true, value, option: { type: 'range', min, max, step, }, }; } async setDestinationToSave(value) { const id = this.destinationToSaveTable.getKey(value); if (id === undefined) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup3, 15, id); } async getDestinationToSaveDesc() { const { destinationToSave } = await this.getCamStatus(); const value = this.destinationToSaveTable.get(destinationToSave) ?? null; return { writable: true, value, option: { type: 'enum', values: ['uninitialized', 'camera', 'pc', 'camera,pc'], }, }; } async getExposureMode() { const { exposureMode } = await this.getCamStatus(); return this.exposureModeTable.get(exposureMode) ?? null; } async setExposureMode(exposureMode) { const id = this.exposureModeTable.getKey(exposureMode); if (!id) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup2, 2, id); } async getExposureModeDesc() { const { exposureMode } = await this.getCamCanSetInfo(); const value = await this.getExposureMode(); const values = exposureMode .map(n => this.exposureModeTable.get(n)) .filter(isntNil); return { writable: values.length > 0, value, option: { type: 'enum', values, }, }; } async getExposureComp() { const { exposureComp } = await this.getCamStatus(); return this.compensationOneThirdTable.get(exposureComp) ?? null; } async setExposureComp(value) { const id = this.compensationOneThirdTable.getKey(value); if (id === undefined) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup1, 5, id); } async getExposureCompDesc() { const { exposureComp: range } = await this.getCamCanSetInfo(); const value = await this.getExposureComp(); if (range.length < 3 || (range[0] === 0 && range[1] === 0)) { return { writable: false, value, }; } const [min, max] = range; const allValues = [...this.compensationOneThirdTable.values()]; const values = allValues .map(v => [v, exposureCompStringToFloat(v)]) .sort((a, b) => a[1] - b[1]) .filter(([, n]) => min - 1e-4 <= n && n <= max + 1e-4) .map(([v]) => v); return { writable: true, value, option: { type: 'enum', values, }, }; function exposureCompStringToFloat(v) { if (v === '0') return 0x0; let negative = false, digits = 0, thirds = 0; const match1 = v.match(/^([+-]?)([0-9]+)( 1\/3| 2\/3)?$/); if (match1) { negative = match1[1] === '-'; digits = parseInt(match1[2]); thirds = !match1[3] ? 0 : match1[3] === ' 1/3' ? 1 : 2; } const match2 = !match1 && v.match(/^([+-]?)(1\/3|2\/3)$/); if (match2) { negative = match2[1] === '-'; thirds = match2[2] === '1/3' ? 1 : 2; } if (!match1 && !match2) { throw new Error(`Cannot parse ${v} as exposure compensation`); } return (negative ? -1 : 1) * (digits + thirds / 3); } } async getFocalLengthDesc() { const { currentLensFocalLength } = await this.getCamStatus(); const value = decodeFocalLength(currentLensFocalLength); const { lensWideFocalLength, lensTeleFocalLength } = await this.getCamStatus(); const min = decodeFocalLength(lensWideFocalLength); const max = decodeFocalLength(lensTeleFocalLength); if (min === 0 && max === 0) { return UnsupportedConfigDesc; } return { writable: false, value, option: { type: 'range', min, max, step: 0, }, }; function decodeFocalLength(byte) { const integer = byte >> 4, fractional = byte & 0b1111; return integer + fractional / 10; } } async getFocusDistanceDesc() { const { focusPosition: range } = await this.getCamCanSetInfo(); const focusPosition = (await this.getCamStatus()).focusPosition[0]; // Small value means far direction, so invert the min/max and normalize it const value = scalar.invlerp(range[1], range[0], focusPosition); // Only writable when AF is on const writable = range.length === 2 && !!(await this.get('canRunAutoFocus')); if (writable) { return { writable, value, option: { type: 'range', min: 0, max: 1, step: 0, }, }; } else { return { writable, value, }; } } async setFocusDistance(value) { const { focusPosition: range } = await this.getCamCanSetInfo(); const focusPosition = scalar.round(scalar.lerp(range[1], range[0], value)); const data = encodeIFD({ focusPosition: { tag: 81, type: IFDType.Short, value: [focusPosition] }, }); try { await this.device.sendData({ label: 'SigmaFP SetCamDataGroupFocus', opcode: OpCodeSigma.SetCamDataGroupFocus, data, }); } catch (err) { return { status: 'invalid parameter' }; } return { status: 'ok' }; } async getFocusMeteringModeDesc() { const { focusArea: ids } = await this.getCamCanSetInfo(); const id = (await this.getCamStatus()).focusArea[0]; const value = this.focusAreaTable.get(id); const values = ids.map(n => this.focusAreaTable.get(n)); return { writable: values.length > 0, value, option: { type: 'enum', values: values, }, }; } async setFocusMeteringMode(value) { const id = this.focusAreaTable.getKey(value); const data = encodeIFD({ focusArea: { tag: 10, type: IFDType.Byte, value: [id] }, }); try { await this.device.sendData({ label: 'SigmaFP SetCamDataGroupFocus', opcode: OpCodeSigma.SetCamDataGroupFocus, data, }); } catch (err) { return { status: 'invalid parameter' }; } return { status: 'ok' }; } /* async getFocusPeakingDesc(): Promise<ConfigDesc<FocusPeaking>> { // TODO: There's no way to retrieve and configure this value in the latest firmware const value = 0 const {focusPeaking: values} = await this.getCamCanSetInfo5() return { writable: values.length > 0, value: this.focusPeakingTable.get(value) ?? false, option: { type: 'enum', values: values .sort() .map(v => this.focusPeakingTable.get(v)) as FocusPeaking[], }, } } async setFocusPeaking(focusPeaking: FocusPeaking): Promise<OperationResult> { const id = this.focusPeakingTable.getKey(focusPeaking) if (id === undefined) return {status: 'invalid parameter'} // const data = encodeIFD({ // focusPeaking: {tag: 702, type: IFDType.Byte, value: [id]}, // }) // try { // await this.device.sendData({ // label: 'SigmaFP SetCamDataGroupFocus', // opcode: OpCodeSigma.SetCamDataGroup, // data, // }) // } catch (err) { // return {status: 'invalid parameter'} // } return {status: 'ok'} }*/ async setImageAspect(imageAspect) { const id = this.imageAspectTable.getKey(imageAspect); if (id === undefined) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup5, 3, id); } async getImageAspectDesc() { const { imageAspect } = await this.getCamStatus(); const value = this.imageAspectTable.get(imageAspect) ?? null; const { imageAspect: values } = await this.getCamCanSetInfo(); return { writable: values.length > 0, value, option: { type: 'enum', values: values.map(v => this.imageAspectTableIFD.get(v)), }, }; } async setImageQuality(imageQuality) { let jpegQuality = null; let dngBitDepth = null; const pattern = imageQuality.match(/^raw (12|14)bit(?:,([a-z]+))?/i); if (pattern) { const [, dngBitDepthStr, jpegQualityStr] = pattern; jpegQuality = jpegQualityStr ?? null; dngBitDepth = parseInt(dngBitDepthStr); } else { jpegQuality = imageQuality; } // Generate imageQuality value for setCamData let imageQualityID; switch (jpegQuality) { case null: imageQualityID = 0x00; break; case 'fine': imageQualityID = 0x02; break; case 'standard': imageQualityID = 0x04; break; case 'low': imageQualityID = 0x08; break; default: return { status: 'invalid parameter' }; } imageQualityID |= dngBitDepth === null ? 0x00 : 0x10; const setImageQualityResult = (await this.setCamData(OpCodeSigma.SetCamDataGroup2, 15, imageQualityID)).status; let setDngBitDepthResult = 'ok'; if (dngBitDepth !== null) { setDngBitDepthResult = (await this.setCamData(OpCodeSigma.SetCamDataGroup4, 9, dngBitDepth)).status; } if (setImageQualityResult === 'ok' && setDngBitDepthResult === 'ok') { return { status: 'ok' }; } else { return { status: 'invalid parameter' }; } } async getImageQualityDesc() { const { imageQuality, dngImageQuality: dngBitDepth } = await this.getCamStatus(); let jpegQuality = null; switch (imageQuality & 0x0f) { case 0x02: jpegQuality = 'fine'; break; case 0x04: jpegQuality = 'standard'; break; case 0x08: jpegQuality = 'low'; break; } const hasDNG = !!(imageQuality & 0x10); const value = [hasDNG ? `raw ${dngBitDepth}bit` : null, jpegQuality] .filter(Boolean) .join(','); return { writable: true, value: value, option: { type: 'enum', values: [ // NOTE: Hard-coded so this might not work for some cases 'low', 'standard', 'fine', 'raw 12bit,fine', 'raw 14bit,fine', 'raw 12bit', 'raw 14bit', ], }, }; } async setImageSize(imageSize) { const id = this.imageSizeTable.getKey(imageSize); if (id === undefined) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup2, 14, id); } async getImageSizeDesc() { const { resolution } = await this.getCamStatus(); const { stillImageResolution } = await this.getCamCanSetInfo(); const value = this.imageSizeTable.get(resolution); const values = stillImageResolution.map(v => this.imageSizeTableIFD.get(v)); if (!value || values.length === 0) { return { writable: false, value: null, }; } return { writable: true, value, option: { type: 'enum', values, }, }; } async getIso() { const { isoAuto, isoSpeed } = await this.getCamStatus(); if (isoAuto === 0x01) { return 'auto'; } else { return this.isoTable.get(isoSpeed) ?? null; } } async setIso(iso) { if (iso === 'auto') { return this.setCamData(OpCodeSigma.SetCamDataGroup1, 3, 0x1); } const id = this.isoTable.getKey(iso); if (!id) return { status: 'invalid parameter' }; const setISOAutoResult = await this.setCamData(OpCodeSigma.SetCamDataGroup1, 3, 0x0); const setISOValueResult = await this.setCamData(OpCodeSigma.SetCamDataGroup1, 4, id); if (setISOAutoResult.status === 'ok' && setISOValueResult.status === 'ok') { return { status: 'ok' }; } else { return { status: 'invalid parameter' }; } } async getIsoDesc() { const { isoManual } = await this.getCamCanSetInfo(); const value = await this.getIso(); const [svMin, svMax] = isoManual; const isoMin = Math.round(3.125 * 2 ** svMin); const isoMax = Math.round(3.125 * 2 ** svMax); const isos = [...this.isoTable.values()]; const values = isos.filter(a => typeof a === 'number' && isoMin <= a && a <= isoMax); values.unshift('auto'); return { writable: true, value, option: { type: 'enum', values, }, }; } async setLiveviewMagnifyRatio(value) { const id = this.liveviewMagnifyRatioTable.getKey(value); if (!id) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup4, 5, id); } async getLiveviewMagnifyRatioDesc() { const { lvMagnifyRatio } = await this.getCamStatus(); const value = this.liveviewMagnifyRatioTable.get(lvMagnifyRatio) ?? null; const { lvMagnificationRate: values } = await this.getCamCanSetInfo(); return { writable: values.length > 0, value, option: { type: 'enum', values, }, }; } async getShutterSpeed() { const { shutterSpeed } = await this.getCamStatus(); if (shutterSpeed === 0x0) return 'auto'; return (this.shutterSpeedOneThirdTable.get(shutterSpeed) ?? this.shutterSpeedHalfTable.get(shutterSpeed) ?? null); } async setShutterSpeed(ss) { const byte = this.shutterSpeedOneThirdTable.getKey(ss); if (!byte) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup1, 0, byte); } async getShutterSpeedDesc() { const { shutterSpeed: range, notApexShutterSpeed } = await this.getCamCanSetInfo(); const value = await this.getShutterSpeed(); if (range.length < 3) { return { writable: false, value, }; } const [tvMin, tvMax, step] = range; const isStepOneThird = Math.abs(step - 1 / 3) < Math.abs(step - 1 / 2); const table = isStepOneThird ? this.shutterSpeedOneThirdTable : this.shutterSpeedHalfTable; const shutterSpeeds = Array.from(table.entries()).filter(e => e[1] !== 'sync' && e[1] !== 'bulb'); const ssMinRaw = 1 / 2 ** tvMin; const ssMaxRaw = 1 / 2 ** tvMax; const ssMinEntry = minBy(shutterSpeeds, e => Math.abs(computeShutterSpeedSeconds(e[1]) - ssMinRaw)); const ssMaxEntry = minBy(shutterSpeeds, e => Math.abs(computeShutterSpeedSeconds(e[1]) - ssMaxRaw)); if (!ssMinEntry || !ssMaxEntry) { throw new Error('Invalid shutter speed range'); } const ssMinIndex = ssMinEntry[0]; const ssMaxIndex = ssMaxEntry[0]; const values = shutterSpeeds .filter(e => ssMinIndex <= e[0] && e[0] <= ssMaxIndex) .map(e => e[1]); if (notApexShutterSpeed.includes(0)) { values.unshift('bulb'); } return { writable: values.length > 0, value, option: { type: 'enum', values, }, }; } async getShutterSoundDesc() { const value = (await this.getCamStatus()).shutterSound - 0x02; const { shutterSound: [min, max, step], } = await this.getCamCanSetInfo(); return { writable: true, value, option: { type: 'range', min, max, step, }, }; } async setShutterSound(value) { const id = value + 0x02; return await this.setCamData(OpCodeSigma.SetCamDataGroup4, 13, id); } async getWhiteBalance() { const { whiteBalance } = await this.getCamStatus(); return this.whiteBalanceTable.get(whiteBalance) ?? null; } async setWhiteBalance(wb) { const id = this.whiteBalanceTable.getKey(wb); if (!id) return { status: 'invalid parameter' }; return this.setCamData(OpCodeSigma.SetCamDataGroup2, 13, id); } async getWhiteBalanceDesc() { const { whiteBalance } = await this.getCamCanSetInfo(); const value = await this.getWhiteBalance(); const values = whiteBalance .map(v => this.whiteBalanceTableIFD.get(v)) .filter(isntNil); return { writable: values.length > 0, value, option: { type: 'enum', values, }, }; } // Actions async takePhoto({ doDownload = true } = {}) { if (this.#isCapturing) { return { status: 'busy' }; } this.#isCapturing = true; const succeedSnapCommand = await this.executeSnapCommand(SnapCaptureMode.NonAFCapture, 1); if (!succeedSnapCommand) return { status: 'general error' }; const picts = []; if (doDownload) { const CHUNK_SIZE = 0x00200000; // SampleApp uses this const pictFileInfos = await this.getPictFileInfo2(); const totalSize = pictFileInfos.reduce((p, i) => p + i.fileSize, 0); let loaded = 0; this.emit('progress', 0); for await (const info of pictFileInfos) { const pictArray = new Uint8Array(info.fileSize); // Download the image with splitting every 2MB chunk for (let offset = 0; offset < info.fileSize; offset += CHUNK_SIZE) { const length = Math.min(info.fileSize - offset, CHUNK_SIZE); const { data } = await this.device.receiveData({ label: 'SigmaFP GetBigPartialPictFile', opcode: OpCodeSigma.GetBigPartialPictFile, parameters: [info.fileAddress, offset, length], // Added 64 bytes for safety maxByteLength: CHUNK_SIZE + 64, }); // First 4 bytes is the length of buffer so splice them const chunkArray = new Uint8Array(data.slice(4, 4 + length)); loaded += length; this.emit('progress', { progress: loaded / totalSize }); // Copy to buffer pictArray.set(chunkArray, offset); } const isRaw = /dng/i.test(info.fileExt); const format = isRaw ? 'raw' : 'jpeg'; const type = isRaw ? 'image/x-adobe-dng' : 'image/jpeg'; const blob = new Blob([pictArray.buffer], { type }); picts.push({ format, blob, filename: info.fileName, }); } } await this.clearImageDBAll(); this.#isCapturing = false; return { status: 'ok', value: picts }; } async runAutoFocus() { const startAFSucceed = await this.executeSnapCommand(SnapCaptureMode.StartAF); if (!startAFSucceed) { await this.executeSnapCommand(SnapCaptureMode.StopAF); return { status: 'general error' }; } const stopAFSucceed = await this.executeSnapCommand(SnapCaptureMode.StopAF); if (!stopAFSucceed) { return { status: 'general error' }; } return { status: 'ok' }; } async #getLiveViewImage() { const { resCode, data } = await this.device.receiveData({ label: 'SigmaFP GetViewFrame', opcode: OpCodeSigma.GetViewFrame, expectedResCodes: [ResCode.OK, ResCode.DeviceBusy], maxByteLength: 1_000_000, // = 1MB }); if (resCode === ResCode.DeviceBusy) return { status: 'busy' }; // Might be quirky but somehow works const jpegData = data.slice(10); return { status: 'ok', value: new Blob([jpegData], { type: 'image/jpg' }), }; } #canvasMediaStream = new CanvasMediaStream(); #liveviewStream = null; async startLiveview() { const stream = await this.#canvasMediaStream.begin(this.#updateLiveviewFrame); this.device.on('idle', this.#updateLiveviewFrame); this.#liveviewStream = stream; this.emit('liveviewChange', readonlyConfigDesc(stream)); return { status: 'ok', value: stream }; } #updateLiveviewFrame = async () => { const lvImage = await this.#getLiveViewImage(); if (lvImage.status !== 'ok') return; const bitmap = await createImageBitmap(lvImage.value); this.#canvasMediaStream.updateWithImage(bitmap); }; async getLiveview() { return this.#liveviewStream; } async stopLiveview() { this.#canvasMediaStream.end(); this.device.off('idle', this.#updateLiveviewFrame); this.#liveviewStream = null; this.emit('liveviewChange', readonlyConfigDesc(null)); return { status: 'ok' }; } async startRec() { const captureMode = SnapCaptureMode.StartRecordingMovieWithoutAF; const captureAmount = 1; const snapState = new Uint8Array([captureMode, captureAmount]).buffer; await this.device.sendData({ label: 'Sigma SnapCommand', opcode: OpCodeSigma.SnapCommand, data: this.encodeParameter(snapState), }); return { status: 'ok' }; } async stopRec() { const captureMode = SnapCaptureMode.StopRecordingMovie; const captureAmount = 1; const snapState = new Uint8Array([captureMode, captureAmount]).buffer; await this.device.sendData({ label: 'Sigma SnapCommand', opcode: OpCodeSigma.SnapCommand, data: this.encodeParameter(snapState), }); // const movieFileInfo = await this.getMovieFileInfo() return { status: 'ok' }; } // NOTE: WIP async startBulb() { if ((await this.getShutterSpeed()) !== 'bulb') { return { status: 'general error' }; } const captureMode = SnapCaptureMode.NonAFCapture; const captureAmount = 1; const snapState = new Uint8Array([captureMode, captureAmount]).buffer; await this.device.sendData({ label: 'Sigma SnapCommand', opcode: OpCodeSigma.SnapCommand, data: this.encodeParameter(snapState), }); return { status: 'ok' }; } // NOTE: WIP async endBulb() { const captureMode = SnapCaptureMode.StopCapture; const captureAmount = 1; const snapState = new Uint8Array([captureMode, captureAmount]).buffer; await this.device.sendData({ label: 'Sigma SnapCommand', opcode: OpCodeSigma.SnapCommand, data: this.encodeParameter(snapState), }); return { status: 'ok' }; } async getStorages() { // fp always returns one storage info even if no SD inserted. const [storage] = await super.getStorages(); const { mediaFreeSpace } = await this.getCamStatus(); storage.freeSpaceInImages = mediaFreeSpace; return [storage]; } // fp SDK async getMovieFileInfo() { const { data } = await this.device.receiveData({ label: 'Sigma GetMovieFileInfo', opcode: OpCodeSigma.GetMovieFileInfo, }); const dv = new PTPDataView(data); const movieFileInfo = { fileFormat: dv.goto(24).readAsciiString(), fileHandler: dv.goto(32).readUint32(), }; return movieFileInfo; } async getCamStatus() { const { data } = await this.device.receiveData({ label: 'SigmaFP GetCamStatus2', opcode: OpCodeSigma.GetCamStatus2, parameters: [0b10000, 0b1111111, 0b0], }); const decoded = decodeIFD(data, { camDataGroup1: { tag: OpCodeSigma.GetCamDataGroup1, type: IFDType.Undefined, }, camDataGroup2: { tag: OpCodeSigma.GetCamDataGroup2, type: IFDType.Undefined, }, camDataGroup3: { tag: OpCodeSigma.GetCamDataGroup3, type: IFDType.Undefined, }, camDataGroup4: { tag: OpCodeSigma.GetCamDataGroup4, type: IFDType.Undefined, }, camDataGroup5: { tag: OpCodeSigma.GetCamDataGroup5, type: IFDType.Undefined, }, camDataGroupFocus: { tag: OpCodeSigma.GetCamDataGroupFocus, type: IFDType.Undefined, }, camDataGroupMovie: { tag: OpCodeSigma.GetCamDataGroupMovie, type: IFDType.Undefined, }, camCanSetInfo5: { tag: OpCodeSigma.GetCamCanSetInfo5, type: IFDType.Undefined, }, }); const group1DataView = new PTPDataView(decoded.camDataGroup1); group1DataView.skip(3); // OC + FieldPreset const group1 = { shutterSpeed: group1DataView.readUint8(), aperture: group1DataView.readUint8(), programShift: group1DataView.readInt8(), isoAuto: group1DataView.readUint8(), isoSpeed: group1DataView.readUint8(), exposureComp: group1DataView.readUint8(), abValue: group1DataView.readUint8(), abSettings: group1DataView.readUint8(), frameBufferState: group1DataView.readUint8(), mediaFreeSpace: group1DataView.readUint16(), mediaStatus: group1DataView.readUint8(), currentLensFocalLength: group1DataView.readUint16(), batteryLevel: group1DataView.readUint8(), abShotRemainNumber: group1DataView.readUint8(), expCompExcludeAB: group1DataView.readUint8(), }; const group2DataView = new PTPDataView(decoded.camDataGroup2); group2DataView.skip(3); // OC + FieldPreset const group2 = { driveMode: group2DataView.readUint8(), specialMode: group2DataView.readUint8(), exposureMode: group2DataView.readUint8(), aeMeteringMode: group2DataView.readUint8(), whiteBalance: group2DataView.goto(3 + 10).readUint8(), resolution: group2DataView.readUint8(), imageQuality: group2DataView.readUint8(), }; const group3DataView = new PTPDataView(decoded.camDataGroup3); group3DataView.skip(3); // OC + FieldPreset const group3 = { contrast: group3DataView.readUint8(), sharpness: group3DataView.readUint8(), saturation: group3DataView.readUint8(), colorSpace: group3DataView.readUint8(), colorMode: group3DataView.readUint8(), batteryKind: group3DataView.readUint8(), lensWideFocalLength: group3DataView.readUint16(), lensTeleFocalLength: group3DataView.readUint16(), afAuxiliaryLight: group3DataView.readUint8(), afBeep: group3DataView.readUint8(), timerSound: group3DataView.readUint8(), destinationToSave: group3DataView.readUint8(), }; const group4DataView = new PTPDataView(decoded.camDataGroup4); group4DataView.skip(3); // OC + FieldPreset const group4 = { dcCropMode: group4DataView.readUint8(), lvMagnifyRatio: group4DataView.readUint8(), isoExtension: group4DataView.readUint8(), continuousShootingSpeed: group4DataView.readUint8(), hdr: group4DataView.readUint8(), dngImageQuality: group4DataView.readUint8(), fillLight: group4DataView.readUint8(), opticalDistortion: group4DataView.readUint8(), opticalAberration: group4DataView.readUint8(), opticalDiffraction: group4DataView.readUint8(), opticalLightIntensity: group4DataView.readUint8(), opticalColorShading: group4DataView.readUint8(), opticalColorShadingGet: group4DataView.readUint8(), imageStabilization: group4DataView.readUint8(), shutterSound: group4DataView.readUint8(), }; const group5DataView = new PTPDataView(decoded.camDataGroup5); group5DataView.skip(3); // OC + FieldPreset const group5 = { intervalTimerSecond: group5DataView.readUint16(), intervalTimerFame: group5DataView.readUint8(), restTimerSecond: group5DataView.readUint16(), restTimerFrame: group5DataView.readUint8(), colorTemperature: group5DataView.readUint16(), imageAspect: group5DataView.readUint8(), toneEffect: group5DataView.readUint8(), }; const groupFocus = decodeIFD(decoded.camDataGroupFocus, { focusMode: { tag: 1, type: IFDType.Byte }, afLock: { tag: 2, type: IFDType.Byte }, afFaceEyePriorMode: { tag: 3, type: IFDType.Byte }, afFaceEyePriorDetectionStatus: { tag: 4, type: IFDType.Byte }, focusArea: { tag: 10, type: IFDType.Byte }, onePointSelectionMethod: { tag: 11, type: IFDType.Byte }, distanceMeasurementFrameSize: { tag: 12, type: IFDType.Byte }, distanceMeasurementFramePosition: { tag: 13, type: IFDType.Undefined }, // distanceMeasurementFrame: {tag: 14, type: IFDType.Byte}, preAlwaysAf: { tag: 51, type: IFDType.Byte }, afLimit: { tag: 52, type: IFDType.Byte }, focusPosition: { tag: 81, type: IFDType.Short }, }); const setInfo = decodeIFD(decoded.camCanSetInfo5, { imageQuality: { tag: 11, type: IFDType.Byte }, dngImageQuality: { tag: 12, type: IFDType.Byte }, stillImageResolution: { tag: 20, type: IFDType.Byte }, imageAspect: { tag: 21, type: IFDType.Byte }, exposureMode: { tag: 200, type: IFDType.Byte }, fValue: { tag: 210, type: IFDType.SignedShort }, shutterSpeed: { tag: 212, type: IFDType.SignedShort }, notApexShutterSpeed: { tag: 213, type: IFDType.Byte }, isoManual: { tag: 215, type: IFDType.SignedShort }, exposureComp: { tag: 217, type: IFDType.SignedShort }, whiteBalance: { tag: 301, type: IFDType.Byte }, colorTemerature: { tag: 302, type: IFDType.Short }, colorMode: { tag: 320, type: IFDType.Byte }, focusMode: { tag: 600, type: IFDType.Byte }, focusArea: { tag: 610, type: IFDType.Byte }, onePointSelectionMethod: { tag: 611, type: IFDType.Byte }, focusOverallArea: { tag: 612, type: IFDType.Short }, focusValidArea: { tag: 613, type: IFDType.Short }, distanceMeasurementFrameSize: { tag: 614, type: IFDType.Byte }, eachDistanceMesasurementFrameSize: { tag: 615, type: IFDType.Short }, distanceMeasurementFrameMovementAmount: { tag: 616, type: IFDType.Byte }, focusPosition: { tag: 658, type: IFDType.Short }, lvImageTransfer: { tag: 700, type: IFDType.Byte }, lvMagnificationRate: { tag: 701, type: IFDType.B