Skip to content

Commit

Permalink
#1: Brightness seems to revert to 80 after computer resumes from standby
Browse files Browse the repository at this point in the history
  • Loading branch information
Aldaviva committed Feb 24, 2023
1 parent 441180b commit da0ee0b
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dotnetpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
New-Item -Name Demo/bin/Release/upload/Windows -Type Directory -ErrorAction SilentlyContinue
New-Item -Name Demo/bin/Release/upload/MacOS -Type Directory -ErrorAction SilentlyContinue
Copy-Item PowerMateVolume/bin/Release/net7.0-windows7.0/win10-x64/publish/PowerMateVolume.exe Demo/bin/Release/upload/Windows/PowerMateVolume.exe
Copy-Item PowerMateVolume/bin/Release/net7.0-windows/win10-x64/publish/PowerMateVolume.exe Demo/bin/Release/upload/Windows/PowerMateVolume.exe
Copy-Item Demo/bin/Release/net7.0/win10-x64/publish/Demo.exe Demo/bin/Release/upload/Windows/Demo-x64.exe
Copy-Item Demo/bin/Release/net7.0/win10-arm64/publish/Demo.exe Demo/bin/Release/upload/Windows/Demo-ARM64.exe
Copy-Item Demo/bin/Release/net7.0/osx-x64/publish/Demo Demo/bin/Release/upload/MacOS/Demo-x64
Expand Down
4 changes: 3 additions & 1 deletion PowerMate.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String></wpf:ResourceDictionary>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Demo_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=PowerMateVolume_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
2 changes: 1 addition & 1 deletion PowerMate/IPowerMateClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public interface IPowerMateClient: IDisposable, INotifyPropertyChanged {
/// <para>Enables or disables a pulsing animation of the blue/cyan LED in the base of the PowerMate.</para>
/// <para>By default, this is <see cref="PowerMate.LightAnimation.Solid"/>, and the light shines at a constant brightness controlled by <see cref="LightBrightness"/>.</para>
/// <para>To enable the pulsing animation, set this to <see cref="PowerMate.LightAnimation.Pulsing"/>, and set <see cref="LightPulseSpeed"/> to control the frequency.</para>
/// <para>By setting this to <see cref="PowerMate.LightAnimation.SolidWhileAwakeAndPulsingDuringComputerStandby"/>, you can set the light to shine at a constant brightness while the computer which the device is attached to is awake, but change to a pulsing animation when the computer goes to sleep.</para>
/// <para>By setting this to <see cref="PowerMate.LightAnimation.SolidWhileAwakeAndPulsingDuringComputerStandby"/>, you can set the light to shine at a constant brightness while the computer which the device is attached to is awake, but change to a pulsing animation when the computer goes to sleep, instead of turning off when the computer goes to sleep.</para>
/// <para>This property does not reflect animation changes made to the device by other programs running on your computer.</para>
/// <para>It is safe to set this property even if a device is not connected. If you do, the animation will be saved until the device reconnects, when your value will be automatically
/// reapplied to the device.</para>
Expand Down
9 changes: 6 additions & 3 deletions PowerMate/LightAnimation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@
public enum LightAnimation {

/// <summary>
/// The light will shine at a constant brightness, controlled by <see cref="IPowerMateClient.LightBrightness"/>.
/// <para>The PowerMate LED will shine at a constant brightness, controlled by <see cref="IPowerMateClient.LightBrightness"/>.</para>
/// <para>When the computer to which the PowerMate is connected goes to sleep, the light will turn off.</para>
/// </summary>
Solid,

/// <summary>
/// The light will get brighter and dimmer in a cyclical animation, the frequency of which is controlled by <see cref="IPowerMateClient.LightPulseSpeed"/>.
/// <para>The light will get brighter and dimmer in a cyclical animation, the frequency of which is controlled by <see cref="IPowerMateClient.LightPulseSpeed"/>.</para>
/// <para>When the computer to which the PowerMate is connected goes to sleep, the light will turn off.</para>
/// </summary>
Pulsing,

/// <summary>
/// While the computer to which the PowerMate is attached is awake, use <see cref="Solid"/>, but while it is asleep, use <see cref="Pulsing"/>.
/// <para>The PowerMate LED will shine at a constant brightness, controlled by <see cref="IPowerMateClient.LightBrightness"/>.</para>
/// <para>When the computer to which the PowerMate is connected goes to sleep, the light will get brighter and dimmer in a cyclical animation, the frequency of which is controlled by <see cref="IPowerMateClient.LightPulseSpeed"/>.</para>
/// </summary>
SolidWhileAwakeAndPulsingDuringComputerStandby

Expand Down
2 changes: 1 addition & 1 deletion PowerMate/PowerMateClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ private void AttachToDevice() {
_hidStream.Closed += ReattachToDevice;
_hidStream.ReadTimeout = Timeout.Infinite;
_cancellationTokenSource = new CancellationTokenSource();
LightAnimation = LightAnimation; //resend all pulsing and brightness values to device
IsConnected = true;
LightAnimation = LightAnimation; //resend all pulsing and brightness values to device

try {
Task.Factory.StartNew(HidReadLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
Expand Down
20 changes: 15 additions & 5 deletions PowerMateVolume/PowerMateVolume.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using PowerMate;
using Microsoft.Win32;
using PowerMate;
using PowerMateVolume;

// ReSharper disable AccessToDisposedClosure - disposal happens at program shutdown, so access won't happen after that

if (!float.TryParse(Environment.GetCommandLineArgs().ElementAtOrDefault(1), out float volumeIncrement)) {
volumeIncrement = 0.01f;
}

using IPowerMateClient powerMateClient = new PowerMateClient();
using VolumeChanger volumeChanger = new VolumeChangerImpl { volumeIncrement = volumeIncrement };
using IVolumeChanger volumeChanger = new VolumeChanger { VolumeIncrement = volumeIncrement };

powerMateClient.LightBrightness = 0;

Expand All @@ -19,18 +22,25 @@
powerMateClient.InputReceived += (_, powerMateEvent) => {
switch (powerMateEvent) {
case { IsPressed: true, RotationDirection: RotationDirection.None }:
volumeChanger.toggleMute();
volumeChanger.ToggleMute();
break;
case { IsPressed: false, RotationDirection: RotationDirection.Clockwise }:
volumeChanger.increaseVolume((int) powerMateEvent.RotationDistance);
volumeChanger.IncreaseVolume((int) powerMateEvent.RotationDistance);
break;
case { IsPressed: false, RotationDirection: RotationDirection.Counterclockwise }:
volumeChanger.increaseVolume(-1 * (int) powerMateEvent.RotationDistance);
volumeChanger.IncreaseVolume(-1 * (int) powerMateEvent.RotationDistance);
break;
default:
break;
}
};

SystemEvents.PowerModeChanged += (_, args) => {
if (args.Mode == PowerModes.Resume) {
// #1: On Jarnsaxa, waking up from sleep resets the PowerMate's light settings, so set them all again
powerMateClient.LightAnimation = powerMateClient.LightAnimation;
}
};

Console.WriteLine("Listening for PowerMate events");
cancellationTokenSource.Token.WaitHandle.WaitOne();
7 changes: 4 additions & 3 deletions PowerMateVolume/PowerMateVolume.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows7.0</TargetFramework>
<TargetFramework>net7.0-windows</TargetFramework>
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand All @@ -14,9 +14,10 @@
<AssemblyTitle>PowerMate Volume</AssemblyTitle> <!-- File description -->
<Version>1.0.0</Version> <!-- Product version -->
<Product>$(AssemblyTitle)</Product> <!-- Product name -->
<FileVersion>$(Version)</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest> <!-- File version -->
<FileVersion>$(Version)</FileVersion> <!-- File version -->
<ApplicationManifest>app.manifest</ApplicationManifest>
<IsPackable>false</IsPackable>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

<ItemGroup>
Expand Down
66 changes: 33 additions & 33 deletions PowerMateVolume/VolumeChanger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,40 @@ namespace PowerMateVolume;
/// <summary>
/// Change the volume of the default Windows output device.
/// </summary>
public interface VolumeChanger: IDisposable {
public interface IVolumeChanger: IDisposable {

/// <summary>
/// <para>How much the volume should change for each increment or decrement.</para>
/// <para>Defaults to <c>0.01</c>, or 1 percentage point.</para>
/// </summary>
/// <exception cref="ArgumentOutOfRangeException" accessor="set">if the value is outside of the range <c>(0, 1]</c></exception>
float volumeIncrement { get; set; }
float VolumeIncrement { get; set; }

/// <summary>
/// <para>Increase or decrease the volume by 1 or more increments of size <see cref="volumeIncrement"/>.</para>
/// <para>Increase or decrease the volume by 1 or more increments of size <see cref="VolumeIncrement"/>.</para>
/// <para>To decrease the volume, pass a negative <paramref name="increments"/>.</para>
/// </summary>
/// <param name="increments"></param>
void increaseVolume(int increments = 1);
void IncreaseVolume(int increments = 1);

/// <summary>
/// <para>If the default audio output device is not currently muted, then mute it. Otherwise, unmute it.</para>
/// </summary>
void toggleMute();
void ToggleMute();

}

public class VolumeChangerImpl: VolumeChanger {
public class VolumeChanger: IVolumeChanger {

private readonly MMDeviceEnumerator mmDeviceEnumerator = new();
private readonly MMDeviceEnumerator _mmDeviceEnumerator = new();

private MMDevice? defaultAudioEndpoint;
private AudioEndpointVolume? audioEndpointVolume;
private MMDevice? _defaultAudioEndpoint;
private AudioEndpointVolume? _audioEndpointVolume;

private float _volumeIncrement = 0.01f;

/// <inheritdoc />
public float volumeIncrement {
public float VolumeIncrement {
get => _volumeIncrement;
set {
if (value is not (> 0 and <= 1)) {
Expand All @@ -49,35 +49,35 @@ public float volumeIncrement {
}
}

public VolumeChangerImpl() {
mmDeviceEnumerator.DefaultDeviceChanged += onDefaultDeviceChanged;
attachToDefaultDevice();
public VolumeChanger() {
_mmDeviceEnumerator.DefaultDeviceChanged += onDefaultDeviceChanged;
AttachToDefaultDevice();
}

private void attachToDefaultDevice(MMDevice? newDefaultAudioEndpoint = null) {
detachFromDefaultDevice();
defaultAudioEndpoint = newDefaultAudioEndpoint ?? mmDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
audioEndpointVolume = AudioEndpointVolume.FromDevice(defaultAudioEndpoint);
private void AttachToDefaultDevice(MMDevice? newDefaultAudioEndpoint = null) {
DetachFromDefaultDevice();
_defaultAudioEndpoint = newDefaultAudioEndpoint ?? _mmDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
_audioEndpointVolume = AudioEndpointVolume.FromDevice(_defaultAudioEndpoint);
}

private void detachFromDefaultDevice() {
audioEndpointVolume?.Dispose();
audioEndpointVolume = null;
defaultAudioEndpoint?.Dispose();
defaultAudioEndpoint = null;
private void DetachFromDefaultDevice() {
_audioEndpointVolume?.Dispose();
_audioEndpointVolume = null;
_defaultAudioEndpoint?.Dispose();
_defaultAudioEndpoint = null;
}

public void increaseVolume(int increments = 1) {
if (audioEndpointVolume is not null && increments != 0) {
float newVolume = Math.Max(0, Math.Min(1, audioEndpointVolume.MasterVolumeLevelScalar + volumeIncrement * increments));
audioEndpointVolume.MasterVolumeLevelScalar = newVolume;
public void IncreaseVolume(int increments = 1) {
if (_audioEndpointVolume is not null && increments != 0) {
float newVolume = Math.Max(0, Math.Min(1, _audioEndpointVolume.MasterVolumeLevelScalar + VolumeIncrement * increments));
_audioEndpointVolume.MasterVolumeLevelScalar = newVolume;
Console.WriteLine($"Set volume to {newVolume:P2}");
}
}

public void toggleMute() {
if (audioEndpointVolume is not null) {
audioEndpointVolume.IsMuted ^= true;
public void ToggleMute() {
if (_audioEndpointVolume is not null) {
_audioEndpointVolume.IsMuted ^= true;
}
}

Expand All @@ -86,16 +86,16 @@ private void onDefaultDeviceChanged(object? sender, DefaultDeviceChangedEventArg
eventArgs.TryGetDevice(out MMDevice? newDefaultAudioEndpoint);

if (newDefaultAudioEndpoint is not null) {
attachToDefaultDevice(newDefaultAudioEndpoint);
AttachToDefaultDevice(newDefaultAudioEndpoint);
}
}
}

protected virtual void Dispose(bool disposing) {
if (disposing) {
detachFromDefaultDevice();
mmDeviceEnumerator.DefaultDeviceChanged -= onDefaultDeviceChanged;
mmDeviceEnumerator.Dispose();
DetachFromDefaultDevice();
_mmDeviceEnumerator.DefaultDeviceChanged -= onDefaultDeviceChanged;
_mmDeviceEnumerator.Dispose();
}
}

Expand Down
8 changes: 4 additions & 4 deletions Tests/PowerMateClientOutputTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public void SetAnimationSolid() {
client.LightAnimation = LightAnimation.Solid;

client.LightAnimation.Should().Be(LightAnimation.Solid);
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappened();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappened();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x01, 0x00, 0xff, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedTwiceExactly();
}

Expand All @@ -89,7 +89,7 @@ public void SetAnimationPulsing() {
client.LightAnimation = LightAnimation.Pulsing;

client.LightAnimation.Should().Be(LightAnimation.Pulsing);
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappened();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
}
Expand All @@ -103,7 +103,7 @@ public void SetAnimationPulsingDuringSleepOnly() {

client.LightAnimation.Should().Be(LightAnimation.SolidWhileAwakeAndPulsingDuringComputerStandby);
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappened();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedOnceExactly();
A.CallTo(() => _stream.SetFeature(A<byte[]>.That.IsSameSequenceAs(new byte[] { 0x00, 0x41, 0x01, 0x01, 0x00, 0x80, 0x00, 0x00, 0x00 }), 0, 9)).MustHaveHappenedTwiceExactly();
}
Expand Down

0 comments on commit da0ee0b

Please sign in to comment.