From aba6d5dda6d8e12077237e4029113f13b75c528f Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Sat, 30 Aug 2025 06:13:29 +0300 Subject: [PATCH 01/10] Feat: add support for Vader4 Pro controller --- DS4Windows/DS4Control/ControlService.cs | 6 + .../DS4Control/ControlServiceDeviceOptions.cs | 103 +++++ .../DTOXml/Vader4ProControllerOptsDTO.cs | 58 +++ .../ControllerRegisterOptionsWindow.xaml | 1 + .../ControllerRegDeviceOptsViewModel.cs | 28 ++ DS4Windows/DS4Library/DS4Devices.cs | 5 +- .../InputDevices/InputDeviceFactory.cs | 8 +- .../InputDevices/Vader4ProDevice.cs | 406 ++++++++++++++++++ .../InputDevices/Vader4ProReport.cs | 168 ++++++++ 9 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 DS4Windows/DS4Control/DTOXml/Vader4ProControllerOptsDTO.cs create mode 100644 DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs create mode 100644 DS4Windows/DS4Library/InputDevices/Vader4ProReport.cs diff --git a/DS4Windows/DS4Control/ControlService.cs b/DS4Windows/DS4Control/ControlService.cs index 87872627d2..fe4f76585e 100644 --- a/DS4Windows/DS4Control/ControlService.cs +++ b/DS4Windows/DS4Control/ControlService.cs @@ -592,6 +592,9 @@ public bool CheckForSupportedDevice(HidDevice device, VidPidInfo metaInfo) case InputDevices.InputDeviceType.DS3: result = deviceOptions.DS3DeviceOpts.Enabled; break; + case InputDevices.InputDeviceType.Vader4Pro: + result = deviceOptions.Vader4ProDeviceOpts.Enabled; + break; default: break; } @@ -802,6 +805,9 @@ private List GetKnownExtraButtons(DS4Device dev) case InputDevices.InputDeviceType.SwitchPro: result.AddRange(new DS4Controls[] { DS4Controls.Capture }); break; + case InputDevices.InputDeviceType.Vader4Pro: + result.AddRange(new DS4Controls[] { DS4Controls.FnL, DS4Controls.FnR, DS4Controls.BLP, DS4Controls.BRP, DS4Controls.SideL, DS4Controls.SideR, DS4Controls.Capture }); + break; default: break; } diff --git a/DS4Windows/DS4Control/ControlServiceDeviceOptions.cs b/DS4Windows/DS4Control/ControlServiceDeviceOptions.cs index 081c86bf67..171c8030ab 100644 --- a/DS4Windows/DS4Control/ControlServiceDeviceOptions.cs +++ b/DS4Windows/DS4Control/ControlServiceDeviceOptions.cs @@ -47,6 +47,9 @@ public class ControlServiceDeviceOptions private DS3DeviceOptions dS3DeviceOpts = new DS3DeviceOptions(); public DS3DeviceOptions DS3DeviceOpts { get => dS3DeviceOpts; } + private Vader4ProDeviceOptions vader4proDeviceOpts = new Vader4ProDeviceOptions(); + public Vader4ProDeviceOptions Vader4ProDeviceOpts { get => vader4proDeviceOpts; } + private bool verboseLogMessages; public bool VerboseLogMessages { get => verboseLogMessages; set => verboseLogMessages = value; } @@ -557,4 +560,104 @@ public override void LoadSettings(XmlDocument xmlDoc, XmlNode node) } } } + + public class Vader4ProDeviceOptions + { + public const bool DEFAULT_ENABLE = true; + private bool enabled = DEFAULT_ENABLE; + public bool Enabled + { + get => enabled; + set + { + if (enabled == value) return; + enabled = value; + EnabledChanged?.Invoke(this, EventArgs.Empty); + } + } + public event EventHandler EnabledChanged; + } + + public class Vader4ProControllerOptions : ControllerOptionsStore + { + public const string XML_ELEMENT_NAME = "Vader4ProSupportSettings"; + + private bool enableHomeLED = true; + public bool EnableHomeLED + { + get => enableHomeLED; + set + { + if (enableHomeLED == value) return; + enableHomeLED = value; + EnableHomeLEDChanged?.Invoke(this, EventArgs.Empty); + } + } + public event EventHandler EnableHomeLEDChanged; + + public Vader4ProControllerOptions(InputDeviceType deviceType) : base(deviceType) + { + } + + public override void PersistSettings(XmlDocument xmlDoc, XmlNode node) + { + string testStr = string.Empty; + XmlSerializer serializer = new XmlSerializer(typeof(Vader4ProControllerOptsDTO)); + + using (Utf8StringWriter strWriter = new Utf8StringWriter()) + { + using XmlWriter xmlWriter = XmlWriter.Create(strWriter, + new XmlWriterSettings() + { + Encoding = Encoding.UTF8, + Indent = false, + OmitXmlDeclaration = true, // only partial XML with no declaration + }); + + // Write root element and children + Vader4ProControllerOptsDTO dto = new Vader4ProControllerOptsDTO(); + dto.MapFrom(this); + // Omit xmlns:xsi and xmlns:xsd from output + serializer.Serialize(xmlWriter, dto, + new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty })); + xmlWriter.Flush(); + xmlWriter.Close(); + + testStr = strWriter.ToString(); + //Trace.WriteLine("TEST OUTPUT"); + //Trace.WriteLine(testStr); + } + + XmlNode tempSwitchProNode = xmlDoc.CreateDocumentFragment(); + tempSwitchProNode.InnerXml = testStr; + + XmlNode tempOptsNode = node.SelectSingleNode(XML_ELEMENT_NAME); + if (tempOptsNode != null) + { + node.RemoveChild(tempOptsNode); + } + + tempOptsNode = tempSwitchProNode; + node.AppendChild(tempOptsNode); + } + + public override void LoadSettings(XmlDocument xmlDoc, XmlNode node) + { + XmlSerializer serializer = new XmlSerializer(typeof(Vader4ProControllerOptsDTO)); + XmlNode baseNode = node.SelectSingleNode(XML_ELEMENT_NAME); + if (baseNode == null) + return; + + try + { + using var stringReader = new StringReader(baseNode.OuterXml); + using var xmlReader = XmlReader.Create(stringReader); + Vader4ProControllerOptsDTO dto = serializer.Deserialize(xmlReader) as Vader4ProControllerOptsDTO; + dto.MapTo(this); + } + catch (InvalidOperationException) + { + } + } + } } diff --git a/DS4Windows/DS4Control/DTOXml/Vader4ProControllerOptsDTO.cs b/DS4Windows/DS4Control/DTOXml/Vader4ProControllerOptsDTO.cs new file mode 100644 index 0000000000..ddb1ea8b6e --- /dev/null +++ b/DS4Windows/DS4Control/DTOXml/Vader4ProControllerOptsDTO.cs @@ -0,0 +1,58 @@ +/* +DS4Windows +Copyright (C) 2023 Travis Nickles + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; +using DS4Windows; + +namespace DS4WinWPF.DS4Control.DTOXml +{ + [XmlRoot(Vader4ProControllerOptions.XML_ELEMENT_NAME)] + public class Vader4ProControllerOptsDTO : IDTO + { + [XmlElement("EnableHomeLED")] + public string EnableHomeLEDString + { + get => EnableHomeLED.ToString(); + set + { + EnableHomeLED = XmlDataUtilities.StrToBool(value); + } + } + + [XmlIgnore] + public bool EnableHomeLED + { + get; set; + } + + public void MapFrom(Vader4ProControllerOptions source) + { + EnableHomeLED = source.EnableHomeLED; + } + + public void MapTo(Vader4ProControllerOptions destination) + { + destination.EnableHomeLED = EnableHomeLED; + } + } +} diff --git a/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml b/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml index 062d5c33cd..fe1221151f 100644 --- a/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml +++ b/DS4Windows/DS4Forms/ControllerRegisterOptionsWindow.xaml @@ -22,6 +22,7 @@ + diff --git a/DS4Windows/DS4Forms/ViewModels/ControllerRegDeviceOptsViewModel.cs b/DS4Windows/DS4Forms/ViewModels/ControllerRegDeviceOptsViewModel.cs index f282a09af2..62c1ae6159 100644 --- a/DS4Windows/DS4Forms/ViewModels/ControllerRegDeviceOptsViewModel.cs +++ b/DS4Windows/DS4Forms/ViewModels/ControllerRegDeviceOptsViewModel.cs @@ -43,12 +43,14 @@ public class ControllerRegDeviceOptsViewModel public bool EnableJoyCon { get => serviceDeviceOpts.JoyConDeviceOpts.Enabled; } public bool EnableDS3 { get => serviceDeviceOpts.DS3DeviceOpts.Enabled; } + public bool EnableVader4Pro { get => serviceDeviceOpts.Vader4ProDeviceOpts.Enabled; } public DS4DeviceOptions DS4DeviceOpts { get => serviceDeviceOpts.DS4DeviceOpts; } public DS3DeviceOptions DS3DeviceOpts { get => serviceDeviceOpts.DS3DeviceOpts; } public DualSenseDeviceOptions DSDeviceOpts { get => serviceDeviceOpts.DualSenseOpts; } public SwitchProDeviceOptions SwitchProDeviceOpts { get => serviceDeviceOpts.SwitchProDeviceOpts; } public JoyConDeviceOptions JoyConDeviceOpts { get => serviceDeviceOpts.JoyConDeviceOpts; } + public Vader4ProDeviceOptions Vader4ProDeviceOpts { get => serviceDeviceOpts.Vader4ProDeviceOpts; } public bool UseMoonlightChanged { @@ -118,6 +120,10 @@ public JoyConControllerOptions CurrentJoyConOptions get => controllerOptionsStores[controllerSelectedIndex] as JoyConControllerOptions; } + public Vader4ProControllerOptions CurrentVader4ProOptions { + get => controllerOptionsStores[controllerSelectedIndex] as Vader4ProControllerOptions; + } + private int currentTabSelectedIndex = 0; public int CurrentTabSelectedIndex { @@ -176,6 +182,9 @@ public int FindTabOptionsIndex() case DS4Windows.InputDevices.InputDeviceType.JoyConR: result = 4; break; + case DS4Windows.InputDevices.InputDeviceType.Vader4Pro: + result = 5; + break; default: // Default to empty control result = 0; @@ -208,6 +217,9 @@ public void FindFittingDataContext() case DS4Windows.InputDevices.InputDeviceType.JoyConR: dataContextObject = new JoyConControllerOptionsWrapper(CurrentJoyConOptions, serviceDeviceOpts.JoyConDeviceOpts); break; + case DS4Windows.InputDevices.InputDeviceType.Vader4Pro: + dataContextObject = new Vader4ProControllerOptionsWrapper(CurrentVader4ProOptions, serviceDeviceOpts.Vader4ProDeviceOpts); + break; default: break; } @@ -312,6 +324,22 @@ public SwitchProControllerOptionsWrapper(SwitchProControllerOptions options, } } + public class Vader4ProControllerOptionsWrapper { + private Vader4ProControllerOptions options; + public Vader4ProControllerOptions Options { get => options; } + + private Vader4ProDeviceOptions parentOptions; + public bool Visible { get => parentOptions.Enabled; } + public event EventHandler VisibleChanged; + + public Vader4ProControllerOptionsWrapper(Vader4ProControllerOptions options, + Vader4ProDeviceOptions parentOpts) { + this.options = options; + this.parentOptions = parentOpts; + parentOptions.EnabledChanged += (sender, e) => { VisibleChanged?.Invoke(this, EventArgs.Empty); }; + } + } + public class JoyConControllerOptionsWrapper { private JoyConControllerOptions options; diff --git a/DS4Windows/DS4Library/DS4Devices.cs b/DS4Windows/DS4Library/DS4Devices.cs index 2f16fccd18..2e12a65989 100644 --- a/DS4Windows/DS4Library/DS4Devices.cs +++ b/DS4Windows/DS4Library/DS4Devices.cs @@ -136,6 +136,8 @@ public class DS4Devices internal const int JOYCON_L_PRODUCT_ID = 0x2006; internal const int JOYCON_R_PRODUCT_ID = 0x2007; internal const int JOYCON_CHARGING_GRIP_PRODUCT_ID = 0x200E; + internal const int FLYDIGI_VID = 0x04B4; + internal const int VADER4PRO_PID = 0x2412; // https://support.steampowered.com/kb_article.php?ref=5199-TOKV-4426&l=english web site has a list of other PS4 compatible device VID/PID values and brand names. // However, not all those are guaranteed to work with DS4Windows app so support is added case by case when users of DS4Windows app tests non-official DS4 gamepads. @@ -184,6 +186,7 @@ public class DS4Devices new VidPidInfo(0x044F, 0xD00E, "Thrustmaster eSwap Pro", InputDeviceType.DS4, VidPidFeatureSet.NoGyroCalib | VidPidFeatureSet.NoBatteryReading), // Thrustmaster eSwap Pro (wired only. No lightbar or gyro) new VidPidInfo(0x054C, 0x0268, "DualShock 3 (SXS)", InputDeviceType.DS3, VidPidFeatureSet.DefaultDS4, checkConnection: DS3Device.DetermineConnectionType), // Sony DualShock 3 using DsHidMini driver (SXS) or Sony Sixaxis driver new VidPidInfo(0x0C12, 0x0E15, "Playmax Wired Controller (PS4)", InputDeviceType.DS4, VidPidFeatureSet.NoBatteryReading | VidPidFeatureSet.NoGyroCalib), // Generic PS4 Controller by Playmax (brand primarily in New Zealand). Standard Wired PS4 controller, no Gyro, no Lightbar, no Battery. There is a newer model but I'm not sure if it uses a different Vid or Pid yet. + new VidPidInfo(FLYDIGI_VID,VADER4PRO_PID,"Flydigi Vader 4 Pro",InputDeviceType.Vader4Pro,VidPidFeatureSet.DefaultDS4 | VidPidFeatureSet.NoBatteryReading| VidPidFeatureSet.NoGyroCalib|VidPidFeatureSet.VendorDefinedDevice) }; @@ -399,7 +402,7 @@ public static void findControllers() } } } - + // Returns DS4 controllers that were found and are running public static IEnumerable getDS4Controllers() { diff --git a/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs b/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs index 54544f1887..7e0554d9c0 100644 --- a/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs +++ b/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs @@ -32,7 +32,8 @@ public enum InputDeviceType : uint JoyConR, JoyConGrip, DualSense, - DS3 + DS3, + Vader4Pro } public abstract class InputDeviceFactory @@ -42,7 +43,7 @@ public static DS4Device CreateDevice(InputDeviceType tempType, { DS4Device temp = null; - switch(tempType) + switch (tempType) { case InputDeviceType.DS4: temp = new DS4Device(hidDevice, disName, featureSet); @@ -61,6 +62,9 @@ public static DS4Device CreateDevice(InputDeviceType tempType, case InputDeviceType.DS3: temp = new DS3Device(hidDevice, disName, featureSet); break; + case InputDeviceType.Vader4Pro: + temp = new Vader4ProDevice(hidDevice, disName, featureSet); + break; } return temp; diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs new file mode 100644 index 0000000000..706ab7df2b --- /dev/null +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Vader4ProReader.Device; +using System.Buffers.Binary; + +namespace DS4Windows.InputDevices +{ + internal class Vader4ProDevice : DS4Device + { + private bool timeStampInit = false; + private uint timeStampPrevious = 0; + private uint deltaTimeCurrent = 0; + private bool outputDirty = false; + private DS4HapticState previousHapticState = new DS4HapticState(); + private byte[] featureReport; + + private Vader4ProControllerOptions nativeOptionsStore; + public Vader4ProControllerOptions NativeOptionsStore { get => nativeOptionsStore; } + public Vader4ProDevice(HidDevice hidDevice, string disName, VidPidFeatureSet featureSet = VidPidFeatureSet.DefaultDS4) : + base(hidDevice, disName, featureSet) + { + synced = true; + + } + + public override event ReportHandler Report = null; + public override event EventHandler BatteryChanged; + public override event EventHandler ChargingChanged; + public override void PostInit() + { + Mac = hDevice.GenerateFakeHwSerial(); + deviceType = InputDeviceType.Vader4Pro; + gyroMouseSensSettings = new GyroMouseSens(); + featureReport = new byte[hDevice.Capabilities.FeatureReportByteLength]; + inputReport = new byte[hDevice.Capabilities.InputReportByteLength]; + outputReport = new byte[hDevice.Capabilities.OutputReportByteLength]; + warnInterval = WARN_INTERVAL_USB; + conType = ConnectionType.USB; + + } + + public override bool DisconnectBT(bool callRemoval = false) + { + // Do Nothing + return true; + } + + public override bool DisconnectDongle(bool remove = false) + { + // Do Nothing + return true; + } + + public override bool DisconnectWireless(bool callRemoval = false) + { + return true; + } + + public override bool IsAlive() + { + return synced; + } + + public override void RefreshCalibration() + { + return; + } + + public override void StartUpdate() + { + this.inputReportErrorCount = 0; + + if (ds4Input == null) + { + ds4Input = new Thread(ReadInput); + ds4Input.Priority = ThreadPriority.AboveNormal; + ds4Input.Name = "DS3 Input thread: " + Mac; + ds4Input.IsBackground = true; + ds4Input.Start(); + } + else + Console.WriteLine("Thread already running for DS4: " + Mac); + } + + private unsafe void ReadInput() + { + unchecked + { + Debouncer = SetupDebouncer(); + firstActive = DateTime.UtcNow; + NativeMethods.HidD_SetNumInputBuffers(hDevice.SafeReadHandle.DangerousGetHandle(), 3); + Queue latencyQueue = new Queue(21); // Set capacity at max + 1 to avoid any resizing + int tempLatencyCount = 0; + long oldtime = 0; + string currerror = string.Empty; + long curtime = 0; + long testelapsed = 0; + timeoutEvent = false; + ds4InactiveFrame = true; + idleInput = true; + bool syncWriteReport = conType != ConnectionType.BT; + + bool tempCharging = charging; + uint tempStamp = 0; + double elapsedDeltaTime = 0.0; + uint tempDelta = 0; + long latencySum = 0; + + sixAxis.ResetContinuousCalibration(); + standbySw.Start(); + + while (!exitInputThread) + { + oldCharging = charging; + currerror = string.Empty; + + if (tempLatencyCount >= 20) + { + latencySum -= latencyQueue.Dequeue(); + tempLatencyCount--; + } + + latencySum += this.lastTimeElapsed; + latencyQueue.Enqueue(this.lastTimeElapsed); + tempLatencyCount++; + + //Latency = latencyQueue.Average(); + Latency = latencySum / (double)tempLatencyCount; + + readWaitEv.Set(); + + HidDevice.ReadStatus res = hDevice.ReadFile(inputReport); + if (res != HidDevice.ReadStatus.Success) + { + + exitInputThread = true; + readWaitEv.Reset(); + StopOutputUpdate(); + isDisconnecting = true; + RunRemoval(); + timeoutExecuted = true; + continue; + } + readWaitEv.Wait(); + readWaitEv.Reset(); + + curtime = Stopwatch.GetTimestamp(); + testelapsed = curtime - oldtime; + lastTimeElapsedDouble = testelapsed * (1.0 / Stopwatch.Frequency) * 1000.0; + lastTimeElapsed = (long)lastTimeElapsedDouble; + oldtime = curtime; + + utcNow = DateTime.UtcNow; // timestamp with UTC in case system time zone changes + + cState.PacketCounter = pState.PacketCounter + 1; + cState.ReportTimeStamp = utcNow; + byte[] buf = new byte[32]; + int copyLen = Math.Min(inputReport.Length, 32); // safe length + Array.Copy(inputReport, buf, copyLen); // copies only what exists + var report = new Vader4ProReport(buf); + + cState.Share = report.IsSelectPressed; + cState.L3 = report.IsLSPressed; + cState.R3 = report.IsRSPressed; + cState.Options = report.IsStartPressed; + cState.DpadUp = report.IsDPadUpPressed; + cState.DpadRight = report.IsDPadRightPressed; + cState.DpadDown = report.IsDPadDownPressed; + cState.DpadLeft = report.IsDPadLeftPressed; + + cState.L2 = report.LT; + cState.R2 = report.RT; + + cState.L1 = report.IsLBPressed; + cState.R1 = report.IsRBPressed; + cState.Triangle = report.IsYPressed; + cState.Circle = report.IsBPressed; + cState.Cross = report.IsAPressed; + cState.Square = report.IsXPressed; + cState.PS = report.IsHOMEPressed; + + cState.L2Btn = cState.L2 > 0; + cState.R2Btn = cState.R2 > 0; + + cState.L2Raw = cState.L2; + cState.R2Raw = cState.R2; + + cState.LX = report.LS_X; + cState.LY = report.LS_Y; + cState.RX = report.RS_X; + cState.RY = report.RS_Y; + + // Adding paddles and c,z buttons + cState.FnL = report.IsM2Pressed; + cState.FnR = report.IsM1Pressed; + cState.BLP = report.IsM4Pressed; + cState.BRP = report.IsM3Pressed; + cState.SideL = report.IsCPressed; + cState.SideR = report.IsZPressed; + cState.Capture = report.IsFNPressed; + + short yaw = (short)-report.YawCalibrated; + short pitch = (short)-report.PitchCalibrated; + short roll = (short)(report.RollCalibrated); + + short ax = (short)(report.AccelXCalibrated); + short ay = (short)(report.AccelYCalibrated); + short az = (short)(report.AccelZCalibrated); + + gyro[0] = (byte)(pitch & 0xFF); + gyro[1] = (byte)((pitch >> 8) & 0xFF); + gyro[2] = (byte)(yaw & 0xFF); + gyro[3] = (byte)((yaw >> 8) & 0xFF); + gyro[4] = (byte)(roll & 0xFF); + gyro[5] = (byte)((roll >> 8) & 0xFF); + + // ---- Accel [X, Y, Z] ---- + accel[0] = (byte)(ax & 0xFF); + accel[1] = (byte)((ax >> 8) & 0xFF); + accel[2] = (byte)(ay & 0xFF); + accel[3] = (byte)((ay >> 8) & 0xFF); + accel[4] = (byte)(az & 0xFF); + accel[5] = (byte)((az >> 8) & 0xFF); + + if (synced) + { + fixed (byte* pbGyro = gyro, pbAccel = accel) + { + sixAxis.handleSixaxis(pbGyro, pbAccel, cState, elapsedDeltaTime); + } + } + + battery = 99; + cState.Battery = 99; + + + if (timeStampInit == false) + { + timeStampInit = true; + deltaTimeCurrent = tempStamp * 1u / 3u; + } + else if (timeStampPrevious > tempStamp) + { + tempDelta = uint.MaxValue - timeStampPrevious + tempStamp + 1u; + deltaTimeCurrent = tempDelta * 1u / 3u; + } + else + { + tempDelta = tempStamp - timeStampPrevious; + deltaTimeCurrent = tempDelta * 1u / 3u; + } + + + // Make sure timestamps don't match + if (deltaTimeCurrent != 0) + { + elapsedDeltaTime = 0.000001 * deltaTimeCurrent; // Convert from microseconds to seconds + cState.totalMicroSec = pState.totalMicroSec + deltaTimeCurrent; + } + else + { + // Duplicate timestamp. Use system clock for elapsed time instead + elapsedDeltaTime = lastTimeElapsedDouble * .001; + cState.totalMicroSec = pState.totalMicroSec + (uint)(elapsedDeltaTime * 1000000); + } + + cState.elapsedTime = elapsedDeltaTime; + cState.ds4Timestamp = (ushort)((tempStamp / 16) % ushort.MaxValue); + timeStampPrevious = tempStamp; + + + if (conType == ConnectionType.USB) + { + if (idleTimeout == 0) + { + lastActive = utcNow; + } + else + { + idleInput = isDS4Idle(); + if (!idleInput) + { + lastActive = utcNow; + } + } + } + + if (fireReport) + { + Report?.Invoke(this, EventArgs.Empty); + } + + PrepareOutReport(); + if (outputDirty) + { + WriteReport(); + previousHapticState = currentHap; + } + + currentHap.dirty = false; + outputDirty = false; + + + if (!string.IsNullOrEmpty(currerror)) + error = currerror; + else if (!string.IsNullOrEmpty(error)) + error = string.Empty; + + cState.CopyTo(pState); + + if (hasInputEvts) + { + lock (eventQueueLock) + { + Action tempAct = null; + for (int actInd = 0, actLen = eventQueue.Count; actInd < actLen; actInd++) + { + tempAct = eventQueue.Dequeue(); + tempAct.Invoke(); + } + + hasInputEvts = false; + } + } + } + + } + timeoutExecuted = true; + } + + + private void PrepareOutReport() + { + MergeStates(); + bool change = false; + bool rumbleSet = currentHap.IsRumbleSet(); + if (currentHap.dirty || !previousHapticState.Equals(currentHap)) + { + change = true; + } + if (change) + { + outputDirty = true; + if (rumbleSet) + { + standbySw.Restart(); + } + else + { + standbySw.Reset(); + } + } + else if (rumbleSet && standbySw.ElapsedMilliseconds >= 4000L) + { + outputDirty = true; + standbySw.Restart(); + } + } + private void WriteReport() + { + SetLed(currentHap.lightbarState.LightBarColor.red, currentHap.lightbarState.LightBarColor.green, currentHap.lightbarState.LightBarColor.blue); + SetRumble(currentHap.rumbleState.RumbleMotorStrengthLeftHeavySlow, currentHap.rumbleState.RumbleMotorStrengthRightLightFast); + } + private void SetRumble(byte leftMain, byte rightMain, byte leftTrigger = 0, byte rightTrigger = 0) + { + // WriteReport([0x05, 0x0f, leftMain, rightMain, leftTrigger, rightTrigger, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + byte[] buf = new byte[32]; + buf[0] = 0x05; + buf[1] = 0x0F; + buf[2] = leftMain; + buf[3] = rightMain; + buf[4] = leftTrigger; + buf[5] = rightTrigger; + hDevice.WriteOutputReportViaInterrupt(buf, READ_STREAM_TIMEOUT); + } + + private void SetLed(byte r, byte g, byte b) + { + //WriteReport([0x05, 0xe0, r, g, b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); + byte[] buf = new byte[32]; + buf[0] = 0x05; + buf[1] = 0xE0; + buf[2] = r; + buf[3] = g; + buf[4] = b; + hDevice.WriteOutputReportViaInterrupt(buf, READ_STREAM_TIMEOUT); + } + + protected override void StopOutputUpdate() + { + byte[] buf = new byte[32]; + buf[0] = 0x05; + buf[1] = 0x10; + buf[2] = 0x01; + buf[3] = 0x01; + buf[4] = 0x01; + hDevice.WriteOutputReportViaInterrupt(buf, READ_STREAM_TIMEOUT); + } + + } +} diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProReport.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProReport.cs new file mode 100644 index 0000000000..05b5596189 --- /dev/null +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProReport.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vader4ProReader.Device +{ + public readonly struct Vader4ProReport + { + [Flags] + enum ButtonCollection0 : byte + { + C = 1, + Z = 2, + M1 = 4, + M2 = 8, + M3 = 16, + M4 = 32 + } + + [Flags] + enum ButtonCollection1 : byte + { + FN = 1, + HOME = 8, + } + + [Flags] + enum ButtonCollection2 : byte + { + DPadUp = 1, + DPadRight = 2, + DPadDown = 4, + DPadLeft = 8, + A = 16, + B = 32, + Select = 64, + X = 128 + } + [Flags] + enum ButtonCollection3 : byte + { + Y = 1, + Start = 2, + LB = 4, + RB = 8, + LT = 16, + RT = 32, + LS = 64, + RS = 128 + } + + /* + * byte offset | description + * ------------|---------------- + * 0 | Report ID + * 1 | unknown (0xFE) + * 2 | unknown (0x66) + * 3 | Air Mouse active flag(0x80) + * 4 | legacy yaw (low 8 bits) + * 5 | legacy pitch (low 4 bits) << 4 | legacy yaw (high 4 bits) + * 6 | legacy pitch (high 8 bits) + * 7 | buttons0 + * 8 | buttons1 + * 9 | buttons2 + * 10 | buttons3 + * 11-12 | AccelXRaw + * 13-14 | AccelZRaw + * 15-16 | AccelYRaw + * 17 | LS_X + * 18,20 | YawRaw (-512~512) + * 19 | LS_Y + * 21 | RS_X + * 22 | RS_Y + * 23 | LT + * 24 | RT + * 25 | unknown (0x00) + * 26-27 | PitchRaw (-512~512) + * 28 | unknown (0x00) + * 29-30 | RollRaw + * 31 | unknown (0x00) + */ + private readonly Memory rawReport; + + public Vader4ProReport(Memory rawReport) + { + if (rawReport.Length != 32) + throw new ArgumentException("Invalid report length", nameof(rawReport)); + this.rawReport = rawReport; + } + + private ButtonCollection0 buttons0 => (ButtonCollection0)rawReport.Span[7]; + private ButtonCollection1 buttons1 => (ButtonCollection1)rawReport.Span[8]; + private ButtonCollection2 buttons2 => (ButtonCollection2)rawReport.Span[9]; + private ButtonCollection3 buttons3 => (ButtonCollection3)rawReport.Span[10]; + + public bool IsCPressed => buttons0.HasFlag(ButtonCollection0.C); + public bool IsZPressed => buttons0.HasFlag(ButtonCollection0.Z); + public bool IsM1Pressed => buttons0.HasFlag(ButtonCollection0.M1); + public bool IsM2Pressed => buttons0.HasFlag(ButtonCollection0.M2); + public bool IsM3Pressed => buttons0.HasFlag(ButtonCollection0.M3); + public bool IsM4Pressed => buttons0.HasFlag(ButtonCollection0.M4); + + public bool IsFNPressed => buttons1.HasFlag(ButtonCollection1.FN); + public bool IsHOMEPressed => buttons1.HasFlag(ButtonCollection1.HOME); + + public bool IsDPadUpPressed => buttons2.HasFlag(ButtonCollection2.DPadUp); + public bool IsDPadRightPressed => buttons2.HasFlag(ButtonCollection2.DPadRight); + public bool IsDPadDownPressed => buttons2.HasFlag(ButtonCollection2.DPadDown); + public bool IsDPadLeftPressed => buttons2.HasFlag(ButtonCollection2.DPadLeft); + public bool IsAPressed => buttons2.HasFlag(ButtonCollection2.A); + public bool IsBPressed => buttons2.HasFlag(ButtonCollection2.B); + public bool IsSelectPressed => buttons2.HasFlag(ButtonCollection2.Select); + public bool IsXPressed => buttons2.HasFlag(ButtonCollection2.X); + + public bool IsYPressed => buttons3.HasFlag(ButtonCollection3.Y); + public bool IsStartPressed => buttons3.HasFlag(ButtonCollection3.Start); + public bool IsLBPressed => buttons3.HasFlag(ButtonCollection3.LB); + public bool IsRBPressed => buttons3.HasFlag(ButtonCollection3.RB); + public bool IsLTPressed => buttons3.HasFlag(ButtonCollection3.LT); + public bool IsRTPressed => buttons3.HasFlag(ButtonCollection3.RT); + public bool IsLSPressed => buttons3.HasFlag(ButtonCollection3.LS); + public bool IsRSPressed => buttons3.HasFlag(ButtonCollection3.RS); + + public byte LS_X => rawReport.Span[17]; + public byte LS_Y => rawReport.Span[19]; + + public byte RS_X => rawReport.Span[21]; + public byte RS_Y => rawReport.Span[22]; + + public byte LT => rawReport.Span[23]; + public byte RT => rawReport.Span[24]; + + public short YawRaw => BitConverter.ToInt16([rawReport.Span[18], rawReport.Span[20]]); + public short PitchRaw => BitConverter.ToInt16(rawReport.Span[26..28]); + public short RollRaw => BitConverter.ToInt16(rawReport.Span[29..31]); + + // LSM6DS* gyroscope max range: +/- 2000 dps + + public float YawCalibrated => YawRaw << 6; + public float PitchCalibrated => PitchRaw << 6; + public float RollCalibrated => RollRaw; + + public short AccelXRaw => BitConverter.ToInt16(rawReport.Span[11..13]); + public short AccelYRaw => BitConverter.ToInt16(rawReport.Span[15..17]); + public short AccelZRaw => BitConverter.ToInt16(rawReport.Span[13..15]); + + + public short AccelXCalibrated => (short)(AccelXRaw << 5); + public short AccelYCalibrated => (short)(AccelYRaw << 5); + public short AccelZCalibrated => (short)(AccelZRaw << 5); + + public bool IsAirMouseActive => (rawReport.Span[3] & 128) != 0; + + + + + // X/Y/Z axis automatically calibrated to 0/256/0 when the device is steady + // statistical analysis shows that Z axis is offset by approx. 32, assume (256*scale)^2 + (32*scale)^2 = g^2 + // this may vary between devices and firmware versions + // alternatively, do optimization with (ScaleX*(X+OffsetX))^2 + (ScaleY*(Y+OffsetY))^2 + (ScaleZ*(Z+OffsetZ))^2 = g^2 + // with steady readings in different orientations + + // scale = 9.80665 / sqrt(256^2 + 32^2) + //const float accelScale = 0.03801141343622691f; + } +} From 9509dc68f57f14cbe4629508e760c39ea22a1130 Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Sat, 30 Aug 2025 17:07:42 +0300 Subject: [PATCH 02/10] fix: detect the correct HID for vader 4 --- DS4Windows/DS4Library/DS4Devices.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/DS4Windows/DS4Library/DS4Devices.cs b/DS4Windows/DS4Library/DS4Devices.cs index 2e12a65989..8b9125e8da 100644 --- a/DS4Windows/DS4Library/DS4Devices.cs +++ b/DS4Windows/DS4Library/DS4Devices.cs @@ -16,13 +16,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +using DS4Windows.InputDevices; +using NLog.Targets; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Security.Principal; -using DS4Windows.InputDevices; namespace DS4Windows { @@ -240,6 +241,7 @@ public static void findControllers() lock (Devices) { IEnumerable hDevices = HidDevices.EnumerateDS4(knownDevices); + hDevices = hDevices.Where(d => { VidPidInfo metainfo = knownDevices.Single(x => x.vid == d.Attributes.VendorId && @@ -248,8 +250,20 @@ public static void findControllers() }); hDevices = hDevices.Where(IsRealDS4).Select(dev => dev); + hDevices = hDevices.Where(d => + { + int vid = d.Attributes.VendorId; + int pid = d.Attributes.ProductId; - //hDevices = from dev in hDevices where IsRealDS4(dev) select dev; + if (vid == FLYDIGI_VID && pid == VADER4PRO_PID) + { + return d.DevicePath.Contains("&mi_02"); + } + + return true; // keep everything else + }); + + //hDevices = from dev in hDevices where IsRealDS4(dev) select dev; // Sort Bluetooth first in case USB is also connected on the same controller. hDevices = hDevices.OrderBy((HidDevice d) => { @@ -313,7 +327,7 @@ public static void findControllers() } catch (Exception) { } } - + // TODO in exclusive mode, try to hold both open when both are connected if (isExclusiveMode && !hDevice.IsOpen) hDevice.OpenDevice(false); @@ -395,7 +409,7 @@ public static void findControllers() DevicePaths.Add(hDevice.DevicePath); deviceSerials.Add(serial); serialDevices.Add(serial, ds4Device); - AppLogger.LogToGui($"{DS4WinWPF.Properties.Resources.FoundController} {ds4Device.getMacAddress()} ({ds4Device.getConnectionType()}) ({ds4Device.DisplayName}).",false); + AppLogger.LogToGui($"{DS4WinWPF.Properties.Resources.FoundController} {ds4Device.getMacAddress()} ({ds4Device.getConnectionType()}) ({ds4Device.DisplayName}).", false); } } } From aa0438fb5544bdfdaebe0ba96cd9022bf11b242c Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Sat, 30 Aug 2025 17:08:04 +0300 Subject: [PATCH 03/10] chore: change paddle mapping for better semantic naming --- .../DS4Library/InputDevices/Vader4ProDevice.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index 706ab7df2b..ade161477a 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -196,12 +196,12 @@ private unsafe void ReadInput() cState.RY = report.RS_Y; // Adding paddles and c,z buttons - cState.FnL = report.IsM2Pressed; - cState.FnR = report.IsM1Pressed; - cState.BLP = report.IsM4Pressed; - cState.BRP = report.IsM3Pressed; - cState.SideL = report.IsCPressed; - cState.SideR = report.IsZPressed; + cState.SideL = report.IsM4Pressed; + cState.SideR = report.IsM3Pressed; + cState.BLP = report.IsM2Pressed; + cState.BRP = report.IsM1Pressed; + cState.FnL = report.IsCPressed; + cState.FnR = report.IsZPressed; cState.Capture = report.IsFNPressed; short yaw = (short)-report.YawCalibrated; From bddf4fe6017e84260700d731ec2646ac5d301207 Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Sat, 30 Aug 2025 18:53:09 +0300 Subject: [PATCH 04/10] feat: update DS4Sixaxis api to add support for raw values direct --- DS4Windows/DS4Library/DS4Sixaxis.cs | 52 +++++++++++-------- .../InputDevices/Vader4ProDevice.cs | 22 +------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/DS4Windows/DS4Library/DS4Sixaxis.cs b/DS4Windows/DS4Library/DS4Sixaxis.cs index b08998adc8..1c9c109839 100644 --- a/DS4Windows/DS4Library/DS4Sixaxis.cs +++ b/DS4Windows/DS4Library/DS4Sixaxis.cs @@ -332,13 +332,13 @@ public void setCalibrationData(ref byte[] calibData, bool useAltGyroCalib) accelZMinus = temInt = (short)((ushort)(calibData[34] << 8) | calibData[33]); int gyroSpeed2x = temInt = (gyroSpeedPlus + gyroSpeedMinus); - calibrationData[0].sensNumer = gyroSpeed2x* SixAxis.GYRO_RES_IN_DEG_SEC; + calibrationData[0].sensNumer = gyroSpeed2x * SixAxis.GYRO_RES_IN_DEG_SEC; calibrationData[0].sensDenom = pitchPlus - pitchMinus; - calibrationData[1].sensNumer = gyroSpeed2x* SixAxis.GYRO_RES_IN_DEG_SEC; + calibrationData[1].sensNumer = gyroSpeed2x * SixAxis.GYRO_RES_IN_DEG_SEC; calibrationData[1].sensDenom = yawPlus - yawMinus; - calibrationData[2].sensNumer = gyroSpeed2x* SixAxis.GYRO_RES_IN_DEG_SEC; + calibrationData[2].sensNumer = gyroSpeed2x * SixAxis.GYRO_RES_IN_DEG_SEC; calibrationData[2].sensDenom = rollPlus - rollMinus; int accelRange = temInt = accelXPlus - accelXMinus; @@ -391,40 +391,37 @@ private void applyCalibs(ref int yaw, ref int pitch, ref int roll, accelZ = temInt = (int)(temInt * (current.sensNumer / (float)current.sensDenom)); } - public unsafe void handleSixaxis(byte* gyro, byte* accel, DS4State state, + public unsafe void handleSixaxisVals( + int yaw, + int pitch, + int roll, + int accelX, + int accelY, + int accelZ, DS4State state, double elapsedDelta) { unchecked { - int currentYaw = (short)((ushort)(gyro[3] << 8) | gyro[2]); - int currentPitch = (short)((ushort)(gyro[1] << 8) | gyro[0]); - int currentRoll = (short)((ushort)(gyro[5] << 8) | gyro[4]); - int AccelX = (short)((ushort)(accel[1] << 8) | accel[0]); - int AccelY = (short)((ushort)(accel[3] << 8) | accel[2]); - int AccelZ = (short)((ushort)(accel[5] << 8) | accel[4]); - - //Console.WriteLine("AccelZ: {0}", AccelZ); - if (calibrationDone) - applyCalibs(ref currentYaw, ref currentPitch, ref currentRoll, ref AccelX, ref AccelY, ref AccelZ); + applyCalibs(ref yaw, ref pitch, ref roll, ref accelX, ref accelY, ref accelZ); if (gyroAverageTimer.IsRunning) { - CalcSensorCamples(ref currentYaw, ref currentPitch, ref currentRoll, ref AccelX, ref AccelY, ref AccelZ); + CalcSensorCamples(ref yaw, ref pitch, ref roll, ref accelX, ref accelY, ref accelZ); } - currentYaw -= gyro_offset_x; - currentPitch -= gyro_offset_y; - currentRoll -= gyro_offset_z; + yaw -= gyro_offset_x; + pitch -= gyro_offset_y; + roll -= gyro_offset_z; SixAxisEventArgs args = null; - if (AccelX != 0 || AccelY != 0 || AccelZ != 0) + if (accelX != 0 || accelY != 0 || accelZ != 0) { if (SixAccelMoved != null) { sPrev.copy(now); - now.populate(currentYaw, currentPitch, currentRoll, - AccelX, AccelY, AccelZ, elapsedDelta, sPrev); + now.populate(yaw, pitch, roll, + accelX, accelY, accelZ, elapsedDelta, sPrev); args = new SixAxisEventArgs(state.ReportTimeStamp, now); state.Motion = now; @@ -433,6 +430,19 @@ public unsafe void handleSixaxis(byte* gyro, byte* accel, DS4State state, } } } + public unsafe void handleSixaxis(byte* gyro, byte* accel, DS4State state, + double elapsedDelta) + { + int currentYaw = (short)((ushort)(gyro[3] << 8) | gyro[2]); + int currentPitch = (short)((ushort)(gyro[1] << 8) | gyro[0]); + int currentRoll = (short)((ushort)(gyro[5] << 8) | gyro[4]); + int AccelX = (short)((ushort)(accel[1] << 8) | accel[0]); + int AccelY = (short)((ushort)(accel[3] << 8) | accel[2]); + int AccelZ = (short)((ushort)(accel[5] << 8) | accel[4]); + + //Console.WriteLine("AccelZ: {0}", AccelZ); + handleSixaxisVals(currentYaw, currentPitch, currentRoll, AccelX, AccelY, AccelZ, state, elapsedDelta); + } public unsafe void handleDS3Sixaxis(byte* gyro, byte* accel, DS4State state, double elapsedDelta, int device) diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index ade161477a..dc636be37d 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -194,7 +194,7 @@ private unsafe void ReadInput() cState.LY = report.LS_Y; cState.RX = report.RS_X; cState.RY = report.RS_Y; - + // Adding paddles and c,z buttons cState.SideL = report.IsM4Pressed; cState.SideR = report.IsM3Pressed; @@ -212,27 +212,9 @@ private unsafe void ReadInput() short ay = (short)(report.AccelYCalibrated); short az = (short)(report.AccelZCalibrated); - gyro[0] = (byte)(pitch & 0xFF); - gyro[1] = (byte)((pitch >> 8) & 0xFF); - gyro[2] = (byte)(yaw & 0xFF); - gyro[3] = (byte)((yaw >> 8) & 0xFF); - gyro[4] = (byte)(roll & 0xFF); - gyro[5] = (byte)((roll >> 8) & 0xFF); - - // ---- Accel [X, Y, Z] ---- - accel[0] = (byte)(ax & 0xFF); - accel[1] = (byte)((ax >> 8) & 0xFF); - accel[2] = (byte)(ay & 0xFF); - accel[3] = (byte)((ay >> 8) & 0xFF); - accel[4] = (byte)(az & 0xFF); - accel[5] = (byte)((az >> 8) & 0xFF); - if (synced) { - fixed (byte* pbGyro = gyro, pbAccel = accel) - { - sixAxis.handleSixaxis(pbGyro, pbAccel, cState, elapsedDeltaTime); - } + sixAxis.handleSixaxisVals(yaw, pitch, roll, ax, ay, az, cState, elapsedDeltaTime); } battery = 99; From f150c613eda49487b308204738066d5739459f9f Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Sat, 30 Aug 2025 21:03:00 +0300 Subject: [PATCH 05/10] hotfix: fix inverted accelerometerX axis --- .../InputDevices/Vader4ProDevice.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index dc636be37d..def7322eb9 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -208,7 +208,7 @@ private unsafe void ReadInput() short pitch = (short)-report.PitchCalibrated; short roll = (short)(report.RollCalibrated); - short ax = (short)(report.AccelXCalibrated); + short ax = (short)-(report.AccelXCalibrated); short ay = (short)(report.AccelYCalibrated); short az = (short)(report.AccelZCalibrated); @@ -217,8 +217,7 @@ private unsafe void ReadInput() sixAxis.handleSixaxisVals(yaw, pitch, roll, ax, ay, az, cState, elapsedDeltaTime); } - battery = 99; - cState.Battery = 99; + cState.Battery = 100; if (timeStampInit == false) @@ -256,20 +255,17 @@ private unsafe void ReadInput() timeStampPrevious = tempStamp; - if (conType == ConnectionType.USB) + if (idleTimeout == 0) { - if (idleTimeout == 0) + lastActive = utcNow; + } + else + { + idleInput = isDS4Idle(); + if (!idleInput) { lastActive = utcNow; } - else - { - idleInput = isDS4Idle(); - if (!idleInput) - { - lastActive = utcNow; - } - } } if (fireReport) From 5496aeb7f35d62f48332fc234f771d5d7ba3e757 Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Mon, 1 Sep 2025 09:05:10 +0300 Subject: [PATCH 06/10] fix: handle command responses not treated as button inputs --- .../InputDevices/Vader4ProDevice.cs | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index def7322eb9..6f13452bc6 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -162,62 +162,67 @@ private unsafe void ReadInput() byte[] buf = new byte[32]; int copyLen = Math.Min(inputReport.Length, 32); // safe length Array.Copy(inputReport, buf, copyLen); // copies only what exists - var report = new Vader4ProReport(buf); - - cState.Share = report.IsSelectPressed; - cState.L3 = report.IsLSPressed; - cState.R3 = report.IsRSPressed; - cState.Options = report.IsStartPressed; - cState.DpadUp = report.IsDPadUpPressed; - cState.DpadRight = report.IsDPadRightPressed; - cState.DpadDown = report.IsDPadDownPressed; - cState.DpadLeft = report.IsDPadLeftPressed; - - cState.L2 = report.LT; - cState.R2 = report.RT; - - cState.L1 = report.IsLBPressed; - cState.R1 = report.IsRBPressed; - cState.Triangle = report.IsYPressed; - cState.Circle = report.IsBPressed; - cState.Cross = report.IsAPressed; - cState.Square = report.IsXPressed; - cState.PS = report.IsHOMEPressed; - - cState.L2Btn = cState.L2 > 0; - cState.R2Btn = cState.R2 > 0; - - cState.L2Raw = cState.L2; - cState.R2Raw = cState.R2; - - cState.LX = report.LS_X; - cState.LY = report.LS_Y; - cState.RX = report.RS_X; - cState.RY = report.RS_Y; - - // Adding paddles and c,z buttons - cState.SideL = report.IsM4Pressed; - cState.SideR = report.IsM3Pressed; - cState.BLP = report.IsM2Pressed; - cState.BRP = report.IsM1Pressed; - cState.FnL = report.IsCPressed; - cState.FnR = report.IsZPressed; - cState.Capture = report.IsFNPressed; - - short yaw = (short)-report.YawCalibrated; - short pitch = (short)-report.PitchCalibrated; - short roll = (short)(report.RollCalibrated); - - short ax = (short)-(report.AccelXCalibrated); - short ay = (short)(report.AccelYCalibrated); - short az = (short)(report.AccelZCalibrated); - - if (synced) + if (buf[1] == 0xFE) { - sixAxis.handleSixaxisVals(yaw, pitch, roll, ax, ay, az, cState, elapsedDeltaTime); + var report = new Vader4ProReport(buf); + // Dpad + cState.DpadUp = report.IsDPadUpPressed; + cState.DpadRight = report.IsDPadRightPressed; + cState.DpadDown = report.IsDPadDownPressed; + cState.DpadLeft = report.IsDPadLeftPressed; + // Left Stick + cState.LX = report.LS_X; + cState.LY = report.LS_Y; + cState.L3 = report.IsLSPressed; + // Right Stick + cState.RX = report.RS_X; + cState.RY = report.RS_Y; + cState.R3 = report.IsRSPressed; + // Share/Options + cState.Share = report.IsSelectPressed; + cState.Options = report.IsStartPressed; + // Left Bumper / Right Bumper + cState.L1 = report.IsLBPressed; + cState.R1 = report.IsRBPressed; + // Left Trigger + cState.L2 = report.LT; + cState.L2Btn = cState.L2 > 0; + cState.L2Raw = cState.L2; + // Right Trigger + cState.R2 = report.RT; + cState.R2Btn = cState.R2 > 0; + cState.R2Raw = cState.R2; + // Face Buttons + cState.Cross = report.IsAPressed; + cState.Circle = report.IsBPressed; + cState.Square = report.IsXPressed; + cState.Triangle = report.IsYPressed; + // PlayStation Button + cState.PS = report.IsHOMEPressed; + // Paddles + cState.SideL = report.IsM4Pressed; + cState.SideR = report.IsM3Pressed; + cState.BLP = report.IsM2Pressed; + cState.BRP = report.IsM1Pressed; + // C,Z buttons + cState.FnL = report.IsCPressed; + cState.FnR = report.IsZPressed; + // FN button + cState.Capture = report.IsFNPressed; + // Gyro / Accelerometer + short yaw = (short)-report.YawCalibrated; + short pitch = (short)-report.PitchCalibrated; + short roll = (short)(report.RollCalibrated); + short ax = (short)-(report.AccelXCalibrated); + short ay = (short)(report.AccelYCalibrated); + short az = (short)(report.AccelZCalibrated); + if (synced) + { + sixAxis.handleSixaxisVals(yaw, pitch, roll, ax, ay, az, cState, elapsedDeltaTime); + } } - cState.Battery = 100; + cState.Battery = 99; if (timeStampInit == false) From 3157584fb6647a7023f36a3fc0bc3c91712b23ef Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Mon, 1 Sep 2025 09:06:05 +0300 Subject: [PATCH 07/10] feat: add mac address detection for vader 4 pro --- DS4Windows/DS4Library/DS4Devices.cs | 6 +++- .../InputDevices/InputDeviceFactory.cs | 4 +-- .../InputDevices/Vader4ProDevice.cs | 20 +++++++------ DS4Windows/HidLibrary/HidDevice.cs | 30 +++++++++++++++++++ 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/DS4Windows/DS4Library/DS4Devices.cs b/DS4Windows/DS4Library/DS4Devices.cs index 8b9125e8da..89d5d372e6 100644 --- a/DS4Windows/DS4Library/DS4Devices.cs +++ b/DS4Windows/DS4Library/DS4Devices.cs @@ -352,6 +352,10 @@ public static void findControllers() // Blank serial will mean that a JoyCon is not docked to a side serial = JoyConDevice.ReadUSBSerial(hDevice); } + else if (metainfo.inputDevType == InputDeviceType.Vader4Pro) + { + serial = hDevice.GetVader4ProMacAddress(); + } else { serial = hDevice.ReadSerial(DS4Device.SERIAL_FEATURE_ID); @@ -390,7 +394,7 @@ public static void findControllers() if (newdev && validSerial) { - DS4Device ds4Device = InputDeviceFactory.CreateDevice(metainfo.inputDevType, hDevice, metainfo.name, metainfo.featureSet); + DS4Device ds4Device = InputDeviceFactory.CreateDevice(metainfo.inputDevType, hDevice, metainfo.name, metainfo.featureSet, serial); //DS4Device ds4Device = new DS4Device(hDevice, metainfo.name, metainfo.featureSet); if (ds4Device == null) { diff --git a/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs b/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs index 7e0554d9c0..6198c4fbdf 100644 --- a/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs +++ b/DS4Windows/DS4Library/InputDevices/InputDeviceFactory.cs @@ -39,7 +39,7 @@ public enum InputDeviceType : uint public abstract class InputDeviceFactory { public static DS4Device CreateDevice(InputDeviceType tempType, - HidDevice hidDevice, string disName, VidPidFeatureSet featureSet = VidPidFeatureSet.DefaultDS4) + HidDevice hidDevice, string disName, VidPidFeatureSet featureSet = VidPidFeatureSet.DefaultDS4, string macAddress = "") { DS4Device temp = null; @@ -63,7 +63,7 @@ public static DS4Device CreateDevice(InputDeviceType tempType, temp = new DS3Device(hidDevice, disName, featureSet); break; case InputDeviceType.Vader4Pro: - temp = new Vader4ProDevice(hidDevice, disName, featureSet); + temp = new Vader4ProDevice(hidDevice, disName, featureSet, macAddress); break; } diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index 6f13452bc6..bb7f64e04f 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -17,15 +17,17 @@ internal class Vader4ProDevice : DS4Device private uint deltaTimeCurrent = 0; private bool outputDirty = false; private DS4HapticState previousHapticState = new DS4HapticState(); - private byte[] featureReport; private Vader4ProControllerOptions nativeOptionsStore; public Vader4ProControllerOptions NativeOptionsStore { get => nativeOptionsStore; } - public Vader4ProDevice(HidDevice hidDevice, string disName, VidPidFeatureSet featureSet = VidPidFeatureSet.DefaultDS4) : + public Vader4ProDevice(HidDevice hidDevice, string disName, VidPidFeatureSet featureSet = VidPidFeatureSet.DefaultDS4, string macAddress = "") : base(hidDevice, disName, featureSet) { synced = true; - + if (macAddress != "") + { + Mac = macAddress; + } } public override event ReportHandler Report = null; @@ -33,15 +35,15 @@ public Vader4ProDevice(HidDevice hidDevice, string disName, VidPidFeatureSet fea public override event EventHandler ChargingChanged; public override void PostInit() { - Mac = hDevice.GenerateFakeHwSerial(); + if (Mac == null || Mac == "" || Mac == BLANK_SERIAL) + { + Mac = hDevice.GenerateFakeHwSerial(); + } deviceType = InputDeviceType.Vader4Pro; gyroMouseSensSettings = new GyroMouseSens(); - featureReport = new byte[hDevice.Capabilities.FeatureReportByteLength]; inputReport = new byte[hDevice.Capabilities.InputReportByteLength]; - outputReport = new byte[hDevice.Capabilities.OutputReportByteLength]; warnInterval = WARN_INTERVAL_USB; conType = ConnectionType.USB; - } public override bool DisconnectBT(bool callRemoval = false) @@ -79,12 +81,12 @@ public override void StartUpdate() { ds4Input = new Thread(ReadInput); ds4Input.Priority = ThreadPriority.AboveNormal; - ds4Input.Name = "DS3 Input thread: " + Mac; + ds4Input.Name = "Vader 4 Pro Input thread: " + Mac; ds4Input.IsBackground = true; ds4Input.Start(); } else - Console.WriteLine("Thread already running for DS4: " + Mac); + Console.WriteLine("Thread already running for Vader 4 Pro: " + Mac); } private unsafe void ReadInput() diff --git a/DS4Windows/HidLibrary/HidDevice.cs b/DS4Windows/HidLibrary/HidDevice.cs index d8a33a1927..96f16ae42a 100644 --- a/DS4Windows/HidLibrary/HidDevice.cs +++ b/DS4Windows/HidLibrary/HidDevice.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.IO; @@ -326,5 +328,33 @@ public string GenerateFakeHwSerial() return MACAddr; } + public string GetVader4ProMacAddress(int timeout = 1000) + { + byte[] command = new byte[32]; + command[0] = 0x05; + command[1] = 236; + bool writeStatus = WriteOutputReportViaInterrupt(command, timeout); + if (!writeStatus) return GenerateFakeHwSerial(); + var sw = Stopwatch.StartNew(); + byte[] buffer = new byte[this.Capabilities.InputReportByteLength]; + while (sw.ElapsedMilliseconds < timeout) + { + var status = ReadFile(buffer); + + if (status == ReadStatus.Success) + { + if (buffer[1] == 0xFF && buffer[15] == 236) + { + Span macSpan = buffer.AsSpan(5, 4); + return string.Join(":", macSpan.ToArray().Select(b => b.ToString("X2"))); + } + } + else if (status == ReadStatus.ReadError) + { + continue; + } + } + return GenerateFakeHwSerial(); + } } } From 422cbe21efdd23f1ef110ee4167ae57217411d68 Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Mon, 1 Sep 2025 09:07:24 +0300 Subject: [PATCH 08/10] feat: adjustable height for profile button selection --- DS4Windows/DS4Forms/ProfileEditor.xaml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/DS4Windows/DS4Forms/ProfileEditor.xaml b/DS4Windows/DS4Forms/ProfileEditor.xaml index 559fa045f5..3b8d556d9b 100644 --- a/DS4Windows/DS4Forms/ProfileEditor.xaml +++ b/DS4Windows/DS4Forms/ProfileEditor.xaml @@ -36,11 +36,15 @@ - - + + - - + + + + + + @@ -193,7 +197,7 @@ - @@ -205,7 +209,7 @@ - + From c295831323e872e57129983240b9c85ab5aba123 Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Thu, 4 Sep 2025 00:20:04 +0300 Subject: [PATCH 09/10] fix: stop default air mouse --- DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index bb7f64e04f..e3f4826a6c 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -44,6 +44,13 @@ public override void PostInit() inputReport = new byte[hDevice.Capabilities.InputReportByteLength]; warnInterval = WARN_INTERVAL_USB; conType = ConnectionType.USB; + byte[] buf = new byte[32]; + buf[0] = 0x05; + buf[1] = 0x10; + buf[2] = 0x01; + buf[3] = 0x01; + buf[4] = 0x01; + hDevice.WriteOutputReportViaInterrupt(buf, READ_STREAM_TIMEOUT); } public override bool DisconnectBT(bool callRemoval = false) From e6ff193c80943426d996eec342087e84564aec4d Mon Sep 17 00:00:00 2001 From: AhmedAmrNabil Date: Thu, 4 Sep 2025 00:21:49 +0300 Subject: [PATCH 10/10] fix: properly turn off rgb, rumble on StopOutputUpdate --- DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs index e3f4826a6c..ee60e03e6b 100644 --- a/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs +++ b/DS4Windows/DS4Library/InputDevices/Vader4ProDevice.cs @@ -360,7 +360,6 @@ private void WriteReport() } private void SetRumble(byte leftMain, byte rightMain, byte leftTrigger = 0, byte rightTrigger = 0) { - // WriteReport([0x05, 0x0f, leftMain, rightMain, leftTrigger, rightTrigger, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); byte[] buf = new byte[32]; buf[0] = 0x05; buf[1] = 0x0F; @@ -373,7 +372,6 @@ private void SetRumble(byte leftMain, byte rightMain, byte leftTrigger = 0, byte private void SetLed(byte r, byte g, byte b) { - //WriteReport([0x05, 0xe0, r, g, b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); byte[] buf = new byte[32]; buf[0] = 0x05; buf[1] = 0xE0; @@ -392,6 +390,8 @@ protected override void StopOutputUpdate() buf[3] = 0x01; buf[4] = 0x01; hDevice.WriteOutputReportViaInterrupt(buf, READ_STREAM_TIMEOUT); + SetLed(0, 0, 0); + setRumble(0, 0); } }