dualsense-ts 6.2.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LINUX_HID.md +85 -0
  2. package/README.md +83 -12
  3. package/dist/dualsense.d.ts +28 -8
  4. package/dist/dualsense.d.ts.map +1 -1
  5. package/dist/dualsense.js +57 -17
  6. package/dist/dualsense.js.map +1 -1
  7. package/dist/hid/bt_checksum.d.ts +7 -0
  8. package/dist/hid/bt_checksum.d.ts.map +1 -1
  9. package/dist/hid/bt_checksum.js +33 -1
  10. package/dist/hid/bt_checksum.js.map +1 -1
  11. package/dist/hid/dualsense_hid.d.ts +77 -0
  12. package/dist/hid/dualsense_hid.d.ts.map +1 -1
  13. package/dist/hid/dualsense_hid.js +193 -0
  14. package/dist/hid/dualsense_hid.js.map +1 -1
  15. package/dist/hid/factory_info.d.ts +53 -0
  16. package/dist/hid/factory_info.d.ts.map +1 -0
  17. package/dist/hid/factory_info.js +166 -0
  18. package/dist/hid/factory_info.js.map +1 -0
  19. package/dist/hid/firmware_info.d.ts +46 -0
  20. package/dist/hid/firmware_info.d.ts.map +1 -0
  21. package/dist/hid/firmware_info.js +109 -0
  22. package/dist/hid/firmware_info.js.map +1 -0
  23. package/dist/hid/hid_provider.d.ts +12 -0
  24. package/dist/hid/hid_provider.d.ts.map +1 -1
  25. package/dist/hid/hid_provider.js +13 -0
  26. package/dist/hid/hid_provider.js.map +1 -1
  27. package/dist/hid/index.d.ts +3 -0
  28. package/dist/hid/index.d.ts.map +1 -1
  29. package/dist/hid/index.js +3 -0
  30. package/dist/hid/index.js.map +1 -1
  31. package/dist/hid/node_hid_provider.d.ts +2 -0
  32. package/dist/hid/node_hid_provider.d.ts.map +1 -1
  33. package/dist/hid/node_hid_provider.js +14 -0
  34. package/dist/hid/node_hid_provider.js.map +1 -1
  35. package/dist/hid/pairing_info.d.ts +9 -0
  36. package/dist/hid/pairing_info.d.ts.map +1 -0
  37. package/dist/hid/pairing_info.js +33 -0
  38. package/dist/hid/pairing_info.js.map +1 -0
  39. package/dist/hid/web_hid_provider.d.ts +14 -0
  40. package/dist/hid/web_hid_provider.d.ts.map +1 -1
  41. package/dist/hid/web_hid_provider.js +79 -8
  42. package/dist/hid/web_hid_provider.js.map +1 -1
  43. package/dist/id.d.ts +4 -0
  44. package/dist/id.d.ts.map +1 -1
  45. package/dist/manager.d.ts +57 -4
  46. package/dist/manager.d.ts.map +1 -1
  47. package/dist/manager.js +248 -66
  48. package/dist/manager.js.map +1 -1
  49. package/nodehid_example/debug.ts +43 -13
  50. package/nodehid_example/single.ts +29 -0
  51. package/package.json +1 -1
  52. package/src/dualsense.ts +73 -23
  53. package/src/hid/bt_checksum.ts +39 -0
  54. package/src/hid/dualsense_hid.ts +230 -0
  55. package/src/hid/factory_info.ts +206 -0
  56. package/src/hid/firmware_info.ts +157 -0
  57. package/src/hid/hid_provider.ts +22 -0
  58. package/src/hid/index.ts +3 -0
  59. package/src/hid/node_hid_provider.ts +14 -0
  60. package/src/hid/pairing_info.ts +33 -0
  61. package/src/hid/web_hid_provider.ts +87 -8
  62. package/src/id.ts +5 -0
  63. package/src/manager.ts +285 -71
  64. package/webhid_example/build/asset-manifest.json +3 -3
  65. package/webhid_example/build/index.html +1 -1
  66. package/webhid_example/build/static/js/main.1c1a2c23.js +3 -0
  67. package/webhid_example/build/static/js/main.1c1a2c23.js.map +1 -0
  68. package/webhid_example/src/App.tsx +7 -1
  69. package/webhid_example/src/hud/AudioIndicator.tsx +116 -0
  70. package/webhid_example/src/hud/BatteryIndicator.tsx +4 -2
  71. package/webhid_example/src/hud/ColorIndicator.tsx +72 -0
  72. package/webhid_example/src/hud/ControllerConnection.tsx +29 -2
  73. package/webhid_example/src/hud/Debugger.tsx +31 -1
  74. package/webhid_example/src/hud/LightbarFadeButtons.tsx +2 -2
  75. package/webhid_example/src/hud/MuteLedControls.tsx +3 -2
  76. package/webhid_example/src/hud/index.tsx +2 -0
  77. package/webhid_example/build/static/js/main.2ac31d24.js +0 -3
  78. package/webhid_example/build/static/js/main.2ac31d24.js.map +0 -1
  79. /package/webhid_example/build/static/js/{main.2ac31d24.js.LICENSE.txt → main.1c1a2c23.js.LICENSE.txt} +0 -0
@@ -9,6 +9,8 @@ import {
9
9
  BatteryIndicator,
10
10
  MuteLedControls,
11
11
  } from "./hud";
12
+ import { AudioIndicator } from "./hud/AudioIndicator";
13
+ import { ColorIndicator } from "./hud/ColorIndicator";
12
14
  import { Debugger } from "./hud/Debugger";
13
15
  import { RightStick } from "./hud/RightStick";
14
16
  import { LeftShoulder, RightShoulder } from "./hud/ShoulderVisualization";
@@ -394,6 +396,7 @@ function useManagerState() {
394
396
  const [activeCount, setActiveCount] = React.useState(
395
397
  manager?.state.active ?? 0,
396
398
  );
399
+ const [pending, setPending] = React.useState(manager?.pending ?? false);
397
400
 
398
401
  React.useEffect(() => {
399
402
  const m = manager;
@@ -401,13 +404,14 @@ function useManagerState() {
401
404
  const update = () => {
402
405
  setControllers([...m.controllers]);
403
406
  setActiveCount(m.state.active);
407
+ setPending(m.pending);
404
408
  };
405
409
  m.on("change", update);
406
410
  const interval = setInterval(update, 500);
407
411
  return () => clearInterval(interval);
408
412
  }, []);
409
413
 
410
- return { controllers, activeCount };
414
+ return { controllers, activeCount, pending };
411
415
  }
412
416
 
413
417
  export const App = () => {
@@ -505,6 +509,8 @@ export const App = () => {
505
509
  <ControllerConnection />
506
510
  <BatteryIndicator />
507
511
  <MuteLedControls />
512
+ <AudioIndicator />
513
+ <ColorIndicator />
508
514
  <LightbarFadeButtons />
509
515
  {connected && (
510
516
  <>
@@ -0,0 +1,116 @@
1
+ import { useContext, useEffect, useState } from "react";
2
+ import { Tag } from "@blueprintjs/core";
3
+
4
+ import { ControllerContext } from "../Controller";
5
+
6
+ export const AudioIndicator = () => {
7
+ const controller = useContext(ControllerContext);
8
+ const [mic, setMic] = useState(controller.microphone.state);
9
+ const [headphone, setHeadphone] = useState(controller.headphone.state);
10
+ const [connected, setConnected] = useState(controller.connection.state);
11
+
12
+ useEffect(() => {
13
+ setMic(controller.microphone.state);
14
+ setHeadphone(controller.headphone.state);
15
+ setConnected(controller.connection.state);
16
+ controller.microphone.on("change", ({ state }) => setMic(state));
17
+ controller.headphone.on("change", ({ state }) => setHeadphone(state));
18
+ controller.connection.on("change", ({ state }) => setConnected(state));
19
+ }, [controller]);
20
+
21
+ if (!connected) return null;
22
+ if (!mic && !headphone) return null;
23
+
24
+ let label: string;
25
+ if (mic && headphone) {
26
+ label = "Headset";
27
+ } else if (headphone) {
28
+ label = "Headphones";
29
+ } else {
30
+ label = "Mic";
31
+ }
32
+
33
+ const icon = (
34
+ <svg
35
+ width="14"
36
+ height="14"
37
+ viewBox="0 0 14 14"
38
+ fill="currentColor"
39
+ style={{ display: "block" }}
40
+ >
41
+ {headphone ? (
42
+ <>
43
+ {/* Headphone band */}
44
+ <path
45
+ d="M2 8 Q2 2 7 2 Q12 2 12 8"
46
+ fill="none"
47
+ stroke="currentColor"
48
+ strokeWidth="1.2"
49
+ strokeLinecap="round"
50
+ />
51
+ {/* Left ear cup */}
52
+ <rect x="1" y="7.5" width="2.5" height="4" rx="1" />
53
+ {/* Right ear cup */}
54
+ <rect x="10.5" y="7.5" width="2.5" height="4" rx="1" />
55
+ {/* Mic boom (only when mic is also connected) */}
56
+ {mic && (
57
+ <>
58
+ <line
59
+ x1="3"
60
+ y1="11"
61
+ x2="5"
62
+ y2="13"
63
+ stroke="currentColor"
64
+ strokeWidth="1.2"
65
+ strokeLinecap="round"
66
+ />
67
+ <circle cx="5.5" cy="13" r="1" />
68
+ </>
69
+ )}
70
+ </>
71
+ ) : (
72
+ <>
73
+ {/* Standalone microphone */}
74
+ <rect x="4.5" y="1" width="5" height="7" rx="2.5" />
75
+ <path
76
+ d="M2.5 6.5 Q2.5 11 7 11 Q11.5 11 11.5 6.5"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ strokeWidth="1.2"
80
+ strokeLinecap="round"
81
+ />
82
+ <line
83
+ x1="7"
84
+ y1="11"
85
+ x2="7"
86
+ y2="13"
87
+ stroke="currentColor"
88
+ strokeWidth="1.2"
89
+ strokeLinecap="round"
90
+ />
91
+ <line
92
+ x1="5"
93
+ y1="13"
94
+ x2="9"
95
+ y2="13"
96
+ stroke="currentColor"
97
+ strokeWidth="1.2"
98
+ strokeLinecap="round"
99
+ />
100
+ </>
101
+ )}
102
+ </svg>
103
+ );
104
+
105
+ return (
106
+ <Tag minimal={true} intent="none" icon={icon} title={
107
+ mic && headphone
108
+ ? "Headset with microphone connected"
109
+ : headphone
110
+ ? "Headphones connected (no microphone)"
111
+ : "Microphone connected (no headphones)"
112
+ }>
113
+ {label}
114
+ </Tag>
115
+ );
116
+ };
@@ -47,11 +47,13 @@ export const BatteryIndicator = () => {
47
47
  const [connected, setConnected] = useState(controller.connection.state);
48
48
 
49
49
  useEffect(() => {
50
+ setLevel(controller.battery.level.state);
51
+ setStatus(controller.battery.status.state);
52
+ setConnected(controller.connection.state);
50
53
  controller.battery.level.on("change", ({ state }) => setLevel(state));
51
54
  controller.battery.status.on("change", ({ state }) => setStatus(state));
52
55
  controller.connection.on("change", ({ state }) => setConnected(state));
53
- // eslint-disable-next-line react-hooks/exhaustive-deps
54
- }, []);
56
+ }, [controller]);
55
57
 
56
58
  if (!connected) return null;
57
59
 
@@ -0,0 +1,72 @@
1
+ import { useContext, useEffect, useState } from "react";
2
+ import { Tag } from "@blueprintjs/core";
3
+ import styled from "styled-components";
4
+ import type { FactoryInfo } from "dualsense-ts";
5
+
6
+ import { ControllerContext } from "../Controller";
7
+
8
+ /** Approximate CSS colors for known DualSense body colors */
9
+ const colorCss: Record<string, string> = {
10
+ "00": "#e8e8e8",
11
+ "01": "#1a1a2e",
12
+ "02": "#c8102e",
13
+ "03": "#f2a6c0",
14
+ "04": "#6b3fa0",
15
+ "05": "#5b9bd5",
16
+ "06": "#8a9a7b",
17
+ "07": "#9b2335",
18
+ "08": "#c0c0c0",
19
+ "09": "#1e3a5f",
20
+ "10": "#2db5a0",
21
+ "11": "#3d4f7c",
22
+ "12": "#e8dfd0",
23
+ "30": "#4a4a4a",
24
+ };
25
+
26
+ const ColorDot = styled.span<{ $color: string }>`
27
+ display: inline-block;
28
+ width: 10px;
29
+ height: 10px;
30
+ border-radius: 50%;
31
+ background: ${(p) => p.$color};
32
+ border: 1px solid rgba(255, 255, 255, 0.3);
33
+ flex-shrink: 0;
34
+ `;
35
+
36
+ export const ColorIndicator = () => {
37
+ const controller = useContext(ControllerContext);
38
+ const [factory, setFactory] = useState<FactoryInfo | undefined>(
39
+ controller.factoryInfo
40
+ );
41
+ const [connected, setConnected] = useState(controller.connection.state);
42
+
43
+ useEffect(() => {
44
+ setConnected(controller.connection.state);
45
+ setFactory(controller.factoryInfo);
46
+ controller.on("change", (c) => {
47
+ if (c.factoryInfo) {
48
+ setFactory(c.factoryInfo);
49
+ }
50
+ });
51
+ controller.connection.on("change", ({ state }) => {
52
+ setConnected(state);
53
+ if (!state) setFactory(undefined);
54
+ });
55
+ }, [controller]);
56
+
57
+ if (!connected || !factory) return null;
58
+
59
+ const css = colorCss[factory.colorCode];
60
+ const label = factory.colorName ?? factory.colorCode;
61
+
62
+ return (
63
+ <Tag
64
+ minimal={true}
65
+ intent="none"
66
+ icon={css ? <ColorDot $color={css} /> : undefined}
67
+ title={`Controller color: ${label}${factory.boardRevision ? ` (${factory.boardRevision})` : ""}`}
68
+ >
69
+ {label}
70
+ </Tag>
71
+ );
72
+ };
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useState, useContext } from "react";
2
- import { Button as BlueprintButton, Tag } from "@blueprintjs/core";
2
+ import { Button as BlueprintButton, Tag, Spinner } from "@blueprintjs/core";
3
3
  import styled, { keyframes } from "styled-components";
4
4
 
5
- import { ControllerContext, requestPermission } from "../Controller";
5
+ import { ControllerContext, ManagerContext, requestPermission } from "../Controller";
6
6
 
7
7
  const StatusContainer = styled.div`
8
8
  display: flex;
@@ -21,14 +21,41 @@ const PulsingButton = styled(BlueprintButton)`
21
21
 
22
22
  export const ControllerConnection = () => {
23
23
  const controller = useContext(ControllerContext);
24
+ const manager = useContext(ManagerContext);
24
25
  const [connected, setConnected] = useState(controller.connection.state);
26
+ const [pending, setPending] = useState(manager?.pending ?? false);
25
27
 
26
28
  useEffect(() => {
29
+ // Sync immediately — the controller may already be connected when the
30
+ // context switches (e.g. after a provisional slot promotes).
31
+ setConnected(controller.connection.state);
27
32
  const handler = ({ state }: { state: boolean }) => setConnected(state);
28
33
  controller.connection.on("change", handler);
29
34
  }, [controller]);
30
35
 
36
+ // The manager doesn't expose a typed event for `pending` flips, so we
37
+ // poll its state alongside the existing 500ms App-level poll.
38
+ useEffect(() => {
39
+ if (!manager) return;
40
+ const tick = () => setPending(manager.pending);
41
+ tick();
42
+ const interval = setInterval(tick, 250);
43
+ return () => clearInterval(interval);
44
+ }, [manager]);
45
+
31
46
  if (!connected) {
47
+ // A controller has been opened but firmware/identity is still loading —
48
+ // show a spinner instead of the "Connect" button so we don't ask the
49
+ // user to take action on a controller that is already wired up.
50
+ if (pending) {
51
+ return (
52
+ <StatusContainer>
53
+ <Tag minimal={true} intent="warning" icon={<Spinner size={12} />}>
54
+ Connecting...
55
+ </Tag>
56
+ </StatusContainer>
57
+ );
58
+ }
32
59
  return (
33
60
  <StatusContainer>
34
61
  <PulsingButton
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import styled from "styled-components";
3
3
  import { Card, Switch } from "@blueprintjs/core";
4
- import { DualsenseHIDState } from "dualsense-ts";
4
+ import { DualsenseHIDState, FirmwareInfo, FactoryInfo, formatFirmwareVersion } from "dualsense-ts";
5
5
 
6
6
  import { ControllerContext } from "../Controller";
7
7
  import { TriggerEffectControls } from "./TriggerEffectControls";
@@ -27,10 +27,22 @@ export const Debugger = ({ panel }: DebuggerProps) => {
27
27
 
28
28
  const [showReport, setShowReport] = React.useState<boolean>(false);
29
29
  const [showState, setShowState] = React.useState<boolean>(false);
30
+ const [firmware, setFirmware] = React.useState<FirmwareInfo | undefined>(
31
+ controller.firmwareInfo
32
+ );
33
+ const [factory, setFactory] = React.useState<FactoryInfo | undefined>(
34
+ controller.factoryInfo
35
+ );
30
36
 
31
37
  React.useEffect(() => {
32
38
  controller.on("change", (controller) => {
33
39
  setDebugState(controller.hid.state);
40
+ if (!firmware && controller.firmwareInfo) {
41
+ setFirmware(controller.firmwareInfo);
42
+ }
43
+ if (!factory && controller.factoryInfo) {
44
+ setFactory(controller.factoryInfo);
45
+ }
34
46
  });
35
47
  // eslint-disable-next-line react-hooks/exhaustive-deps
36
48
  }, []);
@@ -54,6 +66,24 @@ export const Debugger = ({ panel }: DebuggerProps) => {
54
66
 
55
67
  return (
56
68
  <>
69
+ {firmware && (
70
+ <Card compact={true}>
71
+ <p style={{ fontSize: 12, opacity: 0.7, margin: 0 }}>
72
+ Firmware: v{formatFirmwareVersion(firmware.mainFirmwareVersion)}
73
+ {" · "}HW: {firmware.hardwareInfo}
74
+ {" · "}DSP: {firmware.dspFirmwareVersion}
75
+ {" · "}SBL: v{formatFirmwareVersion(firmware.sblFirmwareVersion)}
76
+ {" · "}Built: {firmware.buildDate} {firmware.buildTime}
77
+ </p>
78
+ {factory && (
79
+ <p style={{ fontSize: 12, opacity: 0.7, margin: "4px 0 0" }}>
80
+ Color: {factory.colorName ?? factory.colorCode}
81
+ {" · "}{factory.boardRevision ?? "Unknown board"}
82
+ {" · "}Serial: {factory.serialNumber}
83
+ </p>
84
+ )}
85
+ </Card>
86
+ )}
57
87
  <Card compact={true}>
58
88
  <Switch
59
89
  label="Input State"
@@ -40,9 +40,9 @@ export const LightbarFadeButtons = () => {
40
40
  const [connected, setConnected] = React.useState(controller.connection.state);
41
41
 
42
42
  React.useEffect(() => {
43
+ setConnected(controller.connection.state);
43
44
  controller.connection.on("change", ({ state }) => setConnected(state));
44
- // eslint-disable-next-line react-hooks/exhaustive-deps
45
- }, []);
45
+ }, [controller]);
46
46
 
47
47
  if (!connected) return null;
48
48
 
@@ -9,10 +9,11 @@ export const MuteLedControls = () => {
9
9
  const [connected, setConnected] = useState(controller.connection.state);
10
10
 
11
11
  useEffect(() => {
12
+ setLedOn(controller.mute.status.state);
13
+ setConnected(controller.connection.state);
12
14
  controller.mute.status.on("change", ({ state }) => setLedOn(state));
13
15
  controller.connection.on("change", ({ state }) => setConnected(state));
14
- // eslint-disable-next-line react-hooks/exhaustive-deps
15
- }, []);
16
+ }, [controller]);
16
17
 
17
18
  if (!connected) return null;
18
19
 
@@ -2,8 +2,10 @@
2
2
  * @file Automatically generated by barrelsby.
3
3
  */
4
4
 
5
+ export * from "./AudioIndicator";
5
6
  export * from "./BatteryIndicator";
6
7
  export * from "./BumperVisualization";
8
+ export * from "./ColorIndicator";
7
9
  export * from "./ControllerConnection";
8
10
  export * from "./Debugger";
9
11
  export * from "./DpadVisualization";