fix(android): SC2 sticks — SETTING_ENABLE_RAW_JOYSTICK=0 for calibrated i16
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 52s
decky / build-publish (push) Successful in 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m2s
docker / deploy-docs (push) Successful in 25s
apple / swift (push) Successful in 5m20s
android / android (push) Successful in 12m27s
arch / build-publish (push) Successful in 16m4s
deb / build-publish (push) Successful in 16m59s
ci / rust (push) Successful in 18m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m8s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m15s
apple / screenshots (push) Successful in 21m5s

A Steam Controller 2 opened in raw mode (our capture claims the HID interface)
reports ADC joystick coordinates ~0..3200, which Steam/SDL read as only a few
percent of full travel — the sticks barely move in Steam's controller test even
though menu navigation still crosses its lower threshold. Steam sends
SETTING_ENABLE_RAW_JOYSTICK (0x2e) = 0 during native init to force
firmware-calibrated signed i16; replicate it (NORMALIZE_JOYSTICKS) alongside
lizard-off at claim time and on the 3 s watchdog refresh (the refresh also
repairs a host/driver that re-enabled ADC mode after capture started).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 17:05:56 +02:00
parent e1d7fa2a30
commit 694bec4ead
2 changed files with 25 additions and 5 deletions
@@ -79,6 +79,20 @@ object Sc2Device {
// [4..6] = LIZARD_MODE_OFF (0) — already zero
}
/**
* Force firmware-calibrated signed i16 stick coordinates. Steam sends this during physical
* controller initialization (`SETTING_ENABLE_RAW_JOYSTICK` = 0x2e, value 0); without it a
* controller previously opened in raw mode reports ADC coordinates around 0..3200, which a
* Triton consumer interprets as only a few percent of full travel.
*/
val NORMALIZE_JOYSTICKS: ByteArray = ByteArray(64).also {
it[0] = 0x01 // feature report id
it[1] = 0x87.toByte() // ID_SET_SETTINGS_VALUES
it[2] = 3 // one ControllerSetting {u8 num, u16 value}
it[3] = 0x2E // SETTING_ENABLE_RAW_JOYSTICK
// [4..6] = disabled (0) — firmware emits calibrated signed i16 values
}
const val LIZARD_REFRESH_MS = 3000L
/** Wire mapping: SC2 button bit → punktfunk `Gamepad.BTN_*`, the inverse of the host's
@@ -135,7 +135,7 @@ class Sc2UsbLink(
@Suppress("UnspecifiedRegisterReceiverFlag")
context.registerReceiver(receiver, filter)
}
claimed.forEach { sendFeature(conn, it.iface.id, Sc2Device.DISABLE_LIZARD) }
claimed.forEach { configureInputMode(conn, it.iface.id) }
reader = Thread({ readLoop(conn, claimed) }, "pf-sc2-usb").apply {
isDaemon = true
start()
@@ -215,11 +215,12 @@ class Sc2UsbLink(
while (running) {
val now = android.os.SystemClock.elapsedRealtime()
if (now - lastLizard >= Sc2Device.LIZARD_REFRESH_MS) {
// Refresh on every claimed interface until one is known-active (the dongle
// relays it per-slot); afterwards the active one suffices.
// Refresh both required firmware modes. The raw-joystick setting is normally
// persistent, but replaying it also repairs a host/driver that enabled ADC
// coordinates after capture started.
val target = activeClaim
if (target != null) sendFeature(conn, target.iface.id, Sc2Device.DISABLE_LIZARD)
else live.forEach { sendFeature(conn, it.iface.id, Sc2Device.DISABLE_LIZARD) }
if (target != null) configureInputMode(conn, target.iface.id)
else live.forEach { configureInputMode(conn, it.iface.id) }
lastLizard = now
}
// Submit the next pending OUT report on the active (else first) interface.
@@ -321,6 +322,11 @@ class Sc2UsbLink(
sendReport(conn, ifId, type, data)
}
private fun configureInputMode(conn: UsbDeviceConnection, ifaceId: Int) {
sendFeature(conn, ifaceId, Sc2Device.DISABLE_LIZARD)
sendFeature(conn, ifaceId, Sc2Device.NORMALIZE_JOYSTICKS)
}
private fun sendFeature(conn: UsbDeviceConnection, ifaceId: Int, data: ByteArray) {
sendReport(conn, ifaceId, REPORT_TYPE_FEATURE, data)
}