Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1793349
add guard for cancel
shurkanTwo Dec 6, 2025
44f5e4e
Serialize capture cancel handling
shurkanTwo Dec 7, 2025
161eff2
disable native cancel
shurkanTwo Dec 7, 2025
dacaeee
add option to toggle native cancel
shurkanTwo Dec 7, 2025
66e83ae
add text to checkobox
shurkanTwo Dec 7, 2025
b8bd052
Add native cancel option and improve soft cancel handling
shurkanTwo Dec 7, 2025
ce95fbd
Guard start when camera reports busy to avoid native crashes
shurkanTwo Dec 7, 2025
96980bb
Do not start new exposure while camera is busy
shurkanTwo Dec 7, 2025
e8e4dbd
move to constants
shurkanTwo Dec 8, 2025
daafce9
replace SonyException with TaskCanceledException
shurkanTwo Dec 8, 2025
29dcfb0
Add native cancel hint and start/abort safeguards
shurkanTwo Dec 8, 2025
1d7cdab
undo csharpening
shurkanTwo Dec 8, 2025
886f2c2
Handle start/reset and soft cancel
shurkanTwo Dec 8, 2025
d035092
Simplify cancel constants and setting lookup
shurkanTwo Dec 8, 2025
e0e8601
Simplify StartExposure cancel path
shurkanTwo Dec 8, 2025
be44616
move to static var
shurkanTwo Dec 8, 2025
fca3e40
move native cancel capture hint
shurkanTwo Dec 8, 2025
d6db836
Fix EnableNativeCancel nameof reference
shurkanTwo Dec 8, 2025
d178046
Inject plugin GUID instead of reflecting
shurkanTwo Dec 8, 2025
77cd0c5
Warn when native cancel fails and clarify option text
shurkanTwo Dec 8, 2025
1788191
Warn on soft cancel when native cancel disabled
shurkanTwo Dec 8, 2025
7cb640a
Rename TryCancelCapture and remove redundant guards
shurkanTwo Dec 8, 2025
96fbb68
Recheck status after cancel attempt regardless of success
shurkanTwo Dec 8, 2025
340b7d4
Reuse cancel helper for pre-start reset
shurkanTwo Dec 8, 2025
5aefd72
undo formatter changes
shurkanTwo Dec 8, 2025
6df3c41
Handle unknown capture status responses
shurkanTwo Dec 8, 2025
f5e93df
refactor cancelation logic
shurkanTwo Dec 8, 2025
d0b12ac
use live value of cancelcapture option
shurkanTwo Dec 8, 2025
19a0b2a
Prevent deadlock during rapid start and cancel
shurkanTwo Dec 8, 2025
8d4a65f
Show abort warning only once per exposure
shurkanTwo Dec 8, 2025
b5f1288
Revert "Show abort warning only once per exposure"
shurkanTwo Dec 8, 2025
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
25 changes: 18 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
# Sony Camera Plugin
# Sony Camera Plugin

## 1.0.0.5

- Added native cancel option in plugin settings and defaulted to off to avoid crashes.
- Serialized capture cancel/start/status checks and treated busy starts as soft cancels.
- Prevented new exposures from starting when the camera is busy; improved abort behavior without native cancel.

## 1.0.0.4
* Added the `UpdateSubSampleArea` implementation and bumped the minimum NINA version so the plugin loads in 3.2.
* Restored the ISO/Gain UI by notifying NINA whenever the camera connection updates ISO data and by handling cameras without ISO option lists.
* Probed the registry ISO property (`0xFFFE`) so older Sony bodies still populate the gain dropdown.
* Logged clearer errors when gain min/max cannot be determined and improved the GitHub workflow to publish the release DLL artifact.

- Added the `UpdateSubSampleArea` implementation and bumped the minimum NINA version so the plugin loads in 3.2.
- Restored the ISO/Gain UI by notifying NINA whenever the camera connection updates ISO data and by handling cameras without ISO option lists.
- Probed the registry ISO property (`0xFFFE`) so older Sony bodies still populate the gain dropdown.
- Logged clearer errors when gain min/max cannot be determined and improved the GitHub workflow to publish the release DLL artifact.

## 1.0.0.3

Updated to support new device property "DisplayName" required in NINA 3

## 1.0.0.2

Rebuilt using .NET Core 8 for NINA 3

## 1.0.0.1

Initial release - Supports all functionality provided by the ASCOM version, plus:
* LiveView support
* Actual ISO values displayed in Gain drop-down vs a numeric index (1, 2, 3, etc)

- LiveView support
- Actual ISO values displayed in Gain drop-down vs a numeric index (1, 2, 3, etc)

Note that NINA saves the raw ARW files and does not generate FITS files for DSLRs (per the note in NINA: Options.Imaging).
173 changes: 149 additions & 24 deletions Drivers/CameraDriver.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -20,6 +20,7 @@
using NINA.Image.Interfaces;
using NINA.Profile;
using NINA.Profile.Interfaces;
using NINA.RetroKiwi.Plugin.SonyCamera;
using Sony;

namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers {
Expand All @@ -38,6 +39,14 @@ public class CameraDriver : BaseINPC, ICamera {
private const uint CAPTURE_STARTING = 0x8001;
private const uint CAPTURE_READING = 0x8002;
private const uint CAPTURE_PROCESSING = 0x8003;
private const uint CAPTURE_STATUS_UNKNOWN = 0xFFFFFFFF;
private static readonly uint[] IDLE_STATES = { CAPTURE_CREATED, CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED };
private static readonly uint[] BUSY_STATES = { CAPTURE_CAPTURING, CAPTURE_PROCESSING, CAPTURE_STARTING, CAPTURE_READING };
private static readonly uint[] COMPLETION_STATES = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED };

private readonly PluginOptionsAccessor _pluginSettings;
private readonly bool _enableNativeCancelDefault;
private bool _softCancelRequested;

private SonyCameraInfo _camera = null;
private SonyDevice _device = null;
Expand All @@ -47,11 +56,15 @@ public class CameraDriver : BaseINPC, ICamera {
private short _readoutModeForSnapImages;
private short _readoutModeForNormalImages;
private AsyncObservableCollection<BinningMode> _binningModes;
private readonly object _captureLock = new object();

public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device) {
public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, PluginOptionsAccessor pluginSettings, bool enableNativeCancel) {
_profileService = profileService;
_exposureDataFactory = exposureDataFactory;
_device = device;
_pluginSettings = pluginSettings;
_enableNativeCancelDefault = enableNativeCancel;
_softCancelRequested = false;
}

#region Internal Helpers
Expand All @@ -74,7 +87,7 @@ private IReadOnlyList<PropertyValueOption> GetAvailableIsoOptions() {
return options;
}
} catch (Exception ex) {
Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}");
Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}.");
}
}

Expand All @@ -91,6 +104,80 @@ private void NotifyGainPropertiesChanged() {
RaisePropertyChanged(nameof(Gains));
}

private bool NativeCancelEnabled {
get {
if (_pluginSettings != null) {
try {
var raw = _pluginSettings.GetValueString(nameof(SonyCamera.EnableNativeCancel), _enableNativeCancelDefault.ToString());
if (bool.TryParse(raw, out var enabled)) {
return enabled;
}
} catch (Exception ex) {
Logger.Warning($"EnableNativeCancel read failed; defaulting to {_enableNativeCancelDefault}. {ex.Message}.");
}
}

return _enableNativeCancelDefault;
}
}

private bool TryCancelCaptureIfEnabled(string reason, int lockTimeoutMs = 1000) {
if (_camera == null) {
return false;
}

if (!NativeCancelEnabled) {
Logger.Info($"Native cancel disabled; skipping cancel ({reason}).");
return false;
}

bool lockTaken = false;
try {
if (!Monitor.TryEnter(_captureLock, lockTimeoutMs)) {
Logger.Warning($"Skipping cancel ({reason}); capture lock unavailable after {lockTimeoutMs} ms.");
return false;
}
lockTaken = true;

SonyDriver driver = SonyDriver.GetInstance();
if (!TryGetCaptureStatus(driver, out var status, reason)) {
return false;
}

if (status == CAPTURE_STATUS_UNKNOWN) {
Logger.Warning($"Skipping cancel ({reason}); capture status is unknown (camera did not respond).");
return false;
}

if (!BUSY_STATES.Contains(status)) {
Logger.Debug($"Skipping cancel ({reason}); capture status is {status}.");
return false;
}

Logger.Info($"Issuing cancel ({reason}); capture status is {status}.");
driver.CancelCapture(_camera.Handle);
return true;
} catch (Exception ex) {
Logger.Error($"CancelCapture failed ({reason}).", ex);
return false;
} finally {
if (lockTaken) {
Monitor.Exit(_captureLock);
}
}
}

private bool TryGetCaptureStatus(SonyDriver driver, out uint status, string reason) {
try {
status = driver.GetCaptureStatus(_camera.Handle);
return true;
} catch (Exception ex) {
Logger.Warning($"Unable to get capture status ({reason}); camera may not have responded: {ex.Message}.");
status = CAPTURE_STATUS_UNKNOWN;
return false;
}
}

#endregion

#region Supported Properties
Expand Down Expand Up @@ -282,7 +369,7 @@ public int Gain {

return (int)(value.Value == 0xffffff ? 0 : value.Value);
} catch (Exception ex) {
Logger.Error("Problem getting gain", ex);
Logger.Error("Problem getting gain.", ex);
return -1;
}
} else {
Expand All @@ -296,7 +383,7 @@ public int Gain {
SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value);
RaisePropertyChanged(nameof(Gain));
} catch (Exception ex) {
Logger.Error($"Problem setting gain to {value}", ex);
Logger.Error($"Problem setting gain to {value}.", ex);
}
}
}
Expand Down Expand Up @@ -517,18 +604,37 @@ public void SetupDialog() {
public void StartExposure(CaptureSequence sequence) {
if (_camera != null) {
SonyDriver driver = SonyDriver.GetInstance();
uint captureStatus = driver.GetCaptureStatus(_camera.Handle);

if (captureStatus == CAPTURE_CAPTURING || captureStatus == CAPTURE_PROCESSING || captureStatus == CAPTURE_STARTING ||
captureStatus == CAPTURE_READING || captureStatus == CAPTURE_PROCESSING) {
Notification.ShowWarning("Another exposure still in progress. Cancelling it to start another.");
}
double exposureTime;
lock (_captureLock) {
_softCancelRequested = false;
if (!TryGetCaptureStatus(driver, out var captureStatus, "start exposure preflight") ||
captureStatus == CAPTURE_STATUS_UNKNOWN) {
Logger.Warning("Cannot start exposure: capture status unavailable.");
throw new TaskCanceledException("Cannot start exposure: capture status unavailable.");
}

// Tell the camera to cancel capture, we do this every time regardless - this will reset the status to be non-complete
driver.CancelCapture(_camera.Handle);
TryCancelCaptureIfEnabled("start exposure reset");
if (NativeCancelEnabled && (!TryGetCaptureStatus(driver, out captureStatus, "start exposure post-cancel") || captureStatus == CAPTURE_STATUS_UNKNOWN)) {
Logger.Warning("Cannot start exposure: capture status unavailable after cancel.");
throw new TaskCanceledException("Cannot start exposure: capture status unavailable after cancel.");
}

if (BUSY_STATES.Contains(captureStatus)) {
Notification.ShowWarning("Camera is still busy with a previous exposure; skipping new start.");
throw new TaskCanceledException("Cannot start exposure: camera is still busy with a previous exposure.");
}

if (!IDLE_STATES.Contains(captureStatus)) {
Logger.Warning($"Cannot start exposure: camera in unexpected capture status ({captureStatus}).");
throw new TaskCanceledException($"Cannot start exposure: camera in unexpected capture status ({captureStatus}).");
}

exposureTime = sequence.ExposureTime;
}

double exposureTime = sequence.ExposureTime;
driver.StartCapture(_camera.Handle, (float)exposureTime); //);
// Start capture outside the capture lock to avoid blocking abort/cancel paths if the call hangs.
driver.StartCapture(_camera.Handle, (float)exposureTime);
}
}

Expand All @@ -537,30 +643,49 @@ public void StopExposure() {
}

public void AbortExposure() {
if (_camera != null) {
SonyDriver.GetInstance().CancelCapture(_camera.Handle);
if (TryCancelCaptureIfEnabled("abort request")) {
return;
}

_softCancelRequested = true;
Notification.ShowWarning("Abort requested; native cancel is disabled or the camera did not accept it. Exposure will continue until it finishes.");
Logger.Info("AbortExposure requested; native cancel unavailable; letting capture finish.");
}

public async Task WaitUntilExposureIsReady(CancellationToken token) {
using (token.Register(AbortExposure)) {
uint[] completionStates = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED };

SonyDriver driver = SonyDriver.GetInstance();

try {
uint captureStatus = driver.GetCaptureStatus(_camera.Handle);
Logger.Info(
$"Waiting for image to be ready, current state is {captureStatus}, completion states are {String.Join(", ", completionStates)}");
uint captureStatus;
lock (_captureLock) {
if (!TryGetCaptureStatus(driver, out captureStatus, "wait begin") || captureStatus == CAPTURE_STATUS_UNKNOWN) {
throw new SonyException("Problem while waiting for image to be ready (status unavailable)");
}
}
Logger.Info($"Waiting for image to be ready; current state is {captureStatus}; completion states are {String.Join(", ", COMPLETION_STATES)}.");

while (!completionStates.Contains(captureStatus)) {
while (!COMPLETION_STATES.Contains(captureStatus)) {
await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token);
captureStatus = driver.GetCaptureStatus(_camera.Handle);

lock (_captureLock) {
if (!TryGetCaptureStatus(driver, out captureStatus, "wait poll") || captureStatus == CAPTURE_STATUS_UNKNOWN) {
throw new SonyException("Problem while waiting for image to be ready (status unavailable)");
}
}
}

Logger.Info($"Wait for image ready complete, completion state is {captureStatus}");
Logger.Info($"Wait for image ready complete; completion state is {captureStatus}.");
if (_softCancelRequested || token.IsCancellationRequested) {
_softCancelRequested = false;
throw new TaskCanceledException("Exposure cancelled by user (soft cancel).");
}
} catch (TaskCanceledException) {
Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel.");
throw;
} catch (Exception ex) {
Logger.Error("WaitUntilExposureIsReady got exception", ex);
Logger.Error("WaitUntilExposureIsReady got exception.", ex);
throw new SonyException("Problem while waiting for image to be ready (see log)");
}
}
Expand Down
7 changes: 5 additions & 2 deletions Drivers/CameraProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
using NINA.Equipment.Interfaces.ViewModel;
using NINA.Equipment.Interfaces;
using NINA.Profile.Interfaces;
using NINA.Profile;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.ComponentModel.Composition;
using NINA.RetroKiwi.Plugin.SonyCamera;
using NINA.Image.Interfaces;
using NINA.WPF.Base.Mediator;
using Sony;
Expand All @@ -24,11 +26,13 @@ public class CameraProvider : IEquipmentProvider<ICamera> {
private IProfileService profileService;
private IExposureDataFactory exposureDataFactory;
SonyDriver driver;
private readonly PluginOptionsAccessor pluginSettings;

[ImportingConstructor]
public CameraProvider(IProfileService profileService, IExposureDataFactory exposureDataFactory) {
this.profileService = profileService;
this.exposureDataFactory = exposureDataFactory;
this.pluginSettings = new PluginOptionsAccessor(profileService, SonyCamera.PluginGuid);

if (!DllLoader.IsX86()) {
try {
Expand All @@ -43,14 +47,13 @@ public CameraProvider(IProfileService profileService, IExposureDataFactory expos

public IList<ICamera> GetEquipment() {
var devices = new List<ICamera>();

if (this.driver != null) {
try {
int count = 0;

foreach (var sonyDevice in driver.Cameras()) {
count++;
devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice));
devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice, pluginSettings, false));
}

Logger.Info($"Found {count} Sony Cameras");
Expand Down
19 changes: 8 additions & 11 deletions Options.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@
<!-- In order for this datatemplate to be picked correctly, the key has to follow the naming convention of <IPlugin.Name>_Options -->
<!-- Furthermore the Resource Dictionary has to be exported via code behind export attributes -->
<DataTemplate x:Key="Sony Camera Plugin_Options">
<!-- <StackPanel Orientation="Vertical"> -->
<!-- <StackPanel Orientation="Horizontal"> -->
<!-- <TextBlock Text="Default Notification Message" /> -->
<!-- <TextBox MinWidth="50" Text="{Binding DefaultNotificationMessage}" /> -->
<!-- </StackPanel> -->
<!-- <StackPanel Orientation="Horizontal"> -->
<!-- <TextBlock Text="Profile Specific Notification Message" /> -->
<!-- <TextBox MinWidth="50" Text="{Binding ProfileSpecificNotificationMessage}" /> -->
<!-- </StackPanel> -->
<!-- </StackPanel> -->
<StackPanel Orientation="Vertical" Margin="0,4,0,0">
<TextBlock Text="Camera native capture cancellation" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,4" />
<TextBlock Text="Use the camera's native CancelCapture call during aborts. Stability varies by camera model and Windows driver stack and can cause VCRUNTIME crashes. Disable if aborts crash NINA; enable if you need to force-stop exposures." TextWrapping="Wrap" Opacity="0.7" Margin="0,0,0,8"/>
<CheckBox Content="Enable native cancel"
HorizontalAlignment="Left"
IsChecked="{Binding EnableNativeCancel, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</ResourceDictionary>
4 changes: 2 additions & 2 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

// [MANDATORY] The assembly versioning
//Should be incremented for each new release build of a plugin
[assembly: AssemblyVersion("1.0.0.4")]
[assembly: AssemblyFileVersion("1.0.0.4")]
[assembly: AssemblyVersion("1.0.0.5")]
[assembly: AssemblyFileVersion("1.0.0.5")]

// [MANDATORY] The name of your plugin
[assembly: AssemblyTitle("Sony Camera Plugin")]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ https://github.com/dougforpres/NINASonyCameraPlugin/releases
- Binning and sub-sampling are not supported; captures use the full sensor frame.
- For exposures <= 30s the driver chooses the nearest built-in shutter speed; longer exposures fall back to Bulb.
- Powering off/unplugging the camera while connected can crash NINA via Windows' MTP stack (PortableDeviceApi.dll access violation); disconnect in NINA first to avoid it.
- The “Enable native cancel” option calls the camera’s CancelCapture; stability varies by body and Windows driver stack. Leave it off if you see crashes when aborting exposures.

## Support

Expand All @@ -43,7 +44,7 @@ https://github.com/dougforpres/NINASonyCameraPlugin/releases

## Metadata

- Version: 1.0.0.4
- Version: 1.0.0.5
- Author: Doug Henderson
- Contributors: Lucas Lepski [@ShurkanTwo](https://github.com/shurkanTwo)
- Minimum NINA version: 3.2.0.3001
Expand Down
Loading