@@ -10,19 +10,26 @@ namespace Fanzi.FanControl.Services;
1010
1111/// <summary>
1212/// OpenRGB-backed implementation of <see cref="IRgbService"/>.
13- /// Requires the OpenRGB application to be running with its SDK server enabled
14- /// (Settings → SDK Server, default port 6742).
13+ /// Connects to the OpenRGB SDK server (default port 6742), auto-switches all
14+ /// discovered devices to Direct/Custom mode so per-LED colours actually reach hardware,
15+ /// and caches the device layout to avoid per-frame SDK round-trips.
1516/// </summary>
1617public sealed class OpenRgbService : IRgbService
1718{
1819 private OpenRgbClient ? _client ;
19- private OpenRGB . NET . Device [ ] ? _deviceCache ; // zone/LED layout, refreshed on each device scan
20+ private OpenRGB . NET . Device [ ] ? _deviceCache ;
2021 private bool _disposed ;
2122 private readonly object _lock = new ( ) ;
2223
2324 public bool IsConnected { get ; private set ; }
2425 public string ServerVersion { get ; private set ; } = "Not connected" ;
2526
27+ /// <summary>
28+ /// Summary of the last mode-switch pass — surfaced in the UI so users can see
29+ /// which devices were switched to Direct mode and which (if any) failed.
30+ /// </summary>
31+ public string ModeSwitchStatus { get ; private set ; } = "" ;
32+
2633 // ── Connection ────────────────────────────────────────────────────────────
2734
2835 public Task < bool > TryConnectAsync (
@@ -42,6 +49,7 @@ public Task<bool> TryConnectAsync(
4249 _deviceCache = null ;
4350 IsConnected = false ;
4451 ServerVersion = "Not connected" ;
52+ ModeSwitchStatus = "" ;
4553
4654 var client = new OpenRgbClient (
4755 ip : host ,
@@ -75,6 +83,7 @@ public void Disconnect()
7583 _deviceCache = null ;
7684 IsConnected = false ;
7785 ServerVersion = "Disconnected" ;
86+ ModeSwitchStatus = "" ;
7887 }
7988 }
8089
@@ -93,7 +102,13 @@ public Task<IReadOnlyList<RgbDeviceInfo>> GetDevicesAsync(
93102 try
94103 {
95104 var devices = _client . GetAllControllerData ( ) ;
96- _deviceCache = devices ; // cache layout so per-zone sends don't round-trip each frame
105+ _deviceCache = devices ;
106+
107+ // Auto-switch every device to Direct/Custom mode so per-LED
108+ // colour updates actually reach the hardware. Without this,
109+ // devices stay in their built-in effect (Rainbow, Spectrum,
110+ // etc.) and silently ignore UpdateLeds() calls.
111+ SwitchAllDevicesToDirectMode ( devices ) ;
97112
98113 return devices
99114 . Select ( ( d , i ) =>
@@ -119,15 +134,87 @@ public Task<IReadOnlyList<RgbDeviceInfo>> GetDevicesAsync(
119134 } )
120135 . ToArray ( ) ;
121136 }
122- catch
137+ catch ( Exception ex )
123138 {
139+ ServerVersion = $ "Error: { ex . Message . Split ( '\n ' ) [ 0 ] } ";
124140 IsConnected = false ;
125141 return Array . Empty < RgbDeviceInfo > ( ) ;
126142 }
127143 }
128144 } , cancellationToken ) ;
129145 }
130146
147+ /// <summary>
148+ /// Iterates every discovered device and switches it to "Direct" or "Custom"
149+ /// mode — the mode that lets FANZI drive each LED individually.
150+ /// Devices already in Direct mode are left alone.
151+ /// </summary>
152+ private void SwitchAllDevicesToDirectMode ( OpenRGB . NET . Device [ ] devices )
153+ {
154+ int switched = 0 , already = 0 , failed = 0 ;
155+
156+ for ( int i = 0 ; i < devices . Length ; i ++ )
157+ {
158+ try
159+ {
160+ var dev = devices [ i ] ;
161+ var activeMode = dev . ActiveMode ;
162+
163+ // Already in Direct/Custom/Static-per-LED? Nothing to do.
164+ string modeName = activeMode . Name ?? "" ;
165+ bool isDirect = modeName . Contains ( "Direct" , StringComparison . OrdinalIgnoreCase )
166+ || modeName . Contains ( "Custom" , StringComparison . OrdinalIgnoreCase )
167+ || modeName . Contains ( "Static" , StringComparison . OrdinalIgnoreCase ) ;
168+
169+ if ( isDirect )
170+ {
171+ already ++ ;
172+ continue ;
173+ }
174+
175+ // Find the Direct or Custom mode index on this device.
176+ int directIndex = - 1 ;
177+ for ( int m = 0 ; m < dev . Modes . Length ; m ++ )
178+ {
179+ string name = dev . Modes [ m ] . Name ?? "" ;
180+ if ( name . Equals ( "Direct" , StringComparison . OrdinalIgnoreCase )
181+ || name . Equals ( "Custom" , StringComparison . OrdinalIgnoreCase ) )
182+ {
183+ directIndex = m ;
184+ break ;
185+ }
186+ }
187+
188+ // Prefer "Direct" — if not found, try "Custom" via SetCustomMode.
189+ if ( directIndex >= 0 )
190+ {
191+ _client ! . UpdateMode ( i , directIndex ) ;
192+ switched ++ ;
193+ }
194+ else
195+ {
196+ // SetCustomMode sends the RGBController::SetCustomMode() command
197+ // which on most devices activates the per-LED direct control path.
198+ _client ! . SetCustomMode ( i ) ;
199+ switched ++ ;
200+ }
201+ }
202+ catch
203+ {
204+ failed ++ ;
205+ }
206+ }
207+
208+ var parts = new List < string > ( ) ;
209+ if ( switched > 0 ) parts . Add ( $ "{ switched } switched to Direct") ;
210+ if ( already > 0 ) parts . Add ( $ "{ already } already Direct") ;
211+ if ( failed > 0 ) parts . Add ( $ "{ failed } failed") ;
212+
213+ ModeSwitchStatus = parts . Count > 0
214+ ? string . Join ( ", " , parts )
215+ : "No devices" ;
216+ }
217+
131218 // ── Colour setting ────────────────────────────────────────────────────────
132219
133220 public Task SetDeviceColorAsync (
@@ -143,7 +230,8 @@ public Task SetDeviceColorAsync(
143230 if ( _client is null || ! IsConnected ) return ;
144231 try
145232 {
146- var devices = _client . GetAllControllerData ( ) ;
233+ // Use cached device layout — avoids a full SDK round-trip every frame.
234+ var devices = _deviceCache ?? _client . GetAllControllerData ( ) ;
147235 if ( deviceIndex < 0 || deviceIndex >= devices . Length ) return ;
148236
149237 int count = devices [ deviceIndex ] . Leds . Length ;
@@ -188,11 +276,13 @@ public Task SetAllDevicesColorAsync(
188276 if ( _client is null || ! IsConnected ) return ;
189277 try
190278 {
191- var devices = _client . GetAllControllerData ( ) ;
279+ // Use cached device layout — critical for performance.
280+ var devices = _deviceCache ?? _client . GetAllControllerData ( ) ;
281+ var openColor = ToOpenRgb ( color ) ;
192282 for ( int i = 0 ; i < devices . Length ; i ++ )
193283 {
194284 var colors = Enumerable
195- . Repeat ( ToOpenRgb ( color ) , devices [ i ] . Leds . Length )
285+ . Repeat ( openColor , devices [ i ] . Leds . Length )
196286 . ToArray ( ) ;
197287 _client . UpdateLeds ( i , colors ) ;
198288 }
@@ -215,16 +305,12 @@ public Task SetDeviceZoneColorsAsync(
215305 if ( _client is null || ! IsConnected ) return ;
216306 try
217307 {
218- // Use the cached layout where possible so we don't hit the SDK every frame.
219308 var devices = _deviceCache ?? _client . GetAllControllerData ( ) ;
220309 if ( deviceIndex < 0 || deviceIndex >= devices . Length ) return ;
221310
222311 var dev = devices [ deviceIndex ] ;
223312 int ledTotal = dev . Leds . Length ;
224313
225- // Build the full per-LED buffer for the device, expanding each zone's
226- // colour across its LED span. Pre-fill black so unzoned/trailing LEDs
227- // are always initialised (safe whether Color is a struct or class).
228314 var black = ToOpenRgb ( RgbColor . Black ) ;
229315 var leds = new OpenRGB . NET . Color [ ledTotal ] ;
230316 for ( int k = 0 ; k < ledTotal ; k ++ ) leds [ k ] = black ;
0 commit comments