balena-dslr-js

A js tool to run on a pi zero w to control cameras via gphoto2
git clone git://archive.git.mtrnord.blog/balena-dslr/balena-dslr-js.git
Log | Files | Refs | README | LICENSE

commit 4b607beece2df806795e1e294d748b6fa433b5ca
parent 5fb5fa5f5b6ccbc20dda50a79043f15c1f5a23ae
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sat, 24 Jul 2021 21:58:42 +0200

Initial commit

Diffstat:
MDockerfile.template | 14+++++++++-----
MREADME.md | 17+++++++++++++++--
Mpackage.json | 12++++++++----
Asrc/bluetoothService.ts | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/camera.ts | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/index.ts | 66+++++++++++++++++++++++++++++++++++++++---------------------------
Asrc/utils.ts | 6++++++
7 files changed, 230 insertions(+), 38 deletions(-)

diff --git a/Dockerfile.template b/Dockerfile.template @@ -1,7 +1,11 @@ -FROM balenalib/%%BALENA_MACHINE_NAME%%-node +FROM balenalib/%%BALENA_MACHINE_NAME%%-node:12 + +ENV UDEV=on # Install dependencies -RUN install_packages libgphoto2-dev gphoto2 +RUN install_packages libgphoto2-dev gphoto2 python3 python2 build-essential uhubctl libudev-dev bluetooth bluez libbluetooth-dev libusb-1.0-0-dev + +RUN update-rc.d bluetooth remove # Defines our working directory in container WORKDIR /usr/src/app @@ -11,11 +15,10 @@ COPY package.json ./ # This install npm dependencies on the balena build server, # making sure to clean up the artifacts it creates in order to reduce the image size. -RUN JOBS=MAX npm install --production --unsafe-perm && npm cache verify +RUN JOBS=MAX npm install --unsafe-perm && npm cache verify # This will copy all files in our root to the working directory in the container COPY . ./ - RUN npm run build -CMD ["npm", "start"] +CMD npm start +\ No newline at end of file diff --git a/README.md b/README.md @@ -1,10 +1,22 @@ # balena-DSLR -You can use this project to remotely control your DSLR camera from the balenaCloud dashboard. +You can use this project to remotely control your DSLR camera via BLE using balena to manage the raspberry pi zero w. ## Hardware | Hardware required | Notes | | :--: | :--: | -| A Raspberry Pi | You can use any of the devices listed [here](https://www.balena.io/docs/reference/hardware/devices/) - as we are only interested in the USB functionality of the Pi here| +| A Raspberry Pi Zero W | You also need a way to connect your camera via usb and a phone capable of running Bluetooth Low Energy| | A DSLR camera | This project uses `gphoto2` to communicate with the cameras. See supported cameras [here](http://gphoto.org/proj/libgphoto2/support.php)| + +## Notes + +This project is still very early in development please report any bugs. + +There isnt yet a client available. You will need to manually send the commands to the BLE service. + +The project is only tested on a Canon EOS 4000D/Rebel T100. + +This project eventually is going to get a rust rewrite to reduce performance overhead. + +The images get saved on to the cameras sd card as downloading them with the nodejs lib does lock up my camera. (gphoto2 cli doesnt) +\ No newline at end of file diff --git a/package.json b/package.json @@ -13,12 +13,16 @@ "author": "", "license": "ISC", "dependencies": { - "@typedproject/gphoto2-driver": "^2.4.0", - "lodash": "^4.17.15", + "@typedproject/gphoto2-driver": "^3.0.3", + "bleno": "npm:@abandonware/bleno@^0.5.1-4", + "lodash": "^4.17.21", "segfault-handler": "^1.3.0", - "typescript": "^3.6.4" + "typescript": "^4.3.5", + "usb": "^1.7.1" }, "devDependencies": { - "@types/node": "^13.13.4" + "@types/bleno": "^0.4.2", + "@types/node": "^12.20.1", + "@types/usb": "^1.5.3" } } diff --git a/src/bluetoothService.ts b/src/bluetoothService.ts @@ -0,0 +1,57 @@ +import { PrimaryService, Characteristic } from 'bleno'; +import CameraActions from './camera'; +class BLEService extends Characteristic { + private _value: Buffer; + private _updateValueCallback: any; + private camera: CameraActions; + + constructor() { + super({ + uuid: '14444444444444444444444444444448', + properties: ['write', 'notify'], + value: null + }); + this.camera = new CameraActions(); + this._value = Buffer.alloc(0); + this._updateValueCallback = null; + } + + onWriteRequest(data: Buffer, _offset: number, _withoutResponse: boolean, callback: (result: number) => void) { + this._value = data; + console.log(this._value.toString()); + // TODO use json instead + switch (this._value.toString()) { + case "capture": + this.camera.capture( + false, + false, + true + ); + break; + case "timelapse": + this.camera.run_timelapse( + 20000, + 1500, + false + ); + break; + } + + + if (this._updateValueCallback) { + this._updateValueCallback(this._value); + } + + callback(Characteristic.RESULT_SUCCESS); + } + + onSubscribe(_maxValueSize: number, updateValueCallback: any) { + this._updateValueCallback = updateValueCallback; + } + + onUnsubscribefunction() { + this._updateValueCallback = null; + } +} + +export const bleService = new PrimaryService({ uuid: '14444444444444444444444444444447', characteristics: [new BLEService()] }) +\ No newline at end of file diff --git a/src/camera.ts b/src/camera.ts @@ -0,0 +1,94 @@ +import { Camera, run } from '@typedproject/gphoto2-driver'; +import { sleep } from './utils'; + +export default class CameraActions { + private camera: Camera; + constructor() { + this.camera = Camera.listen(); + } + /** + * Do various capture actions + * + * @param autoFocus Should the camera start to autofocus? (May never finish and lock up camera!) + * @param preview Capture a preview image and save it to the sd card? + * @param capture Save a image in full quality to the sd card + */ + public capture(autoFocus = false, preview = false, capture = false) { + if (this.camera.isClosed()) { + this.camera = Camera.listen(); + } + return run(async () => { + await sleep(2000); + + if (autoFocus) { + console.info("Autofocus ============================="); + this.camera.autoFocus(); + + await sleep(20000); + } + + if (preview) { + console.info("Preview ==============================="); + + this.camera.widgets.apply({ + "/settings/capturetarget": "Memory card", + }); + + const res = await this.camera.capturePreviewAsync(); + res?.close(); + + console.info("File saved"); + } + + if (capture) { + console.info("Capture ==============================="); + + this.camera.widgets.apply({ + "/settings/capturetarget": "Memory card", + }); + + const res = await this.camera.captureImageAsync(); + res?.close(); + console.info("File saved"); + } + }); + } + + /** + * Start a timelapse capture + * + * @param interval_ms The amount of time in between a capture being done in miliseconds + * @param frames The number of frames to overall take + * @param autoExpose If we should try to auto adjust for changing exposure + */ + public async run_timelapse(interval_ms: number, frames: number, autoExpose: boolean) { + if (this.camera.isClosed()) { + this.camera = Camera.listen(); + } + + let current_frame = 1; + + await sleep(2000); + return run(async () => { + while (current_frame <= frames) { + + console.info("Capture ==============================="); + + this.camera.widgets.apply({ + "/settings/capturetarget": "Memory card", + }); + + const res = await this.camera.captureImageAsync(); + res?.close(); + + console.info("File saved ============================"); + + if (autoExpose) { + // TODO change exposure to the next step if needed + } + + await sleep(interval_ms); + } + }); + } +} +\ No newline at end of file diff --git a/src/index.ts b/src/index.ts @@ -1,27 +1,39 @@ -import SegfaultHandler from 'segfault-handler'; -import path from 'path'; -import { CameraList, closeQuietly } from '@typedproject/gphoto2-driver'; - -SegfaultHandler.registerHandler('crash.log'); - -const cameraList = new CameraList().load(); - -console.log('Nb camera', cameraList.size); - -if (cameraList.size) { - const camera = cameraList.getCamera(0); - - - if (camera && !camera.isClosed()) { - console.log('Camera =>', camera); - - const cameraFile = camera.captureImage()!; - - cameraFile.save(path.join(__dirname, '../.tmp/capture.jpeg')); - - closeQuietly(cameraFile); - closeQuietly(camera); - } -} - -cameraList.close(); +import bleno from 'bleno'; +import { bleService } from './bluetoothService'; + + +const name = "CameraControlService" + +// +// Wait until the BLE radio powers on before attempting to advertise. +// If you don't have a BLE radio, then it will never power on! +// +bleno.on('stateChange', function (state) { + if (state === 'poweredOn') { + // + // We will also advertise the service ID in the advertising packet, + // so it's easier to find. + // + bleno.startAdvertising(name, [bleService.uuid], function (err) { + if (err) { + console.log(err); + } + }); + } + else { + bleno.stopAdvertising(); + } +}); + +bleno.on('advertisingStart', function (err) { + if (!err) { + console.log('advertising...'); + // + // Once we are advertising, it's time to set up our services, + // along with our characteristics. + // + bleno.setServices([ + bleService + ]); + } +}); diff --git a/src/utils.ts b/src/utils.ts @@ -0,0 +1,5 @@ +export function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} +\ No newline at end of file