Metadata-Version: 2.4
Name: steamcontroller-original
Version: 2.0.0
Summary: Steam Controller userland driver
Author-email: Stany MARCEL <stanypub@gmail.com>
License-Expression: AGPL-3.0-or-later
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: libusb1>=3.0
Requires-Dist: psutil>=5.9
Provides-Extra: gui
Requires-Dist: PySide6>=6.6; extra == "gui"
Requires-Dist: pyqtgraph>=0.13.4; extra == "gui"
Dynamic: license-file

# steamcontroller-original

A standalone userland driver for the **original Steam Controller**, the
one Valve sold from 2015 to 2019, to be used where the steam client can't
be installed.

"original" is in the name to be unambiguous about which hardware this
drives. It is not for the Steam Deck's built-in controls, and not for any
newer Valve controller. If your controller is not the 2015 one with the
two round trackpads, this is the wrong project.

It installs as `steamcontroller-original` and imports as
`steamcontroller_original`, so it does not collide with the upstream
`python-steamcontroller` it was forked from. Both can be installed side by
side. The `sc-*` command names are unchanged and *are* shared with
upstream, so don't install both if you want a predictable `sc-xbox.py` on
your `PATH`.

Two modes are already working with haptic feedback:
 - xbox360: gamepad emulator
 - desktop: mouse, keyboard mode

The final purpose is to have support for custom mapping created with a stand-alone tool or imported from steam vdf files.

The initial target is *GNU/Linux*, but I'll welcome any contributor that want to port input generation for other OS (OSX, Windows, *BSD, Android/Linux, ...)

This project is licensed under the GNU Affero General Public License v3.0 or
later (AGPLv3+). It was previously MIT-licensed; the original MIT notice is
preserved in [NOTICE](NOTICE) as required by its terms and still applies to
the contributions made under it.

## Installation

This project targets Python 3.10+ and is managed with [uv](https://docs.astral.sh/uv/).

 1. System dependencies (Debian/Ubuntu package names, adjust for your distro):
    - `libusb-1.0-0`, required at runtime to actually talk to the controller
      over USB. Building/installing the project works without it, but any
      script that opens a `SteamController` will fail with a
      `FileNotFoundError` from `libusb1` until it's installed. This is the
      only system package the driver needs.
    - Only if you want `sc-gyro-plot.py` (the optional gyro data plotter):
      a working OpenGL/EGL stack (e.g. `libgl1-mesa-dri`, `libglx-mesa0`) for
      Qt to initialize at all, plus, if you want it in an actual window
      rather than offscreen, `libxcb-cursor0 libxcb-xinerama0
      libxkbcommon-x11-0`.
    - Optional: a C compiler (e.g. `build-essential`) if you want the
      `libuinput` C extension. It is not needed; see "uinput backends" under
      Development for what it is and why you probably don't want it.

 2. Clone the project:
    ```shell
    git clone https://codeberg.org/Theoreticallyhugo/steamcontroller-original.git
    cd steamcontroller-original
    ```

 3. Install with uv:
    ```shell
    uv sync              # core driver + daemons only
    uv sync --extra gui  # also installs PySide6/pyqtgraph for sc-gyro-plot.py
    ```
    This resolves dependencies and creates a `.venv/`. It also tries to
    build the optional `libuinput` C extension; if you have no compiler it
    says so and carries on, which is fine. Run any installed script with
    `uv run <script>`,
    e.g. `uv run sc-xbox.py start` (see Usage below), or install it as a
    standalone tool with `uv tool install .` / `uv tool install '.[gui]'`
    to get the scripts on your `PATH` without prefixing every invocation
    with `uv run`.

 4. Install udev rules (if not already done for steam) in `/etc/udev/rules.d/99-steam-controller.rules`:
 ```shell
 # replace game group by a valid group on your system
 # Steam controller keyboard/mouse mode
 SUBSYSTEM=="usb", ATTRS{idVendor}=="28de", GROUP="games", MODE="0660"

 # Steam controller gamepad mode
 KERNEL=="uinput", MODE="0660", GROUP="games", OPTIONS+="static_node=uinput"
 ```

 5. Reload udev `sudo udevadm control --reload`

## Usage

Exit Steam first, since it grabs the controller for itself.

Every daemon takes the same commands:

```shell
uv run sc-xbox.py start      # fork into the background
uv run sc-xbox.py stop
uv run sc-xbox.py restart
uv run sc-xbox.py debug      # stay in the foreground, ^C to quit
uv run sc-xbox.py -i 0 start # second controller (index 0-3)
```

Drop the `uv run` prefix if you installed with `uv tool install`. Use
`debug` when something isn't working: it runs in the foreground and lets
errors reach your terminal instead of syslog.

### Modes

| Command | What it gives you |
| --- | --- |
| `sc-xbox.py` | Xbox 360 pad. One uinput device, so games see a single standard controller. |
| `sc-gamepad.py` | Generic pad. Like the above but exposes the triggers as buttons too, and keeps running while the controller is disconnected. |
| `sc-desktop.py` | Mouse and keyboard, for using the couch without a keyboard. |
| `sc-mixed.py` | Both: **tap STEAM to switch** between the pad and desktop mappings. |

**Hold STEAM for two seconds to shut the daemon down** from the controller,
in any mode.

### Desktop mode controls

| Control | Does |
| --- | --- |
| Right pad | Mouse, with trackball-style momentum |
| Left pad | Scroll wheel, also with momentum |
| Stick | Arrow keys |
| Left / right trigger | Right / left mouse button |
| A / B / X / Y | Enter / Backspace / Esc / Play-Pause |
| LB / RB | Volume down / up |
| Left / right grip | Browser back / forward |
| Start / Back | Next / previous track |
| Left pad click | Middle click |
| Right pad click | Space |
| STEAM (tap) | Home page |

### Gamepad mode controls

Stick and right pad are the two analog sticks, the left pad is the d-pad,
and the triggers are analog. Face buttons, bumpers, Start/Back and the pad
clicks map to their obvious Xbox equivalents. The grips double as A and B
(`sc-xbox.py`) or as back/forward (`sc-gamepad.py`).

### Running as a service

`res/` holds systemd units for `sc-xbox.py` and `sc-mixed.py`. They assume
the scripts are at `/usr/bin/`, which is **not** where uv puts them, so point
`ExecStart`/`ExecReload`/`ExecStop` at your actual install (`uv tool install
.` gives you `~/.local/bin/sc-xbox.py`) before enabling one.

Note that every daemon defaults to the same pidfile
(`/tmp/steamcontroller.pid`, or `/tmp/steamcontroller{N}.pid` with `-i N`)
regardless of which one you start. That's carried over unchanged from the
original driver, and the `PIDFile=` lines in `res/*.service` depend on those
exact paths, so changing one means changing both.

A pidfile left behind by a crash no longer blocks the next `start`; it is
detected as stale and cleared.

### Other tools

 - `sc-dump.py` : Dump raw message from the controller.
 - `sc-gyro-plot.py` : Plot curves from gyro data. Requires the `gui` extra
   (`uv sync --extra gui` / `uv tool install '.[gui]'`) and a working
   OpenGL/EGL stack; it prints a friendly error and exits instead of
   crashing if either is missing.
 - `sc-test-cmsg.py` : Permit to send control message to the contoller. For example:
   `echo 8f07005e 015e01f4 01000000 | uv run sc-test-cmsg.py` will make the controller beep.
 - `vdf2json.py` : Convert Steam VDF file to JSON.
 - `json2vdf.py` : Convert back JSON to VDF file.

Both converters read stdin and write stdout by default, or take `-i` and
`-o`:

```shell
uv run vdf2json.py -i ~/.steam/steam/config/config.vdf -o config.json
uv run json2vdf.py -i config.json -o config.vdf
```

### Tuning

A few thresholds are plain constants, if the defaults don't suit you:

 - `src/events.py`: `TRIG_PRESS_THRESHOLD` / `TRIG_RELEASE_THRESHOLD` set how
   far a trigger must be pulled to count as a button press (these are your
   mouse buttons in desktop mode), and `STICK_PRESS_THRESHOLD` /
   `STICK_RELEASE_THRESHOLD` do the same for the stick-as-d-pad. The gap
   between each pair is hysteresis, so a control resting near the edge
   doesn't chatter.
 - `src/uinput.py`: `Mouse.DEFAULT_FRICTION` and `DEFAULT_SCR_FRICTION`
   control how long the pointer and the scroll wheel keep coasting.

Re-run `uv sync` after editing so the installed copy picks up the change.

## Development

Run the test suite with `uv run pytest`. It's fully self-contained. The
uinput backend is mocked in `tests/conftest.py`, so no root, real
`/dev/uinput`, or attached controller is needed. The `sc-gyro-plot.py`
screenshot test additionally needs a working Qt/EGL stack (see
Installation above) and skips itself cleanly if that's not available, as
do the tests that compare the two uinput backends when the C extension
hasn't been built.

To exercise the C path as well:

```shell
SC_UINPUT_BACKEND=c uv run pytest
```

### uinput backends

Emulated input is delivered by writing event structs to `/dev/uinput`.
There are two implementations of that, and they produce byte-identical
output:

| `SC_UINPUT_BACKEND` | What it uses |
| --- | --- |
| `python` (default) | `ioctl()` and `write()` straight from Python. No compiled component. |
| `c` | The `libuinput` extension built from `src/uinput.c`, which is what this driver used before. |

The Python backend is the default because it needs no compiler and is, if
anything, slightly faster: each call is a single `write()` syscall either
way, and crossing into C through ctypes costs more than packing the struct
in Python does (measured at roughly 0.48us per event against 0.73us).

The C path is kept for anyone who wants it:

```shell
SC_UINPUT_BACKEND=c uv run sc-desktop.py debug
```

It needs the extension to have been built, which requires a compiler at
install time. If it wasn't built, selecting it reports that clearly rather
than falling back silently.

### Building a universal wheel

Declaring the C extension is what makes setuptools tag a wheel for one
interpreter and one architecture (`cp313-cp313-linux_x86_64`), and it does
that even when the compile was skipped. To leave it out and get a portable
wheel instead:

```shell
SC_NO_C_EXTENSION=1 uv build
```

| Build | Wheel | Installs on |
| --- | --- | --- |
| `uv build` | `...-cp313-cp313-linux_x86_64.whl` | that one Python version, that one architecture |
| `SC_NO_C_EXTENSION=1 uv build` | `...-py3-none-any.whl` | any Python 3.10+, any architecture |

Use the second one for anything you publish or hand to someone else. The
driver's default backend is pure Python, so nothing is lost: only
`SC_UINPUT_BACKEND=c` becomes unavailable, and it reports that clearly.

The plain `uv build` is still the default so that local development keeps
the C extension around, which is what the backend equivalence tests
compare against.

The event codes (`KEY_*`, `BTN_*`, `ABS_*`, `REL_*`) are likewise inlined
in `steamcontroller_original/input_event_codes.py` rather than parsed out of
`/usr/include` on every import, so kernel headers aren't needed at runtime
either. Regenerate that file if you ever need to:

```shell
uv run python -m steamcontroller_original.gen_input_event_codes > src/input_event_codes.py
```

A test compares the inlined copy against the headers on the machine, so
drift shows up rather than going unnoticed.

### Wayland

The daemons (`sc-xbox.py`, `sc-desktop.py`, `sc-mixed.py`, `sc-gamepad.py`)
talk to `/dev/uinput` directly at the kernel level and never touch a display
server at all, so they work identically under X11, Wayland, or a bare TTY.

`sc-gyro-plot.py` is the only GUI piece. It's built on PySide6/pyqtgraph
with no X11-specific APIs (verified: nothing in this codebase references
`DISPLAY`, `xcb`, or `QScreen.grabWindow`; the latter is a no-op under
Wayland by design, which is why the screenshot test uses `QWidget.grab()`
instead). PySide6 ships Qt's native `wayland` platform plugin, and
`QT_QPA_PLATFORM=wayland uv run --extra gui sc-gyro-plot.py` will use it
automatically on a Wayland session.

`res/config.ui` is a Qt Designer file left over from an earlier,
never-finished attempt at a full configuration GUI (see TODO item 6
below), and no code currently loads it.


## TODO / Status

 1. Finish to guess each bytes/bits roles in the usb message (**Done**).
 2. Understand how to configure haptic feed backs (**Done**).
 3. Understand how to enable gyroscopes (**Done**).
 4. Redirect inputs to userland events via uinput (**Done**).
    - Xbox360 uintput device (**Done**)
    - Keyboard uintput device (**Done**)
    - Mouse uintput device with trackball model (**Done**)
 5. Create a simple xbox event mapper (**Done**)
 6. Create a configurable event mapper (**Paused**):
   - Create an event mapper that reads steam vdf files and maps usb inputs to uinput events.
   - Create fallback mappings for unsupported config options.
   - Get all possible configurations of steam config file.
 7. Create a haptic feedback Manager (**Paused**)
 8. Measure latencies.
 9. Support multiple controller in wireless mode (**Done**)
 10. Support multiple controller in wired mode
 11. Support correct deconnexion of controllers (with 2sec press on steam button) (**Done**)
 12. Add support to control light intensity
 13. Add support for gyroscopes in the event mapper:
     - Enable gyro condition (always on, or on specific button event)
     - Use gyro as mouse (add yaw, pitch, roll accell to mouse event with a scale factor).
     - Use gyro as an axis (compute yawn, pitch or roll from quaternion, normalize to -32768 32768 and use it as an axe)
 14. Optimize event mapper.
 15. Verify if pairing between a controller and a dongle is possible without steam or add a tools to do it.
 16. Add support to change "music" for power on off.
 17. Create an tool to convert musical notes, to haptic messages.

## Control Messages Capture

 1. `sudo modprobe usbmon`
 2. `lsusb -d 28de:1142` and look at bus and device numbers (B & D)
 3. `sudo cat /sys/kernel/debug/usb/usbmon/Bu | grep Co:B:D:0` (B=3 and D=003 for example)

### Disable auto feedback on rpad:

 - `81000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000`

### Enable Gyro

 - `87153284 03180000 31020008 07000707 00301400 2f010000 00000000 00000000`

### Disable Gyro

 - `87153284 03180000 31020008 07000707 00300000 2f010000 00000000 00000000`

### Stop Controller
 - `9f046f66 66210000 ...`


## Control Messages formats

### Haptic feedback format:

 - u8  : `8f`
 - u8  : `07`
 - u8  : `00` for Right `01` for Left
 - u16 : Amplitude
 - u16 : Period
 - u16 : count
 - pads the end with `00`
