Skip to content
← Back to Blog ·

Supporting Sony Cameras in eclipseClick

Two SDKs, One Bridge, and a Lot of COM Interop

Adding Sony camera support turned into a two-bridge problem: Sony's official SDK doesn't support their most popular cameras, and making the fallback work on .NET 8 with trimming required rethinking COM interop from scratch.

SonyCamera SDKCOM InteropC++C#PTPWPDWindowsCMake

TL;DR

Sony's Camera Remote SDK (CrSDK) only supports a subset of their cameras. Older and budget models — ZV-E10, A7 III, A6400 — require a completely separate bridge using Windows Portable Devices (WPD) MTP extensions over PTP. Getting the fallback bridge's COM interop working on .NET 8 with trimming required multiple rewrites. Along the way, Sony testing exposed DeviceBusy retry gaps that affected every supported camera vendor.

Background

eclipseClick already supported Canon (via EDSDK) and Nikon (via the MAID SDK on Windows, libgphoto2 on macOS). The architecture uses a generic bridge protocol — each vendor runs as a child process communicating JSON-RPC 2.0 over stdio. Adding Sony was, in theory, just a matter of writing another bridge.

Sony provides the Camera Remote SDK (CrSDK) — a C++ SDK with a reasonably clean API, cross-platform support, and a free registration process. The catch is that it requires registration through Sony's developer portal and cannot be redistributed. That constraint shaped the entire CI pipeline.

What we didn't anticipate: a large fraction of Sony cameras — including some of their most popular mirrorless bodies — are not supported by CrSDK at all.

Challenge 1: A Proprietary SDK That Can't Ship in the Repo

Canon's EDSDK and Nikon's MAID SDK are both proprietary, but Sony's CrSDK has an additional constraint: it must not be distributed, even in a private repository. The SDK must be obtained directly from Sony after registration. This broke our standard CI approach of checking in prebuilt native dependencies.

The solution was to separate the Sony bridge into its own repository (sony-camera-bridge) and make the SDK optional at build time. The CMakeLists.txt detects whether the SDK is present and conditionally compiles the bridge:

if(EXISTS "${SONY_SDK_PATH}/include/CrDefines.h")
  message(STATUS "Sony CrSDK found at ${SONY_SDK_PATH}")
  target_compile_definitions(sony_bridge PRIVATE SONY_SDK_AVAILABLE)
  target_include_directories(sony_bridge PRIVATE "${SONY_SDK_PATH}/include")
else()
  message(WARNING "Sony CrSDK not found — building stub-only")
endif()

This lets the test suite build and run in any CI environment. A second CI workflow downloads the SDK from a private vendor repository (where we store it after registration) for builds that produce the actual shipping binary.

One compiler quirk worth noting: MSVC raised error C7744 on hex escape sequences in test strings. This is a strict-mode diagnostic triggered by multi-character hex sequences like \x8746 that MSVC interprets as ambiguous. The fix is to break the string or use a named constant rather than an inline literal.

Challenge 2: CrSDK Doesn't Support Older Models

After shipping the CrSDK bridge, early beta testers immediately reported that their cameras weren't being detected. The affected models included the ZV-E10, A7 III, A6400, and several other popular bodies — all excluded from Sony's SDK compatibility list.

These cameras still support PTP (Picture Transfer Protocol) — the ISO standard for camera control over USB. On Windows, PTP access without a custom driver is available through the Windows Portable Devices (WPD) API, which exposes MTP extensions for PTP commands.

This became the sony-ptp-bridge: a new C# console app that uses WPD COM interop to issue raw PTP opcodes to Sony cameras and exposes the same JSON-RPC 2.0 interface as the CrSDK bridge. The eclipseClick integration layer uses switchBridge() at runtime to fall through from CrSDK to PTP if the camera isn't detected:

// SonyCameraService — runtime fallback
async connect(): Promise<boolean> {
  const primary = await super.connect();
  if (!primary) {
    // CrSDK didn't find the camera — try PTP fallback
    await this.switchBridge(ptpBridgeConfig);
    return await super.connect();
  }
  return true;
}

One early bug in the fallback: when SonyPtpBridge.exe was missing from the installation, the retry loop spun endlessly — calling connect() over and over with no circuit breaker. The fix was to check for the executable's presence before attempting the fallback, failing fast with a clear error message if it's not found.

Challenge 3: COM Interop on .NET 8 With Trimming

WPD is a COM API. Calling it from .NET requires COM interop — and this is where the sony-ptp-bridge consumed most of its development time.

The initial implementation used System.Runtime.InteropServices with manually-declared COM interface wrappers. This worked in debug builds. When published as a self-contained executable with trimming enabled — which is required to keep the binary size manageable — the COM marshalling infrastructure was trimmed away. The result was runtime crashes with no useful error messages, because the trimming happened silently at publish time.

The fix was to switch to the BuiltInComInteropSupport property in the project file, which preserves the COM interop infrastructure through trimming:

<!-- SonyPtpBridge.csproj -->
<PropertyGroup>
  <TargetFramework>net8.0-windows</TargetFramework>
  <PublishTrimmed>true</PublishTrimmed>
  <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>

This one property preserves the full COM marshalling layer. The tradeoff is a slightly larger binary, but for a bridge executable that users download once, this is the right call.

The COM interop rewrite also required rewriting the corresponding tests. The original tests used Moq to mock the COM interface wrapper classes directly, which was tightly coupled to the implementation type. After switching to BuiltInComInteropSupport, the COM types are generated differently and the old mocks no longer compiled. We rewrote those tests to mock at the service boundary instead, which turned out to be a cleaner design regardless.

The final test count: 239 tests across 9 source files, with xUnit and FluentAssertions.

Challenge 4: DeviceBusy Errors During Bracket Sequences

Eclipse photography involves rapid bracket sequences — multiple exposures taken within a few seconds of totality. Both bridges needed to handle the case where a command arrives before the camera has finished processing the previous one.

Sony's CrSDK returns CrError_Adaptor_DeviceBusy (error code 0x8746) when this happens during a capture. The PTP protocol uses response code 0x2019 (Device Busy) for the same condition. Neither had retry logic initially.

For the CrSDK bridge, we wrapped the capture call in a retry loop:

// sony_controller.cpp
static CrError capture_with_retry(CrDeviceHandle handle, int max_attempts = 5) {
    for (int attempt = 0; attempt < max_attempts; ++attempt) {
        CrError err = SDK::SendCommand(handle, SDK::CrCommandId_Release, 0);
        if (err != CrError_Adaptor_DeviceBusy) return err;
        std::this_thread::sleep_for(std::chrono::milliseconds(200 * (attempt + 1)));
    }
    return CrError_Adaptor_DeviceBusy;
}

The same pattern — wrapping the operation in ExecuteWithBusyRetry — was applied to SetPropertyRaw in the PTP bridge. Property setting is just as likely to hit a busy camera as capture commands, especially when the bracket sequence changes ISO or shutter speed between shots.

While adding this retry logic, we audited the other vendors. Canon's bridge already had DeviceBusy retry built into the EDSDK layer. Nikon's MAID bridge did not — it was silently failing property sets under rapid fire. We added the same retry wrapper there too. Sony testing had exposed a latent cross-vendor bug.

Cross-vendor issues found during Sony testing

  • DSUSB trigger only asserted BIT_SHUTTER (0x01), not BIT_FOCUS (0x02). Some Nikon bodies and Sony bodies require both bits to trigger reliably.
  • Burst grouping threshold was 2000ms — aggressive enough to misgroup shots from the totality sequence into the same bracket set.
  • DSUSB bracket exposure wait was a fixed 100ms hold + 400ms delay, which is insufficient for exposures longer than a fraction of a second. Long-exposure bracket shots were being cut short.
  • Execution logs didn't identify which camera body produced each log entry, making multi-camera session debugging nearly impossible.

The Final Architecture

The two Sony bridges sit behind a single SonyCameraService that extends NativeSdkCameraService. The service tries CrSDK first; if the camera isn't detected within the connection timeout, it calls switchBridge() to swap to the PTP bridge at runtime without restarting the application.

Sony bridge selection (Windows)

SonyCameraService
├── Primary: SonyBridge.exe
│   └── CrSDK (C++)
│       Supported models only
│       backend: 'sony_remote'
└── Fallback: SonyPtpBridge.exe
    └── WPD MTP Extensions (C#)
        All PTP-capable models
        backend: 'sony_ptp'

Sony bridge selection (macOS)

SonyCameraService
├── Primary: SonyBridge (C++)
│   └── CrSDK (universal binary)
│       Supported models only
│       backend: 'sony_remote'
└── Fallback: GPhoto2Bridge
    └── libgphoto2 (C++)
        All PTP-capable models
        backend: 'gphoto2'

Both bridges speak the same JSON-RPC 2.0 protocol over stdio. The VendorBridgeClient layer handles process lifecycle, protocol framing, and reconnection — the camera service layer sees only the logical API.

The installer bundles both bridges: sony-bridge/ contains the CrSDK binary and its DLLs; sony-ptp-bridge/ contains the self-contained .NET 8 executable. Both are listed in electron-builder.yml under extraResources.

Cross-Platform Builds and macOS Universal Binaries

Sony's CrSDK ships platform-specific libraries. On Windows it's a set of DLLs; on macOS it's frameworks; on Linux it's shared objects. The CMake build uses conditional compilation to link the right set:

if(WIN32)
  target_link_libraries(sony_bridge "${SONY_SDK_PATH}/lib/x64/Cr_Core.lib")
elseif(APPLE)
  target_link_libraries(sony_bridge
    "-framework Cr_Core"
    "-framework CrAdapter")
else()
  target_link_libraries(sony_bridge
    "${SONY_SDK_PATH}/lib/linux/libCr_Core.so")
endif()

For macOS, CI produces both x64 and arm64 builds in separate steps. The Sony CrSDK frameworks ship as universal binaries, so the bridge itself just needs to be compiled for each architecture separately and then combined with lipo. This matches the pattern used for the Nikon gPhoto2 bridge on macOS.

One macOS-specific gotcha: the darwin-x64 prebuilds directory was missing from the initial integration. eclipseClick's main process tried to resolve the Sony bridge path for darwin-x64 and threw because the directory didn't exist. The fix was straightforward — add the missing directory and update the resolveSpawn() path resolution — but it only appeared during macOS CI, not on the developer's Apple Silicon machine where the arm64 path was present.

What Each Bridge Supports

Bridge Technology Camera Coverage Platform
SonyBridge.exe CrSDK (C++) Alpha flagships, recent bodies Windows, macOS, Linux
SonyPtpBridge.exe WPD MTP / PTP (C#) ZV-E10, A7 III, A6400, older bodies Windows only
GPhoto2Bridge libgphoto2 (C++) Any PTP-capable Sony body macOS, Linux

Lessons Learned

  1. 1

    Read the SDK compatibility matrix first. Sony's developer portal lists supported camera models. Reading it before writing a line of code would have revealed the two-bridge problem from the start.

  2. 2

    COM interop and .NET trimming don't mix without explicit configuration. PublishTrimmed silently removes COM marshalling infrastructure. BuiltInComInteropSupport is not a default — you have to opt in.

  3. 3

    CI gaps surface on macOS. Missing darwin-x64 prebuilds only appear if CI actually runs on Intel macOS. Developer machines are Apple Silicon, so the bug was invisible locally.

  4. 4

    DeviceBusy is a cross-vendor problem. Every camera SDK has its own busy error code, and all of them need retry logic for rapid command sequences. Audit all vendors when you add retry to one.

  5. 5

    Test coverage prevents regression across the fallback chain. With 239 tests covering the PTP bridge in isolation, we caught the missing fallback circuit breaker before it reached users. Integration tests at the bridge level are worth the investment.

  6. 6

    Make the fallback fail fast. A missing executable in the fallback path should produce an immediate, clear error — not a retry loop. Users need to know which bridge is missing and why, not watch the app spin.