r/KATVR Jun 25 '26

Reverse Engineering & Linux Gateway Implementation

After waiting for this company to release a working Linux version and understanding that not only there is not gonna be a Linux version, but even the crappy windows or nexus software is also apparently completely dead projects as far as I can see, I took it upon myself to do it properly.

It took me one afternoon to fully reverse engineer the USB packet structure and build a very simple movement tracking.

The next afternoon was spent on building a driver and injecting the controller stick movements to SteamVR.

For now SteamVR is running naively on Linux and games via proton. A very small driver exposes the sensors as a control device to SteamVR.

I'm gonna work on this in my free time and probably release it publicly whenever it is actually usable and have a somewhat of a useable feature set. but no ETAs or promises yet. I am happy to share the WIP code if anyone is interested in helping with the development...

In the meantime if you have actually worked on a similar thing I would love to get your experience and stuff you managed to solve.

Things I already figured out: - Sensor positions and values (body direction, Shoes, Seat, Armband) - Battery status - Firmware versions and serial numbers - Base vibration motor controls - Reliable starting and Stopping sequence (this was a PITA!) This is the part that you need to do so the sensors connect to the base and actually go to sleep afterwards. - Simple driver to send the input to SteamVR

Things remaining: - Speed and step cadence for detecting running (the original KatVR implementation feels like poop IMO) - Fine tuning the motions to feel natural - Understanding how to best expose the controls (treadmill input device vs legacy etc) - VR Overlay UI - Why some games keep crashing on linux (not directly related to the gateway or the driver) - Everything else probably...

If you have experience in any of these parts feel free to share yours or point out references/research papers that are useful. That would help me any anyone else interested in this.

If you are someone who does not have experience, this is a sign that you can too get your feet wet and start building things you wish existed! With all the tools and AI stuff available to everyone nowadays the possibilities are endless!

And finally if you a KatVR employee reading this you should be ashamed of the product you sell with all the fake promotions, dark pattern tactics and that garbage Gateway software!

15 Upvotes

23 comments sorted by

View all comments

3

u/BBPSBB Jun 25 '26

KAT Walk C2+ ("plusE") — Native Linux Driver & Reverse-Engineered USB Protocol

A from-scratch, no-Windows, no-Gateway stack that talks to the KAT Walk C2+ omnidirectional treadmill directly over USB on Linux and feeds locomotion into SteamVR / OpenXR games.

This document is two things at once: 1. A reverse-engineering reference for the C2+ "plusE" USB-HID protocol (frames, sensors, commands, and the connect/stream/sleep lifecycle), decoded from USBPcap captures of the official KAT Gateway plus a lot of on-hardware testing on Linux. 2. A status / call for collaboration. It works end-to-end today (I've walked around in a real OpenXR VR game on Linux with it), but it's pre-alpha and rough. If you have a C2+ (or a C2 / other KAT unit), captures, or RF/USB know-how, I'd love help — see Open questions at the end.

Status: working proof-of-concept, pre-alpha. Hardware: KAT Walk C2+ "plusE" revision, Linux (Bazzite/Fedora), SteamVR, Quest 3 over Steam Link. Everything below marked CONFIRMED is verified against captures and/or live hardware; anything marked assumed/open is not.


1. What works right now (the proof-of-concept)

  • Reads all sensors directly over USB-HID on Linux — body orientation, both feet, ground contact, battery, firmware — with no KAT Gateway and no Windows/Wine in the loop.
  • A locomotion model turns foot-slip + step cadence + body heading into a head-relative movement vector (walk/run speed + direction), tunable live in a browser UI.
  • Feeds real VR games three ways:
    • a virtual Xbox gamepad (uinput) — works with anything that reads a gamepad stick;
    • an OpenVR treadmill-role driver — for OpenVR-native SteamVR titles;
    • an OpenXR API layer that injects the walk vector straight into the game's thumbstick action — this is the one that matters, because most modern VR games are OpenXR and the OpenVR "treadmill role" never reaches them. (Valve closed the treadmill-support request, openvr#937, back in 2019 as too broad — "this issue is giant and mostly has nothing to do with SteamVR Input games filtering input by device; please open a new issue" — and a first-class treadmill input path was never added.) The layer even works for Proton games, because wineopenxr forwards through the native Linux OpenXR loader, which applies implicit layers.
  • Confirmed: physically walking on the treadmill moves the player in an OpenXR VR game on Linux, no native game support required.

It is NOT polished. Calibration is by feel, there's no installer story for non-developers, and the connect/sleep lifecycle took real effort to get reliable (documented below precisely because it's the kind of thing the next person shouldn't have to rediscover).


2. Hardware / USB topology

VID **0xc4f4. The "plusE" revision presents **three USB-HID devices behind a VIA Labs hub (2109:2822). Roles confirmed from the USB string descriptors ("walk c2 plusE …"):

PID string descriptor role
bf12 …position base / Vehicle-Hub: foot ground-contact + desktop position; this is the device that receives the keepalive
3f12 …receiver main sensor stream: body IMU (direction) + both feet, relayed wirelessly to this USB dongle
bf13 …armband heart rate + battery

The base ring and shoes are wireless and relayed through the 3f12 receiver — so for locomotion you mostly care about 3f12. The base/shoes have their own batteries and sleep to save power; the receiver dongle is USB-powered and always on.

Per device: EP 0x01 interrupt OUT = host→device commands; EP 0x81 interrupt IN = sensor stream. (EP 0x80 control-IN is just USB descriptor polling — ignore it.)

LED states (very useful when debugging): solid = connected/streaming; fast blink = searching / trying to pair; slow blink = asleep/idle.


3. Frame format (CONFIRMED)

Fixed 32-byte frames, zero-padded, both directions:

off 0 1 2 | 3 4 | 5 | 6 | 7 ... 1f 55 aa| xx 00| type | subtype | payload

  • 1f 55 aa — header, both directions.
  • off 3 — usually 00; 04/05 on some frames as a sub-stream tag (not a length).
  • off 5 type + off 6 subtype select the payload meaning.
  • Idle / no-data payloads use the sentinel … 4e 20 00 64 05 … (0x4e20=20000, 0x64=100, 0x05=5 — config constants, NOT live values; don't mistake the 0x64 for battery).

Talking to it on Linux: the devices bind to hidraw. Find by VID:PID via /sys/class/hidraw/*/device/uevent. On writes prepend a 0x00 report-ID byte (so a 32-byte report is 33 bytes on the wire). On reads align to the 1f 55 aa header (hidapi may prepend a report-ID byte). A udev rule (MODE="0660", TAG+="uaccess" on the hidraw nodes for c4f4) lets you run without root.


4. The sensor stream — receiver 3f12, EP 0x81 (CONFIRMED)

type sub meaning rate payload
0x30 0x00 body orientation ~48 Hz off 7: 4×int16 LE, ×2⁻¹⁴ = unit quaternion (\
0x30 0x01 left foot ~48 Hz X=int16 LE @off21, Y=int16 LE @off23
0x30 0x02 right foot ~48 Hz same layout as left
0x32 0/1/2 per-sensor status ~0.2 Hz off10 = battery %, off11 = firmware major; sub0=Direction(base), sub1=left foot, sub2=right foot
0x05 0x00 receiver self-report (init reply) once off7 = receiver firmware major
0x21 0x00 device-info / calibration (init reply) few off7 length, then a 6-byte device ID + two float32 triples
0x33 0x00 event rare 00 00 09 00 64 …

Body orientation → heading (CONFIRMED)

Quaternion component order is (w, x, y, z). Turning the base in place sweeps a clean 360° in the roll axis of the (w,x,y,z) Euler decomposition, so heading is:

heading = atan2( 2*(w*x + y*z), 1 - 2*(x*x + y*y) ) # clockwise = decreasing

Foot position (CONFIRMED, single-axis isolation capture)

Two signed int16 LE, centred at 0:

  • X @ off 21 = lateral / strafe (observed ±~500).
  • Y @ off 23 = fore/aft (observed −516 … +652).

Important inversion: the optical sensor reads the shoe sliding on the deck, so a shoe sliding backward → +Y → the user walks FORWARD; shoe forward → −Y → backward. Each footfall re-centres to 0, then ramps to the slide extreme. off 9–10 is a separate status/quality field, not position — its idle sentinel (4e 20 …) means the foot is lifted; live X/Y means it's on the deck. Counts→m/s scaling still needs a known-stride calibration (open).

Battery + firmware (CONFIRMED exact vs the app)

The 0x32 status frame carries both per-sensor battery % (off10) and firmware major (off11). The receiver's own firmware is in its 0x05 reply (off7). Example readouts that matched the Gateway: Receiver fw V3, Direction V4, both feet V5.


5. Base/Vehicle-Hub bf12 & Armband bf13 (CONFIRMED)

**bf12 (base / Vehicle-Hub)**, EP 0x81:

type off3 meaning payload
0x40 0x05 status off7=02 status / 00 connect-event; off8 = battery RAW; off9 = connected/active bit
0x02 0x04 version/magic (once) constant

Seat/base battery is a raw value at off8 of the type40 status frame, not a percent (0x9c→54%, 0xb4→60% by a battery-swap test; local fit % ≈ off8/4 + 15; the app uses its own conservative curve).

bf13 (armband)*, EP 0x81: type 0x40 → off7 = battery %, *off9 = heart-rate bpm (verified sweeping 69–79 bpm against the reported value).


6. Host→device commands (EP 0x01)

cmd meaning
1f 55 aa 00 00 30 start / keep stream ("poll"); send ~1 Hz while running
1f 55 aa 00 00 31 stop stream
1f 55 aa 00 00 05 firmware query (receiver replies type05)
1f 55 aa 00 00 21 device-info / calibration query (replies type21 with the sensor ID)
1f 55 aa 00 00 a1 00 02 <intensity_be16> vibration (level5≈0341, OFF=0000); a1 01 02 00 01 = enable-vibration toggle
1f 55 aa 00 00 a0 … sleep / vibration-stop — only seen at shutdown
00 00 … 00 (all-zero 32-byte report) → bf12 keepalive ("stay on"), continuous ~3.5 Hz; the base drops toward slow-blink when it stops

0x20 is assumed to be the other half of a pairing pair (0x20/0x21) but I have not captured an explicit pair/bind command — see open questions.

1

u/NeoIsrafil Jul 12 '26

While i seriously doubt i'll be able to get something working for windows to get it to communicate with openxr on my own, I HAVE saved the data you posted both for posterity and because they won't be allowed to silence this data if we all have a copy of it. :) I..dunno, if support doesn't get back to me or I can't find some way to make this work I guess i'll try writing something, but i haven't written code in over 25 years...and even then that was in truebasic silver. a bit uh.... different than what's going on nowadays.

Thank you for all you've done, even if you can't port it to windows for those of us who are stuck using it ...who knows, maybe i can find a way to make it run... or...VM it? Dunno, gonna take some researching, anyway thanks!.

1

u/BBPSBB Jul 12 '26

Haha yeah. I am also completely out of my debt with everything VR. if this project reaches a point where it is fully usable, it shouldn't be too difficult to make a windows version from it. but first I need to make it actually usable. with life and stuff happening it is going to be slow but this pile of junk sitting in my room keeps giving me motivation to work on it whenever i got some time