feat: implement rumble TX over the GIP data path
Port the rumble packet from upstream driver/gamepad.c: a 9-byte GIP_CMD_RUMBLE payload (motors R|L|RT|LT, intensities scaled to the 0..100 GIP range, duration/repeat constants). xone_controller_rumble now sends it via client::send_rumble; the app button pulses both motors for 0.6s then stops them. Completes the host to controller TX remainder of Phase 3 (the dongle LED is already driven at chip level for pairing and client lifecycle). Co-Authored-By: qwen3.8-27b@q3_k_xl: implemented rumble TX
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.21)
|
cmake_minimum_required(VERSION 3.21)
|
||||||
project(xone_macos VERSION 0.1.26 LANGUAGES CXX Swift)
|
project(xone_macos VERSION 0.1.27 LANGUAGES CXX Swift)
|
||||||
|
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
|||||||
@@ -100,11 +100,12 @@ int xone_controller_get_state(xone_dongle const *d, int index,
|
|||||||
// Enter or leave pairing mode (beacon pairing flag + LED blink).
|
// Enter or leave pairing mode (beacon pairing flag + LED blink).
|
||||||
void xone_set_pairing(xone_dongle *d, bool enable);
|
void xone_set_pairing(xone_dongle *d, bool enable);
|
||||||
|
|
||||||
// Trigger a rumble test on the controller at index. Returns 0 on success,
|
// Send a rumble packet to the controller at index (host to controller TX
|
||||||
// -1 for an invalid session or index. Not implemented yet: host to
|
// over the GIP data path). Intensities use a 0..255 scale; passing zero for
|
||||||
// controller TX (GIP_CMD_RUMBLE payload) is pending, so this currently
|
// both stops the motors. Returns 0 on success, -1 for an invalid session or
|
||||||
// returns -ENOSYS.
|
// index, or if the client is not ready.
|
||||||
int xone_controller_rumble(xone_dongle *d, int index);
|
int xone_controller_rumble(xone_dongle *d, int index, uint8_t left,
|
||||||
|
uint8_t right);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -33,14 +33,23 @@ final class XoneStore: ObservableObject {
|
|||||||
xone_set_pairing(s, pairingActive)
|
xone_set_pairing(s, pairingActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rumble test seam: the C API returns -ENOSYS until host to controller
|
// Rumble test: a full-intensity pulse. The motors keep spinning until a
|
||||||
// TX is implemented.
|
// zero-intensity packet is sent, so schedule the stop; the session is
|
||||||
|
// re-validated then because it may have closed in the meantime.
|
||||||
func testRumble(_ controller: ControllerInput) {
|
func testRumble(_ controller: ControllerInput) {
|
||||||
guard let s = session,
|
guard let s = session,
|
||||||
let index = controllers.firstIndex(where: { $0.id == controller.id })
|
let index = controllers.firstIndex(where: { $0.id == controller.id })
|
||||||
else { return }
|
else { return }
|
||||||
if xone_controller_rumble(s, Int32(index)) != 0 {
|
if xone_controller_rumble(s, Int32(index), 255, 255) != 0 {
|
||||||
NSLog("xone: rumble TX not implemented yet")
|
NSLog("xone: rumble start failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in
|
||||||
|
guard let self = self,
|
||||||
|
let s = self.session,
|
||||||
|
let i = self.controllers.firstIndex(where: { $0.id == controller.id })
|
||||||
|
else { return }
|
||||||
|
xone_controller_rumble(s, Int32(i), 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-6
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cerrno>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -156,6 +155,17 @@ std::mutex session_mutex; // guards current_session
|
|||||||
// 802.11 management frame header length (three addresses, no extensions).
|
// 802.11 management frame header length (three addresses, no extensions).
|
||||||
constexpr std::size_t k_mgmt_hdr_len = 24;
|
constexpr std::size_t k_mgmt_hdr_len = 24;
|
||||||
|
|
||||||
|
// GIP rumble packet constants, port of the gip_gamepad definitions in
|
||||||
|
// upstream driver/gamepad.c.
|
||||||
|
constexpr std::uint8_t k_gp_motor_all = 0x0f; // R | L | RT | LT
|
||||||
|
constexpr std::uint8_t k_gp_rumble_max = 100;
|
||||||
|
|
||||||
|
// Map a 0..255 intensity onto the GIP motor range (0..k_gp_rumble_max).
|
||||||
|
auto scale_rumble(std::uint8_t value) -> std::uint8_t
|
||||||
|
{
|
||||||
|
return static_cast<std::uint8_t>((value * k_gp_rumble_max + 127) / 255);
|
||||||
|
}
|
||||||
|
|
||||||
// Upstream xone loads one common firmware image for every dongle PID.
|
// Upstream xone loads one common firmware image for every dongle PID.
|
||||||
auto firmware_path_for(std::uint16_t) -> std::string
|
auto firmware_path_for(std::uint16_t) -> std::string
|
||||||
{
|
{
|
||||||
@@ -721,10 +731,12 @@ extern "C" void xone_set_pairing(xone_dongle *d, bool enable)
|
|||||||
d->set_pairing(enable);
|
d->set_pairing(enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rumble test seam: host to controller TX is not implemented yet (the
|
// Send a rumble packet to the controller at index (port of the rumble path
|
||||||
// GIP_CMD_RUMBLE payload format is undefined), so this only validates its
|
// in upstream driver/gamepad.c). Intensities use a 0..255 scale mapped onto
|
||||||
// arguments for now.
|
// the GIP motor range; zero for both stops the motors, which otherwise keep
|
||||||
extern "C" int xone_controller_rumble(xone_dongle *d, int index)
|
// spinning until told.
|
||||||
|
extern "C" int xone_controller_rumble(xone_dongle *d, int index, uint8_t left,
|
||||||
|
uint8_t right)
|
||||||
{
|
{
|
||||||
if (!d)
|
if (!d)
|
||||||
return -1;
|
return -1;
|
||||||
@@ -733,5 +745,24 @@ extern "C" int xone_controller_rumble(xone_dongle *d, int index)
|
|||||||
if (index < 0 || index >= static_cast<int>(d->controllers.size()))
|
if (index < 0 || index >= static_cast<int>(d->controllers.size()))
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
return -ENOSYS;
|
auto& c = *d->controllers[index];
|
||||||
|
if (!c.gip_ready || !c.gip_adapter)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
auto* client = c.gip_adapter->get_client(0);
|
||||||
|
if (!client)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// gip_gamepad_pkt_rumble (packed, 9 bytes).
|
||||||
|
std::uint8_t pkt[9] = {
|
||||||
|
0x00, // unknown
|
||||||
|
k_gp_motor_all, // motors: R | L | RT | LT
|
||||||
|
0x00, 0x00, // left/right trigger (unused)
|
||||||
|
scale_rumble(left),
|
||||||
|
scale_rumble(right),
|
||||||
|
0xff, // duration (constant, as in upstream)
|
||||||
|
0x00, // delay
|
||||||
|
0xeb, // repeat (constant, as in upstream)
|
||||||
|
};
|
||||||
|
return client->send_rumble(pkt);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user