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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 277 additions & 9 deletions Drivers/CameraDriver.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Drivers/CaptureController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ private static void ThrowIfCancelled(CaptureRequest request) {
}
}

private static bool IsBusy(uint status) {
internal static bool IsBusy(uint status) {
return status == Capturing || status == Starting || status == Reading || status == Processing;
}
}
Expand Down
4 changes: 3 additions & 1 deletion Drivers/FocuserDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ public bool Connected {
if (_connected) {
UInt32 hCamera = GetCameraHandle();

if (hCamera == 0) {
// While the camera is being reopened after losing USB there is briefly no
// handle; stay connected so focusing works again once it is back.
if (hCamera == 0 && !SonyDriver.GetInstance().IsAwaitingReopen) {
Logger.Debug("Sony camera was disconnected without closing focuser first, cleaning up");
IsConnected = false;
Moving = false;
Expand Down
4 changes: 4 additions & 0 deletions Drivers/ISonyCameraBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ namespace NINA.RetroKiwi.Plugin.SonyCamera.Drivers {
public interface ISonyCameraBackend {
IEnumerable<SonyDevice> Cameras();
SonyCameraInfo OpenCamera(string id);
// Close the handle if still held and open the camera again (e.g. after USB was unplugged).
SonyCameraInfo ReopenCamera(uint handle, string id);
void CloseCamera(uint handle);
bool IsOpen(uint handle);
// False once the backend can no longer open anything (a native call hung).
bool CanReopen { get; }
PropertyValue GetProperty(uint handle, uint propertyId);
void SetProperty(uint handle, uint propertyId, uint value);
byte[] GetLiveView(uint handle);
Expand Down
2 changes: 2 additions & 0 deletions Drivers/NativeSonyCameraBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ internal sealed class NativeSonyCameraBackend : ISonyCameraBackend {

public IEnumerable<SonyDevice> Cameras() => driver.Cameras();
public SonyCameraInfo OpenCamera(string id) => driver.OpenCamera(id);
public SonyCameraInfo ReopenCamera(uint handle, string id) => driver.ReopenCamera(handle, id);
public void CloseCamera(uint handle) => driver.CloseCamera(handle);
public bool IsOpen(uint handle) => driver.IsOpen(handle);
public bool CanReopen => driver.CanReopen;
public PropertyValue GetProperty(uint handle, uint propertyId) => driver.GetProperty(handle, propertyId);
public void SetProperty(uint handle, uint propertyId, uint value) => driver.SetProperty(handle, propertyId, value);
public byte[] GetLiveView(uint handle) => driver.GetLiveView(handle);
Expand Down
42 changes: 40 additions & 2 deletions Drivers/SimulatedSonyCameraBackend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public sealed class SimulatedSonyCameraBackend : ISonyCameraBackend {
public bool FailCapture { get; set; }
public bool IgnoreNativeCancel { get; set; }
public int StatusReadFailures { get; set; }
// Simulates the USB cable being pulled: the handle still looks open, but
// every camera call fails and the camera cannot be opened until plugged back in.
public bool Unplugged { get; set; }
public int Reopens { get; private set; }
// After a reopen the camera reports ISO 0 for this many reads; setting the ISO in that
// window lands on the wrong value, as the real camera's relative ISO steps do.
public int IsoUnreadReadsAfterReopen { get; set; }
private int isoUnreadReads;
public int NativeCancelCalls { get; private set; }
public int CaptureStarts { get; private set; }

Expand Down Expand Up @@ -58,6 +66,9 @@ public SonyCameraInfo OpenCamera(string id) {
if (id != DeviceId) {
throw new InvalidOperationException("Unknown simulated camera.");
}
if (Unplugged) {
throw new InvalidOperationException("Simulated camera is unplugged.");
}
if (connected) {
throw new InvalidOperationException("Simulated camera is already connected.");
}
Expand Down Expand Up @@ -87,9 +98,28 @@ public bool IsOpen(uint cameraHandle) {
lock (cameraLock) return connected && cameraHandle == handle;
}

public bool CanReopen => true;

public SonyCameraInfo ReopenCamera(uint cameraHandle, string id) {
lock (cameraLock) {
Reopens++;
if (connected && cameraHandle == handle) {
connected = false;
captureActive = false;
lastImage = null;
}
SonyCameraInfo camera = OpenCamera(id);
isoUnreadReads = IsoUnreadReadsAfterReopen;
return camera;
}
}

public void CloseCamera(uint cameraHandle) {
lock (cameraLock) {
RequireConnection(cameraHandle);
// Like the native driver, closing works even while the camera is unplugged.
if (!connected || cameraHandle != handle) {
throw new InvalidOperationException("Simulated camera is disconnected or the handle is invalid.");
}
connected = false;
captureActive = false;
lastImage = null;
Expand All @@ -99,6 +129,10 @@ public void CloseCamera(uint cameraHandle) {
public PropertyValue GetProperty(uint cameraHandle, uint propertyId) {
lock (cameraLock) {
RequireConnection(cameraHandle);
if (propertyId == IsoProperty && isoUnreadReads > 0) {
isoUnreadReads--;
return new PropertyValue { Id = propertyId, Value = 0 };
}
return propertyId switch {
IsoProperty => new PropertyValue { Id = propertyId, Value = iso },
BatteryProperty => new PropertyValue { Id = propertyId, Value = 100 },
Expand All @@ -114,7 +148,8 @@ public void SetProperty(uint cameraHandle, uint propertyId, uint value) {
value != 800 && value != 1600 && value != 3200)) {
throw new InvalidOperationException("Unsupported simulated ISO value.");
}
iso = value;
// Stepping from an unread ISO lands one step off
iso = isoUnreadReads > 0 ? (value == 100 ? 200u : 100u) : value;
}
}

Expand Down Expand Up @@ -213,6 +248,9 @@ private void RequireConnection(uint cameraHandle) {
if (!connected || cameraHandle != handle) {
throw new InvalidOperationException("Simulated camera is disconnected or the handle is invalid.");
}
if (Unplugged) {
throw new InvalidOperationException("Simulated camera is unplugged.");
}
}
}
}
81 changes: 69 additions & 12 deletions SonyCameraPluginNative/Sony.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,25 @@ namespace Sony {
// Adapted from @totoantibes's PR #13: camera and focuser share one native
// entry point. A timeout permanently blocks further native calls, including
// close/open/enumeration, because the old P/Invoke cannot be interrupted.
// Repeated native errors (e.g. USB unplugged) only drop the handle: the
// driver still answers, so the camera can be reopened without a restart.
public class SonyDriver {
private static readonly Lazy<SonyDriver> _instance = new Lazy<SonyDriver>(() => new SonyDriver());
private static readonly NativeCallExecutor sharedCalls = new NativeCallExecutor();
private readonly NativeCallExecutor calls;
private readonly SonyMTPCameraDll _sonydll;
private volatile SonyCameraInfo _camera;
// Handle dropped after repeated native failures but not yet closed in the
// native driver; it must be closed once so a reopen does not reuse it.
private uint lostHandle;
private string attachedLensPath;
private int timeoutNotified;
private int consecutiveFailures;
private readonly HashSet<uint> knownGoodProperties = new HashSet<uint>();
private const int LostCameraFailureThreshold = 3;
// The native close waits up to 20 s for an in-progress camera operation.
private const int CloseTimeoutMs = 30000;
private const int OpenTimeoutMs = 60000;

public static SonyDriver GetInstance() => _instance.Value;

Expand Down Expand Up @@ -61,10 +70,12 @@ private T InvokeCamera<T>(uint handle, string operation, Func<T> action, int tim
// Count only returned native errors. An unsupported property,
// verification mismatch or lock wait is not a lost device.
if (++consecutiveFailures >= LostCameraFailureThreshold) {
// The driver answered, so this is not a hang: stop using the
// handle, but leave it to be closed and reopened.
lostHandle = handle;
_camera = null;
_sonydll.DiscardLastImage();
calls.Stop();
Logger.Error($"Sony camera failed {consecutiveFailures} consecutive native operations while trying to {operation}. Treating it as disconnected; restart NINA and power-cycle the camera before reconnecting.", ex);
Logger.Error($"Sony camera failed {consecutiveFailures} consecutive native operations while trying to {operation}. Treating it as disconnected until it is reopened.", ex);
}
throw;
}
Expand All @@ -84,7 +95,62 @@ public bool IsOpen(uint handle) {
return !calls.IsUnavailable && camera != null && camera.Handle == handle;
}

// False only once a native call has hung; then nothing can be reopened until NINA restarts.
public bool CanReopen => !calls.IsUnavailable;

public SonyCameraInfo OpenCamera(string path) => Invoke("open camera", () => {
if (lostHandle != 0) ReleaseHandle(lostHandle);
return OpenHandle(path);
}, timeoutMs: OpenTimeoutMs);

// Close the given handle (if still held, open or lost) and open the camera
// again, restoring the attached lens so the focuser keeps working.
public SonyCameraInfo ReopenCamera(uint handle, string path) => Invoke("reopen camera", () => {
ReleaseHandle(handle);
if (lostHandle != 0) ReleaseHandle(lostHandle);
SonyCameraInfo camera = OpenHandle(path);
if (attachedLensPath != null) {
try {
_sonydll.SetAttachedLens(camera.Handle, attachedLensPath);
} catch (Exception ex) {
Logger.Warning($"Reopened the Sony camera but could not re-attach lens '{attachedLensPath}': {ex.Message}");
}
}
return camera;
}, timeoutMs: CloseTimeoutMs + OpenTimeoutMs);

public void CloseCamera(uint handle) {
Invoke("close camera", () => {
// A deliberate close ends any pending reopen, even if the handle was already lost.
attachedLensPath = null;
if (!ReleaseHandle(handle)) {
throw new SonyException("Cannot close camera: the camera handle is no longer open.");
}
return true;
}, timeoutMs: CloseTimeoutMs, lockTimeoutMs: 5000);
}

// The camera was lost (not closed) with a lens attached and can still be reopened,
// so the focuser should stay connected and pick up the new handle afterwards.
public bool IsAwaitingReopen => CanReopen && _camera == null && attachedLensPath != null;

// Runs inside Invoke. Closes a handle this driver still holds exactly once;
// returns false if it was never ours or has already been closed.
private bool ReleaseHandle(uint handle) {
if (_camera != null && _camera.Handle == handle) {
_camera = null;
} else if (lostHandle != 0 && lostHandle == handle) {
lostHandle = 0;
} else {
return false;
}
_sonydll.DiscardLastImage();
_sonydll.CloseDevice(handle);
return true;
}

// Runs inside Invoke.
private SonyCameraInfo OpenHandle(string path) {
if (_camera != null) throw new SonyException("A Sony camera is already open. Disconnect it before opening another camera; restart NINA if the earlier disconnect timed out.");
uint handle = _sonydll.OpenDevice(path);
if (handle == 0 || handle == SonyMTPCameraDll.INVALID_HANDLE_VALUE) throw new SonyException("Unable to open the Sony camera.");
Expand All @@ -109,15 +175,6 @@ public SonyCameraInfo OpenCamera(string path) => Invoke("open camera", () => {
_sonydll.CloseDevice(handle);
throw;
}
}, timeoutMs: 60000);

public void CloseCamera(uint handle) {
InvokeCamera(handle, "close camera", () => {
_camera = null;
_sonydll.DiscardLastImage();
_sonydll.CloseDevice(handle);
return true;
}, timeoutMs: 5000, lockTimeoutMs: 5000);
}

public PropertyValue GetProperty(uint handle, uint propertyId) =>
Expand Down Expand Up @@ -163,7 +220,7 @@ public bool TryGetFocusLimit(uint handle, int timeoutMs, out uint limit) {
}

public void SetAttachedLens(uint handle, string path) =>
InvokeCamera(handle, "attach lens", () => { _sonydll.SetAttachedLens(handle, path); return true; });
InvokeCamera(handle, "attach lens", () => { _sonydll.SetAttachedLens(handle, path); attachedLensPath = path; return true; });
public void SetFocusPosition(uint handle, uint position) =>
InvokeCamera(handle, "move focus", () => { _sonydll.SetFocusPosition(handle, position); return true; });
public uint GetFocusPosition(uint handle) =>
Expand Down
63 changes: 55 additions & 8 deletions Tests/NativeSonyDriverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ private sealed class Camera {
internal int ExposureCalls;
internal int PropertyCalls;
internal int CloseCalls;
internal bool OpenFails;
internal readonly List<(uint, string)> AttachedLenses = new List<(uint, string)>();
private readonly SonyMTPCameraDll dll;

internal Camera() {
// Skip only the Windows DLL loader. Replace the P/Invoke delegates,
// then exercise the actual SonyDriver and SonyMTPCameraDll methods.
var dll = (SonyMTPCameraDll)RuntimeHelpers.GetUninitializedObject(typeof(SonyMTPCameraDll));
dll = (SonyMTPCameraDll)RuntimeHelpers.GetUninitializedObject(typeof(SonyMTPCameraDll));
SetField(dll, "fGetCaptureStatus", new SonyMTPCameraDll.dGetCaptureStatus((uint h, ref ImageInfo image) => {
StatusCalls++;
image.Status = 2;
Expand All @@ -45,6 +48,10 @@ internal Camera() {
return PropertyError;
}));
SetField(dll, "fCloseDevice", new SonyMTPCameraDll.dCloseDevice(h => { CloseCalls++; return 0; }));
SetField(dll, "fSetAttachedLens", new SonyMTPCameraDll.dSetAttachedLens((uint h, string lens) => {
AttachedLenses.Add((h, lens));
return 0;
}));
Driver = new SonyDriver(dll, new NativeCallExecutor());
uint[] shutters = { 0, 0x001E0001, 0x00010001, 0x00040005, 0x00010002, 0x00010050, 0x00010064 };
var properties = new List<Sony.PropertyInfo> {
Expand All @@ -54,14 +61,25 @@ internal Camera() {
SetField(Driver, "_camera", new SonyCameraInfo(Handle, new DeviceInfo(), new CameraInfo(), properties));
}

// Stub the native open path: an empty property list and default device/camera info.
internal void UseOpenDevice(uint newHandle) {
SetField(dll, "fOpenDevice", new SonyMTPCameraDll.dOpenDevice(path => OpenFails ? SonyMTPCameraDll.INVALID_HANDLE_VALUE : newHandle));
SetField(dll, "fGetPropertyList", new SonyMTPCameraDll.dGetPropertyList((uint h, IntPtr list, ref uint size) => {
size = 0;
return list == IntPtr.Zero ? SonyMTPCameraDll.ERROR_RETRY : 0;
}));
SetField(dll, "fGetDeviceInfo", new SonyMTPCameraDll.dGetDeviceInfo((uint h, ref DeviceInfo info) => 0));
SetField(dll, "fGetCameraInfo", new SonyMTPCameraDll.dGetCameraInfo((uint h, ref CameraInfo info, uint flags) => 0));
}

private static void SetField(object target, string name, object value) =>
target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic).SetValue(target, value);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void ThreeNativeCaptureFailuresDisconnectAndBlockEveryLaterNativeEntry(bool starting) {
public void ThreeNativeCaptureFailuresDropTheHandleWhichIsThenClosedExactlyOnce(bool starting) {
var camera = new Camera { StatusError = 31, StartError = 1223 };
Action operation = () => {
if (starting) camera.Driver.StartCapture(Camera.Handle, 1);
Expand All @@ -72,14 +90,43 @@ public void ThreeNativeCaptureFailuresDisconnectAndBlockEveryLaterNativeEntry(bo
Assert.Equal(i < 2, camera.Driver.IsOpen(Camera.Handle));
}
Assert.Null(camera.Driver.CameraInfo);
Assert.Throws<InvalidOperationException>(operation);
Assert.Throws<InvalidOperationException>(() => camera.Driver.GetProperty(Camera.Handle, 0xD20D));
Assert.Throws<InvalidOperationException>(() => camera.Driver.CloseCamera(Camera.Handle));
Assert.Throws<InvalidOperationException>(() => camera.Driver.OpenCamera("camera"));
Assert.Throws<InvalidOperationException>(() => camera.Driver.Cameras());
Assert.Throws<InvalidOperationException>(() => camera.Driver.Lenses());
// The lost handle no longer reaches the camera, but the driver is still usable.
Assert.True(camera.Driver.CanReopen);
Assert.Throws<SonyException>(operation);
Assert.Throws<SonyException>(() => camera.Driver.GetProperty(Camera.Handle, 0xD20D));
Assert.Equal(3, starting ? camera.StartCalls : camera.StatusCalls);
Assert.Equal(0, camera.CloseCalls);
// It is still released in the native driver, once, so a reconnect starts clean.
camera.Driver.CloseCamera(Camera.Handle);
Assert.Equal(1, camera.CloseCalls);
Assert.Throws<SonyException>(() => camera.Driver.CloseCamera(Camera.Handle));
Assert.Equal(1, camera.CloseCalls);
}

[Fact]
public void ReopenReleasesTheOldHandleOnceOpensANewOneAndReattachesTheLens() {
var camera = new Camera { StatusError = 31 };
camera.UseOpenDevice(newHandle: 8);
camera.Driver.SetAttachedLens(Camera.Handle, "lens-1");
for (int i = 0; i < 3; i++) Assert.Throws<SonyException>(() => camera.Driver.GetCaptureStatus(Camera.Handle));
Assert.True(camera.Driver.IsAwaitingReopen);

camera.OpenFails = true;
Assert.Throws<SonyException>(() => camera.Driver.ReopenCamera(Camera.Handle, "camera"));
Assert.Equal(1, camera.CloseCalls);
Assert.True(camera.Driver.IsAwaitingReopen);

camera.OpenFails = false;
var reopened = camera.Driver.ReopenCamera(Camera.Handle, "camera");
Assert.Equal(8u, reopened.Handle);
Assert.True(camera.Driver.IsOpen(8));
Assert.False(camera.Driver.IsAwaitingReopen);
Assert.Equal(1, camera.CloseCalls);
Assert.Equal(new[] { (Camera.Handle, "lens-1"), (8u, "lens-1") }, camera.AttachedLenses);

camera.Driver.CloseCamera(8);
Assert.Equal(2, camera.CloseCalls);
Assert.False(camera.Driver.IsAwaitingReopen);
}

[Fact]
Expand Down
Loading