/* * 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 #include #include #include #include #include #include #include #include #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; }