Skip to content

Commit

Permalink
✨ HID++: Implement DPI range parsing for the non-extended DPI feature…
Browse files Browse the repository at this point in the history
…. (2201)

The documentation for the format is actually present in the documentation for feature 2202 (Extended Adjustable DPI), which I guess is used for more complex mouses. (It would be necessary to implement X-Y DPI, which is not supported by the basic feature)
  • Loading branch information
hexawyz committed Aug 28, 2024
1 parent 4b9b91f commit 4260f65
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 84 deletions.
25 changes: 25 additions & 0 deletions DeviceTools.Logitech.HidPlusPlus/DpiRange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Diagnostics;

namespace DeviceTools.Logitech.HidPlusPlus;

[DebuggerDisplay("{Minimum,d} - {Maximum,d} ({Step,d})")]
public readonly struct DpiRange : IEquatable<DpiRange>
{
public readonly ushort Minimum;
public readonly ushort Maximum;
public readonly ushort Step;

public DpiRange(ushort value) : this(value, value, 0) { }

public DpiRange(ushort minimum, ushort maximum) : this(minimum, maximum, 1) { }

public DpiRange(ushort minimum, ushort maximum, ushort step)
=> (Minimum, Maximum, Step) = (minimum, maximum, step);

public override bool Equals(object? obj) => obj is DpiRange range && Equals(range);
public bool Equals(DpiRange other) => Minimum == other.Minimum && Maximum == other.Maximum && Step == other.Step;
public override int GetHashCode() => HashCode.Combine(Minimum, Maximum, Step);

public static bool operator ==(DpiRange left, DpiRange right) => left.Equals(right);
public static bool operator !=(DpiRange left, DpiRange right) => !(left == right);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct Response : IMessageResponseParameters, IShortMessageParameters
}
}

public static class GetSensorDpiList
public static class GetSensorDpiRanges
{
public const byte FunctionId = 1;

Expand All @@ -32,79 +32,37 @@ public struct Request : IMessageRequestParameters, IShortMessageParameters
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)]
public struct Response : IMessageResponseParameters, ILongMessageParameters
{
private byte _dpi00;
private byte _dpi01;
private byte _dpi10;
private byte _dpi11;
private byte _dpi20;
private byte _dpi21;
private byte _dpi30;
private byte _dpi31;
private byte _dpi40;
private byte _dpi41;
private byte _dpi50;
private byte _dpi51;
private byte _dpi60;
private byte _dpi61;
private byte _dpi70;
private byte _dpi71;
public byte SensorIndex;
private byte _item00;
private byte _item01;
private byte _item10;
private byte _item11;
private byte _item20;
private byte _item21;
private byte _item30;
private byte _item31;
private byte _item40;
private byte _item41;
private byte _item50;
private byte _item51;
private byte _item60;
private byte _item61;

public ushort this[int index]
{
get => Unsafe.ReadUnaligned<ushort>(ref Unsafe.As<ushort, byte>(ref GetSpan(ref this)[index]));
set => Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref GetSpan(ref this)[index]), value);
}

public ushort Dpi0
{
get => BigEndian.ReadUInt16(_dpi00);
set => BigEndian.Write(ref _dpi00, value);
}

public ushort Dpi1
{
get => BigEndian.ReadUInt16(_dpi10);
set => BigEndian.Write(ref _dpi10, value);
}

public ushort Dpi2
{
get => BigEndian.ReadUInt16(_dpi20);
set => BigEndian.Write(ref _dpi20, value);
}

public ushort Dpi3
{
get => BigEndian.ReadUInt16(_dpi30);
set => BigEndian.Write(ref _dpi30, value);
}

public ushort Dpi4
{
get => BigEndian.ReadUInt16(_dpi40);
set => BigEndian.Write(ref _dpi40, value);
}

public ushort Dpi5
{
get => BigEndian.ReadUInt16(_dpi50);
set => BigEndian.Write(ref _dpi50, value);
}

public ushort Dpi6
{
get => BigEndian.ReadUInt16(_dpi60);
set => BigEndian.Write(ref _dpi60, value);
}

public ushort Dpi7
{
get => BigEndian.ReadUInt16(_dpi70);
set => BigEndian.Write(ref _dpi70, value);
readonly get
{
if ((uint)index >= (uint)ItemCount) throw new ArgumentOutOfRangeException(nameof(index));
return BigEndian.ReadUInt16(in Unsafe.AddByteOffset(ref Unsafe.AsRef(in _item00), 2 * index));
}
set
{
if ((uint)index >= (uint)ItemCount) throw new ArgumentOutOfRangeException(nameof(index));
BigEndian.Write(ref Unsafe.AddByteOffset(ref _item00, 2 * index), value);
}
}

private static Span<ushort> GetSpan(ref Response response)
=> MemoryMarshal.CreateSpan(ref Unsafe.As<byte, ushort>(ref response._dpi00), 16);
public readonly int ItemCount => 7;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using DeviceTools.Logitech.HidPlusPlus.FeatureAccessProtocol.Features;

namespace DeviceTools.Logitech.HidPlusPlus;
Expand All @@ -11,11 +12,14 @@ private sealed class DpiFeatureHandler : FeatureHandler
public override HidPlusPlusFeature Feature => HidPlusPlusFeature.AdjustableDpi;

private byte _sensorCount;
private ushort _currentDpi;

public DpiFeatureHandler(FeatureAccess device, byte featureIndex) : base(device, featureIndex)
{
}

public ushort CurrentDpi => _currentDpi;

public override async Task InitializeAsync(int retryCount, CancellationToken cancellationToken)
{
byte sensorCount =
Expand All @@ -24,9 +28,8 @@ public override async Task InitializeAsync(int retryCount, CancellationToken can
.ConfigureAwait(false)
).SensorCount;

// Can't make any sense of these informations. Usefulness of this feature may depend on the model, but everything reported here seems relatively inaccurate.
// The DPI list seemed to include the max DPI supported by the mouse, which is good, but the current DPI info seems to only be valid in host mode.
// Other infos were pure rubbish ?
var dpiRange = new List<DpiRange>();

for (int i = 0; i < sensorCount; i++)
{
var dpiInformation = await Device.SendWithRetryAsync<AdjustableDpi.GetSensorDpi.Request, AdjustableDpi.GetSensorDpi.Response>
Expand All @@ -38,18 +41,87 @@ public override async Task InitializeAsync(int retryCount, CancellationToken can
cancellationToken
).ConfigureAwait(false);

var dpiList = await Device.SendWithRetryAsync<AdjustableDpi.GetSensorDpiList.Request, AdjustableDpi.GetSensorDpiList.Response>
(
FeatureIndex,
AdjustableDpi.GetSensorDpiList.FunctionId,
new() { SensorIndex = (byte)i },
retryCount,
cancellationToken
).ConfigureAwait(false);
var dpiRanges = await GetDpiRangesAsync((byte)i, cancellationToken).ConfigureAwait(false);

if (i == 0)
{
_currentDpi = dpiInformation.CurrentDpi;
}
}

_sensorCount = sensorCount;
}

public async ValueTask<ImmutableArray<DpiRange>> GetDpiRangesAsync(byte sensorIndex, CancellationToken cancellationToken)
{
var response = await Device.SendWithRetryAsync<AdjustableDpi.GetSensorDpiRanges.Request, AdjustableDpi.GetSensorDpiRanges.Response>
(
FeatureIndex,
AdjustableDpi.GetSensorDpiRanges.FunctionId,
new() { SensorIndex = (byte)sensorIndex },
HidPlusPlusTransportExtensions.DefaultRetryCount,
cancellationToken
).ConfigureAwait(false);

var ranges = ImmutableArray.CreateBuilder<DpiRange>(1);

const int StateMinimumValue = 0;
const int StateStepOrMinimumValue = 1;
const int StateMaximumValue = 2;

int state = StateMinimumValue;
ushort dpi = 0;
ushort step = 0;

for (int i = 0; i < response.ItemCount; i++)
{
ushort value = response[i];

switch (state)
{
case StateMinimumValue:
if (value == 0) goto Completed;
if (value >= 0xE000) goto InvalidDpiValue;
dpi = value;
state = StateStepOrMinimumValue;
break;
case StateStepOrMinimumValue:
if (value >= 0xE000)
{
step = (ushort)(value - 0xE000);
state = StateMaximumValue;
}
else
{
ranges.Add(new(dpi));
if (value == 0) goto Completed;
dpi = value;
}
break;
case StateMaximumValue:
if (value >= 0xE000 || value == 0) goto InvalidDpiValue;
ranges.Add(new(dpi, value, step));
state = StateMinimumValue;
break;
}
}

switch (state)
{
case StateMinimumValue: break;
case StateStepOrMinimumValue:
ranges.Add(new(dpi));
break;
case StateMaximumValue:
throw new InvalidDataException("List of DPI ranges was truncated.");
}

Completed:;
return ranges.DrainToImmutable();

InvalidDpiValue:;
throw new InvalidDataException("Invalid DPI value.");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ private void RegisterDefaultFeatureHandlers(HidPlusPlusFeatureCollection feature
public bool HasLockKeys => HasFeature(_lockKeyFeatureHandler);
public LockKeys LockKeys => GetFeature(in _lockKeyFeatureHandler).LockKeys;

public bool HasAdjustableDpi => HasFeature(_dpiState);
public ushort CurrentDpi => GetFeature(in _dpiState).CurrentDpi;

public bool HasAdjustableReportInterval => HasFeature(_reportRateState);
public ReportIntervals SupportedReportIntervals => GetFeature(in _reportRateState).SupportedReportIntervals;
public byte ReportInterval => GetFeature(in _reportRateState).ReportInterval;
Expand Down
2 changes: 1 addition & 1 deletion DeviceTools.Logitech.HidPlusPlus/ReportIntervals.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace DeviceTools.Logitech.HidPlusPlus;
namespace DeviceTools.Logitech.HidPlusPlus;

[Flags]
public enum ReportIntervals : byte
Expand Down
14 changes: 11 additions & 3 deletions Exo.Devices.Logitech/LogitechUniversalDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ private abstract class FeatureAccess :
IBatteryStateDeviceFeature,
IKeyboardBacklightFeature,
IKeyboardLockKeysFeature,
IMouseDpiFeature,
IMouseConfigurablePollingFrequencyFeature
{
public FeatureAccess(HidPlusPlusDevice.FeatureAccess device, ILogger<FeatureAccess> logger, DeviceConfigurationKey configurationKey, ushort versionNumber)
Expand All @@ -384,9 +385,13 @@ public override ValueTask DisposeAsync()
}

protected IDeviceFeatureSet<IMouseDeviceFeature> CreateMouseFeatures()
=> HasAdjustableReportInterval ?
FeatureSet.Create<IMouseDeviceFeature, FeatureAccess, IMouseConfigurablePollingFrequencyFeature>(this) :
FeatureSet.Empty<IMouseDeviceFeature>();
=> HasAdjustableDpi ?
HasAdjustableReportInterval ?
FeatureSet.Create<IMouseDeviceFeature, FeatureAccess, IMouseDpiFeature, IMouseConfigurablePollingFrequencyFeature>(this) :
FeatureSet.Create<IMouseDeviceFeature, FeatureAccess, IMouseDpiFeature>(this) :
HasAdjustableReportInterval ?
FeatureSet.Create<IMouseDeviceFeature, FeatureAccess, IMouseConfigurablePollingFrequencyFeature>(this) :
FeatureSet.Empty<IMouseDeviceFeature>();

private static BatteryState BuildBatteryState(BatteryPowerState batteryPowerState)
=> new()
Expand Down Expand Up @@ -483,6 +488,7 @@ private void OnBacklightStateChanged(HidPlusPlusDevice.FeatureAccess device, Dev
protected bool HasBattery => Device.HasBatteryInformation;
protected bool HasBacklight => Device.HasBacklight;
protected bool HasLockKeys => Device.HasLockKeys;
protected bool HasAdjustableDpi => Device.HasAdjustableDpi;
protected bool HasAdjustableReportInterval => Device.HasAdjustableReportInterval;

private event Action<Driver, BatteryState>? BatteryStateChanged;
Expand Down Expand Up @@ -513,6 +519,8 @@ event Action<Driver, LockKeys> IKeyboardLockKeysFeature.LockedKeysChanged

LockKeys IKeyboardLockKeysFeature.LockedKeys => (LockKeys)(byte)Device.LockKeys;

MouseDpiStatus IMouseDpiFeature.CurrentDpi => new() { Dpi = new(Device.CurrentDpi) };

ushort IMouseConfigurablePollingFrequencyFeature.PollingFrequency => (ushort)(1000 / Device.ReportInterval);

ImmutableArray<ushort> IMouseConfigurablePollingFrequencyFeature.SupportedPollingFrequencies
Expand Down

0 comments on commit 4260f65

Please sign in to comment.