Skip to content

fix(mouse3d): recognise pucks by their declared HID usage - #482

Draft
michelhonold wants to merge 4 commits into
mainfrom
fix/mouse3d-hid-usage
Draft

michelhonold wants to merge 4 commits into
mainfrom
fix/mouse3d-hid-usage

Conversation

@michelhonold

@michelhonold michelhonold commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #180.

A 3D Mouse is now recognised by what it declares about itself rather than by vendor and product ID lists.

Recognition. hidapi lists one entry per HID top-level collection, so one hidraw node appears several times. A SpaceNavigator shows Generic Desktop / Multi-axis Controller (usage 1/8) plus two vendor-defined collections.

  • Entries are grouped by node. A node is a puck when any of its entries declares 1/8 under a 3Dconnexion VID/PID: vendor 0x256f, or Logitech 0x046d within 0xc600-0xc6ff.
  • These drop out without an exclusion list:
    • a CadMouse (Mouse, 1/2);
    • the 3Dconnexion keyboards and numpads (Keyboard, 1/6);
    • another vendor's multi-axis device;
    • the Universal Receiver's mouse interface.
  • A node whose every entry reports no usage (older hidapi builds) is kept only when its VID/PID has a device profile.
  • When the profile table cannot be read, the vendor range no longer admits every 0x256f device. A declared puck then counts as profiled and is tried, so a broken table shows as an open error.

Readable is separate. A puck without a device profile is still reported once in the log and shown as not supported, never opened.

Local profiles. Four models pyspacemouse has no profile for are read with a sibling's built-in profile under their own IDs and name. The pairs follow Blender's NDOF table and spacenavd. Each model has its sibling's internals in a different housing, so they are not checked on hardware separately:

Model ID Read with the profile of
SpaceNavigator for Notebooks 046d:c628 SpaceNavigator 046d:c626
SpaceMouse Wireless receiver 256f:c62f SpaceMouse Wireless 256f:c62e
SpaceMouse Pro Wireless, cabled 256f:c631 SpaceMouse Pro Wireless 256f:c632
SpaceMouse Module 256f:c636 SpaceMouse Compact 256f:c635
  • A built-in profile for the same ID always wins.
  • The spec is chosen when the node is opened, from the IDs of the device on it at that moment, because a custom spec skips pyspacemouse's own ID check.
  • A spec that cannot be copied leaves only that model unprofiled.

Docs. docs/architecture/input.md gains a "3D Mouse recognition" section; the help drawer's "Not supported" row now says the unit is a 3D Mouse.

Test plan

  • Classification: usage and VID/PID range parametrized separately; per-node classification; the Universal Receiver.
  • Fallbacks: the no-usage fallback and the missing table.
  • Profiles: each local profile and the spec it opens with; a built-in profile winning; the open-time ID lookup on a reused node.
  • Unprofiled pucks: reported once and never opened.
  • Every new or changed test mutation-checked; review rounds until nothing but cosmetic findings were left.
  • make ci-remote on the testing Pi.
  • Before the change, on a Pi 5 with hidapi 0.14: a SpaceNavigator node declares 1/8 plus two vendor-defined collections.
  • On a station: the SpaceNavigator is still recognised and drives a marker.
  • Hardware not on the bench: a CadMouse, the Universal Receiver.

🤖 Generated with Claude Code

michelhonold and others added 3 commits October 20, 2026 23:56
A hidraw node is a puck when any of its hidapi entries declares Generic
Desktop / Multi-axis Controller (usage page 1, usage 8) under a
3Dconnexion VID/PID (vendor 0x256f, or Logitech 0x046d within
0xc600-0xc6ff). hidapi lists one entry per top-level collection, so the
node is classified across all its entries. A CadMouse, the 3Dconnexion
keyboards and numpads, and the Universal Receiver's mouse interface
drop out without an exclusion list.

A node whose entries report no usage at all (older hidapi) is kept only
when its VID/PID has a device profile. The vendor range no longer
admits every 0x256f device when the profile table cannot be read; a
declared puck then counts as profiled and is tried, so a broken table
shows as an open error.

Four models pyspacemouse has no profile for are read with a sibling's
built-in profile under their own IDs and name (siblings per Blender's
NDOF table and spacenavd, not yet verified on hardware): SpaceNavigator
for Notebooks, the SpaceMouse Wireless receiver, the cabled SpaceMouse
Pro Wireless and the SpaceMouse Module. A built-in profile for the same
ID wins.

Fixes #180

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A custom device_spec skips pyspacemouse's own VID/PID check, so a spec
remembered per hidraw path could open another device that took the same
node between two enumeration passes. open now reads the IDs of the
device at the node when it opens it.

Tests: a locally profiled node reporting no usage is kept and opened
with its spec, an unreported unprofiled node stays quiet, and the
classification parametrize is split into a usage case and a VID/PID
range case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pyspacemouse whose DeviceInfo can no longer be copied with
dataclasses.replace made the whole profile table unreadable, so every
declared puck was tried and a puck with no profile showed as an open
failure. A copy that fails now leaves only that local model unprofiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michelhonold michelhonold added the bug Something isn't working label Oct 21, 2026
@michelhonold

Copy link
Copy Markdown
Contributor Author

Review: three rounds

Agent review of the whole change, fixes committed each round. Mutation checks: 22 of 23 mutants killed. The survivor removes an explicit sibling-present check, and is equivalent: the isolated copy then fails the same way.

Round 1 (4502c79d)

  • Should-fix: a local spec remembered per hidraw path could be applied to another device that took the same node between enumeration passes, since a custom spec skips pyspacemouse's ID check. The spec is now picked from the node's current IDs at open, with a regression test.
  • Tests: a no-usage local-profile case was added, and the classification parametrize split into usage and ID range.
  • Considered and rejected: treating vendor-defined-only nodes as "no usage". A profiled ID's separate vendor interface would then become a second puck.

Round 2 (3fb5014e)

  • A profile copy that fails no longer makes the whole table unreadable; only that model is left unprofiled, with a test.
  • The docs now note that hidapi 0.14 lists one entry per collection.

Round 3: nothing substantive.

Accepted edge cases:

  • a microsecond window between the ID read and the open;
  • on a hidapi that reports no usage at all, the Universal Receiver's mouse node is admitted beside its puck, the same as before.

Results:

  • full suite, mypy and ruff clean;
  • mouse3d.py at 100 % line and branch coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(mouse3d): recognize pucks by their HID usage, not by ID lists

1 participant