├── .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 | 
4 | 
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 |
--------------------------------------------------------------------------------