├── .gitattributes ├── .github └── workflows │ └── github-pages.yml ├── .gitignore ├── DualSenseAPI.sln ├── DualSenseAPI ├── AdaptiveTrigger.cs ├── BatteryStatus.cs ├── DualSense.cs ├── DualSenseAPI.csproj ├── IoMode.cs ├── Led.cs ├── State │ ├── ButtonDeltaState.cs │ ├── DualSenseInputState.cs │ ├── DualSenseInputStateButtonDelta.cs │ ├── DualSenseOutputState.cs │ └── Handlers.cs ├── Touch.cs ├── Util │ ├── ByteConverterExtensions.cs │ ├── CRC32Utils.cs │ └── HidScanner.cs ├── Vector.cs └── docs │ ├── .gitignore │ ├── api │ └── .gitignore │ ├── articles │ ├── intro.md │ └── toc.yml │ ├── docfx.json │ ├── index.md │ └── toc.yml ├── LICENSE ├── README.md └── TestDriver ├── Demo.csproj └── Program.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Docs 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | generate-docs: 19 | # The type of runner that the job will run on 20 | runs-on: windows-latest 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | # setup .NET 29 | - name: Setup .NET 30 | uses: actions/setup-dotnet@v1 31 | with: 32 | dotnet-version: '3.1.x' 33 | 34 | - name: Install dependencies 35 | run: dotnet restore 36 | 37 | # Install DocFX 38 | - name: Setup DocFX 39 | uses: crazy-max/ghaction-chocolatey@v1 40 | with: 41 | args: install docfx 42 | 43 | # Build and publish docs 44 | - name: DocFX build 45 | working-directory: DualSenseAPI/docs 46 | run: docfx docfx.json 47 | continue-on-error: false 48 | - name: Publish 49 | if: github.event_name == 'push' 50 | uses: peaceiris/actions-gh-pages@v3 51 | with: 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | publish_dir: DualSenseAPI/docs/_site 54 | force_orphan: true 55 | 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /DualSenseAPI.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31624.102 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DualSenseAPI", "DualSenseAPI\DualSenseAPI.csproj", "{010C3DB7-BF3B-4480-87C7-C0C72A54CAC0}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "TestDriver\Demo.csproj", "{9FA99037-5652-44AE-AFAB-DE98FB5C7DE3}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {010C3DB7-BF3B-4480-87C7-C0C72A54CAC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {010C3DB7-BF3B-4480-87C7-C0C72A54CAC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {010C3DB7-BF3B-4480-87C7-C0C72A54CAC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {010C3DB7-BF3B-4480-87C7-C0C72A54CAC0}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {9FA99037-5652-44AE-AFAB-DE98FB5C7DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {9FA99037-5652-44AE-AFAB-DE98FB5C7DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {9FA99037-5652-44AE-AFAB-DE98FB5C7DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {9FA99037-5652-44AE-AFAB-DE98FB5C7DE3}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {E21F4229-285A-483C-92D4-681FD323F71E} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /DualSenseAPI/AdaptiveTrigger.cs: -------------------------------------------------------------------------------- 1 | namespace DualSenseAPI 2 | { 3 | /// 4 | /// Trigger effect types 5 | /// 6 | internal enum TriggerEffectType : byte 7 | { 8 | ContinuousResistance = 0x01, 9 | SectionResistance = 0x02, 10 | Vibrate = 0x26, 11 | Calibrate = 0xFC, 12 | Default = 0x00 13 | } 14 | 15 | /// 16 | /// Superclass for all trigger effects. 17 | /// 18 | public class TriggerEffect 19 | { 20 | internal TriggerEffectType InternalEffect { get; private set; } = TriggerEffectType.Default; 21 | // Used for all trigger effects that apply resistance 22 | internal float InternalStartPosition { get; private set; } = 0; 23 | // Used for section resistance 24 | internal float InternalEndPosition { get; private set; } = 0; 25 | // Below properties are for EffectEx only 26 | internal float InternalStartForce { get; private set; } = 0; 27 | internal float InternalMiddleForce { get; private set; } = 0; 28 | internal float InternalEndForce { get; private set; } = 0; 29 | internal bool InternalKeepEffect { get; private set; } = false; 30 | internal byte InternalVibrationFrequency { get; private set; } = 0; 31 | private TriggerEffect() { } 32 | 33 | /// 34 | /// Default trigger effect. No resistance. 35 | /// 36 | public static readonly TriggerEffect Default = new SimpleEffect(TriggerEffectType.Default); 37 | 38 | /// 39 | /// Calibration sequence. 40 | /// 41 | public static readonly TriggerEffect Calibrate = new SimpleEffect(TriggerEffectType.Calibrate); 42 | 43 | /// 44 | /// Simple trigger effect that only sets the mode byte. 45 | /// 46 | private sealed class SimpleEffect : TriggerEffect 47 | { 48 | public SimpleEffect(TriggerEffectType effect) 49 | { 50 | InternalEffect = effect; 51 | } 52 | } 53 | 54 | /// 55 | /// Continuous resistance effect. 56 | /// 57 | public sealed class Continuous : TriggerEffect 58 | { 59 | /// 60 | /// Start position of the resistance, as a percentage (from 0 to 1). 61 | /// 62 | public float StartPosition 63 | { 64 | get { return InternalStartPosition; } 65 | } 66 | 67 | /// 68 | /// The resistance force, as a percentage (from 0 to 1). 69 | /// 70 | public float Force 71 | { 72 | get { return InternalStartForce; } 73 | } 74 | 75 | /// 76 | /// Creates a continuous resistance effect 77 | /// 78 | /// Start position of the resistance, as a percentage (from 0 to 1). 79 | /// The resistance force, as a percentage (from 0 to 1). 80 | public Continuous(float startPosition, float forcePercentage) 81 | { 82 | InternalEffect = TriggerEffectType.ContinuousResistance; 83 | InternalStartPosition = startPosition; 84 | InternalStartForce = forcePercentage; 85 | } 86 | } 87 | 88 | /// 89 | /// Effect that applies resistance on a section of the trigger. 90 | /// 91 | public sealed class Section : TriggerEffect 92 | { 93 | /// 94 | /// The start position of the resistance, as a percentage (from 0 to 1). 95 | /// 96 | public float StartPosition 97 | { 98 | get { return InternalStartPosition; } 99 | } 100 | 101 | /// 102 | /// The end position of the resistance, as a percentage (from 0 to 1). 103 | /// 104 | public float EndPosition 105 | { 106 | get { return InternalEndPosition; } 107 | } 108 | 109 | /// 110 | /// Creates a section resistance effect. 111 | /// 112 | /// The start position of the resistance, as a percentage (from 0 to 1). 113 | /// The end position of the resistance, as a percentage (from 0 to 1). 114 | public Section(float startPosition, float endPosition) 115 | { 116 | InternalEffect = TriggerEffectType.SectionResistance; 117 | InternalStartPosition = startPosition; 118 | InternalEndPosition = endPosition; 119 | } 120 | } 121 | 122 | /// 123 | /// Vibration effect. 124 | /// 125 | public sealed class Vibrate : TriggerEffect 126 | { 127 | /// 128 | /// The force at the start of the press, as a percentage (from 0 to 1). 129 | /// 130 | /// 131 | /// The start of the trigger press is roughly when the trigger value is between 0 and 0.5. 132 | /// However, the user-perceived end position may not be exactly 0.5 as the trigger will be vibrating. 133 | /// 134 | public float StartForce { get { return InternalStartForce; } } 135 | 136 | /// 137 | /// The force at the middle of the press, as a percentage (from 0 to 1). 138 | /// 139 | /// 140 | /// The start of the trigger press is roughly when the trigger value is between 0.5 and 1. 141 | /// However, the user-perceived start position may not be exactly 0.5 as the trigger will be vibrating. 142 | /// 143 | public float MiddleForce { get { return InternalMiddleForce; } } 144 | 145 | /// 146 | /// The force at the end of the press, as a percentage (from 0 to 1). Requires to be set. 147 | /// 148 | /// 149 | /// There is a slight gap between when the trigger value hits 1 and when this force starts. This can lead to a small 150 | /// region where there is no effect playing; be mindful of this when creating your effects. 151 | /// 152 | public float EndForce { get { return InternalEndForce; } } 153 | 154 | /// 155 | /// Whether to enable to effect after the trigger is fully pressed. 156 | /// 157 | public bool KeepEffect { get { return InternalKeepEffect; } } 158 | 159 | /// 160 | /// The vibration frequency in hertz. 161 | /// 162 | public byte VibrationFrequency { get { return InternalVibrationFrequency; } } 163 | 164 | /// 165 | /// Creates a vibration trigger effect. 166 | /// 167 | /// The vibration frequency in hertz. 168 | /// The force at the start of the press, as a percentage (from 0 to 1). 169 | /// The force at the middle of the press, as a percentage (from 0 to 1). 170 | /// The force at the end of the press, as a percentage (from 0 to 1). 171 | /// Requires to be set. 172 | /// Whether to enable the effect after the trigger is fully pressed. 173 | public Vibrate(byte vibrationFreqHz, float startForce, float middleForce, float endForce, bool keepEffect = true) 174 | { 175 | InternalEffect = TriggerEffectType.Vibrate; 176 | InternalStartForce = startForce; 177 | InternalMiddleForce = middleForce; 178 | InternalEndForce = endForce; 179 | InternalKeepEffect = keepEffect; 180 | InternalVibrationFrequency = vibrationFreqHz; 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /DualSenseAPI/BatteryStatus.cs: -------------------------------------------------------------------------------- 1 | namespace DualSenseAPI 2 | { 3 | /// 4 | /// The status of a DualSense battery. 5 | /// 6 | public struct BatteryStatus 7 | { 8 | /// 9 | /// Whether the battery is currently charging. 10 | /// 11 | public bool IsCharging; 12 | 13 | /// 14 | /// Whether the battery is done charging. 15 | /// 16 | public bool IsFullyCharged; 17 | 18 | /// 19 | /// The level of the battery, from 1 to 10. 20 | /// 21 | /// 22 | /// Typically, is set sometime when this is between 8 and 10. 23 | /// Exactly when the flag is set varies and is likely due to the battery's overcharge protection. 24 | /// 25 | public float Level; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DualSenseAPI/DualSense.cs: -------------------------------------------------------------------------------- 1 | using Device.Net; 2 | using DualSenseAPI.State; 3 | using DualSenseAPI.Util; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reactive.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace DualSenseAPI 12 | { 13 | /// 14 | /// Interaction logic for DualSense controllers. 15 | /// 16 | public class DualSense 17 | { 18 | // IO parameters 19 | private readonly IDevice underlyingDevice; 20 | private readonly int? readBufferSize; 21 | private readonly int? writeBufferSize; 22 | 23 | // async polling 24 | private IDisposable? pollerSubscription; 25 | 26 | /// 27 | /// State event handler for asynchronous polling. 28 | /// 29 | /// 30 | /// 31 | public event StatePolledHandler? OnStatePolled; 32 | 33 | /// 34 | /// Button state changed event handler for asynchronous polling. 35 | /// 36 | /// 37 | /// 38 | public event ButtonStateChangedHandler? OnButtonStateChanged; 39 | 40 | /// 41 | /// The I/O mode the controller is connected by. 42 | /// 43 | public IoMode IoMode { get; private set; } 44 | 45 | /// 46 | /// Configurable dead zone for gamepad joysticks. A joystick axis with magnitude less than this value will 47 | /// be returned as 0. 48 | /// 49 | public float JoystickDeadZone { get; set; } = 0; 50 | 51 | /// 52 | /// This controller's output state. 53 | /// 54 | public DualSenseOutputState OutputState { get; set; } = new DualSenseOutputState(); 55 | 56 | /// 57 | /// This controller's most recently polled input state. 58 | /// 59 | public DualSenseInputState InputState { get; private set; } = new DualSenseInputState(); 60 | 61 | /// 62 | /// Private constructor for . 63 | /// 64 | /// The underlying low-level device. 65 | /// The device's declared read buffer size. 66 | /// The device's declared write buffer size. 67 | private DualSense(IDevice underlyingDevice, int? readBufferSize, int? writeBufferSize) 68 | { 69 | this.underlyingDevice = underlyingDevice; 70 | this.readBufferSize = readBufferSize; 71 | this.writeBufferSize = writeBufferSize; 72 | IoMode = readBufferSize switch 73 | { 74 | 64 => IoMode.USB, 75 | 78 => IoMode.Bluetooth, 76 | _ => IoMode.Unknown 77 | }; 78 | if (IoMode == IoMode.Unknown) 79 | { 80 | throw new InvalidOperationException("Can't initialize device - supported IO modes are USB and Bluetooth."); 81 | } 82 | } 83 | 84 | /// 85 | /// Acquires the controller. 86 | /// 87 | public void Acquire() 88 | { 89 | if (!underlyingDevice.IsInitialized) 90 | { 91 | underlyingDevice.InitializeAsync().Wait(); 92 | } 93 | } 94 | 95 | /// 96 | /// Releases the controller. 97 | /// 98 | public void Release() 99 | { 100 | if (underlyingDevice.IsInitialized) 101 | { 102 | underlyingDevice.Close(); 103 | } 104 | } 105 | 106 | private async Task ReadWriteOnceAsync() 107 | { 108 | TransferResult result = await underlyingDevice.WriteAndReadAsync(GetOutputDataBytes()); 109 | if (result.BytesTransferred == readBufferSize) 110 | { 111 | // this can effectively determine which input packet you've recieved, USB or bluetooth, and offset by the right amount 112 | int offset = result.Data[0] switch 113 | { 114 | 0x01 => 1, // USB packet flag 115 | 0x31 => 2, // Bluetooth packet flag 116 | _ => 0 117 | }; 118 | return new DualSenseInputState(result.Data.Skip(offset).ToArray(), IoMode, JoystickDeadZone); 119 | } 120 | else 121 | { 122 | throw new IOException("Failed to read data - buffer size mismatch"); 123 | } 124 | } 125 | 126 | /// 127 | /// Updates the input and output states once. This operation is blocking. 128 | /// 129 | /// The polled state, for convenience. This is also updated on the controller instance. 130 | public DualSenseInputState ReadWriteOnce() 131 | { 132 | Task stateTask = ReadWriteOnceAsync(); 133 | stateTask.Wait(); 134 | InputState = stateTask.Result; 135 | return InputState; 136 | } 137 | 138 | /// 139 | /// Process a state event. Wraps around user-provided handler since Reactive needs an Action<>. 140 | /// 141 | /// The receieved input state 142 | private void ProcessEachState(DualSenseInputState nextState) 143 | { 144 | DualSenseInputState prevState = InputState; 145 | InputState = nextState; 146 | // don't take up the burden to diff the changes unless someone cares 147 | if (OnButtonStateChanged != null) 148 | { 149 | DualSenseInputStateButtonDelta delta = new DualSenseInputStateButtonDelta(prevState, nextState); 150 | if (delta.HasChanges) 151 | { 152 | OnButtonStateChanged.Invoke(this, delta); 153 | } 154 | } 155 | OnStatePolled?.Invoke(this); 156 | } 157 | 158 | /// 159 | /// Begins asynchously updating the output state and polling the input state at the specified interval. 160 | /// 161 | /// How long to wait between each I/O loop, in milliseconds 162 | /// 163 | /// Instance state is not thread safe. In other words, when using polling, updating instance state 164 | /// (such as ) both inside and outside of 165 | /// may create unexpected results. When using polling, it is generally expected you will only make 166 | /// modifications to state inside the handler in response to input, or 167 | /// outside of the handler in response to external events (for example, game logic). It's also 168 | /// expected that you will only use the instance passed as an argument to 169 | /// the sender, rather than external references to instance. 170 | /// 171 | public void BeginPolling(uint pollingIntervalMs) 172 | { 173 | if (pollerSubscription != null) 174 | { 175 | throw new InvalidOperationException("Can't begin polling after it's already started."); 176 | } 177 | 178 | IObservable stateObserver = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMilliseconds(pollingIntervalMs)) 179 | .SelectMany(Observable.FromAsync(() => ReadWriteOnceAsync())); 180 | // TODO: figure how we can leverage DistinctUntilChanged (or similar) so we can do filtered eventing (e.g. button pressed only) 181 | // how would we allow both to modify state in a smart way (i.e. without overriding each other?) if needed? 182 | // this also applies for consumers - logically they should not need to worry about race conditions if they're subscribing to both 183 | 184 | pollerSubscription = stateObserver.Subscribe(ProcessEachState); 185 | } 186 | 187 | /// 188 | /// Stop asynchronously updating the output state and polling for new inputs. 189 | /// 190 | public void EndPolling() 191 | { 192 | if (pollerSubscription == null) 193 | { 194 | throw new InvalidOperationException("Can't end polling without starting polling first"); 195 | } 196 | pollerSubscription.Dispose(); 197 | pollerSubscription = null; 198 | } 199 | 200 | /// 201 | /// Builds the output byte array that will be sent to the controller. 202 | /// 203 | /// An array of bytes to send to the controller 204 | private byte[] GetOutputDataBytes() 205 | { 206 | byte[] bytes = new byte[writeBufferSize ?? 0]; 207 | byte[] hidBuffer = OutputState.BuildHidOutputBuffer(); 208 | if (IoMode == IoMode.USB) 209 | { 210 | bytes[0] = 0x02; 211 | Array.Copy(hidBuffer, 0, bytes, 1, 47); 212 | } 213 | else if (IoMode == IoMode.Bluetooth) 214 | { 215 | bytes[0] = 0x31; 216 | bytes[1] = 0x02; 217 | Array.Copy(hidBuffer, 0, bytes, 2, 47); 218 | // make a 32 bit checksum of the first 74 bytes and add it at the end 219 | uint crcChecksum = CRC32Utils.ComputeCRC32(bytes, 74); 220 | byte[] checksumBytes = BitConverter.GetBytes(crcChecksum); 221 | Array.Copy(checksumBytes, 0, bytes, 74, 4); 222 | } 223 | else 224 | { 225 | throw new InvalidOperationException("Can't send data - supported IO modes are USB and Bluetooth."); 226 | } 227 | return bytes; 228 | } 229 | 230 | public override string ToString() 231 | { 232 | return $"DualSense Controller ({IoMode})"; 233 | } 234 | 235 | /// 236 | /// Enumerates available controllers. 237 | /// 238 | /// Enumerable of available controllers. 239 | public static IEnumerable EnumerateControllers() 240 | { 241 | foreach (ConnectedDeviceDefinition deviceDefinition in HidScanner.Instance.ListDevices()) 242 | { 243 | IDevice device = HidScanner.Instance.GetConnectedDevice(deviceDefinition); 244 | yield return new DualSense(device, deviceDefinition.ReadBufferSize, deviceDefinition.WriteBufferSize); 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /DualSenseAPI/DualSenseAPI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | enable 6 | The-Demp 7 | 8 | false 9 | MIT 10 | README.md 11 | (c) 2021 The-Demp 12 | A .NET standard API for interacting with DualSense controllers 13 | DualSense PS5 PlayStation 14 | https://github.com/The-Demp/DualSenseAPI 15 | 1.0.2 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /DualSenseAPI/IoMode.cs: -------------------------------------------------------------------------------- 1 | namespace DualSenseAPI 2 | { 3 | /// 4 | /// Available I/O connectivity modes for a DualSense controller. 5 | /// 6 | public enum IoMode 7 | { 8 | /// 9 | /// Connected via Bluetooth. 10 | /// 11 | Bluetooth, 12 | 13 | /// 14 | /// Connected via USB. 15 | /// 16 | USB, 17 | 18 | /// 19 | /// The connection type could not be identified. 20 | /// 21 | Unknown 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DualSenseAPI/Led.cs: -------------------------------------------------------------------------------- 1 | namespace DualSenseAPI 2 | { 3 | /// 4 | /// Flags for the player LEDs. 5 | /// 6 | /// 7 | public enum PlayerLed 8 | { 9 | /// 10 | /// LEDs off. 11 | /// 12 | None = 0x00, 13 | 14 | /// 15 | /// Leftmost LED on. 16 | /// 17 | Left = 0x01, 18 | 19 | /// 20 | /// Middle-left LED on. 21 | /// 22 | MiddleLeft = 0x02, 23 | 24 | /// 25 | /// Middle LED on. 26 | /// 27 | Middle = 0x04, 28 | 29 | /// 30 | /// Middle-right LED on. 31 | /// 32 | MiddleRight = 0x08, 33 | 34 | /// 35 | /// Rightmost LED on. 36 | /// 37 | Right = 0x10, 38 | 39 | /// 40 | /// Standard LEDs for player 1. 41 | /// 42 | Player1 = Middle, 43 | 44 | /// 45 | /// Standard LEDs for player 2. 46 | /// 47 | Player2 = MiddleLeft | MiddleRight, 48 | 49 | /// 50 | /// Standard LEDs for player 3. 51 | /// 52 | Player3 = Left | Middle | Right, 53 | 54 | /// 55 | /// Standard LEDs for player 4. 56 | /// 57 | Player4 = Left | MiddleLeft | MiddleRight | Right, 58 | 59 | /// 60 | /// All LEDs on. 61 | /// 62 | All = Left | MiddleLeft | Middle | MiddleRight | Right 63 | } 64 | 65 | /// 66 | /// The brightness of the player LEDs. 67 | /// 68 | /// 69 | public enum PlayerLedBrightness 70 | { 71 | /// 72 | /// Low LED brightness. 73 | /// 74 | Low = 0x02, 75 | 76 | /// 77 | /// Medium LED brightness. 78 | /// 79 | Medium = 0x01, 80 | 81 | /// 82 | /// High LED brightness. 83 | /// 84 | High = 0x00 85 | } 86 | 87 | /// 88 | /// Behavior options for the controller's lightbar. 89 | /// 90 | /// 91 | public enum LightbarBehavior 92 | { 93 | /// 94 | /// Default behavior. Pulses the lightbar blue and stays on. 95 | /// 96 | PulseBlue = 0x1, 97 | /// 98 | /// Allows the lightbar to be set a custom color. 99 | /// 100 | CustomColor = 0x2 101 | } 102 | 103 | /// 104 | /// Color of the controller's lightbar. 105 | /// 106 | /// 107 | public struct LightbarColor 108 | { 109 | /// 110 | /// The red component of the color as a percentage (0 to 1). 111 | /// 112 | public float R; 113 | 114 | /// 115 | /// The green component of the color as a percentage (0 to 1). 116 | /// 117 | public float G; 118 | 119 | /// 120 | /// The blue component of the color as a percentage (0 to 1). 121 | /// 122 | public float B; 123 | 124 | /// 125 | /// Creates a LightbarColor. 126 | /// 127 | /// The red component of the color as a percentage (0 to 1). 128 | /// The green component of the color as a percentage (0 to 1). 129 | /// The blue component of the color as a percentage (0 to 1). 130 | public LightbarColor(float r, float g, float b) 131 | { 132 | R = r; 133 | G = g; 134 | B = b; 135 | } 136 | } 137 | 138 | /// 139 | /// Behavior options for the mic mute button LED. 140 | /// 141 | public enum MicLed 142 | { 143 | /// 144 | /// The LED is off. 145 | /// 146 | Off = 0, 147 | 148 | /// 149 | /// The LED is solid on. 150 | /// 151 | On = 1, 152 | 153 | /// 154 | /// The LED slowly pulses between dim and bright. 155 | /// 156 | Pulse = 2 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /DualSenseAPI/State/ButtonDeltaState.cs: -------------------------------------------------------------------------------- 1 | namespace DualSenseAPI.State 2 | { 3 | public enum ButtonDeltaState 4 | { 5 | Pressed, Released, NoChange 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /DualSenseAPI/State/DualSenseInputState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DualSenseAPI.Util; 4 | 5 | namespace DualSenseAPI.State 6 | { 7 | /// 8 | /// All available input variables for a DualSense controller. 9 | /// 10 | public class DualSenseInputState 11 | { 12 | /// 13 | /// Default constructor, initializes all fields to 0/false/default 14 | /// 15 | internal DualSenseInputState() { } 16 | 17 | /// 18 | /// Constructs a DualSenseInputState. Parses the HID input report. 19 | /// 20 | /// The data buffer. 21 | /// The DualSense's input mode. 22 | /// The DualSense's joystick deadzone. 23 | internal DualSenseInputState(byte[] data, IoMode inputMode, float deadZone) 24 | { 25 | // Analog inputs 26 | LeftAnalogStick = ReadAnalogStick(data[0], data[1], deadZone); 27 | RightAnalogStick = ReadAnalogStick(data[2], data[3], deadZone); 28 | L2 = GetModeSwitch(inputMode, data, 4, 7).ToUnsignedFloat(); 29 | R2 = GetModeSwitch(inputMode, data, 5, 8).ToUnsignedFloat(); 30 | 31 | // Buttons 32 | byte btnBlock1 = GetModeSwitch(inputMode, data, 7, 4); 33 | byte btnBlock2 = GetModeSwitch(inputMode, data, 8, 5); 34 | byte btnBlock3 = GetModeSwitch(inputMode, data, 9, 6); 35 | SquareButton = btnBlock1.HasFlag(0x10); 36 | CrossButton = btnBlock1.HasFlag(0x20); 37 | CircleButton = btnBlock1.HasFlag(0x40); 38 | TriangleButton = btnBlock1.HasFlag(0x80); 39 | DPadUpButton = ReadDPadButton(btnBlock1, 0, 1, 7); 40 | DPadRightButton = ReadDPadButton(btnBlock1, 1, 2, 3); 41 | DPadDownButton = ReadDPadButton(btnBlock1, 3, 4, 5); 42 | DPadLeftButton = ReadDPadButton(btnBlock1, 5, 6, 7); 43 | L1Button = btnBlock2.HasFlag(0x01); 44 | R1Button = btnBlock2.HasFlag(0x02); 45 | L2Button = btnBlock2.HasFlag(0x04); 46 | R2Button = btnBlock2.HasFlag(0x08); 47 | CreateButton = btnBlock2.HasFlag(0x10); 48 | MenuButton = btnBlock2.HasFlag(0x20); 49 | L3Button = btnBlock2.HasFlag(0x40); 50 | R3Button = btnBlock2.HasFlag(0x80); 51 | LogoButton = btnBlock3.HasFlag(0x01); 52 | TouchpadButton = btnBlock3.HasFlag(0x02); 53 | MicButton = GetModeSwitch(inputMode, data, 9, -1).HasFlag(0x04); // not supported on the broken BT protocol, otherwise would likely be in btnBlock3 54 | 55 | // Multitouch 56 | Touchpad1 = ReadTouchpad(GetModeSwitch(inputMode, data, 32, -1, 4)); 57 | Touchpad2 = ReadTouchpad(GetModeSwitch(inputMode, data, 36, -1, 4)); 58 | 59 | // 6-axis 60 | // gyro directions seem to follow left-hand rule rather than right, so reverse the directions 61 | Gyro = -ReadAccelAxes( 62 | GetModeSwitch(inputMode, data, 15, -1, 2), 63 | GetModeSwitch(inputMode, data, 17, -1, 2), 64 | GetModeSwitch(inputMode, data, 19, -1, 2) 65 | ); 66 | Accelerometer = ReadAccelAxes( 67 | GetModeSwitch(inputMode, data, 21, -1, 2), 68 | GetModeSwitch(inputMode, data, 23, -1, 2), 69 | GetModeSwitch(inputMode, data, 25, -1, 2) 70 | ); 71 | 72 | // Misc 73 | byte batteryByte = GetModeSwitch(inputMode, data, 52, -1); 74 | byte miscByte = GetModeSwitch(inputMode, data, 53, -1); // this contains various stuff, seems to have both audio and battery info 75 | BatteryStatus = new BatteryStatus 76 | { 77 | IsCharging = batteryByte.HasFlag(0x10), 78 | IsFullyCharged = batteryByte.HasFlag(0x20), 79 | Level = (byte)(batteryByte & 0x0F) 80 | }; 81 | IsHeadphoneConnected = miscByte.HasFlag(0x01); 82 | } 83 | 84 | // TODO: find a way to differentiate between the "valid" and "broken" BT states - now that stuff is working right, 85 | // we can always take the USB index. Hold the other one for when we can fix this (or prove it can't break) 86 | // this seems to be a discovery issue of some kind, other things (like steam and ds4windows) have no problem finding it. 87 | // seems to be fixed permanently after using DS4Windows but ideally we shouldn't have to have that precondition, 88 | // and steam was able to handle it fine before that 89 | /// 90 | /// Gets a data byte at the given index based on the input mode. 91 | /// 92 | /// The current input mode. 93 | /// The data bytes to read from. 94 | /// The index to access in USB or valid Bluetooth input mode. 95 | /// The index to access in the broken Bluetooth input mode. 96 | /// 97 | /// The data at the given index for the input mode, or 0 if the index is negative (allows defaults for 98 | /// values that aren't supported in a given mode). 99 | /// 100 | /// 101 | /// This was due to a previous issue where controllers connected over Bluetooth were providing data bytes 102 | /// in a different order with some data missing. It resolved itself before I could solve the problem but 103 | /// keeping this around for when I can find it again. Currently always uses . 104 | /// 105 | private byte GetModeSwitch(IoMode inputMode, byte[] data, int indexIfUsb, int indexIfBt) 106 | { 107 | return indexIfUsb >= 0 ? data[indexIfUsb] : (byte)0; 108 | //return InputMode switch 109 | //{ 110 | // InputMode.USB => indexIfUsb >= 0 ? readData[indexIfUsb] : (byte)0, 111 | // InputMode.Bluetooth => indexIfBt >= 0 ? readData[indexIfBt] : (byte)0, 112 | // _ => throw new InvalidOperationException("") 113 | //}; 114 | } 115 | 116 | /// 117 | /// Gets several data bytes at the given index based on the input mode. 118 | /// 119 | /// The current input mode. 120 | /// The data bytes to read from. 121 | /// The start index in USB or valid Bluetooth input mode. 122 | /// The start index in the broken Bluetooth input mode. 123 | /// The number of bytes to get. 124 | /// 125 | /// bytes at the given start index for the input mode, or an array of 126 | /// 0's if the index is negative. 127 | /// 128 | /// 129 | /// This was due to a previous issue where controllers connected over Bluetooth were providing data bytes 130 | /// in a different order with some data missing. It resolved itself before I could solve the problem but 131 | /// keeping this around for when I can find it again. Currently always uses . 132 | /// 133 | private byte[] GetModeSwitch(IoMode inputMode, byte[] data, int startIndexIfUsb, int startIndexIfBt, int size) 134 | { 135 | return startIndexIfUsb >= 0 ? data.Skip(startIndexIfUsb).Take(size).ToArray() : new byte[size]; 136 | //return InputMode switch 137 | //{ 138 | // InputMode.USB => startIndexIfUsb >= 0 ? readData.Skip(startIndexIfUsb).Take(size).ToArray() : new byte[size], 139 | // InputMode.Bluetooth => startIndexIfBt >= 0 ? readData.Skip(startIndexIfBt).Take(size).ToArray() : new byte[size], 140 | // _ => throw new InvalidOperationException("") 141 | //}; 142 | } 143 | 144 | /// 145 | /// Reads the 2 bytes of an analog stick and silences the dead zone. 146 | /// 147 | /// The x byte. 148 | /// The y byte. 149 | /// A vector for the joystick input. 150 | private Vec2 ReadAnalogStick(byte x, byte y, float deadZone) 151 | { 152 | float x1 = x.ToSignedFloat(); 153 | float y1 = -y.ToSignedFloat(); 154 | return new Vec2 155 | { 156 | X = Math.Abs(x1) >= deadZone ? x1 : 0, 157 | Y = Math.Abs(y1) >= deadZone ? y1 : 0 158 | }; 159 | } 160 | 161 | /// 162 | /// Checks if the DPad lower nibble is one of the 3 values possible for a button. 163 | /// 164 | /// The dpad byte. 165 | /// The first value. 166 | /// The second value. 167 | /// The third value. 168 | /// Whether the lower nibble of is one of the 3 values. 169 | private static bool ReadDPadButton(byte b, int v1, int v2, int v3) 170 | { 171 | int val = b & 0x0F; 172 | return val == v1 || val == v2 || val == v3; 173 | } 174 | 175 | /// 176 | /// Reads a touchpad. 177 | /// 178 | /// The touchpad's byte array. 179 | /// A parsed . 180 | private static Touch ReadTouchpad(byte[] bytes) 181 | { 182 | // force everything into the right byte order; input bytes are LSB-first 183 | if (!BitConverter.IsLittleEndian) 184 | { 185 | bytes = bytes.Reverse().ToArray(); 186 | } 187 | uint raw = BitConverter.ToUInt32(bytes); 188 | return new Touch 189 | { 190 | X = (raw & 0x000FFF00) >> 8, 191 | Y = (raw & 0xFFF00000) >> 20, 192 | IsDown = (raw & 128) == 0, 193 | Id = bytes[0] 194 | }; 195 | } 196 | 197 | /// 198 | /// Reads 3 axes of the accellerometer. 199 | /// 200 | /// The X axis bytes. 201 | /// The Y axis bytes. 202 | /// The Z axis bytes. 203 | /// A vector for the gyro axes. 204 | private static Vec3 ReadAccelAxes(byte[] x, byte[] y, byte[] z) 205 | { 206 | // force everything into the right byte order; assuming that input bytes is little-endian 207 | if (!BitConverter.IsLittleEndian) 208 | { 209 | x = x.Reverse().ToArray(); 210 | y = y.Reverse().ToArray(); 211 | z = z.Reverse().ToArray(); 212 | } 213 | return new Vec3 214 | { 215 | X = -BitConverter.ToInt16(x), 216 | Y = BitConverter.ToInt16(y), 217 | Z = BitConverter.ToInt16(z) 218 | }; 219 | } 220 | 221 | /// 222 | /// The left analog stick. Values are from -1 to 1. Positive X is right, positive Y is up. 223 | /// 224 | public Vec2 LeftAnalogStick { get; private set; } 225 | 226 | /// 227 | /// The right analog stick. Values are from -1 to 1. Positive X is right, positive Y is up. 228 | /// 229 | public Vec2 RightAnalogStick { get; private set; } 230 | 231 | /// 232 | /// L2's analog value, from 0 to 1. 233 | /// 234 | public float L2 { get; private set; } 235 | 236 | /// 237 | /// R2's analog value, from 0 to 1. 238 | /// 239 | public float R2 { get; private set; } 240 | 241 | /// 242 | /// The status of the square button. 243 | /// 244 | public bool SquareButton { get; private set; } 245 | 246 | /// 247 | /// The status of the cross button. 248 | /// 249 | public bool CrossButton { get; private set; } 250 | 251 | /// 252 | /// The status of the circle button. 253 | /// 254 | public bool CircleButton { get; private set; } 255 | 256 | /// 257 | /// The status of the triangle button. 258 | /// 259 | public bool TriangleButton { get; private set; } 260 | 261 | /// 262 | /// The status of the D-pad up button. 263 | /// 264 | public bool DPadUpButton { get; private set; } 265 | 266 | /// 267 | /// The status of the D-pad right button. 268 | /// 269 | public bool DPadRightButton { get; private set; } 270 | 271 | /// 272 | /// The status of the D-pad down button. 273 | /// 274 | public bool DPadDownButton { get; private set; } 275 | 276 | /// 277 | /// The status of the D-pad left button. 278 | /// 279 | public bool DPadLeftButton { get; private set; } 280 | 281 | /// 282 | /// The status of the L1 button. 283 | /// 284 | public bool L1Button { get; private set; } 285 | 286 | /// 287 | /// The status of the R1 button. 288 | /// 289 | public bool R1Button { get; private set; } 290 | 291 | /// 292 | /// The status of the L2 button. 293 | /// 294 | public bool L2Button { get; private set; } 295 | 296 | /// 297 | /// The status of the R2 button. 298 | /// 299 | public bool R2Button { get; private set; } 300 | 301 | /// 302 | /// The status of the create button. 303 | /// 304 | public bool CreateButton { get; private set; } 305 | 306 | /// 307 | /// The status of the menu button. 308 | /// 309 | public bool MenuButton { get; private set; } 310 | 311 | /// 312 | /// The status of the L3 button. 313 | /// 314 | public bool L3Button { get; private set; } 315 | 316 | /// 317 | /// The status of the R3 button. 318 | /// 319 | public bool R3Button { get; private set; } 320 | 321 | /// 322 | /// The status of the PlayStation logo button. 323 | /// 324 | public bool LogoButton { get; private set; } 325 | 326 | /// 327 | /// The status of the touchpad button. 328 | /// 329 | public bool TouchpadButton { get; private set; } 330 | 331 | /// 332 | /// The status of the mic button. 333 | /// 334 | public bool MicButton { get; private set; } 335 | 336 | /// 337 | /// The first touch point. 338 | /// 339 | public Touch Touchpad1 { get; private set; } 340 | 341 | /// 342 | /// The second touch point. 343 | /// 344 | public Touch Touchpad2 { get; private set; } 345 | 346 | /// 347 | /// The accelerometer's rotational axes. The directions of the axes have been slightly adjusted from the controller's original values 348 | /// to make them behave nicer with standard Newtonian physics. The signs follow normal right-hand rule with respect to 349 | /// 's axes, e.g. +X rotation means counterclockwise around the +X axis and so on. Unit is unclear, but 350 | /// magnitude while stationary is about 0. 351 | /// 352 | public Vec3 Gyro { get; private set; } 353 | 354 | /// 355 | /// The accelerometer's linear axes. The directions of the axes have been slightly adjusted from the controller's original values 356 | /// to make them behave nicer with standard Newtonian physics. +X is to the right. +Y is behind the controller (roughly straight down 357 | /// if the controller is flat on the table). +Z is at the top of the controller (where the USB port is). Unit is unclear, but magnitude 358 | /// while stationary (e.g. just gravity) is about 8000 +- 100. 359 | /// 360 | public Vec3 Accelerometer { get; private set; } 361 | 362 | /// 363 | /// The status of the battery. 364 | /// 365 | public BatteryStatus BatteryStatus { get; private set; } 366 | 367 | /// 368 | /// Whether or not headphones are connected to the controller. 369 | /// 370 | public bool IsHeadphoneConnected { get; private set; } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /DualSenseAPI/State/DualSenseInputStateButtonDelta.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | 6 | namespace DualSenseAPI.State 7 | { 8 | public class DualSenseInputStateButtonDelta 9 | { 10 | /// 11 | /// The change status of the square button. 12 | /// 13 | public ButtonDeltaState SquareButton { get; private set; } = ButtonDeltaState.NoChange; 14 | 15 | /// 16 | /// The change status of the cross button. 17 | /// 18 | public ButtonDeltaState CrossButton { get; private set; } = ButtonDeltaState.NoChange; 19 | 20 | /// 21 | /// The change status of the circle button. 22 | /// 23 | public ButtonDeltaState CircleButton { get; private set; } = ButtonDeltaState.NoChange; 24 | 25 | /// 26 | /// The change status of the triangle button. 27 | /// 28 | public ButtonDeltaState TriangleButton { get; private set; } = ButtonDeltaState.NoChange; 29 | 30 | /// 31 | /// The change status of the D-pad up button. 32 | /// 33 | public ButtonDeltaState DPadUpButton { get; private set; } = ButtonDeltaState.NoChange; 34 | 35 | /// 36 | /// The change status of the D-pad right button. 37 | /// 38 | public ButtonDeltaState DPadRightButton { get; private set; } = ButtonDeltaState.NoChange; 39 | 40 | /// 41 | /// The change status of the D-pad down button. 42 | /// 43 | public ButtonDeltaState DPadDownButton { get; private set; } = ButtonDeltaState.NoChange; 44 | 45 | /// 46 | /// The change status of the D-pad left button. 47 | /// 48 | public ButtonDeltaState DPadLeftButton { get; private set; } = ButtonDeltaState.NoChange; 49 | 50 | /// 51 | /// The change status of the L1 button. 52 | /// 53 | public ButtonDeltaState L1Button { get; private set; } = ButtonDeltaState.NoChange; 54 | 55 | /// 56 | /// The change status of the R1 button. 57 | /// 58 | public ButtonDeltaState R1Button { get; private set; } = ButtonDeltaState.NoChange; 59 | 60 | /// 61 | /// The change status of the L2 button. 62 | /// 63 | public ButtonDeltaState L2Button { get; private set; } = ButtonDeltaState.NoChange; 64 | 65 | /// 66 | /// The change status of the R2 button. 67 | /// 68 | public ButtonDeltaState R2Button { get; private set; } = ButtonDeltaState.NoChange; 69 | 70 | /// 71 | /// The change status of the create button. 72 | /// 73 | public ButtonDeltaState CreateButton { get; private set; } = ButtonDeltaState.NoChange; 74 | 75 | /// 76 | /// The change status of the menu button. 77 | /// 78 | public ButtonDeltaState MenuButton { get; private set; } = ButtonDeltaState.NoChange; 79 | 80 | /// 81 | /// The change status of the L3 button. 82 | /// 83 | public ButtonDeltaState L3Button { get; private set; } = ButtonDeltaState.NoChange; 84 | 85 | /// 86 | /// The change status of the R2 button. 87 | /// 88 | public ButtonDeltaState R3Button { get; private set; } = ButtonDeltaState.NoChange; 89 | 90 | /// 91 | /// The change status of the PlayStation logo button. 92 | /// 93 | public ButtonDeltaState LogoButton { get; private set; } = ButtonDeltaState.NoChange; 94 | 95 | /// 96 | /// The change status of the touchpad button. 97 | /// 98 | public ButtonDeltaState TouchpadButton { get; private set; } = ButtonDeltaState.NoChange; 99 | 100 | /// 101 | /// The change status of the mic button. 102 | /// 103 | public ButtonDeltaState MicButton { get; private set; } = ButtonDeltaState.NoChange; 104 | 105 | /// 106 | /// Whether the delta has any changes. 107 | /// 108 | public bool HasChanges { get; private set; } = false; 109 | 110 | private static readonly List<(PropertyInfo delta, PropertyInfo state)> propertyPairData; 111 | 112 | static DualSenseInputStateButtonDelta() 113 | { 114 | // we know some key things here: 115 | // - on the input state, all the types of button properties are boolean. 116 | // - on the delta, all the types of the button properties are ButtonDeltaState. 117 | // - all the properties of button delta are named the same as the properties on input state - it's a subset. 118 | 119 | // since reflection can be a bit heavy, we'll incur this burden only once at startup so we can get the necessary property info for comparison 120 | 121 | PropertyInfo[] deltaProperties = typeof(DualSenseInputStateButtonDelta).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); 122 | propertyPairData = deltaProperties 123 | .Where(x => x.PropertyType == typeof(ButtonDeltaState)) 124 | .Select(x => (x, typeof(DualSenseInputState).GetProperty(x.Name)!)).ToList(); 125 | } 126 | 127 | /// 128 | /// Internal constructor for a button delta. Diffs previous and next state. 129 | /// 130 | /// The previous/old input state. 131 | /// The next/new input state. 132 | internal DualSenseInputStateButtonDelta(DualSenseInputState prevState, DualSenseInputState nextState) 133 | { 134 | foreach (var (delta, state) in propertyPairData) 135 | { 136 | if (state.GetValue(prevState) is bool oldVal && state.GetValue(nextState) is bool newVal) 137 | { 138 | // otherwise leave at default NoChange 139 | if (oldVal != newVal) 140 | { 141 | delta.SetValue(this, newVal ? ButtonDeltaState.Pressed : ButtonDeltaState.Released); 142 | HasChanges = true; 143 | } 144 | } 145 | else 146 | { 147 | // we should never EVER get here. and if we do, we need to know about it to fix it, 148 | // as a core assumption has been violated. 149 | throw new InvalidOperationException(); 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /DualSenseAPI/State/DualSenseOutputState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DualSenseAPI.Util; 3 | 4 | namespace DualSenseAPI.State 5 | { 6 | /// 7 | /// All available output variables for a DualSense controller. 8 | /// 9 | public class DualSenseOutputState 10 | { 11 | /// 12 | /// Left motor rumble, as a percentage (0 to 1). Defaults to 0. 13 | /// 14 | public float LeftRumble { get; set; } = 0; 15 | 16 | /// 17 | /// Right motor rumble, as a percentage (0 to 1). Defaults to 0. 18 | /// 19 | public float RightRumble { get; set; } = 0; 20 | 21 | /// 22 | /// The mic LED behavior. Defaults to . 23 | /// 24 | public MicLed MicLed { get; set; } = MicLed.Off; 25 | 26 | /// 27 | /// The enabled player LEDs. Defaults to . 28 | /// 29 | public PlayerLed PlayerLed { get; set; } = PlayerLed.None; 30 | 31 | /// 32 | /// The player LED brightness. Defaults to . 33 | /// 34 | public PlayerLedBrightness PlayerLedBrightness { get; set; } = PlayerLedBrightness.High; 35 | 36 | /// 37 | /// The lightbar behavior. Defaults to . 38 | /// 39 | public LightbarBehavior LightbarBehavior { get; set; } = LightbarBehavior.PulseBlue; 40 | 41 | /// 42 | /// The lightbar color. Defaults to blue. Requires to be set to . 43 | /// 44 | /// 45 | public LightbarColor LightbarColor { get; set; } = new LightbarColor(0, 0, 1); 46 | 47 | /// 48 | /// R2's adaptive trigger effect. Defaults to . 49 | /// 50 | /// 51 | public TriggerEffect R2Effect { get; set; } = TriggerEffect.Default; 52 | 53 | /// 54 | /// L2's adaptive trigger effect. Defaults to . 55 | /// 56 | /// 57 | public TriggerEffect L2Effect { get; set; } = TriggerEffect.Default; 58 | 59 | // default no-arg constructor 60 | public DualSenseOutputState() { } 61 | 62 | /// 63 | /// Gets the bytes needed to describe an adaptive trigger effect. 64 | /// 65 | /// The trigger effect properties. 66 | /// A 10 byte array describing the trigger effect, padded with extra 0s as needed. 67 | private static byte[] BuildTriggerReport(TriggerEffect props) 68 | { 69 | byte[] bytes = new byte[10]; 70 | bytes[0] = (byte)props.InternalEffect; 71 | switch (props.InternalEffect) 72 | { 73 | case TriggerEffectType.ContinuousResistance: 74 | bytes[1] = props.InternalStartPosition.UnsignedToByte(); 75 | bytes[2] = props.InternalStartForce.UnsignedToByte(); 76 | break; 77 | case TriggerEffectType.SectionResistance: 78 | bytes[1] = props.InternalStartPosition.UnsignedToByte(); 79 | bytes[2] = props.InternalEndPosition.UnsignedToByte(); 80 | break; 81 | case TriggerEffectType.Vibrate: 82 | bytes[1] = 0xFF; 83 | if (props.InternalKeepEffect) 84 | { 85 | bytes[2] = 0x02; 86 | } 87 | bytes[4] = props.InternalStartForce.UnsignedToByte(); 88 | bytes[5] = props.InternalMiddleForce.UnsignedToByte(); 89 | bytes[6] = props.InternalEndForce.UnsignedToByte(); 90 | bytes[9] = props.InternalVibrationFrequency; 91 | break; 92 | default: 93 | // leave other bytes as 0. this handles Default/No-resist and calibration modes. 94 | break; 95 | } 96 | return bytes; 97 | } 98 | 99 | /// 100 | /// Gets the bytes needed for an output report, independent of connection type. 101 | /// 102 | /// A 47 byte array for the output report to follow the necessary header byte(s). 103 | internal byte[] BuildHidOutputBuffer() 104 | { 105 | byte[] baseBuf = new byte[47]; 106 | 107 | // Feature mask 108 | baseBuf[0x00] = 0xFF; 109 | baseBuf[0x01] = 0xF7; 110 | 111 | // L/R rumble 112 | baseBuf[0x02] = RightRumble.UnsignedToByte(); 113 | baseBuf[0x03] = LeftRumble.UnsignedToByte(); 114 | 115 | // mic led 116 | baseBuf[0x08] = (byte)MicLed; 117 | 118 | // 0x01 to allow customization, 0x02 to enable uninterruptable blue pulse 119 | baseBuf[0x26] = 0x03; 120 | // 0x01 to do a slow-fade to blue (uninterruptable) if 0x26 & 0x01 is set. 121 | // 0x02 to allow a slow-fade-out and set to configured color 122 | baseBuf[0x29] = (byte)LightbarBehavior; 123 | baseBuf[0x2A] = (byte)PlayerLedBrightness; 124 | baseBuf[0x2B] = (byte)(0x20 | (byte)PlayerLed); 125 | 126 | //lightbar 127 | baseBuf[0x2C] = LightbarColor.R.UnsignedToByte(); 128 | baseBuf[0x2D] = LightbarColor.G.UnsignedToByte(); 129 | baseBuf[0x2E] = LightbarColor.B.UnsignedToByte(); 130 | 131 | //adaptive triggers 132 | byte[] r2Bytes = BuildTriggerReport(R2Effect); 133 | Array.Copy(r2Bytes, 0, baseBuf, 0x0A, 10); 134 | byte[] l2Bytes = BuildTriggerReport(L2Effect); 135 | Array.Copy(l2Bytes, 0, baseBuf, 0x15, 10); 136 | 137 | return baseBuf; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /DualSenseAPI/State/Handlers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace DualSenseAPI.State 6 | { 7 | 8 | /// 9 | /// A handler for a state polling IO event. The sender has the 10 | /// from the most recent poll, and can be used to update the next 11 | /// . 12 | /// 13 | /// The instance that was just polled. 14 | public delegate void StatePolledHandler(DualSense sender); 15 | 16 | /// 17 | /// A handler for a button state changed IO event. The sender has the 18 | /// from the most recent poll, and can be used to update the next . 19 | /// 20 | /// The instance that was just polled. 21 | /// The change status of each button. 22 | public delegate void ButtonStateChangedHandler(DualSense sender, DualSenseInputStateButtonDelta changes); 23 | } 24 | -------------------------------------------------------------------------------- /DualSenseAPI/Touch.cs: -------------------------------------------------------------------------------- 1 | namespace DualSenseAPI 2 | { 3 | /// 4 | /// One of the DualSense's 2 touch points. The touchpad is 1920x1080, 0-indexed. 5 | /// 6 | public struct Touch 7 | { 8 | /// 9 | /// The X position of the touchpoint. 0 is the leftmost edge. If the touch point is currently pressed, 10 | /// this is the current position. If the touch point is released, it was the last position before it 11 | /// was released. 12 | /// 13 | public uint X; 14 | 15 | /// 16 | /// The Y position of the touchpoint. 0 is the topmost edge. If the touch point is currently pressed, 17 | /// this is the current position. If the touch point is released, it was the last position before it 18 | /// was released. 19 | /// 20 | public uint Y; 21 | 22 | /// 23 | /// Whether the touch point is currently pressed. 24 | /// 25 | public bool IsDown; 26 | 27 | /// 28 | /// The touch id. This is a counter that changes whenever a touch is pressed or released. 29 | /// 30 | public byte Id; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DualSenseAPI/Util/ByteConverterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DualSenseAPI.Util 4 | { 5 | /// 6 | /// Extension logic to help conversion between bytes and more useful formats. 7 | /// 8 | internal static class ByteConverterExtensions 9 | { 10 | /// 11 | /// Converts a byte to the corresponding signed float. 12 | /// 13 | /// The byte value 14 | /// The byte, scaled and translated to floating point value between -1 and 1. 15 | public static float ToSignedFloat(this byte b) 16 | { 17 | return (b / 255.0f - 0.5f) * 2.0f; 18 | } 19 | 20 | /// 21 | /// Converts a byte to the corresponding unsigned float. 22 | /// 23 | /// The byte value 24 | /// The byte, scaled to a floating point value between 0 and 1. 25 | public static float ToUnsignedFloat(this byte b) 26 | { 27 | return b / 255.0f; 28 | } 29 | 30 | /// 31 | /// Checks whether the provided flag's bits are set on this byte. Similar to . 32 | /// 33 | /// The byte value 34 | /// The flag to check 35 | /// Whether all the bits of the flag are set on the byte. 36 | public static bool HasFlag(this byte b, byte flag) 37 | { 38 | return (b & flag) == flag; 39 | } 40 | 41 | /// 42 | /// Converts an unsigned float to the corresponding byte. 43 | /// 44 | /// The float value 45 | /// The float, clamped and scaled between 0 and 255. 46 | public static byte UnsignedToByte(this float f) 47 | { 48 | return (byte)(Math.Clamp(f, 0, 1) * 255); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DualSenseAPI/Util/CRC32Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DualSenseAPI.Util 4 | { 5 | /// 6 | /// Utilities for creating a CRC32 hash. 7 | /// 8 | internal static class CRC32Utils 9 | { 10 | private static readonly uint[] ChecksumTableCRC32 = 11 | { 12 | 0xd202ef8d, 0xa505df1b, 0x3c0c8ea1, 0x4b0bbe37, 0xd56f2b94, 0xa2681b02, 0x3b614ab8, 0x4c667a2e, 13 | 0xdcd967bf, 0xabde5729, 0x32d70693, 0x45d03605, 0xdbb4a3a6, 0xacb39330, 0x35bac28a, 0x42bdf21c, 14 | 0xcfb5ffe9, 0xb8b2cf7f, 0x21bb9ec5, 0x56bcae53, 0xc8d83bf0, 0xbfdf0b66, 0x26d65adc, 0x51d16a4a, 15 | 0xc16e77db, 0xb669474d, 0x2f6016f7, 0x58672661, 0xc603b3c2, 0xb1048354, 0x280dd2ee, 0x5f0ae278, 16 | 0xe96ccf45, 0x9e6bffd3, 0x762ae69, 0x70659eff, 0xee010b5c, 0x99063bca, 0xf6a70, 0x77085ae6, 17 | 0xe7b74777, 0x90b077e1, 0x9b9265b, 0x7ebe16cd, 0xe0da836e, 0x97ddb3f8, 0xed4e242, 0x79d3d2d4, 18 | 0xf4dbdf21, 0x83dcefb7, 0x1ad5be0d, 0x6dd28e9b, 0xf3b61b38, 0x84b12bae, 0x1db87a14, 0x6abf4a82, 19 | 0xfa005713, 0x8d076785, 0x140e363f, 0x630906a9, 0xfd6d930a, 0x8a6aa39c, 0x1363f226, 0x6464c2b0, 20 | 0xa4deae1d, 0xd3d99e8b, 0x4ad0cf31, 0x3dd7ffa7, 0xa3b36a04, 0xd4b45a92, 0x4dbd0b28, 0x3aba3bbe, 21 | 0xaa05262f, 0xdd0216b9, 0x440b4703, 0x330c7795, 0xad68e236, 0xda6fd2a0, 0x4366831a, 0x3461b38c, 22 | 0xb969be79, 0xce6e8eef, 0x5767df55, 0x2060efc3, 0xbe047a60, 0xc9034af6, 0x500a1b4c, 0x270d2bda, 23 | 0xb7b2364b, 0xc0b506dd, 0x59bc5767, 0x2ebb67f1, 0xb0dff252, 0xc7d8c2c4, 0x5ed1937e, 0x29d6a3e8, 24 | 0x9fb08ed5, 0xe8b7be43, 0x71beeff9, 0x6b9df6f, 0x98dd4acc, 0xefda7a5a, 0x76d32be0, 0x1d41b76, 25 | 0x916b06e7, 0xe66c3671, 0x7f6567cb, 0x862575d, 0x9606c2fe, 0xe101f268, 0x7808a3d2, 0xf0f9344, 26 | 0x82079eb1, 0xf500ae27, 0x6c09ff9d, 0x1b0ecf0b, 0x856a5aa8, 0xf26d6a3e, 0x6b643b84, 0x1c630b12, 27 | 0x8cdc1683, 0xfbdb2615, 0x62d277af, 0x15d54739, 0x8bb1d29a, 0xfcb6e20c, 0x65bfb3b6, 0x12b88320, 28 | 0x3fba6cad, 0x48bd5c3b, 0xd1b40d81, 0xa6b33d17, 0x38d7a8b4, 0x4fd09822, 0xd6d9c998, 0xa1def90e, 29 | 0x3161e49f, 0x4666d409, 0xdf6f85b3, 0xa868b525, 0x360c2086, 0x410b1010, 0xd80241aa, 0xaf05713c, 30 | 0x220d7cc9, 0x550a4c5f, 0xcc031de5, 0xbb042d73, 0x2560b8d0, 0x52678846, 0xcb6ed9fc, 0xbc69e96a, 31 | 0x2cd6f4fb, 0x5bd1c46d, 0xc2d895d7, 0xb5dfa541, 0x2bbb30e2, 0x5cbc0074, 0xc5b551ce, 0xb2b26158, 32 | 0x4d44c65, 0x73d37cf3, 0xeada2d49, 0x9ddd1ddf, 0x3b9887c, 0x74beb8ea, 0xedb7e950, 0x9ab0d9c6, 33 | 0xa0fc457, 0x7d08f4c1, 0xe401a57b, 0x930695ed, 0xd62004e, 0x7a6530d8, 0xe36c6162, 0x946b51f4, 34 | 0x19635c01, 0x6e646c97, 0xf76d3d2d, 0x806a0dbb, 0x1e0e9818, 0x6909a88e, 0xf000f934, 0x8707c9a2, 35 | 0x17b8d433, 0x60bfe4a5, 0xf9b6b51f, 0x8eb18589, 0x10d5102a, 0x67d220bc, 0xfedb7106, 0x89dc4190, 36 | 0x49662d3d, 0x3e611dab, 0xa7684c11, 0xd06f7c87, 0x4e0be924, 0x390cd9b2, 0xa0058808, 0xd702b89e, 37 | 0x47bda50f, 0x30ba9599, 0xa9b3c423, 0xdeb4f4b5, 0x40d06116, 0x37d75180, 0xaede003a, 0xd9d930ac, 38 | 0x54d13d59, 0x23d60dcf, 0xbadf5c75, 0xcdd86ce3, 0x53bcf940, 0x24bbc9d6, 0xbdb2986c, 0xcab5a8fa, 39 | 0x5a0ab56b, 0x2d0d85fd, 0xb404d447, 0xc303e4d1, 0x5d677172, 0x2a6041e4, 0xb369105e, 0xc46e20c8, 40 | 0x72080df5, 0x50f3d63, 0x9c066cd9, 0xeb015c4f, 0x7565c9ec, 0x262f97a, 0x9b6ba8c0, 0xec6c9856, 41 | 0x7cd385c7, 0xbd4b551, 0x92dde4eb, 0xe5dad47d, 0x7bbe41de, 0xcb97148, 0x95b020f2, 0xe2b71064, 42 | 0x6fbf1d91, 0x18b82d07, 0x81b17cbd, 0xf6b64c2b, 0x68d2d988, 0x1fd5e91e, 0x86dcb8a4, 0xf1db8832, 43 | 0x616495a3, 0x1663a535, 0x8f6af48f, 0xf86dc419, 0x660951ba, 0x110e612c, 0x88073096, 0xff000000 44 | }; 45 | 46 | private const uint HASH_SEED = 0xeada2d49; 47 | 48 | /// 49 | /// Computes a CRC32 hash of the provided data. 50 | /// 51 | /// The bytes to hash. 52 | /// The number of bytes to hash. 53 | /// The hash of the data. 54 | public static uint ComputeCRC32(byte[] byteData, int size) 55 | { 56 | if (size < 0) 57 | throw new ArgumentOutOfRangeException("In ComputeCRC32: the Size is negative."); 58 | uint hashResult = HASH_SEED; 59 | for (int i = 0; i < size; ++i) 60 | hashResult = ChecksumTableCRC32[(hashResult & 0xFF) ^ byteData[i]] ^ (hashResult >> 8); 61 | return hashResult; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DualSenseAPI/Util/HidScanner.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | using Device.Net; 5 | using Hid.Net.Windows; 6 | 7 | namespace DualSenseAPI.Util 8 | { 9 | /// 10 | /// Utilities to scann for DualSense controllers on HID. 11 | /// 12 | internal class HidScanner 13 | { 14 | private readonly IDeviceFactory hidFactory; 15 | 16 | private static HidScanner? _instance = null; 17 | /// 18 | /// Singleton HidScanner instance. 19 | /// 20 | internal static HidScanner Instance 21 | { 22 | get 23 | { 24 | if (_instance == null) 25 | { 26 | _instance = new HidScanner(); 27 | } 28 | return _instance; 29 | } 30 | } 31 | 32 | private HidScanner() 33 | { 34 | hidFactory = new FilterDeviceDefinition(1356, 3302, label: "DualSense").CreateWindowsHidDeviceFactory(); 35 | } 36 | 37 | /// 38 | /// Lists connected devices. 39 | /// 40 | /// An enumerable of connected devices. 41 | public IEnumerable ListDevices() 42 | { 43 | Task> scannerTask = hidFactory.GetConnectedDeviceDefinitionsAsync(); 44 | scannerTask.Wait(); 45 | return scannerTask.Result; 46 | } 47 | 48 | /// 49 | /// Gets a device from its information. 50 | /// 51 | /// The information for the connected device. 52 | /// The actual device. 53 | public IDevice GetConnectedDevice(ConnectedDeviceDefinition deviceDefinition) 54 | { 55 | Task connectTask = hidFactory.GetDeviceAsync(deviceDefinition); 56 | connectTask.Wait(); 57 | return connectTask.Result; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DualSenseAPI/Vector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DualSenseAPI 4 | { 5 | /// 6 | /// A 2D vector 7 | /// 8 | public struct Vec2 9 | { 10 | public float X, Y; 11 | 12 | public float Magnitude() 13 | { 14 | return (float)Math.Sqrt(X * X + Y * Y); 15 | } 16 | 17 | public Vec2 Normalize() 18 | { 19 | float m = Magnitude(); 20 | return new Vec2 { X = X / m, Y = Y / m }; 21 | } 22 | 23 | public static Vec2 operator -(Vec2 v) 24 | { 25 | return new Vec2 { X = -v.X, Y = -v.Y }; 26 | } 27 | } 28 | 29 | /// 30 | /// A 3D vector 31 | /// 32 | public struct Vec3 33 | { 34 | public float X, Y, Z; 35 | 36 | public float Magnitude() 37 | { 38 | return (float)Math.Sqrt(X * X + Y * Y + Z * Z); 39 | } 40 | 41 | public Vec3 Normalize() 42 | { 43 | float m = Magnitude(); 44 | return new Vec3 { X = X / m, Y = Y / m, Z = Z / m }; 45 | } 46 | 47 | public static Vec3 operator -(Vec3 v) 48 | { 49 | return new Vec3 { X = -v.X, Y = -v.Y, Z = -v.Z }; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /DualSenseAPI/docs/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | -------------------------------------------------------------------------------- /DualSenseAPI/docs/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /DualSenseAPI/docs/articles/intro.md: -------------------------------------------------------------------------------- 1 | # Add your introductions here! 2 | -------------------------------------------------------------------------------- /DualSenseAPI/docs/articles/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Introduction 2 | href: intro.md 3 | -------------------------------------------------------------------------------- /DualSenseAPI/docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "**.csproj" 8 | ], 9 | "cwd": ".." 10 | } 11 | ], 12 | "dest": "api", 13 | "disableGitFeatures": false, 14 | "disableDefaultFilter": false 15 | } 16 | ], 17 | "build": { 18 | "content": [ 19 | { 20 | "files": [ 21 | "api/**.yml", 22 | "api/index.md" 23 | ] 24 | }, 25 | { 26 | "files": [ 27 | "articles/**.md", 28 | "articles/**/toc.yml", 29 | "toc.yml", 30 | "*.md" 31 | ] 32 | } 33 | ], 34 | "resource": [ 35 | { 36 | "files": [ 37 | "images/**" 38 | ] 39 | } 40 | ], 41 | "overwrite": [ 42 | { 43 | "files": [ 44 | "apidoc/**.md" 45 | ], 46 | "exclude": [ 47 | "obj/**", 48 | "_site/**" 49 | ] 50 | } 51 | ], 52 | "dest": "_site", 53 | "globalMetadataFiles": [], 54 | "fileMetadataFiles": [], 55 | "template": [ 56 | "default" 57 | ], 58 | "postProcessors": [], 59 | "markdownEngineName": "markdig", 60 | "noLangKeyword": false, 61 | "keepFileLink": false, 62 | "cleanupCacheHistory": false, 63 | "disableGitFeatures": false 64 | } 65 | } -------------------------------------------------------------------------------- /DualSenseAPI/docs/index.md: -------------------------------------------------------------------------------- 1 | # DualSenseAPI 2 | This a .NET library for interfacing with the full feature set of a DualSense controller. 3 | 4 | ## Features 5 | - **Basic Input**: Analog sticks and triggers, d-pad, and all buttons. Basically any input library 6 | you will use can offer this, including DirectInput or similar. 7 | - **Advanced Input**: Most of the rest of input features for the DualSense. This includes: 8 | - 6-axis accelerometer (accelerometer and gyroscope) 9 | - 2-point multitouch 10 | - Battery status 11 | - **Output**: Most of the full suite of output features for the DualSense. This includes: 12 | - Haptic motors 13 | - Adaptive triggers 14 | - Lightbar color 15 | - **Flexiblility of Control**: Supports both synchronous and asynchronous/event-driven IO. 16 | 17 | ## Example 18 | This simple example connects to a DualSense controller using asynchronous polling. The repo contains 19 | a more detailed sample and also shows the usage of synchronous polling as well. Check it out 20 | [here](https://github.com/The-Demp/DualSenseAPI/blob/master/TestDriver/Program.cs#L53)! 21 | 22 | ```csharp 23 | static void Main(string[] args) 24 | { 25 | DualSense ds = DualSense.EnumerateControllers().First(); 26 | ds.Acquire(); 27 | ds.JoystickDeadZone = 0.1f; 28 | ds.BeginPolling(20, (sender) => { 29 | DualSenseInputState dss = sender.InputState; 30 | Console.WriteLine($"LS: ({dss.LeftAnalogStick.X:F2}, {dss.LeftAnalogStick.Y:F2})"); 31 | Console.WriteLine($"RS: ({dss.RightAnalogStick.X:F2}, {dss.RightAnalogStick.Y:F2})"); 32 | Console.WriteLine($"Triggers: ({dss.L2:F2}, {dss.R2:F2})"); 33 | Console.WriteLine($"Touch 1: ({dss.Touchpad1.X}, {dss.Touchpad1.Y}, {dss.Touchpad1.IsDown}, {dss.Touchpad1.Id})"); 34 | Console.WriteLine($"Touch 2: ({dss.Touchpad2.X}, {dss.Touchpad2.Y}, {dss.Touchpad2.IsDown}, {dss.Touchpad2.Id})"); 35 | Console.WriteLine($"Gyro: ({dss.Gyro.X}, {dss.Gyro.Y}, {dss.Gyro.Z})"); 36 | Console.WriteLine($"Accel: ({dss.Accelerometer.X}, {dss.Accelerometer.Y}, {dss.Accelerometer.Z}); m={dss.Accelerometer.Magnitude()}"); 37 | Console.WriteLine($"Headphone: {dss.IsHeadphoneConnected}"); 38 | Console.WriteLine($"Battery: {dss.BatteryStatus.IsCharging}, {dss.BatteryStatus.IsFullyCharged}, {dss.BatteryStatus.Level}"); 39 | }); 40 | Console.ReadKey(true); 41 | ds.EndPolling(); 42 | ds.Release(); 43 | } 44 | ``` -------------------------------------------------------------------------------- /DualSenseAPI/docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: API Documentation 2 | href: api/ 3 | homepage: api/DualSenseAPI.yml 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 The-Demp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DualSenseAPI 2 | 3 | ![docs workflow status](https://github.com/BadMagic100/DualSenseAPI/actions/workflows/github-pages.yml/badge.svg) 4 | ![nuget version](https://img.shields.io/nuget/v/DualSenseAPI) 5 | 6 | A .NET standard API for interacting with DualSense controllers. Full docs and API reference are available at https://badmagic100.github.io/DualSenseAPI/. 7 | 8 | ## Features 9 | - **Basic Input**: Analog sticks and triggers, d-pad, and all buttons. Basically any input library 10 | you will use can offer this, including DirectInput or similar. 11 | - **Advanced Input**: Most of the rest of input features for the DualSense. This includes: 12 | - 6-axis accelerometer (accelerometer and gyroscope) 13 | - 2-point multitouch 14 | - Battery status 15 | - **Output**: Most of the full suite of output features for the DualSense. This includes: 16 | - Haptic motors 17 | - Adaptive triggers 18 | - Lightbar color 19 | - **Flexiblility of Control**: Supports both synchronous and asynchronous/event-driven IO. 20 | 21 | ## Example 22 | This simple example connects to a DualSense controller using asynchronous polling. The repo contains 23 | a more detailed sample and also shows the usage of synchronous polling as well. Check it out 24 | [here](https://github.com/BadMagic100/DualSenseAPI/blob/master/TestDriver/Program.cs#L53)! 25 | 26 | ```csharp 27 | static void Main(string[] args) 28 | { 29 | DualSense ds = DualSense.EnumerateControllers().First(); 30 | ds.Acquire(); 31 | ds.JoystickDeadZone = 0.1f; 32 | ds.BeginPolling(20, (sender) => { 33 | DualSenseInputState dss = sender.InputState; 34 | Console.WriteLine($"LS: ({dss.LeftAnalogStick.X:F2}, {dss.LeftAnalogStick.Y:F2})"); 35 | Console.WriteLine($"RS: ({dss.RightAnalogStick.X:F2}, {dss.RightAnalogStick.Y:F2})"); 36 | Console.WriteLine($"Triggers: ({dss.L2:F2}, {dss.R2:F2})"); 37 | Console.WriteLine($"Touch 1: ({dss.Touchpad1.X}, {dss.Touchpad1.Y}, {dss.Touchpad1.IsDown}, {dss.Touchpad1.Id})"); 38 | Console.WriteLine($"Touch 2: ({dss.Touchpad2.X}, {dss.Touchpad2.Y}, {dss.Touchpad2.IsDown}, {dss.Touchpad2.Id})"); 39 | Console.WriteLine($"Gyro: ({dss.Gyro.X}, {dss.Gyro.Y}, {dss.Gyro.Z})"); 40 | Console.WriteLine($"Accel: ({dss.Accelerometer.X}, {dss.Accelerometer.Y}, {dss.Accelerometer.Z}); m={dss.Accelerometer.Magnitude()}"); 41 | Console.WriteLine($"Headphone: {dss.IsHeadphoneConnected}"); 42 | Console.WriteLine($"Battery: {dss.BatteryStatus.IsCharging}, {dss.BatteryStatus.IsFullyCharged}, {dss.BatteryStatus.Level}"); 43 | }); 44 | Console.ReadKey(true); 45 | ds.EndPolling(); 46 | ds.Release(); 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /TestDriver/Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /TestDriver/Program.cs: -------------------------------------------------------------------------------- 1 | using DualSenseAPI; 2 | using DualSenseAPI.State; 3 | using System; 4 | using System.Linq; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | using System.Reflection; 9 | 10 | namespace Demo 11 | { 12 | class Program 13 | { 14 | static T Choose(T[] ts, string prompt) 15 | { 16 | for (int i = 0; i < ts.Length; i++) 17 | { 18 | Console.WriteLine($"{i}: {ts[i]}"); 19 | } 20 | Console.Write(prompt); 21 | 22 | if (ts.Length == 1) 23 | { 24 | Console.WriteLine(0); 25 | return ts[0]; 26 | } 27 | else 28 | { 29 | int idx; 30 | do 31 | { 32 | bool parseSuccess = int.TryParse(Console.ReadLine(), out idx); 33 | if (!parseSuccess) idx = -1; 34 | } while (idx < 0 || idx >= ts.Length); 35 | 36 | return ts[idx]; 37 | } 38 | } 39 | 40 | static DualSense ChooseController() 41 | { 42 | DualSense[] available = DualSense.EnumerateControllers().ToArray(); 43 | while (available.Length == 0) 44 | { 45 | Console.WriteLine("No DualSenses connected, press any key to retry."); 46 | Console.ReadKey(true); 47 | available = DualSense.EnumerateControllers().ToArray(); 48 | } 49 | 50 | return Choose(available, "Found some DualSenses, select one: "); 51 | } 52 | 53 | static void Main(string[] args) 54 | { 55 | DualSense ds = ChooseController(); 56 | Choose(new Action[] { MainAsyncPolling, MainSyncBlocking }.Select(x => x.GetMethodInfo()).ToArray(), 57 | "Choose a demo runner: ").Invoke(null, new object[] { ds }); 58 | } 59 | 60 | static void MainSyncBlocking(DualSense ds) 61 | { 62 | ds.Acquire(); 63 | DualSenseInputState prevState = ds.InputState; 64 | int wheelPos = 0; 65 | 66 | SetInitialProperties(ds); 67 | DualSenseInputState dss; 68 | DualSenseOutputState dso; 69 | do 70 | { 71 | dss = ds.ReadWriteOnce(); 72 | dso = ds.OutputState; 73 | 74 | if (!prevState.MicButton && dss.MicButton) 75 | { 76 | dso.MicLed = dso.MicLed switch 77 | { 78 | MicLed.Off => MicLed.Pulse, 79 | MicLed.Pulse => MicLed.On, 80 | _ => MicLed.Off 81 | }; 82 | } 83 | 84 | if (!prevState.R1Button && dss.R1Button) 85 | { 86 | dso.PlayerLed = dso.PlayerLed switch 87 | { 88 | PlayerLed.None => PlayerLed.Player1, 89 | PlayerLed.Player1 => PlayerLed.Player2, 90 | PlayerLed.Player2 => PlayerLed.Player3, 91 | PlayerLed.Player3 => PlayerLed.Player4, 92 | PlayerLed.Player4 => PlayerLed.All, 93 | _ => PlayerLed.None 94 | }; 95 | } 96 | 97 | if (!prevState.L1Button && dss.L1Button) 98 | { 99 | dso.PlayerLedBrightness = dso.PlayerLedBrightness switch 100 | { 101 | PlayerLedBrightness.High => PlayerLedBrightness.Low, 102 | PlayerLedBrightness.Low => PlayerLedBrightness.Medium, 103 | _ => PlayerLedBrightness.High 104 | }; 105 | } 106 | 107 | wheelPos = ProcessStateLogic(dss, ds.OutputState, wheelPos); 108 | prevState = dss; 109 | 110 | Thread.Sleep(20); 111 | } while (!dss.LogoButton); 112 | ResetToDefaultState(ds); 113 | ds.Release(); 114 | } 115 | 116 | static void MainAsyncPolling(DualSense ds) 117 | { 118 | ds.Acquire(); 119 | int wheelPos = 0; 120 | 121 | ds.OnStatePolled += (sender) => 122 | { 123 | wheelPos = ProcessStateLogic(sender.InputState, sender.OutputState, wheelPos); 124 | }; 125 | ds.OnButtonStateChanged += (sender, delta) => 126 | { 127 | DualSenseOutputState dso = sender.OutputState; 128 | if (delta.MicButton == ButtonDeltaState.Pressed) 129 | { 130 | dso.MicLed = dso.MicLed switch 131 | { 132 | MicLed.Off => MicLed.Pulse, 133 | MicLed.Pulse => MicLed.On, 134 | _ => MicLed.Off 135 | }; 136 | } 137 | 138 | if (delta.R1Button == ButtonDeltaState.Pressed) 139 | { 140 | dso.PlayerLed = dso.PlayerLed switch 141 | { 142 | PlayerLed.None => PlayerLed.Player1, 143 | PlayerLed.Player1 => PlayerLed.Player2, 144 | PlayerLed.Player2 => PlayerLed.Player3, 145 | PlayerLed.Player3 => PlayerLed.Player4, 146 | PlayerLed.Player4 => PlayerLed.All, 147 | _ => PlayerLed.None 148 | }; 149 | } 150 | 151 | if (delta.L1Button == ButtonDeltaState.Pressed) 152 | { 153 | dso.PlayerLedBrightness = dso.PlayerLedBrightness switch 154 | { 155 | PlayerLedBrightness.High => PlayerLedBrightness.Low, 156 | PlayerLedBrightness.Low => PlayerLedBrightness.Medium, 157 | _ => PlayerLedBrightness.High 158 | }; 159 | } 160 | Console.WriteLine("Change event fired"); 161 | }; 162 | 163 | SetInitialProperties(ds); 164 | // note this polling rate is actually slower than the delay above, because it can do the processing while waiting for the next poll 165 | // (20ms/50Hz is actually quite fast and will clear the screen faster than it can write the data) 166 | ds.BeginPolling(100); 167 | //note that readkey is blocking, which means we know this input method is truly async 168 | Console.ReadKey(true); 169 | ds.EndPolling(); 170 | ResetToDefaultState(ds); 171 | ds.Release(); 172 | } 173 | 174 | static void SetInitialProperties(DualSense ds) 175 | { 176 | ds.JoystickDeadZone = 0.1f; 177 | ds.OutputState = new DualSenseOutputState() 178 | { 179 | LightbarBehavior = LightbarBehavior.CustomColor, 180 | R2Effect = new TriggerEffect.Vibrate(20, 1, 1, 1), 181 | L2Effect = new TriggerEffect.Section(0, 0.5f) 182 | }; 183 | } 184 | 185 | static int ProcessStateLogic(DualSenseInputState dss, DualSenseOutputState dso, int wheelPos) 186 | { 187 | Console.Clear(); 188 | 189 | Console.WriteLine($"LS: ({dss.LeftAnalogStick.X:F2}, {dss.LeftAnalogStick.Y:F2})"); 190 | Console.WriteLine($"RS: ({dss.RightAnalogStick.X:F2}, {dss.RightAnalogStick.Y:F2})"); 191 | Console.WriteLine($"Triggers: ({dss.L2:F2}, {dss.R2:F2})"); 192 | Console.WriteLine($"Touch 1: ({dss.Touchpad1.X}, {dss.Touchpad1.Y}, {dss.Touchpad1.IsDown}, {dss.Touchpad1.Id})"); 193 | Console.WriteLine($"Touch 2: ({dss.Touchpad2.X}, {dss.Touchpad2.Y}, {dss.Touchpad2.IsDown}, {dss.Touchpad2.Id})"); 194 | Console.WriteLine($"Gyro: ({dss.Gyro.X}, {dss.Gyro.Y}, {dss.Gyro.Z})"); 195 | Console.WriteLine($"Accel: ({dss.Accelerometer.X}, {dss.Accelerometer.Y}, {dss.Accelerometer.Z}); m={dss.Accelerometer.Magnitude()}"); 196 | Console.WriteLine($"Headphone: {dss.IsHeadphoneConnected}"); 197 | Console.WriteLine($"Battery: {dss.BatteryStatus.IsCharging}, {dss.BatteryStatus.IsFullyCharged}, {dss.BatteryStatus.Level}"); 198 | 199 | ListPressedButtons(dss); 200 | 201 | dso.LeftRumble = Math.Abs(dss.LeftAnalogStick.Y); 202 | dso.RightRumble = Math.Abs(dss.RightAnalogStick.Y); 203 | 204 | dso.LightbarColor = ColorWheel(wheelPos); 205 | 206 | return (wheelPos + 5) % 384; 207 | } 208 | 209 | static void ResetToDefaultState(DualSense ds) 210 | { 211 | ds.OutputState.LightbarBehavior = LightbarBehavior.PulseBlue; 212 | ds.OutputState.PlayerLed = PlayerLed.None; 213 | ds.OutputState.R2Effect = TriggerEffect.Default; 214 | ds.OutputState.L2Effect = TriggerEffect.Default; 215 | ds.OutputState.MicLed = MicLed.Off; 216 | ds.ReadWriteOnce(); 217 | } 218 | 219 | static LightbarColor ColorWheel(int position) 220 | { 221 | int r = 0, g = 0, b = 0; 222 | switch (position / 128) 223 | { 224 | case 0: 225 | r = 127 - position % 128; //Red down 226 | g = position % 128; // Green up 227 | b = 0; //blue off 228 | break; 229 | case 1: 230 | g = 127 - position % 128; //green down 231 | b = position % 128; //blue up 232 | r = 0; //red off 233 | break; 234 | case 2: 235 | b = 127 - position % 128; //blue down 236 | r = position % 128; //red up 237 | g = 0; //green off 238 | break; 239 | } 240 | return new LightbarColor(r / 255f, g / 255f, b / 255f); 241 | } 242 | 243 | static void ListPressedButtons(DualSenseInputState dss) 244 | { 245 | IEnumerable pressedButtons = dss.GetType().GetProperties() 246 | .Where(p => p.Name.EndsWith("Button") && p.PropertyType == typeof(bool)) 247 | .Where(p => (bool)p.GetValue(dss)!) 248 | .Select(p => p.Name.Replace("Button", "")); 249 | string joined = string.Join(", ", pressedButtons); 250 | Console.WriteLine($"Buttons: {joined}"); 251 | } 252 | } 253 | } 254 | --------------------------------------------------------------------------------