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 f2d6826..df93490 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; @@ -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 { @@ -38,6 +39,14 @@ public class CameraDriver : BaseINPC, ICamera { private const uint CAPTURE_STARTING = 0x8001; private const uint CAPTURE_READING = 0x8002; private const uint CAPTURE_PROCESSING = 0x8003; + private const uint CAPTURE_STATUS_UNKNOWN = 0xFFFFFFFF; + private static readonly uint[] IDLE_STATES = { CAPTURE_CREATED, CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; + private static readonly uint[] BUSY_STATES = { CAPTURE_CAPTURING, CAPTURE_PROCESSING, CAPTURE_STARTING, CAPTURE_READING }; + private static readonly uint[] COMPLETION_STATES = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; + + private readonly PluginOptionsAccessor _pluginSettings; + private readonly bool _enableNativeCancelDefault; + private bool _softCancelRequested; private SonyCameraInfo _camera = null; private SonyDevice _device = null; @@ -47,11 +56,15 @@ 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) { + public CameraDriver(IProfileService profileService, IExposureDataFactory exposureDataFactory, SonyDevice device, PluginOptionsAccessor pluginSettings, bool enableNativeCancel) { _profileService = profileService; _exposureDataFactory = exposureDataFactory; _device = device; + _pluginSettings = pluginSettings; + _enableNativeCancelDefault = enableNativeCancel; + _softCancelRequested = false; } #region Internal Helpers @@ -74,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}."); } } @@ -91,6 +104,80 @@ private void NotifyGainPropertiesChanged() { RaisePropertyChanged(nameof(Gains)); } + private bool NativeCancelEnabled { + get { + if (_pluginSettings != null) { + try { + var raw = _pluginSettings.GetValueString(nameof(SonyCamera.EnableNativeCancel), _enableNativeCancelDefault.ToString()); + if (bool.TryParse(raw, out var enabled)) { + return enabled; + } + } catch (Exception ex) { + Logger.Warning($"EnableNativeCancel read failed; defaulting to {_enableNativeCancelDefault}. {ex.Message}."); + } + } + + return _enableNativeCancelDefault; + } + } + + private bool TryCancelCaptureIfEnabled(string reason, int lockTimeoutMs = 1000) { + if (_camera == null) { + return false; + } + + if (!NativeCancelEnabled) { + Logger.Info($"Native cancel disabled; skipping cancel ({reason})."); + return false; + } + + bool lockTaken = false; + try { + if (!Monitor.TryEnter(_captureLock, lockTimeoutMs)) { + Logger.Warning($"Skipping cancel ({reason}); capture lock unavailable after {lockTimeoutMs} ms."); + return false; + } + lockTaken = true; + + SonyDriver driver = SonyDriver.GetInstance(); + if (!TryGetCaptureStatus(driver, out var status, reason)) { + return false; + } + + if (status == CAPTURE_STATUS_UNKNOWN) { + Logger.Warning($"Skipping cancel ({reason}); capture status is unknown (camera did not respond)."); + return false; + } + + if (!BUSY_STATES.Contains(status)) { + Logger.Debug($"Skipping cancel ({reason}); capture status is {status}."); + return false; + } + + Logger.Info($"Issuing cancel ({reason}); capture status is {status}."); + driver.CancelCapture(_camera.Handle); + return true; + } catch (Exception ex) { + Logger.Error($"CancelCapture failed ({reason}).", ex); + return false; + } finally { + if (lockTaken) { + Monitor.Exit(_captureLock); + } + } + } + + private bool TryGetCaptureStatus(SonyDriver driver, out uint status, string reason) { + try { + status = driver.GetCaptureStatus(_camera.Handle); + return true; + } catch (Exception ex) { + Logger.Warning($"Unable to get capture status ({reason}); camera may not have responded: {ex.Message}."); + status = CAPTURE_STATUS_UNKNOWN; + return false; + } + } + #endregion #region Supported Properties @@ -282,7 +369,7 @@ public int Gain { return (int)(value.Value == 0xffffff ? 0 : value.Value); } catch (Exception ex) { - Logger.Error("Problem getting gain", ex); + Logger.Error("Problem getting gain.", ex); return -1; } } else { @@ -296,7 +383,7 @@ public int Gain { SonyDriver.GetInstance().SetProperty(_camera.Handle, PROPID_ISO, (uint)value); RaisePropertyChanged(nameof(Gain)); } catch (Exception ex) { - Logger.Error($"Problem setting gain to {value}", ex); + Logger.Error($"Problem setting gain to {value}.", ex); } } } @@ -517,18 +604,37 @@ public void SetupDialog() { public void StartExposure(CaptureSequence sequence) { if (_camera != null) { SonyDriver driver = SonyDriver.GetInstance(); - uint captureStatus = driver.GetCaptureStatus(_camera.Handle); - if (captureStatus == CAPTURE_CAPTURING || captureStatus == CAPTURE_PROCESSING || captureStatus == CAPTURE_STARTING || - captureStatus == CAPTURE_READING || captureStatus == CAPTURE_PROCESSING) { - Notification.ShowWarning("Another exposure still in progress. Cancelling it to start another."); - } + double exposureTime; + lock (_captureLock) { + _softCancelRequested = false; + if (!TryGetCaptureStatus(driver, out var captureStatus, "start exposure preflight") || + captureStatus == CAPTURE_STATUS_UNKNOWN) { + Logger.Warning("Cannot start exposure: capture status unavailable."); + throw new TaskCanceledException("Cannot start exposure: capture status unavailable."); + } - // Tell the camera to cancel capture, we do this every time regardless - this will reset the status to be non-complete - driver.CancelCapture(_camera.Handle); + TryCancelCaptureIfEnabled("start exposure reset"); + if (NativeCancelEnabled && (!TryGetCaptureStatus(driver, out captureStatus, "start exposure post-cancel") || captureStatus == CAPTURE_STATUS_UNKNOWN)) { + Logger.Warning("Cannot start exposure: capture status unavailable after cancel."); + throw new TaskCanceledException("Cannot start exposure: capture status unavailable after cancel."); + } + + if (BUSY_STATES.Contains(captureStatus)) { + Notification.ShowWarning("Camera is still busy with a previous exposure; skipping new start."); + throw new TaskCanceledException("Cannot start exposure: camera is still busy with a previous exposure."); + } + + if (!IDLE_STATES.Contains(captureStatus)) { + Logger.Warning($"Cannot start exposure: camera in unexpected capture status ({captureStatus})."); + throw new TaskCanceledException($"Cannot start exposure: camera in unexpected capture status ({captureStatus})."); + } + + exposureTime = sequence.ExposureTime; + } - double exposureTime = sequence.ExposureTime; - driver.StartCapture(_camera.Handle, (float)exposureTime); //); + // Start capture outside the capture lock to avoid blocking abort/cancel paths if the call hangs. + driver.StartCapture(_camera.Handle, (float)exposureTime); } } @@ -537,30 +643,49 @@ public void StopExposure() { } public void AbortExposure() { - if (_camera != null) { - SonyDriver.GetInstance().CancelCapture(_camera.Handle); + if (TryCancelCaptureIfEnabled("abort request")) { + return; } + + _softCancelRequested = true; + Notification.ShowWarning("Abort requested; native cancel is disabled or the camera did not accept it. Exposure will continue until it finishes."); + Logger.Info("AbortExposure requested; native cancel unavailable; letting capture finish."); } public async Task WaitUntilExposureIsReady(CancellationToken token) { using (token.Register(AbortExposure)) { - uint[] completionStates = { CAPTURE_CANCELLED, CAPTURE_COMPLETE, CAPTURE_FAILED }; SonyDriver driver = SonyDriver.GetInstance(); try { - uint captureStatus = driver.GetCaptureStatus(_camera.Handle); - Logger.Info( - $"Waiting for image to be ready, current state is {captureStatus}, completion states are {String.Join(", ", completionStates)}"); + uint captureStatus; + lock (_captureLock) { + if (!TryGetCaptureStatus(driver, out captureStatus, "wait begin") || captureStatus == CAPTURE_STATUS_UNKNOWN) { + throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); + } + } + Logger.Info($"Waiting for image to be ready; current state is {captureStatus}; completion states are {String.Join(", ", COMPLETION_STATES)}."); - while (!completionStates.Contains(captureStatus)) { + while (!COMPLETION_STATES.Contains(captureStatus)) { await CoreUtil.Wait(TimeSpan.FromMilliseconds(100), token); - captureStatus = driver.GetCaptureStatus(_camera.Handle); + + lock (_captureLock) { + if (!TryGetCaptureStatus(driver, out captureStatus, "wait poll") || captureStatus == CAPTURE_STATUS_UNKNOWN) { + throw new SonyException("Problem while waiting for image to be ready (status unavailable)"); + } + } } - Logger.Info($"Wait for image ready complete, completion state is {captureStatus}"); + Logger.Info($"Wait for image ready complete; completion state is {captureStatus}."); + if (_softCancelRequested || token.IsCancellationRequested) { + _softCancelRequested = false; + throw new TaskCanceledException("Exposure cancelled by user (soft cancel)."); + } + } catch (TaskCanceledException) { + Logger.Info("WaitUntilExposureIsReady cancelled by token; exiting without native cancel."); + throw; } catch (Exception ex) { - Logger.Error("WaitUntilExposureIsReady got exception", ex); + Logger.Error("WaitUntilExposureIsReady got exception.", ex); throw new SonyException("Problem while waiting for image to be ready (see log)"); } } diff --git a/Drivers/CameraProvider.cs b/Drivers/CameraProvider.cs index 7c78287..94adaa9 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,7 @@ using System.Threading; using System.Threading.Tasks; using System.ComponentModel.Composition; +using NINA.RetroKiwi.Plugin.SonyCamera; using NINA.Image.Interfaces; using NINA.WPF.Base.Mediator; using Sony; @@ -24,11 +26,13 @@ public class CameraProvider : IEquipmentProvider { private IProfileService profileService; private IExposureDataFactory exposureDataFactory; SonyDriver driver; + private readonly PluginOptionsAccessor pluginSettings; [ImportingConstructor] public CameraProvider(IProfileService profileService, IExposureDataFactory exposureDataFactory) { this.profileService = profileService; this.exposureDataFactory = exposureDataFactory; + this.pluginSettings = new PluginOptionsAccessor(profileService, SonyCamera.PluginGuid); if (!DllLoader.IsX86()) { try { @@ -43,14 +47,13 @@ public CameraProvider(IProfileService profileService, IExposureDataFactory expos public IList GetEquipment() { var devices = new List(); - if (this.driver != null) { try { int count = 0; foreach (var sonyDevice in driver.Cameras()) { count++; - devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice)); + devices.Add(new CameraDriver(profileService, exposureDataFactory, sonyDevice, pluginSettings, false)); } Logger.Info($"Found {count} Sony Cameras"); diff --git a/Options.xaml b/Options.xaml index 893755b..daafc7c 100644 --- a/Options.xaml +++ b/Options.xaml @@ -6,15 +6,12 @@ - - - - - - - - - - + + + + + - \ No newline at end of file + diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index ad54320..b4e7953 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")] 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 diff --git a/SonyCameraPlugin.cs b/SonyCameraPlugin.cs index 6b7cfc0..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; @@ -72,6 +73,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 +134,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));