XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

LogiBoardToolBox

【WinUI】罗技鼠标板载驱动程序

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Buffers.Binary;

namespace LogiBoardToolBox.Core.HidPlusPlus;

public sealed record HeroCalibrationInfo(byte Width, ushort Length, ushort DpiX, ushort DpiY);
public sealed record AdvancedMouseState(byte SensorFlags, HeroCalibrationInfo? Calibration, byte? ModeStatus,
    bool LightforceSupported, bool SurfaceModeSupported, string CalibrationIssue = "", byte[]? StartupConfiguration = null)
{
    public bool CalibrationSupported => (SensorFlags & 4) != 0 && Calibration is not null;
    public bool Hybrid => (ModeStatus.GetValueOrDefault() & 1) != 0;
    public int SurfaceMode => (ModeStatus.GetValueOrDefault() >> 1) & 3;
    public bool? StartupEnabled => StartupConfiguration is { Length: >= 10 } data ? data[3] switch { 1 => true, 2 => false, _ => null } : null;
}

/// <summary>Device settings use 8090's masked status field; calibration uses 2202, not guessed flash offsets.</summary>
public sealed class AdvancedMouseController(HidPlusPlusClient client, string deviceName)
{
    private readonly SemaphoreSlim writes = new(1, 1);
    public AdvancedMouseState? State { get; private set; }
    public async Task<AdvancedMouseState> ReadAsync(CancellationToken ct = default)
    {
        byte flags = 0; HeroCalibrationInfo? calibration = null; byte? status = null; var issue = "";
        if (await client.GetFeatureAsync(0x2202, ct).ConfigureAwait(false) != 0)
        {
            var caps = await client.FeatureAsync(0x2202, 1, [0], ct).ConfigureAwait(false);
            if (caps.Length < 3 || caps[0] != 0) throw new InvalidDataException("传感器能力回复不匹配。");
            flags = caps[2];
            if ((flags & 4) != 0)
            {
                var onboard = await client.GetFeatureAsync(0x8100, ct).ConfigureAwait(false) != 0
                    && (await client.FeatureAsync(0x8100, 2, ct: ct).ConfigureAwait(false))[0] == 1;
                if (onboard) issue = "板载模式下不能进行 HERO 校准,请先选择软件配置。";
                else
                {
                try
                {
                    var data = await client.FeatureAsync(0x2202, 8, [0], ct).ConfigureAwait(false);
                    if (data.Length < 8 || data[0] != 0) throw new InvalidDataException("HERO 校准参数回复不完整。");
                    calibration = new(data[1], Be(data, 2), Be(data, 4), Be(data, 6));
                }
                catch (HidPlusPlusException error) { issue = "固件声明支持 HERO 校准,但读取校准参数失败:" + error.Message; }
                }
            }
        }
        if (await client.GetFeatureAsync(0x8090, ct).ConfigureAwait(false) != 0)
        {
            var data = await client.FeatureAsync(0x8090, 0, ct: ct).ConfigureAwait(false);
            if (data.Length < 2) throw new InvalidDataException("LIGHTFORCE 状态回复不完整。");
            status = data[1];
        }
        byte[]? startup = null;
        if (await client.GetFeatureAsync(0x8071, ct).ConfigureAwait(false) != 0)
        {
            var rgb = await client.FeatureAsync(0x8071, 0, [255, 255, 0], ct).ConfigureAwait(false);
            if (rgb.Length >= 5 && (rgb[4] & 1) != 0)
            {
                var data = await client.FeatureAsync(0x8071, 3, [0, 0, 1], ct).ConfigureAwait(false);
                if (data.Length >= 10 && data[1] == 0 && data[2] == 1) startup = data[..10];
            }
        }
        var hits = await client.GetFeatureAsync(0x1B0C, ct).ConfigureAwait(false) != 0;
        var supportedModel = deviceName.Contains("G502 X", StringComparison.OrdinalIgnoreCase) || deviceName.Contains("SUPERLIGHT 2", StringComparison.OrdinalIgnoreCase);
        return State = new(flags, calibration, status, status is not null && !hits && supportedModel,
            status is not null && (flags & 2) != 0, issue, startup);
    }
    public async Task SetStartupEffectAsync(bool enabled, CancellationToken ct = default)
    {
        await writes.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            var state = await ReadAsync(ct).ConfigureAwait(false);
            if (state.StartupConfiguration is not { } data) throw new NotSupportedException("该设备没有提供启动灯效设置。");
            if (state.StartupEnabled == enabled) return;
            var parameters = data.ToArray(); parameters[0] = 1; parameters[3] = (byte)(enabled ? 1 : 2);
            await client.FeatureAsync(0x8071, 3, parameters, ct).ConfigureAwait(false);
            if ((await ReadAsync(ct).ConfigureAwait(false)).StartupEnabled != enabled) throw new IOException("启动灯效设置读回不一致。");
        }
        finally { writes.Release(); }
    }
    public Task SetLightforceAsync(bool hybrid, CancellationToken ct = default) => SetModeAsync(hybrid ? 1 : 0, 1, ct);
    public Task SetSurfaceModeAsync(int mode, CancellationToken ct = default)
    {
        if (mode is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(mode));
        return SetModeAsync(mode << 1, 6, ct);
    }
    private async Task SetModeAsync(int value, byte mask, CancellationToken ct)
    {
        await writes.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            var state = State ?? await ReadAsync(ct).ConfigureAwait(false);
            if (mask == 1 ? !state.LightforceSupported : !state.SurfaceModeSupported) throw new NotSupportedException("当前设备不支持此设置。");
            var before = (await client.FeatureAsync(0x8090, 0, ct: ct).ConfigureAwait(false))[1];
            if ((before & mask) == (value & mask)) return;
            var after = (byte)((before & ~mask) | (value & mask));
            await client.FeatureAsync(0x8090, 1, [0, after, 0, mask], ct).ConfigureAwait(false);
            var read = await ReadAsync(ct).ConfigureAwait(false);
            if ((read.ModeStatus.GetValueOrDefault() & mask) != (after & mask)) throw new IOException("设备设置读回不一致,可能有其他软件同时控制鼠标。");
        }
        finally { writes.Release(); }
    }
    public async Task StartCalibrationAsync(byte axis = 0, CancellationToken ct = default, ushort expectedCount = 0)
    {
        var state = State ?? await ReadAsync(ct).ConfigureAwait(false);
        if (!state.CalibrationSupported) throw new NotSupportedException("该固件未提供 HERO 校准能力。");
        if (axis > 1) throw new ArgumentOutOfRangeException(nameof(axis));
        // Software calibration, bounded start/process timeouts. Completion is not inferred from this acknowledgement.
        await client.FeatureAsync(0x2202, 9, [0, axis, (byte)(expectedCount >> 8), (byte)expectedCount, 1, 60, 60, 60], ct).ConfigureAwait(false);
    }
    public async Task SetCalibrationAsync(short correction, byte axis = 0, CancellationToken ct = default)
    {
        if (axis > 1 || correction is < -1023 or > 1023 && correction != short.MinValue) throw new ArgumentOutOfRangeException(nameof(correction));
        if (State?.CalibrationSupported != true) throw new NotSupportedException("该固件未提供 HERO 校准能力。");
        await client.FeatureAsync(0x2202, 10, [0, axis, (byte)(correction >> 8), (byte)correction], ct).ConfigureAwait(false);
    }
    public static short CalculateCorrection(double referenceCounts, double targetCounts, int referenceDpi, int targetCalibrationDpi)
    {
        if (referenceCounts < 100 || targetCounts < 100 || referenceDpi <= 0 || targetCalibrationDpi <= 0) throw new ArgumentException("移动距离不足,不能生成校准。");
        var factor = targetCalibrationDpi * referenceCounts / (referenceDpi * targetCounts);
        var correction = (int)Math.Round((factor - 1) * 1024);
        if (correction is < -1023 or > 1023) throw new InvalidDataException("校准超出传感器范围,请将两只鼠标紧贴并同时移动后重试。");
        return (short)correction;
    }
    private static ushort Be(byte[] bytes, int offset) => BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(offset));
}