Skip to content
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
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,18 @@ Set ```BLUETOOTH_HCI_SOCKET_FORCE_USB``` environment variable:
sudo BLUETOOTH_HCI_SOCKET_FORCE_USB=1 node <file>.js
```

#### Bound the Linux L2CAP connect wait

The Linux kernel L2CAP workaround waits up to 2 seconds for a connection by
default. Override the limit with `BLUETOOTH_HCI_L2CAP_CONNECT_TIMEOUT_MS`:

```sh
sudo BLUETOOTH_HCI_L2CAP_CONNECT_TIMEOUT_MS=5000 node <file>.js
```

Set the value to `0` to restore the kernel's unbounded connect wait. Invalid or
negative values use the 2-second default.

### FreeBSD

Disable automatic loading of the default Bluetooth stack by putting [no-ubt.conf](https://gist.github.com/myfreeweb/44f4f3e791a057bc4f3619a166a03b87) into ```/usr/local/etc/devd/no-ubt.conf``` and restarting devd (```sudo service devd restart```).
Expand Down Expand Up @@ -312,4 +324,3 @@ set BLUETOOTH_HCI_SOCKET_USB_PID=0x065a

node <file>.js
```

7 changes: 5 additions & 2 deletions include/BluetoothHciL2Socket.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class BluetoothHciSocket;
enum class BluetoothHciL2ConnectResult {
SETUP_FAILED,
CONNECTED,
CONNECTION_FAILED
CONNECTION_FAILED,
CONNECTION_TIMED_OUT
};

// Bluetooth HCI L2CAP Socket class
Expand Down Expand Up @@ -40,7 +41,9 @@ class BluetoothHciL2Socket {
*
* SETUP_FAILED means no controller connection was requested, so the caller
* may safely use another transport. CONNECTION_FAILED means the kernel did
* request a controller connection and reported its failure.
* request a controller connection and reported its failure, so another raw
* attempt would be a duplicate. CONNECTION_TIMED_OUT means the kernel socket
* was closed without a result, so the caller may fall back to raw HCI.
*/
BluetoothHciL2ConnectResult connect();

Expand Down
65 changes: 58 additions & 7 deletions src/BluetoothHciL2Socket.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include <errno.h>
#include <chrono>
#include <climits>
#include <cstdlib>
#include <fcntl.h>
#include <poll.h>
#include <sys/ioctl.h>
Expand All @@ -11,6 +14,51 @@
#include "BluetoothHciL2Socket.h"
#include "BluetoothHciSocket.h"

namespace {
constexpr int L2CAP_CONNECT_TIMEOUT_MS_DEFAULT = 2000;

int getL2capConnectTimeoutMs() {
const char* timeoutEnv = std::getenv("BLUETOOTH_HCI_L2CAP_CONNECT_TIMEOUT_MS");
if (timeoutEnv == nullptr) return L2CAP_CONNECT_TIMEOUT_MS_DEFAULT;

errno = 0;
char* end = nullptr;
const long timeoutMs = std::strtol(timeoutEnv, &end, 10);
if (errno == ERANGE || end == timeoutEnv || *end != '\0' ||
timeoutMs < 0 || timeoutMs > INT_MAX) {
return L2CAP_CONNECT_TIMEOUT_MS_DEFAULT;
}

return static_cast<int>(timeoutMs);
}

int waitForConnect(int socket, int timeoutMs) {
struct pollfd descriptor = { socket, POLLOUT, 0 };

if (timeoutMs == 0) {
int pollResult;
do {
pollResult = poll(&descriptor, 1, -1);
} while (pollResult < 0 && errno == EINTR);
return pollResult;
}

const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(timeoutMs);
int remainingMs = timeoutMs;

while (true) {
const int pollResult = poll(&descriptor, 1, remainingMs);
if (pollResult >= 0 || errno != EINTR) return pollResult;

const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count();
if (remaining <= 0) return 0;
remainingMs = static_cast<int>(remaining);
}
}
}

BluetoothHciL2Socket::BluetoothHciL2Socket(BluetoothHciSocket* parent,
const bdaddr_t* bdaddr_src,
uint8_t src_type,
Expand Down Expand Up @@ -81,13 +129,16 @@ BluetoothHciL2ConnectResult BluetoothHciL2Socket::connect() {
}

// EINPROGRESS confirms that the kernel accepted the request and initiated
// the controller connection. Preserve the previous synchronous behavior,
// but retain this distinction if the controller ultimately reports failure.
struct pollfd descriptor = { this->_socket, POLLOUT, 0 };
int pollResult;
do {
pollResult = poll(&descriptor, 1, -1);
} while (pollResult < 0 && errno == EINTR);
// the controller connection. Bound the synchronous wait because connect()
// runs on the thread driving libuv. A timeout closes the kernel socket before
// allowing the caller to fall back to the original raw HCI command.
const int pollResult = waitForConnect(this->_socket, getL2capConnectTimeoutMs());

if (pollResult == 0) {
close(this->_socket);
this->_socket = -1;
return BluetoothHciL2ConnectResult::CONNECTION_TIMED_OUT;
}

int connectError = 0;
socklen_t connectErrorLength = sizeof(connectError);
Expand Down
7 changes: 7 additions & 0 deletions src/BluetoothHciSocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,13 @@ bool BluetoothHciSocket::kernelConnectWorkArounds(char * data, int length) {
return false;
}

if (connectResult == BluetoothHciL2ConnectResult::CONNECTION_TIMED_OUT) {
// Closing the timed-out kernel socket cancels its pending attempt;
// forward the original command through the raw HCI socket.
this->_l2sockets_connecting.erase(bdaddr_dst);
return false;
}

if (connectResult == BluetoothHciL2ConnectResult::CONNECTION_FAILED) {
// The kernel already sent the controller connection command. Do not
// fall through to Write() and create a second raw HCI attempt.
Expand Down
Loading