diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4091ba..f951d32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.1 with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x @@ -37,25 +37,25 @@ jobs: - name: Test run: dotnet test --no-build --verbosity normal --configuration Release - name: Upload a Build Artifact NET6 - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-NET6 path: bin/net6.0/RGB.NET.*.dll if-no-files-found: error - name: Upload a Build Artifact NET7 - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-NET7 path: bin/net7.0/RGB.NET.*.dll if-no-files-found: error - name: Upload a Build Artifact NET8 - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-NET8 path: bin/net8.0/RGB.NET.*.dll if-no-files-found: error - name: Upload Nuget Build Artifact - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-Nugets path: bin/*nupkg @@ -64,9 +64,3 @@ jobs: run: dotnet nuget push **\*.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json - name: Symbols Push run: dotnet nuget push **\*.snupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json - - name: Create Tag - uses: mathieudutour/github-tag-action@v6.1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - custom_tag: ${{ steps.versioning.outputs.version }} - tag_prefix: v diff --git a/.github/workflows/pr_verify.yml b/.github/workflows/pr_verify.yml index d556fa2..539b728 100644 --- a/.github/workflows/pr_verify.yml +++ b/.github/workflows/pr_verify.yml @@ -10,9 +10,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.1 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6e3df3..75b3d06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,11 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4.1.1 with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | 8.0.x @@ -28,7 +28,7 @@ jobs: uses: PaulHatch/semantic-version@v4.0.3 with: short_tags: false - format: "${major}.${minor}.${patch}" + format: "${major}.${minor}.${patch}-prerelease.${increment}" - name: Restore dependencies run: dotnet restore - name: Build @@ -36,25 +36,25 @@ jobs: - name: Test run: dotnet test --no-build --verbosity normal --configuration Release - name: Upload a Build Artifact NET6 - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-NET6 path: bin/net6.0/RGB.NET.*.dll if-no-files-found: error - name: Upload a Build Artifact NET7 - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-NET7 path: bin/net7.0/RGB.NET.*.dll if-no-files-found: error - name: Upload a Build Artifact NET8 - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-NET8 path: bin/net8.0/RGB.NET.*.dll if-no-files-found: error - name: Upload Nuget Build Artifact - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4.3.1 with: name: RGB.NET-Nugets path: bin/*nupkg diff --git a/RGB.NET.Devices.DMX/E131/E131UpdateQueue.cs b/RGB.NET.Devices.DMX/E131/E131UpdateQueue.cs index 600a1ea..2e513ec 100644 --- a/RGB.NET.Devices.DMX/E131/E131UpdateQueue.cs +++ b/RGB.NET.Devices.DMX/E131/E131UpdateQueue.cs @@ -99,5 +99,12 @@ public sealed class E131UpdateQueue : UpdateQueue return _sequenceNumber++; } + public override void Dispose() + { + base.Dispose(); + + _socket.Dispose(); + } + #endregion } \ No newline at end of file diff --git a/RGB.NET.Devices.Logitech/LogitechDeviceProvider.cs b/RGB.NET.Devices.Logitech/LogitechDeviceProvider.cs index 22411f8..da892ba 100644 --- a/RGB.NET.Devices.Logitech/LogitechDeviceProvider.cs +++ b/RGB.NET.Devices.Logitech/LogitechDeviceProvider.cs @@ -277,6 +277,9 @@ public class LogitechDeviceProvider : AbstractRGBDeviceProvider try { _LogitechGSDK.LogiLedRestoreLighting(); } catch { /* at least we tried */ } + try { _LogitechGSDK.LogiLedShutdown(); } + catch { /* at least we tried */ } + try { _LogitechGSDK.UnloadLogitechGSDK(); } catch { /* at least we tried */ } diff --git a/RGB.NET.Devices.Novation/Enum/NovationDevices.cs b/RGB.NET.Devices.Novation/Enum/NovationDevices.cs index 11ae6e0..5c8d19c 100644 --- a/RGB.NET.Devices.Novation/Enum/NovationDevices.cs +++ b/RGB.NET.Devices.Novation/Enum/NovationDevices.cs @@ -34,5 +34,10 @@ public enum NovationDevices [DeviceId("Launchpad Open")] [ColorCapability(NovationColorCapabilities.RGB)] [LedIdMapping(LedIdMappings.Pro)] - LaunchpadCustomFirmware + LaunchpadCustomFirmware, + + [DeviceId("LPMiniMK3")] + [ColorCapability(NovationColorCapabilities.RGB)] + [LedIdMapping(LedIdMappings.Current)] + LaunchpadMiniMK3, } \ No newline at end of file diff --git a/RGB.NET.Devices.Novation/NovationDeviceProvider.cs b/RGB.NET.Devices.Novation/NovationDeviceProvider.cs index 9a86f6f..e3c9b77 100644 --- a/RGB.NET.Devices.Novation/NovationDeviceProvider.cs +++ b/RGB.NET.Devices.Novation/NovationDeviceProvider.cs @@ -65,11 +65,10 @@ public sealed class NovationDeviceProvider : AbstractRGBDeviceProvider MidiOutCaps outCaps = OutputDeviceBase.GetDeviceCapabilities(index); if (outCaps.name == null) continue; - string deviceName = outCaps.name.ToUpperInvariant(); NovationDevices? deviceId = (NovationDevices?)Enum.GetValues(typeof(NovationDevices)) .Cast() .Where(x => x.GetDeviceId() != null) - .FirstOrDefault(x => deviceName.Contains(x.GetDeviceId()!.ToUpperInvariant())); + .FirstOrDefault(x => outCaps.name.Contains(x.GetDeviceId()!, StringComparison.InvariantCultureIgnoreCase)); if (deviceId == null) continue; diff --git a/RGB.NET.Devices.OpenRGB/Generic/LedMappings.cs b/RGB.NET.Devices.OpenRGB/Generic/LedMappings.cs index d6deb99..972bf12 100644 --- a/RGB.NET.Devices.OpenRGB/Generic/LedMappings.cs +++ b/RGB.NET.Devices.OpenRGB/Generic/LedMappings.cs @@ -141,5 +141,8 @@ internal static class LedMappings ["Key: G9"] = LedId.Keyboard_Programmable9, ["Lighting"] = LedId.Keyboard_Brightness, ["Game Mode"] = LedId.Keyboard_WinLock, + ["Num Lock Indicator"] = LedId.Keyboard_IndicatorNumLock, + ["Caps Lock Indicator"] = LedId.Keyboard_IndicatorCapsLock, + ["Scroll Lock Indicator"] = LedId.Keyboard_IndicatorScrollLock, }; } diff --git a/RGB.NET.Devices.Razer/Enum/RazerEndpointType.cs b/RGB.NET.Devices.Razer/Enum/RazerEndpointType.cs index 01bb060..c48bfcd 100644 --- a/RGB.NET.Devices.Razer/Enum/RazerEndpointType.cs +++ b/RGB.NET.Devices.Razer/Enum/RazerEndpointType.cs @@ -1,42 +1,55 @@ -namespace RGB.NET.Devices.Razer; +using System; + +namespace RGB.NET.Devices.Razer; /// /// Represents a type of Razer SDK endpoint /// +[Flags] public enum RazerEndpointType { + /// + /// No endpoint + /// + None = 0, + /// /// The keyboard endpoint /// - Keyboard, + Keyboard = 1 << 0, /// /// The laptop keyboard endpoint, shares the endpoint but has a different LED layout /// - LaptopKeyboard, + LaptopKeyboard = 1 << 1, /// /// The mouse endpoint /// - Mouse, + Mouse = 1 << 2, /// /// The headset endpoint /// - Headset, + Headset = 1 << 3, /// /// The mousepad endpoint /// - Mousepad, + Mousepad = 1 << 4, /// /// The keypad endpoint /// - Keypad, + Keypad = 1 << 5, /// /// The Chroma Link endpoint /// - ChromaLink, -} \ No newline at end of file + ChromaLink = 1 << 6, + + /// + /// All endpoints + /// + All = ~None +} diff --git a/RGB.NET.Devices.Razer/RazerDeviceProvider.cs b/RGB.NET.Devices.Razer/RazerDeviceProvider.cs index 28bd7da..047705d 100644 --- a/RGB.NET.Devices.Razer/RazerDeviceProvider.cs +++ b/RGB.NET.Devices.Razer/RazerDeviceProvider.cs @@ -50,7 +50,7 @@ public sealed class RazerDeviceProvider : AbstractRGBDeviceProvider /// /// Forces to load the devices represented by the emulator even if they aren't reported to exist. /// - public bool LoadEmulatorDevices { get; set; } = false; + public RazerEndpointType LoadEmulatorDevices { get; set; } = RazerEndpointType.None; private const int VENDOR_ID = 0x1532; @@ -216,6 +216,7 @@ public sealed class RazerDeviceProvider : AbstractRGBDeviceProvider { 0x008F, RGBDeviceType.Mouse, "Naga Pro", LedMappings.Mouse, RazerEndpointType.Mouse }, //this is via usb connection { 0x0090, RGBDeviceType.Mouse, "Naga Pro", LedMappings.Mouse, RazerEndpointType.Mouse }, //this is via bluetooth connection { 0x0091, RGBDeviceType.Mouse, "Viper 8khz", LedMappings.Mouse, RazerEndpointType.Mouse }, + { 0x0093, RGBDeviceType.Mouse, "Naga Classic", LedMappings.Mouse, RazerEndpointType.Mouse }, { 0x0094, RGBDeviceType.Mouse, "Orochi V2", LedMappings.Mouse, RazerEndpointType.Mouse }, { 0x0096, RGBDeviceType.Mouse, "Naga X", LedMappings.Mouse, RazerEndpointType.Mouse }, { 0x0099, RGBDeviceType.Mouse, "Basilisk v3", LedMappings.Mouse, RazerEndpointType.Mouse }, @@ -312,21 +313,26 @@ public sealed class RazerDeviceProvider : AbstractRGBDeviceProvider { DeviceDefinitions.LoadFilter = loadFilter; - IList devices = base.GetLoadedDevices(loadFilter).ToList(); + List devices = base.GetLoadedDevices(loadFilter).ToList(); - if (LoadEmulatorDevices) + if (LoadEmulatorDevices != RazerEndpointType.None) { - if (loadFilter.HasFlag(RGBDeviceType.Keyboard) && devices.All(d => d is not RazerKeyboardRGBDevice)) + if (loadFilter.HasFlag(RGBDeviceType.Keyboard) && (LoadEmulatorDevices.HasFlag(RazerEndpointType.Keyboard) || LoadEmulatorDevices.HasFlag(RazerEndpointType.LaptopKeyboard)) && devices.All(d => d is not RazerKeyboardRGBDevice)) devices.Add(new RazerKeyboardRGBDevice(new RazerKeyboardRGBDeviceInfo("Emulator Keyboard", RazerEndpointType.Keyboard), GetUpdateTrigger(), LedMappings.Keyboard)); - if (loadFilter.HasFlag(RGBDeviceType.Mouse) && devices.All(d => d is not RazerMouseRGBDevice)) + + if (loadFilter.HasFlag(RGBDeviceType.Mouse) && LoadEmulatorDevices.HasFlag(RazerEndpointType.Mouse) && devices.All(d => d is not RazerMouseRGBDevice)) devices.Add(new RazerMouseRGBDevice(new RazerRGBDeviceInfo(RGBDeviceType.Mouse, RazerEndpointType.Mouse, "Emulator Mouse"), GetUpdateTrigger(), LedMappings.Mouse)); - if (loadFilter.HasFlag(RGBDeviceType.Headset) && devices.All(d => d is not RazerHeadsetRGBDevice)) + + if (loadFilter.HasFlag(RGBDeviceType.Headset) && LoadEmulatorDevices.HasFlag(RazerEndpointType.Headset) && devices.All(d => d is not RazerHeadsetRGBDevice)) devices.Add(new RazerHeadsetRGBDevice(new RazerRGBDeviceInfo(RGBDeviceType.Headset, RazerEndpointType.Headset, "Emulator Headset"), GetUpdateTrigger())); - if (loadFilter.HasFlag(RGBDeviceType.Mousepad) && devices.All(d => d is not RazerMousepadRGBDevice)) + + if (loadFilter.HasFlag(RGBDeviceType.Mousepad) && LoadEmulatorDevices.HasFlag(RazerEndpointType.Mousepad) && devices.All(d => d is not RazerMousepadRGBDevice)) devices.Add(new RazerMousepadRGBDevice(new RazerRGBDeviceInfo(RGBDeviceType.Mousepad, RazerEndpointType.Mousepad, "Emulator Mousepad"), GetUpdateTrigger())); - if (loadFilter.HasFlag(RGBDeviceType.Keypad) && devices.All(d => d is not RazerMousepadRGBDevice)) + + if (loadFilter.HasFlag(RGBDeviceType.Keypad) && LoadEmulatorDevices.HasFlag(RazerEndpointType.Keypad) && devices.All(d => d is not RazerMousepadRGBDevice)) devices.Add(new RazerKeypadRGBDevice(new RazerRGBDeviceInfo(RGBDeviceType.Keypad, RazerEndpointType.Keypad, "Emulator Keypad"), GetUpdateTrigger())); - if (loadFilter.HasFlag(RGBDeviceType.Unknown) && devices.All(d => d is not RazerChromaLinkRGBDevice)) + + if (loadFilter.HasFlag(RGBDeviceType.Unknown) && LoadEmulatorDevices.HasFlag(RazerEndpointType.ChromaLink) && devices.All(d => d is not RazerChromaLinkRGBDevice)) devices.Add(new RazerChromaLinkRGBDevice(new RazerRGBDeviceInfo(RGBDeviceType.Unknown, RazerEndpointType.ChromaLink, "Emulator Chroma Link"), GetUpdateTrigger())); } diff --git a/RGB.NET.Devices.SteelSeries/Generic/LedMappings.cs b/RGB.NET.Devices.SteelSeries/Generic/LedMappings.cs index da55ac2..59310bd 100644 --- a/RGB.NET.Devices.SteelSeries/Generic/LedMappings.cs +++ b/RGB.NET.Devices.SteelSeries/Generic/LedMappings.cs @@ -315,6 +315,113 @@ public static class LedMappings { LedId.Keyboard_Custom1, SteelSeriesLedId.Power }, }; + /// + /// Gets the mapping for GE78HX keyboards. + /// + // ReSharper disable once InconsistentNaming + public static LedMapping KeyboardMSIGE78Mapping { get; } = new() + { + { LedId.Keyboard_Escape, SteelSeriesLedId.Escape }, + { LedId.Keyboard_F1, SteelSeriesLedId.F1 }, + { LedId.Keyboard_F2, SteelSeriesLedId.F2 }, + { LedId.Keyboard_F3, SteelSeriesLedId.F3 }, + { LedId.Keyboard_F4, SteelSeriesLedId.F4 }, + { LedId.Keyboard_F5, SteelSeriesLedId.F5 }, + { LedId.Keyboard_F6, SteelSeriesLedId.F6 }, + { LedId.Keyboard_F7, SteelSeriesLedId.F7 }, + { LedId.Keyboard_F8, SteelSeriesLedId.F8 }, + { LedId.Keyboard_F9, SteelSeriesLedId.F9 }, + { LedId.Keyboard_F10, SteelSeriesLedId.F10 }, + { LedId.Keyboard_F11, SteelSeriesLedId.F11 }, + { LedId.Keyboard_F12, SteelSeriesLedId.F12 }, + { LedId.Keyboard_PrintScreen, SteelSeriesLedId.PrintScreen }, + { LedId.Keyboard_Insert, SteelSeriesLedId.Insert }, + { LedId.Keyboard_Delete, SteelSeriesLedId.Delete }, + { LedId.Keyboard_GraveAccentAndTilde, SteelSeriesLedId.Backqoute }, + { LedId.Keyboard_1, SteelSeriesLedId.Keyboard1 }, + { LedId.Keyboard_2, SteelSeriesLedId.Keyboard2 }, + { LedId.Keyboard_3, SteelSeriesLedId.Keyboard3 }, + { LedId.Keyboard_4, SteelSeriesLedId.Keyboard4 }, + { LedId.Keyboard_5, SteelSeriesLedId.Keyboard5 }, + { LedId.Keyboard_6, SteelSeriesLedId.Keyboard6 }, + { LedId.Keyboard_7, SteelSeriesLedId.Keyboard7 }, + { LedId.Keyboard_8, SteelSeriesLedId.Keyboard8 }, + { LedId.Keyboard_9, SteelSeriesLedId.Keyboard9 }, + { LedId.Keyboard_0, SteelSeriesLedId.Keyboard0 }, + { LedId.Keyboard_MinusAndUnderscore, SteelSeriesLedId.Dash }, + { LedId.Keyboard_EqualsAndPlus, SteelSeriesLedId.Equal }, + { LedId.Keyboard_Backspace, SteelSeriesLedId.Backspace }, + { LedId.Keyboard_Tab, SteelSeriesLedId.Tab }, + { LedId.Keyboard_Q, SteelSeriesLedId.Q }, + { LedId.Keyboard_W, SteelSeriesLedId.W }, + { LedId.Keyboard_E, SteelSeriesLedId.E }, + { LedId.Keyboard_R, SteelSeriesLedId.R }, + { LedId.Keyboard_T, SteelSeriesLedId.T }, + { LedId.Keyboard_Y, SteelSeriesLedId.Y }, + { LedId.Keyboard_U, SteelSeriesLedId.U }, + { LedId.Keyboard_I, SteelSeriesLedId.I }, + { LedId.Keyboard_O, SteelSeriesLedId.O }, + { LedId.Keyboard_P, SteelSeriesLedId.P }, + { LedId.Keyboard_BracketLeft, SteelSeriesLedId.LBracket }, + { LedId.Keyboard_BracketRight, SteelSeriesLedId.RBracket }, + { LedId.Keyboard_Backslash, SteelSeriesLedId.Backslash }, + { LedId.Keyboard_CapsLock, SteelSeriesLedId.Caps }, + { LedId.Keyboard_A, SteelSeriesLedId.A }, + { LedId.Keyboard_S, SteelSeriesLedId.S }, + { LedId.Keyboard_D, SteelSeriesLedId.D }, + { LedId.Keyboard_F, SteelSeriesLedId.F }, + { LedId.Keyboard_G, SteelSeriesLedId.G }, + { LedId.Keyboard_H, SteelSeriesLedId.H }, + { LedId.Keyboard_J, SteelSeriesLedId.J }, + { LedId.Keyboard_K, SteelSeriesLedId.K }, + { LedId.Keyboard_L, SteelSeriesLedId.L }, + { LedId.Keyboard_SemicolonAndColon, SteelSeriesLedId.Semicolon }, + { LedId.Keyboard_ApostropheAndDoubleQuote, SteelSeriesLedId.Quote }, + { LedId.Keyboard_Enter, SteelSeriesLedId.Return }, + { LedId.Keyboard_LeftShift, SteelSeriesLedId.LShift }, + { LedId.Keyboard_Z, SteelSeriesLedId.Z }, + { LedId.Keyboard_X, SteelSeriesLedId.X }, + { LedId.Keyboard_C, SteelSeriesLedId.C }, + { LedId.Keyboard_V, SteelSeriesLedId.V }, + { LedId.Keyboard_B, SteelSeriesLedId.B }, + { LedId.Keyboard_N, SteelSeriesLedId.N }, + { LedId.Keyboard_M, SteelSeriesLedId.M }, + { LedId.Keyboard_CommaAndLessThan, SteelSeriesLedId.Comma }, + { LedId.Keyboard_PeriodAndBiggerThan, SteelSeriesLedId.Period }, + { LedId.Keyboard_SlashAndQuestionMark, SteelSeriesLedId.Slash }, + { LedId.Keyboard_RightShift, SteelSeriesLedId.RShift }, + { LedId.Keyboard_LeftCtrl, SteelSeriesLedId.LCtrl }, + { LedId.Keyboard_LeftGui, SteelSeriesLedId.LWin }, + { LedId.Keyboard_LeftAlt, SteelSeriesLedId.LAlt }, + { LedId.Keyboard_Space, SteelSeriesLedId.Spacebar }, + { LedId.Keyboard_RightAlt, SteelSeriesLedId.RAlt }, + { LedId.Keyboard_NonUsBackslash, SteelSeriesLedId.NonUsBackslash }, + { LedId.Keyboard_RightCtrl, SteelSeriesLedId.RCtrl }, + { LedId.Keyboard_Function, SteelSeriesLedId.Fn }, + { LedId.Keyboard_ArrowUp, SteelSeriesLedId.UpArrow }, + { LedId.Keyboard_ArrowLeft, SteelSeriesLedId.LeftArrow }, + { LedId.Keyboard_ArrowDown, SteelSeriesLedId.DownArrow }, + { LedId.Keyboard_ArrowRight, SteelSeriesLedId.RightArrow }, + { LedId.Keyboard_NumAsterisk, SteelSeriesLedId.KeypadTimes }, + { LedId.Keyboard_NumSlash, SteelSeriesLedId.KeypadDivide }, + { LedId.Logo, SteelSeriesLedId.Power }, + { LedId.Keyboard_NumPlus, SteelSeriesLedId.KeypadPlus }, + { LedId.Keyboard_NumMinus, SteelSeriesLedId.KeypadMinus }, + { LedId.Keyboard_NumLock, SteelSeriesLedId.KeypadNumLock }, + { LedId.Keyboard_Num7, SteelSeriesLedId.Keypad7 }, + { LedId.Keyboard_Num8, SteelSeriesLedId.Keypad8 }, + { LedId.Keyboard_Num9, SteelSeriesLedId.Keypad9 }, + { LedId.Keyboard_Num4, SteelSeriesLedId.Keypad4 }, + { LedId.Keyboard_Num5, SteelSeriesLedId.Keypad5 }, + { LedId.Keyboard_Num6, SteelSeriesLedId.Keypad6 }, + { LedId.Keyboard_Num1, SteelSeriesLedId.Keypad1 }, + { LedId.Keyboard_Num2, SteelSeriesLedId.Keypad2 }, + { LedId.Keyboard_Num3, SteelSeriesLedId.Keypad3 }, + { LedId.Keyboard_Num0, SteelSeriesLedId.Keypad0 }, + { LedId.Keyboard_NumPeriodAndDelete, SteelSeriesLedId.KeypadPeriod }, + { LedId.Keyboard_NumEnter, SteelSeriesLedId.KeypadEnter }, + }; + /// /// Gets the mapping for one-zone mice. /// diff --git a/RGB.NET.Devices.SteelSeries/SteelSeriesDeviceProvider.cs b/RGB.NET.Devices.SteelSeries/SteelSeriesDeviceProvider.cs index 0bf1958..4f5a399 100644 --- a/RGB.NET.Devices.SteelSeries/SteelSeriesDeviceProvider.cs +++ b/RGB.NET.Devices.SteelSeries/SteelSeriesDeviceProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using RGB.NET.Core; using RGB.NET.Devices.SteelSeries.API; @@ -76,7 +76,9 @@ public sealed class SteelSeriesDeviceProvider : AbstractRGBDeviceProvider { 0x1600, RGBDeviceType.Keyboard, "Apex M800", LedMappings.KeyboardMappingUk, SteelSeriesDeviceType.PerKey }, { 0x1610, RGBDeviceType.Keyboard, "Apex Pro", LedMappings.KeyboardMappingUk, SteelSeriesDeviceType.PerKey }, { 0x1614, RGBDeviceType.Keyboard, "Apex Pro TKL", LedMappings.KeyboardTklMappingUk, SteelSeriesDeviceType.PerKey }, - { 0x2036, RGBDeviceType.Keyboard, "MSI Notebook", LedMappings.KeyboardNotebookMappingUk, SteelSeriesDeviceType.PerKey }, + { 0x2036, RGBDeviceType.Keyboard, "MSI Notebook", LedMappings.KeyboardNotebookMappingUk, SteelSeriesDeviceType.PerKey }, + { 0x113A, RGBDeviceType.Keyboard, "MSI GE78HX", LedMappings.KeyboardMSIGE78Mapping, SteelSeriesDeviceType.PerKey }, + { 0x1122, RGBDeviceType.Keyboard, "MSI Notebook", LedMappings.KeyboardNotebookMappingUk, SteelSeriesDeviceType.PerKey }, //Headsets { 0x12AA, RGBDeviceType.Headset, "Arctis 5", LedMappings.HeadsetTwoZone, SteelSeriesDeviceType.TwoZone }, diff --git a/RGB.NET.Devices.WLED/API/WledAPI.cs b/RGB.NET.Devices.WLED/API/WledAPI.cs new file mode 100644 index 0000000..c67cda9 --- /dev/null +++ b/RGB.NET.Devices.WLED/API/WledAPI.cs @@ -0,0 +1,33 @@ +using System.Net.Http; +using System.Net.Http.Json; + +namespace RGB.NET.Devices.WLED; + +/// +/// Partial implementation of the WLED-JSON-API +/// +public static class WledAPI +{ + /// + /// Gets the data returned by the 'info' endpoint of the WLED-device. + /// + /// The address of the device to request data from. + /// The data returned by the WLED-device. + public static WledInfo? Info(string address) + { + if (string.IsNullOrEmpty(address)) return null; + + using HttpClient client = new(); + try + { + return client.Send(new HttpRequestMessage(HttpMethod.Get, $"http://{address}/json/info")) + .Content + .ReadFromJsonAsync() + .Result; + } + catch + { + return null; + } + } +} \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/API/WledInfo.cs b/RGB.NET.Devices.WLED/API/WledInfo.cs new file mode 100644 index 0000000..ca92ab8 --- /dev/null +++ b/RGB.NET.Devices.WLED/API/WledInfo.cs @@ -0,0 +1,156 @@ +// ReSharper disable InconsistentNaming +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace RGB.NET.Devices.WLED; + +public class WledInfo +{ + [JsonPropertyName("ver")] + public string Version { get; set; } = ""; + + [JsonPropertyName("vid")] + public uint BuildId { get; set; } + + [JsonPropertyName("leds")] + public LedsInfo Leds { get; set; } = new(); + + [JsonPropertyName("str")] + public bool SyncReceive { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("udpport")] + public ushort UDPPort { get; set; } + + [JsonPropertyName("live")] + public bool IsLive { get; set; } + + [JsonPropertyName("liveseg")] + public short MainSegment { get; set; } + + [JsonPropertyName("lm")] + public string RealtimeDataSource { get; set; } = ""; + + [JsonPropertyName("lip")] + public string RealtimeDataSourceIpAddress { get; set; } = ""; + + [JsonPropertyName("ws")] + public byte ConnectedWebSocketCount { get; set; } + + [JsonPropertyName("fxcount")] + public byte EffectCount { get; set; } + + [JsonPropertyName("palcount")] + public short PaletteCount { get; set; } + + [JsonPropertyName("maps")] + public List LedMaps { get; set; } = []; + + [JsonPropertyName("wifi")] + public Wifi WifiInfo { get; set; } = new(); + + [JsonPropertyName("fs")] + public Fs FilesystemInfo { get; set; } = new(); + + [JsonPropertyName("ndc")] + public short DiscoveredDeviceCount { get; set; } + + [JsonPropertyName("arch")] + public string PlatformName { get; set; } = ""; + + [JsonPropertyName("core")] + public string ArduinoCoreVersion { get; set; } = ""; + + [JsonPropertyName("freeheap")] + public uint FreeHeap { get; set; } + + [JsonPropertyName("uptime")] + public uint Uptime { get; set; } + + [JsonPropertyName("time")] + public string Time { get; set; } = ""; + + [JsonPropertyName("brand")] + public string Brand { get; set; } = ""; + + [JsonPropertyName("product")] + public string Product { get; set; } = ""; + + [JsonPropertyName("ip")] + public string IpAddress { get; set; } = ""; + + + public class LedsInfo + { + [JsonPropertyName("count")] + public ushort Count { get; set; } + + [JsonPropertyName("pwr")] + public ushort PowerUsage { get; set; } + + [JsonPropertyName("fps")] + public ushort RefreshRate { get; set; } + + [JsonPropertyName("maxpwr")] + public ushort MaxPower { get; set; } + + [JsonPropertyName("maxseg")] + public byte MaxSegments { get; set; } + + [JsonPropertyName("matrix")] + public MatrixDims? Matrix { get; set; } + + [JsonPropertyName("seglc")] + public List SegmentLightCapabilities { get; set; } = []; + + [JsonPropertyName("lc")] + public byte CombinedSegmentLightCapabilities { get; set; } + } + + public class Map + { + [JsonPropertyName("id")] + public byte Id { get; set; } + + [JsonPropertyName("n")] + public string Name { get; set; } = ""; + } + + public class Wifi + { + [JsonPropertyName("bssid")] + public string BSSID { get; set; } = ""; + + [JsonPropertyName("rssi")] + public long RSSI { get; set; } + + [JsonPropertyName("signal")] + public byte SignalQuality { get; set; } + + [JsonPropertyName("channel")] + public int Channel { get; set; } + } + + public class Fs + { + [JsonPropertyName("u")] + public uint UsedSpace { get; set; } + + [JsonPropertyName("t")] + public uint TotalSpace { get; set; } + + [JsonPropertyName("pmt")] + public ulong LastPresetsJsonModificationTime { get; set; } + } + + public class MatrixDims + { + [JsonPropertyName("w")] + public ushort Width { get; set; } + + [JsonPropertyName("h")] + public ushort Height { get; set; } + } +} diff --git a/RGB.NET.Devices.WLED/Generic/IWLedRGBDevice.cs b/RGB.NET.Devices.WLED/Generic/IWLedRGBDevice.cs new file mode 100644 index 0000000..f875820 --- /dev/null +++ b/RGB.NET.Devices.WLED/Generic/IWLedRGBDevice.cs @@ -0,0 +1,8 @@ +using RGB.NET.Core; + +namespace RGB.NET.Devices.WLED; + +/// +/// Represents a WLED-device. +/// +internal interface IWledRGBDevice : IRGBDevice; \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/Generic/IWledDeviceDefinition.cs b/RGB.NET.Devices.WLED/Generic/IWledDeviceDefinition.cs new file mode 100644 index 0000000..2ea5299 --- /dev/null +++ b/RGB.NET.Devices.WLED/Generic/IWledDeviceDefinition.cs @@ -0,0 +1,11 @@ +namespace RGB.NET.Devices.WLED; + +/// +/// Represents the data used to create a WLED-device. +/// +public interface IWledDeviceDefinition +{ + string Address { get; } + string? Manufacturer { get; } + string? Model { get; } +} \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/Generic/WLedDeviceUpdateQueue.cs b/RGB.NET.Devices.WLED/Generic/WLedDeviceUpdateQueue.cs new file mode 100644 index 0000000..d16834b --- /dev/null +++ b/RGB.NET.Devices.WLED/Generic/WLedDeviceUpdateQueue.cs @@ -0,0 +1,102 @@ +using System; +using System.Net.Sockets; +using RGB.NET.Core; + +namespace RGB.NET.Devices.WLED; + +/// +/// +/// Represents the update-queue performing updates for WLED devices. +/// +internal sealed class WledDeviceUpdateQueue : UpdateQueue +{ + #region Properties & Fields + + /// + /// The UDP-Connection used to send data. + /// + private readonly UdpClient _socket; + + /// + /// The buffer the UDP-data is stored in. + /// + private byte[] _buffer; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The update trigger used by this queue. + /// The device type used to identify the device. + public WledDeviceUpdateQueue(IDeviceUpdateTrigger updateTrigger, string address, int port, int ledCount) + : base(updateTrigger) + { + _buffer = new byte[2 + (ledCount * 3)]; + _buffer[0] = 2; // protocol: DRGB + _buffer[1] = 2; // Timeout 2s + + _socket = new UdpClient(); + _socket.Connect(address, port); + } + + #endregion + + #region Methods + + /// + protected override void OnUpdate(object? sender, CustomUpdateData customData) + { + try + { + if (customData[CustomUpdateDataIndex.HEARTBEAT] as bool? ?? false) + Update(Array.Empty<(object key, Color color)>()); + else + base.OnUpdate(sender, customData); + } + catch (Exception ex) + { + WledDeviceProvider.Instance.Throw(ex); + } + } + + /// + protected override bool Update(in ReadOnlySpan<(object key, Color color)> dataSet) + { + try + { + Span data = _buffer.AsSpan()[2..]; + foreach ((object key, Color color) in dataSet) + { + int ledIndex = (int)key; + int offset = (ledIndex * 3); + data[offset] = color.GetR(); + data[offset + 1] = color.GetG(); + data[offset + 2] = color.GetB(); + } + + _socket.Send(_buffer); + + return true; + } + catch (Exception ex) + { + WledDeviceProvider.Instance.Throw(ex); + } + + return false; + } + + /// + public override void Dispose() + { + base.Dispose(); + + _socket.Dispose(); + _buffer = []; + } + + #endregion +} \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/Generic/WLedRGBDevice.cs b/RGB.NET.Devices.WLED/Generic/WLedRGBDevice.cs new file mode 100644 index 0000000..8178732 --- /dev/null +++ b/RGB.NET.Devices.WLED/Generic/WLedRGBDevice.cs @@ -0,0 +1,39 @@ +using RGB.NET.Core; + +namespace RGB.NET.Devices.WLED; + +/// +/// +/// +/// Represents a WLED-device. +/// +public sealed class WledRGBDevice : AbstractRGBDevice, IWledRGBDevice, ILedStripe +{ + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + internal WledRGBDevice(WledRGBDeviceInfo info, string address, IDeviceUpdateTrigger updateTrigger) + : base(info, new WledDeviceUpdateQueue(updateTrigger, address, info.Info.UDPPort, info.Info.Leds.Count)) + { + InitializeLayout(); + + RequiresFlush = true; + } + + #endregion + + #region Methods + + private void InitializeLayout() + { + for (int i = 0; i < DeviceInfo.Info.Leds.Count; i++) + AddLed(LedId.LedStripe1 + i, new Point(i * 10, 0), new Size(10, 10)); + } + + /// + protected override object GetLedCustomData(LedId ledId) => ledId - LedId.LedStripe1; + + #endregion +} \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/Generic/WLedRGBDeviceInfo.cs b/RGB.NET.Devices.WLED/Generic/WLedRGBDeviceInfo.cs new file mode 100644 index 0000000..896ca94 --- /dev/null +++ b/RGB.NET.Devices.WLED/Generic/WLedRGBDeviceInfo.cs @@ -0,0 +1,52 @@ +using RGB.NET.Core; + +namespace RGB.NET.Devices.WLED; + +/// +/// +/// Represents a generic information for a WLED-. +/// +public sealed class WledRGBDeviceInfo : IRGBDeviceInfo +{ + #region Properties & Fields + + /// + public RGBDeviceType DeviceType => RGBDeviceType.LedStripe; + + /// + public string DeviceName { get; } + + /// + public string Manufacturer { get; } + + /// + public string Model { get; } + + /// + public object? LayoutMetadata { get; set; } + + /// + /// Gets some info returned by the WLED-device. + /// + public WledInfo Info { get; } + + #endregion + + #region Constructors + + /// + /// Internal constructor of managed . + /// + /// The manufacturer of the device. + /// The represented device model. + internal WledRGBDeviceInfo(WledInfo info, string? manufacturer, string? model) + { + this.Info = info; + this.Manufacturer = manufacturer ?? info.Brand; + this.Model = model ?? info.Name; + + DeviceName = DeviceHelper.CreateDeviceName(Manufacturer, Model); + } + + #endregion +} \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/Generic/WledDeviceDefinition.cs b/RGB.NET.Devices.WLED/Generic/WledDeviceDefinition.cs new file mode 100644 index 0000000..7d70f28 --- /dev/null +++ b/RGB.NET.Devices.WLED/Generic/WledDeviceDefinition.cs @@ -0,0 +1,18 @@ +namespace RGB.NET.Devices.WLED; + +/// +public class WledDeviceDefinition(string address, string? manufacturer = null, string? model = null) : IWledDeviceDefinition +{ + #region Properties & Fields + + /// + public string Address { get; } = address; + + /// + public string? Manufacturer { get; } = manufacturer; + + /// + public string? Model { get; } = model; + + #endregion +} \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/README.md b/RGB.NET.Devices.WLED/README.md new file mode 100644 index 0000000..8c17be2 --- /dev/null +++ b/RGB.NET.Devices.WLED/README.md @@ -0,0 +1,82 @@ +[RGB.NET](https://github.com/DarthAffe/RGB.NET) Device-Provider-Package for [WLED](https://kno.wled.ge/)-devices. + +## Usage +This provider does not load anything by default and requires additional configuration to work. + +```csharp +// Add as many WLED-devices as you like +WledDeviceProvider.Instance.AddDeviceDefinition(new WledDeviceDefinition("")); + +surface.Load(WledDeviceProvider.Instance); +``` + +You can also override the manufacturer and device model in the DeviceDefinition. + +# Required SDK +This provider does not require an additional SDK. + +UDP realtime needs to be enabled on the WLED device. + +# Automatic device discovery +Due to the requirement of an additional dependency and the requirement to be able to configure devices manually anywy automatic discovery is not part of the provider. + +Using the nuget `Tmds.MDns` you can use the following Helper to do this: + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Tmds.MDns; + +namespace RGB.NET.Devices.WLED; + +public static class WledDiscoveryHelper +{ + /// + /// Searches for WLED devices and returns a list of devices found. + /// + /// The time the discovery is waiting for responses. Choose this as short as possible as the method is blocking + /// The maximum amount of devices that are expected in the network. The discovery will stop early if the given amount of devices is found. + /// A list of addresses and device-infos. + public static IEnumerable<(string address, WledInfo info)> DiscoverDevices(int waitFor = 500, int maxDevices = -1) + { + List<(string address, WledInfo info)> devices = []; + using ManualResetEventSlim waitEvent = new(false); + + int devicesToDetect = maxDevices <= 0 ? int.MaxValue : maxDevices; + + ServiceBrowser mdns = new(); + mdns.ServiceAdded += OnServiceAdded; + mdns.StartBrowse("_http._tcp"); + + waitEvent.Wait(TimeSpan.FromMilliseconds(waitFor)); + + mdns.StopBrowse(); + mdns.ServiceAdded -= OnServiceAdded; + + return devices; + + void OnServiceAdded(object? sender, ServiceAnnouncementEventArgs args) + { + string address = args.Announcement.Addresses.FirstOrDefault()?.ToString() ?? string.Empty; + if (!string.IsNullOrWhiteSpace(address)) + { + WledInfo? info = null; + try + { + info = WledAPI.Info(address); + } + catch { } + + if (info != null) + { + devices.Add((address, info)); + if (--devicesToDetect <= 0) + waitEvent.Set(); + } + } + } + } +} +``` \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/RGB.NET.Devices.WLED.csproj b/RGB.NET.Devices.WLED/RGB.NET.Devices.WLED.csproj new file mode 100644 index 0000000..84ceafa --- /dev/null +++ b/RGB.NET.Devices.WLED/RGB.NET.Devices.WLED.csproj @@ -0,0 +1,62 @@ + + + net8.0;net7.0;net6.0 + latest + enable + + Darth Affe + Wyrez + en-US + en-US + RGB.NET.Devices.WLED + RGB.NET.Devices.WLED + RGB.NET.Devices.WLED + RGB.NET.Devices.WLED + RGB.NET.Devices.WLED + WLED-Device-Implementations of RGB.NET + WLED-Device-Implementations of RGB.NET, a C# (.NET) library for accessing various RGB-peripherals + Copyright © Darth Affe 2024 + Copyright © Darth Affe 2024 + icon.png + README.md + https://github.com/DarthAffe/RGB.NET + LGPL-2.1-only + Github + https://github.com/DarthAffe/RGB.NET + True + + + + 0.0.1 + 0.0.1 + 0.0.1 + + ..\bin\ + true + True + True + portable + snupkg + + + + TRACE;DEBUG + true + false + + + + true + $(NoWarn);CS1591;CS1572;CS1573 + RELEASE + + + + + + + + + + + \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/RGB.NET.Devices.WLED.csproj.DotSettings b/RGB.NET.Devices.WLED/RGB.NET.Devices.WLED.csproj.DotSettings new file mode 100644 index 0000000..e43e41f --- /dev/null +++ b/RGB.NET.Devices.WLED/RGB.NET.Devices.WLED.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/RGB.NET.Devices.WLED/WLedDeviceProvider.cs b/RGB.NET.Devices.WLED/WLedDeviceProvider.cs new file mode 100644 index 0000000..978565e --- /dev/null +++ b/RGB.NET.Devices.WLED/WLedDeviceProvider.cs @@ -0,0 +1,113 @@ +// ReSharper disable MemberCanBePrivate.Global + +using System; +using System.Collections.Generic; +using RGB.NET.Core; + +namespace RGB.NET.Devices.WLED; + +/// +/// +/// Represents a device provider responsible for WS2812B- and WS2811-Led-devices. +/// +// ReSharper disable once InconsistentNaming +// ReSharper disable once UnusedType.Global +public sealed class WledDeviceProvider : AbstractRGBDeviceProvider +{ + #region Constants + + private const int HEARTBEAT_TIMER = 250; + + #endregion + + #region Properties & Fields + + // ReSharper disable once InconsistentNaming + private static readonly object _lock = new(); + + private static WledDeviceProvider? _instance; + /// + /// Gets the singleton instance. + /// + public static WledDeviceProvider Instance + { + get + { + lock (_lock) + return _instance ?? new WledDeviceProvider(); + } + } + + /// + /// Gets a list of all defined device-definitions. + /// + public List DeviceDefinitions { get; } = []; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// Thrown if this constructor is called even if there is already an instance of this class. + public WledDeviceProvider() + { + lock (_lock) + { + if (_instance != null) throw new InvalidOperationException($"There can be only one instance of type {nameof(WledDeviceProvider)}"); + _instance = this; + } + } + + #endregion + + #region Methods + + /// + /// Adds the specified to this device-provider. + /// + /// The to add. + public void AddDeviceDefinition(IWledDeviceDefinition deviceDefinition) => DeviceDefinitions.Add(deviceDefinition); + + /// + protected override void InitializeSDK() { } + + /// + protected override IEnumerable LoadDevices() + { + int i = 0; + foreach (IWledDeviceDefinition deviceDefinition in DeviceDefinitions) + { + IDeviceUpdateTrigger updateTrigger = GetUpdateTrigger(i++); + WledRGBDevice? device = CreateWledDevice(deviceDefinition, updateTrigger); + if (device != null) + yield return device; + } + } + + private static WledRGBDevice? CreateWledDevice(IWledDeviceDefinition deviceDefinition, IDeviceUpdateTrigger updateTrigger) + { + WledInfo? wledInfo = WledAPI.Info(deviceDefinition.Address); + if (wledInfo == null) return null; + + return new WledRGBDevice(new WledRGBDeviceInfo(wledInfo, deviceDefinition.Manufacturer, deviceDefinition.Model), deviceDefinition.Address, updateTrigger); + } + + protected override IDeviceUpdateTrigger CreateUpdateTrigger(int id, double updateRateHardLimit) => new DeviceUpdateTrigger(updateRateHardLimit) { HeartbeatTimer = HEARTBEAT_TIMER }; + + /// + protected override void Dispose(bool disposing) + { + lock (_lock) + { + base.Dispose(disposing); + + DeviceDefinitions.Clear(); + + _instance = null; + } + } + + #endregion +} \ No newline at end of file diff --git a/RGB.NET.sln b/RGB.NET.sln index 257bf65..44b786e 100644 --- a/RGB.NET.sln +++ b/RGB.NET.sln @@ -49,6 +49,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGB.NET.Devices.OpenRGB", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGB.NET.Devices.Corsair_Legacy", "RGB.NET.Devices.Corsair_Legacy\RGB.NET.Devices.Corsair_Legacy.csproj", "{66AF690C-27A1-4097-AC53-57C0ED89E286}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGB.NET.Devices.WLED", "RGB.NET.Devices.WLED\RGB.NET.Devices.WLED.csproj", "{C533C5EA-66A8-4826-A814-80791E7593ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -139,6 +141,10 @@ Global {66AF690C-27A1-4097-AC53-57C0ED89E286}.Debug|Any CPU.Build.0 = Debug|Any CPU {66AF690C-27A1-4097-AC53-57C0ED89E286}.Release|Any CPU.ActiveCfg = Release|Any CPU {66AF690C-27A1-4097-AC53-57C0ED89E286}.Release|Any CPU.Build.0 = Release|Any CPU + {C533C5EA-66A8-4826-A814-80791E7593ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C533C5EA-66A8-4826-A814-80791E7593ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C533C5EA-66A8-4826-A814-80791E7593ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C533C5EA-66A8-4826-A814-80791E7593ED}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -161,6 +167,7 @@ Global {EDBA49D6-AE96-4E96-9E6A-30154D93BD5F} = {92D7C263-D4C9-4D26-93E2-93C1F9C2CD16} {F29A96E5-CDD0-469F-A871-A35A7519BC49} = {D13032C6-432E-4F43-8A32-071133C22B16} {66AF690C-27A1-4097-AC53-57C0ED89E286} = {D13032C6-432E-4F43-8A32-071133C22B16} + {C533C5EA-66A8-4826-A814-80791E7593ED} = {D13032C6-432E-4F43-8A32-071133C22B16} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F222AD4-1F9E-4AAB-9D69-D62372D4C1BA}