Problem: Canon R8 shutter button is blocked when using gphoto2 PTP/USB connection (by design).
Solution: Canon CCAPI (Camera Control API) - WiFi-based HTTP REST API that allows manual shutter button usage.
Laptop (Bun Server)
├─ WiFi (wlan0): Connected to Camera WiFi
│ └─ IP: 192.168.1.x
│ └─ Routes: Camera CCAPI traffic (http://192.168.1.1:8080)
│
└─ USB (usb0): Connected to Phone (USB Tethering)
└─ IP: 192.168.42.x
└─ Routes: Internet traffic (WhatsApp/Baileys)
└─ Phone accesses webapp: http://192.168.42.x:3000
Benefits:
- Camera WiFi + Internet simultaneously (no switching)
- Phone webapp has continuous connection
- Phone charges via USB while providing internet
- Photographer presses camera shutter freely
- Canon R8 firmware v1.1.0+ (has CCAPI support)
- Register at Canon Developer Community (free)
- Download CCAPI activation tool
- Run activation tool with R8 connected via USB (one-time)
- Menu → WiFi Settings → Enable WiFi
- Set to Camera Access Point mode
- Camera creates WiFi network:
EOS_R8_XXXX - Default camera IP:
192.168.1.1 - CCAPI port:
8080
- Connect WiFi to
EOS_R8_XXXXnetwork - Enable USB tethering on phone
- Connect phone to laptop via USB
- Verify dual connectivity:
ip route # Should show: # default via 192.168.42.x dev usb0 (internet) # 192.168.1.0/24 dev wlan0 (camera)
GET http://192.168.1.1:8080/ccapi
# Returns camera info and capabilitiesGET http://192.168.1.1:8080/ccapi/ver100/event/polling?continue=offResponse when shutter pressed:
{
"addedcontents": [
{
"url": "/ccapi/ver100/contents/sd/IMG_1234.JPG",
"name": "IMG_1234.JPG"
}
]
}GET http://192.168.1.1:8080/ccapi/ver100/contents/sd/IMG_1234.JPG
# Returns binary JPEG dataOld (gphoto2 USB):
// Spawn process, parse output, multiple USB conflicts
spawn('gphoto2', ['--list-files', '--folder', path]);
spawn('gphoto2', ['--get-file', filename]);New (CCAPI WiFi):
// Simple HTTP REST calls
const response = await fetch(`http://192.168.1.1:8080/ccapi/ver100/event/polling?continue=off`);
const data = await response.json();
if (data.addedcontents) {
for (const content of data.addedcontents) {
const photoUrl = `http://192.168.1.1:8080${content.url}`;
const response = await fetch(photoUrl);
const buffer = await response.arrayBuffer();
await writeFile(localPath, Buffer.from(buffer));
}
}src/services/camera.ts:
- Remove:
spawn()calls, path discovery, USB locking - Add: HTTP fetch-based polling
- Keep: Reconnection logic, download queue, Socket.io events
- Replace:
checkCameraAvailable()→ HTTP ping to camera - Polling interval: 1.5s (same as before)
Key simplifications:
- No more
discoverCameraPath()(fixed IP: 192.168.1.1) - No more
listCameraFiles()parsing - No more USB conflicts
- No more
--list-files,--get-filecomplexity
- Turn on Canon R8 (WiFi AP auto-starts)
- Laptop auto-connects to camera WiFi + USB tethered to phone
- Open webapp on phone:
http://192.168.42.x:3000 - Start session → CCAPI polling begins
- Press camera shutter button → Photo captured
- CCAPI detects new file (1-2s delay)
- Downloads via WiFi (2-3s)
- Appears in webapp grid (~3-5s total)
- Select favorites, click Send
- WhatsApp sends via USB internet (no network switching!)
- Reset for next subject
- Shutter press → Detection: 1-2s
- Download 24MP JPEG: 2-3s
- Total: 3-5 seconds (acceptable for street photography)
- Register Canon Developer Community
- Download CCAPI activation tool
- Activate Canon R8 CCAPI
- Configure R8 WiFi AP mode
- Test manual CCAPI endpoints (curl/Postman)
- Refactor
camera.tsto use HTTP fetch - Test dual network setup (WiFi + USB)
- Verify shutter button works during CCAPI connection
- Test reconnection when camera powers off/on
- Field test: 10+ photo shoot session
If camera IP changes or multiple cameras:
// Use mDNS/Bonjour
import Bonjour from 'bonjour';
const bonjour = Bonjour();
bonjour.find({ type: 'ccapi' }, (service) => {
console.log('Found camera:', service.host);
});- HTTP 404: Camera not ready, retry
- Timeout: Camera powered off, trigger reconnection
- Network unreachable: WiFi disconnected, attempt reconnect
- Canon cameras may sleep and disconnect WiFi after inactivity
- Solution: Keep CCAPI polling alive to prevent sleep
- Alternative: Disable camera auto-sleep in menu
- CCAPI Docs: https://developercommunity.usa.canon.com/s/article/Introduction-to-Camera-Control-API-CCAPI
- Canomate (Python reference): https://github.com/horshack-dpreview/Canomate
- Node.js CCAPI library: https://github.com/camerahacks/canon-ccapi-node
- Canon R8 Firmware: https://www.canon.com/support (ensure v1.1.0+)
| Aspect | gphoto2 (Old) | CCAPI (New) |
|---|---|---|
| Connection | USB | WiFi |
| Shutter button | ❌ Blocked | ✅ Works |
| Canon R8 support | ❌ Not official | ✅ Official |
| Code complexity | 🔴 High (spawn, parse) | 🟢 Low (HTTP fetch) |
| USB conflicts | ❌ Yes | ✅ None |
| Internet access | ✅ Direct | ✅ Via USB tethering |
| Latency | ~1.5s (if worked) | ~3-5s |
| Setup | 🟢 Plug & play | 🟡 One-time activation |
| Reliability | ❌ R8 not supported | ✅ Designed for this |
Verdict: CCAPI is the correct solution for Canon R8 + manual shutter workflow.