using System.Text.Json; using System.Text.RegularExpressions; namespace LogiBoardToolBox.Core.Devices; public sealed record MouseModel(string Id, string GHubModelId, string SlotPrefix, string DisplayName, string[] Aliases, ushort[] ProductIds, string ImageAsset, double ImageAspect, string Layout, string[] ButtonNames, int LightingZoneCount) { public bool IsKnown => Id != "Unknown"; public bool HasLighting => LightingZoneCount > 0; public bool HasSideView => Layout == "G502X"; public string ButtonName(int index) => index < ButtonNames.Length ? ButtonNames[index] : $"按键 {index + 1}"; public bool TryGetButtonIndex(string slot, out int index) { index = -1; if (Layout == "G502X" && slot is "scroll1" or "scroll2") { index = slot == "scroll1" ? 12 : 11; return true; } if (slot.Length > 1 && slot[0] == 'g' && int.TryParse(slot.AsSpan(1), out var number) && number >= 1 && number <= (Layout == "G502X" ? 11 : ButtonNames.Length)) { index = number - 1; return true; } return false; } } /// Identity, labels and G HUB namespaces only. Flash geometry always comes from 0x8100. public static class MouseCatalog { public static IReadOnlyList Models { get; } = Load(); public static MouseModel Unknown { get; } = new("Unknown", "", "", "Logitech Mouse", [], [], "", .53, "Generic", [], 0); private static IReadOnlyList Load() { using var stream = typeof(MouseCatalog).Assembly.GetManifestResourceStream("LogiBoardToolBox.Core.Devices.mice.json") ?? throw new InvalidOperationException("鼠标型号目录缺失。"); return JsonSerializer.Deserialize(stream) ?? throw new InvalidDataException("鼠标型号目录无效。"); } public static MouseModel? BySlotPrefix(string prefix) => Models.SingleOrDefault(m => m.SlotPrefix == prefix); public static MouseModel? ById(string id) => Models.SingleOrDefault(m => m.Id == id); public static MouseModel Resolve(IEnumerable transportProductIds, string name) { var ids = transportProductIds.ToHashSet(); var identified = Models.Where(m => m.ProductIds.Any(ids.Contains)).ToArray(); if (identified.Length == 1) return identified[0]; var normalized = Normalize(name); // Exact aliases prevent SUPERLIGHT 2 or G502 X from inheriting an older model's layout. var named = Models.Where(m => m.Aliases.Any(a => Normalize(a) == normalized)).ToArray(); return named.Length == 1 ? named[0] : Unknown; } private static string Normalize(string text) { var value = Regex.Replace(text.ToUpperInvariant(), @"\b(LOGITECH|GAMING|MOUSE)\b", " "); return Regex.Replace(value, @"\s+", " ").Trim(); } public static IReadOnlyList ReadTransportProductIds(ReadOnlySpan firmwareInfo) { if (firmwareInfo.Length < 13) return []; var ids = new List(); var offset = 7; foreach (var mask in new[] { 1, 2, 4, 8 }) { if ((firmwareInfo[6] & mask) == 0) continue; if (offset + 2 > firmwareInfo.Length) return []; ids.Add((ushort)((firmwareInfo[offset] << 8) | firmwareInfo[offset + 1])); offset += 2; } return ids; } }