ps-access 0.0.1 → 0.0.3

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 (3) hide show
  1. package/README.md +3 -1
  2. package/package.json +2 -2
  3. package/web/xmb.js +65 -25
package/README.md CHANGED
@@ -17,7 +17,9 @@ yourself.
17
17
  ## Requirements
18
18
 
19
19
  - The controller connected by **USB-C** (the profile channel isn’t available over Bluetooth).
20
- - CLI: Node.js (tested on v26) with `node-hid` (`npm install`).
20
+ - CLI: Node.js (tested on v26). `node-hid` is an **optional** dependency (installed
21
+ automatically) needed only to talk to the controller; commands that don't touch the device
22
+ (`presets`, `share`, `show-share`, `help`) and the web tool work even if it isn't built.
21
23
  - Web tool: Chrome or Edge (desktop) for WebHID.
22
24
  - macOS may prompt for **Input Monitoring** permission for the terminal/Chrome on first use.
23
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ps-access",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Read/write PlayStation Access Controller profiles from a PC (no PS5). CLI + WebHID tool + PC input bridge.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@
46
46
  "url": "https://github.com/johnhenry/ps-access/issues"
47
47
  },
48
48
  "author": "johnhenry",
49
- "dependencies": {
49
+ "optionalDependencies": {
50
50
  "node-hid": "^3.1.2"
51
51
  },
52
52
  "license": "MIT"
package/web/xmb.js CHANGED
@@ -619,27 +619,46 @@ async function load() {
619
619
  async function connectOnce() {
620
620
  try {
621
621
  const before = controllers.length;
622
- const ds = await requestControllers();
623
- if (!ds.length) { toast("No controller selected", 2500); return; }
624
- await addDevices(ds);
625
- toast(controllers.length > before ? "Controller connected" : "Controller already connected", 2000);
622
+ await requestControllers(); // user grants one in the chooser (this gesture)
623
+ await addDevices(await grantedControllers()); // reconcile: add every granted controller not yet present
624
+ const added = controllers.length - before;
625
+ if (added > 0) toast(added > 1 ? `${added} controllers connected` : "Controller connected", 2200);
626
+ else toast("That one's already connected — to add another, pick the *other* “Access Controller” in the chooser.", 5500);
626
627
  } catch (e) { toast(String(e.message || e), 4000); }
627
628
  }
628
- async function addDevices(devices) {
629
+
630
+ // Serialize addDevices: granting a device in the chooser fires the `connect` event, whose handler
631
+ // also calls addDevices — running both concurrently on the same new device interleaves its
632
+ // feature-report reads and corrupts the add. Chaining guarantees one batch finishes before the next.
633
+ let _addChain = Promise.resolve();
634
+ function addDevices(devices) {
635
+ const run = _addChain.then(() => _addDevices(devices));
636
+ _addChain = run.catch(() => {}); // keep the chain alive even if a batch throws
637
+ return run;
638
+ }
639
+ async function _addDevices(devices) {
640
+ let added = 0;
629
641
  for (const device of devices) {
630
642
  if (controllers.some((c) => c.device === device)) continue;
631
- await ensureOpen(device);
632
- device.addEventListener("inputreport", onInputReport); // physical buttons + stick, live
633
- const profiles = [];
634
- for (let s = 1; s <= PROFILE_COUNT; s++) {
635
- const p = parseProfile(await readProfileRaw(device, s));
636
- p._physOrient = p.ports[0].kind === "stick" ? p.ports[0].orientation : 3;
637
- profiles.push(p);
643
+ try {
644
+ await ensureOpen(device);
645
+ const profiles = [];
646
+ for (let s = 1; s <= PROFILE_COUNT; s++) {
647
+ const p = parseProfile(await readProfileRaw(device, s));
648
+ p._physOrient = p.ports[0].kind === "stick" ? p.ports[0].orientation : 3;
649
+ profiles.push(p);
650
+ }
651
+ if (controllers.some((c) => c.device === device)) continue; // defensive: added while awaiting
652
+ device.addEventListener("inputreport", onInputReport); // attach only after a clean read
653
+ controllers.push({ device, name: `Controller ${controllers.length + 1}`, profiles });
654
+ added++;
655
+ } catch (e) {
656
+ toast(`Couldn't read a controller: ${e.message || e}`, 4000);
638
657
  }
639
- controllers.push({ device, name: `Controller ${controllers.length + 1}`, profiles });
640
658
  }
641
659
  updateDeviceStatus();
642
660
  render();
661
+ return added;
643
662
  }
644
663
  function updateDeviceStatus() {
645
664
  const c = controllers[activeCtrl];
@@ -866,28 +885,48 @@ function closeHelp() {
866
885
  // Nav scheme (per design): center/stick-click = confirm; any perimeter button = back; stick = directions.
867
886
  const inputEdge = {};
868
887
  let dirRepeatAt = 0;
888
+ // Latest decoded physical state per connected controller. Navigation/highlighting is driven by the
889
+ // *union* of all of them (either controller can move the cursor, confirm, back), while the
890
+ // active-profile indicator, wave tint and monitor follow the controller being edited (activeCtrl).
891
+ const ctrlState = new Map(); // device -> { buttons:Set, axes:[x,y] }
892
+ function mergedInput() {
893
+ const buttons = new Set();
894
+ let axes = [0, 0], best = 0;
895
+ for (const c of controllers) {
896
+ const st = ctrlState.get(c.device);
897
+ if (!st) continue;
898
+ for (const b of st.buttons) buttons.add(b);
899
+ const mag = Math.abs(st.axes[0]) + Math.abs(st.axes[1]); // whichever stick is pushed furthest steers
900
+ if (mag > best) { best = mag; axes = st.axes; }
901
+ }
902
+ return { buttons, axes };
903
+ }
869
904
  function onInputReport(e) {
870
- if (e.device !== controllers[activeCtrl]?.device) return;
905
+ if (!controllers.some((c) => c.device === e.device)) return; // ignore reports from a device mid-add
871
906
  const d = new Uint8Array(e.data.buffer.slice(e.data.byteOffset, e.data.byteOffset + e.data.byteLength));
872
907
  lastInputAt = performance.now();
873
908
  waveConnected = true; // a report is streaming -> the wave is visible
874
909
  const { buttons, axes, profile } = decodePhysical(d);
875
- liveAxes = axes;
876
- phys = buttons;
877
- // Track the controller's *active* profile (changed with the device's profile button). When it
878
- // changes, refresh the top-bar indicator and, if the monitor is open, re-render it for that profile.
879
- if (profile && profile - 1 !== deviceProfile) {
910
+ ctrlState.set(e.device, { buttons, axes });
911
+ const isActive = e.device === controllers[activeCtrl]?.device;
912
+ // Active-profile tracking is per-device only the *edited* controller's profile drives the
913
+ // indicator/wave/monitor. (Refresh the top bar, wave and "✓ Active" marker when it changes.)
914
+ if (isActive && profile && profile - 1 !== deviceProfile) {
880
915
  deviceProfile = profile - 1;
881
916
  updateProfileTag();
882
- setWaveProfile(deviceProfile); // fade the wave's leading curves to match the active profile
917
+ setWaveProfile(deviceProfile);
883
918
  if (monitorMode) $("#mon-render").innerHTML = profileSVG(controllers[activeCtrl].profiles[deviceProfile]);
884
- else if (!monitorArm && !nav.drill) render(); // refresh the "✓ Active on controller" marker
919
+ else if (!monitorArm && !nav.drill) render();
885
920
  }
886
- if (monitorMode) { updateMonitor(buttons, axes, d); setGpStatus(true); return; }
887
- if (monitorArm) { handleArmInput(buttons, axes); setGpStatus(true); return; }
888
- handlePhysInput(buttons, axes);
889
- updateLive();
921
+ // Unified live input (union of every connected controller) drives highlighting + navigation.
922
+ const m = mergedInput();
923
+ liveAxes = m.axes;
924
+ phys = m.buttons;
890
925
  setGpStatus(true);
926
+ if (monitorMode) { if (isActive) updateMonitor(buttons, axes, d); return; } // monitor observes the edited controller
927
+ if (monitorArm) { handleArmInput(m.buttons, m.axes); return; }
928
+ handlePhysInput(m.buttons, m.axes);
929
+ updateLive();
891
930
  }
892
931
 
893
932
  function handlePhysInput(buttons, axes) {
@@ -1031,6 +1070,7 @@ function init() {
1031
1070
  const idx = controllers.findIndex((c) => c.device === e.device);
1032
1071
  if (idx !== -1) {
1033
1072
  e.device.removeEventListener("inputreport", onInputReport);
1073
+ ctrlState.delete(e.device);
1034
1074
  controllers.splice(idx, 1);
1035
1075
  if (activeCtrl >= controllers.length) activeCtrl = Math.max(0, controllers.length - 1);
1036
1076
  deviceProfile = null;