diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2Device.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2Device.kt index 7a202d0b..50b03ca6 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2Device.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2Device.kt @@ -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 diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2UsbLink.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2UsbLink.kt index baf74cc5..040d5d29 100644 --- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2UsbLink.kt +++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/Sc2UsbLink.kt @@ -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) }