Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
57 changes: 57 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
115 changes: 114 additions & 1 deletion YALCY/Integrations/OpenRGB/OpenRgbTalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Device> OffList = new();
public List<Device> LightPodList = new();
public List<Device> StrobeList = new();
public List<Device> FoggerList = new();
public Dictionary<int, Color[]> LightPodStates = new Dictionary<int, Color[]>();

// New zone-based dictionaries
public Dictionary<string, ZoneInfo> OffZones = new();
public Dictionary<string, ZoneInfo> LightPodZones = new();
public Dictionary<string, ZoneInfo> StrobeZones = new();
public Dictionary<string, ZoneInfo> FoggerZones = new();
public Dictionary<string, Color[]> LightPodZoneStates = new Dictionary<string, Color[]>();

private static string name = "YALCY";
private static bool autoConnect = false; // can't catch exceptions in constructor
private static int timeoutMs = 1000;
Expand Down Expand Up @@ -62,6 +78,26 @@ public async Task ConnectToOpenRgbServerAsync(string serverIp, ushort serverPort

try
{
// Clear all device lists to prevent duplicates on reconnection
OffList.Clear();
LightPodList.Clear();
StrobeList.Clear();
FoggerList.Clear();
LightPodStates.Clear();
OffZones.Clear();
LightPodZones.Clear();
StrobeZones.Clear();
FoggerZones.Clear();
LightPodZoneStates.Clear();
Comment on lines +86 to +91

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clearing of zone-based dictionaries (lines 87-91) is not thread-safe, while the legacy LightPodStates dictionary is protected with a lock in other parts of the code (e.g., lines 349, 380, 458). If multiple threads access these dictionaries simultaneously (e.g., during reconnection while effects are running), this could lead to race conditions. Consider adding lock statements around dictionary operations for LightPodZoneStates similar to how LightPodStates is protected, or ensure that reconnection properly cancels all ongoing tasks before clearing.

Suggested change
LightPodStates.Clear();
OffZones.Clear();
LightPodZones.Clear();
StrobeZones.Clear();
FoggerZones.Clear();
LightPodZoneStates.Clear();
lock (LightPodStates)
{
LightPodStates.Clear();
}
lock (OffZones)
{
OffZones.Clear();
}
lock (LightPodZones)
{
LightPodZones.Clear();
}
lock (StrobeZones)
{
StrobeZones.Clear();
}
lock (FoggerZones)
{
FoggerZones.Clear();
}
lock (LightPodZoneStates)
{
LightPodZoneStates.Clear();
}

Copilot uses AI. Check for mistakes.

// Clear visual lists in the UI synchronously
await Dispatcher.UIThread.InvokeAsync(() =>
{
MainWindowViewModel.ClearOpenRgbVisualList();
mainViewModel.DeviceCategories.Clear();
mainViewModel.DevicesWithZones.Clear();
});

client = new OpenRgbClient(serverIp, serverPort, name, autoConnect, timeoutMs, protocolVersionNumber);

// This really should be awaited since it waits for timeoutMs, however it isn't written that way.
Expand All @@ -88,6 +124,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;
Expand Down Expand Up @@ -144,10 +181,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)));
}
Comment on lines +185 to +199

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ClearDevicesWithZones() method is called via Dispatcher.UIThread.InvokeAsync (line 187), but the check for _mainViewModel != null happens outside the dispatcher call. If _mainViewModel becomes null between the check and the dispatcher execution (though unlikely), this could cause issues. For consistency with the pattern used for ClearOpenRgbVisualList (line 183), consider moving the null check inside the dispatcher callback or use the same pattern for both operations.

Suggested change
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)));
}
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_mainViewModel != null)
{
_mainViewModel.ClearDevicesWithZones();
}
});
foreach (var dev in devices)
{
OffList.Add(dev);
OpenRgbDeviceInserted?.Invoke(dev);
Dispatcher.UIThread.InvokeAsync(() =>
{
var vm = _mainViewModel;
if (vm != null)
{
vm.DevicesWithZones.Add(new DeviceWithZones(dev, vm));
}
});

Copilot uses AI. Check for mistakes.
}
Comment on lines 190 to 200

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When clearing OffList and updating the device list, only DevicesWithZones is being repopulated (line 197-198) but DeviceCategories is not. This creates an inconsistency - on initial connection (lines 122-127), both collections are populated, but on device list update, only DevicesWithZones is repopulated. This means the old DataGrid-based UI using DeviceCategories won't update when devices are hot-plugged. Either remove the line that adds to DeviceCategories during initial connection (since it appears deprecated) or also repopulate it here for consistency.

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -252,16 +301,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);
}
Expand All @@ -284,12 +345,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);
}
}
Expand All @@ -309,15 +372,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
Expand Down Expand Up @@ -345,6 +420,37 @@ private void UpdateLightPodColor(byte parameter, Color color, int areaOffset)
// Update LEDs for this device
client.UpdateLeds(device.Index, new Span<Color>(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];
Comment on lines +429 to +431

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At line 431, LightPodZoneStates[zoneKey] is accessed without checking if the key exists. If a zone is added to LightPodZones without properly initializing its state in LightPodZoneStates, this will throw a KeyNotFoundException. This is related to the initialization issue in DeviceZoneCategory constructor where zones are not added to any category list initially. Consider adding a check: if (!LightPodZoneStates.ContainsKey(zoneKey)) continue; or ensure proper initialization in the DeviceZoneCategory constructor.

Suggested change
var keysPerArea = Math.Max(1, zoneInfo.Zone.LedCount / numAreas);
var colors = LightPodZoneStates[zoneKey];
var keysPerArea = Math.Max(1, zoneInfo.Zone.LedCount / numAreas);
if (!LightPodZoneStates.TryGetValue(zoneKey, out var colors))
{
// Zone state not initialized; skip to avoid KeyNotFoundException
continue;
}

Copilot uses AI. Check for mistakes.

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);
}
Comment on lines +440 to +447

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
}
}

// Update LEDs for this zone
client.UpdateZoneLeds(zoneInfo.Device.Index, zoneInfo.ZoneIndex, new Span<Color>(colors));
}
}


Expand All @@ -354,6 +460,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)
{
Expand Down
2 changes: 1 addition & 1 deletion YALCY/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.LogToTrace()

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is trailing whitespace after .LogToTrace(). This should be removed for consistency with code formatting standards.

Suggested change
.LogToTrace()
.LogToTrace()

Copilot uses AI. Check for mistakes.
.UseReactiveUI();
}
Loading
Loading