From 179334936bfbb32bb20d8068066f80ac8828dcec Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sat, 6 Dec 2025 13:17:52 +0100 Subject: [PATCH 01/31] add guard for cancel --- Drivers/CameraDriver.cs | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index f2d6826..a104f83 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -47,6 +47,7 @@ public class CameraDriver : BaseINPC, ICamera { private short _readoutModeForSnapImages; private short _readoutModeForNormalImages; private AsyncObservableCollection _binningModes; + private readonly object _captureLock = new object(); public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device) { _profileService = profileService; @@ -91,6 +92,39 @@ private void NotifyGainPropertiesChanged() { RaisePropertyChanged(nameof(Gains)); } + private bool TryCancelCapture(string reason) { + if (_camera == null) { + return false; + } + + lock (_captureLock) { + try { + SonyDriver driver = SonyDriver.GetInstance(); + uint status; + + try { + status = driver.GetCaptureStatus(_camera.Handle); + } catch (Exception ex) { + Logger.Warning($"Skipping cancel ({reason}) because capture status could not be read: {ex.Message}"); + return false; + } + + uint[] cancellableStates = { CAPTURE_CAPTURING, CAPTURE_STARTING, CAPTURE_READING, CAPTURE_PROCESSING }; + if (!cancellableStates.Contains(status)) { + Logger.Debug($"Skip 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; + } + } + } + #endregion #region Supported Properties @@ -525,7 +559,7 @@ public void StartExposure(CaptureSequence sequence) { } // 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); + TryCancelCapture("start exposure reset"); double exposureTime = sequence.ExposureTime; driver.StartCapture(_camera.Handle, (float)exposureTime); //); @@ -537,9 +571,7 @@ public void StopExposure() { } public void AbortExposure() { - if (_camera != null) { - SonyDriver.GetInstance().CancelCapture(_camera.Handle); - } + TryCancelCapture("abort request"); } public async Task WaitUntilExposureIsReady(CancellationToken token) { From 44f5e4eef6d39f1d1f88cee93574e6bb4dcb57f8 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sun, 7 Dec 2025 19:08:34 +0100 Subject: [PATCH 02/31] Serialize capture cancel handling --- Drivers/CameraDriver.cs | 56 ++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index a104f83..1e9193b 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -100,12 +100,7 @@ private bool TryCancelCapture(string reason) { lock (_captureLock) { try { SonyDriver driver = SonyDriver.GetInstance(); - uint status; - - try { - status = driver.GetCaptureStatus(_camera.Handle); - } catch (Exception ex) { - Logger.Warning($"Skipping cancel ({reason}) because capture status could not be read: {ex.Message}"); + if (!TryGetCaptureStatusLocked(driver, out var status, reason)) { return false; } @@ -125,6 +120,17 @@ private bool TryCancelCapture(string reason) { } } + private bool TryGetCaptureStatusLocked(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}): {ex.Message}"); + status = CAPTURE_FAILED; + return false; + } + } + #endregion #region Supported Properties @@ -551,18 +557,25 @@ 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."); + bool shouldCancel = false; + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { + Logger.Warning("Starting exposure without capture status due to read failure."); + } else 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."); + shouldCancel = true; + } } - // Tell the camera to cancel capture, we do this every time regardless - this will reset the status to be non-complete - TryCancelCapture("start exposure reset"); + if (shouldCancel) { + TryCancelCapture("start exposure reset"); + } - double exposureTime = sequence.ExposureTime; - driver.StartCapture(_camera.Handle, (float)exposureTime); //); + lock (_captureLock) { + double exposureTime = sequence.ExposureTime; + driver.StartCapture(_camera.Handle, (float)exposureTime); //); + } } } @@ -581,13 +594,22 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { SonyDriver driver = SonyDriver.GetInstance(); try { - uint captureStatus = driver.GetCaptureStatus(_camera.Handle); + uint captureStatus; + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) { + 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(", ", completionStates)}"); while (!completionStates.Contains(captureStatus)) { await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); - captureStatus = driver.GetCaptureStatus(_camera.Handle); + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { + throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); + } + } } Logger.Info($"Wait for image ready complete, completion state is {captureStatus}"); From 161eff2e228e6fb8cf2763570b9b1facae72696d Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sun, 7 Dec 2025 19:16:35 +0100 Subject: [PATCH 03/31] disable native cancel --- Drivers/CameraDriver.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 1e9193b..a1815c4 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -38,6 +38,7 @@ 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 bool ENABLE_NATIVE_CANCEL = false; // native CancelCapture is unstable on some bodies private SonyCameraInfo _camera = null; private SonyDevice _device = null; @@ -97,6 +98,11 @@ private bool TryCancelCapture(string reason) { return false; } + if (!ENABLE_NATIVE_CANCEL) { + Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); + return false; + } + lock (_captureLock) { try { SonyDriver driver = SonyDriver.GetInstance(); @@ -584,7 +590,7 @@ public void StopExposure() { } public void AbortExposure() { - TryCancelCapture("abort request"); + Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); } public async Task WaitUntilExposureIsReady(CancellationToken token) { @@ -605,6 +611,11 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { while (!completionStates.Contains(captureStatus)) { await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); + if (token.IsCancellationRequested) { + Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); + return; + } + lock (_captureLock) { if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); @@ -613,6 +624,8 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { } Logger.Info($"Wait for image ready complete, completion state is {captureStatus}"); + } catch (TaskCanceledException) { + Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); } catch (Exception ex) { Logger.Error("WaitUntilExposureIsReady got exception", ex); throw new SonyException("Problem while waiting for image to be ready (see log)"); From dacaeee012666e3dae5a1b2445c9718d0c7415d8 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sun, 7 Dec 2025 19:34:49 +0100 Subject: [PATCH 04/31] add option to toggle native cancel --- Drivers/CameraDriver.cs | 13 +++++++++---- Drivers/CameraProvider.cs | 16 +++++++++++++++- Options.xaml | 17 ++++++----------- SonyCameraPlugin.cs | 12 ++++++++++++ 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index a1815c4..e95442b 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -38,7 +38,7 @@ 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 bool ENABLE_NATIVE_CANCEL = false; // native CancelCapture is unstable on some bodies + private readonly bool _enableNativeCancel; private SonyCameraInfo _camera = null; private SonyDevice _device = null; @@ -50,10 +50,11 @@ public class CameraDriver : BaseINPC, ICamera { private AsyncObservableCollection _binningModes; private readonly object _captureLock = new object(); - public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device) { + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) { _profileService = profileService; _exposureDataFactory = exposureDataFactory; _device = device; + _enableNativeCancel = enableNativeCancel; } #region Internal Helpers @@ -98,7 +99,7 @@ private bool TryCancelCapture(string reason) { return false; } - if (!ENABLE_NATIVE_CANCEL) { + if (!_enableNativeCancel) { Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); return false; } @@ -590,7 +591,11 @@ public void StopExposure() { } public void AbortExposure() { - Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); + if (_enableNativeCancel) { + TryCancelCapture("abort request"); + } else { + Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); + } } public async Task WaitUntilExposureIsReady(CancellationToken token) { diff --git a/Drivers/CameraProvider.cs b/Drivers/CameraProvider.cs index 7c78287..d2a0eff 100644 --- a/Drivers/CameraProvider.cs +++ b/Drivers/CameraProvider.cs @@ -2,6 +2,7 @@ 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; @@ -9,6 +10,8 @@ using System.Threading; using System.Threading.Tasks; using System.ComponentModel.Composition; +using System.Reflection; +using System.Runtime.InteropServices; using NINA.Image.Interfaces; using NINA.WPF.Base.Mediator; using Sony; @@ -24,11 +27,15 @@ public class CameraProvider : IEquipmentProvider { private IProfileService profileService; private IExposureDataFactory exposureDataFactory; SonyDriver driver; + private readonly PluginOptionsAccessor pluginSettings; + private static readonly Guid PluginGuid = + Guid.Parse(((GuidAttribute)Attribute.GetCustomAttribute(typeof(CameraProvider).Assembly, typeof(GuidAttribute))).Value); [ImportingConstructor] public CameraProvider(IProfileService profileService, IExposureDataFactory exposureDataFactory) { this.profileService = profileService; this.exposureDataFactory = exposureDataFactory; + this.pluginSettings = new PluginOptionsAccessor(profileService, PluginGuid); if (!DllLoader.IsX86()) { try { @@ -43,6 +50,13 @@ public CameraProvider(IProfileService profileService, IExposureDataFactory expos public IList GetEquipment() { var devices = new List(); + bool enableNativeCancel = false; + try { + var raw = pluginSettings.GetValueString("EnableNativeCancel", bool.FalseString); + enableNativeCancel = bool.TryParse(raw, out var parsed) && parsed; + } catch (Exception ex) { + Logger.Warning($"Unable to read EnableNativeCancel setting; defaulting to false. {ex.Message}"); + } if (this.driver != null) { try { @@ -50,7 +64,7 @@ public IList GetEquipment() { foreach (var sonyDevice in driver.Cameras()) { count++; - devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice)); + devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice, enableNativeCancel)); } Logger.Info($"Found {count} Sony Cameras"); diff --git a/Options.xaml b/Options.xaml index 893755b..5cb60ae 100644 --- a/Options.xaml +++ b/Options.xaml @@ -6,15 +6,10 @@ - - - - - - - - - - + + + - \ No newline at end of file + diff --git a/SonyCameraPlugin.cs b/SonyCameraPlugin.cs index 6b7cfc0..b64a645 100644 --- a/SonyCameraPlugin.cs +++ b/SonyCameraPlugin.cs @@ -72,6 +72,7 @@ public override Task Teardown() { private void ProfileService_ProfileChanged(object sender, EventArgs e) { // Rase the event that this profile specific value has been changed due to the profile switch RaisePropertyChanged(nameof(ProfileSpecificNotificationMessage)); + RaisePropertyChanged(nameof(EnableNativeCancel)); } private Task ImageSaveMediator_BeforeImageSaved(object sender, BeforeImageSavedEventArgs e) { @@ -132,6 +133,17 @@ public string ProfileSpecificNotificationMessage { } } + public bool EnableNativeCancel { + get { + var raw = pluginSettings.GetValueString(nameof(EnableNativeCancel), bool.FalseString); + return bool.TryParse(raw, out var enabled) && enabled; + } + set { + pluginSettings.SetValueString(nameof(EnableNativeCancel), value.ToString()); + RaisePropertyChanged(); + } + } + public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); From 66e83ae5c491b8db1c0be40c39cf12ac5c709948 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sun, 7 Dec 2025 19:52:47 +0100 Subject: [PATCH 05/31] add text to checkobox --- Options.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Options.xaml b/Options.xaml index 5cb60ae..b6b5aa3 100644 --- a/Options.xaml +++ b/Options.xaml @@ -7,7 +7,10 @@ - + + From b8bd05272ebd9960038fce15298e4407760aaa0b Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sun, 7 Dec 2025 20:04:25 +0100 Subject: [PATCH 06/31] Add native cancel option and improve soft cancel handling --- Drivers/CameraDriver.cs | 14 +++++++++++--- Options.xaml | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index e95442b..20942d6 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -39,6 +39,7 @@ public class CameraDriver : BaseINPC, ICamera { private const uint CAPTURE_READING = 0x8002; private const uint CAPTURE_PROCESSING = 0x8003; private readonly bool _enableNativeCancel; + private bool _softCancelRequested; private SonyCameraInfo _camera = null; private SonyDevice _device = null; @@ -55,6 +56,7 @@ public CameraDriver(IProfileService profileService, IExposureDataFactory exposur _exposureDataFactory = exposureDataFactory; _device = device; _enableNativeCancel = enableNativeCancel; + _softCancelRequested = false; } #region Internal Helpers @@ -566,6 +568,7 @@ public void StartExposure(CaptureSequence sequence) { SonyDriver driver = SonyDriver.GetInstance(); bool shouldCancel = false; lock (_captureLock) { + _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { Logger.Warning("Starting exposure without capture status due to read failure."); } else if (captureStatus == CAPTURE_CAPTURING || captureStatus == CAPTURE_PROCESSING || captureStatus == CAPTURE_STARTING || @@ -594,6 +597,7 @@ public void AbortExposure() { if (_enableNativeCancel) { TryCancelCapture("abort request"); } else { + _softCancelRequested = true; Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); } } @@ -616,9 +620,8 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { while (!completionStates.Contains(captureStatus)) { await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); - if (token.IsCancellationRequested) { - Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); - return; + if (!_enableNativeCancel && token.IsCancellationRequested) { + _softCancelRequested = true; } lock (_captureLock) { @@ -629,8 +632,13 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { } 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); throw new SonyException("Problem while waiting for image to be ready (see log)"); diff --git a/Options.xaml b/Options.xaml index b6b5aa3..369b42b 100644 --- a/Options.xaml +++ b/Options.xaml @@ -7,7 +7,7 @@ - + Date: Sun, 7 Dec 2025 20:22:31 +0100 Subject: [PATCH 07/31] Guard start when camera reports busy to avoid native crashes --- Drivers/CameraDriver.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 20942d6..9396b04 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -571,10 +571,22 @@ public void StartExposure(CaptureSequence sequence) { _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { Logger.Warning("Starting exposure without capture status due to read failure."); - } else 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."); - shouldCancel = true; + } else { + uint[] idleStates = { CAPTURE_CREATED, CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; + uint[] busyStates = { CAPTURE_CAPTURING, CAPTURE_PROCESSING, CAPTURE_STARTING, CAPTURE_READING }; + + if (busyStates.Contains(captureStatus)) { + Notification.ShowWarning("Camera is still busy with a previous exposure. Skipping new start."); + if (_enableNativeCancel) { + shouldCancel = true; + } else { + // Do not attempt to start while camera is busy when native cancel is disabled + return; + } + } else if (!idleStates.Contains(captureStatus)) { + Logger.Warning($"Unexpected capture status {captureStatus} before start; skipping start."); + return; + } } } From 96980bb2bb66943116e83d6533133ce3a4dc114d Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Sun, 7 Dec 2025 20:31:12 +0100 Subject: [PATCH 08/31] Do not start new exposure while camera is busy --- Drivers/CameraDriver.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 9396b04..8bdd091 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -566,7 +566,8 @@ public void SetupDialog() { public void StartExposure(CaptureSequence sequence) { if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); - bool shouldCancel = false; + bool canStart = false; + bool requestCancel = false; lock (_captureLock) { _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { @@ -577,21 +578,22 @@ public void StartExposure(CaptureSequence sequence) { if (busyStates.Contains(captureStatus)) { Notification.ShowWarning("Camera is still busy with a previous exposure. Skipping new start."); - if (_enableNativeCancel) { - shouldCancel = true; - } else { - // Do not attempt to start while camera is busy when native cancel is disabled - return; - } + requestCancel = _enableNativeCancel; } else if (!idleStates.Contains(captureStatus)) { Logger.Warning($"Unexpected capture status {captureStatus} before start; skipping start."); - return; + } else { + canStart = true; } } } - if (shouldCancel) { + if (requestCancel) { TryCancelCapture("start exposure reset"); + return; + } + + if (!canStart) { + return; } lock (_captureLock) { From e8e4dbd7e36dd671c836640a13933cbba5e955f5 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 11:18:19 +0100 Subject: [PATCH 09/31] move to constants --- Drivers/CameraDriver.cs | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 8bdd091..145d8c7 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -38,6 +38,8 @@ public class CameraDriver : BaseINPC, ICamera { private const uint CAPTURE_STARTING = 0x8001; private const uint CAPTURE_READING = 0x8002; private const uint CAPTURE_PROCESSING = 0x8003; + 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 readonly bool _enableNativeCancel; private bool _softCancelRequested; @@ -113,8 +115,7 @@ private bool TryCancelCapture(string reason) { return false; } - uint[] cancellableStates = { CAPTURE_CAPTURING, CAPTURE_STARTING, CAPTURE_READING, CAPTURE_PROCESSING }; - if (!cancellableStates.Contains(status)) { + if (!BUSY_STATES.Contains(status)) { Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); return false; } @@ -566,39 +567,27 @@ public void SetupDialog() { public void StartExposure(CaptureSequence sequence) { if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); - bool canStart = false; - bool requestCancel = false; lock (_captureLock) { _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { - Logger.Warning("Starting exposure without capture status due to read failure."); + throw new SonyException("Cannot start exposure: capture status unavailable."); } else { - uint[] idleStates = { CAPTURE_CREATED, CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; - uint[] busyStates = { CAPTURE_CAPTURING, CAPTURE_PROCESSING, CAPTURE_STARTING, CAPTURE_READING }; - - if (busyStates.Contains(captureStatus)) { - Notification.ShowWarning("Camera is still busy with a previous exposure. Skipping new start."); - requestCancel = _enableNativeCancel; - } else if (!idleStates.Contains(captureStatus)) { - Logger.Warning($"Unexpected capture status {captureStatus} before start; skipping start."); - } else { - canStart = true; + if (BUSY_STATES.Contains(captureStatus)) { + if (_enableNativeCancel) { + TryCancelCapture("start exposure reset"); + } + throw new SonyException("Cannot start exposure: Camera is still busy with a previous exposure."); } - } - } - if (requestCancel) { - TryCancelCapture("start exposure reset"); - return; - } - - if (!canStart) { - return; + if (!IDLE_STATES.Contains(captureStatus)) { + throw new SonyException($"Cannot start exposure: Camera in unexpected capture status ({captureStatus})."); + } + } } lock (_captureLock) { double exposureTime = sequence.ExposureTime; - driver.StartCapture(_camera.Handle, (float)exposureTime); //); + driver.StartCapture(_camera.Handle, (float)exposureTime); } } } From daafce987a772f76007fa76ab75fd598742493a8 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 11:31:33 +0100 Subject: [PATCH 10/31] replace SonyException with TaskCanceledException --- Drivers/CameraDriver.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 145d8c7..8f83deb 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -570,17 +570,20 @@ public void StartExposure(CaptureSequence sequence) { lock (_captureLock) { _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { - throw new SonyException("Cannot start exposure: capture status unavailable."); + Logger.Warning("Cannot start exposure: capture status unavailable."); + throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); } else { if (BUSY_STATES.Contains(captureStatus)) { if (_enableNativeCancel) { TryCancelCapture("start exposure reset"); } - throw new SonyException("Cannot start exposure: Camera is still busy with a previous exposure."); + 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)) { - throw new SonyException($"Cannot start exposure: Camera in unexpected capture status ({captureStatus})."); + Logger.Warning($"Cannot start exposure: Camera in unexpected capture status ({captureStatus})."); + throw new TaskCanceledException($"Cannot start exposure: Camera in unexpected capture status ({captureStatus})."); } } } From 29dcfb0606e90711abcf53d42292c1554b5e236b Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 11:48:49 +0100 Subject: [PATCH 11/31] Add native cancel hint and start/abort safeguards --- CHANGELOG.md | 25 +- Drivers/CameraDriver.cs | 1263 ++++++++++++++++++++---------------- Properties/AssemblyInfo.cs | 6 +- README.md | 3 +- 4 files changed, 725 insertions(+), 572 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 870006f..8eb3bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 8f83deb..5f62182 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -1,724 +1,863 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Media.Imaging; -using FTD2XX_NET; -using NINA.Core.Enum; -using NINA.Core.Model.Equipment; -using NINA.Core.Utility; -using NINA.Core.Utility.Notification; -using NINA.Equipment.Interfaces; -using NINA.Equipment.Interfaces.Mediator; -using NINA.Equipment.Model; -using NINA.Equipment.SDK.CameraSDKs.ASTPANSDK; -using NINA.Equipment.Utility; -using NINA.Image.ImageData; -using NINA.Image.Interfaces; -using NINA.Profile; -using NINA.Profile.Interfaces; -using Sony; - -namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers { - public class CameraDriver : BaseINPC, ICamera { - // Some camera settings we are interested in - private const uint PROPID_BATTERY = 53784; - private const uint PROPID_ISO = 0xD21E; // Actual ISO currently set - private const uint PROPID_ISOS = 0xFFFE; // Registry-backed list of learnt ISOs (may be empty until learnt) - - // Capture Status - private const uint CAPTURE_CREATED = 0x0000; - private const uint CAPTURE_CAPTURING = 0x0001; - private const uint CAPTURE_FAILED = 0x0002; - private const uint CAPTURE_CANCELLED = 0x0003; - private const uint CAPTURE_COMPLETE = 0x0004; - private const uint CAPTURE_STARTING = 0x8001; - private const uint CAPTURE_READING = 0x8002; - private const uint CAPTURE_PROCESSING = 0x8003; - 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 readonly bool _enableNativeCancel; - private bool _softCancelRequested; - - private SonyCameraInfo _camera = null; - private SonyDevice _device = null; - private IProfileService _profileService; - private readonly IExposureDataFactory _exposureDataFactory; - private bool _liveViewEnabled; - private short _readoutModeForSnapImages; - private short _readoutModeForNormalImages; - private AsyncObservableCollection _binningModes; - private readonly object _captureLock = new object(); - - public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) { - _profileService = profileService; - _exposureDataFactory = exposureDataFactory; - _device = device; - _enableNativeCancel = enableNativeCancel; - _softCancelRequested = false; - } - - #region Internal Helpers - - private PropertyValue GetPropertyValue(uint id) { - return SonyDriver.GetInstance().GetProperty(_camera.Handle, id); - } - - private IReadOnlyList GetAvailableIsoOptions() { - if (_camera == null) { - return Array.Empty(); - } - - uint[] propertyCandidates = { PROPID_ISOS, PROPID_ISO }; - - foreach (var propertyId in propertyCandidates) { - try { - var options = _camera.GetPropertyInfo(propertyId)?.Options()?.Where(o => o.Value <= 0x00FFFFFF).ToList(); - if (options != null && options.Count > 0) { - return options; - } - } catch (Exception ex) { - Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}"); - } - } +namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers; - Logger.Warning("Camera did not report any ISO options via known properties (registry ISO list may be empty until the camera learns it)."); - return Array.Empty(); - } +public class CameraDriver : BaseINPC, ICamera +{ + #region constants + private const uint CAPTURE_CANCELLED = 0x0003; + private const uint CAPTURE_CAPTURING = 0x0001; + private const uint CAPTURE_COMPLETE = 0x0004; - private void NotifyGainPropertiesChanged() { - RaisePropertyChanged(nameof(CanGetGain)); - RaisePropertyChanged(nameof(CanSetGain)); - RaisePropertyChanged(nameof(GainMin)); - RaisePropertyChanged(nameof(GainMax)); - RaisePropertyChanged(nameof(Gain)); - RaisePropertyChanged(nameof(Gains)); - } + // Registry-backed list of learnt ISOs (may be empty until learnt) - private bool TryCancelCapture(string reason) { - if (_camera == null) { - return false; - } + // Capture Status + private const uint CAPTURE_CREATED = 0x0000; - if (!_enableNativeCancel) { - Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); - return false; - } + private const uint CAPTURE_FAILED = 0x0002; + private const uint CAPTURE_PROCESSING = 0x8003; + private const uint CAPTURE_READING = 0x8002; + private const uint CAPTURE_STARTING = 0x8001; - lock (_captureLock) { - try { - SonyDriver driver = SonyDriver.GetInstance(); - if (!TryGetCaptureStatusLocked(driver, out var status, reason)) { - return false; - } + // Some camera settings we are interested in + private const uint PROPID_BATTERY = 53784; - if (!BUSY_STATES.Contains(status)) { - Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); - return false; - } + private const uint PROPID_ISO = 0xD21E; - 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; - } - } - } + // Actual ISO currently set + private const uint PROPID_ISOS = 0xFFFE; - private bool TryGetCaptureStatusLocked(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}): {ex.Message}"); - status = CAPTURE_FAILED; - return false; - } - } - - #endregion + #endregion - #region Supported Properties + #region properties - public bool HasShutter => true; + public int BatteryLevel + { + get + { + if (_camera != null) + { + return (int)GetPropertyValue(PROPID_BATTERY).Value; + } + else + { + return 0; + } + } + } - // Although the driver supports camera temperature, it gets it from the ARW's - // metadata after a photo is taken, because this code doesn't request processed - // ARW, the temp cannot be determined. - public double Temperature { - get => double.NaN; - /*{ + public short BayerOffsetX { get => 1; set => throw new NotImplementedException(); } + public short BayerOffsetY { get => 1; set => throw new NotImplementedException(); } - if (_camera != null) { - PropertyValue value = GetPropertyValue(PROPID_TEMPERATURE); + public AsyncObservableCollection BinningModes + { + get + { + if (_binningModes == null) + { + _binningModes = new AsyncObservableCollection(); + _binningModes.Add(new BinningMode(1, 1)); + } - return (value.Value) / 10.0; - } else { - return double.NaN; - } - }*/ + return _binningModes; } + } - public short BinX { get => 1; set => throw new NotImplementedException(); } - public short BinY { get => 1; set => throw new NotImplementedException(); } + public short BinX { get => 1; set => throw new NotImplementedException(); } + public short BinY { get => 1; set => throw new NotImplementedException(); } - public string SensorName { - get { - if (_camera != null) { - return _camera.SensorName; - } else { - return string.Empty; - } + public int BitDepth + { + get + { + if (_camera != null) + { + return _camera.BitsPerPixel; + } + else + { + return 0; } } + } - public SensorType SensorType { get => SensorType.RGGB; set => throw new NotImplementedException(); } - - public short BayerOffsetX { get => 1; set => throw new NotImplementedException(); } - - public short BayerOffsetY { get => 1; set => throw new NotImplementedException(); } + public CameraStates CameraState => CameraStates.NoState; - public int CameraXSize { - get { - if (_camera != null) { - return _camera.ImageSize.Width; - } - else { - return 0; - } + public int CameraXSize + { + get + { + if (_camera != null) + { + return _camera.ImageSize.Width; } - } - - public int CameraYSize { - get { - if (_camera != null) { - return _camera.ImageSize.Height; - } else { - return 0; - } + else + { + return 0; } } + } - public double ExposureMin { - get { - if (_camera != null) { - return _camera.ExposureMin; - } else { - return double.NaN; - } + public int CameraYSize + { + get + { + if (_camera != null) + { + return _camera.ImageSize.Height; } - } - - public double ExposureMax { - get { - if (_camera != null) { - return _camera.ExposureMax; - } else { - return double.NaN; - } + else + { + return 0; } } + } - public short MaxBinX { get => 1; set => throw new NotImplementedException(); } + public bool CanGetGain => GetAvailableIsoOptions().Any(); + public bool CanSetGain => CanGetGain; + public bool CanSetOffset => false; + public bool CanSetTemperature => false; + public bool CanSetUSBLimit => false; - public short MaxBinY { get => 1; set => throw new NotImplementedException(); } + // TODO - public double PixelSizeX { - get { - if (_camera != null) { - return _camera.PixelWidth; - } else { - return double.NaN; - } + public bool CanShowLiveView + { + get + { + if (_camera != null) + { + return _camera.SupportsPreview(); } - } - - public double PixelSizeY { - get { - if (_camera != null) { - return _camera.PixelHeight; - } else { - return double.NaN; - } + else + { + return false; } } + } - public bool CanSetTemperature => false; - - public CameraStates CameraState => CameraStates.NoState; // TODO + public bool CanSubSample => false; + public string Category { get => "Sony"; } - public bool CanShowLiveView { - get { - if (_camera != null) { - return _camera.SupportsPreview(); - } else { - return false; - } - } + public bool Connected + { + get + { + return _camera != null; } + } - public bool LiveViewEnabled { - get => _liveViewEnabled; - set { - _liveViewEnabled = value; - RaisePropertyChanged(); - } + public bool CoolerOn + { + get => false; + set + { } + } - public bool HasBattery => true; + public double CoolerPower => double.NaN; - public int BatteryLevel { - get { - if (_camera != null) { - return (int)GetPropertyValue(PROPID_BATTERY).Value; - } else { - return 0; - } + public string Description + { + get + { + if (_camera != null) + { + return _camera.GetDescription(); + } + else + { + return _device.GetDescription(); } } + } - public int BitDepth { - get { - if (_camera != null) { - return _camera.BitsPerPixel; - } else { - return 0; - } - } + public bool DewHeaterOn + { + get => false; + set + { } + } - public bool CanGetGain => GetAvailableIsoOptions().Any(); + public string DisplayName + { + get => _device.Model; + set => throw new NotImplementedException(); + } - public bool CanSetGain => CanGetGain; + public string DriverInfo => "https://retro.kiwi"; + public string DriverVersion => string.Empty; + public double ElectronsPerADU => double.NaN; + public bool EnableSubSample { get; set; } - public int GainMax { - get { - var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) { - if (_camera != null) { - Logger.Error("Problem getting gain max: camera did not report ISO options."); - } - return -1; - } - - return (int)isoOptions.Last().Value; + public double ExposureMax + { + get + { + if (_camera != null) + { + return _camera.ExposureMax; + } + else + { + return double.NaN; } } + } - public int GainMin { - get { - var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) { - if (_camera != null) { - Logger.Error("Problem getting gain min: camera did not report ISO options."); - } - return -1; - } - - return (int)isoOptions.Min(o => o.Value); + public double ExposureMin + { + get + { + if (_camera != null) + { + return _camera.ExposureMin; + } + else + { + return double.NaN; } } + } - public int Gain { - get { - if (_camera != null) { - try { - PropertyValue value = GetPropertyValue(PROPID_ISO); - - return (int)(value.Value == 0xffffff ? 0 : value.Value); - } catch (Exception ex) { - Logger.Error("Problem getting gain", ex); - return -1; - } - } else { + public int Gain + { + get + { + if (_camera != null) + { + try + { + PropertyValue value = GetPropertyValue(PROPID_ISO); + + return (int)(value.Value == 0xffffff ? 0 : value.Value); + } + catch (Exception ex) + { + Logger.Error("Problem getting gain", ex); return -1; } } - - set { - if (_camera != null) { - try { - SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); - RaisePropertyChanged(nameof(Gain)); - } catch (Exception ex) { - Logger.Error($"Problem setting gain to {value}", ex); - } - } + else + { + return -1; } } - public IList Gains { - get { - List gains = new List(); - - foreach (var iso in GetAvailableIsoOptions()) { - if (iso.Value == 0xffffff) { - gains.Add(0); // AUTO - } else { - gains.Add((int)iso.Value); - } + set + { + if (_camera != null) + { + try + { + SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); + RaisePropertyChanged(nameof(Gain)); + } + catch (Exception ex) + { + Logger.Error($"Problem setting gain to {value}", ex); } - - return gains; } } + } - public string Id => "Sony"; - - public string Name { - get => _device.Model; - set => throw new NotImplementedException(); - } + public int GainMax + { + get + { + var isoOptions = GetAvailableIsoOptions(); + if (!isoOptions.Any()) + { + if (_camera != null) + { + Logger.Error("Problem getting gain max: camera did not report ISO options."); + } + return -1; + } - public string DisplayName { - get => _device.Model; - set => throw new NotImplementedException(); + return (int)isoOptions.Last().Value; } + } - public string Category { get => "Sony"; } - - public bool Connected { - get { - return _camera != null; + public int GainMin + { + get + { + var isoOptions = GetAvailableIsoOptions(); + if (!isoOptions.Any()) + { + if (_camera != null) + { + Logger.Error("Problem getting gain min: camera did not report ISO options."); + } + return -1; } + + return (int)isoOptions.Min(o => o.Value); } + } - public string Description { - get { - if (_camera != null) { - return _camera.GetDescription(); - } else { - return _device.GetDescription(); + public IList Gains + { + get + { + List gains = new List(); + + foreach (var iso in GetAvailableIsoOptions()) + { + if (iso.Value == 0xffffff) + { + gains.Add(0); // AUTO + } + else + { + gains.Add((int)iso.Value); } } + + return gains; } + } - public string DriverInfo => "https://retro.kiwi"; + public bool HasBattery => true; + public bool HasDewHeater => false; - public string DriverVersion => string.Empty; + // TODO!!! WE NEED ONE + public bool HasSetupDialog => false; - public double TemperatureSetPoint { - get => double.NaN; + public bool HasShutter => true; + public string Id => "Sony"; - set { - } + public bool LiveViewEnabled + { + get => _liveViewEnabled; + set + { + _liveViewEnabled = value; + RaisePropertyChanged(); } + } - public bool CanSubSample => false; - - public bool EnableSubSample { get; set; } - - public int SubSampleX { get; set; } - - public int SubSampleY { get; set; } + public short MaxBinX { get => 1; set => throw new NotImplementedException(); } + public short MaxBinY { get => 1; set => throw new NotImplementedException(); } - public int SubSampleWidth { get; set; } + public string Name + { + get => _device.Model; + set => throw new NotImplementedException(); + } - public int SubSampleHeight { get; set; } + public int Offset { get => -1; set => throw new NotImplementedException(); } + public int OffsetMax => 0; + public int OffsetMin => 0; - public bool CoolerOn { - get => false; - set { + public double PixelSizeX + { + get + { + if (_camera != null) + { + return _camera.PixelWidth; + } + else + { + return double.NaN; } } + } - public double CoolerPower => double.NaN; - - public bool HasDewHeater => false; - - public bool DewHeaterOn { - get => false; - set { + public double PixelSizeY + { + get + { + if (_camera != null) + { + return _camera.PixelHeight; + } + else + { + return double.NaN; } } + } - public bool CanSetOffset => false; + public short ReadoutMode + { + get => 0; + set { } + } - public int Offset { get => -1; set => throw new NotImplementedException(); } + public short ReadoutModeForNormalImages + { + get => _readoutModeForNormalImages; + set + { + _readoutModeForNormalImages = value; + RaisePropertyChanged(); + } + } - public int OffsetMin => 0; + public short ReadoutModeForSnapImages + { + get => _readoutModeForSnapImages; + set + { + _readoutModeForSnapImages = value; + RaisePropertyChanged(); + } + } - public int OffsetMax => 0; + public IList ReadoutModes => new List { "Default" }; - public bool CanSetUSBLimit => false; + public string SensorName + { + get + { + if (_camera != null) + { + return _camera.SensorName; + } + else + { + return string.Empty; + } + } + } - public int USBLimit { get => -1; set => throw new NotImplementedException(); } + public SensorType SensorType { get => SensorType.RGGB; set => throw new NotImplementedException(); } + public int SubSampleHeight { get; set; } + public int SubSampleWidth { get; set; } + public int SubSampleX { get; set; } + public int SubSampleY { get; set; } + public IList SupportedActions => new List(); + + // Although the driver supports camera temperature, it gets it from the ARW's + // metadata after a photo is taken, because this code doesn't request processed + // ARW, the temp cannot be determined. + public double Temperature + { + get => double.NaN; + /*{ - public int USBLimitMin => -1; + if (_camera != null) { + PropertyValue value = GetPropertyValue(PROPID_TEMPERATURE); - public int USBLimitMax => -1; + return (value.Value) / 10.0; + } else { + return double.NaN; + } + }*/ + } - public int USBLimitStep => -1; + public double TemperatureSetPoint + { + get => double.NaN; - public double ElectronsPerADU => double.NaN; + set + { + } + } - public IList ReadoutModes => new List { "Default" }; + public int USBLimit { get => -1; set => throw new NotImplementedException(); } + public int USBLimitMax => -1; + public int USBLimitMin => -1; + public int USBLimitStep => -1; + #endregion + + #region fields + private AsyncObservableCollection _binningModes; + private SonyCameraInfo _camera = null; + private readonly object _captureLock = new object(); + private SonyDevice _device = null; + private readonly bool _enableNativeCancel; + private readonly IExposureDataFactory _exposureDataFactory; + private bool _liveViewEnabled; + private IProfileService _profileService; + private short _readoutModeForNormalImages; + private short _readoutModeForSnapImages; + private bool _softCancelRequested; + private static readonly uint[] BUSY_STATES = { CAPTURE_CAPTURING, CAPTURE_PROCESSING, CAPTURE_STARTING, CAPTURE_READING }; + private static readonly uint[] CANCELLABLE_STATES = BUSY_STATES; + private static readonly uint[] IDLE_STATES = { CAPTURE_CREATED, CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; + #endregion + + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) + { + _profileService = profileService; + _exposureDataFactory = exposureDataFactory; + _device = device; + _enableNativeCancel = enableNativeCancel; + _softCancelRequested = false; + } - public short ReadoutMode { - get => 0; - set { } - } + #region methods - public short ReadoutModeForSnapImages { - get => _readoutModeForSnapImages; - set { - _readoutModeForSnapImages = value; - RaisePropertyChanged(); - } + public void AbortExposure() + { + if (_enableNativeCancel) + { + TryCancelCapture("abort request"); } - - public short ReadoutModeForNormalImages { - get => _readoutModeForNormalImages; - set { - _readoutModeForNormalImages = value; - RaisePropertyChanged(); - } + else + { + _softCancelRequested = true; + Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); } + } - public AsyncObservableCollection BinningModes { - get { - if (_binningModes == null) { - _binningModes = new AsyncObservableCollection(); - _binningModes.Add(new BinningMode(1, 1)); - } + public string Action(string actionName, string actionParameters) + { + throw new NotImplementedException(); + } - return _binningModes; + public Task Connect(CancellationToken token) + { + return Task.Run(() => + { + try + { + _camera = SonyDriver.GetInstance().OpenCamera(_device.Id); + } + catch (Exception ex) + { + Logger.Error(ex); + _camera = null; } - } - #endregion + NotifyGainPropertiesChanged(); + return _camera != null; + }); + } - #region Supported Methods + public void Disconnect() + { + if (_camera != null) + { + try + { + SonyDriver.GetInstance().CloseCamera(_camera.Handle); + } + catch (Exception ex) + { + Logger.Error(ex); + } - public void StartLiveView(CaptureSequence sequence) { - LiveViewEnabled = true; + _camera = null; + NotifyGainPropertiesChanged(); } + } - public void StopLiveView() { - LiveViewEnabled = false; - } + public Task DownloadExposure(CancellationToken token) + { + return Task.Run(() => + { + byte[] rawImageData = SonyDriver.GetInstance().GetLastImage(); + + var metaData = new ImageMetaData(); + + return _exposureDataFactory.CreateRAWExposureData( + converter: _profileService.ActiveProfile.CameraSettings.RawConverter, + rawBytes: rawImageData, + rawType: "arw", + bitDepth: this.BitDepth, + metaData: metaData); + }); + } - public Task Connect(CancellationToken token) { - return Task.Run(() => { - try { - _camera = SonyDriver.GetInstance().OpenCamera(_device.Id); - } catch (Exception ex) { - Logger.Error(ex); - _camera = null; - } + public Task DownloadLiveView(CancellationToken token) + { + return Task.Run(() => + { + using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) + { + memStream.Position = 0; - NotifyGainPropertiesChanged(); - return _camera != null; - }); - } + JpegBitmapDecoder decoder = + new JpegBitmapDecoder(memStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.OnLoad); - public void Disconnect() { - if (_camera != null) { - try { - SonyDriver.GetInstance().CloseCamera(_camera.Handle); - } catch (Exception ex) { - Logger.Error(ex); - } + FormatConvertedBitmap bitmap = new FormatConvertedBitmap(); + bitmap.BeginInit(); + bitmap.Source = decoder.Frames[0]; + bitmap.DestinationFormat = System.Windows.Media.PixelFormats.Gray16; + bitmap.EndInit(); - _camera = null; - NotifyGainPropertiesChanged(); + ushort[] outArray = new ushort[bitmap.PixelWidth * bitmap.PixelHeight]; + bitmap.CopyPixels(outArray, 2 * bitmap.PixelWidth, 0); + + var metaData = new ImageMetaData(); + + return _exposureDataFactory.CreateImageArrayExposureData( + input: outArray, + width: bitmap.PixelWidth, + height: bitmap.PixelHeight, + bitDepth: 16, + isBayered: false, + metaData: metaData); } - } + }); + } - public Task DownloadLiveView(CancellationToken token) { - return Task.Run(() => { - using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) { - memStream.Position = 0; + public void SendCommandBlind(string command, bool raw = true) + { + throw new NotImplementedException(); + } - JpegBitmapDecoder decoder = - new JpegBitmapDecoder(memStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.OnLoad); + public bool SendCommandBool(string command, bool raw = true) + { + throw new NotImplementedException(); + } - FormatConvertedBitmap bitmap = new FormatConvertedBitmap(); - bitmap.BeginInit(); - bitmap.Source = decoder.Frames[0]; - bitmap.DestinationFormat = System.Windows.Media.PixelFormats.Gray16; - bitmap.EndInit(); + public string SendCommandString(string command, bool raw = true) + { + throw new NotImplementedException(); + } - ushort[] outArray = new ushort[bitmap.PixelWidth * bitmap.PixelHeight]; - bitmap.CopyPixels(outArray, 2 * bitmap.PixelWidth, 0); + public void SetBinning(short x, short y) + { + // Ignore + } - var metaData = new ImageMetaData(); + public void SetupDialog() + { + throw new NotImplementedException(); + } - return _exposureDataFactory.CreateImageArrayExposureData( - input: outArray, - width: bitmap.PixelWidth, - height: bitmap.PixelHeight, - bitDepth: 16, - isBayered: false, - metaData: metaData); + public void StartExposure(CaptureSequence sequence) + { + if (_camera != null) + { + SonyDriver driver = SonyDriver.GetInstance(); + lock (_captureLock) + { + _softCancelRequested = false; + if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) + { + Logger.Warning("Cannot start exposure: capture status unavailable."); + throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); } - }); - } - public void SetupDialog() { - throw new NotImplementedException(); - } - public void StartExposure(CaptureSequence sequence) { - if (_camera != null) { - SonyDriver driver = SonyDriver.GetInstance(); - lock (_captureLock) { - _softCancelRequested = false; - if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { - Logger.Warning("Cannot start exposure: capture status unavailable."); - throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); - } else { - if (BUSY_STATES.Contains(captureStatus)) { - if (_enableNativeCancel) { - TryCancelCapture("start exposure reset"); - } - 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})."); - } + + + if (BUSY_STATES.Contains(captureStatus)) + { + if (_enableNativeCancel) + { + TryCancelCapture("start exposure reset"); } + 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."); } - lock (_captureLock) { - double exposureTime = sequence.ExposureTime; - driver.StartCapture(_camera.Handle, (float)exposureTime); + 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})."); } + + double exposureTime = sequence.ExposureTime; + driver.StartCapture(_camera.Handle, (float)exposureTime); } } + } - public void StopExposure() { - AbortExposure(); - } + public void StartLiveView(CaptureSequence sequence) + { + LiveViewEnabled = true; + } - public void AbortExposure() { - if (_enableNativeCancel) { - TryCancelCapture("abort request"); - } else { - _softCancelRequested = true; - Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); - } + public void StopExposure() + { + AbortExposure(); + } + + public void StopLiveView() + { + LiveViewEnabled = false; + } + + public void UpdateSubSampleArea() + { + if (_camera == null) + { + EnableSubSample = false; + SubSampleX = 0; + SubSampleY = 0; + SubSampleWidth = 0; + SubSampleHeight = 0; + return; } - public async Task WaitUntilExposureIsReady(CancellationToken token) { - using (token.Register(AbortExposure)) { - uint[] completionStates = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; + if (EnableSubSample && !CanSubSample) + { + Logger.Warning("Sub-sampling requested but not supported for Sony cameras. Falling back to full frame."); + EnableSubSample = false; + } - SonyDriver driver = SonyDriver.GetInstance(); + // Sony cameras currently expose the entire frame, so always reset to the sensor dimensions. + SubSampleX = 0; + SubSampleY = 0; + SubSampleWidth = _camera.ImageSize.Width; + SubSampleHeight = _camera.ImageSize.Height; + } - try { - uint captureStatus; - lock (_captureLock) { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) { - throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); - } + 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; + lock (_captureLock) + { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) + { + 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(", ", completionStates)}"); + + while (!completionStates.Contains(captureStatus)) + { + await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); + if (!_enableNativeCancel && token.IsCancellationRequested) + { + _softCancelRequested = true; } - Logger.Info( - $"Waiting for image to be ready, current state is {captureStatus}, completion states are {String.Join(", ", completionStates)}"); - - while (!completionStates.Contains(captureStatus)) { - await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); - if (!_enableNativeCancel && token.IsCancellationRequested) { - _softCancelRequested = true; - } - lock (_captureLock) { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { - throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); - } + lock (_captureLock) + { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) + { + throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); } } + } - 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); - throw new SonyException("Problem while waiting for image to be ready (see log)"); + 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); + throw new SonyException("Problem while waiting for image to be ready (see log)"); + } } + } - public Task DownloadExposure(CancellationToken token) { - return Task.Run(() => { - byte[] rawImageData = SonyDriver.GetInstance().GetLastImage(); + private IReadOnlyList GetAvailableIsoOptions() + { + if (_camera == null) + { + return Array.Empty(); + } - var metaData = new ImageMetaData(); + uint[] propertyCandidates = { PROPID_ISOS, PROPID_ISO }; - return _exposureDataFactory.CreateRAWExposureData( - converter: _profileService.ActiveProfile.CameraSettings.RawConverter, - rawBytes: rawImageData, - rawType: "arw", - bitDepth: this.BitDepth, - metaData: metaData); - }); + foreach (var propertyId in propertyCandidates) + { + try + { + var options = _camera.GetPropertyInfo(propertyId)?.Options()?.Where(o => o.Value <= 0x00FFFFFF).ToList(); + if (options != null && options.Count > 0) + { + return options; + } + } + catch (Exception ex) + { + Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}"); + } } - #endregion - #region Unsupported Methods + Logger.Warning("Camera did not report any ISO options via known properties (registry ISO list may be empty until the camera learns it)."); + return Array.Empty(); + } - public string Action(string actionName, string actionParameters) { - throw new NotImplementedException(); - } + private PropertyValue GetPropertyValue(uint id) + { + return SonyDriver.GetInstance().GetProperty(_camera.Handle, id); + } + private void NotifyGainPropertiesChanged() + { + RaisePropertyChanged(nameof(CanGetGain)); + RaisePropertyChanged(nameof(CanSetGain)); + RaisePropertyChanged(nameof(GainMin)); + RaisePropertyChanged(nameof(GainMax)); + RaisePropertyChanged(nameof(Gain)); + RaisePropertyChanged(nameof(Gains)); + } - public void SendCommandBlind(string command, bool raw = true) { - throw new NotImplementedException(); + private bool TryCancelCapture(string reason) + { + if (_camera == null) + { + return false; } - public bool SendCommandBool(string command, bool raw = true) { - throw new NotImplementedException(); + if (!_enableNativeCancel) + { + Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); + return false; } - public string SendCommandString(string command, bool raw = true) { - throw new NotImplementedException(); - } + lock (_captureLock) + { + try + { + SonyDriver driver = SonyDriver.GetInstance(); + if (!TryGetCaptureStatusLocked(driver, out var status, reason)) + { + return false; + } - public void SetBinning(short x, short y) { - // Ignore - } - - public void UpdateSubSampleArea() { - if (_camera == null) { - EnableSubSample = false; - SubSampleX = 0; - SubSampleY = 0; - SubSampleWidth = 0; - SubSampleHeight = 0; - return; - } + if (!CANCELLABLE_STATES.Contains(status)) + { + Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); + return false; + } - if (EnableSubSample && !CanSubSample) { - Logger.Warning("Sub-sampling requested but not supported for Sony cameras. Falling back to full frame."); - EnableSubSample = 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; } - - // Sony cameras currently expose the entire frame, so always reset to the sensor dimensions. - SubSampleX = 0; - SubSampleY = 0; - SubSampleWidth = _camera.ImageSize.Width; - SubSampleHeight = _camera.ImageSize.Height; } + } - #endregion - - - // TODO!!! WE NEED ONE - public bool HasSetupDialog => false; - - public IList SupportedActions => new List(); + private bool TryGetCaptureStatusLocked(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}): {ex.Message}"); + status = CAPTURE_FAILED; + return false; + } } -} + + #endregion +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index ad54320..c4d01b7 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -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")] @@ -33,6 +33,8 @@ [assembly: AssemblyMetadata("LicenseURL", "https://www.mozilla.org/en-US/MPL/2.0/")] // The repository where your pluggin is hosted [assembly: AssemblyMetadata("Repository", "https://github.com/dougforpres/NINASonyCameraPlugin")] +// Note on native cancel: stability depends on camera model and Windows driver stack; leave disabled if aborts crash. +[assembly: AssemblyMetadata("NativeCancelHint", "Native CancelCapture stability varies by camera model and Windows driver stack; disable if aborts crash.")] // The following attributes are optional for the official manifest meta data diff --git a/README.md b/README.md index a98c5bd..254ec52 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 From 1d7cdab8083624020defca86ed59b3568b06d69b Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 11:53:20 +0100 Subject: [PATCH 12/31] undo csharpening --- Drivers/CameraDriver.cs | 1262 +++++++++++++++++---------------------- 1 file changed, 561 insertions(+), 701 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 5f62182..c34a917 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -1,863 +1,723 @@ -namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers; - -public class CameraDriver : BaseINPC, ICamera -{ - #region constants - private const uint CAPTURE_CANCELLED = 0x0003; - private const uint CAPTURE_CAPTURING = 0x0001; - private const uint CAPTURE_COMPLETE = 0x0004; - - // Registry-backed list of learnt ISOs (may be empty until learnt) - - // Capture Status - private const uint CAPTURE_CREATED = 0x0000; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using FTD2XX_NET; +using NINA.Core.Enum; +using NINA.Core.Model.Equipment; +using NINA.Core.Utility; +using NINA.Core.Utility.Notification; +using NINA.Equipment.Interfaces; +using NINA.Equipment.Interfaces.Mediator; +using NINA.Equipment.Model; +using NINA.Equipment.SDK.CameraSDKs.ASTPANSDK; +using NINA.Equipment.Utility; +using NINA.Image.ImageData; +using NINA.Image.Interfaces; +using NINA.Profile; +using NINA.Profile.Interfaces; +using Sony; + +namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers { + public class CameraDriver : BaseINPC, ICamera { + // Some camera settings we are interested in + private const uint PROPID_BATTERY = 53784; + private const uint PROPID_ISO = 0xD21E; // Actual ISO currently set + private const uint PROPID_ISOS = 0xFFFE; // Registry-backed list of learnt ISOs (may be empty until learnt) + + // Capture Status + private const uint CAPTURE_CREATED = 0x0000; + private const uint CAPTURE_CAPTURING = 0x0001; + private const uint CAPTURE_FAILED = 0x0002; + private const uint CAPTURE_CANCELLED = 0x0003; + private const uint CAPTURE_COMPLETE = 0x0004; + private const uint CAPTURE_STARTING = 0x8001; + private const uint CAPTURE_READING = 0x8002; + private const uint CAPTURE_PROCESSING = 0x8003; + 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[] CANCELLABLE_STATES = BUSY_STATES; + private readonly bool _enableNativeCancel; + private bool _softCancelRequested; + + private SonyCameraInfo _camera = null; + private SonyDevice _device = null; + private IProfileService _profileService; + private readonly IExposureDataFactory _exposureDataFactory; + private bool _liveViewEnabled; + private short _readoutModeForSnapImages; + private short _readoutModeForNormalImages; + private AsyncObservableCollection _binningModes; + private readonly object _captureLock = new object(); + + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) { + _profileService = profileService; + _exposureDataFactory = exposureDataFactory; + _device = device; + _enableNativeCancel = enableNativeCancel; + _softCancelRequested = false; + } + + #region Internal Helpers + + private PropertyValue GetPropertyValue(uint id) { + return SonyDriver.GetInstance().GetProperty(_camera.Handle, id); + } + + private IReadOnlyList GetAvailableIsoOptions() { + if (_camera == null) { + return Array.Empty(); + } + + uint[] propertyCandidates = { PROPID_ISOS, PROPID_ISO }; + + foreach (var propertyId in propertyCandidates) { + try { + var options = _camera.GetPropertyInfo(propertyId)?.Options()?.Where(o => o.Value <= 0x00FFFFFF).ToList(); + if (options != null && options.Count > 0) { + return options; + } + } catch (Exception ex) { + Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}"); + } + } - private const uint CAPTURE_FAILED = 0x0002; - private const uint CAPTURE_PROCESSING = 0x8003; - private const uint CAPTURE_READING = 0x8002; - private const uint CAPTURE_STARTING = 0x8001; + Logger.Warning("Camera did not report any ISO options via known properties (registry ISO list may be empty until the camera learns it)."); + return Array.Empty(); + } - // Some camera settings we are interested in - private const uint PROPID_BATTERY = 53784; + private void NotifyGainPropertiesChanged() { + RaisePropertyChanged(nameof(CanGetGain)); + RaisePropertyChanged(nameof(CanSetGain)); + RaisePropertyChanged(nameof(GainMin)); + RaisePropertyChanged(nameof(GainMax)); + RaisePropertyChanged(nameof(Gain)); + RaisePropertyChanged(nameof(Gains)); + } - private const uint PROPID_ISO = 0xD21E; + private bool TryCancelCapture(string reason) { + if (_camera == null) { + return false; + } - // Actual ISO currently set - private const uint PROPID_ISOS = 0xFFFE; + if (!_enableNativeCancel) { + Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); + return false; + } - #endregion + lock (_captureLock) { + try { + SonyDriver driver = SonyDriver.GetInstance(); + if (!TryGetCaptureStatusLocked(driver, out var status, reason)) { + return false; + } - #region properties + if (!CANCELLABLE_STATES.Contains(status)) { + Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); + return false; + } - public int BatteryLevel - { - get - { - if (_camera != null) - { - return (int)GetPropertyValue(PROPID_BATTERY).Value; + 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; + } } - else - { - return 0; + } + + private bool TryGetCaptureStatusLocked(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}): {ex.Message}"); + status = CAPTURE_FAILED; + return false; } } - } - public short BayerOffsetX { get => 1; set => throw new NotImplementedException(); } - public short BayerOffsetY { get => 1; set => throw new NotImplementedException(); } + #endregion - public AsyncObservableCollection BinningModes - { - get - { - if (_binningModes == null) - { - _binningModes = new AsyncObservableCollection(); - _binningModes.Add(new BinningMode(1, 1)); - } + #region Supported Properties + + public bool HasShutter => true; + + // Although the driver supports camera temperature, it gets it from the ARW's + // metadata after a photo is taken, because this code doesn't request processed + // ARW, the temp cannot be determined. + public double Temperature { + get => double.NaN; + /*{ + + if (_camera != null) { + PropertyValue value = GetPropertyValue(PROPID_TEMPERATURE); - return _binningModes; + return (value.Value) / 10.0; + } else { + return double.NaN; + } + }*/ } - } - public short BinX { get => 1; set => throw new NotImplementedException(); } - public short BinY { get => 1; set => throw new NotImplementedException(); } + public short BinX { get => 1; set => throw new NotImplementedException(); } + public short BinY { get => 1; set => throw new NotImplementedException(); } - public int BitDepth - { - get - { - if (_camera != null) - { - return _camera.BitsPerPixel; - } - else - { - return 0; + public string SensorName { + get { + if (_camera != null) { + return _camera.SensorName; + } else { + return string.Empty; + } } } - } - public CameraStates CameraState => CameraStates.NoState; + public SensorType SensorType { get => SensorType.RGGB; set => throw new NotImplementedException(); } + + public short BayerOffsetX { get => 1; set => throw new NotImplementedException(); } + + public short BayerOffsetY { get => 1; set => throw new NotImplementedException(); } - public int CameraXSize - { - get - { - if (_camera != null) - { - return _camera.ImageSize.Width; + public int CameraXSize { + get { + if (_camera != null) { + return _camera.ImageSize.Width; + } + else { + return 0; + } } - else - { - return 0; + } + + public int CameraYSize { + get { + if (_camera != null) { + return _camera.ImageSize.Height; + } else { + return 0; + } } } - } - public int CameraYSize - { - get - { - if (_camera != null) - { - return _camera.ImageSize.Height; + public double ExposureMin { + get { + if (_camera != null) { + return _camera.ExposureMin; + } else { + return double.NaN; + } } - else - { - return 0; + } + + public double ExposureMax { + get { + if (_camera != null) { + return _camera.ExposureMax; + } else { + return double.NaN; + } } } - } - public bool CanGetGain => GetAvailableIsoOptions().Any(); - public bool CanSetGain => CanGetGain; - public bool CanSetOffset => false; - public bool CanSetTemperature => false; - public bool CanSetUSBLimit => false; + public short MaxBinX { get => 1; set => throw new NotImplementedException(); } - // TODO + public short MaxBinY { get => 1; set => throw new NotImplementedException(); } - public bool CanShowLiveView - { - get - { - if (_camera != null) - { - return _camera.SupportsPreview(); + public double PixelSizeX { + get { + if (_camera != null) { + return _camera.PixelWidth; + } else { + return double.NaN; + } } - else - { - return false; + } + + public double PixelSizeY { + get { + if (_camera != null) { + return _camera.PixelHeight; + } else { + return double.NaN; + } } } - } - public bool CanSubSample => false; - public string Category { get => "Sony"; } + public bool CanSetTemperature => false; - public bool Connected - { - get - { - return _camera != null; + public CameraStates CameraState => CameraStates.NoState; // TODO + + public bool CanShowLiveView { + get { + if (_camera != null) { + return _camera.SupportsPreview(); + } else { + return false; + } + } } - } - public bool CoolerOn - { - get => false; - set - { + public bool LiveViewEnabled { + get => _liveViewEnabled; + set { + _liveViewEnabled = value; + RaisePropertyChanged(); + } } - } - public double CoolerPower => double.NaN; + public bool HasBattery => true; - public string Description - { - get - { - if (_camera != null) - { - return _camera.GetDescription(); - } - else - { - return _device.GetDescription(); + public int BatteryLevel { + get { + if (_camera != null) { + return (int)GetPropertyValue(PROPID_BATTERY).Value; + } else { + return 0; + } } } - } - public bool DewHeaterOn - { - get => false; - set - { + public int BitDepth { + get { + if (_camera != null) { + return _camera.BitsPerPixel; + } else { + return 0; + } + } } - } - public string DisplayName - { - get => _device.Model; - set => throw new NotImplementedException(); - } + public bool CanGetGain => GetAvailableIsoOptions().Any(); - public string DriverInfo => "https://retro.kiwi"; - public string DriverVersion => string.Empty; - public double ElectronsPerADU => double.NaN; - public bool EnableSubSample { get; set; } + public bool CanSetGain => CanGetGain; - public double ExposureMax - { - get - { - if (_camera != null) - { - return _camera.ExposureMax; - } - else - { - return double.NaN; - } - } - } + public int GainMax { + get { + var isoOptions = GetAvailableIsoOptions(); + if (!isoOptions.Any()) { + if (_camera != null) { + Logger.Error("Problem getting gain max: camera did not report ISO options."); + } + return -1; + } - public double ExposureMin - { - get - { - if (_camera != null) - { - return _camera.ExposureMin; - } - else - { - return double.NaN; + return (int)isoOptions.Last().Value; } } - } - public int Gain - { - get - { - if (_camera != null) - { - try - { - PropertyValue value = GetPropertyValue(PROPID_ISO); - - return (int)(value.Value == 0xffffff ? 0 : value.Value); - } - catch (Exception ex) - { - Logger.Error("Problem getting gain", ex); + public int GainMin { + get { + var isoOptions = GetAvailableIsoOptions(); + if (!isoOptions.Any()) { + if (_camera != null) { + Logger.Error("Problem getting gain min: camera did not report ISO options."); + } return -1; } - } - else - { - return -1; + + return (int)isoOptions.Min(o => o.Value); } } - set - { - if (_camera != null) - { - try - { - SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); - RaisePropertyChanged(nameof(Gain)); + public int Gain { + get { + if (_camera != null) { + try { + PropertyValue value = GetPropertyValue(PROPID_ISO); + + return (int)(value.Value == 0xffffff ? 0 : value.Value); + } catch (Exception ex) { + Logger.Error("Problem getting gain", ex); + return -1; + } + } else { + return -1; } - catch (Exception ex) - { - Logger.Error($"Problem setting gain to {value}", ex); + } + + set { + if (_camera != null) { + try { + SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); + RaisePropertyChanged(nameof(Gain)); + } catch (Exception ex) { + Logger.Error($"Problem setting gain to {value}", ex); + } } } } - } - public int GainMax - { - get - { - var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) - { - if (_camera != null) - { - Logger.Error("Problem getting gain max: camera did not report ISO options."); + public IList Gains { + get { + List gains = new List(); + + foreach (var iso in GetAvailableIsoOptions()) { + if (iso.Value == 0xffffff) { + gains.Add(0); // AUTO + } else { + gains.Add((int)iso.Value); + } } - return -1; + + return gains; } + } - return (int)isoOptions.Last().Value; + public string Id => "Sony"; + + public string Name { + get => _device.Model; + set => throw new NotImplementedException(); } - } - public int GainMin - { - get - { - var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) - { - if (_camera != null) - { - Logger.Error("Problem getting gain min: camera did not report ISO options."); - } - return -1; - } + public string DisplayName { + get => _device.Model; + set => throw new NotImplementedException(); + } - return (int)isoOptions.Min(o => o.Value); + public string Category { get => "Sony"; } + + public bool Connected { + get { + return _camera != null; + } } - } - public IList Gains - { - get - { - List gains = new List(); - - foreach (var iso in GetAvailableIsoOptions()) - { - if (iso.Value == 0xffffff) - { - gains.Add(0); // AUTO - } - else - { - gains.Add((int)iso.Value); + public string Description { + get { + if (_camera != null) { + return _camera.GetDescription(); + } else { + return _device.GetDescription(); } } - - return gains; } - } - public bool HasBattery => true; - public bool HasDewHeater => false; + public string DriverInfo => "https://retro.kiwi"; - // TODO!!! WE NEED ONE - public bool HasSetupDialog => false; + public string DriverVersion => string.Empty; - public bool HasShutter => true; - public string Id => "Sony"; + public double TemperatureSetPoint { + get => double.NaN; - public bool LiveViewEnabled - { - get => _liveViewEnabled; - set - { - _liveViewEnabled = value; - RaisePropertyChanged(); + set { + } } - } - public short MaxBinX { get => 1; set => throw new NotImplementedException(); } - public short MaxBinY { get => 1; set => throw new NotImplementedException(); } + public bool CanSubSample => false; - public string Name - { - get => _device.Model; - set => throw new NotImplementedException(); - } + public bool EnableSubSample { get; set; } - public int Offset { get => -1; set => throw new NotImplementedException(); } - public int OffsetMax => 0; - public int OffsetMin => 0; + public int SubSampleX { get; set; } - public double PixelSizeX - { - get - { - if (_camera != null) - { - return _camera.PixelWidth; - } - else - { - return double.NaN; - } - } - } + public int SubSampleY { get; set; } - public double PixelSizeY - { - get - { - if (_camera != null) - { - return _camera.PixelHeight; - } - else - { - return double.NaN; - } - } - } + public int SubSampleWidth { get; set; } - public short ReadoutMode - { - get => 0; - set { } - } + public int SubSampleHeight { get; set; } - public short ReadoutModeForNormalImages - { - get => _readoutModeForNormalImages; - set - { - _readoutModeForNormalImages = value; - RaisePropertyChanged(); + public bool CoolerOn { + get => false; + set { + } } - } - public short ReadoutModeForSnapImages - { - get => _readoutModeForSnapImages; - set - { - _readoutModeForSnapImages = value; - RaisePropertyChanged(); - } - } + public double CoolerPower => double.NaN; - public IList ReadoutModes => new List { "Default" }; + public bool HasDewHeater => false; - public string SensorName - { - get - { - if (_camera != null) - { - return _camera.SensorName; - } - else - { - return string.Empty; + public bool DewHeaterOn { + get => false; + set { } } - } - public SensorType SensorType { get => SensorType.RGGB; set => throw new NotImplementedException(); } - public int SubSampleHeight { get; set; } - public int SubSampleWidth { get; set; } - public int SubSampleX { get; set; } - public int SubSampleY { get; set; } - public IList SupportedActions => new List(); - - // Although the driver supports camera temperature, it gets it from the ARW's - // metadata after a photo is taken, because this code doesn't request processed - // ARW, the temp cannot be determined. - public double Temperature - { - get => double.NaN; - /*{ + public bool CanSetOffset => false; - if (_camera != null) { - PropertyValue value = GetPropertyValue(PROPID_TEMPERATURE); + public int Offset { get => -1; set => throw new NotImplementedException(); } - return (value.Value) / 10.0; - } else { - return double.NaN; - } - }*/ - } + public int OffsetMin => 0; - public double TemperatureSetPoint - { - get => double.NaN; + public int OffsetMax => 0; - set - { - } - } + public bool CanSetUSBLimit => false; - public int USBLimit { get => -1; set => throw new NotImplementedException(); } - public int USBLimitMax => -1; - public int USBLimitMin => -1; - public int USBLimitStep => -1; - #endregion - - #region fields - private AsyncObservableCollection _binningModes; - private SonyCameraInfo _camera = null; - private readonly object _captureLock = new object(); - private SonyDevice _device = null; - private readonly bool _enableNativeCancel; - private readonly IExposureDataFactory _exposureDataFactory; - private bool _liveViewEnabled; - private IProfileService _profileService; - private short _readoutModeForNormalImages; - private short _readoutModeForSnapImages; - private bool _softCancelRequested; - private static readonly uint[] BUSY_STATES = { CAPTURE_CAPTURING, CAPTURE_PROCESSING, CAPTURE_STARTING, CAPTURE_READING }; - private static readonly uint[] CANCELLABLE_STATES = BUSY_STATES; - private static readonly uint[] IDLE_STATES = { CAPTURE_CREATED, CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; - #endregion - - public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) - { - _profileService = profileService; - _exposureDataFactory = exposureDataFactory; - _device = device; - _enableNativeCancel = enableNativeCancel; - _softCancelRequested = false; - } + public int USBLimit { get => -1; set => throw new NotImplementedException(); } - #region methods + public int USBLimitMin => -1; - public void AbortExposure() - { - if (_enableNativeCancel) - { - TryCancelCapture("abort request"); - } - else - { - _softCancelRequested = true; - Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); - } - } + public int USBLimitMax => -1; - public string Action(string actionName, string actionParameters) - { - throw new NotImplementedException(); - } + public int USBLimitStep => -1; - public Task Connect(CancellationToken token) - { - return Task.Run(() => - { - try - { - _camera = SonyDriver.GetInstance().OpenCamera(_device.Id); - } - catch (Exception ex) - { - Logger.Error(ex); - _camera = null; - } + public double ElectronsPerADU => double.NaN; - NotifyGainPropertiesChanged(); - return _camera != null; - }); - } + public IList ReadoutModes => new List { "Default" }; + + public short ReadoutMode { + get => 0; + set { } + } - public void Disconnect() - { - if (_camera != null) - { - try - { - SonyDriver.GetInstance().CloseCamera(_camera.Handle); + public short ReadoutModeForSnapImages { + get => _readoutModeForSnapImages; + set { + _readoutModeForSnapImages = value; + RaisePropertyChanged(); } - catch (Exception ex) - { - Logger.Error(ex); + } + + public short ReadoutModeForNormalImages { + get => _readoutModeForNormalImages; + set { + _readoutModeForNormalImages = value; + RaisePropertyChanged(); } + } - _camera = null; - NotifyGainPropertiesChanged(); + public AsyncObservableCollection BinningModes { + get { + if (_binningModes == null) { + _binningModes = new AsyncObservableCollection(); + _binningModes.Add(new BinningMode(1, 1)); + } + + return _binningModes; + } } - } - public Task DownloadExposure(CancellationToken token) - { - return Task.Run(() => - { - byte[] rawImageData = SonyDriver.GetInstance().GetLastImage(); - - var metaData = new ImageMetaData(); - - return _exposureDataFactory.CreateRAWExposureData( - converter: _profileService.ActiveProfile.CameraSettings.RawConverter, - rawBytes: rawImageData, - rawType: "arw", - bitDepth: this.BitDepth, - metaData: metaData); - }); - } + #endregion - public Task DownloadLiveView(CancellationToken token) - { - return Task.Run(() => - { - using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) - { - memStream.Position = 0; + #region Supported Methods - JpegBitmapDecoder decoder = - new JpegBitmapDecoder(memStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.OnLoad); + public void StartLiveView(CaptureSequence sequence) { + LiveViewEnabled = true; + } - FormatConvertedBitmap bitmap = new FormatConvertedBitmap(); - bitmap.BeginInit(); - bitmap.Source = decoder.Frames[0]; - bitmap.DestinationFormat = System.Windows.Media.PixelFormats.Gray16; - bitmap.EndInit(); + public void StopLiveView() { + LiveViewEnabled = false; + } - ushort[] outArray = new ushort[bitmap.PixelWidth * bitmap.PixelHeight]; - bitmap.CopyPixels(outArray, 2 * bitmap.PixelWidth, 0); + public Task Connect(CancellationToken token) { + return Task.Run(() => { + try { + _camera = SonyDriver.GetInstance().OpenCamera(_device.Id); + } catch (Exception ex) { + Logger.Error(ex); + _camera = null; + } - var metaData = new ImageMetaData(); + NotifyGainPropertiesChanged(); + return _camera != null; + }); + } - return _exposureDataFactory.CreateImageArrayExposureData( - input: outArray, - width: bitmap.PixelWidth, - height: bitmap.PixelHeight, - bitDepth: 16, - isBayered: false, - metaData: metaData); + public void Disconnect() { + if (_camera != null) { + try { + SonyDriver.GetInstance().CloseCamera(_camera.Handle); + } catch (Exception ex) { + Logger.Error(ex); + } + + _camera = null; + NotifyGainPropertiesChanged(); } - }); - } + } - public void SendCommandBlind(string command, bool raw = true) - { - throw new NotImplementedException(); - } + public Task DownloadLiveView(CancellationToken token) { + return Task.Run(() => { + using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) { + memStream.Position = 0; - public bool SendCommandBool(string command, bool raw = true) - { - throw new NotImplementedException(); - } + JpegBitmapDecoder decoder = + new JpegBitmapDecoder(memStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.OnLoad); - public string SendCommandString(string command, bool raw = true) - { - throw new NotImplementedException(); - } + FormatConvertedBitmap bitmap = new FormatConvertedBitmap(); + bitmap.BeginInit(); + bitmap.Source = decoder.Frames[0]; + bitmap.DestinationFormat = System.Windows.Media.PixelFormats.Gray16; + bitmap.EndInit(); - public void SetBinning(short x, short y) - { - // Ignore - } + ushort[] outArray = new ushort[bitmap.PixelWidth * bitmap.PixelHeight]; + bitmap.CopyPixels(outArray, 2 * bitmap.PixelWidth, 0); - public void SetupDialog() - { - throw new NotImplementedException(); - } + var metaData = new ImageMetaData(); - public void StartExposure(CaptureSequence sequence) - { - if (_camera != null) - { - SonyDriver driver = SonyDriver.GetInstance(); - lock (_captureLock) - { - _softCancelRequested = false; - if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) - { - Logger.Warning("Cannot start exposure: capture status unavailable."); - throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); + return _exposureDataFactory.CreateImageArrayExposureData( + input: outArray, + width: bitmap.PixelWidth, + height: bitmap.PixelHeight, + bitDepth: 16, + isBayered: false, + metaData: metaData); } + }); + } + public void SetupDialog() { + throw new NotImplementedException(); + } + public void StartExposure(CaptureSequence sequence) { + if (_camera != null) { + SonyDriver driver = SonyDriver.GetInstance(); + lock (_captureLock) { + _softCancelRequested = false; + if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { + Logger.Warning("Cannot start exposure: capture status unavailable."); + throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); + } + if (BUSY_STATES.Contains(captureStatus)) { + if (_enableNativeCancel) { + TryCancelCapture("start exposure reset"); + } + 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 (BUSY_STATES.Contains(captureStatus)) - { - if (_enableNativeCancel) - { - TryCancelCapture("start exposure reset"); + 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})."); } - 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})."); + double exposureTime = sequence.ExposureTime; + driver.StartCapture(_camera.Handle, (float)exposureTime); } - - double exposureTime = sequence.ExposureTime; - driver.StartCapture(_camera.Handle, (float)exposureTime); } } - } - - public void StartLiveView(CaptureSequence sequence) - { - LiveViewEnabled = true; - } - - public void StopExposure() - { - AbortExposure(); - } - - public void StopLiveView() - { - LiveViewEnabled = false; - } - public void UpdateSubSampleArea() - { - if (_camera == null) - { - EnableSubSample = false; - SubSampleX = 0; - SubSampleY = 0; - SubSampleWidth = 0; - SubSampleHeight = 0; - return; + public void StopExposure() { + AbortExposure(); } - if (EnableSubSample && !CanSubSample) - { - Logger.Warning("Sub-sampling requested but not supported for Sony cameras. Falling back to full frame."); - EnableSubSample = false; + public void AbortExposure() { + if (_enableNativeCancel) { + TryCancelCapture("abort request"); + } else { + _softCancelRequested = true; + Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); + } } - // Sony cameras currently expose the entire frame, so always reset to the sensor dimensions. - SubSampleX = 0; - SubSampleY = 0; - SubSampleWidth = _camera.ImageSize.Width; - SubSampleHeight = _camera.ImageSize.Height; - } + public async Task WaitUntilExposureIsReady(CancellationToken token) { + using (token.Register(AbortExposure)) { + uint[] completionStates = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; - 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; - lock (_captureLock) - { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) - { - 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(", ", completionStates)}"); - - while (!completionStates.Contains(captureStatus)) - { - await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); - if (!_enableNativeCancel && token.IsCancellationRequested) - { - _softCancelRequested = true; - } + SonyDriver driver = SonyDriver.GetInstance(); - lock (_captureLock) - { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) - { + try { + uint captureStatus; + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) { 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(", ", completionStates)}"); - 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)."); + while (!completionStates.Contains(captureStatus)) { + await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); + if (!_enableNativeCancel && token.IsCancellationRequested) { + _softCancelRequested = true; + } + + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { + throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); + } + } + } + + 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); + throw new SonyException("Problem while waiting for image to be ready (see log)"); } } - catch (TaskCanceledException) - { - Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); - throw; - } - catch (Exception ex) - { - Logger.Error("WaitUntilExposureIsReady got exception", ex); - throw new SonyException("Problem while waiting for image to be ready (see log)"); - } } - } - private IReadOnlyList GetAvailableIsoOptions() - { - if (_camera == null) - { - return Array.Empty(); - } + public Task DownloadExposure(CancellationToken token) { + return Task.Run(() => { + byte[] rawImageData = SonyDriver.GetInstance().GetLastImage(); - uint[] propertyCandidates = { PROPID_ISOS, PROPID_ISO }; + var metaData = new ImageMetaData(); - foreach (var propertyId in propertyCandidates) - { - try - { - var options = _camera.GetPropertyInfo(propertyId)?.Options()?.Where(o => o.Value <= 0x00FFFFFF).ToList(); - if (options != null && options.Count > 0) - { - return options; - } - } - catch (Exception ex) - { - Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}"); - } + return _exposureDataFactory.CreateRAWExposureData( + converter: _profileService.ActiveProfile.CameraSettings.RawConverter, + rawBytes: rawImageData, + rawType: "arw", + bitDepth: this.BitDepth, + metaData: metaData); + }); } + #endregion - Logger.Warning("Camera did not report any ISO options via known properties (registry ISO list may be empty until the camera learns it)."); - return Array.Empty(); - } + #region Unsupported Methods - private PropertyValue GetPropertyValue(uint id) - { - return SonyDriver.GetInstance().GetProperty(_camera.Handle, id); - } + public string Action(string actionName, string actionParameters) { + throw new NotImplementedException(); + } - private void NotifyGainPropertiesChanged() - { - RaisePropertyChanged(nameof(CanGetGain)); - RaisePropertyChanged(nameof(CanSetGain)); - RaisePropertyChanged(nameof(GainMin)); - RaisePropertyChanged(nameof(GainMax)); - RaisePropertyChanged(nameof(Gain)); - RaisePropertyChanged(nameof(Gains)); - } - private bool TryCancelCapture(string reason) - { - if (_camera == null) - { - return false; + public void SendCommandBlind(string command, bool raw = true) { + throw new NotImplementedException(); } - if (!_enableNativeCancel) - { - Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); - return false; + public bool SendCommandBool(string command, bool raw = true) { + throw new NotImplementedException(); } - lock (_captureLock) - { - try - { - SonyDriver driver = SonyDriver.GetInstance(); - if (!TryGetCaptureStatusLocked(driver, out var status, reason)) - { - return false; - } - - if (!CANCELLABLE_STATES.Contains(status)) - { - Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); - return false; - } + public string SendCommandString(string command, bool raw = true) { + throw new NotImplementedException(); + } - Logger.Info($"Issuing cancel ({reason}); capture status is {status}"); - driver.CancelCapture(_camera.Handle); - return true; + public void SetBinning(short x, short y) { + // Ignore + } + + public void UpdateSubSampleArea() { + if (_camera == null) { + EnableSubSample = false; + SubSampleX = 0; + SubSampleY = 0; + SubSampleWidth = 0; + SubSampleHeight = 0; + return; } - catch (Exception ex) - { - Logger.Error($"CancelCapture failed ({reason})", ex); - return false; + + if (EnableSubSample && !CanSubSample) { + Logger.Warning("Sub-sampling requested but not supported for Sony cameras. Falling back to full frame."); + EnableSubSample = false; } - } - } - private bool TryGetCaptureStatusLocked(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}): {ex.Message}"); - status = CAPTURE_FAILED; - return false; + // Sony cameras currently expose the entire frame, so always reset to the sensor dimensions. + SubSampleX = 0; + SubSampleY = 0; + SubSampleWidth = _camera.ImageSize.Width; + SubSampleHeight = _camera.ImageSize.Height; } - } - #endregion -} \ No newline at end of file + #endregion + + + // TODO!!! WE NEED ONE + public bool HasSetupDialog => false; + + public IList SupportedActions => new List(); + } +} From 886f2c2e6a265bb1b43becc4f9c5adfbc752eced Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:16:09 +0100 Subject: [PATCH 13/31] Handle start/reset and soft cancel --- Drivers/CameraDriver.cs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index c34a917..8dffb51 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -569,6 +569,7 @@ public void StartExposure(CaptureSequence sequence) { if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); lock (_captureLock) { + bool issuedCancel = false; _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { Logger.Warning("Cannot start exposure: capture status unavailable."); @@ -577,10 +578,22 @@ public void StartExposure(CaptureSequence sequence) { if (BUSY_STATES.Contains(captureStatus)) { if (_enableNativeCancel) { - TryCancelCapture("start exposure reset"); + try { + Logger.Info($"Cancelling existing capture before starting new one; status {captureStatus}"); + driver.CancelCapture(_camera.Handle); + issuedCancel = true; + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { + Logger.Warning("Cannot start exposure: capture status unavailable after cancel."); + throw new TaskCanceledException("Cannot start exposure: capture status unavailable after cancel."); + } + } catch (Exception ex) { + Logger.Error("CancelCapture failed before start", ex); + } + } + 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."); } - 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)) { @@ -588,6 +601,16 @@ public void StartExposure(CaptureSequence sequence) { throw new TaskCanceledException($"Cannot start exposure: Camera in unexpected capture status ({captureStatus})."); } + // Reset capture state for bodies that require a pre-start cancel, but only when native cancel is enabled. + if (_enableNativeCancel && !issuedCancel) { + try { + driver.CancelCapture(_camera.Handle); + issuedCancel = true; + } catch (Exception ex) { + Logger.Warning($"Pre-start CancelCapture failed; continuing start. {ex.Message}"); + } + } + double exposureTime = sequence.ExposureTime; driver.StartCapture(_camera.Handle, (float)exposureTime); } @@ -624,7 +647,8 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { $"Waiting for image to be ready, current state is {captureStatus}, completion states are {String.Join(", ", completionStates)}"); while (!completionStates.Contains(captureStatus)) { - await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); + var waitToken = _enableNativeCancel ? token : CancellationToken.None; + await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), waitToken); if (!_enableNativeCancel && token.IsCancellationRequested) { _softCancelRequested = true; } From d035092914073db54ad75d59d2225bc89464e8a5 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:19:38 +0100 Subject: [PATCH 14/31] Simplify cancel constants and setting lookup --- Drivers/CameraDriver.cs | 3 +-- Drivers/CameraProvider.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 8dffb51..4242e01 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -40,7 +40,6 @@ public class CameraDriver : BaseINPC, ICamera { private const uint CAPTURE_PROCESSING = 0x8003; 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[] CANCELLABLE_STATES = BUSY_STATES; private readonly bool _enableNativeCancel; private bool _softCancelRequested; @@ -116,7 +115,7 @@ private bool TryCancelCapture(string reason) { return false; } - if (!CANCELLABLE_STATES.Contains(status)) { + if (!BUSY_STATES.Contains(status)) { Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); return false; } diff --git a/Drivers/CameraProvider.cs b/Drivers/CameraProvider.cs index d2a0eff..6c872fc 100644 --- a/Drivers/CameraProvider.cs +++ b/Drivers/CameraProvider.cs @@ -52,7 +52,7 @@ public IList GetEquipment() { var devices = new List(); bool enableNativeCancel = false; try { - var raw = pluginSettings.GetValueString("EnableNativeCancel", bool.FalseString); + var raw = pluginSettings.GetValueString(nameof(EnableNativeCancel), bool.FalseString); enableNativeCancel = bool.TryParse(raw, out var parsed) && parsed; } catch (Exception ex) { Logger.Warning($"Unable to read EnableNativeCancel setting; defaulting to false. {ex.Message}"); From e0e8601759703d70fc37a808a832d61946e909ad Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:22:33 +0100 Subject: [PATCH 15/31] Simplify StartExposure cancel path --- Drivers/CameraDriver.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 4242e01..ee9af82 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -568,7 +568,6 @@ public void StartExposure(CaptureSequence sequence) { if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); lock (_captureLock) { - bool issuedCancel = false; _softCancelRequested = false; if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { Logger.Warning("Cannot start exposure: capture status unavailable."); @@ -577,16 +576,10 @@ public void StartExposure(CaptureSequence sequence) { if (BUSY_STATES.Contains(captureStatus)) { if (_enableNativeCancel) { - try { - Logger.Info($"Cancelling existing capture before starting new one; status {captureStatus}"); - driver.CancelCapture(_camera.Handle); - issuedCancel = true; - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { - Logger.Warning("Cannot start exposure: capture status unavailable after cancel."); - throw new TaskCanceledException("Cannot start exposure: capture status unavailable after cancel."); - } - } catch (Exception ex) { - Logger.Error("CancelCapture failed before start", ex); + TryCancelCapture("start exposure reset"); + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { + 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)) { @@ -601,10 +594,9 @@ public void StartExposure(CaptureSequence sequence) { } // Reset capture state for bodies that require a pre-start cancel, but only when native cancel is enabled. - if (_enableNativeCancel && !issuedCancel) { + if (_enableNativeCancel) { try { driver.CancelCapture(_camera.Handle); - issuedCancel = true; } catch (Exception ex) { Logger.Warning($"Pre-start CancelCapture failed; continuing start. {ex.Message}"); } From be446160705ba8b1e1d84eed0e0fa786d1535fad Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:25:55 +0100 Subject: [PATCH 16/31] move to static var --- Drivers/CameraDriver.cs | 529 +++++++++++++++++++++++++++------------- 1 file changed, 357 insertions(+), 172 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index ee9af82..44fde91 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -22,24 +22,27 @@ using NINA.Profile.Interfaces; using Sony; -namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers { - public class CameraDriver : BaseINPC, ICamera { +namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers +{ + public class CameraDriver : BaseINPC, ICamera + { // Some camera settings we are interested in private const uint PROPID_BATTERY = 53784; private const uint PROPID_ISO = 0xD21E; // Actual ISO currently set private const uint PROPID_ISOS = 0xFFFE; // Registry-backed list of learnt ISOs (may be empty until learnt) // Capture Status - private const uint CAPTURE_CREATED = 0x0000; - private const uint CAPTURE_CAPTURING = 0x0001; - private const uint CAPTURE_FAILED = 0x0002; - private const uint CAPTURE_CANCELLED = 0x0003; - private const uint CAPTURE_COMPLETE = 0x0004; - private const uint CAPTURE_STARTING = 0x8001; - private const uint CAPTURE_READING = 0x8002; + private const uint CAPTURE_CREATED = 0x0000; + private const uint CAPTURE_CAPTURING = 0x0001; + private const uint CAPTURE_FAILED = 0x0002; + private const uint CAPTURE_CANCELLED = 0x0003; + private const uint CAPTURE_COMPLETE = 0x0004; + private const uint CAPTURE_STARTING = 0x8001; + private const uint CAPTURE_READING = 0x8002; private const uint CAPTURE_PROCESSING = 0x8003; 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 bool _enableNativeCancel; private bool _softCancelRequested; @@ -53,7 +56,8 @@ public class CameraDriver : BaseINPC, ICamera { private AsyncObservableCollection _binningModes; private readonly object _captureLock = new object(); - public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) { + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) + { _profileService = profileService; _exposureDataFactory = exposureDataFactory; _device = device; @@ -63,24 +67,32 @@ public CameraDriver(IProfileService profileService, IExposureDataFactory exposur #region Internal Helpers - private PropertyValue GetPropertyValue(uint id) { + private PropertyValue GetPropertyValue(uint id) + { return SonyDriver.GetInstance().GetProperty(_camera.Handle, id); } - private IReadOnlyList GetAvailableIsoOptions() { - if (_camera == null) { + private IReadOnlyList GetAvailableIsoOptions() + { + if (_camera == null) + { return Array.Empty(); } uint[] propertyCandidates = { PROPID_ISOS, PROPID_ISO }; - foreach (var propertyId in propertyCandidates) { - try { + foreach (var propertyId in propertyCandidates) + { + try + { var options = _camera.GetPropertyInfo(propertyId)?.Options()?.Where(o => o.Value <= 0x00FFFFFF).ToList(); - if (options != null && options.Count > 0) { + if (options != null && options.Count > 0) + { return options; } - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}"); } } @@ -89,7 +101,8 @@ private IReadOnlyList GetAvailableIsoOptions() { return Array.Empty(); } - private void NotifyGainPropertiesChanged() { + private void NotifyGainPropertiesChanged() + { RaisePropertyChanged(nameof(CanGetGain)); RaisePropertyChanged(nameof(CanSetGain)); RaisePropertyChanged(nameof(GainMin)); @@ -98,24 +111,31 @@ private void NotifyGainPropertiesChanged() { RaisePropertyChanged(nameof(Gains)); } - private bool TryCancelCapture(string reason) { - if (_camera == null) { + private bool TryCancelCapture(string reason) + { + if (_camera == null) + { return false; } - if (!_enableNativeCancel) { + if (!_enableNativeCancel) + { Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); return false; } - lock (_captureLock) { - try { + lock (_captureLock) + { + try + { SonyDriver driver = SonyDriver.GetInstance(); - if (!TryGetCaptureStatusLocked(driver, out var status, reason)) { + if (!TryGetCaptureStatusLocked(driver, out var status, reason)) + { return false; } - if (!BUSY_STATES.Contains(status)) { + if (!BUSY_STATES.Contains(status)) + { Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); return false; } @@ -123,18 +143,24 @@ private bool TryCancelCapture(string reason) { Logger.Info($"Issuing cancel ({reason}); capture status is {status}"); driver.CancelCapture(_camera.Handle); return true; - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Error($"CancelCapture failed ({reason})", ex); return false; } } } - private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, string reason) { - try { + private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, string reason) + { + try + { status = driver.GetCaptureStatus(_camera.Handle); return true; - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Warning($"Unable to get capture status ({reason}): {ex.Message}"); status = CAPTURE_FAILED; return false; @@ -150,7 +176,8 @@ private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, strin // Although the driver supports camera temperature, it gets it from the ARW's // metadata after a photo is taken, because this code doesn't request processed // ARW, the temp cannot be determined. - public double Temperature { + public double Temperature + { get => double.NaN; /*{ @@ -167,11 +194,16 @@ public double Temperature { public short BinX { get => 1; set => throw new NotImplementedException(); } public short BinY { get => 1; set => throw new NotImplementedException(); } - public string SensorName { - get { - if (_camera != null) { + public string SensorName + { + get + { + if (_camera != null) + { return _camera.SensorName; - } else { + } + else + { return string.Empty; } } @@ -183,42 +215,61 @@ public string SensorName { public short BayerOffsetY { get => 1; set => throw new NotImplementedException(); } - public int CameraXSize { - get { - if (_camera != null) { + public int CameraXSize + { + get + { + if (_camera != null) + { return _camera.ImageSize.Width; } - else { + else + { return 0; } } } - public int CameraYSize { - get { - if (_camera != null) { + public int CameraYSize + { + get + { + if (_camera != null) + { return _camera.ImageSize.Height; - } else { + } + else + { return 0; } } } - public double ExposureMin { - get { - if (_camera != null) { + public double ExposureMin + { + get + { + if (_camera != null) + { return _camera.ExposureMin; - } else { + } + else + { return double.NaN; } } } - public double ExposureMax { - get { - if (_camera != null) { + public double ExposureMax + { + get + { + if (_camera != null) + { return _camera.ExposureMax; - } else { + } + else + { return double.NaN; } } @@ -228,21 +279,31 @@ public double ExposureMax { public short MaxBinY { get => 1; set => throw new NotImplementedException(); } - public double PixelSizeX { - get { - if (_camera != null) { + public double PixelSizeX + { + get + { + if (_camera != null) + { return _camera.PixelWidth; - } else { + } + else + { return double.NaN; } } } - public double PixelSizeY { - get { - if (_camera != null) { + public double PixelSizeY + { + get + { + if (_camera != null) + { return _camera.PixelHeight; - } else { + } + else + { return double.NaN; } } @@ -252,19 +313,26 @@ public double PixelSizeY { public CameraStates CameraState => CameraStates.NoState; // TODO - public bool CanShowLiveView { - get { - if (_camera != null) { + public bool CanShowLiveView + { + get + { + if (_camera != null) + { return _camera.SupportsPreview(); - } else { + } + else + { return false; } } } - public bool LiveViewEnabled { + public bool LiveViewEnabled + { get => _liveViewEnabled; - set { + set + { _liveViewEnabled = value; RaisePropertyChanged(); } @@ -272,21 +340,31 @@ public bool LiveViewEnabled { public bool HasBattery => true; - public int BatteryLevel { - get { - if (_camera != null) { + public int BatteryLevel + { + get + { + if (_camera != null) + { return (int)GetPropertyValue(PROPID_BATTERY).Value; - } else { + } + else + { return 0; } } } - public int BitDepth { - get { - if (_camera != null) { + public int BitDepth + { + get + { + if (_camera != null) + { return _camera.BitsPerPixel; - } else { + } + else + { return 0; } } @@ -296,11 +374,15 @@ public int BitDepth { public bool CanSetGain => CanGetGain; - public int GainMax { - get { + public int GainMax + { + get + { var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) { - if (_camera != null) { + if (!isoOptions.Any()) + { + if (_camera != null) + { Logger.Error("Problem getting gain max: camera did not report ISO options."); } return -1; @@ -310,11 +392,15 @@ public int GainMax { } } - public int GainMin { - get { + public int GainMin + { + get + { var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) { - if (_camera != null) { + if (!isoOptions.Any()) + { + if (_camera != null) + { Logger.Error("Problem getting gain min: camera did not report ISO options."); } return -1; @@ -324,42 +410,61 @@ public int GainMin { } } - public int Gain { - get { - if (_camera != null) { - try { + public int Gain + { + get + { + if (_camera != null) + { + try + { PropertyValue value = GetPropertyValue(PROPID_ISO); return (int)(value.Value == 0xffffff ? 0 : value.Value); - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Error("Problem getting gain", ex); return -1; } - } else { + } + else + { return -1; } } - set { - if (_camera != null) { - try { + set + { + if (_camera != null) + { + try + { SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); RaisePropertyChanged(nameof(Gain)); - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Error($"Problem setting gain to {value}", ex); } } } } - public IList Gains { - get { + public IList Gains + { + get + { List gains = new List(); - foreach (var iso in GetAvailableIsoOptions()) { - if (iso.Value == 0xffffff) { + foreach (var iso in GetAvailableIsoOptions()) + { + if (iso.Value == 0xffffff) + { gains.Add(0); // AUTO - } else { + } + else + { gains.Add((int)iso.Value); } } @@ -370,29 +475,38 @@ public IList Gains { public string Id => "Sony"; - public string Name { + public string Name + { get => _device.Model; set => throw new NotImplementedException(); } - public string DisplayName { + public string DisplayName + { get => _device.Model; set => throw new NotImplementedException(); } public string Category { get => "Sony"; } - public bool Connected { - get { + public bool Connected + { + get + { return _camera != null; } } - public string Description { - get { - if (_camera != null) { + public string Description + { + get + { + if (_camera != null) + { return _camera.GetDescription(); - } else { + } + else + { return _device.GetDescription(); } } @@ -402,10 +516,12 @@ public string Description { public string DriverVersion => string.Empty; - public double TemperatureSetPoint { + public double TemperatureSetPoint + { get => double.NaN; - set { + set + { } } @@ -421,9 +537,11 @@ public double TemperatureSetPoint { public int SubSampleHeight { get; set; } - public bool CoolerOn { + public bool CoolerOn + { get => false; - set { + set + { } } @@ -431,9 +549,11 @@ public bool CoolerOn { public bool HasDewHeater => false; - public bool DewHeaterOn { + public bool DewHeaterOn + { get => false; - set { + set + { } } @@ -459,30 +579,38 @@ public bool DewHeaterOn { public IList ReadoutModes => new List { "Default" }; - public short ReadoutMode { + public short ReadoutMode + { get => 0; set { } } - public short ReadoutModeForSnapImages { + public short ReadoutModeForSnapImages + { get => _readoutModeForSnapImages; - set { + set + { _readoutModeForSnapImages = value; RaisePropertyChanged(); } } - public short ReadoutModeForNormalImages { + public short ReadoutModeForNormalImages + { get => _readoutModeForNormalImages; - set { + set + { _readoutModeForNormalImages = value; RaisePropertyChanged(); } } - public AsyncObservableCollection BinningModes { - get { - if (_binningModes == null) { + public AsyncObservableCollection BinningModes + { + get + { + if (_binningModes == null) + { _binningModes = new AsyncObservableCollection(); _binningModes.Add(new BinningMode(1, 1)); } @@ -495,19 +623,26 @@ public AsyncObservableCollection BinningModes { #region Supported Methods - public void StartLiveView(CaptureSequence sequence) { + public void StartLiveView(CaptureSequence sequence) + { LiveViewEnabled = true; } - public void StopLiveView() { + public void StopLiveView() + { LiveViewEnabled = false; } - public Task Connect(CancellationToken token) { - return Task.Run(() => { - try { + public Task Connect(CancellationToken token) + { + return Task.Run(() => + { + try + { _camera = SonyDriver.GetInstance().OpenCamera(_device.Id); - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Error(ex); _camera = null; } @@ -517,11 +652,16 @@ public Task Connect(CancellationToken token) { }); } - public void Disconnect() { - if (_camera != null) { - try { + public void Disconnect() + { + if (_camera != null) + { + try + { SonyDriver.GetInstance().CloseCamera(_camera.Handle); - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Error(ex); } @@ -530,9 +670,12 @@ public void Disconnect() { } } - public Task DownloadLiveView(CancellationToken token) { - return Task.Run(() => { - using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) { + public Task DownloadLiveView(CancellationToken token) + { + return Task.Run(() => + { + using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) + { memStream.Position = 0; JpegBitmapDecoder decoder = @@ -560,44 +703,58 @@ public Task DownloadLiveView(CancellationToken token) { }); } - public void SetupDialog() { + public void SetupDialog() + { throw new NotImplementedException(); } - public void StartExposure(CaptureSequence sequence) { - if (_camera != null) { + public void StartExposure(CaptureSequence sequence) + { + if (_camera != null) + { SonyDriver driver = SonyDriver.GetInstance(); - lock (_captureLock) { + lock (_captureLock) + { _softCancelRequested = false; - if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { + if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) + { Logger.Warning("Cannot start exposure: capture status unavailable."); throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); } - if (BUSY_STATES.Contains(captureStatus)) { - if (_enableNativeCancel) { + if (BUSY_STATES.Contains(captureStatus)) + { + if (_enableNativeCancel) + { TryCancelCapture("start exposure reset"); - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) + { 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)) { + 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)) { + 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})."); } // Reset capture state for bodies that require a pre-start cancel, but only when native cancel is enabled. - if (_enableNativeCancel) { - try { + if (_enableNativeCancel) + { + try + { driver.CancelCapture(_camera.Handle); - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Warning($"Pre-start CancelCapture failed; continuing start. {ex.Message}"); } } @@ -608,66 +765,86 @@ public void StartExposure(CaptureSequence sequence) { } } - public void StopExposure() { + public void StopExposure() + { AbortExposure(); } - public void AbortExposure() { - if (_enableNativeCancel) { + public void AbortExposure() + { + if (_enableNativeCancel) + { TryCancelCapture("abort request"); - } else { + } + else + { _softCancelRequested = true; Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); } } - public async Task WaitUntilExposureIsReady(CancellationToken token) { - using (token.Register(AbortExposure)) { - uint[] completionStates = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; + public async Task WaitUntilExposureIsReady(CancellationToken token) + { + using (token.Register(AbortExposure)) + { SonyDriver driver = SonyDriver.GetInstance(); - try { + try + { uint captureStatus; - lock (_captureLock) { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) { + lock (_captureLock) + { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) + { 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(", ", completionStates)}"); + $"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)) + { var waitToken = _enableNativeCancel ? token : CancellationToken.None; await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), waitToken); - if (!_enableNativeCancel && token.IsCancellationRequested) { + if (!_enableNativeCancel && token.IsCancellationRequested) + { _softCancelRequested = true; } - lock (_captureLock) { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { + lock (_captureLock) + { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) + { throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); } } } Logger.Info($"Wait for image ready complete, completion state is {captureStatus}"); - if (_softCancelRequested || token.IsCancellationRequested) { + if (_softCancelRequested || token.IsCancellationRequested) + { _softCancelRequested = false; throw new TaskCanceledException("Exposure cancelled by user (soft cancel)."); } - } catch (TaskCanceledException) { + } + catch (TaskCanceledException) + { Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); throw; - } catch (Exception ex) { + } + catch (Exception ex) + { Logger.Error("WaitUntilExposureIsReady got exception", ex); throw new SonyException("Problem while waiting for image to be ready (see log)"); } } } - public Task DownloadExposure(CancellationToken token) { - return Task.Run(() => { + public Task DownloadExposure(CancellationToken token) + { + return Task.Run(() => + { byte[] rawImageData = SonyDriver.GetInstance().GetLastImage(); var metaData = new ImageMetaData(); @@ -684,29 +861,36 @@ public Task DownloadExposure(CancellationToken token) { #region Unsupported Methods - public string Action(string actionName, string actionParameters) { + public string Action(string actionName, string actionParameters) + { throw new NotImplementedException(); } - public void SendCommandBlind(string command, bool raw = true) { + public void SendCommandBlind(string command, bool raw = true) + { throw new NotImplementedException(); } - public bool SendCommandBool(string command, bool raw = true) { + public bool SendCommandBool(string command, bool raw = true) + { throw new NotImplementedException(); } - public string SendCommandString(string command, bool raw = true) { + public string SendCommandString(string command, bool raw = true) + { throw new NotImplementedException(); } - public void SetBinning(short x, short y) { + public void SetBinning(short x, short y) + { // Ignore } - - public void UpdateSubSampleArea() { - if (_camera == null) { + + public void UpdateSubSampleArea() + { + if (_camera == null) + { EnableSubSample = false; SubSampleX = 0; SubSampleY = 0; @@ -715,7 +899,8 @@ public void UpdateSubSampleArea() { return; } - if (EnableSubSample && !CanSubSample) { + if (EnableSubSample && !CanSubSample) + { Logger.Warning("Sub-sampling requested but not supported for Sony cameras. Falling back to full frame."); EnableSubSample = false; } From fca3e40b88882bbaf86a2e390860c1f223b0b8a8 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:28:16 +0100 Subject: [PATCH 17/31] move native cancel capture hint --- Options.xaml | 2 +- Properties/AssemblyInfo.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Options.xaml b/Options.xaml index 369b42b..ed1e99e 100644 --- a/Options.xaml +++ b/Options.xaml @@ -12,7 +12,7 @@ + ToolTip="When enabled, the driver will call the camera's native CancelCapture. Native CancelCapture stability varies by camera model and Windows driver stack. It can cause VCRUNTIME crashes. Disable if aborts cause Nina crashes." /> diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index c4d01b7..b4e7953 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -33,8 +33,6 @@ [assembly: AssemblyMetadata("LicenseURL", "https://www.mozilla.org/en-US/MPL/2.0/")] // The repository where your pluggin is hosted [assembly: AssemblyMetadata("Repository", "https://github.com/dougforpres/NINASonyCameraPlugin")] -// Note on native cancel: stability depends on camera model and Windows driver stack; leave disabled if aborts crash. -[assembly: AssemblyMetadata("NativeCancelHint", "Native CancelCapture stability varies by camera model and Windows driver stack; disable if aborts crash.")] // The following attributes are optional for the official manifest meta data From d6db836438779202ea43908f099089b49fcb1065 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:30:07 +0100 Subject: [PATCH 18/31] Fix EnableNativeCancel nameof reference --- Drivers/CameraProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Drivers/CameraProvider.cs b/Drivers/CameraProvider.cs index 6c872fc..48a714e 100644 --- a/Drivers/CameraProvider.cs +++ b/Drivers/CameraProvider.cs @@ -12,6 +12,7 @@ using System.ComponentModel.Composition; using System.Reflection; using System.Runtime.InteropServices; +using NINA.RetroKiwi.Plugin.SonyCamera; using NINA.Image.Interfaces; using NINA.WPF.Base.Mediator; using Sony; @@ -52,7 +53,7 @@ public IList GetEquipment() { var devices = new List(); bool enableNativeCancel = false; try { - var raw = pluginSettings.GetValueString(nameof(EnableNativeCancel), bool.FalseString); + var raw = pluginSettings.GetValueString(nameof(SonyCamera.EnableNativeCancel), bool.FalseString); enableNativeCancel = bool.TryParse(raw, out var parsed) && parsed; } catch (Exception ex) { Logger.Warning($"Unable to read EnableNativeCancel setting; defaulting to false. {ex.Message}"); From d17804606890e756dbc90e79e496b0470df65568 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:33:20 +0100 Subject: [PATCH 19/31] Inject plugin GUID instead of reflecting --- Drivers/CameraProvider.cs | 6 +----- SonyCameraPlugin.cs | 3 ++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Drivers/CameraProvider.cs b/Drivers/CameraProvider.cs index 48a714e..df536df 100644 --- a/Drivers/CameraProvider.cs +++ b/Drivers/CameraProvider.cs @@ -10,8 +10,6 @@ using System.Threading; using System.Threading.Tasks; using System.ComponentModel.Composition; -using System.Reflection; -using System.Runtime.InteropServices; using NINA.RetroKiwi.Plugin.SonyCamera; using NINA.Image.Interfaces; using NINA.WPF.Base.Mediator; @@ -29,14 +27,12 @@ public class CameraProvider : IEquipmentProvider { private IExposureDataFactory exposureDataFactory; SonyDriver driver; private readonly PluginOptionsAccessor pluginSettings; - private static readonly Guid PluginGuid = - Guid.Parse(((GuidAttribute)Attribute.GetCustomAttribute(typeof(CameraProvider).Assembly, typeof(GuidAttribute))).Value); [ImportingConstructor] public CameraProvider(IProfileService profileService, IExposureDataFactory exposureDataFactory) { this.profileService = profileService; this.exposureDataFactory = exposureDataFactory; - this.pluginSettings = new PluginOptionsAccessor(profileService, PluginGuid); + this.pluginSettings = new PluginOptionsAccessor(profileService, SonyCamera.PluginGuid); if (!DllLoader.IsX86()) { try { diff --git a/SonyCameraPlugin.cs b/SonyCameraPlugin.cs index b64a645..74b6fbf 100644 --- a/SonyCameraPlugin.cs +++ b/SonyCameraPlugin.cs @@ -28,6 +28,7 @@ namespace NINA.RetroKiwi.Plugin.SonyCamera { /// [Export(typeof(IPluginManifest))] public class SonyCamera : PluginBase, INotifyPropertyChanged { + public static readonly Guid PluginGuid = Guid.Parse("f3fd7bb5-2b69-40cc-846f-4f4a2ff62518"); private readonly IPluginOptionsAccessor pluginSettings; private readonly IProfileService profileService; private readonly IImageSaveMediator imageSaveMediator; @@ -44,7 +45,7 @@ public SonyCamera(IProfileService profileService, IOptionsVM options, IImageSave } // This helper class can be used to store plugin settings that are dependent on the current profile - this.pluginSettings = new PluginOptionsAccessor(profileService, Guid.Parse(this.Identifier)); + this.pluginSettings = new PluginOptionsAccessor(profileService, PluginGuid); this.profileService = profileService; // React on a changed profile profileService.ProfileChanged += ProfileService_ProfileChanged; From 77cd0c5271f536e7463c2037cc1359bd2520c4f0 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:40:33 +0100 Subject: [PATCH 20/31] Warn when native cancel fails and clarify option text --- Drivers/CameraDriver.cs | 5 ++++- Options.xaml | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 44fde91..ccd020e 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -774,7 +774,10 @@ public void AbortExposure() { if (_enableNativeCancel) { - TryCancelCapture("abort request"); + if (!TryCancelCapture("abort request")) + { + Notification.ShowWarning("Abort requested, but the camera did not accept native cancel; exposure will continue until it finishes."); + } } else { diff --git a/Options.xaml b/Options.xaml index ed1e99e..daafc7c 100644 --- a/Options.xaml +++ b/Options.xaml @@ -8,11 +8,10 @@ - + + IsChecked="{Binding EnableNativeCancel, Mode=TwoWay}" /> From 178819155d33d05623640733d4156285151ecfbe Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:42:39 +0100 Subject: [PATCH 21/31] Warn on soft cancel when native cancel disabled --- Drivers/CameraDriver.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index ccd020e..5bae3ff 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -782,6 +782,7 @@ public void AbortExposure() else { _softCancelRequested = true; + Notification.ShowWarning("Abort requested; native cancel is disabled. Exposure will continue until it finishes."); Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); } } From 7cb640af4869bb5940a582d12f9c43383710791b Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:46:30 +0100 Subject: [PATCH 22/31] Rename TryCancelCapture and remove redundant guards --- Drivers/CameraDriver.cs | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 5bae3ff..b735e29 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -111,7 +111,7 @@ private void NotifyGainPropertiesChanged() RaisePropertyChanged(nameof(Gains)); } - private bool TryCancelCapture(string reason) + private bool TryCancelCaptureIfEnabled(string reason) { if (_camera == null) { @@ -724,14 +724,11 @@ public void StartExposure(CaptureSequence sequence) if (BUSY_STATES.Contains(captureStatus)) { - if (_enableNativeCancel) + bool attemptedCancel = TryCancelCaptureIfEnabled("start exposure reset"); + if (attemptedCancel && !TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { - TryCancelCapture("start exposure reset"); - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) - { - Logger.Warning("Cannot start exposure: capture status unavailable after cancel."); - throw new TaskCanceledException("Cannot start exposure: capture status unavailable after cancel."); - } + 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)) { @@ -772,19 +769,14 @@ public void StopExposure() public void AbortExposure() { - if (_enableNativeCancel) - { - if (!TryCancelCapture("abort request")) - { - Notification.ShowWarning("Abort requested, but the camera did not accept native cancel; exposure will continue until it finishes."); - } - } - else + if (TryCancelCaptureIfEnabled("abort request")) { - _softCancelRequested = true; - Notification.ShowWarning("Abort requested; native cancel is disabled. Exposure will continue until it finishes."); - Logger.Info("AbortExposure requested; native cancel disabled; letting capture finish."); + 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) From 96fbb687784b681443ec109210a3027668d58526 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:48:29 +0100 Subject: [PATCH 23/31] Recheck status after cancel attempt regardless of success --- Drivers/CameraDriver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index b735e29..562b112 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -724,8 +724,8 @@ public void StartExposure(CaptureSequence sequence) if (BUSY_STATES.Contains(captureStatus)) { - bool attemptedCancel = TryCancelCaptureIfEnabled("start exposure reset"); - if (attemptedCancel && !TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) + TryCancelCaptureIfEnabled("start exposure reset"); + if (_enableNativeCancel && !TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { Logger.Warning("Cannot start exposure: capture status unavailable after cancel."); throw new TaskCanceledException("Cannot start exposure: capture status unavailable after cancel."); From 340b7d4e52e6390f930d14152dad0db90c62d617 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:50:49 +0100 Subject: [PATCH 24/31] Reuse cancel helper for pre-start reset --- Drivers/CameraDriver.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index 562b112..a7ae88c 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -744,17 +744,7 @@ public void StartExposure(CaptureSequence sequence) } // Reset capture state for bodies that require a pre-start cancel, but only when native cancel is enabled. - if (_enableNativeCancel) - { - try - { - driver.CancelCapture(_camera.Handle); - } - catch (Exception ex) - { - Logger.Warning($"Pre-start CancelCapture failed; continuing start. {ex.Message}"); - } - } + TryCancelCaptureIfEnabled("start exposure pre-start reset"); double exposureTime = sequence.ExposureTime; driver.StartCapture(_camera.Handle, (float)exposureTime); From 5aefd72d93ac2a0b338261a2c9f515f1958d893a Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 12:58:13 +0100 Subject: [PATCH 25/31] undo formatter changes --- Drivers/CameraDriver.cs | 511 +++++++++++++--------------------------- 1 file changed, 167 insertions(+), 344 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index a7ae88c..e731979 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -22,27 +22,26 @@ using NINA.Profile.Interfaces; using Sony; -namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers -{ - public class CameraDriver : BaseINPC, ICamera - { +namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers { + public class CameraDriver : BaseINPC, ICamera { // Some camera settings we are interested in private const uint PROPID_BATTERY = 53784; private const uint PROPID_ISO = 0xD21E; // Actual ISO currently set private const uint PROPID_ISOS = 0xFFFE; // Registry-backed list of learnt ISOs (may be empty until learnt) // Capture Status - private const uint CAPTURE_CREATED = 0x0000; - private const uint CAPTURE_CAPTURING = 0x0001; - private const uint CAPTURE_FAILED = 0x0002; - private const uint CAPTURE_CANCELLED = 0x0003; - private const uint CAPTURE_COMPLETE = 0x0004; - private const uint CAPTURE_STARTING = 0x8001; - private const uint CAPTURE_READING = 0x8002; + private const uint CAPTURE_CREATED = 0x0000; + private const uint CAPTURE_CAPTURING = 0x0001; + private const uint CAPTURE_FAILED = 0x0002; + private const uint CAPTURE_CANCELLED = 0x0003; + private const uint CAPTURE_COMPLETE = 0x0004; + private const uint CAPTURE_STARTING = 0x8001; + private const uint CAPTURE_READING = 0x8002; private const uint CAPTURE_PROCESSING = 0x8003; 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 bool _enableNativeCancel; private bool _softCancelRequested; @@ -56,8 +55,7 @@ public class CameraDriver : BaseINPC, ICamera private AsyncObservableCollection _binningModes; private readonly object _captureLock = new object(); - public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) - { + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) { _profileService = profileService; _exposureDataFactory = exposureDataFactory; _device = device; @@ -67,32 +65,24 @@ public CameraDriver(IProfileService profileService, IExposureDataFactory exposur #region Internal Helpers - private PropertyValue GetPropertyValue(uint id) - { + private PropertyValue GetPropertyValue(uint id) { return SonyDriver.GetInstance().GetProperty(_camera.Handle, id); } - private IReadOnlyList GetAvailableIsoOptions() - { - if (_camera == null) - { + private IReadOnlyList GetAvailableIsoOptions() { + if (_camera == null) { return Array.Empty(); } uint[] propertyCandidates = { PROPID_ISOS, PROPID_ISO }; - foreach (var propertyId in propertyCandidates) - { - try - { + foreach (var propertyId in propertyCandidates) { + try { var options = _camera.GetPropertyInfo(propertyId)?.Options()?.Where(o => o.Value <= 0x00FFFFFF).ToList(); - if (options != null && options.Count > 0) - { + if (options != null && options.Count > 0) { return options; } - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Warning($"Unable to enumerate ISO options for property 0x{propertyId:X}: {ex.Message}"); } } @@ -101,8 +91,7 @@ private IReadOnlyList GetAvailableIsoOptions() return Array.Empty(); } - private void NotifyGainPropertiesChanged() - { + private void NotifyGainPropertiesChanged() { RaisePropertyChanged(nameof(CanGetGain)); RaisePropertyChanged(nameof(CanSetGain)); RaisePropertyChanged(nameof(GainMin)); @@ -111,31 +100,24 @@ private void NotifyGainPropertiesChanged() RaisePropertyChanged(nameof(Gains)); } - private bool TryCancelCaptureIfEnabled(string reason) - { - if (_camera == null) - { + private bool TryCancelCaptureIfEnabled(string reason) { + if (_camera == null) { return false; } - if (!_enableNativeCancel) - { + if (!_enableNativeCancel) { Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); return false; } - lock (_captureLock) - { - try - { + lock (_captureLock) { + try { SonyDriver driver = SonyDriver.GetInstance(); - if (!TryGetCaptureStatusLocked(driver, out var status, reason)) - { + if (!TryGetCaptureStatusLocked(driver, out var status, reason)) { return false; } - if (!BUSY_STATES.Contains(status)) - { + if (!BUSY_STATES.Contains(status)) { Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); return false; } @@ -143,24 +125,18 @@ private bool TryCancelCaptureIfEnabled(string reason) Logger.Info($"Issuing cancel ({reason}); capture status is {status}"); driver.CancelCapture(_camera.Handle); return true; - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Error($"CancelCapture failed ({reason})", ex); return false; } } } - private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, string reason) - { - try - { + private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, string reason) { + try { status = driver.GetCaptureStatus(_camera.Handle); return true; - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Warning($"Unable to get capture status ({reason}): {ex.Message}"); status = CAPTURE_FAILED; return false; @@ -176,8 +152,7 @@ private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, strin // Although the driver supports camera temperature, it gets it from the ARW's // metadata after a photo is taken, because this code doesn't request processed // ARW, the temp cannot be determined. - public double Temperature - { + public double Temperature { get => double.NaN; /*{ @@ -194,16 +169,11 @@ public double Temperature public short BinX { get => 1; set => throw new NotImplementedException(); } public short BinY { get => 1; set => throw new NotImplementedException(); } - public string SensorName - { - get - { - if (_camera != null) - { + public string SensorName { + get { + if (_camera != null) { return _camera.SensorName; - } - else - { + } else { return string.Empty; } } @@ -215,61 +185,42 @@ public string SensorName public short BayerOffsetY { get => 1; set => throw new NotImplementedException(); } - public int CameraXSize - { - get - { - if (_camera != null) - { + public int CameraXSize { + get { + if (_camera != null) { return _camera.ImageSize.Width; } - else - { + else { return 0; } } } - public int CameraYSize - { - get - { - if (_camera != null) - { + public int CameraYSize { + get { + if (_camera != null) { return _camera.ImageSize.Height; - } - else - { + } else { return 0; } } } - public double ExposureMin - { - get - { - if (_camera != null) - { + public double ExposureMin { + get { + if (_camera != null) { return _camera.ExposureMin; - } - else - { + } else { return double.NaN; } } } - public double ExposureMax - { - get - { - if (_camera != null) - { + public double ExposureMax { + get { + if (_camera != null) { return _camera.ExposureMax; - } - else - { + } else { return double.NaN; } } @@ -279,31 +230,21 @@ public double ExposureMax public short MaxBinY { get => 1; set => throw new NotImplementedException(); } - public double PixelSizeX - { - get - { - if (_camera != null) - { + public double PixelSizeX { + get { + if (_camera != null) { return _camera.PixelWidth; - } - else - { + } else { return double.NaN; } } } - public double PixelSizeY - { - get - { - if (_camera != null) - { + public double PixelSizeY { + get { + if (_camera != null) { return _camera.PixelHeight; - } - else - { + } else { return double.NaN; } } @@ -313,26 +254,19 @@ public double PixelSizeY public CameraStates CameraState => CameraStates.NoState; // TODO - public bool CanShowLiveView - { - get - { - if (_camera != null) - { + public bool CanShowLiveView { + get { + if (_camera != null) { return _camera.SupportsPreview(); - } - else - { + } else { return false; } } } - public bool LiveViewEnabled - { + public bool LiveViewEnabled { get => _liveViewEnabled; - set - { + set { _liveViewEnabled = value; RaisePropertyChanged(); } @@ -340,31 +274,21 @@ public bool LiveViewEnabled public bool HasBattery => true; - public int BatteryLevel - { - get - { - if (_camera != null) - { + public int BatteryLevel { + get { + if (_camera != null) { return (int)GetPropertyValue(PROPID_BATTERY).Value; - } - else - { + } else { return 0; } } } - public int BitDepth - { - get - { - if (_camera != null) - { + public int BitDepth { + get { + if (_camera != null) { return _camera.BitsPerPixel; - } - else - { + } else { return 0; } } @@ -374,15 +298,11 @@ public int BitDepth public bool CanSetGain => CanGetGain; - public int GainMax - { - get - { + public int GainMax { + get { var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) - { - if (_camera != null) - { + if (!isoOptions.Any()) { + if (_camera != null) { Logger.Error("Problem getting gain max: camera did not report ISO options."); } return -1; @@ -392,15 +312,11 @@ public int GainMax } } - public int GainMin - { - get - { + public int GainMin { + get { var isoOptions = GetAvailableIsoOptions(); - if (!isoOptions.Any()) - { - if (_camera != null) - { + if (!isoOptions.Any()) { + if (_camera != null) { Logger.Error("Problem getting gain min: camera did not report ISO options."); } return -1; @@ -410,61 +326,42 @@ public int GainMin } } - public int Gain - { - get - { - if (_camera != null) - { - try - { + public int Gain { + get { + if (_camera != null) { + try { PropertyValue value = GetPropertyValue(PROPID_ISO); return (int)(value.Value == 0xffffff ? 0 : value.Value); - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Error("Problem getting gain", ex); return -1; } - } - else - { + } else { return -1; } } - set - { - if (_camera != null) - { - try - { + set { + if (_camera != null) { + try { SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); RaisePropertyChanged(nameof(Gain)); - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Error($"Problem setting gain to {value}", ex); } } } } - public IList Gains - { - get - { + public IList Gains { + get { List gains = new List(); - foreach (var iso in GetAvailableIsoOptions()) - { - if (iso.Value == 0xffffff) - { + foreach (var iso in GetAvailableIsoOptions()) { + if (iso.Value == 0xffffff) { gains.Add(0); // AUTO - } - else - { + } else { gains.Add((int)iso.Value); } } @@ -475,38 +372,29 @@ public IList Gains public string Id => "Sony"; - public string Name - { + public string Name { get => _device.Model; set => throw new NotImplementedException(); } - public string DisplayName - { + public string DisplayName { get => _device.Model; set => throw new NotImplementedException(); } public string Category { get => "Sony"; } - public bool Connected - { - get - { + public bool Connected { + get { return _camera != null; } } - public string Description - { - get - { - if (_camera != null) - { + public string Description { + get { + if (_camera != null) { return _camera.GetDescription(); - } - else - { + } else { return _device.GetDescription(); } } @@ -516,12 +404,10 @@ public string Description public string DriverVersion => string.Empty; - public double TemperatureSetPoint - { + public double TemperatureSetPoint { get => double.NaN; - set - { + set { } } @@ -537,11 +423,9 @@ public double TemperatureSetPoint public int SubSampleHeight { get; set; } - public bool CoolerOn - { + public bool CoolerOn { get => false; - set - { + set { } } @@ -549,11 +433,9 @@ public bool CoolerOn public bool HasDewHeater => false; - public bool DewHeaterOn - { + public bool DewHeaterOn { get => false; - set - { + set { } } @@ -579,38 +461,30 @@ public bool DewHeaterOn public IList ReadoutModes => new List { "Default" }; - public short ReadoutMode - { + public short ReadoutMode { get => 0; set { } } - public short ReadoutModeForSnapImages - { + public short ReadoutModeForSnapImages { get => _readoutModeForSnapImages; - set - { + set { _readoutModeForSnapImages = value; RaisePropertyChanged(); } } - public short ReadoutModeForNormalImages - { + public short ReadoutModeForNormalImages { get => _readoutModeForNormalImages; - set - { + set { _readoutModeForNormalImages = value; RaisePropertyChanged(); } } - public AsyncObservableCollection BinningModes - { - get - { - if (_binningModes == null) - { + public AsyncObservableCollection BinningModes { + get { + if (_binningModes == null) { _binningModes = new AsyncObservableCollection(); _binningModes.Add(new BinningMode(1, 1)); } @@ -623,26 +497,19 @@ public AsyncObservableCollection BinningModes #region Supported Methods - public void StartLiveView(CaptureSequence sequence) - { + public void StartLiveView(CaptureSequence sequence) { LiveViewEnabled = true; } - public void StopLiveView() - { + public void StopLiveView() { LiveViewEnabled = false; } - public Task Connect(CancellationToken token) - { - return Task.Run(() => - { - try - { + public Task Connect(CancellationToken token) { + return Task.Run(() => { + try { _camera = SonyDriver.GetInstance().OpenCamera(_device.Id); - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Error(ex); _camera = null; } @@ -652,16 +519,11 @@ public Task Connect(CancellationToken token) }); } - public void Disconnect() - { - if (_camera != null) - { - try - { + public void Disconnect() { + if (_camera != null) { + try { SonyDriver.GetInstance().CloseCamera(_camera.Handle); - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Error(ex); } @@ -670,12 +532,9 @@ public void Disconnect() } } - public Task DownloadLiveView(CancellationToken token) - { - return Task.Run(() => - { - using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) - { + public Task DownloadLiveView(CancellationToken token) { + return Task.Run(() => { + using (var memStream = new MemoryStream(SonyDriver.GetInstance().GetLiveView(_camera.Handle))) { memStream.Position = 0; JpegBitmapDecoder decoder = @@ -703,42 +562,33 @@ public Task DownloadLiveView(CancellationToken token) }); } - public void SetupDialog() - { + public void SetupDialog() { throw new NotImplementedException(); } - public void StartExposure(CaptureSequence sequence) - { - if (_camera != null) - { + public void StartExposure(CaptureSequence sequence) { + if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); - lock (_captureLock) - { + lock (_captureLock) { _softCancelRequested = false; - if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) - { + if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { Logger.Warning("Cannot start exposure: capture status unavailable."); throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); } - if (BUSY_STATES.Contains(captureStatus)) - { + if (BUSY_STATES.Contains(captureStatus)) { TryCancelCaptureIfEnabled("start exposure reset"); - if (_enableNativeCancel && !TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) - { + if (_enableNativeCancel && !TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { 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)) - { + 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)) - { + 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})."); } @@ -752,15 +602,12 @@ public void StartExposure(CaptureSequence sequence) } } - public void StopExposure() - { + public void StopExposure() { AbortExposure(); } - public void AbortExposure() - { - if (TryCancelCaptureIfEnabled("abort request")) - { + public void AbortExposure() { + if (TryCancelCaptureIfEnabled("abort request")) { return; } @@ -769,68 +616,52 @@ public void AbortExposure() Logger.Info("AbortExposure requested; native cancel unavailable; letting capture finish."); } - public async Task WaitUntilExposureIsReady(CancellationToken token) - { - using (token.Register(AbortExposure)) - { + public async Task WaitUntilExposureIsReady(CancellationToken token) { + using (token.Register(AbortExposure)) { SonyDriver driver = SonyDriver.GetInstance(); - try - { + try { uint captureStatus; - lock (_captureLock) - { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) - { + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) { 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 (!COMPLETION_STATES.Contains(captureStatus)) - { + while (!COMPLETION_STATES.Contains(captureStatus)) { var waitToken = _enableNativeCancel ? token : CancellationToken.None; await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), waitToken); - if (!_enableNativeCancel && token.IsCancellationRequested) - { + if (!_enableNativeCancel && token.IsCancellationRequested) { _softCancelRequested = true; } - lock (_captureLock) - { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) - { + lock (_captureLock) { + if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); } } } Logger.Info($"Wait for image ready complete, completion state is {captureStatus}"); - if (_softCancelRequested || token.IsCancellationRequested) - { + if (_softCancelRequested || token.IsCancellationRequested) { _softCancelRequested = false; throw new TaskCanceledException("Exposure cancelled by user (soft cancel)."); } - } - catch (TaskCanceledException) - { + } catch (TaskCanceledException) { Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); throw; - } - catch (Exception ex) - { + } catch (Exception ex) { Logger.Error("WaitUntilExposureIsReady got exception", ex); throw new SonyException("Problem while waiting for image to be ready (see log)"); } } } - public Task DownloadExposure(CancellationToken token) - { - return Task.Run(() => - { + public Task DownloadExposure(CancellationToken token) { + return Task.Run(() => { byte[] rawImageData = SonyDriver.GetInstance().GetLastImage(); var metaData = new ImageMetaData(); @@ -847,36 +678,29 @@ public Task DownloadExposure(CancellationToken token) #region Unsupported Methods - public string Action(string actionName, string actionParameters) - { + public string Action(string actionName, string actionParameters) { throw new NotImplementedException(); } - public void SendCommandBlind(string command, bool raw = true) - { + public void SendCommandBlind(string command, bool raw = true) { throw new NotImplementedException(); } - public bool SendCommandBool(string command, bool raw = true) - { + public bool SendCommandBool(string command, bool raw = true) { throw new NotImplementedException(); } - public string SendCommandString(string command, bool raw = true) - { + public string SendCommandString(string command, bool raw = true) { throw new NotImplementedException(); } - public void SetBinning(short x, short y) - { + public void SetBinning(short x, short y) { // Ignore } - - public void UpdateSubSampleArea() - { - if (_camera == null) - { + + public void UpdateSubSampleArea() { + if (_camera == null) { EnableSubSample = false; SubSampleX = 0; SubSampleY = 0; @@ -885,8 +709,7 @@ public void UpdateSubSampleArea() return; } - if (EnableSubSample && !CanSubSample) - { + if (EnableSubSample && !CanSubSample) { Logger.Warning("Sub-sampling requested but not supported for Sony cameras. Falling back to full frame."); EnableSubSample = false; } From 6df3c4134d63857f6aeeb45024acdfd66eb6282b Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 13:09:33 +0100 Subject: [PATCH 26/31] Handle unknown capture status responses --- Drivers/CameraDriver.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index e731979..aa30900 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -38,6 +38,7 @@ 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 }; @@ -113,7 +114,12 @@ private bool TryCancelCaptureIfEnabled(string reason) { lock (_captureLock) { try { SonyDriver driver = SonyDriver.GetInstance(); - if (!TryGetCaptureStatusLocked(driver, out var status, reason)) { + if (!TryGetCaptureStatus(driver, out var status, reason)) { + return false; + } + + if (status == CAPTURE_STATUS_UNKNOWN) { + Logger.Warning($"Skip cancel ({reason}); capture status is unknown (camera did not respond)."); return false; } @@ -132,13 +138,13 @@ private bool TryCancelCaptureIfEnabled(string reason) { } } - private bool TryGetCaptureStatusLocked(SonyDriver driver, out uint status, string reason) { + 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}): {ex.Message}"); - status = CAPTURE_FAILED; + Logger.Warning($"Unable to get capture status ({reason}); camera may not have responded: {ex.Message}"); + status = CAPTURE_STATUS_UNKNOWN; return false; } } @@ -571,14 +577,15 @@ public void StartExposure(CaptureSequence sequence) { SonyDriver driver = SonyDriver.GetInstance(); lock (_captureLock) { _softCancelRequested = false; - if (!TryGetCaptureStatusLocked(driver, out var captureStatus, "start exposure preflight")) { + 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."); } if (BUSY_STATES.Contains(captureStatus)) { TryCancelCaptureIfEnabled("start exposure reset"); - if (_enableNativeCancel && !TryGetCaptureStatusLocked(driver, out captureStatus, "start exposure post-cancel")) { + if (_enableNativeCancel && (!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."); } @@ -624,7 +631,7 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { try { uint captureStatus; lock (_captureLock) { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait begin")) { + if (!TryGetCaptureStatus(driver, out captureStatus, "wait begin") || captureStatus == CAPTURE_STATUS_UNKNOWN) { throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); } } @@ -639,7 +646,7 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { } lock (_captureLock) { - if (!TryGetCaptureStatusLocked(driver, out captureStatus, "wait poll")) { + if (!TryGetCaptureStatus(driver, out captureStatus, "wait poll") || captureStatus == CAPTURE_STATUS_UNKNOWN) { throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); } } From f5e93dffc56c2c03c9811770f325d0465a98808d Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 13:31:16 +0100 Subject: [PATCH 27/31] refactor cancelation logic --- Drivers/CameraDriver.cs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index aa30900..cad0e98 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -583,16 +583,15 @@ public void StartExposure(CaptureSequence sequence) { throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); } + TryCancelCaptureIfEnabled("start exposure reset"); + if (_enableNativeCancel && (!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)) { - TryCancelCaptureIfEnabled("start exposure reset"); - if (_enableNativeCancel && (!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."); - } + 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)) { @@ -600,9 +599,6 @@ public void StartExposure(CaptureSequence sequence) { throw new TaskCanceledException($"Cannot start exposure: Camera in unexpected capture status ({captureStatus})."); } - // Reset capture state for bodies that require a pre-start cancel, but only when native cancel is enabled. - TryCancelCaptureIfEnabled("start exposure pre-start reset"); - double exposureTime = sequence.ExposureTime; driver.StartCapture(_camera.Handle, (float)exposureTime); } @@ -639,11 +635,7 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { $"Waiting for image to be ready, current state is {captureStatus}, completion states are {String.Join(", ", COMPLETION_STATES)}"); while (!COMPLETION_STATES.Contains(captureStatus)) { - var waitToken = _enableNativeCancel ? token : CancellationToken.None; - await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), waitToken); - if (!_enableNativeCancel && token.IsCancellationRequested) { - _softCancelRequested = true; - } + await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); lock (_captureLock) { if (!TryGetCaptureStatus(driver, out captureStatus, "wait poll") || captureStatus == CAPTURE_STATUS_UNKNOWN) { From d0b12accfb771c6db8b57162214910e4fa3cd41b Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 13:33:16 +0100 Subject: [PATCH 28/31] use live value of cancelcapture option --- Drivers/CameraDriver.cs | 30 +++++++++++++++++++++++++----- Drivers/CameraProvider.cs | 10 +--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index cad0e98..de7dff5 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -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 { @@ -43,7 +44,8 @@ public class CameraDriver : BaseINPC, ICamera { 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 bool _enableNativeCancel; + private readonly PluginOptionsAccessor _pluginSettings; + private readonly bool _enableNativeCancelDefault; private bool _softCancelRequested; private SonyCameraInfo _camera = null; @@ -56,11 +58,12 @@ public class CameraDriver : BaseINPC, ICamera { private AsyncObservableCollection _binningModes; private readonly object _captureLock = new object(); - public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, bool enableNativeCancel) { + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, PluginOptionsAccessor pluginSettings, bool enableNativeCancel) { _profileService = profileService; _exposureDataFactory = exposureDataFactory; _device = device; - _enableNativeCancel = enableNativeCancel; + _pluginSettings = pluginSettings; + _enableNativeCancelDefault = enableNativeCancel; _softCancelRequested = false; } @@ -101,12 +104,29 @@ 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($"Unable to read EnableNativeCancel setting; defaulting to {_enableNativeCancelDefault}. {ex.Message}"); + } + } + + return _enableNativeCancelDefault; + } + } + private bool TryCancelCaptureIfEnabled(string reason) { if (_camera == null) { return false; } - if (!_enableNativeCancel) { + if (!NativeCancelEnabled) { Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); return false; } @@ -584,7 +604,7 @@ public void StartExposure(CaptureSequence sequence) { } TryCancelCaptureIfEnabled("start exposure reset"); - if (_enableNativeCancel && (!TryGetCaptureStatus(driver, out captureStatus, "start exposure post-cancel") || captureStatus == CAPTURE_STATUS_UNKNOWN)) { + 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."); } diff --git a/Drivers/CameraProvider.cs b/Drivers/CameraProvider.cs index df536df..94adaa9 100644 --- a/Drivers/CameraProvider.cs +++ b/Drivers/CameraProvider.cs @@ -47,21 +47,13 @@ public CameraProvider(IProfileService profileService, IExposureDataFactory expos public IList GetEquipment() { var devices = new List(); - bool enableNativeCancel = false; - try { - var raw = pluginSettings.GetValueString(nameof(SonyCamera.EnableNativeCancel), bool.FalseString); - enableNativeCancel = bool.TryParse(raw, out var parsed) && parsed; - } catch (Exception ex) { - Logger.Warning($"Unable to read EnableNativeCancel setting; defaulting to false. {ex.Message}"); - } - if (this.driver != null) { try { int count = 0; foreach (var sonyDevice in driver.Cameras()) { count++; - devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice, enableNativeCancel)); + devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice, pluginSettings, false)); } Logger.Info($"Found {count} Sony Cameras"); From 19a0b2a6615ae7b81c93b54ad2e84d1ab2b0c991 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 13:50:03 +0100 Subject: [PATCH 29/31] Prevent deadlock during rapid start and cancel --- Drivers/CameraDriver.cs | 84 +++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index de7dff5..df93490 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -87,7 +87,7 @@ private IReadOnlyList 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}."); } } @@ -113,7 +113,7 @@ private bool NativeCancelEnabled { return enabled; } } catch (Exception ex) { - Logger.Warning($"Unable to read EnableNativeCancel setting; defaulting to {_enableNativeCancelDefault}. {ex.Message}"); + Logger.Warning($"EnableNativeCancel read failed; defaulting to {_enableNativeCancelDefault}. {ex.Message}."); } } @@ -121,40 +121,49 @@ private bool NativeCancelEnabled { } } - private bool TryCancelCaptureIfEnabled(string reason) { + private bool TryCancelCaptureIfEnabled(string reason, int lockTimeoutMs = 1000) { if (_camera == null) { return false; } if (!NativeCancelEnabled) { - Logger.Info($"Native cancel disabled; skipping cancel ({reason})"); + Logger.Info($"Native cancel disabled; skipping cancel ({reason})."); return false; } - lock (_captureLock) { - try { - SonyDriver driver = SonyDriver.GetInstance(); - if (!TryGetCaptureStatus(driver, out var status, 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; - if (status == CAPTURE_STATUS_UNKNOWN) { - Logger.Warning($"Skip cancel ({reason}); capture status is unknown (camera did not respond)."); - return false; - } + SonyDriver driver = SonyDriver.GetInstance(); + if (!TryGetCaptureStatus(driver, out var status, reason)) { + return false; + } - if (!BUSY_STATES.Contains(status)) { - Logger.Debug($"Skip cancel ({reason}); capture status is {status}"); - return false; - } + if (status == CAPTURE_STATUS_UNKNOWN) { + Logger.Warning($"Skipping cancel ({reason}); capture status is unknown (camera did not respond)."); + 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); + 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); + } } } @@ -163,7 +172,7 @@ private bool TryGetCaptureStatus(SonyDriver driver, out uint status, string reas 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}"); + Logger.Warning($"Unable to get capture status ({reason}); camera may not have responded: {ex.Message}."); status = CAPTURE_STATUS_UNKNOWN; return false; } @@ -360,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 { @@ -374,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); } } } @@ -595,6 +604,8 @@ public void SetupDialog() { public void StartExposure(CaptureSequence sequence) { if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); + + double exposureTime; lock (_captureLock) { _softCancelRequested = false; if (!TryGetCaptureStatus(driver, out var captureStatus, "start exposure preflight") || @@ -610,18 +621,20 @@ public void StartExposure(CaptureSequence sequence) { } 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."); + 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})."); + Logger.Warning($"Cannot start exposure: camera in unexpected capture status ({captureStatus})."); + throw new TaskCanceledException($"Cannot start exposure: camera in unexpected capture status ({captureStatus})."); } - double exposureTime = sequence.ExposureTime; - driver.StartCapture(_camera.Handle, (float)exposureTime); + exposureTime = sequence.ExposureTime; } + + // Start capture outside the capture lock to avoid blocking abort/cancel paths if the call hangs. + driver.StartCapture(_camera.Handle, (float)exposureTime); } } @@ -651,8 +664,7 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { 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)}"); + Logger.Info($"Waiting for image to be ready; current state is {captureStatus}; completion states are {String.Join(", ", COMPLETION_STATES)}."); while (!COMPLETION_STATES.Contains(captureStatus)) { await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); @@ -664,7 +676,7 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { } } - 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)."); @@ -673,7 +685,7 @@ public async Task WaitUntilExposureIsReady(CancellationToken token) { 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)"); } } From 8d4a65f197f5f77daba6c0fa1e6c5c142d8e0aba Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 13:51:03 +0100 Subject: [PATCH 30/31] Show abort warning only once per exposure --- Drivers/CameraDriver.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index df93490..e8aab0e 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -47,6 +47,7 @@ public class CameraDriver : BaseINPC, ICamera { private readonly PluginOptionsAccessor _pluginSettings; private readonly bool _enableNativeCancelDefault; private bool _softCancelRequested; + private bool _abortWarningShown; private SonyCameraInfo _camera = null; private SonyDevice _device = null; @@ -65,6 +66,7 @@ public CameraDriver(IProfileService profileService, IExposureDataFactory exposur _pluginSettings = pluginSettings; _enableNativeCancelDefault = enableNativeCancel; _softCancelRequested = false; + _abortWarningShown = false; } #region Internal Helpers @@ -608,6 +610,7 @@ public void StartExposure(CaptureSequence sequence) { double exposureTime; lock (_captureLock) { _softCancelRequested = false; + _abortWarningShown = false; if (!TryGetCaptureStatus(driver, out var captureStatus, "start exposure preflight") || captureStatus == CAPTURE_STATUS_UNKNOWN) { Logger.Warning("Cannot start exposure: capture status unavailable."); @@ -648,7 +651,10 @@ public void AbortExposure() { } _softCancelRequested = true; - Notification.ShowWarning("Abort requested; native cancel is disabled or the camera did not accept it. Exposure will continue until it finishes."); + if (!_abortWarningShown) { + Notification.ShowWarning("Abort requested; native cancel is disabled or the camera did not accept it. Exposure will continue until it finishes."); + _abortWarningShown = true; + } Logger.Info("AbortExposure requested; native cancel unavailable; letting capture finish."); } From b5f128832e9706aebe3d407ddd4c6bb2f9e08536 Mon Sep 17 00:00:00 2001 From: Lucas Lepski Date: Mon, 8 Dec 2025 13:52:04 +0100 Subject: [PATCH 31/31] Revert "Show abort warning only once per exposure" This reverts commit 8d4a65f197f5f77daba6c0fa1e6c5c142d8e0aba. --- Drivers/CameraDriver.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Drivers/CameraDriver.cs b/Drivers/CameraDriver.cs index e8aab0e..df93490 100644 --- a/Drivers/CameraDriver.cs +++ b/Drivers/CameraDriver.cs @@ -47,7 +47,6 @@ public class CameraDriver : BaseINPC, ICamera { private readonly PluginOptionsAccessor _pluginSettings; private readonly bool _enableNativeCancelDefault; private bool _softCancelRequested; - private bool _abortWarningShown; private SonyCameraInfo _camera = null; private SonyDevice _device = null; @@ -66,7 +65,6 @@ public CameraDriver(IProfileService profileService, IExposureDataFactory exposur _pluginSettings = pluginSettings; _enableNativeCancelDefault = enableNativeCancel; _softCancelRequested = false; - _abortWarningShown = false; } #region Internal Helpers @@ -610,7 +608,6 @@ public void StartExposure(CaptureSequence sequence) { double exposureTime; lock (_captureLock) { _softCancelRequested = false; - _abortWarningShown = false; if (!TryGetCaptureStatus(driver, out var captureStatus, "start exposure preflight") || captureStatus == CAPTURE_STATUS_UNKNOWN) { Logger.Warning("Cannot start exposure: capture status unavailable."); @@ -651,10 +648,7 @@ public void AbortExposure() { } _softCancelRequested = true; - if (!_abortWarningShown) { - Notification.ShowWarning("Abort requested; native cancel is disabled or the camera did not accept it. Exposure will continue until it finishes."); - _abortWarningShown = 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."); }