Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
run: dotnet test CanonSDK.sln --configuration Release --no-build

- name: Publish Canon.API as standalone executable
run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --runtime win-x64 --self-contained true -p:PublishSingleFile=true --output ./publish
run: dotnet publish Canon.API/Canon.API.csproj --configuration Release --framework net10.0-windows --runtime win-x64 --self-contained true -p:PublishSingleFile=true --output ./publish

- name: Create release package
run: |
Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@ Versions match the Git tags (`v1.0.0.11`) that trigger the release workflow.

## [Unreleased]

### Added
- `tools/Test-Camera.ps1` (with `Test-Camera.cmd`): runs the photo booth camera checks against the API (live view,
captures, flash changes during the live view, power cycle) and saves the pictures, the results and the API log.
- Linux x64 support: the `net10.0` target of `Canon.API` runs on Linux with the Linux version of the EDSDK
(`EDSDK/linux-x64/libEDSDK.so`, needs `libusb-1.0`). No automatic update and no release package on Linux yet.
- Debug logs (`Logging:LogLevel:Canon.Core` = `Debug`): every EDSDK event, and each step of a capture (UI lock, flash
settings, shutter button, transfer) with its result and duration.

### Changed
- `Canon.API` has two target frameworks: `dotnet run` and `dotnet publish` need `-f net10.0-windows` (Windows) or
`-f net10.0` (Linux). `Canon.Core` and the unit tests target `net10.0`.
- A failed UI unlock after setting the flash is logged as a warning; it was ignored.
- The problem title returned when the Canon EDSDK cannot be loaded now names both platforms: "Canon EDSDK could not be
loaded (EDSDK.dll or libEDSDK.so missing, wrong architecture, or libusb-1.0 missing on Linux)" (was "...(EDSDK.dll
missing or wrong architecture)"). Client-visible text.
- Regenerating `docs/openapi.json` at build time now uses
`dotnet build Canon.API -f net10.0 -t:Build -t:GenerateOpenApiDocuments` (Windows or Linux); the former
`-p:OpenApiGenerateDocumentsOnBuild=true` form generates nothing now that `Canon.API` has two target frameworks.

### Fixed
- The camera froze when a picture was taken while the live view ran, as in the photo booth: the shutter stayed busy
(503 "Camera busy"), no picture was returned, and the camera did not respond until it was turned off and on. Writing
the flash setting during the live view freezes the camera (EOS R100), and `Canon:ForceFlashFiring` wrote it before
every capture. It is now written when the camera connects, before the live view starts (once per live view session)
and before a capture taken without live view; `POST /flash` pauses the live view while it writes it. With
`Canon:ForceFlashFiring`, a value set with `POST /flash` therefore stays until the live view stops (it was set back to
Fire before every capture), and `GET /flash` returns the last value set while the live view runs.
- Linux: the application crashed when the camera was turned off or unplugged. The SDK closes the session itself then,
and releasing the camera closed it a second time, which crashed (`CLinuxPtpHelper::Terminate` from `EdsRelease`). On
Linux the camera object of a camera that is gone is now left to the SDK.
- The session of a camera that is gone (turned off, unplugged, or a call failed because it is disconnected) is no longer
closed before the camera is released, on Windows too, as in the Canon samples.
- Linux: a camera turned back on or plugged back in was not reconnected, because the SDK lists it a moment after
reporting it (about 1 s). The connection is now retried for about 10 s.

## [1.4.0.0] - 2026-09-30

### Added
Expand Down
19 changes: 11 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ CanonWebAPI is a .NET 10 web API for remotely controlling Canon DSLR and mirrorl

The solution consists of 4 projects:

- **Canon.API**: ASP.NET Core Web API (main entry point)
- **Canon.API**: ASP.NET Core Web API (main entry point); targets `net10.0-windows` (Windows release, AutoUpdater.NET) and `net10.0` (Linux, no automatic update)
- **Canon.Core**: Core library wrapping Canon EDSDK functionality
- **Canon.Core.Tests**: xUnit unit tests of Canon.Core (no camera needed, run on Linux too)
- **Canon.API.Tests**: xUnit unit tests of the pure parts of Canon.API, compiled from linked sources (Canon.API needs the Windows Desktop runtime)
- **Canon.API.Tests**: xUnit unit tests of the pure parts of Canon.API, compiled from the Canon.API sources so they do not depend on the API project and its packages

### Key Components

Expand All @@ -30,9 +30,10 @@ The solution consists of 4 projects:
### Dependencies

- Requires Canon EDSDK 13.20.21 **64-bit** DLLs (`EDSDK.dll`, `EdsImage.dll`, from `EDSDK_64/Dll` of the SDK) in the EDSDK folder
- Linux: `EDSDK/linux-x64/libEDSDK.so` (from `EDSDK/Library/x86_64` of the 13.20.21 Linux SDK), needs `libusb-1.0`
- Uses Serilog for logging
- Swagger/OpenAPI for API documentation
- Platform target: x64 (Windows only)
- Platform target: x64 (Windows; Linux through the `net10.0` target of Canon.API)

## Development Commands

Expand All @@ -43,7 +44,7 @@ dotnet build CanonSDK.sln

### Run API Server
```bash
dotnet run --project Canon.API
dotnet run --project Canon.API -f net10.0-windows # Linux: -f net10.0 --launch-profile http
```

### Run Unit Tests
Expand Down Expand Up @@ -79,7 +80,7 @@ The Canon.API project exposes these REST endpoints (OpenAPI at `/openapi/v1.json
- GET `/latestpicture` - Retrieve last captured image
- POST `/autofocus` - Trigger autofocus
- GET `/cameraname`, `/mode`, `/temperature`, `/status` - Camera information
- GET/POST `/flash` - "Flash firing" setting (forced to Fire before each capture by default)
- GET/POST `/flash` - "Flash firing" setting (forced to Fire by default, outside the live view)

Errors are problem details: 400 invalid value, 409 capture refused or setting locked by the shooting mode, 503 not connected/busy, 504 capture timeout.

Expand All @@ -88,7 +89,7 @@ Errors are problem details: 400 invalid value, 409 capture refused or setting lo
- The camera can be connected via USB before or after the application starts (automatic connection and reconnection)
- Canon EOS Utility must NOT be running (conflicts with EDSDK access)
- Requires compatible Canon camera with EDSDK support
- All projects target .NET 10 (`net10.0-windows`) with Windows-specific dependencies
- Canon.Core and the tests target `net10.0`; Canon.API targets `net10.0-windows` (release, AutoUpdater.NET) and `net10.0` (Linux). Publish the release with `-f net10.0-windows`
- Uses structured logging with Serilog (logs to console and `logs/canon-api.log` next to the executable; levels from `Logging:LogLevel`)
- Content root is the executable folder: settings files are read next to the executable whatever the working directory
- Operator settings go in `appsettings.Local.json`, never shipped: automatic updates overwrite `appsettings.json`
Expand All @@ -97,6 +98,8 @@ Errors are problem details: 400 invalid value, 409 capture refused or setting lo

## AutoUpdater Integration

Applies to the `net10.0-windows` target only: the `net10.0` target (Linux) has no automatic update.

The application includes AutoUpdater.NET with these configurations:
- Automatic version checking on startup
- No admin privileges required (`RunUpdateAsAdmin = false`)
Expand All @@ -122,9 +125,9 @@ The project uses GitHub Actions for automated releases (record every change in t
- Downloads go to memory streams; the progress callback is registered before `EdsDownload`
- Live view is started with `Evf_OutputDevice` and stopped when no client uses it
- "Device busy" answers are retried after ~500 ms, as in the Canon samples; only the shutter press is retried, the release is always sent
- Flash (`EdsCreateFlashSettingRef`, `Flash_Target`, `Flash_Firing`): the UI must be locked while setting it; the SDK only reports values set remotely, so the setting is forced before each capture (studio flash on the shoe in the photo booth)
- Flash (`EdsCreateFlashSettingRef`, `Flash_Target`, `Flash_Firing`): the UI must be locked while setting it; the SDK only reports values set remotely, so the setting is forced when the camera connects, before the live view starts and before a capture without live view (studio flash on the shoe in the photo booth). Never write it while the live view runs: the EOS R100 freezes (shutter busy until it is turned off and on); POST /flash pauses the live view
- Settings: `CameraSettingRules` (pure, tested) decides what is settable per AE mode; EdsGetPropertyDesc gives the accepted values (its `form`/`access` fields are reserved, always 0); an empty list means not settable now (as in the Canon samples). Set only values from that list (API reference 3.1.20)
- Memory management critical due to unmanaged EDSDK resources: release every ref (`EdsRelease`)
- Memory management critical due to unmanaged EDSDK resources: release every ref (`EdsRelease`), except, on Linux, a camera that is gone: the SDK has already closed its session and releasing it crashes (`CLinuxPtpHelper::Terminate`)

## Testing

Expand Down
2 changes: 2 additions & 0 deletions Canon.API.Tests/CameraExceptionHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class CameraExceptionHandlerTests
{ NotApplied(new EdsException(EDSDK.EDS_ERR_DEVICE_BUSY, "x")), 503 },
{ NotApplied(new EdsException(EDSDK.EDS_ERR_INVALID_DEVICEPROP_VALUE, "x")), 400 },
{ new InvalidOperationException(), 500 },
{ new DllNotFoundException("x"), 500 },
{ new BadImageFormatException("x"), 500 },
};

private static CameraSettingsException Refused(SettingErrorReason reason) =>
Expand Down
7 changes: 2 additions & 5 deletions Canon.API.Tests/Canon.API.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<!--
Canon.API needs the Windows Desktop runtime (AutoUpdater.NET): referencing it would make these tests Windows only.
The parts under test do not depend on it, so their sources are compiled here instead.
-->
<!-- The parts under test are compiled from the Canon.API sources, so these tests do not depend on the API project and its packages. -->
<ItemGroup>
<Compile Include="..\Canon.API\Infrastructure\CameraExceptionHandler.cs" Link="Linked\CameraExceptionHandler.cs" />
<Compile Include="..\Canon.API\Infrastructure\LogLevels.cs" Link="Linked\LogLevels.cs" />
Expand Down
50 changes: 41 additions & 9 deletions Canon.API/Canon.API.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<!-- net10.0-windows: Windows release, with AutoUpdater.NET (Windows Desktop runtime). net10.0: Linux, no automatic update. -->
<TargetFrameworks>net10.0-windows;net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x64</PlatformTarget>
Expand All @@ -11,17 +12,20 @@
<FileVersion>1.4.0.0</FileVersion>
<Version>1.4.0.0</Version>

<!-- Self-Contained Single-File Deployment -->
<!-- OpenAPI document generated at build time only when requested (see README). Keep false: the package's outer-build hook
would otherwise generate from net10.0-windows before every solution build. -->
<OpenApiGenerateDocumentsOnBuild>false</OpenApiGenerateDocumentsOnBuild>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../docs</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

<!-- Self-Contained Single-File Deployment (Windows release) -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-windows'">
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>

<!-- OpenAPI document generated at build time only when requested (see README) -->
<OpenApiGenerateDocumentsOnBuild>false</OpenApiGenerateDocumentsOnBuild>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../docs</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name openapi</OpenApiGenerateDocumentsOptions>
</PropertyGroup>

<ItemGroup>
Expand All @@ -37,17 +41,45 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Autoupdater.NET.Official" Version="1.9.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.12" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.2.3" />
<!-- Build-time generation of docs/openapi.json, opt-in: dotnet build -p:OpenApiGenerateDocumentsOnBuild=true -->
<!-- Build-time generation of docs/openapi.json, opt-in: dotnet build -f net10.0 -t:Build -t:GenerateOpenApiDocuments
(the package hook skips the inner builds of a multi-targeted project, so -p:OpenApiGenerateDocumentsOnBuild=true
with -f generates nothing; net10.0 keeps the Windows update check out of the generator, which runs the application's
entry point) -->
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-windows'">
<PackageReference Include="Autoupdater.NET.Official" Version="1.9.3" />
</ItemGroup>

<!-- Canon EDSDK native libraries, next to the executable (outside the single file). -->
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0-windows'">
<None Include="..\EDSDK\EDSDK.dll" Link="EDSDK.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Include="..\EDSDK\EdsImage.dll" Link="EdsImage.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<None Include="..\EDSDK\linux-x64\libEDSDK.so" Link="libEDSDK.so">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Canon.Core\Canon.Core.csproj" />
</ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions Canon.API/Controllers/CanonController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ public async Task<IActionResult> GetStatus()

[HttpGet("flash")]
[EndpointSummary("State of the \"flash firing\" setting")]
[EndpointDescription("The SDK only reports values set remotely: 'firing' is the value last set by the API (null before the first one). A change made in the camera menu is not visible. With Canon:ForceFlashFiring (default), the setting is set to Fire when the camera connects and before every capture.")]
[EndpointDescription("The SDK only reports values set remotely: 'firing' is the value last set by the API (null before the first one). A change made in the camera menu is not visible. With Canon:ForceFlashFiring (default), the setting is set to Fire when the camera connects, before the live view starts and before a capture taken without live view (never while the live view runs, which would freeze the camera).")]
[ProducesResponseType<FlashStatus>(StatusCodes.Status200OK, "application/json")]
public async Task<IActionResult> GetFlash() => Ok(await camera.GetFlashStatus());

[HttpPost("flash")]
[EndpointSummary("Set the \"flash firing\" setting (\"fire\" or \"off\")")]
[EndpointDescription("The camera must be in P, Tv, Av or M. With Canon:ForceFlashFiring (default), the setting is set back to Fire before the next capture.")]
[EndpointDescription("The camera must be in P, Tv, Av or M. A running live view is paused while the setting is written, because writing it during the live view freezes the camera. With Canon:ForceFlashFiring (default), the setting is set back to Fire the next time the live view starts or before the next capture taken without live view.")]
[ProducesResponseType<FlashStatus>(StatusCodes.Status200OK, "application/json")]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest, "application/problem+json")]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status409Conflict, "application/problem+json")]
Expand Down
2 changes: 1 addition & 1 deletion Canon.API/Infrastructure/CameraExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ private static void AddSettingsExtensions(ProblemDetails problem, CameraSettings
EdsException { IsRefused: true } => (StatusCodes.Status409Conflict, "Operation refused by the camera"),
EdsException { IsInvalidValue: true } => (StatusCodes.Status400BadRequest, "Value not accepted by the camera"),
EdsException => (StatusCodes.Status500InternalServerError, "Camera error"),
DllNotFoundException or BadImageFormatException => (StatusCodes.Status500InternalServerError, "Canon EDSDK could not be loaded (EDSDK.dll missing or wrong architecture)"),
DllNotFoundException or BadImageFormatException => (StatusCodes.Status500InternalServerError, "Canon EDSDK could not be loaded (EDSDK.dll or libEDSDK.so missing, wrong architecture, or libusb-1.0 missing on Linux)"),
_ => (StatusCodes.Status500InternalServerError, "Internal server error")
};
}
8 changes: 8 additions & 0 deletions Canon.API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Canon.API.Infrastructure;
using Serilog;
#if WINDOWS
using AutoUpdaterDotNET;
#endif

// Next to the executable, whatever the working directory (shortcut, scheduled task...).
var logFile = Path.Combine(AppContext.BaseDirectory, "logs", "canon-api.log");
Expand Down Expand Up @@ -48,10 +50,14 @@

var app = builder.Build();

#if WINDOWS
if (app.Configuration.GetValue("AutoUpdate:Enabled", true))
ConfigureAutoUpdater(app);
else
Log.Information("Automatic updates are disabled");
#else
Log.Information("Automatic updates are not available on this platform");
#endif

app.UseExceptionHandler();

Expand All @@ -75,6 +81,7 @@
Log.CloseAndFlush();
}

#if WINDOWS
static void ConfigureAutoUpdater(WebApplication app)
{
// Configure AutoUpdater.NET for automatic updates
Expand All @@ -99,3 +106,4 @@ static void ConfigureAutoUpdater(WebApplication app)

AutoUpdater.Start(app.Configuration["AutoUpdate:Url"] ?? "https://dansleboby.github.io/CanonWebAPI/autoupdate.xml");
}
#endif
2 changes: 1 addition & 1 deletion Canon.Core.Tests/Canon.Core.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
Expand Down
19 changes: 19 additions & 0 deletions Canon.Core.Tests/EdsdkHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,23 @@ public void Capture_errors_are_translated(uint code, string message)
{
Assert.Equal(message, EdsdkHelper.GetCaptureErrorMessage(code));
}

[Theory]
[InlineData(EDSDK.StateEvent_Shutdown, "StateEvent_Shutdown")]
[InlineData(EDSDK.StateEvent_CaptureError, "StateEvent_CaptureError")]
[InlineData(EDSDK.ObjectEvent_DirItemRequestTransfer, "ObjectEvent_DirItemRequestTransfer")]
[InlineData(EDSDK.PropertyEvent_PropertyChanged, "PropertyEvent_PropertyChanged")]
[InlineData(0xABCDu, "0xABCD")]
public void Events_are_named_after_their_constant(uint eventId, string name)
{
Assert.Equal(name, EdsdkHelper.DescribeEvent(eventId));
}

[Fact]
public void Results_are_described_with_their_code()
{
Assert.Equal("OK", EdsdkHelper.DescribeResult(EDSDK.EDS_ERR_OK));
Assert.Equal("Device is busy (0x2019)", EdsdkHelper.DescribeResult(EDSDK.EDS_ERR_PTP_DEVICE_BUSY));
Assert.Equal("0x12345678", EdsdkHelper.DescribeResult(0x12345678));
}
}
Loading
Loading