├── 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 | 
6 | 
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 |
55 | Start the Bluetooth remote pair process
59 | on your camera.
60 |
61 |
62 | Set your camera
66 | to the "Self-timer: 10 sec/remote control" drive mode.
67 |
68 |
69 | Set your camera
73 | to the "Bulb" exposure mode.
74 |
75 |
76 | Press Connect and choose your
77 | camera among the shown devices.
78 |
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 |
--------------------------------------------------------------------------------