Skip to content

Initial release #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,62 @@
# linux-bluetooth-connection-fix
Tries to connect bluetooth devices on Linux despite error `hci0: command 0x0c24 tx timeout`
Automated process to connect bluetooth devices on Linux despite the error `hci0: command <code> tx timeout`.
Related errors

```bash
Bluetooth: hci0: command 0x0c24 tx timeout
Bluetooth: hci0: command 0x0c52 tx timeout
Bluetooth: hci0: command 0x0c1a tx timeout
```

# Running it

Download the [latest release][1]:

Discover your device ID, for example `94:CC:56:E5:72:85` is my headphone:
```bash
$ bluetoothctl devices
Device 94:CC:56:E5:72:85 WH-1000XM4
```

Then let's connect to it:
```bash
$ java -jar linux-bluetooth-connection-fix.jar 94:CC:56:E5:72:85
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - found=false, code=0, out=null
[main] WARN com.mageddo.linux.bluetoothfix.BluetoothConnector - systemctl will ask you for root password to restart bluetooth service ...
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=restarted, code=0, out=null
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=tryConnecting, device=94:DB:56:F5:78:41
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - found=true, code=0, out=null
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=done, occurrence=CONNECTED
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=tried, occurrence=CONNECTED, time=18218
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=successfullyConnected!, device=94:CC:56:E5:72:85, totalTime=19218
```

# Requirements

* Linux
* JRE 7+

# How it works
After buy a new bluetooth usb dongle, I noticed it was very difficult to make it connect to my headphones, also
noticed if I restart the bluetooth service and try to connect sometimes it will work at some moment, so what I did
was jut automate this process.

About the bluetooth issue root cause:

I wasn't able to find a real fix for the bluetooth problem, looks like it doesn't even exist, all people advise to buy a new
hardware at the end, then I made this program as a workaround.

Related issues

* https://bbs.archlinux.org/viewtopic.php?id=198718
* https://bbs.archlinux.org/viewtopic.php?id=270693
* https://bbs.archlinux.org/viewtopic.php?id=195886&p=2
* https://unix.stackexchange.com/questions/581974/alpine-linux-failed-to-start-discovery-org-bluez-error-inprogress

# Compiling from source

```bash
$ ./gradlew build shadowJar
```

[1]: https://github.com/mageddo-projects/linux-bluetooth-connection-fix/releases
37 changes: 37 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
plugins {
id "java"
id "com.github.johnrengelman.shadow" version "7.1.2"
}

repositories {
mavenCentral()
}

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

dependencies {
compileOnly 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok:1.18.26'

implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.6'
implementation group: 'org.apache.commons', name: 'commons-exec', version: '1.3'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'

testCompileOnly 'org.projectlombok:lombok:1.18.26'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.26'
}

shadowJar {

// System.setProperty("logFileName", "linux-bluetooth-connection-fix")

manifest {
attributes("Main-Class": "com.mageddo.linux.bluetoothfix.Main")
}

mergeServiceFiles()
from sourceSets.main.output
configurations = [project.configurations.runtimeClasspath]

}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version=0.1.0
85 changes: 85 additions & 0 deletions src/main/java/com/mageddo/commons/exec/CommandLines.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.mageddo.commons.exec;

import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.ToString;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DaemonExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.PumpStreamHandler;

import java.io.ByteArrayOutputStream;

public class CommandLines {

public static Result exec(String commandLine, Object... args) {
return exec(CommandLine.parse(String.format(commandLine, args)),
ExecuteWatchdog.INFINITE_TIMEOUT
);
}

public static Result exec(long timeout, String commandLine, Object... args) {
return exec(CommandLine.parse(String.format(commandLine, args)), timeout);
}

public static Result exec(CommandLine commandLine) {
return exec(commandLine, ExecuteWatchdog.INFINITE_TIMEOUT);
}

@SneakyThrows
public static Result exec(CommandLine commandLine, long timeout) {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final DaemonExecutor executor = new DaemonExecutor();
final PumpStreamHandler streamHandler = new PumpStreamHandler(out);
executor.setStreamHandler(streamHandler);
int exitCode;
try {
executor.setWatchdog(new ExecuteWatchdog(timeout));
exitCode = executor.execute(commandLine);
} catch (ExecuteException e) {
exitCode = e.getExitValue();
}
return Result
.builder()
.executor(executor)
.out(out)
.exitCode(exitCode)
.build();
}

@Getter
@Builder
@ToString(of = {"exitCode"})
public static class Result {

@NonNull
private Executor executor;

@NonNull
private ByteArrayOutputStream out;

private int exitCode;

public String getOutAsString() {
return this.out.toString();
}

public Result checkExecution() {
if (this.executor.isFailure(this.getExitCode())) {
throw new ExecutionValidationFailedException(this);
}
return this;
}

public String toString(boolean printOut) {
return String.format(
"code=%d, out=%s",
this.exitCode, printOut ? this.getOutAsString() : null
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mageddo.commons.exec;

public class ExecutionValidationFailedException extends RuntimeException {
private final CommandLines.Result result;

public ExecutionValidationFailedException(CommandLines.Result result) {
super(String.format("error, code=%d, error=%s", result.getExitCode(), result.getOutAsString()));
this.result = result;
}

public CommandLines.Result result() {
return this.result;
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/mageddo/commons/lang/Threads.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mageddo.commons.lang;

import lombok.SneakyThrows;

public class Threads {
@SneakyThrows
public static void sleep(int millis) {
Thread.sleep(millis);
}
}
164 changes: 164 additions & 0 deletions src/main/java/com/mageddo/linux/bluetoothfix/BluetoothConnector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.mageddo.linux.bluetoothfix;


import com.mageddo.commons.exec.CommandLines;
import com.mageddo.commons.exec.ExecutionValidationFailedException;
import com.mageddo.commons.lang.Threads;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.lang3.time.StopWatch;

@Slf4j
public class BluetoothConnector {
public static final boolean PRINT_OUT = false;
public static final int BLUETOOTH_POWER_ON_DELAY = 1000;
private final int timeoutSecs = 10;

public void connect(String deviceId) {

if (this.isSoundDeviceConfigured(deviceId)) {
log.info("status=bluetooth-device-already-configured-and-working, deviceId={}", deviceId);
return;
}

final StopWatch stopWatch = StopWatch.createStarted();
Occurrence status = null;
do {

stopWatch.split();

if (status != null) {
switch (status) {
case CONNECTED_BUT_SOUND_NOT_CONFIGURED:
case ERROR_CONNECTION_BUSY:
this.disconnect(deviceId);
break;
}
}

this.restartService();
status = this.connect0(deviceId);

log.info(
"status=tried, occurrence={}, time={}",
status, stopWatch.getTime() - stopWatch.getSplitTime()
);
Threads.sleep(1000);

} while (status != Occurrence.CONNECTED);
log.info(
"status=successfullyConnected!, device={}, totalTime={}",
deviceId, stopWatch.getTime()
);
}

boolean disconnect(String deviceId) {
try {
final CommandLines.Result result = CommandLines.exec(
"bluetoothctl --timeout %d disconnect %s", timeoutSecs, deviceId
)
.checkExecution();
log.info("status=disconnected, {}", result.toString(PRINT_OUT));
return true;
} catch (ExecutionValidationFailedException e) {
log.info("status=failedToDisconnect, {}", e.result()
.toString(PRINT_OUT));
return false;
}
}

CommandLines.Result restartService() {

log.warn("systemctl will ask you for root password to restart bluetooth service ...");

final CommandLine cmd = new CommandLine("/bin/sh")
.addArguments(new String[]{
"-c",
"systemctl restart bluetooth.service",
}, false);
final CommandLines.Result result = CommandLines.exec(cmd)
.checkExecution();
log.info("status=restarted, {}", result.toString(PRINT_OUT));
Threads.sleep(BLUETOOTH_POWER_ON_DELAY); // wait some time to bluetooth power on
return result;
}

boolean isConnected(String deviceId) {
final CommandLines.Result result = CommandLines.exec(
"bluetoothctl info %s", deviceId
)
.checkExecution();
final String out = result.getOutAsString();
if (out.contains("Connected: yes")) {
return true;
} else if (out.contains("Connected: no")) {
return false;
} else {
throw new IllegalStateException(String.format("cant check if it's connected: %s", out));
}
}

Occurrence connect0(String deviceId) {
try {
log.info("status=tryConnecting, device={}", deviceId);
final CommandLines.Result result = CommandLines
.exec(
"bluetoothctl --timeout %d connect %s", timeoutSecs, deviceId
)
.checkExecution();
final BluetoothConnector.Occurrence occurrence = OccurrenceParser.parse(result);
if (occurrence != null) {
return occurrence;
}
final Occurrence occur = this.connectionOccurrenceCheck(deviceId);
log.info("status=done, occurrence={}", occur);
return occur;
} catch (ExecutionValidationFailedException e) {
return OccurrenceParser.parse(e.result());
}
}

Occurrence connectionOccurrenceCheck(String deviceId) {
final boolean connected = this.isConnected(deviceId);
if (connected) {
if (this.isSoundDeviceConfigured(deviceId)) {
return Occurrence.CONNECTED;
}
return Occurrence.CONNECTED_BUT_SOUND_NOT_CONFIGURED;
} else {
return Occurrence.DISCONNECTED;
}
}

/**
* A device like the following must be displayed when bluetooth audio is working
* bluez_sink.94_DB_56_F5_78_41.a2dp_sink
*/
boolean isSoundDeviceConfigured(String deviceId) {
final String audioSinkId = String.format(
"bluez_sink.%s.a2dp_sink", deviceId.replaceAll(":", "_")
);
final CommandLine cmd = new CommandLine("/bin/sh")
.addArguments(new String[]{"-c", "pactl list | grep 'Sink'"}, false);

final CommandLines.Result result = CommandLines.exec(cmd)
.checkExecution();

final boolean found = result
.getOutAsString()
.contains(audioSinkId);

log.info("found={}, {}", found, result.toString(PRINT_OUT));
return found;

}

public enum Occurrence {
ERROR_CONNECTION_BUSY,
CONNECTED,
DISCONNECTED,
ERROR_UNKNOWN,
CONNECTED_BUT_SOUND_NOT_CONFIGURED;
}

}
17 changes: 17 additions & 0 deletions src/main/java/com/mageddo/linux/bluetoothfix/Linux.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mageddo.linux.bluetoothfix;

import com.sun.security.auth.module.UnixSystem;

public class Linux {

private Linux() {
}

public static long findUserId() {
return new UnixSystem().getUid();
}

public static boolean runningAsRoot() {
return findUserId() == 0;
}
}
Loading