diff --git a/gaiaV3/BluetoothSocketWrapper.cpp b/gaiaV3/BluetoothSocketWrapper.cpp
index a0132ad..e8fc988 100644
--- a/gaiaV3/BluetoothSocketWrapper.cpp
+++ b/gaiaV3/BluetoothSocketWrapper.cpp
@@ -1,8 +1,71 @@
 #include <QTimer>
+#include <QThread>
 #include "BluetoothSocketWrapper.h"
 
+extern "C" {
+#include <bluetooth/bluetooth.h>
+#include <bluetooth/sdp.h>
+#include <bluetooth/sdp_lib.h>
+#include <bluetooth/rfcomm.h>
+}
+#include <sys/socket.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <cerrno>
+#include <cstring>
+
 using namespace Qt::Literals::StringLiterals;
 
+// GAIA RFCOMM SDP service class UUID: a2129ff3-081b-4c45-8afe-469d9c4842ec
+static const uint8_t kGaiaUuid128[16] = {
+    0xa2, 0x12, 0x9f, 0xf3, 0x08, 0x1b, 0x4c, 0x45,
+    0x8a, 0xfe, 0x46, 0x9d, 0x9c, 0x48, 0x42, 0xec
+};
+
+static int findRfcommChannel(const QBluetoothAddress &address) {
+    bdaddr_t dst;
+    quint64 addrInt = address.toUInt64();
+    memcpy(&dst, &addrInt, sizeof(bdaddr_t));
+
+    bdaddr_t bdaddr_any = {{0, 0, 0, 0, 0, 0}};
+    sdp_session_t *session = sdp_connect(&bdaddr_any, &dst, SDP_RETRY_IF_BUSY);
+    if (!session) {
+        qWarning() << "GAIA: SDP connect failed:" << strerror(errno);
+        return -1;
+    }
+
+    uuid_t uuid;
+    sdp_uuid128_create(&uuid, kGaiaUuid128);
+    sdp_list_t *search_list = sdp_list_append(nullptr, &uuid);
+
+    uint32_t range = 0x0000ffff;
+    sdp_list_t *attrid_list = sdp_list_append(nullptr, &range);
+    sdp_list_t *response_list = nullptr;
+
+    int channel = -1;
+    if (sdp_service_search_attr_req(session, search_list, SDP_ATTR_REQ_RANGE, attrid_list, &response_list) == 0) {
+        for (sdp_list_t *r = response_list; r; r = r->next) {
+            auto *rec = static_cast<sdp_record_t *>(r->data);
+            sdp_list_t *proto_list = nullptr;
+            if (sdp_get_access_protos(rec, &proto_list) == 0) {
+                channel = sdp_get_proto_port(proto_list, RFCOMM_UUID);
+                sdp_list_free(proto_list, nullptr);
+            }
+            sdp_record_free(rec);
+            if (channel > 0)
+                break;
+        }
+        sdp_list_free(response_list, nullptr);
+    } else {
+        qWarning() << "GAIA: SDP search failed:" << strerror(errno);
+    }
+
+    sdp_list_free(search_list, nullptr);
+    sdp_list_free(attrid_list, nullptr);
+    sdp_close(session);
+    return channel;
+}
+
 BluetoothSocketWrapper::BluetoothSocketWrapper(QObject *parent) :
     QObject(parent),
     isDemo{false},
@@ -21,14 +84,32 @@ bool BluetoothSocketWrapper::isOpen() const {
     if (isDemo){
         return true;
     }
-    return m_socket->isOpen();
+    return m_rawFd >= 0;
+}
+
+void BluetoothSocketWrapper::closeRaw() {
+    if (m_readNotifier) {
+        m_readNotifier->setEnabled(false);
+        m_readNotifier->deleteLater();
+        m_readNotifier = nullptr;
+    }
+    if (m_exceptNotifier) {
+        m_exceptNotifier->setEnabled(false);
+        m_exceptNotifier->deleteLater();
+        m_exceptNotifier = nullptr;
+    }
+    if (m_rawFd >= 0) {
+        ::close(m_rawFd);
+        m_rawFd = -1;
+    }
 }
 
 void BluetoothSocketWrapper::close() {
     if (isDemo){
         Q_EMIT disconnected();
+        return;
     }
-    m_socket->close();
+    closeRaw();
 }
 
 QByteArray BluetoothSocketWrapper::readAll() {
@@ -44,7 +125,29 @@ QByteArray BluetoothSocketWrapper::readAll() {
         return data;
 
     }
-    return m_socket->readAll();
+    if (m_rawFd < 0)
+        return {};
+
+    QByteArray result;
+    char buf[4096];
+    while (true) {
+        ssize_t n = ::read(m_rawFd, buf, sizeof(buf));
+        if (n > 0) {
+            result.append(buf, static_cast<int>(n));
+        } else if (n == 0) {
+            closeRaw();
+            Q_EMIT disconnected();
+            break;
+        } else {
+            if (errno == EAGAIN || errno == EWOULDBLOCK)
+                break;
+            qWarning() << "GAIA: read error:" << strerror(errno);
+            closeRaw();
+            Q_EMIT disconnected();
+            break;
+        }
+    }
+    return result;
 }
 
 void
@@ -52,10 +155,84 @@ BluetoothSocketWrapper::connectToService(const QBluetoothAddress &address, quint
     if (address.toString() == "11:11:11:11:11:11") {
         isDemo = true;
         Q_EMIT connected();
-    } else {
-        isDemo = false;
-        m_socket->connectToService(address, port, openMode);
+        return;
+    }
+    isDemo = false;
+    closeRaw();
+
+    auto *thread = QThread::create([this, address, openMode] {
+        int channel = findRfcommChannel(address);
+        QMetaObject::invokeMethod(this, [this, address, channel, openMode] {
+            if (channel > 0) {
+                doRawConnect(address, channel, openMode);
+            } else {
+                qWarning() << "GAIA: no RFCOMM channel found for" << address;
+            }
+        });
+    });
+    connect(thread, &QThread::finished, thread, &QThread::deleteLater);
+    thread->start();
+}
+
+void BluetoothSocketWrapper::doRawConnect(const QBluetoothAddress &address, int channel, QIODevice::OpenMode) {
+    int fd = ::socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);
+    if (fd < 0) {
+        qWarning() << "GAIA: socket() failed:" << strerror(errno);
+        return;
     }
+
+    struct sockaddr_rc addr = {};
+    addr.rc_family = AF_BLUETOOTH;
+    quint64 addrInt = address.toUInt64();
+    memcpy(&addr.rc_bdaddr, &addrInt, sizeof(bdaddr_t));
+    addr.rc_channel = static_cast<uint8_t>(channel);
+
+    fcntl(fd, F_SETFL, O_NONBLOCK);
+
+    int ret = ::connect(fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
+    if (ret < 0 && errno != EINPROGRESS) {
+        qWarning() << "GAIA: connect() failed:" << strerror(errno);
+        ::close(fd);
+        return;
+    }
+
+    auto *writeNotifier = new QSocketNotifier(fd, QSocketNotifier::Write, this);
+    connect(writeNotifier, &QSocketNotifier::activated, this,
+            [this, fd, writeNotifier](QSocketDescriptor, QSocketNotifier::Type) {
+        writeNotifier->setEnabled(false);
+        writeNotifier->deleteLater();
+
+        int err = 0;
+        socklen_t errlen = sizeof(err);
+        ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen);
+
+        if (err != 0) {
+            qWarning() << "GAIA: RFCOMM connect failed:" << strerror(err);
+            ::close(fd);
+            return;
+        }
+
+        adoptRawFd(fd);
+    });
+}
+
+void BluetoothSocketWrapper::adoptRawFd(int fd) {
+    m_rawFd = fd;
+
+    m_readNotifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
+    connect(m_readNotifier, &QSocketNotifier::activated, this,
+            [this](QSocketDescriptor, QSocketNotifier::Type) {
+        Q_EMIT readyRead();
+    });
+
+    m_exceptNotifier = new QSocketNotifier(fd, QSocketNotifier::Exception, this);
+    connect(m_exceptNotifier, &QSocketNotifier::activated, this,
+            [this](QSocketDescriptor, QSocketNotifier::Type) {
+        closeRaw();
+        Q_EMIT disconnected();
+    });
+
+    Q_EMIT connected();
 }
 
 qint64 BluetoothSocketWrapper::write(const QByteArray &data) {
@@ -68,7 +245,9 @@ qint64 BluetoothSocketWrapper::write(const QByteArray &data) {
         }
         return data.size();
     }
-    return m_socket->write(data);
+    if (m_rawFd < 0)
+        return -1;
+    return ::write(m_rawFd, data.data(), data.size());
 }
 
 BluetoothSocketWrapper::BluetoothSocketWrapper(QBluetoothSocket *socket)
diff --git a/gaiaV3/BluetoothSocketWrapper.h b/gaiaV3/BluetoothSocketWrapper.h
index 0ae0775..7ed3189 100644
--- a/gaiaV3/BluetoothSocketWrapper.h
+++ b/gaiaV3/BluetoothSocketWrapper.h
@@ -5,6 +5,7 @@
 #include <QObject>
 #include <QBluetoothSocket>
 #include <QQueue>
+#include <QSocketNotifier>
 
 class BluetoothSocketWrapper: public QObject {
 
@@ -27,11 +28,18 @@ protected:
 private:
 
     void prepareData();
+    void doRawConnect(const QBluetoothAddress &address, int channel, QIODevice::OpenMode openMode);
+    void adoptRawFd(int fd);
+    void closeRaw();
 
 
     QMap<QByteArray, QByteArray> m_dataMap;
     QQueue<QByteArray> m_queue;
 
+    int m_rawFd = -1;
+    QSocketNotifier *m_readNotifier = nullptr;
+    QSocketNotifier *m_exceptNotifier = nullptr;
+
 signals:
     void connected();
     void disconnected();
diff --git a/gaiaV3/CMakeLists.txt b/gaiaV3/CMakeLists.txt
index b1ac7f6..c195c50 100644
--- a/gaiaV3/CMakeLists.txt
+++ b/gaiaV3/CMakeLists.txt
@@ -53,6 +53,6 @@ qt_add_qml_module(gaiaV3
             GAIARfcommClient.h
 )
 
-target_link_libraries(gaiaV3 PRIVATE Qt6::Core Qt6::Quick Qt6::Bluetooth)
+target_link_libraries(gaiaV3 PRIVATE Qt6::Core Qt6::Quick Qt6::Bluetooth bluetooth)
 
 add_subdirectory(tests)
\ No newline at end of file
