Skip to content

Commit 2668438

Browse files
committed
source files
1 parent 2716ec7 commit 2668438

File tree

7 files changed

+324
-0
lines changed

7 files changed

+324
-0
lines changed

build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ plugins {
33
id "com.github.johnrengelman.shadow" version "7.1.2"
44
}
55

6+
repositories {
7+
mavenCentral()
8+
}
9+
10+
dependencies {
11+
compileOnly 'org.projectlombok:lombok:1.18.26'
12+
annotationProcessor 'org.projectlombok:lombok:1.18.26'
13+
14+
implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.6'
15+
implementation group: 'org.apache.commons', name: 'commons-exec', version: '1.3'
16+
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
17+
18+
testCompileOnly 'org.projectlombok:lombok:1.18.26'
19+
testAnnotationProcessor 'org.projectlombok:lombok:1.18.26'
20+
}
21+
622
shadowJar {
723

824
// System.setProperty("logFileName", "linux-bluetooth-connection-fix")
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.mageddo.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, String sudoPassword) {
18+
19+
if (this.isSoundDeviceConfigured(deviceId)) {
20+
log.info("status=bluetooth-device-already-configured-and-working, deviceId={}", deviceId);
21+
return;
22+
}
23+
24+
final var 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, ERROR_CONNECTION_BUSY -> {
33+
this.disconnect(deviceId);
34+
}
35+
}
36+
}
37+
38+
this.restartService(sudoPassword);
39+
status = this.connect0(deviceId);
40+
41+
log.debug(
42+
"status=tried, occurrence={}, time={}",
43+
status, stopWatch.getTime() - stopWatch.getSplitTime()
44+
);
45+
Threads.sleep(1000);
46+
47+
} while (status != Occurrence.CONNECTED);
48+
log.debug(
49+
"status=successfullyConnected!, device={}, totalTime={}",
50+
deviceId, stopWatch.getTime()
51+
);
52+
}
53+
54+
boolean disconnect(String deviceId) {
55+
try {
56+
final var result = CommandLines.exec(
57+
"bluetoothctl --timeout %d disconnect %s", timeoutSecs, deviceId
58+
)
59+
.checkExecution();
60+
log.debug("status=disconnected, {}", result.toString(PRINT_OUT));
61+
return true;
62+
} catch (ExecutionValidationFailedException e) {
63+
log.debug("status=failedToDisconnect, {}", e.result()
64+
.toString(PRINT_OUT));
65+
return false;
66+
}
67+
}
68+
69+
CommandLines.Result restartService(String sudoPassword) {
70+
final var cmd = new CommandLine("/bin/sh")
71+
.addArguments(new String[]{
72+
"-c",
73+
String.format(
74+
"echo %s | /usr/bin/sudo -S systemctl restart bluetooth.service",
75+
sudoPassword
76+
)
77+
}, false);
78+
final var result = CommandLines.exec(cmd)
79+
.checkExecution();
80+
log.debug("status=restarted, {}", result.toString(PRINT_OUT));
81+
Threads.sleep(BLUETOOTH_POWER_ON_DELAY); // wait some time to bluetooth power on
82+
return result;
83+
}
84+
85+
boolean isConnected(String deviceId) {
86+
final var result = CommandLines.exec(
87+
"bluetoothctl info %s", deviceId
88+
)
89+
.checkExecution();
90+
final var out = result.getOutAsString();
91+
if (out.contains("Connected: yes")) {
92+
return true;
93+
} else if (out.contains("Connected: no")) {
94+
return false;
95+
} else {
96+
throw new IllegalStateException(String.format("cant check if it's connected: %s", out));
97+
}
98+
}
99+
100+
Occurrence connect0(String deviceId) {
101+
try {
102+
log.debug("status=tryConnecting, device={}", deviceId);
103+
final var result = CommandLines
104+
.exec(
105+
"bluetoothctl --timeout %d connect %s", timeoutSecs, deviceId
106+
)
107+
.checkExecution();
108+
final var occurrence = OccurrenceParser.parse(result);
109+
if (occurrence != null) {
110+
return occurrence;
111+
}
112+
final var occur = this.connectionOccurrenceCheck(deviceId);
113+
log.debug("status=done, occurrence={}", occur);
114+
return occur;
115+
} catch (ExecutionValidationFailedException e) {
116+
return OccurrenceParser.parse(e.result());
117+
}
118+
}
119+
120+
Occurrence connectionOccurrenceCheck(String deviceId) {
121+
final var connected = this.isConnected(deviceId);
122+
if (connected) {
123+
if (this.isSoundDeviceConfigured(deviceId)) {
124+
return Occurrence.CONNECTED;
125+
}
126+
return Occurrence.CONNECTED_BUT_SOUND_NOT_CONFIGURED;
127+
} else {
128+
return Occurrence.DISCONNECTED;
129+
}
130+
}
131+
132+
/**
133+
* A device like the following must be displayed when bluetooth audio is working
134+
* bluez_sink.94_DB_56_F5_78_41.a2dp_sink
135+
*/
136+
boolean isSoundDeviceConfigured(String deviceId) {
137+
final var audioSinkId = String.format(
138+
"bluez_sink.%s.a2dp_sink", deviceId.replaceAll(":", "_")
139+
);
140+
final var cmd = new CommandLine("/bin/sh")
141+
.addArguments(new String[]{"-c", "pactl list | grep 'Sink'"}, false);
142+
143+
final var result = CommandLines.exec(cmd)
144+
.checkExecution();
145+
146+
final var found = result
147+
.getOutAsString()
148+
.contains(audioSinkId);
149+
150+
log.debug("found={}, {}", found, result.toString(PRINT_OUT));
151+
return found;
152+
153+
}
154+
155+
public enum Occurrence {
156+
ERROR_CONNECTION_BUSY,
157+
CONNECTED,
158+
DISCONNECTED,
159+
ERROR_UNKNOWN,
160+
161+
CONNECTED_BUT_SOUND_NOT_CONFIGURED;
162+
}
163+
164+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.mageddo.bluetoothfix;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
import javax.swing.*;
6+
7+
@Slf4j
8+
public class Main {
9+
public static void main(String[] args) {
10+
new BluetoothConnector().connect("94:DB:56:F5:78:41", askForPassword());
11+
}
12+
13+
static String askForPassword() {
14+
return JOptionPane.showInputDialog(
15+
null, "Sudo password to restart bluetooth service",
16+
"Sudo Password", JOptionPane.QUESTION_MESSAGE
17+
);
18+
}
19+
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.mageddo.bluetoothfix;
2+
3+
4+
import com.mageddo.commons.exec.CommandLines;
5+
6+
public class OccurrenceParser {
7+
public static BluetoothConnector.Occurrence parse(CommandLines.Result result) {
8+
final var out = result.getOutAsString();
9+
if (out.contains("br-connection-busy")) {
10+
return BluetoothConnector.Occurrence.ERROR_CONNECTION_BUSY;
11+
} else {
12+
return null;
13+
}
14+
}
15+
}
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 var out = new ByteArrayOutputStream();
36+
final var executor = new DaemonExecutor();
37+
final var 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+
}

0 commit comments

Comments
 (0)