Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Steering Hub

A small Windows program that sits between a force-feedback wheel and a game.

The game computes the force at the steering rack from its own tyre model and sends it here over a local WebSocket. The hub drives the wheel's motor through DirectInput and sends back the steering angle, the pedals, the buttons and the shifter. It also synthesises the fast texture (road surface, kerbs, ABS, engine idle, impacts) on its own 1 kHz thread, from intensities the game sends.

Built for games that want to own their force model: the game owns the tyre model and the steering geometry, the hub owns the wheel and the fast detail.

What you need

  • Windows x64.
  • A wheel with a DirectInput driver, which is any wheel: the manufacturer's driver provides that layer.
  • The .NET 10 SDK, only to build. The finished exe is self-contained.

Build

Double-click build.cmd. SteeringHub.exe appears next to it: one file, runtime inside, movable to any machine. The script checks for the .NET 10 SDK and says where to get it if it is missing.

By hand, the same thing:

dotnet publish src\SteeringHub\SteeringHub.csproj -c Release -r win-x64 -o publish

Settings and the log live in %LOCALAPPDATA%\SteeringHub\.

The exe is about 18 MB. Half of that is Skia, the renderer Avalonia draws with, plus HarfBuzz for text; the rest is the .NET runtime, trimmed to what the program uses. A build that leaves the runtime out is no smaller as a file, because the SDK cannot compress a runtime-less bundle, and only smaller as a download:

Build Exe Zipped Needs on the machine
self-contained, trimmed (default) 17.8 MB 12.7 MB nothing
framework-dependent 18.3 MB 7.3 MB the .NET 10 runtime (Steam installs it with s&box)

The framework-dependent build is dotnet publish src\SteeringHub\SteeringHub.csproj -c Release -r win-x64 -p:SelfContained=false -p:PublishTrimmed=false. On a machine without the runtime it shows a dialog with a download link instead of a window.

Build only on Windows. DirectInput is a Windows API and there is no cross-platform force feedback.

First run

  1. Plug the wheel in and start the exe. If it is the only force-feedback device attached, the hub picks it by itself.
  2. On the Wheel page press Start. The setup takes about two minutes: rest position, left lock, right lock, throttle, brake, clutch, handbrake, shift up, shift down, and finally the force direction. Nothing asks you which hardware axis a pedal is on; the hub watches all eight and takes the one that moved. The last step is the only thing that cannot be read from the hardware: the hub pulls the wheel to one side and asks which way it went.
  3. Set Rotation range to what your wheel's own driver software is set to. The game uses it to place the steering lock.
  4. Start the game and turn on wheel support in its settings. It connects to the hub by itself.

Every change is saved as you make it. There is no Save button.

The window can be minimised. Polling and force run on their own thread and do not depend on the window being drawn. Mute in the top corner parks the wheel (every force to zero) for alt-tabbing out of a live session.

Language

The picker sits next to Mute, and every entry names its language in that language. On a first run the hub takes the language Windows itself is in, and falls back to English for the ones it has no file for. The choice is saved and applies immediately: the window re-says itself without restarting, wizard included.

Shipped: English, Русский, Українська, Deutsch, Français, Español, Italiano, Português (BR), Polski, Čeština, Türkçe, 日本語, 한국어, 简体中文.

Adding one is adding a file. Copy src/SteeringHub/Assets/Lang/en.json to the language's code, translate the values (_name is what the picker shows), and rebuild — the folder is listed at startup, so there is no table to extend. python Tools/check_lang.py reports any key or {0} placeholder that drifted from English before the player finds it. SteeringHub.exe --lang=fr runs in one language for one run without touching the saved choice, which is how to look at a translation you are working on.

Setting the force

On the Force page, adjust Overall force while looking at the trace, not while guessing by feel. The bright line is the force from the tyres; the grey line adds road texture. Red marks along the top mean the sum asked for more than the wheel can give. Detail disappears there, and that feels exactly like a weak wheel, which is why strength usually gets turned up further. The right setting is the one where the marks appear only in the hardest corners.

Turn off the driver's own centring spring is on by default and should stay on. Every consumer wheel ships with a centring spring for desktop use; left on, it pulls the rim to centre and masks everything the game computes. It is the most common reason a correctly set up wheel still feels vague.

Test works without a game: pull left, pull right, spring, damper, and a 0.5 to 8 Hz sweep that finds the wheel's own resonance and the frequency above which its belt stops transmitting anything.

How it works

  game                                          hub (this program)
  ────                                          ──────────────────
  aligning torque from the tyre model  ──┐
  load, slip, surface, ABS, impacts      │  GameFrame  ┌─ mixer, 1 kHz ─────┐
  engine speed                         ──┴── ~200 Hz ──┤  oscillators for   │──> one constant force
                                                        │  kerb / ABS /      │    + damper / friction /
  steering angle, pedals              <──── 500 Hz ────┤  engine / surface  │      spring conditions
  buttons, shifter                       InputFrame    └─ DirectInput ──────┘

The division of labour is the point:

  • The game computes the steady force. Only the game has the tyre model. A real aligning torque collapses when the slip angle passes the peak, and that collapse is why a real wheel goes light at exactly the moment the front starts to wash out. A force model built on lateral acceleration does the opposite: it keeps rising while the car is already leaving the road.
  • The hub renders the fast texture. A 40 Hz kerb rumble computed at a 100 Hz physics step arrives as a 25 Hz thud. The same oscillator on the hub's own thread is clean.

The steady force arrives once per physics step of the game (every 20 ms at 50 Hz, every 13 at 76) and the hub interpolates between frames over the interval it measures, so the wheel sees a straight ramp rather than a staircase. The cost is one frame of lag; the alternative is a buzz at the physics rate.

Everything is summed into a single constant-force effect that is updated every tick, rather than a stack of periodic effects. Updates change the effect's parameters while it plays; the effect is not stopped and restarted, which on some wheels is felt as a spike on every update. Periodic effects are played by the wheel's firmware at its own sample period (usually 10 ms), and a wheel has between two and eight effect slots. Damper, friction and spring stay as device-side conditions: they are functions of the rim's position and velocity, which the device knows exactly and continuously and the hub only knows one poll late.

Protocol

The reference implementation is one dependency-free C# file: src/SteeringHub/Protocol/WheelProtocol.cs. Copy it into the game. It uses nothing beyond BitConverter and arithmetic, allocates nothing per frame, and is explicitly little-endian.

The hub listens for binary WebSocket frames on ws://127.0.0.1:8443/. Any path is accepted. A text frame is decoded as base64 of the same bytes, so a hand-written test client works too.

Every packet is a 4-byte header (type, protocol version, payload length as u16) followed by the payload. The version byte is checked on every packet, and a client speaking another version is reported in the log and on the Diagnostics page rather than silently misread.

Packet Direction When Contents
Hello game → hub once per connection game name and version, car name, requested input rate, wheel range
Status hub → game in reply to Hello, and when anything changes device flags, device name, hub version, granted input rate, rotation range, master gain
Frame game → hub every physics step rack torque, damper, friction, spring and its dead band, rack position, surface and kerb intensities, kerb frequency, ABS pulse, engine vibration and frequency, signed impact, slip and load, speed, rpm, gear, flags
Input hub → game at the input rate, default 500 Hz steering (-1..1 and degrees), throttle, brake, clutch, handbrake, 32-bit button mask, H-pattern gear, flags
Release game → hub leaving the car, pausing, unloading nothing; every effect goes to zero
Ping / Pong either once a second sequence number

Sign convention, stated on both ends: positive torque pulls the rim to the right (clockwise from the seat), and the steer axis reads positive to the right.

The Frame carries intensities, not waveforms, for the texture channels. The game says how rough the road is; the hub owns the noise generator.

A UDP listener on 127.0.0.1:29760 exists for external tools and tests and is off by default.

Games in a sandbox

s&box game code can open a loopback connection only on ports 80, 443, 8080 and 8443, has no System.Net.Sockets, and does have a binary-capable WebSocket client. That is why the default is a WebSocket on 8443. If your engine has similar restrictions, the port is on the Diagnostics page under Ports and rates.

SimHub

Tick Send telemetry to SimHub on the Diagnostics page. The hub writes steeringhub.simdef next to the exe and a registration in %LOCALAPPDATA%\SimHub\ExternalSims\Registrations\. Nothing to copy by hand. Speed, rpm, gear, steering angle, lateral and longitudinal G and slip go out at 60 Hz over UDP.

If something does not work

The game does not see the hub. The hub has to be running. The address is printed on the Diagnostics page; the game reconnects by itself with a growing pause of up to five seconds.

Force stops when the game gets focus. The hub asks the driver for background exclusive access, which is what lets it drive the motor while the game is in front. Some drivers refuse and the hub falls back to foreground access; the status bar shows an amber dot and the Wheel page says so. Reopen the device while the hub window is active, or check the driver's own settings.

The wheel does not pull at all. Force page, Test, Pull left. If nothing happens there either, the effects are most likely bound to the wrong axis: under Manual mapping on the Wheel page, set the steering axis, and the effects follow it.

The wheel pulls the wrong way. Run the setup again and answer the last step, or tick Invert force direction on the Force page.

The force buzzes, or comes in spikes. Look at the trace on the Force page. Red marks along the top are clipping; without them, a buzz at a steady pitch while turning slowly is the game's physics rate showing through, and the log line saying the driver refused a live effect update means this wheel restarts its effect on every change, which the hub then cannot avoid.

Diagnostics. The log on the Diagnostics page is also written to %LOCALAPPDATA%\SteeringHub\hub.log, truncated at every start. --log-level=debug on the command line makes it talkative. If the process dies before the window appears, crash.txt is written next to the exe and a dialog says where the log is.

Licence

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages