diff --git a/PowerMate/IPowerMateClient.cs b/PowerMate/IPowerMateClient.cs index 16c2ca5..d381315 100644 --- a/PowerMate/IPowerMateClient.cs +++ b/PowerMate/IPowerMateClient.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using HidClient; namespace PowerMate; @@ -6,42 +7,17 @@ namespace PowerMate; /// Listen for events and control the light on a connected Griffin PowerMate USB device. /// To get started, construct a new instance of . /// Once you have constructed an instance, you can subscribe to events to be notified when the PowerMate knob is rotated, pressed, or released. -/// You can also subscribe to or to be notified when it connects or disconnects from a PowerMate. +/// You can also subscribe to or to be notified when it connects or disconnects from a PowerMate. /// The light is controlled by the , , and properties. /// Remember to dispose of this instance when you're done using it by calling , or with a statement or declaration. /// -public interface IPowerMateClient: IDisposable, INotifyPropertyChanged { - - /// - /// if the client is currently connected to a PowerMate device, or if it is disconnected, possibly because there is no PowerMate device - /// plugged into the computer. - /// will automatically try to connect to a PowerMate device when you construct a new instance, so you don't have to call any additional methods in order to make - /// it start connecting. - /// If a PowerMate device is plugged in, will already be by the time the constructor returns. - /// To receive notifications when this property changes, you can subscribe to the or events. - /// - bool IsConnected { get; } +public interface IPowerMateClient: IHidClient { /// /// Fired whenever the PowerMate knob is rotated, pressed, or released. /// event EventHandler InputReceived; - /// - /// Fired whenever the connection state of the PowerMate changes. Not fired when constructing or disposing the instance. - /// The event argument contains the new value of . - /// This value can also be accessed at any time by reading the property. - /// If you want to use data binding which expects events, also implements - /// , so you can use that event instead. - /// - event EventHandler IsConnectedChanged; - - /// - /// on which to run event callbacks. Useful if your delegates need to update a user interface on the main thread. Callbacks run on the current thread by - /// default. - /// - SynchronizationContext EventSynchronizationContext { get; set; } - /// /// Get or set how bright the blue/cyan LED in the base of the PowerMate is, between 0 (off) and 255 (brightest), inclusive. When the device is first plugged in, it defaults to /// 80. diff --git a/PowerMate/PowerMate.csproj b/PowerMate/PowerMate.csproj index 8f81985..f0f3ce0 100644 --- a/PowerMate/PowerMate.csproj +++ b/PowerMate/PowerMate.csproj @@ -1,7 +1,7 @@ - 1.0.0 + 1.0.1 Ben Hutchison Ben Hutchison PowerMate @@ -36,7 +36,7 @@ - + diff --git a/PowerMate/PowerMateClient.cs b/PowerMate/PowerMateClient.cs index 5f1b35d..abab496 100644 --- a/PowerMate/PowerMateClient.cs +++ b/PowerMate/PowerMateClient.cs @@ -1,137 +1,53 @@ using System.ComponentModel; using System.Runtime.CompilerServices; +using HidClient; using HidSharp; namespace PowerMate; -/// -public class PowerMateClient: IPowerMateClient { +/// +public class PowerMateClient: AbstractHidClient, IPowerMateClient { - private const int PowerMateVendorId = 0x077d; - private const int PowerMateProductId = 0x0410; private const byte DefaultLightBrightness = 80; - private readonly object _hidStreamLock = new(); - - private DeviceList? _deviceList; - private CancellationTokenSource? _cancellationTokenSource; - private bool _isConnected; - private HidStream? _hidStream; - private byte _lightBrightness = DefaultLightBrightness; - private LightAnimation _lightAnimation = LightAnimation.Solid; - private int _lightPulseSpeed = 12; - /// - public event EventHandler? IsConnectedChanged; + protected override int VendorId { get; } = 0x077d; /// - public event PropertyChangedEventHandler? PropertyChanged; + protected override int ProductId { get; } = 0x0410; + + private byte _lightBrightness = DefaultLightBrightness; + private LightAnimation _lightAnimation = LightAnimation.Solid; + private int _lightPulseSpeed = 12; + private DateTime? _mostRecentFeatureSetTime; /// public event EventHandler? InputReceived; /// - public SynchronizationContext EventSynchronizationContext { get; set; } = SynchronizationContext.Current ?? new SynchronizationContext(); - - /// - /// Constructs a new instance that communicates with a PowerMate device. - /// Upon construction, the new instance will immediately attempt to connect to any PowerMate connected to your computer. If none are connected, it will wait and connect when one is plugged - /// in. If a PowerMate disconnects, it will try to reconnect whenever one is plugged in again. - /// If multiple PowerMate devices are present, it will pick one arbitrarily and connect to it. - /// Once you have constructed an instance, you can subscribe to events to be notified when the PowerMate knob is rotated, pressed, or released. - /// You can also subscribe to or to be notified when it connects or disconnects from a PowerMate. - /// The light is controlled by the , , and properties. - /// Remember to dispose of this instance when you're done using it by calling , or with a statement or declaration. - /// - public PowerMateClient(): this(DeviceList.Local) { } - - internal PowerMateClient(DeviceList deviceList) { - _deviceList = deviceList; - _deviceList.Changed += onDeviceListChanged; - AttachToDevice(); - } + public PowerMateClient() { } /// - public bool IsConnected { - get => _isConnected; - private set { - if (value != _isConnected) { - _isConnected = value; - EventSynchronizationContext.Post(_ => { - IsConnectedChanged?.Invoke(this, value); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsConnected))); - }, null); - } - } - } + public PowerMateClient(DeviceList deviceList): base(deviceList) { } - private void onDeviceListChanged(object? sender, DeviceListChangedEventArgs e) { - AttachToDevice(); - } - - private void AttachToDevice() { - bool isNewStream = false; - lock (_hidStreamLock) { - if (_hidStream == null) { - HidDevice? newDevice = _deviceList?.GetHidDeviceOrNull(PowerMateVendorId, PowerMateProductId); - if (newDevice != null) { - _hidStream = newDevice.Open(); - isNewStream = true; - } - } - } - - if (_hidStream != null && isNewStream) { - _hidStream.Closed += ReattachToDevice; - _hidStream.ReadTimeout = Timeout.Infinite; - _cancellationTokenSource = new CancellationTokenSource(); - IsConnected = true; - LightAnimation = LightAnimation; //resend all pulsing and brightness values to device - - try { - Task.Factory.StartNew(HidReadLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); - } catch (TaskCanceledException) { } - } - } - - private async Task HidReadLoop() { - CancellationToken cancellationToken = _cancellationTokenSource!.Token; - - try { - byte[] readBuffer = new byte[7]; - while (!cancellationToken.IsCancellationRequested) { - int readBytes = await _hidStream!.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken); - if (readBuffer.Length == readBytes) { - PowerMateInput powerMateInput = new(readBuffer); - EventSynchronizationContext.Post(_ => { InputReceived?.Invoke(this, powerMateInput); }, null); - } - } - } catch (IOException) { - ReattachToDevice(); - } + /// + protected override void OnConnect() { + LightAnimation = LightAnimation; //resend all pulsing and brightness values to device } - private void ReattachToDevice(object? sender = null, EventArgs? e = null) { - bool disconnected = false; - lock (_hidStreamLock) { - if (_hidStream != null) { - _hidStream.Closed -= ReattachToDevice; - _hidStream.Close(); - _hidStream.Dispose(); - _hidStream = null; - disconnected = true; - } - } - - if (disconnected) { - IsConnected = false; + /// + protected override void OnHidRead(byte[] readBuffer) { + Console.WriteLine($"Read HID bytes {string.Join(" ", readBuffer.Select(b => $"{b:x2}"))}"); //FIXME development + 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; } - - try { - _cancellationTokenSource?.Cancel(); - } catch (AggregateException) { } - - AttachToDevice(); } /// @@ -193,12 +109,22 @@ public class PowerMateClient: IPowerMateClient { // 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)); - _hidStream?.SetFeature(featureData); + try { + DeviceStream?.SetFeature(featureData); + } 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); + } + } } + /// in the range [0, 24] + /// two big-endian bytes to send to the PowerMate to set its pulsing speed private static byte[] EncodePulseSpeed(int pulseSpeed) { byte[] encoded = BitConverter.GetBytes((ushort) (pulseSpeed switch { < 8 => Math.Min(0x00e, (7 - pulseSpeed) * 2), @@ -210,56 +136,12 @@ public class PowerMateClient: IPowerMateClient { (encoded[0], encoded[1]) = (encoded[1], encoded[0]); } + Console.WriteLine($"Encoded pulse speed {pulseSpeed} to {encoded[0]:x2} {encoded[1]:x2}"); //FIXME debugging return encoded; } private void TriggerPropertyChangedEvent([CallerMemberName] string propertyName = "") { - EventSynchronizationContext.Post(_ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)), null); - } - - /// - /// Clean up managed and, optionally, unmanaged resources. - /// When inheriting from , you should override this method, dispose of your managed resources if is , then - /// free your unmanaged resources regardless of the value of , and finally call this base implementation. - /// For more information, see Implement - /// the dispose pattern for a derived class. - /// - /// Should be when called from a finalizer, and when called from the method. In other words, it is - /// when deterministically called and when non-deterministically called. - protected virtual void Dispose(bool disposing) { - if (disposing) { - try { - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - } catch (AggregateException) { } - - lock (_hidStreamLock) { - if (_hidStream != null) { - _hidStream.Closed -= ReattachToDevice; - _hidStream.Close(); - _hidStream.Dispose(); - _hidStream = null; - } - } - - if (_deviceList != null) { - _deviceList.Changed -= onDeviceListChanged; - _deviceList = null; - } - } - } - - /// - /// Disconnect from any connected PowerMate device and clean up managed resources. - /// and events will not be emitted if a PowerMate is disconnected during disposal. - /// Subclasses of should override . - /// For more information, see Cleaning Up Unmanaged Resources and - /// Implementing a Dispose Method. - /// - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + EventSynchronizationContext.Post(_ => OnPropertyChanged(propertyName), null); } } \ No newline at end of file diff --git a/PowerMate/PowerMateInput.cs b/PowerMate/PowerMateInput.cs index c80f95c..ac40180 100644 --- a/PowerMate/PowerMateInput.cs +++ b/PowerMate/PowerMateInput.cs @@ -3,6 +3,16 @@ /// /// Event data describing how the PowerMate knob was rotated, pressed, or released. Emitted by . /// +/// +/// This is received as a 7-byte array. +/// 0: always 0? +/// 1: 1 if knob is pushed down, 0 otherwise +/// 2: knob rotation direction and distance, or 0 if not rotated +/// 3: always 0? +/// 4: led brightness, 0-255, reflects the pulsing brightness too +/// 5: 0x10 when not pulsing, 0x21 when pulsing at speed 9, 10, or 11, 0x11 pulse speed 8 +/// 6: 0 when not pulsing, 0x1 speed 8, 0x2 speed 9, 0x4 when pulsing with speed 10, 0x6 when pulsing with speed 11, 0x8 when pulsing with speed 12 +/// public readonly struct PowerMateInput { /// @@ -27,11 +37,16 @@ /// public readonly uint RotationDistance = 0; + internal readonly byte ActualLightBrightness; + internal readonly LightAnimation ActualLightAnimation; + internal readonly int ActualLightPulseSpeed; + /// /// Parse input from raw HID bytes. /// /// 7-item byte array from the HID update - public PowerMateInput(IReadOnlyList rawData): this(rawData[1] == 0x01, GetRotationDirection(rawData), GetRotationDistance(rawData)) { } + public PowerMateInput(IReadOnlyList rawData): this(rawData[1] == 0x01, DecodeRotationDirection(rawData), DecodeRotationDistance(rawData), rawData[4], DecodeLightAnimation(rawData), + DecodeLightPulseSpeed(rawData)) { } /// /// Construct a synthetic instance, useful for testing. @@ -45,18 +60,39 @@ /// events, each with this set to 1. As you rotate it faster, updates are batched and this number increases to 2 or more. The highest value I have seen is 8.This /// is always non-negative, regardless of the rotation direction; use to determine the direction.If the knob is pressed without being rotated, this /// will be 0 and will be . - public PowerMateInput(bool isPressed, RotationDirection rotationDirection, uint rotationDistance) { - IsPressed = isPressed; - RotationDirection = rotationDirection; - RotationDistance = rotationDistance; + public PowerMateInput(bool isPressed, RotationDirection rotationDirection, uint rotationDistance): this(isPressed, rotationDirection, rotationDistance, 0, LightAnimation.Solid, 0) { } + + private PowerMateInput(bool isPressed, RotationDirection rotationDirection, uint rotationDistance, byte actualLightBrightness, LightAnimation actualLightAnimation, int actualLightPulseSpeed) { + IsPressed = isPressed; + RotationDirection = rotationDirection; + RotationDistance = rotationDistance; + ActualLightBrightness = actualLightBrightness; + ActualLightAnimation = actualLightAnimation; + ActualLightPulseSpeed = actualLightPulseSpeed; } - private static uint GetRotationDistance(IReadOnlyList rawData) => (uint) Math.Abs((int) (sbyte) rawData[2]); + private static uint DecodeRotationDistance(IReadOnlyList rawData) => (uint) Math.Abs((int) (sbyte) rawData[2]); - private static RotationDirection GetRotationDirection(IReadOnlyList rawData) => rawData[2] switch { + private static RotationDirection DecodeRotationDirection(IReadOnlyList rawData) => rawData[2] switch { > 0 and < 128 => RotationDirection.Clockwise, > 128 => RotationDirection.Counterclockwise, - 0 or _ => RotationDirection.None + _ => RotationDirection.None + }; + + /// on unknown values + private static LightAnimation DecodeLightAnimation(IReadOnlyList rawData) => (rawData[5] & 0b111) switch { + 0 => LightAnimation.Solid, + 1 => LightAnimation.Pulsing, + 4 => LightAnimation.SolidWhileAwakeAndPulsingDuringComputerStandby, + _ => throw new ArgumentOutOfRangeException($"Unknown light animation read: 0x{rawData[5]:x2}") + }; + + /// on unknown values + private static int DecodeLightPulseSpeed(IReadOnlyList rawData) => (rawData[5] >> 4) switch { + 0 => 7 - rawData[6] / 2, + 1 => 7 + rawData[6], + 2 => 8 + rawData[6] / 2, + _ => throw new ArgumentOutOfRangeException($"Unknown light pulse speed read: 0x{rawData[5]:x2} 0x{rawData[6]:x2}") }; /// diff --git a/PowerMateVolume/PowerMateVolume.cs b/PowerMateVolume/PowerMateVolume.cs index 21d242f..13c59be 100644 --- a/PowerMateVolume/PowerMateVolume.cs +++ b/PowerMateVolume/PowerMateVolume.cs @@ -8,10 +8,10 @@ volumeIncrement = 0.01f; } -using IPowerMateClient powerMateClient = new PowerMateClient(); -using IVolumeChanger volumeChanger = new VolumeChanger { VolumeIncrement = volumeIncrement }; +using IPowerMateClient powerMate = new PowerMateClient(); +using IVolumeChanger volumeChanger = new VolumeChanger { VolumeIncrement = volumeIncrement }; -powerMateClient.LightBrightness = 0; +powerMate.LightBrightness = 0; CancellationTokenSource cancellationTokenSource = new(); Console.CancelKeyPress += (_, eventArgs) => { @@ -19,7 +19,7 @@ cancellationTokenSource.Cancel(); }; -powerMateClient.InputReceived += (_, powerMateEvent) => { +powerMate.InputReceived += (_, powerMateEvent) => { switch (powerMateEvent) { case { IsPressed: true, RotationDirection: RotationDirection.None }: volumeChanger.ToggleMute(); @@ -35,10 +35,11 @@ } }; +//FIXME remove this once the light settings are being reset based on HID reads, not the computer resuming from standby 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; + powerMate.LightAnimation = powerMate.LightAnimation; } }; diff --git a/PowerMateVolume/PowerMateVolume.csproj b/PowerMateVolume/PowerMateVolume.csproj index 8b4d347..7d1d3a8 100644 --- a/PowerMateVolume/PowerMateVolume.csproj +++ b/PowerMateVolume/PowerMateVolume.csproj @@ -12,13 +12,14 @@ Ben Hutchison © 2023 $(Authors) PowerMate Volume - 1.0.0 + 1.0.1 $(AssemblyTitle) $(Version) app.manifest false true powermate.ico + 1701;1702;NU1701 diff --git a/PowerMateVolume/VolumeChanger.cs b/PowerMateVolume/VolumeChanger.cs index 9a1d0ab..13be51a 100644 --- a/PowerMateVolume/VolumeChanger.cs +++ b/PowerMateVolume/VolumeChanger.cs @@ -32,8 +32,8 @@ public class VolumeChanger: IVolumeChanger { private readonly MMDeviceEnumerator _mmDeviceEnumerator = new(); - private MMDevice? _defaultAudioEndpoint; - private AudioEndpointVolume? _audioEndpointVolume; + private MMDevice? _audioOutputEndpoint; + private AudioEndpointVolume? _audioOutputVolume; private float _volumeIncrement = 0.01f; @@ -51,33 +51,40 @@ public class VolumeChanger: IVolumeChanger { public VolumeChanger() { _mmDeviceEnumerator.DefaultDeviceChanged += onDefaultDeviceChanged; - AttachToDefaultDevice(); + AttachToDevice(); } - private void AttachToDefaultDevice(MMDevice? newDefaultAudioEndpoint = null) { - DetachFromDefaultDevice(); - _defaultAudioEndpoint = newDefaultAudioEndpoint ?? _mmDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); - _audioEndpointVolume = AudioEndpointVolume.FromDevice(_defaultAudioEndpoint); + private void AttachToDevice(MMDevice? newAudioEndpoint = null) { + DetachFromCurrentDevice(); + try { + _audioOutputEndpoint = newAudioEndpoint ?? _mmDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); + } catch (CoreAudioAPIException) { + // program started when there were no audio output devices connected + // leave _defaultAudioEndpoint null and wait for onDefaultDeviceChanged to update it when a device is connected + return; + } + + _audioOutputVolume = AudioEndpointVolume.FromDevice(_audioOutputEndpoint); } - private void DetachFromDefaultDevice() { - _audioEndpointVolume?.Dispose(); - _audioEndpointVolume = null; - _defaultAudioEndpoint?.Dispose(); - _defaultAudioEndpoint = null; + private void DetachFromCurrentDevice() { + _audioOutputVolume?.Dispose(); + _audioOutputVolume = null; + _audioOutputEndpoint?.Dispose(); + _audioOutputEndpoint = 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; - Console.WriteLine($"Set volume to {newVolume:P2}"); + if (_audioOutputVolume is not null && increments != 0) { + float newVolume = Math.Max(0, Math.Min(1, _audioOutputVolume.MasterVolumeLevelScalar + VolumeIncrement * increments)); + _audioOutputVolume.MasterVolumeLevelScalar = newVolume; + // Console.WriteLine($"Set volume to {newVolume:P2}"); } } public void ToggleMute() { - if (_audioEndpointVolume is not null) { - _audioEndpointVolume.IsMuted ^= true; + if (_audioOutputVolume is not null) { + _audioOutputVolume.IsMuted ^= true; } } @@ -86,14 +93,14 @@ public class VolumeChanger: IVolumeChanger { eventArgs.TryGetDevice(out MMDevice? newDefaultAudioEndpoint); if (newDefaultAudioEndpoint is not null) { - AttachToDefaultDevice(newDefaultAudioEndpoint); + AttachToDevice(newDefaultAudioEndpoint); } } } protected virtual void Dispose(bool disposing) { if (disposing) { - DetachFromDefaultDevice(); + DetachFromCurrentDevice(); _mmDeviceEnumerator.DefaultDeviceChanged -= onDefaultDeviceChanged; _mmDeviceEnumerator.Dispose(); } diff --git a/PowerMateVolume/packages.lock.json b/PowerMateVolume/packages.lock.json index d4e7440..fb88f80 100644 --- a/PowerMateVolume/packages.lock.json +++ b/PowerMateVolume/packages.lock.json @@ -8,6 +8,14 @@ "resolved": "1.2.1.2", "contentHash": "DoQKUcdZLQawo3mOg8CvujJD9YkvTt6NtEu7S+OCo8+ySl8eCeUw4LFyL2E1OcAkPU918HZM6sUNcvIMcXWg6g==" }, + "HidClient": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "VtwKEL99yG3NHdTNbJQyyrx4KfzpQEmuIgWqd57f45pHf0qOHnzFjJ4lRKjmKIkNu28QXCVhHKnfV7ORyX+Z1w==", + "dependencies": { + "HidSharp": "2.1.0" + } + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -16,7 +24,7 @@ "powermate": { "type": "Project", "dependencies": { - "HidSharp": "[2.1.0, )" + "HidClient": "[1.0.1, )" } } }, diff --git a/Tests/.editorconfig b/Tests/.editorconfig new file mode 100644 index 0000000..7fc29bf --- /dev/null +++ b/Tests/.editorconfig @@ -0,0 +1,41 @@ +[*] + +# Exception Analyzers: Exception adjustments syntax error +# default = error +; dotnet_diagnostic.Ex0001.severity = none + +# Exception Analyzers: Exception adjustments syntax error: Symbol does not exist or identifier is invalid +# default = warning +; dotnet_diagnostic.Ex0002.severity = none + +# Exception Analyzers: Member may throw undocumented exception +# default = warning +dotnet_diagnostic.Ex0100.severity = none + +# Exception Analyzers: Member accessor may throw undocumented exception +# default = warning +dotnet_diagnostic.Ex0101.severity = none + +# Exception Analyzers: Implicit constructor may throw undocumented exception +# default = warning +dotnet_diagnostic.Ex0103.severity = none + +# Exception Analyzers: Member initializer may throw undocumented exception +# default = warning +dotnet_diagnostic.Ex0104.severity = none + +# Exception Analyzers: Delegate created from member may throw undocumented exception +# default = silent +; dotnet_diagnostic.Ex0120.severity = none + +# Exception Analyzers: Delegate created from anonymous function may throw undocumented exception +# default = silent +; dotnet_diagnostic.Ex0121.severity = none + +# Exception Analyzers: Member is documented as throwing exception not documented on member in base or interface type +# default = warning +dotnet_diagnostic.Ex0200.severity = none + +# Exception Analyzers: Member accessor is documented as throwing exception not documented on member in base or interface type +# default = warning +dotnet_diagnostic.Ex0201.severity = none \ No newline at end of file diff --git a/Tests/PowerMateClientInputTest.cs b/Tests/PowerMateClientInputTest.cs index 0419faf..fc86e9c 100644 --- a/Tests/PowerMateClientInputTest.cs +++ b/Tests/PowerMateClientInputTest.cs @@ -1,4 +1,5 @@ using HidSharp; +using PowerMate; namespace Tests; @@ -18,12 +19,13 @@ public class PowerMateClientInputTest { A.CallTo(_device).Where(call => call.Method.Name == "OpenDeviceAndRestrictAccess").WithReturnType().Returns(_stream); A.CallTo(() => _stream.ReadAsync(A._, An._, An._, A._)).ReturnsLazily(call => { - byte[] buffer = (byte[]) call.Arguments[0]!; - int offset = (int) call.Arguments[1]!; - int count = (int) call.Arguments[2]!; - byte[] fakeHidBytes = Convert.FromHexString("000100004F1000"); - Array.Copy(fakeHidBytes, 0, buffer, offset, count); - return Task.FromResult(Math.Min(count, fakeHidBytes.Length)); + byte[] buffer = (byte[]) call.Arguments[0]!; + int offset = (int) call.Arguments[1]!; + int count = (int) call.Arguments[2]!; + byte[] fakeHidBytes = Convert.FromHexString("000100004F1000"); + int occupiedCount = Math.Min(count, fakeHidBytes.Length); + Array.Copy(fakeHidBytes, 0, buffer, offset, occupiedCount); + return Task.FromResult(occupiedCount); }); } @@ -48,94 +50,4 @@ public class PowerMateClientInputTest { actualEvent!.Value.RotationDistance.Should().Be(0); } - [Fact] - public void LateAttach() { - A.CallTo(() => _deviceList.GetDevices(A._)).ReturnsNextFromSequence( - Enumerable.Empty(), - new[] { _device }); - - bool? connectedEventArg = null; - PowerMateClient client = new(_deviceList); - client.IsConnected.Should().BeFalse(); - PowerMateInput? actualEvent = null; - ManualResetEventSlim inputReceived = new(); - ManualResetEventSlim isConnectedChanged = new(); - client.InputReceived += (_, @event) => { - actualEvent = @event; - inputReceived.Set(); - }; - client.IsConnectedChanged += (_, b) => { - connectedEventArg = b; - isConnectedChanged.Set(); - }; - - _deviceList.RaiseChanged(); - - inputReceived.Wait(TestTimeout); - isConnectedChanged.Wait(TestTimeout); - - client.IsConnected.Should().BeTrue(); - connectedEventArg.HasValue.Should().BeTrue(); - connectedEventArg!.Value.Should().BeTrue(); - actualEvent.HasValue.Should().BeTrue(); - actualEvent!.Value.IsPressed.Should().BeTrue(); - actualEvent!.Value.RotationDirection.Should().Be(RotationDirection.None); - actualEvent!.Value.RotationDistance.Should().Be(0); - } - - [Fact] - public void Reconnect() { - A.CallTo(() => _stream.ReadAsync(A._, An._, An._, A._)) - .ThrowsAsync(new IOException("fake disconnected")).Once().Then - .ReturnsLazily(call => { - byte[] buffer = (byte[]) call.Arguments[0]!; - int offset = (int) call.Arguments[1]!; - int count = (int) call.Arguments[2]!; - byte[] fakeHidBytes = Convert.FromHexString("000100004F1000"); - Array.Copy(fakeHidBytes, 0, buffer, offset, count); - return Task.FromResult(Math.Min(count, fakeHidBytes.Length)); - }); - - ManualResetEventSlim eventArrived = new(); - PowerMateClient client = new(_deviceList); - PowerMateInput? actualEvent = null; - client.InputReceived += (_, @event) => { - actualEvent = @event; - eventArrived.Set(); - }; - - _deviceList.RaiseChanged(); - - eventArrived.Wait(TestTimeout); - actualEvent.HasValue.Should().BeTrue(); - actualEvent!.Value.IsPressed.Should().BeTrue(); - actualEvent!.Value.RotationDirection.Should().Be(RotationDirection.None); - actualEvent!.Value.RotationDistance.Should().Be(0); - } - - [Fact] - public void SynchronizationContext() { - ManualResetEventSlim eventArrived = new(); - SynchronizationContext synchronizationContext = A.Fake(); - A.CallTo(() => synchronizationContext.Post(A._, An._)).Invokes(() => eventArrived.Set()); - - PowerMateClient client = new(_deviceList) { EventSynchronizationContext = synchronizationContext }; - eventArrived.Wait(TestTimeout); - - A.CallTo(() => synchronizationContext.Post(A._, An._)).MustHaveHappenedOnceOrMore(); - } - - [Fact] - public void Dispose() { - PowerMateClient client = new(_deviceList); - client.Dispose(); - } - - [Fact] - public void DisposeIdempotent() { - PowerMateClient client = new(_deviceList); - client.Dispose(); - client.Dispose(); - } - } \ No newline at end of file diff --git a/Tests/PowerMateClientOutputTest.cs b/Tests/PowerMateClientOutputTest.cs index 13c9989..3a221b1 100644 --- a/Tests/PowerMateClientOutputTest.cs +++ b/Tests/PowerMateClientOutputTest.cs @@ -1,7 +1,10 @@ -using HidSharp; +using System.Diagnostics.CodeAnalysis; +using HidSharp; +using PowerMate; namespace Tests; +[SuppressMessage("Style", "IDE0017:Simplify object initialization", Justification = "explicit invocation ordering for tests")] public class PowerMateClientOutputTest { private readonly HidDevice _device = A.Fake(); diff --git a/Tests/PowerMateInputTest.cs b/Tests/PowerMateInputTest.cs index 3fa89f3..e96e9d5 100644 --- a/Tests/PowerMateInputTest.cs +++ b/Tests/PowerMateInputTest.cs @@ -1,3 +1,5 @@ +using PowerMate; + namespace Tests; public class PowerMateInputTest { @@ -96,4 +98,78 @@ public class PowerMateInputTest { new PowerMateInput(false, RotationDirection.None, 0).ToString().Should().Be("Not turning while not pressed"); } + [Theory] + [InlineData(0x01, 0x0e, 0)] + [InlineData(0x01, 0x0c, 1)] + [InlineData(0x01, 0x0a, 2)] + [InlineData(0x01, 0x08, 3)] + [InlineData(0x01, 0x06, 4)] + [InlineData(0x01, 0x04, 5)] + [InlineData(0x01, 0x02, 6)] + [InlineData(0x01, 0x01, 7)] + [InlineData(0x11, 0x01, 8)] + [InlineData(0x21, 0x02, 9)] + [InlineData(0x21, 0x04, 10)] + [InlineData(0x21, 0x06, 11)] + [InlineData(0x21, 0x08, 12)] + [InlineData(0x21, 0x0a, 13)] + [InlineData(0x21, 0x0c, 14)] + [InlineData(0x21, 0x0e, 15)] + [InlineData(0x21, 0x10, 16)] + [InlineData(0x21, 0x12, 17)] + [InlineData(0x21, 0x14, 18)] + [InlineData(0x21, 0x16, 19)] + [InlineData(0x21, 0x18, 20)] + [InlineData(0x21, 0x1a, 21)] + [InlineData(0x21, 0x1c, 22)] + [InlineData(0x21, 0x1e, 23)] + [InlineData(0x21, 0x20, 24)] + public void DecodeAlwaysPulsingSpeed(byte inputByte5, byte inputByte6, int expected) { + PowerMateInput actual = new(new byte[] { 0, 0, 1, 0, 73, inputByte5, inputByte6 }); + actual.ActualLightAnimation.Should().Be(LightAnimation.Pulsing); + actual.ActualLightPulseSpeed.Should().Be(expected); + } + + [Theory] + [InlineData(0x04, 0x0e, 0)] + [InlineData(0x04, 0x0c, 1)] + [InlineData(0x04, 0x0a, 2)] + [InlineData(0x04, 0x08, 3)] + [InlineData(0x04, 0x06, 4)] + [InlineData(0x04, 0x04, 5)] + [InlineData(0x04, 0x02, 6)] + [InlineData(0x04, 0x01, 7)] + [InlineData(0x14, 0x01, 8)] + [InlineData(0x24, 0x02, 9)] + [InlineData(0x24, 0x04, 10)] + [InlineData(0x24, 0x06, 11)] + [InlineData(0x24, 0x08, 12)] + [InlineData(0x24, 0x0a, 13)] + [InlineData(0x24, 0x0c, 14)] + [InlineData(0x24, 0x0e, 15)] + [InlineData(0x24, 0x10, 16)] + [InlineData(0x24, 0x12, 17)] + [InlineData(0x24, 0x14, 18)] + [InlineData(0x24, 0x16, 19)] + [InlineData(0x24, 0x18, 20)] + [InlineData(0x24, 0x1a, 21)] + [InlineData(0x24, 0x1c, 22)] + [InlineData(0x24, 0x1e, 23)] + [InlineData(0x24, 0x20, 24)] + public void DecodeStandbyPulsingSpeed(byte inputByte5, byte inputByte6, int expected) { + PowerMateInput actual = new(new byte[] { 0, 0, 1, 0, 73, inputByte5, inputByte6 }); + actual.ActualLightAnimation.Should().Be(LightAnimation.SolidWhileAwakeAndPulsingDuringComputerStandby); + actual.ActualLightPulseSpeed.Should().Be(expected); + } + + [Theory] + [InlineData(0x0, 0x20, 0)] + [InlineData(0x7f, 0x20, 127)] + [InlineData(0xff, 0x20, 255)] + public void DecodeBrightness(byte inputByte4, byte inputByte5, byte expected) { + PowerMateInput actual = new(new byte[] { 0, 0, 1, 0, inputByte4, inputByte5, 0x0a }); + actual.ActualLightAnimation.Should().Be(LightAnimation.Solid); + actual.ActualLightBrightness.Should().Be(expected); + } + } \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index b0ed22d..70c94ba 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -9,15 +9,15 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Tests/Usings.cs b/Tests/Usings.cs index 905afbc..b5bb7f1 100644 --- a/Tests/Usings.cs +++ b/Tests/Usings.cs @@ -1,4 +1,3 @@ global using Xunit; global using FluentAssertions; -global using FakeItEasy; -global using PowerMate; \ No newline at end of file +global using FakeItEasy; \ No newline at end of file