Skip to content

Commit

Permalink
Implement Legion Go per-controller-gyro support (#204)
Browse files Browse the repository at this point in the history
* Testing left gyro

* start implementing per-controller gyro support

* improve the automatic sensor switching logic

- when a controller is targeted, use its sensors if available
- when unplugged, if controller had sensors, pick next available
- if a controller is plugged and doesn't have sensors, pick next available

* implement LegionControllerGyroIndex on DevicePage

* fix type on gZ
  • Loading branch information
Valkirie authored and CasperH2O committed Apr 8, 2024
1 parent 53a91f2 commit 913e8e5
Show file tree
Hide file tree
Showing 12 changed files with 933 additions and 1,000 deletions.
545 changes: 274 additions & 271 deletions HandheldCompanion/App.config

Large diffs are not rendered by default.

101 changes: 78 additions & 23 deletions HandheldCompanion/Controllers/LegionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,28 +78,30 @@ public override bool IsWireless
}
}

// Define some constants for the touchpad logic
private bool IsPassthrough = false;
private uint LongPressTime = 1000; // The minimum time in milliseconds for a long press
private const int MaxDistance = 40; // Maximum distance tolerance between touch and untouch in pixels

// Variables to store the touchpad state
private bool touchpadTouched = false; // Whether the touchpad is currently touched
private Vector2 touchpadPosition = Vector2.Zero; // The current position of the touchpad
private Vector2 touchpadFirstPosition = Vector2.Zero; // The first position of the touchpad when touched
private long touchpadStartTime = 0; // The start time of the touchpad when touched
private long touchpadEndTime = 0; // The end time of the touchpad when untouched
private bool touchpadDoubleTapped = false; // Whether the touchpad has been double tapped
private bool touchpadLongTapped = false; // Whether the touchpad has been long tapped

private int GyroIndex = LegionGo.RightJoyconIndex;

private uint LongPressTime = 1000; // The minimum time in milliseconds for a long press
private const int MaxDistance = 40; // Maximum distance tolerance between touch and untouch in pixels
private bool touchpadTouched = false; // Whether the touchpad is currently touched
private Vector2 touchpadPosition = Vector2.Zero; // The current position of the touchpad
private Vector2 touchpadFirstPosition = Vector2.Zero; // The first position of the touchpad when touched
private long touchpadStartTime = 0; // The start time of the touchpad when touched
private long touchpadEndTime = 0; // The end time of the touchpad when untouched
private bool touchpadDoubleTapped = false; // Whether the touchpad has been double tapped
private bool touchpadLongTapped = false; // Whether the touchpad has been long tapped
private long lastTap = 0;
private Vector2 lastTapPosition = Vector2.Zero; // The current position of the touchpad
private Vector2 lastTapPosition = Vector2.Zero; // The current position of the touchpad



public LegionController() : base()
{ }

public LegionController(PnPDetails details) : base(details)
{
{
Capabilities |= ControllerCapabilities.MotionSensor;

// get long press time from system settings
SystemParametersInfo(0x006A, 0, ref LongPressTime, 0);

Expand Down Expand Up @@ -134,6 +136,7 @@ protected override void InitializeInputOutput()
protected override void UpdateSettings()
{
SetPassthrough(SettingsManager.GetBoolean("LegionControllerPassthrough"));
SetGyroIndex(SettingsManager.GetInt("LegionControllerGyroIndex"));
}

private void SettingsManager_SettingValueChanged(string name, object value)
Expand All @@ -143,6 +146,9 @@ private void SettingsManager_SettingValueChanged(string name, object value)
case "LegionControllerPassthrough":
SetPassthrough(Convert.ToBoolean(value));
break;
case "LegionControllerGyroIndex":
SetGyroIndex(Convert.ToInt32(value));
break;
}
}

Expand Down Expand Up @@ -259,15 +265,62 @@ public override void UpdateInputs(long ticks, bool commit)

// handle touchpad if passthrough is off
if (!IsPassthrough)
HandleTouchpadInput(touched, TouchpadX, TouchpadY);

HandleTouchpadInput(touched, TouchpadX, TouchpadY);

/*
Inputs.AxisState[AxisFlags.LeftStickX] += (short)InputUtils.MapRange(Data[29], byte.MinValue, byte.MaxValue, short.MinValue, short.MaxValue);
Inputs.AxisState[AxisFlags.LeftStickY] -= (short)InputUtils.MapRange(Data[30], byte.MinValue, byte.MaxValue, short.MinValue, short.MaxValue);
Inputs.AxisState[AxisFlags.RightStickX] += (short)InputUtils.MapRange(Data[31], byte.MinValue, byte.MaxValue, short.MinValue, short.MaxValue);
Inputs.AxisState[AxisFlags.RightStickY] -= (short)InputUtils.MapRange(Data[32], byte.MinValue, byte.MaxValue, short.MinValue, short.MaxValue);
*/
*/

short aX, aZ, aY = 0;
short gX, gZ, gY = 0;

switch (GyroIndex)
{
default:
case LegionGo.LeftJoyconIndex:
{
aX = (short)(Data[34] << 8 | Data[35]);
aZ = (short)(Data[36] << 8 | Data[37]);
aY = (short)(Data[38] << 8 | Data[39]);

Inputs.GyroState.Accelerometer.X = aX * (2.0f / short.MaxValue);
Inputs.GyroState.Accelerometer.Y = aY * (2.0f / short.MaxValue);
Inputs.GyroState.Accelerometer.Z = aZ * -(2.0f / short.MaxValue);

gX = (short)(Data[40] << 8 | Data[41]);
gZ = (short)(Data[42] << 8 | Data[43]);
gY = (short)(Data[44] << 8 | Data[45]);

Inputs.GyroState.Gyroscope.X = gX * -(2048.0f / short.MaxValue);
Inputs.GyroState.Gyroscope.Y = gY * (2048.0f / short.MaxValue);
Inputs.GyroState.Gyroscope.Z = gZ * -(2048.0f / short.MaxValue);
}
break;

case LegionGo.RightJoyconIndex:
{
aX = (short)(Data[49] << 8 | Data[50]);
aZ = (short)(Data[47] << 8 | Data[48]);
aY = (short)(Data[51] << 8 | Data[52]);

Inputs.GyroState.Accelerometer.X = aX * (2.0f / short.MaxValue);
Inputs.GyroState.Accelerometer.Y = aY * (2.0f / short.MaxValue);
Inputs.GyroState.Accelerometer.Z = aZ * (2.0f / short.MaxValue);

gX = (short)(Data[55] << 8 | Data[56]);
gZ = (short)(Data[53] << 8 | Data[54]);
gY = (short)(Data[57] << 8 | Data[58]);

Inputs.GyroState.Gyroscope.X = gX * -(2048.0f / short.MaxValue);
Inputs.GyroState.Gyroscope.Y = gY * (2048.0f / short.MaxValue);
Inputs.GyroState.Gyroscope.Z = gZ * (2048.0f / short.MaxValue);
}
break;
}

base.UpdateInputs(ticks);
}
Expand All @@ -284,10 +337,8 @@ private async void dataThreadLoop(object? obj)
if (report is not null)
{
// check if packet is safe
if (READY_STATES.Contains(report.Data[STATUS_IDX]))
{
Data = report.Data;
}
if (READY_STATES.Contains(report.Data[STATUS_IDX]))
Data = report.Data;
}
}
}
Expand Down Expand Up @@ -420,11 +471,15 @@ public void HandleTouchpadInput(bool touched, ushort x, ushort y)
}
}

internal void SetPassthrough(bool enabled)
public void SetPassthrough(bool enabled)
{
SetTouchPadStatus(enabled ? 1 : 0);
IsPassthrough = enabled;
}

public void SetGyroIndex(int idx)
{
GyroIndex = idx + LegionGo.LeftJoyconIndex;
}
}
}
17 changes: 15 additions & 2 deletions HandheldCompanion/Devices/Lenovo/LegionGo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ private Task SetCPUPowerLimit(CapabilityID capabilityID, int limit) =>

public override bool IsOpen => hidDevices.ContainsKey(INPUT_HID_ID) && hidDevices[INPUT_HID_ID].IsOpen;

public static int LeftJoyconIndex = 3;
public static int RightJoyconIndex = 4;
public const int LeftJoyconIndex = 3;
public const int RightJoyconIndex = 4;

public LegionGo()
{
Expand Down Expand Up @@ -208,6 +208,18 @@ public LegionGo()

Init();

// make sure both left and right gyros are enabled
SetLeftGyroStatus(1);
SetRightGyroStatus(1);

// make sure both left and right gyros are reporting values
SetGyroModeStatus(2, 1, 1);
SetGyroModeStatus(2, 2, 2);

// make sure both left and right gyros are reporting raw values
SetGyroSensorDataOnorOff(LeftJoyconIndex, 0x02);
SetGyroSensorDataOnorOff(RightJoyconIndex, 0x02);

Task<bool> task = Task.Run(async () => await GetFanFullSpeedAsync());
bool FanFullSpeed = task.Result;
}
Expand Down Expand Up @@ -253,6 +265,7 @@ public override bool Open()
SetQuickLightingEffect(0, 1);
SetQuickLightingEffect(3, 1);
SetQuickLightingEffect(4, 1);

SetQuickLightingEffectEnable(0, false);
SetQuickLightingEffectEnable(3, false);
SetQuickLightingEffectEnable(4, false);
Expand Down
2 changes: 1 addition & 1 deletion HandheldCompanion/Devices/Lenovo/SapientiaUsb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public VERSION(int verPro, int verCMD, int verFir, int verHard)
public static extern bool SetStickCustomDeadzone(int device, int deadzone);

[DllImport("SapientiaUsb.dll", CallingConvention = CallingConvention.StdCall)]
public static extern bool GetGyroSensorDataOnorOff(int device);
public static extern bool SetGyroSensorDataOnorOff(int device, int status);

// Range is 0-99 on Deadzone and Margin
[DllImport("SapientiaUsb.dll", CallingConvention = CallingConvention.StdCall)]
Expand Down
24 changes: 15 additions & 9 deletions HandheldCompanion/Managers/ControllerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,13 @@ private static void ProcessManager_ForegroundChanged(ProcessEx processEx, Proces
private static void CurrentDevice_KeyReleased(ButtonFlags button)
{
// calls current controller (if connected)
var controller = GetTargetController();
controller?.InjectButton(button, false, true);
targetController?.InjectButton(button, false, true);
}

private static void CurrentDevice_KeyPressed(ButtonFlags button)
{
// calls current controller (if connected)
var controller = GetTargetController();
controller?.InjectButton(button, true, false);
targetController?.InjectButton(button, true, false);
}

private static void CheckControllerScenario()
Expand Down Expand Up @@ -580,13 +578,16 @@ private static async void HidDeviceRemoved(PnPDetails details, DeviceEventArgs o
// are we power cycling ?
PowerCyclers.TryGetValue(details.baseContainerDeviceInstanceId, out bool IsPowerCycling);

// is controller current target ?
bool WasTarget = targetController?.GetContainerInstancePath() == details.baseContainerDeviceInstanceId;

// unhide on remove
if (!IsPowerCycling)
{
controller.Unhide(false);

// unplug controller, if needed
if (GetTargetController()?.GetContainerInstancePath() == details.baseContainerDeviceInstanceId)
if (WasTarget)
ClearTargetController();
else
controller.Unplug();
Expand All @@ -598,7 +599,7 @@ private static async void HidDeviceRemoved(PnPDetails details, DeviceEventArgs o
LogManager.LogDebug("Generic controller {0} unplugged", controller.ToString());

// raise event
ControllerUnplugged?.Invoke(controller, IsPowerCycling);
ControllerUnplugged?.Invoke(controller, IsPowerCycling, WasTarget);
}

private static void watchdogThreadLoop(object? obj)
Expand Down Expand Up @@ -846,14 +847,17 @@ private static async void XUsbDeviceRemoved(PnPDetails details, DeviceEventArgs
// are we power cycling ?
PowerCyclers.TryGetValue(details.baseContainerDeviceInstanceId, out bool IsPowerCycling);

// is controller current target ?
bool WasTarget = targetController?.GetContainerInstancePath() == details.baseContainerDeviceInstanceId;

// controller was unplugged
if (!IsPowerCycling)
{
controller.Unhide(false);
Controllers.TryRemove(details.baseContainerDeviceInstanceId, out _);

// controller is current target
if (targetController?.GetContainerInstancePath() == details.baseContainerDeviceInstanceId)
if (WasTarget)
ClearTargetController();
else
controller.Unplug();
Expand All @@ -862,7 +866,7 @@ private static async void XUsbDeviceRemoved(PnPDetails details, DeviceEventArgs
LogManager.LogDebug("XInput controller {0} unplugged", controller.ToString());

// raise event
ControllerUnplugged?.Invoke(controller, IsPowerCycling);
ControllerUnplugged?.Invoke(controller, IsPowerCycling, WasTarget);
}

private static object targetLock = new object();
Expand Down Expand Up @@ -1087,6 +1091,8 @@ private static void UpdateInputs(ControllerState controllerState)
case SensorFamily.SerialUSBIMU:
SensorsManager.UpdateReport(controllerState);
break;
default:
break;
}

// pass to MotionManager for calculations
Expand Down Expand Up @@ -1141,7 +1147,7 @@ internal static IController GetEmulatedController()
public delegate void ControllerPluggedEventHandler(IController Controller, bool IsPowerCycling);

public static event ControllerUnpluggedEventHandler ControllerUnplugged;
public delegate void ControllerUnpluggedEventHandler(IController Controller, bool IsPowerCycling);
public delegate void ControllerUnpluggedEventHandler(IController Controller, bool IsPowerCycling, bool WasTarget);

public static event ControllerSelectedEventHandler ControllerSelected;
public delegate void ControllerSelectedEventHandler(IController Controller);
Expand Down
2 changes: 1 addition & 1 deletion HandheldCompanion/Managers/HotkeysManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private static void ControllerManager_ControllerPlugged(IController Controller,
}
}

private static void ControllerManager_ControllerUnplugged(IController Controller, bool IsPowerCycling)
private static void ControllerManager_ControllerUnplugged(IController Controller, bool IsPowerCycling, bool WasTarget)
{
// when the target emulated controller is Xbox Controller
// only enable HIDmode switch hotkey when controller is unplugged (last stage of HIDmode change in this case)
Expand Down
26 changes: 14 additions & 12 deletions HandheldCompanion/Managers/SensorsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,43 +35,45 @@ static SensorsManager()
private static void ControllerManager_ControllerSelected(IController Controller)
{
// select controller as current sensor if current sensor selection is none
if (sensorFamily == SensorFamily.None && Controller.Capabilities.HasFlag(ControllerCapabilities.MotionSensor))
if (Controller.Capabilities.HasFlag(ControllerCapabilities.MotionSensor))
SettingsManager.SetProperty("SensorSelection", (int)SensorFamily.Controller);
else
PickNextSensor();
}

private static void ControllerManager_ControllerUnplugged(IController Controller, bool IsPowerCycling)
private static void ControllerManager_ControllerUnplugged(IController Controller, bool IsPowerCycling, bool WasTarget)
{
if (sensorFamily != SensorFamily.Controller)
return;

// skip if controller isn't current or doesn't have motion sensor anyway
if (!Controller.HasMotionSensor() || Controller != ControllerManager.GetTargetController())
if (!Controller.HasMotionSensor() || !WasTarget)
return;
if (sensorFamily == SensorFamily.Controller)
PickNextSensor();

// pick next available sensor
PickNextSensor();
}

private static void DeviceManager_UsbDeviceRemoved(PnPDevice device, DeviceEventArgs obj)
{
if (USBSensor is null)
if (USBSensor is null || sensorFamily != SensorFamily.SerialUSBIMU)
return;

// If the USB Gyro is unplugged, close serial connection
USBSensor.Close();

if (sensorFamily == SensorFamily.SerialUSBIMU)
PickNextSensor();
// pick next available sensor
PickNextSensor();
}

private static void PickNextSensor()
{
if (MainWindow.CurrentDevice.Capabilities.HasFlag(DeviceCapabilities.InternalSensor))
if (ControllerManager.GetTargetController() is not null && ControllerManager.GetTargetController().HasMotionSensor())
SettingsManager.SetProperty("SensorSelection", (int)SensorFamily.Controller);
else if (MainWindow.CurrentDevice.Capabilities.HasFlag(DeviceCapabilities.InternalSensor))
SettingsManager.SetProperty("SensorSelection", (int)SensorFamily.Windows);
else if (MainWindow.CurrentDevice.Capabilities.HasFlag(DeviceCapabilities.ExternalSensor))
SettingsManager.SetProperty("SensorSelection", (int)SensorFamily.SerialUSBIMU);
else if (ControllerManager.GetTargetController() is not null && ControllerManager.GetTargetController().HasMotionSensor())
SettingsManager.SetProperty("SensorSelection", (int)SensorFamily.Controller);
else
SettingsManager.SetProperty("SensorSelection", (int)SensorFamily.None);
}
Expand Down
Loading

0 comments on commit 913e8e5

Please sign in to comment.