Skip to content

Commit 658e693

Browse files
authored
Merge pull request #1 from mageddo-projects/initial-release
Initial release
2 parents 28ede25 + 2817154 commit 658e693

File tree

10 files changed

+419
-1
lines changed

10 files changed

+419
-1
lines changed

README.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,62 @@
11
# linux-bluetooth-connection-fix
2-
Tries to connect bluetooth devices on Linux despite error `hci0: command 0x0c24 tx timeout`
2+
Automated process to connect bluetooth devices on Linux despite the error `hci0: command <code> tx timeout`.
3+
Related errors
4+
5+
```bash
6+
Bluetooth: hci0: command 0x0c24 tx timeout
7+
Bluetooth: hci0: command 0x0c52 tx timeout
8+
Bluetooth: hci0: command 0x0c1a tx timeout
9+
```
10+
11+
# Running it
12+
13+
Download the [latest release][1]:
14+
15+
Discover your device ID, for example `94:CC:56:E5:72:85` is my headphone:
16+
```bash
17+
$ bluetoothctl devices
18+
Device 94:CC:56:E5:72:85 WH-1000XM4
19+
```
20+
21+
Then let's connect to it:
22+
```bash
23+
$ java -jar linux-bluetooth-connection-fix.jar 94:CC:56:E5:72:85
24+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - found=false, code=0, out=null
25+
[main] WARN com.mageddo.linux.bluetoothfix.BluetoothConnector - systemctl will ask you for root password to restart bluetooth service ...
26+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=restarted, code=0, out=null
27+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=tryConnecting, device=94:DB:56:F5:78:41
28+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - found=true, code=0, out=null
29+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=done, occurrence=CONNECTED
30+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=tried, occurrence=CONNECTED, time=18218
31+
[main] INFO com.mageddo.linux.bluetoothfix.BluetoothConnector - status=successfullyConnected!, device=94:CC:56:E5:72:85, totalTime=19218
32+
```
33+
34+
# Requirements
35+
36+
* Linux
37+
* JRE 7+
38+
39+
# How it works
40+
After buy a new bluetooth usb dongle, I noticed it was very difficult to make it connect to my headphones, also
41+
noticed if I restart the bluetooth service and try to connect sometimes it will work at some moment, so what I did
42+
was jut automate this process.
43+
44+
About the bluetooth issue root cause:
45+
46+
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
47+
hardware at the end, then I made this program as a workaround.
48+
49+
Related issues
50+
51+
* https://bbs.archlinux.org/viewtopic.php?id=198718
52+
* https://bbs.archlinux.org/viewtopic.php?id=270693
53+
* https://bbs.archlinux.org/viewtopic.php?id=195886&p=2
54+
* https://unix.stackexchange.com/questions/581974/alpine-linux-failed-to-start-discovery-org-bluez-error-inprogress
55+
56+
# Compiling from source
57+
58+
```bash
59+
$ ./gradlew build shadowJar
60+
```
61+
62+
[1]: https://github.com/mageddo-projects/linux-bluetooth-connection-fix/releases

build.gradle

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
plugins {
2+
id "java"
3+
id "com.github.johnrengelman.shadow" version "7.1.2"
4+
}
5+
6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
sourceCompatibility = JavaVersion.VERSION_1_7
11+
targetCompatibility = JavaVersion.VERSION_1_7
12+
13+
dependencies {
14+
compileOnly 'org.projectlombok:lombok:1.18.26'
15+
annotationProcessor 'org.projectlombok:lombok:1.18.26'
16+
17+
implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.6'
18+
implementation group: 'org.apache.commons', name: 'commons-exec', version: '1.3'
19+
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
20+
21+
testCompileOnly 'org.projectlombok:lombok:1.18.26'
22+
testAnnotationProcessor 'org.projectlombok:lombok:1.18.26'
23+
}
24+
25+
shadowJar {
26+
27+
// System.setProperty("logFileName", "linux-bluetooth-connection-fix")
28+
29+
manifest {
30+
attributes("Main-Class": "com.mageddo.linux.bluetoothfix.Main")
31+
}
32+
33+
mergeServiceFiles()
34+
from sourceSets.main.output
35+
configurations = [project.configurations.runtimeClasspath]
36+
37+
}

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
version=0.1.0
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.mageddo.commons.exec;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import lombok.NonNull;
6+
import lombok.SneakyThrows;
7+
import lombok.ToString;
8+
import org.apache.commons.exec.CommandLine;
9+
import org.apache.commons.exec.DaemonExecutor;
10+
import org.apache.commons.exec.ExecuteException;
11+
import org.apache.commons.exec.ExecuteWatchdog;
12+
import org.apache.commons.exec.Executor;
13+
import org.apache.commons.exec.PumpStreamHandler;
14+
15+
import java.io.ByteArrayOutputStream;
16+
17+
public class CommandLines {
18+
19+
public static Result exec(String commandLine, Object... args) {
20+
return exec(CommandLine.parse(String.format(commandLine, args)),
21+
ExecuteWatchdog.INFINITE_TIMEOUT
22+
);
23+
}
24+
25+
public static Result exec(long timeout, String commandLine, Object... args) {
26+
return exec(CommandLine.parse(String.format(commandLine, args)), timeout);
27+
}
28+
29+
public static Result exec(CommandLine commandLine) {
30+
return exec(commandLine, ExecuteWatchdog.INFINITE_TIMEOUT);
31+
}
32+
33+
@SneakyThrows
34+
public static Result exec(CommandLine commandLine, long timeout) {
35+
final ByteArrayOutputStream out = new ByteArrayOutputStream();
36+
final DaemonExecutor executor = new DaemonExecutor();
37+
final PumpStreamHandler streamHandler = new PumpStreamHandler(out);
38+
executor.setStreamHandler(streamHandler);
39+
int exitCode;
40+
try {
41+
executor.setWatchdog(new ExecuteWatchdog(timeout));
42+
exitCode = executor.execute(commandLine);
43+
} catch (ExecuteException e) {
44+
exitCode = e.getExitValue();
45+
}
46+
return Result
47+
.builder()
48+
.executor(executor)
49+
.out(out)
50+
.exitCode(exitCode)
51+
.build();
52+
}
53+
54+
@Getter
55+
@Builder
56+
@ToString(of = {"exitCode"})
57+
public static class Result {
58+
59+
@NonNull
60+
private Executor executor;
61+
62+
@NonNull
63+
private ByteArrayOutputStream out;
64+
65+
private int exitCode;
66+
67+
public String getOutAsString() {
68+
return this.out.toString();
69+
}
70+
71+
public Result checkExecution() {
72+
if (this.executor.isFailure(this.getExitCode())) {
73+
throw new ExecutionValidationFailedException(this);
74+
}
75+
return this;
76+
}
77+
78+
public String toString(boolean printOut) {
79+
return String.format(
80+
"code=%d, out=%s",
81+
this.exitCode, printOut ? this.getOutAsString() : null
82+
);
83+
}
84+
}
85+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.mageddo.commons.exec;
2+
3+
public class ExecutionValidationFailedException extends RuntimeException {
4+
private final CommandLines.Result result;
5+
6+
public ExecutionValidationFailedException(CommandLines.Result result) {
7+
super(String.format("error, code=%d, error=%s", result.getExitCode(), result.getOutAsString()));
8+
this.result = result;
9+
}
10+
11+
public CommandLines.Result result() {
12+
return this.result;
13+
}
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.mageddo.commons.lang;
2+
3+
import lombok.SneakyThrows;
4+
5+
public class Threads {
6+
@SneakyThrows
7+
public static void sleep(int millis) {
8+
Thread.sleep(millis);
9+
}
10+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.mageddo.linux.bluetoothfix;
2+
3+
4+
import com.mageddo.commons.exec.CommandLines;
5+
import com.mageddo.commons.exec.ExecutionValidationFailedException;
6+
import com.mageddo.commons.lang.Threads;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.apache.commons.exec.CommandLine;
9+
import org.apache.commons.lang3.time.StopWatch;
10+
11+
@Slf4j
12+
public class BluetoothConnector {
13+
public static final boolean PRINT_OUT = false;
14+
public static final int BLUETOOTH_POWER_ON_DELAY = 1000;
15+
private final int timeoutSecs = 10;
16+
17+
public void connect(String deviceId) {
18+
19+
if (this.isSoundDeviceConfigured(deviceId)) {
20+
log.info("status=bluetooth-device-already-configured-and-working, deviceId={}", deviceId);
21+
return;
22+
}
23+
24+
final StopWatch stopWatch = StopWatch.createStarted();
25+
Occurrence status = null;
26+
do {
27+
28+
stopWatch.split();
29+
30+
if (status != null) {
31+
switch (status) {
32+
case CONNECTED_BUT_SOUND_NOT_CONFIGURED:
33+
case ERROR_CONNECTION_BUSY:
34+
this.disconnect(deviceId);
35+
break;
36+
}
37+
}
38+
39+
this.restartService();
40+
status = this.connect0(deviceId);
41+
42+
log.info(
43+
"status=tried, occurrence={}, time={}",
44+
status, stopWatch.getTime() - stopWatch.getSplitTime()
45+
);
46+
Threads.sleep(1000);
47+
48+
} while (status != Occurrence.CONNECTED);
49+
log.info(
50+
"status=successfullyConnected!, device={}, totalTime={}",
51+
deviceId, stopWatch.getTime()
52+
);
53+
}
54+
55+
boolean disconnect(String deviceId) {
56+
try {
57+
final CommandLines.Result result = CommandLines.exec(
58+
"bluetoothctl --timeout %d disconnect %s", timeoutSecs, deviceId
59+
)
60+
.checkExecution();
61+
log.info("status=disconnected, {}", result.toString(PRINT_OUT));
62+
return true;
63+
} catch (ExecutionValidationFailedException e) {
64+
log.info("status=failedToDisconnect, {}", e.result()
65+
.toString(PRINT_OUT));
66+
return false;
67+
}
68+
}
69+
70+
CommandLines.Result restartService() {
71+
72+
log.warn("systemctl will ask you for root password to restart bluetooth service ...");
73+
74+
final CommandLine cmd = new CommandLine("/bin/sh")
75+
.addArguments(new String[]{
76+
"-c",
77+
"systemctl restart bluetooth.service",
78+
}, false);
79+
final CommandLines.Result result = CommandLines.exec(cmd)
80+
.checkExecution();
81+
log.info("status=restarted, {}", result.toString(PRINT_OUT));
82+
Threads.sleep(BLUETOOTH_POWER_ON_DELAY); // wait some time to bluetooth power on
83+
return result;
84+
}
85+
86+
boolean isConnected(String deviceId) {
87+
final CommandLines.Result result = CommandLines.exec(
88+
"bluetoothctl info %s", deviceId
89+
)
90+
.checkExecution();
91+
final String out = result.getOutAsString();
92+
if (out.contains("Connected: yes")) {
93+
return true;
94+
} else if (out.contains("Connected: no")) {
95+
return false;
96+
} else {
97+
throw new IllegalStateException(String.format("cant check if it's connected: %s", out));
98+
}
99+
}
100+
101+
Occurrence connect0(String deviceId) {
102+
try {
103+
log.info("status=tryConnecting, device={}", deviceId);
104+
final CommandLines.Result result = CommandLines
105+
.exec(
106+
"bluetoothctl --timeout %d connect %s", timeoutSecs, deviceId
107+
)
108+
.checkExecution();
109+
final BluetoothConnector.Occurrence occurrence = OccurrenceParser.parse(result);
110+
if (occurrence != null) {
111+
return occurrence;
112+
}
113+
final Occurrence occur = this.connectionOccurrenceCheck(deviceId);
114+
log.info("status=done, occurrence={}", occur);
115+
return occur;
116+
} catch (ExecutionValidationFailedException e) {
117+
return OccurrenceParser.parse(e.result());
118+
}
119+
}
120+
121+
Occurrence connectionOccurrenceCheck(String deviceId) {
122+
final boolean connected = this.isConnected(deviceId);
123+
if (connected) {
124+
if (this.isSoundDeviceConfigured(deviceId)) {
125+
return Occurrence.CONNECTED;
126+
}
127+
return Occurrence.CONNECTED_BUT_SOUND_NOT_CONFIGURED;
128+
} else {
129+
return Occurrence.DISCONNECTED;
130+
}
131+
}
132+
133+
/**
134+
* A device like the following must be displayed when bluetooth audio is working
135+
* bluez_sink.94_DB_56_F5_78_41.a2dp_sink
136+
*/
137+
boolean isSoundDeviceConfigured(String deviceId) {
138+
final String audioSinkId = String.format(
139+
"bluez_sink.%s.a2dp_sink", deviceId.replaceAll(":", "_")
140+
);
141+
final CommandLine cmd = new CommandLine("/bin/sh")
142+
.addArguments(new String[]{"-c", "pactl list | grep 'Sink'"}, false);
143+
144+
final CommandLines.Result result = CommandLines.exec(cmd)
145+
.checkExecution();
146+
147+
final boolean found = result
148+
.getOutAsString()
149+
.contains(audioSinkId);
150+
151+
log.info("found={}, {}", found, result.toString(PRINT_OUT));
152+
return found;
153+
154+
}
155+
156+
public enum Occurrence {
157+
ERROR_CONNECTION_BUSY,
158+
CONNECTED,
159+
DISCONNECTED,
160+
ERROR_UNKNOWN,
161+
CONNECTED_BUT_SOUND_NOT_CONFIGURED;
162+
}
163+
164+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.mageddo.linux.bluetoothfix;
2+
3+
import com.sun.security.auth.module.UnixSystem;
4+
5+
public class Linux {
6+
7+
private Linux() {
8+
}
9+
10+
public static long findUserId() {
11+
return new UnixSystem().getUid();
12+
}
13+
14+
public static boolean runningAsRoot() {
15+
return findUserId() == 0;
16+
}
17+
}

0 commit comments

Comments
 (0)