Skip to content

Commit

Permalink
#2: PowerMateVolume: receive reliable notifications when the computer…
Browse files Browse the repository at this point in the history
… resumes from standby, so device settings can be reset
  • Loading branch information
Aldaviva committed Jul 15, 2023
1 parent c200d93 commit 74af5bb
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 26 deletions.
23 changes: 19 additions & 4 deletions Demo/Demo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@

using IPowerMateClient powerMate = new PowerMateClient();

powerMate.LightBrightness = 255;
powerMate.LightBrightness = 128;
// powerMate.LightPulseSpeed = 3;
// powerMate.LightAnimation = LightAnimation.Pulsing;
Console.WriteLine($"Set brightness to {powerMate.LightBrightness}.");

powerMate.IsConnectedChanged += (_, isConnected) => Console.WriteLine(isConnected ? "Connected to PowerMate" : "Disconnected from PowerMate, attempting reconnection");

powerMate.InputReceived += (_, input) => Console.WriteLine($"Received event from PowerMate: {input}");
// powerMate.InputReceived += (_, input) => Console.WriteLine($"Received event from PowerMate: {input}");

// using Timer parameterChangeTimer = new(_ => { Console.WriteLine(DateTime.Now); }, null, 0, 10000);
// using Timer timer2 = new(10000) { AutoReset = true, Enabled = true };
// timer2.Elapsed += (_, _) => Console.WriteLine(DateTime.Now);

Console.WriteLine(powerMate.IsConnected ? "Listening for PowerMate events" : "Waiting for PowerMate to be connected");
using Timer setAllFeaturesTimer = new(_ => {
try {
bool didSetFeatures = powerMate.SetAllFeaturesIfStale();
Console.WriteLine($"Features {(didSetFeatures ? "were" : "were not")} reset automatically");
} catch (Exception e) {
Console.WriteLine("Exception while setting features: " + e);
}
}, null, 0, 3000);

CancellationTokenSource cancellationTokenSource = new();
Console.CancelKeyPress += (_, eventArgs) => {
eventArgs.Cancel = true;
cancellationTokenSource.Cancel();
};

Console.WriteLine(powerMate.IsConnected ? "Listening for PowerMate events" : "Waiting for PowerMate to be connected");
cancellationTokenSource.Token.WaitHandle.WaitOne();
2 changes: 2 additions & 0 deletions PowerMate/IPowerMateClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ public interface IPowerMateClient: IHidClient {
/// </summary>
int LightPulseSpeed { get; set; }

bool SetAllFeaturesIfStale();

}
36 changes: 29 additions & 7 deletions PowerMate/PowerMateClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class PowerMateClient: AbstractHidClient, IPowerMateClient {

private const byte DefaultLightBrightness = 80;

private static readonly TimeSpan MinFeatureResetInterval = TimeSpan.FromMilliseconds(500);

/// <inheritdoc />
protected override int VendorId { get; } = 0x077d;

Expand All @@ -32,7 +34,7 @@ public PowerMateClient(DeviceList deviceList): base(deviceList) { }

/// <inheritdoc />
protected override void OnConnect() {
LightAnimation = LightAnimation; //resend all pulsing and brightness values to device
SetAllFeatures();
}

/// <inheritdoc />
Expand All @@ -41,12 +43,8 @@ protected override void OnHidRead(byte[] readBuffer) {
PowerMateInput input = new(readBuffer);
EventSynchronizationContext.Post(_ => { InputReceived?.Invoke(this, input); }, null);

if ((LightAnimation != input.ActualLightAnimation
|| (LightAnimation != LightAnimation.Pulsing && LightBrightness != input.ActualLightBrightness)
|| (LightAnimation == LightAnimation.Pulsing && LightPulseSpeed != input.ActualLightPulseSpeed))
&& _mostRecentFeatureSetTime is not null && DateTime.Now - _mostRecentFeatureSetTime > TimeSpan.FromMilliseconds(500)) {
Console.WriteLine("Resetting features...");
LightAnimation = LightAnimation;
if (!AreDeviceFeaturesUpToDate(input) && EnoughTimeHasPassedThatFeaturesCanBeAutoReset()) {
SetAllFeatures();
}
}

Expand Down Expand Up @@ -123,6 +121,30 @@ private void SetFeature(PowerMateFeature feature, params byte[] payload) {
}
}

private bool AreDeviceFeaturesUpToDate(PowerMateInput deviceInput) =>
LightAnimation == deviceInput.ActualLightAnimation
&& (LightAnimation == LightAnimation.Pulsing || LightBrightness == deviceInput.ActualLightBrightness)
&& (LightAnimation != LightAnimation.Pulsing || LightPulseSpeed == deviceInput.ActualLightPulseSpeed);

private bool EnoughTimeHasPassedThatFeaturesCanBeAutoReset() => _mostRecentFeatureSetTime is not null && DateTime.Now - _mostRecentFeatureSetTime > MinFeatureResetInterval;

private void SetAllFeatures() {
LightAnimation = LightAnimation;
}

/// <inheritdoc />
public bool SetAllFeaturesIfStale() {
byte[] featureBuffer = new byte[9];
DeviceStream?.GetFeature(featureBuffer);

bool shouldSetFeatures = DeviceStream is not null && !AreDeviceFeaturesUpToDate(new PowerMateInput(featureBuffer)) && EnoughTimeHasPassedThatFeaturesCanBeAutoReset();
if (shouldSetFeatures) {
SetAllFeatures();
}

return shouldSetFeatures;
}

/// <param name="pulseSpeed">in the range [0, 24]</param>
/// <returns>two big-endian bytes to send to the PowerMate to set its pulsing speed</returns>
private static byte[] EncodePulseSpeed(int pulseSpeed) {
Expand Down
18 changes: 9 additions & 9 deletions PowerMateVolume/PowerMateVolume.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Microsoft.Win32;
using PowerMate;
using PowerMate;
using PowerMateVolume;

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

if (!float.TryParse(Environment.GetCommandLineArgs().ElementAtOrDefault(1), out float volumeIncrement)) {
volumeIncrement = 0.01f;
Expand Down Expand Up @@ -35,12 +34,13 @@
}
};

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
powerMate.LightAnimation = powerMate.LightAnimation;
}
};
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);

Console.WriteLine("Listening for PowerMate events");
cancellationTokenSource.Token.WaitHandle.WaitOne();
4 changes: 2 additions & 2 deletions PowerMateVolume/PowerMateVolume.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<Authors>Ben Hutchison</Authors>
<Copyright>© 2023 $(Authors)</Copyright>
<AssemblyTitle>PowerMate Volume</AssemblyTitle> <!-- File description -->
<Version>1.0.1</Version> <!-- Product version -->
<Version>1.0.2</Version> <!-- Product version -->
<Product>$(AssemblyTitle)</Product> <!-- Product name -->
<FileVersion>$(Version)</FileVersion> <!-- File version -->
<ApplicationManifest>app.manifest</ApplicationManifest>
Expand All @@ -23,7 +23,7 @@
</PropertyGroup>

<ItemGroup>
<Content Include="powermate.ico" />
<Content Include="powermate.ico" />
</ItemGroup>

<ItemGroup>
Expand Down
61 changes: 61 additions & 0 deletions PowerMateVolume/StandbyEventEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Diagnostics.Eventing.Reader;

namespace PowerMateVolume;

public interface IStandbyListener: IDisposable {

event EventHandler StandingBy;
event EventHandler Resumed;
event EventHandler<Exception> FatalError;

}

public class EventLogStandbyListener: IStandbyListener {

public event EventHandler? StandingBy;
public event EventHandler? Resumed;
public event EventHandler<Exception>? FatalError;

private readonly EventLogWatcher _logWatcher;

/// <exception cref="EventLogNotFoundException">if the given event log or file was not found</exception>
/// <exception cref="UnauthorizedAccessException">if the log did not already exist and this program is not running elevated</exception>
public EventLogStandbyListener() {
_logWatcher = new EventLogWatcher(new EventLogQuery("System", PathType.LogName, "*[System[Provider/@Name=\"Microsoft-Windows-Kernel-Power\" and (EventID=42 or EventID=107)]]"));

_logWatcher.EventRecordWritten += onEventRecord;

try {
_logWatcher.Enabled = true;
} catch (EventLogNotFoundException) {
_logWatcher.Dispose();
throw;
} catch (UnauthorizedAccessException) {
_logWatcher.Dispose();
throw;
}
}

private void onEventRecord(object? sender, EventRecordWrittenEventArgs e) {
if (e.EventException is { } exception) {
FatalError?.Invoke(this, exception);
Dispose();
} else {
using EventRecord? record = e.EventRecord;
switch (record?.Id) {
case 42:
StandingBy?.Invoke(this, EventArgs.Empty);
break;
case 107:
Resumed?.Invoke(this, EventArgs.Empty);
break;
}
}
}

public void Dispose() {
_logWatcher.Dispose();
GC.SuppressFinalize(this);
}

}
8 changes: 4 additions & 4 deletions Tests/Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
Expand All @@ -11,9 +11,9 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="7.4.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down

0 comments on commit 74af5bb

Please sign in to comment.