From 8feefb2b4f319587380d9d3d51e7a702685e5ed9 Mon Sep 17 00:00:00 2001 From: Paulo Cardoso Date: Thu, 5 Feb 2026 15:47:16 +0000 Subject: [PATCH 1/2] Adds OpenRGB zone support and improves UI Introduces zone-based control for OpenRGB devices, allowing finer-grained control over lighting effects. Updates the UI to display devices and their zones, enabling users to select a function (Off, LightPod, Strobe, Fogger) for each zone. Retains backward compatibility with device-based settings. --- .vscode/launch.json | 32 +++ .vscode/tasks.json | 57 +++++ YALCY/Integrations/OpenRGB/OpenRgbTalker.cs | 95 ++++++++- YALCY/ViewModels/MainWindowViewModel.cs | 140 ++++++++++++- YALCY/ViewModels/OpenRgbSettings.cs | 13 +- YALCY/Views/Tabs/OpenRgbTabView.axaml | 217 +++++++++++++++----- 6 files changed, 501 insertions(+), 53 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..315c771 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (YALCY)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/YALCY/bin/Debug/net9.0/YALCY.dll", + "args": [], + "cwd": "${workspaceFolder}/YALCY", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Launch (YALCY.CLI)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build-cli", + "program": "${workspaceFolder}/YALCY.CLI/bin/Debug/net9.0/YALCY.CLI.dll", + "args": [], + "cwd": "${workspaceFolder}/YALCY.CLI", + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..b4d2f8f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,57 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/YALCY/YALCY.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "build-cli", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/YALCY.CLI/YALCY.CLI.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/YALCY/YALCY.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/YALCY/YALCY.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/YALCY/Integrations/OpenRGB/OpenRgbTalker.cs b/YALCY/Integrations/OpenRGB/OpenRgbTalker.cs index b076727..8442dbe 100644 --- a/YALCY/Integrations/OpenRGB/OpenRgbTalker.cs +++ b/YALCY/Integrations/OpenRGB/OpenRgbTalker.cs @@ -14,16 +14,32 @@ namespace YALCY.Integrations.OpenRGB; +public class ZoneInfo +{ + public Device Device { get; set; } = null!; + public int ZoneIndex { get; set; } + public Zone Zone { get; set; } = null!; +} + public class OpenRgbTalker { private CancellationTokenSource cts = new(); private Task updateTask = Task.CompletedTask; + // Legacy device-based lists (kept for backward compatibility) public List OffList = new(); public List LightPodList = new(); public List StrobeList = new(); public List FoggerList = new(); public Dictionary LightPodStates = new Dictionary(); + + // New zone-based dictionaries + public Dictionary OffZones = new(); + public Dictionary LightPodZones = new(); + public Dictionary StrobeZones = new(); + public Dictionary FoggerZones = new(); + public Dictionary LightPodZoneStates = new Dictionary(); + private static string name = "YALCY"; private static bool autoConnect = false; // can't catch exceptions in constructor private static int timeoutMs = 1000; @@ -88,6 +104,7 @@ public async Task ConnectToOpenRgbServerAsync(string serverIp, ushort serverPort OffList.Add(device); OpenRgbDeviceInserted?.Invoke(device); mainViewModel.DeviceCategories.Add(new DeviceCategory(device, 0, mainViewModel)); + mainViewModel.DevicesWithZones.Add(new DeviceWithZones(device, mainViewModel)); } UsbDeviceMonitor.OnStageKitCommand += OnStageKitEvent; @@ -144,10 +161,22 @@ private void OnDeviceLisUpdate(object o, EventArgs e) var devices = client.GetAllControllerData(); OffList.Clear(); Dispatcher.UIThread.InvokeAsync(MainWindowViewModel.ClearOpenRgbVisualList); + + if (_mainViewModel != null) + { + Dispatcher.UIThread.InvokeAsync(() => _mainViewModel.ClearDevicesWithZones()); + } + foreach (var dev in devices) { OffList.Add(dev); OpenRgbDeviceInserted?.Invoke(dev); + + if (_mainViewModel != null) + { + Dispatcher.UIThread.InvokeAsync(() => + _mainViewModel.DevicesWithZones.Add(new DeviceWithZones(dev, _mainViewModel))); + } } } @@ -252,16 +281,28 @@ private void StartStrobeEffect(int speed, float bpm) { while (!cts.Token.IsCancellationRequested) { + // Support both legacy device-based and new zone-based strobes foreach (var device in StrobeList) { ToggleDeviceLeds(device, true); } + + foreach (var zoneInfo in StrobeZones.Values) + { + ToggleZoneLeds(zoneInfo, true); + } await Task.Delay(interval); + foreach (var device in StrobeList) { ToggleDeviceLeds(device, false); } + + foreach (var zoneInfo in StrobeZones.Values) + { + ToggleZoneLeds(zoneInfo, false); + } await Task.Delay(interval); } @@ -284,12 +325,14 @@ private void StartBreathingEffect() for (byte brightness = 0; brightness <= 255; brightness += 5) { SetDeviceBrightness(brightness); + SetZoneBrightness(brightness); await Task.Delay(30); } for (byte brightness = 255; brightness >= 0; brightness -= 5) { SetDeviceBrightness(brightness); + SetZoneBrightness(brightness); await Task.Delay(30); } } @@ -309,15 +352,27 @@ private void SetDeviceBrightness(byte brightness) client.UpdateLeds(device.Index, colors); } } + + private void SetZoneBrightness(byte brightness) + { + foreach (var zoneInfo in FoggerZones.Values) + { + var color = new Color(brightness, brightness, brightness); + var colors = Enumerable.Repeat(color, (int)zoneInfo.Zone.LedCount).ToArray(); + client.UpdateZoneLeds(zoneInfo.Device.Index, zoneInfo.ZoneIndex, colors); + } + } private void UpdateLightPodColor(byte parameter, Color color, int areaOffset) { - if (LightPodList.Count == 0) + if (LightPodList.Count == 0 && LightPodZones.Count == 0) { return; } const int numAreas = 32; + + // Legacy device-based lightpods foreach (var device in LightPodList) { // Adjust the number of LEDs per area, ensuring at least one LED per area @@ -345,6 +400,37 @@ private void UpdateLightPodColor(byte parameter, Color color, int areaOffset) // Update LEDs for this device client.UpdateLeds(device.Index, new Span(colors)); } + + // New zone-based lightpods + foreach (var kvp in LightPodZones) + { + var zoneInfo = kvp.Value; + var zoneKey = kvp.Key; + + var keysPerArea = Math.Max(1, zoneInfo.Zone.LedCount / numAreas); + var colors = LightPodZoneStates[zoneKey]; + + for (int area = areaOffset; area < areaOffset + 8; area++) + { + for (int key = 0; key < keysPerArea; key++) + { + var ledIndex = area * keysPerArea + key; + if (ledIndex >= zoneInfo.Zone.LedCount) continue; + + if ((parameter & (1 << (area - areaOffset))) != 0) + { + colors[ledIndex] = color; + } + else + { + colors[ledIndex] = new Color(0, 0, 0); + } + } + } + + // Update LEDs for this zone + client.UpdateZoneLeds(zoneInfo.Device.Index, zoneInfo.ZoneIndex, new Span(colors)); + } } @@ -354,6 +440,13 @@ private void ToggleDeviceLeds(Device device, bool turnOn) var colors = Enumerable.Repeat(color, device.Leds.Length).ToArray(); client.UpdateLeds(device.Index, colors); } + + private void ToggleZoneLeds(ZoneInfo zoneInfo, bool turnOn) + { + var color = turnOn ? new Color(255, 255, 255) : new Color(0, 0, 0); + var colors = Enumerable.Repeat(color, (int)zoneInfo.Zone.LedCount).ToArray(); + client.UpdateZoneLeds(zoneInfo.Device.Index, zoneInfo.ZoneIndex, colors); + } private int CalculateDelay(int noteValue, float bpm) { diff --git a/YALCY/ViewModels/MainWindowViewModel.cs b/YALCY/ViewModels/MainWindowViewModel.cs index 8fb0268..04725f6 100644 --- a/YALCY/ViewModels/MainWindowViewModel.cs +++ b/YALCY/ViewModels/MainWindowViewModel.cs @@ -170,7 +170,7 @@ private void InitializeEnableSettings() SettingsManager.OpenRgbEnabledSettingIsEnabled, "YALCY is talking OpenRGB!", "YALCY is NOT talking to OpenRGB!", - async (isEnabled) => OpenRgbTalker.EnableOpenRgbTalker(isEnabled, OpenRgbServerIp, OpenRgbServerPort), + async (isEnabled) => await OpenRgbTalker.EnableOpenRgbTalker(isEnabled, OpenRgbServerIp ?? "127.0.0.1", OpenRgbServerPort), "Enable or disable output to a OpenRGB client" ); } @@ -184,7 +184,7 @@ private void InitializeCommands() { RegisterHueBridgeCommand = ReactiveCommand.CreateFromTask(() => HueTalker.RegisterHueBridgeAsync(HueBridgeIp)); ConnectToOpenRgbServerCommand = ReactiveCommand.CreateFromTask(() => - OpenRgbTalker.ConnectToOpenRgbServerAsync(OpenRgbServerIp, OpenRgbServerPort)); + OpenRgbTalker.ConnectToOpenRgbServerAsync(OpenRgbServerIp ?? "127.0.0.1", OpenRgbServerPort)); } private async void ShutdownRequested(object? sender, ShutdownRequestedEventArgs e) @@ -201,7 +201,7 @@ public async Task ShutdownAsync() SettingsManager.SaveSettings(this); // Turn off the OpenRGB talker - await OpenRgbTalker.EnableOpenRgbTalker(false, OpenRgbServerIp, OpenRgbServerPort, this); + await OpenRgbTalker.EnableOpenRgbTalker(false, OpenRgbServerIp ?? "127.0.0.1", OpenRgbServerPort, this); // Turn off the RB3E Talker Rb3ETalker.EnableRb3eTalker(false); @@ -278,6 +278,140 @@ public EnableSetting(string label, bool isEnabled, string onString, string offSt } } +public class DeviceWithZones : ReactiveObject +{ + public Device Device { get; set; } + public ObservableCollection Zones { get; set; } + + public DeviceWithZones(Device device, MainWindowViewModel? viewModel = null) + { + Device = device; + Zones = new ObservableCollection(); + + // Create a zone category for each zone in the device + for (int i = 0; i < device.Zones.Length; i++) + { + var zone = device.Zones[i]; + Zones.Add(new DeviceZoneCategory(device, zone, i, 0, viewModel)); + } + } +} + +public class DeviceZoneCategory : ReactiveObject, INotifyPropertyChanged +{ + public new event PropertyChangedEventHandler? PropertyChanged; + public Device Device { get; set; } + public Zone Zone { get; set; } + public int ZoneIndex { get; set; } + private int _category; + private MainWindowViewModel? _viewModel; + + public int Category + { + get => _category; + set + { + RemoveFromCategoryList(_category); + _category = value; + UpdateDeviceCategoryList(_category); + OnPropertyChanged(nameof(Category)); + } + } + + public DeviceZoneCategory(Device device, Zone zone, int zoneIndex, int initialCategory, MainWindowViewModel? viewModel = null) + { + Device = device; + Zone = zone; + ZoneIndex = zoneIndex; + _category = initialCategory; + _viewModel = viewModel; + } + + public void SetViewModel(MainWindowViewModel viewModel) + { + _viewModel = viewModel; + } + + private void RemoveFromCategoryList(int category) + { + if (_viewModel == null) return; + + var key = GetZoneKey(); + + switch (category) + { + case 0: + _viewModel.OpenRgbTalker.OffZones.Remove(key); + break; + + case 1: + _viewModel.OpenRgbTalker.LightPodZones.Remove(key); + lock (_viewModel.OpenRgbTalker.LightPodZoneStates) + { + _viewModel.OpenRgbTalker.LightPodZoneStates.Remove(key); + } + break; + + case 2: + _viewModel.OpenRgbTalker.StrobeZones.Remove(key); + break; + + case 3: + _viewModel.OpenRgbTalker.FoggerZones.Remove(key); + break; + } + } + + private void UpdateDeviceCategoryList(int category) + { + if (_viewModel == null) return; + + var key = GetZoneKey(); + var zoneInfo = new ZoneInfo { Device = Device, ZoneIndex = ZoneIndex, Zone = Zone }; + + switch (category) + { + case 0: + _viewModel.OpenRgbTalker.OffZones[key] = zoneInfo; + break; + + case 1: + _viewModel.OpenRgbTalker.LightPodZones[key] = zoneInfo; + lock (_viewModel.OpenRgbTalker.LightPodZoneStates) + { + if (!_viewModel.OpenRgbTalker.LightPodZoneStates.ContainsKey(key)) + { + // Initialize the light pod state for this zone + int ledCount = (int)Zone.LedCount; + _viewModel.OpenRgbTalker.LightPodZoneStates[key] = new Color[ledCount]; + } + } + break; + + case 2: + _viewModel.OpenRgbTalker.StrobeZones[key] = zoneInfo; + break; + + case 3: + _viewModel.OpenRgbTalker.FoggerZones[key] = zoneInfo; + break; + } + } + + private string GetZoneKey() + { + return $"{Device.Index}_{ZoneIndex}"; + } + + public string GetZoneKeyProperty => GetZoneKey(); + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} + +// Keep the old DeviceCategory for backward compatibility public class DeviceCategory : ReactiveObject, INotifyPropertyChanged { public new event PropertyChangedEventHandler? PropertyChanged; diff --git a/YALCY/ViewModels/OpenRgbSettings.cs b/YALCY/ViewModels/OpenRgbSettings.cs index c2849d1..0f112eb 100644 --- a/YALCY/ViewModels/OpenRgbSettings.cs +++ b/YALCY/ViewModels/OpenRgbSettings.cs @@ -10,10 +10,11 @@ public partial class MainWindowViewModel { private string? _openRgbServerIp; private ushort _openRgbServerPort; - private string _openRgbStatus; + private string _openRgbStatus = null!; private static ObservableCollection? OpenRgbDevices { get; set; } - public ICommand ConnectToOpenRgbServerCommand { set; get; } - public ObservableCollection DeviceCategories { get; set; } + public ICommand ConnectToOpenRgbServerCommand { set; get; } = null!; + public ObservableCollection DeviceCategories { get; set; } = null!; + public ObservableCollection DevicesWithZones { get; set; } = null!; public string OpenRgbStatus { @@ -55,6 +56,7 @@ private void InitializeOpenRgbCollections() { OpenRgbDevices = new ObservableCollection(); DeviceCategories = new ObservableCollection(); + DevicesWithZones = new ObservableCollection(); } public static void OnOpenRgbDeviceInserted(Device device) @@ -71,4 +73,9 @@ public static void ClearOpenRgbVisualList() { OpenRgbDevices?.Clear(); } + + public void ClearDevicesWithZones() + { + DevicesWithZones?.Clear(); + } } diff --git a/YALCY/Views/Tabs/OpenRgbTabView.axaml b/YALCY/Views/Tabs/OpenRgbTabView.axaml index 7c1462f..cc2bfef 100644 --- a/YALCY/Views/Tabs/OpenRgbTabView.axaml +++ b/YALCY/Views/Tabs/OpenRgbTabView.axaml @@ -6,17 +6,32 @@ x:Class="YALCY.Views.Tabs.OpenRgbTabView" x:DataType="vm:MainWindowViewModel" mc:Ignorable="d"> - + + + + + + + + + + - + IsChecked="{Binding OpenRgbEnabledSetting.IsEnabled, Mode=TwoWay}" + Margin="5" /> + - - - + + + + + + - + @@ -33,45 +48,155 @@