From cf6c75a6764619eb23b0a010ba6159d436ce5b29 Mon Sep 17 00:00:00 2001 From: Ben Hutchison Date: Mon, 4 Sep 2023 21:07:32 -0700 Subject: [PATCH] #3: Crash on resume if device is unplugged (or not responding) [retry setting features once after 2 seconds if it fails] --- PowerMate/PowerMate.csproj | 2 +- PowerMate/PowerMateClient.cs | 12 ++++++-- PowerMateVolume/PowerMateVolume.cs | 27 +++++++++++------ PowerMateVolume/PowerMateVolume.csproj | 2 +- PowerMateVolume/StandbyEventEmitter.cs | 10 +++++-- PowerMateVolume/VolumeChanger.cs | 40 ++++++++++++++++++++++++-- 6 files changed, 74 insertions(+), 19 deletions(-) diff --git a/PowerMate/PowerMate.csproj b/PowerMate/PowerMate.csproj index f9681e6..a47b2cf 100644 --- a/PowerMate/PowerMate.csproj +++ b/PowerMate/PowerMate.csproj @@ -13,7 +13,7 @@ true git Apache-2.0 - griffin powermate hid rotary-encoder + griffin powermate hid rotary-encoder usb knob dial icon.jpg Readme.md diff --git a/PowerMate/PowerMateClient.cs b/PowerMate/PowerMateClient.cs index fa35c59..87dde5d 100644 --- a/PowerMate/PowerMateClient.cs +++ b/PowerMate/PowerMateClient.cs @@ -107,16 +107,22 @@ public int LightPulseSpeed { // ExceptionAdjustment: M:System.Array.Copy(System.Array,System.Int32,System.Array,System.Int32,System.Int32) -T:System.RankException // ExceptionAdjustment: M:System.Array.Copy(System.Array,System.Int32,System.Array,System.Int32,System.Int32) -T:System.ArrayTypeMismatchException private void SetFeature(PowerMateFeature feature, params byte[] payload) { - _mostRecentFeatureSetTime = DateTime.Now; byte[] featureData = { 0x00, 0x41, 0x01, (byte) feature, 0x00, /* payload copied here */ 0x00, 0x00, 0x00, 0x00 }; Array.Copy(payload, 0, featureData, 5, Math.Min(payload.Length, 4)); try { - DeviceStream?.SetFeature(featureData); + SetFeatureAndTime(); } catch (IOException e) { if (e.InnerException is Win32Exception { NativeErrorCode: 0 }) { // retry once with no delay if we get a "The operation completed successfully" error - DeviceStream?.SetFeature(featureData); + SetFeatureAndTime(); + } + } + + void SetFeatureAndTime() { + if (DeviceStream != null) { + DeviceStream.SetFeature(featureData); + _mostRecentFeatureSetTime = DateTime.Now; } } } diff --git a/PowerMateVolume/PowerMateVolume.cs b/PowerMateVolume/PowerMateVolume.cs index 3ffc49c..95e3fed 100644 --- a/PowerMateVolume/PowerMateVolume.cs +++ b/PowerMateVolume/PowerMateVolume.cs @@ -12,12 +12,6 @@ powerMate.LightBrightness = 0; -CancellationTokenSource cancellationTokenSource = new(); -Console.CancelKeyPress += (_, eventArgs) => { - eventArgs.Cancel = true; - cancellationTokenSource.Cancel(); -}; - powerMate.InputReceived += (_, powerMateEvent) => { switch (powerMateEvent) { case { IsPressed: true, RotationDirection: RotationDirection.None }: @@ -37,7 +31,24 @@ using IStandbyListener standbyListener = new EventLogStandbyListener(); standbyListener.FatalError += (_, exception) => MessageBox.Show("Event log subscription is broken, continuing without resume detection: " + exception, "PowerMateVolume", MessageBoxButtons.OK, MessageBoxIcon.Error); -standbyListener.Resumed += (_, _) => powerMate.SetAllFeaturesIfStale(); +standbyListener.Resumed += (_, _) => { + try { + powerMate.SetAllFeaturesIfStale(); + } catch (IOException) { + Thread.Sleep(2000); + try { + powerMate.SetAllFeaturesIfStale(); + } catch (IOException) { + // device is probably in a bad state, but there's nothing we can do about it + } + } +}; + +CancellationTokenSource exitTokenSource = new(); +Console.CancelKeyPress += (_, eventArgs) => { + eventArgs.Cancel = true; + exitTokenSource.Cancel(); +}; Console.WriteLine("Listening for PowerMate events"); -cancellationTokenSource.Token.WaitHandle.WaitOne(); \ No newline at end of file +exitTokenSource.Token.WaitHandle.WaitOne(); \ No newline at end of file diff --git a/PowerMateVolume/PowerMateVolume.csproj b/PowerMateVolume/PowerMateVolume.csproj index e1f29b6..ba9213b 100644 --- a/PowerMateVolume/PowerMateVolume.csproj +++ b/PowerMateVolume/PowerMateVolume.csproj @@ -12,7 +12,7 @@ Ben Hutchison © 2023 $(Authors) PowerMate Volume - 1.0.2 + 1.0.3 $(AssemblyTitle) $(Version) app.manifest diff --git a/PowerMateVolume/StandbyEventEmitter.cs b/PowerMateVolume/StandbyEventEmitter.cs index 735b7f9..1676fd9 100644 --- a/PowerMateVolume/StandbyEventEmitter.cs +++ b/PowerMateVolume/StandbyEventEmitter.cs @@ -12,6 +12,9 @@ public interface IStandbyListener: IDisposable { public class EventLogStandbyListener: IStandbyListener { + private const int StandByEventId = 42; + private const int ResumeEventId = 107; + public event EventHandler? StandingBy; public event EventHandler? Resumed; public event EventHandler? FatalError; @@ -21,7 +24,8 @@ public class EventLogStandbyListener: IStandbyListener { /// if the given event log or file was not found /// if the log did not already exist and this program is not running elevated public EventLogStandbyListener() { - _logWatcher = new EventLogWatcher(new EventLogQuery("System", PathType.LogName, "*[System[Provider/@Name=\"Microsoft-Windows-Kernel-Power\" and (EventID=42 or EventID=107)]]")); + _logWatcher = new EventLogWatcher(new EventLogQuery("System", PathType.LogName, + $"*[System[Provider/@Name=\"Microsoft-Windows-Kernel-Power\" and (EventID={StandByEventId} or EventID={ResumeEventId})]]")); _logWatcher.EventRecordWritten += onEventRecord; @@ -43,10 +47,10 @@ private void onEventRecord(object? sender, EventRecordWrittenEventArgs e) { } else { using EventRecord? record = e.EventRecord; switch (record?.Id) { - case 42: + case StandByEventId: StandingBy?.Invoke(this, EventArgs.Empty); break; - case 107: + case ResumeEventId: Resumed?.Invoke(this, EventArgs.Empty); break; } diff --git a/PowerMateVolume/VolumeChanger.cs b/PowerMateVolume/VolumeChanger.cs index 13be51a..3379e8c 100644 --- a/PowerMateVolume/VolumeChanger.cs +++ b/PowerMateVolume/VolumeChanger.cs @@ -21,11 +21,23 @@ public interface IVolumeChanger: IDisposable { /// void IncreaseVolume(int increments = 1); + /// + /// Get or set the default audio output device's volume. + /// + /// The absolute volume level, in the range [0, 1]. Will be clipped if it's set to a value outside that range. + float Volume { get; set; } + /// /// If the default audio output device is not currently muted, then mute it. Otherwise, unmute it. /// void ToggleMute(); + /// + /// Mute or unmute the default audio output device, or get whether or not it's currently muted. + /// + /// if the device is or should be muted, or if it is or should be unmuted. + bool Muted { get; set; } + } public class VolumeChanger: IVolumeChanger { @@ -74,17 +86,39 @@ private void DetachFromCurrentDevice() { _audioOutputEndpoint = null; } + /// public void IncreaseVolume(int increments = 1) { if (_audioOutputVolume is not null && increments != 0) { - float newVolume = Math.Max(0, Math.Min(1, _audioOutputVolume.MasterVolumeLevelScalar + VolumeIncrement * increments)); - _audioOutputVolume.MasterVolumeLevelScalar = newVolume; + Volume = _audioOutputVolume.MasterVolumeLevelScalar + VolumeIncrement * increments; // Console.WriteLine($"Set volume to {newVolume:P2}"); } } + /// + public float Volume { + get => _audioOutputVolume?.MasterVolumeLevelScalar ?? 0; + set { + float newVolume = Math.Max(0, Math.Min(1, value)); + if (_audioOutputVolume != null) { + _audioOutputVolume.MasterVolumeLevelScalar = newVolume; + } + } + } + + /// public void ToggleMute() { if (_audioOutputVolume is not null) { - _audioOutputVolume.IsMuted ^= true; + Muted = !Muted; + } + } + + /// + public bool Muted { + get => _audioOutputVolume?.IsMuted ?? true; + set { + if (_audioOutputVolume is not null) { + _audioOutputVolume.IsMuted = value; + } } }