Skip to content

Commit 9617965

Browse files
committed
feat: add Logitech Steering Wheel SDK support for G923 RPM LEDs
Phase 0 now uses LogiPlayLeds() from LogitechSteeringWheelEnginesWrapper.dll which is the standard API used by racing games for RPM LED control. The DLL communicates with G Hub which handles the kernel driver layer. Key changes: - Search multiple paths for the steering wheel SDK DLL - Call LogiUpdate() multiple times after init (fixes LogiIsConnected false) - SetLEDsFromPercent passes RPM directly for smooth LED progression - LED SDK (keyboard/mouse) demoted to Phase 1 fallback
1 parent 493b353 commit 9617965

File tree

2 files changed

+227
-17
lines changed

2 files changed

+227
-17
lines changed

Common Files/LogitechLED.cpp

Lines changed: 218 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,21 @@ static bool IsKnownPID(USHORT pid)
4141
return false;
4242
}
4343

44-
// --- G Hub SDK function pointers ---
44+
// --- Steering Wheel SDK function pointers ---
45+
46+
typedef bool (__cdecl *LogiSteeringInit_t)(bool);
47+
typedef bool (__cdecl *LogiUpdate_t)();
48+
typedef bool (__cdecl *LogiIsConnected_t)(int);
49+
typedef bool (__cdecl *LogiPlayLeds_t)(int, float, float, float);
50+
typedef void (__cdecl *LogiSteeringShutdown_t)();
51+
52+
static LogiSteeringInit_t g_SteeringInit = NULL;
53+
static LogiUpdate_t g_SteeringUpdate = NULL;
54+
static LogiIsConnected_t g_IsConnected = NULL;
55+
static LogiPlayLeds_t g_PlayLeds = NULL;
56+
static LogiSteeringShutdown_t g_SteeringShutdown = NULL;
57+
58+
// --- G Hub LED SDK function pointers ---
4559

4660
typedef bool (*LogiLedInit_t)();
4761
typedef bool (*LogiLedInitWithName_t)(const char*);
@@ -73,6 +87,7 @@ LogitechLED::LogitechLED()
7387
, m_ledFunctionId(0)
7488
, m_deviceIdx(0xFF)
7589
, m_sdkDll(NULL)
90+
, m_steeringDll(NULL)
7691
{
7792
}
7893

@@ -83,9 +98,18 @@ LogitechLED::~LogitechLED()
8398

8499
void LogitechLED::Close()
85100
{
101+
if (m_method == METHOD_STEERING_SDK && g_SteeringShutdown)
102+
g_SteeringShutdown();
103+
86104
if (m_method == METHOD_SDK && g_LedShutdown)
87105
g_LedShutdown();
88106

107+
if (m_steeringDll)
108+
{
109+
FreeLibrary(m_steeringDll);
110+
m_steeringDll = NULL;
111+
}
112+
89113
if (m_sdkDll)
90114
{
91115
FreeLibrary(m_sdkDll);
@@ -100,6 +124,11 @@ void LogitechLED::Close()
100124

101125
m_available = false;
102126
m_method = METHOD_NONE;
127+
g_SteeringInit = NULL;
128+
g_SteeringUpdate = NULL;
129+
g_IsConnected = NULL;
130+
g_PlayLeds = NULL;
131+
g_SteeringShutdown = NULL;
103132
g_LedInit = NULL;
104133
g_LedSetLighting = NULL;
105134
g_LedShutdown = NULL;
@@ -110,11 +139,150 @@ bool LogitechLED::IsAvailable() const
110139
return m_available;
111140
}
112141

113-
// --- Phase 0: G Hub LED SDK ---
142+
// --- Phase 0: Logitech Steering Wheel SDK ---
143+
144+
bool LogitechLED::TrySteeringSDK()
145+
{
146+
Log("=== Phase 0: Steering Wheel SDK ===");
147+
148+
// Search paths for LogitechSteeringWheelEnginesWrapper.dll
149+
const char* searchPaths[] = {
150+
// 1. Same directory as game/DLL (LoadLibrary default search)
151+
"LogitechSteeringWheelEnginesWrapper.dll",
152+
// 2. Logitech Steering Wheel SDK install (x86)
153+
"C:\\Program Files\\Logitech\\Logitech Steering Wheel SDK\\Lib\\GameEnginesWrapper\\x86\\LogitechSteeringWheelEnginesWrapper.dll",
154+
"C:\\Program Files (x86)\\Logitech\\Logitech Steering Wheel SDK\\Lib\\GameEnginesWrapper\\x86\\LogitechSteeringWheelEnginesWrapper.dll",
155+
#ifdef _WIN64
156+
// 3. x64 variants
157+
"C:\\Program Files\\Logitech\\Logitech Steering Wheel SDK\\Lib\\GameEnginesWrapper\\x64\\LogitechSteeringWheelEnginesWrapper.dll",
158+
"C:\\Program Files (x86)\\Logitech\\Logitech Steering Wheel SDK\\Lib\\GameEnginesWrapper\\x64\\LogitechSteeringWheelEnginesWrapper.dll",
159+
#endif
160+
// 4. G Hub directory (unlikely but check)
161+
"C:\\Program Files\\LGHUB\\LogitechSteeringWheelEnginesWrapper.dll",
162+
"C:\\Program Files\\LGHUB\\sdks\\LogitechSteeringWheelEnginesWrapper.dll",
163+
NULL
164+
};
165+
166+
for (int i = 0; searchPaths[i]; i++)
167+
{
168+
m_steeringDll = LoadLibraryA(searchPaths[i]);
169+
if (m_steeringDll)
170+
{
171+
Log(" Loaded: %s", searchPaths[i]);
172+
break;
173+
}
174+
}
175+
176+
if (!m_steeringDll)
177+
{
178+
Log(" DLL not found. Searched:");
179+
for (int i = 0; searchPaths[i]; i++)
180+
Log(" - %s", searchPaths[i]);
181+
Log(" >>> Place LogitechSteeringWheelEnginesWrapper.dll next to the game .exe <<<");
182+
return false;
183+
}
184+
185+
// Get function pointers - try plain names first, then mangled
186+
const char* initNames[] = { "LogiSteeringInitialize", "_LogiSteeringInitialize", NULL };
187+
const char* updateNames[] = { "LogiUpdate", "_LogiUpdate", NULL };
188+
const char* connNames[] = { "LogiIsConnected", "_LogiIsConnected", NULL };
189+
const char* ledsNames[] = { "LogiPlayLeds", "_LogiPlayLeds", NULL };
190+
const char* shutNames[] = { "LogiSteeringShutdown", "_LogiSteeringShutdown", NULL };
191+
192+
for (int i = 0; initNames[i] && !g_SteeringInit; i++)
193+
g_SteeringInit = (LogiSteeringInit_t)GetProcAddress(m_steeringDll, initNames[i]);
194+
for (int i = 0; updateNames[i] && !g_SteeringUpdate; i++)
195+
g_SteeringUpdate = (LogiUpdate_t)GetProcAddress(m_steeringDll, updateNames[i]);
196+
for (int i = 0; connNames[i] && !g_IsConnected; i++)
197+
g_IsConnected = (LogiIsConnected_t)GetProcAddress(m_steeringDll, connNames[i]);
198+
for (int i = 0; ledsNames[i] && !g_PlayLeds; i++)
199+
g_PlayLeds = (LogiPlayLeds_t)GetProcAddress(m_steeringDll, ledsNames[i]);
200+
for (int i = 0; shutNames[i] && !g_SteeringShutdown; i++)
201+
g_SteeringShutdown = (LogiSteeringShutdown_t)GetProcAddress(m_steeringDll, shutNames[i]);
202+
203+
Log(" LogiSteeringInitialize: %s", g_SteeringInit ? "FOUND" : "NOT FOUND");
204+
Log(" LogiUpdate: %s", g_SteeringUpdate ? "FOUND" : "NOT FOUND");
205+
Log(" LogiIsConnected: %s", g_IsConnected ? "FOUND" : "NOT FOUND");
206+
Log(" LogiPlayLeds: %s", g_PlayLeds ? "FOUND" : "NOT FOUND");
207+
Log(" LogiSteeringShutdown: %s", g_SteeringShutdown ? "FOUND" : "NOT FOUND");
208+
209+
if (!g_SteeringInit || !g_SteeringUpdate || !g_IsConnected || !g_PlayLeds)
210+
{
211+
Log(" Required functions missing");
212+
FreeLibrary(m_steeringDll);
213+
m_steeringDll = NULL;
214+
return false;
215+
}
216+
217+
// Initialize - ignoreXInputControllers=false so we see Xbox wheels
218+
bool ok = g_SteeringInit(false);
219+
Log(" LogiSteeringInitialize(false) -> %s", ok ? "OK" : "FAIL");
220+
221+
if (!ok)
222+
{
223+
Log(" Init failed. Is G Hub running?");
224+
FreeLibrary(m_steeringDll);
225+
m_steeringDll = NULL;
226+
return false;
227+
}
228+
229+
// CRITICAL: Must call LogiUpdate() multiple times for G Hub to enumerate
230+
// devices. Without this, LogiIsConnected() always returns false.
231+
for (int retry = 0; retry < 10; retry++)
232+
{
233+
Sleep(200);
234+
g_SteeringUpdate();
235+
236+
if (g_IsConnected(0))
237+
{
238+
Log(" Wheel connected at index 0 (after %d updates)", retry + 1);
239+
break;
240+
}
241+
}
242+
243+
bool connected = g_IsConnected(0);
244+
Log(" LogiIsConnected(0) -> %s", connected ? "YES" : "NO");
245+
246+
if (!connected)
247+
{
248+
// Try index 1
249+
connected = g_IsConnected(1);
250+
Log(" LogiIsConnected(1) -> %s", connected ? "YES" : "NO");
251+
}
252+
253+
if (!connected)
254+
{
255+
Log(" No wheel detected by SDK. Trying LogiPlayLeds anyway...");
256+
}
257+
258+
// Test LEDs - all on (redline)
259+
g_SteeringUpdate();
260+
bool led = g_PlayLeds(0, 100.0f, 0.0f, 100.0f);
261+
Log(" LogiPlayLeds(0, 100, 0, 100) -> %s *** ALL LEDs ON ***", led ? "OK" : "FAIL");
262+
Sleep(1000);
263+
264+
// Test LEDs - half
265+
g_SteeringUpdate();
266+
led = g_PlayLeds(0, 50.0f, 0.0f, 100.0f);
267+
Log(" LogiPlayLeds(0, 50, 0, 100) -> %s *** HALF LEDs ***", led ? "OK" : "FAIL");
268+
Sleep(1000);
269+
270+
// Test LEDs - off
271+
g_SteeringUpdate();
272+
led = g_PlayLeds(0, 0.0f, 0.0f, 100.0f);
273+
Log(" LogiPlayLeds(0, 0, 0, 100) -> %s *** LEDs OFF ***", led ? "OK" : "FAIL");
274+
275+
m_method = METHOD_STEERING_SDK;
276+
m_available = true;
277+
Log("=== LED CONTROL ACTIVE (Steering Wheel SDK) ===");
278+
return true;
279+
}
280+
281+
// --- Phase 1: G Hub LED SDK ---
114282

115283
bool LogitechLED::TrySDK()
116284
{
117-
Log("=== Phase 0: G Hub LED SDK ===");
285+
Log("=== Phase 1: G Hub LED SDK ===");
118286

119287
#ifdef _WIN64
120288
const char* dllPath = "C:\\Program Files\\LGHUB\\sdks\\sdk_legacy_led_x64.dll";
@@ -452,18 +620,23 @@ bool LogitechLED::TryLegacy(HANDLE h, USHORT outLen)
452620

453621
bool LogitechLED::Init()
454622
{
455-
Log("=== LogitechLED Init v4 ===");
623+
Log("=== LogitechLED Init v5 ===");
456624
Log("");
457625

458626
if (m_available) return true;
459627

460-
// Phase 0: G Hub LED SDK (preferred - works with kernel drivers)
628+
// Phase 0: Steering Wheel SDK (LogiPlayLeds - standard for racing games)
629+
if (TrySteeringSDK())
630+
return true;
631+
632+
// Phase 1: G Hub LED SDK (keyboard/mouse - fallback)
633+
Log("");
461634
if (TrySDK())
462635
return true;
463636

464-
// Phase 1: HID++ enumeration (diagnostic only - kernel drivers block)
637+
// Phase 2: HID++ enumeration (diagnostic only - kernel drivers block)
465638
Log("");
466-
Log("=== Phase 1: HID++ enumeration ===");
639+
Log("=== Phase 2: HID++ enumeration ===");
467640

468641
HIDCandidate candidates[MAX_CANDIDATES];
469642
int numCandidates = 0;
@@ -485,9 +658,9 @@ bool LogitechLED::Init()
485658
CloseHandle(h);
486659
}
487660

488-
// Phase 2: Legacy fallback
661+
// Phase 3: Legacy fallback
489662
Log("");
490-
Log("=== Phase 2: Legacy ===");
663+
Log("=== Phase 3: Legacy ===");
491664

492665
for (int ci = 0; ci < numCandidates; ci++)
493666
{
@@ -520,19 +693,37 @@ bool LogitechLED::SetLEDs(BYTE ledMask)
520693
{
521694
if (!m_available) return false;
522695

696+
if (m_method == METHOD_STEERING_SDK && g_PlayLeds && g_SteeringUpdate)
697+
{
698+
// Map 5-bit LED mask to RPM percentage for LogiPlayLeds
699+
int numLeds = 0;
700+
for (int i = 0; i < 5; i++)
701+
if (ledMask & (1 << i)) numLeds++;
702+
703+
float rpm = numLeds * 20.0f; // 0-100
704+
g_SteeringUpdate();
705+
bool ok = g_PlayLeds(0, rpm, 0.0f, 100.0f);
706+
707+
g_setLedsCallCount++;
708+
if (g_setLedsCallCount <= 20 || !ok)
709+
Log("SetLEDs(0x%02X) [SteeringSDK rpm=%.0f] -> %s (#%d)",
710+
ledMask, rpm, ok ? "OK" : "FAIL", g_setLedsCallCount);
711+
712+
return ok;
713+
}
714+
523715
if (m_method == METHOD_SDK && g_LedSetLighting)
524716
{
525-
// Map 5-bit LED mask to brightness percentage
526717
int numLeds = 0;
527718
for (int i = 0; i < 5; i++)
528719
if (ledMask & (1 << i)) numLeds++;
529720

530-
int pct = numLeds * 20; // 0-100%
721+
int pct = numLeds * 20;
531722
bool ok = g_LedSetLighting(pct, pct, pct);
532723

533724
g_setLedsCallCount++;
534725
if (g_setLedsCallCount <= 20 || !ok)
535-
Log("SetLEDs(0x%02X) [SDK pct=%d] -> %s (#%d)",
726+
Log("SetLEDs(0x%02X) [LedSDK pct=%d] -> %s (#%d)",
536727
ledMask, pct, ok ? "OK" : "FAIL", g_setLedsCallCount);
537728

538729
return ok;
@@ -581,6 +772,21 @@ bool LogitechLED::SetLEDsFromPercent(double percent)
581772
if (percent < 0.0) percent = 0.0;
582773
if (percent > 1.0) percent = 1.0;
583774

775+
// For steering SDK, pass percentage directly as RPM for smooth LED progression
776+
if (m_method == METHOD_STEERING_SDK && g_PlayLeds && g_SteeringUpdate)
777+
{
778+
float rpm = (float)(percent * 100.0);
779+
g_SteeringUpdate();
780+
bool ok = g_PlayLeds(0, rpm, 0.0f, 100.0f);
781+
782+
g_setLedsCallCount++;
783+
if (g_setLedsCallCount <= 20 || !ok)
784+
Log("SetLEDsFromPercent(%.2f) [SteeringSDK rpm=%.0f] -> %s (#%d)",
785+
percent, rpm, ok ? "OK" : "FAIL", g_setLedsCallCount);
786+
787+
return ok;
788+
}
789+
584790
BYTE mask = 0;
585791
if (percent >= 0.2) mask |= 0x01;
586792
if (percent >= 0.4) mask |= 0x02;

Common Files/LogitechLED.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
#include <windows.h>
44

55
// Logitech G923/G29 RPM LED controller.
6-
// Phase 0: G Hub LED SDK (requires G Hub running + kernel drivers)
7-
// Phase 1: HID++ 2.0 feature discovery (G923 Xbox, requires G Hub closed)
8-
// Phase 2: Legacy [F8 12] (G29/G923-PS)
6+
// Phase 0: Steering Wheel SDK (LogiPlayLeds - works with G Hub)
7+
// Phase 1: G Hub LED SDK (keyboard/mouse backlighting fallback)
8+
// Phase 2: HID++ 2.0 feature discovery (diagnostic only)
9+
// Phase 3: Legacy [F8 12] (G29/G923-PS)
910

1011
class LogitechLED
1112
{
@@ -36,18 +37,21 @@ class LogitechLED
3637
bool m_available;
3738
USHORT m_reportLen;
3839

39-
enum LEDMethod { METHOD_NONE, METHOD_LEGACY, METHOD_HIDPP, METHOD_SDK };
40+
enum LEDMethod { METHOD_NONE, METHOD_LEGACY, METHOD_HIDPP, METHOD_SDK, METHOD_STEERING_SDK };
4041
LEDMethod m_method;
4142
BYTE m_ledFeatureIdx;
4243
BYTE m_ledFunctionId;
4344
BYTE m_deviceIdx;
4445

45-
// G Hub SDK
46+
// G Hub LED SDK
4647
HMODULE m_sdkDll;
48+
// Steering Wheel SDK
49+
HMODULE m_steeringDll;
4750

4851
void EnumerateCandidates(HIDCandidate* out, int* count);
4952
bool SendReport(HANDLE h, const BYTE* report, USHORT len);
5053
bool ReadReport(HANDLE h, BYTE* report, USHORT len, DWORD timeoutMs);
54+
bool TrySteeringSDK();
5155
bool TrySDK();
5256
bool TryHIDPPDiscovery(HANDLE h, USHORT outLen, USHORT inLen);
5357
bool TryLegacy(HANDLE h, USHORT outLen);

0 commit comments

Comments
 (0)