mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
JogCon force feedback on host wheels (SDL haptics, opt-in)
The emulated JogCon kept its motor command as device state only. With game.toml [controller] jogcon_force_feedback = true, a presented JogCon's motor byte (command nibble + strength) now drives a host wheel's SDL haptics: commands 1 / 2 a constant force signed by direction and scaled by strength, any other non-stop command a centring spring. Only pads whose haptic device supports constant or spring effects are driven, so gamepads are untouched. jogcon_force_feedback_invert flips the turn direction (not checked on JogCon hardware). SIO keeps the strength nibble beside the command (sio_get_pad_jogcon_motor). PSX_RUMBLE_TRACE logs the commands, headless too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1080,6 +1080,12 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
rt.direct_shortcut_button =
|
||||
toml::find<std::string>(ct, "direct_shortcut_button");
|
||||
}
|
||||
if (ct.contains("jogcon_force_feedback"))
|
||||
rt.jogcon_force_feedback =
|
||||
toml::find<bool>(ct, "jogcon_force_feedback");
|
||||
if (ct.contains("jogcon_force_feedback_invert"))
|
||||
rt.jogcon_force_feedback_invert =
|
||||
toml::find<bool>(ct, "jogcon_force_feedback_invert");
|
||||
if (ct.contains("anti_deadzone")) {
|
||||
const auto n = toml::find<int64_t>(ct, "anti_deadzone");
|
||||
if (n < 0 || n > 32767)
|
||||
|
||||
@@ -806,6 +806,12 @@ struct RuntimeConfig {
|
||||
std::string direct_shortcut;
|
||||
std::string direct_shortcut_button;
|
||||
|
||||
// jogcon_force_feedback: drive a host wheel's SDL haptics (constant force
|
||||
// / centring spring) from a presented JogCon's motor commands. Opt-in;
|
||||
// gamepads are never driven. _invert flips the turn direction.
|
||||
bool jogcon_force_feedback = false;
|
||||
bool jogcon_force_feedback_invert = false;
|
||||
|
||||
// multitap_analog: DualShock-on-tap hack (default true). When true,
|
||||
// multitap bulk seats may report 0x73 + stick bytes; when false (faithful),
|
||||
// tap seats stay plain digital. Overridable by settings.toml / match_caps.
|
||||
|
||||
@@ -709,6 +709,10 @@ if(BUILD_TESTING)
|
||||
add_executable(host_rumble_test tests/test_host_rumble.c)
|
||||
target_include_directories(host_rumble_test PRIVATE include)
|
||||
add_test(NAME host_rumble_test COMMAND host_rumble_test)
|
||||
# JogCon motor byte -> host wheel haptics ([controller] jogcon_force_feedback).
|
||||
add_executable(jogcon_ffb_test tests/test_jogcon_ffb.c)
|
||||
target_include_directories(jogcon_ffb_test PRIVATE include)
|
||||
add_test(NAME jogcon_ffb_test COMMAND jogcon_ffb_test)
|
||||
|
||||
add_executable(controller_type_test tests/test_controller_type.cpp)
|
||||
target_include_directories(controller_type_test PRIVATE include)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/* psx_jogcon_ffb.h - JogCon force feedback onto a host wheel's haptics.
|
||||
*
|
||||
* The JogCon's motor takes one byte per 0x42 poll at the motor slot the game
|
||||
* mapped with 0x4D: high nibble = command, low nibble = strength (0..15).
|
||||
* Command 0 stops the motor; 1 and 2 turn the wheel one way or the other;
|
||||
* any other command holds it against the player's turn. A host wheel with
|
||||
* SDL haptics gets the same thing: a constant force for 1 / 2 (signed by
|
||||
* direction, scaled by strength) and a centring spring for a hold. Gamepads
|
||||
* are not driven (no wheel to turn). Opt-in per title with game.toml
|
||||
* [controller] jogcon_force_feedback = true. Which way 1 and 2 turn has not
|
||||
* been checked on JogCon hardware; jogcon_force_feedback_invert flips it.
|
||||
* Pure (no SDL), so tests link it directly. */
|
||||
#ifndef PSX_JOGCON_FFB_H
|
||||
#define PSX_JOGCON_FFB_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define PSX_JOGCON_MOTOR_STOP 0u
|
||||
#define PSX_JOGCON_MOTOR_CW 1u
|
||||
#define PSX_JOGCON_MOTOR_CCW 2u
|
||||
|
||||
typedef struct PsxJogconFfb {
|
||||
int16_t constant; /* SDL constant-force level, -32767..32767 */
|
||||
uint16_t spring; /* SDL spring coefficient, 0..32767 */
|
||||
} PsxJogconFfb;
|
||||
|
||||
static inline PsxJogconFfb psx_jogcon_ffb_map(uint8_t command, uint8_t strength,
|
||||
int invert) {
|
||||
PsxJogconFfb f = {0, 0};
|
||||
const int level = (int)(strength & 0x0Fu) * 32767 / 15;
|
||||
command &= 0x0Fu;
|
||||
if (command == PSX_JOGCON_MOTOR_STOP || !level) return f;
|
||||
if (command == PSX_JOGCON_MOTOR_CW || command == PSX_JOGCON_MOTOR_CCW) {
|
||||
int v = command == PSX_JOGCON_MOTOR_CW ? level : -level;
|
||||
if (invert) v = -v;
|
||||
f.constant = (int16_t)v;
|
||||
} else {
|
||||
f.spring = (uint16_t)level;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
#endif /* PSX_JOGCON_FFB_H */
|
||||
@@ -202,6 +202,9 @@ void sio_set_pad_config_capable(int slot, int capable);
|
||||
* motor byte; `large` is the variable-strength low-frequency motor byte.
|
||||
* The frontend translates these values to its host controller API. */
|
||||
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large);
|
||||
/* JogCon motor: last command nibble and strength (0..15) the guest sent; 0, 0
|
||||
* unless the slot is a connected JogCon. Host force feedback reads it. */
|
||||
void sio_get_pad_jogcon_motor(int slot, uint8_t *command, uint8_t *strength);
|
||||
|
||||
/* Return current pad button state (for debug server). _slot targets a logical
|
||||
* pad 0 .. PSX_MAX_PLAYERS-1. */
|
||||
|
||||
@@ -68,6 +68,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "latency_ring.h"
|
||||
#include "sio.h"
|
||||
#include "psx_host_rumble.h"
|
||||
#include "psx_jogcon_ffb.h"
|
||||
#ifndef PSX_MAX_PLAYERS
|
||||
#define PSX_MAX_PLAYERS 2
|
||||
#endif
|
||||
@@ -432,7 +433,17 @@ struct PlayerInput {
|
||||
bool rumble_known = false;
|
||||
bool rumble_warned = false;
|
||||
bool steering_wheel = false;
|
||||
/* JogCon force feedback on this pad's haptics ([controller]
|
||||
* jogcon_force_feedback; psx_jogcon_ffb.h). -1 = no such effect. */
|
||||
SDL_Haptic* haptic = nullptr;
|
||||
int ffb_constant = -1;
|
||||
int ffb_spring = -1;
|
||||
bool ffb_probed = false;
|
||||
PsxJogconFfb ffb_last = {0, 0};
|
||||
};
|
||||
/* game.toml [controller] jogcon_force_feedback(_invert): opt-in. */
|
||||
static bool g_jogcon_ffb = false;
|
||||
static bool g_jogcon_ffb_invert = false;
|
||||
static PlayerInput g_players[PSX_MAX_PLAYERS];
|
||||
/* Offline SIO sample loop bound (from game.toml players; clamped). */
|
||||
static int g_offline_pad_count = 2;
|
||||
@@ -5184,7 +5195,18 @@ static void load_input_config(const char* argv0) {
|
||||
psx_keybinds_init(argv0);
|
||||
}
|
||||
|
||||
static void close_player_haptic(PlayerInput& p) {
|
||||
if (p.haptic) {
|
||||
SDL_HapticClose(p.haptic); /* stops and frees its effects */
|
||||
p.haptic = nullptr;
|
||||
}
|
||||
p.ffb_constant = p.ffb_spring = -1;
|
||||
p.ffb_probed = false;
|
||||
p.ffb_last = PsxJogconFfb{0, 0};
|
||||
}
|
||||
|
||||
static void close_player(PlayerInput& p) {
|
||||
close_player_haptic(p);
|
||||
if (p.handle) {
|
||||
if (p.rumble_small || p.rumble_large)
|
||||
(void)SDL_GameControllerRumble(p.handle, 0, 0, 0);
|
||||
@@ -5285,6 +5307,101 @@ static void open_player(PlayerInput& p, int self_slot) {
|
||||
* the small motor is fixed-strength/high-frequency. Active effects are renewed
|
||||
* every VBlank with a short lifetime so a crash or unplug cannot leave a host
|
||||
* controller vibrating indefinitely. */
|
||||
#if defined(PSX_SDL3)
|
||||
#define PSX_SDL_OK(call) ((call) ? 0 : -1)
|
||||
#else
|
||||
#define PSX_SDL_OK(call) (call)
|
||||
#endif
|
||||
|
||||
/* Open the pad's haptic device and make the two JogCon effects, once per
|
||||
* pad. Pads without constant / spring support (gamepads) get none. */
|
||||
static void probe_jogcon_haptic(PlayerInput& p) {
|
||||
p.ffb_probed = true;
|
||||
if (!p.handle) return;
|
||||
if (!SDL_WasInit(SDL_INIT_HAPTIC) &&
|
||||
PSX_SDL_OK(SDL_InitSubSystem(SDL_INIT_HAPTIC)) != 0)
|
||||
return;
|
||||
SDL_Joystick* joy = SDL_GameControllerGetJoystick(p.handle);
|
||||
p.haptic = joy ? SDL_HapticOpenFromJoystick(joy) : nullptr;
|
||||
if (!p.haptic) return;
|
||||
const unsigned features = (unsigned)SDL_HapticQuery(p.haptic);
|
||||
SDL_HapticEffect e;
|
||||
if (features & SDL_HAPTIC_CONSTANT) {
|
||||
std::memset(&e, 0, sizeof e);
|
||||
e.type = SDL_HAPTIC_CONSTANT;
|
||||
e.constant.direction.type = SDL_HAPTIC_CARTESIAN;
|
||||
e.constant.direction.dir[0] = 1;
|
||||
e.constant.length = SDL_HAPTIC_INFINITY;
|
||||
p.ffb_constant = SDL_HapticNewEffect(p.haptic, &e);
|
||||
}
|
||||
if (features & SDL_HAPTIC_SPRING) {
|
||||
std::memset(&e, 0, sizeof e);
|
||||
e.type = SDL_HAPTIC_SPRING;
|
||||
e.condition.direction.type = SDL_HAPTIC_CARTESIAN;
|
||||
e.condition.length = SDL_HAPTIC_INFINITY;
|
||||
e.condition.right_sat[0] = e.condition.left_sat[0] = 0xFFFF;
|
||||
p.ffb_spring = SDL_HapticNewEffect(p.haptic, &e);
|
||||
}
|
||||
if (p.ffb_constant < 0 && p.ffb_spring < 0) {
|
||||
SDL_HapticClose(p.haptic);
|
||||
p.haptic = nullptr;
|
||||
} else {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp runtime: JogCon force feedback on this controller "
|
||||
"(constant %s, spring %s)\n", p.ffb_constant >= 0 ? "yes" : "no",
|
||||
p.ffb_spring >= 0 ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_jogcon_effect(PlayerInput& p, int effect, bool on,
|
||||
SDL_HapticEffect* e) {
|
||||
if (!p.haptic || effect < 0) return;
|
||||
if (on) {
|
||||
(void)SDL_HapticUpdateEffect(p.haptic, effect, e);
|
||||
(void)SDL_HapticRunEffect(p.haptic, effect, 1);
|
||||
} else {
|
||||
(void)SDL_HapticStopEffect(p.haptic, effect);
|
||||
}
|
||||
}
|
||||
|
||||
/* JogCon force feedback ([controller] jogcon_force_feedback): the guest's
|
||||
* motor command and strength become a constant force or a centring spring on
|
||||
* a host wheel with SDL haptics. Gamepads are left alone. */
|
||||
static void update_jogcon_ffb(int s, PlayerInput& p, bool trace) {
|
||||
PsxJogconFfb f = {0, 0};
|
||||
uint8_t command = 0, strength = 0;
|
||||
if (g_jogcon_ffb && sio_get_pad_analog(s) == SIO_PAD_JOGCON) {
|
||||
sio_get_pad_jogcon_motor(s, &command, &strength);
|
||||
f = psx_jogcon_ffb_map(command, strength, g_jogcon_ffb_invert ? 1 : 0);
|
||||
if (!p.ffb_probed) probe_jogcon_haptic(p);
|
||||
}
|
||||
if (f.constant == p.ffb_last.constant && f.spring == p.ffb_last.spring)
|
||||
return;
|
||||
if (p.haptic) {
|
||||
SDL_HapticEffect e;
|
||||
std::memset(&e, 0, sizeof e);
|
||||
e.type = SDL_HAPTIC_CONSTANT;
|
||||
e.constant.direction.type = SDL_HAPTIC_CARTESIAN;
|
||||
e.constant.direction.dir[0] = 1;
|
||||
e.constant.length = SDL_HAPTIC_INFINITY;
|
||||
e.constant.level = f.constant;
|
||||
apply_jogcon_effect(p, p.ffb_constant, f.constant != 0, &e);
|
||||
std::memset(&e, 0, sizeof e);
|
||||
e.type = SDL_HAPTIC_SPRING;
|
||||
e.condition.direction.type = SDL_HAPTIC_CARTESIAN;
|
||||
e.condition.length = SDL_HAPTIC_INFINITY;
|
||||
e.condition.right_sat[0] = e.condition.left_sat[0] = 0xFFFF;
|
||||
e.condition.right_coeff[0] = e.condition.left_coeff[0] = (Sint16)f.spring;
|
||||
apply_jogcon_effect(p, p.ffb_spring, f.spring != 0, &e);
|
||||
}
|
||||
if (trace)
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp ffb: slot=%d jogcon cmd=%u strength=%u constant=%d "
|
||||
"spring=%u host=%s\n", s + 1, (unsigned)command, (unsigned)strength,
|
||||
(int)f.constant, (unsigned)f.spring, p.haptic ? "wheel" : "none");
|
||||
p.ffb_last = f;
|
||||
}
|
||||
|
||||
static void update_controller_rumble(void) {
|
||||
static const bool trace = [] {
|
||||
const char* e = std::getenv("PSX_RUMBLE_TRACE");
|
||||
@@ -5293,6 +5410,7 @@ static void update_controller_rumble(void) {
|
||||
++g_rumble_vblank;
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
PlayerInput& p = g_players[s];
|
||||
update_jogcon_ffb(s, p, trace);
|
||||
uint8_t small = 0, large = 0;
|
||||
sio_get_pad_rumble(s, &small, &large);
|
||||
psx_host_rumble_merge(&g_mod_rumble[s], g_rumble_vblank, &small, &large);
|
||||
@@ -16617,6 +16735,8 @@ int main(int argc, char** argv) {
|
||||
g_auto_skip_fmv = gc.runtime.video_auto_skip_fmv ? 1 : 0;
|
||||
/* [controller] game-declared input defaults (settings.toml/launcher
|
||||
* still override below). */
|
||||
g_jogcon_ffb = gc.runtime.jogcon_force_feedback;
|
||||
g_jogcon_ffb_invert = gc.runtime.jogcon_force_feedback_invert;
|
||||
if (!gc.runtime.direct_shortcut.empty())
|
||||
apply_title_direct_shortcut(gc.runtime.direct_shortcut,
|
||||
gc.runtime.direct_shortcut_button);
|
||||
|
||||
@@ -140,6 +140,9 @@ static PSX_BSS uint8_t pad_in_config[PSX_MAX_PLAYERS];
|
||||
* the frontend's vibration/force-feedback capabilities. */
|
||||
static PSX_BSS int8_t pad_jogcon_last_steering[PSX_MAX_PLAYERS];
|
||||
static PSX_BSS uint8_t pad_jogcon_motor_command[PSX_MAX_PLAYERS];
|
||||
/* The same motor byte's strength nibble, for host force feedback only (not
|
||||
* echoed to the guest and not savestated: every poll rewrites it). */
|
||||
static PSX_BSS uint8_t pad_jogcon_motor_strength[PSX_MAX_PLAYERS];
|
||||
|
||||
/* Whether the pad on a logical slot is config-capable (DualShock/JogCon, 1) or a
|
||||
* plain digital controller (0). A real SCPH-1080 digital pad (poll id 0x41)
|
||||
@@ -854,6 +857,7 @@ void sio_init(void) {
|
||||
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
|
||||
memset(pad_jogcon_last_steering, 0, sizeof(pad_jogcon_last_steering));
|
||||
memset(pad_jogcon_motor_command, 0, sizeof(pad_jogcon_motor_command));
|
||||
memset(pad_jogcon_motor_strength, 0, sizeof(pad_jogcon_motor_strength));
|
||||
memset(pad_negcon_analog, 0, sizeof(pad_negcon_analog));
|
||||
for (int i = 0; i < PSX_MAX_PLAYERS; i++) {
|
||||
pad_analog_device[i] = SIO_PAD_DUALSHOCK;
|
||||
@@ -1061,6 +1065,7 @@ void sio_set_pad_connected(int slot, int connected) {
|
||||
pad_rumble_large[slot] = 0;
|
||||
pad_jogcon_last_steering[slot] = 0;
|
||||
pad_jogcon_motor_command[slot] = 0;
|
||||
pad_jogcon_motor_strength[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,6 +1082,17 @@ void sio_set_pad_config_capable(int slot, int capable) {
|
||||
}
|
||||
}
|
||||
|
||||
void sio_get_pad_jogcon_motor(int slot, uint8_t *command, uint8_t *strength) {
|
||||
uint8_t c = 0, s = 0;
|
||||
if (slot >= 0 && slot < PSX_MAX_PLAYERS && (pad_connected & (1u << slot)) &&
|
||||
pad_analog[slot] == SIO_PAD_JOGCON) {
|
||||
c = pad_jogcon_motor_command[slot];
|
||||
s = pad_jogcon_motor_strength[slot];
|
||||
}
|
||||
if (command) *command = c;
|
||||
if (strength) *strength = s;
|
||||
}
|
||||
|
||||
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large) {
|
||||
uint8_t s = 0, l = 0;
|
||||
if (slot >= 0 && slot < PSX_MAX_PLAYERS && (pad_connected & (1u << slot))) {
|
||||
@@ -1111,6 +1127,7 @@ static void pad_commit_type(int slot, uint8_t type) {
|
||||
if (from != type) {
|
||||
pad_jogcon_last_steering[slot] = 0;
|
||||
pad_jogcon_motor_command[slot] = 0;
|
||||
pad_jogcon_motor_strength[slot] = 0;
|
||||
}
|
||||
if (pad_type_is_device_swap(from, type)) {
|
||||
pad_in_config[slot] = 0;
|
||||
@@ -1615,6 +1632,7 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
const uint8_t motor = pad_rumble_map[rs][map_index];
|
||||
if (pad_analog[rs] == SIO_PAD_JOGCON && motor == 0x00) {
|
||||
pad_jogcon_motor_command[rs] = (uint8_t)(tx_byte >> 4);
|
||||
pad_jogcon_motor_strength[rs] = (uint8_t)(tx_byte & 0x0Fu);
|
||||
} else if (motor == 0x00)
|
||||
pad_rumble_small[rs] = tx_byte;
|
||||
else if (motor == 0x01)
|
||||
@@ -3139,6 +3157,7 @@ static int sio_snap_parse(PstR *r) {
|
||||
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
|
||||
memset(pad_jogcon_last_steering, 0, sizeof(pad_jogcon_last_steering));
|
||||
memset(pad_jogcon_motor_command, 0, sizeof(pad_jogcon_motor_command));
|
||||
memset(pad_jogcon_motor_strength, 0, sizeof(pad_jogcon_motor_strength));
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
pad_rumble_map[s][0] = 0x00;
|
||||
pad_rumble_map[s][1] = 0x01;
|
||||
@@ -3155,6 +3174,7 @@ static int sio_snap_parse(PstR *r) {
|
||||
if (r->p == r->end) {
|
||||
memset(pad_jogcon_last_steering, 0, sizeof(pad_jogcon_last_steering));
|
||||
memset(pad_jogcon_motor_command, 0, sizeof(pad_jogcon_motor_command));
|
||||
memset(pad_jogcon_motor_strength, 0, sizeof(pad_jogcon_motor_strength));
|
||||
sio_pad_mode_legacy_defaults();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* psx_jogcon_ffb.h: JogCon motor byte -> wheel constant force / spring. */
|
||||
#include <stdio.h>
|
||||
#include "psx_jogcon_ffb.h"
|
||||
|
||||
static int s_fail;
|
||||
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL %s\n", m); ++s_fail; } } while (0)
|
||||
|
||||
int main(void) {
|
||||
PsxJogconFfb f = psx_jogcon_ffb_map(0, 15, 0);
|
||||
CHECK(f.constant == 0 && f.spring == 0, "stop: no force");
|
||||
f = psx_jogcon_ffb_map(1, 0, 0);
|
||||
CHECK(f.constant == 0 && f.spring == 0, "strength 0: no force");
|
||||
f = psx_jogcon_ffb_map(1, 15, 0);
|
||||
CHECK(f.constant == 32767 && f.spring == 0, "command 1 full: +constant");
|
||||
f = psx_jogcon_ffb_map(2, 15, 0);
|
||||
CHECK(f.constant == -32767, "command 2 full: -constant");
|
||||
f = psx_jogcon_ffb_map(2, 15, 1);
|
||||
CHECK(f.constant == 32767, "invert flips the direction");
|
||||
f = psx_jogcon_ffb_map(1, 5, 0);
|
||||
CHECK(f.constant == 10922, "strength scales linearly (5/15)");
|
||||
{
|
||||
int prev = 0;
|
||||
for (uint8_t st = 1; st <= 15; ++st) {
|
||||
f = psx_jogcon_ffb_map(1, st, 0);
|
||||
CHECK(f.constant > prev, "monotonic in strength");
|
||||
prev = f.constant;
|
||||
}
|
||||
}
|
||||
f = psx_jogcon_ffb_map(3, 8, 0);
|
||||
CHECK(f.constant == 0 && f.spring == 8 * 32767 / 15, "hold: centring spring");
|
||||
f = psx_jogcon_ffb_map(0x21, 0x0F, 0);
|
||||
CHECK(f.constant == 32767, "only the low nibbles count");
|
||||
if (s_fail) return 1;
|
||||
puts("JogCon force feedback tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -286,6 +286,14 @@ int main(void) {
|
||||
(void)xchg(0, 0x00);
|
||||
(void)xchg(0, 0x21);
|
||||
for (int i = 0; i < 5; i++) (void)xchg(0, 0x00);
|
||||
{ /* host force feedback reads command and strength */
|
||||
uint8_t cmd = 0xEE, strength = 0xEE;
|
||||
sio_get_pad_jogcon_motor(0, &cmd, &strength);
|
||||
EXPECT("motor.host.command", 0x02, cmd);
|
||||
EXPECT("motor.host.strength", 0x01, strength);
|
||||
sio_get_pad_jogcon_motor(1, &cmd, &strength);
|
||||
EXPECT("motor.host.other_slot", 0x00, cmd | strength);
|
||||
}
|
||||
EXPECT("motor.status.prefix", 0xFF, xchg(0, 0x01));
|
||||
EXPECT("motor.status.id", 0xF3, xchg(0, 0x42));
|
||||
for (int i = 0; i < 5; i++) (void)xchg(0, 0x00);
|
||||
|
||||
Reference in New Issue
Block a user