diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index a463ad9f5..251621318 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -16,7 +16,18 @@ jobs: arch: amd64 - platform: linux/arm64 arch: arm64 + # CI builds every base for compatibility coverage / downloadable + # artifacts, but releases attach only the most-compatible jammy pair + # (Ubuntu 22.04, glibc 2.35 - runs on everything newer: Ubuntu 22.04+, + # Debian 12+, Arch, etc.); see the release job's download pattern below. + # Add bases here freely - they won't bloat the release. base: + # jammy stays in the fork even though upstream dropped it: it is the + # RELEASE base (glibc 2.35 covers Ubuntu 22.04 LTS onwards). + - os: ubuntu + codename: jammy + pubkey: 871920D1991BC93C + recipe: AppImageBuilder.yml - os: ubuntu codename: noble pubkey: 871920D1991BC93C @@ -122,6 +133,8 @@ jobs: # os: ubuntu-24.04-arm base: + - os: ubuntu + codename: jammy - os: ubuntu codename: noble - os: ubuntu @@ -173,9 +186,21 @@ jobs: - name: Download artifacts uses: actions/download-artifact@v8 with: - pattern: appimage-* + # CI builds all bases, but only the most-compatible jammy AppImages + # (one per arch) are attached to the release. Widen this pattern to + # ship additional bases. + pattern: appimage-jammy-* path: assets merge-multiple: true + # Kept in the fork although upstream dropped it: our releases ship + # per-asset .sha256 checksums. + - name: Create checksum for release assets + shell: bash + run: | + algo="${SHA_ALGORITHM:-256}" + find assets -type f | while read -r asset; do + shasum --binary --algorithm "${algo}" "${asset}" >"${asset}.sha${algo}" + done - name: Upload artifacts to GitHub release uses: softprops/action-gh-release@v3 with: diff --git a/AppImageBuilder.debian.yml b/AppImageBuilder.debian.yml index c59279f03..2ebc616d5 100644 --- a/AppImageBuilder.debian.yml +++ b/AppImageBuilder.debian.yml @@ -17,7 +17,7 @@ script: AppDir: app_info: id: org.c0rn3j.sc-controller - name: sc-controller + name: sc-controller-cc version: "{{APPIMAGE_VERSION}}" icon: sc-controller exec: usr/bin/python3 @@ -214,4 +214,4 @@ AppDir: AppImage: arch: "{{APPIMAGE_ARCH}}" - update-information: "gh-releases-zsync|C0rn3j|sc-controller|latest|sc-controller-*-{{APPIMAGE_APT_DISTRO}}-{{APPIMAGE_ARCH}}.AppImage.zsync" + update-information: "gh-releases-zsync|Patola|sc-controller-cc|latest|sc-controller-cc-*-{{APPIMAGE_APT_DISTRO}}-{{APPIMAGE_ARCH}}.AppImage.zsync" diff --git a/AppImageBuilder.yml b/AppImageBuilder.yml index f5698e6e1..b0fe7ddff 100644 --- a/AppImageBuilder.yml +++ b/AppImageBuilder.yml @@ -17,7 +17,7 @@ script: AppDir: app_info: id: org.c0rn3j.sc-controller - name: sc-controller + name: sc-controller-cc version: "{{APPIMAGE_VERSION}}" icon: sc-controller exec: usr/bin/python3 @@ -207,4 +207,4 @@ AppDir: AppImage: arch: "{{APPIMAGE_ARCH}}" - update-information: "gh-releases-zsync|C0rn3j|sc-controller|latest|sc-controller-*-{{APPIMAGE_APT_DISTRO}}-{{APPIMAGE_ARCH}}.AppImage.zsync" + update-information: "gh-releases-zsync|Patola|sc-controller-cc|latest|sc-controller-cc-*-{{APPIMAGE_APT_DISTRO}}-{{APPIMAGE_ARCH}}.AppImage.zsync" diff --git a/Dockerfile b/Dockerfile index 2624e2def..e69c6c33e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,12 @@ RUN <>/etc/apt/sources.list.d/jammy-proposed.list 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-proposed universe' + echo >>/etc/apt/sources.list.d/jammy-proposed.list 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-proposed universe' + fi apt-get update export DEBIAN_FRONTEND=noninteractive @@ -40,8 +45,19 @@ RUN <=3.11 source-install + # floor, but the jammy AppImage bundles jammy's Python 3.10, where the + # runtime is deliberately kept working (the enums use the functional + # IntEnum API); without the flag pip refuses the install on jammy. + pip install --prefix "${TARGET}/usr" --no-warn-script-location --ignore-requires-python dist/*.whl # Save version PYTHONPATH=$(find "${TARGET}" -type d -name site-packages) \ diff --git a/README.md b/README.md index 79afbd569..82871814c 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ User-mode driver, mapper and GTK3 based GUI for Steam Controller, DS4 and many o ## Features - Allows to setup, configure and use the Steam Controller without ever launching Steam -- Connect multiple controllers at the same time +- Connect multiple controllers at the same time, each with its own remembered profile - Supports profiles switchable in GUI or with controller button - Stick, Pads and Gyroscope input +- Steam Controller 2 (2026) support, including its capacitive stick-touch and grip sensors — bind actions to them directly, or use them as conditions in mode-shift combinations - Haptic Feedback and in-game Rumble support - OSD, Menus, On-Screen Keyboard for desktop *and* in games. - Automatic profile switching based on active window. @@ -23,6 +24,50 @@ User-mode driver, mapper and GTK3 based GUI for Steam Controller, DS4 and many o Based on [Standalone Steam Controller Driver](https://github.com/ynsta/steamcontroller) by [Ynsta](https://github.com/ynsta). +## Using multiple controllers + +SC Controller can drive several controllers at once — Steam Controllers (v1 and +v2), a DualShock 4 and others can all be connected together. + +- **One window, two bars: pick the controller, then its profile.** Just connect + them: a controller-selector bar lists every connected controller (by type, + numbered when you have more than one of the same model) together with its + current profile; choosing one shows it on the big controller image, and a + second bar sets that controller's profile. There is no separate window per + device. The controller that connected *first* is the primary one — it is the + one drawn by default and the target when a command (a menu, the OSD) does not + name a specific controller. +- **Each controller keeps its own profile.** Selecting a controller and setting + a profile applies only to that controller. The choice is remembered and + restored automatically the next time that controller connects, so you do not + have to re-pick it every session. +- **Disconnecting is safe.** Turning one controller off (or letting it go idle) + leaves the window and the other controllers untouched; when it comes back it + returns to its remembered profile. + +![SC Controller with three controllers connected](docs/multiple-controllers.jpg?raw=true) + +*Two Steam Controller v1s and a Steam Controller v2 connected at once: the +selector at the top lists each controller — numbered when there are duplicates — +alongside its current profile.* + +### Telling controllers apart + +How a controller is identified — and therefore which remembered profile and +per-controller settings it gets — is governed by **Use Serial Numbers to +Identify Controllers** in *Settings*: + +- **Off (default):** controllers are identified by connection order (first + connected, second connected, …). This is simplest for a fixed setup, but if + you change which controller powers on first they will swap profiles. +- **On:** each controller is identified by its own hardware serial number, so + its profile and settings follow the physical device no matter what order + things connect in. + +Turn this **on** when you regularly use more than one controller — especially +two of the same model, such as two Steam Controllers — and want each to reliably +keep its own profile. + ## Like what I'm doing? You can check out the ways to donate on [my website](https://rys.rs/donate), or just go straight to my [Ko-Fi](https://ko-fi.com/martinrys). @@ -40,6 +85,23 @@ Linux: - **Void Linux:** Packaged as [sc-controller](https://github.com/void-linux/void-packages/blob/master/srcpkgs/sc-controller/template) - Run `xbps-install -S sc-controller` in a terminal, points to archived Ryochan7's fork at the time of writing - **Others:** You can attempt to use one of the AppImages (try all, AppImages built on older distributions tend to work better), or a package meant for your parent distribution if applicable. Flatpak is planned. +### AppImage: install the udev rules + +The AppImage is self-contained but **cannot install the udev rules** it needs (those live in a system directory). Without them your user can't access the controller and SC Controller can't create the virtual gamepad (`/dev/uinput`), so a detected controller appears to "do nothing". Distro packages install these rules for you; **AppImage users must do it once, by hand:** + +1. Download `69-sc-controller.rules` from the [latest release](https://github.com/Patola/sc-controller-cc/releases/latest). +2. Copy it into place — this needs `sudo`: + ```sh + sudo cp 69-sc-controller.rules /etc/udev/rules.d/69-sc-controller.rules + ``` +3. Reload and re-apply the rules: + ```sh + sudo udevadm control --reload-rules && sudo udevadm trigger + ``` +4. Unplug and replug the controller (or its wireless dongle) — or reboot. + +Only the AppImage needs this; the Arch and other distro packages already ship these rules. **The Steam Deck doesn't need it either** — SteamOS already ships udev rules for Steam devices, so the AppImage works out of the box there. + Windows: - It should be possible to get it running as per the [wiki](https://github.com/C0rn3j/sc-controller/wiki/Running-SC-Controller-on-Windows), but this is untested and might be broken, report a bug if so @@ -79,3 +141,32 @@ docker build -o build-output --build-arg BASE_CODENAME=noble . - Optionally checkout a branch or a tag, like `python3`(default) or `v0.4.9.8.8` - Execute `./run.sh`, this automatically builds the project into a venv called `.env`, activates it and runs sc-controller, which in turn runs scc-daemon if one does not run already - If you are debugging an issue, running `./run.sh daemon` first will launch the daemon in debug mode, allowing you to launch sc-controller in another terminal with `./run.sh` + +### Regenerating controller artwork (for contributors) + +Some SVG assets under `images/` are **generated** from source drawings by scripts +in `tools/`, so edit the source and rerun the script rather than hand-editing the +committed output. All scripts run from the repository root and optimise their +output with [`svgo`](https://github.com/svg/svgo) when it is on `PATH` (optional; +without it the SVGs are just left un-minified). The `svgo` config +(`tools/svgo.config.js`) deliberately preserves the element ids, `` +geometry, `viewBox` and `display:none` layers that the GUI relies on. + +- **`tools/gen_sc2_image.py`** — builds the Steam Controller v2 GUI artwork + (`images/controller-images/sc2.svg`, the face-button glyphs and side-panel + icons) from the traced sources in `tools/` (`sc2-source.svg`, `sc2-assets/`). + +- **`tools/gen_binding_display.py`** — builds the per-controller *Display Current + Bindings* templates in `images/binding-display/.svg`. Instead of + a hand-drawn asset per controller, it derives each template straight from that + controller's GUI drawing (`images/controller-images/.svg`): it scales the + drawing into the OSD canvas, recolours it into the binding-display palette + (green outlines over two greys on a dark backdrop) and drops a marker ring at + each control's `AREA_*` anchor so the binding boxes can draw connector lines to + them. The OSD then picks the file up automatically via the controller's gui + `background` name (see `scc/osd/binding_display.py`, `_resolve_image`). + + To add a controller: give it an entry in the script's `CONTROLLERS` table (its + source drawing + which `AREA_*` anchors each binding box points at) and a + matching box layout in `LAYOUTS` in `scc/osd/binding_display.py`, then rerun the + script. Controllers that share a physical control set can share a layout. diff --git a/TODO.md b/TODO.md index 7d5070afd..2882e92b1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,135 @@ List of (possibly) planned features in no particular order: +- Selectable output device: virtual Xbox (today's default), virtual DS4/DS5, + or NO virtual controller at all. The current "Xbox 360 pad" is just a + generic uinput device wearing an X360 identity defined entirely in + config["output"] (scc/config.py: vendor/product/name/buttons/axes), so the + architecture already treats identity as data. Three tiers of work: + - no-controller mode (keyboard+mouse only) ALREADY EXISTS as the + undocumented SCC_NOGAMEPAD env var (scc/mapper.py create_gamepad); + promote it to a config key + GUI toggle (trivial), or per-profile + (moderate: create/destroy the uinput gamepad on profile switch). + Essential for games that refuse mouse/keyboard input while any + controller is detected; + - evdev-level DS4/DS5 identity presets (Sony VID/PID + the button/axis + layout SDL's gamecontrollerdb expects on the classic path): preset + table + GUI dropdown; gives PlayStation glyphs in most games, but no + gyro/touchpad/lightbar (those ride on hidraw, which uinput cannot + fake; SDL HIDAPI just falls back to evdev); + - faithful DS4/DS5 emulation via /dev/uhid: present a real HID device so + the kernel's hid-playstation binds and exposes motion/touchpad nodes + -> native in-game gyro from ANY supported controller. Big: a uhid + backend beside uinput, authentic report descriptors/streams, a udev + rule for /dev/uhid. Prior art exists (fake-DS4-over-uhid projects). + +- DualShock 4 / DualSense (ds4/ds5) polish. The HID driver is now functional + (mapper rstick/dpad guards, touchpad coordinate scaling + click highlight), but + rough edges remain: + - the two analog sticks are asymmetric in Input Test -- the left and right + stick brighten / behave differently from each other; + - the input icons drawn around the controller are the generic ones, not the + DualShock face symbols (cross / circle / square / triangle); + - the DS4 gyro works as relative and absolute (host-side euler integration in + _step_orientation, verified on hardware) but the absolute path is gyro-only, + so it DRIFTS and gimbals on large combined rotations. The clean next step is + accelerometer drift-correction: fuse gyro + accel (complementary filter) to + pin pitch/roll to the gravity vector -- the raw accel is still in q1-q3 right + after decode, before _step_orientation overwrites them (yaw has no absolute + reference without a magnetometer, so it drifts inherently); + - gyro -> MOUSE routes to the stick instead: an Axes/Rels IntEnum value + collision (ABS_X == REL_X) makes a mouse axis serialize/round-trip as a stick + axis. The gyro editor labels + chooser display are fixed, but the save/reload + path still needs a proper Axes-vs-Rels disambiguation; use gamepad axes for + gyro meanwhile; + - no rumble: neither DS4Controller nor DS4HidRawController drives the pad (the + DS5 driver does) -- port the DS5/kernel output report; over Bluetooth the + DS4HidRawController is input-only, so rumble + lightbar there need output + reports with the BT CRC32 wrapper (mirror DS5HidRawController); + - the lightbar LED is not driven (no DS4-specific set_led); + - DS5 is UNVERIFIED (no DualSense hardware here): its HID touchpad scaling was + added by analogy to the DS4 (DualSense pad assumed 1920x1080) and not tested; + the DS5HidRawController touchpad is still unscaled and stores cpad as unsigned + c_uint16, which can't hold the signed scaled range -- it needs a field type + change as well as scaling. - Multiple on-screen menus (and possibly keyboards) when using multiple controllers - Injecting emulated xbox controller into wine +- mnuImage right-click "change background" menu has no `sc2` entry (the v2 + image is selected automatically via sc2.config.json `gui.background`, but + it can't be picked manually from that menu yet). +- Custom small (24px) controller icons per supported controller. Today only the + Steam Controller v1 (sc-*) and v2 (sc2-*) have bespoke top-down glyphs; every + other type (deck, ds4, ds5, evdev, hid, scbt, fake) reuses the same generic + silhouette, just recolored. Draw a distinct glyph per type so each controller + is recognisable at a glance. The v2 glyph could also be refined further (its + trackpads are necessarily small at 24px). +- Steam Controller v1 GET_SERIAL reliability (nicety). The flaky v1 serial read + is now handled gracefully - usb.py retries a stalled control request instead of + tearing the dongle down, and sc_dongle falls back to a generated id if it never + reads - so multiple v1s with "Use Serial Numbers" on are detected reliably. + Remaining nicety: investigate *why* GET_SERIAL stalls, so a v1 always ends up + with its real serial (today a persistent stall yields a positional id instead). +- Continuous "HD rumble" for the Steam Controller v2 (and v1/Deck). The SC pads + are LRA voice-coil actuators, not ERM spin-motors. We already drive single + pulses (v1: FEEDBACK report 0x8F; v2: interrupt-OUT report 0x82, effect 0x01 = + one click) which suit pad/scroll detents but NOT sustained, amplitude/ + frequency-modulated game rumble. Gap: the v2's continuous-rumble report is + unknown (see sc2.py feedback(): "sustained game rumble may need another report, + not yet found"); the v2 uses its own report scheme (interrupt-OUT 0x82), + distinct from the Deck's feature-report commands, so it needs confirming for + the v2 specifically. Approach (do NOT brute-force the HID space by trial and + error - a wrong report just does nothing and gives no signal): + 1. Read the canonical implementations: SDL's hidapi Steam driver + (SDL_hidapi_steam.c / SDL_hidapi_steamdeck.c - ID_TRIGGER_RUMBLE_CMD plus + the left/right gain "magic numbers") and the Linux kernel + drivers/hid/hid-steam.c (FF play_effect, derived from SDL's Deck code). + 2. Check first whether SDL3 already rumbles the v2 by its VID/PID - if so, + its source *is* the v2 report format and no capture is needed. + 3. Otherwise capture ground truth: run Steam Input on the v2, trigger rumble + (Steam's controller rumble test, or a rumbling game) and capture the USB + OUTPUT reports with usbmon + Wireshark; decode the continuous-rumble + report Steam actually sends. + 4. Replicate it in sc2.py feedback() and diff the emitted bytes against the + capture to confirm. + 5. Map the emulated gamepad's FF_RUMBLE strong/weak magnitudes to the LRA's + amplitude/frequency/gain and tune for feel (LRA != ERM, so a curve is + needed). + Plumbing already exists (emulated gamepad FF -> controller.feedback()); the + missing piece is the v2 continuous-rumble report itself. Refs: SDL hidapi steam + driver, kernel hid-steam.c, and Alice Mikhaylenko's "Steam Deck, HID, and + libmanette adventures" writeup. +- Deck OSD menu fixes. (a) "Display Current Bindings..." and "Run Program..." + ship disabled in the menu settings; once enabled they appear in the OSD, but + selecting them does nothing - their shell() actions (scc-osd-show-bindings, + scc-osd-launcher) don't actually run/work on the Deck. Make them functional. + (b) Remove "Turn Controller OFF" from the Deck's OSD menu - the Deck's + built-in controller can't be powered off (today it shows and does nothing). + Entries defined in scc/gui/global_settings.py (~L45-58, e.g. + "Turn Controller OFF" -> osd(turnoff())); menu data in + default_menus/Default.menu. +- Generalize the OSD "Turn Controller OFF" hiding. It's currently hidden only + for the Deck's built-in controls (controller type == "deck", checked in + scc/osd/menu.py against the --controller-type the daemon passes). Replace that + hardcoded type check with a per-controller capability (a ControllerFlags bit or + a controller.can_turnoff()) so any controller that can't be powered off + remotely hides the entry, not just the Deck. +- Deck tray/status icon not visible. On the Steam Deck the status (tray) icon + doesn't appear even with the option enabled - works on desktop now that + libdbusmenu is bundled, so this is a Deck/gamescope SNI-tray-host issue to + investigate. +- Rebrand the AppImage desktop app-id. app_info.id in AppImageBuilder.yml / + AppImageBuilder.debian.yml is still org.c0rn3j.sc-controller (upstream), so the + installed .desktop carries the upstream id, and the after_bundle step symlinks + it as org.c0rn3j.sc-controller.desktop. Switch both to org.patola.sc-controller-cc + once the fork is stable and we have committed/PR'd to upstream. +- Replace the last deprecated GTK stock-icon calls. macro_editor.py (the + up/down/delete buttons) and modeshift_editor.py (the clear button) still call + `Gtk.Image.new_from_stock("gtk-go-up" / "gtk-go-down" / "gtk-delete", ...)`. + They render today (GTK maps the stock id to an icon internally) but the stock + API is deprecated; move them to `Gtk.Image.new_from_icon_name` with freedesktop + names (go-up / go-down / edit-delete, or the -symbolic variants - all present in + Adwaita/Breeze). Same class as the profile_switcher.py save/edit buttons, which + were actively blank because `new_from_icon_name` was handed the stock ids + "gtk-save"/"gtk-edit" (now document-save / document-edit). Hard stuff: - Injecting emulated xbox controller into PlayOnLinux @@ -10,7 +138,22 @@ Very hard stuff: - Visual feedback in binding editor ( [what this guy says](https://www.reddit.com/r/linux_gaming/comments/5pcdmr/sc_controller_use_steam_controller_without_steam/dcqpvf4/) ) **Done** stuff: +- "Act on release" (inverted button): a general InvertedButtonModifier plus a + checkbox in the button action editor (next to Toggle/Repeat) that fires a + binding on *release* instead of press - for always-on sensors like the + capacitive grips. Round-trips with the Custom Action `inverted(...)` token. +- Dedicated v2 controller artwork: traced SVG (tools/sc2-source.svg) wired by + tools/gen_sc2_image.py into controller-images/sc2.svg + v2 face-overlay + glyphs (button-images/sc2_*.svg, lifted from the drawn symbols so the face + buttons are blank in the art -> no duplication, monochrome ABXY, round Steam, + single dots) + v2 side-panel icons (images/sc2/*.svg, per-controller override + added in app.apply_gui_config_buttons). Control-name ids on sticks/pads/dpad/ + bumpers + grip-touch shapes so everything highlights on hover; darker body + (#b8b8b8). sc2.config.json points at it all. Replaces the borrowed Deck image. - Multicontroller support +- Per-controller profile memory: each controller's profile is remembered by id + (config["controllers"][id]["profile"]) and restored on (re)connect - follows + the physical device with "Use Serial Numbers" on, per-slot otherwise. - Configurable gamepad type (e.g. 4 axes and 16 buttons) - Steam Profile import - Radial Menu for the Joystick/Trackpad diff --git a/default_profiles/.scc-osd.profile_editor.sccprofile b/default_profiles/.scc-osd.profile_editor.sccprofile deleted file mode 100644 index ddf879012..000000000 --- a/default_profiles/.scc-osd.profile_editor.sccprofile +++ /dev/null @@ -1,49 +0,0 @@ -{ - "_": "", - "buttons": { - "A": { - "action": "button(Keys.KEY_SPACE)" - }, - "B": { - "action": "button(Keys.KEY_ESC)" - }, - "C": { - "action": "restart()" - }, - "LB": { - "action": "button(Keys.KEY_BACKSPACE)" - }, - "LPAD": { - "action": "button(Keys.BTN_RIGHT)" - }, - "RB": { - "action": "button(Keys.KEY_SPACE)" - }, - "RPAD": { - "action": "button(Keys.BTN_LEFT)" - }, - "Y": { - "action": "button(Keys.KEY_F2)" - } - }, - "cpad": {}, - "gyro": {}, - "is_template": false, - "menus": {}, - "pad_left": { - "action": "feedback(LEFT, 4090, 16, ball(XY(mouse(Rels.REL_HWHEEL), mouse(Rels.REL_WHEEL))))" - }, - "pad_right": { - "action": "smooth(8, 0.78, 2.0, feedback(RIGHT, 256, ball(mouse())))" - }, - "stick": { - "action": "dpad(button(Keys.KEY_UP), button(Keys.KEY_DOWN), button(Keys.KEY_LEFT), button(Keys.KEY_RIGHT))" - }, - "trigger_left": { - "action": "trigger(50, 50, button(Keys.BTN_RIGHT))" - }, - "trigger_right": { - "action": "trigger(50, 255, button(Keys.BTN_LEFT))" - }, - "version": 1.4 -} \ No newline at end of file diff --git a/docs/actions.md b/docs/actions.md index fa49425e8..5b3b8dfa8 100755 --- a/docs/actions.md +++ b/docs/actions.md @@ -325,6 +325,14 @@ A button whenever physical button is pressed. #### released(action) Creates action that occurs for brief moment when button is released. +#### inverted(action) +Acts on release: swaps press and release, so the wrapped action is held while +the physical button is NOT pressed and released while it is. Meant for +always-on sensors such as the capacitive handle grips, which read "on" the +whole time the controller is held - inverting them fires the action when you +let go. Unlike `pressed`/`released`, which emit a momentary tap, this is a +true held inversion of the button state. + #### pressed(action) Creates action that occurs for brief moment when finger touches pad. diff --git a/docs/multiple-controllers.jpg b/docs/multiple-controllers.jpg new file mode 100644 index 000000000..788ebfa1c Binary files /dev/null and b/docs/multiple-controllers.jpg differ diff --git a/docs/steam-controller-v2-protocol.md b/docs/steam-controller-v2-protocol.md new file mode 100644 index 000000000..dfe1870a8 --- /dev/null +++ b/docs/steam-controller-v2-protocol.md @@ -0,0 +1,201 @@ +# Steam Controller (2025, "v2") — USB/HID protocol notes + +Reverse-engineering notes for adding support for the **new Steam Controller** +(released May 2026), distinct from the original Steam Controller (here "v1", +`28de:1102` wired / `28de:1142` dongle) and the Steam Deck (`28de:1205`). + +Status: **input report (`0x42`) mapped**; command channel (lizard-mode +disable, gyro enable) and the IMU stream are **not yet reverse-engineered**. +Captured live from real hardware on Linux (`hid-generic`, no Steam running). + +## Device topology + +The controller ships with a wireless **"Controller Puck"** (dongle / charging +dock). USB IDs (all vendor `0x28de`, Valve): + +| Product ID | Device | +|---|---| +| `0x1302` | the controller itself (USB-C cable) | +| `0x1303` | the controller over Bluetooth LE | +| `0x1304` | the **Controller Puck** (wireless dongle) | + +The puck enumerates as a composite device with **6 interfaces**: + +- IF 0+1: **CDC ACM** serial (`/dev/ttyACMx`) — purpose TBD (debug/config?). +- IF 2–5: **4 identical HID interfaces** — one per controller slot (like the + v1 dongle's multi-slot design), each a `/dev/hidrawN`. With one controller + paired, slot 1 (the first HID interface) carries its data. + +When the controller is instead **wired over USB-C** (`0x1302`) it enumerates as +a **single HID interface 0** (interrupt IN `0x81`, OUT `0x01`, no CDC) with the +**same report descriptor and `0x42` report**, so all parsing and commands are +identical — only the interface index (0) and the endpoint numbers differ. +(Verified end-to-end: input, gyro, and haptics all work over the cable.) + +Each HID interface's report descriptor declares (vendor usage page `0xFF00`): + +- Input reports: `0x40` (mouse), `0x41` (keyboard) — **lizard mode** (the + controller emulates mouse+keyboard by default), plus the gamepad reports + `0x42` (53 B), `0x43` (14 B), `0x45` (45 B), `0x44` (5 B), `0x7b` (12 B), + `0x79` (1 B). +- Output reports: `0x80`–`0x89` (3–63 B). +- Feature reports: `0x01`–`0x04` (63 B each) — the likely **command channel**. + +By default the controller streams **`0x42` (the main gamepad state) at +~260 Hz even while lizard mode is active** — so raw gamepad data can be read +passively from hidraw with no handshake. The mouse/keyboard emulation runs in +parallel and must eventually be disabled (see Open questions). + +## Report `0x42` — main gamepad state (54 bytes incl. report ID) + +All multi-byte values are **little-endian**. Offsets are into the hidraw read +(offset 0 = the report-ID byte). + +| Offset | Type | Field | +|---|---|---| +| 0 | u8 | Report ID = `0x42` | +| 1 | u8 | Packet counter (increments, wraps 0–255) | +| 2 | u8 | Button byte 0 (see bit table) | +| 3 | u8 | Button byte 1 | +| 4 | u8 | Button byte 2 | +| 5 | u8 | Button byte 3 (rest = `0x30`: grip-touch bits set when held/on table) | +| 6–7 | u16 | Left trigger, analog (0 … ~32767) | +| 8–9 | u16 | Right trigger, analog | +| 10–11 | i16 | Left stick X (±32767; off-center & noisy at rest → needs deadzone) | +| 12–13 | i16 | Left stick Y | +| 14–15 | i16 | Right stick X | +| 16–17 | i16 | Right stick Y | +| 18–19 | i16 | Left pad X | +| 20–21 | i16 | Left pad Y | +| 22–23 | u16 | Left pad pressure (0 … ~650 seen) | +| 24–25 | i16 | Right pad X | +| 26–27 | i16 | Right pad Y | +| 28–29 | u16 | Right pad pressure | +| 30–33 | u32 | IMU timestamp/counter | +| 34–39 | i16×3 | **accelerometer** X/Y/Z (Z ≈ +16271 ≈ 1 g flat at rest) | +| 40–45 | i16×3 | **gyro angular rates** about x/y/z (≈0 whenever still) | +| 46–53 | i16×4 | **orientation quaternion**, laid out **w`@46` x`@48` y`@50` z`@52`** | + +Offsets 30–53 are populated **only when the gyro is enabled** (constant +otherwise). + +The rates/quaternion split was pinned down from held-pose captures: the four +i16 at 46/48/50/52 have **norm exactly 32768 in every orientation** (a unit +quaternion), while 40–45 read ≈0 whenever the controller is still (rates). +An earlier revision of this document had the boundary wrong (quaternion at +40–47, rates at 48–53) — that misread the orientation components as rates, +which is why they "followed the angle". + +**IMU axes** (right-handed, Z up), verified from seven held poses: +x = pitch (nose-up +), y = roll (roll-right +), z = yaw (yaw-left +). +Accelerometer: gravity reads **+Z** flat, **−Y** nose-down, **−X** roll-right. +The quaternion is firmware-fused: pitch/roll are gravity-referenced (drift +free); yaw is referenced to the enable-time orientation and drifts slowly. +The driver converts the quaternion to euler angles host-side and reports +EUREL (2¹⁵/π fixed-point) angles in DS4 conventions. + +### Button bits (offsets 2–5) + +| Byte | bit `0x01` | `0x02` | `0x04` | `0x08` | `0x10` | `0x20` | `0x40` | `0x80` | +|---|---|---|---|---|---|---|---|---| +| **2** | A | B | X | Y | QuickAccess (…) | R3 (rstick click) | Menu (☰) | R4 | +| **3** | R5 | R1 (bumper) | Dpad Down | Dpad Right | Dpad Left | Dpad Up | View (⧉) | L3 (lstick click) | +| **4** | Steam | L4 | L5 | L1 (bumper) | RStick touch | RPad touch | RPad click | RT full-pull (digital) | +| **5** | LStick touch | LPad touch | LPad click | LT full-pull (digital) | **R** grip touch | **L** grip touch | *?* | *?* | + +Notes: +- **Capacitive touch** is reported for both thumbsticks (off4 `0x10`, off5 + `0x01`) and both trackpads (off4 `0x20`, off5 `0x02`). +- **Capacitive grip/handle sensors** (off5 `0x10`/`0x20`) are the feature this + controller adds over a bare Steam Deck. They read **on whenever the handles + are touched — including resting on a table** (byte 5 rests at `0x30`). With + hands fully off the handles, byte 5 reads `0x00`. +- Four **system buttons**: Steam (off4 `0x01`), Menu ☰ (off2 `0x40`), View ⧉ + (off3 `0x40`), QuickAccess … (off2 `0x10`). The driver maps them to + `C` / `START` / `BACK` / `DOTS` respectively. +- Triggers report both a 16-bit analog value (off 6–9) **and** a digital + full-pull bit (off4 `0x80` / off5 `0x08`). +- Unknown bits remaining: off5 `0x40`, off5 `0x80` (may be unused or rare + inputs not present/triggered on the test unit). + +## Comparison to the Steam Deck (`scc/drivers/steamdeck.py`) + +Same *field set* as `DeckInput`, but a **different, more compact byte layout**: + +- Buttons packed into **4 bytes**, not the Deck's `u64`. +- Sticks come right after the triggers; trackpads carry an **extra pressure** + word the Deck struct doesn't have. +- The IMU (accel/gyro/quaternion) is **not in the default stream**, whereas the + Deck always sends it. + +So `DeckInput` can't be reused verbatim, but the Deck **driver skeleton** is the +right model: claim the HID interface, parse the packet into an input struct, +map to `SCButtons`, and **periodically re-disable lizard mode** (the Deck calls +`clear_mappings()` every `UNLIZARD_INTERVAL` frames — see +`steamdeck.py::_on_input`). + +## Command channel (host → device) + +Captured by sniffing Steam's USB traffic (`usbmon`) as it grabbed the +controller. Commands are USB control transfers — **`SET_REPORT`** +(`bmRequestType=0x21`, `bRequest=0x09`): + +- `wValue` = `0x03` for a **Feature** report (e.g. `0x0301` = feature + report `0x01`, the command channel) or `0x02` for an **Output** report; +- `wIndex` = the **interface number (2–5)** — i.e. which of the four puck slots; +- `wLength` = `0x0040` (64-byte payload). + +Payload layout: `[reportID, packetType, length, params… , 0-pad to 64]` — the +same shape as the v1/Deck command packets. The `packetType` opcodes match +`SCPacketType` in `scc/drivers/sc_dongle.py`: + +| Opcode | Name | Observed | Meaning | +|---|---|---|---| +| `0x81` | CLEAR_MAPPINGS | `01 81 00…` (resent ~periodically) | **disable lizard mode** / clear mappings (heartbeat) | +| `0x8E` | LIZARD_MODE | `01 8e 00…` | lizard-mode control | +| `0x87` | CONFIGURE / LED | see below | settings & LED | +| `0xAE` | GET_SERIAL | `01 ae 15 01…` | request serial (read back via GET_REPORT) | +| `0xC1` | SET_AUDIO_INDICES | `01 c1 10 …` | audio indices | +| `0xB4` | *(v2, not in v1)* | `b4 00…` via feature report `0x00`, polled continuously | wireless poll / keepalive? | +| `0xED`/`0xAD`/`0xDC`/`0xE2` | *(v2)* | `01 ed … "user/wireless_transport"`, `"esb/bond"` | v2 key/value pairing & transport config | + +`CONFIGURE` (`0x87`) = `87 `: +- `87 03 2d ` — **LED brightness** (`configType 0x2D`; `0x64` = 100%). +- `87 0f 30 00 07 07 00 08 07 00 31 02 00 52 03` — main config block + (`configType 0x30`, len `0x0f`). The byte after `30` is the **gyro/accel + enable**: `0x18` turns the IMU on, `0x00` off (confirmed live; cf. the Deck's + `0x1C`). Once enabled, IMU data streams in report `0x42` at offsets ~31–53. +- `87 06 34 ffff 35 ffff`, `87 03 22 64`, `87 03 23 50` — `(register, u16)` writes. + +Haptics: **Output report `0x82`** on the **interrupt-OUT** endpoint (its number +equals the interface, e.g. EP `0x02` for slot 1) — the device **stalls it over +SET_REPORT control**, so it must go to the interrupt endpoint. Layout +`82 `: `side` 0=left / 1=right / 2=both; `effect` +`0x01`=click (`0x02`=longer click); `amplitude` `0x00`(medium)…`0xff`(strong). +This is a single *click* per report (verified live). Continuous variable rumble, +if the controller supports it, likely uses a different report (not yet captured; +the connect-time `0x80` report is **not** felt). The `0x81` output reports seen +at connect are unrelated (not haptic). + +**Implication:** the existing `sc_dongle.py` command builders port over; the +differences are the **transport** (SET_REPORT/feature to a per-slot interface +index, not v1's bulk endpoint) and the **CONFIGURE register layout**. + +## Open questions / TODO + +1. **IMU rate scale**: the angular-rate unit (LSB per deg/s) at offsets 40–45 + is not measured yet — relative-mode sensitivity may need tuning vs the DS4. + (Layout, axes and signs of rates/quaternion/accel are now verified; see the + report-0x42 section.) +2. **Trackpad pressure** scaling and the exact meaning of the pressure word. +3. The remaining unknown button bits. +4. The **CDC ACM** interface's purpose. + +## How this was captured + +See `tools/sc2-probe/` — a passive hidraw capture harness (read-only on the +device). Method: capture a still **baseline**, then capture each control while +actuating it, and diff per-byte ranges / changed bits against the baseline. +The two analog sticks were told apart from the IMU by checking which 16-bit +field saturates to ±32767 for which stick. Grip-touch bits were isolated with a +"hands off the handles" reference hold vs. a normal hold. diff --git a/glade/action_editor.glade b/glade/action_editor.glade index cf7025d6a..ecb228bed 100644 --- a/glade/action_editor.glade +++ b/glade/action_editor.glade @@ -161,7 +161,6 @@ True dialog - diff --git a/glade/ae/buttons.glade b/glade/ae/buttons.glade index d4b4f3a31..2a3f4746b 100644 --- a/glade/ae/buttons.glade +++ b/glade/ae/buttons.glade @@ -58,6 +58,27 @@ 1 + + + Act on release + 150 + True + True + False + start + end + 0 + True + Fire when the button or sensor is released instead of pressed. Useful for always-on sensors like the capacitive handle grips, which read "on" while the controller is held. + + + + False + True + end + 2 + + False diff --git a/glade/ae/dpad.glade b/glade/ae/dpad.glade index cd3120aca..4a45820a2 100644 --- a/glade/ae/dpad.glade +++ b/glade/ae/dpad.glade @@ -86,6 +86,14 @@ Right Grip RGRIP + + Left Grip 2 + LGRIP2 + + + Right Grip 2 + RGRIP2 + - x @@ -111,9 +119,29 @@ x - Stick Press + Left Stick Press STICKPRESS + + Right Stick Press + RSTICKPRESS + + + Left Stick Touched + LSTICKTOUCH + + + Right Stick Touched + RSTICKTOUCH + + + Left Grip Touched + LGRIPTOUCH + + + Right Grip Touched + RGRIPTOUCH + Left Pad Press LPAD diff --git a/glade/ae/special_action.glade b/glade/ae/special_action.glade index c705d3be8..8d90c4398 100644 --- a/glade/ae/special_action.glade +++ b/glade/ae/special_action.glade @@ -127,6 +127,14 @@ Right Grip RGRIP + + Left Grip 2 + LGRIP2 + + + Right Grip 2 + RGRIP2 + - x @@ -152,9 +160,29 @@ x - Stick Press + Left Stick Press STICKPRESS + + Right Stick Press + RSTICKPRESS + + + Left Stick Touched + LSTICKTOUCH + + + Right Stick Touched + RSTICKTOUCH + + + Left Grip Touched + LGRIPTOUCH + + + Right Grip Touched + RGRIPTOUCH + Left Pad Press LPAD diff --git a/glade/app.glade b/glade/app.glade index 08cd62ee0..2d241af2a 100644 --- a/glade/app.glade +++ b/glade/app.glade @@ -2,264 +2,6 @@ - - False - 2 - False - True - True - False - False - False - False - - - True - False - 5 - 5 - 5 - 5 - - - 75 - True - False - - - True - False - gtk-missing-image - 3 - - - False - True - 5 - 0 - - - - - True - False - Exit - - - - - - - False - True - 1 - - - - - False - True - 5 - 0 - - - - - 75 - False - True - - - True - False - gtk-missing-image - 3 - - - False - True - 5 - 0 - - - - - True - False - Activate - - - - - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - 75 - True - False - - - True - False - gtk-missing-image - 3 - - - False - True - 5 - 0 - - - - - True - False - OK - - - - - - - False - True - 1 - - - - - False - True - 5 - 2 - - - - - 75 - False - True - - - True - False - gtk-missing-image - 3 - - - False - True - 5 - 0 - - - - - True - False - Close - - - - - - - False - True - 1 - - - - - False - True - 5 - 3 - - - - - 75 - True - False - - - True - False - gtk-missing-image - 3 - - - False - True - 5 - 0 - - - - - True - False - Save - - - - - - - False - True - 1 - - - - - False - True - 5 - 4 - - - - - True - False - - - True - True - 5 - - - - - True - False - - - True - True - 6 - - - - - @@ -644,8 +386,9 @@ - + 170 + True True True @@ -659,9 +402,8 @@ - + 170 - True True True @@ -735,8 +477,9 @@ - + 170 + True True True @@ -750,9 +493,8 @@ - + 170 - True True True @@ -809,6 +551,21 @@ False 12 bottom + + + 220 + True + True + + + + + + False + True + 0 + + 220 @@ -900,6 +657,21 @@ 6 + + + 220 + True + True + + + + + + False + True + 8 + + 0 diff --git a/glade/controller_settings.glade b/glade/controller_settings.glade index 3a55154d8..3853a43ba 100644 --- a/glade/controller_settings.glade +++ b/glade/controller_settings.glade @@ -77,6 +77,14 @@ Right Grip RGRIP + + Left Grip 2 + LGRIP2 + + + Right Grip 2 + RGRIP2 + - x @@ -102,9 +110,29 @@ x - Stick Press + Left Stick Press STICKPRESS + + Right Stick Press + RSTICKPRESS + + + Left Stick Touched + LSTICKTOUCH + + + Right Stick Touched + RSTICKTOUCH + + + Left Grip Touched + LGRIPTOUCH + + + Right Grip Touched + RGRIPTOUCH + Left Pad Press LPAD diff --git a/glade/modeshift_editor.glade b/glade/modeshift_editor.glade index c9268994c..d17f192b7 100644 --- a/glade/modeshift_editor.glade +++ b/glade/modeshift_editor.glade @@ -658,6 +658,101 @@ False + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 5 + 15 + + + 150 + True + False + When touched + 0 + + + 0 + 0 + + + + + True + True + True + True + + + + 1 + 0 + + + + + True + True + True + + + + True + False + gtk-clear + + + + + 2 + 0 + + + + + False + True + 0 + + + + + True + False + 10 + True + Action triggered when the capacitive sensor under the stick is touched (no press needed). + True + + + False + True + 1 + + + + + + + True + False + True + Touch + + + 3 + False + + 0 diff --git a/glade/simple_chooser.glade b/glade/simple_chooser.glade index bce863de3..98f4d2696 100644 --- a/glade/simple_chooser.glade +++ b/glade/simple_chooser.glade @@ -17,7 +17,14 @@ False True - + + Clear + True + True + True + Unset this axis + + diff --git a/images/binding-display/deck.svg b/images/binding-display/deck.svg new file mode 100644 index 000000000..27f17985a --- /dev/null +++ b/images/binding-display/deck.svg @@ -0,0 +1 @@ +L4L5R5R4X \ No newline at end of file diff --git a/images/binding-display/ds4.svg b/images/binding-display/ds4.svg new file mode 100644 index 000000000..5184b84fa --- /dev/null +++ b/images/binding-display/ds4.svg @@ -0,0 +1 @@ +X \ No newline at end of file diff --git a/images/binding-display/ds5.svg b/images/binding-display/ds5.svg new file mode 100644 index 000000000..2501e08e2 --- /dev/null +++ b/images/binding-display/ds5.svg @@ -0,0 +1 @@ +X \ No newline at end of file diff --git a/images/binding-display/sc2.svg b/images/binding-display/sc2.svg new file mode 100644 index 000000000..95d5b91e8 --- /dev/null +++ b/images/binding-display/sc2.svg @@ -0,0 +1 @@ +R4R5L5L4X \ No newline at end of file diff --git a/images/binding-display/x360.svg b/images/binding-display/x360.svg new file mode 100644 index 000000000..96821e2dd --- /dev/null +++ b/images/binding-display/x360.svg @@ -0,0 +1 @@ +X \ No newline at end of file diff --git a/images/button-images/DOTS.svg b/images/button-images/DOTS.svg index 1aa42bda3..e084c592e 100644 --- a/images/button-images/DOTS.svg +++ b/images/button-images/DOTS.svg @@ -51,7 +51,7 @@ inkscape:groupmode="layer" id="layer1"> - - - - - - + + + + + - + image/svg+xml - + - - - - + + + + STEAM - + \ No newline at end of file diff --git a/images/button-images/ELIPSE.svg b/images/button-images/ELIPSE.svg index b421c8dac..7a36d7303 100644 --- a/images/button-images/ELIPSE.svg +++ b/images/button-images/ELIPSE.svg @@ -1,76 +1,18 @@ - - - - - - - + + + + + - + image/svg+xml - + - - - - + + + + STEAM - + \ No newline at end of file diff --git a/images/button-images/sc2_A.svg b/images/button-images/sc2_A.svg new file mode 100644 index 000000000..87783ea95 --- /dev/null +++ b/images/button-images/sc2_A.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_B.svg b/images/button-images/sc2_B.svg new file mode 100644 index 000000000..2d1a38c69 --- /dev/null +++ b/images/button-images/sc2_B.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_BACK.svg b/images/button-images/sc2_BACK.svg new file mode 100644 index 000000000..5905ef7f9 --- /dev/null +++ b/images/button-images/sc2_BACK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_C.svg b/images/button-images/sc2_C.svg new file mode 100644 index 000000000..f5d697949 --- /dev/null +++ b/images/button-images/sc2_C.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_DOTS.svg b/images/button-images/sc2_DOTS.svg new file mode 100644 index 000000000..59111270a --- /dev/null +++ b/images/button-images/sc2_DOTS.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_START.svg b/images/button-images/sc2_START.svg new file mode 100644 index 000000000..84f48bdd1 --- /dev/null +++ b/images/button-images/sc2_START.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_X.svg b/images/button-images/sc2_X.svg new file mode 100644 index 000000000..3d9a0e502 --- /dev/null +++ b/images/button-images/sc2_X.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc2_Y.svg b/images/button-images/sc2_Y.svg new file mode 100644 index 000000000..f0581e58f --- /dev/null +++ b/images/button-images/sc2_Y.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/button-images/sc_C.svg b/images/button-images/sc_C.svg new file mode 100644 index 000000000..2253a099e --- /dev/null +++ b/images/button-images/sc_C.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-0.svg b/images/controller-icons/sc2-0.svg new file mode 100644 index 000000000..5d6cbbd37 --- /dev/null +++ b/images/controller-icons/sc2-0.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-1.svg b/images/controller-icons/sc2-1.svg new file mode 100644 index 000000000..d1ccf4de8 --- /dev/null +++ b/images/controller-icons/sc2-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-2.svg b/images/controller-icons/sc2-2.svg new file mode 100644 index 000000000..ef26d814b --- /dev/null +++ b/images/controller-icons/sc2-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-3.svg b/images/controller-icons/sc2-3.svg new file mode 100644 index 000000000..b86f65721 --- /dev/null +++ b/images/controller-icons/sc2-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-4.svg b/images/controller-icons/sc2-4.svg new file mode 100644 index 000000000..e742e0953 --- /dev/null +++ b/images/controller-icons/sc2-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-5.svg b/images/controller-icons/sc2-5.svg new file mode 100644 index 000000000..187b387da --- /dev/null +++ b/images/controller-icons/sc2-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-icons/sc2-6.svg b/images/controller-icons/sc2-6.svg new file mode 100644 index 000000000..1bae4aa02 --- /dev/null +++ b/images/controller-icons/sc2-6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/controller-images/deck.svg b/images/controller-images/deck.svg index fdf7910e0..84fbe78be 100644 --- a/images/controller-images/deck.svg +++ b/images/controller-images/deck.svg @@ -1,741 +1 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +L4L5R5R4 \ No newline at end of file diff --git a/images/controller-images/ds4.svg b/images/controller-images/ds4.svg index dca86fd85..a6cec567e 100644 --- a/images/controller-images/ds4.svg +++ b/images/controller-images/ds4.svg @@ -330,9 +330,9 @@ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#00000a;fill-opacity:0.04313725;fill-rule:evenodd;stroke:#00b400;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" id="AREA_LPADTEST" width="90.008965" - height="1" + height="90.008965" x="46" - y="105" + y="60.495517" ry="0" /> @@ -344,9 +344,9 @@ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#00000a;fill-opacity:0.04313725;fill-rule:evenodd;stroke:#00b400;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" id="AREA_RPADTEST" width="63.352852" - height="0.25254059" + height="63.352852" x="46.561966" - y="79.079262" + y="47.529106" ry="0" /> - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/controller-images/sc2.svg b/images/controller-images/sc2.svg new file mode 100644 index 000000000..54242a09a --- /dev/null +++ b/images/controller-images/sc2.svg @@ -0,0 +1 @@ +R4R5L5L4 \ No newline at end of file diff --git a/images/controller-images/snes.svg b/images/controller-images/snes.svg index e720c95fd..b12d15c0e 100644 --- a/images/controller-images/snes.svg +++ b/images/controller-images/snes.svg @@ -203,9 +203,9 @@ ry="0" /> @@ -317,9 +317,9 @@ ry="0" /> diff --git a/images/controller-images/x360.svg b/images/controller-images/x360.svg index bf42254cb..f46836b95 100644 --- a/images/controller-images/x360.svg +++ b/images/controller-images/x360.svg @@ -427,9 +427,9 @@ ry="0" /> @@ -451,17 +451,17 @@ ry="0" /> diff --git a/images/deck/BACK.svg b/images/deck/BACK.svg new file mode 100644 index 000000000..d818d4ce4 --- /dev/null +++ b/images/deck/BACK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/deck/C.svg b/images/deck/C.svg new file mode 100644 index 000000000..0e06f2248 --- /dev/null +++ b/images/deck/C.svg @@ -0,0 +1 @@ +STEAM \ No newline at end of file diff --git a/images/deck/DOTS.svg b/images/deck/DOTS.svg new file mode 100644 index 000000000..fc0f4b4cb --- /dev/null +++ b/images/deck/DOTS.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/deck/LGRIP.svg b/images/deck/LGRIP.svg new file mode 100644 index 000000000..cff736a50 --- /dev/null +++ b/images/deck/LGRIP.svg @@ -0,0 +1 @@ +L5 \ No newline at end of file diff --git a/images/deck/LGRIP2.svg b/images/deck/LGRIP2.svg new file mode 100644 index 000000000..119da8e4c --- /dev/null +++ b/images/deck/LGRIP2.svg @@ -0,0 +1 @@ +L4 \ No newline at end of file diff --git a/images/deck/LPAD.svg b/images/deck/LPAD.svg new file mode 100644 index 000000000..5cc901816 --- /dev/null +++ b/images/deck/LPAD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/deck/RGRIP.svg b/images/deck/RGRIP.svg new file mode 100644 index 000000000..0849c27e8 --- /dev/null +++ b/images/deck/RGRIP.svg @@ -0,0 +1 @@ +R5 \ No newline at end of file diff --git a/images/deck/RGRIP2.svg b/images/deck/RGRIP2.svg new file mode 100644 index 000000000..8c93744eb --- /dev/null +++ b/images/deck/RGRIP2.svg @@ -0,0 +1 @@ +R4 \ No newline at end of file diff --git a/images/deck/RPAD.svg b/images/deck/RPAD.svg new file mode 100644 index 000000000..5cc901816 --- /dev/null +++ b/images/deck/RPAD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/deck/START.svg b/images/deck/START.svg new file mode 100644 index 000000000..252773da4 --- /dev/null +++ b/images/deck/START.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc-config.json b/images/sc-config.json index 8812f31e7..9c347ee7b 100644 --- a/images/sc-config.json +++ b/images/sc-config.json @@ -1,26 +1,14 @@ { - "_ " : "Currently, this file is used only when changing controller image", - "__" : "manually from context menu", + "_ ": "Loaded for the Steam Controller (v1) via SCController.get_gui_config_file", + "__": "and on manual image change. GUI section only: button/axis sets stay at", + "___": "their defaults; this just points the Steam (C) button at the logo image.", "gui": { "background": "sc", "buttons": [ "A", "B", "X", "Y", "BACK", - "C", "START", "LB", "RB", "LT", "RT", + "sc_C", "START", "LB", "RB", "LT", "RT", "STICK", "LPAD", "RPAD", "LG", "RG" ] - }, - - "gyros": true, - "buttons": { - "1": "A", - "2": "B", - "3": "X", - "4": "Y", - "5": "RB", - "6": "LB", - "7": "C", - "9": "BACK", - "10": "START" } -} \ No newline at end of file +} diff --git a/images/sc/C.svg b/images/sc/C.svg new file mode 100644 index 000000000..5bb1a60a9 --- /dev/null +++ b/images/sc/C.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2.config.json b/images/sc2.config.json new file mode 100644 index 000000000..c917f4a7e --- /dev/null +++ b/images/sc2.config.json @@ -0,0 +1,22 @@ +{ + "_comment": "GUI config for the new Steam Controller (2026, v2). Its buttons/axes match the Steam Deck (so the buttons/axes lists mirror deck.config.json), but it uses its own traced background image controller-images/sc2.svg and its own face-button glyphs (button-images/sc2_*.svg) and side-panel icons (images/sc2/*.svg), all generated by tools/gen_sc2_image.py from tools/sc2-source.svg.", + "gui": { + "background": "sc2", + "buttons": [ + "sc2_A", "sc2_B", "sc2_X", "sc2_Y", "sc2_BACK", "sc2_C", "sc2_START", + "LB", "RB", "LT", "RT", "STICK", "TOUCHPAD", "TOUCHPAD", "RGRIP", "LGRIP", "sc2_DOTS" + ] + }, + "buttons": [ + "DOTS", "RSTICKTOUCH", "LSTICKTOUCH", "RGRIP2", "LGRIP2", "RSTICKPRESS", + "LSTICKPRESS", "RPADTOUCH", "LPADTOUCH", "RPADPRESS", "LPADPRESS", "C", + "RGRIP", "LGRIP", "RGRIPTOUCH", "LGRIPTOUCH", "START", "BACK", + "A", "X", "B", "Y", "LB", "RB" + ], + "axes": [ + "stick_x", "stick_y", "rstick_x", "rstick_y", + "lpad_x", "lpad_x", "rpad_y", "rpad_y", + "dpad_x", "dpad_y", "ltrig", "rtrig" + ], + "gyros": true +} diff --git a/images/sc2/BACK.svg b/images/sc2/BACK.svg new file mode 100644 index 000000000..0467bb60f --- /dev/null +++ b/images/sc2/BACK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/C.svg b/images/sc2/C.svg new file mode 100644 index 000000000..5bb1a60a9 --- /dev/null +++ b/images/sc2/C.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/DOTS.svg b/images/sc2/DOTS.svg new file mode 100644 index 000000000..ac6611706 --- /dev/null +++ b/images/sc2/DOTS.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/DPAD.svg b/images/sc2/DPAD.svg new file mode 100644 index 000000000..e50526291 --- /dev/null +++ b/images/sc2/DPAD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/LB.svg b/images/sc2/LB.svg new file mode 100644 index 000000000..db34ce7ee --- /dev/null +++ b/images/sc2/LB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/LGRIP.svg b/images/sc2/LGRIP.svg new file mode 100644 index 000000000..514f1ddd0 --- /dev/null +++ b/images/sc2/LGRIP.svg @@ -0,0 +1 @@ +L4 \ No newline at end of file diff --git a/images/sc2/LGRIP2.svg b/images/sc2/LGRIP2.svg new file mode 100644 index 000000000..c126d2671 --- /dev/null +++ b/images/sc2/LGRIP2.svg @@ -0,0 +1 @@ +L5 \ No newline at end of file diff --git a/images/sc2/LGRIPTOUCH.svg b/images/sc2/LGRIPTOUCH.svg new file mode 100644 index 000000000..d67968c45 --- /dev/null +++ b/images/sc2/LGRIPTOUCH.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/LPAD.svg b/images/sc2/LPAD.svg new file mode 100644 index 000000000..65c487df6 --- /dev/null +++ b/images/sc2/LPAD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/LT.svg b/images/sc2/LT.svg new file mode 100644 index 000000000..0c9322547 --- /dev/null +++ b/images/sc2/LT.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/RB.svg b/images/sc2/RB.svg new file mode 100644 index 000000000..d9a7fa85d --- /dev/null +++ b/images/sc2/RB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/RGRIP.svg b/images/sc2/RGRIP.svg new file mode 100644 index 000000000..97f24af89 --- /dev/null +++ b/images/sc2/RGRIP.svg @@ -0,0 +1 @@ +R4 \ No newline at end of file diff --git a/images/sc2/RGRIP2.svg b/images/sc2/RGRIP2.svg new file mode 100644 index 000000000..9cb606ad7 --- /dev/null +++ b/images/sc2/RGRIP2.svg @@ -0,0 +1 @@ +R5 \ No newline at end of file diff --git a/images/sc2/RGRIPTOUCH.svg b/images/sc2/RGRIPTOUCH.svg new file mode 100644 index 000000000..ec7cb3581 --- /dev/null +++ b/images/sc2/RGRIPTOUCH.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/RPAD.svg b/images/sc2/RPAD.svg new file mode 100644 index 000000000..aec1f54c9 --- /dev/null +++ b/images/sc2/RPAD.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/RSTICK.svg b/images/sc2/RSTICK.svg new file mode 100644 index 000000000..f4022dd25 --- /dev/null +++ b/images/sc2/RSTICK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/RT.svg b/images/sc2/RT.svg new file mode 100644 index 000000000..c18d7c04d --- /dev/null +++ b/images/sc2/RT.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/START.svg b/images/sc2/START.svg new file mode 100644 index 000000000..54b5652f2 --- /dev/null +++ b/images/sc2/START.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/sc2/STICK.svg b/images/sc2/STICK.svg new file mode 100644 index 000000000..d9acd5037 --- /dev/null +++ b/images/sc2/STICK.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package.nix b/package.nix new file mode 100644 index 000000000..5fe49cf0f --- /dev/null +++ b/package.nix @@ -0,0 +1,221 @@ +# package.nix for sc-controller-cc +# +# sc-controller-cc — an LLM-assisted fork of sc-controller, the user-mode driver +# and GTK3 GUI for the Steam Controller, DS4 and similar controllers. +# Upstream: https://github.com/Patola/sc-controller-cc +# +# This is a portable, source-built Nix derivation (nothing here is specific to +# any one machine), so it works both on this rootless nix-user-chroot (e.g. on SteamOS) +# and on any other Nix/NixOS system. It is modelled on the Arch `sc-controller-cc` +# PKGBUILD (same dependency set, build a wheel then install it), plus three +# portability fixes the PKGBUILD gets "for free" from a normal FHS system but +# that a self-contained Nix package must handle itself: +# +# 1. Kernel input constants — scc/uinput.py builds its Keys/Axes enums by +# parsing linux/input-event-codes.h at *runtime*. On FHS it reads +# /usr/include; here we copy that one header (from linuxHeaders, a +# build-time-only input) into the installed scc/ package dir, which is the +# first location scc/uinput.py looks. Nothing from linuxHeaders ends up in +# the runtime closure — just the copied text file. +# +# 2. ctypes-loaded X11 libs — scc/lib/xwrappers.py dlopens libX11/libXfixes/ +# libXext by soname, and scc/lib/xinput.py shells out to the `xinput` +# binary. The Nix Python loader does not search /usr/lib, so we put those +# libraries on LD_LIBRARY_PATH and `xinput` on PATH via the wrapper. +# +# 3. libudev — scc/lib/eudevmonitor.py dlopens libudev.so.1 for controller +# hotplug detection. We ship libudev-zero (a tiny, self-contained +# libudev.so.1 with no systemd dependency). The host's real libudev cannot +# be used from a Nix process: it needs libcap.so.2 etc. from /usr/lib, and +# putting all of /usr/lib on LD_LIBRARY_PATH would shadow this package's +# own glibc/GTK libraries. libudev-zero implements the netlink monitor and +# sysfs enumeration scc relies on, so hotplug works without systemd. +# +# It is wired into the flake overlay as: +# sc-controller-cc = final.callPackage ./sc-controller-cc.nix { }; +# and referenced in home.nix's `home.packages`. +# +# Note on udev rules: the package installs scripts/69-sc-controller.rules to +# $out/lib/udev/rules.d, but a rootless home-manager setup cannot load system +# udev rules. On a Steam Deck the controllers are already accessible so no rules +# are needed; on other systems, copy that file into /etc/udev/rules.d (as root) +# if a non-Valve controller needs write access. +# +# To bump the version: change `rev` (and `version`), set `hash` to +# lib.fakeHash, run `home-manager switch`, then paste the real hash from the +# "got:" line of the mismatch error. +{ lib +, fetchFromGitHub +, python3 +, gobject-introspection +, wrapGAppsHook3 +, gtk3 +, gtk-layer-shell +, libayatana-appindicator +, librsvg +, zlib +, xorg +, libudev-zero +, bluez +, linuxHeaders +}: + +let + # Libraries scc dlopens by soname at runtime via ctypes (the Nix Python loader + # does not search /usr/lib). Shared by the runtime wrapper and the test phase. + runtimeLibraries = [ + xorg.libX11 + xorg.libXfixes + xorg.libXext + libudev-zero + bluez # libbluetooth.so.3, for Bluetooth controller detection + ]; +in +python3.pkgs.buildPythonApplication rec { + pname = "sc-controller-cc"; + version = "0.6.0.4"; + pyproject = true; + + src = fetchFromGitHub { + owner = "Patola"; + repo = "sc-controller-cc"; + rev = "v${version}"; + hash = "sha256-NMu+2yHb85Sjv5LgMxKbrWeOYX02DHAtdHtqE38fTN4="; + }; + + # The project's version is "dynamic" via setuptools_scm, which reads it from + # git tags. fetchFromGitHub strips the .git directory, so hand setuptools_scm + # the version explicitly instead of letting it fail with "no version found". + env.SETUPTOOLS_SCM_PRETEND_VERSION = version; + + build-system = with python3.pkgs; [ + setuptools + setuptools-scm + wheel + ]; + + # gobject-introspection + wrapGAppsHook3 make the GTK3 GUI find its GObject + # typelibs (Gtk, GtkLayerShell, AyatanaAppIndicator3), GdkPixbuf SVG loaders + # and GSettings schemas at runtime. + nativeBuildInputs = [ + gobject-introspection + wrapGAppsHook3 + # Test runner for the post-install pytest gate (see postFixup). + python3.pkgs.pytest + python3.pkgs.toml + ]; + + buildInputs = [ + gtk3 + gtk-layer-shell + libayatana-appindicator + librsvg # so GTK can render the app's SVG icons via gdk-pixbuf + zlib # the libcemuhook C extension links against libz + ]; + + # Python runtime dependencies — mirrors the PKGBUILD's python-* depends. + # Note: libusb1 (the Python binding) already loads the C libusb by an absolute + # /nix/store path baked into its loader by nixpkgs (usb1/_libusb1.py), so — + # unlike the old official sc-controller package — the C libusb does NOT need to + # go on LD_LIBRARY_PATH; it is a pinned closure dependency and cannot silently + # go missing. + dependencies = with python3.pkgs; [ + evdev + pygobject3 + ioctl-opt + libusb1 + pylibacl + vdf + setuptools # scc imports pkg_resources at runtime + ]; + + # Tests run in postFixup (see below), not checkPhase: buildPythonApplication's + # checkPhase runs before install, but the CC fork's scc/constants.py needs the + # package's installed dist metadata at import time + # (packages_distributions()["scc"]), which only exists once the wheel is in $out. + doCheck = false; + + # scc/device_monitor.py resolves libbluetooth via ctypes.util.find_library, + # which does not consult LD_LIBRARY_PATH and returns None under Nix (so + # HAVE_BLUETOOTH_LIB stays False and Bluetooth controller detection is + # incomplete). Point it straight at the soname; the actual load then honours + # the bluez path we add to LD_LIBRARY_PATH below. Same fix the official + # nixpkgs sc-controller package uses. + postPatch = '' + substituteInPlace scc/device_monitor.py \ + --replace-fail 'find_library("bluetooth")' '"libbluetooth.so.3"' + ''; + + # scc/uinput.py parses linux/input-event-codes.h at import time. Its preferred + # source is a copy sitting next to the module itself, so drop one there (from + # linuxHeaders) to make the package self-contained instead of depending on the + # host having kernel headers under /usr/include (SteamOS ships none). + postInstall = '' + install -Dm644 ${linuxHeaders}/include/linux/input-event-codes.h \ + "$out/${python3.sitePackages}/scc/input-event-codes.h" + ''; + + # scc/x11/scc-osd-daemon.py and scc-autoswitch-daemon.py are NOT console + # entry points — the daemon runs them directly as `[sys.executable,