705 lines
12 KiB
C
705 lines
12 KiB
C
/*
|
|
* controller-off.c
|
|
*
|
|
* Global SDL3 controller disconnect utility.
|
|
*
|
|
* Button combination:
|
|
*
|
|
* Guide/Home + North
|
|
*
|
|
* SDL mapping:
|
|
*
|
|
* PlayStation -> Home + Triangle
|
|
* Xbox -> Guide + Y
|
|
* Nintendo -> Home + X
|
|
*
|
|
* Build:
|
|
*
|
|
* cc controller-off.c -o controller-off \
|
|
* $(pkg-config --cflags --libs sdl3)
|
|
*
|
|
* Run:
|
|
*
|
|
* ./controller-off
|
|
*/
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <ctype.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#define MAX_CONTROLLERS 32
|
|
#define MAC_LEN 18
|
|
|
|
typedef struct {
|
|
SDL_JoystickID id;
|
|
SDL_Gamepad *gamepad;
|
|
|
|
int guide_pressed;
|
|
int north_pressed;
|
|
|
|
/*
|
|
* Prevent repeated triggers while both buttons
|
|
* remain held.
|
|
*/
|
|
int triggered;
|
|
} Controller;
|
|
|
|
static Controller controllers[MAX_CONTROLLERS];
|
|
static int controller_count = 0;
|
|
|
|
static volatile sig_atomic_t running = 1;
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Check for XX:XX:XX:XX:XX:XX
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static int is_mac(const char *s)
|
|
{
|
|
if (!s || strlen(s) != 17)
|
|
return 0;
|
|
|
|
for (int i = 0; i < 17; i++) {
|
|
|
|
if (i == 2 ||
|
|
i == 5 ||
|
|
i == 8 ||
|
|
i == 11 ||
|
|
i == 14) {
|
|
|
|
if (s[i] != ':')
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
if (!isxdigit(
|
|
(unsigned char)s[i]))
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Find a connected Bluetooth device whose name matches
|
|
* the SDL controller name.
|
|
*
|
|
* bluetoothctl output:
|
|
*
|
|
* Device AA:BB:CC:DD:EE:FF PS3 Controller
|
|
*
|
|
* We use:
|
|
*
|
|
* bluetoothctl devices Connected
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static int find_bluetooth_device(
|
|
const char *controller_name,
|
|
char mac[MAC_LEN])
|
|
{
|
|
if (!controller_name)
|
|
return 0;
|
|
|
|
FILE *fp = popen(
|
|
"bluetoothctl devices Connected 2>/dev/null",
|
|
"r"
|
|
);
|
|
|
|
if (!fp)
|
|
return 0;
|
|
|
|
char line[1024];
|
|
|
|
while (fgets(
|
|
line,
|
|
sizeof(line),
|
|
fp
|
|
)) {
|
|
|
|
char candidate_mac[MAC_LEN];
|
|
char device_name[768];
|
|
|
|
memset(
|
|
candidate_mac,
|
|
0,
|
|
sizeof(candidate_mac)
|
|
);
|
|
|
|
memset(
|
|
device_name,
|
|
0,
|
|
sizeof(device_name)
|
|
);
|
|
|
|
|
|
/*
|
|
* bluetoothctl:
|
|
*
|
|
* Device MAC NAME
|
|
*/
|
|
if (sscanf(
|
|
line,
|
|
"Device %17s %[^\n]",
|
|
candidate_mac,
|
|
device_name
|
|
) != 2) {
|
|
|
|
continue;
|
|
}
|
|
|
|
|
|
if (!is_mac(candidate_mac))
|
|
continue;
|
|
|
|
|
|
/*
|
|
* SDL and bluetoothctl may use slightly
|
|
* different names.
|
|
*
|
|
* First try exact match.
|
|
*/
|
|
if (strcmp(
|
|
controller_name,
|
|
device_name
|
|
) == 0) {
|
|
|
|
strncpy(
|
|
mac,
|
|
candidate_mac,
|
|
MAC_LEN
|
|
);
|
|
|
|
mac[MAC_LEN - 1] = '\0';
|
|
|
|
pclose(fp);
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
* PS3-specific fallback.
|
|
*
|
|
* SDL:
|
|
*
|
|
* PS3 Controller
|
|
*
|
|
* BlueZ:
|
|
*
|
|
* PLAYSTATION(R)3 Controller
|
|
*/
|
|
if (
|
|
strstr(controller_name, "PS3") &&
|
|
strstr(device_name, "3 Controller")
|
|
) {
|
|
|
|
strncpy(
|
|
mac,
|
|
candidate_mac,
|
|
MAC_LEN
|
|
);
|
|
|
|
mac[MAC_LEN - 1] = '\0';
|
|
|
|
pclose(fp);
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
pclose(fp);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Disconnect Bluetooth device.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static void disconnect_bluetooth(
|
|
const char *mac)
|
|
{
|
|
if (!is_mac(mac))
|
|
return;
|
|
|
|
|
|
printf(
|
|
"Disconnecting Bluetooth device %s\n",
|
|
mac
|
|
);
|
|
|
|
fflush(stdout);
|
|
|
|
|
|
pid_t pid = fork();
|
|
|
|
if (pid < 0) {
|
|
perror("fork");
|
|
return;
|
|
}
|
|
|
|
|
|
if (pid == 0) {
|
|
|
|
/*
|
|
* Suppress bluetoothctl output.
|
|
*/
|
|
FILE *devnull =
|
|
fopen("/dev/null", "w");
|
|
|
|
if (devnull) {
|
|
|
|
dup2(
|
|
fileno(devnull),
|
|
STDOUT_FILENO
|
|
);
|
|
|
|
dup2(
|
|
fileno(devnull),
|
|
STDERR_FILENO
|
|
);
|
|
|
|
fclose(devnull);
|
|
}
|
|
|
|
|
|
execlp(
|
|
"bluetoothctl",
|
|
"bluetoothctl",
|
|
"disconnect",
|
|
mac,
|
|
(char *)NULL
|
|
);
|
|
|
|
|
|
_exit(127);
|
|
}
|
|
|
|
|
|
/*
|
|
* Reap child process.
|
|
*/
|
|
waitpid(
|
|
pid,
|
|
NULL,
|
|
0
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Handle the button combination.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static void disconnect_controller(
|
|
Controller *controller)
|
|
{
|
|
if (!controller ||
|
|
!controller->gamepad)
|
|
return;
|
|
|
|
|
|
const char *name =
|
|
SDL_GetGamepadName(
|
|
controller->gamepad
|
|
);
|
|
|
|
|
|
printf("\n");
|
|
printf(
|
|
"PS/Home + North detected\n"
|
|
);
|
|
|
|
printf(
|
|
"Controller: %s\n",
|
|
name ? name : "Unknown"
|
|
);
|
|
|
|
|
|
char mac[MAC_LEN];
|
|
|
|
|
|
if (!find_bluetooth_device(
|
|
name,
|
|
mac
|
|
)) {
|
|
|
|
fprintf(
|
|
stderr,
|
|
"Could not find a connected Bluetooth "
|
|
"device matching \"%s\".\n",
|
|
name ? name : "Unknown"
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
printf(
|
|
"Bluetooth MAC: %s\n",
|
|
mac
|
|
);
|
|
|
|
|
|
disconnect_bluetooth(mac);
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Find SDL controller.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static Controller *find_controller(
|
|
SDL_JoystickID id)
|
|
{
|
|
for (int i = 0;
|
|
i < controller_count;
|
|
i++) {
|
|
|
|
if (controllers[i].id == id)
|
|
return &controllers[i];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Add controller.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static void add_controller(
|
|
SDL_JoystickID id)
|
|
{
|
|
if (controller_count >=
|
|
MAX_CONTROLLERS) {
|
|
|
|
fprintf(
|
|
stderr,
|
|
"Controller limit reached.\n"
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
SDL_Gamepad *gamepad =
|
|
SDL_OpenGamepad(id);
|
|
|
|
|
|
if (!gamepad) {
|
|
|
|
fprintf(
|
|
stderr,
|
|
"SDL_OpenGamepad failed: %s\n",
|
|
SDL_GetError()
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
Controller *controller =
|
|
&controllers[
|
|
controller_count++
|
|
];
|
|
|
|
|
|
memset(
|
|
controller,
|
|
0,
|
|
sizeof(*controller)
|
|
);
|
|
|
|
|
|
controller->id = id;
|
|
controller->gamepad = gamepad;
|
|
|
|
|
|
const char *name =
|
|
SDL_GetGamepadName(gamepad);
|
|
|
|
|
|
printf(
|
|
"Controller connected: %s\n",
|
|
name ? name : "Unknown"
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Remove controller.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static void remove_controller(
|
|
SDL_JoystickID id)
|
|
{
|
|
for (int i = 0;
|
|
i < controller_count;
|
|
i++) {
|
|
|
|
if (controllers[i].id != id)
|
|
continue;
|
|
|
|
|
|
SDL_CloseGamepad(
|
|
controllers[i].gamepad
|
|
);
|
|
|
|
|
|
/*
|
|
* Fill the removed slot with the last
|
|
* controller.
|
|
*/
|
|
controllers[i] =
|
|
controllers[
|
|
controller_count - 1
|
|
];
|
|
|
|
|
|
controller_count--;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Signal handler.
|
|
* ------------------------------------------------------------
|
|
*/
|
|
static void handle_signal(
|
|
int signal)
|
|
{
|
|
(void)signal;
|
|
|
|
running = 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* ------------------------------------------------------------
|
|
* Main
|
|
* ------------------------------------------------------------
|
|
*/
|
|
int main(void)
|
|
{
|
|
signal(
|
|
SIGINT,
|
|
handle_signal
|
|
);
|
|
|
|
signal(
|
|
SIGTERM,
|
|
handle_signal
|
|
);
|
|
|
|
|
|
if (!SDL_Init(
|
|
SDL_INIT_GAMEPAD)) {
|
|
|
|
fprintf(
|
|
stderr,
|
|
"SDL_Init failed: %s\n",
|
|
SDL_GetError()
|
|
);
|
|
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
* Receive controller events even though we have
|
|
* no window/focus.
|
|
*/
|
|
SDL_SetHint(
|
|
SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
|
|
"1"
|
|
);
|
|
|
|
|
|
printf(
|
|
"====================================\n"
|
|
" Controller Off\n"
|
|
"====================================\n"
|
|
"Hold Home/Guide + Triangle/North\n"
|
|
"to disconnect the controller.\n\n"
|
|
);
|
|
|
|
|
|
/*
|
|
* Open controllers that already exist.
|
|
*/
|
|
int count = 0;
|
|
|
|
SDL_JoystickID *ids =
|
|
SDL_GetGamepads(&count);
|
|
|
|
|
|
if (ids) {
|
|
|
|
for (int i = 0;
|
|
i < count;
|
|
i++) {
|
|
|
|
add_controller(ids[i]);
|
|
}
|
|
|
|
SDL_free(ids);
|
|
}
|
|
|
|
|
|
SDL_Event event;
|
|
|
|
|
|
while (running) {
|
|
|
|
while (SDL_PollEvent(&event)) {
|
|
|
|
|
|
/*
|
|
* Controller added.
|
|
*/
|
|
if (event.type ==
|
|
SDL_EVENT_GAMEPAD_ADDED) {
|
|
|
|
add_controller(
|
|
event.gdevice.which
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Controller removed.
|
|
*/
|
|
else if (event.type ==
|
|
SDL_EVENT_GAMEPAD_REMOVED) {
|
|
|
|
remove_controller(
|
|
event.gdevice.which
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Button press/release.
|
|
*/
|
|
else if (
|
|
event.type ==
|
|
SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
|
|
|
|
event.type ==
|
|
SDL_EVENT_GAMEPAD_BUTTON_UP
|
|
) {
|
|
|
|
Controller *controller =
|
|
find_controller(
|
|
event.gbutton.which
|
|
);
|
|
|
|
|
|
if (!controller)
|
|
continue;
|
|
|
|
|
|
int pressed =
|
|
event.type ==
|
|
SDL_EVENT_GAMEPAD_BUTTON_DOWN;
|
|
|
|
|
|
/*
|
|
* Guide/Home/PS.
|
|
*/
|
|
if (event.gbutton.button ==
|
|
SDL_GAMEPAD_BUTTON_GUIDE) {
|
|
|
|
controller->guide_pressed =
|
|
pressed;
|
|
}
|
|
|
|
|
|
/*
|
|
* North:
|
|
*
|
|
* PS = Triangle
|
|
* Xbox = Y
|
|
* Nintendo = X
|
|
*/
|
|
if (event.gbutton.button ==
|
|
SDL_GAMEPAD_BUTTON_NORTH) {
|
|
|
|
controller->north_pressed =
|
|
pressed;
|
|
}
|
|
|
|
|
|
/*
|
|
* Trigger only once per press.
|
|
*/
|
|
if (controller->guide_pressed &&
|
|
controller->north_pressed &&
|
|
!controller->triggered) {
|
|
|
|
controller->triggered = 1;
|
|
|
|
disconnect_controller(
|
|
controller
|
|
);
|
|
}
|
|
|
|
|
|
/*
|
|
* Allow another trigger after
|
|
* either button is released.
|
|
*/
|
|
if (!controller->guide_pressed ||
|
|
!controller->north_pressed) {
|
|
|
|
controller->triggered = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
SDL_Delay(5);
|
|
}
|
|
|
|
|
|
/*
|
|
* Cleanup.
|
|
*/
|
|
for (int i = 0;
|
|
i < controller_count;
|
|
i++) {
|
|
|
|
if (controllers[i].gamepad)
|
|
SDL_CloseGamepad(
|
|
controllers[i].gamepad
|
|
);
|
|
}
|
|
|
|
|
|
SDL_Quit();
|
|
|
|
return 0;
|
|
}
|
|
|