diff --git a/PowerMate/ExceptionAdjustments.txt b/PowerMate/ExceptionAdjustments.txt index f6b80a5..ae44038 100644 --- a/PowerMate/ExceptionAdjustments.txt +++ b/PowerMate/ExceptionAdjustments.txt @@ -8,3 +8,4 @@ P:System.Array.Length get -T:System.OverflowException M:System.IO.Stream.ReadAsync(System.Byte[],System.Int32,System.Int32,System.Threading.CancellationToken) -T:System.NotSupportedException M:System.Math.Abs(System.Int32) -T:System.OverflowException +M:System.TimeSpan.FromMilliseconds(System.Double) -T:System.OverflowException diff --git a/PowerMate/IPowerMateClient.cs b/PowerMate/IPowerMateClient.cs index c908241..045b811 100644 --- a/PowerMate/IPowerMateClient.cs +++ b/PowerMate/IPowerMateClient.cs @@ -53,6 +53,12 @@ public interface IPowerMateClient: IHidClient { /// int LightPulseSpeed { get; set; } + /// + /// Reapplies all of the values you set for , , and from this library's internal state to the PowerMate device. + /// This is useful because the PowerMate loses these values when the computer it is attached to enters and leaves standby mode. After resuming from standby, the PowerMate device will load its default settings, erasing your changes. To fix this and restore the values you set before standby, call this method when the computer resumes. + /// To detect when a Windows computer resumes from standby, it is more reliable to read from the Windows Event Log than to subscribe to the Microsoft.Win32.SystemEvents.PowerModeChanged event, which occasionally does not fire an event when resuming from standby. For an example Event Log listener implementation, see . + /// + /// bool SetAllFeaturesIfStale(); } \ No newline at end of file diff --git a/PowerMate/PowerMate.csproj b/PowerMate/PowerMate.csproj index f0f3ce0..f9681e6 100644 --- a/PowerMate/PowerMate.csproj +++ b/PowerMate/PowerMate.csproj @@ -1,7 +1,7 @@ - 1.0.1 + 1.1.0 Ben Hutchison Ben Hutchison PowerMate @@ -15,6 +15,7 @@ Apache-2.0 griffin powermate hid rotary-encoder icon.jpg + Readme.md netstandard2.0 latest @@ -42,6 +43,7 @@ + diff --git a/PowerMateVolume/PowerMateVolume.cs b/PowerMateVolume/PowerMateVolume.cs index 367e1ae..3ffc49c 100644 --- a/PowerMateVolume/PowerMateVolume.cs +++ b/PowerMateVolume/PowerMateVolume.cs @@ -37,10 +37,7 @@ 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 += (_, _) => MessageBox.Show(powerMate.SetAllFeaturesIfStale() - ? "On resume, PowerMate had wrong settings, so PowerMateVolume reset all the features on the device." - : "On resume, PowerMate had right settings, so PowerMateVolume did not reset the features on the device.", - "PowerMateVolume", MessageBoxButtons.OK, MessageBoxIcon.Information); +standbyListener.Resumed += (_, _) => powerMate.SetAllFeaturesIfStale(); Console.WriteLine("Listening for PowerMate events"); cancellationTokenSource.Token.WaitHandle.WaitOne(); \ No newline at end of file diff --git a/Readme.md b/Readme.md index c0d28c2..3ca3a0e 100644 --- a/Readme.md +++ b/Readme.md @@ -19,6 +19,7 @@ PowerMate - [`LightBrightness` property](#lightbrightness-property) - [`LightAnimation` property](#lightanimation-property) - [`LightPulseSpeed` property](#lightpulsespeed-property) + - [PowerMate light state loss on resume](#powermate-light-state-loss-on-resume) 1. [Demos](#demos) - [Simple demo](#simple-demo) - [Volume control](#volume-control) @@ -193,6 +194,29 @@ powerMate.LightPulseSpeed = 12; powerMate.LightAnimation = LightAnimation.Pulsing; ``` +### PowerMate light state loss on resume + +When the computer goes into standby mode and then resumes, the PowerMate loses all of its state and resets its settings to their default values, erasing your light control changes. There are two techniques to fix this, and you should use both of them. + +#### Automatically reapply settings on stale input + +Each time the PowerMate device sends an input to the computer, such as a knob turn or press, it also sends the current state of the lights. This library checks that device state and compares it to the values you set using `LightBrightness`, `LightAnimation`, and `LightPulseSpeed`. If any of them differ, it will automatically send the correct values to the PowerMate. You don't have to do anything to enable this behavior. + +#### Manually reapply settings on resume + +Unfortunately, the user is likely to see the incorrect light state before they send an input with the PowerMate: it will be wrong as soon as the computer resumes, and they may not need to touch the PowerMate until much later. + +To fix this, your program should also wait for the computer to resume from standby, and when it does, force this library to resend all of the light control property values to the device by calling **`SetAllFeaturesIfStale()`**. + +To detect when a Windows computer resumes from standby, a successful strategy is to [listen for event ID 107 from the Kernel-Power source in the System log](https://github.com/Aldaviva/PowerMate/blob/74af5bb2daad6cbc0e07b823b1378cab172175c1/PowerMateVolume/StandbyEventEmitter.cs). + +```cs +using IStandbyListener standbyListener = new EventLogStandbyListener(); +standbyListener.Resumed += (_, _) => powerMate.SetAllFeaturesIfStale(); +``` + +⛔ You should not listen for [`SystemEvents.PowerModeChanged`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.win32.systemevents.powermodechanged?view=windowsdesktop-7.0) events because they are unreliable and do not get sent about 5% of the time. + ## Demos ### Simple demo diff --git a/Tests/PowerMateClientInputTest.cs b/Tests/PowerMateClientInputTest.cs index fc86e9c..5d613ed 100644 --- a/Tests/PowerMateClientInputTest.cs +++ b/Tests/PowerMateClientInputTest.cs @@ -50,4 +50,17 @@ public void Pressed() { actualEvent!.Value.RotationDistance.Should().Be(0); } + [Fact] + public void ResetAllFeaturesOnStaleRead() { + PowerMateClient client = new(_deviceList) { + LightBrightness = 255 + }; + byte[] expected = { 0x00, 0x41, 0x01, 0x01, 0x00, 0xFF, 0x00, 0x00, 0x00 }; + A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(expected), A._, A._)).MustHaveHappenedOnceExactly(); + + Thread.Sleep(750); + + A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(expected), A._, A._)).MustHaveHappenedTwiceOrMore(); + } + } \ No newline at end of file diff --git a/Tests/PowerMateClientOutputTest.cs b/Tests/PowerMateClientOutputTest.cs index 3a221b1..f0b7dd1 100644 --- a/Tests/PowerMateClientOutputTest.cs +++ b/Tests/PowerMateClientOutputTest.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using HidSharp; using PowerMate; @@ -111,4 +112,31 @@ public void SetAnimationPulsingDuringSleepOnly() { A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedTwiceExactly(); } + [Fact] + public void RetrySetFeatureOnceOnFailure() { + PowerMateClient client = new(_deviceList); + byte[] expected = { 0x00, 0x41, 0x01, 0x01, 0x00, 0xFF, 0x00, 0x00, 0x00 }; + A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(expected), A._, A._)).Throws(new IOException("The operation completed successfully", new Win32Exception(0))).Once() + .Then.DoesNothing(); + + client.LightBrightness = 255; + + A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(expected), A._, A._)).MustHaveHappenedTwiceExactly(); + } + + [Fact] + public void SetAllFeaturesIfStale() { + PowerMateClient client = new(_deviceList); + client.LightBrightness = 255; + byte[] expected = { 0x00, 0x41, 0x01, 0x01, 0x00, 0xFF, 0x00, 0x00, 0x00 }; + A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(expected), A._, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _stream.GetFeature(A._, A._, A._)).Invokes((byte[] buffer, int offset, int count) => { Array.Fill(buffer, (byte) 0, offset, count); }); + + Thread.Sleep(750); + bool actual = client.SetAllFeaturesIfStale(); + + actual.Should().BeTrue(); + A.CallTo(() => _stream.SetFeature(A.That.IsSameSequenceAs(expected), A._, A._)).MustHaveHappenedTwiceOrMore(); + } + } \ No newline at end of file diff --git a/Tests/PowerMateInputTest.cs b/Tests/PowerMateInputTest.cs index e96e9d5..4baa328 100644 --- a/Tests/PowerMateInputTest.cs +++ b/Tests/PowerMateInputTest.cs @@ -172,4 +172,15 @@ public void DecodeBrightness(byte inputByte4, byte inputByte5, byte expected) { actual.ActualLightBrightness.Should().Be(expected); } + [Theory] + [InlineData(7)] // illegal animation + [InlineData(48)] // illegal pulse speed + public void ArgumentOutOfRange(byte inputByte5) { +#pragma warning disable CA1806 // the side effect of the constructor is an exception, which we want to test + // ReSharper disable once ObjectCreationAsStatement - the side effect of the constructor is an exception, which we want to test + Action thrower = () => new PowerMateInput(new byte[] { 0, 0, 1, 0, 0, inputByte5, 0x0a }); +#pragma warning restore CA1806 + thrower.Should().Throw(); + } + } \ No newline at end of file