tethr
Version:
Controlls USB-connected cameras, webcam, and smartphone camera from browser
1,245 lines (1,240 loc) • 66.2 kB
JavaScript
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