Steam Controller Auto-Charge steers with 70 Hz haptics and a WASM detector
Slam the controller into the magnetic puck until it charges
At a glance
- What is it?
- Steam Controller Auto-Charge is a browser app that flies a Steam Controller onto its magnetic puck by combining an OpenCV.js optical flow loop with WebHID haptic pulses, plus a Rust CNN compiled to WebAssembly for object avoidance. It works, at the cost of an overhead camera, three clicks to mark points, and a hard dependency on Nix.
- Who is it for?
- Steam Controller Auto-Charge is worth your afternoon if you already own the puck, a Steam Controller and a spare webcam, and you like watching a machine solve a problem it was not designed to solve. It is not the tool for anyone who just wants the controller charged: docking by hand takes two seconds and no build.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 92 days ago.
- What is it written in?
- Mainly Vue, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two loops: optical flow steers, a WASM CNN avoids
The control loop lives in App.vue, which handles the camera stream, UI reactivity, a PID tracking loop, and the OpenCV.js Lucas-Kanade optical flow computed through calcOpticalFlowPyrLK. Points you select on the overhead camera image become the tracked features, and the PID loop turns their motion into a steering signal.
Object avoidance is separate and runs off the main thread. objectDetector.ts and objectWorker.ts push detection into a Web Worker so the tracking loop stays fluid, and the detector itself is a Rust implementation in wasm-object-detect compiled to WebAssembly for what the README calls high-performance visual processing.
That split is the whole engineering argument. Optical flow tells you where the puck moved to; the detector tells you what else is in frame. Running the second on a worker is what stops a frame of detection from stalling the loop that is physically driving a haptic motor.
The wasm side has its own build step, wired into package.json as wasm:build, which calls wasm-pack against src/wasm-object-detect and writes the output to ../wasm-pkg.
Steering is 70 Hz asymmetric haptic pulses, not a motor
There is no robot arm and no servo. The controller is moved by its own motors: steamController.ts maps standard WebHID calls onto the byte payloads the Steam Controller expects, and navigation works by firing 70 Hz asymmetric haptic pulses through the internal dual Linear Resonant Actuators, the LRAs.
Asymmetric is the operative word. Each pulse pushes harder in one direction than the other, so a fixed frequency becomes a steering command rather than a vibration. At 70 Hz the controller creeps across the desk toward the puck on its own power.
Telemetry rides the same wire. The app connects to what the README calls the Triton Controller natively through WebHID and streams input and telemetry, with Report 67 named as the telemetry report. There is nothing to configure in the browser and no page extension to install; the browser is the remote control.
Inside 150 pixels the pulse rate halves
Creep mode is the detail that makes docking gentle. When the controller comes within 150 pixels of the puck on the camera image, the haptic pulse frequency is automatically cut by 50 percent, so the last stretch of travel is slow enough that the magnetic dock captures the controller rather than launching it across the room.
The threshold is measured in pixels, and that is worth thinking about before you trust it. Change your webcam resolution, or move the camera closer or further, and 150 pixels stops meaning the same physical distance. A setup tuned at 720p may arrive at the puck too fast on a 4K feed, or crawl the last few centimetres on a low one.
This is also the one setting in the project that is expressed in the units of the camera rather than the units of the desk, and the README gives no override for it.
Report 121 is how it knows the dock worked
Success is read, not assumed. Battery Status Polling intercepts Report ID 121, written 0x79, to confirm that magnetic charging actually started. Report ID 67, written 0x43, is parsed to show live battery percentage and battery cell voltage in millivolts.
Those two byte identifiers are the whole telemetry contract, and they are version-specific to this controller. The repository ships triton_2026_steam_controller_spec.md at the root, which is where the payload layouts are written down. If you ever need to debug a controller that will not pair or a report that never arrives, that file is the place to start, along with the WebHID abstraction in steamController.ts.
Nix is not optional, because postinstall calls it
The README calls the Nix Package Manager the only build dependency you need and says it works on Windows, Mac and Linux. The documented start is one command:
nix-shell --run "npm install && npm run dev"That command fetches dependencies and builds the WASM module in the same pass. What the README does not spell out is that this is not a preference. package.json carries a postinstall script:
"wasm:build": "cd src/wasm-object-detect && wasm-pack build --target web --out-dir ../wasm-pkg",
"dev": "vite",The postinstall script runs nix-shell itself, so a plain npm install on a machine without Nix fails at that step rather than later. The dependency list explains why: onnxruntime-node, @tensorflow/tfjs-node, canvas and sharp are all native modules, and wasm-pack has to be on the path when the postinstall fires.
Two more version details from the same file. The OpenCV.js binding is pinned to a release candidate, ^5.0.0-release.1, so the vision code is running against a pre-release API. And the package is still named vite-vue-typescript-starter at version 0.0.0 and marked private, which is the default identity of the scaffold this was built on.
Auto-Track comes back on its own after a reload
Clicking the Auto-Track button engages automatic tracking, the button highlights to show it is active, and clicking again disengages. The behaviour worth knowing is in between: tracking resumes automatically on page reload.
That is convenient while you are tuning the camera and slightly less convenient if you reload the page for another reason, because haptics start again without a fresh confirmation.
Automatic does not mean hands-off either. Manual tracking is still there and still needs three clicks in a specific order: the puck, then the top of the controller, then the bottom of the controller. Those are the points the optical flow loop follows, so the first setup of a session is a calibration step. Auto-Track handles everything after that, using those points plus the detector for anything in the way.
A camera above the desk is the real dependency
The hardware list is short. An overhead webcam pointing down at your desk, the charging puck on that desk, the controller standing upright beside it, and a Chromium-based browser with WebHID. Pairing happens from the web interface with the Connect Steam Controller button.
The browser requirement is the one with no workaround written down. WebHID is the entire transport for both the telemetry and the haptic pulses, and the README asks for a Chromium-based browser rather than describing a fallback path. Safari and Firefox are not in scope here.
The README also states that Nix works on Windows, Mac and Linux without going into platform-specific steps for any of them. For NAS-style or Portainer-managed setups there is nothing; the repository's own pages carry the instructions and stop there.
One thing the README does not mention at all is the test setup, and there is one: package.json wires a Node test runner over tests/**/*.test.ts plus a Playwright script, and playwright.config.ts sits at the root.
One release, and package.json still says 0.0.0
There is a single release, v0.1, published on 2026-06-27 and described as a simple pure js version with manually selected tracking points. The README you are reading describes something else: a Rust CNN compiled to WebAssembly doing object avoidance, plus proximity creep mode at a halved pulse rate. So the only tagged artefact is behind the current README, and there is no tag for the state described here.
The last push on the repository was on 2026-07-02, and it is not archived.
The repository is more organised than the release history suggests. Alongside the usual build files it carries AGENTS.md for coding agents working inside it, data/ for assets, a scripts/ directory, shell.nix, and the controller protocol document named above. The licence is MIT.
Inspiration is credited to Very Lazy Pixel, with the video linked from the README, and the honest summary of the whole project is in the repository description: slam the controller into the magnetic puck until it charges.
Editorial conclusion
Steam Controller Auto-Charge is worth your afternoon if you already own the puck, a Steam Controller and a spare webcam, and you like watching a machine solve a problem it was not designed to solve. It is not the tool for anyone who just wants the controller charged: docking by hand takes two seconds and no build. Verify two things before you start, whether postinstall calling nix-shell works on your machine outside the documented one-liner, and whether your camera's resolution puts the dock inside the 150-pixel proximity threshold or well outside it. There is exactly one release, v0.1, and the repository's last push was on 2026-07-02.
Frequently asked questions
What does Steam Controller Auto-Charge need in order to run?
An overhead webcam pointing down at your desk, a Chromium-based browser supporting the WebHID API, and the Nix Package Manager, which the README calls the only build dependency you need.
How does Steam Controller Auto-Charge move the controller?
It navigates the controller towards the puck by firing 70 Hz asymmetric haptic pulses through the controller's internal dual Linear Resonant Actuators, streamed over WebHID. It reads no motors of its own.
Do I still have to pick tracking points by hand?
Yes. Manual tracking means clicking the puck, then the top of the controller, then the bottom of the controller. Auto-Track uses those points afterwards and resumes automatically after a page reload.
How does the app know the controller reached the puck?
It intercepts Report ID 121, written 0x79, to confirm magnetic charging started, and parses Report ID 67, written 0x43, to display live battery percentage and cell voltage in millivolts.
How do I start Steam Controller Auto-Charge?
With one command, nix-shell --run "npm install && npm run dev", which fetches dependencies and builds the WASM module. Note that the npm install step triggers a postinstall script that calls nix-shell again to run the Rust to WebAssembly build.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fossprime-steam-controller-auto-charge)