├── CNAME ├── jsconfig.json ├── LICENSE ├── README.md ├── .gitignore ├── index.js └── index.html /CNAME: -------------------------------------------------------------------------------- 1 | eos.rreverser.com -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeAcquisition": { 3 | "include": ["web-bluetooth"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ingvar Stepanyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial Web Bluetooth remote for Canon cameras 2 | 3 | This is a [Web Bluetooth](https://developer.chrome.com/articles/bluetooth/)-based implementation of remote control for Canon EOS cameras based on [prior reverse engineering work](https://iandouglasscott.com/2017/09/04/reverse-engineering-the-canon-t7i-s-bluetooth-work-in-progress/) by [Ian Douglas Scott](https://fosstodon.org/@ids1024). 4 | 5 | ![Screenshot of the connection screen showing my Canon EOS250D camera in the list.](https://user-images.githubusercontent.com/557590/222991209-e7f1dc7d-d11f-4f70-8019-39f530ceed1b.png) 6 | ![Screenshot of the Intervalometer screen, showing settings for number of shots, exposure time and delay between shots followed by a disabled button that says "Shooting 3/10" and a progress bar below.](https://user-images.githubusercontent.com/557590/222991212-f05f743c-db3d-44a7-92eb-25d7e5492d4a.png) 7 | 8 | ## Disclaimers 9 | 10 | First of all, this project is in no way affiliated with Canon and you use it at your own risk. 11 | 12 | It mostly works, but I'm having an issue where, once camera goes to sleep, it can't connect again and needs to be unpaired, power-cycled & paired again - still not sure whether that's just my camera or the implementation should be doing some kind of regular pings to keep connection alive. 13 | 14 | ## See also 15 | 16 | [web-gphoto2](https://github.com/GoogleChromeLabs/web-gphoto2) - my other project controlling arbitrary cameras over the cable, powered by gphoto2, WebAssembly and WebUSB. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const BUTTON_RELEASE = 0b10000000, 2 | BUTTON_FOCUS = 0b01000000, 3 | BUTTON_TELE = 0b00100000, 4 | BUTTON_WIDE = 0b00010000, 5 | MODE_IMMEDIATE = 0b00001100, 6 | MODE_DELAY = 0b00000100, 7 | MODE_MOVIE = 0b00001000; 8 | 9 | const UUID_SERVICE = '00050000-0000-1000-0000-d8492fffa821', 10 | UUID_PAIR = '00050002-0000-1000-0000-d8492fffa821', 11 | UUID_SHOOT = '00050003-0000-1000-0000-d8492fffa821'; 12 | 13 | function encodePairCommand(name) { 14 | let result = new Uint8Array(name.length + 1); 15 | result[0] = 3; 16 | for (let i = 0; i < name.length; i++) { 17 | result[i + 1] = name.charCodeAt(i); 18 | } 19 | return result; 20 | } 21 | 22 | const pairCmd = encodePairCommand('Web'); 23 | 24 | function timeout(sec) { 25 | return new Promise(resolve => setTimeout(resolve, sec * 1000)); 26 | } 27 | 28 | /** @type {BluetoothDevice} */ 29 | let device; 30 | 31 | /** @type {BluetoothRemoteGATTCharacteristic} */ 32 | let shootCharacteristic; 33 | 34 | function onConnectionChange() { 35 | let isConnected = !!device; 36 | connectGroup.classList.toggle('hidden', isConnected); 37 | shootGroup.classList.toggle('hidden', !isConnected); 38 | } 39 | 40 | addEventListener('beforeunload', () => { 41 | if (device) { 42 | device.gatt.disconnect(); 43 | } 44 | }); 45 | 46 | connectBtn.onclick = async () => { 47 | connectBtn.disabled = true; 48 | try { 49 | device = await navigator.bluetooth.requestDevice({ 50 | filters: [ 51 | { 52 | services: [UUID_SERVICE] 53 | } 54 | ] 55 | }); 56 | await device.gatt.connect(); 57 | await timeout(1); 58 | device.addEventListener( 59 | 'gattserverdisconnected', 60 | () => { 61 | device = undefined; 62 | onConnectionChange(); 63 | }, 64 | { 65 | once: true 66 | } 67 | ); 68 | let service = await device.gatt.getPrimaryService(UUID_SERVICE); 69 | let pairCharacteristic = await service.getCharacteristic(UUID_PAIR); 70 | await pairCharacteristic.writeValue(pairCmd); 71 | shootCharacteristic = await service.getCharacteristic(UUID_SHOOT); 72 | onConnectionChange(); 73 | } catch (e) { 74 | alert(e); 75 | device = undefined; 76 | } finally { 77 | connectBtn.disabled = false; 78 | } 79 | }; 80 | 81 | async function pressBtn() { 82 | console.log('Pressing release button'); 83 | await shootCharacteristic.writeValue( 84 | Uint8Array.of(BUTTON_RELEASE | MODE_IMMEDIATE) 85 | ); 86 | console.log('Releasing release button'); 87 | await shootCharacteristic.writeValue(Uint8Array.of(MODE_IMMEDIATE)); 88 | } 89 | 90 | shootBtn.onclick = async () => { 91 | shootBtn.disabled = true; 92 | progressBar.style.visibility = 'visible'; 93 | 94 | try { 95 | let number = numberInput.valueAsNumber; 96 | let exposure = exposureInput.valueAsNumber; 97 | let delay = delayInput.valueAsNumber; 98 | let totalTime = exposure * number + delay * (number - 1); 99 | progressBar.value = 0; 100 | progressBar.max = totalTime; 101 | 102 | for (let i = 1; i <= number; i++) { 103 | if (i !== 1) { 104 | shootBtn.textContent = `Waiting after shot #${i}/${number}`; 105 | await timeout(delay); 106 | progressBar.value += delay; 107 | } 108 | shootBtn.textContent = `Shooting #${i}/${number}`; 109 | console.group(`Shot ${i}`); 110 | await pressBtn(); 111 | await timeout(exposure); 112 | await pressBtn(); 113 | progressBar.value += exposure; 114 | console.groupEnd(`Shot ${i}`); 115 | } 116 | } catch (e) { 117 | alert(e); 118 | } finally { 119 | shootBtn.textContent = 'Start'; 120 | shootBtn.disabled = false; 121 | } 122 | }; 123 | 124 | if (!navigator.bluetooth) { 125 | document.body.textContent = 126 | 'Web Bluetooth is not supported in this browser. Please use latest Chromium-based browser.'; 127 | } 128 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Intervalometer 5 | 9 | 14 | 36 | 37 | 38 |
39 |
40 | This is an unofficial 41 | Web Bluetooth-based intervalometer for Canon cameras by 44 | @RReverser.
45 |
46 | Check out the code and description on 47 | GitHub. 48 |
49 |
50 | If you're comfortable, feel free to try it out with your own 51 | Bluetooth-capable Canon camera 52 | (entirely at your own risk!):
53 |
    54 |
  1. 55 | Start the Bluetooth remote pair process 59 | on your camera. 60 |
  2. 61 |
  3. 62 | Set your camera 66 | to the "Self-timer: 10 sec/remote control" drive mode. 67 |
  4. 68 |
  5. 69 | Set your camera 73 | to the "Bulb" exposure mode. 74 |
  6. 75 |
  7. 76 | Press and choose your 77 | camera among the shown devices. 78 |
  8. 79 |
80 |

81 | Occasionally you might get pairing issues or reconnection issues, e.g. 82 | after camera got asleep. 83 |

84 |

85 | When that happens, I found it helpful to "Delete connection 86 | information" on the camera, restart it and go through the pairing 87 | process again. 88 |

89 |
90 | 123 |
124 | 125 | 126 | 127 | --------------------------------------------------------------------------------